# service.rb - service manager # # Copyright (C) 2002 Yoshinori K. Okuji # # This file is part of BugCommunicator. # # BugCommunicator is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # BugCommunicator is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with BugCommunicator; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA require 'erb/erbl' require 'cgi' require 'net/smtp' require 'time' require 'timeout' require 'rmail/parser' require 'rmail/message' require 'rmail/address' require 'bugcomm/utils' require 'bugcomm/config' require 'bugcomm/database' require 'bugcomm/exception' require 'bugcomm/field' require 'bugcomm/followup' require 'bugcomm/attachment' require 'bugcomm/bug' require 'bugcomm/action' # Adjust the standard CGI class for BugCommunicator. class CGI def self.cleanup_hack() remove_const(:CGI_PARAMS) if const_defined?(:CGI_PARAMS) remove_const(:CGI_COOKIES) if const_defined?(:CGI_COOKIES) end @@bugcomm_mutex = Mutex.new alias :original_initialize :initialize def initialize(env, input, type = 'query') @bugcomm_env = env @bugcomm_input = BugCommunicator::StringIO.new(input) @@bugcomm_mutex.synchronize do CGI.cleanup_hack() original_initialize(type) end end alias :env_table_old :env_table def env_table @bugcomm_env end alias :stdinput_old :stdinput def stdinput @bugcomm_input end end module BugCommunicator # IO-like String class for CGI. class StringIO def initialize(str) @str = str @offset = 0 end def binmode end def read(length = nil) max = @str.length - @offset length = max if length.nil? or max < length raise ArgumentError, 'length is negative' if length < 0 return nil unless max > 0 @offset += length @str[@offset - length, length] end end class Service include BugCommunicator def initialize(config) @config = config @parser = RMail::Parser.new end private def h(s) CGI.escapeHTML(s) end def u(s) CGI.escape(s) end def compile(name, mod) debug("compiling `#{name}' with ERbLight") File.open(File.expand_path(name, @config.template_dir), 'rb') do |f| return ERbLight.new(f.read).result(mod.module_eval('binding()')) end end def error_log(exception) error("#{exception.message} (#{exception.class})") exception.backtrace.each do |s| error(s) end end def rfc2822_date(time = Time.now) time.rfc2822 end def rfc2822_msg_id "<#{Time.now.to_i}.#{rand(10000)}.bugcomm@#{@@config.host}>" end def rfc2047_q_encoding(str) if us_ascii?(str) str else new_str = '' str.each_byte do |byte| case byte when 0x20 new_str << '_' when 0x21..0x3c, 0x3e, 0x40..0x5e, 0x60..0x7e new_str << byte.chr else new_str << sprintf('=%02X', byte) end end new_str end end def get_params(hash, *params) params.collect do |param| name, re, strip, default = param val = hash[name] unless val.nil? val = val.first val = val.read if val.respond_to?('read') end if val.nil? if default.nil? raise MalformedRequestError, "`#{name}' is not specified" else val = default end else val = val.strip if strip if re and re !~ val if default.nil? raise MalformedRequestError, "invalid `#{name}': `#{val}'" else val = default end end end val end end public # What mail can do: # 1. Add a followup into a bug. # 2. Confirm an authoritative action, so performe the actual action. # # Magic strings are required for distinguish these mails. For # confirmations, use something like `[confirmation:01234abcd]'. # For followups, use something like `[project #5678]'. def serve_mail(message) db = nil begin msg = @parser.parse(message.gsub(/\r\n/, "\n")) header = msg.header from = if header.mbox_from header.mbox_from else header['from'] end info("mail from `#{from}'") unless header.field?('to') or header.field?('cc') error('neither To: nor Cc: is present') return nil end unless header.field?('subject') error('no subject is present') return nil end if header.field?('x-bugcommunicator-version') error("maybe looping message") return nil end # Add some predefined fields. header.add('X-BugCommunicator-Version', BUGCOMM_VERSION) case Utils.decode_encoded_words(header['subject']) when /\[confirmation\s+([a-fA-F0-9]+)\]/ # Confirmation. cid = $1 db = Database.new db.confirm(cid) when /\[([a-zA-Z0-9_+-]+)\s+#([1-9][0-9]*)\]/ # Followup. project = $1 bid = $2 db = Database.new db.add_followup(project, bid, message) end rescue BugCommError, NoMemoryError, ScriptError, StandardError => e error_log(e) return "#{e.message} (#{e.class})\n" + e.backtrace.join("\n") ensure db.close() unless db.nil? end nil end # What CGI can do: # 1. View all projects, bugs in a project, followups in a bug. # 2. Request an authoritative action, so send a confirmation mail. # 3. Add a new bug into a project. # # Authoritative actions consists of these below. Each pair of brackets # defines who may confirm the action. S is site administrators. P is # project administrators. A is both of them. # # 1. Add a new project. [S] # 2. Remove a project. [S] # 3. Add new project administrators into a project. [A] # 4. Remove project administrators. [A] # 5. Change mailing list addresses. [A] # 6. Change field definitions. [A] # 7. Change the status of a bug. [P] def serve_cgi(metavars, request) debug("metavars = #{metavars.inspect}") debug("request = #{request.inspect}") db = nil begin cgi = nil timeout(30) do cgi = CGI.new(metavars, request) end debug("keys = #{cgi.keys.inspect}") debug("params = #{cgi.params.inspect}") mode = cgi['mode'].first mode = mode.read if mode.respond_to?('read') html = nil case mode when nil html = cgi_top(db = Database.new(@config)) when 'project' html = cgi_project(cgi, db = Database.new(@config)) when 'add' html = cgi_add(cgi, db = Database.new(@config)) when 'remove' html = cgi_remove(cgi, db = Database.new(@config)) when 'config' html = cgi_config(cgi, db = Database.new(@config)) when 'browse' html = cgi_browse(cgi, db = Database.new(@config)) when 'search' html = cgi_search(cgi, db = Database.new(@config)) when 'submit' html = cgi_submit(cgi, db = Database.new(@config)) when 'send' html = cgi_send(cgi, db = Database.new(@config)) when 'commit' html = cgi_commit(cgi, db = Database.new(@config)) when 'get-attachment' return cgi_get_attachment(cgi, db = Database.new(@config)) else raise MalformedRequestError, "invalid mode `#{mode}'" end html = lf_to_crlf(html) "Content-Type: text/html; charset=utf-8\r\n" + "Content-Length: #{html.length}\r\n\r\n" + html rescue BugCommError => e error_log(e) return "Content-Type: text/html\r\n\r\n" + "Error\r\n" + "

