=begin Copyright 2010-2013 Tasos Laskos Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. =end module Arachni module Component # # {Component} error namespace. # # All {Component} errors inherit from and live under it. # # @author Tasos "Zapotek" Laskos # class Error < Arachni::Error # # Raised when a specified component could not be found/does not exist. # # @author Tasos "Zapotek" Laskos # class NotFound < Error end end require Options.dir['lib'] + 'component/options' # # Handles modules, reports, path extractor modules, plug-ins, pretty much # every modular aspect of the framework. # # It is usually extended to fill-in for system specific functionality. # # @example # # # create a namespace for our components # module Components # end # # LIB = "#{File.dirname( __FILE__ )}/lib/" # NAMESPACE = Components # # # $ ls LIB # # component1.rb component2.rb # # # # $ cat LIB/component1.rb # # class Components::Component1 # # end # # # # $ cat LIB/component2.rb # # class Components::Component2 # # end # # # p components = Arachni::Component::Manager.new( LIB, NAMESPACE ) # #=> {} # # p components.available # #=> ["component2", "component1"] # # p components.load_all # #=> ["component2", "component1"] # # p components # #=> {"component2"=>Components::Component2, "component1"=>Components::Component1} # # p components.clear # #=> {} # # p components.load :component1 # #=> ["component1"] # # p components # #=> {"component1"=>Components::Component1} # # p components.clear # #=> {} # # p components[:component2] # #=> Components::Component2 # # # @author Tasos "Zapotek" Laskos # class Manager < Hash include UI::Output WILDCARD = '*' EXCLUDE = '-' # @return [String] The path to the component library/directory. attr_reader :lib # @return [Module] # Namespace under which all components are directly defined. attr_reader :namespace # # @param [String] lib The path to the component library/directory. # @param [Module,Class] namespace # Namespace under which all components are directly defined. # def initialize( lib, namespace ) @lib = lib @namespace = namespace end # # Loads components. # # @param [Array] components # Components to load. # # @return [Array] Names of loaded components. # def load( *components ) parse( [components].flatten ).each { |component| self.[]( component ) } end # # Loads all components, equivalent of `load '*'`. # # @return [Array] Names of loaded components. # def load_all load '*' end # # Loads components by the tags found in the `Hash` returned by their `.info` method # (tags should be in either: `:tags` or `:issue[:tags]`). # # @param [Array] tags Tags to look for in components. # # @return [Array] Components loaded. # def load_by_tags( tags ) return [] if !tags tags = [tags].flatten.compact.map( &:to_s ) return [] if tags.empty? load_all map do |k, v| component_tags = [v.info[:tags]] component_tags |= [v.info[:issue][:tags]] if v.info[:issue] component_tags = [component_tags].flatten.uniq.compact if !component_tags.includes_tags?( tags ) delete( k ) next end k end.compact end # # Validates and prepares options for a given component. # # @param [String] component_name Name of the component. # @param [Class] component Component. # @param [Hash] user_opts User options. # # @return [Hash] Prepared options to be passed to the component. # # @raise [Component::Options::Error::Invalid] # If given options are invalid. # def prep_opts( component_name, component, user_opts = {} ) info = component.info return {} if !info.include?( :options ) || info[:options].empty? user_opts ||= {} options = {} errors = {} info[:options].each do |opt| name = opt.name val = user_opts[name] || opt.default if opt.empty_required_value?( val ) errors[name] = { opt: opt, value: val, type: :empty_required_value } elsif !opt.valid?( val ) errors[name] = { opt: opt, value: val, type: :invalid } end options[name] = opt.normalize( val ) end if !errors.empty? fail Component::Options::Error::Invalid, format_error_string( component_name, errors ) end options end # # It parses the component array making sure that its structure is valid # and takes into consideration {WILDCARD wildcard} and {EXCLUDE exclusion} # modifiers. # # @param [Array] components Component names. # # @return [Array] Components to load. # def parse( components ) unload = [] load = [] components = [components].flatten.map( &:to_s ) return load if components[0] == EXCLUDE components = components.deep_clone components.each do |component| if component[0] == EXCLUDE component[0] = '' if component[WILDCARD] unload |= wilcard_to_names( component ) else unload << component end end end if !components.include?( WILDCARD ) avail_components = available( ) components.each do |component| if component.substring?( WILDCARD ) load |= wilcard_to_names( component ) else if avail_components.include?( component ) load << component else fail Error::NotFound, "Component '#{component}' could not be found." end end end load.flatten! else available.each{ |component| load << component } end load - unload end # # Fetches a component's class by name, loading it on the fly if need be. # # @param [String, Symbol] name Component name. # # @return [Class] Component. # def []( name ) name = name.to_s return fetch( name ) if include?( name ) self[name] = load_from_path( name_to_path( name ) ) end def include?( k ) super( k.to_s ) end alias :loaded? :include? # Unloads all loaded components. def clear keys.each { |l| delete( l ) } end alias :unload_all :clear # # Unloads a component by name. # # @param [String, Symbol] name Component name. # def delete( name ) name = name.to_s begin @namespace.send( :remove_const, fetch( name ).to_s.split( ':' ).last.to_sym ) rescue end super( name ) end alias :unload :delete # @return [Array] Names of available components. def available paths.map{ |path| path_to_name( path ) } end # @return [Array] Names of loaded components. def loaded keys end # # Converts the name of a component to a its file's path. # # @param [String] name Name of the component. # # @return [String] Path to component file. # def name_to_path( name ) paths.each { |path| return path if name.to_s == path_to_name( path ) } nil end # # Converts the path of a component to a component name. # # @param [String] path File-path of the component. # # @return [String] Component name. # def path_to_name( path ) File.basename( path, '.rb' ) end # @return [Array] # Paths of all available components (excluding helper files). def paths Dir.glob( File.join( "#{@lib}**", "*.rb" ) ).reject{ |path| helper?( path ) } end private def wilcard_to_names( name ) if name[WILDCARD] paths.map do |path| path_to_name( path ) if path.match( name.gsub( '*', '(.*)' ) ) end.compact end end def format_error_string( name, errors ) "Invalid options for component: #{name}\n" + errors.map do |optname, error| val = error[:value].nil? ? '' : error[:value] msg = (error[:type] == :invalid) ? "Invalid type" : "Empty required value" " * #{msg}: #{optname} => '#{val}'\n" + " * Expected type: #{error[:opt].type}" end.join( "\n\n" ) end def load_from_path( path ) pre = classes ::Kernel::load( path ) post = classes return if pre == post get_obj( (post - pre).first ) end def classes @namespace.constants.reject{ |c| !get_obj( c ).is_a?( Class ) } end def get_obj( sym ) @namespace.const_get( sym ) end def helper?( path ) File.exist?( File.dirname( path ) + '.rb' ) end end end end