# frozen_string_literal: true require_relative 'haml_wrapper' require_relative 'version' require_relative '../../core_extensions' module Intranet class Core # @!visibility protected # Builder for the Intranet. The builder is in charge of storing registered modules (responders # instances) and to call the appropriate responder according to the received URL. class Builder include HamlWrapper # The tree-like structure containing all registered responders (for Haml) # @return [CoreExtensions::Tree] attr_reader :responders # The metadata concerning all registered modules (for Haml) # @return [Hash] attr_reader :modules # Initializes a new builder. # @param logger [Object] The logger. def initialize(logger) @logger = logger @responders = CoreExtensions::Tree.new @modules = { NAME => { version: VERSION, homepage: HOMEPAGE_URL } } end # Finalizes the builder. Each registered responder is called for +finalize+. def finalize @responders.to_h.each do |path, responder| next if responder.nil? @logger.debug('Intranet::Builder: finalize responder at \'' + path + '\'') responder.finalize end end # Processes the given URL path and query. The corresponding responder is called to get the # page content. If no responder can handle the request, HTTP error 404 is returned. If the # responder returns a partial HTML content (HTTP error 206), it is assumed to be the page # body and integrated into the Intranet template. # @param path [String] The requested path, relative to the web server root. This path is # supposed secured and normalized (no '../' in particular). # @param query [Hash] The content of the GET parameters of the URL. # @return [Array] The HTTP return code, the MIME type and the answer body. def do_get(path, query = {}) resp, responder_path = get_responder(path) status, mime_type, body = resp.generate_page(responder_path, query) # Generate header and footer when partial content is returned by the responder if status == 206 && mime_type == 'text/html' body = to_markup('skeleton', is_home: path == '/index.html', body: body, css: resp.css_dependencies, js: resp.js_dependencies) status = 200 end [status, mime_type, body] rescue KeyError, NoMethodError [404, '', ''] end # Registers a new responder. If a responder is already registered with the same path, the new # one overrides the old one. # @param responder [Intranet::AbstractResponder] The responder instance of the module. # @param path [Array] The path, relative to the web server root, representing the module root # directory. If empty, the responder will be registered as Home responder # (to serve /index.html in particular). Subdirectories are allowed using # an array element for each directory level. # @raise [ArgumentError] If the +responder+ is not a valid responder instance. # @raise [ArgumentError] If one of the element of the +path+ contains invalid characters. def register(responder, path = []) raise ArgumentError unless responder.class.superclass.to_s == 'Intranet::AbstractResponder' raise ArgumentError unless path.all?(&:urlized?) insert_responder(responder, path) store_module_metadata(responder) end private # Get the responder instance associated with the given path. # @param path [String] The absolute URL path. # @return [Array] The responder instance (possibly nil) and the remaining of the URL path that # has not been parsed. def get_responder(path) current_treenode = @responders relative_path = path[1..-1].split('/').delete_if do |part| if current_treenode.child_exists?(part) current_treenode = current_treenode.child_node(part) true end end [current_treenode.value, '/' + relative_path.join('/')] end # Inserts a responder instance in the appropriate Tree node according the given +path+. # Missing Tree nodes are created. # @param responder [Intranet::AbstractResponder] The responder instance of the module. # @param path [Array] See Intranet::Builder::register(). def insert_responder(responder, path) current_node = @responders path.each do |part| next if part.empty? || part == '.' current_node = current_node.add_child_node(part) end current_node.value = responder end # Stores the module name, version and homepage URL. These information are used to display the # 'about' modal window when partial content is returned by a module. The hash structure # ensures that each module metadata are stored once only. # @param responder [Intranet::AbstractResponder] The responder instance of the module. def store_module_metadata(responder) @modules[responder.class.module_name] = { version: responder.class.module_version, homepage: responder.class.module_homepage } end end end end