improve user feedback for AJAX authentication
[coquelicot.git] / lib / coquelicot / app.rb
1 require 'lockfile'
2 require 'sinatra/base'
3 require 'sinatra/config_file'
4 require 'haml'
5 require 'sass'
6 require 'digest/sha1'
7 require 'gettext'
8 require 'haml_gettext'
9
10 module Coquelicot
11   class << self
12     def settings
13       (class << self; Application; end)
14     end
15     def depot
16       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
17       @depot
18     end
19   end
20
21   class Application < Sinatra::Base
22     register Sinatra::ConfigFile
23     register Coquelicot::Auth::Extension
24
25     set :root, Proc.new { app_file && File.expand_path('../../..', app_file) }
26     set :depot_path, Proc.new { File.join(root, 'files') }
27     set :default_expire, 60
28     set :maximum_expire, 60 * 24 * 30 # 1 month
29     set :gone_period, 60 * 24 * 7 # 1 week
30     set :filename_length, 20
31     set :random_pass_length, 16
32     set :about_text, ''
33     set :additional_css, ''
34     set :authentication_method, :name => :simplepass,
35                                 :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
36
37     config_file File.expand_path('../../../conf/settings.yml', __FILE__)
38
39     GetText::bindtextdomain('coquelicot')
40     before do
41       GetText::set_current_locale(params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en')
42     end
43
44     not_found do
45       'Not found'
46     end
47
48     get '/style.css' do
49       content_type 'text/css', :charset => 'utf-8'
50       sass :style
51     end
52
53     get '/' do
54       haml :index
55     end
56
57     get '/random_pass' do
58       "#{Coquelicot.gen_random_pass}"
59     end
60
61     get '/ready/:link' do |link|
62       not_found if link.nil?
63
64       link, pass = link.split '-' if link.include? '-'
65       begin
66         file = Coquelicot.depot.get_file(link, nil)
67       rescue Errno::ENOENT => ex
68         not_found
69       end
70       @expire_at = file.expire_at
71       @base = request.url.gsub(/\/ready\/[^\/]*$/, '')
72       @name = "#{link}"
73       unless pass.nil?
74         @name << "-#{pass}"
75         @unprotected = true
76       end
77       @url = "#{@base}/#{@name}"
78       haml :ready
79     end
80
81     post '/authenticate' do
82       pass unless request.xhr?
83       begin
84         unless authenticate(params)
85           error 403, "Forbidden"
86         end
87         'OK'
88       rescue Coquelicot::Auth::Error => ex
89         error 503, ex.message
90       end
91     end
92
93     post '/upload' do
94       begin
95         unless authenticate(params)
96           error 403, "Forbidden"
97         end
98       rescue Coquelicot::Auth::Error => ex
99         error 503, ex.message
100       end
101
102       if params[:file] then
103         tmpfile = params[:file][:tempfile]
104         name = params[:file][:filename]
105       end
106       if tmpfile.nil? || name.nil? then
107         @error = "No file selected"
108         return haml(:index)
109       end
110       if tmpfile.lstat.size == 0 then
111         @error = "#{name} is empty"
112         return haml(:index)
113       end
114       if params[:expire].nil? or params[:expire].to_i == 0 then
115         params[:expire] = settings.default_expire
116       elsif params[:expire].to_i > settings.maximum_expire then
117         error 403
118       end
119       expire_at = Time.now + 60 * params[:expire].to_i
120       one_time_only = params[:one_time] and params[:one_time] == 'true'
121       if params[:file_key].nil? or params[:file_key].empty?then
122         pass = Coquelicot.gen_random_pass
123       else
124         pass = params[:file_key]
125       end
126       src = params[:file][:tempfile]
127       link = Coquelicot.depot.add_file(
128          src, pass,
129          { "Expire-at" => expire_at.to_i,
130            "One-time-only" => one_time_only,
131            "Filename" => params[:file][:filename],
132            "Length" => src.stat.size,
133            "Content-Type" => params[:file][:type],
134          })
135       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
136       redirect "ready/#{link}"
137     end
138
139     def expired
140       throw :halt, [410, haml(:expired)]
141     end
142
143     def send_stored_file(file)
144       last_modified file.created_at.httpdate
145       attachment file.meta['Filename']
146       response['Content-Length'] = "#{file.meta['Length']}"
147       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
148       throw :halt, [200, file]
149     end
150
151     def send_link(link, pass)
152       file = Coquelicot.depot.get_file(link, pass)
153       return false if file.nil?
154       return expired if file.expired?
155
156       if file.one_time_only?
157         begin
158           # unlocking done in file.close
159           file.lockfile.lock
160         rescue Lockfile::TimeoutLockError
161           error 409, "Download currently in progress"
162         end
163       end
164       send_stored_file(file)
165     end
166
167     get '/:link-:pass' do |link, pass|
168       not_found if link.nil? || pass.nil?
169
170       link = Coquelicot.remap_base32_extra_characters(link)
171       pass = Coquelicot.remap_base32_extra_characters(pass)
172       not_found unless send_link(link, pass)
173     end
174
175     get '/:link' do |link|
176       not_found if link.nil?
177
178       link = Coquelicot.remap_base32_extra_characters(link)
179       not_found unless Coquelicot.depot.file_exists? link
180       @link = link
181       haml :enter_file_key
182     end
183
184     post '/:link' do |link|
185       pass = params[:file_key]
186       return 403 if pass.nil? or pass.empty?
187       begin
188         # send Forbidden even if file is not found
189         return 403 unless send_link(link, pass)
190       rescue Coquelicot::BadKey => ex
191         403
192       end
193     end
194
195     helpers do
196       def base_href
197         url = request.scheme + "://"
198         url << request.host
199         if request.scheme == "https" && request.port != 443 ||
200             request.scheme == "http" && request.port != 80
201           url << ":#{request.port}"
202         end
203         url << request.script_name
204         "#{url}/"
205       end
206
207       def clone_url
208         settings.respond_to?(:clone_url) ? settings.clone_url : "#{base_href}coquelicot.git"
209       end
210
211       def authenticate(params)
212         Coquelicot.settings.authenticator.authenticate(params)
213       end
214
215       def auth_method
216         Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase
217       end
218     end
219   end
220 end
221
222 Coquelicot::Application.run! if __FILE__ == $0