module PandaPal class Session < PandaPalRecord belongs_to :panda_pal_organization, class_name: 'PandaPal::Organization', optional: true after_initialize do self.session_key ||= SecureRandom.urlsafe_base64(60) end def [](key) if self.class.column_names.include?(key.to_s) super else data[key] end end def []=(key, value) if self.class.column_names.include?(key.to_s) super else data[key] = value end end def cache(key, &blk) data[:cache] ||= {} data[:cache][key] ||= blk.call end def launch_params data[:launch_params] || {} end def lti_platform return nil unless data[:lti_platform].present? @lti_platform ||= Platform.from_serialized(data[:lti_platform]) end def lti_launch_placement launch_params['https://www.instructure.com/placement'] || launch_params[:launch_type] end def custom_lti_params @custom_lti_params ||= begin # LT 1.3 custom_params = launch_params["https://purl.imsglobal.org/spec/lti/claim/custom"] return custom_params if custom_params.present? # LTI 1.0/1.1 custom_params = {} launch_params.each do |k, v| next unless k.start_with?("custom_") custom_params[k[7..-1]] = v end custom_params.with_indifferent_access end end def get_lti_cust_param(key, default: :if_not_var) nkey = key.to_s.gsub(/^custom_/, '') default_value = ->() { PandaPal.lti_custom_params[nkey] || PandaPal.lti_custom_params["custom_#{nkey}"] } val = launch_params.dig("https://purl.imsglobal.org/spec/lti/claim/custom", nkey) || launch_params[nkey] || launch_params["custom_#{nkey}"] if default == :if_not_var if val.is_a?(String) && /\$[\.\w]+/.match?(val) && val == default_value[] return nil end elsif default && !val.present? return default_value[] elsif !default && val == default_value[] return nil end val end def canvas_role_labels labels = get_lti_cust_param('custom_canvas_role') labels.is_a?(String) ? labels.split(',') : [] end def canvas_account_role_labels(account = 'self') account = 'self' if account.to_s == "root" account = account.canvas_id if account.respond_to?(:canvas_id) if defined?(::Admin) && ::Admin < ::ActiveRecord::Base account = current_organization.canvas_account_id if account == 'self' adm_query = ::Admin.where(canvas_account_id: account, workflow_state: "active") adm_query.pluck(:role_name) else Rails.cache.fetch([self.class.name, "AccountAdminLinks", account, canvas_user_id], expires_in: 1.hour) do admin_entries = canvas_sync_client.account_admins(account, user_id: [canvas_user_id]) admin_entries = admin_entries.select{|ent| ent[:workflow_state] == 'active' } admin_entries.map{|ent| ent[:role] } end end end def lti_roles @lti_roles ||= RoleStore.new(launch_params["https://purl.imsglobal.org/spec/lti/claim/roles"] || launch_params['ext_roles'] || '') end def canvas_user_id get_lti_cust_param('canvas_user_id') end def user @user ||= ::User.find_by(canvas_id: canvas_user_id) if defined?(::User) && ::User < ::ActiveRecord::Base end def canvas_site_admin? lti_roles.system_roles.include?("sys_admin") end class RoleStore ContextTypeURN = 'urn:lti:context-type:ims/lis/'.freeze ROLE_TYPES = [ { type: "system", urn: "urn:lti:sysrole:ims/lis/", url: "http://purl.imsglobal.org/vocab/lis/v2/system/person#" }, { type: "institution", urn: "urn:lti:instrole:ims/lis/", url: "http://purl.imsglobal.org/vocab/lis/v2/institution/person#" }, { type: "context", urn: "urn:lti:role:ims/lis/", url: "http://purl.imsglobal.org/vocab/lis/v2/membership#" }, ] attr_accessor :roles, :context_types, :other_roles def initialize(roles = '') roles = roles.split(',') if roles.is_a?(String) @roles = roles || [] @context_types = [] @other_roles = [] @parsed_roles = HashWithIndifferentAccess.new map_roles end def system_roles; @parsed_roles[:system] || []; end def institution_roles; @parsed_roles[:institution] || []; end def context_roles; @parsed_roles[:context] || []; end def to_h { 'roles' => roles, 'context_type' => context_types, 'system_roles' => system_roles, 'institution_roles' => institution_roles, 'context_roles' => context_roles } end protected def map_roles roles.each do |role| if role.downcase.include?(ContextTypeURN) context_types << clean_role(ContextTypeURN, role) elsif (simple_role, type = match_role(role)).present? (@parsed_roles[type[:type]] ||= []) << simple_role else other_roles << role end end end def match_role(role) ROLE_TYPES.each do |rt| # LIS V1 if role.downcase.start_with?(rt[:urn]) return clean_role(ContextTypeURN, role), rt end # LIS V2 if role.downcase.start_with?(rt[:url]) return role.split('#')[1].underscore, rt end end nil end def clean_role(urn_prefix, role) role.gsub(/#{Regexp.quote(urn_prefix)}/i, '').gsub('/', '').underscore end end class DataSerializer def self.load(str) return {} unless str.present? begin parsed = JSON.parse(str) rescue JSON::ParserError parsed = yaml_load(str) end parsed.is_a?(Hash) ? HashWithIndifferentAccess.new(parsed) : parsed end def self.dump(obj) JSON.dump(obj) end private if YAML.respond_to?(:unsafe_load) def self.yaml_load(payload) YAML.unsafe_load(payload) end else def self.yaml_load(payload) YAML.load(payload) end end end serialize :data, DataSerializer end end