blob: c1405e47e9601ef1e39b6e88b30f024557404e13 [file] [log] [blame]
#
# Simple web server that routes requests to views based on URLs.
#
require 'wunderbar/sinatra'
require 'wunderbar/bootstrap'
require 'wunderbar/vue'
require 'ruby2js/es2017/strict'
require 'ruby2js/filter/functions'
require 'ruby2js/filter/require'
require 'erb'
require 'sanitize'
require 'escape'
require_relative 'personalize'
require_relative 'helpers'
require_relative 'models/mailbox'
require_relative 'models/safetemp'
require_relative 'models/events'
require_relative 'tasks'
# monkey patch mail gem to work around a regression introduced in 2.7.0:
# https://github.com/mikel/mail/pull/1168
module Mail
class Message
def raw_source=(value)
@raw_source = ::Mail::Utilities.to_crlf(value)
end
end
module Utilities
def self.safe_for_line_ending_conversion?(string)
if RUBY_VERSION >= '1.9'
string.ascii_only? or
(string.encoding != Encoding::BINARY and string.valid_encoding?)
else
string.ascii_only?
end
end
end
end
require 'whimsy/asf'
require 'whimsy/asf/memapps'
ASF::Mail.configure
SECS_TO_DAYS = 60*60*24
set :show_exceptions, true
disable :logging # suppress log of requests to stderr/error.log
# list of messages
get '/' do
redirect to('/') if env['REQUEST_URI'] == env['SCRIPT_NAME']
# determine latest month for which there are messages
archives = Dir[File.join(ARCHIVE, '*.yml')].select {|name| name =~ %r{/\d{6}\.yml$}}
@mbox = archives.empty? ? nil : File.basename(archives.sort.last, '.yml')
if @mbox
@mbox = [Date.today.strftime('%Y%m'), @mbox].min
@messages = Mailbox.new(@mbox).client_headers.select do |message|
message[:status] != :deleted
end
else
@messages = [] # ensure the array exists
end
# Show outstanding emeritus requests
ASF::EmeritusRequestFiles.listnames(true).each do |epoch, file|
days = (((Time.now.to_i - epoch.to_i).to_f/SECS_TO_DAYS)).round(1)
id = File.basename(file,'.*')
@messages << {
time: Time.at(epoch.to_i).to_s,
href: "/roster/committer/#{id}",
from: ASF::Person.find(id).cn,
subject: "Pending emeritus request - #{days.to_s} days old",
status: days < 10.0 ? 'Pending' : 'Ready'
}
end
@cssmtime = File.mtime('public/secmail.css').to_i
@appmtime = Wunderbar::Asset.convert(File.join(settings.views, 'app.js.rb')).mtime.to_i
_html :index
end
# alias for root directory
get '/index.html' do
call env.merge('PATH_INFO' => '/')
end
# support for fetching previous month's worth of messages
get %r{/(\d{6})} do |mbox|
@mbox = mbox
_json :index # This invokes workbench/views/index.json.rb
end
# retrieve a single message
get %r{/(\d{6})/(\w+)/} do |month, hash|
@message = Mailbox.new(month).headers[hash]
pass unless @message
_html :message
end
# task lists
post '/tasklist/:file' do
@jsmtime = File.mtime('public/tasklist.js').to_i
@cssmtime = File.mtime('public/secmail.css').to_i
if request.content_type == 'application/json'
_json(:"actions/#{params[:file]}")
else
@dryrun = JSON.parse(_json(:"actions/#{params[:file]}"))
_html :tasklist
end
end
# posted actions
post '/actions/:file' do
_json :"actions/#{params[:file]}"
end
# mark a single message as deleted
delete %r{/(\d+)/(\w+)/} do |month, hash|
success = false
Mailbox.update(month) do |headers|
if headers[hash]
headers[hash][:status] = :deleted
success = true
end
end
pass unless success
_json success: true
end
# update a single message
patch %r{/(\d{6})/(\w+)/} do |month, hash|
success = false
Mailbox.update(month) do |headers|
if headers[hash]
updates = JSON.parse(request.env['rack.input'].read)
# special processing for entries with symbols as keys
headers[hash].each do |key, value|
if Symbol === key and updates.has_key? key.to_s
headers[hash][key] = updates.delete(key.to_s)
end
end
headers[hash].merge! updates
success = true
end
end
pass unless success
[204, {}, '']
end
# list of parts for a single message
get %r{/(\d{6})/(\w+)/_index_} do |month, hash|
message = Mailbox.new(month).find(hash)
pass unless message
@attachments = message.attachments
@headers = message.headers.dup
@headers.delete :attachments
@cssmtime = File.mtime('public/secmail.css').to_i
@appmtime = Wunderbar::Asset.convert(File.join(settings.views, 'app.js.rb')).mtime.to_i
@projects = (ASF::Podling.current+ASF::Committee.pmcs).map(&:name).sort
# Section 4.1 of the ASF bylaws provides requirements for when membership
# applications can be accepted. Two days are added to cover the adjournment
# period of the meeting during which the vote takes place.
received = Dir["#{ASF::SVN['Meetings']}/2*/memapp-received.txt"].sort.last
@meeting = Date.today - Date.parse(received[/\d{8}/]) <= 32 rescue true # rescue crash in local testing
_html :parts
end
# message body for a single message
get %r{/(\d{6})/(\w+)/_body_} do |month, hash|
@message = Mailbox.new(month).find(hash)
@cssmtime = File.mtime('public/secmail.css').to_i
@appmtime = Wunderbar::Asset.convert(File.join(settings.views, 'app.js.rb')).mtime.to_i
pass unless @message
_html :body
end
# header data for a single message
get %r{/(\d{6})/(\w+)/_headers_} do |month, hash|
@headers = Mailbox.new(month).headers[hash]
pass unless @headers
_html :headers
end
# raw data for a single message
get %r{/(\d{6})/(\w+)/_raw_} do |month, hash|
message = Mailbox.new(month).find(hash)
pass unless message
[200, {'Content-Type' => 'text/plain'}, message.raw]
end
# intercede for potentially dangerous message attachments
get %r{/(\d{6})/(\w+)/_danger_/(.*?)} do |month, hash, name|
message = Mailbox.new(month).find(hash)
pass unless message
@part = message.find(URI.decode(name))
pass unless @part
_html :danger
end
# a specific attachment for a message
get %r{/(\d{6})/(\w+)/(.*?)} do |month, hash, name|
message = Mailbox.new(month).find(hash)
pass unless message
part = message.find(URI.decode(name))
pass unless part
[200, {'Content-Type' => part.content_type}, part.body.to_s]
end
# parse memapp-received
get '/memapp.json' do
_json :memapp
end
# return email for a given id
get '/email.json' do
_json do
{email: ASF::Person.find(params[:id]).mail.first}
end
end
# return a list of iclas
get '/iclas.json' do
list = []
ASF::ICLA.each do |icla|
list << {
filename: icla.claRef,
id: icla.id,
name: icla.name,
fullname: icla.legal_name,
email: icla.email
}
end
list.to_json
end
# return a list of members
get '/members.json' do
list = []
ASF.members.each do |member|
list << {
id: member.name,
name: member.public_name
}
end
_json list
end
# redirect to an icla
get %r{/icla/(.*)} do |filename|
checkout = ASF::SVN.svnurl('iclas')
file = ASF::ICLAFiles.match_claRef(filename)
pass unless file
redirect to(checkout + '/' + file)
end
# event stream for server sent events (a.k.a EventSource)
get '/events', provides: 'text/event-stream' do
events = Events.new
stream :keep_open do |out|
out.callback {events.close}
loop do
event = events.pop
if Hash === event or Array === event
out << "data: #{JSON.dump(event)}\n\n"
elsif event == :heartbeat
out << ":\n"
elsif event == :exit
out.close
break
else
out << "data: #{event.inspect}\n\n"
end
end
end
end