-
Notifications
You must be signed in to change notification settings - Fork 0
/
cms.rb
379 lines (323 loc) · 9.1 KB
/
cms.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
require 'sinatra'
require 'sinatra/reloader'
require 'tilt/erubis'
require 'redcarpet'
require 'yaml'
require 'bcrypt'
require 'fileutils'
ACCEPTABLE_EXTENSIONS = %w[.txt .md .jpg .jpeg .png].freeze
# Gives path for data depending on if its
# in testing or production
def data_path
if ENV['RACK_ENV'] == 'test'
File.expand_path('../test/data', __FILE__)
else
File.expand_path('../data', __FILE__)
end
end
# Path where restores reside
def restore_path
File.expand_path('../restores', data_path)
end
# Gets all the full paths of all the restores for filename
def restores_for(filename)
path = File.join(restore_path, filename)
FileUtils.mkdir_p(path) unless File.directory?(path)
Dir[File.join(path, '*')]
end
# Restore file to version
def restore(file, version)
path_for_restore = restores_for(file)[version[1..nil].to_i - 1]
content = File.read(path_for_restore)
File.write(File.join(data_path, file), content)
end
# Make restore point of file
def make_restore_point(file, content)
basenames = restores_for(file).map { |path| File.basename(path).to_i }
version = (basenames.max || 0) + 1
path_for_restore = File.join(restore_path, file, version.to_s)
File.write(path_for_restore, content)
end
# Convert markdown to html
def markdown(text)
Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(text)
end
# Tests if extension is image
def image?(filename)
%w[.jpg .jpeg .png].include? File.extname(filename)
end
# Allow us to use flash messages and login
configure do
enable :sessions
set :session_secret, 'secret'
end
# Loads header for content type and returns correctly
# formatted file contentn
def file_content(path)
content = File.read(path)
case File.extname(path).downcase
when '.md'
headers['Content-Type'] = 'text/html;charset=utf-8'
erb markdown(content), layout: :layout
when '.txt'
headers['Content-Type'] = 'text/plain'
content
when '.jpg', '.jpeg'
headers['Content-Type'] = 'image/jpeg'
content
when '.png'
headers['Content-Type'] = 'image/png'
content
else
headers['Content-Type'] = 'text/plain'
content
end
end
# Credentials from users.yml file
def credentials
Psych.load_file(File.expand_path('../users.yml', data_path))
end
# Check credentials for username and password match
def valid_user?(username, password)
credentials.key?(username) &&
BCrypt::Password.new(credentials[username]) == password
end
# List of all usernames
def usernames
credentials.keys
end
# Check if user is authorized
def authorized?
session.key?(:uname)
end
# Make sure user is authorized and redirect
# if not
def check_authorization
return if authorized?
session[:error] = 'You must be signed in to do that.'
redirect '/'
end
# List of all filenames in CMS
def filenames
pattern = File.join(data_path, '*')
Dir[pattern].map { |path| File.basename(path) }
end
# Generates error for filename or nil if no error
def error_for_filename(name)
extname = File.extname(name)
if !(name =~ /\A[\s\w\-]+\.[\s\w\-]+\z/)
'A proper filename is required.'
elsif !ACCEPTABLE_EXTENSIONS.include? extname
joined_extensions = ACCEPTABLE_EXTENSIONS.join(', ')
"File extension must be one of: #{joined_extensions}."
elsif filenames.include? name
'Filename must be unique.'
end
end
# Generates error for username or nil if no error
def error_for_credentials(username, password)
if !(4..16).cover? username.size
'Username must be between 4 and 16 characters.'
elsif !(username =~ /\A\w+\z/)
'Usernameame must only alphanumeric characters.'
elsif usernames.include? username
"Sorry, #{username} is already taken."
elsif !(8..16).cover? password.size
'Password must be between 8 and 16 characters.'
end
end
# Generates error for image filename or nil if no error
def error_for_image(name, type)
if !%w[image/jpeg image/png].include? type
"File must be one of: #{IMAGE_EXTENSIONS.join(', ')}"
elsif (error = error_for_filename(name))
error
end
end
# Adds user to users.yml
def add_user(username, password)
File.open(File.expand_path('../users.yml', data_path), 'a') do |file|
encrypted_password = BCrypt::Password.create(password)
file.write("#{username}: #{encrypted_password}\n")
end
end
# Main page. Loads either list of files + extras or
# Sign in button if user is not authorized
get '/' do
@filenames = filenames
erb :index, layout: :layout
end
# Loads sign in form for users to get authorized
get '/users/signin' do
redirect '/' if authorized?
erb :signin, layout: :layout
end
# Authorizes user or asks user to try again
post '/users/signin' do
redirect '/' if authorized?
uname = params[:uname]
psswd = params[:psswd]
if valid_user?(uname, psswd)
session[:uname] = uname
session[:success] = 'Welcome!'
redirect '/'
else
session[:error] = 'Invalid credentials. Please try again.'
status 422
erb :signin, layout: :layout
end
end
# Signs user out
post '/users/signout' do
session.delete(:uname)
session[:success] = 'You have been signed out.'
redirect '/'
end
# Renders signup form
get '/users/signup' do
redirect '/' if authorized?
erb :signup, layout: :layout
end
# Creates new user
post '/users/signup' do
redirect '/' if authorized?
uname = params[:uname].strip
psswd = params[:psswd].strip
if (error = error_for_credentials(uname, psswd))
session[:error] = error
status 422
erb :signup, layout: :layout
else
add_user(uname, psswd)
session[:uname] = uname
session[:success] = "Welcome to the CMS, #{uname}!"
redirect '/'
end
end
# Renders template form for creating new file
get '/new' do
check_authorization
erb :new, layout: :layout
end
# Handles submission of form rendered above
# Creates file if valid filename
post '/create' do
check_authorization
name = params[:name].strip
if (error = error_for_filename(name))
session[:error] = error
status 422
erb :new, layout: :layout
else
make_restore_point(name, '')
File.write(File.join(data_path, name), '')
session[:success] = "#{name} has been created."
redirect '/'
end
end
# Displays file content if file exists
get '/:filename' do |filename|
path = "#{data_path}/#{filename}"
if File.file?(path)
file_content(path)
else
session[:error] = "#{filename} does not exist."
redirect '/'
end
end
# Displays form for editing files
# Content is preloaded into the textarea
get '/:filename/edit' do |filename|
check_authorization
if image?(filename)
session[:error] = 'Cannot edit image file.'
redirect '/'
end
@content = File.read(File.join(data_path, filename))
erb :edit, layout: :layout
end
# Updates the file with what is in form from above
post '/:filename' do |filename|
check_authorization
if image?(filename)
session[:error] = 'Cannot edit image file.'
redirect '/'
end
content = params[:content]
make_restore_point(filename, content)
File.write(File.join(data_path, filename), content)
session[:success] = "#{filename} has been updated."
redirect '/'
end
# Deletes a file
post '/:filename/delete' do |filename|
check_authorization
File.delete(File.join(data_path, filename))
session[:success] = "#{filename} has been deleted."
redirect '/'
end
# Renders form for duplicating file
get '/:filename/duplicate' do
check_authorization
erb :duplicate, layout: :layout
end
# Handles file duplication
post '/:filename/duplicate' do |filename|
check_authorization
if (error = error_for_filename(params[:name]))
session[:error] = error
status 422
erb :duplicate, layout: :layout
else
content = File.read(File.join(data_path, filename))
make_restore_point(filename, content)
File.write(File.join(data_path, params[:name]), content)
session[:success] = "#{filename} has been duplicated into #{params[:name]}"
redirect '/'
end
end
# Handles image uploading
post '/upload/image' do
check_authorization
name = params[:file_upload][:filename]
content = params[:file_upload][:tempfile]
if (error = error_for_image(name, params[:file_upload][:type]))
session[:error] = error
redirect '/new'
else
File.write(File.join(data_path, name), content.read)
session[:success] = 'Image successfully uploaded.'
redirect '/'
end
end
# Render the restore template that displays
# all the different versions of that file on record
get '/:filename/restores' do |filename|
if image?(filename)
session[:error] = 'Cannot restore image file.'
redirect '/'
end
@restore_count = restores_for(filename).size
erb :restores, layout: :layout
end
# Displays file to user of specific restore point
get '/:filename/restores/:version' do |filename, version|
path = restores_for(filename)[version[1..nil].to_i - 1]
if File.file?(path)
file_content(path)
else
session[:error] = "#{filename} does not exist."
redirect '/'
end
end
# Restores file back to original point
post '/:filename/restore/:version' do |filename, version|
check_authorization
if image?(filename)
session[:error] = 'Cannot restore image file.'
redirect '/'
end
restore(filename, version)
session[:success] = "#{filename} successfully restored to #{version}."
redirect '/'
end