diff options
Diffstat (limited to 'app/models')
-rw-r--r-- | app/models/foi_attachment.rb | 7 | ||||
-rw-r--r-- | app/models/incoming_message.rb | 55 | ||||
-rw-r--r-- | app/models/info_request.rb | 100 | ||||
-rw-r--r-- | app/models/info_request_event.rb | 2 | ||||
-rw-r--r-- | app/models/outgoing_message.rb | 24 | ||||
-rw-r--r-- | app/models/profile_photo.rb | 2 | ||||
-rw-r--r-- | app/models/user.rb | 9 |
7 files changed, 104 insertions, 95 deletions
diff --git a/app/models/foi_attachment.rb b/app/models/foi_attachment.rb index fcde379e0..0340f2b83 100644 --- a/app/models/foi_attachment.rb +++ b/app/models/foi_attachment.rb @@ -71,7 +71,12 @@ class FoiAttachment < ActiveRecord::Base tries = 0 delay = 1 begin - @cached_body = File.open(self.filepath, "rb" ).read + binary_data = File.open(self.filepath, "rb" ).read + if self.content_type =~ /^text/ + @cached_body = convert_string_to_utf8_or_binary(binary_data, 'UTF-8') + else + @cached_body = binary_data + end rescue Errno::ENOENT # we've lost our cached attachments for some reason. Reparse them. if tries > BODY_MAX_TRIES diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index c914edb7e..4910d43f4 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -31,12 +31,9 @@ # Move some of the (e.g. quoting) functions here into rblib, as they feel # general not specific to IncomingMessage. -require 'alaveteli_file_types' require 'htmlentities' require 'rexml/document' require 'zip/zip' -require 'mapi/msg' -require 'mapi/convert' require 'iconv' unless RUBY_VERSION >= '1.9' class IncomingMessage < ActiveRecord::Base @@ -132,6 +129,7 @@ class IncomingMessage < ActiveRecord::Base end self.valid_to_reply_to = self._calculate_valid_to_reply_to self.last_parsed = Time.now + self.foi_attachments reload=true self.save! end end @@ -173,15 +171,29 @@ class IncomingMessage < ActiveRecord::Base super end - # And look up by URL part number to get an attachment + # And look up by URL part number and display filename to get an attachment # XXX relies on extract_attachments calling MailHandler.ensure_parts_counted - def self.get_attachment_by_url_part_number(attachments, found_url_part_number) - attachments.each do |a| - if a.url_part_number == found_url_part_number - return a + # The filename here is passed from the URL parameter, so it's the + # display_filename rather than the real filename. + def self.get_attachment_by_url_part_number_and_filename(attachments, found_url_part_number, display_filename) + attachment_by_part_number = attachments.detect { |a| a.url_part_number == found_url_part_number } + if attachment_by_part_number && attachment_by_part_number.display_filename == display_filename + # Then the filename matches, which is fine: + attachment_by_part_number + else + # Otherwise if the URL part number and filename don't + # match - this is probably due to a reparsing of the + # email. In that case, try to find a unique matching + # filename from any attachment. + attachments_by_filename = attachments.select { |a| + a.display_filename == display_filename + } + if attachments_by_filename.length == 1 + attachments_by_filename[0] + else + nil end end - return nil end # Converts email addresses we know about into textual descriptions of them @@ -556,9 +568,11 @@ class IncomingMessage < ActiveRecord::Base text end - # Returns part which contains main body text, or nil if there isn't one - def get_main_body_text_part - leaves = self.foi_attachments + # Returns part which contains main body text, or nil if there isn't one, + # from a set of foi_attachments. If the leaves parameter is empty or not + # supplied, uses its own foi_attachments. + def get_main_body_text_part(leaves=[]) + leaves = self.foi_attachments if leaves.empty? # Find first part which is text/plain or text/html # (We have to include HTML, as increasingly there are mail clients that @@ -592,6 +606,7 @@ class IncomingMessage < ActiveRecord::Base # nil in this case) return p end + # Returns attachments that are uuencoded in main body part def _uudecode_and_save_attachments(text) # Find any uudecoded things buried in it, yeuchly @@ -645,12 +660,16 @@ class IncomingMessage < ActiveRecord::Base attachment = self.foi_attachments.find_or_create_by_hexdigest(attrs[:hexdigest]) attachment.update_attributes(attrs) attachment.save! - attachments << attachment.id + attachments << attachment end + # Reload to refresh newly created foi_attachments self.reload - main_part = get_main_body_text_part + # get the main body part from the set of attachments we just created, + # not from the self.foi_attachments association - some of the total set of + # self.foi_attachments may now be obsolete + main_part = get_main_body_text_part(attachments) # we don't use get_main_body_text_internal, as we want to avoid charset # conversions, since /usr/bin/uudecode needs to deal with those. # e.g. for https://secure.mysociety.org/admin/foi/request/show_raw_email/24550 @@ -661,12 +680,14 @@ class IncomingMessage < ActiveRecord::Base c += 1 uudecode_attachment.url_part_number = c uudecode_attachment.save! - attachments << uudecode_attachment.id + attachments << uudecode_attachment end end + attachment_ids = attachments.map{ |attachment| attachment.id } # now get rid of any attachments we no longer have - FoiAttachment.destroy_all("id NOT IN (#{attachments.join(',')}) AND incoming_message_id = #{self.id}") + FoiAttachment.destroy_all(["id NOT IN (?) AND incoming_message_id = ?", + attachment_ids, self.id]) end # Returns body text as HTML with quotes flattened, and emails removed. @@ -692,7 +713,7 @@ class IncomingMessage < ActiveRecord::Base text.strip! # if there is nothing but quoted stuff, then show the subject if text == "FOLDED_QUOTED_SECTION" - text = "[Subject only] " + CGI.escapeHTML(self.subject) + text + text = "[Subject only] " + CGI.escapeHTML(self.subject || '') + text end # and display link for quoted stuff text = text.gsub(/FOLDED_QUOTED_SECTION/, "\n\n" + '<span class="unfold_link"><a href="?unfold=1#incoming-'+self.id.to_s+'">'+_("show quoted sections")+'</a></span>' + "\n\n") diff --git a/app/models/info_request.rb b/app/models/info_request.rb index adb944a7e..8f15a4ea4 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -108,6 +108,12 @@ class InfoRequest < ActiveRecord::Base states end + # Possible reasons that a request could be reported for administrator attention + def report_reasons + ["Contains defamatory material", "Not a valid request", "Request for personal information", + "Contains personal information", "Vexatious", "Other"] + end + def must_be_valid_state errors.add(:described_state, "is not a valid state") if !InfoRequest.enumerate_states.include? described_state @@ -150,6 +156,10 @@ class InfoRequest < ActiveRecord::Base end end + def user_json_for_api + is_external? ? { :name => user_name || _("Anonymous user") } : user.json_for_api + end + @@custom_states_loaded = false begin if !Rails.env.test? @@ -189,21 +199,6 @@ class InfoRequest < ActiveRecord::Base self.comments.find(:all, :conditions => 'visible') end - # Central function to do all searches - # (Not really the right place to put it, but everything can get it here, and it - # does *mainly* find info requests, via their events, so hey) - def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - offset = (page - 1) * per_page - - return ::ActsAsXapian::Search.new( - models, query, - :offset => offset, :limit => per_page, - :sort_by_prefix => order, - :sort_by_ascending => ascending, - :collapse_by_prefix => collapse - ) - end - # If the URL name has changed, then all request: queries will break unless # we update index for every event. Also reindex if prominence changes. after_update :reindex_some_request_events @@ -232,17 +227,6 @@ class InfoRequest < ActiveRecord::Base end end - # For debugging - def InfoRequest.profile_search(query) - t = Time.now.usec - for i in (1..10) - t = Time.now.usec - t - secs = t / 1000000.0 - STDOUT.write secs.to_s + " query " + i.to_s + "\n" - results = InfoRequest.full_search([InfoRequestEvent], query, "created_at", true, nil, 25, 1).results - end - end - public # When name is changed, also change the url name def title=(title) @@ -294,7 +278,7 @@ public end end def email_subject_followup(incoming_message = nil) - if incoming_message.nil? || !incoming_message.valid_to_reply_to? + if incoming_message.nil? || !incoming_message.valid_to_reply_to? || !incoming_message.subject 'Re: ' + self.email_subject_request else if incoming_message.subject.match(/^Re:/i) @@ -351,7 +335,10 @@ public # copying an email, and that doesn't matter) def InfoRequest.find_by_incoming_email(incoming_email) id, hash = InfoRequest._extract_id_hash_from_email(incoming_email) - return self.find_by_magic_email(id, hash) + if hash_from_id(id) == hash + # Not using find(id) because we don't exception raised if nothing found + find_by_id(id) + end end # Return list of info requests which *might* be right given email address @@ -478,6 +465,17 @@ public incoming_message = IncomingMessage.new ActiveRecord::Base.transaction do + + # To avoid a deadlock when simultaneously dealing with two + # incoming emails that refer to the same InfoRequest, we + # lock the row for update. In Rails 3.2.0 and later this + # can be done with info_request.with_lock or + # info_request.lock!, but upgrading to that version of + # Rails creates many other problems at the moment. In the + # interim, just use raw SQL to do the SELECT ... FOR UPDATE + raw_sql = "SELECT * FROM info_requests WHERE id = #{self.id} LIMIT 1 FOR UPDATE" + ActiveRecord::Base.connection.execute(raw_sql) + raw_email = RawEmail.new incoming_message.raw_email = raw_email incoming_message.info_request = self @@ -555,6 +553,15 @@ public ['requires_admin', 'error_message', 'attention_requested'].include?(described_state) end + # Report this request for administrator attention + def report!(reason, message, user) + ActiveRecord::Base.transaction do + set_described_state('attention_requested', user, "Reason: #{reason}\n\n#{message}") + self.attention_requested = true # tells us if attention has ever been requested + save! + end + end + # change status, including for last event for later historical purposes def set_described_state(new_state, set_by = nil, message = "") old_described_state = described_state @@ -892,24 +899,6 @@ public return Digest::SHA1.hexdigest(id.to_s + AlaveteliConfiguration::incoming_email_secret)[0,8] end - # Called by find_by_incoming_email - and used to be called by separate - # function for envelope from address, until we abandoned it. - def InfoRequest.find_by_magic_email(id, hash) - expected_hash = InfoRequest.hash_from_id(id) - #print "expected: " + expected_hash + "\nhash: " + hash + "\n" - if hash != expected_hash - return nil - else - begin - return self.find(id) - rescue ActiveRecord::RecordNotFound - # so error email is sent to admin, rather than the exception sending weird - # error to the public body. - return nil - end - end - end - # Used to find when event last changed def InfoRequest.last_event_time_clause(event_type=nil) event_type_clause = '' @@ -1060,25 +1049,6 @@ public InfoRequest.update_all "allow_new_responses_from = 'nobody' where updated_at < (now() - interval '1 year') and allow_new_responses_from in ('anybody', 'authority_only') and url_title <> 'holding_pen'" end - # Returns a random FOI request - def InfoRequest.random - max_id = InfoRequest.connection.select_value('select max(id) as a from info_requests').to_i - info_request = nil - count = 0 - while info_request.nil? - if count > 100 - return nil - end - id = rand(max_id) + 1 - begin - count += 1 - info_request = find(id, :conditions => ["prominence = 'normal'"]) - rescue ActiveRecord::RecordNotFound - end - end - return info_request - end - def json_for_api(deep) ret = { :id => self.id, diff --git a/app/models/info_request_event.rb b/app/models/info_request_event.rb index 469aabc4a..0967e3940 100644 --- a/app/models/info_request_event.rb +++ b/app/models/info_request_event.rb @@ -420,7 +420,7 @@ class InfoRequestEvent < ActiveRecord::Base if deep ret[:info_request] = self.info_request.json_for_api(false) ret[:public_body] = self.info_request.public_body.json_for_api - ret[:user] = self.info_request.user.json_for_api + ret[:user] = self.info_request.user_json_for_api end return ret diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index 11711090e..aedfb9cad 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -23,6 +23,14 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class OutgoingMessage < ActiveRecord::Base + include Rails.application.routes.url_helpers + include LinkToHelper + self.default_url_options[:host] = AlaveteliConfiguration::domain + # https links in emails if forcing SSL + if AlaveteliConfiguration::force_ssl + self.default_url_options[:protocol] = "https" + end + strip_attributes! belongs_to :info_request @@ -80,15 +88,15 @@ class OutgoingMessage < ActiveRecord::Base end if self.what_doing == 'internal_review' - "Please pass this on to the person who conducts Freedom of Information reviews." + + _("Please pass this on to the person who conducts Freedom of Information reviews.") + "\n\n" + - "I am writing to request an internal review of " + - self.info_request.public_body.name + - "'s handling of my FOI request " + - "'" + self.info_request.title + "'." + + _("I am writing to request an internal review of {{public_body_name}}'s handling of my FOI request '{{info_request_title}}'.", + :public_body_name => self.info_request.public_body.name, + :info_request_title => self.info_request.title) + "\n\n\n\n [ " + self.get_internal_review_insert_here_note + " ] \n\n\n\n" + - "A full history of my FOI request and all correspondence is available on the Internet at this address:\n" + - "http://" + AlaveteliConfiguration::domain + "/request/" + self.info_request.url_title + _("A full history of my FOI request and all correspondence is available on the Internet at this address: {{url}}", + :url => request_url(self.info_request)) + + "\n" else "" end @@ -269,7 +277,7 @@ class OutgoingMessage < ActiveRecord::Base end end if self.body =~ /#{get_signoff}\s*\Z/m - errors.add(:body, _("Please sign at the bottom with your name, or alter the \"%{signoff}\" signature" % { :signoff => get_signoff })) + errors.add(:body, _("Please sign at the bottom with your name, or alter the \"{{signoff}}\" signature", :signoff => get_signoff)) end if !MySociety::Validate.uses_mixed_capitals(self.body) errors.add(:body, _('Please write your message using a mixture of capital and lower case letters. This makes it easier for others to read.')) diff --git a/app/models/profile_photo.rb b/app/models/profile_photo.rb index 8a6fe1636..5d542daf1 100644 --- a/app/models/profile_photo.rb +++ b/app/models/profile_photo.rb @@ -85,7 +85,7 @@ class ProfilePhoto < ActiveRecord::Base end if !self.draft && (self.image.columns != WIDTH || self.image.rows != HEIGHT) - errors.add(:data, N_("Failed to convert image to the correct size: at %{cols}x%{rows}, need %{width}x%{height}" % { :cols => self.image.columns, :rows => self.image.rows, :width => WIDTH, :height => HEIGHT })) + errors.add(:data, N_("Failed to convert image to the correct size: at {{cols}}x{{rows}}, need {{width}}x{{height}}", :cols => self.image.columns, :rows => self.image.rows, :width => WIDTH, :height => HEIGHT)) end if self.draft && self.user_id diff --git a/app/models/user.rb b/app/models/user.rb index 63dd5b1dd..9da4ad743 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -106,7 +106,12 @@ class User < ActiveRecord::Base name.strip! end if self.public_banned? - name = _("{{user_name}} (Account suspended)", :user_name=>name) + # Use interpolation to return a string rather than a SafeBuffer so that + # gsub can be called on it until we upgrade to Rails 3.2. The name returned + # is not marked as HTML safe so will be escaped automatically in views. We + # do this in two steps so the string still gets picked up for translation + name = _("{{user_name}} (Account suspended)", :user_name=> name.html_safe) + name = "#{name}" end name end @@ -298,7 +303,7 @@ class User < ActiveRecord::Base text = CGI.escapeHTML(text) text = MySociety::Format.make_clickable(text, :contract => 1) text = text.gsub(/\n/, '<br>') - return text + return text.html_safe end # Returns domain part of user's email address |