fix redirect after successful upload
[coquelicot.git] / lib / coquelicot / app.rb
1 # Coquelicot: "one-click" file sharing with a focus on users' privacy.
2 # Copyright © 2010-2012 potager.org <jardiniers@potager.org>
3 #           © 2011 mh / immerda.ch <mh+coquelicot@immerda.ch>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 require 'lockfile'
19 require 'sinatra/base'
20 require 'sinatra/config_file'
21 require 'haml'
22 require 'haml/magic_translations'
23 require 'sass'
24 require 'digest/sha1'
25 require 'fast_gettext'
26
27 module Coquelicot
28   class << self
29     def settings
30       (class << self; Application; end)
31     end
32     def depot
33       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
34       @depot
35     end
36   end
37
38   class Application < Sinatra::Base
39     register Sinatra::ConfigFile
40     register Coquelicot::Auth::Extension
41
42     set :root, Proc.new { app_file && File.expand_path('../../..', app_file) }
43     set :depot_path, Proc.new { File.join(root, 'files') }
44     set :default_expire, 60
45     set :maximum_expire, 60 * 24 * 30 # 1 month
46     set :gone_period, 60 * 24 * 7 # 1 week
47     set :filename_length, 20
48     set :random_pass_length, 16
49     set :about_text, ''
50     set :additional_css, ''
51     set :url, '' # compute instance URL using request data
52     set :authentication_method, :name => :simplepass,
53                                 :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
54
55     config_file File.expand_path('../../../conf/settings.yml', __FILE__)
56
57     FastGettext.add_text_domain 'coquelicot', :path => 'po', :type => 'po'
58     FastGettext.available_locales = [ 'en', 'fr', 'de' ]
59     Haml::MagicTranslations.enable(:fast_gettext)
60     before do
61       FastGettext.text_domain = 'coquelicot'
62       FastGettext.locale = params[:lang] || request.env['HTTP_ACCEPT_LANGUAGE'] || 'en'
63     end
64
65     not_found do
66       'Not found'
67     end
68
69     get '/style.css' do
70       content_type 'text/css', :charset => 'utf-8'
71       sass :style
72     end
73
74     get '/' do
75       haml :index
76     end
77
78     get '/random_pass' do
79       "#{Coquelicot.gen_random_pass}"
80     end
81
82     get '/ready/:link' do |link|
83       not_found if link.nil?
84
85       link, pass = link.split '-' if link.include? '-'
86       begin
87         file = Coquelicot.depot.get_file(link, nil)
88       rescue Errno::ENOENT => ex
89         not_found
90       end
91       @expire_at = file.expire_at
92       @name = "#{link}"
93       unless pass.nil?
94         @name << "-#{pass}"
95         @unprotected = true
96       end
97       @url = "#{base_href}#{@name}"
98       haml :ready
99     end
100
101     post '/authenticate' do
102       pass unless request.xhr?
103       begin
104         unless authenticate(params)
105           error 403, "Forbidden"
106         end
107         'OK'
108       rescue Coquelicot::Auth::Error => ex
109         error 503, ex.message
110       end
111     end
112
113     post '/upload' do
114       begin
115         unless authenticate(params)
116           error 403, "Forbidden"
117         end
118       rescue Coquelicot::Auth::Error => ex
119         error 503, ex.message
120       end
121
122       if params[:file] then
123         tmpfile = params[:file][:tempfile]
124         name = params[:file][:filename]
125       end
126       if tmpfile.nil? || name.nil? then
127         @error = "No file selected"
128         return haml(:index)
129       end
130       if tmpfile.lstat.size == 0 then
131         @error = "#{name} is empty"
132         return haml(:index)
133       end
134       if params[:expire].nil? or params[:expire].to_i == 0 then
135         params[:expire] = settings.default_expire
136       elsif params[:expire].to_i > settings.maximum_expire then
137         error 403
138       end
139       expire_at = Time.now + 60 * params[:expire].to_i
140       one_time_only = params[:one_time] and params[:one_time] == 'true'
141       if params[:file_key].nil? or params[:file_key].empty?then
142         pass = Coquelicot.gen_random_pass
143       else
144         pass = params[:file_key]
145       end
146       src = params[:file][:tempfile]
147       link = Coquelicot.depot.add_file(
148          src, pass,
149          { "Expire-at" => expire_at.to_i,
150            "One-time-only" => one_time_only,
151            "Filename" => params[:file][:filename],
152            "Length" => src.stat.size,
153            "Content-Type" => params[:file][:type],
154          })
155       redirect to("/ready/#{link}-#{pass}") if params[:file_key].nil? or params[:file_key].empty?
156       redirect to("/ready/#{link}")
157     end
158
159     def expired
160       throw :halt, [410, haml(:expired)]
161     end
162
163     def send_stored_file(file)
164       last_modified file.created_at.httpdate
165       attachment file.meta['Filename']
166       response['Content-Length'] = "#{file.meta['Length']}"
167       response['Content-Type'] = file.meta['Content-Type'] || 'application/octet-stream'
168       throw :halt, [200, file]
169     end
170
171     def send_link(link, pass)
172       file = Coquelicot.depot.get_file(link, pass)
173       return false if file.nil?
174       return expired if file.expired?
175
176       if file.one_time_only?
177         begin
178           # unlocking done in file.close
179           file.lockfile.lock
180         rescue Lockfile::TimeoutLockError
181           error 409, "Download currently in progress"
182         end
183       end
184       send_stored_file(file)
185     end
186
187     get '/:link-:pass' do |link, pass|
188       not_found if link.nil? || pass.nil?
189
190       link = Coquelicot.remap_base32_extra_characters(link)
191       pass = Coquelicot.remap_base32_extra_characters(pass)
192       not_found unless send_link(link, pass)
193     end
194
195     get '/:link' do |link|
196       not_found if link.nil?
197
198       link = Coquelicot.remap_base32_extra_characters(link)
199       not_found unless Coquelicot.depot.file_exists? link
200       @link = link
201       haml :enter_file_key
202     end
203
204     post '/:link' do |link|
205       pass = params[:file_key]
206       return 403 if pass.nil? or pass.empty?
207       begin
208         # send Forbidden even if file is not found
209         return 403 unless send_link(link, pass)
210       rescue Coquelicot::BadKey => ex
211         403
212       end
213     end
214
215     helpers do
216       def base_href
217         return settings.url unless settings.url.empty?
218
219         url = request.scheme + "://"
220         url << request.host
221         if request.scheme == "https" && request.port != 443 ||
222             request.scheme == "http" && request.port != 80
223           url << ":#{request.port}"
224         end
225         url << request.script_name
226         "#{url}/"
227       end
228
229       def clone_url
230         settings.respond_to?(:clone_url) ? settings.clone_url : "#{base_href}coquelicot.git"
231       end
232
233       def authenticate(params)
234         Coquelicot.settings.authenticator.authenticate(params)
235       end
236
237       def auth_method
238         Coquelicot.settings.authenticator.class.name.gsub(/Coquelicot::Auth::([A-z0-9]+)Authenticator$/, '\1').downcase
239       end
240     end
241   end
242 end
243
244 Coquelicot::Application.run! if __FILE__ == $0