# 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 # The Home page URL # @return [String] attr_reader :home_url # 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 } } @home_url = '/index.html' 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 = {}) responder, parsed_path, unparsed_path = get_responder(path) status, mime_type, body = responder.generate_page(unparsed_path, query) # Generate header and footer when partial content is returned by the responder if status == 206 && mime_type == 'text/html' body = add_header_and_footer(body, responder, parsed_path) status = 200 end [status, mime_type, body] rescue KeyError, NoMethodError => e @logger.debug(e) [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. # @raise [ArgumentError] If the +responder+ is not a valid responder instance. # @raise [ArgumentError] If the +path+ is invalid. def register(responder, path = []) raise ArgumentError unless responder.class.superclass.to_s == 'Intranet::AbstractResponder' raise ArgumentError unless valid_path?(path) insert_responder(responder, path) store_module_metadata(responder) end # Defines the URL of the home page. # @param url [String] The absolute URL of the home page. # @raise [ArgumentError] If the URL is not an absolute path. def home_url=(url) raise ArgumentError unless url.start_with?('/') @home_url = url end private # Get the responder instance associated with the given path. # @param path [String] The absolute URL path. # @return [Intranet::AbstractResponder, String, String] The responder instance (possibly nil) # the parsed part of the path, and the # unparsed part of the path. def get_responder(path) parsed_path = [] current_treenode = @responders unparsed_path = path[1..-1].split('/').delete_if do |part| if current_treenode.child_exists?(part) current_treenode = current_treenode.child_node(part) parsed_path << part true end end [current_treenode.value, "/#{parsed_path.join('/')}", "/#{unparsed_path.join('/')}"] end # Encapsulate the given body in the default page skeleton, effectively # adding page header and footer. # @param body [String] The body of the page # @param responder [Intranet::AbstractResponder] The responder that produced the body # @param path [String] The path to the responder def add_header_and_footer(body, responder, path) body[:stylesheets] ||= responder.css_dependencies body[:scripts] ||= responder.js_dependencies.map { |url| { src: url, defer: 'defer' } } to_markup('skeleton', body: body, current_path: path) end # Check whether a path is valid. In particular, this function excludes a path containing an # empty ('') or dot ('.') part, for instance '/path//to/./responder'. # @param path [Array] The path to test. # @return [Boolean] True if the path is valid, False otherwise. def valid_path?(path) (path.size == 1 || path.size == 2) && path.none?(&:empty?) && path.all?(&:urlized?) rescue NoMethodError false 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| 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