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