A skeleton web application configured to use Sinatra and ActiveRecord
at master 281 lines 7.5 kB view raw
1# 2# Copyright (c) 2017-2018 joshua stein <jcs@jcs.org> 3# 4# Permission to use, copy, modify, and distribute this software for any 5# purpose with or without fee is hereby granted, provided that the above 6# copyright notice and this permission notice appear in all copies. 7# 8# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15# 16 17Encoding.default_internal = Encoding.default_external = Encoding::UTF_8 18 19APP_ROOT = File.realpath(File.dirname(__FILE__) + "/../") 20 21require "active_record" 22require "active_support/time" 23require "securerandom" 24 25require "sinatra/base" 26require "sinatra/namespace" 27require "sinatra/activerecord" 28require "cgi" 29require "rack/csrf" 30 31# configure mail early in case of exceptions 32require "pony" 33require "#{APP_ROOT}/config/mail.rb" 34 35# setup our custom logging to STDOUT 36require "#{APP_ROOT}/lib/logging.rb" 37 38# patch up Rack::Csrf to look at request.path for matching skipping rather than 39# request.path_info which is relative to the controller's path. since we don't 40# route to the per-request controller if Rack::Csrf aborts, we can't do 41# controller-relative matching anyway. 42module Rack 43 class Csrf 44 def any?(list, request) 45 pi = request.path.empty? ? '/' : request.path 46 list.any? do |route| 47 if route =~ (request.request_method + ':' + pi) 48 return true 49 end 50 end 51 end 52 end 53end 54 55class App < Sinatra::Base 56 register Sinatra::Namespace 57 register Sinatra::ActiveRecordExtension 58 59 # defaults, to be overridden with App.X = "..." in config/app.rb 60 61 # app name used in various places (emails, etc.) 62 cattr_accessor :name 63 @@name = "App" 64 65 # for controllers to be relative to a global base path 66 cattr_accessor :base_path 67 @@base_path = "/" 68 69 # to be replaced by an absolute url with scheme/domain 70 cattr_accessor :base_url 71 @@base_url = "/" 72 73 # email addresses to be notified of exceptions 74 cattr_accessor :exception_recipients 75 @@exception_recipients = [] 76 77 # parameters to be filtered from exception notifications and verbose logging 78 cattr_accessor :filtered_parameters 79 # regexes or strings 80 @@filtered_parameters = [ /password/ ] 81 82 83 # where we're at 84 set :root, File.realpath(__dir__ + "/../") 85 86 # config for active record 87 set :database_file, "#{App.root}/db/config.yml" 88 89 # gathered later from all controllers 90 @@all_routes = {} 91 cattr_accessor :all_routes 92 93 # app/views/(controller)/ 94 set :views, Proc.new { App.root + "/app/views/#{cur_controller}/" } 95 96 # app/views/layouts/(controller).erb or app/views/layouts/application.erb 97 set :erb, :layout => Proc.new { 98 @@layouts ||= {} 99 cc = cur_controller 100 101 if File.exist?(f = App.root + "/app/views/layouts/#{cc}.erb") 102 @@layouts[cc] ||= File.read(f) 103 else 104 @@layouts["application"] ||= File.read(App.root + 105 "/app/views/layouts/application.erb") 106 end 107 } 108 109 # store timestamps in the db in UTC, but convert to Time.zone time when 110 # instantiating objects 111 Time.zone = "Central Time (US & Canada)" 112 ActiveRecord::Base.time_zone_aware_attributes = true 113 114 # disable built-in apache-style logging in sinatra and disable color from AR 115 disable :logging 116 ActiveSupport::LogSubscriber.colorize_logging = false 117 @@logger = ::Logger.new(STDOUT) 118 use Sinatree::Logger, @@logger 119 120 # encrypted sessions, requiring a per-app secret to be configured 121 enable :sessions 122 set :sessions, { 123 :key => "_session", 124 :httponly => true, 125 :same_site => :lax, 126 } 127 begin 128 set :session_secret, File.read("#{App.root}/config/session_secret") 129 rescue => e 130 STDERR.puts e.message 131 STDERR.puts "no session secret file", 132 "ruby -e 'require \"securerandom\"; " + 133 "print SecureRandom.hex(64)' > config/session_secret" 134 exit 1 135 end 136 137 # allow erb views to be named view.html.erb 138 Tilt.prefer Tilt::ErubiTemplate 139 Tilt.register Tilt::ErubiTemplate, "html.erb" 140 141 # before every request, store controller for Logger 142 before do 143 request.current_controller = self.class 144 end 145 146 class << self 147 alias_method :env, :environment 148 attr_accessor :path 149 150 def cur_controller 151 raise 152 rescue => e 153 e.backtrace.each do |z| 154 if m = z.match(/app\/controllers\/(.+?)_controller\.rb:/) 155 return m[1] 156 end 157 end 158 159 nil 160 end 161 162 def filter_parameters(params) 163 params.reduce({}) do |acc, (key,value)| 164 if value.is_a?(Hash) 165 acc[key] = filter_parameters(value) 166 elsif App.filtered_parameters.detect{|fp| key.match(fp) } 167 acc[key] = "[filtered]" 168 else 169 acc[key] = value 170 end 171 acc 172 end 173 end 174 175 def logger 176 @@logger 177 end 178 179 def production? 180 env.to_s == "production" 181 end 182 def development? 183 env.to_s == "development" 184 end 185 def test? 186 env.to_s == "test" 187 end 188 end 189 190 def flash 191 session[:flash] ||= {} 192 end 193 194 def logger 195 @@logger 196 end 197 198 # per-environment configuration 199 if File.exist?(_c = "#{App.root}/config/#{App.environment}.rb") 200 require _c 201 end 202 203 # per-app initialization, not specific to environment 204 if File.exist?(_c = "#{App.root}/config/app.rb") 205 require _c 206 end 207end 208 209# bring in model base 210require "#{App.root}/lib/db.rb" 211require "#{App.root}/lib/db_model.rb" 212 213# and sinatra_more helpers 214require "#{App.root}/lib/sinatra_more/markup_plugin.rb" 215require "#{App.root}/lib/sinatra_more/render_plugin.rb" 216 217class App 218 include SinatraMore::AssetTagHelpers 219 include SinatraMore::FormHelpers 220 include SinatraMore::FormatHelpers 221 include SinatraMore::OutputHelpers 222 include SinatraMore::RenderHelpers 223 include SinatraMore::TagHelpers 224end 225 226# bring in user's mixins, models, and helpers 227Dir.glob("#{App.root}/app/mixins/*.rb").each{|f| require f } 228Dir.glob("#{App.root}/app/models/*.rb").each{|f| require f } 229 230require "#{App.root}/lib/helpers.rb" 231Dir.glob("#{App.root}/app/helpers/*.rb").each do |f| 232 require f 233end 234 235# and controllers, binding each's helper 236( 237 [ "#{App.root}/app/controllers/application_controller.rb" ] + 238 Dir.glob("#{App.root}/app/controllers/*.rb") 239).uniq.each do |f| 240 mc = Module.constants 241 require f 242 newmc = (Module.constants - mc) 243 244 if newmc.count != 1 245 raise "#{f} introduced #{newmc.count} new classes instead of 1" 246 end 247 cont = newmc[0] 248 249 if Kernel.const_defined?(c = cont.to_s.gsub(/Controller$/, "Helper")) 250 Kernel.const_get(cont).send(:helpers, Kernel.const_get(c)) 251 end 252end 253 254# and extras 255Dir.glob("#{App.root}/app/extras/*.rb").each{|f| require f } 256 257# bring up active record connection 258Db.connect(:environment => App.env.to_s) 259 260# dynamically build routes for config.ru 261ApplicationController.subclasses.each do |subklass| 262 path = subklass.path 263 if !path 264 # WidgetsController -> "widgets" 265 path = "/" << ActiveSupport::Inflector.underscore( 266 subklass.to_s.gsub(/Controller$/, "")) 267 end 268 269 if !path.is_a?(Array) 270 path = [ path ] 271 end 272 273 path.each do |p| 274 if App.all_routes[p] 275 raise "duplicate route for #{p.inspect}: " << 276 "#{App.all_routes[p].inspect} and #{subklass.inspect}" 277 end 278 279 App.all_routes[p] = subklass 280 end 281end