diff options
-rw-r--r-- | src/guff.cr | 622 | ||||
-rw-r--r-- | src/guff/handlers.cr | 624 |
2 files changed, 624 insertions, 622 deletions
diff --git a/src/guff.cr b/src/guff.cr index 13b296c..67258ec 100644 --- a/src/guff.cr +++ b/src/guff.cr @@ -12,629 +12,7 @@ end require "./guff/**" -private macro include_api_modules(modules) - {% for mod in modules.resolve %} - include {{ mod.id }} - {% end %} -end - -private macro api_method_dispatch(modules) - case namespace - {% for mod in modules.resolve %} - {% mod_name = mod.resolve.name.gsub(/^.*:(.*)API$/, "\\1").downcase %} - when {{ mod_name.stringify }} - case method - {% for mod_method in mod.resolve.methods %} - {% method_name = mod_method.name.gsub(/^do_([^_]+)_/, "") %} - when {{ method_name.stringify }} - {{ mod_method.name.id }}(params) - {% end %} - else - raise "unknown method: #{namespace}/#{method}" - end - {% end %} - else - raise "unknown namespace: #{namespace}" - end -end - module Guff - module Handlers - abstract class Handler < HTTP::Handler - def initialize(@context : Context) - super() - end - - protected def valid_origin_headers?(headers : HTTP::Headers) - # FIXME: need to compare these against something rather than - # just making sure that they are there - %w{origin referer}.any? do |key| - headers[key]? && headers[key].size > 0 - end - end - - protected def get_site_id(host : String?) : Int64? - @context.models.site.get_id(host) - end - end - - abstract class AuthenticatedHandler < Handler - def initialize(context : Context, @roles : Array(String)) - super(context) - end - - def call(context : HTTP::Server::Context) - if @context.has_role?(@roles) - authenticated_call(context) - else - call_next(context) - end - end - - abstract def authenticated_call(context : HTTP::Server::Context) - end - - class StubHandler < Handler - def call(context : HTTP::Server::Context) - STDERR.puts "%s %s" % [ - context.request.method, - context.request.path.not_nil! - ] - - call_next(context) - end - end - - class SessionHandler < Handler - def call(context : HTTP::Server::Context) - # clear session - @context.session.clear - - if cookie = context.request.cookies[Guff::Session::COOKIE]? - # load session - @context.session.load(cookie.value) - end - - call_next(context) - - if @context.session.valid? - @context.session.save - end - end - end - - class APIHandler < Handler - PATH_RE = %r{^/guff/api/(?<namespace>[\w_-]+)/(?<method>[\w_-]+)$} - - API_MODULES = [ - APIs::PostAPI, - APIs::UserAPI, - APIs::PageAPI, - APIs::ProjectAPI, - APIs::BlogAPI, - APIs::SiteAPI, - ] - - include_api_modules(API_MODULES) - - def call(context : HTTP::Server::Context) - if context.request.method == "POST" || - (@context.development? && context.request.method == "GET") - if md = PATH_RE.match(context.request.path.not_nil!) - namespace, method = %w{namespace method}.map { |k| md[k] } - - # get query parameteres - params = if (context.request.method == "GET") - context.request.query_params - else - HTTP::Params.parse(context.request.body || "") - end - - code, data = begin - { 200, api_method_dispatch(API_MODULES) } - rescue err - STDERR.puts "ERROR: #{err}" - { 400, { "error": err.to_s } } - end - - # send json response - context.response.status_code = code - context.response.content_type = "application/json; charset=utf-8" - data.to_json(context.response) - else - call_next(context) - end - else - call_next(context) - end - end - end - - class AssetsHandler < Handler - def initialize(context : Context) - super(context) - @etags = {} of String => String - end - - def call(context : HTTP::Server::Context) - req_path = context.request.path.not_nil! - - if matching_request?(context.request.method, req_path) && - valid_origin_headers?(context.request.headers) - # get expanded path to file - if abs_path = expand_path(req_path) - # get file digest - etag = get_file_etag(abs_path) - - # check for cache header - if context.request.headers["if-none-match"]? == etag - # cached, send 304 not modified - context.response.status_code = 304 - else - # not cached, set code and send headers - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.status_code = 200 - context.response.content_type = AssetMimeType.from_path(abs_path) - context.response.content_length = File.size(abs_path) - context.response.headers["etag"] = etag - - if context.request.method == "GET" - # send body for GET requests - File.open(abs_path) do |fh| - STDERR.puts "sending #{abs_path}" - IO.copy(fh, context.response) - STDERR.puts "done sending #{abs_path}" - end - end - end - else - # expanded path does not exist - call_next(context) - end - else - # not a matching request - call_next(context) - end - end - - VALID_METHODS = %w{GET HEAD} - PATH_RE = %r{^/guff/assets/} - - private def matching_request?(method, path) - VALID_METHODS.includes?(method) && PATH_RE.match(path) - end - - private def expand_path(req_path : String) : String? - # unescape path, check for nil byte - path = URI.unescape(req_path) - return nil if path.includes?('\0') - - # build absolute path - r = File.join( - @context.config.system_dir, - "assets", - File.expand_path(path.gsub(PATH_RE, ""), "/") - ) - - # return path if file exists, or nil otherwise - File.file?(r) ? r : nil - end - - private def get_file_etag(path : String) : String - # FIXME: rather than a hash this should be an HMAC - @etags[path] ||= OpenSSL::Digest.new("SHA1").file(path).hexdigest - end - end - - class AdminPageHandler < AuthenticatedHandler - def initialize(context : Context) - super(context, %w{admin editor}) - end - - PATH_RE = %r{^/guff/admin\.html$} - - def authenticated_call(context : HTTP::Server::Context) - if context.request.path.not_nil!.match(PATH_RE) - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - Views::AdminPageView.new(@context).to_s(context.response) - else - call_next(context) - end - end - end - - class LoginPageHandler < Handler - PATH_RE = %r{^/guff/login\.html$} - VALID_METHODS = %w{GET POST} - - def call(context : HTTP::Server::Context) - if VALID_METHODS.includes?(context.request.method) && - PATH_RE.match(context.request.path.not_nil!) - case context.request.method - when "GET" - reply(context.response) - when "POST" - begin - # check for valid origin or referer header - unless valid_origin_headers?(context.request.headers) - raise "missing origin and referer headers" - end - - # create session - session_id = @context.session.create({ - "user_id" => login(context.request.body).to_s, - }) - - # add cookie - context.response.cookies << HTTP::Cookie.new( - name: Guff::Session::COOKIE, - value: session_id as String, - http_only: true, - - # TODO - # expires: - # secure: - ) - - # redirect to admin panel - context.response.headers["location"] = "/guff/admin.html" - context.response.status_code = 302 - rescue err - # log error - STDERR.puts "login failed: #{err}" - reply(context.response, "invalid login") - end - else - raise "invalid HTTP method" - end - else - call_next(context) - end - end - - private def reply( - response : HTTP::Server::Response, - error : String? = nil - ) - response.headers["x-frame-options"] = "SAMEORIGIN" - response.content_type = "text/html; charset=utf-8" - response.status_code = 200 - Views::LoginPageView.new(@context, error).to_s(response) - end - - private def login(body : String?) - # check body - raise "empty request body" if body.nil? || body.size == 0 - - # parse request parameters - params = HTTP::Params.parse(body.not_nil!) - - # check login parameters - raise "missing login parameters" unless %w{ - username - password - csrf_token - }.all? do |key| - params.has_key?(key) && params[key].size > 0 - end - - # check csrf token - unless @context.models.csrf.use_token(params["csrf_token"]) - raise "invalid csrf token" - end - - # try login - user_id = @context.models.user.login( - params["username"], - params["password"] - ) - - # check user id - raise "invalid credentials" unless user_id - - # return user id - user_id.not_nil! - end - end - - class LogoutPageHandler < Handler - PATH_RE = %r{^/guff/logout\.html$} - - def call(context : HTTP::Server::Context) - if context.request.method == "GET" && - PATH_RE.match(context.request.path.not_nil!) && - valid_origin_headers?(context.request.headers) - # delete session - @context.session.delete - - # clear cookie - context.response.cookies << HTTP::Cookie.new( - name: Guff::Session::COOKIE, - value: "", - expires: Time.epoch(0), - http_only: true, - ) - - # build remaining headers - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - - # draw page - Views::LogoutPageView.new(@context).to_s(context.response) - else - call_next(context) - end - end - end - - class PageHandler < Handler - PATH_RE = %r{^/(?<slug>[^/]+)\.html$} - - def call(context : HTTP::Server::Context) - if post_id = get_post_id(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "page: #{post_id}" - else - # unknown page - call_next(context) - end - end - - private def get_post_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.page.get_id( - site_id: site_id, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class ProjectHandler < Handler - PATH_RE = %r{^/(?<slug>[^/]+)/?$} - - def call(context : HTTP::Server::Context) - if post_id = get_post_id(context.request) - path = context.request.path.not_nil! - - if /\/$/.match(path) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "project: #{post_id}" - else - # redirect to project - context.response.headers["location"] = path + "/" - context.response.status_code = 302 - end - else - # unknown page - call_next(context) - end - end - - private def get_post_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.project.get_id( - site_id: site_id, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class BlogPostHandler < Handler - PATH_RE = %r{^ - (/blog)? - /(?<year>\d{4}) - /(?<month>\d{2}) - /(?<day>\d{2}) - /(?<slug>[^/]+)\.html - $}x - - def call(context : HTTP::Server::Context) - if id = get_id(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "blog post id: #{id}" - else - # unknown page - call_next(context) - end - end - - private def get_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.blog.get_id( - site_id: site_id, - year: md["year"].to_i, - month: md["month"].to_i, - day: md["day"].to_i, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class BlogListHandler < Handler - # TODO: make index page configurable - PATH_RE = %r{^/ - (blog/?)? - ( - (?<year>\d{4})/ - ( - (?<month>\d{2})/ - ((?<day>\d{2})/)? - )? - )? - $}x - - def call(context : HTTP::Server::Context) - if ids = get_ids(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - - Views::Blog::ListView.new(@context, ids).to_s(context.response) - else - # unknown page - call_next(context) - end - end - - private def get_ids(request : HTTP::Request) : Array(Int64)? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - # get request parameters - params = request.query_params - - r = @context.models.blog.get_ids( - site_id: site_id, - year: md["year"]? ? md["year"].to_i : nil, - month: md["month"]? ? md["month"].to_i : nil, - day: md["day"]? ? md["day"].to_i : nil, - page: params["page"]? ? params["page"].to_i : nil, - ) - end - end - end - - # return result - r - end - end - - HANDLERS = [{ - :dev => true, - :id => :stub, - }, { - :dev => true, - :id => :error, - }, { - :dev => false, - :id => :log, - }, { - :dev => false, - :id => :deflate, - }, { - :dev => false, - :id => :assets, - }, { - :dev => false, - :id => :page, - }, { - :dev => false, - :id => :project, - }, { - :dev => false, - :id => :blog_post, - }, { - :dev => false, - :id => :blog_list, - }, { - :dev => false, - :id => :login, - }, { - :dev => false, - :id => :session, - }, { - :dev => false, - :id => :api, - }, { - :dev => false, - :id => :admin, - }, { - :dev => false, - :id => :logout, - }] - - def self.get(context : Context) : Array(HTTP::Handler) - HANDLERS.select { |row| - !(row[:dev] as Bool) || context.development? - }.map { |row| - make_handler(row[:id] as Symbol, context) - } - end - - def self.make_handler( - handler_id : Symbol, - context : Context - ) : HTTP::Handler - case handler_id - when :stub - StubHandler.new(context) - when :error - HTTP::ErrorHandler.new - when :log - HTTP::LogHandler.new - when :deflate - HTTP::DeflateHandler.new - when :session - SessionHandler.new(context) - when :api - APIHandler.new(context) - when :assets - AssetsHandler.new(context) - when :admin - AdminPageHandler.new(context) - when :login - LoginPageHandler.new(context) - when :logout - LogoutPageHandler.new(context) - when :page - PageHandler.new(context) - when :blog_post - BlogPostHandler.new(context) - when :blog_list - BlogListHandler.new(context) - when :project - ProjectHandler.new(context) - else - raise "unknown handler id: #{handler_id}" - end - end - end - module CLI module Actions abstract class Action diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr new file mode 100644 index 0000000..8b5d96c --- /dev/null +++ b/src/guff/handlers.cr @@ -0,0 +1,624 @@ +# +# TODO: refactor into handlers/*.cr eventually +# +private macro include_api_modules(modules) + {% for mod in modules.resolve %} + include {{ mod.id }} + {% end %} +end + +private macro api_method_dispatch(modules) + case namespace + {% for mod in modules.resolve %} + {% mod_name = mod.resolve.name.gsub(/^.*:(.*)API$/, "\\1").downcase %} + when {{ mod_name.stringify }} + case method + {% for mod_method in mod.resolve.methods %} + {% method_name = mod_method.name.gsub(/^do_([^_]+)_/, "") %} + when {{ method_name.stringify }} + {{ mod_method.name.id }}(params) + {% end %} + else + raise "unknown method: #{namespace}/#{method}" + end + {% end %} + else + raise "unknown namespace: #{namespace}" + end +end + +module Guff::Handlers + abstract class Handler < HTTP::Handler + def initialize(@context : Context) + super() + end + + protected def valid_origin_headers?(headers : HTTP::Headers) + # FIXME: need to compare these against something rather than + # just making sure that they are there + %w{origin referer}.any? do |key| + headers[key]? && headers[key].size > 0 + end + end + + protected def get_site_id(host : String?) : Int64? + @context.models.site.get_id(host) + end + end + + abstract class AuthenticatedHandler < Handler + def initialize(context : Context, @roles : Array(String)) + super(context) + end + + def call(context : HTTP::Server::Context) + if @context.has_role?(@roles) + authenticated_call(context) + else + call_next(context) + end + end + + abstract def authenticated_call(context : HTTP::Server::Context) + end + + class StubHandler < Handler + def call(context : HTTP::Server::Context) + STDERR.puts "%s %s" % [ + context.request.method, + context.request.path.not_nil! + ] + + call_next(context) + end + end + + class SessionHandler < Handler + def call(context : HTTP::Server::Context) + # clear session + @context.session.clear + + if cookie = context.request.cookies[Guff::Session::COOKIE]? + # load session + @context.session.load(cookie.value) + end + + call_next(context) + + if @context.session.valid? + @context.session.save + end + end + end + + class APIHandler < Handler + PATH_RE = %r{^/guff/api/(?<namespace>[\w_-]+)/(?<method>[\w_-]+)$} + + API_MODULES = [ + APIs::PostAPI, + APIs::UserAPI, + APIs::PageAPI, + APIs::ProjectAPI, + APIs::BlogAPI, + APIs::SiteAPI, + ] + + include_api_modules(API_MODULES) + + def call(context : HTTP::Server::Context) + if context.request.method == "POST" || + (@context.development? && context.request.method == "GET") + if md = PATH_RE.match(context.request.path.not_nil!) + namespace, method = %w{namespace method}.map { |k| md[k] } + + # get query parameteres + params = if (context.request.method == "GET") + context.request.query_params + else + HTTP::Params.parse(context.request.body || "") + end + + code, data = begin + { 200, api_method_dispatch(API_MODULES) } + rescue err + STDERR.puts "ERROR: #{err}" + { 400, { "error": err.to_s } } + end + + # send json response + context.response.status_code = code + context.response.content_type = "application/json; charset=utf-8" + data.to_json(context.response) + else + call_next(context) + end + else + call_next(context) + end + end + end + + class AssetsHandler < Handler + def initialize(context : Context) + super(context) + @etags = {} of String => String + end + + def call(context : HTTP::Server::Context) + req_path = context.request.path.not_nil! + + if matching_request?(context.request.method, req_path) && + valid_origin_headers?(context.request.headers) + # get expanded path to file + if abs_path = expand_path(req_path) + # get file digest + etag = get_file_etag(abs_path) + + # check for cache header + if context.request.headers["if-none-match"]? == etag + # cached, send 304 not modified + context.response.status_code = 304 + else + # not cached, set code and send headers + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.status_code = 200 + context.response.content_type = AssetMimeType.from_path(abs_path) + context.response.content_length = File.size(abs_path) + context.response.headers["etag"] = etag + + if context.request.method == "GET" + # send body for GET requests + File.open(abs_path) do |fh| + STDERR.puts "sending #{abs_path}" + IO.copy(fh, context.response) + STDERR.puts "done sending #{abs_path}" + end + end + end + else + # expanded path does not exist + call_next(context) + end + else + # not a matching request + call_next(context) + end + end + + VALID_METHODS = %w{GET HEAD} + PATH_RE = %r{^/guff/assets/} + + private def matching_request?(method, path) + VALID_METHODS.includes?(method) && PATH_RE.match(path) + end + + private def expand_path(req_path : String) : String? + # unescape path, check for nil byte + path = URI.unescape(req_path) + return nil if path.includes?('\0') + + # build absolute path + r = File.join( + @context.config.system_dir, + "assets", + File.expand_path(path.gsub(PATH_RE, ""), "/") + ) + + # return path if file exists, or nil otherwise + File.file?(r) ? r : nil + end + + private def get_file_etag(path : String) : String + # FIXME: rather than a hash this should be an HMAC + @etags[path] ||= OpenSSL::Digest.new("SHA1").file(path).hexdigest + end + end + + class AdminPageHandler < AuthenticatedHandler + def initialize(context : Context) + super(context, %w{admin editor}) + end + + PATH_RE = %r{^/guff/admin\.html$} + + def authenticated_call(context : HTTP::Server::Context) + if context.request.path.not_nil!.match(PATH_RE) + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + Views::AdminPageView.new(@context).to_s(context.response) + else + call_next(context) + end + end + end + + class LoginPageHandler < Handler + PATH_RE = %r{^/guff/login\.html$} + VALID_METHODS = %w{GET POST} + + def call(context : HTTP::Server::Context) + if VALID_METHODS.includes?(context.request.method) && + PATH_RE.match(context.request.path.not_nil!) + case context.request.method + when "GET" + reply(context.response) + when "POST" + begin + # check for valid origin or referer header + unless valid_origin_headers?(context.request.headers) + raise "missing origin and referer headers" + end + + # create session + session_id = @context.session.create({ + "user_id" => login(context.request.body).to_s, + }) + + # add cookie + context.response.cookies << HTTP::Cookie.new( + name: Guff::Session::COOKIE, + value: session_id as String, + http_only: true, + + # TODO + # expires: + # secure: + ) + + # redirect to admin panel + context.response.headers["location"] = "/guff/admin.html" + context.response.status_code = 302 + rescue err + # log error + STDERR.puts "login failed: #{err}" + reply(context.response, "invalid login") + end + else + raise "invalid HTTP method" + end + else + call_next(context) + end + end + + private def reply( + response : HTTP::Server::Response, + error : String? = nil + ) + response.headers["x-frame-options"] = "SAMEORIGIN" + response.content_type = "text/html; charset=utf-8" + response.status_code = 200 + Views::LoginPageView.new(@context, error).to_s(response) + end + + private def login(body : String?) + # check body + raise "empty request body" if body.nil? || body.size == 0 + + # parse request parameters + params = HTTP::Params.parse(body.not_nil!) + + # check login parameters + raise "missing login parameters" unless %w{ + username + password + csrf_token + }.all? do |key| + params.has_key?(key) && params[key].size > 0 + end + + # check csrf token + unless @context.models.csrf.use_token(params["csrf_token"]) + raise "invalid csrf token" + end + + # try login + user_id = @context.models.user.login( + params["username"], + params["password"] + ) + + # check user id + raise "invalid credentials" unless user_id + + # return user id + user_id.not_nil! + end + end + + class LogoutPageHandler < Handler + PATH_RE = %r{^/guff/logout\.html$} + + def call(context : HTTP::Server::Context) + if context.request.method == "GET" && + PATH_RE.match(context.request.path.not_nil!) && + valid_origin_headers?(context.request.headers) + # delete session + @context.session.delete + + # clear cookie + context.response.cookies << HTTP::Cookie.new( + name: Guff::Session::COOKIE, + value: "", + expires: Time.epoch(0), + http_only: true, + ) + + # build remaining headers + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + + # draw page + Views::LogoutPageView.new(@context).to_s(context.response) + else + call_next(context) + end + end + end + + class PageHandler < Handler + PATH_RE = %r{^/(?<slug>[^/]+)\.html$} + + def call(context : HTTP::Server::Context) + if post_id = get_post_id(context.request) + # TODO: render page + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + context.response << "page: #{post_id}" + else + # unknown page + call_next(context) + end + end + + private def get_post_id(request : HTTP::Request) : Int64? + r = nil + + if request.method == "GET" + if md = PATH_RE.match(request.path.not_nil!) + if site_id = get_site_id(request.headers["host"]?) + r = @context.models.page.get_id( + site_id: site_id, + slug: md["slug"], + ) + end + end + end + + # return result + r + end + end + + class ProjectHandler < Handler + PATH_RE = %r{^/(?<slug>[^/]+)/?$} + + def call(context : HTTP::Server::Context) + if post_id = get_post_id(context.request) + path = context.request.path.not_nil! + + if /\/$/.match(path) + # TODO: render page + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + context.response << "project: #{post_id}" + else + # redirect to project + context.response.headers["location"] = path + "/" + context.response.status_code = 302 + end + else + # unknown page + call_next(context) + end + end + + private def get_post_id(request : HTTP::Request) : Int64? + r = nil + + if request.method == "GET" + if md = PATH_RE.match(request.path.not_nil!) + if site_id = get_site_id(request.headers["host"]?) + r = @context.models.project.get_id( + site_id: site_id, + slug: md["slug"], + ) + end + end + end + + # return result + r + end + end + + class BlogPostHandler < Handler + PATH_RE = %r{^ + (/blog)? + /(?<year>\d{4}) + /(?<month>\d{2}) + /(?<day>\d{2}) + /(?<slug>[^/]+)\.html + $}x + + def call(context : HTTP::Server::Context) + if id = get_id(context.request) + # TODO: render page + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + context.response << "blog post id: #{id}" + else + # unknown page + call_next(context) + end + end + + private def get_id(request : HTTP::Request) : Int64? + r = nil + + if request.method == "GET" + if md = PATH_RE.match(request.path.not_nil!) + if site_id = get_site_id(request.headers["host"]?) + r = @context.models.blog.get_id( + site_id: site_id, + year: md["year"].to_i, + month: md["month"].to_i, + day: md["day"].to_i, + slug: md["slug"], + ) + end + end + end + + # return result + r + end + end + + class BlogListHandler < Handler + # TODO: make index page configurable + PATH_RE = %r{^/ + (blog/?)? + ( + (?<year>\d{4})/ + ( + (?<month>\d{2})/ + ((?<day>\d{2})/)? + )? + )? + $}x + + def call(context : HTTP::Server::Context) + if ids = get_ids(context.request) + # TODO: render page + context.response.headers["x-frame-options"] = "SAMEORIGIN" + context.response.content_type = "text/html; charset=utf-8" + context.response.status_code = 200 + + Views::Blog::ListView.new(@context, ids).to_s(context.response) + else + # unknown page + call_next(context) + end + end + + private def get_ids(request : HTTP::Request) : Array(Int64)? + r = nil + + if request.method == "GET" + if md = PATH_RE.match(request.path.not_nil!) + if site_id = get_site_id(request.headers["host"]?) + # get request parameters + params = request.query_params + + r = @context.models.blog.get_ids( + site_id: site_id, + year: md["year"]? ? md["year"].to_i : nil, + month: md["month"]? ? md["month"].to_i : nil, + day: md["day"]? ? md["day"].to_i : nil, + page: params["page"]? ? params["page"].to_i : nil, + ) + end + end + end + + # return result + r + end + end + + HANDLERS = [{ + :dev => true, + :id => :stub, + }, { + :dev => true, + :id => :error, + }, { + :dev => false, + :id => :log, + }, { + :dev => false, + :id => :deflate, + }, { + :dev => false, + :id => :assets, + }, { + :dev => false, + :id => :page, + }, { + :dev => false, + :id => :project, + }, { + :dev => false, + :id => :blog_post, + }, { + :dev => false, + :id => :blog_list, + }, { + :dev => false, + :id => :login, + }, { + :dev => false, + :id => :session, + }, { + :dev => false, + :id => :api, + }, { + :dev => false, + :id => :admin, + }, { + :dev => false, + :id => :logout, + }] + + def self.get(context : Context) : Array(HTTP::Handler) + HANDLERS.select { |row| + !(row[:dev] as Bool) || context.development? + }.map { |row| + make_handler(row[:id] as Symbol, context) + } + end + + def self.make_handler( + handler_id : Symbol, + context : Context + ) : HTTP::Handler + case handler_id + when :stub + StubHandler.new(context) + when :error + HTTP::ErrorHandler.new + when :log + HTTP::LogHandler.new + when :deflate + HTTP::DeflateHandler.new + when :session + SessionHandler.new(context) + when :api + APIHandler.new(context) + when :assets + AssetsHandler.new(context) + when :admin + AdminPageHandler.new(context) + when :login + LoginPageHandler.new(context) + when :logout + LogoutPageHandler.new(context) + when :page + PageHandler.new(context) + when :blog_post + BlogPostHandler.new(context) + when :blog_list + BlogListHandler.new(context) + when :project + ProjectHandler.new(context) + else + raise "unknown handler id: #{handler_id}" + end + end +end |