Error

#{h(e.message)}
\r\n" + "" rescue NoMemoryError, ScriptError, StandardError => e error_log(e) return "Content-Type: text/html\r\n\r\n" + "Server internal error\r\n" + "

Server internal error

\r\n" + "
#{h(e.message)} (#{h(e.class.to_s)})\r\n" +
	  h(e.backtrace.join("\r\n")) + "

\r\n" + "Please send a bug report to " + "<#{h(@config.admin_address)}>." + "

" ensure db.close() unless db.nil? end end private def send_followup(project, followup, *to_addrs) header = followup.message.header # Add Recent-*. header.each_with_index do |*args| name, index = args[0][0].downcase, args[1] if 'return-path' != name and 'received' != name header.add('Recent-Date', rfc2822_date(), index) header.add('Recent-From', @config.admin_address, index + 1) header.add('Recent-To', to_addrs.join(', '), index + 2) header.add('Recent-Message-ID', rfc2822_msg_id(), index + 3) break end end # Remove hampering fields. header.delete('mail-followup-to') header.delete('reply-to') # Make sure that any followups for this mail will be sent to # BugCommunicator. header['Mail-Followup-To'] = @config.address header['Reply-To'] = @config.address Net::SMTP.start(@config.smtp_host, 25, @config.host) do |smtp| smtp.send_mail(followup.message.to_s, @config.admin_address, *to_addrs) end end def send_confirmation(cid, actions, *to_addrs) mod = Module.new url = @config.url from = @config.address sender = @config.admin_address date = rfc2822_date() msg_id = rfc2822_msg_id() mod.module_eval do @@url = url @@date = date @@from = from @@to = to_addrs.join(', ') @@subject = "BugCommunicator: [confirmation #{cid}]" @@sender = sender @@msg_id = msg_id @@actions = actions @@version = BUGCOMM_VERSION end Net::SMTP.start(@config.smtp_host, 25, @config.host) do |smtp| msg = compile('confirmation.rtxt', mod).strip smtp.send_mail(msg, sender, *to_addrs) end end def send_bug(project, bug, selection_fields, text_fields, *to_addrs) msg = RMail::Message.new header = msg.header header['Date'] = rfc2822_date() header['From'] = bug['reporter'] header['To'] = to_addrs.join(', ') header['Subject'] = "[#{project} ##{bug.bug_id}] " + rfc2047_q_encoding(bug['summary']) header['Sender'] = @config.admin_address header['Message-ID'] = rfc2822_msg_id() header['Mail-Followup-To'] = @config.address header['Mail-Reply-To'] = bug['reporter'] header['Reply-To'] = @config.address header['Mime-Version'] = "1.0 (generated by BugCommunicator #{BUGCOMM_VERSION})" header['X-BugCommunicator-Version'] = BUGCOMM_VERSION header['X-BugCommunicator-URL'] = @config.url + "?bug_id=#{u(bug.bug_id.to_s)};mode=browse;project=#{u(project)}" text = RMail::Message.new body = '' text_fields.each do |f| next if f.admin_only? next if f.multiple_lines? body << f.display_name << ': ' << bug[f.name] << "\r\n" end selection_fields.each do |f| next if f.admin_only? body << f.display_name << ': ' << bug[f.name] << "\r\n" end body << "\r\n" text_fields.each do |f| next if f.admin_only? next unless f.multiple_lines? body << f.display_name << ":\r\n" << lf_to_crlf(bug[f.name]) << "\r\n" body << "\r\n" if bug[f.name][-1] != ?\n end body.strip! if us_ascii?(body) text.body = body text.header['Content-Type'] = 'text/plain; charset=US-ASCII' text.header['Content-Transfer-Encoding'] = '7bit' else text.body = body.to_a.pack('M*') text.header['Content-Type'] = 'text/plain; charset=UTF-8' text.header['Content-Transfer-Encoding'] = 'quoted-printable' end if bug.has_attachment? file = RMail::Message.new content_type = bug.attachment.content_type contents = bug.attachment.contents if %r!\Atext/!i =~ content_type if us_ascii?(contents) file.body = lf_to_crlf(contents) file.header['Content-Type'] = content_type + '; charset=US-ASCII' file.header['Content-Transfer-Encoding'] = '7bit' else file.body = lf_to_crlf(contents).to_a.pack('M*') file.header['Content-Type'] = content_type + '; charset=UTF-8' file.header['Content-Transfer-Encoding'] = 'quoted-printable' end else file.body = lf_to_crlf(contents).to_a.pack('m*') file.header['Content-Type'] = content_type file.header['Content-Transfer-Encoding'] = 'base64' end # file.header['Content-Description'] = header['Content-Type'] = 'multipart/mixed' header['Content-Transfer-Encoding'] = '7bit' msg.preamble = "This is a multi-part MIME message.\r\n" msg.body = [text, file] else header['Content-Type'] = text.header['Content-Type'] header['Content-Transfer-Encoding'] = text.header['Content-Transfer-Enocding'] msg.body = text.body end Net::SMTP.start(@config.smtp_host, 25, @config.host) do |smtp| smtp.send_mail(msg.to_s, @config.admin_address, *to_addrs) end end # The top page. def cgi_top(db) mod = Module.new url = @config.url mod.module_eval do @@url = url @@projects = db.projects end compile('top.rhtml', mod) end # The project page. def cgi_project(cgi, db) project, = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE]) unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project) mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@selection_fields = db.selection_fields(project) @@text_fields = db.text_fields(project) end compile('project.rhtml', mod) ensure db.unlock end end # Add a project. def cgi_add(cgi, db) project, address, admins, lists = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['address'], ['admins', nil, true], ['lists', nil, true]) if RMail::Address.new(address).address.nil? raise MalformedRequestError, "invalid address `#{address}'" end admins = admins.split(/\s*,\s*/) if admins.empty? raise MalformedRequestError, "invalid admins `#{tmp}'" end admins.each do |a| if RMail::Address.new(a).address.nil? raise MalformedRequestError, "invalid admin address `#{a}'" end end lists = lists.split(/\s*,\s*/) if lists.empty? raise MalformedRequestError, "invalid lists `#{tmp}'" end lists.each do |l| if RMail::Address.new(l).address.nil? raise MalformedRequestError, "invalid list address `#{l}'" end end if db.include?(project) raise MalformedRequestError, "the project `#{project}' already exists" end action = AddProjectAction.new(address, project, admins, lists) cid = db.store_actions(action) send_confirmation(cid, action, @config.admin_address) mod = Module.new url = @config.url to_addrs = @config.admin_address.to_a mod.module_eval do @@url = url @@project = nil @@to_addrs = to_addrs end compile('add-success.rhtml', mod) end # Remove a project. def cgi_remove(cgi, db) project, address = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['address']) if RMail::Address.new(address).address.nil? raise MalformedRequestError, "invalid address `#{address}'" end unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end action = RemoveProjectAction.new(address, project) cid = db.store_actions(action) send_confirmation(cid, action, @config.admin_address) mod = Module.new url = @config.url to_addrs = @config.admin_address.to_a mod.module_eval do @@url = url @@project = nil @@to_addrs = to_addrs end compile('remove-success.rhtml', mod) end # Configure a project. def cgi_config(cgi, db) project, = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE]) unless db.include?(project) raise MalformedRequstError, "no such project `#{project}'" end begin db.lock(project) mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@selection_fields = db.selection_fields(project) @@text_fields = db.text_fields(project) @@admins = db.admins(project) @@lists = db.lists(project) end compile('config.rhtml', mod) ensure db.unlock end end # Browse a bug. def cgi_browse(cgi, db) project, bug_id = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['bug_id', /\A[0-9]+\z/, true]) unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project) mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@selection_fields = db.selection_fields(project) @@text_fields = db.text_fields(project) @@bug = db.bug(project, bug_id.to_i) @@followups = [] @@bug.followup_ids.each do |f_id| @@followups << db.followup(project, f_id) end end compile('browse.rhtml', mod) ensure db.unlock() end end # Search bugs. def cgi_search(cgi, db) project, keywords, search_followups, max, offset = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['keywords', nil, true], ['search-followups', nil, nil, false], ['max', /\A[1-9][0-9]*\z/], ['offset', /\A[0-9]+\z/]) keywords = keywords.split(/\s+/) max = max.to_i offset = offset.to_i search_text_fields = cgi['search-text-fields'] or [] unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project) mod = Module.new url = @config.url selection_fields = db.selection_fields(project) text_fields = db.text_fields(project) search_selection_fields = {} cgi.params.each do |key, val| if /\Asf-([a-zA-Z0-9_]+)\z/ =~ key name = $1 field = selection_fields.detect {|f| f.name == name} if field.nil? raise MalformedRequestError, "unknown selection field `#{name}'" end next if val.include?('any') or val.empty? val.each do |k| next if field.keywords.include?(k) next if (not field.always_set?) and k == 'none' raise MalformedRequestError, "unknown keyword `#{k}' for the selection field `#{name}'" end search_selection_fields[name] = val end end search_text_fields.each do |name| unless text_fields.detect {|f| f.name == name} raise MalformedRequestError, "unknown text field `#{name}'" end end mod.module_eval do @@url = url @@project = project @@selection_fields = selection_fields @@text_fields = text_fields @@bugs, @@size = db.search_bugs(project, search_selection_fields, keywords, search_text_fields, search_followups, offset, max) @@max = max @@offset = offset @@params = cgi.params end compile('search.rhtml', mod) ensure db.unlock() end end # Submit a bug. def cgi_submit(cgi, db) project, = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE]) attachment = cgi['attachment'].first if attachment.nil? raise MalformedRequestError, "attachment is not specified" end if attachment.original_filename.empty? attachment = nil else description, = get_params(cgi.params, ['description', /\A.{0,255}\z/, true]) attachment = Attachment.new(attachment.original_filename, attachment.content_type, description, nil, attachment.read) end unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project, false) fields = {} selection_fields = db.selection_fields(project) text_fields = db.text_fields(project) selection_fields.each do |f| next if f.admin_only? val, = get_params(cgi.params, ["sf-#{f.name}"]) unless f.keywords.include?(val) if f.always_set? raise MalformedRequestError, "invalid keyword `#{val}' for the selection field `#{f.name}'" else if val != 'none' raise MalformedRequestError, "invalid keyword `#{val}' for the selection field `#{f.name}'" end end end fields[f.name] = val end text_fields.each do |f| next if f.admin_only? val, = get_params(cgi.params, ["tf-#{f.name}"]) if (not f.can_be_empty?) and val.empty? raise MalformedRequestError, "the text field `#{f.name}' cannot be empty" end unless f.regexp.empty? if Regexp.new(f.regexp) !~ val raise MalformedRequestError, "invalid text `#{val}' for `#{f.name}'" end end unless f.multiple_lines? val.gsub!(/[\r\n]/, ' ') val.strip! if val.length > 255 raise MalformedRequestError, "the text field `#{f.name}' is too long" end end fields[f.name] = val end bug_id = db.add_bug(project, Bug.new(nil, nil, fields, attachment)) lists = db.lists(project) send_bug(project, db.bug(project, bug_id), selection_fields, text_fields, *lists) mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@to_addrs = lists end compile('submit-success.rhtml', mod) ensure db.unlock() end end def cgi_send(cgi, db) project, address, bug_id, followup_id = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['address'], ['bug_id', /\A[1-9][0-9]+\z/, nil, false], ['followup_id', /\A[1-9][0-9]+\z/, nil, false]) if RMail::Address.new(address).address.nil? raise MalformedRequestError, "invalid address `#{address}'" end if (not bug_id) and (not followup_id) raise MalformedRequestError, "neither bug_id nor followup_id is specified" end if bug_id and followup_id raise MalformedRequestError, "both bug_id and followup_id are specified" end unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project) if bug_id bug = db.bug(project, bug_id.to_i) send_bug(project, bug, db.selection_fields(project), db.text_fields(project), address) else followup = db.followup(project, followup_id.to_i) send_followup(project, followup, address) end mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@to_addr = address end compile('send-success.rhtml', mod) ensure db.unlock() end end def cgi_commit(cgi, db) project, admins, lists, address = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['admins', nil, true], ['lists', nil, true], ['address']) admins = admins.split(/\s*,\s*/) if admins.empty? raise MalformedRequestError, "admins is empty" end admins.each do |a| if RMail::Address.new(a).address.nil? raise MalformedRequestError, "invalid administrator address `#{a}'" end end lists = lists.split(/\s*,\s*/) if lists.empty? raise MalformedRequestError, "lists is empty" end lists.each do |l| if RMail::Address.new(l).address.nil? raise MalformedRequestError, "invalid list address `#{l}'" end end if RMail::Address.new(address).address.nil? raise MalformedRequestError, "invalid address `#{address}'" end begin db.lock(project) actions = [] if db.admins(project) != admins actions << ChangeAdminsAction.new(address, project, db.admins(project), admins) end if db.lists(project) != lists actions << ChangeListsAction.new(address, project, db.lists(project), lists) end selection_fields = db.selection_fields(project) text_fields = db.text_fields(project) selection_fields.each do |f| display_name, note, keywords = get_params(cgi.params, ["sf-#{f.name}-display-name", /\A.{1,255}\z/, true], ["sf-#{f.name}-note", /\A.{0,255}\z/, true], ["sf-#{f.name}-keywords", /\A.{1,255}\z/, true]) keywords = keywords.split(/\s*,\s*/) if keywords.empty? raise MalformedRequestError, "keywords for `#{f.name}' is empty" end if keywords.length != keywords.uniq.length raise MalformedRequestError, "keywords are not unique" end keywords.each do |k| if k.empty? raise MalformedRequestError, "empty keyword for `#{f.name}'" end if BugCommunicator::RESERVED_KEYWORDS.include?(k) raise MalformedRequestError, "the keyword `#{k}' is reserved" end end admin_only, searchable, always_set, remove = get_params(cgi.params, ["sf-#{f.name}-admin-only", /\Atrue\z/, nil, false], ["sf-#{f.name}-searchable", /\Atrue\z/, nil, false], ["sf-#{f.name}-always-set", /\Atrue\z/, nil, false], ["sf-#{f.name}-remove", /\Atrue\z/, nil, false]) if f.required? and remove raise MalformedRequestError, "the selection field `#{f.name}' cannot be removed" end if remove actions << RemoveFieldAction.new(address, project, f) else new_f = f.dup new_f.display_name = display_name new_f.note = note new_f.keywords = keywords new_f.admin_only = admin_only new_f.searchable = searchable new_f.always_set = always_set if new_f != f debug("old selection field: #{f.inspect}") debug("new selection field: #{new_f.inspect}") actions << ChangeFieldAction.new(address, project, f, new_f) end end end 3.times do |n| name, = get_params(cgi.params, ["new-sf-#{n}-name", BugCommunicator::FIELD_RE, true, false]) next unless name if selection_fields.detect {|f| f.name == name} or text_fields.detect {|f| f.name == name} raise MalformedRequestError, "`#{name}' already exists" end display_name, note, keywords = get_params(cgi.params, ["new-sf-#{n}-display-name", /\A.{1,255}\z/, true], ["new-sf-#{n}-note", /\A.{0,255}\z/, true], ["new-sf-#{n}-keywords", /\A.{1,255}\z/, true]) keywords = keywords.split(/\s*,\s*/) if keywords.empty? raise MalformedRequestError, "keywords for `#{name}' is empty" end if keywords.length != keywords.uniq.length raise MalformedRequestError, "keywords are not unique" end keywords.each do |k| if k.empty? raise MalformedRequestError, "empty keyword for `#{name}'" end if BugCommunicator::RESERVED_KEYWORDS.include?(k) raise MalformedRequestError, "the keyword `#{k}' is reserved" end end admin_only, searchable, always_set = get_params(cgi.params, ["new-sf-#{n}-admin-only", /\Atrue\z/, nil, false], ["new-sf-#{n}-searchable", /\Atrue\z/, nil, false], ["new-sf-#{n}-always-set", /\Atrue\z/, nil, false]) f = SelectionField.new(name, display_name, note, admin_only, searchable, false, false, keywords, always_set) actions << AddFieldAction.new(address, project, f) end text_fields.each do |f| display_name, note, regexp = get_params(cgi.params, ["tf-#{f.name}-display-name", /\A.{1,255}\z/, true], ["tf-#{f.name}-note", /\A.{0,255}\z/, true], ["tf-#{f.name}-regexp", /\A.{0,255}\z/, true]) if /\#\{/ =~ regexp raise MalformedRequestError, "dangerous regular expression `#{regexp}'" end admin_only, searchable, can_be_empty, multiple_lines, remove = get_params(cgi.params, ["tf-#{f.name}-admin-only", /\Atrue\z/, nil, false], ["tf-#{f.name}-searchable", /\Atrue\z/, nil, false], ["tf-#{f.name}-can-be-empty", /\Atrue\z/, nil, false], ["tf-#{f.name}-multiple-lines", /\Atrue\z/, nil, false], ["tf-#{f.name}-remove", /\Atrue\z/, nil, false]) if f.required? and remove raise MalformedRequestError, "the text field `#{f.name}' cannot be removed" end if remove actions << RemoveFieldAction.new(address, project, f) else new_f = f.dup new_f.display_name = display_name new_f.note = note new_f.admin_only = admin_only new_f.searchable = searchable new_f.can_be_empty = can_be_empty new_f.multiple_lines = multiple_lines new_f.regexp = regexp if new_f != f debug("old text field: #{f.inspect}") debug("new text field: #{new_f.inspect}") actions << ChangeFieldAction.new(address, project, f, new_f) end end end 3.times do |n| name, = get_params(cgi.params, ["new-tf-#{n}-name", BugCommunicator::FIELD_RE, true, false]) next unless name if selection_fields.detect {|f| f.name == name} or text_fields.detect {|f| f.name == name} raise MalformedRequestError, "`#{name}' already exists" end display_name, note, regexp = get_params(cgi.params, ["new-tf-#{n}-display-name", /\A.{1,255}\z/, true], ["new-tf-#{n}-note", /\A.{0,255}\z/, true], ["new-tf-#{n}-regexp", /\A.{0,255}\z/, true]) if /\#\{/ =~ regexp raise MalformedRequestError, "dangerous regular expression `#{regexp}'" end admin_only, searchable, can_be_empty, multiple_lines = get_params(cgi.params, ["tf-#{f.name}-admin-only", /\Atrue\z/, nil, false], ["tf-#{f.name}-searchable", /\Atrue\z/, nil, false], ["tf-#{f.name}-can-be-empty", /\Atrue\z/, nil, false], ["tf-#{f.name}-multiple-lines", /\Atrue\z/, nil, false]) f = TextField.new(name, display_name, note, admin_only, searchable, false, false, can_be_empty, multiple_lines, regexp, false) actions << AddFieldAction(address, project, f) end cid = db.store_actions(actions) admins = db.admins(project) to_addrs = if admins.include?(address) address.to_a else admins end send_confirmation(cid, actions, *to_addrs) mod = Module.new url = @config.url mod.module_eval do @@url = url @@project = project @@to_addrs = to_addrs end compile('commit-success.rhtml', mod) ensure db.close() end end def cgi_get_attachment(cgi, db) project, bug_id = get_params(cgi.params, ['project', BugCommunicator::PROJECT_RE], ['bug_id', /\A[1-9][0-9]+\z/]) unless db.include?(project) raise MalformedRequestError, "no such project `#{project}'" end begin db.lock(project) bug = db.bug(project, bug_id.to_i) unless bug.has_attachment? raise MalformedRequestError, "bug `#{bug_id}' has no attachment" end attachment = bug.attachment "Content-Type: #{attachment.content_type}\r\n" + "Content-Length: #{attachment.size}\r\n\r\n" + attachment.contents ensure db.unlock() end end end end