summaryrefslogtreecommitdiffstats
path: root/extensions/EditTable
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/EditTable')
-rw-r--r--extensions/EditTable/Config.pm15
-rw-r--r--extensions/EditTable/Extension.pm180
-rw-r--r--extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/EditTable/template/en/default/pages/edit_table.html.tmpl43
-rw-r--r--extensions/EditTable/web/js/edit_table.js131
-rw-r--r--extensions/EditTable/web/styles/edit_table.css39
7 files changed, 436 insertions, 0 deletions
diff --git a/extensions/EditTable/Config.pm b/extensions/EditTable/Config.pm
new file mode 100644
index 000000000..d601951a4
--- /dev/null
+++ b/extensions/EditTable/Config.pm
@@ -0,0 +1,15 @@
+# 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::EditTable;
+use strict;
+
+use constant NAME => 'EditTable';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditTable/Extension.pm b/extensions/EditTable/Extension.pm
new file mode 100644
index 000000000..a10a30e57
--- /dev/null
+++ b/extensions/EditTable/Extension.pm
@@ -0,0 +1,180 @@
+# 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.
+
+
+# this is a quick and dirty table editor, designed to allow admins to quickly
+# maintain tables.
+#
+# each table must be defined via the editable_tables hook
+#
+# this extension doesn't currently provide any ability to modify or validate
+# values. use with caution!
+
+package Bugzilla::Extension::EditTable;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Util qw(trick_taint);
+use JSON;
+use Storable qw(dclone);
+
+our $VERSION = '1';
+
+# definitions for tables which we can edit with the quick-and-dirty editor
+#
+# $table_name => {
+# id_field => name of the "id" field
+# order_by => the field to sort rows by (optional, defaults to the id_field)
+# blurb => text which describes the table
+# group => group required to edit this table (optional, defaults to "admin")
+# }
+#
+# example:
+# 'antispam_domain_blocklist' => {
+# id_field => 'id',
+# order_by => 'domain',
+# blurb => 'List of fully qualified domain names to block at account creation time.',
+# group => 'can_configure_antispam',
+# },
+
+sub EDITABLE_TABLES {
+ my $tables = {};
+ Bugzilla::Hook::process("editable_tables", { tables => $tables });
+ return $tables;
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my ($vars, $page) = @$args{qw(vars page_id)};
+ return unless $page eq 'edit_table.html';
+ my $input = Bugzilla->input_params;
+
+ # we only support editing a particular set of tables
+ my $table_name = $input->{table};
+ exists $self->EDITABLE_TABLES()->{$table_name}
+ || ThrowUserError('edittable_unsupported', { table => $table_name } );
+ my $table = $self->EDITABLE_TABLES()->{$table_name};
+ my $id_field = $table->{id_field};
+ my $order_by = $table->{order_by} || $id_field;
+ my $group = $table->{group} || 'admin';
+ trick_taint($table_name);
+
+ Bugzilla->user->in_group($group)
+ || ThrowUserError('auth_failure', { group => $group,
+ action => 'edit',
+ object => 'tables' });
+
+ # load columns
+ my $dbh = Bugzilla->dbh;
+ my @fields = sort
+ grep { $_ ne $id_field && $_ ne $order_by; }
+ $dbh->bz_table_columns($table_name);
+ if ($order_by ne $id_field) {
+ unshift @fields, $order_by;
+ }
+
+ # update table
+ my $data = $input->{table_data};
+ my $edits = [];
+ if ($data) {
+ $data = from_json($data)->{data};
+ $edits = dclone($data);
+ eval {
+ $dbh->bz_start_transaction;
+
+ foreach my $row (@$data) {
+ map { trick_taint($_) } @$row;
+ if ($row->[0] eq '-') {
+ # add
+ shift @$row;
+ next unless grep { $_ ne '' } @$row;
+ my $placeholders = join(',', split(//, '?' x scalar(@fields)));
+ $dbh->do(
+ "INSERT INTO $table_name(" . join(',', @fields) . ") " .
+ "VALUES ($placeholders)",
+ undef,
+ @$row
+ );
+ }
+ elsif ($row->[0] < 0) {
+ # delete
+ $dbh->do(
+ "DELETE FROM $table_name WHERE $id_field=?",
+ undef,
+ -$row->[0]
+ );
+ }
+ else {
+ # update
+ my $id = shift @$row;
+ $dbh->do(
+ "UPDATE $table_name " .
+ "SET " . join(',', map { "$_ = ?" } @fields) . " " .
+ "WHERE $id_field = ?",
+ undef,
+ @$row, $id
+ );
+ }
+ }
+
+ $dbh->bz_commit_transaction;
+ $vars->{updated} = 1;
+ $edits = [];
+ };
+ if ($@) {
+ my $error = $@;
+ $error =~ s/^DBD::[^:]+::db do failed: //;
+ $error =~ s/^(.+) \[for Statement ".+$/$1/s;
+ $vars->{error} = $error;
+ $dbh->bz_rollback_transaction;
+ }
+ }
+
+ # load data from table
+ unshift @fields, $id_field;
+ $data = $dbh->selectall_arrayref(
+ "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by"
+ );
+
+ # we don't support nulls currently
+ foreach my $row (@$data) {
+ if (grep { !defined($_) } @$row) {
+ ThrowUserError('edittable_nulls', { table => $table_name } );
+ }
+ }
+
+ # apply failed edits
+ foreach my $edit (@$edits) {
+ if ($edit->[0] eq '-') {
+ push @$data, $edit;
+ }
+ else {
+ my $id = $edit->[0];
+ foreach my $row (@$data) {
+ if ($row->[0] == $id) {
+ @$row = @$edit;
+ last;
+ }
+ }
+ }
+ }
+
+ $vars->{table_name} = $table_name;
+ $vars->{blurb} = $table->{blurb};
+ $vars->{table_data} = to_json({
+ fields => \@fields,
+ id_field => $id_field,
+ data => $data,
+ });
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..f86fb4c86
--- /dev/null
+++ b/extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'tables' %]
+ tables
+[% END %]
diff --git a/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..d87270b98
--- /dev/null
+++ b/extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# 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 == "edittable_unsupported" %]
+ [% title = "Unsupported Table" %]
+ You cannot edit the table '[% table FILTER html %]'.
+
+[% ELSIF error == "edittable_nulls" %]
+ [% title = "Table Contains NULLs" %]
+ EditTable cannot edit the table '[% table FILTER html %]' as it contains NULL
+ values.
+[% END %]
diff --git a/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl
new file mode 100644
index 000000000..d81291640
--- /dev/null
+++ b/extensions/EditTable/template/en/default/pages/edit_table.html.tmpl
@@ -0,0 +1,43 @@
+[%# 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.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Edit Table"
+ javascript_urls = [ 'extensions/EditTable/web/js/edit_table.js' ]
+ style_urls = [ "extensions/EditTable/web/styles/edit_table.css" ]
+%]
+
+<h2>[% table_name FILTER html %]</h2>
+
+[% IF updated %]
+ <p id="message">Table [% table_name FILTER html %] updated.</p>
+[% ELSIF error %]
+ <p class="throw_error">[% error FILTER html FILTER html_line_break %]</p>
+[% END %]
+
+<p>[% blurb FILTER html FILTER html_line_break %]</p>
+
+<div id="edit_table"></div>
+<br>
+<form method="post" action="page.cgi" enctype="multipart/form-data"
+ onsubmit="editTable.to_json('table_data')">
+<input type="hidden" name="id" value="edit_table.html">
+<input type="hidden" name="table" value="[% table_name FILTER html %]">
+<input type="hidden" name="table_data" id="table_data">
+<input type="submit" value="Commit Changes" id="commit_btn" class="bz_default_hidden">
+</form>
+
+<script>
+ var table_data = [% table_data FILTER none %];
+ var editTable = new EditTable('edit_table', table_data);
+ editTable.render();
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/EditTable/web/js/edit_table.js b/extensions/EditTable/web/js/edit_table.js
new file mode 100644
index 000000000..ae239759b
--- /dev/null
+++ b/extensions/EditTable/web/js/edit_table.js
@@ -0,0 +1,131 @@
+/* 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. */
+
+
+function EditTable(parent_el, table_data) {
+ this.parent_el = YAHOO.util.Dom.get(parent_el);
+ this.table_data = table_data;
+ this.field_count = table_data.fields.length;
+ if (!JSON) JSON = YAHOO.lang.JSON;
+
+ this.render = function() {
+ // create table
+ this.parent_el.innerHTML = '';
+ var table = document.createElement('table');
+
+ // header
+ var tr = document.createElement('tr');
+ for (var i = 0; i < this.field_count; i++) {
+ var th = document.createElement('th');
+ th.appendChild(document.createTextNode(this.table_data.fields[i]));
+ tr.appendChild(th);
+ }
+ var td = document.createElement('td');
+ td.innerHTML = '&nbsp;&nbsp;';
+ tr.appendChild(td);
+ table.appendChild(tr);
+
+ // rows
+ for (var i = 0; i < table_data.data.length; i++) {
+ // skip deleted rows
+ if (this.table_data.data[i][0] < 0)
+ continue;
+ var tr = document.createElement('tr');
+ for (var j = 0; j < this.field_count; j++) {
+ var td = document.createElement('td');
+ td.appendChild(document.createTextNode(this.table_data.data[i][j]));
+ tr.appendChild(td);
+
+ if (this.table_data.fields[j] != this.table_data.id_field) {
+ td.className = 'editable';
+ td.contentEditable = true;
+ YAHOO.util.Event.addListener(td, 'keydown', this._edit_keydown, this);
+ YAHOO.util.Event.addListener(td, 'blur', this._save, this);
+ d = td;
+ }
+ }
+ var td = document.createElement('td');
+ var a = document.createElement('a');
+ a.href = '#';
+ a.innerHTML = 'x';
+ YAHOO.util.Event.addListener(a, 'click', this._remove_row, this);
+ td.appendChild(a);
+ td.className = 'action';
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+
+ this.parent_el.appendChild(table);
+
+ var add_btn = document.createElement('button');
+ add_btn.innerHTML = 'Add';
+ YAHOO.util.Event.addListener(add_btn, 'click', this._add_row, this);
+ this.parent_el.appendChild(add_btn);
+ },
+
+ this.to_json = function(target) {
+ YAHOO.util.Dom.get(target).value = JSON.stringify(this.table_data);
+ },
+
+ this._add_row = function(event, obj) {
+ var row = [];
+ for (var i = 0; i < obj.field_count; i++) {
+ row.push(obj.table_data.fields[i] == obj.table_data.id_field ? '-' : '');
+ }
+ obj.table_data.data.push(row);
+ obj.render();
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ event.preventDefault();
+ },
+
+ this._remove_row = function(event, obj) {
+ var row = event.target.parentElement.parentElement.rowIndex - 1;
+ if (obj.table_data.data[row][0] == '-') {
+ // removing a newly added row
+ obj.table_data.data.splice(row, 1);
+ }
+ else {
+ // to remove a db row we set its id to negative
+ // it'll be skipped by render, and the update script knows which id to delete
+ obj.table_data.data[row][0] = -obj.table_data.data[row][0];
+ }
+ obj.render();
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ event.preventDefault();
+ },
+
+ this._save = function(event, obj) {
+ var row = event.target.parentElement.rowIndex - 1;
+ var col = event.target.cellIndex;
+ var value = event.target.textContent;
+ if (obj.table_data.data[row][col] != event.target.textContent) {
+ obj.table_data.data[row][col] = event.target.textContent;
+ YAHOO.util.Dom.removeClass('commit_btn', 'bz_default_hidden');
+ }
+ },
+
+ this._revert = function(event, obj) {
+ var row = event.target.parentElement.rowIndex - 1;
+ var col = event.target.cellIndex;
+ event.target.replaceChild(
+ document.createTextNode(obj.table_data.data[row][col]),
+ event.target.firstChild
+ );
+ },
+
+ this._edit_keydown = function(event, obj) {
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ obj._save(event, obj);
+ document.activeElement.blur(event.target);
+ }
+ else if (event.keyCode == 27) {
+ event.preventDefault();
+ obj._revert(event, obj);
+ }
+ }
+};
diff --git a/extensions/EditTable/web/styles/edit_table.css b/extensions/EditTable/web/styles/edit_table.css
new file mode 100644
index 000000000..0b1c72db6
--- /dev/null
+++ b/extensions/EditTable/web/styles/edit_table.css
@@ -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. */
+
+
+#edit_table table {
+ border-spacing: 0;
+ border-collapse: collapse;
+ margin-bottom: 1em;
+}
+
+#edit_table td, #edit_table th {
+ padding: 5px;
+}
+
+#edit_table th {
+ background: #ccc;
+ text-align: left;
+}
+
+#edit_table .editable {
+ background: #fff;
+}
+
+#edit_table tr:hover {
+ background: #eee;
+}
+
+#edit_table .action {
+ display: none;
+}
+
+#edit_table tr:hover .action {
+ display: block;
+}
+