ncbo/bioportal_web_ui

View on GitHub
app/controllers/application_controller.rb

Summary

Maintainability
F
4 days
Test Coverage
require 'uri'
require 'net/http'
require 'net/https'
require 'net/ftp'
require 'json'
require 'cgi'
require 'rexml/document'
require 'rest-client'
require 'ontologies_api_client'

# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  helper :all # include all helpers, all the time
  helper_method :bp_config_json, :current_license, :using_captcha?

  # Pull configuration parameters for REST connection.
  REST_URI = $REST_URL
  API_KEY = $API_KEY
  PROXY_URI = $PROXY_URL
  REST_URI_BATCH = REST_URI + '/batch'

  # Rails.cache expiration
  EXPIRY_RI_STATS = 60 * 60 * 24       # 24:00 hours
  EXPIRY_RI_ONTOLOGIES = 60 * 60 * 24  # 24:00 hours
  EXPIRY_SEMANTIC_TYPES = 60 * 60 * 24 # 24:00 hours
  EXPIRY_RECENT_MAPPINGS = 60 * 60     #  1:00 hours
  EXPIRY_ONTOLOGY_SIMPLIFIED = 60 * 1  #  0:01 minute

  RETRY_LIMIT = 1

  $trial_license_initialized = false

  if !$EMAIL_EXCEPTIONS.nil? && $EMAIL_EXCEPTIONS == true
    include ExceptionNotifiable
  end

  # See ActionController::RequestForgeryProtection for details
  protect_from_forgery

  before_action :set_global_thread_values, :domain_ontology_set, :authorize_miniprofiler, :clean_empty_strings_from_params_arrays, :init_trial_license

  def set_global_thread_values
    Thread.current[:session] = session
    Thread.current[:request] = request
  end

  def clean_empty_strings_from_params_arrays(params = nil)
    params ||= params()
    params.keys.each do |k|
      clean_empty_strings_from_params_arrays(params[k]) if params[k].is_a?(Hash)
      params[k] = params[k].select {|e| !e.eql?("")} if params[k].is_a?(Array)
    end
  end

  def domain_ontology_set
    @subdomain_filter = { :active => false, :name => "", :acronym => "" }

    if !$ENABLE_SLICES.nil? && $ENABLE_SLICES == true
      host = request.host
      host_parts = host.split(".")
      subdomain = host_parts[0].downcase

      slices = LinkedData::Client::Models::Slice.all
      slices_acronyms = slices.map {|s| s.acronym}

      # Set custom ontologies if we're on a subdomain that has them
      # Else, make sure user ontologies are set appropriately
      if slices_acronyms && slices_acronyms.include?(subdomain)
        slice = slices.select {|s| s.acronym.eql?(subdomain)}.first
        @subdomain_filter[:active] = true
        @subdomain_filter[:name] = slice.name
        @subdomain_filter[:acronym] = slice.acronym
      end
    end

    Thread.current[:slice] = @subdomain_filter
  end

  def anonymous_user
    user = DataAccess.getUser($ANONYMOUS_USER)
    user ||= User.new({"id" => 0})
  end

  def not_found
    if request.xhr?
      render plain: "Error: load failed"
      return
    end
    
    raise ActiveRecord::RecordNotFound.new('Not Found')
  end

  NOTIFICATION_TYPES = { :notes => "CREATE_NOTE_NOTIFICATION", :all => "ALL_NOTIFICATION" }

  def to_param(name) # Paramaterizes URLs without encoding
    unless name.nil?
      name.to_s.gsub(' ',"_")
    end
  end

  def undo_param(name) #Undo Paramaterization
    unless name.nil?
      name.to_s.gsub('_'," ")
    end
  end

  def bp_config_json
    # For config settings, see
    # config/bioportal_config.rb
    # config/initializers/ontologies_api_client.rb
    config = {
        org: $ORG,
        org_url: $ORG_URL,
        site: $SITE,
        org_site: $ORG_SITE,
        ui_url: $UI_URL,
        apikey: LinkedData::Client.settings.apikey,
        userapikey: get_apikey,
        rest_url: LinkedData::Client.settings.rest_url,
        proxy_url: $PROXY_URL,
        biomixer_url: $BIOMIXER_URL
    }
    config[:ncbo_slice] = @subdomain_filter[:acronym] if (@subdomain_filter[:active] && !@subdomain_filter[:acronym].empty?)
    config.to_json
  end

  def remote_file_exists?(url)
    begin
      url = URI.parse(url)

      if url.kind_of?(URI::FTP)
        check = check_ftp_file(url)
      else
        check = check_http_file(url)
      end

    rescue
      return false
    end

    check
  end

  def check_http_file(url)
    session = Net::HTTP.new(url.host, url.port)
    session.use_ssl = true if url.port == 443
    session.start do |http|
      response_valid = http.head(url.request_uri).code.to_i < 400
      return response_valid
    end
  end

  def check_ftp_file(uri)
    ftp = Net::FTP.new(uri.host, uri.user, uri.password)
    ftp.login
    begin
      file_exists = ftp.size(uri.path) > 0
    rescue
      # Check using another method
      path = uri.path.split("/")
      filename = path.pop
      path = path.join("/")
      ftp.chdir(path)
      files = ftp.dir
      # Dumb check, just see if the filename is somewhere in the list
      files.each { |file| return true if file.include?(filename) }
    end
    file_exists
  end

  def parse_response_body(response)
    return nil if response.nil?

    if response.respond_to?(:errors) && response.errors
      response
    else
      OpenStruct.new(JSON.parse(response.body, symbolize_names: true))
    end
  end

  def response_errors(error_struct)
    error_struct = parse_response_body(error_struct)
    errors = {error: "There was an error, please try again"}
    return errors unless error_struct
    return errors unless error_struct.respond_to?(:errors)
    errors = {}
    error_struct.errors.each do |error|
      if error.is_a?(OpenStruct) || error.is_a?(Struct)
        errors.merge!(struct_to_hash(error))
      else
        errors[:error] = error
      end
    end
    errors
  end

  def response_success?(response)
    return true if response.nil?

    if response.respond_to?(:status) && response.status
        response.status.to_i < 400
    else
      !(response.respond_to?(:errors) && response.errors)
    end
  end

  def response_error?(response)
    !response_success?(response)
  end

  def struct_to_hash(struct)
    hash = {}
    struct.members.each do |attr|
      next if [:links, :context].include?(attr)
      if struct[attr].is_a?(Struct) || struct[attr].is_a?(OpenStruct)
        hash[attr] = struct_to_hash(struct[attr])
      else
        hash[attr] = struct[attr]
      end
    end
    hash
  end

  def redirect_to_browse # Redirect to the browse Ontologies page
    redirect_to "/ontologies"
  end

  def redirect_to_home # Redirect to Home Page
    redirect_to "/"
  end

  def redirect_new_api(class_view = false)
    # Hack to make ontologyid and conceptid work in addition to id and ontology params
    params[:ontology] = params[:ontology].nil? ? params[:ontologyid] : params[:ontology]
    # Error checking
    if params[:ontology].nil? || params[:id] && params[:ontology].nil?
      @error = "Please provide an ontology id or concept id with an ontology id."
      return
    end
    acronym = BPIDResolver.id_to_acronym(params[:ontology])
    not_found unless acronym
    if class_view
      @ontology = LinkedData::Client::Models::Ontology.find_by_acronym(acronym).first
      @submission = get_ontology_submission_ready(@ontology)
      concept = get_class(params, @submission).first.to_s
      redirect_to "/ontologies/#{acronym}?p=classes#{params_string_for_redirect(params, prefix: "&")}", :status => :moved_permanently
    else
      redirect_to "/ontologies/#{acronym}#{params_string_for_redirect(params)}", :status => :moved_permanently
    end
  end

  def params_cleanup_new_api
    params = @_params
    if params[:ontology] && params[:ontology].to_i > 0
      params[:ontology] = BPIDResolver.id_to_acronym(params[:ontology])
    end

    params
  end

  def params_string_for_redirect(params, options = {})
    prefix = options[:prefix] || "?"
    stop_words = options[:stop_words] || ["ontology", "controller", "action", "id", "acronym"]
    params_array = []
    params.each do |key,value|
      next if stop_words.include?(key.to_s) || value.nil? || value.empty?
      params_array << "#{key}=#{CGI.escape(value)}"
    end
    params_array.empty? ? "" : "#{prefix}#{params_array.join('&')}"
  end

  # rack-mini-profiler authorization
  def authorize_miniprofiler
    if params[:enable_profiler] && params[:enable_profiler].eql?("true") && session[:user] && session[:user].admin?
      Rack::MiniProfiler.authorize_request
    else
      Rack::MiniProfiler.deauthorize_request
    end
  end

  # Verifies if user is logged in
  def authorize_and_redirect
    unless session[:user]
      redirect_to_home
    end
  end

  # Verifies that a user owns an object
  def authorize_owner(id=nil)
    if id.nil?
      id = params[:id].to_i
    end

    id.map! {|i| i.to_i} if id.kind_of?(Array)

    if session[:user].nil?
      redirect_to_home
    else
      if id.kind_of?(Array)
        redirect_to_home if !session[:user].admin? && !id.include?(session[:user].id.to_i)
      else
        redirect_to_home if !session[:user].admin? && !session[:user].id.to_i.eql?(id)
      end
    end
  end

  def authorize_admin
    admin = session[:user] && session[:user].admin?
    redirect_to_home unless admin
  end

  def current_user_admin?
    session[:user] && session[:user].admin?
  end

  def update_tab(ontology, concept)
    onts = session[:ontologies] || []
    found = false
    onts.each do |ont|
      if ont.ontology_id.eql? ontology.id
        ont.concept = concept
        found = true
      end
    end

    onts << History.new(ontology.id, ontology.name, ontology.acronym, concept) unless found

    # The "Recently Viewed" menu item displays the contents of session[:ontologies]
    session[:ontologies] = onts
  end

  def check_delete_mapping_permission(mappings)
    # ensure mappings is an Array of mappings (some calls may provide only a single mapping instance)
    mappings = [mappings] if mappings.instance_of? LinkedData::Client::Models::Mapping
    return false if mappings.all? {|m| m.id.to_s.empty?}
    delete_mapping_permission = false
    if session[:user]
      delete_mapping_permission = session[:user].admin?
      mappings.each do |mapping|
        break if delete_mapping_permission
        delete_mapping_permission = mapping.creator == session[:user].id
      end
    end
    delete_mapping_permission
  end

  def using_captcha?
    ENV['USE_RECAPTCHA'].present? && ENV['USE_RECAPTCHA'] == 'true'
  end

  def get_class(params, submission)
    lang = helpers.request_lang(submission)

    if @ontology.flat?
      ignore_concept_param = params[:conceptid].nil? ||
          params[:conceptid].empty? ||
          params[:conceptid].eql?("root") ||
          params[:conceptid].eql?("bp_fake_root")
      if ignore_concept_param
        # Don't display any classes in the tree
        @concept = LinkedData::Client::Models::Class.new
        @concept.prefLabel = "Please search for a class using the Jump To field above"
        @concept.obsolete = false
        @concept.id = "bp_fake_root"
        @concept.properties = {}
        @concept.children = []
      else
        # Display only the requested class in the tree
        @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid])
        @concept.children = []
      end
      @root = LinkedData::Client::Models::Class.new
      @root.children = [@concept]
    else
      # not ignoring 'bp_fake_root' here
      ignore_concept_param = params[:conceptid].nil? ||
          params[:conceptid].empty? ||
          params[:conceptid].eql?("root")

      if ignore_concept_param
        # get the top level nodes for the root
        # TODO_REV: Support views? Replace old view call: @ontology.top_level_classes(view)
        roots = @ontology.explore.roots(lang: lang)

        if roots.nil? || roots.empty?
          LOG.add :debug, "Missing roots for #{@ontology.acronym}"
          not_found
        end

        @root = LinkedData::Client::Models::Class.new(read_only: true)
        @root.children = roots.sort{|x,y|
          x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty?
          y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty?
          (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase}

        # get the initial concept to display
        root_child = @root.children.first
        @concept = root_child.explore.self(full: true, lang: lang)

        # Some ontologies have "too many children" at their root. These will not process and are handled here.
        if @concept.nil?
          LOG.add :debug, "Missing class #{root_child.links.self}"
          not_found
        end
      else
        # if the id is coming from a param, use that to get concept
        @concept = @ontology.explore.single_class({full: true, lang: lang}, params[:conceptid])
        if @concept.nil? || @concept.errors
          LOG.add :debug, "Missing class #{@ontology.acronym} / #{params[:conceptid]}"
          not_found
        end

        # Create the tree
        rootNode = @concept.explore.tree(include: "prefLabel,hasChildren,obsolete", lang: lang)

        if rootNode.nil? || rootNode.empty?
          roots = @ontology.explore.roots(lang: lang)

          if roots.nil? || roots.empty?
            LOG.add :debug, "Missing roots for #{@ontology.acronym}"
            not_found
          end

          if roots.any? {|c| c.id == @concept.id}
            rootNode = roots
          else
            rootNode = [@concept]
          end
        end
        @root = LinkedData::Client::Models::Class.new(read_only: true)
        @root.children = rootNode.sort{|x,y|
          x.prefLabel = helpers.link_last_part(x.id) if x.prefLabel.to_s.empty?
          y.prefLabel = helpers.link_last_part(y.id) if y.prefLabel.to_s.empty?
          (x.prefLabel || "").downcase <=> (y.prefLabel || "").downcase}
      end
    end
    @concept
  end

  def get_metrics_hash
    metrics_hash = {}
    # TODO: Metrics do not return for views on the backend, need to enable include_views param there
    @metrics = LinkedData::Client::Models::Metrics.all(include_views: true)
    @metrics.each {|m| metrics_hash[m.links['ontology']] = m }
    return metrics_hash
  end

  def get_ontology_submission_ready(ontology)
    # Get the latest 'ready' submission
    submission = ontology.explore.latest_submission({:include_status => 'ready'})
    # Fallback to the latest submission, even if it's not ready.
    submission = ontology.explore.latest_submission if submission.nil?
    return submission
  end

  def get_simplified_ontologies_hash()
    # Note the simplify_ontology_model will cache individual ontology data.
    simple_ontologies = {}
    begin
      ontology_models = LinkedData::Client::Models::Ontology.all({:include_views => true})
      ontology_models.each {|o| simple_ontologies[o.id] = simplify_ontology_model(o) }
    rescue Exception => e
      LOG.add :error, e.message
      return nil
    end
    return simple_ontologies
  end

  def get_ontology_details(ont_uri)
    # Note the simplify_ontology_model will cache individual ontology data.
    begin
      ont_model = LinkedData::Client::Models::Ontology.find(ont_uri)
      ont = simplify_ontology_model(ont_model)
    rescue Exception => e
      LOG.add :error, e.message
      return nil
    end
    return ont
  end

  def simplify_classes(classes)
    # Simplify the classes batch service data for the UI
    # It takes a list of class objects (hashes or models) and the
    # data structure returned is a hash of class hashes, which will
    # contain details for the ontology they belong to.  For example:
    #{
    # "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#C12439" => {
    #    :id => "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#C12439",
    #    :ui => "http://ncbo-stg-app-12.stanford.edu/ontologies/NCIT?p=classes&conceptid=http%3A%2F%2Fncicb.nci.nih.gov%2Fxml%2Fowl%2FEVS%2FThesaurus.owl%23C12439",
    #    :uri => "http://stagedata.bioontology.org/ontologies/NCIT/classes/http%3A%2F%2Fncicb.nci.nih.gov%2Fxml%2Fowl%2FEVS%2FThesaurus.owl%23C12439",
    #    :prefLabel => "Brain",
    #    :ontology => {
    #      :id => "http://stagedata.bioontology.org/ontologies/NCIT",
    #      :uri => "http://stagedata.bioontology.org/ontologies/NCIT",
    #      :acronym => "NCIT",
    #      :name => "National Cancer Institute Thesaurus",
    #      :ui => "http://ncbo-stg-app-12.stanford.edu/ontologies/NCIT"
    #    },
    #  },
    #}
    @ontologies_hash ||= get_simplified_ontologies_hash
    classes_hash = {}
    classes.each do |cls|
      c = simplify_class_model(cls)
      c[:ontology] = @ontologies_hash[ c[:ontology] ]
      classes_hash[c[:id]] = c
    end
    return classes_hash
  end

  def simplify_class_model(cls_model)
    # Simplify the class required required by the UI.
    # No modification of the class ontology here, see simplify_classes.
    # Default simple class model
    cls = { :id => nil, :ontology => nil, :prefLabel => nil, :uri => nil, :ui => nil, :obsolete => false }
    begin
      if cls_model.instance_of? Hash
        cls = {
            :id => cls_model['@id'],
            :ui =>  cls_model['links']['ui'],
            :uri => cls_model['links']['self'],  # different from id
            :ontology => cls_model['links']['ontology']
        }
        # Try to carry through a prefLabel and the obsolete attribute, if they exist.
        cls[:prefLabel] = cls_model['prefLabel']
        cls[:obsolete] = cls_model['obsolete'] || false
      else
        # try to work with a struct object or a LinkedData::Client::Models::Class
        # if not a struct, then: cls_model.instance_of? LinkedData::Client::Models::Class
        cls = {
            :id => cls_model.id,
            :ui =>  cls_model.links['ui'],
            :uri => cls_model.links['self'],  # different from id
            :ontology => cls_model.links['ontology'],
        }
        # Try to carry through a prefLabel and the obsolete attribute, if they exist.
        cls[:prefLabel] = cls_model.prefLabel if cls_model.respond_to?('prefLabel')
        cls[:obsolete] = cls_model.respond_to?('obsolete') && cls_model.obsolete || false
      end
    rescue Exception => e
      LOG.add :error, e.message
      LOG.add :error, "Failure to simplify class: #{cls}"
    end
    return cls
  end

  def simplify_ontology_model(ont_model)
    id = nil
    if ont_model.instance_of? Hash
      id = ont_model['@id']
    elsif ont_model.instance_of? LinkedData::Client::Models::Ontology
      id = ont_model.id
    end
    ont = Rails.cache.read(id)
    return ont unless ont.nil?
    # No cache or it has expired
    LOG.add :debug, "No cache or expired cache for ontology: #{id}"
    ont = {}
    ont[:id] = id
    ont[:uri] = id
    if ont_model.instance_of? Hash
      ont[:acronym] = ont_model['acronym']
      ont[:name] = ont_model['name']
      ont[:ui] = ont_model['links']['ui']
    else
      # try to work with a struct object or a LinkedData::Client::Models::Ontology
      # if not a struct, then: ont_model.instance_of? LinkedData::Client::Models::Ontology
      ont[:acronym] = ont_model.acronym
      ont[:name] = ont_model.name
      ont[:ui] = ont_model.links['ui']
    end
    # Only cache a complete representation of a simplified ontology
    if ont[:id].nil? || ont[:uri].nil? || ont[:acronym].nil? || ont[:name].nil? || ont[:ui].nil?
      raise "Incomplete simple ontology: #{id}, #{ont}"
    else
      Rails.cache.write(ont[:id], ont, expires_in: EXPIRY_ONTOLOGY_SIMPLIFIED)
    end
    return ont
  end

  def get_apikey()
    apikey = API_KEY
    if session[:user]
      apikey = session[:user].apikey
    end
    return apikey
  end

  def parse_json(uri)
    begin
      response = Net::HTTP.get(URI(uri), { 'Authorization' => "apikey token=#{get_apikey}" })
    rescue StandardError => e
      @retries ||= 0
      raise e unless @retries < RETRY_LIMIT

      @retries += 1
      retry
    end
    JSON.parse(response)
  end

  def get_batch_results(params)
    begin
      response = RestClient.post REST_URI_BATCH, params.to_json, :content_type => :json, :accept => :json, :authorization => "apikey token=#{get_apikey}"
    rescue Exception => error
      @retries ||= 0
      if @retries < 1  # retry once only
        @retries += 1
        retry
      else
        LOG.add :error, "\nERROR: batch POST, uri: #{REST_URI_BATCH}"
        LOG.add :error, "\nERROR: batch POST, params: #{params.to_json}"
        LOG.add :error, "\nERROR: batch POST, error response: #{error.response}"
        raise error
      end
    end
    response
  end

  # Get the latest manual mappings
  # All mapping classes are bidirectional.
  # Each class in the list maps to all other classes in the list.
  def get_recent_mappings
    recent_mappings = {
        :mappings => [],
        :classes => {}
    }
    begin
      recent_url = "#{REST_URI}/mappings/recent/"
      cached_mappings_key = recent_url
      cached_mappings = Rails.cache.read(cached_mappings_key)
      return cached_mappings unless (cached_mappings.nil? || cached_mappings.empty?)
      # No cache or it has expired
      class_details = {}
      mappings = LinkedData::Client::HTTP.get(recent_url, {size: 20, display: "prefLabel"})
      recent_mappings[:mappings] = mappings
      unless mappings.nil? || mappings.empty?
        # Only cache a successful retrieval
        Rails.cache.write(cached_mappings_key, recent_mappings, expires_in: EXPIRY_RECENT_MAPPINGS)
      end
    rescue Exception => e
      LOG.add :error, e.message
      # leave recent mappings empty.
    end
    return recent_mappings
  end

  def total_mapping_count
    total_count = 0
    
    begin
      stats = LinkedData::Client::HTTP.get("#{REST_URI}/mappings/statistics/ontologies")
      unless stats.blank?
        stats = stats.to_h.compact
        # Some of the mapping counts are erroneously stored as strings
        stats.transform_values!(&:to_i)
        total_count = stats.values.sum
      end
    rescue
      LOG.add :error, e.message
    end
    
    return total_count
  end

  def determine_layout
    if Rails.env.appliance?
      'appliance'
    else
      'ontology'
    end
  end

  def current_license
    @current_license = License.current_license.first
  end

  def init_trial_license
    unless $trial_license_initialized
      unless License.where(encrypted_key: 'trial').exists?
        License.create(encrypted_key: 'trial', created_at: Time.current)
      end
      $trial_license_initialized = true
    end
  end

end