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