summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDave Lawrence <dlawrence@mozilla.com>2012-03-28 23:23:41 +0200
committerDave Lawrence <dlawrence@mozilla.com>2012-03-28 23:23:41 +0200
commit2838dad07581a783cfb4bcf32a2c003e22cb13e5 (patch)
treec2f04939781d4942d0d39fff6c02ef908b9328ae
parenta4c234e96660489c24ec68c08d4d66f3fe1b1e19 (diff)
downloadbugzilla-2838dad07581a783cfb4bcf32a2c003e22cb13e5.tar.gz
bugzilla-2838dad07581a783cfb4bcf32a2c003e22cb13e5.tar.xz
Bug 678146: Tell-Us-More extension
-rw-r--r--extensions/TellUsMore/Config.pm14
-rw-r--r--extensions/TellUsMore/Extension.pm140
-rw-r--r--extensions/TellUsMore/lib/Constants.pm89
-rw-r--r--extensions/TellUsMore/lib/Process.pm263
-rw-r--r--extensions/TellUsMore/lib/VersionMirror.pm207
-rw-r--r--extensions/TellUsMore/lib/WebService.pm259
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl10
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl36
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl24
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.header.tmpl10
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.html.tmpl36
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl24
-rw-r--r--extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl39
13 files changed, 1151 insertions, 0 deletions
diff --git a/extensions/TellUsMore/Config.pm b/extensions/TellUsMore/Config.pm
new file mode 100644
index 000000000..9a20858b7
--- /dev/null
+++ b/extensions/TellUsMore/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore;
+
+use strict;
+
+use constant NAME => 'TellUsMore';
+
+__PACKAGE__->NAME;
diff --git a/extensions/TellUsMore/Extension.pm b/extensions/TellUsMore/Extension.pm
new file mode 100644
index 000000000..deffec9fe
--- /dev/null
+++ b/extensions/TellUsMore/Extension.pm
@@ -0,0 +1,140 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::TellUsMore::Constants;
+use Bugzilla::Extension::TellUsMore::VersionMirror qw(update_versions);
+use Bugzilla::Extension::TellUsMore::Process;
+
+use Scalar::Util;
+use Bugzilla::Util qw(url_quote);
+
+our $VERSION = '1';
+
+#
+# initialisation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'tell_us_more'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ token => {
+ TYPE => 'varchar(16)',
+ NOTNULL => 1,
+ },
+ mail => {
+ TYPE => 'varchar(255)',
+ NOTNULL => 1,
+ },
+ creation_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ content => {
+ TYPE => 'LONGBLOB',
+ NOTNULL => 1,
+ },
+ ],
+ };
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ # trigger a version sync during checksetup
+ my $mirror = Bugzilla::Extension::TellUsMore::VersionMirror->new();
+ if (!$mirror->check_setup(1)) {
+ print $mirror->setup_error, "\n";
+ return;
+ }
+ $mirror->refresh();
+}
+
+#
+# version mirror hooks
+#
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->created($object);
+ }
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->updated($args->{'old_object'}, $object);
+ }
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->deleted($object);
+ }
+}
+
+sub is_version {
+ my ($self, $object) = @_;
+ my $class = Scalar::Util::blessed($object);
+ return $class eq 'Bugzilla::Version';
+}
+
+sub _mirror {
+ my ($self) = @_;
+ $self->{'mirror'} ||= Bugzilla::Extension::TellUsMore::VersionMirror->new();
+ return $self->{'mirror'};
+}
+
+#
+# token validation page
+#
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+
+ if ($page eq 'tellusmore.html') {
+ my $process = Bugzilla::Extension::TellUsMore::Process->new();
+ my ($bug, $is_new_user) = $process->execute(Bugzilla->input_params->{'token'});
+ my $url;
+ if ($bug) {
+ $url = sprintf(RESULT_URL_SUCCESS, url_quote($bug->id), ($is_new_user ? '1' : '0'));
+ } else {
+ $url = sprintf(RESULT_URL_FAILURE, url_quote($process->error));
+ }
+ print Bugzilla->cgi->redirect($url);
+ exit;
+ }
+}
+
+#
+# web service
+#
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{TellUsMore} = "Bugzilla::Extension::TellUsMore::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TellUsMore/lib/Constants.pm b/extensions/TellUsMore/lib/Constants.pm
new file mode 100644
index 000000000..110146ef6
--- /dev/null
+++ b/extensions/TellUsMore/lib/Constants.pm
@@ -0,0 +1,89 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::Constants;
+
+use strict;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ TELL_US_MORE_LOGIN
+
+ MAX_ATTACHMENT_COUNT
+ MAX_ATTACHMENT_SIZE
+
+ MAX_REPORTS_PER_MINUTE
+
+ TARGET_PRODUCT
+ SECURITY_GROUP
+
+ DEFAULT_VERSION
+ DEFAULT_COMPONENT
+
+ MANDATORY_BUG_FIELDS
+ OPTIONAL_BUG_FIELDS
+
+ MANDATORY_ATTACH_FIELDS
+ OPTIONAL_ATTACH_FIELDS
+
+ TOKEN_EXPIRY_DAYS
+
+ VERSION_SOURCE_PRODUCTS
+ VERSION_TARGET_PRODUCT
+
+ RESULT_URL_SUCCESS
+ RESULT_URL_FAILURE
+);
+
+use constant TELL_US_MORE_LOGIN => 'tellusmore@input.bugs';
+
+use constant MAX_ATTACHMENT_COUNT => 2;
+use constant MAX_ATTACHMENT_SIZE => 512; # kilobytes
+
+use constant MAX_REPORTS_PER_MINUTE => 2;
+
+use constant TARGET_PRODUCT => 'Untriaged Bugs';
+use constant SECURITY_GROUP => 'core-security';
+
+use constant DEFAULT_VERSION => 'unspecified';
+use constant DEFAULT_COMPONENT => 'General';
+
+use constant MANDATORY_BUG_FIELDS => qw(
+ creator
+ description
+ product
+ summary
+ user_agent
+);
+
+use constant OPTIONAL_BUG_FIELDS => qw(
+ attachments
+ creator_name
+ restricted
+ url
+ version
+);
+
+use constant MANDATORY_ATTACH_FIELDS => qw(
+ filename
+ content_type
+ content
+);
+
+use constant OPTIONAL_ATTACH_FIELDS => qw(
+ description
+);
+
+use constant TOKEN_EXPIRY_DAYS => 7;
+
+use constant VERSION_SOURCE_PRODUCTS => ('Firefox', 'Fennec');
+use constant VERSION_TARGET_PRODUCT => 'Untriaged Bugs';
+
+use constant RESULT_URL_SUCCESS => 'http://input.mozilla.org/bug/thanks/?bug_id=%s&is_new_user=%s';
+use constant RESULT_URL_FAILURE => 'http://input.mozilla.org/bug/thanks/?error=%s';
+
+1;
diff --git a/extensions/TellUsMore/lib/Process.pm b/extensions/TellUsMore/lib/Process.pm
new file mode 100644
index 000000000..a73866468
--- /dev/null
+++ b/extensions/TellUsMore/lib/Process.pm
@@ -0,0 +1,263 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::Process;
+
+use strict;
+use warnings;
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util;
+use Bugzilla::Version;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+use Data::Dumper;
+use File::Basename;
+use MIME::Base64;
+use Safe;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = {};
+ bless($object, $class);
+ return $object;
+}
+
+sub execute {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my ($bug, $user, $is_new_user);
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ eval {
+ $self->_delete_stale_issues();
+ my ($mail, $params) = $self->_deserialise_token($token);
+
+ $dbh->bz_start_transaction();
+
+ $self->_fix_invalid_params($params);
+
+ ($user, $is_new_user) = $self->_get_user($mail, $params);
+
+ $bug = $self->_create_bug($user, $params);
+ $self->_post_bug_hook($bug);
+
+ $self->_delete_token($token);
+ $dbh->bz_commit_transaction();
+
+ $self->_send_mail($bug, $user);
+ };
+ $self->{error} = $@;
+ Bugzilla->error_mode(ERROR_MODE_WEBPAGE);
+ return $self->{error} ? undef : ($bug, $is_new_user);
+}
+
+sub error {
+ my ($self) = @_;
+ return $self->{error};
+}
+
+sub _delete_stale_issues {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # delete issues older than TOKEN_EXPIRY_DAYS
+
+ $dbh->do("
+ DELETE FROM tell_us_more
+ WHERE creation_ts < NOW() - " .
+ $dbh->sql_interval(TOKEN_EXPIRY_DAYS, 'DAY')
+ );
+}
+
+sub _deserialise_token {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # validate token
+
+ trick_taint($token);
+ my ($mail, $params) = $dbh->selectrow_array(
+ "SELECT mail,content FROM tell_us_more WHERE token=?",
+ undef, $token
+ );
+ ThrowUserError('token_does_not_exist') unless $mail;
+
+ # deserialise, return ($mail, $params)
+
+ my $compartment = Safe->new();
+ $compartment->reval($params)
+ || ThrowUserError('token_does_not_exist');
+ $params = ${$compartment->varglob('VAR1')};
+
+ return ($mail, $params);
+}
+
+sub _fix_invalid_params {
+ my ($self, $params) = @_;
+
+ # silently adjust any params which are no longer valid
+ # so we don't lose the submission
+
+ my $product = Bugzilla::Product->new({ name => TARGET_PRODUCT })
+ || ThrowUserError('invalid_product_name', { product => TARGET_PRODUCT });
+
+ # component --> general
+
+ my $component = Bugzilla::Component->new({ product => $product, name => $params->{component} })
+ || Bugzilla::Component->new({ product => $product, name => DEFAULT_COMPONENT })
+ || ThrowUserError('tum_invalid_component', { product => TARGET_PRODUCT, name => DEFAULT_COMPONENT });
+ $params->{component} = $component->name;
+
+ # version --> unspecified
+
+ my $version = Bugzilla::Version->new({ product => $product, name => $params->{version} })
+ || Bugzilla::Version->new({ product => $product, name => DEFAULT_VERSION });
+ $params->{version} = $version->name;
+}
+
+sub _get_user {
+ my ($self, $mail, $params) = @_;
+
+ # return existing bmo user
+
+ my $user = Bugzilla::User->new({ name => $mail });
+ return ($user, 0) if $user;
+
+ # or create new user
+
+ $user = Bugzilla::User->create({
+ login_name => $mail,
+ cryptpassword => '*',
+ realname => $params->{creator_name},
+ });
+ return ($user, 1);
+}
+
+sub _create_bug {
+ my ($self, $user, $params) = @_;
+ my $template = Bugzilla->template;
+ my $vars = {};
+
+ # login as the user
+
+ Bugzilla->set_user($user);
+
+ # create the bug
+
+ my $create = {
+ product => $params->{product},
+ component => $params->{component},
+ short_desc => $params->{summary},
+ comment => $params->{description},
+ version => $params->{version},
+ rep_platform => $params->{rep_platform},
+ op_sys => $params->{op_sys},
+ bug_severity => $params->{bug_severity},
+ priority => $params->{priority},
+ bug_file_loc => $params->{bug_file_loc},
+ };
+ if ($params->{group}) {
+ $create->{groups} = [ $params->{group} ];
+ };
+
+ my $bug = Bugzilla::Bug->create($create);
+
+ # add attachments
+
+ foreach my $attachment (@{$params->{attachments}}) {
+ $self->_add_attachment($bug, $attachment);
+ }
+ if (scalar @{$params->{attachments}}) {
+ $bug->update();
+ }
+
+ return $bug;
+}
+
+sub _add_attachment {
+ my ($self, $bug, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # init
+
+ my $timestamp = $dbh->selectrow_array('SELECT creation_ts FROM bugs WHERE bug_id=?', undef, $bug->bug_id);
+ my $data = decode_base64($params->{content});
+
+ my $description;
+ if ($params->{description}) {
+ $description = $params->{description};
+ } else {
+ $description = $params->{filename};
+ $description =~ s/\\/\//g;
+ $description = basename($description);
+ }
+
+ # trigger content-type auto detection
+
+ Bugzilla->input_params->{'contenttypemethod'} = 'autodetect';
+
+ # add attachment
+
+ my $attachment = Bugzilla::Attachment->create({
+ bug => $bug,
+ creation_ts => $timestamp,
+ data => $data,
+ description => $description,
+ filename => $params->{filename},
+ mimetype => $params->{content_type},
+ });
+
+ # add comment
+
+ $bug->add_comment('', {
+ isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id,
+ });
+}
+
+sub _post_bug_hook {
+ my ($self, $bug) = @_;
+
+ # trigger post_bug_after_creation hook
+
+ my $vars = {
+ id => $bug->bug_id,
+ bug => $bug,
+ };
+ Bugzilla::Hook::process('post_bug_after_creation', { vars => $vars });
+}
+
+sub _send_mail {
+ my ($self, $bug, $user) = @_;
+
+ # send new-bug email
+
+ Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
+}
+
+sub _delete_token {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # delete token
+
+ trick_taint($token);
+ $dbh->do('DELETE FROM tell_us_more WHERE token=?', undef, $token);
+}
+
+1;
+
diff --git a/extensions/TellUsMore/lib/VersionMirror.pm b/extensions/TellUsMore/lib/VersionMirror.pm
new file mode 100644
index 000000000..24c645d91
--- /dev/null
+++ b/extensions/TellUsMore/lib/VersionMirror.pm
@@ -0,0 +1,207 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::VersionMirror;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(update_versions);
+
+use Bugzilla::Constants;
+use Bugzilla::Product;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = {};
+ bless($object, $class);
+ return $object;
+}
+
+sub created {
+ my ($self, $created) = @_;
+ return unless $self->_should_process($created);
+
+ my $version = $self->_get($created);
+ if ($version) {
+ # version already exists, reactivate if required
+ if (!$version->is_active) {
+ $version->set_is_active(1);
+ $version->update();
+ }
+ } else {
+ # create version
+ $self->_create_version($created->name);
+ }
+}
+
+sub updated {
+ my ($self, $old, $new) = @_;
+ return unless $self->_should_process($old);
+
+ my $version = $self->_get($old)
+ or return;
+
+ my $updated = 0;
+ if ($version->name ne $new->name) {
+ if ($version->bug_count) {
+ # version renamed, but old name has bugs
+ # create a new version to avoid touching bugs
+ $self->_create_version($new->name);
+ return;
+ } else {
+ # renaming the version is safe as it is unused
+ $version->set_name($new->name);
+ $updated = 1;
+ }
+ }
+
+ if ($version->is_active != $new->is_active) {
+ if ($new->is_active) {
+ # activating, always safe
+ $version->set_is_active(1);
+ $updated = 1;
+ } else {
+ # can only deactivate when all source products agree
+ my $active = 0;
+ foreach my $product ($self->_sources) {
+ foreach my $product_version (@{$product->versions}) {
+ next unless _version_eq($product_version, $new);
+ if ($product_version->is_active) {
+ $active = 1;
+ last;
+ }
+ }
+ last if $active;
+ }
+ if (!$active) {
+ $version->set_is_active(0);
+ $updated = 1;
+ }
+ }
+ }
+
+ if ($updated) {
+ $version->update();
+ }
+}
+
+sub deleted {
+ my ($self, $deleted) = @_;
+ return unless $self->_should_process($deleted);
+
+ my $version = $self->_get($deleted)
+ or return;
+
+ # can only delete when all source products agreee
+ foreach my $product ($self->_sources) {
+ next if $product->name eq $deleted->product->name;
+ if (grep { _version_eq($_, $version) } @{$product->versions}) {
+ return;
+ }
+ }
+
+ if ($version->bug_count) {
+ # if there's active bugs, deactivate instead of deleting
+ $version->set_is_active(0);
+ $version->update();
+ } else {
+ # no bugs, safe to delete
+ $version->remove_from_db();
+ }
+}
+
+sub check_setup {
+ my ($self, $full) = @_;
+ $self->{setup_error} = '';
+
+ if (!$self->_target) {
+ $self->{setup_error} = "TellUsMore: Error: Target product '" . VERSION_TARGET_PRODUCT . "' does not exist.\n";
+ return 0;
+ }
+ return 1 unless $full;
+
+ foreach my $name (VERSION_SOURCE_PRODUCTS) {
+ my $product = Bugzilla::Product->new({ name => $name });
+ if (!$product) {
+ $self->{setup_error} .= "TellUsMore: Warning: Source product '$name' does not exist.\n";
+ next;
+ }
+ my $component = Bugzilla::Component->new({ product => $self->_target, name => $name });
+ if (!$component) {
+ $self->{setup_error} .= "TellUsMore: Warning: Target component '$name' does not exist.\n";
+ }
+ }
+ return $self->{setup_error} ? 0 : 1;
+}
+
+sub setup_error {
+ my ($self) = @_;
+ return $self->{setup_error};
+}
+
+sub refresh {
+ my ($self) = @_;
+ foreach my $product ($self->_sources) {
+ foreach my $version (@{$product->versions}) {
+ if (!$self->_get($version)) {
+ $self->created($version);
+ }
+ }
+ }
+}
+
+sub _should_process {
+ my ($self, $version) = @_;
+ return 0 unless $self->check_setup();
+ foreach my $product ($self->_sources) {
+ return 1 if $version->product->name eq $product->name;
+ }
+ return 0;
+}
+
+sub _get {
+ my ($self, $query) = @_;
+ my $name = ref($query) ? $query->name : $query;
+ my @versions = grep { $_->name eq $name } @{$self->_target->versions};
+ return scalar @versions ? $versions[0] : undef;
+}
+
+sub _sources {
+ my ($self) = @_;
+ if (!$self->{sources} || scalar(@{$self->{sources}}) != scalar VERSION_SOURCE_PRODUCTS) {
+ my @sources;
+ foreach my $name (VERSION_SOURCE_PRODUCTS) {
+ my $product = Bugzilla::Product->new({ name => $name });
+ push @sources, $product if $product;
+ }
+ $self->{sources} = \@sources;
+ }
+ return @{$self->{sources}};
+}
+
+sub _target {
+ my ($self) = @_;
+ $self->{target} ||= Bugzilla::Product->new({ name => VERSION_TARGET_PRODUCT });
+ return $self->{target};
+}
+
+sub _version_eq {
+ my ($version_a, $version_b) = @_;
+ return lc($version_a->name) eq lc($version_b->name);
+}
+
+sub _create_version {
+ my ($self, $name) = @_;
+ Bugzilla::Version->create({ product => $self->_target, value => $name });
+ # remove bugzilla's cached list of versions
+ delete $self->_target->{versions};
+}
+
+1;
diff --git a/extensions/TellUsMore/lib/WebService.pm b/extensions/TellUsMore/lib/WebService.pm
new file mode 100644
index 000000000..3ace06ef3
--- /dev/null
+++ b/extensions/TellUsMore/lib/WebService.pm
@@ -0,0 +1,259 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService Bugzilla::Extension);
+
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Mailer;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::UserAgent;
+use Bugzilla::Util;
+use Bugzilla::Version;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+use Data::Dumper;
+use Email::MIME;
+use MIME::Base64;
+
+sub submit {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # validation
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ if ($user->email ne TELL_US_MORE_LOGIN) {
+ ThrowUserError('tum_auth_failure');
+ }
+
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowUserError('tum_updates_disabled');
+ }
+
+ $self->_validate_params($params);
+ $self->_set_missing_params($params);
+
+ my $creator = $self->_get_user($params->{creator});
+ if ($creator && $creator->disabledtext ne '') {
+ ThrowUserError('tum_account_disabled', { user => $creator });
+ }
+
+ $self->_validate_rate($params);
+
+ # create transient entry and email
+
+ $dbh->bz_start_transaction();
+ my $token = Bugzilla::Token::GenerateUniqueToken('tell_us_more', 'token');
+ my $id = $self->_insert($params, $token);
+ my $email = $self->_generate_email($params, $token, $creator);
+ $dbh->bz_commit_transaction();
+
+ # send email
+
+ MessageToMTA($email);
+
+ # done, return the id from the tell_us_more table
+
+ return $id;
+}
+
+sub _validate_params {
+ my ($self, $params) = @_;
+
+ $self->_validate_mandatory($params, 'Submission', MANDATORY_BUG_FIELDS);
+ $self->_remove_invalid_fields($params, MANDATORY_BUG_FIELDS, OPTIONAL_BUG_FIELDS);
+
+ if (!validate_email_syntax($params->{creator})) {
+ ThrowUserError('illegal_email_address', { addr => $params->{creator} });
+ }
+
+ if ($params->{attachments}) {
+ if (scalar @{$params->{attachments}} > MAX_ATTACHMENT_COUNT) {
+ ThrowUserError('tum_too_many_attachments', { max => MAX_ATTACHMENT_COUNT });
+ }
+ my $i = 0;
+ foreach my $attachment (@{$params->{attachments}}) {
+ $i++;
+ $self->_validate_mandatory($attachment, "Attachment $i", MANDATORY_ATTACH_FIELDS);
+ $self->_remove_invalid_fields($attachment, MANDATORY_ATTACH_FIELDS, OPTIONAL_ATTACH_FIELDS);
+ if (length(decode_base64($attachment->{content})) > MAX_ATTACHMENT_SIZE * 1024) {
+ ThrowUserError('tum_attachment_too_large', { filename => $attachment->{filename}, max => MAX_ATTACHMENT_SIZE });
+ }
+ }
+ }
+
+ # products are mapped to components of the target-product
+
+ Bugzilla::Component->new({ name => $params->{product}, product => $self->_target_product })
+ || ThrowUserError('invalid_product_name', { product => $params->{product} });
+}
+
+sub _set_missing_params {
+ my ($self, $params) = @_;
+
+ # set the product and component correctly
+
+ $params->{component} = $params->{product};
+ $params->{product} = TARGET_PRODUCT;
+
+ # priority, bug_severity
+
+ $params->{priority} = Bugzilla->params->{defaultpriority};
+ $params->{bug_severity} = Bugzilla->params->{defaultseverity};
+
+ # map invalid versions to 'unspecified'
+
+ if (!$params->{version}) {
+ $params->{version} = DEFAULT_VERSION;
+ } else {
+ Bugzilla::Version->new({ product => $self->_target_product, name => $params->{version} })
+ || ($params->{version} = DEFAULT_VERSION);
+ }
+
+ # set url
+
+ $params->{bug_file_loc} = $params->{url};
+
+ # detect the opsys and platform from user_agent
+
+ $ENV{HTTP_USER_AGENT} = $params->{user_agent};
+ $params->{rep_platform} = detect_platform();
+ $params->{op_sys} = detect_op_sys();
+
+ # set group based on restricted
+
+ $params->{group} = $params->{restricted} ? SECURITY_GROUP : '';
+ delete $params->{restricted};
+}
+
+sub _get_user {
+ my ($self, $email) = @_;
+
+ return Bugzilla::User->new({ name => $email });
+}
+
+sub _insert {
+ my ($self, $params, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ local $Data::Dumper::Purity = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ my $content = Dumper($params);
+ trick_taint($content);
+
+ my $sth = $dbh->prepare('
+ INSERT INTO tell_us_more(token, mail, creation_ts, content)
+ VALUES(?, ?, ?, ?)
+ ');
+ $sth->bind_param(1, $token);
+ $sth->bind_param(2, $params->{creator});
+ $sth->bind_param(3, $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'));
+ $sth->bind_param(4, $content, $dbh->BLOB_TYPE);
+ $sth->execute();
+
+ return $dbh->bz_last_key('tell_us_more', 'id');
+}
+
+sub _generate_email {
+ my ($self, $params, $token, $user) = @_;
+
+ # create email parts
+
+ my $template = Bugzilla->template_inner;
+ my ($message_header, $message_text, $message_html);
+ my $vars = {
+ token_url => correct_urlbase() . 'page.cgi?id=tellusmore.html&token=' . url_quote($token),
+ recipient_email => $params->{creator},
+ recipient_name => ($user ? $user->name : $params->{creator_name}),
+ };
+
+ my $prefix = $user ? 'existing' : 'new';
+ $template->process("email/$prefix-account.header.tmpl", $vars, \$message_header)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+ $template->process("email/$prefix-account.txt.tmpl", $vars, \$message_text)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+ $template->process("email/$prefix-account.html.tmpl", $vars, \$message_html)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+
+ # create email object
+
+ my @parts = (
+ Email::MIME->create(
+ attributes => { content_type => "text/plain" },
+ body => $message_text,
+ ),
+ Email::MIME->create(
+ attributes => { content_type => "text/html" },
+ body => $message_html,
+ ),
+ );
+ my $email = new Email::MIME("$message_header\n");
+ $email->content_type_set('multipart/alternative');
+ $email->parts_set(\@parts);
+
+ return $email;
+}
+
+sub _validate_mandatory {
+ my ($self, $params, $name, @fields) = @_;
+
+ my @missing_fields;
+ foreach my $field (@fields) {
+ if (!exists $params->{$field} || $params->{$field} eq '') {
+ push @missing_fields, $field;
+ }
+ }
+
+ if (scalar @missing_fields) {
+ ThrowUserError('tum_missing_fields', { name => $name, missing => \@missing_fields });
+ }
+}
+
+sub _remove_invalid_fields {
+ my ($self, $params, @valid_fields) = @_;
+
+ foreach my $field (keys %$params) {
+ if (!grep { $_ eq $field } @valid_fields) {
+ delete $params->{$field};
+ }
+ }
+}
+
+sub _validate_rate {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my ($report_count) = $dbh->selectrow_array('
+ SELECT COUNT(*)
+ FROM tell_us_more
+ WHERE mail = ?
+ AND creation_ts >= NOW() - ' . $dbh->sql_interval(1, 'MINUTE')
+ , undef, $params->{creator}
+ );
+ if ($report_count + 1 > MAX_REPORTS_PER_MINUTE) {
+ ThrowUserError('tum_rate_exceeded', { max => MAX_REPORTS_PER_MINUTE });
+ }
+}
+
+sub _target_product {
+ my ($self) = @_;
+
+ my $product = Bugzilla::Product->new({ name => TARGET_PRODUCT })
+ || ThrowUserError('invalid_product_name', { product => TARGET_PRODUCT });
+ return $product;
+}
+
+1;
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl
new file mode 100644
index 000000000..b4d5b10dc
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Subject: Thank you filing a [% terms.bug %] on Firefox!
+From: Mozilla Development Team <nobody@mozilla.org>
+To: [% recipient_name FILTER none %] <[% recipient_email FILTER none %]>
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl
new file mode 100644
index 000000000..923caded6
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+<p>
+Thank you filing a [% terms.bug %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+</p>
+
+<p>
+Wait one second though! Before this [% terms.bug %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+</p>
+
+<p>
+<a href="[% token_url FILTER none %]">[% token_url FILTER html %]</a>
+</p>
+
+<p>
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+</p>
+
+<p>
+<a href="http://www.mozilla.org/contribute/">http://www.mozilla.org/contribute/</a>
+</p>
+
+<p>
+Thank You For Your Help,<br>
+The Firefox Development Team
+</p>
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl
new file mode 100644
index 000000000..e3d82e37f
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Thank you filing a [% terms.bug %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+
+Wait one second though! Before this [% terms.bug %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+
+[% token_url FILTER none %]
+
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+
+http://www.mozilla.org/contribute/
+
+Thank You For Your Help,
+The Firefox Development Team
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl
new file mode 100644
index 000000000..b4d5b10dc
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Subject: Thank you filing a [% terms.bug %] on Firefox!
+From: Mozilla Development Team <nobody@mozilla.org>
+To: [% recipient_name FILTER none %] <[% recipient_email FILTER none %]>
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl
new file mode 100644
index 000000000..41006592b
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+<p>
+Thank you filing a [% terms.bugs %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+</p>
+
+<p>
+Wait one second though! Before this [% terms.bugs %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+</p>
+
+<p>
+<a href="[% token_url FILTER none %]">[% token_url FILTER html %]</a>
+</p>
+
+<p>
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+</p>
+
+<p>
+<a href="http://www.mozilla.org/contribute/">http://www.mozilla.org/contribute/</a>
+</p>
+
+<p>
+Thank You For Your Help,<br>
+The Firefox Development Team
+</p>
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl
new file mode 100644
index 000000000..93d2a3eea
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Thank you filing a [% terms.bugs %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+
+Wait one second though! Before this [% terms.bugs %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+
+[% token_url FILTER none %]
+
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+
+http://www.mozilla.org/contribute/
+
+Thank You For Your Help,
+The Firefox Development Team
diff --git a/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..16fe2f1a3
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,39 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tum_auth_failure" %]
+ You are not authorized to use this feature.
+
+[% ELSIF error == "tum_updates_disabled" %]
+ Issue reporting is temporarily disabled; please wait a few minutes and try again.
+
+[% ELSIF error == "tum_account_disabled" %]
+ The account associated with '[% user.login FILTER none %]' is disabled:
+ [% user.disabledtext FILTER none %]
+
+[% ELSIF error == "tum_too_many_attachments" %]
+ More than [% max FILTER none %] attachments were provided.
+
+[% ELSIF error == "tum_attachment_too_large" %]
+ [% filename FILTER none %] is larger than [% max FILTER none %]k.
+
+[% ELSIF error == "tum_missing_fields" %]
+ [% name FILTER none %] is missing the following mandatory field
+ [%~ "s" IF missing.size > 1 %]: [%%]
+ [% FOREACH field = missing %]
+ [% field FILTER none %]
+ [%~ ", " UNLESS loop.last() %]
+ [% END %]
+
+[% ELSIF error == "tum_rate_exceeded" %]
+ You cannot report more than [% max FILTER none %] issues per minute.
+
+[% ELSIF error == "tum_invalid_component" %]
+ The product [% product FILTER none %] is missing the component [% name FILTER none %].
+
+[% END %]