simplify parameters handling in authentication modules
[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       unless authenticate(params) then
84         error 403, "Forbidden"
85       end
86       'OK'
87     end
88
89     post '/upload' do
90       unless authenticate(params) then
91         error 403, "Forbidden"
92       end
93       if params[:file] then
94         tmpfile = params[:file][:tempfile]
95         name = params[:file][:filename]
96       end
97       if tmpfile.nil? || name.nil? then
98         @error = "No file selected"
99         return haml(:index)
100       end
101       if tmpfile.lstat.size == 0 then
102         @error = "#{name} is empty"
103         return haml(:index)
104       end
105       if params[:expire].nil? or params[:expire].to_i == 0 then
106         params[:expire] = settings.default_expire
107       elsif params[:expire].to_i > settings.maximum_expire then
108         error 403
109       end
110       expire_at = Time.now + 60 * params[:expire].to_i
111       one_time_only = params[:one_time] and params[:one_time] == 'true'
112       if params[:file_key].nil? or params[:file_key].empty?then
113         pass = Coquelicot.gen_random_pass
114       else
115         pass = params[:file_key]
116       end
117       src = params[:file][:tempfile]
118       link = Coquelicot.depot.add_file(
119          src, pass,
120          { "Expire-at" => expire_at.to_i,
121            "One-time-only" => one_time_only,
122            "Filename" => params[:file][:filename],
123            "Length" => src.stat.size,
124            "Content-Type" => params[:file][:type],
125          })
126       redirect "ready/#{link}-#{pass}" if params[:file_key].nil? or params[:file_key].empty?
127       redirect "ready/#{link}"
128     end
129
130     def expired
131       throw :halt, [410, haml(:expired)]
132     end
133
134     def send_stored_file(file)
135       last_modified file.created_at.httpdate
136       attachment file.meta['Filename']
137       response['Content-Length'] = "#{file.meta['Length']}"
138       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
139       throw :halt, [200, file]
140     end
141
142     def send_link(link, pass)
143       file = Coquelicot.depot.get_file(link, pass)
144       return false if file.nil?
145       return expired if file.expired?
146
147       if file.one_time_only?
148         begin
149           # unlocking done in file.close
150           file.lockfile.lock
151         rescue Lockfile::TimeoutLockError
152           error 409, "Download currently in progress"
153         end
154       end
155       send_stored_file(file)
156     end
157
158     get '/:link-:pass' do |link, pass|
159       not_found if link.nil? || pass.nil?
160
161       link = Coquelicot.remap_base32_extra_characters(link)
162       pass = Coquelicot.remap_base32_extra_characters(pass)
163       not_found unless send_link(link, pass)
164     end
165
166     get '/:link' do |link|
167       not_found if link.nil?
168
169       link = Coquelicot.remap_base32_extra_characters(link)
170       not_found unless Coquelicot.depot.file_exists? link
171       @link = link
172       haml :enter_file_key
173     end
174
175     post '/:link' do |link|
176       pass = params[:file_key]
177       return 403 if pass.nil? or pass.empty?
178       begin
179         # send Forbidden even if file is not found
180         return 403 unless send_link(link, pass)
181       rescue Coquelicot::BadKey => ex
182         403
183       end
184     end
185
186     helpers do
187       def base_href
188         url = request.scheme + "://"
189         url << request.host
190         if request.scheme == "https" && request.port != 443 ||
191             request.scheme == "http" && request.port != 80
192           url << ":#{request.port}"
193         end
194         url << request.script_name
195         "#{url}/"
196       end
197
198       def clone_url
199         settings.respond_to?(:clone_url) ? settings.clone_url : "#{base_href}coquelicot.git"
200       end
201
202       def authenticate(params)
203         Coquelicot.settings.authenticator.authenticate(params)
204       end
205
206       def auth_method
207         Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase
208       end
209     end
210   end
211 end
212
213 Coquelicot::Application.run! if __FILE__ == $0