diff options
Diffstat (limited to 'extensions')
200 files changed, 19181 insertions, 67 deletions
diff --git a/extensions/BMO/Config.pm b/extensions/BMO/Config.pm new file mode 100644 index 000000000..0ad817768 --- /dev/null +++ b/extensions/BMO/Config.pm @@ -0,0 +1,38 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 BMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Gervase Markham +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::BMO; +use strict; + +use constant NAME => 'BMO'; + +use constant REQUIRED_MODULES => [ + { + package => 'Tie-IxHash', + module => 'Tie::IxHash', + version => 0 + } +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm new file mode 100644 index 000000000..c800ade3d --- /dev/null +++ b/extensions/BMO/Extension.pm @@ -0,0 +1,746 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 BMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Gervase Markham. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> +# David Lawrence <dkl@mozilla.com> +# Byron Jones <glob@mozilla.com> + +package Bugzilla::Extension::BMO; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Field; +use Bugzilla::Constants; +use Bugzilla::Status; +use Bugzilla::User; +use Bugzilla::User::Setting; +use Bugzilla::Util qw(html_quote trick_taint trim datetime_from detaint_natural); +use Bugzilla::Token; +use Bugzilla::Error; +use Bugzilla::Mailer; + +use Scalar::Util qw(blessed); +use Date::Parse; +use DateTime; + +use Bugzilla::Extension::BMO::FakeBug; +use Bugzilla::Extension::BMO::Data qw($cf_visible_in_products + $cf_flags + $cf_disabled_flags + %group_to_cc_map + $blocking_trusted_setters + $blocking_trusted_requesters + $status_trusted_wanters + $status_trusted_setters + $other_setters + %always_fileable_group + %product_sec_groups); +use Bugzilla::Extension::BMO::Reports qw(user_activity_report + triage_reports + group_admins); + +our $VERSION = '0.1'; + +# +# Monkey-patched methods +# + +BEGIN { + *Bugzilla::Bug::last_closed_date = \&_last_closed_date; +} + +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product; + $vars->{'cf_flag_disabled'} = \&cf_flag_disabled; + + if ($file =~ /^list\/list/) { + # Purpose: enable correct sorting of list table + # Matched to changes in list/table.html.tmpl + my %db_order_column_name_map = ( + 'map_components.name' => 'component', + 'map_products.name' => 'product', + 'map_reporter.login_name' => 'reporter', + 'map_assigned_to.login_name' => 'assigned_to', + 'delta_ts' => 'opendate', + 'creation_ts' => 'changeddate', + ); + + my @orderstrings = split(/,\s*/, $vars->{'order'}); + + # contains field names of the columns being used to sort the table. + my @order_columns; + foreach my $o (@orderstrings) { + $o =~ s/bugs.//; + $o = $db_order_column_name_map{$o} if + grep($_ eq $o, keys(%db_order_column_name_map)); + next if (grep($_ eq $o, @order_columns)); + push(@order_columns, $o); + } + + $vars->{'order_columns'} = \@order_columns; + + # fields that have a custom sortkey. (So they are correctly sorted + # when using js) + my @sortkey_fields = qw(bug_status resolution bug_severity priority + rep_platform op_sys); + + my %columns_sortkey; + foreach my $field (@sortkey_fields) { + $columns_sortkey{$field} = _get_field_values_sort_key($field); + } + $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones'); + + $vars->{'columns_sortkey'} = \%columns_sortkey; + } + elsif ($file =~ /^bug\/create\/create[\.-]/) { + if (!$vars->{'cloned_bug_id'}) { + # Allow status whiteboard values to be bookmarked + $vars->{'status_whiteboard'} = + Bugzilla->cgi->param('status_whiteboard') || ""; + } + + # Purpose: for pretty product chooser + $vars->{'format'} = Bugzilla->cgi->param('format'); + + # Data needed for "this is a security bug" checkbox + $vars->{'sec_groups'} = \%product_sec_groups; + } + + + if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) { + # hack to allow the bug entry templates to use check_can_change_field + # to see if various field values should be available to the current user. + $vars->{'default'} = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {}); + } +} + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'user_activity.html') { + user_activity_report($vars); + + } elsif ($page eq 'triage_reports.html') { + triage_reports($vars); + + } elsif ($page eq 'upgrade-3.6.html') { + $vars->{'bzr_history'} = sub { + return `cd /data/www/bugzilla.mozilla.org; /usr/bin/bzr log -n0 -rlast:10..`; + }; + } + elsif ($page eq 'fields.html') { + # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl + # were changed for better performance and are now only loaded once. + # I have not found an easy way to allow our hook template to check if + # it is called from pages/fields.html.tmpl. So we set a value in request_cache + # that our hook template can see. + Bugzilla->request_cache->{'bmo_fields_page'} = 1; + } + elsif ($page eq 'group_admins.html') { + group_admins($vars); + } +} + +sub _get_field_values_sort_key { + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + "SELECT value, sortkey FROM $field + ORDER BY sortkey, value"); + + my %field_values; + foreach my $field (@$fields) { + my ($value, $sortkey) = @$field; + $field_values{$value} = $sortkey; + } + return \%field_values; +} + +sub cf_hidden_in_product { + my ($field_name, $product_name, $component_name, $custom_flag_mode) = @_; + + # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product + # elsewhere, we just pass the name of the product. + $product_name = blessed($product_name) ? $product_name->name + : $product_name; + + # Also in buglist.cgi, we pass in a list of components instead + # of a single compoent name everywhere else. + my $component_list = ref $component_name ? $component_name + : [ $component_name ]; + + if ($custom_flag_mode) { + if ($custom_flag_mode == 1) { + # skip custom flags + foreach my $flag_re (@$cf_flags) { + return 1 if $field_name =~ $flag_re; + } + } elsif ($custom_flag_mode == 2) { + # custom flags only + my $found = 0; + foreach my $flag_re (@$cf_flags) { + if ($field_name =~ $flag_re) { + $found = 1; + last; + } + } + return 1 unless $found; + } + } + + foreach my $field_re (keys %$cf_visible_in_products) { + if ($field_name =~ $field_re) { + # If no product given, for example more than one product + # in buglist.cgi, then hide field by default + return 1 if !$product_name; + + my $products = $cf_visible_in_products->{$field_re}; + foreach my $product (keys %$products) { + my $components = $products->{$product}; + + my $found_component = 0; + if (@$components) { + foreach my $component (@$components) { + if (grep($_ eq $component, @$component_list)) { + $found_component = 1; + last; + } + } + } + + # If product matches and at at least one component matches + # from component_list (if a matching component was required), + # we allow the field to be seen + if ($product eq $product_name && (!@$components || $found_component)) { + return 0; + } + } + + return 1; + } + } + + return 0; +} + +sub cf_flag_disabled { + my ($field_name, $bug) = @_; + return 0 unless grep { $field_name eq $_ } @$cf_disabled_flags; + my $value = $bug->{$field_name}; + return $value eq '---' || $value eq ''; +} + +# Purpose: CC certain email addresses on bugmail when a bug is added or +# removed from a particular group. +sub bugmail_recipients { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + if (@$diffs) { + # Changed bug + foreach my $ref (@$diffs) { + my ($who, $whoname, $what, $when, + $old, $new, $attachid, $fieldname) = (@$ref); + + if ($fieldname eq "bug_group") { + _cc_if_special_group($old, $recipients); + _cc_if_special_group($new, $recipients); + } + } + } else { + # Determine if it's a new bug, or a comment without a field change + my $comment_count = scalar @{$bug->comments}; + if ($comment_count == 1) { + # New bug + foreach my $group (@{ $bug->groups_in }) { + _cc_if_special_group($group->{'name'}, $recipients); + } + } + } +} + +sub _cc_if_special_group { + my ($group, $recipients) = @_; + + return if !$group; + + if ($group_to_cc_map{$group}) { + my $id = login_to_id($group_to_cc_map{$group}); + $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT(); + } +} + +sub _check_trusted { + my ($field, $trusted, $priv_results) = @_; + + my $needed_group = $trusted->{'_default'} || ""; + foreach my $dfield (keys %$trusted) { + if ($field =~ $dfield) { + $needed_group = $trusted->{$dfield}; + } + } + if ($needed_group && !Bugzilla->user->in_group($needed_group)) { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } +} + +sub _is_field_set { + my $value = shift; + return $value ne '---' && $value ne '?'; +} + +sub bug_check_can_change_field { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $field = $args->{'field'}; + my $new_value = $args->{'new_value'}; + my $old_value = $args->{'old_value'}; + my $priv_results = $args->{'priv_results'}; + my $user = Bugzilla->user; + + # Only users in the appropriate drivers group can change the + # cf_blocking_* fields or cf_tracking_* fields + + if ($field =~ /^cf_(?:blocking|tracking)_/) { + # 0 -> 1 is used by show_bug, always allow so we skip this whole part + if (!($old_value eq '0' && $new_value eq '1')) { + # require privileged access to set a flag + if (_is_field_set($new_value)) { + _check_trusted($field, $blocking_trusted_setters, $priv_results); + } + + # require editbugs to clear or re-nominate a set flag + elsif (_is_field_set($old_value) + && !$user->in_group('editbugs', $bug->{'product_id'})) + { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + + if ($new_value eq '?') { + _check_trusted($field, $blocking_trusted_requesters, $priv_results); + } + if ($user->id) { + push (@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + + } elsif ($field =~ /^cf_status_/) { + # Only drivers can set wanted. + if ($new_value eq 'wanted') { + _check_trusted($field, $status_trusted_wanters, $priv_results); + } elsif (_is_field_set($new_value)) { + _check_trusted($field, $status_trusted_setters, $priv_results); + } + if ($user->id) { + push (@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + + } elsif ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') { + # "other" custom field setters restrictions + if (exists $other_setters->{$field}) { + my $in_group = 0; + foreach my $group (@{$other_setters->{$field}}) { + if ($user->in_group($group, $bug->product_id)) { + $in_group = 1; + last; + } + } + if (!$in_group) { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + + } elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') { + # The EXPIRED resolution should only be settable by gerv. + if ($user->login ne 'gerv@mozilla.org') { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + + } elsif ($field eq 'resolution' && $new_value eq 'FIXED') { + # You need at least canconfirm to mark a bug as FIXED + if (!$user->in_group('canconfirm', $bug->{'product_id'})) { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + + } elsif ($user->in_group('canconfirm', $bug->{'product_id'})) { + # Canconfirm is really "cantriage"; users with canconfirm can also mark + # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE. + if ($field eq 'bug_status' + && is_open_state($old_value) + && !is_open_state($new_value)) + { + push (@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + elsif ($field eq 'resolution' && + ($new_value eq 'DUPLICATE' || + $new_value eq 'WORKSFORME' || + $new_value eq 'INCOMPLETE')) + { + push (@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + + } elsif ($field eq 'bug_status') { + # Disallow reopening of bugs which have been resolved for > 1 year + if (is_open_state($new_value) + && !is_open_state($old_value) + && $bug->resolution eq 'FIXED') + { + my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $days_ago->subtract(days => 365); + my $last_closed = datetime_from($bug->last_closed_date); + if ($last_closed lt $days_ago) { + push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + } +} + +# Purpose: link up various Mozilla-specific strings. +sub _link_uuid { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + + return qq{<a href="https://crash-stats.mozilla.com/report/index/$match">bp-$match</a>}; +} + +sub _link_cve { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + + return qq{<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=$match">$match</a>}; +} + +sub _link_svn { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + + return qq{<a href="http://viewvc.svn.mozilla.org/vc?view=rev&revision=$match">r$match</a>}; +} + +sub _link_hg { + my $args = shift; + my $text = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $id = html_quote($args->{matches}->[2]); + + return qq{<a href="https://hg.mozilla.org/$repo/rev/$id">$text</a>}; +} + +sub _link_bzr { + my $args = shift; + my $preamble = html_quote($args->{matches}->[0]); + my $url = html_quote($args->{matches}->[1]); + my $text = html_quote($args->{matches}->[2]); + my $id = html_quote($args->{matches}->[3]); + + $url =~ s/\s+$//; + $url =~ s/\/$//; + + return qq{$preamble<a href="http://$url/revision/$id">$text</a>}; +} + +sub bug_format_comment { + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; + + # Only match if not already in an URL using the negative lookbehind (?<!\/) + push (@$regexes, { + match => qr/(?<!\/)\b(?:UUID\s+|bp\-)([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\- + [a-f0-9]{4}\-[a-f0-9]{12})\b/x, + replace => \&_link_uuid + }); + + push (@$regexes, { + match => qr/(?<!\/)\b((?:CVE|CAN)-\d{4}-\d{4})\b/, + replace => \&_link_cve + }); + + push (@$regexes, { + match => qr/\b((?:CVE|CAN)-\d{4}-\d{4})\b/, + replace => \&_link_cve + }); + + push (@$regexes, { + match => qr/\br(\d{4,})\b/, + replace => \&_link_svn + }); + + push (@$regexes, { + match => qr/\b(Committing\sto:\sbzr\+ssh:\/\/ + (?:[^\@]+\@)?(bzr\.mozilla\.org[^\n]+)\n.*?\nCommitted\s) + (revision\s(\d+))\./sx, + replace => \&_link_bzr + }); + + # Note: for grouping in this regexp, always use non-capturing parentheses. + my $hgrepos = join('|', qw!(?:releases/)?comm-[\w.]+ + (?:releases/)?mozilla-[\w.]+ + (?:releases/)?mobile-[\w.]+ + tracemonkey + tamarin-[\w.]+ + camino!); + + push (@$regexes, { + match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/, + replace => \&_link_hg + }); +} + +# Purpose: make it always possible to file bugs in certain groups. +sub bug_check_groups { + my ($self, $args) = @_; + my $group_names = $args->{'group_names'}; + my $add_groups = $args->{'add_groups'}; + + $group_names = ref $group_names + ? $group_names + : [ map { trim($_) } split(',', $group_names) ]; + + foreach my $name (@$group_names) { + if ($always_fileable_group{$name}) { + my $group = new Bugzilla::Group({ name => $name }) or next; + $add_groups->{$group->id} = $group; + } + } +} + +# Purpose: generically handle generating pretty blocking/status "flags" from +# custom field names. +sub quicksearch_map { + my ($self, $args) = @_; + my $map = $args->{'map'}; + + foreach my $name (keys %$map) { + if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) { + my $type = $1; + my $product = $2; + my $version = $3; + + if ($version) { + $version = join('.', split(//, $version)); + } + + my $pretty_name = $type; + if ($product) { + $pretty_name .= "-" . $product; + } + if ($version) { + $pretty_name .= $version; + } + + $map->{$pretty_name} = $name; + } + elsif ($name =~ /cf_crash_signature$/) { + $map->{'sig'} = $name; + } + } +} + +# Restrict content types attachable by non-privileged people +my @mimetype_whitelist = ('^image\/', 'application\/pdf'); + +sub object_end_of_create_validators { + my ($self, $args) = @_; + my $class = $args->{'class'}; + + if ($class->isa('Bugzilla::Attachment')) { + my $params = $args->{'params'}; + my $bug = $params->{'bug'}; + if (!Bugzilla->user->in_group('editbugs', $bug->product_id)) { + my $mimetype = $params->{'mimetype'}; + if (!grep { $mimetype =~ /$_/ } @mimetype_whitelist ) { + # Need to neuter MIME type to something non-executable + if ($mimetype =~ /^text\//) { + $params->{'mimetype'} = "text/plain"; + } + else { + $params->{'mimetype'} = "application/octet-stream"; + } + } + } + } +} + +sub install_before_final_checks { + my ($self, $args) = @_; + + # Add product chooser setting (although it was added long ago, so add_setting + # will just return every time). + add_setting('product_chooser', + ['pretty_product_chooser', 'full_product_chooser'], + 'pretty_product_chooser'); + + # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix' + my $dbh = Bugzilla->dbh; + if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) { + $dbh->bz_start_transaction(); + $dbh->do("UPDATE profile_setting + SET setting_value='on-temp' + WHERE setting_name='gmail_threading' AND setting_value='Off'"); + $dbh->do("UPDATE profile_setting + SET setting_value='off' + WHERE setting_name='gmail_threading' AND setting_value='On'"); + $dbh->do("UPDATE profile_setting + SET setting_value='on' + WHERE setting_name='gmail_threading' AND setting_value='on-temp'"); + $dbh->do("UPDATE profile_setting + SET setting_name='bugmail_new_prefix' + WHERE setting_name='gmail_threading'"); + $dbh->do("DELETE FROM setting WHERE name='gmail_threading'"); + $dbh->bz_commit_transaction(); + } +} + +# Migrate old is_active stuff to new patch (is in core in 4.2), The old column +# name was 'is_active', the new one is 'isactive' (no underscore). +sub install_update_db { + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_column_info('milestones', 'is_active')) { + $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;"); + $dbh->bz_drop_column('milestones', 'is_active'); + $dbh->bz_drop_column('milestones', 'is_searchable'); + } +} + +sub _last_closed_date { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + return $self->{'last_closed_date'} if defined $self->{'last_closed_date'}; + + my $closed_statuses = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'"; + my $status_field_id = get_field_id('bug_status'); + + $self->{'last_closed_date'} = $dbh->selectrow_array(" + SELECT bugs_activity.bug_when + FROM bugs_activity + WHERE bugs_activity.fieldid = ? + AND bugs_activity.added IN ($closed_statuses) + AND bugs_activity.bug_id = ? + ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1), + undef, $status_field_id, $self->id + ); + + return $self->{'last_closed_date'}; +} + +sub field_end_of_create { + my ($self, $args) = @_; + my $field = $args->{'field'}; + + # email mozilla's DBAs so they can update the grants for metrics + # this really should create a bug in mozilla.org/Server Operations: Database + + if (Bugzilla->params->{'urlbase'} ne 'https://bugzilla.mozilla.org/') { + return; + } + + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "Emailing notification to infra-dbnotices\@mozilla.com\n"; + } + + my $name = $field->name; + my @message; + push @message, 'To: infra-dbnotices@mozilla.com'; + push @message, "Subject: custom field '$name' added to bugzilla.mozilla.org"; + push @message, 'From: ' . Bugzilla->params->{mailfrom}; + push @message, ''; + push @message, "The custom field '$name' has been added to the BMO database."; + push @message, ''; + push @message, 'Please run the following on tm-bugs01-master01:'; + push @message, " GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.2.70.20_';"; + push @message, " GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.2.70.20_';"; + push @message, ''; + MessageToMTA(join("\n", @message)); +} + +sub webservice { + my ($self, $args) = @_; + + my $dispatch = $args->{dispatch}; + $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService"; +} + +our $search_content_matches; +BEGIN { + $search_content_matches = \&Bugzilla::Search::_content_matches; +} + +sub search_operator_field_override { + my ($self, $args) = @_; + my $search = $args->{'search'}; + my $operators = $args->{'operators'}; + + my $cgi = Bugzilla->cgi; + my @comments = $cgi->param('comments'); + my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments; + + if ($cgi->param('query_format') eq 'specific' && $exclude_comments) { + # use the non-comment operator + $operators->{'content'}->{matches} = \&_short_desc_matches; + $operators->{'content'}->{notmatches} = \&_short_desc_matches; + + } else { + # restore default content operator + $operators->{'content'}->{matches} = $search_content_matches; + $operators->{'content'}->{notmatches} = $search_content_matches; + } +} + +sub _short_desc_matches { + # copy of Bugzilla::Search::_content_matches + + my $self = shift; + my %func_args = @_; + my ($chartid, $supptables, $term, $groupby, $fields, $t, $v) = + @func_args{qw(chartid supptables term groupby fields t v)}; + my $dbh = Bugzilla->dbh; + + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$$chartid"; + push(@$supptables, "LEFT JOIN bugs_fulltext AS $table " . + "ON bugs.bug_id = $table.bug_id"); + + # Create search terms to add to the SELECT and WHERE clauses. + my ($term1, $rterm1) = $dbh->sql_fulltext_search("$table.short_desc", $$v, 1); + $rterm1 = $term1 if !$rterm1; + + # The term to use in the WHERE clause. + $$term = $term1; + if ($$t =~ /not/i) { + $$term = "NOT($$term)"; + } + + my $current = Bugzilla::Search::COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; + # For NOT searches, we just add 0 to the relevance. + my $select_term = $$t =~ /not/ ? 0 : "($current$rterm1)"; + Bugzilla::Search::COLUMNS->{'relevance'}->{name} = $select_term; +} + +__PACKAGE__->NAME; diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm new file mode 100644 index 000000000..ccc729a6d --- /dev/null +++ b/extensions/BMO/lib/Data.pm @@ -0,0 +1,266 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 BMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> +# Reed Loden <reed@reedloden.com> + +package Bugzilla::Extension::BMO::Data; +use strict; + +use base qw(Exporter); +use Tie::IxHash; + +our @EXPORT_OK = qw($cf_visible_in_products + $cf_flags $cf_disabled_flags + %group_to_cc_map + $blocking_trusted_setters + $blocking_trusted_requesters + $status_trusted_wanters + $status_trusted_setters + $other_setters + %always_fileable_group + %product_sec_groups); + +# Which custom fields are visible in which products and components. +# +# By default, custom fields are visible in all products. However, if the name +# of the field matches any of these regexps, it is only visible if the +# product (and component if necessary) is a member of the attached hash. [] +# for component means "all". +# +# IxHash keeps them in insertion order, and so we get regexp priorities right. +our $cf_visible_in_products; +tie(%$cf_visible_in_products, "Tie::IxHash", + qr/^cf_blocking_fennec/ => { + "addons.mozilla.org" => [], + "AUS" => [], + "Core" => [], + "Fennec" => [], + "mozilla.org" => ["Release Engineering"], + "Mozilla Services" => [], + "NSPR" => [], + "support.mozilla.com" => [], + "Toolkit" => [], + "Tech Evangelism" => [], + "Mozilla Localizations" => [], + }, + qr/^cf_tracking_thunderbird|cf_blocking_thunderbird|cf_status_thunderbird/ => { + "support.mozillamessaging.com" => [], + "Thunderbird" => [], + "MailNews Core" => [], + "Mozilla Messaging" => [], + "Websites" => ["www.mozillamessaging.com"], + }, + qr/^(cf_(blocking|tracking)_seamonkey|cf_status_seamonkey)/ => { + "Composer" => [], + "MailNews Core" => [], + "Mozilla Localizations" => [], + "Other Applications" => [], + "SeaMonkey" => [], + }, + qr/^cf_blocking_|cf_tracking_|cf_status/ => { + "Add-on SDK" => [], + "addons.mozilla.org" => [], + "AUS" => [], + "Core Graveyard" => [], + "Core" => [], + "Directory" => [], + "Fennec" => [], + "Firefox" => [], + "MailNews Core" => [], + "mozilla.org" => ["Release Engineering"], + "Mozilla Localizations" => [], + "Mozilla Services" => [], + "NSPR" => [], + "NSS" => [], + "Other Applications" => [], + "SeaMonkey" => [], + "support.mozilla.com" => [], + "Tech Evangelism" => [], + "Testing" => [], + "Toolkit" => [], + "Websites" => ["getpersonas.com"], + "Webtools" => [], + "Plugins" => [], + }, + qr/^cf_colo_site$/ => { + "mozilla.org" => [ + "Server Operations", + "Server Operations: Projects", + "Server Operations: RelEng", + "Server Operations: Security", + ], + }, + qw/^cf_office$/ => { + "mozilla.org" => ["Server Operations: Desktop Issues"], + }, + qr/^cf_crash_signature$/ => { + "addons.mozilla.org" => [], + "Add-on SDK" => [], + "Calendar" => [], + "Camino" => [], + "Composer" => [], + "Fennec" => [], + "Firefox" => [], + "Mozilla Localizations" => [], + "Mozilla Services" => [], + "Other Applications" => [], + "Penelope" => [], + "SeaMonkey" => [], + "Thunderbird" => [], + "Core" => [], + "Directory" => [], + "JSS" => [], + "MailNews Core" => [], + "NSPR" => [], + "NSS" => [], + "Plugins" => [], + "Rhino" => [], + "Tamarin" => [], + "Testing" => [], + "Toolkit" => [], + "Mozilla Labs" => [], + "mozilla.org" => [], + "Tech Evangelism" => [], + }, + qw/^cf_due_date$/ => { + "Mozilla Reps" => [], + }, +); + +# Which custom fields are acting as flags (ie. custom flags) +our $cf_flags = [ + qr/^cf_(?:blocking|tracking|status)_/, +]; + +# List of disabled fields. +# Temp kludge until custom fields can be disabled correctly upstream. +# Disabled fields are hidden unless they have a value set +our $cf_disabled_flags = [ + 'cf_blocking_20', + 'cf_status_20', + 'cf_tracking_firefox5', + 'cf_status_firefox5', + 'cf_blocking_thunderbird32', + 'cf_status_thunderbird32', + 'cf_blocking_thunderbird30', + 'cf_status_thunderbird30', + 'cf_blocking_seamonkey21', + 'cf_status_seamonkey21', + 'cf_tracking_seamonkey22', + 'cf_status_seamonkey22', + 'cf_tracking_firefox6', + 'cf_status_firefox6', + 'cf_tracking_thunderbird6', + 'cf_status_thunderbird6', +]; + +# Who to CC on particular bugmails when certain groups are added or removed. +our %group_to_cc_map = ( + 'bugzilla-security' => 'security@bugzilla.org', + 'client-services-security' => 'amo-admins@mozilla.org', + 'core-security' => 'security@mozilla.org', + 'tamarin-security' => 'tamarinsecurity@adobe.com', + 'websites-security' => 'website-drivers@mozilla.org', + 'webtools-security' => 'webtools-security@mozilla.org', +); + +# Only users in certain groups can change certain custom fields in +# certain ways. +# +# Who can set cf_blocking_* or cf_tracking_* to +/- +our $blocking_trusted_setters = { + 'cf_blocking_fennec' => 'fennec-drivers', + 'cf_blocking_20' => 'mozilla-next-drivers', + qr/^cf_tracking_firefox/ => 'mozilla-next-drivers', + qr/^cf_blocking_thunderbird/ => 'thunderbird-drivers', + qr/^cf_tracking_thunderbird/ => 'thunderbird-drivers', + qr/^cf_tracking_seamonkey/ => 'seamonkey-council', + qr/^cf_blocking_seamonkey/ => 'seamonkey-council', + '_default' => 'mozilla-stable-branch-drivers', +}; + +# Who can request cf_blocking_* or cf_tracking_* +our $blocking_trusted_requesters = { + qr/^cf_blocking_thunderbird/ => 'thunderbird-trusted-requesters', + '_default' => 'everyone', +}; + +# Who can set cf_status_* to "wanted"? +our $status_trusted_wanters = { + 'cf_status_20' => 'mozilla-next-drivers', + qr/^cf_status_thunderbird/ => 'thunderbird-drivers', + qr/^cf_status_seamonkey/ => 'seamonkey-council', + '_default' => 'mozilla-stable-branch-drivers', +}; + +# Who can set cf_status_* to values other than "wanted"? +our $status_trusted_setters = { + qr/^cf_status_thunderbird/ => 'editbugs', + '_default' => 'canconfirm', +}; + +# Who can set other custom flags (use full field names only, not regex's) +our $other_setters = { + 'cf_colo_site' => ['infra', 'build'], +}; + +# Groups in which you can always file a bug, whoever you are. +our %always_fileable_group = ( + 'bugzilla-security' => 1, + 'client-services-security' => 1, + 'consulting' => 1, + 'core-security' => 1, + 'infra' => 1, + 'marketing-private' => 1, + 'mozilla-confidential' => 1, + 'mozilla-corporation-confidential' => 1, + 'mozilla-messaging-confidential' => 1, + 'tamarin-security' => 1, + 'websites-security' => 1, + 'webtools-security' => 1, +); + +# Mapping of products to their security bits +our %product_sec_groups = ( + "mozilla.org" => 'mozilla-confidential', + "Webtools" => 'webtools-security', + "Marketing" => 'marketing-private', + "addons.mozilla.org" => 'client-services-security', + "AUS" => 'client-services-security', + "Mozilla Services" => 'client-services-security', + "Mozilla Corporation" => 'mozilla-corporation-confidential', + "Mozilla Metrics" => 'metrics-private', + "Legal" => 'legal', + "Mozilla Messaging" => 'mozilla-messaging-confidential', + "Websites" => 'websites-security', + "Mozilla Developer Network" => 'websites-security', + "support.mozilla.com" => 'websites-security', + "quality.mozilla.org" => 'websites-security', + "Skywriter" => 'websites-security', + "support.mozillamessaging.com" => 'websites-security', + "Bugzilla" => 'bugzilla-security', + "bugzilla.mozilla.org" => 'bugzilla-security', + "Testopia" => 'bugzilla-security', + "Tamarin" => 'tamarin-security', + "Mozilla PR" => 'pr-private', + "_default" => 'core-security' +); + +1; diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm new file mode 100644 index 000000000..5610f5433 --- /dev/null +++ b/extensions/BMO/lib/FakeBug.pm @@ -0,0 +1,40 @@ +package Bugzilla::Extension::BMO::FakeBug; + +# hack to allow the bug entry templates to use check_can_change_field to see if +# various field values should be available to the current user + +use Bugzilla::Bug; + +our $AUTOLOAD; + +sub new { + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; +} + +sub AUTOLOAD { + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; +} + +sub check_can_change_field { + my $self = shift; + return Bugzilla::Bug::check_can_change_field($self, @_) +} + +sub _changes_everconfirmed { + my $self = shift; + return Bugzilla::Bug::_changes_everconfirmed($self, @_) +} + +sub everconfirmed { + my $self = shift; + return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1; +} + +1; + diff --git a/extensions/BMO/lib/Reports.pm b/extensions/BMO/lib/Reports.pm new file mode 100644 index 000000000..f291e72e7 --- /dev/null +++ b/extensions/BMO/lib/Reports.pm @@ -0,0 +1,522 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 BMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Byron Jones. Portions created +# by the Initial Developer are Copyright (C) 2011 the Mozilla Foundation. All +# Rights Reserved. +# +# Contributor(s): +# Byron Jones <glob@mozilla.com> + +package Bugzilla::Extension::BMO::Reports; +use strict; + +use Bugzilla::User; +use Bugzilla::Util qw(trim detaint_natural); +use Bugzilla::Error; +use Bugzilla::Constants; + +use Date::Parse; +use DateTime; + +use base qw(Exporter); + +our @EXPORT_OK = qw(user_activity_report + triage_reports + group_admins); + +sub user_activity_report { + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + + my @who = (); + my $from = trim($input->{'from'}); + my $to = trim($input->{'to'}); + + if ($input->{'action'} eq 'run') { + if ($input->{'who'} eq '') { + ThrowUserError('user_activity_missing_username'); + } + Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); + + ThrowUserError('user_activity_missing_from_date') unless $from; + my $from_time = str2time($from) + or ThrowUserError('user_activity_invalid_date', { date => $from }); + my $from_dt = DateTime->from_epoch(epoch => $from_time) + ->set_time_zone('local') + ->truncate(to => 'day'); + $from = $from_dt->ymd(); + + ThrowUserError('user_activity_missing_to_date') unless $to; + my $to_time = str2time($to) + or ThrowUserError('user_activity_invalid_date', { date => $to }); + my $to_dt = DateTime->from_epoch(epoch => $to_time) + ->set_time_zone('local') + ->truncate(to => 'day'); + $to = $to_dt->ymd(); + # add one day to include all activity that happened on the 'to' date + $to_dt->add(days => 1); + + my ($activity_joins, $activity_where) = ('', ''); + my ($attachments_joins, $attachments_where) = ('', ''); + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) + { + $activity_joins = "LEFT JOIN attachments + ON attachments.attach_id = bugs_activity.attach_id"; + $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0"; + $attachments_where = $activity_where; + } + + my @who_bits; + foreach my $who ( + ref $input->{'who'} + ? @{$input->{'who'}} + : $input->{'who'} + ) { + push @who, $who; + push @who_bits, '?'; + } + my $who_bits = join(',', @who_bits); + + if (!@who) { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $vars = {}; + $vars->{'script'} = $cgi->url(-relative => 1); + $vars->{'fields'} = {}; + $vars->{'matches'} = []; + $vars->{'matchsuccess'} = 0; + $vars->{'matchmultiple'} = 1; + print $cgi->header(); + $template->process("global/confirm-user-match.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + + $from_dt = $from_dt->ymd() . ' 00:00:00'; + $to_dt = $to_dt->ymd() . ' 23:59:59'; + my @params; + for (1..4) { + push @params, @who; + push @params, ($from_dt, $to_dt); + } + + my $comment_filter = ''; + if (!Bugzilla->user->is_insider) { + $comment_filter = 'AND longdescs.isprivate = 0'; + } + + my $query = " + SELECT + fielddefs.name, + bugs_activity.bug_id, + bugs_activity.attach_id, + ".$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s')." AS ts, + bugs_activity.removed, + bugs_activity.added, + profiles.login_name, + bugs_activity.comment_id, + bugs_activity.bug_when + FROM bugs_activity + $activity_joins + LEFT JOIN fielddefs + ON bugs_activity.fieldid = fielddefs.id + INNER JOIN profiles + ON profiles.userid = bugs_activity.who + WHERE profiles.login_name IN ($who_bits) + AND bugs_activity.bug_when >= ? AND bugs_activity.bug_when <= ? + $activity_where + + UNION ALL + + SELECT + 'bug_id' AS name, + bugs.bug_id, + NULL AS attach_id, + ".$dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + '(new bug)' AS removed, + bugs.short_desc AS added, + profiles.login_name, + NULL AS comment_id, + bugs.creation_ts AS bug_when + FROM bugs + INNER JOIN profiles + ON profiles.userid = bugs.reporter + WHERE profiles.login_name IN ($who_bits) + AND bugs.creation_ts >= ? AND bugs.creation_ts <= ? + + UNION ALL + + SELECT + 'longdesc' AS name, + longdescs.bug_id, + NULL AS attach_id, + DATE_FORMAT(longdescs.bug_when, '%Y.%m.%d %H:%i:%s') AS ts, + '' AS removed, + '' AS added, + profiles.login_name, + longdescs.comment_id AS comment_id, + longdescs.bug_when + FROM longdescs + INNER JOIN profiles + ON profiles.userid = longdescs.who + WHERE profiles.login_name IN ($who_bits) + AND longdescs.bug_when >= ? AND longdescs.bug_when <= ? + $comment_filter + + UNION ALL + + SELECT + 'attachments.filename' AS name, + attachments.bug_id, + attachments.attach_id, + ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + '' AS removed, + attachments.description AS added, + profiles.login_name, + NULL AS comment_id, + attachments.creation_ts AS bug_when + FROM attachments + INNER JOIN profiles + ON profiles.userid = attachments.submitter_id + WHERE profiles.login_name IN ($who_bits) + AND attachments.creation_ts >= ? AND attachments.creation_ts <= ? + $attachments_where + + ORDER BY bug_when "; + + my $list = $dbh->selectall_arrayref($query, undef, @params); + + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; + + foreach my $entry (@$list) { + my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who, + $comment_id) = @$entry; + my %change; + my $activity_visible = 1; + + next unless Bugzilla->user->can_see_bug($bugid); + + # check if the user should see this field's activity + if ($fieldname eq 'remaining_time' + || $fieldname eq 'estimated_time' + || $fieldname eq 'work_time' + || $fieldname eq 'deadline') + { + $activity_visible = Bugzilla->user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' + && !Bugzilla->user->is_insider + && $added) + { + $activity_visible = 0; + } + else { + $activity_visible = 1; + } + + if ($activity_visible) { + # Check for the results of an old Bugzilla data corruption bug + if (($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) { + $incomplete_data = 1; + } + + # An operation, done by 'who' at time 'when', has a number of + # 'changes' associated with it. + # If this is the start of a new operation, store the data from the + # previous one, and set up the new one. + if ($operation->{'who'} + && ($who ne $operation->{'who'} + || $when ne $operation->{'when'})) + { + $operation->{'changes'} = $changes; + push (@operations, $operation); + $operation = {}; + $changes = []; + } + + $operation->{'bug'} = $bugid; + $operation->{'who'} = $who; + $operation->{'when'} = $when; + + $change{'fieldname'} = $fieldname; + $change{'attachid'} = $attachid; + $change{'removed'} = $removed; + $change{'added'} = $added; + + if ($comment_id) { + $change{'comment'} = Bugzilla::Comment->new($comment_id); + next if $change{'comment'}->count == 0; + } + + push (@$changes, \%change); + } + } + + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push (@operations, $operation); + } + + $vars->{'incomplete_data'} = $incomplete_data; + $vars->{'operations'} = \@operations; + + } else { + + if ($from eq '') { + my ($yy, $mm) = (localtime)[5, 4]; + $from = sprintf("%4d-%02d-01", $yy + 1900, $mm + 1); + } + if ($to eq '') { + my ($yy, $mm, $dd) = (localtime)[5, 4, 3]; + $to = sprintf("%4d-%02d-%02d", $yy + 1900, $mm + 1, $dd); + } + } + + $vars->{'action'} = $input->{'action'}; + $vars->{'who'} = join(',', @who); + $vars->{'from'} = $from; + $vars->{'to'} = $to; +} + +sub triage_reports { + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + if ($input->{'action'} eq 'run' && $input->{'product'}) { + + # load product and components from input + + my $product = Bugzilla::Product->new({ name => $input->{'product'} }) + || ThrowUserError('invalid_object', { object => 'Product', value => $input->{'product'} }); + + my @component_ids; + if ($input->{'component'} ne '') { + my $ra_components = ref($input->{'component'}) + ? $input->{'component'} : [ $input->{'component'} ]; + foreach my $component_name (@$ra_components) { + my $component = Bugzilla::Component->new({ name => $component_name, product => $product }) + || ThrowUserError('invalid_object', { object => 'Component', value => $component_name }); + push @component_ids, $component->id; + } + } + + # determine which comment filters to run + + my $filter_commenter = $input->{'filter_commenter'}; + my $filter_commenter_on = $input->{'commenter'}; + my $filter_last = $input->{'filter_last'}; + my $filter_last_period = $input->{'last'}; + + if (!$filter_commenter || $filter_last) { + $filter_commenter = '1'; + $filter_commenter_on = 'reporter'; + } + + my $filter_commenter_id; + if ($filter_commenter && $filter_commenter_on eq 'is') { + Bugzilla::User::match_field({ 'commenter_is' => {'type' => 'single'} }); + my $user = Bugzilla::User->new({ name => $input->{'commenter_is'} }) + || ThrowUserError('invalid_object', { object => 'User', value => $input->{'commenter_is'} }); + $filter_commenter_id = $user ? $user->id : 0; + } + + my $filter_last_time; + if ($filter_last) { + if ($filter_last_period eq 'is') { + $filter_last_period = -1; + $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0; + } else { + detaint_natural($filter_last_period); + $filter_last_period = 14 if $filter_last_period < 14; + } + } + + # form sql queries + + my $now = (time); + my $bugs_sql = " + SELECT bug_id, short_desc, reporter, creation_ts + FROM bugs + WHERE product_id = ? + AND bug_status = 'UNCONFIRMED'"; + if (@component_ids) { + $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")"; + } + $bugs_sql .= " + ORDER BY creation_ts + "; + + my $comment_count_sql = " + SELECT COUNT(*) + FROM longdescs + WHERE bug_id = ? + "; + + my $comment_sql = " + SELECT who, bug_when, type, thetext, extra_data + FROM longdescs + WHERE bug_id = ? + "; + if (!Bugzilla->user->is_insider) { + $comment_sql .= " AND isprivate = 0 "; + } + $comment_sql .= " + ORDER BY bug_when DESC + LIMIT 1 + "; + + my $attach_sql = " + SELECT description, isprivate + FROM attachments + WHERE attach_id = ? + "; + + # work on an initial list of bugs + + my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id); + my @bugs; + + foreach my $entry (@$list) { + my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry; + + next unless $user->can_see_bug($bug_id); + + # get last comment information + + my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id); + my ($commenter_id, $comment_ts, $type, $comment, $extra) + = $dbh->selectrow_array($comment_sql, undef, $bug_id); + my $commenter = 0; + + # apply selected filters + + if ($filter_commenter) { + next if $comment_count <= 1; + + if ($filter_commenter_on eq 'reporter') { + next if $commenter_id != $reporter_id; + + } elsif ($filter_commenter_on eq 'noconfirm') { + $commenter = Bugzilla::User->new($commenter_id); + next if $commenter_id != $reporter_id + || $commenter->in_group('canconfirm'); + + } elsif ($filter_commenter_on eq 'is') { + next if $commenter_id != $filter_commenter_id; + } + } else { + $input->{'commenter'} = ''; + $input->{'commenter_is'} = ''; + } + + if ($filter_last) { + my $comment_time = str2time($comment_ts) + or next; + if ($filter_last_period == -1) { + next if $comment_time >= $filter_last_time; + } else { + next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period; + } + } else { + $input->{'last'} = ''; + $input->{'last_is'} = ''; + } + + # get data for attachment comments + + if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) { + my ($description, $is_private) = $dbh->selectrow_array($attach_sql, undef, $extra); + next if $is_private && !Bugzilla->user->is_insider; + $comment = "(Attachment) " . $description; + } + + # truncate long comments + + if (length($comment) > 80) { + $comment = substr($comment, 0, 80) . '...'; + } + + # build bug hash for template + + my $bug = {}; + $bug->{id} = $bug_id; + $bug->{summary} = $summary; + $bug->{reporter} = Bugzilla::User->new($reporter_id); + $bug->{creation_ts} = $creation_ts; + $bug->{commenter} = $commenter || Bugzilla::User->new($commenter_id); + $bug->{comment_ts} = $comment_ts; + $bug->{comment} = $comment; + $bug->{comment_count} = $comment_count; + push @bugs, $bug; + } + + @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs; + + $vars->{bugs} = \@bugs; + } else { + $input->{action} = ''; + } + + if (!$input->{filter_commenter} && !$input->{filter_last}) { + $input->{filter_commenter} = 1; + } + + $vars->{'input'} = $input; +} + +sub group_admins { + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + $user->in_group('editusers') + || ThrowUserError('auth_failure', { group => 'editusers', + action => 'run', + object => 'group_admins' }); + + my $query = " + SELECT groups.name, " . + $dbh->sql_group_concat('profiles.login_name', "','", 1) . " + FROM groups + LEFT JOIN user_group_map + ON user_group_map.group_id = groups.id + AND user_group_map.isbless = 1 + AND user_group_map.grant_type = 0 + LEFT JOIN profiles + ON user_group_map.user_id = profiles.userid + WHERE groups.isbuggroup = 1 + GROUP BY groups.name"; + + my @groups; + foreach my $group (@{ $dbh->selectall_arrayref($query) }) { + my @admins; + if ($group->[1]) { + foreach my $admin (split(/,/, $group->[1])) { + push(@admins, Bugzilla::User->new({ name => $admin })); + } + } + push(@groups, { name => $group->[0], admins => \@admins }); + } + + $vars->{'groups'} = \@groups; +} + +1; diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm new file mode 100644 index 000000000..95bdcbdf3 --- /dev/null +++ b/extensions/BMO/lib/WebService.pm @@ -0,0 +1,207 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 BMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Mozilla Foundation. Portions created +# by the Initial Developer are Copyright (C) 2011 the Mozilla Foundation. All +# Rights Reserved. +# +# Contributor(s): +# Dave Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::BMO::WebService; + +use strict; +use warnings; + +use base qw(Bugzilla::WebService); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::WebService::Util qw(validate); +use Bugzilla::Field; + +sub getBugsConfirmer { + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; + + defined($params->{names}) + || ThrowCodeError('params_required', + { function => 'BMO.getBugsConfirmer', params => ['names'] }); + + my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + + # start filtering to remove duplicate user ids + @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + + my $fieldid = get_field_id('bug_status'); + + my $query = "SELECT DISTINCT bugs_activity.bug_id + FROM bugs_activity + LEFT JOIN bug_group_map + ON bugs_activity.bug_id = bug_group_map.bug_id + WHERE bugs_activity.fieldid = ? + AND bugs_activity.added = 'NEW' + AND bugs_activity.removed = 'UNCONFIRMED' + AND bugs_activity.who = ? + AND bug_group_map.bug_id IS NULL + ORDER BY bugs_activity.bug_id"; + + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } + + return \%users; +} + +sub getBugsVerifier { + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; + + defined($params->{names}) + || ThrowCodeError('params_required', + { function => 'BMO.getBugsVerifier', params => ['names'] }); + + my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + + # start filtering to remove duplicate user ids + @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + + my $fieldid = get_field_id('bug_status'); + + my $query = "SELECT DISTINCT bugs_activity.bug_id + FROM bugs_activity + LEFT JOIN bug_group_map + ON bugs_activity.bug_id = bug_group_map.bug_id + WHERE bugs_activity.fieldid = ? + AND bugs_activity.removed = 'RESOLVED' + AND bugs_activity.added = 'VERIFIED' + AND bugs_activity.who = ? + AND bug_group_map.bug_id IS NULL + ORDER BY bugs_activity.bug_id"; + + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } + + return \%users; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Extension::BMO::Webservice - The BMO WebServices API + +=head1 DESCRIPTION + +This module contains API methods that are useful to user's of bugzilla.mozilla.org. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. + +=head2 getBugsConfirmer + +B<UNSTABLE> + +=over + +=item B<Description> + +This method returns public bug ids that a given user has confirmed (changed from +C<UNCONFIRMED> to C<NEW>). + +=item B<Params> + +You pass a field called C<names> that is a list of Bugzilla login names to find bugs for. + +=over + +=item C<names> (array) - An array of strings representing Bugzilla login names. + +=back + +=item B<Returns> + +=over + +A hash of Bugzilla login names. Each name points to an array of bug ids that the user has confirmed. + +=back + +=item B<Errors> + +=over + +=back + +=item B<History> + +=over + +=item Added in BMO Bugzilla B<4.0>. + +=back + +=back + +=head2 getBugsVerifier + +B<UNSTABLE> + +=over + +=item B<Description> + +This method returns public bug ids that a given user has verified (changed from +C<RESOLVED> to C<VERIFIED>). + +=item B<Params> + +You pass a field called C<names> that is a list of Bugzilla login names to find bugs for. + +=over + +=item C<names> (array) - An array of strings representing Bugzilla login names. + +=back + +=item B<Returns> + +=over + +A hash of Bugzilla login names. Each name points to an array of bug ids that the user has verified. + +=back + +=item B<Errors> + +=over + +=back + +=item B<History> + +=over + +=item Added in BMO Bugzilla B<4.0>. + +=back + +=back diff --git a/extensions/BMO/template/en/default/account/create.html.tmpl b/extensions/BMO/template/en/default/account/create.html.tmpl new file mode 100644 index 000000000..47355cf00 --- /dev/null +++ b/extensions/BMO/template/en/default/account/create.html.tmpl @@ -0,0 +1,181 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Byron Jones <glob@mozilla.com> + #%] + +[%# INTERFACE + # none + # + # Param("maintainer") is used to display the maintainer's email. + # Param("emailsuffix") is used to pre-fill the email field. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% title = BLOCK %] + Create a new [% terms.Bugzilla %] account +[% END %] + +[% PROCESS global/header.html.tmpl + title = title + style_urls = [ 'extensions/BMO/web/styles/create_account.css' ] +%] + +<script type="text/javascript"> +function onSubmit() { + var email = document.getElementById('login').value; + if (email == '') { + alert('You must enter your email address.'); + return false; + } + var isValid = + email.match(/@/) + && email.match(/@.+\./) + && !email.match(/\.$/) + && !email.match(/[\\()&<>,'"\[\]]/) + ; + if (!isValid) { + alert( + "The e-mail address doesn't pass our syntax checking for a legal " + + "email address.\n\nA legal address must contain exactly one '@', and " + + "at least one '.' after the @.\n\nIt must also not contain any of " + + "these special characters: \ ( ) & < > , ; : \" [ ], or any whitespace." + ); + return false; + } + return true; +} +</script> + +<table border="0" id="create-account"> +<tr> + +<td width="50%" id="create-account-left" valign="top"> + + <h2 class="column-header">I need help using a Mozilla Product</h2> + + <table border="0" id="product-list"> + [% INCLUDE product + icon = "firefox" + name = "Firefox Support" + url = "http://support.mozilla.com/" + desc = "Support for the Firefox web browser." + %] + [% INCLUDE product + icon = "firefox" + name = "Firefox for Mobile Support" + url = "http://support.mozilla.com/mobile" + desc = "Support for the Firefox Mobile web browser." + %] + [% INCLUDE product + icon = "thunderbird" + name = "Thunderbird Support" + url = "http://www.mozillamessaging.com/support/" + desc = "Support for Thunderbird email client." + %] + [% INCLUDE product + icon = "other" + name = "Support for other products" + url = "http://www.mozilla.org/support/" + desc = "Support for products not listed here." + %] + [% INCLUDE product + icon = "input" + name = "Feedback" + url = "http://input.mozilla.com/feedback" + desc = "Report issues with web site you use, or provide quick feedback for Firefox." + %] + [% INCLUDE product + icon = "idea" + name = "Ideas" + url = "http://input.mozilla.com/idea" + desc = "Offer us ideas on how to enhance Firefox." + %] + </table> + +</td> + +<td width="50%" id="create-account-right" valign="top"> + + <h2 class="column-header">I want to help</h2> + + <div id="right-blurb"> + <p> + Great! There are three things to know and do: + </p> + <ol> + <li> + Please consider reading our + <a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">bug writing guidelines</a>. + </li> + <li> + Bugzilla is a public place, so what you type and your email address will be visible + to all logged-in users. Some people use an + <a href="http://email.about.com/od/freeemailreviews/tp/free_email.htm" target="_blank">alternative email address</a> + for this reason. + </li> + <li> + Please give us an email address you want to use. Once we confirm that it works, + you'll be asked to set a password and then you can start filing bugs and helping fix them. + </li> + </ol> + </div> + + <h2 class="column-header">Create an account</h2> + + <form method="post" action="createaccount.cgi" onsubmit="return onSubmit()"> + <table id="create-account-form"> + <tr> + <td class="label">Email Address:</td> + <td> + <input size="35" id="login" name="login" placeholder="you@example.com">[% Param('emailsuffix') FILTER html %]</td> + <td> + <input type="submit" value="Create Account"> + </td> + </tr> + </table> + </form> + +</td> + +</tr> +</table> + +<p id="bmo-admin"> + If you think there's something wrong with Bugzilla, you can + <a href="mailto:bugzilla-admin@mozilla.org">send an email to the admins</a>, but + remember, they can't file bugs for you, or solve tech support problems. +</p> + +[% PROCESS global/footer.html.tmpl %] + +[% BLOCK product %] + <tr> + <td valign="top"> + <a href="[% url FILTER none %]"><img + src="extensions/BMO/web/producticons/[% icon FILTER uri %].png" + border="0" width="64" height="64"></a> + </td> + <td valign="top"> + <h2><a href="[% url FILTER none %]">[% name FILTER html %]</a></h2> + <div>[% desc FILTER html %]</div> + </td> + </tr> +[% END %] + diff --git a/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl new file mode 100644 index 000000000..216f2c53a --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl @@ -0,0 +1,48 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] +[%# INTERFACE: + # This template has no interface. + # + # Form variables from a bug submission (i.e. the fields on a template from + # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to + # pull out various custom fields and format an initial Description entry + # from them. + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +Requester: [% cgi.param('firstname') %] [%+ cgi.param('lastname') %] +Email: [% cgi.param('email') %] +Event Name: [% cgi.param('eventname') %] +Event Date: [% cgi.param("date") %] +Event Location: [% cgi.param("locations") %] +Event Website: [% cgi.param("website") %] +Event Description and Objectives: +[%+ cgi.param("goals") %] + +Attendees: [% IF cgi.param('attendees') != "--Please Select--" %][% cgi.param('attendees') %][% END %] +Target Audience: [% IF cgi.param('audience') != "--Please Select--" %][% cgi.param('audience') %][% END %] + +We'll be doing: [% cgi.param("doing").join(", ") %][% IF cgi.param('doing-other-what') %]: [% cgi.param('doing-other-what') %][% END %] + +Success will be measured by: +[%+ cgi.param('successmeasure') %] + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl new file mode 100644 index 000000000..c62461d42 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl @@ -0,0 +1,44 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] +[%# INTERFACE: + # This template has no interface. + # + # Form variables from a bug submission (i.e. the fields on a template from + # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to + # pull out various custom fields and format an initial Description entry + # from them. + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +List Name: [% cgi.param("listName") %] +List Admin: [% cgi.param("listAdmin") %] + +Short Description: +[%+ cgi.param("listShortDesc") %] + +[% IF cgi.param("listType") != "mozilla.com" %] +Long Description: +[%+ cgi.param("listLongDesc") %] +[% END %] + +Justification / Special Instructions: + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl new file mode 100644 index 000000000..0ec7687d4 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl @@ -0,0 +1,48 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] +[%# INTERFACE: + # This template has no interface. + # + # Form variables from a bug submission (i.e. the fields on a template from + # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to + # pull out various custom fields and format an initial Description entry + # from them. + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +Requester: [% cgi.param('firstname') %] [% cgi.param('lastname') %] +Email: [% cgi.param('email') %] + +Additional Swag: [% cgi.param("additional") %] + +Ship to: +[%+ cgi.param("shiptofirstname") +%] [%+ cgi.param("shiptolastname") +%] +[%+ cgi.param("shiptoaddress") +%] +[%+ cgi.param("shiptoaddress2") +%] +[%+ cgi.param("shiptocity") +%] [%+ cgi.param("shiptostate") +%] [%+ cgi.param("shiptopcode") +%] +[%+ cgi.param("shiptocountry") %] + +Phone: [% cgi.param("shiptophone") %] +[%+ IF cgi.param("shiptoidrut") %]Personal ID/RUT: [% cgi.param("shiptoidrut") %][% END %] + +Additional comments: + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl new file mode 100644 index 000000000..e4c6cabfb --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl @@ -0,0 +1,221 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Corporation Brownbag Requests" + style_urls = [ 'skins/standard/attachment.css', 'skins/custom/calendar.css' ] + style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }" + javascript_urls = [ 'js/attachment.js', 'js/field.js', 'js/util.js' ] + yui = [ 'autocomplete', 'calendar' ] +%] + +[% USE Bugzilla %] + +<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Brownbag Request</div> + +<p><strong>Brownbag Request:</strong> Please use this form if you plan on hosting a brownbag so that IT will be able to properly provide support. </p> + +<p>Process:</p> + +<ol><li>Complete and submit request below.</li> + <li>Your request will be reviewed and assigned to the appropriate person in IT.</li> +</ol> + +<p>These requests will only be visible internally in all cases and only to the +person who submitted the request and any persons designated in the CC line.</p> + +<script type="text/javascript"> +function trySubmit() { + var out = 'Topic: the_topic\r\nPresenter: the_presenter\r\nDate: the_date\r\nTime: the_time\r\nAudience: the_audience\r\nAir Mozilla: air_mozilla\r\nDial-in: dial_in\r\nArchive: to_archive\r\nMember of IT to help with A/V: it_help\r\nDescription: the_description'; + + var topic = document.getElementById('topic').value; + var presenter = document.getElementById('presenter').value; + var date = document.getElementById('date').value; + var time = document.getElementById('time_hour').value + ':' + document.getElementById('time_minute').value + document.getElementById('ampm').value; + var shortdesc = 'Brownbag Request - ' + topic + ' (' + date + ' ' + time + ')'; + var airmozilla = document.getElementById('airmozilla').checked? 'yes' : 'no'; + var dialin = document.getElementById('dialin').checked? 'yes' : 'no'; + var archive = document.getElementById('archive').checked? 'yes' : 'no'; + var ithelp = document.getElementById('ithelp').checked? 'yes' : 'no'; + + out = out.replace( /the_topic/, topic ); + out = out.replace( /the_presenter/, presenter ); + out = out.replace( /the_date/, date); + out = out.replace( /the_time/, time); + out = out.replace( /the_audience/, document.getElementById('audience').value ); + out = out.replace( /air_mozilla/, airmozilla); + out = out.replace( /dial_in/, dialin); + out = out.replace( /the_description/, document.getElementById('description').value ); + out = out.replace( /to_archive/, archive); + out = out.replace( /it_help/, ithelp); + + document.getElementById('comment').value = out; + document.getElementById('short_desc').value = shortdesc; + + return true; +} + +</script> + +<form method="post" action="post_bug.cgi" id="brownbagRequestForm" enctype="multipart/form-data" + onSubmit="return trySubmit();"> + + <input type="hidden" name="product" value="mozilla.org"> + <input type="hidden" name="component" value="Server Operations: Desktop Issues"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="other"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + <input type="hidden" name="comment" id="comment" value=""> + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-corporation-confidential"> +<table> + +<tr> + <td align="right"><strong>Presenter:</strong></td> + <td> + <input type="text" name="presenter" id="presenter" value="" size="60" /> + </td> + +</tr> + +<tr> + <td align="right"><strong>Topic:</strong></td> + <td> + <input type="text" name="topic" id="topic" value="" size="60" /> + </td> +</tr> + +<tr> + <td align="right"><strong>Date:</strong></td> + <td> + <input type="text" name="date" size="10" id="date" + align="right" value="" maxlength="10" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" id="button_calendar_date" + onclick="showCalendar('date')"><span>Calendar</span> + </button> + <div id="con_calendar_date"></div> + </td> +</tr> + +<tr> + <td align="right"><strong>Start Time:</strong></td> + <td> + <select name="time_hour" id="time_hour"> + <option value="12" selected>12</option> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + <option value="11">11</option> + </select>:<select name="time_minute" id="time_minute"> + <option value="00" selected>00</option> + <option value="15">15</option> + <option value="30">30</option> + <option value="45">45</option> + </select> + <select name="ampm" id="ampm"> + <option value="AM" selected>AM</option> + <option value="PM">PM</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>Intended Audience:</strong></td> + <td> + <select name="audience" id="audience"> + <option value="Public" selected>Open to Public</option> + <option value="Employees Only">Employees Only</option> + <option value="Interns">Interns</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>Air Mozilla Broadcasting?</strong></td> + <td align="left"><input type="checkbox" name="airmozilla" id="airmozilla"></td> +</tr> + +<tr> + <td align="right"><strong>Dial In?</strong></td> + <td align="left"><input type="checkbox" name="dialin" id="dialin"></td> +</tr> + +<tr> +<td align="right"><strong>Archive this?</strong></td> +<td align="left"><input type="checkbox" name="archive" id="archive" value="yes"></td> +</tr> + + +<tr> +<td align="right"><strong>Need IT to help run A/V?</strong></td> +<td align="left"><input type="checkbox" name="ithelp" id="ithelp" value="yes" checked></td> +</tr> + +<tr> + <td align="right"><strong>CC (optional):</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 60 + multiple => 5 + %] + </td> +</tr> + +<tr> + <th><label for="description">Description</label>:</th> + <td> + <em>Please briefly describe the brownbag and any specific needs you might have.</em><br> + + <textarea id="description" name="description" rows="10" cols="80"></textarea> + </td> +</tr> + + </table> + + <br> + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your request. + +</p> + +<script type="text/javascript"> + createCalendar('date'); +</script> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl new file mode 100644 index 000000000..08207fd8c --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl @@ -0,0 +1,187 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Ville Skyttä <ville.skytta@iki.fi> + # John Hoogstrate <hoogstrate@zeelandnet.nl> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Corporation/Foundation IT Requests" + javascript_urls = [ 'js/field.js' ] + yui = [ 'autocomplete' ] +%] + +[% USE Bugzilla %] + +<p><strong>Please use this form for IT requests only!</strong></p> +<p>If you have a bug to file, go <a href="enter_bug.cgi">here</a>.</p> + +<form method="post" action="post_bug.cgi" id="itRequestForm" enctype="multipart/form-data"> + <input type="hidden" name="product" value="mozilla.org"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="other"> + <script type="text/javascript"> + <!-- + function setsevdesc(theSelect) { + var theValue = theSelect.options[theSelect.selectedIndex].value; + if (theValue == 'blocker') { + document.getElementById('blockerdesc').style.display = 'block'; + document.getElementById('critdesc').style.display = 'none'; + } else if (theValue == 'critical') { + document.getElementById('blockerdesc').style.display = 'none'; + document.getElementById('critdesc').style.display = 'block'; + } else { + document.getElementById('blockerdesc').style.display = 'none'; + document.getElementById('critdesc').style.display = 'none'; + } + } + var compdesc = new Array(); + [% FOREACH comp IN product.components %] + compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]'; + [% END %] + compdesc['invalid'] = ''; + function setcompdesc(theRadio) { + var theValue = theRadio.value; + document.getElementById('compdescription').innerHTML = compdesc[theValue]; + } + // --> + </script> + <table> + <tr> + + <td align="right"> + <strong>Urgency:</strong> + </td> + + <td> + <select id="bug_severity" name="bug_severity" onchange="setsevdesc(this)"> + <option value="blocker">All work for IT stops until this is done</option> + <option value="critical">IT should work on it soon as possible (urgent)</option> + <option value="major">IT should get to it within 24 hours</option> + <option value="normal">IT should get to it within the next week</option> + <option value="minor" selected="selected">No rush, but hopefully IT can get to it soon</option> + <option value="trivial">Whenever IT can get around to it</option> + <option value="enhancement">This is just an idea, filing it so we don't forget</option> + </select> + </td> + <td> + <div id="blockerdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 30 minutes.</div> + <div id="critdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 8 hours.</div> + </td> + + </tr> + <tr> + <td align="right"><strong>Request Type:</strong></td> + <td style="white-space: nowrap;"> + <input type="radio" name="component" id="componentac" onclick="setcompdesc(this)" value="Server Operations: Account Requests"> + <label for="componentac">Request an LDAP/E-mail/etc. account</label><br> + <input type="radio" name="component" id="componentmvd" onclick="setcompdesc(this)" value="Server Operations: Desktop Issues"> + <label for="componentmvd">Desktop/Laptop/Printer/Phone/Tablet/License problem/order/request</label><br> + <input type="radio" name="component" id="componenttbm" onclick="setcompdesc(this)" value="Server Operations: RelEng"> + <label for="componenttbm">Report a problem with a tinderbox machine</label><br> + <input type="radio" name="component" id="componentwcp" onclick="setcompdesc(this)" value="Server Operations: Web Operations"> + <label for="componentwcp">Report a problem with a Mozilla website, or to request a change or push</label><br> + <input type="radio" name="component" id="componentacl" onclick="setcompdesc(this)" value="Server Operations: ACL Request"> + <label for="componentacl">Request a firewall change</label><br> + <input type="radio" name="component" id="componentso" onclick="setcompdesc(this)" value="Server Operations"> + <label for="componentso">Any other issue</label><br> + Mailing list requests should be filed <a href="[% ulrbase FILTER none %]enter_bug.cgi?product=mozilla.org&format=mozlist">here</a> instead. + </td> + <td id="compdescription" align="left" style="color: green; padding-left: 1em"> + </td> + <tr> + <td align="right"><strong>Summary:</strong></td> + <td colspan="3"> + <input name="short_desc" size="60" value="[% short_desc FILTER html %]"> + </td> + </tr> + + <tr> + <td align="right"><strong>CC (optional):</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 60 + multiple => 5 + %] + </td> + </tr> + + <tr><td align="right" valign="top"><strong>Description:</strong></td> + <td colspan="3"> + <textarea name="comment" rows="10" cols="80"> + [% comment FILTER html %]</textarea> + <br> + </td> + </tr> + + <tr> + <td align="right"><strong>URL (optional):</strong></td> + <td colspan="3"> + <input name="bug_file_loc" size="60" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> + + <tr><td colspan="4"> </td></tr> + + <tr> + <td colspan="4"> + <strong>Attachment (optional):</strong> + </td> + </tr> + + <tr> + <td align="right">File:</td> + <td colspan="3"> + <em>Enter the path to the file on your computer.</em><br> + <input type="file" id="data" name="data" size="50"> + <input type="hidden" name="contenttypemethod" value="autodetect" /> + </td> + </tr> + + <tr> + <td align="right">Description:</td> + <td colspan="3"> + <em>Describe the attachment briefly.</em><br> + <input type="text" id="description" name="description" size="60" maxlength="200"> + </td> + </tr> + </table> + + <br> + <!-- infra bugs --> + <input type="checkbox" name="groups" id="groups" value="infra" checked="checked"><label for="groups"><strong>This is an internal issue which should not be publicly visible.</strong></label><br>(please uncheck this box if it isn't) + <br><br> + + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your + request. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl new file mode 100644 index 000000000..c690370fe --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl @@ -0,0 +1,216 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Mark Smith <mark@mozilla.com> + # Reed Loden <reed@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Corporation Legal Requests" + style_urls = [ 'skins/standard/attachment.css' ] + javascript_urls = [ 'js/attachment.js', 'js/field.js' ] + yui = [ 'autocomplete' ] +%] + +[% IF user.in_group("mozilla-corporation-confidential") + OR user.in_group("mozilla-messaging-confidential") %] + +<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>MoLegal</div> + +<p><strong>Welcome to MoLegal.</strong> If you need legal assistance please complete the form below. To best service +your request, it is essential that you give us complete information so we can respond +properly and in a timely fashion. Please use this form only for requests for legal +assistance related to the Mozilla Corporation. If you’ve been arrested, do not use +this form or contact us.</p> + +<p>Process:</p> + +<ol><li>Complete and submit request below.</li> + <li>Your request will be reviewed and assigned to the appropriate legal service provider as necessary.</li> + <li>We will contact you to confirm receipt, gather any additional information, and agree on a completion timeline.</li> +</ol> + +<p>These requests will only be visible internally and in all cases and only to the +person who submitted the request and any persons designated in the CC line.</p> + +<p><strong>All Submissions, And Information Provided In Response To This Request, +Are Confidential And Subject To The Attorney-Client Privilege And Work Product Doctrine.</strong></p> + +<script type="text/javascript"> +function trySubmit() { + var out = 'description\r\n\r\n--- First Response:\r\nfirst_response\r\n\r\n--- First Response Timeframe:\r\nfirst_resp_time\r\n\r\n' + + '--- Business Objective:\r\nbusiness_objective\r\n\r\n--- Other Party:\r\nother_party'; + + out = out.replace( /description/, document.getElementById('ncomment').value ); + out = out.replace( /first_response/, document.getElementById('firstresp').value ); + out = out.replace( /first_resp_time/, document.getElementById('firstrespwhen').value ); + out = out.replace( /business_objective/, document.getElementById('busobj').value ); + out = out.replace( /other_party/, document.getElementById('otherparty').value ); + + document.getElementById('comment').value = out; + + return true; +} +</script> + +<form method="post" action="post_bug.cgi" id="legalRequestForm" enctype="multipart/form-data" + onSubmit="return trySubmit();"> + + <input type="hidden" name="product" value="Legal"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="unspecified"> +<input type="hidden" name="comment" id="comment" value=""> + +<table> + +<tr> + <td align="right"><strong>Request Type:</strong></td> + <td> + <select name="component"> + [%- FOREACH c = product.components %] + [% NEXT IF NOT c.is_active %] + <option value="[% c.name FILTER html %]" + [% " selected=\"selected\"" IF c.name == "General" %]> + [% c.name FILTER html -%] + </option> + [%- END %] + </select> + </td> +</tr> + +<tr> + <td align="right"> + <strong>Urgency: </strong> + </td> + <td> + <select id="bug_severity" name="bug_severity"> + <option value="blocker">Immediately - Don't leave the office, don't sleep until this is done (use with caution!)</option> + <option value="critical">A few days timeframe - critical priority</option> + <option value="major">One week timeframe - high priority</option> + <option value="normal">Two weeks timeframe - medium priority</option> + <option value="minor" selected="selected">One month timeframe - low priority</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>First Response:</strong></td> + <td> +<em>If your request is a project with numerous milestones or stages, please describe the first task you would like to see completed.</em><br> + <textarea id="firstresp" name="firstresp" cols="80" rows="3"></textarea><br /> + <select id="firstrespwhen" name="firstrespwhen"> + <option value="24 hours">Urgent! Please do this within 24 hours!</option> + <option value="Few days">Important, but I can wait a few days for this.</option> + <option value="One week">Sometime within the next week is fine.</option> + <option value="Few weeks" selected="selected">Anytime in the next few weeks would be acceptable.</option> + </select> + </td> +</tr> + + <tr> + <td align="right"><strong>Summary:</strong></td> + <td colspan="3"> + <input name="short_desc" size="60" value="[% short_desc FILTER html %]"> + </td> + </tr> + + <tr> + <td align="right"><strong>CC (optional):</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 60 + multiple => 5 + %] + </td> + </tr> + +<tr> + <td align="right"><strong>Name of Other Party:</strong></td> + <td> +<em>If applicable, include full legal entity name, address, and any other relevant contact information.</em><br> + <textarea id="otherparty" name="otherparty" rows="3" cols="80"></textarea> + </td> +</tr> + +<tr> + <td align="right"><strong>Business Objective:</strong></td> + <td> + <input type="text" name="busobj" id="busobj" value="" size="60" /> + </td> +</tr> + + <tr><td align="right" valign="top"><strong>Description:</strong></td> + <td colspan="3"> +<em>Describe your question, what you want and/or provide any relevant deal terms, restrictions, or provisions that are applicable.</em> + <textarea id="ncomment" name="ncomment" rows="10" cols="80"> + [% comment FILTER html %]</textarea> + <br> + </td> + </tr> + + <tr> + <td align="right"><strong>URL (optional):</strong></td> + <td colspan="3"> + <input name="bug_file_loc" size="60" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> + +<tr><td colspan=3><br><br></td></tr> +<tr><td></td><td colspan=2 style='background-color: #ddd;'><strong>Attachment (this is optional)</strong></td></tr> + +<tr> + <th><label for="data">File</label>:</th> + <td> + <em>Enter the path to the file on your computer.</em><br> + <input type="file" id="data" name="data" size="50"> + <input type="hidden" name="contenttypemethod" value="autodetect" /> + </td> +</tr> +<tr> + <th><label for="description">Description</label>:</th> + <td> + <em>Describe the attachment briefly.</em><br> + <input type="text" id="description" name="description" size="60" maxlength="200"> + </td> +</tr> + + </table> + + <br> + + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your request. +</p> + +[% ELSE %] + +<p>Sorry, you do not have access to this page.</p> + +[% END %] + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl new file mode 100644 index 000000000..d6b6c188f --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl @@ -0,0 +1,250 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Event Request Form" + javascript_urls = [ 'extensions/BMO/web/js/form_validate.js', + 'js/field.js', + 'js/util.js' ] + style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }" + yui = [ 'autocomplete', 'calendar' ] +%] + +<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Event Request Form</div> + +<p><strong>Event Request:</strong> Please use this form to file a request for an event. <b>This form is not a request for swag.</b></p> + +<p>Process:</p> + +<ol> + <li>Complete and submit request below.</li> + <li>Your request will be reviewed by the appropriate person in the Engagement team.</li> +</ol> + +<p>These requests will only be visible to the person who submitted the request, +any persons designated in the CC line, and authorized members of the Mozilla +Engagement team.</p> + +<script language="javascript" type="text/javascript"> +function trySubmit() { + + var date = document.getElementById('date').value; + var eventname = document.getElementById('eventname').value; + var shortdesc = 'Event Request (' + date + ') - ' + eventname; + document.getElementById('short_desc').value = shortdesc; + if(!isChecked('doing-other')) document.getElementById('doing-other-what').value = ''; + return true; +} + +function validateAndSubmit() { + + var alert_text = ''; + if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n"; + if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n"; + if(!isValidEmail(document.getElementById('email').value)) alert_text += "Please enter a valid email address\n"; + if(!isFilledOut('eventname')) alert_text += "Please enter an event name.\n"; + if(!isFilledOut('website')) alert_text += "Please enter an event website.\n"; + if(!isFilledOut('goals')) alert_text += "Please enter the Description and Objectives.\n"; + if(!isFilledOut('date')) alert_text += "Please enter an event date.\n"; + if(!isFilledOut('goals')) alert_text += "Please enter the event goals.\n"; + if(!isFilledOut('successmeasure')) alert_text += "Please enter how you will measure the success of the event.\n"; + + if(!isChecked('doing-booth') && + !isChecked('doing-speaking') && + !isChecked('doing-outreach') && + !isChecked('doing-other') + ) alert_text += "Please indicate what you'll be doing at the event.\n"; + if(isChecked('doing-other') && !isFilledOut('doing-other-what')) alert_text += "Please describe what 'other' thing you'll be doing at the event.\n"; + + //Everything required is filled out..try to submit the form! + if(alert_text == '') { + return trySubmit(); + } + + //alert text, stay here on the pagee + alert(alert_text); + return false; +} + +</script> + +<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data" + onSubmit="return validateAndSubmit();"> + + <input type="hidden" name="format" value="mktgevent"> + <input type="hidden" name="product" value="Marketing"> + <input type="hidden" name="component" value="Event Requests"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="unspecified"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-corporation-confidential"> +<table> + +<tr> + <td align="right"><strong>First Name: <span style="color: red;">*</span></strong></td> + <td align="left"> + <input type="text" name="firstname" id="firstname" value="" size="20" maxlength="20" /> + </td> +</tr> +<tr> + <td align="right"><strong>Last Name: <span style="color: red;">*</span></strong></td> + <td align="left"> + <input type="text" name="lastname" id="lastname" value="" size="20" maxlength="20"/> + </td> +</tr> + +<tr> + <td align="right"><strong>Email Address: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="email" id="email" value="" size="50" maxlength="50"/> + </td> +</tr> + +<tr> + <td align="right"><strong>CC (optional):</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 50 + multiple => 5 + %] + </td> +</tr> + +<tr> + <td><!-- spacer --> </td> +</tr> + +<tr> + <td><!-- spacer --> </td> +</tr> + +<tr> + <td align="right"><strong>Event Name: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="eventname" id="eventname" value="" size="50" maxlength="50"/> + </td> +</tr> + +<tr> + <td align="right"><strong>Event Date: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" id="date" name="date" size="10" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" id="button_calendar_date" + onclick="showCalendar('date')"><span>Calendar</span> + </button> + <div id="con_calendar_date"></div> + </td> +</tr> + +<tr> + <td align="right"><strong>Event Location: <span style="color: red;">*</span></strong><br /><small>(City, Country; or City, State)</small></td> + <td> + <input type="text" name="location" id="location" value="" size="50" maxlength="80" /> + </td> +</tr> + +<tr> + <td align="right"><strong>Event Website: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="website" id="website" value="" size="50" maxlength="120" /> + </td> +</tr> + +<tr> + <td align="right"><strong>Event Description and Key Objectives: <span style="color: red;">*</span></strong></td> + <td> + <textarea id="goals" name="goals" rows="5" cols="50"></textarea> + </td> +</tr> + +<tr> + <td align="right"><strong>What will you be doing at the event? <span style="color: red;">*</span></strong></td> + <td> + <input type="checkbox" id="doing-booth" name="doing" value="Booth or Table"><label for="doing-booth">Booth/Table</label><br /> + <input type="checkbox" id="doing-speaking" name="doing" value="Speaking Opportunity"><label for="doing-speaking">Speaking Opportunity</label><br /> + <input type="checkbox" id="doing-outreach" name="doing" value="PR or Media Outreach"><label for="doing-outreach">PR or Media Outreach</label><br /> + <input type="checkbox" id="doing-other" name="doing" value="Other"><label for="doing-other">Other, please explain:</label> + <input type="text" name="doing-other-what" id="doing-other-what" value="" size="50" maxlength="120"><br /> + </td> +</tr> + +<tr> + <td><!-- spacer --> </td> +</tr> + +<tr> + <td align="right"><strong>How many attendees will be at the event?</strong></td> + <td> + <select name="attendees" id="attendees"> + <option value="--Please Select--" selected>--Please Select--</option> + <option value="1-99">1-99</option> + <option value="100-499">100-499</option> + <option value="500-999">500-999</option> + <option value="1000+">1000+</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>Targeted Audience:</strong></td> + <td> + <select name="audience" id="audience"> + <option value="--Please Select--" selected>--Please Select--</option> + <option value="Contributors">Contributors (Developers, Education, Security, Designers, Localization, Support & Marketing)</option> + <option value="Potential New Users">Potential New Users</option> + <option value="User Community">User Community (Recent adopters, fans, Mozilla product evangelizers)</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>How will you measure the<br />success of your event? <span style="color: red;">*</span></strong></td> + <td> + <textarea id="successmeasure" name="successmeasure" rows="5" cols="50"></textarea> + </td> +</tr> + + + </table> + <br> + <input type="submit" id="commit" value="Submit Request"> + +<p> + <strong><span style="color: red;">*</span></strong> - Required field<br /> + Thanks for contacting us. + +</p> + +<script type="text/javascript"> + createCalendar('date'); +</script> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl new file mode 100644 index 000000000..138f1754b --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl @@ -0,0 +1,306 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Ville Skyttä <ville.skytta@iki.fi> + # John Hoogstrate <hoogstrate@zeelandnet.nl> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Discussion Forum / Mailing List Requests" + javascript_urls = [ 'extensions/BMO/web/js/form_validate.js', + 'js/field.js' ] + yui = [ 'autocomplete' ] +%] + +<script type="text/javascript"> +<!-- + function toggleGroup (theRadio) { + var radioValue = theRadio.value; + var groupDiv = YAHOO.util.Dom.get('groups'); + var group = YAHOO.util.Dom.get('group_35'); + if (radioValue == 'lists.mozilla.org') { + YAHOO.util.Dom.setStyle('listNameTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listNameOther', 'display', 'none'); + YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'none'); + YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('CCTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('URLTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('groups', 'display', 'none'); + group.disabled = true; + YAHOO.util.Dom.setStyle('submitDiv', 'display', 'block'); + } else if (radioValue == 'mozilla.org') { + YAHOO.util.Dom.setStyle('listNameTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'none'); + YAHOO.util.Dom.setStyle('listNameOther', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'none'); + YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('CCTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('URLTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('groups', 'display', 'table-row'); + group.disabled = false; + YAHOO.util.Dom.setStyle('submitDiv', 'display', 'block'); + } else { + YAHOO.util.Dom.setStyle('listNameTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'none'); + YAHOO.util.Dom.setStyle('listNameOther', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'none'); + YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'none'); + YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'inline'); + YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'none'); + YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('CCTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('URLTR', 'display', 'table-row'); + YAHOO.util.Dom.setStyle('groups', 'display', 'table-row'); + group.disabled = false; + YAHOO.util.Dom.setStyle('submitDiv', 'display', 'block'); + } + } + + function trySubmit() { + var listName = document.getElementById('listName').value; + var listAdmin = document.getElementById('listAdmin').value; + var listTypeRadio = document.getElementsByName('listType'); + var listType = ""; + + for (var i = 0; i < listTypeRadio.length; i++) { + if (listTypeRadio[i].checked) { + listType = listTypeRadio[i].value; + } + } + + var alert_text = ""; + var short_desc = ""; + + if (listType) { + if (listType == "lists.mozilla.org") { + document.getElementById('component').value = "Discussion Forums"; + short_desc = "Discussion Forum: " + listName; + } else if (listType == "mozilla.com" ) { + document.getElementById('component').value = "Server Operations: Desktop Issues"; + short_desc = "[Zimbra Distribution List Request] " + listName + "@" + listType; + } else { + document.getElementById('component').value = "Server Operations"; + short_desc = "[Mailman List Request] " + listName + "@" + listType; + } + } else { + alert_text += "Please select a list type\n"; + } + + if (!isFilledOut('listName')) { + alert_text += "Please enter the list name\n"; + } + + if ((!isValidEmail(listAdmin)) && (listType != 'mozilla.com')) { + alert_text += "Please enter a valid email address for the list administrator\n"; + } + + if (alert_text) { + alert(alert_text); + return false; + } + + document.getElementById('short_desc').value = short_desc; + + return true; + } +// --> +</script> + +<form method="post" action="post_bug.cgi" id="mozListRequestForm" + enctype="multipart/form-data" onSubmit="return trySubmit();"> + <input type="hidden" id="format" name="format" value="mozlist"> + <input type="hidden" id="product" name="product" value="mozilla.org"> + <input type="hidden" name="token" value="[% token FILTER html %]"> + <input type="hidden" id="rep_platform" name="rep_platform" value="All"> + <input type="hidden" id="op_sys" name="op_sys" value="Other"> + <input type="hidden" id="priority" name="priority" value="--"> + <input type="hidden" id="version" name="version" value="other"> + <input type="hidden" id="short_desc" name="short_desc" value=""> + <input type="hidden" id="component" name="component" value=""> + <input type="hidden" id="bug_severity" name="bug_severity" value="normal"> + + <table> + <tr id="listTypeTR" style="display: table-row;"> + <td align="right" width="15%"><strong>List Type:</strong></td> + <td> + <dl> + <dt> + <input type="radio" name="listType" id="lists_mozilla_org" + onclick="toggleGroup(this);" value="lists.mozilla.org"> + <label for="lists_mozilla_org">Standard Discussion Forum</label> + </dt> + <dd> + <label for="lists_mozilla_org"> + This option gives you a Mozilla <a href="https://www.mozilla.org/about/forums/">Discussion Forum</a>. + These are the normal mechanism for public discussion in the Mozilla project. They are made up of a + mailing list, a newsgroup and a Google Group (which maintains the list archives), all linked together. + Users can add and remove themselves. If you aren't sure, pick this one. + </label> + </dd> + <dt> + <input type="radio" name="listType" id="mozilla_org" + onclick="toggleGroup(this);" value="mozilla.org"> + <label for="mozilla_org">Mailing List Only</label> + </dt> + <dd> + <label for="mozilla_org"> + This option gives you a mailing list without the other access mechanisms. The + list can be private, although Mozilla is an "open by default" organization so + you need a good reason to have a private list. The archives are maintained by + Mailman, our mailing list software. Subscription can be open or + moderator-controlled. This type of list is normally hosted on the <b>mozilla.org</b> domain. + </label> + </dd> + <dt> + <input type="radio" name="listType" id="mozilla_com" + onclick="toggleGroup(this)" value="mozilla.com"> + <label for="mozilla_com">Distribution List</label> + </dt> + <dd> + <label for="mozilla_com"> + This option gives you a distribution list - basically, a mail exploder - on + mozilla.com. (This option is only appropriate for things which relate + specifically to Mozilla Corporation, such as confidential partner projects + or internal department lists.) Send email to the address, and it gets remailed + out to everyone on the list. There are no archives, and the "subscriber" list + is controlled by the IT team. + </label> + </dd> + </dl> + <hr> + </td> + </tr> + <tr id="listNameTR" style="display: none;"> + <td align="right" valign="top"><strong>List Name:</strong></td> + <td> + <input name="listName" id="listName" size="60" value="[% listName FILTER html %]"><br> + <span id="listNameDiscussion" style="display: none;">The desired name for the newsgroup. Should start with 'mozilla.' and fit somewhere in the hierarchy described <a href="https://www.mozilla.org/about/forums/">here</a>.</span> + <span id="listNameOther" style="display: none;">Only enter the part before the @, the domain name is chosen based on the list type.</span> + <hr> + </td> + </tr> + <tr id="listAdminTR" style="display: none;"> + <td align="right" valign="top" width="15%"><strong>List Administrator:</strong></td> + <td> + [% INCLUDE global/userselect.html.tmpl + id => "listAdmin" + name => "listAdmin" + value => "" + size => 60 + multiple => 5 + %] + <br> + <b>Note:</b>The list administrator is also initially considered to be the list moderator + and will be responsible for moderation tasks unless delegated to someone else. For + convenience, Bugzilla user accounts will autocomplete. The administrator is not required + to have a Bugzilla account, and you can enter an address that doesn't autocomplete if + necessary.<hr /> + </td> + </tr> + <tr id="listShortDescTR" style="display: none;"> + <td align="right" valign="top"><strong>Short one-line description:</strong></td> + <td> + <input name="listShortDesc" id="listShortDesc" size="60" value="[% listShortDesc FILTER html %]"><br /> + <span id="listShortDescMailman" style="display: none;">This will be shown to users on the index of lists on the server.</span> + <span id="listShortDescZimbra" style="display: none;">This will be shown as the "real name" in the Zimbra address book.</span> + <hr /> + </td> + </tr> + <tr id="listLongDescTR" style="display: none;"> + <td align="right" valign="top"><strong>Long description:</strong></td> + <td colspan="3"> + [% INCLUDE global/textarea.html.tmpl + name = 'listLongDesc' + id = 'listLongDesc' + minrows = 10 + maxrows = 25 + cols = constants.COMMENT_COLS + defaultcontent = listLongDesc + %] + <br>This will be shown at the top of the list's listinfo page. + <hr> + </td> + </tr> + <tr id="listCommentTR" style="display: none;"> + <td align="right" valign="top"><strong>Additional comments:</strong></td> + <td colspan="3"> + Justification for the list, special instructions, etc.<br> + [% INCLUDE global/textarea.html.tmpl + name = 'comment' + id = 'comment' + minrows = 10 + maxrows = 25 + cols = constants.COMMENT_COLS + defaultcontent = comment + %] + <hr> + </td> + </tr> + <tr id="CCTR" style="display: none;"> + <td align="right"><strong>CC (optional):</strong></td> + <td> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 60 + multiple => 5 + %] + </td> + </tr> + <tr id="URLTR" style="display: none;"> + <td align="right"><strong>URL (optional):</strong></td> + <td colspan="3"> + <input name="bug_file_loc" size="60" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> + </table> + <br> + + <div id="groups" style="display:none;"> + <!-- infra bugs --> + <input type="checkbox" name="groups" id="group_35" value="infra" disabled="true"> + <label for="group_35"><strong>This is an internal issue which should not be publicly visible.</strong></label> + <br><br> + </div> + + <div id="submitDiv" style="display: none;"> + <input type="submit" id="commit" value="Submit Request"> + +<p> + Thanks for contacting us. You will be notified by email of any progress made + in resolving your request. +</p> + </div> +</form> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl new file mode 100644 index 000000000..28a37a31e --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl @@ -0,0 +1,651 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Ville Skyttä <ville.skytta@iki.fi> + # Shane H. W. Travis <travis@sedsystems.ca> + # Marc Schumann <wurblzap@gmail.com> + # Akamai Technologies <bugzilla-dev@akamai.com> + # Max Kanat-Alexander <mkanat@bugzilla.org> + # Frédéric Buclin <LpSolit@gmail.com> + #%] + +[% PROCESS "global/field-descs.none.tmpl" %] + +[% title = BLOCK %]Create a PR Request[% END %] + +[% PROCESS global/header.html.tmpl + title = title + style_urls = [ 'skins/standard/attachment.css' ] + javascript_urls = [ "js/attachment.js", "js/util.js", + "js/field.js", "js/TUI.js" ] + onload = 'set_assign_to();' + yui = [ 'autocomplete' ] +%] + +<script type="text/javascript"> +<!-- + +var initialowners = new Array([% product.components.size %]); +var last_initialowner; +var initialccs = new Array([% product.components.size %]); +var components = new Array([% product.components.size %]); +var comp_desc = new Array([% product.components.size %]); +var flags = new Array([% product.components.size %]); +[% IF Param("useqacontact") %] + var initialqacontacts = new Array([% product.components.size %]); + var last_initialqacontact; +[% END %] +[% count = 0 %] +[%- FOREACH c = product.components %] + [% NEXT IF NOT c.is_active %] + components[[% count %]] = "[% c.name FILTER js %]"; + comp_desc[[% count %]] = "[% c.description FILTER html_light FILTER js %]"; + initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]"; + [% flag_list = [] %] + [% FOREACH f = c.flag_types(is_active=>1).bug %] + [% flag_list.push(f.id) %] + [% END %] + [% FOREACH f = c.flag_types(is_active=>1).attachment %] + [% flag_list.push(f.id) %] + [% END %] + flags[[% count %]] = [[% flag_list.join(",") FILTER js %]]; + [% IF Param("useqacontact") %] + initialqacontacts[[% count %]] = "[% c.default_qa_contact.login FILTER js %]"; + [% END %] + + [% SET initial_cc_list = [] %] + [% FOREACH cc_user = c.initial_cc %] + [% initial_cc_list.push(cc_user.login) %] + [% END %] + initialccs[[% count %]] = "[% initial_cc_list.join(', ') FILTER js %]"; + + [% count = count + 1 %] +[%- END %] + +function set_assign_to() { + // Based on the selected component, fill the "Assign To:" field + // with the default component owner, and the "QA Contact:" field + // with the default QA Contact. It also selectively enables flags. + var form = document.Create; + var assigned_to = form.assigned_to.value; + +[% IF Param("useqacontact") %] + var qa_contact = form.qa_contact.value; +[% END %] + + var index = -1; + if (form.component.type == 'select-one') { + index = form.component.selectedIndex; + } else if (form.component.type == 'hidden') { + // Assume there is only one component in the list + index = 0; + } + if (index != -1) { + var owner = initialowners[index]; + var component = components[index]; + if (assigned_to == last_initialowner + || assigned_to == owner + || assigned_to == '') { + form.assigned_to.value = owner; + last_initialowner = owner; + } + + document.getElementById('initial_cc').innerHTML = initialccs[index]; + document.getElementById('comp_desc').innerHTML = comp_desc[index]; + + [% IF Param("useqacontact") %] + var contact = initialqacontacts[index]; + if (qa_contact == last_initialqacontact + || qa_contact == contact + || qa_contact == '') { + form.qa_contact.value = contact; + last_initialqacontact = contact; + } + [% END %] + + // First, we disable all flags. Then we re-enable those + // which are available for the selected component. + var inputElements = document.getElementsByTagName("select"); + var inputElement, flagField; + for ( var i=0 ; i<inputElements.length ; i++ ) { + inputElement = inputElements.item(i); + if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) { + var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1"); + inputElement.disabled = true; + // Also disable the requestee field, if it exists. + inputElement = document.getElementById("requestee_type-" + id); + if (inputElement) inputElement.disabled = true; + } + } + // Now enable flags available for the selected component. + for (var i = 0; i < flags[index].length; i++) { + flagField = document.getElementById("flag_type-" + flags[index][i]); + // Do not enable flags the user cannot set nor request. + if (flagField && flagField.options.length > 1) { + flagField.disabled = false; + // Re-enabling the requestee field depends on the status + // of the flag. + toggleRequesteeField(flagField, 1); + } + } + } +} + +function fix_component() { + var form = document.Create; + var location = form.location.options[form.location.selectedIndex].value; + var fakecomp = form.fakecomp.options[form.fakecomp.selectedIndex].value; + var newcomp = location + " - " + fakecomp; + form.component.value = newcomp; + set_assign_to(); +} + +function handleWantsAttachment(wants_attachment) { + if (wants_attachment) { + document.getElementById('attachment_false').style.display = 'none'; + document.getElementById('attachment_true').style.display = 'block'; + } + else { + document.getElementById('attachment_false').style.display = 'block'; + document.getElementById('attachment_true').style.display = 'none'; + clearAttachmentFields(); + } +} + + +TUI_alternates['expert_fields'] = 'Show Advanced Fields'; +// Hide the Advanced Fields by default, unless the user has a cookie +// that specifies otherwise. +TUI_hide_default('expert_fields'); + +--> +</script> + +[% IF user.in_group("mozilla-confidential") %] + +[% USE Bugzilla %] +[% SET select_fields = {} %] +[% FOREACH field = Bugzilla.get_fields( + { type => constants.FIELD_TYPE_SINGLE_SELECT, custom => 0 }) +%] + [% select_fields.${field.name} = field %] +[% END %] + +<form name="Create" id="Create" method="post" action="post_bug.cgi" + enctype="multipart/form-data"> +<input type="hidden" name="product" value="[% product.name FILTER html %]"> +<input type="hidden" name="token" value="[% token FILTER html %]"> + +<table cellspacing="4" cellpadding="2" border="0" style="background: url(extensions/BMO/web/images/presshat.png) top right no-repeat"> +<tbody> + <tr> + <td colspan="2"> + <a id="expert_fields_controller" class="controller bz_default_hidden" + href="javascript:TUI_toggle_class('expert_fields')">Hide + Advanced Fields</a> + [%# Show the link if the browser supports JS %] + <script type="text/javascript"> + YAHOO.util.Dom.removeClass('expert_fields_controller', + 'bz_default_hidden'); + </script> + </td> + <td colspan="2"> + (<span class="required_star">*</span> = + <span class="required_explanation">Required Field</span>) + </td> + </tr> + + <tr> + <th>Product:</th> + <td width="10%">[% product.name FILTER html %]</td> + + <th>Reporter:</th> + <td width="100%">[% user.login FILTER html %]</td> + </tr> + + [%# We can't use the select block in these two cases for various reasons. %] +[% matches = default.component_.matches('^(.*) - (.*)$') %] +[% default.location = matches.0 %] +[% default.fakecomp = matches.1 %] +[% IF default.location == '' %] + [% default.location = 'US' %] +[% END %] +[% locations = [] %] +[% fakecomps = [] %] +[% FOREACH c = product.components %] + [% matches = c.name.match('^(.*) - (.*)$') %] + [% locations.push(matches.0) %] + [% fakecomps.push(matches.1) %] +[% END %] +[% locations = locations.unique %] +[% fakecomps = fakecomps.unique %] + <tr> + <th class="required"> + Location: + </th> + <td> + + <select name="location" onchange="fix_component();" size="7"> + [% FOREACH l = locations %] + <option value="[% l FILTER html %]" [% " selected=\"selected\"" IF l == default.location %]> + [% l FILTER html %] + </option> + [% END %] + </select> + <select name="component" onchange="set_assign_to();" size="7" + aria-required="true" class="required" style="display: none;"> + [%# Build the lists of assignees and QA contacts if "usemenuforusers" is enabled. %] + [% IF Param("usemenuforusers") %] + [% assignees_list = user.get_userlist.clone %] + [% qa_contacts_list = user.get_userlist.clone %] + [% END %] + + [%- FOREACH c = product.components %] + [% NEXT IF NOT c.is_active %] + <option value="[% c.name FILTER html %]" + [% " selected=\"selected\"" IF c.name == default.component_ %]> + [% c.name FILTER html -%] + </option> + [% IF Param("usemenuforusers") %] + [% INCLUDE build_userlist default_user = c.default_assignee, + userlist = assignees_list %] + [% INCLUDE build_userlist default_user = c.default_qa_contact, + userlist = qa_contacts_list %] + [% END %] + [%- END %] + </select> + </td> + + </tr> + <tr> + <th> + Request type: + </th> + <td> + + <select name="fakecomp" onchange="fix_component();" size="7"> + [% FOREACH f = fakecomps %] + <option value="[% f FILTER html %]" [% " selected=\"selected\"" IF f == default.fakecomp %]> + [% f FILTER html %] + </option> + [% END %] + </select> + </td> + <td colspan="2"> + [%# Enclose the fieldset in a nested table so that its width changes based + # on the length on the component description. %] + <table> + <tr> + <td> + <fieldset> + <legend>Request Description</legend> + <div id="comp_desc" class="comment">Select a request type to read its description.</div> + </fieldset> + </td> + </tr> + </table> + <input type="hidden" name="bug_severity" value="[% default.bug_severity %]"> + <input type="hidden" name="rep_platform" value="[% default.rep_platform %]"> + <input type="hidden" name="op_sys" value="[% default.op_sys %]"> + <input type="hidden" name="version" value="unspecified"> + </td> + </tr> +</tbody> + +<tbody class="expert_fields"> + <tr> + <td colspan="4"> </td> + </tr> + + <tr> +[% IF bug_status.size <= 1 %] + <input type="hidden" name="bug_status" + value="[% default.bug_status FILTER html %]"> + <th>Initial State:</th> + <td>[% display_value("bug_status", default.bug_status) FILTER html %]</td> +[% ELSE %] + [% INCLUDE bug/field.html.tmpl + bug = default, field = bug_fields.bug_status, + editable = (bug_status.size > 1), value = default.bug_status + override_legal_values = bug_status %] +[% END %] + + <td> </td> + [%# Calculate the number of rows we can use for flags %] + [% num_rows = 6 + (Param("useqacontact") ? 1 : 0) + + (user.is_timetracker ? 3 : 0) + + (Param("usebugaliases") ? 1 : 0) + %] + + <td rowspan="[% num_rows FILTER html %]"> + [% IF product.flag_types(is_active=>1).bug.size > 0 %] + [% display_flag_headers = 0 %] + [% any_flags_requesteeble = 0 %] + + [% FOREACH flag_type = product.flag_types(is_active=>1).bug %] + [% display_flag_headers = 1 %] + [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %] + [% END %] + + [% IF display_flag_headers %] + [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types(is_active=>1).bug + any_flags_requesteeble = any_flags_requesteeble + flag_table_id = "bug_flags" + %] + [% END %] + [% END %] + </td> + </tr> + + <tr> + <th><a href="page.cgi?id=fields.html#assigned_to">Assign To</a>:</th> + <td colspan="2"> + [% INCLUDE global/userselect.html.tmpl + id => "assigned_to" + name => "assigned_to" + value => assigned_to + disabled => assigned_to_disabled + size => 30 + emptyok => 1 + custom_userlist => assignees_list + %] + <noscript>(Leave blank to assign to component's default assignee)</noscript> + </td> + </tr> + +[% IF Param("useqacontact") %] + <tr> + <th>QA Contact:</th> + <td colspan="2"> + [% INCLUDE global/userselect.html.tmpl + id => "qa_contact" + name => "qa_contact" + value => qa_contact + disabled => qa_contact_disabled + size => 30 + emptyok => 1 + custom_userlist => qa_contacts_list + %] + <noscript>(Leave blank to assign to default qa contact)</noscript> + </td> + </tr> +[% END %] + + <tr> + <th>CC:</th> + <td colspan="2"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + disabled => cc_disabled + size => 30 + multiple => 5 + %] + </td> + </tr> + + <tr> + <th>Default CC:</th> + <td colspan="2"> + <div id="initial_cc"> + </div> + </td> + </tr> + + <tr> + <td colspan="3"> </td> + </tr> + +[% IF user.is_timetracker %] + <tr> + <th>Estimated Hours:</th> + <td colspan="2"> + <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]"> + </td> + </tr> + <tr> + <th>Deadline:</th> + <td colspan="2"> + <input name="deadline" size="10" maxlength="10" value="[% deadline FILTER html %]"> + <small>(YYYY-MM-DD)</small> + </td> + </tr> + + <tr> + <td colspan="3"> </td> + </tr> +[% END %] + +[% IF Param("usebugaliases") %] + <tr> + <th>Alias:</th> + <td colspan="2"> + <input name="alias" size="20" value="[% alias FILTER html %]"> + </td> + </tr> +[% END %] + + <tr> + <th>URL:</th> + <td colspan="2"> + <input name="bug_file_loc" size="40" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> +</tbody> + +<tbody> + + <tr> + <th class="required">Summary:</th> + <td colspan="3"> + <input name="short_desc" size="70" value="[% short_desc FILTER html %]" + maxlength="255" spellcheck="true" aria-required="true" + class="required"> + </td> + </tr> + + <tr> + <th>Description:</th> + <td colspan="3"> + [% defaultcontent = BLOCK %] + [% IF cloned_bug_id %] ++++ This [% terms.bug %] was initially created as a clone of [% terms.Bug %] #[% cloned_bug_id %] +++ + + + [% END %] + [%-# We are within a BLOCK. The comment will be correctly HTML-escaped + # by global/textarea.html.tmpl. So we must not escape the comment here. %] + [% comment FILTER none %] + [%- END %] + [% INCLUDE global/textarea.html.tmpl + name = 'comment' + id = 'comment' + minrows = 10 + maxrows = 25 + cols = constants.COMMENT_COLS + defaultcontent = defaultcontent + %] + <br> + </td> + </tr> + + [% IF user.is_insider %] + <tr class="expert_fields"> + <th> </th> + <td colspan="3"> + + <input type="checkbox" id="commentprivacy" name="commentprivacy" + [% " checked=\"checked\"" IF commentprivacy %]> + <label for="commentprivacy"> + Make description private (visible only to members of the + <strong>[% Param('insidergroup') FILTER html %]</strong> group) + </label> + </td> + </tr> + [% END %] + + <tr> + <th>Attachment:</th> + <td colspan="3"> + <script type="text/javascript"> + <!-- + document.write( '<div id="attachment_false">' + + '<input type="button" value="Add an attachment" ' + + 'onClick="handleWantsAttachment(true)"> ' + + '<em style="display: none">This button has no ' + + 'functionality for you because your browser does ' + + 'not support CSS or does not use it.<\/em>' + + '<\/div>' + + '<div id="attachment_true" style="display: none">' + + '<input type="button" ' + + 'value="Don\'t add an attachment " ' + + 'onClick="handleWantsAttachment(false)">'); + //--> + </script> + <fieldset> + <legend>Add an attachment</legend> + <table class="attachment_entry"> + [% PROCESS attachment/createformcontents.html.tmpl + flag_types = product.flag_types(is_active=>1).attachment + any_flags_requesteeble = 1 + flag_table_id ="attachment_flags" %] + </table> + </fieldset> + <script type="text/javascript"> + <!-- + document.write('<\/div>'); + //--> + </script> + </td> + </tr> +</tbody> + +<tbody class="expert_fields"> + [% IF user.in_group('editbugs', product.id) %] + [% IF use_keywords %] + <tr> + [% INCLUDE bug/field.html.tmpl + bug = default, field = bug_fields.keywords, editable = 1, + value = keywords, desc_url = "describekeywords.cgi", + value_span = 3 %] + </tr> + [% END %] + + <tr> + <th>Status Whiteboard:</th> + <td colspan="3"> + <input id="status_whiteboard" name="status_whiteboard" size="70" + value="[% status_whiteboard %]"> + </td> + </tr> + <tr> + <th>Depends on:</th> + <td colspan="3"> + <input name="dependson" accesskey="d" value="[% dependson FILTER html %]"> + </td> + </tr> + <tr> + <th>Blocks:</th> + <td colspan="3"> + <input name="blocked" accesskey="b" value="[% blocked FILTER html %]"> + </td> + </tr> + [% END %] +</tbody> + +<tbody class="expert_fields"> + [% IF group.size %] + <tr> + <th> </th> + <td colspan="3"> + <br> + <strong> + Only users in all of the selected groups can view this [% terms.bug %]: + </strong> + <br> + <font size="-1"> + (Leave all boxes unchecked to make this a public [% terms.bug %].) + </font> + <br> + <br> + + <!-- Checkboxes --> + [% FOREACH g = group %] + + <input type="checkbox" id="bit-[% g.bit %]" + name="bit-[% g.bit %]" value="1" + [% " checked=\"checked\"" IF g.checked %]> + <label for="bit-[% g.bit %]">[% g.description FILTER html_light %]</label><br> + [% END %] + </td> + </tr> + [% END %] +</tbody> + +<tbody> + [%# Form controls for entering additional data about the bug being created. %] + [% Hook.process("form") %] + + <tr> + <th> </th> + <td colspan="3"> + <input type="submit" id="commit" value="Submit [% terms.Bug %]" + onclick="if (this.form.short_desc.value == '') + { alert('Please enter a summary sentence for this [% terms.bug %].'); + return false; } return true;"> + + <input type="submit" name="maketemplate" id="maketemplate" + value="Remember values as bookmarkable template" + class="expert_fields"> + </td> + </tr> +</tbody> + </table> + <input type="hidden" name="form_name" value="enter_bug"> +</form> + +[%# Links or content with more information about the bug being created. %] +[% Hook.process("end") %] + +[% ELSE %] + +<p>Sorry, you do not have access to this page.</p> + +[% END %] + +[% PROCESS global/footer.html.tmpl %] + +[% BLOCK build_userlist %] + [% user_found = 0 %] + [% default_login = default_user.login %] + [% RETURN UNLESS default_login %] + + [% FOREACH user = userlist %] + [% IF user.login == default_login %] + [% user_found = 1 %] + [% LAST %] + [% END %] + [% END %] + + [% userlist.push({login => default_login, + identity => default_user.identity, + visible => 1}) + UNLESS user_found %] +[% END %] diff --git a/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl new file mode 100644 index 000000000..c0b3ea237 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl @@ -0,0 +1,85 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Ville SkyttŠ <ville.skytta@iki.fi> + # John Hoogstrate <hoogstrate@zeelandnet.nl> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Powered by Mozilla Logo Requests" +%] + +[% USE Bugzilla %] + +<p>If you are interested in using the <a href="http://www.mozilla.org/poweredby">Powered by Mozilla logo</a>, +please provide some information about your application or product.</p> + +<p><strong>Please use this form for Powered by Mozilla logo requests only.</strong></p> + +<form method="post" action="post_bug.cgi" id="tmRequestForm"> + + <input type="hidden" name="product" value="Websites"> + <input type="hidden" name="component" value="www.mozilla.org"> + <input type="hidden" name="bug_severity" value="enhancement"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="version" value="other"> + <!-- Marketing-Private --> + <input type="hidden" name="groups" value="marketing-private"> + + <table> + <tr> + <td align="right"><strong>Application or Product Name:</strong></td> + <td colspan="3"> + <input name="short_desc" size="60" value="Powered by Mozilla request for: [% short_desc FILTER html %]"> + </td> + </tr> + + <tr> + <td align="right"><strong>URL (optional):</strong></td> + <td colspan="3"> + <input name="bug_file_loc" size="60" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> + + <tr><td align="right" valign="top"><strong>Comments (optional):</strong></td> + <td colspan="3"> + <textarea name="comment" rows="10" cols="80"> + [% comment FILTER html %]</textarea> + <br> + </td> + </tr> + + </table> + + <br> + + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your + request. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl new file mode 100644 index 000000000..cdbce5c8c --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl @@ -0,0 +1,217 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Corporation Mountain View Presentation Request" + javascript_urls = [ 'js/field.js', 'js/util.js' ] + style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }" + yui = [ 'autocomplete', 'calendar' ] +%] + +<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Mountain View Presentation Request</div> + +<p><strong>Mountain View Presentation Request:</strong> Please use this form if you plan on hosting a presentation so that IT will be able to properly provide support. </p> + +<p>Process:</p> + +<ol><li>Complete and submit request below.</li> + <li>Your request will be reviewed and assigned to the appropriate person in IT.</li> +</ol> + +<p>These requests will only be visible internally in all cases and only to the +person who submitted the request and any persons designated in the CC line.</p> + +<script type="text/javascript"> +function trySubmit() { + var out = 'Topic: the_topic\r\nPresenter: the_presenter\r\nDate: the_date\r\nTime: the_time\r\nAudience: the_audience\r\nAir Mozilla: air_mozilla\r\nDial-in: dial_in\r\nArchive: to_archive\r\nMember of IT to help with A/V: it_help\r\nDescription: the_description'; + + var topic = document.getElementById('topic').value; + var presenter = document.getElementById('presenter').value; + var date = document.getElementById('date').value; + var time = document.getElementById('time_hour').value + ':' + document.getElementById('time_minute').value + document.getElementById('ampm').value; + var shortdesc = 'Mountain View Presentation Request - ' + topic + ' (' + date + ' ' + time + ')'; + var airmozilla = document.getElementById('airmozilla').checked? 'yes' : 'no'; + var dialin = document.getElementById('dialin').checked? 'yes' : 'no'; + var archive = document.getElementById('archive').checked? 'yes' : 'no'; + var ithelp = document.getElementById('ithelp').checked? 'yes' : 'no'; + + out = out.replace( /the_topic/, topic ); + out = out.replace( /the_presenter/, presenter ); + out = out.replace( /the_date/, date); + out = out.replace( /the_time/, time); + out = out.replace( /the_audience/, document.getElementById('audience').value ); + out = out.replace( /air_mozilla/, airmozilla); + out = out.replace( /dial_in/, dialin); + out = out.replace( /the_description/, document.getElementById('description').value ); + out = out.replace( /to_archive/, archive); + out = out.replace( /it_help/, ithelp); + + document.getElementById('comment').value = out; + document.getElementById('short_desc').value = shortdesc; + + return true; +} + +</script> + +<form method="post" action="post_bug.cgi" id="presentationRequestForm" enctype="multipart/form-data" + onSubmit="return trySubmit();"> + + <input type="hidden" name="product" value="mozilla.org"> + <input type="hidden" name="component" value="Server Operations: Desktop Issues"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="other"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + <input type="hidden" name="comment" id="comment" value=""> + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-corporation-confidential"> +<table> + +<tr> + <td align="right"><strong>Presenter:</strong></td> + <td> + <input type="text" name="presenter" id="presenter" value="" size="60" /> + </td> + +</tr> + +<tr> + <td align="right"><strong>Topic:</strong></td> + <td> + <input type="text" name="topic" id="topic" value="" size="60" /> + </td> +</tr> + +<tr> + <td align="right"><strong>Date:</strong></td> + <td> + <input type="text" id="date" name="date" size="10" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" id="button_calendar_date" + onclick="showCalendar('date')"><span>Calendar</span> + </button> + <div id="con_calendar_date"></div> + </td> +</tr> + +<tr> + <td align="right"><strong>Start Time:</strong></td> + <td> + <select name="time_hour" id="time_hour"> + <option value="12" selected>12</option> + <option value="1">1</option> + <option value="2">2</option> + <option value="3">3</option> + <option value="4">4</option> + <option value="5">5</option> + <option value="6">6</option> + <option value="7">7</option> + <option value="8">8</option> + <option value="9">9</option> + <option value="10">10</option> + <option value="11">11</option> + </select>:<select name="time_minute" id="time_minute"> + <option value="00" selected>00</option> + <option value="15">15</option> + <option value="30">30</option> + <option value="45">45</option> + </select> + <select name="ampm" id="ampm"> + <option value="AM" selected>AM</option> + <option value="PM">PM</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>Intended Audience:</strong></td> + <td> + <select name="audience" id="audience"> + <option value="Public" selected>Open to Public</option> + <option value="Employees Only">Employees Only</option> + <option value="Interns">Interns</option> + </select> + </td> +</tr> + +<tr> + <td align="right"><strong>Air Mozilla Broadcasting?</strong></td> + <td align="left"><input type="checkbox" name="airmozilla" id="airmozilla"></td> +</tr> + +<tr> + <td align="right"><strong>Dial In?</strong></td> + <td align="left"><input type="checkbox" name="dialin" id="dialin"></td> +</tr> + +<tr> +<td align="right"><strong>Archive this?</strong></td> +<td align="left"><input type="checkbox" name="archive" id="archive" value="yes"></td> +</tr> + + +<tr> +<td align="right"><strong>Need IT to help run A/V?</strong></td> +<td align="left"><input type="checkbox" name="ithelp" id="ithelp" value="yes" checked></td> +</tr> + +<tr> + <td align="right"><strong>CC (optional):</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 60 + multiple => 5 + %] + </td> +</tr> + +<tr> + <th><label for="description">Description</label>:</th> + <td> + <em>Please briefly describe the presentation and any specific needs you might have.</em><br> + + <textarea id="description" name="description" rows="10" cols="80"></textarea> + </td> +</tr> + + </table> + + <br> + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your request. + +</p> + +<script type="text/javascript"> + createCalendar('date'); +</script> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl new file mode 100644 index 000000000..f7edccbe1 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl @@ -0,0 +1,221 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Swag Request Form" + javascript_urls = [ 'extensions/BMO/web/js/swag.js', + 'extensions/BMO/web/js/form_validate.js', + 'js/field.js' ] + yui = [ 'autocomplete' ] +%] + +<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Swag Request Form</div> + +<p><strong>Swag Request:</strong> Please use this form to file a request for swag. </p> + +<ol> + <li>You first need submit a <a href="/enter_bug.cgi?product=Marketing&format=mktgevent">Event Request Form</a>. You'll be asked for the bug number below.</li> + <li>Complete and submit request below.</li> + <li>Your request will be reviewed by the appropriate person in the Engagement team.</li> + <li>Your swag request will be reviewed and if approved shipped to you from + one of our two fulfillment houses. <i>Please note that swag is expensive and + products change over time - we are happy to send you a small quantity of swag + to use at your event!</i></li> +</ol> + +<p>These requests will only be visible to the person who submitted the request, +any persons designated in the CC line, and authorized members of the Mozilla +Engagement team.</p> + +<script language="javascript" type="text/javascript"> +function trySubmit() { + + var firstname = document.getElementById('firstname').value; + var lastname = document.getElementById('lastname').value; + var requester = firstname + ' ' + lastname; + var shortdesc = 'Swag Request - ' + requester; + document.getElementById('short_desc').value = shortdesc; + + // the following fields we don't let the user mess with because they're + // calculated, but they need to be submitted, and disabled fields don't submit + document.getElementById('Totalswag').disabled = false; + document.getElementById('mens_total').disabled = false; + document.getElementById('womens_total').disabled = false; + + return true; +} + +function validateAndSubmit() { + + var alert_text = ''; + if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n"; + if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n"; + if(!isFilledOut('dependson')) alert_text += "Please enter the bug number for your Event Request Form\n"; + if(!isValidEmail(document.getElementById('email').value)) alert_text += "Please enter a valid email address\n"; + + //Everything required is filled out..try to submit the form! + if(alert_text == '') { + return trySubmit(); + } + + //alert text, stay here on the pagee + alert(alert_text); + return false; +} + +</script> + +<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data" + onSubmit="return validateAndSubmit();"> + + <input type="hidden" name="format" value="swag"> + <input type="hidden" name="product" value="Marketing"> + <input type="hidden" name="component" value="Swag Requests"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="unspecified"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-corporation-confidential"> +<table> + +<tr> + <td align="right"><strong>First Name: <span style="color: red;">*</span></strong></td> + <td align="left"> + <input type="text" name="firstname" id="firstname" value="" size="20" maxlength="20" /> + </td> +</tr> +<tr> + <td align="right"><strong>Last Name: <span style="color: red;">*</span></strong></td> + <td align="left"> + <input type="text" name="lastname" id="lastname" value="" size="20" maxlength="20"/> + </td> +</tr> + +<tr> + <td align="right"><strong>Email Address: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="email" id="email" value="" size="50" maxlength="50"/> + </td> +</tr> + +<tr> + <td align="right"><strong>CC:</strong></td> + <td colspan="3"> + [% INCLUDE global/userselect.html.tmpl + id => "cc" + name => "cc" + value => cc + size => 50 + multiple => 5 + %] + </td> +</tr> + +<tr> + <td><!-- spacer --> </td> +</tr> + +<tr> + <td><!-- spacer --> </td> +</tr> + +<tr> + <td align="right"><strong>Bug number assigned to previously- <br>submitted <a href="/enter_bug.cgi?product=Marketing&format=mktgevent">Event Request Form</a>: <span style="color: red;">*</span></strong></td> + <td colspan="3"><input name="dependson" id="dependson"></td> +</tr> + +<tr> + <td align="right"><strong>Specific swag needed?</strong></td> + <td> + <textarea id="additional" name="additional" rows="5" cols="50"></textarea> + </td> +</tr> + +<tr> + <td align="right"><br><br><strong>Ship to:</strong></td> + <td colspan="3"></td> +</tr> +<tr> + <td align="right"><strong>First name:</strong></td> + <td colspan="3"><input name="shiptofirstname" id="shiptofirstname"></td> +</tr> +<tr> + <td align="right"><strong>Last name:</strong></td> + <td colspan="3"><input name="shiptolastname" id="shiptolastname"></td> +</tr> +<tr> + <td align="right"><strong>Address</strong></td> + <td colspan="3"><input name="shiptoaddress" id="shiptoaddress" size="60"></td> +</tr> +<tr> + <td align="right"><strong>Address 2</strong></td> + <td colspan="3"><input name="shiptoaddress2" id="shiptoaddress2" size="60"></td> +</tr> +<tr> + <td align="right"><strong>City</strong></td> + <td colspan="3"><input name="shiptocity" id="shiptocity"></td> +</tr> +<tr> + <td align="right"><strong>State</strong></td> + <td colspan="3"><input name="shiptostate" id="shiptostate"></td> +</tr> +<tr> + <td align="right"><strong>Country</strong></td> + <td colspan="3"><input name="shiptocountry" id="shiptocountry"></td> +</tr> +<tr> + <td align="right"><strong>Postal Code</strong></td> + <td colspan="3"><input name="shiptopcode" id="shiptopcode"></td> +</tr> +<tr> + <td align="right"><strong>Telephone</strong></td> + <td colspan="3"><input name="shiptophone" id="shiptophone"></td> +</tr> +<tr> + <td align="right"><strong>Personal ID/RUT</strong><br><small>(if your country requires this)</small></td> + <td colspan="3"><input name="shiptoidrut" id="shiptoidrut"></td> +</tr> + +<tr><td colspan="4"><br><br></td></tr> + +<tr> + <td align="right"><label for="comment"><strong>Any additional comments?</strong></label></td> + <td> + <textarea id="comment" name="comment" rows="5" cols="50"></textarea> + </td> +</tr> + + </table> + <br> + <input type="submit" id="commit" value="Submit Request"> + +<p> + <strong><span style="color: red;">*</span></strong> - Required field<br /> + Thanks for contacting us. + You will be notified by email of any progress made in resolving your request. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl new file mode 100644 index 000000000..116564255 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl @@ -0,0 +1,87 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Ville Skyttä <ville.skytta@iki.fi> + # John Hoogstrate <hoogstrate@zeelandnet.nl> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Trademark Usage Requests" +%] + +[% USE Bugzilla %] + +<p> + If, after reading + <a href="http://www.mozilla.org/foundation/trademarks/">the trademark policy + documents</a>, you know you need permission to use a certain trademark, this + is the place to be. +</p> + +<p><strong>Please use this form for trademark requests only!</strong></p> + +<form method="post" action="post_bug.cgi" id="tmRequestForm"> + + <input type="hidden" name="product" value="Marketing"> + <input type="hidden" name="component" value="Trademark Permissions"> + <input type="hidden" name="bug_severity" value="enhancement"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="priority" value="P3"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="version" value="unspecified"> + <!-- Marketing-Private --> + <input type="hidden" name="groups" value="marketing-private"> + + <table> + <tr> + <td align="right"><strong>Summary:</strong></td> + <td colspan="3"> + <input name="short_desc" size="60" value="[% short_desc FILTER html %]"> + </td> + </tr> + + <tr><td align="right" valign="top"><strong>Description:</strong></td> + <td colspan="3"> + <textarea name="comment" rows="10" cols="80"> + [% comment FILTER html %]</textarea> + <br> + </td> + </tr> + <tr> + <td align="right"><strong>URL (optional):</strong></td> + <td colspan="3"> + <input name="bug_file_loc" size="60" + value="[% bug_file_loc FILTER html %]"> + </td> + </tr> + </table> + + <br> + + <input type="submit" id="commit" value="Submit Request"> +</form> + +<p>Thanks for contacting us. + You will be notified by email of any progress made in resolving your + request. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl b/extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl new file mode 100644 index 000000000..e9a480090 --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl @@ -0,0 +1,38 @@ +[%# 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 BMO Bugzilla Extension. + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Byron Jones <glob@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps - Application Form" + +%] + +<h1>Thank you!</h1> + +<p> +Thank you for submitting your Mozilla Reps Application Form. A Mozilla Rep +mentor will contact you shortly at your bugzilla email address. +</p> + +<p style="font-size: x-small"> +Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a> +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl new file mode 100644 index 000000000..e7cea5d2b --- /dev/null +++ b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl @@ -0,0 +1,140 @@ +<!-- 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Matthew Tuck <matty@chariot.net.au> + #%] + +[%# Migration note: this file corresponds to the old Param + # 'entryheaderhtml' + #%] + +[%# You can make the output of this template product-specific by using + # Template Toolkit IF statements. The current product name is stored in + # the 'product' variable. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% IF product == "Bugzilla" %] + [% IF format == "guided" %] +<p align="center"> +<table WIDTH="90%" BGCOLOR="#FFFFFF" style="border: 8px double red;"><tr> +<td valign="middle" align="center"><img width="200" height="200" src="extensions/BMO/web/images/stop-sign.gif"></td> +<td align="center" valign="middle"> +<font size="+2"><b> +Are you here because you have a problem with +<a href="http://www.mozilla.com/firefox/">Firefox</a> or +<a href="http://www.mozilla.com/thunderbird/">Thunderbird</a>?</b><br> +<b>If so, you're in the wrong place.</b></font><br> + +If you are having a problem with Firefox, +<a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox">file +your [% terms.bug %] in the Firefox product</a>.<br> + +If you are having a problem with Thunderbird, +<a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Thunderbird">file +your [% terms.bug %] in the Thunderbird product</a>.<br> + +If none of those is a good fit for your [% terms.bug %], you can pick from +the <a href="https://bugzilla.mozilla.org/enter_bug.cgi">full product list</a>. +</td></tr></table> + +</p> + +<h3>Step 0 - Make sure you are in the right place</h3> + + [% END %] + +<p> +<b>The product you have chosen is for [% terms.bug %] reports and enhancement requests for the +<a href="http://www.bugzilla.org/">Bugzilla [% terms.bug %] tracking software</a> only.</b> +If your [% terms.bug %] is not reporting that Bugzilla is broken or that you'd like +a new feature in Bugzilla, your [% terms.bug %] report does not belong in this product. +[% IF format == "guided" %] + See the instructions next to the stop sign above. +[% ELSE %] + Please <a href="https://bugzilla.mozilla.org/enter_bug.cgi">choose a different product</a>. +[% END %] +</p> + +<p><b>We WILL NOT accept [% terms.bug %] reports for Bugzilla +installed via the Debian packaging system. If you obtained Bugzilla from Debian, +please visit the <a href="http://www.debian.org/support">Debian Support page</a>, +or file a bug on the <a href="http://bugs.debian.org/cgi-bin/pkgreport.cgi?pkg=bugzilla">Debian +Bug Tracker</a>.</b> The Debian package maintainer will then determine whether the bug +is specific to the package or not, and can move the bug "upstream" if needed.</p> + +<p> +Bugs specific to bugzilla.mozilla.org, rather than the Bugzilla software in +general (which is used by many sites), should be filed in the +<a href="enter_bug.cgi?product=mozilla.org">mozilla.org product</a>. +</p> + +<p> +Please do not file test [% terms.bugs %] or support requests here! You +can test Bugzilla at +<a href="http://landfill.bugzilla.org/">landfill.bugzilla.org</a> and ask +for support in the +<a href="news://news.mozilla.org/mozilla.support.bugzilla"> +mozilla.support.bugzilla newsgroup</a>, +<a href="mailto:support-bugzilla@lists.mozilla.org"> +support-bugzilla@lists.mozilla.org mailing list</a>, or +<a href="irc://irc.mozilla.org/mozwebtools">#mozwebtools IRC channel</a>. +</p> + +<hr> + +[% ELSE %] + + [% IF format != "guided" %] +<p> +[% UNLESS cloned_bug_id %] +Consider using the +<a href="enter_bug.cgi?product=[% product.name FILTER html %]&format=guided"> +Bugzilla Helper</a> instead of this form. +[% END +%] +Before reporting a [% terms.bug %], make sure you've read our +<a href="http://www.mozilla.org/quality/bug-writing-guidelines.html"> +[% terms.bug %] writing guidelines</a> and double checked that your [% terms.bug %] hasn't already +been reported. Consult our list of +<a href="https://bugzilla.mozilla.org/duplicates.cgi"> +most frequently reported [% terms.bugs %]</a> and +<a href="https://bugzilla.mozilla.org/query.cgi">search through descriptions</a> +of previously reported [% terms.bugs %]. +</p> + [% ELSE %] +<p> + This form prompts you for the information required to + file a good [% terms.bug %] report (or enhancement request.) + It may seem lengthy, but developers need all this information + to understand and reproduce the [% terms.bug %], which is the first step towards + fixing it. Please note that <strong>we do not accept [% terms.bug %] reports by + email</strong> - please do not email developers or mozilla.org staff + with [% terms.bug %] reports. <font color="red">Also, please do not + file [% terms.bugs %] on browser/email software older than two weeks - first, + download a newer build of + <a href="http://ftp.mozilla.org/pub/mozilla.org/firefox/nightly/latest-trunk/">Firefox</a>, + <a href="http://ftp.mozilla.org/pub/mozilla.org/thunderbird/nightly/latest-comm-central/">Thunderbird</a>, + <a href="http://ftp.mozilla.org/pub/mozilla.org/seamonkey/nightly/latest-comm-central-trunk/">SeaMonkey</a>, + or + <a href="http://ftp.mozilla.org/pub/mozilla.org/camino/nightly/">Camino</a> + and check that the problem is still present. + </font> +</p> + [% END %] +[% END %] diff --git a/extensions/BMO/template/en/default/global/choose-product.html.tmpl b/extensions/BMO/template/en/default/global/choose-product.html.tmpl new file mode 100644 index 000000000..a65605a93 --- /dev/null +++ b/extensions/BMO/template/en/default/global/choose-product.html.tmpl @@ -0,0 +1,182 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] + +[%# INTERFACE: + # classifications: array of hashes, with an 'object' key representing a + # classification object and 'products' the list of + # product objects the user can enter bugs into. + # target: the script that displays this template. + # cloned_bug_id: ID of the bug being cloned. + # format: the desired format to display the target. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% IF target == "enter_bug.cgi" %] + [% title = "Enter $terms.Bug" %] + [% h2 = BLOCK %]First, you must pick a product on which to enter [% terms.abug %]. [% END %] +[% ELSIF target == "describecomponents.cgi" %] + [% title = "Browse" %] + [% h2 = "Please specify the product whose components you want described." %] +[% END %] + +[% DEFAULT title = "Choose a Product" %] +[% PROCESS global/header.html.tmpl %] + +<center> +<hr> +<p><span style="font-family: verdana,helvetica;">Looking for technical support or help getting your site to work with Mozilla? <a href="http://www.mozilla.org/support/">Visit the mozilla.org support page</a> before filing bugs.</span></p> +<hr> +</center> + +<br> +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +[% SET classification = cgi.param('classification') %] +[% IF NOT ((cgi.param("full")) OR (user.settings.product_chooser.value == 'full_product_chooser')) %] +[% IF target == "enter_bug.cgi" %] +<h2 align="center">Which product is affected by the problem you would like to report?</h2> +[% END %] +<table align="center" border="0" width="600" cellpadding="5" cellspacing="0"> +[% INCLUDE easyproduct + name="Core" + icon="dino.png" +%] +[% INCLUDE easyproduct + name="Firefox" + icon="firefox.png" +%] +[% INCLUDE easyproduct + name="Thunderbird" + icon="thunderbird.png" +%] +[% INCLUDE easyproduct + name="Calendar" + icon="sunbird.png" +%] +[% INCLUDE easyproduct + name="Camino" + icon="camino.png" +%] +[% INCLUDE easyproduct + name="SeaMonkey" + icon="seamonkey.png" +%] +[% INCLUDE easyproduct + name="Fennec" + icon="fennec.png" +%] +[% INCLUDE easyproduct + name="Mozilla Localizations" + icon="dino.png" +%] +[% INCLUDE easyproduct + name="Mozilla Labs" + icon="labs.png" +%] +[% INCLUDE easyproduct + name="Mozilla Services" + icon="dino.png" +%] +<tr> + <td><a href="[% target FILTER uri i%]?full=1 + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%] + [%- IF classification %]&classification=[% classification FILTER uri %][% END -%] + [%- IF format %]&format=[% format FILTER uri %][% END %]"> + <img src="extensions/BMO/web/producticons/other.png" height="64" width="64" border="0"></a></td> + <td><h2 align="left" style="margin-bottom: 0px;"><a href="[% target FILTER uri i%]?full=1 + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%] + [%- IF classification %]&classification=[% classification FILTER uri %][% END -%] + [%- IF format %]&format=[% format FILTER uri %][% END %]"> + Other Products</a></h2> + <p style="margin-top: 0px;">Other Mozilla products which aren't listed here</p> + </td> +</tr> +</table> +[% ELSE %] +<h2>[% h2 FILTER html %]</h2> + +<table> + +[% FOREACH c = classifications %] + [% IF c.object %] + <tr> + <td align="right"><h2>[% c.object.name FILTER html %]</h2></td> + <td><strong>[%+ c.object.description FILTER html_light %]</strong></td> + </tr> + [% END %] + + [% FOREACH p = c.products %] + <tr> + <th align="right" valign="top"> + [% IF p.name == "Mozilla PR" AND target == "enter_bug.cgi" AND NOT format AND NOT cgi.param("debug") %] + <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%] + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END %]&format=mozpr"> + [% p.name FILTER html FILTER no_break %]</a>: + [% ELSE %] + <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%] + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%] + [%- IF format %]&format=[% format FILTER uri %][% END %]"> + [% p.name FILTER html FILTER no_break %]</a>: + [% END %] + </th> + <td valign="top">[% p.description FILTER html_light %]</td> + </tr> + [% END %] +[% END %] + +</table> + +<br> +[% IF target == "enter_bug.cgi" AND user.settings.product_chooser.value != 'full_product_chooser' %] +<p align="center">You can choose to get this screen by default when you click "New Bug" by changing your <a href="userprefs.cgi?tab=settings">preferences</a>.</p> +[% END %] +[% END %] +<br> + +[% PROCESS global/footer.html.tmpl %] + +[%###########################################################################%] +[%# Block for "easy" product sections #%] +[%###########################################################################%] + +[% BLOCK easyproduct %] + [% FOREACH c = classifications %] + [% FOREACH p = c.products %] + [% IF p.name == name %] + <tr> + <td><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %] + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%] + [%- IF format %]&format=[% format FILTER uri %][% END %]"> + <img src="extensions/BMO/web/producticons/[% icon FILTER uri %]" height="64" width="64" border="0"></a></td> + <td><h2 align="left" style="margin-bottom: 0px"><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %] + [%- IF cloned_bug_id %]&cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%] + [%- IF format %]&format=[% format FILTER uri %][% END %]"> + [% p.name FILTER html FILTER no_break %]</a>:</h2> + [% IF p.description %] + <p style="margin-top: 0px;">[% p.description FILTER html_light %]</p> + [% END %] + </td> + </tr> + [% LAST %] + [% END %] + [% END %] + [% END %] +[% END %] diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl new file mode 100644 index 000000000..3dc727b87 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl @@ -0,0 +1,2 @@ +[% mimetypes.push({type => "image/svg+xml", desc => "SVG image"}) %] +[% mimetypes.push({type => "application/vnd.mozilla.xul+xml", desc => "XUL"}) %]
\ No newline at end of file diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl new file mode 100644 index 000000000..ea80fdc5e --- /dev/null +++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl @@ -0,0 +1 @@ +<em>You can <a href="http://developer.mozilla.org/en/docs/Getting_your_patch_in_the_tree">read about the patch submission and approval process</a>.</em><br> diff --git a/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl new file mode 100644 index 000000000..fd5477c9f --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl @@ -0,0 +1,7 @@ +[%# *** Disclaimer for Legal bugs *** %] +[% IF bug.product == "Legal" %] + <div id="legal_disclaimer"> + The material and information contained herein is Confidential and + subject to Attorney-Client Privilege and Work Product Doctrine. + </div> +[% END %] diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl new file mode 100644 index 000000000..1b1f1d67d --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl @@ -0,0 +1,18 @@ + <tr> + <th>Security:</th> + <td colspan="3"> + [% sec_group = sec_groups.${product.name} || sec_groups._default %] + + <input type="checkbox" name="groups" + value="[% sec_group FILTER none %]" id="groups" + [% FOREACH g = group %] + [% IF g.name == sec_group %] + [% " checked=\"checked\"" IF g.checked %] + [% END %] + [% END %] + > + <label for="groups">Many users could be harmed by this security problem: + it should be kept hidden from the public until it is resolved.</label> + <br><br> + </td> + </tr> diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl new file mode 100644 index 000000000..ee88aac7c --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl @@ -0,0 +1,22 @@ + <tr bgcolor="[% tablecolour %]"> + <td valign="middle" align="right"> + <b>Security</b> + </td> + <td valign="top"> + <p> + [% sec_group = sec_groups.${product.name} || sec_groups._default %] + + <input type="checkbox" name="groups" + id="groups" value="[% sec_group FILTER none %]" + [% FOREACH g = group %] + [% IF g.name == sec_group %] + [% " checked=\"checked\"" IF g.checked %] + [% END %] + [% END %] + > + <label for="groups"> + Many users could be harmed by this security problem: it should be kept + hidden from the public until it is resolved.</label> + </p> + </td> + </tr> diff --git a/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl new file mode 100644 index 000000000..0c68d8e72 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl @@ -0,0 +1,83 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 BMO Bugzilla Extension; + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** + #%] + +[% show_custom_flags = 0 %] +[% FOREACH field = Bugzilla.active_custom_fields %] + [% NEXT IF NOT user.id AND bug.${field.name} == "---" %] + [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component, 2) %] + [% show_custom_flags = 1 %] + [% LAST %] +[% END %] + +[% IF show_custom_flags %] + [% custom_flags = [] %] + <tr> + <td class="" colspan="2"> + <label><b>Tracking Flags:</b></label> + [% IF user.id %] + <span id="edit_tracking_fields_action"> + (<a onclick="bmo_show_tracking_flags()" href="javascript:void(0)">edit</a>) + </span> + [% END %] + </td> + </tr> + <tr> + <td colspan="2"> + <table id="custom-flags"> + [% FOREACH field = Bugzilla.active_custom_fields %] + [% NEXT IF NOT user.id AND field.value == "---" %] + [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component, 2) %] + [% NEXT IF cf_flag_disabled(field.name, bug) %] + [% custom_flags.push(field.name) %] + <tr id="row_[% field.name FILTER js %]"> + <td> </td> + <td> + <label for="[% field.name FILTER html %]"> + [% field_descs.${field.name} FILTER html %]: + </label> + </td> + <td> + [% PROCESS bug/field.html.tmpl value = bug.${field.name} + editable = user.id + no_tds = 1 %] + [% IF user.id %] + <span id="ro_[% field.name FILTER html %]" class="bz_hidden"> + [% bug.${field.name} FILTER html %] + </span> + [% END %] + </td> + </tr> + [% END %] + </table> + </td> + </tr> + <script type="text/javascript"> + var bmo_custom_flags = new Array([% custom_flags.size FILTER none %]); + [% FOREACH field_name = custom_flags %] + bmo_custom_flags['[% field_name FILTER js %]'] = '[% bug.${field_name} FILTER js %]'; + [% END %] + bmo_hide_tracking_flags(); + </script> +[% END %] diff --git a/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl new file mode 100644 index 000000000..70132d7e0 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl @@ -0,0 +1,114 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Dave Lawrence <dkl@mozilla.com> + #%] + +[% USE Bugzilla %] +[% IF Bugzilla.request_cache.bmo_fields_page %] + [% filtered_severity_blocker = display_value("bug_severity", "blocker") FILTER html %] + [% filtered_severity_critical = display_value("bug_severity", "critical") FILTER html %] + [% filtered_severity_major = display_value("bug_severity", "major") FILTER html %] + [% filtered_severity_normal = display_value("bug_severity", "normal") FILTER html %] + [% filtered_severity_minor = display_value("bug_severity", "minor") FILTER html %] + [% filtered_severity_trivial = display_value("bug_severity", "trivial") FILTER html %] + [% filtered_severity_enhancement = display_value("bug_severity", "enhancement") FILTER html %] + + [% filtered_platform_all = display_value("rep_platform", "All") FILTER html %] + [% filtered_platform_x86_64 = display_value("rep_platform", "x86_64") FILTER html %] + [% filtered_platform_arm = display_value("rep_platform", "ARM") FILTER html %] + + [% filtered_opsys_all = display_value("op_sys", "All") FILTER html %] + [% filtered_opsys_windows = display_value("op_sys", "Windows 7") FILTER html %] + [% filtered_opsys_mac = display_value("op_sys", "Mac OS X") FILTER html %] + [% filtered_opsys_linux = display_value("op_sys", "Linux") FILTER html %] + + [% filtered_status_new = display_value("bug_status", "NEW") FILTER html %] + + [% + vars.help_html.priority = + "This field describes the importance and order in which $terms.abug + should be fixed compared to other ${terms.bugs}. This field is utilized + by the programmers/engineers to prioritize their work to be done." + + vars.help_html.bug_severity = + "This field describes the impact of ${terms.abug}. + <table> + <tr> + <th>$filtered_severity_blocker</th> + <td>Blocks development and/or testing work</td> + </tr> + <tr> + <th>$filtered_severity_critical</th> + <td>crashes, loss of data, severe memory leak</td> + </tr> + <tr> + <th>$filtered_severity_major</th> + <td>major loss of function</td> + </tr> + <tr> + <th>$filtered_severity_normal</th> + <td>regular issue, some loss of functionality under specific circumstances</td> + </tr> + <tr> + <th>$filtered_severity_minor</th> + <td>minor loss of function, or other problem where easy + workaround is present</td> + </tr> + <tr> + <th>$filtered_severity_trivial</th> + <td>cosmetic problem like misspelled words or misaligned + text</td> + </tr> + <tr> + <th>$filtered_severity_enhancement</th> + <td>Request for enhancement</td> + </table>" + + vars.help_html.rep_platform = + "This is the hardware platform against which the $terms.bug was reported. + Legal platforms include: + <ul> + <li>$filtered_platform_all (happens on all platforms; cross-platform ${terms.bug})</li> + <li>$filtered_platform_x86_64</li> + <li>$filtered_platform_arm</li> + </ul> + <b>Note:</b> When searching, selecting the option + <em>$filtered_platform_all</em> does not + select $terms.bugs assigned against any platform. It merely selects + $terms.bugs that are marked as occurring on all platforms, i.e. are + designated <em>$filtered_platform_all</em>.", + + vars.help_html.op_sys = + "This is the operating system against which the $terms.bug was + reported. Legal operating systems include: + <ul> + <li>$filtered_opsys_all (happens on all operating systems; cross-platform ${terms.bug})</li> + <li>$filtered_opsys_windows</li> + <li>$filtered_opsys_mac</li> + <li>$filtered_opsys_linux</li> + </ul> + Sometimes the operating system implies the platform, but not + always. For example, Linux can run on x86_64, ARM, and others.", + + vars.help_html.assigned_to = + "This is the person in charge of resolving the ${terms.bug}. Every time + this field changes, the status changes to + <b>$filtered_status_new</b> to make it + easy to see which new $terms.bugs have appeared on a person's list.</p>", + %] +[% END %] diff --git a/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl new file mode 100644 index 000000000..55e428e01 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl @@ -0,0 +1,27 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 BMO Bugzilla Extension; + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** + #%] + +[% style_urls.push('extensions/BMO/web/styles/edit_bug.css') %] +[% javascript_urls.push('extensions/BMO/web/js/edit_bug.js') %] diff --git a/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl b/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl new file mode 100644 index 000000000..b5bb4719c --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl @@ -0,0 +1 @@ +<a href="https://www.mozilla.org/about/policies/privacy-policy.html">Privacy Policy</a> diff --git a/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl new file mode 100644 index 000000000..0277f3e7e --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl @@ -0,0 +1,60 @@ +[%# + # 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 BMOHeader Bugzilla Extension. + # + # The Initial Developer of the Original Code is Reed Loden. + # Portions created by the Initial Developer are Copyright (C) 2010 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Reed Loden <reed@reedloden.com> + #%] + +<link rel="shortcut icon" href="extensions/BMO/web/images/bugzilla.png"> +[% IF bug %] +<link id="shorturl" rev="canonical" href="https://bugzil.la/[% bug.bug_id %]"> +[% END %] + +<style type="text/css"> +body { + background: url("extensions/BMO/web/images/background.png") repeat-x; +} +</style> + +[%# *** Bug List Navigation *** %] +[% IF bug %] + [% SET my_search = user.recent_search_for(bug) %] + [% IF my_search %] + [% SET last_bug_list = my_search.bug_list %] + [% SET this_bug_idx = lsearch(last_bug_list, bug.id) %] + <link rel="Up" href="buglist.cgi?regetlastlist= + [%- my_search.id FILTER uri %]"> + <link rel="First" href="show_bug.cgi?id= + [%- last_bug_list.first FILTER uri %]&list_id= + [%- my_search.id FILTER uri %]"> + <link rel="Last" href="show_bug.cgi?id= + [%- last_bug_list.last FILTER uri %]&list_id= + [%- my_search.id FILTER uri %]"> + [% IF this_bug_idx > 0 %] + [% prev_bug = this_bug_idx - 1 %] + <link rel="Prev" href="show_bug.cgi?id= + [%- last_bug_list.$prev_bug FILTER uri %]&list_id= + [%- my_search.id FILTER uri %]"> + [% END %] + [% IF this_bug_idx + 1 < last_bug_list.size %] + [% next_bug = this_bug_idx + 1 %] + <link rel="Next" href="show_bug.cgi?id= + [%- last_bug_list.$next_bug FILTER uri %]&list_id= + [%- my_search.id FILTER uri %]"> + [% END %] + [% END %] +[% END %] diff --git a/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl new file mode 100644 index 000000000..3c2f90e19 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl @@ -0,0 +1,3 @@ +[% IF template.name == 'list/list.html.tmpl' %] + [% javascript_urls.push('extensions/BMO/web/js/sorttable.js') %] +[% END %]
\ No newline at end of file diff --git a/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl new file mode 100644 index 000000000..666621d8b --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl @@ -0,0 +1,5 @@ +[% + setting_descs.product_chooser = "Product chooser to use when entering bugs", + setting_descs.pretty_product_chooser = "Pretty chooser with common products and icons", + setting_descs.full_product_chooser = "Full chooser with all products", +%] diff --git a/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl new file mode 100644 index 000000000..067347f3b --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl @@ -0,0 +1,3 @@ +[% IF object == 'group_admins' %] + the group administrators report +[% END %] diff --git a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..85881aca7 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,40 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% IF error == "user_activity_missing_username" %] + [% title = "Missing Username" %] + You must provide at least one email address to report on. + +[% ELSIF error == "user_activity_missing_from_date" %] + [% title = "Missing Date" %] + You must provided the period start date. + +[% ELSIF error == "user_activity_missing_to_date" %] + [% title = "Missing Date" %] + You must provided the period end date. + +[% ELSIF error == "user_activity_invalid_date" %] + [% title = "Invalid Date" %] + The date '[% date FILTER html %]' is invalid. + +[% ELSIF error == "invalid_object" %] + Invalid [% object FILTER html %]: "[% value FILTER html %]" + +[% END %] diff --git a/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl new file mode 100644 index 000000000..5f6ca946a --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl @@ -0,0 +1,29 @@ +<!-- 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + # Reed Loden <reed@reedloden.com> + #%] + +[% IF (group == "canconfirm" OR group == "editbugs") AND !reason %] + <p> + If you are attempting to confirm an unconfirmed bug or edit the fields of a bug, + <a href="http://www.gerv.net/hacking/before-you-mail-gerv.html#bugzilla-permissions">find + out how to get the necessary permissions</a>. + </p> +[% END %] diff --git a/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl new file mode 100644 index 000000000..89eef6fc4 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl @@ -0,0 +1,3 @@ +[% + terms.BugzillaTitle = "Bugzilla@Mozilla" +%] diff --git a/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl new file mode 100644 index 000000000..56a133c6c --- /dev/null +++ b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl @@ -0,0 +1,10 @@ +<li> +| +<a href="page.cgi?id=etiquette.html"> + [%- terms.Bugzilla %] Etiquette</a> +</li> +<li> +| +<a href="https://developer.mozilla.org/en/Bug_writing_guidelines"> + [%- terms.Bug %] Writing Guidelines</a> +</li> diff --git a/extensions/BMO/template/en/default/hook/index-intro.html.tmpl b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl new file mode 100644 index 000000000..d81d91491 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl @@ -0,0 +1,2 @@ +<a id="get_help" class="bz_common_actions" + href="page.cgi?id=get_help.html"><span>Get Help</span></a>
\ No newline at end of file diff --git a/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl new file mode 100644 index 000000000..4d12ab345 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl @@ -0,0 +1,13 @@ +<dt> + [% display_value("resolution", "INCOMPLETE") FILTER html %] +</dt> +<dd> + The problem is vaguely described with no steps to reproduce, + or is a support request. The reporter should be directed to the + product's support page for help diagnosing the issue. If there + are only a few comments in the [% terms.bug %], it may be reopened only if + the original reporter provides more info, or confirms someone + else's steps to reproduce. If the [% terms.bug %] is long, when enough info + is provided a new [% terms.bug %] should be filed and the original [% terms.bug %] + marked as a duplicate of it. +</dd> diff --git a/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl new file mode 100644 index 000000000..4d12ab345 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl @@ -0,0 +1,13 @@ +<dt> + [% display_value("resolution", "INCOMPLETE") FILTER html %] +</dt> +<dd> + The problem is vaguely described with no steps to reproduce, + or is a support request. The reporter should be directed to the + product's support page for help diagnosing the issue. If there + are only a few comments in the [% terms.bug %], it may be reopened only if + the original reporter provides more info, or confirms someone + else's steps to reproduce. If the [% terms.bug %] is long, when enough info + is provided a new [% terms.bug %] should be filed and the original [% terms.bug %] + marked as a duplicate of it. +</dd> diff --git a/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl new file mode 100644 index 000000000..6e8463020 --- /dev/null +++ b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl @@ -0,0 +1,41 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +<h2>Other Reports</h2> + +<ul> + <li> + <strong><a href="[% urlbase FILTER none + %]page.cgi?id=user_activity.html">User Changes</a></strong> - Show changes + made by an individual user. + </li> + <li> + <strong><a href="[% urlbase FILTER none + %]page.cgi?id=triage_reports.html">Triage Report</a></strong> - Report + on UNCONFIRMED [% terms.bugs %] to assist triage. + </li> + [% IF user.in_group('editusers') %] + <li> + <strong><a href="[% urlbase FILTER none + %]page.cgi?id=group_admins.html">Group Admins</a></strong> - Group Admins Report + </li> + [% END %] +</ul> + diff --git a/extensions/BMO/template/en/default/list/list.microsummary.tmpl b/extensions/BMO/template/en/default/list/list.microsummary.tmpl new file mode 100644 index 000000000..8f6b13cbd --- /dev/null +++ b/extensions/BMO/template/en/default/list/list.microsummary.tmpl @@ -0,0 +1,29 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Ronaldo Maia <rmaia@everythingsolved.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + + +[% IF searchname %] + [% searchname %] ([% bugs.size %]) +[% ELSE %] + [% terms.Bug %] List ([% bugs.size %]) +[% END %] diff --git a/extensions/BMO/template/en/default/list/server-push.html.tmpl b/extensions/BMO/template/en/default/list/server-push.html.tmpl new file mode 100644 index 000000000..40432a35b --- /dev/null +++ b/extensions/BMO/template/en/default/list/server-push.html.tmpl @@ -0,0 +1,52 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Myk Melez <myk@mozilla.org> + #%] + +[%# INTERFACE: + # debug: boolean. True if we want the search displayed while we wait. + # query: string. The SQL query which makes the buglist. + #%] + +[% PROCESS global/variables.none.tmpl %] + +<html> + <head> + <title>[% terms.Bugzilla %] is pondering your search</title> + </head> + <body> + <div style="margin-top: 15%; text-align: center;"> + <center><img src="extensions/BMO/web/images/mozchomp.gif" alt="" + width="160" height="87"></center> + <h1>Please wait while your bugs are retrieved.</h1> + </div> + + [% IF debug %] + <p> + [% FOREACH debugline = debugdata %] + <code>[% debugline FILTER html %]</code><br> + [% END %] + </p> + <p> + <code>[% query FILTER html %]</code> + </p> + [% END %] + + </body> +</html> diff --git a/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl new file mode 100644 index 000000000..f326d1821 --- /dev/null +++ b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl @@ -0,0 +1,25 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): David Lawrence <dkl@mozilla.com> + #%] + +<html> + <head> + <meta http-equiv="refresh" content="0;url=https://developer.mozilla.org/en/Bug_writing_guidelines"> + </head> +</html> diff --git a/extensions/BMO/template/en/default/pages/etiquette.html.tmpl b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl new file mode 100644 index 000000000..281057eeb --- /dev/null +++ b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl @@ -0,0 +1,147 @@ +<!-- 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Stefan Seifert <nine@detonation.org> + # Gervase Markham <gerv@gerv.net> + #%] + +[% INCLUDE global/header.html.tmpl + title = "Bugzilla Etiquette" + style = "li { margin: 5px } .heading { font-weight: bold }" %] + +<p> + There's a number of <i lang="fr">faux pas</i> you can commit when using + Bugzilla. At the very + least, these will make Mozilla contributors upset at you; if committed enough + times they will cause those contributors to demand the disabling of your + Bugzilla account. So, ignore this advice at your peril. +</p> + +<p> + That said, Mozilla developers are generally a friendly bunch, and will be + friendly towards you as long as you follow these guidelines. +</p> + +<h3>1. Commenting</h3> + +<p> + This is the most important section. +</p> + +<ol> + <li> + <span class="heading">No pointless comments</span>. + Unless you have something constructive and helpful to say, do not add a + comment to a bug. In bugs where there is a heated debate going on, you + should be even more + inclined not to add a comment. Unless you have something new to contribute, + then the bug owner is aware of all the issues, and will make a judgement + as to what to do. If you agree the bug should be fixed, vote for it. + Additional "I see this too" or "It works for me" comments are unnecessary + unless they are on a different platform or a significantly different build. + Constructive and helpful thoughts unrelated to the topic of the bug + should go in the appropriate + <a href="http://www.mozilla.org/about/forums/">newsgroup</a>. + </li> + + <li> + <span class="heading">No obligation</span>. + "Open Source" is not the same as "the developers must do my bidding." + Everyone here wants to help, but the only person who has any + <i>obligation</i> to fix the bugs you want fixed is you. Therefore, you + should not act as if you expect someone to fix a bug by a particular date + or release. Aggressive or repeated demands will not be received + well and will almost certainly diminish the impact and interest in your + suggestions. + </li> + + <li> + <span class="heading">No abusing people</span>. + Constant and intense critique is one of the reasons we build great products. + It's harder to fall into group-think if there is always a healthy amount of + dissent. We want to encourage vibrant debate inside of the Mozilla + community, we want you to disagree with us, and we want you to effectively + argue your case. However, we require that in the process, you attack + <i>things</i>, not <i>people</i>. Examples of things include: interfaces, + algorithms, and schedules. Examples of people include: developers, + designers and users. <b>Attacking a person may result in you being banned + from Bugzilla.</b> + </li> + + <li> + <span class="heading">No private email</span>. + Unless the bug owner or another respected project contributor has asked you + to email them with specific information, please place all information + relating to bugs + in the bug itself. Do not send them by private email; no-one else can read + them if you do that, and they'll probably just get ignored. If a file + is too big for Bugzilla, add a comment giving the file size and contents + and ask what to do. + </li> +</ol> + +<h3>2. Changing Fields</h3> + +<ol> + <li> + <span class="heading">No messing with other people's bugs</span>. + Unless you are the bug assignee, or have some say over the use of their + time, never change the Priority or Target Milestone fields. If in doubt, + do not change the fields of bugs you do not own - add a comment + instead, suggesting the change. + </li> + + <li> + <span class="heading">No whining about decisions</span>. + If a respected project contributor has marked a bug as INVALID, then it is + invalid. Someone filing another duplicate of it does not change this. Unless + you have further important evidence, do not post a comment arguing that an + INVALID or WONTFIX bug should be reopened. + </li> + +</ol> + +<h3>3. Applicability</h3> + +<ol> + <li> + Some of these rules may not apply to you. If they do not, you will know + exactly which ones do not, and why they do not apply. If you are not + sure, then they definitely all apply to you. + </li> +</ol> + +<p> + If you see someone not following these rules, the first step is, as an exception + to guideline 1.4, to make them aware of this document by <em>private</em> mail. + Flaming people publically in bugs violates guidelines 1.1 and 1.3. In the case of + persistent offending you should report the matter to + <a href="mailto:gerv@mozilla.org">Gerv</a>. +</p> + +<p> + This entire document can be summed up in one sentence: + do unto others as you would have them do unto you. +</p> + +<p> + Other useful documents: + <a href="page.cgi?id=bug-writing.html">The Bug Writing Guidelines</a>. +</p> + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/pages/get_help.html.tmpl b/extensions/BMO/template/en/default/pages/get_help.html.tmpl new file mode 100644 index 000000000..70ff0a12b --- /dev/null +++ b/extensions/BMO/template/en/default/pages/get_help.html.tmpl @@ -0,0 +1,42 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): David Miller <justdave@bugzilla.org> + #%] + +[% PROCESS global/variables.none.tmpl %] +[% INCLUDE global/header.html.tmpl title = "Get Help with Mozilla Products" %] + +<div id="steps"> +<h2>Got a problem?</h2> + +<ul> +<li><a href="http://www.mozilla.org/support/">Get help with your mozilla.org product</a></li> +<li><a href="http://hendrix.mozilla.org/">Leave quick feedback</a></li> +<li><a href="http://input.mozilla.com/feedback">Report a broken website</a></li> +<li><a href="enter_bug.cgi">Report a [% terms.bug %]</a> - latest release only + [% IF NOT user.id %] + (you'll need an + <a href="createaccount.cgi">account</a>) + [% END %] +</li> +</ul> +</div> + +<br> + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/pages/group_admins.html.tmpl b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl new file mode 100644 index 000000000..a55b6e2ad --- /dev/null +++ b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl @@ -0,0 +1,53 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # David Lawrence <dkl@mozilla.com> + #%] + +[% INCLUDE global/header.html.tmpl + title = "Group Admins Report" + style_urls = [ "extensions/BMO/web/styles/reports.css" ] +%] + +[% IF groups.size > 0 %] + <table border="0" cellspacing="0" id="report" width="100%"> + <tr id="report-header"> + <th align="left">Name</th> + <th align="left">Admins</th> + </tr> + + [% FOREACH group = groups %] + [% count = loop.count() %] + <tr class="report_item [% count % 2 == 1 ? "report_row_odd" : "report_row_even" %]"> + <td> + [% group.name FILTER html %] + </td> + <td> + [% FOREACH admin = group.admins %] + [% INCLUDE global/user.html.tmpl who = admin %][% ", " UNLESS loop.last %] + [% END %] + </td> + </tr> + [% END %] + </table> +[% ELSE %] + <p> + No groups found. + </p> +[% END %] + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl new file mode 100644 index 000000000..023dc4791 --- /dev/null +++ b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl @@ -0,0 +1,199 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% js_data = BLOCK %] +var useclassification = false; +var first_load = true; +var last_sel = []; +var cpts = new Array(); +[% n = 1 %] +[% FOREACH p = user.get_selectable_products %] + cpts['[% n FILTER js %]'] = [ + [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; + [% n = n+1 %] +[% END %] + +var selected_components = [ + [%- FOREACH c = input.component %]'[% c FILTER js %]' + [%- ',' UNLESS loop.last %] [%- END ~%] ]; + +[% END %] + +[% INCLUDE global/header.html.tmpl + title = "Triage Reports" + yui = [ 'autocomplete', 'calendar' ] + javascript = js_data + javascript_urls = [ "js/util.js", "js/field.js", "js/productform.js", + "extensions/BMO/web/js/triage_reports.js" ] + style_urls = [ "skins/standard/buglist.css", + "extensions/BMO/web/styles/triage_reports.css" ] +%] + +<noscript> +<h2>Javascript is required to use this report.</h2> +</noscript> + +[% PROCESS "global/field-descs.none.tmpl" %] + +<form id="activity_form" name="activity_form" action="page.cgi" method="get" + onSubmit="return onGenerateReport()"> +<input type="hidden" name="id" value="triage_reports.html"> +<input type="hidden" name="action" value="run"> + +Show UNCONFIRMED bugs with: +<table id="triage_form"> + +<tr> + <th>Product:</th> + <td> + <select name="product" id="product" onChange="onSelectProduct()"> + <option value=""></option> + [% FOREACH p = user.get_selectable_products %] + <option value="[% p.name FILTER html %]" + [% " selected" IF input.product == p.name %]> + [% p.name FILTER html %] + </option> + [% END %] + </select> + </td> + <td rowspan="2" valign="top"> + <b>Comment:</b><br> + + <input type="checkbox" name="filter_commenter" id="filter_commenter" value="1" + [% 'checked' IF input.filter_commenter %]> + <label for="filter_commenter">where the last commenter</label> + <select name="commenter" id="commenter" onChange="onCommenterChange()"> + <option value="reporter" [% 'selected' IF input.commenter == 'reporter' %]>is the reporter</option> + <option value="noconfirm" [% 'selected' IF input.commenter == 'noconfirm' %]>does not have canconfirm</option> + <option value="is" [% 'selected' IF input.commenter == 'is' %]>is</option> + </select> + [%+ INCLUDE global/userselect.html.tmpl + id => "commenter_is" + name => "commenter_is" + value => input.commenter_is + size => 20 + emptyok => 0 + classes = input.commenter == "is" ? "" : "hidden" + %] + <br> + + <input type="checkbox" name="filter_last" id="filter_last" value="1" + [% 'checked' IF input.filter_last %]> + <label for="filter_last">where the last comment is older than</label> + <select name="last" id="last" onChange="onLastChange()"> + <option value="30" [% 'selected' IF input.last == '30' %]>30 days</option> + <option value="60" [% 'selected' IF input.last == '60' %]>60 days</option> + <option value="90" [% 'selected' IF input.last == '90' %]>90 days</option> + <option value="365" [% 'selected' IF input.last == '365' %]>one year</option> + <option value="is" [% 'selected' IF input.last == 'is' %]>the date</option> + </select> + <span id="last_is_span" class="[% 'hidden' IF input.last != 'is' %]"> + <input type="text" id="last_is" name="last_is" size="11" maxlength="10" + value="[% input.last_is FILTER html %]" + onChange="updateCalendarLastIs(this)"> + <button type="button" class="calendar_button" id="button_calendar_last_is" + onClick="showCalendar('last_is')"><span>Calendar</span> + </button> + <div id="con_calendar_last_is"></div> + </span> + <br> + </td> +</tr> + +<tr> + <th>Component:</th> + <td> + <select name="component" id="component" multiple size="5"> + </select> + </td> +</tr> + +<tr> + <td> </td> + <td> + <input type="submit" value="Generate Report"> + </td> +</tr> + +</table> + +</form> +<script> + createCalendar('last_is'); +</script> + +[% IF input.action == 'run' %] +<hr> +[% IF bugs.size > 0 %] + <p> + Found [% bugs.size %] [%+ terms.bug %][% 's' IF bugs.size != 1 %]: + </p> + <table border="0" cellspacing="0" id="report" width="100%"> + <tr id="report-header"> + <th>Bug / Date</th> + <th>Summary</th> + <th>Reporter / Commenter</th> + <th>Comment Date</th> + <th>Last Comment</th> + </tr> + + [% FOREACH bug = bugs %] + [% count = loop.count() %] + <tr class="bz_bugitem [% count % 2 == 1 ? "bz_row_odd" : "bz_row_even" %]"> + <td> + [% bug.id FILTER bug_link(bug.id) FILTER none %]<br> + [% bug.creation_ts.replace(' .*' '') FILTER html FILTER no_break %] + </td> + <td> + [% bug.summary FILTER html %] + </td> + <td> + [% INCLUDE global/user.html.tmpl who = bug.reporter %] + [% IF bug.commenter.id != bug.reporter.id %] + <br>[% INCLUDE global/user.html.tmpl who = bug.commenter %] + [% END %] + </td> + <td> + [% bug.comment_ts FILTER html FILTER no_break %] + </td> + <td> + [% bug.comment FILTER html %] + </td> + </tr> + [% END %] + </table> + + <p> + <a href="buglist.cgi?bug_id= + [%- FOREACH bug = bugs %][% bug.id FILTER uri %],[% END -%] + ">Show as a [% terms.Bug %] List</a> + </p> + +[% ELSE %] + <p> + No bugs found. + </p> +[% END %] + +[% END %] + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl new file mode 100644 index 000000000..8fa944ae6 --- /dev/null +++ b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl @@ -0,0 +1,304 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): David Miller <justdave@bugzilla.org> + # Reed Loden <reed@reedloden.com> + #%] + +[% PROCESS global/variables.none.tmpl %] +[% INCLUDE global/header.html.tmpl + title = "Bugzilla 3.6 Upgrade" +%] +[% USE date %] + +<p><b>Last Updated:</b> [% date.format(template.modtime, "%d-%b-%Y %H:%M %Z") %]</p> + +<p>On Friday, July 9, 2010, at 11:40pm PDT (0640 UTC), bugzilla.mozilla.org was + <a href="show_bug.cgi?id=558044">upgraded</a> to Bugzilla 3.6.1+. Please + <a href="enter_bug.cgi?product=mozilla.org&component=Bugzilla:+Other+b.m.o+Issues&blocked=bmo-regressions">file + any regressions</a> for tracking purposes.</p> + +<h3>Known Issues</h3> + +<p>The following is a list of issues which are known to be broken or incomplete with this upgrade so far.</p> + +<ul> + +<li>The <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=577801&hide_resolved=1">stuff filed in Bugzilla</a>.</li> + +</ul> + +<h3>What's New</h3> + +<h4>Custom bugzilla.mozilla.org Changes</h4> + +<ul> + <li>Addition of autocomplete support for all user-related fields (assignee, + QA contact, and CC list) and the keywords field.</li> + <li>New attachment details UI.</li> + <li>New icons for the front page.</li> + <li>Removal of unused "Patches" column from buglist.</li> + <li>Initial support for <a href="http://en.wikipedia.org/wiki/Strict_Transport_Security">Strict-Transport-Security</a> (STS) header.</li> +</ul> + +<h4>General Usability Improvements</h4> + +<p>A <a href="https://wiki.mozilla.org/Bugzilla:CMU_HCI_Research_2008">scientific + usability study</a> was done on [% terms.Bugzilla %] by researchers + from Carnegie-Mellon University. As a result of this study, + <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=490786&hide_resolved=0">several + usability issues</a> were prioritized to be fixed, based on specific data + from the study.</p> + +<p>As a result, you will see many small improvements in [% terms.Bugzilla %]'s + usability, such as using Javascript to validate certain forms before + they are submitted, standardizing the words that we use in the user interface, + being clearer about what [% terms.Bugzilla %] needs from the user, + and other changes, all of which are also listed individually in this New + Features section.</p> + +<p>Work continues on improving usability for the next release of + [%+ terms.Bugzilla %], but the results of the research have already + had an impact on this 3.6 release.</p> + +<h4>Improved Quicksearch</h4> + +<p>The "quicksearch" box that appears on the front page of + [%+ terms.Bugzilla %] and in the header/footer of every page + is now simplified and made more powerful. There is a + <kbd>[?]</kbd> link next to the box that will take you to + the simplified <a href="page.cgi?id=quicksearch.html">Quicksearch Help</a>, + which describes every single feature of the system in a simple layout, + including new features such as the ability to use partial field names + when searching.</p> + +<p>Quicksearch should also be much faster than it was before, particularly + on large installations.</p> + +<p>Note that in order to implement the new quicksearch, certain old + and rarely-used features had to be removed: + +<ul> + <li><b>+</b> as a prefix to mean "search additional resolutions", and + <b>+</b> as a prefix to mean "search just the summary". You can + instead use <kbd>summary:</kbd> to explicitly search summaries.</li> + <li>Searching the Severity field if you type something that matches + the first few characters of a severity. You can explicitly search + the Severity field if you want to find [% terms.bugs %] by severity.</li> + <li>Searching the Priority field if you typed something that exactly + matched the name of a priority. You can explicitly search the + Priority field if you want to find [% terms.bugs %] by priority.</li> + <li>Searching the Platform and OS fields if you typed in one of a + certain hard-coded list of strings (like "pc", "windows", etc.). + You can explicitly search these fields, instead, if you want to + find [% terms.bugs %] with a specific Platform or OS set.</li> +</ul> + +<h4>Simple "Browse" Interface</h4> + +<p>There is now a "Browse" link in the header of each [% terms.Bugzilla %] + page that presents a very basic interface that allows users to simply + browse through all open [% terms.bugs %] in particular components.</p> + +<h4>JSON-RPC Interface</h4> + +<p>[% terms.Bugzilla %] now has support for the + <a href="http://json-rpc.org/">JSON-RPC</a> WebServices protocol via + <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Server/JSONRPC.html">jsonrpc.cgi</a>. + The JSON-RPC interface is experimental in this release--if you want any + fundamental changes in how it works, + <a href="http://www.bugzilla.org/developers/reporting_bugs.html">let us + know</a>, for the next release of [% terms.Bugzilla %].</p> + +<h3>New Features</h3> + +<h4>Enhancements for Users</h4> + +<ul> + <li><b>[% terms.Bug %] Filing:</b> When filing [% terms.abug %], + [%+ terms.Bugzilla %] now visually indicates which fields are + mandatory.</li> + <li><b>[% terms.Bug %] Filing:</b> "Bookmarkable templates" now + support the "alias" and "estimated hours" fields.</li> + + <li><b>[% terms.Bug %] Editing:</b> In previous versions of + [%+ terms.Bugzilla %], if you added a private comment to [% terms.abug %], + then <em>none</em> of the changes that you made at that time were + sent to users who couldn't see the private comment. Now, for users + who can't see private comments, public changes are sent, but the private + comment is excluded from their email notification.</li> + <li><b>[% terms.Bug %] Editing:</b> The controls for groups now + appear to the right of the attachment and time-tracking tables, + when editing [% terms.abug %].</li> + <li><b>[% terms.Bug %] Editing:</b> The "Collapse All Comments" + and "Expand All Comments" links now appear to the right of the + comment list instead of above it.</li> + <li><b>[% terms.Bug %] Editing:</b> The See Also field now supports + URLs for Google Code Issues and the Debian B[% %]ug-Tracking System.</li> + <li><b>[% terms.Bug %] Editing:</b> There have been significant performance + improvements in <kbd>show_bug.cgi</kbd> (the script that displays the + [% terms.bug %]-editing form), particularly for [% terms.bugs %] that + have lots of comments or attachments.</li> + + <li><b>Attachments:</b> The "Details" page of an attachment + now displays itself as uneditable if you can't edit the fields + there.</li> + <li><b>Attachments:</b> We now make sure that there is + a Description specified for an attachment, using JavaScript, before + the form is submitted.</li> + <li><b>Attachments:</b> There is now a link back to the [% terms.bug %] + at the bottom of the "Details" page for an attachment.</li> + <li><b>Attachments:</b> When you click on an "attachment 12345" link + in a comment, if the attachment is a patch, you will now see the + formatted "Diff" view instead of the raw patch.</li> + <li><b>Attachments</b>: For text attachments, we now let the browser + auto-detect the character encoding, instead of forcing the browser to + always assume the attachment is in UTF-8.</li> + + <li><b>Search:</b> You can now display [% terms.bug %] flags as a column + in search results.</li> + <li><b>Search:</b> When viewing search results, you can see which columns are + being sorted on, and which direction the sort is on, as indicated + by arrows next to the column headers.</li> + <li><b>Search:</b> You can now search the Deadline field using relative + dates (like "1d", "2w", etc.).</li> + <li><b>Search:</b> The iCalendar format of search results now includes + a PRIORITY field.</li> + <li><b>Search:</b> It is no longer an error to enter an invalid search + order in a search URL--[% terms.Bugzilla %] will simply warn you that + some of your order options are invalid.</li> + <li><b>Search:</b> When there are no search results, some helpful + links are displayed, offering actions you might want to take.</li> + <li><b>Search:</b> For those who like to make their own + <kbd>buglist.cgi</kbd> URLs (and for people working on customizations), + <kbd>buglist.cgi</kbd> now accepts nearly every valid field in + [%+ terms.Bugzilla %] as a direct URL parameter, like + <kbd>&field=value</kbd>.</li> + + <li><b>Requests:</b> When viewing the "My Requests" page, you can now + see the lists as a normal search result by clicking a link at the + bottom of each table.</li> + <li><b>Requests:</b> When viewing the "My Requests" page, if you are + using Classifications, the Product drop-down will be grouped by + Classification.</li> + + <li>If there are multiple languages available for your + [%+ terms.Bugzilla %], you can now select what language you want + [%+ terms.Bugzilla %] displayed in using links at the top of every + page.</li> + <li>When creating a new account, you will be automatically logged in + after setting your password.</li> + <li>There is no longer a maximum password length for accounts.</li> + <li>In the Dusk skin, it's now easier to see links.</li> + <li>In the Whining system, you can now choose to receive emails even + if there are no [% terms.bugs %] that match your searches.</li> + <li>The arrows in dependency graphs now point the other way, so that + [%+ terms.bugs %] point at their dependencies.</li> + + <li><b>New Charts:</b> You can now convert an existing Saved Search + into a data series for New Charts.</li> + <li><b>New Charts:</b> There is now an interface that allows you to + delete data series.</li> + <li><b>New Charts:</b> When deleting a product, you now have the option + to delete the data series that are associated with that product.</li> +</ul> + +<h4>Enhancements for Administrators and Developers</h4> + +<ul> + <li>Depending on how your workflow is set up, it is now possible to + have both UNCONFIRMED and REOPENED show up as status choices for + a closed [% terms.bug %]. If you only want one or the other to + show up, you should edit your status workflow appropriately + (possibly by removing or disabling the REOPENED status).</li> + <li>You can now "disable" field values so that they don't show + up as choices on [% terms.abug %] unless they are already set as + the value for that [% terms.bug %]. This doesn't work for the + per-product field values (component, target_milestone, and version) + yet, though.</li> + <li>Users are now locked out of their accounts for 30 minutes after + trying five bad passwords in a row during login. Every time a + user is locked out like this, the user in the "maintainer" parameter + will get an email.</li> + <li>The minimum length allowed for a password is now 6 characters.</li> + <li>The <kbd>UNCONFIRMED</kbd> status being enabled in a product + is now unrelated to the voting parameters. Instead, there is a checkbox + to enable the <kbd>UNCONFIRMED</kbd> status in a product.</li> + <li>Information about duplicates is now stored in the database instead + of being stored in the <kbd>data/</kbd> directory. On large installations + this could save several hundred megabytes of disk space.</li> + + <li>When editing a group, you can now specify that members of a group + are allowed to grant others membership in that group itself.</li> + <li>The ability to compress BMP attachments to PNGs is now an Extension. + To enable the feature, remove the file + <kbd>extensions/BmpConvert/disabled</kbd> and then run checksetup.pl.</li> + <li>The default list of values for the Priority field are now clear English + words instead of P1, P2, etc.</li> + <li><kbd>config.cgi</kbd> now returns an ETag header and understands + the If-None-Match header in HTTP requests.</li> + <li>The XML format of <kbd>show_bug.cgi</kbd> now returns more information: + the numeric id of each comment, whether an attachment is a URL, + the modification time of an attachment, the numeric id of a flag, + and the numeric id of a flag's type.</li> +</ul> + +<h4>WebService Changes</h4> + +<ul> + <li>The WebService now returns all dates and times in the UTC timezone. + <kbd>B[% %]ugzilla.time</kbd> now acts as though the [% terms.Bugzilla %] + server were in the UTC timezone, always. If you want to write clients + that are compatible across all [% terms.Bugzilla %] versions, + check the timezone from <kbd>B[% %]ugzilla.timezone</kbd> or + <kbd>B[% %]ugzilla.time</kbd>, and always input times in that timezone + and expect times to be returned in that format.</li> + <li>You can now log in by passing <kbd>Bugzilla_login</kbd> and + <kbd>Bugzilla_password</kbd> as arguments to any WebService function. + See the + <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService.html#LOGGING_IN">Bugzilla::WebService</a> + documentation for details.</li> + <li>New Method: + <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#attachments">B[% %]ug.attachments</a> + which allows getting information about attachments.</li> + <li>New Method: + <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#fields">B[% %]ug.fields</a>, + which gets information about all the fields that [% terms.abug %] can have + in [% terms.Bugzilla %], include custom fields and legal values for + all fields. The <kbd>B[% %]ug.legal_values</kbd> method is now deprecated.</li> + <li>In the <kbd>B[% %]ug.add_comment</kbd> method, the "private" parameter + has been renamed to "is_private" (for consistency with other methods). + You can still use "private", though, for backwards-compatibility.</li> + <li>The WebService now has Perl's "taint mode" turned on. This means that + it validates all data passed in before sending it to the database. + Also, all parameter names are validated, and if you pass in a parameter + whose name contains anything other than letters, numbers, or underscores, + that parameter will be ignored. Mostly this just affects + customizers--[% terms.Bugzilla %]'s WebService is not functionally + affected by these changes.</li> + <li>In previous versions of [% terms.Bugzilla %], error messages were + sent word-wrapped to the client, from the WebService. Error messages + are now sent as one unbroken line.</li> +</ul> + +<h3>Last Ten Commits</h3> + +<pre>[% bzr_history.join('') FILTER html %]</pre> + +<br> + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/pages/user_activity.html.tmpl b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl new file mode 100644 index 000000000..dd16595ab --- /dev/null +++ b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl @@ -0,0 +1,180 @@ +[%# 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 BMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% INCLUDE global/header.html.tmpl + title = "User Activity Report" + yui = [ 'autocomplete', 'calendar' ] + javascript_urls = [ "js/util.js", "js/field.js" ] +%] + +<style> +#report tr:hover { + background: #dddddd; +} + +#report-header { + background: #dddddd; +} +</style> + +[% PROCESS "global/field-descs.none.tmpl" %] +[% PROCESS bug/time.html.tmpl %] + +<form id="activity_form" name="activity_form" action="page.cgi" method="get"> +<input type="hidden" name="id" value="user_activity.html"> +<input type="hidden" name="action" value="run"> +<table> +<tr> + <td> + <b>Who</b>: + </td> + <td> + [% INCLUDE global/userselect.html.tmpl + id => "who" + name => "who" + value => who + size => 40 + emptyok => 0 + title => "One or more email address (comma delimited)" + %] + + </td> + <td> + <b>Period</b>: + </td> + <td> + <input type="text" id="from" name="from" size="11" + align="right" value="[% from FILTER html %]" maxlength="10" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" id="button_calendar_from" + onclick="showCalendar('from')"><span>Calendar</span> + </button> + <div id="con_calendar_from"></div> + </td> + <td> + - + </td> + <td> + <input type="text" name="to" size="11" id="to" + align="right" value ="[% to FILTER html %]" maxlength="10" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" id="button_calendar_to" + onclick="showCalendar('to')"><span>Calendar</span> + </button> + <div id="con_calendar_to"></div> + </td> + <td> + <input type="submit" id="run" value="Generate Report"> + </td> +</tr> +</table> +</form> + +<script type="text/javascript"> + createCalendar('from'); + createCalendar('to'); +</script> + +[% IF action == 'run' %] + +[% IF incomplete_data %] + <p> + There used to be an issue in <a href="http://www.bugzilla.org/">Bugzilla</a> + which caused activity data to be lost if there were a large number of cc's + or dependencies. That has been fixed, but some data was already lost in + your activity table that could not be regenerated. The changes that + could not reliably determine are prefixed by '?'. + </p> +[% END %] + +[% IF operations.size > 0 %] + <br> + <table border="1" cellpadding="4" cellspacing="0" id="report"> + <tr id="report-header"> + <th>Who</th> + <th>When</th> + <th>Bug</th> + <th>What</th> + <th>Removed</th> + <th>Added</th> + </tr> + + [% FOREACH operation = operations %] + <tr> + <td rowspan="[% operation.changes.size %]" valign="top"> + [% operation.who FILTER email FILTER html %] + </td> + <td rowspan="[% operation.changes.size %]" valign="top"> + [% operation.when FILTER time %] + </td> + <td rowspan="[% operation.changes.size %]" valign="top"> + [% operation.bug FILTER bug_link(operation.bug) FILTER none %] + </td> + [% FOREACH change = operation.changes %] + [% "</tr><tr>" IF loop.index > 0 %] + <td> + [% IF change.attachid %] + <a href="attachment.cgi?id=[% change.attachid %]">Attachment #[% change.attachid %]</a> + [% ELSIF change.comment.defined && change.fieldname == 'longdesc' %] + [% "Comment $change.comment.count" FILTER bug_link(operation.bug, comment_num => change.comment.count) %] + [% ELSE %] + [%+ field_descs.${change.fieldname} FILTER html %] + [% END %] + </td> + [% PROCESS change_column change_type = change.removed %] + [% PROCESS change_column change_type = change.added %] + [% END %] + </tr> + [% END %] + </table> +[% ELSE %] + <p> + No changes. + </p> +[% END %] + +[% BLOCK change_column %] + <td> + [% IF change_type.defined %] + [% IF change.fieldname == 'estimated_time' || + change.fieldname == 'remaining_time' || + change.fieldname == 'work_time' %] + [% PROCESS formattimeunit time_unit=change_type %] + [% ELSIF change.fieldname == 'blocked' || + change.fieldname == 'dependson' %] + [% change_type FILTER bug_list_link FILTER none %] + [% ELSIF change.fieldname == 'assigned_to' || + change.fieldname == 'reporter' || + change.fieldname == 'qa_contact' || + change.fieldname == 'cc' || + change.fieldname == 'flagtypes.name' %] + [% display_value(change.fieldname, change_type) FILTER email FILTER html %] + [% ELSE %] + [% display_value(change.fieldname, change_type) FILTER html %] + [% END %] + [% ELSE %] + + [% END %] + </td> +[% END %] +[% END %] + + +[% INCLUDE global/footer.html.tmpl %] diff --git a/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl new file mode 100644 index 000000000..5d187bf40 --- /dev/null +++ b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl @@ -0,0 +1,24 @@ +[%# 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): Frédéric Buclin <LpSolit@gmail.com> + # + #%] +[% PROCESS global/variables.none.tmpl %] +<?xml version="1.0" encoding="UTF-8"?> +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> +<ShortName>[% terms.BugzillaTitle %]</ShortName> +<Description>[% terms.BugzillaTitle %] Quick Search</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAABGdBTUEAAK%2FINwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz%2F%2Fz8DJQAggJiQOe%2Ffv2fv7Oz8rays%2FN%2BVkfG%2FiYnJfyD%2F1%2BrVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw%2F8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi%2FG%2BQKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo%2BMXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia%2BCuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq%2FvLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg%2FkdypqCg4H8lUIACnQ%2FSOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD%2BaDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg%3D%3D</Image> +<Url type="text/html" method="GET" template="[% urlbase FILTER xml %]buglist.cgi?quicksearch={searchTerms}"/> +</OpenSearchDescription> diff --git a/extensions/BMO/web/images/background.png b/extensions/BMO/web/images/background.png Binary files differnew file mode 100644 index 000000000..eb254aab9 --- /dev/null +++ b/extensions/BMO/web/images/background.png diff --git a/extensions/BMO/web/images/bugzilla.png b/extensions/BMO/web/images/bugzilla.png Binary files differnew file mode 100644 index 000000000..4b7c10284 --- /dev/null +++ b/extensions/BMO/web/images/bugzilla.png diff --git a/extensions/BMO/web/images/groups/bugzilla-approvers.png b/extensions/BMO/web/images/groups/bugzilla-approvers.png Binary files differnew file mode 100644 index 000000000..d2414e041 --- /dev/null +++ b/extensions/BMO/web/images/groups/bugzilla-approvers.png diff --git a/extensions/BMO/web/images/groups/calendar-drivers.png b/extensions/BMO/web/images/groups/calendar-drivers.png Binary files differnew file mode 100644 index 000000000..fc2c1d1e5 --- /dev/null +++ b/extensions/BMO/web/images/groups/calendar-drivers.png diff --git a/extensions/BMO/web/images/mozchomp.gif b/extensions/BMO/web/images/mozchomp.gif Binary files differnew file mode 100644 index 000000000..ac6549527 --- /dev/null +++ b/extensions/BMO/web/images/mozchomp.gif diff --git a/extensions/BMO/web/images/presshat.png b/extensions/BMO/web/images/presshat.png Binary files differnew file mode 100644 index 000000000..a61de59e5 --- /dev/null +++ b/extensions/BMO/web/images/presshat.png diff --git a/extensions/BMO/web/images/stop-sign.gif b/extensions/BMO/web/images/stop-sign.gif Binary files differnew file mode 100644 index 000000000..9b420ec6c --- /dev/null +++ b/extensions/BMO/web/images/stop-sign.gif diff --git a/extensions/BMO/web/js/edit_bug.js b/extensions/BMO/web/js/edit_bug.js new file mode 100644 index 000000000..5fc2ab6f7 --- /dev/null +++ b/extensions/BMO/web/js/edit_bug.js @@ -0,0 +1,71 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 BMO Bugzilla Extension; + * + * The Initial Developer of the Original Code is the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 the + * Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + */ + +// --- custom flags +var Dom = YAHOO.util.Dom; + +function bmo_hide_tracking_flags() { + for (var field in bmo_custom_flags) { + var el = Dom.get(field); + var value = el ? el.value : bmo_custom_flags[field]; + if (el && (value != bmo_custom_flags[field])) { + bmo_show_tracking_flags(); + return; + } + if (value == '---') { + Dom.addClass('row_' + field, 'bz_hidden'); + } else { + Dom.addClass(field, 'bz_hidden'); + Dom.removeClass('ro_' + field, 'bz_hidden'); + } + } +} + +function bmo_show_tracking_flags() { + Dom.addClass('edit_tracking_fields_action', 'bz_hidden'); + for (var field in bmo_custom_flags) { + if (Dom.get(field).value == '---') { + Dom.removeClass('row_' + field, 'bz_hidden'); + } else { + Dom.removeClass(field, 'bz_hidden'); + Dom.addClass('ro_' + field, 'bz_hidden'); + } + } +} + +// -- make attachment table, comments, new comment textarea equal widths + +YAHOO.util.Event.onDOMReady(function() { + var comment_tables = Dom.getElementsByClassName('bz_comment_table', 'table', 'comments'); + if (comment_tables.length) { + var comment_width = comment_tables[0].getElementsByTagName('td')[0].clientWidth + 'px'; + var attachment_table = Dom.get('attachment_table'); + if (attachment_table) + attachment_table.style.width = comment_width; + var new_comment = Dom.get('comment'); + if (new_comment) + new_comment.style.width = comment_width; + } +}); diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js new file mode 100644 index 000000000..9ce78d9ce --- /dev/null +++ b/extensions/BMO/web/js/edituser_menu.js @@ -0,0 +1,26 @@ +var admin_usermenu; + +YAHOO.util.Event.onDOMReady(function() { + admin_usermenu = new YAHOO.widget.Menu('admin_usermenu', { position : 'dynamic' }); + admin_usermenu.addItems([ + { text: 'Edit', url: '#', target: '_new' }, + { text: 'Activity', url: '#', target: '_new' }, + { text: 'Mail', url: '#', target: '_new' } + ]); + admin_usermenu.render(document.body); +}); + +function show_admin_username(event, id, email) { + if (!admin_usermenu) + return; + admin_usermenu.getItem(0).cfg.setProperty('url', 'editusers.cgi?action=edit&userid=' + id); + admin_usermenu.getItem(1).cfg.setProperty('url', + 'page.cgi?id=user_activity.html&action=run' + + '&from=' + YAHOO.util.Date.format(new Date(new Date() - (1000 * 60 * 60 * 24 * 14)), {format: '%Y-%m-%d'}) + + '&to=' + YAHOO.util.Date.format(new Date(), {format: '%Y-%m-%d'}) + + '&who=' + escape(email)); + admin_usermenu.getItem(2).cfg.setProperty('url', 'mailto:' + escape(email)); + admin_usermenu.cfg.setProperty('xy', YAHOO.util.Event.getXY(event)); + admin_usermenu.show(); +} + diff --git a/extensions/BMO/web/js/form_validate.js b/extensions/BMO/web/js/form_validate.js new file mode 100644 index 000000000..6c8fa6f07 --- /dev/null +++ b/extensions/BMO/web/js/form_validate.js @@ -0,0 +1,21 @@ +/** + * Some Form Validation and Interaction + **/ +//Makes sure that there is an '@' in the address with a '.' +//somewhere after it (and at least one character in between them + +function isValidEmail(email) { + var at_index = email.indexOf("@"); + var last_dot = email.lastIndexOf("."); + return at_index > 0 && last_dot > (at_index + 1); +} + +//Takes a DOM element id and makes sure that it is filled out +function isFilledOut(elem_id) { + var str = document.getElementById(elem_id).value; + return str.length>0 && str!="noneselected"; +} + +function isChecked(elem_id) { + return document.getElementById(elem_id).checked; +} diff --git a/extensions/BMO/web/js/sorttable.js b/extensions/BMO/web/js/sorttable.js new file mode 100644 index 000000000..1703a06ae --- /dev/null +++ b/extensions/BMO/web/js/sorttable.js @@ -0,0 +1,709 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add <script src="sorttable.js"></script> to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + /* + * Prepares the table so that it can be sorted + * + */ + makeSortable: function(table) { + + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + //if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i<table.rows.length; i++) { + if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { + sortbottomrows[sortbottomrows.length] = table.rows[i]; + } + } + + if (sortbottomrows) { + if (table.tFoot == null) { + // table doesn't have a tfoot. Create one. + tfo = document.createElement('tfoot'); + table.appendChild(tfo); + } + for (var i=0; i<sortbottomrows.length; i++) { + tfo.appendChild(sortbottomrows[i]); + } + delete sortbottomrows; + } + + sorttable._walk_through_headers(table); + }, + + /* + * Helper function for preparing the table + * + */ + _walk_through_headers: function(table) { + // First, gather some information we need to sort the table. + var bodies = []; + var table_rows = []; + var body_size = table.tBodies[0].rows.length; + + // We need to get all the rows + for (var i=0; i<table.tBodies.length; i++) { + if (!table.tBodies[i].className.match(/\bsorttable_body\b/)) + continue; + + bodies[bodies.length] = table.tBodies[i]; + for (j=0; j<table.tBodies[i].rows.length; j++) { + table_rows[table_rows.length] = table.tBodies[i].rows[j]; + } + } + + table.sorttable_rows = table_rows; + table.sorttable_body_size = body_size; + table.sorttable_bodies = bodies; + + + // work through each column and calculate its type + + // For each row in the header.. + for (var row_index=0; row_index < table.tHead.rows.length; row_index++) { + + headrow = table.tHead.rows[row_index].cells; + // ... Walk through each column and calculate the type. + for (var i=0; i<headrow.length; i++) { + // Don't sort this column, please + if (headrow[i].className.match(/\bsorttable_nosort\b/)) continue; + + // Override sort column index. + column_index = i; + mtch = headrow[i].className.match(/\bsortable_column_([a-z0-9]+)\b/); + if (mtch) column_index = mtch[1]; + + + // Manually override the type with a sorttable_type attribute + // Override sort function + mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); + if (mtch) override = mtch[1]; + + if (mtch && typeof sorttable["sort_"+override] == 'function') { + headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; + } else { + headrow[i].sorttable_sortfunction = sorttable.guessType(table, column_index); + } + + // make it clickable to sort + headrow[i].sorttable_columnindex = column_index; + headrow[i].table = table; + + // If the header contains a link, clear the href. + for (var k=0; k<headrow[i].childNodes.length; k++) { + if (headrow[i].childNodes[k].tagName == 'A') { + headrow[i].childNodes[k].href = "javascript:void(0);"; + } + } + + dean_addEvent(headrow[i], "click", sorttable._on_column_header_clicked); + + } // inner for (var i=0; i<headrow.length; i++) + } // outer for + }, + + + + + /* + * Helper function for the _on_column_header_clicked handler + * + */ + + _remove_sorted_classes: function(header) { + // For each row in the header.. + for (var j=0; j< header.rows.length; j++) { + // ... Walk through each column and calculate the type. + row = header.rows[j].cells; + + for (var i=0; i<row.length; i++) { + cell = row[i]; + if (cell.nodeType != 1) return; // an element + + mtch = cell.className.match(/\bsorted_([0-9]+)\b/); + if (mtch) { + cell.className = cell.className.replace('sorted_'+mtch[1], + 'sorted_'+(parseInt(mtch[1])+1)); + } + + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + } + }, + + _check_already_sorted: function(cell) { + if (cell.className.search(/\bsorttable_sorted\b/) != -1) { + // if we're already sorted by this column, just + // reverse the table, which is quicker + sorttable.reverse_table(cell); + + sorttable._mark_column_as_sorted(cell, '▲', 1); + return 1; + } + + if (cell.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse_table(cell); + + sorttable._mark_column_as_sorted(cell, '▼', 0); + + return 1; + } + + return 0; + }, + + /* Visualy mark the cell as sorted. + * + * @param cell: the cell being marked + * @param text: the text being used to mark. you can use html + * @param reversed: whether the column is reversed or not. + * + */ + _mark_column_as_sorted: function(cell, text, reversed) { + // remove eventual class + cell.className = cell.className.replace('sorttable_sorted', ''); + cell.className = cell.className.replace('sorttable_sorted_reverse', ''); + + // the column is reversed + if (reversed) { + cell.className += ' sorttable_sorted_reverse'; + } + else { + // remove eventual class + cell.className += ' sorttable_sorted'; + } + + sorttable._remove_sorting_marker(); + + marker = document.createElement('span'); + marker.id = "sorttable_sort_mark"; + marker.className = "bz_sort_order_primary"; + marker.innerHTML = text; + cell.appendChild(marker); + }, + + _remove_sorting_marker: function() { + mark = document.getElementById('sorttable_sort_mark'); + if (mark) { mark.parentNode.removeChild(mark); } + els = sorttable._getElementsByClassName('bz_sort_order_primary'); + for(var i=0,j=els.length; i<j; i++) { + els[i].parentNode.removeChild(els[i]); + } + els = sorttable._getElementsByClassName('bz_sort_order_secondary'); + for(var i=0,j=els.length; i<j; i++) { + els[i].parentNode.removeChild(els[i]); + } + }, + + _getElementsByClassName: function(classname, node) { + if(!node) node = document.getElementsByTagName("body")[0]; + var a = []; + var re = new RegExp('\\b' + classname + '\\b'); + var els = node.getElementsByTagName("*"); + for(var i=0,j=els.length; i<j; i++) + if(re.test(els[i].className))a.push(els[i]); + return a; + }, + + /* + * This is the callback for when the table header is clicked. + * + * @param evt: the event that triggered this callback + */ + _on_column_header_clicked: function(evt) { + + // The table is already sorted by this column. Just reverse it. + if (sorttable._check_already_sorted(this)) + return; + + + // First, remove sorttable_sorted classes from the other header + // that is currently sorted and its marker (the simbol indicating + // that its sorted. + sorttable._remove_sorted_classes(this.table.tHead); + mtch = this.className.match(/\bsorted_([0-9]+)\b/); + if (mtch) { + this.className = this.className.replace('sorted_'+mtch[1], ''); + } + this.className += ' sorted_0 '; + + // This is the text that indicates that the column is sorted. + sorttable._mark_column_as_sorted(this, '▼', 0); + + sorttable.sort_table(this); + + }, + + sort_table: function(cell) { + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + col = cell.sorttable_columnindex; + rows = cell.table.sorttable_rows; + + var BUGLIST = ''; + + for (var j = 0; j < cell.table.sorttable_rows.length; j++) { + rows[j].sort_data = sorttable.getInnerText(rows[j].cells[col]); + } + + /* If you want a stable sort, uncomment the following line */ + sorttable.shaker_sort(rows, cell.sorttable_sortfunction); + /* and comment out this one */ + //rows.sort(cell.sorttable_sortfunction); + + // Rebuild the table, using he sorted rows. + tb = cell.table.sorttable_bodies[0]; + body_size = cell.table.sorttable_body_size; + body_index = 0; + + for (var j=0; j<rows.length; j++) { + if (j % 2) + rows[j].className = rows[j].className.replace('bz_row_even', + 'bz_row_odd'); + else + rows[j].className = rows[j].className.replace('bz_row_odd', + 'bz_row_even'); + + tb.appendChild(rows[j]); + var bug_id = sorttable.getInnerText(rows[j].cells[0].childNodes[1]); + BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id; + + if (j % body_size == body_size-1) { + body_index++; + if (body_index < cell.table.sorttable_bodies.length) { + tb = cell.table.sorttable_bodies[body_index]; + } + } + } + + document.cookie = 'BUGLIST='+BUGLIST; + + cell.table.sorttable_rows = rows; + }, + + reverse_table: function(cell) { + oldrows = cell.table.sorttable_rows; + newrows = []; + + for (var i=0; i < oldrows.length; i++) { + newrows[newrows.length] = oldrows[i]; + } + + tb = cell.table.sorttable_bodies[0]; + body_size = cell.table.sorttable_body_size; + body_index = 0; + + var BUGLIST = ''; + + cell.table.sorttable_rows = []; + for (var i = newrows.length-1; i >= 0; i--) { + if (i % 2) + newrows[i].className = newrows[i].className.replace('bz_row_even', + 'bz_row_odd'); + else + newrows[i].className = newrows[i].className.replace('bz_row_odd', + 'bz_row_even'); + + tb.appendChild(newrows[i]); + cell.table.sorttable_rows.push(newrows[i]); + + var bug_id = sorttable.getInnerText(newrows[i].cells[0].childNodes[1]); + BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id; + + if ((newrows.length-1-i) % body_size == body_size-1) { + body_index++; + if (body_index < cell.table.sorttable_bodies.length) { + tb = cell.table.sorttable_bodies[body_index]; + } + } + + } + + document.cookie = 'BUGLIST='+BUGLIST; + + delete newrows; + }, + + guessType: function(table, column) { + // guess the type of a column based on its first non-blank row + sortfn = sorttable.sort_alpha; + for (var i=0; i<table.sorttable_bodies[0].rows.length; i++) { + text = sorttable.getInnerText(table.sorttable_bodies[0].rows[i].cells[column]); + if (text != '') { + if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { + return sorttable.sort_numeric; + } + // check for a date: dd/mm/yyyy or dd/mm/yy + // can have / or . or - as separator + // can be mm/dd as well + possdate = text.match(sorttable.DATE_RE) + if (possdate) { + // looks like a date + first = parseInt(possdate[1]); + second = parseInt(possdate[2]); + if (first > 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for <input> fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (typeof node.getAttribute != 'undefined' && node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a.sort_data and b.sort_data */ + sort_numeric: function(a,b) { + aa = parseFloat(a.sort_data.replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b.sort_data.replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + + sort_alpha: function(a,b) { + if (a.sort_data.toLowerCase()==b.sort_data.toLowerCase()) return 0; + if (a.sort_data.toLowerCase()<b.sort_data.toLowerCase()) return -1; + return 1; + }, + + sort_ddmm: function(a,b) { + mtch = a.sort_data.match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b.sort_data.match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + + sort_mmdd: function(a,b) { + mtch = a.sort_data.match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b.sort_data.match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + + shaker_sort: function(list, comp_func) { + // A stable sort function to allow multi-level sorting of data + // see: http://en.wikipedia.org/wiki/Cocktail_sort + // thanks to Joseph Nahmias + var b = 0; + var t = list.length - 1; + var swap = true; + + while(swap) { + swap = false; + for(var i = b; i < t; ++i) { + if ( comp_func(list[i], list[i+1]) > 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + // IE doesn't have a way to test if the DOM is loaded + // doing a deferred script load with onReadyStateChange checks is + // problematic, so poll the document until it is scrollable + // http://blogs.atlassian.com/developer/2008/03/when_ie_says_dom_is_ready_but.html + var loadTestTimer = function() { + try { + if (document.readyState != "loaded" && document.readyState != "complete") { + document.documentElement.doScroll("left"); + } + sorttable.init(); // call the onload handler + } catch(error) { + setTimeout(loadTestTimer, 100); + } + }; + loadTestTimer(); +/*@end @*/ + +/* for Safari */ +if (/WebKit/i.test(navigator.userAgent)) { // sniff + var _timer = setInterval(function() { + if (/loaded|complete/.test(document.readyState)) { + sorttable.init(); // call the onload handler + } + }, 10); +} + +/* for other browsers */ +window.onload = sorttable.init; + +// written by Dean Edwards, 2005 +// with input from Tino Zijdel, Matthias Miller, Diego Perini + +// http://dean.edwards.name/weblog/2005/10/add-event/ + +function dean_addEvent(element, type, handler) { + if (element.addEventListener) { + element.addEventListener(type, handler, false); + } else { + // assign each event handler a unique ID + if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; + // create a hash table of event types for the element + if (!element.events) element.events = {}; + // create a hash table of event handlers for each element/event pair + var handlers = element.events[type]; + if (!handlers) { + handlers = element.events[type] = {}; + // store the existing event handler (if there is one) + if (element["on" + type]) { + handlers[0] = element["on" + type]; + } + } + // store the event handler in the hash table + handlers[handler.$$guid] = handler; + // assign a global event handler to do all the work + element["on" + type] = handleEvent; + } +}; +// a counter used to create unique IDs +dean_addEvent.guid = 1; + +function removeEvent(element, type, handler) { + if (element.removeEventListener) { + element.removeEventListener(type, handler, false); + } else { + // delete the event handler from the hash table + if (element.events && element.events[type]) { + delete element.events[type][handler.$$guid]; + } + } +}; + +function handleEvent(event) { + var returnValue = true; + // grab the event object (IE uses a global event object) + event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); + // get a reference to the hash table of event handlers + var handlers = this.events[event.type]; + // execute each event handler + for (var i in handlers) { + this.$$handleEvent = handlers[i]; + if (this.$$handleEvent(event) === false) { + returnValue = false; + } + } + return returnValue; +}; + +function fixEvent(event) { + // add W3C standard event methods + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + return event; +}; +fixEvent.preventDefault = function() { + this.returnValue = false; +}; +fixEvent.stopPropagation = function() { + this.cancelBubble = true; +} + +// Dean's forEach: http://dean.edwards.name/base/forEach.js +/* + forEach, version 1.0 + Copyright 2006, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ + +// array-like enumeration +if (!Array.forEach) { // mozilla already supports this + Array.forEach = function(array, block, context) { + for (var i = 0; i < array.length; i++) { + block.call(context, array[i], i, array); + } + }; +} + +// generic enumeration +Function.prototype.forEach = function(object, block, context) { + for (var key in object) { + if (typeof this.prototype[key] == "undefined") { + block.call(context, object[key], key, object); + } + } +}; + +// character enumeration +String.forEach = function(string, block, context) { + Array.forEach(string.split(""), function(chr, index) { + block.call(context, chr, index, string); + }); +}; + +// globally resolve forEach enumeration +var forEach = function(object, block, context) { + if (object) { + var resolve = Object; // default + if (object instanceof Function) { + // functions have a "length" property + resolve = Function; + } else if (object.forEach instanceof Function) { + // the object implements a custom forEach method so use that + object.forEach(block, context); + return; + } else if (typeof object == "string") { + // the object is a string + resolve = String; + } else if (typeof object.length == "number") { + // the object is array-like + resolve = Array; + } + resolve.forEach(object, block, context); + } +}; + diff --git a/extensions/BMO/web/js/swag.js b/extensions/BMO/web/js/swag.js new file mode 100644 index 000000000..47886b2a9 --- /dev/null +++ b/extensions/BMO/web/js/swag.js @@ -0,0 +1,60 @@ +/** + * Swag Request Form Functions + * Form Interal Swag Request Form + * dtran + * 7/6/09 + **/ + + +function evalToNumber(numberString) { + if(numberString=='') return 0; + return parseInt(numberString); +} + +function evalToNumberString(numberString) { + if(numberString=='') return '0'; + return numberString; +} +//item_array should be an array of DOM element ids +function getTotal(item_array) { + var total = 0; + for(var i in item_array) { + total += evalToNumber(document.getElementById(item_array[i]).value); + } + return total; +} + +function calculateTotalSwag() { + document.getElementById('Totalswag').value = + getTotal( new Array('Lanyards', + 'Stickers', + 'Bracelets', + 'Tattoos', + 'Buttons', + 'Posters')); + +} + + +function calculateTotalMensShirts() { + document.getElementById('mens_total').value = + getTotal( new Array('mens_s', + 'mens_m', + 'mens_l', + 'mens_xl', + 'mens_xxl', + 'mens_xxxl')); + +} + + +function calculateTotalWomensShirts() { + document.getElementById('womens_total').value = + getTotal( new Array('womens_s', + 'womens_m', + 'womens_l', + 'womens_xl', + 'womens_xxl', + 'womens_xxxl')); + +} diff --git a/extensions/BMO/web/js/triage_reports.js b/extensions/BMO/web/js/triage_reports.js new file mode 100644 index 000000000..855b577d7 --- /dev/null +++ b/extensions/BMO/web/js/triage_reports.js @@ -0,0 +1,83 @@ +var Dom = YAHOO.util.Dom; + +function onSelectProduct() { + var component = Dom.get('component'); + if (Dom.get('product').value == '') { + bz_clearOptions(component); + return; + } + selectProduct(Dom.get('product'), component); + // selectProduct only supports __Any__ on both elements + // we only want it on component, so add it back in + try { + component.add(new Option('__Any__', ''), component.options[0]); + } catch(e) { + // support IE + component.add(new Option('__Any__', ''), 0); + } + component.value = ''; +} + +function onCommenterChange() { + var commenter_is = Dom.get('commenter_is'); + if (Dom.get('commenter').value == 'is') { + Dom.removeClass(commenter_is, 'hidden'); + } else { + Dom.addClass(commenter_is, 'hidden'); + } +} + +function onLastChange() { + var last_is_span = Dom.get('last_is_span'); + if (Dom.get('last').value == 'is') { + Dom.removeClass(last_is_span, 'hidden'); + } else { + Dom.addClass(last_is_span, 'hidden'); + } +} + +function onGenerateReport() { + if (Dom.get('product').value == '') { + alert('You must select a product.'); + return false; + } + if (Dom.get('component').value == '' && !Dom.get('component').options[0].selected) { + alert('You must select at least one component.'); + return false; + } + if (!(Dom.get('filter_commenter').checked || Dom.get('filter_last').checked)) { + alert('You must select at least one comment filter.'); + return false; + } + if (Dom.get('filter_commenter').checked + && Dom.get('commenter').value == 'is' + && Dom.get('commenter_is').value == '') + { + alert('You must specify the last commenter\'s email address.'); + return false; + } + if (Dom.get('filter_last').checked + && Dom.get('last').value == 'is' + && Dom.get('last_is').value == '') + { + alert('You must specify the "comment is older than" date.'); + return false; + } + return true; +} + +YAHOO.util.Event.onDOMReady(function() { + onSelectProduct(); + onCommenterChange(); + onLastChange(); + + var component = Dom.get('component'); + if (selected_components.length == 0) + return; + component.options[0].selected = false; + for (var i = 0, n = selected_components.length; i < n; i++) { + var index = bz_optionIndex(component, selected_components[i]); + if (index != -1) + component.options[index].selected = true; + } +}); diff --git a/extensions/BMO/web/producticons/camino.png b/extensions/BMO/web/producticons/camino.png Binary files differnew file mode 100644 index 000000000..c833b4d04 --- /dev/null +++ b/extensions/BMO/web/producticons/camino.png diff --git a/extensions/BMO/web/producticons/dino.png b/extensions/BMO/web/producticons/dino.png Binary files differnew file mode 100644 index 000000000..9e0470a07 --- /dev/null +++ b/extensions/BMO/web/producticons/dino.png diff --git a/extensions/BMO/web/producticons/fennec.png b/extensions/BMO/web/producticons/fennec.png Binary files differnew file mode 100644 index 000000000..ebad7e358 --- /dev/null +++ b/extensions/BMO/web/producticons/fennec.png diff --git a/extensions/BMO/web/producticons/firefox.png b/extensions/BMO/web/producticons/firefox.png Binary files differnew file mode 100644 index 000000000..582a6952a --- /dev/null +++ b/extensions/BMO/web/producticons/firefox.png diff --git a/extensions/BMO/web/producticons/idea.png b/extensions/BMO/web/producticons/idea.png Binary files differnew file mode 100644 index 000000000..9480dce62 --- /dev/null +++ b/extensions/BMO/web/producticons/idea.png diff --git a/extensions/BMO/web/producticons/input.png b/extensions/BMO/web/producticons/input.png Binary files differnew file mode 100644 index 000000000..81f355d85 --- /dev/null +++ b/extensions/BMO/web/producticons/input.png diff --git a/extensions/BMO/web/producticons/labs.png b/extensions/BMO/web/producticons/labs.png Binary files differnew file mode 100644 index 000000000..346e0ef06 --- /dev/null +++ b/extensions/BMO/web/producticons/labs.png diff --git a/extensions/BMO/web/producticons/mozilla.png b/extensions/BMO/web/producticons/mozilla.png Binary files differnew file mode 100644 index 000000000..e506328bc --- /dev/null +++ b/extensions/BMO/web/producticons/mozilla.png diff --git a/extensions/BMO/web/producticons/other.png b/extensions/BMO/web/producticons/other.png Binary files differnew file mode 100644 index 000000000..e436c22ae --- /dev/null +++ b/extensions/BMO/web/producticons/other.png diff --git a/extensions/BMO/web/producticons/seamonkey.png b/extensions/BMO/web/producticons/seamonkey.png Binary files differnew file mode 100644 index 000000000..fcb261ae1 --- /dev/null +++ b/extensions/BMO/web/producticons/seamonkey.png diff --git a/extensions/BMO/web/producticons/sunbird.png b/extensions/BMO/web/producticons/sunbird.png Binary files differnew file mode 100644 index 000000000..6b15c257d --- /dev/null +++ b/extensions/BMO/web/producticons/sunbird.png diff --git a/extensions/BMO/web/producticons/thunderbird.png b/extensions/BMO/web/producticons/thunderbird.png Binary files differnew file mode 100644 index 000000000..f3523183a --- /dev/null +++ b/extensions/BMO/web/producticons/thunderbird.png diff --git a/extensions/BMO/web/styles/create_account.css b/extensions/BMO/web/styles/create_account.css new file mode 100644 index 000000000..27ea9912a --- /dev/null +++ b/extensions/BMO/web/styles/create_account.css @@ -0,0 +1,62 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +#create-account h2 { + margin: 0px; +} + +.column-header { + padding: 20px 20px 20px 0px; +} + +#create-account-left { + border-right: 2px solid #888888; + padding-right: 10px; +} + +#product-list td { + padding-top: 10px; +} + +#product-list img { + padding-right: 10px; +} + +#create-account-right { + padding-left: 10px; +} + +#right-blurb { + font-size: large; +} + +#right-blurb li { + padding-bottom: 1em; +} + +#create-account-form { + margin-bottom: 5em; +} + diff --git a/extensions/BMO/web/styles/edit_bug.css b/extensions/BMO/web/styles/edit_bug.css new file mode 100644 index 000000000..ecfb2f80c --- /dev/null +++ b/extensions/BMO/web/styles/edit_bug.css @@ -0,0 +1,32 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 BMO Bugzilla Extension; + * + * The Initial Developer of the Original Code is the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 the + * Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + */ + +#custom-flags { + width: auto; +} + +.bz_hidden { + display: none; +} diff --git a/extensions/BMO/web/styles/reports.css b/extensions/BMO/web/styles/reports.css new file mode 100644 index 000000000..3106a6295 --- /dev/null +++ b/extensions/BMO/web/styles/reports.css @@ -0,0 +1,37 @@ +.hidden { + display: none; +} + +#triage_form th { + text-align: left; +} + +#product, #component { + width: 20em; +} + +#report tr.bugitem:hover { + background: #ccccff; +} + +#report td { + padding: 1px 10px 1px 10px; +} + +#report-header { + background: #dddddd; +} + +.report_row_odd { + background-color: #F7F7F7; + color: #000000; +} + +.report_row_even { + background-color: #FFFFFF; + color: #000000; +} + +tr.report_item:hover { + background-color: #ccccff; +} diff --git a/extensions/BMO/web/styles/triage_reports.css b/extensions/BMO/web/styles/triage_reports.css new file mode 100644 index 000000000..6190fd32c --- /dev/null +++ b/extensions/BMO/web/styles/triage_reports.css @@ -0,0 +1,23 @@ +.hidden { + display: none; +} + +#triage_form th { + text-align: left; +} + +#product, #component { + width: 20em; +} + +#report tr.bugitem:hover { + background: #ccccff; +} + +#report td { + padding: 1px 10px 1px 10px; +} + +#report-header { + background: #dddddd; +} diff --git a/extensions/BzAPI/Config.pm b/extensions/BzAPI/Config.pm new file mode 100644 index 000000000..0de081097 --- /dev/null +++ b/extensions/BzAPI/Config.pm @@ -0,0 +1,63 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# 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 BzAPI Bugzilla Extension. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +package Bugzilla::Extension::BzAPI; +use strict; + +use constant NAME => 'BzAPI'; + +use constant REQUIRED_MODULES => [ + { + package => 'SOAP-Lite', + module => 'SOAP::Lite', + # 0.710.04 is required for correct UTF-8 handling, but .04 and .05 are + # affected by bug 468009. + version => '0.710.06', + }, + { + package => 'Test-Taint', + module => 'Test::Taint', + version => 0, + }, + { + package => 'JSON', + module => 'JSON', + version => 0, + }, +]; + +__PACKAGE__->NAME; diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm new file mode 100644 index 000000000..aeaa0bce4 --- /dev/null +++ b/extensions/BzAPI/Extension.pm @@ -0,0 +1,71 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# 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 BzAPI Bugzilla Extension. +# +# The Initial Developer of the Original Code is +# the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +package Bugzilla::Extension::BzAPI; +use strict; +use base qw(Bugzilla::Extension); + +our $VERSION = '0.1'; + +# Add JSON filter for JSON templates +sub template_before_create { + my ($self, $args) = @_; + my $config = $args->{'config'}; + + $config->{'FILTERS'}->{'json'} = sub { + my ($var) = @_; + $var =~ s/([\\\"\/])/\\$1/g; + $var =~ s/\n/\\n/g; + $var =~ s/\r/\\r/g; + $var =~ s/\f/\\f/g; + $var =~ s/\t/\\t/g; + return $var; + }; +} + +sub template_before_process { + my ($self, $args) = @_; + my $vars = $args->{'vars'}; + my $file = $args->{'file'}; + + if ($file =~ /config\.json\.tmpl$/) { + $vars->{'initial_status'} = Bugzilla::Status->can_change_to; + $vars->{'status_objects'} = [Bugzilla::Status->get_all]; + } +} + +__PACKAGE__->NAME; diff --git a/extensions/BzAPI/template/en/default/config.json.tmpl b/extensions/BzAPI/template/en/default/config.json.tmpl new file mode 100644 index 000000000..d567c6c55 --- /dev/null +++ b/extensions/BzAPI/template/en/default/config.json.tmpl @@ -0,0 +1,316 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] + +[% + # Pinched from Bugzilla/API/Model/Utils.pm in BzAPI - need to keep in sync +OLD2NEW = { + 'opendate' => 'creation_time', # query + 'creation_ts' => 'creation_time', + 'changeddate' => 'last_change_time', # query + 'delta_ts' => 'last_change_time', + 'bug_id' => 'id', + 'rep_platform' => 'platform', + 'bug_severity' => 'severity', + 'bug_status' => 'status', + 'short_desc' => 'summary', + 'short_short_desc' => 'summary', + 'bug_file_loc' => 'url', + 'status_whiteboard' => 'whiteboard', + 'reporter' => 'creator', + 'reporter_realname' => 'creator_realname', + 'cclist_accessible' => 'is_cc_accessible', + 'reporter_accessible' => 'is_creator_accessible', + 'everconfirmed' => 'is_confirmed', + 'dependson' => 'depends_on', + 'blocked' => 'blocks', + 'attachment' => 'attachments', + 'flag' => 'flags', + 'flagtypes.name' => 'flag', + 'bug_group' => 'group', + 'group' => 'groups', + 'longdesc' => 'comment', + 'bug_file_loc_type' => 'url_type', + 'bugidtype' => 'id_mode', + 'longdesc_type' => 'comment_type', + 'short_desc_type' => 'summary_type', + 'status_whiteboard_type' => 'whiteboard_type', + 'emailassigned_to1' => 'email1_assigned_to', + 'emailassigned_to2' => 'email2_assigned_to', + 'emailcc1' => 'email1_cc', + 'emailcc2' => 'email2_cc', + 'emailqa_contact1' => 'email1_qa_contact', + 'emailqa_contact2' => 'email2_qa_contact', + 'emailreporter1' => 'email1_creator', + 'emailreporter2' => 'email2_creator', + 'emaillongdesc1' => 'email1_comment_creator', + 'emaillongdesc2' => 'email2_comment_creator', + 'emailtype1' => 'email1_type', + 'emailtype2' => 'email2_type', + 'chfieldfrom' => 'changed_after', + 'chfieldto' => 'changed_before', + 'chfield' => 'changed_field', + 'chfieldvalue' => 'changed_field_to', + 'deadlinefrom' => 'deadline_after', + 'deadlineto' => 'deadline_before', + 'attach_data.thedata' => 'attachment.data', + 'longdescs.isprivate' => 'comment.is_private', + 'commenter' => 'comment.creator', + 'flagtypes.name' => 'flag', + 'requestees.login_name' => 'flag.requestee', + 'setters.login_name' => 'flag.setter', + 'days_elapsed' => 'idle', + 'owner_idle_time' => 'assignee_idle', + 'dup_id' => 'dupe_of', + 'isopened' => 'is_open', + 'flag_type' => 'flag_types', + 'token' => 'update_token' +}; + +OLDATTACH2NEW = { + 'submitter' => 'attacher', + 'description' => 'description', + 'filename' => 'file_name', + 'delta_ts' => 'last_change_time', + 'isobsolete' => 'is_obsolete', + 'ispatch' => 'is_patch', + 'isprivate' => 'is_private', + 'mimetype' => 'content_type', + 'contenttypeentry' => 'content_type', + 'date' => 'creation_time', + 'attachid' => 'id', + 'desc' => 'description', + 'flag' => 'flags', + 'type' => 'content_type', + 'token' => 'update_token' +}; + +%] + +[%# Add attachment stuff to the main hash - but with right prefix. (This is + # the way the code is structured in BzAPI, and changing it makes it harder + # to keep the two in sync.) + #%] +[% FOREACH entry IN OLDATTACH2NEW %] + [% newkey = 'attachments.' _ entry.key %] + [% OLD2NEW.${newkey} = 'attachment.' _ OLDATTACH2NEW.${entry.key} %] +[% END %] + +[% all_visible_flag_types = {} %] + +{ + "version": "[% constants.BUGZILLA_VERSION FILTER json %]", + "maintainer": "[% Param('maintainer') FILTER json %]", + "announcement": "[% Param('announcehtml') FILTER json %]", + "max_attachment_size": [% (Param('maxattachmentsize') * 1000) FILTER json %], + +[% IF Param('useclassification') %] + [% classifications = user.get_selectable_classifications() %] + [% cl_name_for = {} %] + "classification": { + [% FOREACH cl IN classifications %] + [% cl_name_for.${cl.id} = cl.name %] + "[% cl.name FILTER json %]": { + "id": [% cl.id %], + "description": "[% cl.description FILTER json %]", + "products": [ + [% FOREACH product IN cl.products %] + "[% product.name FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ] + }[% ',' UNLESS loop.last() %] + [% END %] + }, +[% END %] + + "product": { + [% FOREACH product = products %] + "[% product.name FILTER json %]": { + "id": [% product.id %], + "description": "[% product.description FILTER json %]", + "is_active": [% product.isactive ? "true" : "false" %], + "is_permitting_unconfirmed": [% product.allows_unconfirmed ? "true" : "false" %], +[% IF Param('useclassification') %] + "classification": "[% cl_name_for.${product.classification_id} FILTER json %]", +[% END %] + "component": { + [% FOREACH component = product.components %] + "[% component.name FILTER json %]": { + "id": [% component.id %], +[% IF show_flags %] + "flag_type": [ + [% flag_types = + component.flag_types(is_active=>1).bug.merge(component.flag_types(is_active=>1).attachment) %] + [%-# "first" flag used to get commas right; can't use loop.last() in case + # last flag is inactive %] + [% first = 1 %] + [% FOREACH flag_type = flag_types %] + [% all_visible_flag_types.${flag_type.id} = flag_type %] + [% ',' UNLESS first %][% flag_type.id FILTER json %][% first = 0 %] + [% END %]], +[% END %] + "description": "[% component.description FILTER json %]" + } [% ',' UNLESS loop.last() %] + [% END %] + }, + "version": [ + [% FOREACH version = product.versions %] + "[% version.name FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], + +[% IF Param('usetargetmilestone') %] + "default_target_milestone": "[% product.defaultmilestone FILTER json %]", + "target_milestone": [ + [% FOREACH milestone = product.milestones %] + "[% milestone.name FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], +[% END %] + + "group": [ + [% FOREACH group = product.groups_valid %] + [% group.id %][% ',' UNLESS loop.last() %] + [% END %] + ] + }[% ',' UNLESS loop.last() %] + [% END %] + }, + + "group": { + [% FOREACH group = product.groups_valid %] + "[% group.id %]": { + "name": "[% group.name FILTER json %]", + "description": "[% group.description FILTER json %]", + "is_accepting_bugs": [% group.is_bug_group ? 'true' : 'false' %], + "is_active": [% group.is_active ? 'true' : 'false' %] + }[% ',' UNLESS loop.last() %] + [% END %] + }, + +[% IF show_flags %] + "flag_type": { + [% FOREACH flag_type = all_visible_flag_types.values.sort('name') %] + "[%+ flag_type.id %]": { + "name": "[% flag_type.name FILTER json %]", + "description": "[% flag_type.description FILTER json %]", + [% IF user.in_group("editcomponents") %] + [% IF flag_type.request_group_id %] + "request_group": [% flag_type.request_group_id %], + [% END %] + [% IF flag_type.grant_group_id %] + "grant_group": [% flag_type.grant_group_id %], + [% END %] + [% END %] + "is_for_bugs": [% flag_type.target_type == "bug" ? 'true' : 'false' %], + "is_requestable": [% flag_type.is_requestable ? 'true' : 'false' %], + "is_specifically_requestable": [% flag_type.is_requesteeble ? 'true' : 'false' %], + "is_multiplicable": [% flag_type.is_multiplicable ? 'true' : 'false' %] + }[% ',' UNLESS loop.last() %] + [% END %] + }, +[% END %] + + [% PROCESS "global/field-descs.none.tmpl" %] + + [%# Put custom field value data where below loop expects to find it %] + [% FOREACH cf = custom_fields %] + [% ${cf.name} = [] %] + [% FOREACH value = cf.legal_values %] + [% ${cf.name}.push(value.name) %] + [% END %] + [% END %] + + [%# Built-in fields do not have type IDs. There aren't ID values for all + # the types of the built-in fields, but we do what we can, and leave the + # rest as "0" (unknown). + #%] + [% type_id_for = { + "id" => 6, + "summary" => 1, + "classification" => 2, + "version" => 2, + "url" => 1, + "whiteboard" => 1, + "keywords" => 3, + "component" => 2, + "attachment.description" => 1, + "attachment.file_name" => 1, + "attachment.content_type" => 1, + "target_milestone" => 2, + "comment" => 4, + "alias" => 1, + "deadline" => 5, + } %] + + "field": { + [% FOREACH item = field %] + [% newname = OLD2NEW.${item.name} || item.name %] + "[% newname FILTER json %]": { + "description": "[% (field_descs.${item.name} OR + item.description) FILTER json %]", + "is_active": [% field.obsolete ? "false" : "true" %], + [% blacklist = ["version", "group", "product", "component"] %] + [% IF ${newname} AND NOT blacklist.contains(newname) %] + "values": [ + [% FOREACH value = ${newname} %] + "[% value FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], + [% END %] + [% paramname = newname.replace("_", "") %] [%# For op_sys... %] + [% IF paramname != "query" AND Param('default' _ paramname) %] + "default": "[% Param('default' _ paramname) %]", + [% END %] + [%-# The 'status' hash has a lot of extra stuff %] + [% IF newname == "status" %] + "open": [ + [% FOREACH value = open_status %] + "[% value FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], + "closed": [ + [% FOREACH value = closed_status %] + "[% value FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], + "transitions": { + "{Start}": [ + [% FOREACH target = initial_status %] + "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ], + [% FOREACH status = status_objects %] + [% targets = status.can_change_to() %] + "[% status.name FILTER json %]": [ + [% FOREACH target = targets %] + "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %] + [% END %] + ][% ',' UNLESS loop.last() %] + [% END %] + }, + [% END %] + [% IF newname.match("^cf_") %] + "is_on_bug_entry": [% item.enter_bug ? 'true' : 'false' %], + [% END %] + "type": [% item.type || type_id_for.$newname || 0 %] + }[% ',' UNLESS loop.last() %] + [% END %] + } +} diff --git a/extensions/ComponentWatching/Config.pm b/extensions/ComponentWatching/Config.pm new file mode 100644 index 000000000..534afaa61 --- /dev/null +++ b/extensions/ComponentWatching/Config.pm @@ -0,0 +1,26 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Component Watching Extension +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::ComponentWatching; +use strict; +use constant NAME => 'ComponentWatching'; + +__PACKAGE__->NAME; diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm new file mode 100644 index 000000000..d73ce47cd --- /dev/null +++ b/extensions/ComponentWatching/Extension.pm @@ -0,0 +1,343 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Component Watching Extension +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::ComponentWatching; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Group; +use Bugzilla::User; +use Bugzilla::User::Setting; + +our $VERSION = '1.1'; + +use constant REL_COMPONENT_WATCHER => 15; + +# +# installation +# + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'component_watch'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => { + TABLE => 'components', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => { + TABLE => 'products', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + ], + }; +} + +# +# templates +# + +sub template_before_create { + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{CONSTANTS}; + $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; +} + +# +# preferences +# + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'component_watch'; + + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $user = Bugzilla->user; + + if ($save) { + my ($sth, $sthAdd, $sthDel); + + if (Bugzilla->input_params->{'add'}) { + # add watch + + my $productName = Bugzilla->input_params->{'add_product'}; + my $ra_componentNames = Bugzilla->input_params->{'add_component'}; + $ra_componentNames = [$ra_componentNames] unless ref($ra_componentNames); + + # load product and verify access + my $product = Bugzilla::Product->new({ name => $productName }); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', { product => $productName }); + } + + if (grep { $_ eq '' } @$ra_componentNames) { + # watching a product + _addProductWatch($user, $product); + + } else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component = Bugzilla::Component->new({ name => $componentName, product => $product }); + unless ($component) { + ThrowUserError('product_access_denied', { product => $productName }); + } + _addComponentWatch($user, $component); + } + } + + _addDefaultSettings($user); + + } else { + # remove watch(s) + + foreach my $name (keys %{Bugzilla->input_params}) { + if ($name =~ /^del_(\d+)$/) { + _deleteProductWatch($user, $1); + } elsif ($name =~ /^del_(\d+)_(\d+)$/) { + _deleteComponentWatch($user, $1, $2); + } + } + } + } + + $args->{'vars'}->{'watches'} = _getWatches($user); + + $$handled = 1; +} + +# +# bugmail +# + +sub bugmail_recipients { + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); + my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id); + + # notify when the product/component is switch from one being watched + if (@$diffs) { + # we need the product to process the component, so scan for that first + my $product; + foreach my $ra (@$diffs) { + my (undef, undef, undef, undef, $old, $new, undef, $field) = @$ra; + if ($field eq 'product') { + $product = Bugzilla::Product->new({ name => $old }); + $oldProductId = $product->id; + } + } + if (!$product) { + $product = Bugzilla::Product->new($oldProductId); + } + foreach my $ra (@$diffs) { + my (undef, undef, undef, undef, $old, $new, undef, $field) = @$ra; + if ($field eq 'component') { + my $component = Bugzilla::Component->new({ name => $old, product => $product }); + $oldComponentId = $component->id; + } + } + } + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT user_id + FROM component_watch + WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL) + OR (component_id = ? OR component_id = ?) + "); + $sth->execute($oldProductId, $newProductId, $oldComponentId, $newComponentId); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); + } + } +} + +sub bugmail_relationships { + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; +} + +# +# db +# + +sub _getWatches { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT product_id, component_id + FROM component_watch + WHERE user_id = ? + "); + $sth->execute($user->id); + my @watches; + while (my ($productId, $componentId) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new($productId); + next unless $product && $user->can_access_product($product); + + my %watch = ( product => $product ); + if ($componentId) { + my $component = Bugzilla::Component->new($componentId); + next unless $component; + $watch{'component'} = $component; + } + + push @watches, \%watch; + } + + @watches = sort { + $a->{'product'}->name cmp $b->{'product'}->name + || $a->{'component'}->name cmp $b->{'component'}->name + } @watches; + + return \@watches; +} + +sub _addProductWatch { + my ($user, $product) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id IS NULL + "); + $sth->execute($user->id, $product->id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? + "); + $sth->execute($user->id, $product->id); + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id) + VALUES (?, ?) + "); + $sth->execute($user->id, $product->id); +} + +sub _addComponentWatch { + my ($user, $component) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM component_watch + WHERE user_id = ? + AND (component_id = ? OR (product_id = ? AND component_id IS NULL)) + "); + $sth->execute($user->id, $component->id, $component->product_id); + return if $sth->fetchrow_array; + + $sth = $dbh->prepare(" + INSERT INTO component_watch(user_id, product_id, component_id) + VALUES (?, ?, ?) + "); + $sth->execute($user->id, $component->product_id, $component->id); +} + +sub _deleteProductWatch { + my ($user, $productId) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id IS NULL + "); + $sth->execute($user->id, $productId); +} + +sub _deleteComponentWatch { + my ($user, $productId, $componentId) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + DELETE FROM component_watch + WHERE user_id = ? AND product_id = ? AND component_id = ? + "); + $sth->execute($user->id, $productId, $componentId); +} + +sub _addDefaultSettings { + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + my $sth = $dbh->prepare(" + SELECT 1 + FROM email_setting + WHERE user_id = ? AND relationship = ? + "); + $sth->execute($user->id, REL_COMPONENT_WATCHER); + return if $sth->fetchrow_array; + + my @defaultEvents = ( + EVT_OTHER, + EVT_COMMENT, + EVT_ATTACHMENT, + EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, + EVT_OPENED_CLOSED, + EVT_KEYWORD, + EVT_DEPEND_BLOCK, + EVT_BUG_CREATED, + ); + foreach my $event (@defaultEvents) { + $dbh->do( + "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, + $user->id, REL_COMPONENT_WATCHER, $event + ); + } +} + +__PACKAGE__->NAME; diff --git a/extensions/ComponentWatching/reset-watch-preferences.pl b/extensions/ComponentWatching/reset-watch-preferences.pl new file mode 100755 index 000000000..f190ebd48 --- /dev/null +++ b/extensions/ComponentWatching/reset-watch-preferences.pl @@ -0,0 +1,75 @@ +#!/usr/bin/perl + +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Component Watching Extension +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +use strict; +use warnings; + +use lib '.'; +$| = 1; + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Install::Util qw(indicate_progress); + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +my @DEFAULT_EVENTS = qw(0 2 3 4 5 6 7 9 10 50); +my $REL_COMP_WATCH = 15; + +print "This script resets the component watching preferences back to\n"; +print "default values. It is required to be run when upgrading from\n"; +print "version 1.0 to 1.1\n"; +print "Press <ENTER> to start, or CTRL+C to cancel... "; +getc(); +print "\n"; + +my $dbh = Bugzilla->dbh; + +$dbh->bz_start_transaction(); + +my @users; +my $ra_user_ids = $dbh->selectcol_arrayref( + "SELECT DISTINCT user_id FROM component_watch" +); + +my $total = scalar @$ra_user_ids; +my $count = 0; +foreach my $user_id (@$ra_user_ids) { + indicate_progress({ current => $count++, total => $total }) if $total > 10; + $dbh->do( + "DELETE FROM email_setting WHERE user_id=? AND relationship=?", + undef, + $user_id, $REL_COMP_WATCH + ); + foreach my $event (@DEFAULT_EVENTS) { + $dbh->do( + "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, + $user_id, $REL_COMP_WATCH, $event + ); + } +} + +$dbh->bz_commit_transaction(); + +print "Done.\n"; diff --git a/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl new file mode 100644 index 000000000..057a32e36 --- /dev/null +++ b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl @@ -0,0 +1,148 @@ +[%# + # 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 Component Watching Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[%# initialise product to component mapping #%] + +[% SET selectable_products = user.get_selectable_products %] + +<script> +var Dom = YAHOO.util.Dom; +var useclassification = false; +var first_load = true; +var last_sel = []; +var cpts = new Array(); +[% n = 0 %] +[% FOREACH prod = selectable_products %] + cpts['[% n %]'] = [ + [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ]; + [% n = n+1 %] +[% END %] +</script> +<script type="text/javascript" + src="[% 'js/productform.js' FILTER mtime FILTER html %]"> +</script> + +<script> +function onSelectProduct() { + var component = Dom.get('component'); + selectProduct(Dom.get('product'), component); + // selectProduct only supports __Any__ on both elements + // we only want it on component, so add it back in + try { + component.add(new Option('__Any__', ''), component.options[0]); + } catch(e) { + // support IE + component.add(new Option('__Any__', ''), 0); + } +} + +YAHOO.util.Event.onDOMReady(onSelectProduct); +</script> + +<p> +Select the components you want to watch. To watch all components in a product, +watch "__Any__". +</p> + +<table border="0" cellpadding="3" cellspacing="0"> +<tr> + <td align="right">Product</td> + <td> + <select name="add_product" id="product" onChange="onSelectProduct()"> + [% FOREACH product IN selectable_products %] + <option>[% product.name FILTER html %]</option> + [% END %] + </select> + </td> +</tr> +<tr> + <td align="right" valign="top">Component</td> + <td> + <select name="add_component" id="component" multiple size="5"> + <option value="">__Any__</option> + [% FOREACH product IN selectable_products %] + [% FOREACH component IN product.components %] + <option>[% component.name FILTER html %]</option> + [% END %] + [% END %] + </select> + </td> +</tr> +<tr> + <td> </td> + <td><input type="submit" id="add" name="add" value="Add"></td> +</tr> +</table> + +<p> +Use <a href="userprefs.cgi?tab=email">Email Preferences</a> to filter which +notification emails you receive. +</p> + +<hr> +<p> +You are currently watching: +</p> + +[% IF watches.size %] + +<table border="0" cellpadding="5" cellspacing="0"> +<tr> + <td> </td> + <td><b>Product</b></td> + <td><b>Component</b></td> +</tr> +[% FOREACH watch IN watches %] + <tr> + [% IF (watch.component) %] + <td><input type="checkbox" name="del_[% watch.product.id %]_[% watch.component.id %]" value="1"></td> + <td>[% watch.component.product.name FILTER html %]</td> + <td> + <a href="buglist.cgi?product=[% watch.product.name FILTER url ~%] + &component=[% watch.component.name FILTER url %]&resolution=---"> + [% watch.component.name FILTER html %] + </a> + </td> + [% ELSE %] + <td><input type="checkbox" name="del_[% watch.product.id %]" value="1"></td> + <td>[% watch.product.name FILTER html %]</td> + <td> + <a href="describecomponents.cgi?product=[% watch.product.name FILTER url %]"> + __Any__ + </a> + </td> + [% END %] + </tr> +[% END %] +</table> + +<p> +Select the items you want to stop watching. +</p> + +[% ELSE %] + +<p> +<i>You are not watching any components.</i> +</p> + +[% END %] + diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl new file mode 100644 index 000000000..cab637967 --- /dev/null +++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl @@ -0,0 +1,22 @@ +[%# 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 Component Watching Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% relationships.push({ id = constants.REL_COMPONENT_WATCHER, description = "Component" }) %] +[% no_added_removed.push(constants.REL_COMPONENT_WATCHER) %] diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl new file mode 100644 index 000000000..7a615a8ac --- /dev/null +++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl @@ -0,0 +1,27 @@ +[%# + # 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 Component Watching Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% tabs = tabs.import([{ + name => "component_watch", + label => "Component Watching", + link => "userprefs.cgi?tab=component_watch", + saveable => 1 + }]) %] diff --git a/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl new file mode 100644 index 000000000..2621f85e4 --- /dev/null +++ b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl @@ -0,0 +1,22 @@ +[%# 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 Component Watching Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% watch_reason_descs.${constants.REL_COMPONENT_WATCHER} = + "You are watching the component for the ${terms.bug}." %] diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm index 4498d2b22..ef20a28f0 100644 --- a/extensions/Example/Extension.pm +++ b/extensions/Example/Extension.pm @@ -211,12 +211,15 @@ sub search_operator_field_override { sub _component_nonchanged { my $original = shift; - my ($invocant, $args) = @_; + my $invocant = shift; + + my %func_args = @_; + $invocant->$original(%func_args); - $invocant->$original($args); # Actually, it does not change anything in the result, # just an example. - $args->{term} = $args->{term} . " OR 1=2"; + my ($term) = @func_args{qw(term)}; + $$term = $$term . " OR 1=2"; } sub bugmail_recipients { @@ -261,14 +264,9 @@ sub config_modify_panels { my $auth_params = $panels->{'auth'}->{params}; my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params); my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params); - + push(@{ $info_class->{choices} }, 'CGI,Example'); push(@{ $verify_class->{choices} }, 'Example'); - - push(@$auth_params, { name => 'param_example', - type => 't', - default => 0, - checker => \&check_numeric }); } sub db_schema_abstract_schema { @@ -456,26 +454,6 @@ sub install_update_db { # $dbh->bz_add_index('example', 'example_new_column_idx', [qw(value)]); } -sub install_update_db_fielddefs { - my $dbh = Bugzilla->dbh; -# $dbh->bz_add_column('fielddefs', 'example_column', -# {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => ''}); -} - -sub job_map { - my ($self, $args) = @_; - - my $job_map = $args->{job_map}; - - # This adds the named class (an instance of TheSchwartz::Worker) as a - # handler for when a job is added with the name "some_task". - $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass'; - - # Schedule a job like this: - # my $queue = Bugzilla->job_queue(); - # $queue->insert('some_task', { some_parameter => $some_variable }); -} - sub mailer_before_send { my ($self, $args) = @_; diff --git a/extensions/Example/lib/Config.pm b/extensions/Example/lib/Config.pm index 75db22957..a126e82df 100644 --- a/extensions/Example/lib/Config.pm +++ b/extensions/Example/lib/Config.pm @@ -25,8 +25,6 @@ use warnings; use Bugzilla::Config::Common; -our $sortkey = 5000; - sub get_param_list { my ($class) = @_; diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm new file mode 100644 index 000000000..924fe568c --- /dev/null +++ b/extensions/GuidedBugEntry/Config.pm @@ -0,0 +1,33 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 GuidedBugEntry Bugzilla Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <glob@mozilla.com> + +package Bugzilla::Extension::GuidedBugEntry; +use strict; + +use constant NAME => 'GuidedBugEntry'; + +use constant REQUIRED_MODULES => [ +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/extensions/GuidedBugEntry/Extension.pm b/extensions/GuidedBugEntry/Extension.pm new file mode 100644 index 000000000..977aecfb6 --- /dev/null +++ b/extensions/GuidedBugEntry/Extension.pm @@ -0,0 +1,123 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 GuidedBugEntry Bugzilla Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <glob@mozilla.com> + +package Bugzilla::Extension::GuidedBugEntry; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Token; +use Bugzilla::Error; +use Bugzilla::Status; +use Bugzilla::Util 'url_quote'; +use Bugzilla::UserAgent; + +our $VERSION = '1'; + +sub enter_bug_start { + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + + # hack for skipping old guided code when enabled + $vars->{'disable_guided'} = 1; + + # force guided format for new users + my $format = $cgi->param('format') || ''; + if ( + $format eq 'guided' || + ( + $format eq '' && + !$user->in_group('canconfirm') + ) + ) { + # skip the first step if a product is provided + if ($cgi->param('product')) { + print $cgi->redirect('enter_bug.cgi?format=guided#h=dupes|' . + url_quote($cgi->param('product'))); + exit; + } + + $self->_init_vars($vars); + print $cgi->header(); + $template->process('guided/guided.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + + # we use the __default__ format to bypass the guided entry + # it isn't understood upstream, so remove it once a product + # has been selected. + $cgi->delete('format') + if $cgi->param('format') + && $cgi->param('format') eq "__default__" + && $cgi->param('product') ne ''; +} + +sub _init_vars { + my ($self, $vars) = @_; + my $user = Bugzilla->user; + + my @enterable_products = @{$user->get_enterable_products}; + ThrowUserError('no_products') unless scalar(@enterable_products); + + my @classifications = ({object => undef, products => \@enterable_products}); + + my $class; + foreach my $product (@enterable_products) { + $class->{$product->classification_id}->{'object'} ||= + new Bugzilla::Classification($product->classification_id); + push(@{$class->{$product->classification_id}->{'products'}}, $product); + } + @classifications = + sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class); + $vars->{'classifications'} = \@classifications; + + my @open_states = BUG_STATE_OPEN(); + $vars->{'open_states'} = \@open_states; + + $vars->{'token'} = issue_session_token('createbug:'); + + $vars->{'platform'} = detect_platform(); + $vars->{'op_sys'} = detect_op_sys(); +} + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + return unless $page eq 'guided_products.js'; + + # import product -> security group mappings from the BMO ext + + our %product_sec_groups; + eval q#use Bugzilla::Extension::BMO::Data '%product_sec_groups'#; + return if $@; + + $vars->{'products'} = \%product_sec_groups; +} + +__PACKAGE__->NAME; diff --git a/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl new file mode 100644 index 000000000..6b0de9466 --- /dev/null +++ b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl @@ -0,0 +1,25 @@ +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +User Agent: [% cgi.param('user_agent') %] +[% IF cgi.param('build_id') %] +Build ID: [% cgi.param('build_id') %][% END %] + +[% IF cgi.param('bug_steps') %] +Steps to reproduce: + +[%+ cgi.param('bug_steps') %] +[% END %] + +[% IF cgi.param('actual') %] + +Actual results: + +[%+ cgi.param('actual') %] +[% END %] + +[% IF cgi.param('expected') %] + +Expected results: + +[%+ cgi.param('expected') %] +[% END %] diff --git a/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl new file mode 100644 index 000000000..8f8817344 --- /dev/null +++ b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl @@ -0,0 +1,534 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 GuidedBugEntry Bugzilla Extension. + # + # The Initial Developer of the Original Code is + # the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** */ + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Enter A Bug" + javascript_urls = [ + 'extensions/GuidedBugEntry/web/js/products.js', + 'extensions/GuidedBugEntry/web/js/guided.js', + 'js/field.js', 'js/TUI.js', 'js/bug.js' ] + style_urls = [ 'extensions/GuidedBugEntry/web/style/guided.css', + 'js/yui/assets/skins/sam/container.css' ] + yui = [ 'history', 'datatable', 'container' ] +%] + +<iframe id="yui-history-iframe" src="extensions/GuidedBugEntry/web/yui-history-iframe.txt"></iframe> +<input id="yui-history-field" type="hidden"> + +<noscript> +You require JavaScript to use this bug entry form.<br><br> +Please use the <a href="enter_bug.cgi?format=__default__">advanced bug entry form</a>. +</noscript> + +<div id="loading" class="hidden"> +Please wait... +</div> +<script type="text/javascript"> +YAHOO.util.Dom.removeClass('loading', 'hidden'); +</script> + +<div id="steps"> +[% INCLUDE product_step %] +[% INCLUDE otherProducts_step %] +[% INCLUDE dupes_step %] +[% INCLUDE bugForm_step %] +</div> + +<div id="advanced"> + <a id="advanced_img" href="enter_bug.cgi?format=__default__"><img + src="extensions/GuidedBugEntry/web/images/advanced.png" width="16" height="16" border="0"></a> + <a id="advanced_link" href="enter_bug.cgi?format=__default__">Switch to the advanced bug entry form</a> +</div> + +<script type="text/javascript"> +YAHOO.util.Dom.addClass('loading', 'hidden'); +guided.init(); +guided.detectedPlatform = '[% platform FILTER js %]'; +guided.detectedOpSys = '[% op_sys FILTER js %]'; +guided.currentUser = '[% user.login FILTER js %]'; +guided.openStates = [ +[% FOREACH state = open_states %] + '[% state FILTER js%]' + [%- "," UNLESS loop.last %] +[% END %] +]; +dupes.setLabels( + { + id: "[% field_descs.bug_id FILTER js %]", + summary: "[% field_descs.short_desc FILTER js %]", + status: "[% field_descs.bug_status FILTER js %]", + } +); +</script> +<script type="text/javascript" src="page.cgi?id=guided_products.js"></script> +[% PROCESS global/footer.html.tmpl %] + +[%############################################################################%] +[%# page title #%] +[%############################################################################%] + +[% BLOCK page_title %] + <div id="page_title"> + <h2>Enter A Bug</h2> + <h3>Step [% step_number FILTER html %] of 3</h3> + </div> +[% END %] + +[%############################################################################%] +[%# product step #%] +[%############################################################################%] + +[% BLOCK product_step %] +<div id="product_step" class="step hidden"> + +[% INCLUDE page_title + step_number = "1" +%] + +[% INCLUDE exits + show = "all" +%] + +<table id="products"> +[% INCLUDE 'guided/products.html.tmpl' %] +[% INCLUDE product_block + name="Other Products" + icon="other.png" + desc="Other Mozilla products which aren't listed here" + onclick="guided.setStep('otherProducts')" +%] +</table> + +</div> +[% END %] + +[% BLOCK product_block %] + [% IF !caption %] + [% caption = name %] + [% END %] + [% IF !desc %] + [% FOREACH c = classifications %] + [% FOREACH p = c.products %] + [% IF p.name == name %] + [% desc = p.description %] + [% LAST %] + [% END %] + [% END %] + [% END %] + [% END %] + <tr> + <td class="product_img"> + <a href="javascript:void(0)" + [% IF onclick %] + onclick="[% onclick %]" + [% ELSE %] + onclick="product.select('[% name FILTER js %]')" + [% END %] + ><img src="extensions/GuidedBugEntry/web/images/products/[% icon FILTER uri %]" width="64" height="64" + ></a> + </td> + <td> + <h2> + <a href="javascript:void(0)" + [% IF onclick %] + onclick="[% onclick %]" + [% ELSE %] + onclick="product.select('[% name FILTER js %]')" + [% END %] + >[% caption FILTER html %]</a> + </h2> + <p> + [% desc FILTER html_light %] + </p> + </td> + </tr> +[% END %] + +[%############################################################################%] +[%# other products step #%] +[%############################################################################%] + +[% BLOCK otherProducts_step %] +<div id="otherProducts_step" class="step hidden"> + +[% INCLUDE page_title + step_number = "1" +%] + +[% INCLUDE exits + show = "all" +%] + +<table id="other_products"> +[% FOREACH c = classifications %] + [% IF c.object %] + <tr class="classification"> + <th align="right" valign="top"> + [% c.object.name FILTER html %]: + </th> + <td> + [% c.object.description FILTER html_light %] + </td> + </tr> + [% END %] + [% FOREACH p = c.products %] + <tr> + <th align="right" valign="top"> + <a href="javascript:void(0)" onclick="product.select('[% p.name FILTER js %]')"> + [% p.name FILTER html FILTER no_break %]</a>: + </th> + + <td valign="top">[% p.description FILTER html_light %]</td> + </tr> + [% END %] + <tr> + <td> </td> + </tr> +[% END %] +</table> + +</div> +[% END %] + +[%############################################################################%] +[%# exits (support/input) #%] +[%############################################################################%] + +[% BLOCK exits %] +<table class="exits"> + <tr> + <td> + <div class="exit_img"> + <a href="http://www.mozilla.org/support/" + ><img src="extensions/GuidedBugEntry/web/images/support.png" width="32" height="32" + ></a> + </div> + </td> + <td width="100%"> + <h2> + <a href="http://www.mozilla.org/support/">I need technical support</a> + </h2> + For technical support or help getting your site to work with Mozilla. + </td> + </tr> + <tr> + <td> + <div class="exit_img"> + <a href="http://input.mozilla.org/idea/" + ><img src="extensions/GuidedBugEntry/web/images/idea.png" width="32" height="32" + ></a> + </div> + </td> + <td width="100%"> + <h2> + <a href="http://input.mozilla.org/idea/">I have an idea for firefox</a> + </h2> + For offering us ideas on how to enhance Firefox. + </td> + </tr> + <tr> + <td> + <div class="exit_img"> + <a href="http://input.mozilla.org/feedback/" + ><img src="extensions/GuidedBugEntry/web/images/input.png" width="32" height="32" + ></a> + </div> + </td> + <td width="100%"> + • <a href="http://input.mozilla.org/feedback/">Provide other feedback about Firefox</a><br> + • <a href="http://input.mozilla.org/feedback/#sad">Report an issue with a web site that I use</a><br> + • <a href="enter_bug.cgi?format=guided&product=Core">Report an issue with Firefox on a site that I've developed</a><br> + </td> + </tr> +</table> +<h3> + None of the above; my bug is in: +</h3> +[% END %] + +[% BLOCK exit_block %] + <tr> + <td> + <div class="exit_img"> + <a href="[% href %]" + ><img src="extensions/GuidedBugEntry/web/images/[% icon FILTER uri %]" width="32" height="32" + ></a> + </div> + </td> + <td width="100%"> + <h2> + <a href="[% href %]">[% name FILTER html %]</a> + </h2> + [% desc FILTER html %] + </td> + </tr> +[% END %] + +[%############################################################################%] +[%# duplicates step #%] +[%############################################################################%] + +[% BLOCK dupes_step %] +<div id="dupes_step" class="step hidden"> + +[% INCLUDE page_title + step_number = "2" +%] + +<p> +Product: <b><span id="dupes_product_name">?</span></b>: +(<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>) +</p> + +<table border="0" cellpadding="5" cellspacing="0" id="product_support" class="hidden"> +<tr> +<td> + <img src="extensions/GuidedBugEntry/web/images/message.png" width="24" height="24"> +</td> +<td id="product_support_message"> </td> +</table> + +<div id="dupe_form"> + <p> + Please summarise your issue or request in one sentence: + </p> + <input id="dupes_summary" value="Short summary of issue" spellcheck="true" placeholder="Short summary of issue"> + <button id="dupes_search">Find similar issues</button> + <button id="dupes_continue_button_top" onclick="guided.setStep('bugForm')">My issue is not listed</button> +</div> + +<div id="dupes_list"></div> +<div id="dupes_continue"> +<button id="dupes_continue_button_bottom" onclick="guided.setStep('bugForm')">My issue is not listed</button> +</div> + +</div> +[% END %] + +[%############################################################################%] +[%# bug form step #%] +[%############################################################################%] + +[% BLOCK bugForm_step %] +<div id="bugForm_step" class="step hidden"> + +[% INCLUDE page_title + step_number = "3" +%] + +<form method="post" action="post_bug.cgi" enctype="multipart/form-data" onsubmit="return bugForm.validate()"> +<input type="hidden" name="token" value="[% token FILTER html %]"> +<input type="hidden" name="product" id="product" value=""> +<input type="hidden" name="component" id="component" value=""> +<input type="hidden" name="bug_severity" value="normal"> +<input type="hidden" name="rep_platform" id="rep_platform" value="All"> +<input type="hidden" name="priority" value="--"> +<input type="hidden" name="op_sys" id="op_sys" value="All"> +<input type="hidden" name="version" id="version" value=""> +<input type="hidden" name="comment" id="comment" value=""> +<input type="hidden" name="format" value="guided"> +<input type="hidden" name="user_agent" id="user_agent" value=""> +<input type="hidden" name="build_id" id="build_id" value=""> + +<ul> +<li>Please fill out this form clearly, precisely and in as much detail as you can manage.</li> +<li>Please report only a single problem at a time.</li> +<li><a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">These guidelines</a> +explain how to write effective bug reports.</li> +</ul> + +<table id="bugForm" cellspacing="0"> + +<tr class="odd"> + <td class="label">Summary:</td> + <td width="100%" colspan="2"> + <input name="short_desc" id="short_desc" class="textInput" spellcheck="true"> + </td> + <td valign="top"> + [% PROCESS help id="summary_help" %] + <div id="summary_help" class="hidden help"> + A sentence which summarises the problem. Please be descriptive and use lots of keywords.<br> + <br> + <span class="help-bad">Bad example</span>: mail crashed<br> + <span class="help-good">Good example</span>: crash if I close the mail window while checking for new POP mail + </div> + </td> +</tr> + +<tr class="even"> + <td class="label">Product:</td> + <td id="productTD"> + <span id="product_label"></span> + (<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>) + </td> + <td id="versionTD" class="hidden"> + <span class="label">Version: + <select id="version_select" onchange="bugForm.onVersionChange(this.value)"> + </select> + </td> + <td valign="top"> + [% PROCESS help id="product_help" %] + <div id="product_help" class="hidden help"> + The Product and Version you are reporting the issue with. + </div> +</tr> + +<tr class="odd"> + <td valign="top"> + <div class="label"> + Component: + </div> + (<a id="list_comp" href="describecomponents.cgi" target="_blank" + title="Show a list of all components and descriptions (in a new window)." + >List</a>) + </td> + <td valign="top" colspan="2"> + <select id="component_select" onchange="bugForm.onComponentChange(this.value)" class="mandatory"> + </select> + <div id="component_description"></div> + </td> + <td valign="top"> + [% PROCESS help id="component_help" %] + <div id="component_help" class="hidden help"> + The area where the problem occurs.<br> + <br> + If you are unsure which component to use, select a 'General' component. + </div> +</tr> + +<tr class="even"> + <td class="label" colspan="3">What did you do?</td> + <td valign="top"> + [% PROCESS help id="steps_help" %] + <div id="steps_help" class="hidden help"> + Please be as specific as possible about what what you did + to cause the problem. Providing step-by-step instructions + would be ideal.<br> + <br> + Include any relevant URLs and special setup steps.<br> + <br> + <span class="help-bad">Bad example</span>: Mozilla crashed. You suck!<br> + <span class="help-good">Good example</span>: After a crash which happened + when I was sorting in the Bookmark Manager, all of my top-level bookmark + folders beginning with the letters Q to Z are no longer present. + </div> + </td> +</tr> +<tr class="even"> + <td colspan="3"><textarea id="bug_steps" name="bug_steps" rows="5"></textarea></td> + <td> </td> +</tr> + +<tr class="odd"> + <td class="label" colspan="3">What happened?</td> + <td valign="top"> + [% PROCESS help id="actual_help" %] + <div id="actual_help" class="hidden help"> + What happened after you performed the steps above? + </div> +</tr> +<tr class="odd"> + <td colspan="3"><textarea id="actual" name="actual" rows="5"></textarea></td> + <td> </td> +</tr> + +<tr class="even"> + <td class="label" colspan="3">What should have happened?</td> + <td valign="top"> + [% PROCESS help id="expected_help" %] + <div id="expected_help" class="hidden help"> + What should the software have done instead? + </div> +</tr> +<tr class="even"> + <td colspan="3"><textarea id="expected" name="expected" rows="5"></textarea></td> + <td> </td> +</tr> + +<tr class="odd"> + <td class="label">Attach a file:</td> + <td colspan="2"> + <input type="file" name="data" id="data" size="50" onchange="bugForm.onFileChange()"> + <input type="hidden" name="contenttypemethod" value="autodetect"> + <button id="reset_data" onclick="return bugForm.onFileClear()" disabled>Clear</button> + </td> + <td valign="top"> + [% PROCESS help id="file_help" %] + <div id="file_help" class="hidden help"> + If a file helps explain the issue better, such as a screenshot, please + attach one here. + </div> + </td> +</tr> +<tr class="odd"> + <td class="label">File Description:</td> + <td colspan="2"><input type="text" name="description" id="data_description" class="textInput" disabled></td> + <td> </td> +</tr> + +<tr class="even"> + <td class="label">Security:</td> + <td colspan="2"> + <table border="0" cellpadding="0" cellspacing="0"> + <tr> + <td> + <input type="checkbox" name="groups" value="core-security" id="groups"> + </td> + <td> + <label for="groups">Many users could be harmed by this security problem: + it should be kept hidden from the public until it is resolved.</label> + </td> + </tr> + </table> + </td> + <td> </td> +</tr> + +<tr class="odd"> + <td> </td> + <td colspan="2" id="submitTD"> + <input type="submit" id="submit" value="Submit Bug"> + </td> +</tr> + +</table> + +</form> + +</div> +[% END %] + +[%############################################################################%] +[%# help block #%] +[%############################################################################%] + +[% BLOCK help %] +<img src="extensions/GuidedBugEntry/web/images/help.png" width="16" height="16" class="help_image" + helpid="[% id FILTER html %]" onMouseOver="bugForm.showHelp(this)" onMouseOut="bugForm.hideHelp(this)" + > +[% END %] diff --git a/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl new file mode 100644 index 000000000..23d73d61f --- /dev/null +++ b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl @@ -0,0 +1,63 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 GuidedBugEntry Bugzilla Extension. + # + # The Initial Developer of the Original Code is + # the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** */ + #%] + +[% INCLUDE product_block + name="Firefox" + icon="firefox.png" +%] +[% INCLUDE product_block + name="Fennec" + caption="Firefox for Mobile (Fennec)" + icon="fennec.png" +%] +[% INCLUDE product_block + name="Thunderbird" + icon="thunderbird.png" +%] +[% INCLUDE product_block + name="Mozilla Services" + icon="dino.png" +%] +[% INCLUDE product_block + name="SeaMonkey" + icon="seamonkey.png" +%] +[% INCLUDE product_block + name="Mozilla Localizations" + icon="dino.png" +%] +[% INCLUDE product_block + name="Mozilla Labs" + icon="labs.png" +%] +[% INCLUDE product_block + name="Calendar" + icon="sunbird.png" +%] +[% INCLUDE product_block + name="Core" + icon="core.png" +%] diff --git a/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl new file mode 100644 index 000000000..e8697a5a1 --- /dev/null +++ b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl @@ -0,0 +1,36 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 GuidedBugEntry Bugzilla Extension. + # + # The Initial Developer of the Original Code is + # the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 + # the Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** */ + #%] + +[%# this file allows us to pull in data defined in the BMO ext %] + +[% IF products %] + [% FOREACH product = products %] + if (!products['[% product.key FILTER js %]']) + products['[% product.key FILTER js %]'] = {}; + products['[% product.key FILTER js %]'].secgroup = '[% product.value FILTER js %]'; + [% END %] +[% END %] + diff --git a/extensions/GuidedBugEntry/web/images/advanced.png b/extensions/GuidedBugEntry/web/images/advanced.png Binary files differnew file mode 100644 index 000000000..71a3fcb78 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/advanced.png diff --git a/extensions/GuidedBugEntry/web/images/help.png b/extensions/GuidedBugEntry/web/images/help.png Binary files differnew file mode 100644 index 000000000..5c870176d --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/help.png diff --git a/extensions/GuidedBugEntry/web/images/idea.png b/extensions/GuidedBugEntry/web/images/idea.png Binary files differnew file mode 100644 index 000000000..0a0ce6c79 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/idea.png diff --git a/extensions/GuidedBugEntry/web/images/input.png b/extensions/GuidedBugEntry/web/images/input.png Binary files differnew file mode 100644 index 000000000..34c10e989 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/input.png diff --git a/extensions/GuidedBugEntry/web/images/message.png b/extensions/GuidedBugEntry/web/images/message.png Binary files differnew file mode 100644 index 000000000..55b6add19 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/message.png diff --git a/extensions/GuidedBugEntry/web/images/products/camino.png b/extensions/GuidedBugEntry/web/images/products/camino.png Binary files differnew file mode 100644 index 000000000..c833b4d04 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/camino.png diff --git a/extensions/GuidedBugEntry/web/images/products/core.png b/extensions/GuidedBugEntry/web/images/products/core.png Binary files differnew file mode 100644 index 000000000..b9c5053f6 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/core.png diff --git a/extensions/GuidedBugEntry/web/images/products/dino.png b/extensions/GuidedBugEntry/web/images/products/dino.png Binary files differnew file mode 100644 index 000000000..9e0470a07 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/dino.png diff --git a/extensions/GuidedBugEntry/web/images/products/fennec.png b/extensions/GuidedBugEntry/web/images/products/fennec.png Binary files differnew file mode 100644 index 000000000..ebad7e358 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/fennec.png diff --git a/extensions/GuidedBugEntry/web/images/products/firefox.png b/extensions/GuidedBugEntry/web/images/products/firefox.png Binary files differnew file mode 100644 index 000000000..582a6952a --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/firefox.png diff --git a/extensions/GuidedBugEntry/web/images/products/labs.png b/extensions/GuidedBugEntry/web/images/products/labs.png Binary files differnew file mode 100644 index 000000000..346e0ef06 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/labs.png diff --git a/extensions/GuidedBugEntry/web/images/products/mozilla.png b/extensions/GuidedBugEntry/web/images/products/mozilla.png Binary files differnew file mode 100644 index 000000000..e506328bc --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/mozilla.png diff --git a/extensions/GuidedBugEntry/web/images/products/other.png b/extensions/GuidedBugEntry/web/images/products/other.png Binary files differnew file mode 100644 index 000000000..e436c22ae --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/other.png diff --git a/extensions/GuidedBugEntry/web/images/products/seamonkey.png b/extensions/GuidedBugEntry/web/images/products/seamonkey.png Binary files differnew file mode 100644 index 000000000..fcb261ae1 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/seamonkey.png diff --git a/extensions/GuidedBugEntry/web/images/products/sunbird.png b/extensions/GuidedBugEntry/web/images/products/sunbird.png Binary files differnew file mode 100644 index 000000000..6b15c257d --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/sunbird.png diff --git a/extensions/GuidedBugEntry/web/images/products/thunderbird.png b/extensions/GuidedBugEntry/web/images/products/thunderbird.png Binary files differnew file mode 100644 index 000000000..f3523183a --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/products/thunderbird.png diff --git a/extensions/GuidedBugEntry/web/images/sumo.png b/extensions/GuidedBugEntry/web/images/sumo.png Binary files differnew file mode 100644 index 000000000..d5773647c --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/sumo.png diff --git a/extensions/GuidedBugEntry/web/images/support.png b/extensions/GuidedBugEntry/web/images/support.png Binary files differnew file mode 100644 index 000000000..2320ea74a --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/support.png diff --git a/extensions/GuidedBugEntry/web/images/throbber.gif b/extensions/GuidedBugEntry/web/images/throbber.gif Binary files differnew file mode 100644 index 000000000..bc4fa6561 --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/throbber.gif diff --git a/extensions/GuidedBugEntry/web/images/warning.png b/extensions/GuidedBugEntry/web/images/warning.png Binary files differnew file mode 100644 index 000000000..86bed170d --- /dev/null +++ b/extensions/GuidedBugEntry/web/images/warning.png diff --git a/extensions/GuidedBugEntry/web/js/guided.js b/extensions/GuidedBugEntry/web/js/guided.js new file mode 100644 index 000000000..e3efd6446 --- /dev/null +++ b/extensions/GuidedBugEntry/web/js/guided.js @@ -0,0 +1,824 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 GuidedBugEntry Bugzilla Extension. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +// global + +var Dom = YAHOO.util.Dom; +var Event = YAHOO.util.Event; +var History = YAHOO.util.History; + +var guided = { + _currentStep: '', + detectedPlatform: '', + detectedOpSys: '', + currentUser: '', + openStates: [], + + setStep: function(newStep, noSetHistory) { + // initialise new step + eval(newStep + '.onShow()'); + + // change visibility of _step div + if (this._currentStep) + Dom.addClass(this._currentStep + '_step', 'hidden'); + this._currentStep = newStep; + Dom.removeClass(this._currentStep + '_step', 'hidden'); + + // scroll to top of page to mimic real navigation + scroll(0,0); + + // update history + if (History && !noSetHistory) { + History.navigate('h', newStep + "|" + product.getName()); + } + }, + + init: function() { + // init history manager + try { + History.register('h', History.getBookmarkedState('h') || 'product', + this._onStateChange); + History.initialize("yui-history-field", "yui-history-iframe"); + History.onReady(function () { + guided._onStateChange(History.getCurrentState('h'), true); + }); + } catch(err) { + History = false; + } + + // init steps + product.onInit(); + dupes.onInit(); + bugForm.onInit(); + }, + + _onStateChange: function(state, noSetHistory) { + state = state.split("|"); + product.setName(state[1] || ''); + guided.setStep(state[0], noSetHistory); + }, + + setAdvancedLink: function() { + href = 'enter_bug.cgi?format=__default__' + + '&product=' + escape(product.getName()) + + '&short_desc=' + escape(dupes.getSummary()); + Dom.get('advanced_img').href = href; + Dom.get('advanced_link').href = href; + } +}; + +// product step + +var product = { + details: false, + _counter: 0, + _loaded: '', + + onInit: function() { }, + + onShow: function() { + Dom.removeClass('advanced', 'hidden'); + }, + + select: function(productName) { + // called when a product is selected + this.setName(productName); + dupes.reset(); + guided.setStep('dupes'); + }, + + getName: function() { + return Dom.get('product').value; + }, + + _getNameAndRelated: function() { + var result = []; + + var name = this.getName(); + result.push(name); + + if (products[name] && products[name].related) { + for (var i = 0, n = products[name].related.length; i < n; i++) { + result.push(products[name].related[i]); + } + } + + return result; + }, + + setName: function(productName) { + if (productName == this.getName() && this.details) + return; + + // display the product name + Dom.get('product').value = productName; + Dom.get('product_label').innerHTML = YAHOO.lang.escapeHTML(productName); + Dom.get('dupes_product_name').innerHTML = YAHOO.lang.escapeHTML(productName); + Dom.get('list_comp').href = 'describecomponents.cgi?product=' + escape(productName); + guided.setAdvancedLink(); + + if (productName == '') { + Dom.addClass("product_support", "hidden"); + return; + } + + // use the correct security group + if (products[productName] && products[productName].secgroup) { + Dom.get('groups').value = products[productName].secgroup; + } else { + Dom.get('groups').value = products['_default'].secgroup; + } + + // use the correct platform & op_sys + if (products[productName] && products[productName].detectPlatform) { + Dom.get('rep_platform').value = guided.detectedPlatform; + Dom.get('op_sys').value = guided.detectedOpSys; + } else { + Dom.get('rep_platform').value = 'All'; + Dom.get('op_sys').value = 'All'; + } + + // show support message + if (products[productName] && products[productName].support) { + Dom.get("product_support_message").innerHTML = products[productName].support; + Dom.removeClass("product_support", "hidden"); + } else { + Dom.addClass("product_support", "hidden"); + } + + if (this._loaded == productName) + return; + + // grab the product information + this.details = false; + this._loaded = productName; + YAHOO.util.Connect.setDefaultPostHeader('text/plain; charset=UTF-8'); + YAHOO.util.Connect.asyncRequest( + 'POST', + 'jsonrpc.cgi', + { + success: function(res) { + try { + data = YAHOO.lang.JSON.parse(res.responseText); + if (data.error) + throw(data.error.message); + product.details = data.result.products[0]; + bugForm.onProductUpdated(); + } catch (err) { + product.details = false; + bugForm.onProductUpdated(); + if (err) { + alert('Failed to retreive components for product "' + + productName + '":' + "\n\n" + err); + if (console) + console.error(err); + } + } + }, + failure: function(res) { + this._loaded = ''; + product.details = false; + bugForm.onProductUpdated(); + if (res.responseText) { + alert('Failed to retreive components for product "' + + productName + '":' + "\n\n" + res.responseText); + if (console) + console.error(res); + } + } + }, + YAHOO.lang.JSON.stringify({ + version: "1.1", + method: "Product.get", + id: ++this._counter, + params: { + names: [productName], + exclude_fields: ['internals', 'milestones'] + } + } + ) + ); + } +}; + +// other products step + +var otherProducts = { + onInit: function() { }, + + onShow: function() { + Dom.removeClass('advanced', 'hidden'); + } +}; + +// duplicates step + +var dupes = { + _counter: 0, + _dataTable: null, + _dataTableColumns: null, + _elSummary: null, + _elSearch: null, + _elList: null, + _currentSearchQuery: '', + + onInit: function() { + this._elSummary = Dom.get('dupes_summary'); + this._elSearch = Dom.get('dupes_search'); + this._elList = Dom.get('dupes_list'); + + Event.onBlur(this._elSummary, this._onSummaryBlur); + Event.addListener(this._elSummary, 'input', this._onSummaryBlur); + Event.addListener(this._elSummary, 'keydown', this._onSummaryKeyDown); + Event.addListener(this._elSummary, 'keyup', this._onSummaryKeyUp); + Event.addListener(this._elSearch, 'click', this._doSearch); + }, + + setLabels: function(labels) { + this._dataTableColumns = [ + { key: "id", label: labels.id, formatter: this._formatId }, + { key: "summary", label: labels.summary, formatter: "text" }, + { key: "status", label: labels.status, formatter: this._formatStatus }, + { key: "update_token", label: '', formatter: this._formatCc } + ]; + }, + + _initDataTable: function() { + var dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi"); + dataSource.connTimeout = 15000; + dataSource.connMethodPost = true; + dataSource.connXhrMode = "cancelStaleRequests"; + dataSource.maxCacheEntries = 3; + dataSource.responseSchema = { + resultsList : "result.bugs", + metaFields : { error: "error", jsonRpcId: "id" } + }; + // DataSource can't understand a JSON-RPC error response, so + // we have to modify the result data if we get one. + dataSource.doBeforeParseData = + function(oRequest, oFullResponse, oCallback) { + if (oFullResponse.error) { + oFullResponse.result = {}; + oFullResponse.result.bugs = []; + if (console) + console.error("JSON-RPC error:", oFullResponse.error); + } + return oFullResponse; + }; + dataSource.subscribe('dataErrorEvent', + function() { + dupes._currentSearchQuery = ''; + } + ); + + this._dataTable = new YAHOO.widget.DataTable( + 'dupes_list', + this._dataTableColumns, + dataSource, + { + initialLoad: false, + MSG_EMPTY: 'No similar issues found.', + MSG_ERROR: 'An error occurred while searching for similar issues,' + + ' please try again.' + } + ); + }, + + _formatId: function(el, oRecord, oColumn, oData) { + el.innerHTML = '<a href="show_bug.cgi?id=' + oData + + '" target="_blank">' + oData + '</a>'; + }, + + _formatStatus: function(el, oRecord, oColumn, oData) { + var resolution = oRecord.getData('resolution'); + var bug_status = display_value('bug_status', oData); + if (resolution) { + el.innerHTML = bug_status + ' ' + + display_value('resolution', resolution); + } else { + el.innerHTML = bug_status; + } + }, + + _formatCc: function(el, oRecord, oColumn, oData) { + var cc = oRecord.getData('cc'); + var isCCed = false; + for (var i = 0, n = cc.length; i < n; i++) { + if (cc[i] == guided.currentUser) { + isCCed = true; + break; + } + } + dupes._buildCcHTML(el, oRecord.getData('id'), oRecord.getData('status'), + isCCed); + }, + + _buildCcHTML: function(el, id, bugStatus, isCCed) { + while (el.childNodes.length > 0) + el.removeChild(el.firstChild); + + var isOpen = false; + for (var i = 0, n = guided.openStates.length; i < n; i++) { + if (guided.openStates[i] == bugStatus) { + isOpen = true; + break; + } + } + + if (!isOpen && !isCCed) { + // you can't cc yourself to a closed bug here + return; + } + + var button = document.createElement('button'); + button.setAttribute('type', 'button'); + if (isCCed) { + button.innerHTML = 'Stop following'; + button.onclick = function() { + dupes.updateFollowing(el, id, bugStatus, button, false); return false; + }; + } else { + button.innerHTML = 'Follow bug'; + button.onclick = function() { + dupes.updateFollowing(el, id, bugStatus, button, true); return false; + }; + } + el.appendChild(button); + }, + + updateFollowing: function(el, bugID, bugStatus, button, follow) { + button.disabled = true; + button.innerHTML = 'Updating...'; + + var ccObject; + if (follow) { + ccObject = { add: [ guided.currentUser ] }; + } else { + ccObject = { remove: [ guided.currentUser ] }; + } + + YAHOO.util.Connect.setDefaultPostHeader('text/plain; charset=UTF-8'); + YAHOO.util.Connect.asyncRequest( + 'POST', + 'jsonrpc.cgi', + { + success: function(res) { + data = YAHOO.lang.JSON.parse(res.responseText); + if (data.error) + throw(data.error.message); + dupes._buildCcHTML(el, bugID, bugStatus, follow); + }, + failure: function(res) { + dupes._buildCcHTML(el, bugID, bugStatus, !follow); + if (res.responseText) + alert("Update failed:\n\n" + res.responseText); + } + }, + YAHOO.lang.JSON.stringify({ + version: "1.1", + method: "Bug.update", + id: ++this._counter, + params: { + ids: [ bugID ], + cc : ccObject + } + }) + ); + }, + + reset: function() { + this._elSummary.value = ''; + Dom.addClass(this._elList, 'hidden'); + Dom.addClass('dupes_continue', 'hidden'); + this._elList.innerHTML = ''; + this._showProductSupport(); + this._currentSearchQuery = ''; + }, + + _showProductSupport: function() { + var elSupport = Dom.get('product_support_' + + product.getName().replace(' ', '_').toLowerCase()); + var supportElements = Dom.getElementsByClassName('product_support'); + for (var i = 0, n = supportElements.length; i < n; i++) { + if (supportElements[i] == elSupport) { + Dom.removeClass(elSupport, 'hidden'); + } else { + Dom.addClass(supportElements[i], 'hidden'); + } + } + }, + + onShow: function() { + this._showProductSupport(); + this._onSummaryBlur(); + + // hide the advanced form and top continue button entry until + // a search has happened + Dom.addClass('advanced', 'hidden'); + Dom.addClass('dupes_continue_button_top', 'hidden'); + + if (!this._elSearch.disabled && this.getSummary().length >= 4) { + // do an immediate search after a page refresh if there's a query + this._doSearch(); + + } else { + // prepare for a search + this.reset(); + } + }, + + _onSummaryBlur: function() { + dupes._elSearch.disabled = dupes._elSummary.value == ''; + guided.setAdvancedLink(); + }, + + _onSummaryKeyDown: function(e) { + // map <enter> to doSearch() + if (e && (e.keyCode == 13)) { + dupes._doSearch(); + Event.stopPropagation(e); + } + }, + + _onSummaryKeyUp: function(e) { + // disable search button until there's a query + dupes._elSearch.disabled = YAHOO.lang.trim(dupes._elSummary.value) == ''; + }, + + _doSearch: function() { + if (dupes.getSummary().length < 4) { + alert('The summary must be at least 4 characters long.'); + return; + } + dupes._elSummary.blur(); + + // don't query if we already have the results (or they are pending) + if (dupes._currentSearchQuery == dupes.getSummary()) + return; + dupes._currentSearchQuery = dupes.getSummary(); + + // initialise the datatable as late as possible + dupes._initDataTable(); + + try { + // run the search + Dom.removeClass(dupes._elList, 'hidden'); + + dupes._dataTable.showTableMessage( + 'Searching for similar issues... ' + + '<img src="extensions/GuidedBugEntry/web/images/throbber.gif"' + + ' width="16" height="11">', + YAHOO.widget.DataTable.CLASS_LOADING + ); + var json_object = { + version: "1.1", + method: "Bug.possible_duplicates", + id: ++dupes._counter, + params: { + product: product._getNameAndRelated(), + summary: dupes.getSummary(), + limit: 12, + include_fields: [ "id", "summary", "status", "resolution", + "update_token", "cc" ] + } + }; + + dupes._dataTable.getDataSource().sendRequest( + YAHOO.lang.JSON.stringify(json_object), + { + success: dupes._onDupeResults, + failure: dupes._onDupeResults, + scope: dupes._dataTable, + argument: dupes._dataTable.getState() + } + ); + + Dom.get('dupes_continue_button_top').disabled = true; + Dom.get('dupes_continue_button_bottom').disabled = true; + Dom.removeClass('dupes_continue', 'hidden'); + } catch(err) { + if (console) + console.error(err.message); + } + }, + + _onDupeResults: function(sRequest, oResponse, oPayload) { + Dom.removeClass('advanced', 'hidden'); + Dom.removeClass('dupes_continue_button_top', 'hidden'); + Dom.get('dupes_continue_button_top').disabled = false; + Dom.get('dupes_continue_button_bottom').disabled = false; + dupes._dataTable.onDataReturnInitializeTable(sRequest, oResponse, + oPayload); + }, + + getSummary: function() { + var summary = YAHOO.lang.trim(this._elSummary.value); + // work around chrome bug + if (summary == dupes._elSummary.getAttribute('placeholder')) { + return ''; + } else { + return summary; + } + } +}; + +// bug form step + +var bugForm = { + _visibleHelpPanel: null, + _mandatoryFields: [ 'short_desc', 'version_select' ], + + onInit: function() { + Dom.get('user_agent').value = navigator.userAgent; + if (navigator.buildID && navigator.buildID != navigator.userAgent) { + Dom.get('build_id').value = navigator.buildID; + } + Event.addListener(Dom.get('short_desc'), 'blur', function() { + Dom.get('dupes_summary').value = Dom.get('short_desc').value; + guided.setAdvancedLink(); + }); + }, + + onShow: function() { + Dom.removeClass('advanced', 'hidden'); + // default the summary to the dupes query + Dom.get('short_desc').value = dupes.getSummary(); + this.resetSubmitButton(); + if (Dom.get('component_select').length == 0) + this.onProductUpdated(); + this.onFileChange(); + for (var i = 0, n = this._mandatoryFields.length; i < n; i++) { + Dom.removeClass(this._mandatoryFields[i], 'missing'); + } + }, + + resetSubmitButton: function() { + Dom.get('submit').disabled = false; + Dom.get('submit').value = 'Submit Bug'; + }, + + onProductUpdated: function() { + var productName = product.getName(); + + // init + var elComponents = Dom.get('component_select'); + Dom.addClass('component_description', 'hidden'); + elComponents.options.length = 0; + + var elVersions = Dom.get('version_select'); + elVersions.length = 0; + + // product not loaded yet, bail out + if (!product.details) { + Dom.addClass('versionTH', 'hidden'); + Dom.addClass('versionTD', 'hidden'); + Dom.get('productTD').colSpan = 2; + Dom.get('submit').disabled = true; + return; + } + Dom.get('submit').disabled = false; + + // build components + // if there's a general component in it, make it selected by default + var defaultComponent = false; + for (var i = 0, n = product.details.components.length; i < n; i++) { + var component = product.details.components[i]; + if (component.is_active == '1') { + elComponents.options[elComponents.options.length] = + new Option(component.name, component.name); + if (!defaultComponent && component.name.match(/general/i)) { + defaultComponent = component.name; + } + } + } + + var elComponent = Dom.get('component'); + if (elComponent.value == '' && defaultComponent) + elComponent.value = defaultComponent; + if (elComponent.value != '') { + elComponents.value = elComponent.value; + this.onComponentChange(elComponent.value); + } + + // build versions + var defaultVersion = ''; + var currentVersion = Dom.get('version').value; + for (var i = 0, n = product.details.versions.length; i < n; i++) { + var version = product.details.versions[i]; + if (version.is_active == '1') { + elVersions.options[elVersions.options.length] = + new Option(version.name, version.name); + if (currentVersion == version.name) + defaultVersion = version.name; + } + } + + if (!defaultVersion) { + // try to detect version on a per-product basis + if (products[productName] && products[productName].version) { + var detectedVersion = products[productName].version(); + var options = elVersions.options; + for (var i = 0, n = options.length; i < n; i++) { + if (options[i].value == detectedVersion) { + defaultVersion = detectedVersion; + break; + } + } + } + } + if (!defaultVersion) { + // load last selected version + defaultVersion = YAHOO.util.Cookie.get('VERSION-' + productName); + } + + if (elVersions.length > 1) { + // more than one version, show select + Dom.get('productTD').colSpan = 1; + Dom.removeClass('versionTH', 'hidden'); + Dom.removeClass('versionTD', 'hidden'); + + } else { + // if there's only one version, we don't need to ask the user + Dom.addClass('versionTH', 'hidden'); + Dom.addClass('versionTD', 'hidden'); + Dom.get('productTD').colSpan = 2; + defaultVersion = elVersions.options[0].value; + } + + if (defaultVersion) { + elVersions.value = defaultVersion; + + } else { + // no default version, select an empty value to force a decision + var opt = new Option('', ''); + try { + // standards + elVersions.add(opt, elVersions.options[0]); + } catch(ex) { + // ie only + elVersions.add(opt, 0); + } + elVersions.value = ''; + } + bugForm.onVersionChange(elVersions.value); + }, + + onComponentChange: function(componentName) { + // show the component description + Dom.get('component').value = componentName; + var elComponentDesc = Dom.get('component_description'); + elComponentDesc.innerHTML = ''; + for (var i = 0, n = product.details.components.length; i < n; i++) { + var component = product.details.components[i]; + if (component.name == componentName) { + elComponentDesc.innerHTML = component.description; + break; + } + } + Dom.removeClass(elComponentDesc, 'hidden'); + }, + + onVersionChange: function(version) { + Dom.get('version').value = version; + }, + + onFileChange: function() { + // toggle ui enabled when a file is uploaded or cleared + var elFile = Dom.get('data'); + var elReset = Dom.get('reset_data'); + var elDescription = Dom.get('data_description'); + var filename = bugForm._getFilename(); + if (filename) { + elReset.disabled = false; + elDescription.value = filename; + elDescription.disabled = false; + } else { + elReset.disabled = true; + elDescription.value = ''; + elDescription.disabled = true; + } + }, + + onFileClear: function() { + Dom.get('data').value = ''; + this.onFileChange(); + return false; + }, + + _getFilename: function() { + var filename = Dom.get('data').value; + if (!filename) + return ''; + filename = filename.replace(/^.+[\\\/]/, ''); + return filename; + }, + + _mandatoryCheck: function() { + result = true; + for (var i = 0, n = this._mandatoryFields.length; i < n; i++ ) { + id = this._mandatoryFields[i]; + el = Dom.get(id); + + if (el.type.toString() == "checkbox") { + value = el.checked; + } else { + value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + el.value = value; + } + + if (value == '') { + Dom.addClass(id, 'missing'); + result = false; + } else { + Dom.removeClass(id, 'missing'); + } + } + return result; + }, + + validate: function() { + + // check mandatory fields + + if (!bugForm._mandatoryCheck()) { + if (Dom.hasClass('short_desc', 'missing') + && Dom.hasClass('version_select', 'missing')) { + alert('Please enter the summary, and select the relevant version.'); + } else if (Dom.hasClass('short_desc', 'missing')) { + alert('Please enter the summary.'); + } else { + alert('Please select the relevant version.\n\n' + + 'If you are unsure select "unspecified".'); + } + + return false; + } + + if (Dom.get('data').value && !Dom.get('data_description').value) + Dom.get('data_description').value = bugForm._getFilename(); + + Dom.get('submit').disabled = true; + Dom.get('submit').value = 'Submitting Bug...'; + + return true; + }, + + _initHelp: function(el) { + var help_id = el.getAttribute('helpid'); + if (!el.panel) { + if (!el.id) + el.id = help_id + '_parent'; + el.panel = new YAHOO.widget.Panel( + help_id, + { + width: "320px", + visible: false, + close: false, + context: [el.id, 'tl', 'tr', null, [5, 0]] + } + ); + el.panel.render(); + Dom.removeClass(help_id, 'hidden'); + } + }, + + showHelp: function(el) { + this._initHelp(el); + if (this._visibleHelpPanel) + this._visibleHelpPanel.hide(); + el.panel.show(); + this._visibleHelpPanel = el.panel; + }, + + hideHelp: function(el) { + if (!el.panel) + return; + if (this._visibleHelpPanel) + this._visibleHelpPanel.hide(); + el.panel.hide(); + this._visibleHelpPanel = null; + } +} + diff --git a/extensions/GuidedBugEntry/web/js/products.js b/extensions/GuidedBugEntry/web/js/products.js new file mode 100644 index 000000000..cdab695fc --- /dev/null +++ b/extensions/GuidedBugEntry/web/js/products.js @@ -0,0 +1,117 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 GuidedBugEntry Bugzilla Extension. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +/* Product-specifc configuration for guided bug entry + * + * related: array of product names which will also be searched for duplicates + * version: function which returns a version (eg. detected from UserAgent) + * support: string which is displayed at the top of the duplicates page + * secgroup: the group to place confidential bugs into + * detectPlatform: when true the platform and op_sys will be set from the + * browser's user agent. when false, these will be set to All + */ + +var products = { + + "Firefox": { + related: [ "Core", "Toolkit" ], + version: function() { + var re = /Firefox\/(\d+)\.(\d+)/i; + var match = re.exec(navigator.userAgent); + if (match) { + var maj = match[1]; + var min = match[2]; + if (maj * 1 >= 5) { + return maj + " Branch"; + } else { + return maj + "." + min + " Branch"; + } + } else { + return false; + } + }, + detectPlatform: true, + support: + 'If you are new to Firefox or Bugzilla, please consider checking ' + + '<a href="http://support.mozilla.com/">' + + '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' + + ' <b>Firefox Help</b></a> instead of creating a bug.' + }, + + "Fennec": { + related: [ "Core", "Toolkit" ], + detectPlatform: true, + support: + 'If you are new to Firefox or Bugzilla, please consider checking ' + + '<a href="http://support.mozilla.com/">' + + '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' + + ' <b>Firefox Help</b></a> instead of creating a bug.' + }, + + "SeaMonkey": { + related: [ "Core", "Toolkit" ], + detectPlatform: true, + version: function() { + var re = /SeaMonkey\/(\d+)\.(\d+)/i; + var match = re.exec(navigator.userAgent); + if (match) { + var maj = match[1]; + var min = match[2]; + return "SeaMonkey " + maj + "." + min + " Branch"; + } else { + return false; + } + } + }, + + "Camino": { + related: [ "Core", "Toolkit" ], + detectPlatform: true + }, + + "Core": { + detectPlatform: true + }, + + "Thunderbird": { + related: [ "Core", "Toolkit", "MailNews Core" ], + detectPlatform: true + }, + + "Penelope": { + related: [ "Core", "Toolkit", "MailNews Core" ] + }, + + "Bugzilla": { + support: + 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> to file "test bugs".' + }, + + "bugzilla.mozilla.org": { + related: [ "Bugzilla" ], + support: + 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> to file "test bugs".' + } +} diff --git a/extensions/GuidedBugEntry/web/style/guided.css b/extensions/GuidedBugEntry/web/style/guided.css new file mode 100644 index 000000000..b0c766d11 --- /dev/null +++ b/extensions/GuidedBugEntry/web/style/guided.css @@ -0,0 +1,233 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 GuidedBugEntry Bugzilla Extension. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** */ + +/* global */ + +#page_title { +} + +#page_title h2 { + margin-bottom: 0px; +} + +#page_title h3 { + margin-top: 0px; +} + +.hidden { + display: none; +} + +#yui-history-iframe { + position: absolute; + top: 0; + left: 0; + width: 1px; + height: 1px; + visibility: hidden; +} + +.step { + margin-left: 20px; + margin-bottom: 25px; +} + +#steps a img { + border: none; +} + +#advanced { + margin-top: 50px; +} + +#advanced img { + vertical-align: middle; +} + +#advanced a { + cursor: pointer; +} + +/* remove the shaded background from data_table header + it looks out of place */ +.yui-skin-sam .yui-dt th { + background: #f0f0f0; +} + +/* products and other_products step */ + +.exits { + width: 600px; + margin-bottom: 10px; + border: 1px solid #aaa; + border-radius: 5px; +} + +.exits td { + padding: 5px; +} + +.exits h2 { + margin: 0px; + font-size: 90%; +} + +.exit_img { + width: 64px; + text-align: right; +} + +#products { + width: 600px; +} + +#products td { + padding: 5px; + padding-bottom: 10px; +} + +#products h2 { + margin-bottom: 0px; +} + +#products p { + margin-top: 0px; +} + +.product_img { + width: 64px; +} + +#other_products .classification { + font-weight: bold; +} + +#other_products .classification th { + font-size: large; +} + +/* duplicates step */ + +#dupes_summary { + width: 500px; +} + +#dupes_list { + margin-top: 1em; + margin-bottom: 1em; +} + +#product_support { + border: 1px solid #dddddd; +} + +/* bug form step */ + +#bugForm { + width: 600px; + border: 4px solid #e0e0e0; + -moz-border-radius: 5px; + border-radius: 5px; +} + +#bugForm th, #bugForm td { + padding: 5px; +} + +#bugForm .even th, #bugForm .even td { + background: #e0e0e0; +} + +#bugForm .label { + text-align: left; + font-weight: bold; + white-space: nowrap +} + +#bugzilla-body #bugForm th { + vertical-align: middle; +} + +#bugForm .textInput { + width: 450px; +} + +#bugForm textarea { + font-family: Verdana, sans-serif; + font-size: small; + width: 590px; +} + +#bugForm .mandatory_mark { + color: red; + font-size: 80%; +} + +#bugForm .mandatory { +} + +#bugForm .textInput[disabled] { + background: transparent; + border: 1px solid #dddddd; +} + +#versionTD { + text-align: right; + white-space: nowrap +} + +#component_select { + width: 450px; +} + +#component_description { + padding: 5px; +} + +#bugForm .missing { + border: 1px solid red; + box-shadow: 0px 0px 4px #ff0000; + -webkit-box-shadow: 0px 0px 4px #ff0000; + -moz-box-shadow: 0px 0px 4px #ff0000; +} + +#submitTD { + text-align: right; +} + +.help { + position: absolute; + background: #ffffff; + padding: 2px; + cursor: default; +} + +.help-bad { + color: #990000; +} + +.help-good { + color: #009900; +} diff --git a/extensions/GuidedBugEntry/web/yui-history-iframe.txt b/extensions/GuidedBugEntry/web/yui-history-iframe.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/extensions/GuidedBugEntry/web/yui-history-iframe.txt diff --git a/extensions/InlineHistory/Config.pm b/extensions/InlineHistory/Config.pm new file mode 100644 index 000000000..dd4002856 --- /dev/null +++ b/extensions/InlineHistory/Config.pm @@ -0,0 +1,31 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1 +# +# 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 InlineHistory Bugzilla Extension; +# Derived from the Bugzilla Tweaks Addon. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 the Initial +# Developer. All Rights Reserved. +# +# Contributor(s): +# Johnathan Nightingale <johnath@mozilla.com> +# Ehsan Akhgari <ehsan@mozilla.com> +# Byron Jones <glob@mozilla.com> +# +# ***** END LICENSE BLOCK ***** + +package Bugzilla::Extension::InlineHistory; +use strict; + +use constant NAME => 'InlineHistory'; + +__PACKAGE__->NAME; diff --git a/extensions/InlineHistory/Extension.pm b/extensions/InlineHistory/Extension.pm new file mode 100644 index 000000000..63a67304b --- /dev/null +++ b/extensions/InlineHistory/Extension.pm @@ -0,0 +1,183 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1 +# +# 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 InlineHistory Bugzilla Extension; +# Derived from the Bugzilla Tweaks Addon. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2011 the Initial +# Developer. All Rights Reserved. +# +# Contributor(s): +# Johnathan Nightingale <johnath@mozilla.com> +# Ehsan Akhgari <ehsan@mozilla.com> +# Byron Jones <glob@mozilla.com> +# +# ***** END LICENSE BLOCK ***** + +package Bugzilla::Extension::InlineHistory; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::User::Setting; +use Bugzilla::Constants; +use Bugzilla::Attachment; + +our $VERSION = '1.4'; + +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + return unless $user && $user->id && $user->settings; + return unless $user->settings->{'inline_history'}->{'value'} eq 'on'; + + # in the header we just need to set the var, to ensure the css and + # javascript get included + if ($file eq 'bug/show-header.html.tmpl') { + $vars->{'ih_activity'} = 1; + return; + } elsif ($file ne 'bug/edit.html.tmpl') { + return; + } + + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug_id = exists $vars->{'bugs'} + ? $vars->{'bugs'}[0]->id + : $vars->{'bug'}->id; + + # build bug activity + my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id); + $activity = _add_duplicates($bug_id, $activity); + + # augment and tweak + foreach my $operation (@$activity) { + # make operation.who an object + $operation->{who} = Bugzilla::User->new({ name => $operation->{who} }); + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = Bugzilla::Attachment->new($change->{attachid}); + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + # split multiple flag changes (must be processed last) + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + next if scalar(@added) <= 1 && scalar(@removed) <= 1; + # remove current change + splice(@{$operation->{changes}}, $i, 1); + # restructure into added/removed for each flag + my %flags; + foreach my $added (@added) { + my ($value, $name) = $added =~ /^((.+).)$/; + $flags{$name}{added} = $value; + $flags{$name}{removed} |= ''; + } + foreach my $removed (@removed) { + my ($value, $name) = $removed =~ /^((.+).)$/; + $flags{$name}{added} |= ''; + $flags{$name}{removed} = $value; + } + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + splice(@{$operation->{changes}}, $i, 0, $flag_change); + } + $i--; + } + } + } + + $vars->{'ih_activity'} = $activity; +} + +sub _add_duplicates { + # insert 'is a dupe of this bug' comment to allow js to display + # as activity + + my ($bug_id, $activity) = @_; + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT profiles.login_name, " . + $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ", + extra_data, + thetext + FROM longdescs + INNER JOIN profiles ON profiles.userid = longdescs.who + WHERE bug_id = ? + AND ( + type = ? + OR thetext LIKE '%has been marked as a duplicate of this%' + ) + ORDER BY bug_when + "); + $sth->execute($bug_id, CMT_HAS_DUPE); + + while (my($who, $when, $dupe_id, $the_text) = $sth->fetchrow_array) { + if (!$dupe_id) { + next unless $the_text =~ / (\d+) has been marked as a duplicate of this/; + $dupe_id = $1; + } + my $entry = { + 'when' => $when, + 'who' => $who, + 'changes' => [ + { + 'removed' => '', + 'added' => $dupe_id, + 'attachid' => undef, + 'fieldname' => 'dupe', + 'dupe' => 1, + } + ], + }; + push @$activity, $entry; + } + + return [ sort { $a->{when} cmp $b->{when} } @$activity ]; +} + +sub install_before_final_checks { + my ($self, $args) = @_; + add_setting('inline_history', ['on', 'off'], 'off'); +} + +__PACKAGE__->NAME; diff --git a/extensions/InlineHistory/README b/extensions/InlineHistory/README new file mode 100644 index 000000000..f5aaf163f --- /dev/null +++ b/extensions/InlineHistory/README @@ -0,0 +1,10 @@ +InlineHistory inserts bug activity inline with the comments when viewing a bug. +It was derived from the Bugzilla Tweaks Addon by Ehasn Akhgari. + +For technical and performance reasons it is only available to logged in users, +and is enabled by a User Preference. + +It works with an unmodified install of Bugzilla 4.0 and 4.2. + +If you have modified your show_bug template, the javascript in +web/inline-history.js may need to be updated to suit your installation. diff --git a/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl new file mode 100644 index 000000000..4af08dca8 --- /dev/null +++ b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl @@ -0,0 +1,135 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 InlineHistory Bugzilla Extension; + # Derived from the Bugzilla Tweaks Addon. + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Johnathan Nightingale <johnath@mozilla.com> + # Ehsan Akhgari <ehsan@mozilla.com> + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** + #%] + +[% IF ih_activity %] +[%# this div exists to allow bugzilla-tweaks to detect when we're active %] +<div id="inline-history-ext"></div> + +<script> + var ih_activity = new Array(); + var ih_activity_flags = new Array(); + var ih_activity_sort_order = '[% user.settings.comment_sort_order.value FILTER js %]'; + [% FOREACH operation = ih_activity %] + var html = ''; + [% has_cc = 0 %] + [% has_flag = 0 %] + [% FOREACH change = operation.changes %] + [%# track flag changes %] + [% IF change.fieldname == 'flagtypes.name' && change.added != '' %] + var item = new Array(5); + item[0] = '[% operation.who.login FILTER js %]'; + item[1] = '[% operation.when FILTER time FILTER js %]'; + item[2] = '[% change.attachid FILTER js %]'; + item[3] = '[% change.added FILTER js %]'; + item[4] = '[% operation.who.identity FILTER js %]'; + ih_activity_flags.push(item); + [% has_flag = 1 %] + [% END %] + + [%# wrap CC changes in a span for toggling visibility %] + [% IF change.fieldname == 'cc' %] + html += '<span class="ih_cc">'; + [% has_cc = 1 %] + [% END %] + + [%# make attachment changes better %] + [% IF change.attachid %] + html += '<a ' + + 'href="attachment.cgi?action=edit&id=[% change.attachid FILTER none %]" ' + + 'title="[% change.attach.description FILTER js %]" ' + + 'class="[% "bz_obsolete" IF change.attach.isobsolete %]"' + + '>Attachment #[% change.attachid FILTER none %]</a> - '; + [% END %] + + [%# display duplicates as history rather than comments %] + [% IF change.dupe %] + html += 'Duplicate of this [% terms.bug %]: '; + [% ELSE %] + html += '[% field_descs.${change.fieldname} FILTER js %]: '; + [% END %] + + [% IF change.removed != '' %] + [% IF change.added == '' %] + html += '<span class="ih_deleted">'; + [% END %] + [% PROCESS add_change value = change.removed, is_old = 1 %] + [% IF change.added == '' %] + html += '</span>'; + [% ELSE %] + html += ' ➔ '; + [% END %] + [% END %] + [% PROCESS add_change value = change.added, is_old = 0 %] + [% "html += '<br>';" UNLESS loop.last %] + + [% IF change.fieldname == 'cc' %] + html += '</span>'; + [% END %] + [% END %] + var item = new Array(7); + item[0] = '[% operation.who.login FILTER js %]'; + item[1] = '[% operation.when FILTER time FILTER js %]'; + item[2] = html; + item[3] = '<div class="bz_comment_head">' + + '<span class="bz_comment_user">' + + '[% INCLUDE global/user.html.tmpl who = operation.who FILTER js %]' + + '</span>' + + '<span class="bz_comment_time"> ' + item[1] + ' </span>' + + '</div>'; + item[4] = [% IF has_cc && (operation.changes.size == 1) %]true[% ELSE %]false[% END %]; + item[5] = [% IF change.dupe %][% change.added FILTER js %][% ELSE %]0[% END %]; + item[6] = [% IF has_flag %]true[% ELSE %]false[% END %]; + ih_activity[[% loop.index %]] = item; + [% END %] + inline_history.init(); +</script> +[% END %] + +[% BLOCK add_change %] + html += '[%~%] + [% '<span class="old">' IF is_old %] + [% IF change.fieldname == 'estimated_time' || + change.fieldname == 'remaining_time' || + change.fieldname == 'work_time' %] + [% PROCESS formattimeunit time_unit = value FILTER html FILTER js %] + [% ELSIF change.fieldname == 'blocked' || + change.fieldname == 'dependson' || + change.fieldname == 'dupe' %] + [% value FILTER bug_list_link FILTER js %] + [% ELSIF change.fieldname == 'assigned_to' || + change.fieldname == 'reporter' || + change.fieldname == 'qa_contact' || + change.fieldname == 'cc' || + change.fieldname == 'flagtypes.name' %] + [% display_value(change.fieldname, value) FILTER email FILTER js %] + [% ELSE %] + [% display_value(change.fieldname, value) FILTER html FILTER js %] + [% END %] + [% '</span>' IF is_old %] + [%~ %]'; +[% END %] diff --git a/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl new file mode 100644 index 000000000..221175105 --- /dev/null +++ b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl @@ -0,0 +1,33 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 InlineHistory Bugzilla Extension; + # Derived from the Bugzilla Tweaks Addon. + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Johnathan Nightingale <johnath@mozilla.com> + # Ehsan Akhgari <ehsan@mozilla.com> + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** + #%] + +[% IF ih_activity %] + [% style_urls.push('extensions/InlineHistory/web/style.css') %] + [% javascript_urls.push('extensions/InlineHistory/web/inline-history.js') %] +[% END %] + diff --git a/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl new file mode 100644 index 000000000..852dac22f --- /dev/null +++ b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl @@ -0,0 +1,31 @@ +[%# ***** BEGIN LICENSE BLOCK ***** + # Version: MPL 1.1 + # + # 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 InlineHistory Bugzilla Extension; + # Derived from the Bugzilla Tweaks Addon. + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by the Initial Developer are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Johnathan Nightingale <johnath@mozilla.com> + # Ehsan Akhgari <ehsan@mozilla.com> + # Byron Jones <glob@mozilla.com> + # + # ***** END LICENSE BLOCK ***** + #%] + +[% + setting_descs.inline_history = "When viewing a $terms.bug, show all $terms.bug activity", +%] diff --git a/extensions/InlineHistory/web/inline-history.js b/extensions/InlineHistory/web/inline-history.js new file mode 100644 index 000000000..9fb860e3c --- /dev/null +++ b/extensions/InlineHistory/web/inline-history.js @@ -0,0 +1,396 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 InlineHistory Bugzilla Extension; + * Derived from the Bugzilla Tweaks Addon. + * Derived from the Bugzilla Tweaks Addon. + * + * The Initial Developer of the Original Code is the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 the Initial + * Developer. All Rights Reserved. + * + * Contributor(s): + * Johnathan Nightingale <johnath@mozilla.com> + * Ehsan Akhgari <ehsan@mozilla.com> + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + */ + +var inline_history = { + _ccDivs: null, + _hasAttachmentFlags: false, + _hasBugFlags: false, + + init: function() { + Dom = YAHOO.util.Dom; + + // remove 'has been marked as a duplicate of this bug' comments + var reDuplicate = /\*\*\* \S+ \d+ has been marked as a duplicate of this/; + var reBugId = /show_bug\.cgi\?id=(\d+)/; + var comments = Dom.getElementsByClassName("bz_comment", 'div', 'comments'); + for (var i = 1, il = comments.length; i < il; i++) { + var textDiv = Dom.getElementsByClassName('bz_comment_text', 'pre', comments[i]); + if (textDiv) { + var match = reDuplicate.exec(textDiv[0].textContent || textDiv[0].innerText); + if (match) { + // grab the comment and bug number from the element + var comment = comments[i]; + var number = comment.id.substr(1); + var time = this.trim(Dom.getElementsByClassName('bz_comment_time', 'span', comment)[0].innerHTML); + var dupeId = 0; + match = reBugId.exec(Dom.get('comment_text_' + number).innerHTML); + if (match) + dupeId = match[1]; + // remove the element + comment.parentNode.removeChild(comment); + // update the html for the history item to include the comment number + if (dupeId == 0) + continue; + for (var j = 0, jl = ih_activity.length; j < jl; j++) { + var item = ih_activity[j]; + if (item[5] == dupeId && item[1] == time) { + // insert comment number and link into the header + item[3] = item[3].substr(0, item[3].length - 6) // remove trailing </div> + // add comment number + + '<span class="bz_comment_number" id="c' + number + '">' + + '<a href="#c' + number + '">Comment ' + number + '</a>' + + '</span>' + + '</div>'; + break; + } + } + } + } + } + + // ensure new items are placed immediately after the last comment + var commentDivs = Dom.getElementsByClassName('bz_comment', 'div', 'comments'); + if (!commentDivs.length) return; + var lastCommentDiv = commentDivs[commentDivs.length - 1]; + + // insert activity into the correct location + var commentTimes = Dom.getElementsByClassName('bz_comment_time', 'span', 'comments'); + for (var i = 0, il = ih_activity.length; i < il; i++) { + var item = ih_activity[i]; + // item[0] : who + // item[1] : when + // item[2] : change html + // item[3] : header html + // item[4] : bool; cc-only + // item[5] : int; dupe bug id (or 0) + // item[6] : bool; is flag + var user = item[0]; + var time = item[1]; + + var reachedEnd = false; + var start_index = ih_activity_sort_order == 'newest_to_oldest_desc_first' ? 1 : 0; + for (var j = start_index, jl = commentTimes.length; j < jl; j++) { + var commentHead = commentTimes[j].parentNode; + var mainUser = Dom.getElementsByClassName('email', 'a', commentHead)[0].href.substr(7); + var text = commentTimes[j].textContent || commentTimes[j].innerText; + var mainTime = this.trim(text); + + if (ih_activity_sort_order == 'oldest_to_newest' ? time > mainTime : time < mainTime) { + if (j < commentTimes.length - 1) { + continue; + } else { + reachedEnd = true; + } + } + + var inline = (mainUser == user && time == mainTime); + var currentDiv = document.createElement("div"); + + // place ih_cc class on parent container if it's the only child + var containerClass = ''; + if (item[4]) { + item[2] = item[2].replace('"ih_cc"', '""'); + containerClass = 'ih_cc'; + } + + if (inline) { + // assume that the change was made by the same user + commentHead.parentNode.appendChild(currentDiv); + currentDiv.innerHTML = item[2]; + Dom.addClass(currentDiv, 'ih_inlinehistory'); + Dom.addClass(currentDiv, containerClass); + if (item[6]) + this.setFlagChangeID(item, commentHead.parentNode.id); + + } else { + // the change was made by another user + if (!reachedEnd) { + var parentDiv = commentHead.parentNode; + var previous = this.previousElementSibling(parentDiv); + if (previous && previous.className.indexOf("ih_history") >= 0) { + currentDiv = this.previousElementSibling(parentDiv); + } else { + parentDiv.parentNode.insertBefore(currentDiv, parentDiv); + } + } else { + var parentDiv = commentHead.parentNode; + var next = this.nextElementSibling(parentDiv); + if (next && next.className.indexOf("ih_history") >= 0) { + currentDiv = this.nextElementSibling(parentDiv); + } else { + lastCommentDiv.parentNode.insertBefore(currentDiv, lastCommentDiv.nextSibling); + } + } + + var itemHtml = '<div class="ih_history_item ' + containerClass + '" ' + + 'id="h' + i + '">' + + item[3] + item[2] + + '</div>'; + + if (ih_activity_sort_order == 'oldest_to_newest') { + currentDiv.innerHTML = currentDiv.innerHTML + itemHtml; + } else { + currentDiv.innerHTML = itemHtml + currentDiv.innerHTML; + } + currentDiv.setAttribute("class", "bz_comment ih_history"); + if (item[6]) + this.setFlagChangeID(item, 'h' + i); + } + break; + } + } + + // find comment blocks which only contain cc changes, shift the ih_cc + var historyDivs = Dom.getElementsByClassName('ih_history', 'div', 'comments'); + for (var i = 0, il = historyDivs.length; i < il; i++) { + var historyDiv = historyDivs[i]; + var itemDivs = Dom.getElementsByClassName('ih_history_item', 'div', historyDiv); + var ccOnly = true; + for (var j = 0, jl = itemDivs.length; j < jl; j++) { + if (!Dom.hasClass(itemDivs[j], 'ih_cc')) { + ccOnly = false; + break; + } + } + if (ccOnly) { + for (var j = 0, jl = itemDivs.length; j < jl; j++) { + Dom.removeClass(itemDivs[j], 'ih_cc'); + } + Dom.addClass(historyDiv, 'ih_cc'); + } + } + + if (this._hasAttachmentFlags) + this.linkAttachmentFlags(); + if (this._hasBugFlags) + this.linkBugFlags(); + + ih_activity = undefined; + ih_activity_flags = undefined; + + this._ccDivs = Dom.getElementsByClassName('ih_cc', '', 'comments'); + this.hideCC(); + YAHOO.util.Event.onDOMReady(this.addCCtoggler); + }, + + setFlagChangeID: function(changeItem, id) { + // put the ID for the change into ih_activity_flags + for (var i = 0, il = ih_activity_flags.length; i < il; i++) { + var flagItem = ih_activity_flags[i]; + // flagItem[0] : who.login + // flagItem[1] : when + // flagItem[2] : attach id + // flagItem[3] : flag + // flagItem[4] : who.identity + // flagItem[5] : change div id + if (flagItem[0] == changeItem[0] && flagItem[1] == changeItem[1]) { + // store the div + flagItem[5] = id; + // tag that we have flags to process + if (flagItem[2]) { + this._hasAttachmentFlags = true; + } else { + this._hasBugFlags = true; + } + // don't break as there may be multiple flag changes at once + } + } + }, + + linkAttachmentFlags: function() { + var rows = Dom.get('attachment_table').getElementsByTagName('tr'); + for (var i = 0, il = rows.length; i < il; i++) { + + // deal with attachments with flags only + var tr = rows[i]; + if (!tr.id || tr.id == 'a0') + continue; + var attachFlagTd = Dom.getElementsByClassName('bz_attach_flags', 'td', tr); + if (attachFlagTd.length == 0) + continue; + attachFlagTd = attachFlagTd[0]; + + // get the attachment id + var attachId = 0; + var anchors = tr.getElementsByTagName('a'); + for (var j = 0, jl = anchors.length; j < jl; j++) { + var match = anchors[j].href.match(/attachment\.cgi\?id=(\d+)/); + if (match) { + attachId = match[1]; + break; + } + } + if (!attachId) + continue; + + var html = ''; + + // there may be multiple flags, split by <br> + var attachFlags = attachFlagTd.innerHTML.split('<br>'); + for (var j = 0, jl = attachFlags.length; j < jl; j++) { + var match = attachFlags[j].match(/^\s*(<span.+\/span>):([^\?\-\+]+[\?\-\+])([\s\S]*)/); + if (!match) continue; + var setterSpan = match[1]; + var flag = this.trim(match[2].replace('\u2011', '-', 'g')); + var requestee = this.trim(match[3]); + var requesteeLogin = ''; + + match = setterSpan.match(/title="([^"]+)"/); + if (!match) continue; + var setterIdentity = this.htmlDecode(match[1]); + + if (requestee) { + match = requestee.match(/title="([^"]+)"/); + if (!match) continue; + requesteeLogin = this.htmlDecode(match[1]); + match = requesteeLogin.match(/<([^>]+)>/); + if (!match) continue; + requesteeLogin = match[1]; + } + + var flagValue = requestee ? flag + '(' + requesteeLogin + ')' : flag; + // find the id for this change + var found = false; + for (var k = 0, kl = ih_activity_flags.length; k < kl; k++) { + flagItem = ih_activity_flags[k]; + if ( + flagItem[2] == attachId + && flagItem[3] == flagValue + && flagItem[4] == setterIdentity + ) { + html += + setterSpan + ': ' + + '<a href="#' + flagItem[5] + '">' + flag + '</a>' + + requestee + '<br>'; + found = true; + break; + } + } + if (!found) { + // something went wrong, insert the flag unlinked + html += attachFlags[j] + '<br>'; + } + } + + if (html) + attachFlagTd.innerHTML = html; + } + }, + + linkBugFlags: function() { + var rows = Dom.get('flags').getElementsByTagName('tr'); + for (var i = 0, il = rows.length; i < il; i++) { + var cells = rows[i].getElementsByTagName('td'); + if (!cells[1]) continue; + + var match = cells[0].innerHTML.match(/title="([^"]+)"/); + if (!match) continue; + var setterIdentity = this.htmlDecode(match[1]); + + var flagValue = cells[2].getElementsByTagName('select'); + if (!flagValue.length) continue; + flagValue = flagValue[0].value; + + var flagLabel = cells[1].getElementsByTagName('label'); + if (!flagLabel.length) continue; + flagLabel = flagLabel[0]; + var flagName = this.trim(flagLabel.innerHTML).replace('\u2011', '-', 'g'); + + for (var j = 0, jl = ih_activity_flags.length; j < jl; j++) { + flagItem = ih_activity_flags[j]; + if ( + !flagItem[2] + && flagItem[3] == flagName + flagValue + && flagItem[4] == setterIdentity + ) { + flagLabel.innerHTML = + '<a href="#' + flagItem[5] + '">' + flagName + '</a>'; + break; + } + } + } + }, + + hideCC: function() { + Dom.addClass(this._ccDivs, 'ih_hidden'); + }, + + showCC: function() { + Dom.removeClass(this._ccDivs, 'ih_hidden'); + }, + + addCCtoggler: function() { + var ul = Dom.getElementsByClassName('bz_collapse_expand_comments'); + if (ul.length == 0) + return; + ul = ul[0]; + var a = document.createElement('a'); + a.href = 'javascript:void(0)'; + a.id = 'ih_toggle_cc'; + YAHOO.util.Event.addListener(a, 'click', function(e) { + if (Dom.get('ih_toggle_cc').innerHTML == 'Show CC Changes') { + a.innerHTML = 'Hide CC Changes'; + inline_history.showCC(); + } else { + a.innerHTML = 'Show CC Changes'; + inline_history.hideCC(); + } + }); + a.innerHTML = 'Show CC Changes'; + var li = document.createElement('li'); + li.appendChild(a); + ul.appendChild(li); + }, + + previousElementSibling: function(el) { + if (el.previousElementSibling) + return el.previousElementSibling; + while (el = el.previousSibling) { + if (el.nodeType == 1) + return el; + } + }, + + nextElementSibling: function(el) { + if (el.nextElementSibling) + return el.nextElementSibling; + while (el = el.nextSibling) { + if (el.nodeType == 1) + return el; + } + }, + + htmlDecode: function(v) { + var e = document.createElement('div'); + e.innerHTML = v; + return e.childNodes.length == 0 ? '' : e.childNodes[0].nodeValue; + }, + + trim: function(s) { + return s.replace(/^\s+|\s+$/g, ''); + } +} diff --git a/extensions/InlineHistory/web/style.css b/extensions/InlineHistory/web/style.css new file mode 100644 index 000000000..bca3a197b --- /dev/null +++ b/extensions/InlineHistory/web/style.css @@ -0,0 +1,54 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * 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 InlineHistory Bugzilla Extension; + * Derived from the Bugzilla Tweaks Addon. + * + * The Initial Developer of the Original Code is the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 the Initial + * Developer. All Rights Reserved. + * + * Contributor(s): + * Johnathan Nightingale <johnath@mozilla.com> + * Ehsan Akhgari <ehsan@mozilla.com> + * Byron Jones <glob@mozilla.com> + * + * ***** END LICENSE BLOCK ***** + */ + +.ih_history { + background: none !important; + color: #444; +} + +.ih_inlinehistory { + font-weight: normal; + font-size: small; + color: #444; + border-top: 1px dotted #C8C8BA; + padding-top: 5px; +} + +.bz_comment.ih_history { + padding: 5px 5px 0px 5px +} + +.ih_history_item { + margin-bottom: 5px; +} + +.ih_hidden { + display: none; +} + +.ih_deleted { + text-decoration: line-through; +} diff --git a/extensions/InlineImages/Config.pm b/extensions/InlineImages/Config.pm new file mode 100644 index 000000000..77a1b09de --- /dev/null +++ b/extensions/InlineImages/Config.pm @@ -0,0 +1,33 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 InlineImages Bugzilla Extension. +# +# The Initial Developer of the Original Code is Guy Pyrzak +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Guy Pyrzak <guy.pyrzak@gmail.com> + +package Bugzilla::Extension::InlineImages; +use strict; + +use constant NAME => 'InlineImages'; + +use constant REQUIRED_MODULES => [ +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/extensions/InlineImages/Extension.pm b/extensions/InlineImages/Extension.pm new file mode 100644 index 000000000..dcfd76e1b --- /dev/null +++ b/extensions/InlineImages/Extension.pm @@ -0,0 +1,63 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 InlineImages Bugzilla Extension. +# +# The Initial Developer of the Original Code is Guy Pyrzak +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Guy Pyrzak <guy.pyrzak@gmail.com> +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::InlineImages; +use strict; +use base qw(Bugzilla::Extension); +use Bugzilla::Template; + +use constant NAME => 'InlineImages'; + +our $VERSION = '0.2'; + +sub bug_format_comment { + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; + + push(@$regexes, { + match => qr~\b(attachment\s*\#?\s*(\d+))~, + replace => \&_inlineAttachments + }); +} + +sub _inlineAttachments { + my $args = shift @_; + my $attachment_id = $args->{matches}->[1]; + my $attachment_string = $args->{matches}->[0]; + + # We need to call get_attachment_link because otherwise it will be skipped + my $msg = Bugzilla::Template::get_attachment_link($attachment_id, + $attachment_string); + + my $dbh = Bugzilla->dbh; + my ($mimetype) = + $dbh->selectrow_array('SELECT mimetype + FROM attachments WHERE attach_id = ?', + undef, $attachment_id); + if ($mimetype =~ /^image\/(gif|png|jpeg)$/) { + $msg =~ s/(?=name="attach_)/ class="is_image" /; + } + + return $msg; +}; + +__PACKAGE__->NAME; diff --git a/extensions/InlineImages/disabled b/extensions/InlineImages/disabled new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/extensions/InlineImages/disabled diff --git a/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl new file mode 100644 index 000000000..531c18981 --- /dev/null +++ b/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl @@ -0,0 +1,111 @@ +[%# +# 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 InlineImages Bugzilla Extension. +# +# The Initial Developer of the Original Code is Guy Pyrzak +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Guy Pyrzak <guy.pyrzak@gmail.com> +# Gervase Markham <gerv@gerv.net> +#%] + +[% IF Param("allow_attachment_display") %] +<script> + YAHOO.util.Event.onDOMReady(function() { + var Dom = YAHOO.util.Dom; + + // Don't bother doing this if there are no images as attachments + if (Dom.getElementsByClassName("is_image", "a", "comments").length == 0) { + return; + } + + var comments_expand_collapse = + Dom.getElementsByClassName('bz_collapse_expand_comments', + 'ul', + 'comments'); + + // Check that what we're looking for is here + if (comments_expand_collapse.length == 0) { + // Find the table we're looking for + var commentsTable = Dom.getElementsByClassName("bz_comment_table", + "table", + "comments"); + secondColumn = commentsTable[0].getElementsByTagName("td")[1]; + var newUL = document.createElement("ul"); + if (secondColumn) { + secondColumn.appendChild(newUL); + comments_expand_collapse[0] = newUL; + } + } + + // Insert the li into the dom + var li = document.createElement("li"); + var link = document.createElement("a"); + link.id = "toggle_images"; + link.href = "#"; + link.innerHTML = "Show Inline Images"; + link.onclick = YAHOO.bz_ext_inlineImage.toggleImages; + li.appendChild(link); + if (comments_expand_collapse.length > 0) { + comments_expand_collapse[0].appendChild(li); + } + + // Check to see if user has the inlineImagesCookie == on. + // If it is, go ahead and show images for the user + var inlineImagesCookie = YAHOO.util.Cookie.get("inlineImagesCookie"); + if (inlineImagesCookie && inlineImagesCookie == "on") { + YAHOO.bz_ext_inlineImage.toggleImages(); + } + }); + + var Dom = YAHOO.util.Dom; + YAHOO.namespace("bz_ext_inlineImage"); + YAHOO.bz_ext_inlineImage.toggleImages = function(event) { + var imgs = Dom.getElementsByClassName("inline_image", "img", "comments"); + var toggle_link_text = ""; + + if (imgs.length == 0) { + // Show inline images + + var alinks = Dom.getElementsByClassName("is_image", "a", "comments"); + for (var i = 0; i < alinks.length; i++) { + var img = document.createElement("img"); + img.src = alinks[i].href; + // Might want to add some support to hide obsolete images + img.className = "inline_image"; + img.style.display = "block"; + Dom.insertAfter(img, alinks[i].parentNode); + } + + YAHOO.util.Cookie.set("inlineImagesCookie", "on"); + toggle_link_text = "Hide Inline Images"; + } + else { + // Hide inline images + + for (var i = 0; i < imgs.length; i++) { + imgs[i].parentNode.removeChild(imgs[i]); + } + + YAHOO.util.Cookie.set("inlineImagesCookie", "off"); + toggle_link_text = "Show Inline Images"; + } + + var link = document.getElementById("toggle_images"); + link.innerHTML = toggle_link_text; + + return false; + } +</script> +[% END %] diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm new file mode 100644 index 000000000..4622f26bc --- /dev/null +++ b/extensions/LimitedEmail/Config.pm @@ -0,0 +1,38 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 LimitedEmail Extension. + +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::LimitedEmail; + +use strict; +use constant NAME => 'LimitedEmail'; +use constant REQUIRED_MODULES => [ ]; +use constant OPTIONAL_MODULES => [ ]; + +use constant FILTERS => [ + qr/^(glob|dkl|justdave)\@mozilla\.com$/i, + qr/^gerv\@mozilla\.org$/i, + qr/^reed\@reedloden\.com/i, +]; + +use constant BLACK_HOLE => 'nobody@mozilla.org'; + +__PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/Extension.pm b/extensions/LimitedEmail/Extension.pm new file mode 100644 index 000000000..253c3d900 --- /dev/null +++ b/extensions/LimitedEmail/Extension.pm @@ -0,0 +1,60 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 LimitedEmail Extension. + +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::LimitedEmail; +use strict; +use base qw(Bugzilla::Extension); + +our $VERSION = '1'; + +use Bugzilla::User; + +sub bugmail_recipients { + my ($self, $args) = @_; + foreach my $user_id (keys %{$args->{recipients}}) { + my $user = Bugzilla::User->new($user_id); + if (!deliver_to($user->email)) { + delete $args->{recipients}{$user_id}; + } + } +} + +sub mailer_before_send { + my ($self, $args) = @_; + my $email = $args->{email}; + if (!deliver_to($email->{header}->header('to'))) { + $email->{header}->header_set(to => Bugzilla::Extension::LimitedEmail::BLACK_HOLE); + } +} + +sub deliver_to { + my $email = shift; + my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS; + foreach my $re (@$ra_filters) { + if ($email =~ $re) { + return 1; + } + } + return 0; +} + +__PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/disabled b/extensions/LimitedEmail/disabled new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/extensions/LimitedEmail/disabled diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm new file mode 100644 index 000000000..778301fbb --- /dev/null +++ b/extensions/Profanivore/Config.pm @@ -0,0 +1,35 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Profanivore Bugzilla Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::Profanivore; +use strict; + +use constant NAME => 'Profanivore'; + +use constant REQUIRED_MODULES => [ + { + package => 'Regexp-Common', + module => 'Regexp::Common', + version => 0 + } +]; + +__PACKAGE__->NAME;
\ No newline at end of file diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm new file mode 100644 index 000000000..266ab9ff0 --- /dev/null +++ b/extensions/Profanivore/Extension.pm @@ -0,0 +1,86 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Profanivore Bugzilla Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::Profanivore; +use strict; +use base qw(Bugzilla::Extension); + +use Regexp::Common 'RE_ALL'; + +our $VERSION = '0.01'; + +sub bug_format_comment { + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; + my $comment = $args->{'comment'}; + + # Censor profanities if the comment author is not reasonably trusted. + # However, allow people to see their own profanities, which might stop + # them immediately noticing and trying to go around the filter. (I.e. + # it tries to stop an arms race starting.) + if ($comment && + !$comment->author->in_group('editbugs') && + $comment->author->id != Bugzilla->user->id) + { + push (@$regexes, { + match => RE_profanity(), + replace => \&_replace_profanity + }); + } +} + +sub _replace_profanity { + # We don't have access to the actual profanity. + return "****"; +} + +sub mailer_before_send { + my ($self, $args) = @_; + my $email = $args->{'email'}; + + my $author = $email->header("X-Bugzilla-Who"); + my $recipient = $email->header("To"); + + if ($author && $recipient) { + my $email_suffix = Bugzilla->params->{'emailsuffix'}; + if ($email_suffix ne '') { + $recipient =~ s/\Q$email_suffix\E$//; + $author =~ s/\Q$email_suffix\E$//; + } + + $author = new Bugzilla::User({ name => $author }); + $recipient = new Bugzilla::User({ name => $recipient }); + + if ($author->id && + !$author->in_group('editbugs') && + $author->id ne $recipient->id) + { + my $body = $email->body_str(); + + my $offensive = RE_profanity(); + $body =~ s/$offensive/****/g; + + $email->body_str_set($body); + } + } +} + +__PACKAGE__->NAME; diff --git a/extensions/Profanivore/README b/extensions/Profanivore/README new file mode 100644 index 000000000..5ccab103f --- /dev/null +++ b/extensions/Profanivore/README @@ -0,0 +1,14 @@ +Profanivore 'eats' English profanities in comments, leaving behind instead a +trail of droppings ('****'). It finds its food using a standard library Perl +regexp. The profanity is only eaten where the comment was written by a user +who does not have the global 'editbugs' privilege. The digestion happens at +display time, so the comment in the database is unaltered. + +However, it does not eat profanities when showing people their own comments; +the aim here is to prevent people immediately noticing they are being +censored, and getting 'creative'. + +The purpose of Profanivore is to make it a little harder for trolls to +vandalise public Bugzilla installations. + +It does not currently affect fields other than comments. diff --git a/extensions/REMO/Config.pm b/extensions/REMO/Config.pm new file mode 100644 index 000000000..625e2afd9 --- /dev/null +++ b/extensions/REMO/Config.pm @@ -0,0 +1,34 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 REMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <glob@mozilla.com> +# David Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::REMO; +use strict; + +use constant NAME => 'REMO'; + +use constant REQUIRED_MODULES => [ +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/extensions/REMO/Extension.pm b/extensions/REMO/Extension.pm new file mode 100644 index 000000000..a0091281b --- /dev/null +++ b/extensions/REMO/Extension.pm @@ -0,0 +1,184 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 REMO Bugzilla Extension. +# +# The Initial Developer of the Original Code is Mozilla Foundation +# Portions created by the Initial Developer are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <glob@mozilla.com> +# David Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::REMO; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Constants; +use Bugzilla::Util qw(trick_taint trim detaint_natural); +use Bugzilla::Token; +use Bugzilla::Error; + +our $VERSION = '0.01'; + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'remo-form-payment.html') { + _remo_form_payment($vars); + } +} + +sub _remo_form_payment { + my ($vars) = @_; + my $input = Bugzilla->input_params; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + if ($input->{'action'} eq 'commit') { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + + my $bug_id = $input->{'bug_id'}; + detaint_natural($bug_id); + my $bug = Bugzilla::Bug->check($bug_id); + + # Detect if the user already used the same form to submit again + my $token = trim($input->{'token'}); + if ($token) { + my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token); + if (!$creator_id + || $creator_id != $user->id + || $old_attach_id !~ "^remo_form_payment:") + { + # The token is invalid. + ThrowUserError('token_does_not_exist'); + } + + $old_attach_id =~ s/^remo_form_payment://; + if ($old_attach_id) { + ThrowUserError('remo_payment_cancel_dupe', + { bugid => $bug_id, attachid => $old_attach_id }); + } + } + + # Make sure the user can attach to this bug + if (!$bug->user->{'canedit'}) { + ThrowUserError("remo_payment_bug_edit_denied", + { bug_id => $bug->id }); + } + + # Make sure the bug is under the correct product/component + if ($bug->product ne 'Mozilla Reps' + || $bug->component ne 'Budget Requests') + { + ThrowUserError('remo_payment_invalid_product'); + } + + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + + $dbh->bz_start_transaction; + + # Create the comment to be added based on the form fields from rep-payment-form + my $comment; + $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment) + || ThrowTemplateError($template->error()); + $bug->add_comment($comment, { isprivate => 0 }); + + # Attach expense report + # FIXME: Would be nice to be able to have the above prefilled comment and + # the following attachments all show up under a single comment. But the longdescs + # table can only handle one attach_id per comment currently. At least only one + # email is sent the way it is done below. + my $attachment; + if (defined $cgi->upload('expenseform')) { + # Determine content-type + my $content_type = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create( + { bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('expenseform'), + description => 'Expense Form', + filename => scalar $cgi->upload('expenseform'), + ispatch => 0, + isprivate => 0, + isurl => 0, + mimetype => $content_type, + store_in_file => 0, + }); + + # Insert comment for attachment + $bug->add_comment('', { isprivate => 0, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id }); + } + + # Attach receipts file + if (defined $cgi->upload("receipts")) { + # Determine content-type + my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create( + { bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('receipts'), + description => "Receipts", + filename => scalar $cgi->upload("receipts"), + ispatch => 0, + isprivate => 0, + isurl => 0, + mimetype => $content_type, + store_in_file => 0, + }); + + # Insert comment for attachment + $bug->add_comment('', { isprivate => 0, + type => CMT_ATTACHMENT_CREATED, + extra_data => $attachment->id }); + } + + $bug->update($timestamp); + + if ($token) { + trick_taint($token); + $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef, + ("remo_form_payment:" . $attachment->id, $token)); + } + + $dbh->bz_commit_transaction; + + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; + $vars->{'bugs'} = [ new Bugzilla::Bug($bug_id) ]; + $vars->{'header_done'} = 1; + $vars->{'contenttypemethod'} = 'autodetect'; + + my $recipients = { 'changer' => $user }; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients); + + print $cgi->header(); + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/created.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + else { + $vars->{'token'} = issue_session_token('remo_form_payment:'); + } +} + +__PACKAGE__->NAME; diff --git a/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl new file mode 100644 index 000000000..29544d669 --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl @@ -0,0 +1,81 @@ +[%# 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 REMO Bugzilla Extension. + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Byron Jones <glob@mozilla.com> + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] +First Name: +[%+ cgi.param('first_name') %] + +Last Name: +[%+ cgi.param('last_name') %] + +Under 18 years old: +[%+ IF cgi.param('underage') %]Yes[% ELSE %]No[% END %] + +Sex: +[%+ cgi.param('sex') %] + +City: +[%+ cgi.param('city') %] + +Country: +[%+ cgi.param('country') %] + +Local Community: +[% IF cgi.param('community') %] +[%+ cgi.param('community') %] +[% ELSE %] +- +[% END %] + +IM: +[% IF cgi.param('im') %] +[%+ cgi.param('im') %] +[% ELSE %] +- +[% END %] + +References: +[% IF cgi.param('references') %] +[%+ cgi.param('references') %] +[% ELSE %] +- +[% END %] + +Currently Involved with Mozilla: +[% IF cgi.param('involved') %] +[%+ cgi.param('involved') %] +[% ELSE %] +- +[% END %] + +Languages Spoken: +[%+ cgi.param('languages') %] + +How did you lean about Mozilla Reps: +[%+ cgi.param('learn') %] + +What motivates you most about joining Mozilla Reps: +[%+ cgi.param('motivation') %] + +Comments: +[% IF cgi.param('comments') %] +[%+ cgi.param('comments') %] +[% ELSE %] +- +[% END %] diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl new file mode 100644 index 000000000..9486c56fe --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl @@ -0,0 +1,64 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] +[%# INTERFACE: + # This template has no interface. + # + # Form variables from a bug submission (i.e. the fields on a template from + # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to + # pull out various custom fields and format an initial Description entry + # from them. + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] + +Requester info: + +Requester: [% cgi.param('firstname') %] [%+ cgi.param('lastname') %] +Wiki user profile: [% cgi.param('wikiprofile') %] +Event wiki page: [% cgi.param('wikipage') %] +Advance payment needed: [% IF cgi.param('advancepayment') %]Yes[% ELSE %]No[% END %] + +Budget breakdown: + +Total amount requested in $USD: [% cgi.param('budgettotal') %] +Costs per service: +Service 1: [% cgi.param('service1') %] Cost: [% cgi.param('cost1') %] +Service 2: [% cgi.param('service2') %] Cost: [% cgi.param('cost2') %] +Service 3: [% cgi.param('service3') %] Cost: [% cgi.param('cost3') %] +Service 4: [% cgi.param('service4') %] Cost: [% cgi.param('cost4') %] +Service 5: [% cgi.param('service5') %] Cost: [% cgi.param('cost5') %] + +Additional costs: (add comment box) +[% cgi.param('costadditional') %] + +Success measurement: + +How will the event help push the Mozilla project forward? +[%+ cgi.param('successmeasure') %] + +Metric 1: [% cgi.param('metric1') %] Success scenario: [% cgi.param('success1') %] +Metric 2: [% cgi.param('metric2') %] Success scenario: [% cgi.param('success2') %] +Metric 3: [% cgi.param('metric3') %] Success scenario: [% cgi.param('success3') %] + +Additional information: +[%+ cgi.param('successadditional') %] + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl new file mode 100644 index 000000000..985a8924d --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl @@ -0,0 +1,72 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Netscape Communications + # Corporation. Portions created by Netscape are + # Copyright (C) 1998 Netscape Communications Corporation. All + # Rights Reserved. + # + # Contributor(s): Gervase Markham <gerv@gerv.net> + #%] +[%# INTERFACE: + # This template has no interface. + # + # Form variables from a bug submission (i.e. the fields on a template from + # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to + # pull out various custom fields and format an initial Description entry + # from them. + #%] +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] + +Requester info: + +First name: [% cgi.param('firstname') %] +Last name: [% cgi.param('lastname') %] +Wiki user profile: [% cgi.param('wikiprofile') %] +Event name: [% cgi.param('eventname') %] +Event wiki page: [% cgi.param('wikipage') %] +Estimated attendance: [% cgi.param('attendance') %] + +Shipping details: + +Ship swag before: [% cgi.param('cf_due_date') %] + +First name: [% cgi.param("shiptofirstname") %] +Last name: [% cgi.param("shiptolastname") %] +Address line 1: [% cgi.param("shiptoaddress1") %] +Address line 2: [% cgi.param("shiptoaddress2") %] +City: [% cgi.param("shiptocity") %] +State/Region: [% cgi.param("shiptostate") %] +Postal code: [% cgi.param("shiptopcode") %] +Country: [% cgi.param("shiptocountry") %] +Phone: [% cgi.param("shiptophone") %] +[%+ IF cgi.param("shiptoidrut") %]Custom reference: [% cgi.param("shiptoidrut") %][% END %] + +Addition information for delivery person: +[%+ cgi.param('shipadditional') %] + +Swag requested: + +Stickers: [% IF cgi.param('stickers') %]Yes[% ELSE %]No[% END %] +Buttons: [% IF cgi.param('buttons') %]Yes[% ELSE %]No[% END %] +Posters: [% IF cgi.param('posters') %]Yes[% ELSE %]No[% END %] +Lanyards: [% IF cgi.param('lanyards') %]Yes[% ELSE %]No[% END %] +T-shirts: [% IF cgi.param('tshirts') %]Yes[% ELSE %]No[% END %] +Roll-up banners: [% IF cgi.param('rollupbanners') %]Yes[% ELSE %]No[% END %] +Horizontal banners: [% IF cgi.param('horizontalbanners') %]Yes[% ELSE %]No[% END %] +Booth cloth: [% IF cgi.param('boothcloth') %]Yes[% ELSE %]No[% END %] +Pens: [% IF cgi.param('pens') %]Yes[% ELSE %]No[% END %] +Other: [% IF cgi.param('otherswag') %][% cgi.param('otherswag') %][% ELSE %]No[% END %] + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl new file mode 100644 index 000000000..8b126f9dd --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl @@ -0,0 +1,204 @@ +[%# 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 REMO Bugzilla Extension. + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Byron Jones <glob@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps - Application Form" + style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ] +%] + +[% USE Bugzilla %] +[% mandatory = '<span class="mandatory" title="Required">*</span>' %] + +<script type="text/javascript"> +var Dom = YAHOO.util.Dom; + +function mandatory(ids) { + result = true; + for (i in ids) { + id = ids[i]; + el = Dom.get(id); + + if (el.type.toString() == "checkbox") { + value = el.checked; + } else { + value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); + el.value = value; + } + + if (value == '') { + Dom.addClass(id, 'missing'); + result = false; + } else { + Dom.removeClass(id, 'missing'); + } + } + return result; +} + +function submitForm() { + if (!mandatory([ 'first_name', 'last_name', 'sex', 'city', 'country', + 'languages', 'learn', 'motivation', 'privacy' ]) + ) { + alert('Please enter all the required fields.'); + return false; + } + + Dom.get('short_desc').value = + "Application Form: " + Dom.get('first_name').value + ' ' + Dom.get('last_name').value; + + return true; +} + +</script> + +<noscript> +<h1>Javascript is required to use this form.</h1> +</noscript> + +<h1>Mozilla Reps - Application Form</h1> + +<form method="post" action="post_bug.cgi" id="tmRequestForm"> +<input type="hidden" name="product" value="Mozilla Reps"> +<input type="hidden" name="component" value="Mentorship"> +<input type="hidden" name="bug_severity" value="normal"> +<input type="hidden" name="rep_platform" value="All"> +<input type="hidden" name="priority" value="--"> +<input type="hidden" name="op_sys" value="Other"> +<input type="hidden" name="version" value="unspecified"> +<input type="hidden" name="groups" value="mozilla-reps"> +<input type="hidden" name="format" value="[% format FILTER html %]"> +<input type="hidden" name="created-format" value="[% format FILTER html %]"> +<input type="hidden" name="comment" id="comment" value=""> +<input type="hidden" name="short_desc" id="short_desc" value=""> + +<table id="reps-form"> + +<tr class="odd"> + <th>First Name:[% mandatory %]</th> + <td><input id="first_name" name="first_name" size="40" placeholder="John"></td> +</tr> + +<tr class="even"> + <th>Last Name:[% mandatory %]</th> + <td><input id="last_name" name="last_name" size="40" placeholder="Doe"></td> +</tr> + +<tr class="odd"> + <th>Are you under 18 years old?:</th> + <td><input type="checkbox" id="underage" name="underage" value="1"></td> +</tr> + +<tr class="even"> + <th>Sex:[% mandatory %]</th> + <td> + <select id="sex" name="sex"> + <option value="Male">Male</option> + <option value="Female">Female</option> + <option value="Other">Other</option> + </select> + </td> +</tr> + +<tr class="odd"> + <th>City:[% mandatory %]</th> + <td><input id="city" name="city" size="40" placeholder="Your city"></td> +</tr> + +<tr class="even"> + <th>Country:[% mandatory %]</th> + <td><input id="country" name="country" size="40" placeholder="Your country"></td> +</tr> + +<tr class="odd"> + <th>Local Community you participate in:</th> + <td><input id="community" name="community" size="40" placeholder="Name of your community"></td> +</tr> + +<tr class="even"> + <th>IM (specify service):</th> + <td><input id="im" name="im" size="40"></td> +</tr> + +<tr class="odd"> + <th colspan="2"> + References: + </th> +</tr> +<tr class="odd"> + <td colspan="2"> + <textarea id="references" name="references" rows="4" + placeholder="Add contact info of people referencing you."></textarea> + </td> +</tr> + +<tr class="even"> + <th colspan="2"> + How are you involved with Mozilla? + </th> +</tr> +<tr class="even"> + <td colspan="2"> + <textarea id="involved" name="involved" rows="4" placeholder="Add-ons, l10n, SUMO, QA, ..."></textarea> + </td> +</tr> + +<tr class="odd"> + <th>Languages Spoken:[% mandatory %]</th> + <td><input id="languages" name="languages" size="40"></td> +</tr> + +<tr class="even"> + <th>How did you learn about Mozilla Reps?[% mandatory %]</th> + <td><input id="learn" name="learn" size="40"></td> +</tr> + +<tr class="odd"> + <th colspan="2">What motivates you most about joining Mozilla Reps?[% mandatory %]</th> +</tr> +<tr class="odd"> + <td colspan="2"><textarea id="motivation" name="motivation" rows="4"></textarea></td> +</tr> + +<tr class="even"> + <th colspan="2">Comments:</th> +</tr> +<tr class="even"> + <td colspan="2"><textarea id="comments" name="comments" rows="4"></textarea></td> +</tr> + +<tr class="odd"> + <th> + I have read the + <a href="http://www.mozilla.com/en-US/privacy-policy" target="_blank">Mozilla Privacy Policy</a>:[% mandatory %] + </th> + <td><input id="privacy" type="checkbox"></td> +</tr> + +<tr class="even"> + <td> </td> + <td align="right"><input type="submit" value="Submit" onclick="return submitForm()"></td> +</tr> + +</table> + +</form> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl new file mode 100644 index 000000000..267b25e3a --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl @@ -0,0 +1,254 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps Budget Request Form" + style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ] + javascript_urls = [ 'extensions/REMO/web/js/form_validate.js', + 'js/util.js', + 'js/field.js' ] +%] + +[% IF user.in_group("mozilla-reps") %] + +<p>These requests will only be visible to the person who submitted the request, +any persons designated in the CC line, and authorized members of the Mozilla +Rep team.</p> + +<script language="javascript" type="text/javascript"> +function trySubmit() { + var firstname = document.getElementById('firstname').value; + var lastname = document.getElementById('lastname').value; + var wikipage = document.getElementById('wikipage').value; + var shortdesc = 'Budget Request - ' + firstname + ' ' + lastname + ' - ' + wikipage; + document.getElementById('short_desc').value = shortdesc; + return true; +} + +function validateAndSubmit() { + var alert_text = ''; + if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n"; + if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n"; + if(!isFilledOut('wikiprofile')) alert_text += "Please enter a wiki user profile.\n"; + if(!isFilledOut('wikipage')) alert_text += "Please enter a wiki page address.\n"; + if(!isFilledOut('successmeasure')) alert_text += "Please enter how you will measure the success of the event.\n"; + if(!isFilledOut('budgettotal')) alert_text += "Please enter the total budget for the event.\n"; + if(!isFilledOut('service1') || !isFilledOut('cost1')) alert_text += "Please enter at least one service and cost value.\n"; + if(!isFilledOut('metric1') || !isFilledOut('success1')) alert_text += "Please enter at least one metric and success scenario.\n"; + + //Everything required is filled out..try to submit the form! + if(alert_text == '') { + return trySubmit(); + } + + //alert text, stay here on the pagee + alert(alert_text); + return false; +} + +function togglePaymentInfo (e) { + var div = document.getElementById('paymentinfo'); + if (e.checked == true) { + div.style.display = 'block'; + } + else { + div.style.display = 'none'; + } +} + +</script> + +<h1>Mozilla Reps - Budget Request Form</h1> + +<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data" + onSubmit="return validateAndSubmit();"> + + <input type="hidden" name="format" value="remo-budget"> + <input type="hidden" name="product" value="Mozilla Reps"> + <input type="hidden" name="component" value="Budget Requests"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="unspecified"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-reps"> + +<table id="reps-form"> + +<tr class="odd"> + <td><strong>First Name: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John"> + </td> +</tr> + +<tr class="even"> + <td><strong>Last Name: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe"> + </td> +</tr> + +<tr class="odd"> + <td><strong>Wiki user profile:<span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="wikiprofile" id="wikiprofile" value="" size="40" placeholder="JohnDoe"> + </td> +</tr> + +<tr class="even"> + <td><strong>Event wiki page: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="wikipage" id="wikipage" value="" size="40" placeholder="http://wiki.mozilla.org/JohnDoe"> + </td> +</tr> + +<tr class="odd"> + <td><!--spacer--> </td> + <td><!--spacer--> </td> +</tr> + +<tr class="even"> + <td colspan="2"> + <strong>Is advance payment needed?</strong> + <input type="checkbox" name="advancepayment" id="advancepayment" value="1" + onchange="togglePaymentInfo(this);"> + <div id="paymentinfo" style="display:none;"> + Please send an email to William at mozilla.com with all the information below:<br> + <br> + Payment information:<br> + Bank name:<br> + Bank address: <br> + IBAN:<br> + Swift code/BIC:<br> + Additional bank details (if necessary): + </div> + </td> +</tr> + +<tr class="odd"> + <td><!--spacer--> </td> + <td><!--spacer--> </td> +</tr> + +<tr class="even"> + <td colspan="2"> + <strong>Budget breakdown:</strong><br> + Total amount requested in $USD: <span style="color: red;">*</span> + <input type="text" name="budgettotal" id="budgettotal" value="" size="40"><br> + Costs per service: + <table> + <tr> + <td>Service 1: <span style="color: red;">*</span></td> + <td><input type="text" id="service1" name="service1" size="30"></td> + <td>Cost 1: <span style="color: red;">*</span></td> + <td><input type="text" id="cost1" name="cost1" size="30"></td> + </tr> + <tr> + <td>Service 2:</td> + <td><input type="text" id="service2" name="service2" size="30"></td> + <td>Cost 2:</td> + <td><input type="text" id="cost2" name="cost2" size="30"></td> + </tr> + <tr> + <td>Service 3:</td> + <td><input type="text" id="service3" name="service3" size="30"></td> + <td>Cost 3:</td> + <td><input type="text" id="cost3" name="cost3" size="30"></td> + </tr> + <tr> + <td>Service 4:</td> + <td><input type="text" id="service4" name="service4" size="30"></td> + <td>Cost 4:</td> + <td><input type="text" id="cost4" name="cost4" size="30"></td> + </tr> + <tr> + <td>Service 5:</td> + <td><input type="text" id="service5" name="service5" size="30"></td> + <td>Cost 5:</td> + <td><input type="text" id="cost5" name="cost5" size="30"></td> + </tr> + </table> + Additional costs:<br> + <textarea id="costadditional" name="costadditional" rows="5" cols="50"></textarea> + </td> +</tr> + +<tr class="odd"> + <td><!--spacer--> </td> + <td><!--spacer--> </td> +</tr> + +<tr class="even"> + <td colspan="2"> + <strong>Success measurement:</strong><br> + How will the event help push the Mozilla project forward? + <span style="color: red;">*</span><br> + <textarea id="successmeasure" name="successmeasure" rows="5" cols="50"></textarea> + <table> + </tr> + <td>Metric 1: <span style="color: red;">*</span></td> + <td><input type="text" id="metric1" name="metric1" size="30"> + <td>Success scenario: <span style="color: red;">*</span></td> + <td><input type="text" id="success1" name="success1" size="30"> + </tr> + <tr> + <td>Metric 2:</td> + <td><input type="text" id="metric2" name="metric2" size="30"> + <td>Success scenario:</td> + <td><input type="text" id="success2" name="success2" size="30"> + </tr> + <tr> + <td>Metric 3:</td> + <td><input type="text" id="metric3" name="metric3" size="30"> + <td>Success scenario:</td> + <td><input type="text" id="success3" name="success3" size="30"> + </tr> + </table> + Additional information:<br> + <textarea id="successadditional" name="successadditional" rows="5" cols="50"></textarea> + </td> +</tr> + +<tr class="odd"> + <td> </td> + <td align="right"> + <input type="submit" id="commit" value="Submit Request"> + </td> +</tr> + +</table> + +</form> + +<p> + <strong><span style="color: red;">*</span></strong> - Required field<br /> + Thanks for contacting us. +</p> + +[% ELSE %] + <p>Sorry, you do not have access to this page.</p> +[% END %] + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl new file mode 100644 index 000000000..ae24e667a --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl @@ -0,0 +1,305 @@ +[%# 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. + # + # The Initial Developer of the Original Code is Mozilla Corporation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla + # Corporation. All Rights Reserved. + # + # Contributor(s): Reed Loden <reed@mozilla.com> + # David Tran <dtran@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps Swag Request Form" + javascript_urls = [ 'extensions/REMO/web/js/swag.js', + 'extensions/REMO/web/js/form_validate.js', + 'js/field.js', + 'js/util.js' ] + style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ] + yui = [ 'calendar' ] +%] + +[% IF user.in_group("mozilla-reps") %] + +<p>These requests will only be visible to the person who submitted the request, +any persons designated in the CC line, and authorized members of the Mozilla Rep team.</p> + +<script language="javascript" type="text/javascript"> +function trySubmit() { + var eventname = document.getElementById('eventname').value; + var shortdesc = 'Swag Request - ' + eventname; + document.getElementById('short_desc').value = shortdesc; + return true; +} + +function validateAndSubmit() { + var alert_text = ''; + if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n"; + if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n"; + if(!isFilledOut('wikiprofile')) alert_text += "Please enter your wiki user profile\n"; + if(!isFilledOut('eventname')) alert_text += "Please enter your event name\n"; + if(!isFilledOut('wikipage')) alert_text += "Please enter the event wiki page.\n"; + if(!isFilledOut('attendance')) alert_text += "Please enter the estimated attendance.\n"; + if(!isFilledOut('shiptofirstname')) alert_text += "Please enter the shipping first name\n"; + if(!isFilledOut('shiptolastname')) alert_text += "Please enter the shipping last name\n"; + if(!isFilledOut('shiptoaddress1')) alert_text += "Please enter the ship to address\n"; + if(!isFilledOut('shiptocity')) alert_text += "Please enter the ship to city\n"; + if(!isFilledOut('shiptocountry')) alert_text += "Please enter the ship to country\n"; + if(!isFilledOut('shiptopcode')) alert_text += "Please enter the ship to postal code\n"; + if(!isFilledOut('shiptophone')) alert_text += "Please enter the ship to contact number\n"; + + //Everything required is filled out..try to submit the form! + if(alert_text == '') { + return trySubmit(); + } + + //alert text, stay here on the pagee + alert(alert_text); + return false; +} + +</script> + +<h1>Mozilla Reps - Swag Request Form</h1> + +<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data" + onSubmit="return validateAndSubmit();"> + + <input type="hidden" name="format" value="remo-swag"> + <input type="hidden" name="product" value="Mozilla Reps"> + <input type="hidden" name="component" value="Swag Requests"> + <input type="hidden" name="rep_platform" value="All"> + <input type="hidden" name="op_sys" value="Other"> + <input type="hidden" name="priority" value="--"> + <input type="hidden" name="version" value="unspecified"> + <input type="hidden" name="bug_severity" id="bug_severity" value="normal"> + <input type="hidden" name="short_desc" id="short_desc" value=""> + <input type="hidden" name="groups" value="mozilla-reps"> + +<table id="reps-form"> + +<tr class="odd"> + <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <input type="text" name="firstname" id="firstname" placeholder="John" size="40"> + </td> +</tr> + +<tr class="even"> + <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <input type="text" name="lastname" id="lastname" placeholder="Doe" size="40"> + </td> +</tr> + +<tr class="odd"> + <td><strong>Wiki User Profile: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <input type="text" name="wikiprofile" id="wikiprofile" size="40"> + </td> +</tr> + +<tr class="even"> + <td><strong>Event Name: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <input type="text" name="eventname" id="eventname" size="40"> + </td> +</tr> + +<tr class="odd"> + <td><strong>Event Wiki Page: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <input type="text" name="wikipage" id="wikipage" size="40"> + </td> +</tr> + +<tr class="even"> + <td><strong>Estimated Attendance: <span style="color: red;" title="Required">*</span></strong></td> + <td> + <select id="attendance" name="attendance"> + <option value="1-50">1-50</option> + <option value="51-200">51-200</option> + <option value="201-500">201-500</option> + <option value="501-1000+">501-1000+</option> + </select> + </td> +</tr + +<tr class="odd"> + <td><!--spacer--> </td> + <td><!--spacer--> </td> +</tr> + +<tr class="even"> + <td colspan="2"><strong>Shipping Details:</strong></td> +</tr> + +<tr class="odd"> + <td><strong>Ship Before:</strong> + <td> + [% INCLUDE bug/field.html.tmpl + bug = default, + field = bug_fields.cf_due_date + value = default.cf_due_date, + editable = 1, + no_tds = 1 + %] + </td> +</tr> + +<tr class="even"> + <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptofirstname" id="shiptofirstname" placeholder="John" size="40"></td> +</tr> + +<tr class="odd"> + <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptolastname" id="shiptolastname" placeholder="Doe" size="40"></td> +</tr> + +<tr class="even"> + <td><strong>Address Line 1: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptoaddress1" id="shiptoaddress1" placeholder="123 Main St." size="40"></td> +</tr> + +<tr class="odd"> + <td><strong>Address Line 2:</strong></td> + <td><input name="shiptoaddress2" id="shiptoaddress2" size="40"></td> +</tr> + +<tr class="even"> + <td><strong>City: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptocity" id="shiptocity" size="40" placeholder="Anytown"></td> +</tr> + +<tr class="odd"> + <td><strong>State/Region (if applicable):</strong></td> + <td><input name="shiptostate" id="shiptostate" placeholder="CA" size="40"></td> +</tr> + +<tr class="even"> + <td><strong>Country: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptocountry" id="shiptocountry" placeholder="USA" size="40"></td> +</tr> + +<tr class="odd"> + <td><strong>Postal Code: <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptopcode" id="shiptopcode" placeholder="90210" size="40"></td> +</tr> + +<tr class="even"> + <td><strong>Phone (including country code): <span style="color: red;" title="Required">*</span></strong></td> + <td><input name="shiptophone" id="shiptophone" placeholder="919-555-1212" size="40"></td> +</tr> + +<tr class="odd"> + <td><strong>Custom Reference (Fiscal or VAT-number, if known):</strong><br><small>(if your country requires this)</small></td> + <td><input name="shiptoidrut" id="shiptoidrut" size="40"></td> +</tr> + +<tr class="even"> + <td colspan="2"> + <strong>Addition information for delivery person:</strong><br> + <textarea id="shipadditional" name="shipadditional" rows="4"></textarea> + </td> +</tr> + +<tr class="even"> + <td><!--spacer--> </td> + <td><!--spacer--> </td> +</tr> + +<tr class="odd"> + <td colspan="2"><strong>Swag Requested:</strong></td> +</tr> + +<tr class="even"> + <td><strong>Stickers:</strong></td> + <td><input type="checkbox" id="stickers" name="stickers" value="1"></td> +</tr> + +<tr class="odd"> + <td><strong>Buttons:</strong></td> + <td><input type="checkbox" id="buttons" name="buttons" value="1"></td> +</tr> + +<tr class="even"> + <td><strong>Posters:</strong></td> + <td><input type="checkbox" id="posters" name="posters" value="1"></td> +</tr> + +<tr class="odd"> + <td><strong>Lanyards:</strong></td> + <td><input type="checkbox" id="lanyards" name="lanyards" value="1"></td> +</tr> + +<tr class="even"> + <td><strong>T-Shirts:</strong></td> + <td><input type="checkbox" id="tshirts" name="tshirts" value="1"></td> +</tr> + +<tr class="odd"> + <td><strong>Roll-Up Banners:</strong></td> + <td><input type="checkbox" id="rollupbanners" name="rollupbanners" value="1"></td> +</tr> + +<tr class="even"> + <td><strong>Horizontal Banner:</strong></td> + <td><input type="checkbox" id="horizontalbanner" name="horizontalbanner" value="1"></td> +</tr> + +<tr class="odd"> + <td><strong>Booth Cloth:</strong></td> + <td><input type="checkbox" id="boothcloth" name="boothcloth" value="1"></td> +</tr> + +<tr class="even"> + <td><strong>Pens:</strong></td> + <td><input type="checkbox" id="pens" name="pens" value="1"></td> +</tr> + +<tr class="odd"> + <td><strong>Other:</strong> (please specify)</td> + <td><input type="text" id="otherswag" name="otherswag" size="40"></td> +</tr> + +<tr class="even"> + <td> </td> + <td align="right"> + <input type="submit" id="commit" value="Submit Request"> + </td> +</tr> + +</table> + +<p> + Quantities of different swag items requested that will actually be shipped + depend on stock availability and number of attendees. Mozilla cannot guarantee + that all items requested will be in stock at the time of shipment and you will + be notified in case an item cannot be shipped. Please request swag at least 1 + month before desired delivery date. +</p> + +<p> + <strong><span style="color: red;">*</span></strong> - Required field<br /> + Thanks for contacting us. + You will be notified by email of any progress made in resolving your request. +</p> + +[% ELSE %] + <p>Sorry, you do not have access to this page.</p> +[% END %] + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl new file mode 100644 index 000000000..378ab45d0 --- /dev/null +++ b/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl @@ -0,0 +1,38 @@ +[%# 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 REMO Bugzilla Extension. + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Byron Jones <glob@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps - Application Form" + +%] + +<h1>Thank you!</h1> + +<p> +Thank you for submitting your Mozilla Reps Application Form. A Mozilla Rep +mentor will contact you shortly at your bugzilla email address. +</p> + +<p style="font-size: x-small"> +Reference: <a href="show_bug.cgi?id=[% id %]">#[% id %]</a> +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..200e678be --- /dev/null +++ b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,40 @@ +[%# 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 REMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + # David Lawrence <dkl@mozilla.com> + #%] + +[% IF error == "remo_payment_invalid_product" %] + [% title = "Mozilla Reps Payment Invalid Bug" %] + You can only attach budget payment information to [% terms.bugs %] under + the product 'Mozilla Reps' and component 'Budget Requests'. + +[% ELSIF error == "remo_payment_bug_edit_denied" %] + [% title = "Mozilla Reps Payment Bug Edit Denied" %] + You do not have permission to edit [% terms.bug %] '[% bug_id FILTER html %]'. + +[% ELSIF error == "remo_payment_cancel_dupe" %] + [% title = "Already filed payment request" %] + You already used the form to file + <a href="[% urlbase FILTER html %]attachment.cgi?id=[% attachid FILTER uri %]&action=edit"> + attachment [% attachid FILTER uri %]</a>.<br> + <br> + You can either <a href="[% urlbase FILTER html %]page.cgi?id=remo-form-payment.html"> + create a new payment request</a> or [% "go back to $terms.bug $bugid" FILTER bug_link(bugid) FILTER none %]. + +[% END %] diff --git a/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl b/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl new file mode 100644 index 000000000..95c0af6e8 --- /dev/null +++ b/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl @@ -0,0 +1,37 @@ +[%# 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 REMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Dave Lawrence <dkl@mozilla.com> + #%] + +[% USE Bugzilla %] +[% cgi = Bugzilla.cgi %] + +Mozilla Reps Payment Request +---------------------------- + +Requester info: + +First name: [% cgi.param('firstname') %] +Last name: [% cgi.param('lastname') %] +Wiki user profile: [% cgi.param('wikiprofile') %] +Event wiki page: [% cgi.param('wikipage') %] +Budget request [% terms.bug %]: [% cgi.param('bug_id') %] +Have you already received payment for this event? [% IF cgi.param('receivedpayment') %]Yes[% ELSE %]No[% END %] + +[%+ cgi.param("comment") IF cgi.param("comment") %] + diff --git a/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl b/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl new file mode 100644 index 000000000..0f5f206d3 --- /dev/null +++ b/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl @@ -0,0 +1,243 @@ +[%# 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 REMO Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Dave Lawrence <dkl@mozilla.com> + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% PROCESS global/header.html.tmpl + title = "Mozilla Reps Payment Form" + style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ] + javascript_urls = [ 'extensions/REMO/web/js/form_validate.js', + 'js/util.js', + 'js/field.js' ] + yui = ['connection', 'json'] +%] + +<script language="javascript" type="text/javascript"> + +var bug_cache = {}; + +function validateAndSubmit() { + var alert_text = ''; + if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n"; + if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n"; + if(!isFilledOut('wikiprofile')) alert_text += "Please enter a wiki user profile.\n"; + if(!isFilledOut('wikipage')) alert_text += "Please enter a wiki page address.\n"; + if(!isFilledOut('bug_id')) alert_text += "Please enter a valid [% terms.bug %] id to attach this additional information to.\n"; + if(!isFilledOut('expenseform')) alert_text += "Please enter an expense form to upload.\n"; + if(!isFilledOut('receipts')) alert_text += "Please enter a receipts file to upload.\n"; + + if (alert_text) { + alert(alert_text); + return false; + } + + return true; +} + +function togglePaymentInfo (e) { + var div = document.getElementById('paymentinfo'); + if (e.checked == false) { + div.style.display = 'block'; + } + else { + div.style.display = 'none'; + } +} + +function getBugInfo (e, div) { + var bug_id = e.value; + div = document.getElementById(div); + + if (!bug_id) { + div.innerHTML = ""; + return true; + } + + div.style.display = 'block'; + + if (bug_cache[bug_id]) { + div.innerHTML = bug_cache[bug_id]; + e.disabled = false; + return true; + } + + e.disabled = true; + div.innerHTML = 'Getting [% terms.bug %] info...'; + + YAHOO.util.Connect.setDefaultPostHeader('application/json', true); + YAHOO.util.Connect.asyncRequest( + 'POST', + 'jsonrpc.cgi', + { + success: function(res) { + var bug_message = ""; + data = YAHOO.lang.JSON.parse(res.responseText); + if (data.error) { + bug_message = "Get [% terms.bug %] failed: " + data.error.message; + } + else if (data.result) { + if (data.result.bugs[0].product !== 'Mozilla Reps' + || data.result.bugs[0].component !== 'Budget Requests') + { + bug_message = "You can only attach budget payment " + + "information to [% terms.bugs %] under the product " + + "'Mozilla Reps' and component 'Budget Requests'."; + } + else { + bug_message = "[% terms.Bug %] " + bug_id + " - " + data.result.bugs[0].status + + " - " + data.result.bugs[0].summary; + } + } + else { + bug_message = "Get [% terms.bug %] failed: " + res.responseText; + } + div.innerHTML = bug_message; + bug_cache[bug_id] = bug_message; + e.disabled = false; + }, + failure: function(res) { + if (res.responseText) { + div.innerHTML = "Get [% terms.bug %] failed: " + res.responseText; + } + } + }, + YAHOO.lang.JSON.stringify({ + version: "1.1", + method: "Bug.get", + id: bug_id, + params: { + ids: [ bug_id ], + include_fields: [ 'product', 'component', 'status', 'summary' ] + } + }) + ); +} + +</script> + +<h1>Mozilla Reps - Payment Form</h1> + +<form method="post" action="page.cgi" id="paymentForm" enctype="multipart/form-data" + onSubmit="return validateAndSubmit();"> +<input type="hidden" id="id" name="id" value="remo-form-payment.html"> +<input type="hidden" id="token" name="token" value="[% token FILTER html %]"> +<input type="hidden" id="action" name="action" value="commit"> + +<table id="reps-form"> + +<tr class="odd"> + <td width="25%"><strong>First Name: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John"> + </td> +</tr> + +<tr class="even"> + <td><strong>Last Name: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe"> + </td> +</tr> + +<tr class="odd"> + <td><strong>Wiki user profile:<span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="wikiprofile" id="wikiprofile" value="" size="40" placeholder="JohnDoe"> + </td> +</tr> + +<tr class="even"> + <td><strong>Event wiki page: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="wikipage" id="wikipage" value="" size="40"> + </td> +</tr> + +<tr class="odd"> + <td><strong>Budget request [% terms.bug %]: <span style="color: red;">*</span></strong></td> + <td> + <input type="text" name="bug_id" id="bug_id" value="" size="40" + onblur="getBugInfo(this,'bug_info');")> + </td> +</tr> + +<tr class="odd"> + <td colspan="2"> + <div id="bug_info" style="display:none;"></div> + </td> +</tr> + +<tr class="even"> + <td colspan="2"> + <strong>Have you already received payment for this event?</strong> + <input type="checkbox" name="receivedpayment" id="receivedpayment" value="1" + onchange="togglePaymentInfo(this);" checked="true"> + <div id="paymentinfo" style="display:none;"> + Please send an email to William at mozilla.com with all the information below:<br> + <br> + Payment information:<br> + Bank name:<br> + Bank address: <br> + IBAN:<br> + Swift code/BIC:<br> + Additional bank details (if necessary): + </div> + </td> +</tr> + +<tr class="odd"> + <td colspan="2"> + <strong>Expense form and scanned receipts/invoices:</strong> + </td> +</tr> + +<tr class="odd"> + <td>Expense Form: <span style="color: red;">*</span></td> + <td><input type="file" id="expenseform" name="expenseform" size="40"></td> +</tr> + +<tr class="odd"> + <td valign="top">Receipts File: <span style="color: red;">*</span></td> + <td> + <input type="file" id="receipts" name="receipts" size="40"><br> + <font style="color:red;"> + Please black out any bank account information included<br> + on receipts before attaching them. + </font> + </td> +</tr> + +<tr class="even"> + <td> </td> + <td align="right"> + <input type="submit" id="commit" value="Submit Request"> + </td> +</tr> + +</table> + +</form> + +<p> + <strong><span style="color: red;">*</span></strong> - Required field<br> + Thanks for contacting us. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/REMO/web/js/form_validate.js b/extensions/REMO/web/js/form_validate.js new file mode 100644 index 000000000..6c8fa6f07 --- /dev/null +++ b/extensions/REMO/web/js/form_validate.js @@ -0,0 +1,21 @@ +/** + * Some Form Validation and Interaction + **/ +//Makes sure that there is an '@' in the address with a '.' +//somewhere after it (and at least one character in between them + +function isValidEmail(email) { + var at_index = email.indexOf("@"); + var last_dot = email.lastIndexOf("."); + return at_index > 0 && last_dot > (at_index + 1); +} + +//Takes a DOM element id and makes sure that it is filled out +function isFilledOut(elem_id) { + var str = document.getElementById(elem_id).value; + return str.length>0 && str!="noneselected"; +} + +function isChecked(elem_id) { + return document.getElementById(elem_id).checked; +} diff --git a/extensions/REMO/web/js/swag.js b/extensions/REMO/web/js/swag.js new file mode 100644 index 000000000..47886b2a9 --- /dev/null +++ b/extensions/REMO/web/js/swag.js @@ -0,0 +1,60 @@ +/** + * Swag Request Form Functions + * Form Interal Swag Request Form + * dtran + * 7/6/09 + **/ + + +function evalToNumber(numberString) { + if(numberString=='') return 0; + return parseInt(numberString); +} + +function evalToNumberString(numberString) { + if(numberString=='') return '0'; + return numberString; +} +//item_array should be an array of DOM element ids +function getTotal(item_array) { + var total = 0; + for(var i in item_array) { + total += evalToNumber(document.getElementById(item_array[i]).value); + } + return total; +} + +function calculateTotalSwag() { + document.getElementById('Totalswag').value = + getTotal( new Array('Lanyards', + 'Stickers', + 'Bracelets', + 'Tattoos', + 'Buttons', + 'Posters')); + +} + + +function calculateTotalMensShirts() { + document.getElementById('mens_total').value = + getTotal( new Array('mens_s', + 'mens_m', + 'mens_l', + 'mens_xl', + 'mens_xxl', + 'mens_xxxl')); + +} + + +function calculateTotalWomensShirts() { + document.getElementById('womens_total').value = + getTotal( new Array('womens_s', + 'womens_m', + 'womens_l', + 'womens_xl', + 'womens_xxl', + 'womens_xxxl')); + +} diff --git a/extensions/REMO/web/styles/moz_reps.css b/extensions/REMO/web/styles/moz_reps.css new file mode 100644 index 000000000..989733c41 --- /dev/null +++ b/extensions/REMO/web/styles/moz_reps.css @@ -0,0 +1,44 @@ +#reps-form { + width: 700px; + border-spacing: 0px; + border: 4px solid #e0e0e0; +} + +#reps-form th, #reps-form td { + padding: 5px; +} + +#reps-form .even th, #reps-form .even td { + background: #e0e0e0; +} + +#reps-form th { + text-align: left; +} + +#reps-form textarea { + font-family: Verdana, sans-serif; + font-size: small; + width: 590px; +} + +#reps-form .mandatory { + color: red; + font-size: 80%; +} + +#reps-form .missing { + box-shadow: #FF0000 0 0 1.5px 1px; +} + +#reps-form .hidden { + display: none; +} + +#reps-form .subTH { + padding-left: 2em; +} + +#reps-form .missing { + background: #FFC1C1; +} diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm new file mode 100644 index 000000000..436b4753d --- /dev/null +++ b/extensions/SecureMail/Config.pm @@ -0,0 +1,41 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 SecureMail Extension +# +# The Initial Developer of the Original Code is Mozilla. +# Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation. +# All Rights Reserved. +# +# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::SecureMail; +use strict; +use constant NAME => 'SecureMail'; + +use constant REQUIRED_MODULES => [ + { + package => 'Crypt-OpenPGP', + module => 'Crypt::OpenPGP', + # 1.02 added the ability for new() to take KeyRing objects for the + # PubRing argument. + version => '1.02', + }, + { + package => 'Crypt-SMIME', + module => 'Crypt::SMIME', + version => 0, + }, +]; + +__PACKAGE__->NAME; diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm new file mode 100644 index 000000000..ca5516c0a --- /dev/null +++ b/extensions/SecureMail/Extension.pm @@ -0,0 +1,326 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 SecureMail Extension +# +# The Initial Developer of the Original Code is the Mozilla Foundation. +# Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation. +# All Rights Reserved. +# +# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> +# Gervase Markham <gerv@gerv.net> + +package Bugzilla::Extension::SecureMail; +use strict; +use base qw(Bugzilla::Extension); + +use Bugzilla::Group; +use Bugzilla::Object; +use Bugzilla::User; +use Bugzilla::Util qw(correct_urlbase trim trick_taint); +use Bugzilla::Error; +use Crypt::OpenPGP::KeyRing; +use Crypt::OpenPGP; +use Crypt::SMIME; + +our $VERSION = '0.4'; + +############################################################################## +# Creating new columns +# +# secure_mail boolean in the 'groups' table - whether to send secure mail +# public_key text in the 'profiles' table - stores public key +############################################################################## +sub install_update_db { + my ($self, $args) = @_; + + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('groups', 'secure_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'public_key', { TYPE => 'LONGTEXT' }); +} + +############################################################################## +# Maintaining new columns +############################################################################## +# Make sure generic functions know about the additional fields in the user +# and group objects. +sub object_columns { + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $columns = $args->{'columns'}; + + if ($class->isa('Bugzilla::Group')) { + push(@$columns, 'secure_mail'); + } + elsif ($class->isa('Bugzilla::User')) { + push(@$columns, 'public_key'); + } +} + +# Plug appropriate validators so we can check the validity of the two +# fields created by this extension, when new values are submitted. +sub object_validators { + my ($self, $args) = @_; + my %args = %{ $args }; + my ($invocant, $validators) = @args{qw(class validators)}; + + if ($invocant->isa('Bugzilla::Group')) { + $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean; + } + elsif ($invocant->isa('Bugzilla::User')) { + $validators->{'public_key'} = sub { + my ($self, $value) = @_; + $value = trim($value) || ''; + + return $value if $value eq ''; + + if ($value =~ /PUBLIC KEY/) { + # PGP keys must be ASCII-armoured. + my $ring = new Crypt::OpenPGP::KeyRing(Data => $value); + $ring->read if $ring; + if (!defined $ring || !scalar $ring->blocks) { + ThrowUserError('securemail_invalid_key'); + } + } + elsif ($value =~ /BEGIN CERTIFICATE/) { + # S/MIME Keys must be in PEM format (Base64-encoded X.509) + # + # Crypt::SMIME seems not to like tainted values - it claims + # they aren't scalars! + trick_taint($value); + + my $smime = Crypt::SMIME->new(); + + eval { + $smime->setPublicKey([$value]); + }; + if ($@) { + ThrowUserError('securemail_invalid_key'); + } + } + else { + ThrowUserError('securemail_invalid_key'); + } + + return $value; + }; + } +} + +# When creating a 'group' object, set up the secure_mail field appropriately. +sub object_before_create { + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $params = $args->{'params'}; + + if ($class->isa('Bugzilla::Group')) { + $params->{secure_mail} = Bugzilla->cgi->param('secure_mail'); + } +} + +# On update, make sure the updating process knows about our new columns. +sub object_update_columns { + my ($self, $args) = @_; + my $object = $args->{'object'}; + my $columns = $args->{'columns'}; + + if ($object->isa('Bugzilla::Group')) { + # This seems like a convenient moment to extract this value... + $object->set('secure_mail', Bugzilla->cgi->param('secure_mail')); + + push(@$columns, 'secure_mail'); + } + elsif ($object->isa('Bugzilla::User')) { + push(@$columns, 'public_key'); + } +} + +# Handle the setting and changing of the public key. +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + + return unless $tab eq 'securemail'; + + # Create a new user object so we don't mess with the main one, as we + # don't know where it's been... + my $user = new Bugzilla::User(Bugzilla->user->id); + + if ($save) { + my $public_key = Bugzilla->input_params->{'public_key'}; + $user->set('public_key', $public_key); + $user->update(); + } + + $vars->{'public_key'} = $user->{'public_key'}; + + # Set the 'handled' scalar reference to true so that the caller + # knows the panel name is valid and that an extension took care of it. + $$handled = 1; +} + +############################################################################## +# Encrypting the email +############################################################################## +sub mailer_before_send { + my ($self, $args) = @_; + + my $email = $args->{'email'}; + + # Decide whether to make secure. + # This is a bit of a hack; it would be nice if it were more clear + # what sort a particular email is. + my $is_bugmail = $email->header('X-Bugzilla-Status'); + my $is_passwordmail = !$is_bugmail && ($email->body =~ /cfmpw.*cxlpw/s); + + if ($is_bugmail || $is_passwordmail) { + # Convert the email's To address into a User object + my $login = $email->header('To'); + my $emailsuffix = Bugzilla->params->{'emailsuffix'}; + $login =~ s/$emailsuffix$//; + my $user = new Bugzilla::User({ name => $login }); + + # Default to secure. (Of course, this means if this extension has a + # bug, lots of people are going to get bugmail falsely claiming their + # bugs are secure and they need to add a key...) + my $make_secure = 1; + + if ($is_bugmail) { + # This is also a bit of a hack, but there's no header with the + # bug ID in. So we take the first number in the subject. + my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/); + my $bug = new Bugzilla::Bug($bug_id); + if ($bug && !grep($_->{secure_mail}, @{ $bug->groups_in })) { + $make_secure = 0; + } + } + elsif ($is_passwordmail) { + # Mail is made unsecure only if the user does not have a public + # key and is not in any security groups. So specifying a public + # key OR being in a security group means the mail is kept secure + # (but, as noted above, the check is the other way around because + # we default to secure). + if ($user && + !$user->{'public_key'} && + !grep($_->{secure_mail}, @{ $user->groups })) + { + $make_secure = 0; + } + } + + # If finding the user fails for some reason, but we determine we + # should be encrypting, we want to make the mail safe. An empty key + # does that. + my $public_key = $user ? $user->{'public_key'} : ''; + + if ($make_secure) { + _make_secure($email, $public_key, $is_bugmail); + } + } +} + +sub _make_secure { + my ($email, $key, $is_bugmail) = @_; + + my $bug_id = undef; + my $subject = $email->header('Subject'); + + # We only change the subject if it's a bugmail; password mails don't have + # confidential information in the subject. + if ($is_bugmail) { + $subject =~ /^[^\d]+(\d+)/; + $bug_id = $1; + + my $new_subject = $subject; + # This is designed to still work if the admin changes the word + # 'bug' to something else. However, it could break if they change + # the format of the subject line in another way. + $new_subject =~ s/($bug_id\])\s+(.*)$/$1 (Secure bug updated)/; + $email->header_set('Subject', $new_subject); + } + + if ($key && $key =~ /PUBLIC KEY/) { + ################## + # PGP Encryption # + ################## + my $body = $email->body; + if ($is_bugmail) { + # Subject gets placed in the body so it can still be read + $body = "Subject: $subject\n\n" . $body; + } + + my $pubring = new Crypt::OpenPGP::KeyRing(Data => $key); + my $pgp = new Crypt::OpenPGP(PubRing => $pubring); + + # "@" matches every key in the public key ring, which is fine, + # because there's only one key in our keyring. + # + # We use the CAST5 cipher because the Rijndael (AES) module doesn't + # like us for some reason I don't have time to debug fully. + # ("key must be an untainted string scalar") + my $encrypted = $pgp->encrypt(Data => $body, + Recipients => "@", + Cipher => 'CAST5', + Armour => 1); + if (defined $encrypted) { + $email->body_set($encrypted); + } + else { + $email->body_set('Error during Encryption: ' . $pgp->errstr); + } + } + elsif ($key && $key =~ /BEGIN CERTIFICATE/) { + ##################### + # S/MIME Encryption # + ##################### + my $smime = Crypt::SMIME->new(); + my $encrypted; + + eval { + $smime->setPublicKey([$key]); + $encrypted = $smime->encrypt($email->as_string()); + }; + + if (!$@) { + # We can't replace the Email::MIME object, so we have to swap + # out its component parts. + my $enc_obj = new Email::MIME($encrypted); + $email->header_obj_set($enc_obj->header_obj()); + $email->body_set($enc_obj->body()); + } + else { + $email->body_set('Error during Encryption: ' . $@); + } + } + else { + # No encryption key provided; send a generic, safe email. + my $template = Bugzilla->template; + my $message; + my $vars = { + 'urlbase' => correct_urlbase(), + 'bug_id' => $bug_id, + 'maintainer' => Bugzilla->params->{'maintainer'} + }; + + $template->process('account/email/encryption-required.txt.tmpl', + $vars, \$message) + || ThrowTemplateError($template->error()); + + $email->body_set($message); + } +} + +__PACKAGE__->NAME; diff --git a/extensions/SecureMail/README b/extensions/SecureMail/README new file mode 100644 index 000000000..ac3484291 --- /dev/null +++ b/extensions/SecureMail/README @@ -0,0 +1,8 @@ +This extension should be placed in a directory called "SecureMail" in the +Bugzilla extensions/ directory. After installing it, remove the file +"disabled" (if present) and then run checksetup.pl. + +Instructions for user key formats: + +S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with BEGIN CERTIFICATE +PGP keys must be ASCII-armoured - i.e. text, with BEGIN PGP PUBLIC KEY. diff --git a/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl new file mode 100644 index 000000000..7341992c8 --- /dev/null +++ b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl @@ -0,0 +1,15 @@ +This email would have contained sensitive information, and you have not set +a PGP/GPG key or SMIME certificate in the "Secure Mail" section of your user +preferences. + +[% IF bug_id %] +In order to receive the full text of similar mails in the future, please +go to: +[%+ urlbase %]userprefs.cgi?tab=securemail +and provide a key or certificate. + +You can see this bug's current state at: +[%+ urlbase %]show_bug.cgi?id=[% bug_id %] +[% ELSE %] +You will have to contact [% maintainer %] to reset your password. +[% END %] diff --git a/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl new file mode 100644 index 000000000..dadfbb537 --- /dev/null +++ b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl @@ -0,0 +1,32 @@ +[%# 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. + # + # The Initial Developer of the Original Code is the Mozilla Corporation. + # Portions created by the Initial Developer are Copyright (C) 2008 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +<p>Some bugs in this [% terms.Bugzilla %] are in groups the administrator has +deemed 'secure'. This means emails containing information about those bugs +will only be sent encrypted. Enter your PGP/GPG public key or +SMIME certificate here to receive full update emails for such bugs.</p> + +<p>If you are a member of a secure group, or if you enter a key here, your password reset email will also be sent to you encrypted. If you are a member of a secure group and do not enter a key, you will not be able to reset your password without the assistance of an administrator.</p> + +<p><a href="page.cgi?id=securemail/help.html">More help is available</a>.</p> + +[% Hook.process('moreinfo') %] + +<textarea id="public_key" name="public_key" cols="72" rows="12"> + [%- public_key FILTER html %]</textarea> diff --git a/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl new file mode 100644 index 000000000..70a40e592 --- /dev/null +++ b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl @@ -0,0 +1,28 @@ +[%# -*- Mode: perl; indent-tabs-mode: nil -*- + # + # 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 SecureMail Extension + # + # The Initial Developer of the Original Code is Mozilla. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation. + # All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + # Gervase Markham <gerv@gerv.net> + #%] + +[% tabs = tabs.import([{ + name => "securemail", + label => "Secure Mail", + link => "userprefs.cgi?tab=securemail", + saveable => 1 + }]) %] diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl new file mode 100644 index 000000000..27c644d02 --- /dev/null +++ b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl @@ -0,0 +1,25 @@ +[%# 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. + # + # The Initial Developer of the Original Code is the Mozilla Corporation. + # Portions created by the Initial Developer are Copyright (C) 2008 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + #%] +<tr> + <th>Secure Bugmail:</th> + <td colspan="3"> + <input type="checkbox" id="secure_mail" name="secure_mail" + [% ' checked="checked"' IF group.secure_mail %]> + </td> +</tr> diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl new file mode 100644 index 000000000..81436a46c --- /dev/null +++ b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl @@ -0,0 +1,27 @@ +[%# 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. + # + # The Initial Developer of the Original Code is the Mozilla Corporation. + # Portions created by the Initial Developer are Copyright (C) 2008 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + #%] +[% IF group.is_bug_group %] + <tr> + <th>Secure Bugmail:</th> + <td> + <input type="checkbox" id="secure_mail" name="secure_mail" + [% ' checked="checked"' IF group.secure_mail %]> + </td> + </tr> +[% END %] diff --git a/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..f48a5389f --- /dev/null +++ b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,27 @@ +[%# 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. + # + # The Initial Developer of the Original Code is the Mozilla Corporation. + # Portions created by the Initial Developer are Copyright (C) 2008 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF error == "securemail_invalid_key" %] + [% title = "Invalid Public Key" %] + We were unable to read the public key that you entered. Make sure + that you are entering either an ASCII-armored PGP/GPG public key, + including the "BEGIN PGP PUBLIC KEY BLOCK" and "END PGP PUBLIC KEY BLOCK" + lines, or a PEM format (Base64-encoded X.509) S/MIME key, including the + BEGIN CERTIFICATE and END CERTIFICATE lines. +[% END %]
\ No newline at end of file diff --git a/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl new file mode 100644 index 000000000..76525eac2 --- /dev/null +++ b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl @@ -0,0 +1,99 @@ +[%# + # 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 SecureMail Extension. + # + # The Initial Developer of the Original Code is the Mozilla Foundation. + # Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation. + # All Rights Reserved. + # + # Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> + # Gervase Markham <gerv@gerv.net> + # Dave Lawrence <dkl@mozilla.com> + #%] + +[% PROCESS global/header.html.tmpl + title = "SecureMail Help" +%] + +Bugzilla considers certain groups as "secure". If a bug is in one of those groups, Bugzilla will not send unencrypted +email about it. To receive encrypted email rather than just a "something changed" placeholder, you must provide either +a S/MIME or a GPG/PGP key on the <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail">SecureMail preferences tab</a>.<br> +<br> +In addition, if you have uploaded a S/MIME or GPG/PGP key using the <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail"> +SecureMail preferences tab</a>, if you request your password to be reset, Bugzilla will send the reset email encrypted and you will +be required to decrypt it to view the reset instructions. + +<h2>S/MIME</h2> + +<b>S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with the first line containing BEGIN CERTIFICATE.</b></p> + +<p> +S/MIME certificates can be obtained from a number of providers. You can get a free one from <a href="https://www.startssl.com/?app=12">StartCom</a>. +Once you have it, <a href="https://www.startssl.com/?app=25#52">export it from your browser as a .p12 file and import it into your mail client</a>. +You'll need to provide a password when you export - pick a strong one, and then back up the .p12 file somewhere safe.</p> + +<p> +Then, you need to convert it to a .pem file. If you have OpenSSL installed, one way is as follows:</p> + +<p> +<code>openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes</code></p> + +<p> +Open the .pem file in a text editor. You can recognise the public key because it starts "BEGIN CERTIFICATE" and ends "END CERTIFICATE" and +has an appropriate friendly name (e.g. "StartCom Free Certificate Member's StartCom Ltd. ID"). It is <b>not</b> the section beginning +"BEGIN RSA PRIVATE KEY", and it is not any of the intermediate certificates or root certificates.</p> + +<p> +<b>Note: the .pem file has your private key in plaintext. Delete it once you have copied the public key out of it!</b></p> + +<h2>PGP</h2> + +<b>PGP keys must be ASCII-armoured - i.e. text, with the first line containing BEGIN PGP PUBLIC KEY.</b></p> + +<p> +If you already have your own PGP key in a keyring, skip straight to step 3. Otherwise:</p> + +<ol> + +<li>Install the GPG suite of utilities for your operating system, either using your package manager or downloaded from <a href="http://www.gnupg.org/download/index.en.html">gnupg.org</a>.</p> + +<li><p>Generate a private key.</p> + +<p><code>gpg --gen-key</code></p> + +<p> +You’ll have to answer several questions:</p> + +<p> +<ul> + <li>What kind and size of key you want; the defaults are probably good enough.</li> + <li>How long the key should be valid; you can safely choose a non-expiring key.</li> + <li>Your real name and e-mail address; these are necessary for identifying your key in a larger set of keys.</li> + <li>A comment for your key; the comment can be empty.</li> + <li>A passphrase. Whatever you do, don’t forget it! Your key, and all your encrypted files, will be useless if you do.</li> +</ul> + +<li><p>Generate an ASCII version of your public key.</p> + +<p><code>gpg --armor --output pubkey.txt --export 'Your Name'</code></p> + +<p>Paste the contents of pubkey.txt into the SecureMail text field in Bugzilla. + +<li>Configure your email client to use your associated private key to decrypt the encrypted emails. For Thunderbird, you need the <a href="https://addons.mozilla.org/en-us/thunderbird/addon/enigmail/">Enigmail</a> extension.</p> +</ol> + +<p> +Further reading: <a href="http://www.madboa.com/geek/gpg-quickstart">GPG Quickstart</a>. + +[% PROCESS global/footer.html.tmpl %] + + diff --git a/extensions/SiteMapIndex/Config.pm b/extensions/SiteMapIndex/Config.pm new file mode 100644 index 000000000..e10d6ec8b --- /dev/null +++ b/extensions/SiteMapIndex/Config.pm @@ -0,0 +1,36 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Sitemap Bugzilla Extension. +# +# The Initial Developer of the Original Code is Everything Solved, Inc. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Max Kanat-Alexander <mkanat@bugzilla.org> +# Dave Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::SiteMapIndex; +use strict; + +use constant NAME => 'SiteMapIndex'; + +use constant REQUIRED_MODULES => [ + { + package => 'IO-Compress-Gzip', + module => 'IO::Compress::Gzip', + version => 0, + } +]; + +__PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/Extension.pm b/extensions/SiteMapIndex/Extension.pm new file mode 100644 index 000000000..f36fa8c81 --- /dev/null +++ b/extensions/SiteMapIndex/Extension.pm @@ -0,0 +1,157 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Sitemap Bugzilla Extension. +# +# The Initial Developer of the Original Code is Everything Solved, Inc. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Max Kanat-Alexander <mkanat@bugzilla.org> +# Dave Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::SiteMapIndex; +use strict; +use base qw(Bugzilla::Extension); + +our $VERSION = '1.0'; + +use Bugzilla::Constants qw(bz_locations ON_WINDOWS); +use Bugzilla::Util qw(correct_urlbase get_text); +use Bugzilla::Install::Filesystem; + +use Bugzilla::Extension::SiteMapIndex::Constants; +use Bugzilla::Extension::SiteMapIndex::Util; + +use DateTime; +use IO::File; +use POSIX; + +######### +# Pages # +######### + +sub template_before_process { + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + + return if !$file eq 'global/header.html.tmpl'; + return unless (exists $vars->{bug} or exists $vars->{bugs}); + my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}]; + return if !ref $bugs eq 'ARRAY'; + + foreach my $bug (@$bugs) { + if (!bug_is_ok_to_index($bug)) { + $vars->{sitemap_noindex} = 1; + last; + } + } +} + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{page_id}; + + if ($page =~ m{^sitemap/sitemap\.}) { + my $map = generate_sitemap(__PACKAGE__->NAME); + print Bugzilla->cgi->header('text/xml'); + print $map; + exit; + } +} + +################ +# Installation # +################ + +sub install_before_final_checks { + my ($self) = @_; + if (!correct_urlbase()) { + print STDERR get_text('sitemap_no_urlbase'), "\n"; + return; + } + if (Bugzilla->params->{'requirelogin'}) { + print STDERR get_text('sitemap_requirelogin'), "\n"; + return; + } + + $self->_fix_robots_txt(); +} + +sub install_filesystem { + my ($self, $args) = @_; + my $create_dirs = $args->{'create_dirs'}; + my $recurse_dirs = $args->{'recurse_dirs'}; + my $htaccess = $args->{'htaccess'}; + + # Create the sitemap directory to store the index and sitemap files + my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; + + $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE; + + $recurse_dirs->{$sitemap_path} = { + files => Bugzilla::Install::Filesystem::CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE, + dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE + }; + + # Create a htaccess file that allows the sitemap files to be served out + $htaccess->{"$sitemap_path/.htaccess"} = { + perms => Bugzilla::Install::Filesystem::WS_SERVE, + contents => <<EOT +# Allow access to sitemap files created by the SiteMapIndex extension +<FilesMatch ^sitemap.*\\.xml(.gz)?\$> + Allow from all +</FilesMatch> +Deny from all +EOT + }; +} + +sub _fix_robots_txt { + my ($self) = @_; + my $cgi_path = bz_locations()->{'cgi_path'}; + my $robots_file = "$cgi_path/robots.txt"; + my $current_fh = new IO::File("$cgi_path/robots.txt", 'r'); + if (!$current_fh) { + warn "$robots_file: $!"; + return; + } + + my $current_contents; + { local $/; $current_contents = <$current_fh> } + $current_fh->close(); + + return if $current_contents =~ m{^Allow: \/\*show_bug\.cgi}ms; + my $backup_name = "$cgi_path/robots.txt.old"; + print get_text('sitemap_fixing_robots', { current => $robots_file, + backup => $backup_name }), "\n"; + rename $robots_file, $backup_name or die "backup failed: $!"; + + my $new_fh = new IO::File($self->package_dir . '/robots.txt', 'r'); + $new_fh || die "Could not open new robots.txt template file: $!"; + my $new_contents; + { local $/; $new_contents = <$new_fh> } + $new_fh->close() || die "Could not close new robots.txt template file: $!"; + + my $sitemap_url = correct_urlbase() . SITEMAP_URL; + $new_contents =~ s/SITEMAP_URL/$sitemap_url/; + $new_fh = new IO::File("$cgi_path/robots.txt", 'w'); + $new_fh || die "Could not open new robots.txt file: $!"; + print $new_fh $new_contents; + $new_fh->close() || die "Could not close new robots.txt file: $!"; +} + +__PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/lib/Constants.pm b/extensions/SiteMapIndex/lib/Constants.pm new file mode 100644 index 000000000..fce858121 --- /dev/null +++ b/extensions/SiteMapIndex/lib/Constants.pm @@ -0,0 +1,47 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Sitemap Bugzilla Extension. +# +# The Initial Developer of the Original Code is Everything Solved, Inc. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Max Kanat-Alexander <mkanat@bugzilla.org> + +package Bugzilla::Extension::SiteMapIndex::Constants; +use strict; +use base qw(Exporter); +our @EXPORT = qw( + SITEMAP_AGE + SITEMAP_MAX + SITEMAP_DELAY + SITEMAP_URL +); + +# This is the amount of hours a sitemap index and it's files are considered +# valid before needing to be regenerated. +use constant SITEMAP_AGE => 12; + +# This is the largest number of entries that can be in a single sitemap file, +# per the sitemaps.org standard. +use constant SITEMAP_MAX => 50_000; + +# We only show bugs that are at least 12 hours old, because if somebody +# files a bug that's a security bug but doesn't protect it, we want to give +# them time to fix that. +use constant SITEMAP_DELAY => 12; + +use constant SITEMAP_URL => 'page.cgi?id=sitemap/sitemap.xml'; + +1; diff --git a/extensions/SiteMapIndex/lib/Util.pm b/extensions/SiteMapIndex/lib/Util.pm new file mode 100644 index 000000000..6b5491d1c --- /dev/null +++ b/extensions/SiteMapIndex/lib/Util.pm @@ -0,0 +1,205 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Sitemap Bugzilla Extension. +# +# The Initial Developer of the Original Code is Everything Solved, Inc. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Max Kanat-Alexander <mkanat@bugzilla.org> +# Dave Lawrence <dkl@mozilla.com> + +package Bugzilla::Extension::SiteMapIndex::Util; +use strict; +use base qw(Exporter); +our @EXPORT = qw( + generate_sitemap + bug_is_ok_to_index +); + +use Bugzilla::Extension::SiteMapIndex::Constants; + +use Bugzilla::Util qw(correct_urlbase datetime_from url_quote); +use Bugzilla::Constants qw(bz_locations); + +use Scalar::Util qw(blessed); +use IO::Compress::Gzip qw(gzip $GzipError); + +sub too_young_date { + my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $hours_ago->subtract(hours => SITEMAP_DELAY); + return $hours_ago; +} + +sub bug_is_ok_to_index { + my ($bug) = @_; + return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug'); + my $creation_ts = datetime_from($bug->creation_ts); + return ($creation_ts lt too_young_date()) ? 1 : 0; +} + +# We put two things in the Sitemap: a list of Browse links for products, +# and links to bugs. +sub generate_sitemap { + my ($extension_name) = @_; + + # If file is less than SITEMAP_AGE hours old, then read in and send to caller. + # If greater, then regenerate and send the new version. + my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + if (-e $index_file) { + my $index_mtime = (stat($index_file))[9]; + my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours + if ($index_hours < SITEMAP_AGE) { + my $index_fh = new IO::File($index_file, 'r'); + $index_fh || die "Could not open current sitemap index: $!"; + my $index_xml; + { local $/; $index_xml = <$index_fh> } + $index_fh->close() || die "Could not close current sitemap index: $!"; + + return $index_xml; + } + } + + # Set the atime and mtime of the index file to the current time + # in case another request is made before we finish. + utime(undef, undef, $index_file); + + # Sitemaps must never contain private data. + Bugzilla->logout_request(); + my $user = Bugzilla->user; + my $products = $user->get_accessible_products; + + my $num_bugs = SITEMAP_MAX - scalar(@$products); + # We do this date math outside of the database because databases + # usually do better with a straight comparison value. + my $hours_ago = too_young_date(); + + # We don't use Bugzilla::Bug objects, because this could be a tremendous + # amount of data, and we only want a little. Also, we only display + # bugs that are not in any group. We show the last $num_bugs + # most-recently-updated bugs. + my $dbh = Bugzilla->dbh; + my $bug_sth = $dbh->prepare( + 'SELECT bugs.bug_id, bugs.delta_ts + FROM bugs + LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id + WHERE bug_group_map.bug_id IS NULL AND creation_ts < ? + ' . $dbh->sql_limit($num_bugs, '?')); + + my $filecount = 1; + my $filelist = []; + my $offset = 0; + + while (1) { + my $bugs = []; + + $bug_sth->execute($hours_ago, $offset); + + while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) { + push(@$bugs, { bug_id => $bug_id, delta_ts => $delta_ts }); + } + + last if !@$bugs; + + # We only need the product links in the first sitemap file + $products = [] if $filecount > 1; + + push(@$filelist, _generate_sitemap_file($extension_name, $filecount, $products, $bugs)); + + $filecount++; + $offset += $num_bugs; + } + + # Generate index file + return _generate_sitemap_index($extension_name, $filelist); +} + +sub _generate_sitemap_index { + my ($extension_name, $filelist) = @_; + + my $dbh = Bugzilla->dbh; + my $timestamp = $dbh->selectrow_array( + "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + + my $index_xml = <<END; +<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +END + + foreach my $filename (@$filelist) { + $index_xml .= " + <sitemap> + <loc>" . correct_urlbase() . "data/$extension_name/$filename</loc> + <lastmod>$timestamp</lastmod> + </sitemap> +"; + } + + $index_xml .= <<END; +</sitemapindex> +END + + my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + my $index_fh = new IO::File($index_file, 'w'); + $index_fh || die "Could not open new sitemap index: $!"; + print $index_fh $index_xml; + $index_fh->close() || die "Could not close new sitemap index: $!"; + + return $index_xml; +} + +sub _generate_sitemap_file { + my ($extension_name, $filecount, $products, $bugs) = @_; + + my $bug_url = correct_urlbase() . 'show_bug.cgi?id='; + my $product_url = correct_urlbase() . 'describecomponents.cgi?product='; + + my $sitemap_xml = <<END; +<?xml version="1.0" encoding="UTF-8"?> +<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +END + + foreach my $product (@$products) { + $sitemap_xml .= " + <url> + <loc>" . $product_url . url_quote($product->name) . "</loc> + <changefreq>daily</changefreq> + <priority>0.4</priority> + </url> +"; + } + + foreach my $bug (@$bugs) { + $sitemap_xml .= " + <url> + <loc>" . $bug_url . $bug->{bug_id} . "</loc> + <lastmod>" . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . "</lastmod> + </url> +"; + } + + $sitemap_xml .= <<END; +</urlset> +END + + # Write the compressed sitemap data to a file in the cgi root so that they can + # be accessed by the search engines. + my $filename = "sitemap$filecount.xml.gz"; + gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename" + || die "gzip failed: $GzipError\n"; + + return $filename; +} + +1; diff --git a/extensions/SiteMapIndex/robots.txt b/extensions/SiteMapIndex/robots.txt new file mode 100644 index 000000000..139edbf93 --- /dev/null +++ b/extensions/SiteMapIndex/robots.txt @@ -0,0 +1,9 @@ +User-agent: * +Disallow: /*.cgi +Disallow: /*show_bug.cgi*ctype=* +Allow: / +Allow: /*index.cgi +Allow: /*page.cgi +Allow: /*show_bug.cgi +Allow: /*describecomponents.cgi +Sitemap: SITEMAP_URL diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl new file mode 100644 index 000000000..682f6093f --- /dev/null +++ b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl @@ -0,0 +1,23 @@ +[%# 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 Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by Everything Solved are Copyright (C) 2010 + # Everything Solved. All Rights Reserved. + # + # The Original Code is the Bugzilla Sitemap Extension. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% SET meta_robots = ['noarchive'] %] +[% meta_robots.push('noindex') IF sitemap_noindex %] +<meta name="robots" content="[% meta_robots.join(',') FILTER html %]"> diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl new file mode 100644 index 000000000..0d0e9fd74 --- /dev/null +++ b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl @@ -0,0 +1,37 @@ +[%# 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 Initial Developer of the Original Code is Everything Solved, Inc. + # Portions created by Everything Solved are Copyright (C) 2010 + # Everything Solved. All Rights Reserved. + # + # The Original Code is the Bugzilla Sitemap Extension. + # + # Contributor(s): + # Max Kanat-Alexander <mkanat@bugzilla.org> + #%] + +[% IF message_tag == "sitemap_fixing_robots" %] + Replacing [% current FILTER html %]. (The old version will be saved + as "[% backup FILTER html %]". You can delete the old version if you + do not need its contents.) + +[% ELSIF message_tag == "sitemap_requirelogin" %] + Not updating search engines with your sitemap, because you have the + "requirelogin" parameter turned on, and so search engines will not be + able to access your sitemap. + +[% ELSIF message_tag == "sitemap_no_urlbase" %] + You have not yet set the "urlbase" parameter. We cannot update + search engines and inform them about your sitemap without a + urlbase. Please set the "urlbase" parameter and re-run + checksetup.pl. + +[% END %] diff --git a/extensions/Splinter/Config.pm b/extensions/Splinter/Config.pm new file mode 100644 index 000000000..d36a28922 --- /dev/null +++ b/extensions/Splinter/Config.pm @@ -0,0 +1,5 @@ +package Bugzilla::Extension::Splinter; +use strict; +use constant NAME => 'Splinter'; + +__PACKAGE__->NAME; diff --git a/extensions/Splinter/Extension.pm b/extensions/Splinter/Extension.pm new file mode 100644 index 000000000..75c0dbb0c --- /dev/null +++ b/extensions/Splinter/Extension.pm @@ -0,0 +1,137 @@ +package Bugzilla::Extension::Splinter; + +use strict; + +use base qw(Bugzilla::Extension); + +use Bugzilla; +use Bugzilla::Bug; +use Bugzilla::Template; +use Bugzilla::Attachment; +use Bugzilla::BugMail; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::Util qw(trim detaint_natural); + +use Bugzilla::Extension::Splinter::Util; + +our $VERSION = '0.1'; + +sub page_before_template { + my ($self, $args) = @_; + + my ($vars, $page) = @$args{qw(vars page_id)}; + my $input = Bugzilla->input_params; + + if ($page eq 'splinter.html') { + # Login is required for performing a review + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # We can either provide just a bug id to see a list + # of prior reviews by the user, or just an attachment + # id to go directly to a review page for the attachment. + # If both are give they will be checked later to make + # sure they are connected. + + if ($input->{'bug'}) { + $vars->{'bug_id'} = $input->{'bug'}; + $vars->{'attach_id'} = $input->{'attachment'}; + $vars->{'bug'} = Bugzilla::Bug->check($input->{'bug'}); + } + + if ($input->{'attachment'}) { + my $attachment = Bugzilla::Attachment->check({ id => $input->{'attachment'} }); + + # Check to see if the user can see the bug this attachment is connected to. + Bugzilla::Bug->check($attachment->bug_id); + if ($attachment->isprivate && $user->id != $attachment->attacher->id + && !$user->is_insider) + { + ThrowUserError('auth_failure', {action => 'access', + object => 'attachment'}); + } + + # If the user provided both a bug id and an attachment id, they must + # be connected to each other + if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) { + ThrowUserError('bug_attach_id_mismatch'); + } + + # The patch is going to be displayed in a HTML page and if the utf8 + # param is enabled, we have to encode attachment data as utf8. + if (Bugzilla->params->{'utf8'}) { + $attachment->data; # load data + utf8::decode($attachment->{data}); + } + + $vars->{'attach_id'} = $attachment->id; + $vars->{'attach_data'} = $attachment->data; + } + + my $field_object = new Bugzilla::Field({ name => 'attachments.status' }); + my $statuses; + if ($field_object) { + $statuses = [map { $_->name } @{ $field_object->legal_values }]; + } else { + $statuses = []; + } + $vars->{'attachment_statuses'} = $statuses; + } +} + + +sub bug_format_comment { + my ($self, $args) = @_; + + my $bug = $args->{'bug'}; + my $regexes = $args->{'regexes'}; + my $text = $args->{'text'}; + + # Add [review] link to the end of "Created attachment" comments + # + # We need to work around the way that the hook works, which is intended + # to avoid overlapping matches, since we *want* an overlapping match + # here (the normal handling of "Created attachment"), so we add in + # dummy text and then replace in the regular expression we return from + # the hook. + $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?) + ~(push(@$regexes, { match => qr/__REVIEW__$2/, + replace => get_review_link("$2", "[review]") })) && + (attachment_id_is_patch($2) ? "$1 __REVIEW__$2" : $1) + ~egmx; + + # And linkify "Review of attachment", this is less of a workaround since + # there is no issue with overlap; note that there is an assumption that + # there is only one match in the text we are linkifying, since they all + # get the same link. + my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/; + + if ($$text =~ $REVIEW_RE) { + my $review_link = get_review_link($bug, $1, "Review"); + my $attach_link = Bugzilla::Template::get_attachment_link($1, "attachment $1"); + + push(@$regexes, { match => $REVIEW_RE, + replace => "$review_link of $attach_link:"}); + } +} + +sub config_add_panels { + my ($self, $args) = @_; + + my $modules = $args->{panel_modules}; + $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config"; +} + +sub mailer_before_send { + my ($self, $args) = @_; + + # Post-process bug mail to add review links to bug mail. + # It would be nice to be able to hook in earlier in the + # process when the email body is being formatted in the + # style of the bug-format_comment link for HTML but this + # is the only hook available as of Bugzilla-3.4. + add_review_links_to_email($args->{'email'}); +} + +__PACKAGE__->NAME; diff --git a/extensions/Splinter/lib/Config.pm b/extensions/Splinter/lib/Config.pm new file mode 100644 index 000000000..90f1c8073 --- /dev/null +++ b/extensions/Splinter/lib/Config.pm @@ -0,0 +1,46 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Example Plugin. +# +# The Initial Developer of the Original Code is Canonical Ltd. +# Portions created by Canonical Ltd. are Copyright (C) 2008 +# Canonical Ltd. All Rights Reserved. +# +# Contributor(s): Max Kanat-Alexander <mkanat@bugzilla.org> +# Bradley Baetz <bbaetz@acm.org> +# Owen Taylor <otaylor@redhat.com> + +package Bugzilla::Extension::Splinter::Config; + +use strict; +use warnings; + +use Bugzilla::Config::Common; + +our $sortkey = 30; + +sub get_param_list { + my ($class) = @_; + + my @param_list = ( + { + name => 'splinter_base', + type => 't', + default => 'page.cgi?id=splinter.html', + }, + ); + + return @param_list; +} + +1; diff --git a/extensions/Splinter/lib/Util.pm b/extensions/Splinter/lib/Util.pm new file mode 100644 index 000000000..1861d7ab6 --- /dev/null +++ b/extensions/Splinter/lib/Util.pm @@ -0,0 +1,141 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 Splinter Bugzilla Extension. +# +# The Initial Developer of the Original Code is Red Hat, Inc. +# Portions created by Red Hat, Inc. are Copyright (C) 2009 +# Red Hat Inc. All Rights Reserved. +# +# Contributor(s): +# Owen Taylor <otaylor@fishsoup.net> + +package Bugzilla::Extension::Splinter::Util; + +use Bugzilla; +use Bugzilla::Util; + +use base qw(Exporter); + +@Bugzilla::Extension::Splinter::Util::EXPORT = qw( + attachment_is_visible + attachment_id_is_patch + get_review_url + get_review_link + add_review_links_to_email +); + +# Checks if the current user can see an attachment +# Based on code from attachment.cgi +sub attachment_is_visible { + my $attachment = shift; + + $attachment->isa('Bugzilla::Attachment') || return 0; + + return (Bugzilla->user->can_see_bug($attachment->bug->id) + && (!$attachment->isprivate + || Bugzilla->user->id == $attachment->attacher->id + || Bugzilla->user->is_insider)); +} + +sub attachment_id_is_patch { + my $attach_id = shift; + my $attachment = new Bugzilla::Attachment($attach_id); + + # The check on attachment_is_visible here is to prevent a tiny + # information leak where someone could check if a private + # attachment was a patch by creating text that would get linkified + # differently. Likely excess paranoia + return (defined $attachment + && attachment_is_visible($attachment) + && $attachment->ispatch); +} + +sub get_review_url { + my ($bug, $attach_id, $absolute) = @_; + my $base = Bugzilla->params->{'splinter_base'}; + my $bug_id = $bug->id; + + if (defined $absolute && $absolute) { + my $urlbase = correct_urlbase(); + $urlbase =~ s!/$!! if $base =~ "^/"; + $base = $urlbase . $base; + } + + if ($base =~ /\?/) { + return "$base&bug=$bug_id&attachment=$attach_id"; + } + else { + return "$base?bug=$bug_id&attachment=$attach_id"; + } +} + +sub get_review_link { + my ($attach_id, $link_text) = @_; + + my $attachment = Bugzilla::Attachment->new($attach_id); + + if ($attachment && $attachment->ispatch) { + return "<a href='" . html_quote(get_review_url($attachment->bug, $attach_id)) . "'>$link_text</a>"; + } +} + +sub munge_create_attachment { + my ($bug, $intro_text, $attach_id, $view_link) = @_; + + if (attachment_id_is_patch ($attach_id)) { + return ("$intro_text" . + " View: $view_link\015\012" . + " Review: " . get_review_url($bug, $attach_id, 1) . "\015\012"); + } + else { + return ("$intro_text --> ($view_link)"); + } +} + +# This adds review links into a bug mail before we send it out. +# Since this is happening after newlines have been converted into +# RFC-2822 style \r\n, we need handle line ends carefully. +# (\015 and \012 are used because Perl \n is platform-dependent) +sub add_review_links_to_email { + my $email = shift; + my $body = $email->body; + my $new_body = 0; + my $bug; + + if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/ + && Bugzilla->user->can_see_bug($1)) + { + $bug = Bugzilla::Bug->new($1); + } + + return unless defined $bug; + + if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) { + $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:) + ~"$1\015\012 --> (" . get_review_url($bug, $2, 1) . ")" + ~egx; + $new_body = 1; + } + + if ($body =~ /Created attachment \d+\015\012 --> /) { + $body =~ s~(Created\ attachment\ (\d+)\015\012) + \ -->\ \(([^\015\012]*)\)[^\015\012]* + ~munge_create_attachment($bug, $1, $2, $3) + ~egx; + $new_body = 1; + } + + $email->body_set($body) if $new_body; +} + +1; diff --git a/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl new file mode 100644 index 000000000..834c0c7d4 --- /dev/null +++ b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl @@ -0,0 +1,38 @@ +[%# + # 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 Example Plugin. + # + # The Initial Developer of the Original Code is Canonical Ltd. + # Portions created by Canonical Ltd. are Copyright (C) 2008 + # Canonical Ltd. All Rights Reserved. + # + # Contributor(s): Bradley Baetz <bbaetz@acm.org> + # Owen Taylor <otaylor@redhat.com> + #%] +[% + title = "Splinter Patch Review" + desc = "Configure Splinter" +%] + +[% param_descs = { + splinter_base => "This is the base URL for the Splinter patch review page; " _ + "the default value '/page.cgi?id=splinter.html' works without " _ + "further configuration, however you may want to internally forward " _ + "/review to that URL in your web server's configuration and then change " _ + "this parameter. For example, withr the Apache HTTP server you can add " _ + "the following lines to the .htaccess for Bugzilla: " _ + "<pre>" _ + "RewriteEngine On\n" _ + "RewriteRule ^review(\?(.*))? page.cgi?id=splinter.html&$2 [L]" _ + "</pre>" + } +%] diff --git a/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl new file mode 100644 index 000000000..ba564d4b4 --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl @@ -0,0 +1,31 @@ +[%# + # 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 Splinter Bugzilla Extension. + # + # The Initial Developer of the Original Code is Red Hat, Inc. + # Portions created by Red Hat, Inc. are Copyright (C) 2008 + # Red Hat, Inc. All Rights Reserved. + # + # Contributor(s): Owen Taylor <otaylor@redhat.com> + # David Lawrence <dkl@mozilla.com> + #%] + +[% IF attachment.ispatch %] +  | + [% IF Param("splinter_base").search('\?') %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&bug=[% attachment.bug_id FILTER uri %]&attachment=[% attachment.id FILTER uri%]"> + Splinter Review</a> + [% ELSE %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% attachment.bug_id FILTER uri %]&attachment=[% attachment.id FILTER uri %]"> + Splinter Review</a> + [% END %] +[% END %] diff --git a/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl new file mode 100644 index 000000000..51babf079 --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl @@ -0,0 +1,31 @@ +[%# + # 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 Splinter Bugzilla Extension. + # + # The Initial Developer of the Original Code is Red Hat, Inc. + # Portions created by Red Hat, Inc. are Copyright (C) 2008 + # Red Hat, Inc. All Rights Reserved. + # + # Contributor(s): Owen Taylor <otaylor@redhat.com> + # David Lawrence <dkl@mozilla.com> + #%] + +[% IF attachment.ispatch %] +  | + [% IF Param("splinter_base").search('\?') %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&bug=[% bugid FILTER uri %]&attachment=[% attachment.id FILTER uri %]"> + Splinter Review</a> + [% ELSE %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% bugid FILTER uri %]&attachment=[% attachment.id FILTER uri %]"> + Splinter Review</a> + [% END %] +[% END %] diff --git a/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..17ef5c08f --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,5 @@ +[% IF error == "bug_attach_id_mismatch" %] + [% title = "Bug ID and Attachment ID Mismatch" %] + The [% terms.bug %] id and attachment id you provided + are not connected to each other. +[% END %] diff --git a/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl new file mode 100644 index 000000000..320d20a82 --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl @@ -0,0 +1,6 @@ +[% IF flag && flag.status == '?' && flag.type.name == 'review' && attachment && attachment.ispatch %] + +Splinter Review +[%+ urlbase FILTER none %][% Param('splinter_base') %]&bug=[% bug.bug_id FILTER uri %]&attachment=[% attachment.id FILTER uri %] +[%- END %] + diff --git a/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl new file mode 100644 index 000000000..5d1c7a2bb --- /dev/null +++ b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl @@ -0,0 +1,8 @@ +[% IF column == 'attachment' && request.ispatch %] + + [% IF Param("splinter_base").search('\?') %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&bug=[% request.bug_id FILTER uri %]&attachment=[% request.attach_id FILTER uri %]">[review]</a> + [% ELSE %] + <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% request.bug_id FILTER uri %]&attachment=[% request.attach_id FILTER uri %]">[review]</a> + [% END %] +[% END %] diff --git a/extensions/Splinter/template/en/default/pages/splinter.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl new file mode 100644 index 000000000..4728967c7 --- /dev/null +++ b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl @@ -0,0 +1,263 @@ +[%# + # 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 Splinter Bugzilla Extension. + # + # The Initial Developer of the Original Code is Red Hat, Inc. + # Portions created by Red Hat, Inc. are Copyright (C) 2008 + # Red Hat, Inc. All Rights Reserved. + # + # Contributor(s): Owen Taylor <otaylor@redhat.com> + #%] + +[% PROCESS global/header.html.tmpl + title = "Patch Review" + header = "Patch Review" + style_urls = [ "js/yui/assets/skins/sam/container.css", + "js/yui/assets/skins/sam/button.css", + "js/yui/assets/skins/sam/datatable.css", + "extensions/Splinter/web/splinter.css" ] + javascript_urls = [ "js/yui/element/element-min.js", + "js/yui/container/container-min.js", + "js/yui/button/button-min.js", + "js/yui/json/json-min.js", + "js/yui/datasource/datasource-min.js", + "js/yui/datatable/datatable-min.js", + "extensions/Splinter/web/splinter.js" ] +%] + +[% can_edit = 0 %] + +<script type="text/javascript"> + Splinter.configBase = '[% urlbase FILTER none %][% Param('splinter_base') FILTER js %]'; + Splinter.configBugzillaUrl = '[% urlbase FILTER none %]'; + Splinter.configHaveExtension = true; + Splinter.configHelp = '[% urlbase FILTER none %]page.cgi?id=splinter/help.html'; + Splinter.configNote = ''; + + Splinter.configAttachmentStatuses = [ + [% FOREACH status = attachment_statuses %] + '[% status FILTER js %]', + [% END %] + ]; + + Splinter.bugId = Splinter.Utils.isDigits('[% bug_id FILTER js %]') ? parseInt('[% bug_id FILTER js %]') : NaN; + Splinter.attachmentId = Splinter.Utils.isDigits('[% attach_id FILTER html %]') ? parseInt('[% attach_id FILTER js %]') : NaN; + + if (!isNaN(Splinter.bugId)) { + var theBug = new Splinter.Bug.Bug(); + theBug.id = parseInt('[% bug.id FILTER js %]'); + theBug.token = '[% update_token FILTER js %]'; + theBug.shortDesc = Splinter.Utils.strip('[% bug.short_desc FILTER js %]'); + theBug.creationDate = Splinter.Bug.parseDate('[% bug.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]'); + theBug.reporterEmail = Splinter.Utils.strip('[% bug.reporter.email FILTER js %]'); + theBug.reporterName = Splinter.Utils.strip('[% bug.reporter.name FILTER js %]'); + + [% FOREACH comment = bug.comments %] + var comment = new Splinter.Bug.Comment(); + comment.whoName = Splinter.Utils.strip('[% comment.author.name FILTER js %]'); + comment.whoEmail = Splinter.Utils.strip('[% comment.author.email FILTER js %]'); + comment.date = Splinter.Bug.parseDate('[% comment.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]'); + comment.text = '[% comment.thetext FILTER js %]'; + theBug.comments.push(comment); + [% END %] + + [% FOREACH attachment = bug.attachments %] + [% NEXT IF attachment.isprivate && !user.is_insider && attachment.attacher.id != user.id %] + var attachid = parseInt('[% attachment.id FILTER js %]'); + var attachment = new Splinter.Bug.Attachment('', attachid); + [% IF attachment.id == attach_id && attachment.ispatch %] + [% flag_types = attachment.flag_types %] + [% can_edit = attachment.validate_can_edit %] + attachment.data = '[% attach_data FILTER js %]'; + attachment.token = '[% issue_hash_token([attachment.id, attachment.modification_time]) FILTER js %]'; + [% END %] + attachment.description = Splinter.Utils.strip('[% attachment.description FILTER js %]'); + attachment.filename = Splinter.Utils.strip('[% attachment.filename FILTER js %]'); + attachment.contenttypeentry = Splinter.Utils.strip('[% attachment.contenttypeentry FILTER js %]'); + attachment.date = Splinter.Bug.parseDate('[% attachment.attached FILTER time("%Y-%m-%d %T %z") FILTER js %]'); + attachment.whoName = Splinter.Utils.strip('[% attachment.attacher.name FILTER js %]'); + attachment.whoEmail = Splinter.Utils.strip('[% attachment.attacher.email FILTER js %]'); + // attachment.status = Splinter.Utils.strip($(this).children('status').text()); + // if (attachment.status == "") + // attachment.status = null; + attachment.isPatch = [% attachment.ispatch ? 1 : 0 %]; + attachment.isObsolete = [% attachment.isobsolete ? 1 : 0 %]; + attachment.isPrivate = [% attachment.isprivate ? 1 : 0 %]; + theBug.attachments.push(attachment); + [% END %] + + Splinter.theBug = theBug; + } +</script> + +<!--[if lt IE 7]> +<p style="border: 1px solid #880000; padding: 1em; background: #ffee88; font-size: 120%;"> + Splinter Patch Review requires a modern browser, such as + <a href="http://www.firefox.com">Firefox</a>, for correct operation. +</p> +<![endif]--> + +<div id="helpful-links"> + <a id="allReviewsLink" href="[% urlbase FILTER none %][% Param('splinter_base') FILTER js %]"> + [reviews]</a> + <a id='helpLink' target='splinterHelp' + href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html"> + [help]</a> +</div> + +<div id="bugInfo" style="display: none;"> + <b>Bug <a id="bugLink"><span id="bugId"></span></a>:</b> + <span id="bugShortDesc"></span> - + <span id="bugReporter"></span> - + <span id="bugCreationDate"></span> +</div> + +<div id="attachInfo" style="display:none;"> + <span id="attachObsolete"></span> + <b>Attachment <a id="attachLink"><span id="attachId"></span></a>:</b> + <span id="attachDesc"></span> - + <span id="attachCreator"></span> - + <span id="attachDate"></span> + [% IF feature_enabled('patch_viewer') %] + <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&action=diff" + target="_blank">[diff]</a> + [% END %] + <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&action=edit" + target="_blank">[details]</a> +</div> + +<div id="error" style="display: none;"> </div> + +<div id="enterBug" style="display: none;"> + Bug to review: + <input id="enterBugInput" /> + <input id="enterBugGo" type="button" value="Go" /> + <div id="chooseReview" style="display: none;"> + Drafts and published reviews: + <div id="chooseReviewTable"></div> + </div> +</div> + +<div id="chooseAttachment" style="display: none;"> + <div id="chooseAttachmentTable"></div> +</div> + +<div id="quickHelpShow" style="display:none;"> + <p> + <a href="javascript:Splinter.quickHelpToggle();" title="Show the quick help section" id="quickHelpToggle"> + Show Quick Help</a> + </p> +</div> + +<div id="quickHelpContent" style="display:none;"> + <p> + <a href="javascript:Splinter.quickHelpToggle();" title="Hide the quick help section" id="quickHelpToggle">Close Quick Help</a> + </p> + <ul id="quickHelpList"> + <li>From the Overview page, you can add a more generic overview comment that will appear at the beginning of your review.</li> + <li>To comment on a specific lines in the patch, first select the filename from the file navigation links.</li> + <li>Then double click the line you want to review and a comment box will appear below the line.</li> + <li>When the review is complete and you publish it, the overview comment and all line specific comments with their context, + will be combined together into a single review comment on the bug report.</li> + <li>For more detailed instructions, read the Splinter + <a id='helpLink' target='splinterHelp' href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html">help page</a>. + </li> + </ul> +</div> + +<div id="navigationContainer" style="display: none;"> + <b>Navigation:</b> <span id="navigation"></span> +</div> + +<div id="overview" style="display: none;"> + <div id="patchIntro"></div> + <div> + <span id="restored" style="display: none;"> + (Restored from draft; last edited <span id="restoredLastModified"></span>) + </span> + </div> + <div> + <div id="myCommentFrame"> + <textarea id="myComment"></textarea> + <div id="emptyCommentNotice"><Overall Comment></div> + </div> + <div id="myPatchComments"></div> + <form id="publish" method="post" action="attachment.cgi" onsubmit="normalizeComments();"> + <input type="hidden" id="publish_token" name="token" value=""> + <input type="hidden" id="publish_action" name="action" value="update"> + <input type="hidden" id="publish_review" name="comment" value=""> + <input type="hidden" id="publish_attach_id" name="id" value=""> + <input type="hidden" id="publish_attach_desc" name="description" value=""> + <input type="hidden" id="publish_attach_filename" name="filename" value=""> + <input type="hidden" id="publish_attach_contenttype" name="contenttypeentry" value=""> + <input type="hidden" id="publish_attach_ispatch" name="ispatch" value=""> + <input type="hidden" id="publish_attach_isobsolete" name="isobsolete" value=""> + <input type="hidden" id="publish_attach_isprivate" name="isprivate" value=""> + <div id="attachment_flags"> + [% any_flags_requesteeble = 0 %] + [% FOREACH flag_type = flag_types %] + [% NEXT UNLESS flag_type.is_active %] + [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %] + [% END %] + [% IF flag_types.size > 0 %] + [% PROCESS "flag/list.html.tmpl" bug_id = bug_id + attach_id = attach_d + flag_types = flag_types + read_only_flags = !can_edit + any_flags_requesteeble = any_flags_requesteeble + %] + [% END %] + <script> + [% FOREACH flag_type = flag_types %] + [% NEXT UNLESS flag_type.is_active %] + Event.addListener('flag_type-[% flag_type.id FILTER js %]', 'change', + function() { Splinter.flagChanged = 1; + Splinter.queueUpdateHaveDraft(); }); + [% FOREACH flag = flag_type.flags %] + Event.addListener('flag-[% flag.id FILTER js %]', 'change', + function() { Splinter.flagChanged = 1; + Splinter.queueUpdateHaveDraft(); }); + [% END %] + [% END %] + </script> + </div> + </form> + <div id="buttonBox"> + <span id="attachmentStatusSpan">Patch Status: + <select id="attachmentStatus"> </select> + </span> + <input id="publishButton" type="button" value="Publish" /> + <input id="cancelButton" type="button" value="Cancel" /> + </div> + <div class="clear"></div> + </div> + <div id="oldReviews" style="display: none;"> + <div class="review-title"> + Previous Reviews + </div> + </div> +</div> + +<div id="files" style="display: none;"> + <div id="file-collapse-all" style="display:none;"> + <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'none')">Collapse All</a> | + <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'block')">Expand All</a> + </div> +</div> + +<div id="credits"> + Powered by <a href="http://fishsoup.net/software/splinter">Splinter</a> +</div> + +<div id="saveDraftNotice" style="display: none;"></div> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl new file mode 100644 index 000000000..7c797c94d --- /dev/null +++ b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl @@ -0,0 +1,153 @@ +[%# + # 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 Splinter Bugzilla Extension. + # + # The Initial Developer of the Original Code is Red Hat, Inc. + # Portions created by Red Hat, Inc. are Copyright (C) 2008 + # Red Hat, Inc. All Rights Reserved. + # + # Contributor(s): Owen Taylor <otaylor@redhat.com> + #%] + +[% PROCESS global/header.html.tmpl + title = "Patch Review Help" + header = "Patch Review Help" +%] + +<h2>Splinter Patch Review</h2> +<p> + Splinter is an add-on for Bugzilla to allow conveniently + reviewing patches that people have attached to + Bugzilla. <a href="http://fishsoup.net/software/splinter">More + information about Splinter</a>. +</p> +<h3>The patch review view</h3> +<p> + If you get to Splinter by clicking on a link next to an + attachment in Bugzilla, you are presented with the patch + review view. This view has a number of different pages that can + be switched between with the links at the top of the screen. + The first page is the Overview page, the other pages correspond to + individual files changed by the review. +</p> +<p> + On the Overview page, from top to bottom are shown: +</p> +<ul> + <li>Introductory text to the patch. For a patch that was created + using 'git format-patch' this will be the Git commit + message.</li> + <li>Controls for creating a new review</li> + <li>Previous reviews that other people have written.</li> +</ul> +<p> + The pages for each file show a two-column view of the changes. + The left column is the previous contents of the file, + the right column is the new contents of the file. (If the file + is an entirely new file or an entirely deleted file, only one + column will be shown.) Red indicates lines that have been + removed, green lines that have been added, and blue lines that + were modified. +</p> +<p> + If people have previously made comments on individual lines of + the patch, they will show up both summarized on the Overview + page and also inline when looking at the files of the patch. +</p> +<h3>Reviewing an existing patch</h3> +<p> + There are three components to a review: +</p> +<ul> + <li> + An overall comment. The text area on the first page allows + you to enter your overall thoughts on the bug. + </li> + <li> + Detailed comments on changes within the files. To comment on a + line in a patch, double click on it, and a text area will open + beneath that comment. When you are done, click the Save button + to save your comment or the Cancel button to throw your + comment away. You can double-click on a saved comment to start + editing it again and make further changes. + </li> + <li> + A change to the attachment status. (This is specific to + Bugzilla instances that have attachment status, which is a + non-upstream patch. It's somewhat similar to attachment flags, + which splinter doesn't currently support displaying or + changing.) This allows you to mark a patch as read to commit + or needing additional work. This is done by changing the + drop-down next to the Publish button. + </li> +</ul> +<p> + Once you are done writing your review, go back to Overview page + and click the "Publish" button to submit it as a comment on the + bug. The comment will have a link back to the review page so + that people can see your comments with the full context. +</p> +<h3>Saved drafts</h3> +<p> + Whenever you start making changes, a draft is automatically + saved. If you come back to the patch review page for the same + attachment, that draft will automatically be resumed. Drafts are + not visible to anybody else until published. +</p> +<p> + Note that saving drafts requires the your browser to have support + for the "DOM Storage" standard. At time of writing, this is + available only in a few very recent browsers, like Firefox + 3.5. Strict privacy protections like disabling cookies may also + disable DOM Storage, since it provides another mechanism for + sites to track information about their users. +</p> +<h3>Responding to someone's review</h3> +<p> + A response is treated just like any other review and created the + same way. A couple of features are helpful when responding: you + can double-click on an inline comment to respond to it. And on + the overview page, when you click on a detailed comment, you are + taken directly to the original location of the comment. +</p> +<h3>Uploading patches for review</h3> +<p> + Splinter doesn't really care how patches are provided to + Bugzilla, as long as they are well-formatted patches. If you are + using Git for version control, you can either format changes as + patches + using <a href="http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html">'git + format-patch</a> and attach them manually to the bug, or you + can + use <a href="http://fishsoup.net/software/git-bz">git-bz</a>. + git-bz is highly recommended; it automates most of the steps + that Splinter can't handle: it files new bugs, attaches updated + attachments to existing bugs, and closes bugs when you push the + corresponding git commits to your central repository. +</p> +<h3>The bug review view</h3> +<p> + Splinter also has a view where it shows all patches attached to + the bug with their status and links to review them. You are + taken to this page after publishing a review. You can also get + to this page with the bug link in the upper-right corner of the + patch review view. +</p> +<h3>Your reviews</h3> +<p> + Splinter can also show you a list of all your draft and + published reviews. Access this page with the "Your reviews" + link at the bottom of the bug review view. In-progress drafts + are shown in bold. +</p> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/Splinter/web/splinter.css b/extensions/Splinter/web/splinter.css new file mode 100644 index 000000000..3f2eb84fe --- /dev/null +++ b/extensions/Splinter/web/splinter.css @@ -0,0 +1,415 @@ +textarea:focus { + background: #f7f2d0; +} + +#note { + background: #ffee88; + padding: 0.5em; +} + +#error { + border: 1px solid black; + padding: 0.5em; + color: #bb0000; +} + +#chooseReview { + margin-top: 1em; +} + +.review-draft .review-desc, .review-draft .review-attachment { + font-weight: bold; +} + +#bugInfo, #attachInfo { + margin-top: 0.5em; + margin-bottom: 1em; +} + +#helpful-links { + float:right; +} + +#chooseAttachment table { + margin-bottom: 1em; +} + +#attachObsolete { + font-weight: bold; + color: #c00000; +} + +.attachment-draft .attachment-id, .attachment-draft .attachment-desc { + font-weight: bold; +} + +.attachment-obsolete .attachment-desc { + text-decoration: line-through ; +} + +#navigation { + color: #888888; +} + +.navigation-link { + text-decoration: none; + white-space: nowrap; +} + +.navigation-link-selected { + color: black; +} + +#haveDraftNotice { + float: right; + color: #bb0000; + font-weight: bold; +} + +#overview { + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +#patchIntro { + border: 1px solid #888888; + font-size: 90%; + margin-bottom: 1em; + padding: 0.5em; +} + +.reviewer-box { + padding: 0.5em; +} + +.reviewer-0 .reviewer-box { + border-left: 10px solid green; +} + +.reviewer-1 .reviewer-box { + border-left: 10px solid blue; +} + +.reviewer-2 .reviewer-box { + border-left: 10px solid red; +} + +.reviewer-3 .reviewer-box { + border-left: 10px solid yellow; +} + +.reviewer-4 .reviewer-box { + border-left: 10px solid purple; +} + +.reviewer-5 .reviewer-box { + border-left: 10px solid orange; +} + +.reviewer { + float: left; +} + +.review-date { + float: right; +} + +.review-info-bottom { + clear: both; +} + +.review { + border: 1px solid black; + font-size: 90%; + margin-top: 0.25em; + margin-bottom: 1em; +} + +.review-intro { + margin-top: 0.5em; +} + +.review-patch-file { + margin-top: 0.5em; + font-weight: bold; +} + +.review-patch-comment { + border: 1px solid white; + padding: 1px; + margin-top: 0.5em; + margin-bottom: 0.5em; + cursor: pointer; +} + +.review-patch-comment:hover { + border: 1px solid #8888ff; +} + +.review-patch-comment .file-table { + width: 50%; +} + +.review-patch-comment .file-table-changed { + width: 100%; +} + +.review-patch-comment-separator { + margin: 0.5em; + border-bottom: 1px solid #888888; +} + +div.review-patch-comment-text { + margin-left: 2em; +} + +.review-patch-comment .reviewer-box { + border-left-width: 4px; +} + +#restored { + color: #bb0000; + margin-bottom: 0.5em; +} + +#myCommentFrame { + margin-top: 0.25em; + position: relative; + border: 1px solid black; + padding-right: 8px; /* compensate for child's padding */ +} + +#myComment { + border: 0px solid black; + padding: 4px; + margin: 0px; + width: 100%; + height: 10em; +} + +#emptyCommentNotice { + position: absolute; + top: 4px; + left: 4px; + color: #888888; +} + +#myPatchComments { + border: 1px solid black; + border-top-width: 0px; + padding: 0.5em; + font-size: 90%; +} + +#buttonBox { + margin-top: 0.5em; + float: right; +} + +.clear { + clear: both; +} + +/* Used for IE <= 7, overridden for modern browsers */ +.pre-wrap { + white-space: pre; + word-wrap: break-word; +} + +.pre-wrap { + white-space: pre-wrap; +} + +#files { + position: relative; + margin-top: 0.5em; + margin-bottom: 0.5em; +} + +.file-label { + margin-top: 1em; + margin-bottom: 0.5em; +} + +.file-label-name { + font-weight: bold; +} + +.hunk-header td { + background: #ddccbb; + font-family: "DejaVu Sans Mono", monospace; + font-size: 80%; +} + +.hunk-cell { + padding: 2px; +} + +.old-line, .new-line { + font-family: "DejaVu Sans Mono", monospace; + font-size: 80%; + white-space: pre-wrap; /* CSS 3 & 2.1 */ + white-space: -moz-pre-wrap; /* Gecko */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ +} + +.removed-line { + background: #ffccaa;; +} + +.added-line { + background: #bbffbb; +} + +.changed-line { + background: #aaccff; +} + +.file-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +.line-number { + font-size: 80%; + text-align: right; + padding-right: 0.25em; + color: #888888; + -moz-user-select: none; +} + +.line-number-column { + width: 2em; +} + +.file-table-wide-numbers .line-number-column { + width: 3em; +} + +.middle-column { + width: 3px; +} + +.file-table-changed .comment-removed { + width: 50%; + float: left; +} + +.file-table-changed .comment-changed { + margin-left: 25%; + margin-right: 25%; + clear: both; +} + +.file-table-changed .comment-added { + width: 50%; + float: right; +} + +.comment-frame { + border: 1px solid black; + margin-top: 5px; + margin-bottom: 5px; + margin-left: 2em; +} + +.file-table-wide-numbers .comment-frame { + margin-left: 3em; +} + +.comment .review-info { + margin-top: 0.5em; + font-size: 80%; +} + +#commentTextFrame { + border: 1px solid #ffeeaa; + margin-bottom: 5px; +} + +#commentEditor.focused #commentTextFrame { + border: 1px solid #8888bb; +} + +#commentEditorInner { + background: #ffeeaa; + padding: 0.5em; + margin-left: 2em; +} + +.file-table-wide-numbers #commentEditorInner { + margin-left: 3em; +} + +#commentEditor textarea { + width: 100%; + height: 10em; + border: 0px; +} + +#commentEditor textarea:focus { + background: white; +} + +#commentEditorLeftButtons { + float: left; +} + +#commentEditorLeftButtons input { + margin-right: 0.5em; +} + +#commentEditorRightButtons { + float: right; +} + +.comment-separator-removed { + clear: left; +} + +.comment-separator-added { + clear: right; +} + +#saveDraftNotice { + border: 1px solid black; + padding: 0.5em; + background: #ffccaa; + position: fixed; + bottom: 0px; + right: 0px; +} + +#credits { + font-size: 80%; + color: #888888; + padding: 10px; + text-align: center; +} + +#quickHelpShow a, #quickHelpContent a { + text-decoration: none; +} + +#quickHelpContent { + border: 1px solid #000; + -moz-border-radius: 10px; + border-radius: 10px; + padding-left: 0.5em; + margin-bottom: 10px; +} + +.file-label-collapse { + padding-right: 5px; + font-family: monospace; +} + +.file-review-label { + font-size: 80%; +} + +.file-reviewed-nav { + text-decoration: line-through; +} diff --git a/extensions/Splinter/web/splinter.js b/extensions/Splinter/web/splinter.js new file mode 100644 index 000000000..87a8b49d5 --- /dev/null +++ b/extensions/Splinter/web/splinter.js @@ -0,0 +1,2560 @@ +// Splinter - patch review add-on for Bugzilla +// By Owen Taylor <otaylor@fishsoup.net> +// Copyright 2009, Red Hat, Inc. +// Licensed under MPL 1.1 or later, or GPL 2 or later +// http://git.fishsoup.net/cgit/splinter +// Converted to YUI by David Lawrence <dkl@mozilla.com> + +YAHOO.namespace('Splinter'); + +var Dom = YAHOO.util.Dom; +var Event = YAHOO.util.Event; +var Splinter = YAHOO.Splinter; +var Element = YAHOO.util.Element; + +Splinter.domCache = { + cache : [0], + expando : 'data' + new Date(), + data : function (elem) { + var cacheIndex = elem[Splinter.domCache.expando]; + var nextCacheIndex = Splinter.domCache.cache.length; + if (!cacheIndex) { + cacheIndex = elem[Splinter.domCache.expando] = nextCacheIndex; + Splinter.domCache.cache[cacheIndex] = {}; + } + return Splinter.domCache.cache[cacheIndex]; + } +}; + +Splinter.Utils = { + assert : function(condition) { + if (!condition) { + throw new Error("Assertion failed"); + } + }, + + assertNotReached : function() { + throw new Error("Assertion failed: should not be reached"); + }, + + strip : function(string) { + return (/^\s*([\s\S]*?)\s*$/).exec(string)[1]; + }, + + lstrip : function(string) { + return (/^\s*([\s\S]*)$/).exec(string)[1]; + }, + + rstrip : function(string) { + return (/^([\s\S]*?)\s*$/).exec(string)[1]; + }, + + formatDate : function(date, now) { + if (now == null) { + now = new Date(); + } + var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000); + if (daysAgo < 0 && now.getDate() != date.getDate()) { + return date.toLocaleDateString(); + } else if (daysAgo < 1 && now.getDate() == date.getDate()) { + return date.toLocaleTimeString(); + } else if (daysAgo < 7 && now.getDay() != date.getDay()) { + return ['Sun', 'Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()] + " " + date.toLocaleTimeString(); + } else { + return date.toLocaleDateString(); + } + }, + + preWrapLines : function(el, text) { + while ((m = Splinter.LINE_RE.exec(text)) != null) { + var div = document.createElement("div"); + div.className = "pre-wrap"; + div.appendChild(document.createTextNode(m[1].length == 0 ? " " : m[1])); + el.appendChild(div); + } + }, + + isDigits : function (str) { + return str.match(/^[0-9]+$/); + } +}; + +Splinter.Bug = { + TIMEZONES : { + CEST: '200', + CET: '100', + BST: '100', + GMT: '000', + UTC: '000', + EDT: '-400', + EST: '-500', + CDT: '-500', + CST: '-600', + MDT: '-600', + MST: '-700', + PDT: '-700', + PST: '-800' + }, + + parseDate : function(d) { + var m = /^\s*(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)(?::(\d+))?\s+(?:([A-Z]{3,})|([-+]\d{3,}))\s*$/.exec(d); + if (!m) { + return null; + } + + var year = parseInt(m[1], 10); + var month = parseInt(m[2] - 1, 10); + var day = parseInt(m[3], 10); + var hour = parseInt(m[4], 10); + var minute = parseInt(m[5], 10); + var second = m[6] ? parseInt(m[6], 10) : 0; + + var tzoffset = 0; + if (m[7]) { + if (m[7] in Splinter.Bug.TIMEZONES) { + tzoffset = Splinter.Bug.TIMEZONES[m[7]]; + } + } else { + tzoffset = parseInt(m[8], 10); + } + + var unadjustedDate = new Date(Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5])); + + // 430 => 4:30. Easier to do this computation for only positive offsets + var sign = tzoffset < 0 ? -1 : 1; + tzoffset *= sign; + var adjustmentHours = Math.floor(tzoffset/100); + var adjustmentMinutes = tzoffset - adjustmentHours * 100; + + return new Date(unadjustedDate.getTime() - + sign * adjustmentHours * 3600000 - + sign * adjustmentMinutes * 60000); + }, + + _formatWho : function(name, email) { + if (name && email) { + return name + " <" + email + ">"; + } else if (name) { + return name; + } else { + return email; + } + } +}; + +Splinter.Bug.Attachment = function(bug, id) { + this._init(bug, id); +}; + +Splinter.Bug.Attachment.prototype = { + _init : function(bug, id) { + this.bug = bug; + this.id = id; + } +}; + +Splinter.Bug.Comment = function(bug) { + this._init(bug); +}; + +Splinter.Bug.Comment.prototype = { + _init : function(bug) { + this.bug = bug; + }, + + getWho : function() { + return Splinter.Bug._formatWho(this.whoName, this.whoEmail); + } +}; + +Splinter.Bug.Bug = function() { + this._init(); +}; + +Splinter.Bug.Bug.prototype = { + _init : function() { + this.attachments = []; + this.comments = []; + }, + + getAttachment : function(attachmentId) { + var i; + for (i = 0; i < this.attachments.length; i++) { + if (this.attachments[i].id == attachmentId) { + return this.attachments[i]; + } + } + return null; + }, + + getReporter : function() { + return Splinter.Bug._formatWho(this.reporterName, this.reporterEmail); + } +}; + +Splinter.Dialog = function() { + this._init.apply(this, arguments); +}; + +Splinter.Dialog.prototype = { + _init: function(prompt) { + this.buttons = []; + this.dialog = new YAHOO.widget.SimpleDialog('dialog', { + width: "300px", + fixedcenter: true, + visible: false, + modal: true, + draggable: false, + close: false, + hideaftersubmit: true, + constraintoviewport: true + }); + this.dialog.setHeader(prompt); + }, + + addButton : function (label, callback, isdefault) { + this.buttons.push({ text : label, + handler : function () { this.hide(); callback(); }, + isDefault : isdefault }); + this.dialog.cfg.queueProperty("buttons", this.buttons); + }, + + show : function () { + this.dialog.render(document.body); + this.dialog.show(); + } +}; + +Splinter.Patch = { + ADDED : 1 << 0, + REMOVED : 1 << 1, + CHANGED : 1 << 2, + NEW_NONEWLINE : 1 << 3, + OLD_NONEWLINE : 1 << 4, + + FILE_START_RE : /^(?:(?:Index|index|===|RCS|diff).*\n)*\-\-\-[ \t]*(\S+).*\n\+\+\+[ \t]*(\S+).*\n(?=@@)/mg, + HUNK_START_RE : /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@(.*)\n/mg, + HUNK_RE : /((?:[ +\\-].*\n)*)/mg, + + _cleanIntro : function(intro) { + var m; + + intro = Splinter.Utils.strip(intro); + + // Git: remove leading 'From <commit_id> <date' + m = /^From\s+[a-f0-9]{40}.*\n/.exec(intro); + if (m) { + intro = intro.substr(m.index + m[0].length); + } + + // Git: remove 'diff --stat' output from the end + m = /^---\n(?:^\s.*\n)+\s+\d+\s+files changed.*\n?(?!.)/m.exec(intro); + if (m) { + intro = intro.substr(0, m.index); + } + + return intro; + } +}; + +Splinter.Patch.Hunk = function(oldStart, oldCount, newStart, newCount, functionLine, text) { + this._init(oldStart, oldCount, newStart, newCount, functionLine, text); +}; + +Splinter.Patch.Hunk.prototype = { + _init : function(oldStart, oldCount, newStart, newCount, functionLine, text) { + var rawlines = text.split("\n"); + if (rawlines.length > 0 && Splinter.Utils.strip(rawlines[rawlines.length - 1]) == "") { + rawlines.pop(); // Remove trailing element from final \n + } + + this.oldStart = oldStart; + this.oldCount = oldCount; + this.newStart = newStart; + this.newCount = newCount; + this.functionLine = Splinter.Utils.strip(functionLine); + this.comment = null; + + var lines = []; + var totalOld = 0; + var totalNew = 0; + + var currentStart = -1; + var currentOldCount = 0; + var currentNewCount = 0; + + // A segment is a series of lines added/removed/changed with no intervening + // unchanged lines. We make the classification of Patch.ADDED/Patch.REMOVED/Patch.CHANGED + // in the flags for the entire segment + function startSegment() { + if (currentStart < 0) { + currentStart = lines.length; + } + } + + function endSegment() { + if (currentStart >= 0) { + if (currentOldCount > 0 && currentNewCount > 0) { + var j; + for (j = currentStart; j < lines.length; j++) { + lines[j][2] &= ~(Splinter.Patch.ADDED | Splinter.Patch.REMOVED); + lines[j][2] |= Splinter.Patch.CHANGED; + } + } + + currentStart = -1; + currentOldCount = 0; + currentNewCount = 0; + } + } + + var i; + for (i = 0; i < rawlines.length; i++) { + var line = rawlines[i]; + var op = line.substr(0, 1); + var strippedLine = line.substring(1); + var noNewLine = 0; + if (i + 1 < rawlines.length && rawlines[i + 1].substr(0, 1) == '\\') { + noNewLine = op == '-' ? Splinter.Patch.OLD_NONEWLINE : Splinter.Patch.NEW_NONEWLINE; + } + + if (op == ' ') { + endSegment(); + totalOld++; + totalNew++; + lines.push([strippedLine, strippedLine, 0]); + } else if (op == '-') { + totalOld++; + startSegment(); + lines.push([strippedLine, null, Splinter.Patch.REMOVED | noNewLine]); + currentOldCount++; + } else if (op == '+') { + totalNew++; + startSegment(); + if (currentStart + currentNewCount >= lines.length) { + lines.push([null, strippedLine, Splinter.Patch.ADDED | noNewLine]); + } else { + lines[currentStart + currentNewCount][1] = strippedLine; + lines[currentStart + currentNewCount][2] |= Splinter.Patch.ADDED | noNewLine; + } + currentNewCount++; + } // else if (op == '\\') { + // Handled with preceding line + // } else { + // Junk in the patch - hope the patch got line wrapped and just ignoring + // it produces something meaningful. (For a patch displayer, anyways. + // would be bad for applying the patch.) + // Utils.assertNotReached(); + // } + } + + // git mail-formatted patches end with --\n<git version> like a signature + // This is troublesome since it looks like a subtraction at the end + // of last hunk of the last file. Handle this specifically rather than + // generically stripping excess lines to be kind to hand-edited patches + if (totalOld > oldCount && + lines[lines.length - 1][1] == null && + lines[lines.length - 1][0].substr(0, 1) == '-') + { + lines.pop(); + currentOldCount--; + if (currentOldCount == 0 && currentNewCount == 0) { + currentStart = -1; + } + } + + endSegment(); + + this.lines = lines; + }, + + iterate : function(cb) { + var i; + var oldLine = this.oldStart; + var newLine = this.newStart; + for (i = 0; i < this.lines.length; i++) { + var line = this.lines[i]; + cb(this.location + i, oldLine, line[0], newLine, line[1], line[2], line); + if (line[0] != null) { + oldLine++; + } + if (line[1] != null) { + newLine++; + } + } + } +}; + +Splinter.Patch.File = function(filename, status, hunks) { + this._init(filename, status, hunks); +}; + +Splinter.Patch.File.prototype = { + _init : function(filename, status, hunks) { + this.filename = filename; + this.status = status; + this.hunks = hunks; + this.fileReviewed = false; + + var l = 0; + var i; + for (i = 0; i < this.hunks.length; i++) { + var hunk = this.hunks[i]; + hunk.location = l; + l += hunk.lines.length; + } + }, + + // A "location" is just a linear index into the lines of the patch in this file + getLocation : function(oldLine, newLine) { + var i; + for (i = 0; i < this.hunks.length; i++) { + var hunk = this.hunks[i]; + if (oldLine != null && hunk.oldStart > oldLine) { + continue; + } + if (newLine != null && hunk.newStart > newLine) { + continue; + } + + if ((oldLine != null && oldLine < hunk.oldStart + hunk.oldCount) || + (newLine != null && newLine < hunk.newStart + hunk.newCount)) + { + var location = -1; + hunk.iterate(function(loc, oldl, oldText, newl, newText, flags) { + if ((oldLine == null || oldl == oldLine) && + (newLine == null || newl == newLine)) + { + location = loc; + } + }); + + if (location != -1) { + return location; + } + } + } + + throw "Bad oldLine,newLine: " + oldLine + "," + newLine; + }, + + getHunk : function(location) { + var i; + for (i = 0; i < this.hunks.length; i++) { + var hunk = this.hunks[i]; + if (location >= hunk.location && location < hunk.location + hunk.lines.length) { + return hunk; + } + } + + throw "Bad location: " + location; + }, + + toString : function() { + return "Splinter.Patch.File(" + this.filename + ")"; + } +}; + +Splinter.Patch.Patch = function(text) { + this._init(text); +}; + +Splinter.Patch.Patch.prototype = { + // cf. parsing in Review.Review.parse() + _init : function(text) { + // Canonicalize newlines to simplify the following + if (/\r/.test(text)) { + text = text.replace(/(\r\n|\r|\n)/g, "\n"); + } + + this.files = []; + + var m = Splinter.Patch.FILE_START_RE.exec(text); + if (m != null) { + this.intro = Splinter.Patch._cleanIntro(text.substring(0, m.index)); + } else { + throw "Not a patch"; + } + + while (m != null) { + // git and hg show a diff between a/foo/bar.c and b/foo/bar.c + // or between a/foo/bar.c and /dev/null for removals and the + // reverse for additions. + var filename; + var status = undefined; + + if (/^a\//.test(m[1]) && /^b\//.test(m[2])) { + filename = m[1].substring(2); + status = Splinter.Patch.CHANGED; + } else if (/^a\//.test(m[1]) && /^\/dev\/null/.test(m[2])) { + filename = m[1].substring(2); + status = Splinter.Patch.REMOVED; + } else if (/^\/dev\/null/.test(m[1]) && /^b\//.test(m[2])) { + filename = m[2].substring(2); + status = Splinter.Patch.ADDED; + // Handle non-git and non-hg cases as well + } else if (!/^\/dev\/null/.test(m[1]) && /^\/dev\/null/.test(m[2])) { + filename = m[1]; + status = Splinter.Patch.REMOVED; + } else if (/^\/dev\/null/.test(m[1]) && !/^\/dev\/null/.test(m[2])) { + filename = m[2]; + status = Splinter.Patch.ADDED; + } else { + filename = m[1]; + } + + var hunks = []; + var pos = Splinter.Patch.FILE_START_RE.lastIndex; + while (true) { + Splinter.Patch.HUNK_START_RE.lastIndex = pos; + var m2 = Splinter.Patch.HUNK_START_RE.exec(text); + if (m2 == null || m2.index != pos) { + break; + } + + var oldStart = parseInt(m2[1], 10); + var oldCount = parseInt(m2[2], 10); + var newStart = parseInt(m2[3], 10); + var newCount = parseInt(m2[4], 10); + + pos = Splinter.Patch.HUNK_START_RE.lastIndex; + Splinter.Patch.HUNK_RE.lastIndex = pos; + var m3 = Splinter.Patch.HUNK_RE.exec(text); + if (m3 == null || m3.index != pos) { + break; + } + + pos = Splinter.Patch.HUNK_RE.lastIndex; + hunks.push(new Splinter.Patch.Hunk(oldStart, oldCount, newStart, newCount, m2[5], m3[1])); + } + + if (status === undefined) { + // For non-Hg/Git we use assume patch was generated non-zero context + // and just look at the patch to detect added/removed. Bzr actually + // says added/removed in the diff, but SVN/CVS don't + if (hunks.length == 1 && hunks[0].oldCount == 0) { + status = Splinter.Patch.ADDED; + } else if (hunks.length == 1 && hunks[0].newCount == 0) { + status = Splinter.Patch.REMOVED; + } else { + status = Splinter.Patch.CHANGED; + } + } + + this.files.push(new Splinter.Patch.File(filename, status, hunks)); + + Splinter.Patch.FILE_START_RE.lastIndex = pos; + m = Splinter.Patch.FILE_START_RE.exec(text); + } + }, + + getFile : function(filename) { + var i; + for (i = 0; i < this.files.length; i++) { + if (this.files[i].filename == filename) { + return this.files[i]; + } + } + + return null; + } +}; + +Splinter.Review = { + _removeFromArray : function(a, element) { + var i; + for (i = 0; i < a.length; i++) { + if (a[i] === element) { + a.splice(i, 1); + return; + } + } + }, + + _noNewLine : function(flags, flag) { + return ((flags & flag) != 0) ? "\n\\ No newline at end of file" : ""; + }, + + _lineInSegment : function(line) { + return (line[2] & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) != 0; + }, + + _compareSegmentLines : function(a, b) { + var op1 = a[0]; + var op2 = b[0]; + if (op1 == op2) { + return 0; + } else if (op1 == ' ') { + return -1; + } else if (op2 == ' ') { + return 1; + } else { + return op1 == '-' ? -1 : 1; + } + }, + + FILE_START_RE : /^:::[ \t]+(\S+)[ \t]*\n/mg, + HUNK_START_RE : /^@@[ \t]+(?:-(\d+),(\d+)[ \t]+)?(?:\+(\d+),(\d+)[ \t]+)?@@.*\n/mg, + HUNK_RE : /((?:(?!@@|:::).*\n?)*)/mg, + REVIEW_RE : /^\s*review\s+of\s+attachment\s+(\d+)\s*:\s*/i +}; + +Splinter.Review.Comment = function(file, location, type, comment) { + this._init(file, location, type, comment); +}; + +Splinter.Review.Comment.prototype = { + _init : function(file, location, type, comment) { + this.file = file; + this.type = type; + this.location = location; + this.comment = comment; + }, + + getHunk : function() { + return this.file.patchFile.getHunk(this.location); + }, + + getInReplyTo : function() { + var i; + var hunk = this.getHunk(); + var line = hunk.lines[this.location - hunk.location]; + for (i = 0; i < line.reviewComments.length; i++) { + var comment = line.reviewComments[0]; + if (comment === this) { + return null; + } + if (comment.type == this.type) { + return comment; + } + } + + return null; + }, + + remove : function() { + var hunk = this.getHunk(); + var line = hunk.lines[this.location - hunk.location]; + Splinter.Review._removeFromArray(this.file.comments, this); + Splinter.Review._removeFromArray(line.reviewComments, this); + } +}; + +Splinter.Review.File = function(review, patchFile) { + this._init(review, patchFile); +}; + +Splinter.Review.File.prototype = { + _init : function(review, patchFile) { + this.review = review; + this.patchFile = patchFile; + this.comments = []; + }, + + addComment : function(location, type, comment) { + var hunk = this.patchFile.getHunk(location); + var line = hunk.lines[location - hunk.location]; + comment = new Splinter.Review.Comment(this, location, type, comment); + if (line.reviewComments == null) { + line.reviewComments = []; + } + line.reviewComments.push(comment); + var i; + for (i = 0; i <= this.comments.length; i++) { + if (i == this.comments.length || + this.comments[i].location > location || + (this.comments[i].location == location && this.comments[i].type > type)) { + this.comments.splice(i, 0, comment); + break; + } else if (this.comments[i].location == location && + this.comments[i].type == type) { + throw "Two comments at the same location"; + } + } + + return comment; + }, + + getComment : function(location, type) { + var i; + for (i = 0; i < this.comments.length; i++) { + if (this.comments[i].location == location && + this.comments[i].type == type) + { + return this.comments[i]; + } + } + + return null; + }, + + toString : function() { + var str = "::: " + this.patchFile.filename + "\n"; + var first = true; + + var i; + for (i = 0; i < this.comments.length; i++) { + if (first) { + first = false; + } else { + str += '\n'; + } + var comment = this.comments[i]; + var hunk = comment.getHunk(); + + // Find the range of lines we might want to show. That's everything in the + // same segment as the commented line, plus up two two lines of non-comment + // diff before. + + var contextFirst = comment.location - hunk.location; + if (Splinter.Review._lineInSegment(hunk.lines[contextFirst])) { + while (contextFirst > 0 && Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) { + contextFirst--; + } + } + + var j; + for (j = 0; j < 5; j++) { + if (contextFirst > 0 && !Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) { + contextFirst--; + } + } + + // Now get the diff lines (' ', '-', '+' for that range of lines) + + var patchOldStart = null; + var patchNewStart = null; + var patchOldLines = 0; + var patchNewLines = 0; + var unchangedLines = 0; + var patchLines = []; + + function addOldLine(oldLine) { + if (patchOldLines == 0) { + patchOldStart = oldLine; + } + patchOldLines++; + } + + function addNewLine(newLine) { + if (patchNewLines == 0) { + patchNewStart = newLine; + } + patchNewLines++; + } + + hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags) { + if (loc >= hunk.location + contextFirst && loc <= comment.location) { + if ((flags & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) == 0) { + patchLines.push('> ' + oldText + Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE | Splinter.Patch.NEW_NONEWLINE)); + addOldLine(oldLine); + addNewLine(newLine); + unchangedLines++; + } else { + if ((comment.type == Splinter.Patch.REMOVED + || comment.type == Splinter.Patch.CHANGED) + && oldText != null) + { + patchLines.push('> -' + oldText + + Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE)); + addOldLine(oldLine); + } + if ((comment.type == Splinter.Patch.ADDED + || comment.type == Splinter.Patch.CHANGED) + && newText != null) + { + patchLines.push('> +' + newText + + Splinter.Review._noNewLine(flags, Splinter.Patch.NEW_NONEWLINE)); + addNewLine(newLine); + } + } + } + }); + + // Sort them into global order ' ', '-', '+' + patchLines.sort(Splinter.Review._compareSegmentLines); + + // Completely blank context isn't useful so remove it; however if we are commenting + // on blank lines at the start of a segment, we have to leave something or things break + while (patchLines.length > 1 && patchLines[0].match(/^\s*$/)) { + patchLines.shift(); + patchOldStart++; + patchNewStart++; + patchOldLines--; + patchNewLines--; + unchangedLines--; + } + + if (comment.type == Splinter.Patch.CHANGED) { + // For a CHANGED comment, we have to show the the start of the hunk - but to save + // in length we can trim unchanged context before it + + if (patchOldLines + patchNewLines - unchangedLines > 5) { + var toRemove = Math.min(unchangedLines, patchOldLines + patchNewLines - unchangedLines - 5); + patchLines.splice(0, toRemove); + patchOldStart += toRemove; + patchNewStart += toRemove; + patchOldLines -= toRemove; + patchNewLines -= toRemove; + unchangedLines -= toRemove; + } + + str += '@@ -' + patchOldStart + ',' + patchOldLines + ' +' + patchNewStart + ',' + patchNewLines + ' @@\n'; + + // We will use up to 10 lines more: + // 5 old lines or 4 old lines and a "... <N> more ... " line + // 5 new lines or 4 new lines and a "... <N> more ... " line + + var patchRemovals = patchOldLines - unchangedLines; + var showPatchRemovals = patchRemovals > 5 ? 4 : patchRemovals; + var patchAdditions = patchNewLines - unchangedLines; + var showPatchAdditions = patchAdditions > 5 ? 4 : patchAdditions; + + j = 0; + while (j < unchangedLines + showPatchRemovals) { + str += "> " + patchLines[j] + "\n"; + j++; + } + if (showPatchRemovals < patchRemovals) { + str += "> ... " + patchRemovals - showPatchRemovals + " more ...\n"; + j += patchRemovals - showPatchRemovals; + } + while (j < unchangedLines + patchRemovals + showPatchAdditions) { + str += "> " + patchLines[j] + "\n"; + j++; + } + if (showPatchAdditions < patchAdditions) { + str += "> ... " + patchAdditions - showPatchAdditions + " more ...\n"; + j += patchAdditions - showPatchAdditions; + } + } else { + // We limit Patch.ADDED/Patch.REMOVED comments strictly to 5 lines after the header + if (patchOldLines + patchNewLines - unchangedLines > 5) { + var toRemove = patchOldLines + patchNewLines - unchangedLines - 5; + patchLines.splice(0, toRemove); + patchOldStart += toRemove; + patchNewStart += toRemove; + patchOldLines -= toRemove; + patchNewLines -= toRemove; + } + + if (comment.type == Splinter.Patch.REMOVED) { + str += '@@ -' + patchOldStart + ',' + patchOldLines + ' @@\n'; + } else { + str += '@@ +' + patchNewStart + ',' + patchNewLines + ' @@\n'; + } + str += patchLines.join("\n") + "\n"; + } + str += "\n" + comment.comment + "\n"; + } + + return str; + } +}; + +Splinter.Review.Review = function(patch, who, date) { + this._init(patch, who, date); +}; + +Splinter.Review.Review.prototype = { + _init : function(patch, who, date) { + this.date = null; + this.patch = patch; + this.who = who; + this.date = date; + this.intro = null; + this.files = []; + + var i; + for (i = 0; i < patch.files.length; i++) { + this.files.push(new Splinter.Review.File(this, patch.files[i])); + } + }, + + // cf. parsing in Patch.Patch._init() + parse : function(text) { + Splinter.Review.FILE_START_RE.lastIndex = 0; + var m = Splinter.Review.FILE_START_RE.exec(text); + + var intro; + if (m != null) { + this.setIntro(text.substr(0, m.index)); + } else{ + this.setIntro(text); + return; + } + + while (m != null) { + var filename = m[1]; + var file = this.getFile(filename); + if (file == null) { + throw "Review.Review refers to filename '" + filename + "' not in reviewed Patch."; + } + + var pos = Splinter.Review.FILE_START_RE.lastIndex; + + while (true) { + Splinter.Review.HUNK_START_RE.lastIndex = pos; + var m2 = Splinter.Review.HUNK_START_RE.exec(text); + if (m2 == null || m2.index != pos) { + break; + } + + pos = Splinter.Review.HUNK_START_RE.lastIndex; + + var oldStart, oldCount, newStart, newCount; + if (m2[1]) { + oldStart = parseInt(m2[1], 10); + oldCount = parseInt(m2[2], 10); + } else { + oldStart = oldCount = null; + } + + if (m2[3]) { + newStart = parseInt(m2[3], 10); + newCount = parseInt(m2[4], 10); + } else { + newStart = newCount = null; + } + + var type; + if (oldStart != null && newStart != null) { + type = Splinter.Patch.CHANGED; + } else if (oldStart != null) { + type = Splinter.Patch.REMOVED; + } else if (newStart != null) { + type = Splinter.Patch.ADDED; + } else { + throw "Either old or new line numbers must be given"; + } + + var oldLine = oldStart; + var newLine = newStart; + + Splinter.Review.HUNK_RE.lastIndex = pos; + var m3 = Splinter.Review.HUNK_RE.exec(text); + if (m3 == null || m3.index != pos) { + break; + } + + pos = Splinter.Review.HUNK_RE.lastIndex; + + var rawlines = m3[1].split("\n"); + if (rawlines.length > 0 && rawlines[rawlines.length - 1].match('^/s+$')) { + rawlines.pop(); // Remove trailing element from final \n + } + + var commentText = null; + + var lastSegmentOld = 0; + var lastSegmentNew = 0; + var i; + for (i = 0; i < rawlines.length; i++) { + var line = rawlines[i]; + var count = 1; + if (i < rawlines.length - 1 && rawlines[i + 1].match(/^... \d+\s+/)) { + var m3 = /^\.\.\.\s+(\d+)\s+/.exec(rawlines[i + 1]); + count += parseInt(m3[1], 10); + i += 1; + } + // The check for /^$/ is because if Bugzilla is line-wrapping it also + // strips completely whitespace lines + if (line.match(/^>?\s+/) || line.match(/^$/)) { + oldLine += count; + newLine += count; + lastSegmentOld = 0; + lastSegmentNew = 0; + } else if (line.match(/^(> )?-/)) { + oldLine += count; + lastSegmentOld += count; + } else if (line.match(/^(> )?\+/)) { + newLine += count; + lastSegmentNew += count; + } else if (line.match(/^\\/)) { + // '\ No newline at end of file' - ignore + } else { + // Ignore assumming it's a result of line-wrapping + // https://bugzilla.mozilla.org/show_bug.cgi?id=509152 + YAHOO.log("WARNING: Bad content in hunk: " + line); + } + + if ((oldStart == null || oldLine == oldStart + oldCount) && + (newStart == null || newLine == newStart + newCount)) + { + commentText = rawlines.slice(i + 1).join("\n"); + break; + } + } + + if (commentText == null) { + YAHOO.log("WARNING: No comment found in hunk"); + commentText = ""; + } + + + var location; + if (type == Splinter.Patch.CHANGED) { + if (lastSegmentOld >= lastSegmentNew) { + oldLine--; + } + if (lastSegmentOld <= lastSegmentNew) { + newLine--; + } + location = file.patchFile.getLocation(oldLine, newLine); + } else if (type == Splinter.Patch.REMOVED) { + oldLine--; + location = file.patchFile.getLocation(oldLine, null); + } else if (type == Splinter.Patch.ADDED) { + newLine--; + location = file.patchFile.getLocation(null, newLine); + } + file.addComment(location, type, Splinter.Utils.strip(commentText)); + } + + Splinter.Review.FILE_START_RE.lastIndex = pos; + m = Splinter.Review.FILE_START_RE.exec(text); + } + }, + + setIntro : function (intro) { + intro = Splinter.Utils.strip(intro); + this.intro = intro != "" ? intro : null; + }, + + getFile : function (filename) { + var i; + for (i = 0; i < this.files.length; i++) { + if (this.files[i].patchFile.filename == filename) { + return this.files[i]; + } + } + + return null; + }, + + // Making toString() serialize to our seriaization format is maybe a bit sketchy + // But the serialization format is designed to be human readable so it works + // pretty well. + toString : function () { + var str = ''; + if (this.intro != null) { + str += Splinter.Utils.strip(this.intro); + str += '\n'; + } + + var first = this.intro == null; + var i; + for (i = 0; i < this.files.length; i++) { + var file = this.files[i]; + if (file.comments.length > 0) { + if (first) { + first = false; + } else { + str += '\n'; + } + str += file.toString(); + } + } + + return str; + } +}; + +Splinter.ReviewStorage = {}; + +Splinter.ReviewStorage.LocalReviewStorage = function() { + this._init(); +}; + +Splinter.ReviewStorage.LocalReviewStorage.available = function() { + // The try is a workaround for + // https://bugzilla.mozilla.org/show_bug.cgi?id=517778 + // where if cookies are disabled or set to ask, then the first attempt + // to access the localStorage property throws a security error. + try { + return 'localStorage' in window && window.localStorage != null; + } catch (e) { + return false; + } +}; + +Splinter.ReviewStorage.LocalReviewStorage.prototype = { + _init : function() { + var reviewInfosText = localStorage.splinterReviews; + if (reviewInfosText == null) { + this._reviewInfos = []; + } else { + this._reviewInfos = YAHOO.lang.JSON.parse(reviewInfosText); + } + }, + + listReviews : function() { + return this._reviewInfos; + }, + + _reviewPropertyName : function(bug, attachment) { + return 'splinterReview_' + bug.id + '_' + attachment.id; + }, + + loadDraft : function(bug, attachment, patch) { + var propertyName = this._reviewPropertyName(bug, attachment); + var reviewText = localStorage[propertyName]; + if (reviewText != null) { + var review = new Splinter.Review.Review(patch); + review.parse(reviewText); + return review; + } else { + return null; + } + }, + + _findReview : function(bug, attachment) { + var i; + for (i = 0 ; i < this._reviewInfos.length; i++) { + if (this._reviewInfos[i].bugId == bug.id && this._reviewInfos[i].attachmentId == attachment.id) { + return i; + } + } + + return -1; + }, + + _updateOrCreateReviewInfo : function(bug, attachment, props) { + var reviewIndex = this._findReview(bug, attachment); + var reviewInfo; + + var nowTime = Date.now(); + if (reviewIndex >= 0) { + reviewInfo = this._reviewInfos[reviewIndex]; + this._reviewInfos.splice(reviewIndex, 1); + } else { + reviewInfo = { + bugId: bug.id, + bugShortDesc: bug.shortDesc, + attachmentId: attachment.id, + attachmentDescription: attachment.description, + creationTime: nowTime + }; + } + + reviewInfo.modificationTime = nowTime; + for (var prop in props) { + reviewInfo[prop] = props[prop]; + } + + this._reviewInfos.push(reviewInfo); + localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos); + }, + + _deleteReviewInfo : function(bug, attachment) { + var reviewIndex = this._findReview(bug, attachment); + if (reviewIndex >= 0) { + this._reviewInfos.splice(reviewIndex, 1); + localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos); + } + }, + + saveDraft : function(bug, attachment, review, extraProps) { + var propertyName = this._reviewPropertyName(bug, attachment); + if (!extraProps) { + extraProps = {}; + } + extraProps.isDraft = true; + this._updateOrCreateReviewInfo(bug, attachment, extraProps); + localStorage[propertyName] = "" + review; + }, + + deleteDraft : function(bug, attachment, review) { + var propertyName = this._reviewPropertyName(bug, attachment); + + this._deleteReviewInfo(bug, attachment); + delete localStorage[propertyName]; + }, + + draftPublished : function(bug, attachment) { + var propertyName = this._reviewPropertyName(bug, attachment); + + this._updateOrCreateReviewInfo(bug, attachment, { isDraft: false }); + delete localStorage[propertyName]; + } +}; + +Splinter.saveDraftNoticeTimeoutId = null; +Splinter.navigationLinks = {}; +Splinter.reviewers = {}; +Splinter.savingDraft = false; +Splinter.UPDATE_ATTACHMENT_SUCCESS = /<title>\s*Changes\s+Submitted/; +Splinter.LINE_RE = /(?!$)([^\r\n]*)(?:\r\n|\r|\n|$)/g; + +Splinter.displayError = function (msg) { + var el = new Element(document.createElement('p')); + el.appendChild(document.createTextNode(msg)); + Dom.get('error').appendChild(Dom.get(el)); + Dom.setStyle('error', 'display', 'block'); +}; + +//Splinter.updateAttachmentStatus = function (attachment, newStatus, success, failure) { +// var data = { +// action: 'update', +// id: attachment.id, +// description: attachment.description, +// filename: attachment.filename, +// ispatch: attachment.isPatch ? 1 : 0, +// isobsolete: attachment.isObsolete ? 1 : 0, +// isprivate: attachment.isPrivate ? 1 : 0, +// 'attachments.status': newStatus +// }; +// +// if (attachment.token) { +// data.token = attachment.token; +// } +// +// $.ajax({ +// data : data, +// dataType : 'text', +// error : function(xmlHttpRequest, textStatus, errorThrown) { +// failure(); +// }, +// success : function(data, textStatus) { +// if (data.search(Splinter.UPDATE_ATTACHMENT_SUCCESS) != -1) { +// success(); +// } else { +// failure(); +// } +// }, +// type : 'POST', +// url : "attachment.cgi" +// }); +//} + +Splinter.publishReview = function () { + Splinter.saveComment(); + Splinter.theReview.setIntro(Dom.get('myComment').value); + + if (Splinter.reviewStorage) { + Splinter.reviewStorage.draftPublished(Splinter.theBug, + Splinter.theAttachment); + } + + var publish_form = Dom.get('publish'); + var publish_token = Dom.get('publish_token'); + var publish_attach_id = Dom.get('publish_attach_id'); + var publish_attach_desc = Dom.get('publish_attach_desc'); + var publish_attach_filename = Dom.get('publish_attach_filename'); + var publish_attach_contenttype = Dom.get('publish_attach_contenttype'); + var publish_attach_ispatch = Dom.get('publish_attach_ispatch'); + var publish_attach_isobsolete = Dom.get('publish_attach_isobsolete'); + var publish_attach_isprivate = Dom.get('publish_attach_isprivate'); + var publish_attach_status = Dom.get('publish_attach_status'); + var publish_review = Dom.get('publish_review'); + + publish_token.value = Splinter.theAttachment.token; + publish_attach_id.value = Splinter.theAttachment.id; + publish_attach_desc.value = Splinter.theAttachment.description; + publish_attach_filename.value = Splinter.theAttachment.filename; + publish_attach_contenttype.value = Splinter.theAttachment.contenttypeentry; + publish_attach_ispatch.value = Splinter.theAttachment.isPatch; + publish_attach_isobsolete.value = Splinter.theAttachment.isObsolete; + publish_attach_isprivate.value = Splinter.theAttachment.isPrivate; + + // This is a "magic string" used to identify review comments + if (Splinter.theReview.toString()) { + var comment = "Review of attachment " + Splinter.theAttachment.id + ":\n" + + "-----------------------------------------------------------------\n\n" + + Splinter.theReview.toString(); + publish_review.value = comment; + } + + if (Splinter.theAttachment.status + && Dom.get('attachmentStatus').value != Splinter.theAttachment.status) + { + publish_attach_status.value = Dom.get('attachmentStatus').value; + } + + publish_form.submit(); +}; + +Splinter.doDiscardReview = function () { + if (Splinter.theAttachment.status) { + Dom.get('attachmentStatus').value = Splinter.theAttachment.status; + } + + Dom.get('myComment').value = ''; + Dom.setStyle('emptyCommentNotice', 'display', 'block'); + + var i; + for (i = 0; i < Splinter.theReview.files.length; i++) { + while (Splinter.theReview.files[i].comments.length > 0) { + Splinter.theReview.files[i].comments[0].remove(); + } + } + + Splinter.updateMyPatchComments(); + Splinter.updateHaveDraft(); + Splinter.saveDraft(); +}; + +Splinter.discardReview = function () { + var dialog = new Splinter.Dialog("Really discard your changes?"); + dialog.addButton('No', function() {}, true); + dialog.addButton('Yes', Splinter.doDiscardReview, false); + dialog.show(); +}; + +Splinter.haveDraft = function () { + if (Splinter.theAttachment.status && Dom.get('attachmentStatus').value != Splinter.theAttachment.status) { + return true; + } + + if (Dom.get('myComment').value != '') { + return true; + } + + var i; + for (i = 0; i < Splinter.theReview.files.length; i++) { + if (Splinter.theReview.files[i].comments.length > 0) { + return true; + } + } + + for (i = 0; i < Splinter.thePatch.files.length; i++) { + if (Splinter.thePatch.files[i].fileReviewed) { + return true; + } + } + + if (Splinter.flagChanged == 1) { + return true; + } + + return false; +}; + +Splinter.updateHaveDraft = function () { + clearTimeout(Splinter.updateHaveDraftTimeoutId); + Splinter.updateHaveDraftTimeoutId = null; + + if (Splinter.haveDraft()) { + Dom.get('publishButton').removeAttribute('disabled'); + Dom.get('cancelButton').removeAttribute('disabled'); + Dom.setStyle('haveDraftNotice', 'display', 'block'); + } else { + Dom.get('publishButton').setAttribute('disabled', 'true'); + Dom.get('cancelButton').setAttribute('disabled', 'true'); + Dom.setStyle('haveDraftNotice', 'display', 'none'); + } +}; + +Splinter.queueUpdateHaveDraft = function () { + if (Splinter.updateHaveDraftTimeoutId == null) { + Splinter.updateHaveDraftTimeoutId = setTimeout(Splinter.updateHaveDraft, 0); + } +}; + +Splinter.hideSaveDraftNotice = function () { + clearTimeout(Splinter.saveDraftNoticeTimeoutId); + Splinter.saveDraftNoticeTimeoutId = null; + Dom.setStyle('saveDraftNotice', 'display', 'none'); +}; + +Splinter.saveDraft = function () { + if (Splinter.reviewStorage == null) { + return; + } + + clearTimeout(Splinter.saveDraftTimeoutId); + Splinter.saveDraftTimeoutId = null; + + Splinter.savingDraft = true; + Dom.get('saveDraftNotice').innerHTML = "Saving Draft..."; + Dom.setStyle('saveDraftNotice', 'display', 'block'); + clearTimeout(Splinter.saveDraftNoticeTimeoutId); + setTimeout(Splinter.hideSaveDraftNotice, 3000); + + if (Splinter.currentEditComment) { + Splinter.currentEditComment.comment = Splinter.Utils.strip(Dom.get("commentEditor").getElementsByTagName("textarea")[0].value); + // Messy, we don't want the empty comment in the saved draft, so remove it and + // then add it back. + if (!Splinter.currentEditComment.comment) { + Splinter.currentEditComment.remove(); + } + } + + Splinter.theReview.setIntro(Dom.get('myComment').value); + + var draftSaved = false; + if (Splinter.haveDraft()) { + var filesReviewed = {}; + for (var i = 0; i < Splinter.thePatch.files.length; i++) { + var file = Splinter.thePatch.files[i]; + if (file.fileReviewed) { + filesReviewed[file.filename] = true; + } + } + Splinter.reviewStorage.saveDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview, + { 'filesReviewed' : filesReviewed }); + draftSaved = true; + } else { + Splinter.reviewStorage.deleteDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview); + } + + if (Splinter.currentEditComment && !Splinter.currentEditComment.comment) { + Splinter.currentEditComment = Splinter.currentEditComment.file.addComment(Splinter.currentEditComment.location, + Splinter.currentEditComment.type, ""); + } + + Splinter.savingDraft = false; + if (draftSaved) { + Dom.get('saveDraftNotice').innerHTML = "Saved Draft"; + } else { + Splinter.hideSaveDraftNotice(); + } +}; + +Splinter.queueSaveDraft = function () { + if (Splinter.saveDraftTimeoutId == null) { + Splinter.saveDraftTimeoutId = setTimeout(Splinter.saveDraft, 10000); + } +}; + +Splinter.flushSaveDraft = function () { + if (Splinter.saveDraftTimeoutId != null) { + Splinter.saveDraft(); + } +}; + +Splinter.ensureCommentArea = function (row) { + var file = Splinter.domCache.data(row).patchFile; + var colSpan = file.status == Splinter.Patch.CHANGED ? 5 : 2; + + if (!row.nextSibling || row.nextSibling.className != "comment-area") { + var tr = new Element(document.createElement('tr')); + Dom.addClass(tr, 'comment-area'); + var td = new Element(document.createElement('td')); + Dom.setAttribute(td, 'colspan', colSpan); + td.appendTo(tr); + Dom.insertAfter(tr, row); + } + + return row.nextSibling.firstChild; +}; + +Splinter.getTypeClass = function (type) { + switch (type) { + case Splinter.Patch.ADDED: + return "comment-added"; + case Splinter.Patch.REMOVED: + return "comment-removed"; + case Splinter.Patch.CHANGED: + return "comment-changed"; + } + + return null; +}; + +Splinter.getSeparatorClass = function (type) { + switch (type) { + case Splinter.Patch.ADDED: + return "comment-separator-added"; + case Splinter.Patch.REMOVED: + return "comment-separator-removed"; + } + + return null; +}; + +Splinter.getReviewerClass = function (review) { + var reviewerIndex; + if (review == Splinter.theReview) { + reviewerIndex = 0; + } else { + reviewerIndex = (Splinter.reviewers[review.who] - 1) % 5 + 1; + } + + return "reviewer-" + reviewerIndex; +}; + +Splinter.addCommentDisplay = function (commentArea, comment) { + var review = comment.file.review; + + var separatorClass = Splinter.getSeparatorClass(comment.type); + if (separatorClass) { + var div = new Element(document.createElement('div')); + Dom.addClass(div, separatorClass); + Dom.addClass(div, Splinter.getReviewerClass(review)); + div.appendTo(commentArea); + } + + var commentDiv = new Element(document.createElement('div')); + Dom.addClass(commentDiv, 'comment'); + Dom.addClass(commentDiv, Splinter.getTypeClass(comment.type)); + Dom.addClass(commentDiv, Splinter.getReviewerClass(review)); + + Event.addListener(Dom.get(commentDiv), 'dblclick', function () { + Splinter.saveComment(); + Splinter.insertCommentEditor(commentArea, comment.file.patchFile, + comment.location, comment.type); + }); + + var commentFrame = new Element(document.createElement('div')); + Dom.addClass(commentFrame, 'comment-frame'); + commentFrame.appendTo(commentDiv); + + var reviewerBox = new Element(document.createElement('div')); + Dom.addClass(reviewerBox, 'reviewer-box'); + reviewerBox.appendTo(commentFrame); + + var commentText = new Element(document.createElement('div')); + Dom.addClass(commentText, 'comment-text'); + Splinter.Utils.preWrapLines(commentText, comment.comment); + commentText.appendTo(reviewerBox); + + commentDiv.appendTo(commentArea); + + if (review != Splinter.theReview) { + var reviewInfo = new Element(document.createElement('div')); + Dom.addClass(reviewInfo, 'review-info'); + + var reviewer = new Element(document.createElement('div')); + Dom.addClass(reviewer, 'reviewer'); + reviewer.appendChild(document.createTextNode(review.who)); + reviewer.appendTo(reviewInfo); + + var reviewDate = new Element(document.createElement('div')); + Dom.addClass(reviewDate, 'review-date'); + reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date))); + reviewDate.appendTo(reviewInfo); + + var reviewInfoBottom = new Element(document.createElement('div')); + Dom.addClass(reviewInfoBottom, 'review-info-bottom'); + reviewInfoBottom.appendTo(reviewInfo); + + reviewInfo.appendTo(reviewerBox); + } + + comment.div = commentDiv; +}; + +Splinter.saveComment = function () { + var comment = Splinter.currentEditComment; + if (!comment) { + return; + } + + var commentEditor = Dom.get('commentEditor'); + var commentArea = commentEditor.parentNode; + var reviewFile = comment.file; + + var hunk = comment.getHunk(); + var line = hunk.lines[comment.location - hunk.location]; + + var value = Splinter.Utils.strip(commentEditor.getElementsByTagName('textarea')[0].value); + if (value != "") { + comment.comment = value; + Splinter.addCommentDisplay(commentArea, comment); + } else { + comment.remove(); + } + + if (line.reviewComments.length > 0) { + commentEditor.parentNode.removeChild(commentEditor); + var commentEditorSeparator = Dom.get('commentEditorSeparator'); + if (commentEditorSeparator) { + commentEditorSeparator.parentNode.removeChild(commentEditorSeparator); + } + } else { + var parentToRemove = commentArea.parentNode; + commentArea.parentNode.parentNode.removeChild(parentToRemove); + } + + Splinter.currentEditComment = null; + Splinter.saveDraft(); + Splinter.queueUpdateHaveDraft(); +}; + +Splinter.cancelComment = function (previousText) { + Dom.get("commentEditor").getElementsByTagName("textarea")[0].value = previousText; + Splinter.saveComment(); +}; + +Splinter.deleteComment = function () { + Dom.get('commentEditor').getElementsByTagName('textarea')[0].value = ""; + Splinter.saveComment(); +}; + +Splinter.insertCommentEditor = function (commentArea, file, location, type) { + Splinter.saveComment(); + + var reviewFile = Splinter.theReview.getFile(file.filename); + var comment = reviewFile.getComment(location, type); + if (!comment) { + comment = reviewFile.addComment(location, type, ""); + Splinter.queueUpdateHaveDraft(); + } + + var previousText = comment.comment; + + var typeClass = Splinter.getTypeClass(type); + var separatorClass = Splinter.getSeparatorClass(type); + + var nodes = Dom.getElementsByClassName('reviewer-0', 'div', commentArea); + var i; + for (i = 0; i < nodes.length; i++) { + if (separatorClass && Dom.hasClass(nodes[i], separatorClass)) { + nodes[i].parentNode.removeChild(nodes[i]); + } + if (Dom.hasClass(nodes[i], typeClass)) { + nodes[i].parentNode.removeChild(nodes[i]); + } + } + + if (separatorClass) { + var commentEditorSeparator = new Element(document.createElement('div')); + commentEditorSeparator.set('id', 'commentEditorSeparator'); + Dom.addClass(commentEditorSeparator, separatorClass); + commentEditorSeparator.appendTo(commentArea); + } + + var commentEditor = new Element(document.createElement('div')); + Dom.setAttribute(commentEditor, 'id', 'commentEditor'); + Dom.addClass(commentEditor, typeClass); + commentEditor.appendTo(commentArea); + + var commentEditorInner = new Element(document.createElement('div')); + Dom.setAttribute(commentEditorInner, 'id', 'commentEditorInner'); + commentEditorInner.appendTo(commentEditor); + + var commentTextFrame = new Element(document.createElement('div')); + Dom.setAttribute(commentTextFrame, 'id', 'commentTextFrame'); + commentTextFrame.appendTo(commentEditorInner); + + var commentTextArea = new Element(document.createElement('textarea')); + Dom.setAttribute(commentTextArea, 'id', 'commentTextArea'); + commentTextArea.appendChild(document.createTextNode(previousText)); + commentTextArea.appendTo(commentTextFrame); + Event.addListener('commentTextArea', 'keydown', function (e) { + if (e.which == 13 && e.ctrlKey) { + Splinter.saveComment(); + } else { + Splinter.queueSaveDraft(); + } + }); + Event.addListener('commentTextArea', 'focusin', function () { Dom.addClass(commentEditor, 'focused'); }); + Event.addListener('commentTextArea', 'focusout', function () { Dom.removeClass(commentEditor, 'focused'); }); + Dom.get(commentTextArea).focus(); + + var commentEditorLeftButtons = new Element(document.createElement('div')); + commentEditorLeftButtons.set('id', 'commentEditorLeftButtons'); + commentEditorLeftButtons.appendTo(commentEditorInner); + + var commentCancel = new Element(document.createElement('input')); + commentCancel.set('id','commentCancel'); + commentCancel.set('type', 'button'); + commentCancel.set('value', 'Cancel'); + commentCancel.appendTo(commentEditorLeftButtons); + Event.addListener('commentCancel', 'click', function () { Splinter.cancelComment(previousText); }); + + if (previousText) { + var commentDelete = new Element(document.createElement('input')); + commentDelete.set('id','commentDelete'); + commentDelete.set('type', 'button'); + commentDelete.set('value', 'Delete'); + commentDelete.appendTo(commentEditorLeftButtons); + Event.addListener('commentDelete', 'click', Splinter.deleteComment); + } + + var commentEditorRightButtons = new Element(document.createElement('div')); + commentEditorRightButtons.set('id', 'commentEditorRightButtons'); + commentEditorRightButtons.appendTo(commentEditorInner); + + var commentSave = new Element(document.createElement('input')); + commentSave.set('id','commentSave'); + commentSave.set('type', 'button'); + commentSave.set('value', 'Save'); + commentSave.appendTo(commentEditorRightButtons); + Event.addListener('commentSave', 'click', Splinter.saveComment); + + var clear = new Element(document.createElement('div')); + Dom.addClass(clear, 'clear'); + clear.appendTo(commentEditorInner); + + Splinter.currentEditComment = comment; +}; + +Splinter.insertCommentForRow = function (clickRow, clickType) { + var file = Splinter.domCache.data(clickRow).patchFile; + var clickLocation = Splinter.domCache.data(clickRow).patchLocation; + + var row = clickRow; + var location = clickLocation; + var type = clickType; + + Splinter.saveComment(); + var commentArea = Splinter.ensureCommentArea(row); + Splinter.insertCommentEditor(commentArea, file, location, type); +}; + +Splinter.EL = function (element, cls, text, title) { + var e = document.createElement(element); + if (text != null) { + e.appendChild(document.createTextNode(text)); + } + if (cls) { + e.className = cls; + } + if (title) { + Dom.setAttribute(e, 'title', title); + } + + return e; +}; + +Splinter.getElementPosition = function (element) { + var left = element.offsetLeft; + var top = element.offsetTop; + var parent = element.offsetParent; + while (parent && parent != document.body) { + left += parent.offsetLeft; + top += parent.offsetTop; + parent = parent.offsetParent; + } + + return [left, top]; +}; + +Splinter.scrollToElement = function (element) { + var windowHeight; + if ('innerHeight' in window) { // Not IE + windowHeight = window.innerHeight; + } else { // IE + windowHeight = document.documentElement.clientHeight; + } + var pos = Splinter.getElementPosition(element); + var yCenter = pos[1] + element.offsetHeight / 2; + window.scrollTo(0, yCenter - windowHeight / 2); +}; + +Splinter.onRowDblClick = function (e) { + var file = Splinter.domCache.data(this).patchFile; + var type; + + if (file.status == Splinter.Patch.CHANGED) { + var pos = Splinter.getElementPosition(this); + var delta = e.pageX - (pos[0] + this.offsetWidth/2); + if (delta < - 20) { + type = Splinter.Patch.REMOVED; + } else if (delta < 20) { + type = Splinter.Patch.CHANGED; + } else { + type = Splinter.Patch.ADDED; + } + } else { + type = file.status; + } + + Splinter.insertCommentForRow(this, type); +}; + +Splinter.appendPatchTable = function (type, maxLine, parentDiv) { + var fileTableContainer = new Element(document.createElement('div')); + Dom.addClass(fileTableContainer, 'file-table-container'); + fileTableContainer.appendTo(parentDiv); + + var fileTable = new Element(document.createElement('table')); + Dom.addClass(fileTable, 'file-table'); + fileTable.appendTo(fileTableContainer); + + var colQ = new Element(document.createElement('colgroup')); + colQ.appendTo(fileTable); + + var col1, col2; + if (type != Splinter.Patch.ADDED) { + col1 = new Element(document.createElement('col')); + Dom.addClass(col1, 'line-number-column'); + Dom.setAttribute(col1, 'span', '1'); + col1.appendTo(colQ); + col2 = new Element(document.createElement('col')); + Dom.addClass(col2, 'old-column'); + Dom.setAttribute(col2, 'span', '1'); + col2.appendTo(colQ); + } + if (type == Splinter.Patch.CHANGED) { + col1 = new Element(document.createElement('col')); + Dom.addClass(col1, 'middle-column'); + Dom.setAttribute(col1, 'span', '1'); + col1.appendTo(colQ); + } + if (type != Splinter.Patch.REMOVED) { + col1 = new Element(document.createElement('col')); + Dom.addClass(col1, 'line-number-column'); + Dom.setAttribute(col1, 'span', '1'); + col1.appendTo(colQ); + col2 = new Element(document.createElement('col')); + Dom.addClass(col2, 'new-column'); + Dom.setAttribute(col2, 'span', '1'); + col2.appendTo(colQ); + } + + if (type == Splinter.Patch.CHANGED) { + Dom.addClass(fileTable, 'file-table-changed'); + } + + if (maxLine >= 1000) { + Dom.addClass(fileTable, "file-table-wide-numbers"); + } + + var tbody = new Element(document.createElement('tbody')); + tbody.appendTo(fileTable); + + return tbody; +}; + +Splinter.appendPatchHunk = function (file, hunk, tableType, includeComments, clickable, tbody, filter) { + hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags, line) { + if (filter && !filter(loc)) { + return; + } + + var tr = document.createElement("tr"); + + var oldStyle = ""; + var newStyle = ""; + if ((flags & Splinter.Patch.CHANGED) != 0) { + oldStyle = newStyle = "changed-line"; + } else if ((flags & Splinter.Patch.REMOVED) != 0) { + oldStyle = "removed-line"; + } else if ((flags & Splinter.Patch.ADDED) != 0) { + newStyle = "added-line"; + } + + var title = "Double click the line to add a review comment"; + + if (tableType != Splinter.Patch.ADDED) { + if (oldText != null) { + tr.appendChild(Splinter.EL("td", "line-number", oldLine.toString(), title)); + tr.appendChild(Splinter.EL("td", "old-line " + oldStyle, oldText != "" ? oldText : "\u00a0", title)); + oldLine++; + } else { + tr.appendChild(Splinter.EL("td", "line-number")); + tr.appendChild(Splinter.EL("td", "old-line")); + } + } + + if (tableType == Splinter.Patch.CHANGED) { + tr.appendChild(Splinter.EL("td", "line-middle")); + } + + if (tableType != Splinter.Patch.REMOVED) { + if (newText != null) { + tr.appendChild(Splinter.EL("td", "line-number", newLine.toString(), title)); + tr.appendChild(Splinter.EL("td", "new-line " + newStyle, newText != "" ? newText : "\u00a0", title)); + newLine++; + } else if (tableType == Splinter.Patch.CHANGED) { + tr.appendChild(Splinter.EL("td", "line-number")); + tr.appendChild(Splinter.EL("td", "new-line")); + } + } + + if (clickable) { + Splinter.domCache.data(tr).patchFile = file; + Splinter.domCache.data(tr).patchLocation = loc; + Event.addListener(tr, 'dblclick', Splinter.onRowDblClick); + } + + tbody.appendChild(tr); + + if (includeComments && line.reviewComments != null) { + var k; + for (k = 0; k < line.reviewComments.length; k++) { + var commentArea = Splinter.ensureCommentArea(tr); + Splinter.addCommentDisplay(commentArea, line.reviewComments[k]); + } + } + }); +}; + +Splinter.addPatchFile = function (file) { + var fileDiv = new Element(document.createElement('div')); + Dom.addClass(fileDiv, 'file'); + fileDiv.appendTo(Dom.get('files')); + file.div = fileDiv; + + var statusString; + switch (file.status) { + case Splinter.Patch.ADDED: + statusString = " (new file)"; + break; + case Splinter.Patch.REMOVED: + statusString = " (removed)"; + break; + case Splinter.Patch.CHANGED: + statusString = ""; + break; + } + + var fileLabel = new Element(document.createElement('div')); + Dom.addClass(fileLabel, 'file-label'); + fileLabel.appendTo(fileDiv); + + var fileCollapseLink = new Element(document.createElement('a')); + Dom.addClass(fileCollapseLink, 'file-label-collapse'); + fileCollapseLink.appendChild(document.createTextNode('[-]')); + Dom.setAttribute(fileCollapseLink, 'href', 'javascript:void(0);') + Dom.setAttribute(fileCollapseLink, 'onclick', "Splinter.toggleCollapsed('" + + encodeURIComponent(file.filename) + "');"); + Dom.setAttribute(fileCollapseLink, 'title', 'Click to expand or collapse this file table'); + fileCollapseLink.appendTo(fileLabel); + + var fileLabelName = new Element(document.createElement('span')); + Dom.addClass(fileLabelName, 'file-label-name'); + fileLabelName.appendChild(document.createTextNode(file.filename)); + fileLabelName.appendTo(fileLabel); + + var fileLabelStatus = new Element(document.createElement('span')); + Dom.addClass(fileLabelStatus, 'file-label-status'); + fileLabelStatus.appendChild(document.createTextNode(statusString)); + fileLabelStatus.appendTo(fileLabel); + + var fileReviewed = new Element(document.createElement('span')); + Dom.addClass(fileReviewed, 'file-review'); + Dom.setAttribute(fileReviewed, 'title', 'Indicates that a review has been completed for this file. ' + + 'This is for personal tracking purposes only and has no effect ' + + 'on the published review.'); + fileReviewed.appendTo(fileLabel); + + var fileReviewedInput = new Element(document.createElement('input')); + Dom.setAttribute(fileReviewedInput, 'type', 'checkbox'); + Dom.setAttribute(fileReviewedInput, 'id', 'file-review-checkbox-' + encodeURIComponent(file.filename)); + Dom.setAttribute(fileReviewedInput, 'onchange', "Splinter.toggleFileReviewed('" + + encodeURIComponent(file.filename) + "');"); + if (file.fileReviewed) { + Dom.setAttribute(fileReviewedInput, 'checked', 'true'); + } + fileReviewedInput.appendTo(fileReviewed); + + var fileReviewedLabel = new Element(document.createElement('label')); + Dom.addClass(fileReviewedLabel, 'file-review-label') + Dom.setAttribute(fileReviewedLabel, 'for', 'file-review-checkbox-' + encodeURIComponent(file.filename)); + fileReviewedLabel.appendChild(document.createTextNode(' Reviewed')); + fileReviewedLabel.appendTo(fileReviewed); + + var lastHunk = file.hunks[file.hunks.length - 1]; + var lastLine = Math.max(lastHunk.oldStart + lastHunk.oldCount - 1, + lastHunk.newStart + lastHunk.newCount - 1); + + var tbody = Splinter.appendPatchTable(file.status, lastLine, fileDiv); + + var i; + for (i = 0; i < file.hunks.length; i++) { + var hunk = file.hunks[i]; + if (hunk.oldStart > 1) { + var hunkHeader = Splinter.EL("tr", "hunk-header"); + tbody.appendChild(hunkHeader); + hunkHeader.appendChild(Splinter.EL("td")); // line number column + var hunkCell = Splinter.EL("td", "hunk-cell", hunk.functionLine ? hunk.functionLine : "\u00a0"); + hunkCell.colSpan = file.status == Splinter.Patch.CHANGED ? 4 : 1; + hunkHeader.appendChild(hunkCell); + } + + Splinter.appendPatchHunk(file, hunk, file.status, true, true, tbody); + } +}; + +Splinter.appendReviewComment = function (comment, parentDiv) { + var commentDiv = Splinter.EL("div", "review-patch-comment"); + Event.addListener(commentDiv, 'click', function() { + Splinter.showPatchFile(comment.file.patchFile); + if (comment.file.review == Splinter.theReview) { + // Immediately start editing the comment again + var commentDivParent = Dom.getAncestorByClassName(comment.div, 'comment-area'); + var commentArea = commentDivParent.getElementsByTagName('td')[0]; + Splinter.insertCommentEditor(commentArea, comment.file.patchFile, comment.location, comment.type); + Splinter.scrollToElement(Dom.get('commentEditor')); + } else { + // Just scroll to the comment, don't start a reply yet + Splinter.scrollToElement(Dom.get(comment.div)); + } + }); + + var inReplyTo = comment.getInReplyTo(); + if (inReplyTo) { + var div = new Element(document.createElement('div')); + Dom.addClass(div, Splinter.getReviewerClass(inReplyTo.file.review)); + div.appendTo(commentDiv); + + var reviewerBox = new Element(document.createElement('div')); + Dom.addClass(reviewerBox, 'reviewer-box'); + Splinter.Utils.preWrapLines(reviewerBox, inReplyTo.comment); + reviewerBox.appendTo(div); + + var reviewPatchCommentText = new Element(document.createElement('div')); + Dom.addClass(reviewPatchCommentText, 'review-patch-comment-text'); + Splinter.Utils.preWrapLines(reviewPatchCommentText, comment.comment); + reviewPatchCommentText.appendTo(commentDiv); + + } else { + var hunk = comment.getHunk(); + + var lastLine = Math.max(hunk.oldStart + hunk.oldCount- 1, + hunk.newStart + hunk.newCount- 1); + var tbody = Splinter.appendPatchTable(comment.type, lastLine, commentDiv); + + Splinter.appendPatchHunk(comment.file.patchFile, hunk, comment.type, false, false, tbody, + function(loc) { + return (loc <= comment.location && comment.location - loc < 5); + }); + + var tr = new Element(document.createElement('tr')); + var td = new Element(document.createElement('td')); + td.appendTo(tr); + td = new Element(document.createElement('td')); + Dom.addClass(td, 'review-patch-comment-text'); + Splinter.Utils.preWrapLines(td, comment.comment); + td.appendTo(tr); + tr.appendTo(tbody); + } + + parentDiv.appendChild(commentDiv); +}; + +Splinter.appendReviewComments = function (review, parentDiv) { + var i; + for (i = 0; i < review.files.length; i++) { + var file = review.files[i]; + + if (file.comments.length == 0) { + continue; + } + + parentDiv.appendChild(Splinter.EL("div", "review-patch-file", file.patchFile.filename)); + var firstComment = true; + var j; + for (j = 0; j < file.comments.length; j++) { + if (firstComment) { + firstComment = false; + } else { + parentDiv.appendChild(Splinter.EL("div", "review-patch-comment-separator")); + } + + Splinter.appendReviewComment(file.comments[j], parentDiv); + } + } +}; + +Splinter.updateMyPatchComments = function () { + var myPatchComments = Dom.get("myPatchComments"); + myPatchComments.innerHTML = ''; + Splinter.appendReviewComments(Splinter.theReview, myPatchComments); + if (Dom.getChildren(myPatchComments).length > 0) { + Dom.setStyle(myPatchComments, 'display', 'block'); + } else { + Dom.setStyle(myPatchComments, 'display', 'none'); + } +}; + +Splinter.selectNavigationLink = function (identifier) { + var navigationLinks = Dom.getElementsByClassName('navigation-link'); + var i; + for (i = 0; i < navigationLinks.length; i++) { + Dom.removeClass(navigationLinks[i], 'navigation-link-selected'); + } + Dom.addClass(Splinter.navigationLinks[identifier], 'navigation-link-selected'); +}; + +Splinter.addNavigationLink = function (identifier, title, callback, selected) { + var navigationDiv = Dom.get('navigation'); + if (Dom.getChildren(navigationDiv).length > 0) { + navigationDiv.appendChild(document.createTextNode(' | ')); + } + + var navigationLink = new Element(document.createElement('a')); + Dom.addClass(navigationLink, 'navigation-link'); + Dom.setAttribute(navigationLink, 'href', 'javascript:void(0);'); + Dom.setAttribute(navigationLink, 'id', 'switch-' + encodeURIComponent(identifier)); + Dom.setAttribute(navigationLink, 'title', identifier); + navigationLink.appendChild(document.createTextNode(title)); + navigationLink.appendTo(navigationDiv); + + // FIXME: Find out why I need to use an id here instead of just passing + // navigationLink to Event.addListener() + Event.addListener('switch-' + encodeURIComponent(identifier), 'click', function () { + if (!Dom.hasClass(this, 'navigation-link-selected')) { + callback(); + } + }); + + if (selected) { + Dom.addClass(navigationLink, 'navigation-link-selected'); + } + + Splinter.navigationLinks[identifier] = navigationLink; +}; + +Splinter.showOverview = function () { + Splinter.selectNavigationLink('__OVERVIEW__'); + Dom.setStyle('overview', 'display', 'block'); + Dom.getElementsByClassName('file', 'div', '', function (node) { + Dom.setStyle(node, 'display', 'none'); + }); + Splinter.updateMyPatchComments(); +}; + +Splinter.showAllFiles = function () { + Splinter.selectNavigationLink('__ALL__'); + Dom.setStyle('overview', 'display', 'none'); + Dom.setStyle('file-collapse-all', 'display', 'block'); + + var i; + for (i = 0; i < Splinter.thePatch.files.length; i++) { + var file = Splinter.thePatch.files[i]; + if (!file.div) { + Splinter.addPatchFile(file); + } else { + Dom.setStyle(file.div, 'display', 'block'); + } + } +} + +Splinter.toggleCollapsed = function (filename, display) { + filename = decodeURIComponent(filename); + var i; + for (i = 0; i < Splinter.thePatch.files.length; i++) { + var file = Splinter.thePatch.files[i]; + if (!filename || filename == file.filename) { + var fileTableContainer = file.div.getElementsByClassName('file-table-container')[0]; + var fileCollapseLink = file.div.getElementsByClassName('file-label-collapse')[0]; + if (!display) { + display = Dom.getStyle(fileTableContainer, 'display') == 'block' ? 'none' : 'block'; + } + Dom.setStyle(fileTableContainer, 'display', display); + fileCollapseLink.innerHTML = display == 'block' ? '[-]' : '[+]'; + } + } +} + +Splinter.toggleFileReviewed = function (filename) { + var checkbox = Dom.get('file-review-checkbox-' + filename); + if (checkbox) { + filename = decodeURIComponent(filename); + for (var i = 0; i < Splinter.thePatch.files.length; i++) { + var file = Splinter.thePatch.files[i]; + if (file.filename == filename) { + file.fileReviewed = checkbox.checked; + + Splinter.saveDraft(); + Splinter.queueUpdateHaveDraft(); + + // Strike through file names to show review was completed + var fileNavLink = Dom.get('switch-' + encodeURIComponent(filename)); + if (file.fileReviewed) { + Dom.addClass(fileNavLink, 'file-reviewed-nav'); + } + else { + Dom.removeClass(fileNavLink, 'file-reviewed-nav'); + } + } + } + } +} + +Splinter.showPatchFile = function (file) { + Splinter.selectNavigationLink(file.filename); + Dom.setStyle('overview', 'display', 'none'); + Dom.setStyle('file-collapse-all', 'display', 'none'); + + Dom.getElementsByClassName('file', 'div', '', function (node) { + Dom.setStyle(node, 'display', 'none'); + }); + + if (file.div) { + Dom.setStyle(file.div, 'display', 'block'); + } else { + Splinter.addPatchFile(file); + } +}; + +Splinter.addFileNavigationLink = function (file) { + var basename = file.filename.replace(/.*\//, ""); + Splinter.addNavigationLink(file.filename, basename, function() { + // Splinter.addNavigationLink(file.filename, file.filename, function() { + Splinter.showPatchFile(file); + }); +}; + +Splinter.start = function () { + Dom.setStyle('attachmentInfo', 'display', 'block'); + Dom.setStyle('navigationContainer', 'display', 'block'); + Dom.setStyle('overview', 'display', 'block'); + Dom.setStyle('files', 'display', 'block'); + + //for (var i = 0; i < Splinter.configAttachmentStatuses.length; i++) { + // $("<option></option").text(Splinter.configAttachmentStatuses[i]) + // .appendTo($("#attachmentStatus")); } + + //if (Splinter.theAttachment.status != null) + // $("#attachmentStatus") + // .val(Splinter.theAttachment.status) + // .change(Splinter.queueUpdateHaveDraft); + //else + Dom.setStyle('attachmentStatusSpan', 'display', 'none'); + + if (Splinter.thePatch.intro) { + Splinter.Utils.preWrapLines(Dom.get('patchIntro'), Splinter.thePatch.intro); + } else { + Dom.setStyle('patchIntro', 'display', 'none'); + } + + Splinter.addNavigationLink('__OVERVIEW__', "Overview", Splinter.showOverview, true); + Splinter.addNavigationLink('__ALL__', "All Files", Splinter.showAllFiles, false); + + var i; + for (i = 0; i < Splinter.thePatch.files.length; i++) { + Splinter.addFileNavigationLink(Splinter.thePatch.files[i]); + } + + var navigation = Dom.get('navigation'); + + var haveDraftNotice = new Element(document.createElement('div')); + Dom.setAttribute(haveDraftNotice, 'id', 'haveDraftNotice'); + haveDraftNotice.appendChild(document.createTextNode('Draft')); + haveDraftNotice.appendTo(navigation); + + var clear = new Element(document.createElement('div')); + Dom.addClass(clear, 'clear'); + clear.appendTo(navigation); + + var numReviewers = 0; + for (i = 0; i < Splinter.theBug.comments.length; i++) { + var comment = Splinter.theBug.comments[i]; + var m = Splinter.Review.REVIEW_RE.exec(comment.text); + + if (m && parseInt(m[1], 10) == Splinter.attachmentId) { + var review = new Splinter.Review.Review(Splinter.thePatch, comment.getWho(), comment.date); + review.parse(comment.text.substr(m[0].length)); + + var reviewerIndex; + if (review.who in Splinter.reviewers) { + reviewerIndex = Splinter.reviewers[review.who]; + } else { + reviewerIndex = ++numReviewers; + Splinter.reviewers[review.who] = reviewerIndex; + } + + var reviewDiv = new Element(document.createElement('div')); + Dom.addClass(reviewDiv, 'review'); + Dom.addClass(reviewDiv, Splinter.getReviewerClass(review)); + reviewDiv.appendTo(Dom.get('oldReviews')); + + var reviewerBox = new Element(document.createElement('div')); + Dom.addClass(reviewerBox, 'reviewer-box'); + reviewerBox.appendTo(reviewDiv); + + var reviewer = new Element(document.createElement('div')); + Dom.addClass(reviewer, 'reviewer'); + reviewer.appendChild(document.createTextNode(review.who)); + reviewer.appendTo(reviewerBox); + + var reviewDate = new Element(document.createElement('div')); + Dom.addClass(reviewDate, 'review-date'); + reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date))); + reviewDate.appendTo(reviewerBox); + + var reviewInfoBottom = new Element(document.createElement('div')); + Dom.addClass(reviewInfoBottom, 'review-info-bottom'); + reviewInfoBottom.appendTo(reviewerBox); + + var reviewIntro = new Element(document.createElement('div')); + Dom.addClass(reviewIntro, 'review-intro'); + Splinter.Utils.preWrapLines(reviewIntro, review.intro? review.intro : ""); + reviewIntro.appendTo(reviewerBox); + + Dom.setStyle('oldReviews', 'display', 'block'); + + Splinter.appendReviewComments(review, reviewerBox); + } + } + + // We load the saved draft or create a new review *after* inserting the existing reviews + // so that the ordering comes out right. + + if (Splinter.reviewStorage) { + Splinter.theReview = Splinter.reviewStorage.loadDraft(Splinter.theBug, Splinter.theAttachment, Splinter.thePatch); + if (Splinter.theReview) { + var storedReviews = Splinter.reviewStorage.listReviews(); + Dom.setStyle('restored', 'display', 'block'); + for (i = 0; i < storedReviews.length; i++) { + if (storedReviews[i].bugId == Splinter.theBug.id && + storedReviews[i].attachmentId == Splinter.theAttachment.id) + { + Dom.get("restoredLastModified").innerHTML = Splinter.Utils.formatDate(new Date(storedReviews[i].modificationTime)); + // Restore file reviewed checkboxes + if (storedReviews[i].filesReviewed) { + for (var j = 0; j < Splinter.thePatch.files.length; j++) { + var file = Splinter.thePatch.files[j]; + if (storedReviews[i].filesReviewed[file.filename]) { + file.fileReviewed = true; + // Strike through file names to show that review was completed + var fileNavLink = Dom.get('switch-' + encodeURIComponent(file.filename)); + Dom.addClass(fileNavLink, 'file-reviewed-nav'); + } + } + } + } + } + } + } + + if (!Splinter.theReview) { + Splinter.theReview = new Splinter.Review.Review(Splinter.thePatch); + } + + if (Splinter.theReview.intro) { + Dom.setStyle('emptyCommentNotice', 'display', 'none'); + } + + var myComment = Dom.get('myComment'); + myComment.value = Splinter.theReview.intro ? Splinter.theReview.intro : ""; + Event.addListener(myComment, 'focus', function () { + Dom.setStyle('emptyCommentNotice', 'display', 'none'); + }); + Event.addListener(myComment, 'blur', function () { + if (myComment.value == '') { + Dom.setStyle('emptyCommentNotice', 'display', 'block'); + } + }); + Event.addListener(myComment, 'keydown', function () { + Splinter.queueSaveDraft(); + Splinter.queueUpdateHaveDraft(); + }); + + Splinter.updateMyPatchComments(); + + Splinter.queueUpdateHaveDraft(); + + Event.addListener("publishButton", "click", Splinter.publishReview); + Event.addListener("cancelButton", "click", Splinter.discardReview); +}; + +Splinter.newPageUrl = function (newBugId, newAttachmentId) { + var newUrl = Splinter.configBase; + if (newBugId != null) { + newUrl += (newUrl.indexOf("?") < 0) ? "?" : "&"; + newUrl += "bug=" + escape("" + newBugId); + if (newAttachmentId != null) { + newUrl += "&attachment=" + escape("" + newAttachmentId); + } + } + + return newUrl; +}; + +Splinter.showNote = function () { + var noteDiv = Dom.get("note"); + if (noteDiv && Splinter.configNote) { + noteDiv.innerHTML = Splinter.configNote; + Dom.setStyle(noteDiv, 'display', 'block'); + } +}; + +Splinter.showEnterBug = function () { + Splinter.showNote(); + + Event.addListener("enterBugGo", "click", function () { + var newBugId = Splinter.Utils.strip(Dom.get("enterBugInput").value); + document.location = Splinter.newPageUrl(newBugId); + }); + + Dom.setStyle('enterBug', 'display', 'block'); + + if (!Splinter.reviewStorage) { + return; + } + + var storedReviews = Splinter.reviewStorage.listReviews(); + if (storedReviews.length == 0) { + return; + } + + var i; + var reviewData = []; + for (i = storedReviews.length - 1; i >= 0; i--) { + var reviewInfo = storedReviews[i]; + var modificationDate = Splinter.Utils.formatDate(new Date(reviewInfo.modificationTime)); + var extra = reviewInfo.isDraft ? "(draft)" : ""; + + reviewData.push([ + reviewInfo.bugId, + reviewInfo.bugId + ":" + reviewInfo.attachmentId + ":" + reviewInfo.attachmentDescription, + modificationDate, + extra + ]); + } + + var attachLink = function (elLiner, oRecord, oColumn, oData) { + var splitResult = oData.split(':', 3); + elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(splitResult[0], splitResult[1]) + + "\">" + splitResult[1] + " - " + splitResult[2] + "</a>"; + }; + + var bugLink = function (elLiner, oRecord, oColumn, oData) { + elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(oData) + + "\">" + oData + "</a>"; + }; + + dsConfig = { + responseType: YAHOO.util.DataSource.TYPE_JSARRAY, + responseSchema: { fields:["bug_id","attachment", "date", "extra"] } + }; + + var columnDefs = [ + { key: "bug_id", label: "Bug", formatter: bugLink }, + { key: "attachment", label: "Attachment", formatter: attachLink }, + { key: "date", label: "Date" }, + { key: "extra", label: "Extra" } + ]; + + var dataSource = new YAHOO.util.LocalDataSource(reviewData, dsConfig); + var dataTable = new YAHOO.widget.DataTable("chooseReviewTable", columnDefs, dataSource); + + Dom.setStyle('chooseReview', 'display', 'block'); +}; + +Splinter.showChooseAttachment = function () { + var drafts = {}; + var published = {}; + if (Splinter.reviewStorage) { + var storedReviews = Splinter.reviewStorage.listReviews(); + var j; + for (j = 0; j < storedReviews.length; j++) { + var reviewInfo = storedReviews[j]; + if (reviewInfo.bugId == Splinter.theBug.id) { + if (reviewInfo.isDraft) { + drafts[reviewInfo.attachmentId] = 1; + } else { + published[reviewInfo.attachmentId] = 1; + } + } + } + } + + var attachData = []; + + var i; + for (i = 0; i < Splinter.theBug.attachments.length; i++) { + var attachment = Splinter.theBug.attachments[i]; + + if (!attachment.isPatch || attachment.isObsolete) { + continue; + } + + var href = Splinter.newPageUrl(Splinter.theBug.id, attachment.id); + + var date = Splinter.Utils.formatDate(attachment.date); + var status = (attachment.status && attachment.status != 'none') ? attachment.status : ''; + + var extra = ''; + if (attachment.id in drafts) { + extra = '(draft)'; + } else if (attachment.id in published) { + extra = '(published)'; + } + + attachData.push([ attachment.id, attachment.description, attachment.date, extra ]); + } + + var attachLink = function (elLiner, oRecord, oColumn, oData) { + elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(Splinter.theBug.id, oData) + + "\">" + oData + "</a>"; + }; + + dsConfig = { + responseType: YAHOO.util.DataSource.TYPE_JSARRAY, + responseSchema: { fields:["id","description","date", "extra"] } + }; + + var columnDefs = [ + { key: "id", label: "ID", formatter: attachLink }, + { key: "description", label: "Description" }, + { key: "date", label: "Date" }, + { key: "extra", label: "Extra" } + ]; + + var dataSource = new YAHOO.util.LocalDataSource(attachData, dsConfig); + var dataTable = new YAHOO.widget.DataTable("chooseAttachmentTable", columnDefs, dataSource); + + Dom.setStyle('chooseAttachment', 'display', 'block'); +}; + +Splinter.quickHelpToggle = function () { + var quickHelpShow = Dom.get('quickHelpShow'); + var quickHelpContent = Dom.get('quickHelpContent'); + var quickHelpToggle = Dom.get('quickHelpToggle'); + + if (quickHelpContent.style.display == 'none') { + quickHelpContent.style.display = 'block'; + quickHelpShow.style.display = 'none'; + } else { + quickHelpContent.style.display = 'none'; + quickHelpShow.style.display = 'block'; + } +}; + +Splinter.init = function () { + Splinter.showNote(); + + if (Splinter.ReviewStorage.LocalReviewStorage.available()) { + Splinter.reviewStorage = new Splinter.ReviewStorage.LocalReviewStorage(); + } + + if (Splinter.theBug == null) { + Splinter.showEnterBug(); + return; + } + + Dom.get("bugId").innerHTML = Splinter.theBug.id; + Dom.get("bugLink").setAttribute('href', Splinter.configBugzillaUrl + "show_bug.cgi?id=" + Splinter.theBug.id); + Dom.get("bugShortDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theBug.shortDesc); + Dom.get("bugReporter").appendChild(document.createTextNode(Splinter.theBug.getReporter())); + Dom.get("bugCreationDate").innerHTML = Splinter.Utils.formatDate(Splinter.theBug.creationDate); + Dom.setStyle('bugInfo', 'display', 'block'); + + if (Splinter.attachmentId) { + Splinter.theAttachment = Splinter.theBug.getAttachment(Splinter.attachmentId); + + if (Splinter.theAttachment == null) { + Splinter.displayError("Attachment " + Splinter.attachmentId + " is not an attachment to bug " + Splinter.theBug.id); + } + else if (!Splinter.theAttachment.isPatch) { + Splinter.displayError("Attachment " + Splinter.attachmentId + " is not a patch"); + Splinter.theAttachment = null; + } + } + + if (Splinter.theAttachment == null) { + Splinter.showChooseAttachment(); + + } else { + Dom.get("attachId").innerHTML = Splinter.theAttachment.id; + Dom.get("attachLink").setAttribute('href', Splinter.configBugzillaUrl + "attachment.cgi?id=" + Splinter.theAttachment.id); + Dom.get("attachDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theAttachment.description); + Dom.get("attachCreator").appendChild(document.createTextNode(Splinter.Bug._formatWho(Splinter.theAttachment.whoName, + Splinter.theAttachment.whoEmail))); + Dom.get("attachDate").innerHTML = Splinter.Utils.formatDate(Splinter.theAttachment.date); + if (Splinter.theAttachment.isObsolete) { + Dom.get("attachObsolete").innerHTML = 'OBSOLETE'; + } + Dom.setStyle('attachInfo', 'display', 'block'); + + Dom.setStyle('quickHelpShow', 'display', 'block'); + + document.title = "Patch Review of Attachment " + Splinter.theAttachment.id + + " for Bug " + Splinter.theBug.id; + + Splinter.thePatch = new Splinter.Patch.Patch(Splinter.theAttachment.data); + if (Splinter.thePatch != null) { + Splinter.start(); + } + } +}; + +YAHOO.util.Event.addListener(window, 'load', Splinter.init); diff --git a/extensions/TagNewUsers/Config.pm b/extensions/TagNewUsers/Config.pm new file mode 100644 index 000000000..cfa635c32 --- /dev/null +++ b/extensions/TagNewUsers/Config.pm @@ -0,0 +1,33 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 TagNewUsers Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::TagNewUsers; +use strict; + +use constant NAME => 'TagNewUsers'; + +use constant REQUIRED_MODULES => [ +]; + +use constant OPTIONAL_MODULES => [ +]; + +__PACKAGE__->NAME; diff --git a/extensions/TagNewUsers/Extension.pm b/extensions/TagNewUsers/Extension.pm new file mode 100644 index 000000000..d71c4ea20 --- /dev/null +++ b/extensions/TagNewUsers/Extension.pm @@ -0,0 +1,242 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 TagNewUsers Extension. +# +# The Initial Developer of the Original Code is the Mozilla Foundation +# Portions created by the Initial Developers are Copyright (C) 2011 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Byron Jones <bjones@mozilla.com> + +package Bugzilla::Extension::TagNewUsers; +use strict; +use base qw(Bugzilla::Extension); +use Bugzilla::Field; +use Bugzilla::User; +use Bugzilla::Install::Util qw(indicate_progress); +use Date::Parse; +use Scalar::Util qw(blessed); + +# users younger than PROFILE_AGE days will be tagged as new +use constant PROFILE_AGE => 60; + +# users with fewer comments than COMMENT_COUNT will be tagged as new +use constant COMMENT_COUNT => 25; + +our $VERSION = '1'; + +# +# install +# + +sub install_update_db { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if (!$dbh->bz_column_info('profiles', 'comment_count')) { + $dbh->bz_add_column('profiles', 'comment_count', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?'); + my $ra = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who'); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({ current => $count++, total => $total, every => 25 }); + my ($user_id, $count) = @$ra_row; + $sth->execute($count, $user_id); + } + } + + if (!$dbh->bz_column_info('profiles', 'creation_ts')) { + $dbh->bz_add_column('profiles', 'creation_ts', + {TYPE => 'DATETIME'}); + my $creation_date_fieldid = get_field_id('creation_ts'); + my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?'); + my $ra = $dbh->selectall_arrayref(" + SELECT p.userid, a.profiles_when + FROM profiles p + LEFT JOIN profiles_activity a ON a.userid=p.userid + AND a.fieldid=$creation_date_fieldid + "); + my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({ current => $count++, total => $total, every => 25 }); + my ($user_id, $when) = @$ra_row; + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " . + $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " . + $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " . + $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + $when = $now; + } + + $sth->execute($when, $user_id); + } + } + + if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) { + $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'}); + my $sth_update = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?'); + my $sth_select = $dbh->prepare( + 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' . $dbh->sql_limit(1) + ); + my $ra = $dbh->selectcol_arrayref('SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1'); + my $count = 1; + my $total = scalar @$ra; + foreach my $user_id (@$ra) { + indicate_progress({ current => $count++, total => $total, every => 25 }); + $sth_select->execute($user_id); + my ($bug_id) = $sth_select->fetchrow_array; + $sth_update->execute($bug_id, $user_id); + } + } +} + +# +# objects +# + +BEGIN { + *Bugzilla::User::update_comment_count = \&_update_comment_count; + *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id; +} + +sub object_columns { + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + push(@$columns, qw(comment_count creation_ts first_patch_bug_id)); + } +} + +sub object_before_create { + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + if ($class->isa('Bugzilla::User')) { + my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + $params->{comment_count} = 0; + $params->{creation_ts} = $timestamp; + } elsif ($class->isa('Bugzilla::Attachment')) { + if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) { + Bugzilla->user->first_patch_bug_id($params->{bug}->id); + } + } +} + +sub bug_end_of_create { + Bugzilla->user->update_comment_count(); +} + +sub bug_end_of_update { + Bugzilla->user->update_comment_count(); +} + +sub _update_comment_count { + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $id = $self->id; + my ($count) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM longdescs WHERE who=?", + undef, $id + ); + return if $self->{comment_count} == $count; + $dbh->do( + 'UPDATE profiles SET comment_count=? WHERE userid=?', + undef, $count, $id + ); + $self->{comment_count} = $count; +} + +sub _first_patch_bug_id { + my ($self, $bug_id) = @_; + return $self->{first_patch_bug_id} unless defined $bug_id; + + Bugzilla->dbh->do( + 'UPDATE profiles SET first_patch_bug_id=? WHERE userid=?', + undef, $bug_id, $self->id + ); + $self->{first_patch_bug_id} = $bug_id; +} + +# +# +# + +sub template_before_process { + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + if ($file eq 'bug/comments.html.tmpl') { + + # only users in canconfirm will see the new-to-bugzilla tag + return unless Bugzilla->user->in_group('canconfirm'); + + # calculate if each user that has commented on the bug is new + foreach my $comment (@{$vars->{bug}{comments}}) { + my $user = $comment->author; + $user->{is_new} = $self->_user_is_new($user); + } + } +} + +sub _user_is_new { + my ($self, $user) = (shift, shift); + + # if the user can confirm bugs, they are no longer new + return 0 if $user->in_group('canconfirm'); + + # store the age in days, for the 'new to bugzilla' tooltip + my $age = sprintf("%.0f", (time() - str2time($user->{creation_ts})) / 86400); + $user->{creation_age} = $age; + + return + ($user->{comment_count} <= COMMENT_COUNT) + || ($user->{creation_age} <= PROFILE_AGE); +} + +sub webservice_user_get { + my ($self, $args) = @_; + my ($webservice, $params, $users) = @$args{qw(webservice params users)}; + + foreach my $user (@$users) { + # Most of the time the hash values are XMLRPC::Data objects + my $email = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'}; + if ($email) { + my $user_obj = Bugzilla::User->new({ name => $email }); + $user->{'is_new'} + = $webservice->type('boolean', $self->_user_is_new($user_obj) ? 1 : 0); + } + } +} + +__PACKAGE__->NAME; diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl new file mode 100644 index 000000000..6201c587a --- /dev/null +++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl @@ -0,0 +1,25 @@ +[%# + # 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 TagNewUsers Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +<link + href="[% 'extensions/TagNewUsers/web/style.css' FILTER mtime FILTER html %]" + rel="stylesheet" type="text/css" > + diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl new file mode 100644 index 000000000..274d26435 --- /dev/null +++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl @@ -0,0 +1,37 @@ +[%# + # 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 TagNewUsers Extension + # + # The Initial Developer of the Original Code is the Mozilla Foundation + # Portions created by the Initial Developers are Copyright (C) 2011 the + # Initial Developer. All Rights Reserved. + # + # Contributor(s): + # Byron Jones <bjones@mozilla.com> + #%] + +[% IF comment.author.is_new %] +<span + class="new_user" + title=" +[%- comment.author.comment_count %] comment[% "s" IF comment.author.comment_count != 1 -%] +, created [% +IF comment.author.creation_age == 0 %]today[% +ELSIF comment.author.creation_age > 365 %]more than a year ago[% +ELSE %][% comment.author.creation_age %] day[% "s" IF comment.author.creation_age != 1 %] ago[% END %]." + > +(New to [% terms.Bugzilla %]) +</span> +[% END %] +[% IF comment.is_about_attachment && comment.author.first_patch_bug_id == bug.id %] +<span class="new_user">(First Patch)</span> +[% END %] diff --git a/extensions/TagNewUsers/web/style.css b/extensions/TagNewUsers/web/style.css new file mode 100644 index 000000000..2e863eb13 --- /dev/null +++ b/extensions/TagNewUsers/web/style.css @@ -0,0 +1,16 @@ +/* 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. + * + */ + +.new_user { + color: #448844; +} + diff --git a/extensions/TypeSniffer/Config.pm b/extensions/TypeSniffer/Config.pm new file mode 100644 index 000000000..6ad03b362 --- /dev/null +++ b/extensions/TypeSniffer/Config.pm @@ -0,0 +1,40 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 TypeSniffer Bugzilla Extension. +# +# The Initial Developer of the Original Code is The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@mozilla.org> + +package Bugzilla::Extension::TypeSniffer; +use strict; + +use constant NAME => 'TypeSniffer'; + +use constant REQUIRED_MODULES => [ + { + package => 'File-MimeInfo', + module => 'File::MimeInfo::Magic', + version => '0' + }, + { + package => 'IO-stringy', + module => 'IO::Scalar', + version => '0' + }, +]; + +__PACKAGE__->NAME;
\ No newline at end of file diff --git a/extensions/TypeSniffer/Extension.pm b/extensions/TypeSniffer/Extension.pm new file mode 100644 index 000000000..fcd4f7dd6 --- /dev/null +++ b/extensions/TypeSniffer/Extension.pm @@ -0,0 +1,58 @@ +# -*- Mode: perl; indent-tabs-mode: nil -*- +# +# 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 TypeSniffer Bugzilla Extension. +# +# The Initial Developer of the Original Code is The Mozilla Foundation. +# Portions created by the Initial Developer are Copyright (C) 2010 the +# Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Gervase Markham <gerv@mozilla.org> + +package Bugzilla::Extension::TypeSniffer; +use strict; +use base qw(Bugzilla::Extension); + +use File::MimeInfo::Magic; +use IO::Scalar; + +our $VERSION = '0.02'; +################################################################################ +# This extension uses magic to guess MIME types for data where the browser has +# told us it's application/octet-stream (probably because there's no file +# extension, or it's a text type with a non-.txt file extension). +################################################################################ +sub attachment_process_data { + my ($self, $args) = @_; + my $attributes = $args->{'attributes'}; + my $params = Bugzilla->input_params; + + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer. + if ($params->{'contenttypemethod'} eq 'autodetect' && + $attributes->{'mimetype'} eq 'application/octet-stream') + { + # data is either a filehandle, or the data itself + my $fh = ${$args->{'data'}}; + if (!ref($fh)) { + $fh = IO::Scalar->new(\$fh); + } + + my $mimetype = mimetype($fh); + if ($mimetype) { + $attributes->{'mimetype'} = $mimetype; + } + } +} + +__PACKAGE__->NAME; diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm index 6a90176ec..44344d7f5 100644 --- a/extensions/Voting/Extension.pm +++ b/extensions/Voting/Extension.pm @@ -48,24 +48,13 @@ use constant DEFAULT_VOTES_PER_BUG => 1; use constant CMT_POPULAR_VOTES => 3; use constant REL_VOTER => 4; -################ -# Installation # -################ - BEGIN { - *Bugzilla::Bug::votes = \&votes; + *Bugzilla::Bug::user_votes = \&_bug_user_votes; } -sub votes { - my $self = shift; - my $dbh = Bugzilla->dbh; - - return $self->{votes} if exists $self->{votes}; - - $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', - undef, $self->id); - return $self->{votes}; -} +################ +# Installation # +################ sub db_schema_abstract_schema { my ($self, $args) = @_; @@ -123,6 +112,15 @@ sub install_update_db { # Objects # ########### +sub _bug_user_votes { + my ($self) = @_; + return $self->{'user_votes'} if exists $self->{'user_votes'}; + $self->{'user_votes'} = Bugzilla->dbh->selectrow_array( + "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?", + undef, $self->id, Bugzilla->user->id); + return $self->{'user_votes'}; +} + sub object_columns { my ($self, $args) = @_; my ($class, $columns) = @$args{qw(class columns)}; @@ -678,7 +676,7 @@ sub _modify_bug_votes { } } - $changes->{'_too_many_votes'} = \@toomanyvotes_list; + $changes->{'too_many_votes'} = \@toomanyvotes_list; # 2. too many total votes for a single user. # This part doesn't work in the general case because _remove_votes @@ -725,7 +723,7 @@ sub _modify_bug_votes { } } - $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; + $changes->{'too_many_total_votes'} = \@toomanytotalvotes_list; # 3. enough votes to confirm my $bug_list = $dbh->selectcol_arrayref( @@ -738,7 +736,7 @@ sub _modify_bug_votes { my $confirmed = _confirm_if_vote_confirmed($bug_id); push (@updated_bugs, $bug_id) if $confirmed; } - $changes->{'_confirmed_bugs'} = \@updated_bugs; + $changes->{'confirmed_bugs'} = \@updated_bugs; # Now that changes are done, we can send emails to voters. foreach my $msg (@msgs) { @@ -748,7 +746,7 @@ sub _modify_bug_votes { foreach my $bug_id (@updated_bugs) { my $sent_bugmail = Bugzilla::BugMail::Send( $bug_id, { changer => Bugzilla->user }); - $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; + $changes->{'confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; } } @@ -821,7 +819,7 @@ sub _remove_votes { }; my $voter = new Bugzilla::User($userid); - my $template = Bugzilla->template_inner($voter->setting('lang')); + my $template = Bugzilla->template_inner($voter->settings->{'lang'}->{'value'}); my $msg; $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); diff --git a/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl index 15fb1efe0..af2b1c102 100644 --- a/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl +++ b/extensions/Voting/template/en/default/hook/admin/products/updated-changes.html.tmpl @@ -56,8 +56,8 @@ <p>Checking existing votes in this product for anybody who now has too many votes for [% terms.abug %]...<br> - [% IF changes._too_many_votes.size %] - [% FOREACH detail = changes._too_many_votes %] + [% IF changes.too_many_votes.size %] + [% FOREACH detail = changes.too_many_votes %] →removed votes for [% terms.bug %] <a href="show_bug.cgi?id= [%- detail.id FILTER uri %]"> [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br> @@ -69,8 +69,8 @@ <p>Checking existing votes in this product for anybody who now has too many total votes...<br> - [% IF changes._too_many_total_votes.size %] - [% FOREACH detail = changes._too_many_total_votes %] + [% IF changes.too_many_total_votes.size %] + [% FOREACH detail = changes.too_many_total_votes %] →removed votes for [% terms.bug %] <a href="show_bug.cgi?id= [%- detail.id FILTER uri %]"> [%- detail.id FILTER html %]</a> from [% detail.name FILTER html %]<br> @@ -82,15 +82,15 @@ <p>Checking unconfirmed [% terms.bugs %] in this product for any which now have sufficient votes...<br> - [% IF changes._confirmed_bugs.size %] - [% FOREACH id = changes._confirmed_bugs %] + [% IF changes.confirmed_bugs.size %] + [% FOREACH id = changes.confirmed_bugs %] [%# This is INCLUDED instead of PROCESSED to avoid variables getting overwritten, which happens otherwise %] [% INCLUDE bug/process/results.html.tmpl type = 'votes' header_done = 1 - sent_bugmail = changes._confirmed_bugs_sent_bugmail.$id + sent_bugmail = changes.confirmed_bugs_sent_bugmail.$id id = id %] [% END %] diff --git a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl index f73ffaebd..b57a5cb27 100644 --- a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl +++ b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl @@ -29,6 +29,9 @@ [% ELSE %] votes [% END %]</a> + [% IF bug.user_votes %] + including you + [% END %] [% END %] (<a href="page.cgi?id=voting/user.html&bug_id= [%- bug.id FILTER uri %]#vote_ diff --git a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl index 61eaf8491..169e9995d 100644 --- a/extensions/Voting/template/en/default/pages/voting/user.html.tmpl +++ b/extensions/Voting/template/en/default/pages/voting/user.html.tmpl @@ -111,15 +111,8 @@ [% FOREACH bug = product.bugs %] <tr [% IF bug.id == this_bug.id && canedit %] class="bz_bug_being_voted_on" [% END %]> - <td> - [% IF bug.id == this_bug.id && canedit %] - [% IF product.onevoteonly %] - Vote For This [% terms.Bug %] → - [% ELSE %] - Enter Votes Here → - [% END %] - [%- END %] - </td> + <td>[% IF bug.id == this_bug.id && canedit %]Enter New Vote here → + [%- END %]</td> <td align="right"><a name="vote_[% bug.id FILTER html %]"> [% IF canedit %] [% IF product.onevoteonly %] |