aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/alaveteli_external_command.rb69
-rw-r--r--lib/alaveteli_text_masker.rb127
-rw-r--r--lib/category_and_heading_migrator.rb91
-rw-r--r--lib/configuration.rb1
-rw-r--r--lib/languages.rb6
-rw-r--r--lib/mail_handler/mail_handler.rb4
-rw-r--r--lib/public_body_categories.rb11
-rw-r--r--lib/quiet_opener.rb15
-rw-r--r--lib/tasks/gettext.rake70
9 files changed, 252 insertions, 142 deletions
diff --git a/lib/alaveteli_external_command.rb b/lib/alaveteli_external_command.rb
index 086a461c8..ddf968f90 100644
--- a/lib/alaveteli_external_command.rb
+++ b/lib/alaveteli_external_command.rb
@@ -5,61 +5,72 @@ module AlaveteliExternalCommand
# Final argument can be a hash of options.
# Valid options are:
# :append_to - string to append the output of the process to
+ # :append_errors_to - string to append the errors produced by the process to
# :stdin_string - stdin string to pass to the process
- # :binary_output - boolean flag for treating the output as binary or text (only significant
- # ruby 1.9 and above)
+ # :binary_output - boolean flag for treating the output as binary or text encoded with
+ # the default external encoding (only significant in ruby 1.9 and above)
+ # :binary_input - boolean flag for treating the input as binary or as text encoded with
+ # the default external encoding (only significant in ruby 1.9 and above)
# :memory_limit - maximum amount of memory (in bytes) available to the process
+ # :timeout - maximum amount of time (in s) to allow the process to run for
+ # :env - hash of environment variables to set for the process
def run(program_name, *args)
# Run an external program, and return its output.
# Standard error is suppressed unless the program
# fails (i.e. returns a non-zero exit status).
+ # If the program fails, returns nil and writes any error to stderr.
+ # TODO: calling code should be able to specify error stream - may want to log it or
+ # otherwise act upon it.
opts = {}
- if !args.empty? && args[-1].is_a?(Hash)
- opts = args.pop
- end
-
- if program_name =~ %r(^/)
- program_path = program_name
- else
- found = false
- AlaveteliConfiguration::utility_search_path.each do |d|
- program_path = File.join(d, program_name)
- if File.file? program_path and File.executable? program_path
- found = true
- break
- end
- end
- raise "Could not find #{program_name} in any of #{AlaveteliConfiguration::utility_search_path.join(', ')}" if !found
+ if !args.empty? && args.last.is_a?(Hash)
+ opts = args.last
end
+ program_path = find_program(program_name)
xc = ExternalCommand.new(program_path, *args)
- if opts.has_key? :append_to
- xc.out = opts[:append_to]
- end
- if opts.has_key? :binary_output
- xc.binary_mode = opts[:binary_output]
- end
- if opts.has_key? :memory_limit
- xc.memory_limit = opts[:memory_limit]
+ begin
+ xc.run
+ rescue ExternalCommand::ChildUnterminated => e
+ $stderr.puts(e.message)
+ return nil
end
- xc.run(opts[:stdin_string] || "", opts[:env] || {})
if !xc.exited
# Crash or timeout
- $stderr.puts("#{program_name} #{args.join(' ')}:exited abnormally")
+ if xc.timed_out
+ $stderr.puts(%Q[External Command: "#{program_name} #{args.join(' ')}" timed out at #{opts[:timeout]}s])
+ else
+ $stderr.puts(%Q[External Command: "#{program_name} #{args.join(' ')}" exited abnormally])
+ end
+ $stderr.print(xc.err)
return nil
+
elsif xc.status != 0
# Error
- $stderr.puts("Error from #{program_name} #{args.join(' ')}:")
+ $stderr.puts(%Q[External Command: Error from command "#{program_name} #{args.join(' ')}":])
$stderr.print(xc.err)
return nil
else
if opts.has_key? :append_to
opts[:append_to] << "\n\n"
else
+
return xc.out
end
end
end
+
+ def find_program(program_name)
+ if program_name =~ %r(^/)
+ return program_name
+ else
+ search_path = AlaveteliConfiguration::utility_search_path
+ search_path.each do |d|
+ program_path = File.join(d, program_name)
+ return program_name if File.file? program_path and File.executable? program_path
+ end
+ raise "Could not find #{program_name} in any of #{search_path.join(', ')}"
+ end
+ end
end
end
diff --git a/lib/alaveteli_text_masker.rb b/lib/alaveteli_text_masker.rb
new file mode 100644
index 000000000..68ff0d318
--- /dev/null
+++ b/lib/alaveteli_text_masker.rb
@@ -0,0 +1,127 @@
+module AlaveteliTextMasker
+ extend self
+ DoNotBinaryMask = [ 'image/tiff',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/png',
+ 'image/bmp',
+ 'application/zip' ]
+
+ # Replaces all email addresses in (possibly binary) data
+ # Also applies custom masks and censor items
+ def apply_masks!(text, content_type, options = {})
+ # See if content type is one that we mask - things like zip files and
+ # images may get broken if we try to. We err on the side of masking too
+ # much, as many unknown types will really be text.
+
+ # Special cases for some content types
+ case content_type
+ when *DoNotBinaryMask
+ # do nothing
+ when 'text/html'
+ apply_text_masks!(text, options)
+ when 'application/pdf'
+ apply_pdf_masks!(text, options)
+ else
+ apply_binary_masks!(text, options)
+ end
+ end
+
+ def apply_pdf_masks!(text, options = {})
+ uncompressed_text = nil
+ uncompressed_text = AlaveteliExternalCommand.run("pdftk", "-", "output", "-", "uncompress",
+ :stdin_string => text)
+ # if we managed to uncompress the PDF...
+ if !uncompressed_text.blank?
+ # then censor stuff (making a copy so can compare again in a bit)
+ censored_uncompressed_text = uncompressed_text.dup
+ apply_binary_masks!(censored_uncompressed_text, options)
+ # if the censor rule removed something...
+ if censored_uncompressed_text != uncompressed_text
+ # then use the altered file (recompressed)
+ recompressed_text = nil
+ if AlaveteliConfiguration::use_ghostscript_compression == true
+ command = ["gs", "-sDEVICE=pdfwrite", "-dCompatibilityLevel=1.4", "-dPDFSETTINGS=/screen", "-dNOPAUSE", "-dQUIET", "-dBATCH", "-sOutputFile=-", "-"]
+ else
+ command = ["pdftk", "-", "output", "-", "compress"]
+ end
+ recompressed_text = AlaveteliExternalCommand.run(*(command + [{:stdin_string=>censored_uncompressed_text}]))
+ if recompressed_text.blank?
+ # buggy versions of pdftk sometimes fail on
+ # compression, I don't see it's a disaster in
+ # these cases to save an uncompressed version?
+ recompressed_text = censored_uncompressed_text
+ logger.warn "Unable to compress PDF; problem with your pdftk version?"
+ end
+ if !recompressed_text.blank?
+ text.replace recompressed_text
+ end
+ end
+ end
+ end
+
+ private
+
+ # Replace text in place
+ def apply_binary_masks!(text, options = {})
+ # Keep original size, so can check haven't resized it
+ orig_size = text.mb_chars.size
+
+ # Replace ASCII email addresses...
+ text.gsub!(MySociety::Validate.email_find_regexp) do |email|
+ email.gsub(/[^@.]/, 'x')
+ end
+
+ # And replace UCS-2 ones (for Microsoft Office documents)...
+ # Find emails, by finding them in parts of text that have ASCII
+ # equivalents to the UCS-2
+ ascii_chars = text.gsub(/\0/, "")
+ emails = ascii_chars.scan(MySociety::Validate.email_find_regexp)
+
+ # Convert back to UCS-2, making a mask at the same time
+ if String.method_defined?(:encode)
+ emails.map! do |email|
+ # We want the ASCII representation of UCS-2
+ [email[0].encode('UTF-16LE').force_encoding('US-ASCII'),
+ email[0].gsub(/[^@.]/, 'x').encode('UTF-16LE').force_encoding('US-ASCII')]
+ end
+ else
+ emails.map! {|email| [
+ Iconv.conv('ucs-2le', 'ascii', email[0]),
+ Iconv.conv('ucs-2le', 'ascii', email[0].gsub(/[^@.]/, 'x'))
+ ] }
+ end
+
+ # Now search and replace the UCS-2 email with the UCS-2 mask
+ for email, mask in emails
+ text.gsub!(email, mask)
+ end
+
+ # Replace censor items
+ censor_rules = options[:censor_rules] || []
+ censor_rules.each{ |censor_rule| censor_rule.apply_to_binary!(text) }
+ raise "internal error in apply_binary_masks!" if text.mb_chars.size != orig_size
+ return text
+ end
+
+ # Remove any email addresses, login links and mobile phone numbers
+ def default_text_masks
+ [{ :to_replace => MySociety::Validate.email_find_regexp,
+ :replacement => "[#{_("email address")}]" },
+ { :to_replace => /(Mobile|Mob)([\s\/]*(Fax|Tel))*\s*:?[\s\d]*\d/,
+ :replacement => "[#{_("mobile number")}]" },
+ { :to_replace => /https?:\/\/#{AlaveteliConfiguration::domain}\/c\/[^\s]+/,
+ :replacement => "[#{_("{{site_name}} login link",
+ :site_name => AlaveteliConfiguration::site_name)}]" }]
+ end
+
+ def apply_text_masks!(text, options = {})
+ masks = options[:masks] || []
+ masks += default_text_masks
+ censor_rules = options[:censor_rules] || []
+ masks.each{ |mask| text.gsub!(mask[:to_replace], mask[:replacement]) }
+ censor_rules.each{ |censor_rule| censor_rule.apply_to_text!(text) }
+ text
+ end
+
+end
diff --git a/lib/category_and_heading_migrator.rb b/lib/category_and_heading_migrator.rb
deleted file mode 100644
index 402ea7204..000000000
--- a/lib/category_and_heading_migrator.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-module CategoryAndHeadingMigrator
-
- # This module migrates data from public_body_categories_[locale].rb files
- # into PublicBodyHeading and PublicBodyCategory models
-
- # Load all the data from public_body_categories_[locale].rb files.
- def self.migrate_categories_and_headings
- if PublicBodyCategory.count > 0
- puts "PublicBodyCategories exist already, not migrating."
- else
- @first_locale = true
- I18n.available_locales.each do |locale|
- begin
- load "public_body_categories_#{locale}.rb"
- rescue MissingSourceFile
- end
- @first_locale = false
- end
- end
- end
-
- # Load the categories and headings for a locale
- def self.add_categories_and_headings_from_list(locale, data_list)
- # set the counter for headings loaded from this locale
- @@locale_heading_display_order = 0
- current_heading = nil
- data_list.each do |list_item|
- if list_item.is_a?(Array)
- # item is list of category data
- add_category(list_item, current_heading, locale)
- else
- # item is heading name
- current_heading = add_heading(list_item, locale, @first_locale)
- end
- end
- end
-
- def self.add_category(category_data, heading, locale)
- tag, title, description = category_data
- category = PublicBodyCategory.find_by_category_tag(tag)
- if category
- add_category_in_locale(category, title, description, locale)
- else
- category = PublicBodyCategory.create(:category_tag => tag,
- :title => title,
- :description => description)
-
- # add the translation if this is not the default locale
- # (occurs when a category is not defined in default locale)
- unless category.translations.map { |t| t.locale }.include?(locale)
- add_category_in_locale(category, title, description, locale)
- end
- end
- heading.add_category(category)
- end
-
- def self.add_category_in_locale(category, title, description, locale)
- I18n.with_locale(locale) do
- category.title = title
- category.description = description
- category.save
- end
- end
-
- def self.add_heading(name, locale, first_locale)
- heading = nil
- I18n.with_locale(locale) do
- heading = PublicBodyHeading.find_by_name(name)
- end
- # For multi-locale installs, we assume that all public_body_[locale].rb files
- # use the same headings in the same order, so we add translations to the heading
- # that was in the same position in the list loaded from other public_body_[locale].rb
- # files.
- if heading.nil? && !@first_locale
- heading = PublicBodyHeading.where(:display_order => @@locale_heading_display_order).first
- end
-
- if heading
- I18n.with_locale(locale) do
- heading.name = name
- heading.save
- end
- else
- I18n.with_locale(locale) do
- heading = PublicBodyHeading.create(:name => name)
- end
- end
- @@locale_heading_display_order += 1
- heading
- end
-end
diff --git a/lib/configuration.rb b/lib/configuration.rb
index 2144f9954..90fd30d5f 100644
--- a/lib/configuration.rb
+++ b/lib/configuration.rb
@@ -19,6 +19,7 @@ module AlaveteliConfiguration
:ADMIN_PASSWORD => '',
:ADMIN_USERNAME => '',
:ALLOW_BATCH_REQUESTS => false,
+ :AUTHORITY_MUST_RESPOND => true,
:AVAILABLE_LOCALES => '',
:BLACKHOLE_PREFIX => 'do-not-reply-to-this-address',
:BLOG_FEED => '',
diff --git a/lib/languages.rb b/lib/languages.rb
index 42231ef56..a45071a67 100644
--- a/lib/languages.rb
+++ b/lib/languages.rb
@@ -36,7 +36,7 @@ class LanguageNames
'kw' => 'Kernewek',
'co' => 'corsu',
'cr' => 'ᓀᐦᐃᔭᐍᐏᐣ',
- 'hr' => 'hrvatski',
+ 'hr' => 'Hrvatski',
'cs' => 'česky',
'da' => 'dansk',
'dv' => 'ދިވެހި',
@@ -111,11 +111,11 @@ class LanguageNames
'mn' => 'монгол',
'na' => 'Ekakairũ Naoero',
'nv' => 'Diné bizaad',
- 'nb' => 'Norsk bokmål',
+ 'nb' => 'Bokmål',
'nd' => 'isiNdebele',
'ne' => 'नेपाली',
'ng' => 'Owambo',
- 'nn' => 'Norsk nynorsk',
+ 'nn' => 'Nynorsk',
'no' => 'Norsk',
'ii' => 'ꆈꌠ꒿ Nuosuhxop',
'nr' => 'isiNdebele',
diff --git a/lib/mail_handler/mail_handler.rb b/lib/mail_handler/mail_handler.rb
index 47015f207..33d939e22 100644
--- a/lib/mail_handler/mail_handler.rb
+++ b/lib/mail_handler/mail_handler.rb
@@ -78,7 +78,9 @@ module MailHandler
tempfile.binmode
tempfile.print body
tempfile.flush
- default_params = { :append_to => text, :binary_output => false }
+ default_params = { :append_to => text,
+ :binary_output => false,
+ :timeout => 1200 }
if content_type == 'application/vnd.ms-word'
AlaveteliExternalCommand.run("wvText", tempfile.path, tempfile.path + ".txt",
{ :memory_limit => 536870912, :timeout => 120 } )
diff --git a/lib/public_body_categories.rb b/lib/public_body_categories.rb
deleted file mode 100644
index 3528e85b1..000000000
--- a/lib/public_body_categories.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# Allow the PublicBodyCategory model to be addressed using the same syntax
-# as the old PublicBodyCategories class without needing to rename everything,
-# make sure we're not going to break any themes
-class PublicBodyCategories
-
- def self.method_missing(method, *args, &block)
- warn 'Use of PublicBodyCategories is deprecated and will be removed in release 0.21. Please use PublicBodyCategory instead.'
- PublicBodyCategory.send(method, *args, &block)
- end
-
-end
diff --git a/lib/quiet_opener.rb b/lib/quiet_opener.rb
index 16ea27b8e..c6e259b93 100644
--- a/lib/quiet_opener.rb
+++ b/lib/quiet_opener.rb
@@ -7,8 +7,19 @@ end
def quietly_try_to_open(url)
begin
result = open(url).read.strip
- rescue OpenURI::HTTPError, SocketError, Errno::ETIMEDOUT, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET
- Rails.logger.warn("Unable to open third-party URL #{url}")
+ rescue OpenURI::HTTPError,
+ SocketError,
+ Errno::ETIMEDOUT,
+ Errno::ECONNREFUSED,
+ Errno::EHOSTUNREACH,
+ Errno::ECONNRESET,
+ Timeout::Error => exception
+ e = Exception.new("Unable to open third-party URL #{url}: #{exception.message}")
+ e.set_backtrace(exception.backtrace)
+ if !AlaveteliConfiguration.exception_notifications_from.blank? && !AlaveteliConfiguration.exception_notifications_to.blank?
+ ExceptionNotifier::Notifier.exception_notification(request.env, e).deliver
+ end
+ Rails.logger.warn(e.message)
result = ""
end
return result
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 3f357213f..4df92b008 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -11,11 +11,7 @@ namespace :gettext do
desc "Update pot/po files for a theme."
task :find_theme => :environment do
- theme = ENV['THEME']
- unless theme
- puts "Usage: Specify an Alaveteli-theme with THEME=[theme directory name]"
- exit(0)
- end
+ theme = find_theme(ENV['THEME'])
load_gettext
msgmerge = Rails.application.config.gettext_i18n_rails.msgmerge
msgmerge ||= %w[--sort-output --no-location --no-wrap]
@@ -28,6 +24,70 @@ namespace :gettext do
)
end
+ desc 'Rewrite theme .po files into a consistent msgmerge format'
+ task :clean_theme do
+ theme = find_theme(ENV['THEME'])
+ load_gettext
+
+ Dir.glob("#{ theme_locale_path(theme) }/*/app.po") do |po_file|
+ GetText::msgmerge(po_file, po_file, 'alaveteli',
+ :msgmerge => [:sort_output, :no_location, :no_wrap])
+ end
+ end
+
+ desc 'Update locale files with slightly changed English msgids using a csv file of old to new strings'
+ task :update_msgids_from_csv do
+ mapping_file = find_mapping_file(ENV['MAPPING_FILE'])
+ mappings = {}
+ CSV.parse(clean_csv_mapping_file(mapping_file)) do |csv_line|
+ from,to = csv_line
+ mappings[from] = to
+ end
+ Dir.glob("locale/**/app.po").each do |po_file|
+ lang_mappings = mappings.clone
+ lines = []
+ File.read(po_file).each_line do |line|
+ /^msgid "(.*)"/ =~ line
+ if $1 && mappings[$1]
+ lines << "msgid \"#{lang_mappings.delete($1)}\""
+ else
+ lines << line
+ end
+ end
+ puts "Mappings unused in #{po_file}: #{lang_mappings.keys}" unless lang_mappings.empty?
+ File.open(po_file, "w") { |f| f.puts(lines) }
+ end
+ end
+
+ # Use a quote for quote-escaping as CSV errors on the \" with "Missing or stray quote"
+ def clean_csv_mapping_file(file)
+ data = ''
+ File.foreach(file) do |line|
+ data += line.gsub('\"', '""')
+ end
+ data
+ end
+
+ def find_theme(theme)
+ unless theme
+ puts "Usage: Specify an Alaveteli-theme with THEME=[theme directory name]"
+ exit(1)
+ end
+ theme
+ end
+
+ def find_mapping_file(file)
+ unless file
+ puts "Usage: Specify a csv file mapping old to new strings with MAPPING_FILE=[file name]"
+ exit(1)
+ end
+ unless File.exists?(file)
+ puts "Error: MAPPING_FILE #{file} not found"
+ exit(1)
+ end
+ file
+ end
+
def theme_files_to_translate(theme)
Dir.glob("{lib/themes/#{theme}/lib}/**/*.{rb,erb}")
end