Source: src/js/collections/AccessPolicy.js

"use strict";

define(["jquery", "underscore", "backbone", "models/AccessRule"], function (
  $,
  _,
  Backbone,
  AccessRule,
) {
  /**
   * @class AccessPolicy
   * @classdesc An AccessPolicy collection is a collection of AccessRules that specify
   * the permissions set on a DataONEObject
   * @classcategory Collections
   * @extends Backbone.Collection
   */
  var AccessPolicy = Backbone.Collection.extend(
    /** @lends AccessPolicy.prototype */
    {
      model: AccessRule,

      /**
       * The DataONEObject that will be saved with this AccessPolicy
       * @type {DataONEObject}
       */
      dataONEObject: null,

      initialize: function () {
        //When a model triggers the event "removeMe", remove it from this collection
        this.on("removeMe", this.removeAccessRule);
      },

      /**
       * Parses the given access policy XML and creates AccessRule models for
       * each rule in the access policy XML. Adds these models to this collection.
       * @param {Element} The <accessPolicy> XML DOM that contains a set of
       *   access rules.
       */
      parse: function (accessPolicyXML) {
        var originalLength = this.length,
          newLength = 0;

        //Parse each "allow" access rule
        _.each(
          $(accessPolicyXML).children(),
          function (accessRuleXML, i) {
            var accessRuleModel;

            //Update the AccessRule models that already exist in the collection, first.
            // This is important to keep listeners thoughout the app intact.
            if (AccessRule.prototype.isPrototypeOf(this.models[i])) {
              accessRuleModel = this.models[i];
            }
            //Create new AccessRules for all others
            else {
              accessRuleModel = new AccessRule();
              this.add(accessRuleModel);
            }

            newLength++;

            //Reset all the values first
            accessRuleModel.set(accessRuleModel.defaults());
            //Parse the AccessRule model and update the model attributes
            accessRuleModel.set(accessRuleModel.parse(accessRuleXML));
            //Save a reference to the DataONEObbject
            accessRuleModel.set("dataONEObject", this.dataONEObject);
          },
          this,
        );

        //If there are more AccessRules in this collection than were in the
        // system metadata XML, then remove the extras
        if (originalLength > newLength) {
          for (var i = 0; i < originalLength - newLength; i++) {
            this.pop();
          }
        }
      },

      /**
       * Creates AccessRule member models from the `defaultAccessPolicy`
       * setting in the AppModel.
       */
      createDefaultPolicy: function () {
        //For each access policy in the AppModel, create an AccessRule model
        _.each(
          MetacatUI.appModel.get("defaultAccessPolicy"),
          function (accessRule) {
            accessRule.dataONEObject = this.dataONEObject;

            this.add(new AccessRule(accessRule));
          },
          this,
        );
      },

      /**
       * Copies all the AccessRules from the given AccessPolicy and replaces this AccessPolicy
       * @param {AccessPolicy} otherAccessPolicy
       * @fires Backbone.Collection#reset
       * @since 2.15.0
       */
      copyAccessPolicy: function (otherAccessPolicy) {
        try {
          let accessRules = [];

          //For each access policy in the AppModel, create an AccessRule model
          otherAccessPolicy.each(function (accessRule) {
            //Convert the AccessRule model to JSON and update the reference to the DataONEObject
            let accessRuleJSON = accessRule.toJSON();
            accessRuleJSON.dataONEObject = this.dataONEObject;
            accessRules.push(accessRuleJSON);
          }, this);

          //Reset the Collection with these AccessRules
          this.reset(accessRules);
        } catch (e) {
          console.error(e);
        }
      },

      /**
       * Creates an access policy XML from the values set on the member
       * AccessRule models.
       * @returns {object} A XML object of the access policy or null if empty
       */
      serialize: function () {
        if (this.length === 0) {
          return null;
        }

        // Create the access policy node which will contain all the rules
        var accessPolicyElement = document.createElement("accesspolicy");

        // Serialize each AccessRule member model and add to the policy DOM
        this.each(function (accessRule) {
          var accessRuleNode = accessRule.serialize();
          if (accessRuleNode) {
            accessPolicyElement.appendChild(accessRuleNode);
          }
        });

        return accessPolicyElement;
      },

      /**
       * Removes access rules that grant public access and sets an access rule
       * that denies public read.
       */
      makePrivate: function () {
        var alreadyPrivate = false;

        //Find the public access rules and remove them
        this.each(function (accessRule) {
          if (typeof accessRule === "undefined") return;

          //If the access rule subject is `public` and they are given any kind of access,
          if (
            accessRule.get("subject") == "public" &&
            (accessRule.get("read") ||
              accessRule.get("write") ||
              accessRule.get("changePermission"))
          ) {
            //Remove this AccessRule model from the collection
            this.remove(accessRule);
          }
        }, this);
      },

      /**
       * Removes any AccessRule that denies public read and adds an AccessRule
       * that allows public read
       */
      makePublic: function () {
        var alreadyPublic = false;

        //Find any public read rule and set read=true
        this.each(function (accessRule) {
          if (typeof accessRule === "undefined") return;

          //If the access rule subject is `public` and they are denied read access
          if (accessRule.get("subject") == "public") {
            //Remove this AccessRule model from the collection
            accessRule.set("read", true);
            alreadyPublic = true;
          }
        }, this);

        //If this policy does not already allow the public read access, then add that rule
        if (!alreadyPublic) {
          //Create an access rule that allows public read
          var publicAllow = new AccessRule({
            subject: "public",
            read: true,
            dataONEObject: this.dataONEObject,
          });
          //Add this access rule
          this.add(publicAllow);
        }
      },

      /**
       * Returns true if this access policy specifies that it is accessible to
       * the public in any way
       * @return {boolean}
       */
      isPublic: function () {
        var isPublic = false;

        this.each(function (accessRule) {
          if (
            accessRule.get("subject") == "public" &&
            (accessRule.get("read") ||
              accessRule.get("write") ||
              accessRule.get("changePermission"))
          ) {
            isPublic = true;
          }
        });

        return isPublic;
      },

      /**
       * Checks if the current user is authorized to perform the given action
       * based on the current access rules in this collection
       *
       * @param {string} action - The action to check authorization for. Can
       *   be either `read`, `write`, or `changePermission`
       * @return {boolean} - Returns true is the user can perform this action,
       *   false if not.
       */
      isAuthorized: function (action) {
        if (typeof action == "undefined" || !action) return false;

        //Get the access rules for the user's subject or groups
        var allSubjects = [];
        if (!MetacatUI.appUserModel.get("loggedIn")) allSubjects = "public";
        else {
          allSubjects = _.union(
            MetacatUI.appUserModel.get("identities"),
            _.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId"),
            [MetacatUI.appUserModel.get("username")],
          );
        }

        //Find the access rules that match the given action and user subjects
        var applicableRules = this.filter(function (accessRule) {
          if (
            accessRule.get(action) &&
            _.contains(allSubjects, accessRule.get("subject"))
          ) {
            return true;
          }
        }, this);

        if (applicableRules.length) return true;
        else if (
          _.contains(allSubjects, this.dataONEObject.get("rightsHolder"))
        )
          return true;
        else return false;
      },

      /**
       * Checks if the user is authorized to update the system metadata.
       * Updates to system metadata will fail if the user doesn't have changePermission permission,
       * *unless* the user is performing an update() at the same time and has `write` permission
       * @returns {boolean}
       * @since 2.15.0
       */
      isAuthorizedUpdateSysMeta: function () {
        try {
          //Yes, if the user has changePermission
          if (this.isAuthorized("changePermission")) {
            return true;
          }
          //Yes, if the user just uploaded this object and is saving it for the first time
          else if (this.isAuthorized("write") && this.dataONEObject.isNew()) {
            return true;
          } else {
            return false;
          }
        } catch (e) {
          console.error("Failed to determing authorization: ", e);
          return false;
        }
      },

      /**
       * Gets the subject info for all of the subjects in this access policy.
       * Sets the subject info on each corresponding model.
       */
      getSubjectInfo: function () {
        //If there are more than 5 subjects in the access policy, then get the entire list of subjects in the DataONE/CN system
        /*  if( this.length > 5 ){
              //TODO: Get everything from the /accounts endpoint
            }
            */
        //If there are less than 5, then send individual requests to get the subject info
        this.invoke("getSubjectInfo");
      },

      /**
       * Remove the given AccessRule from this AccessPolicy
       * @param {AccessRule} accessRule - The AccessRule model to remove
       */
      removeAccessRule: function (accessRule) {
        this.remove(accessRule);
      },

      /**
       * Checks if there is at least one AccessRule with changePermission permission
       * in this AccessPolicy.
       * @returns {boolean}
       */
      hasOwner: function () {
        try {
          var owners = this.where({ changePermission: true });

          //Check if there are any other subjects with ownership levels
          if (!owners || owners.length == 0) {
            //If there is a rightsHolder, that counts as an owner
            /*  if( this.dataONEObject && this.dataONEObject.get("rightsHolder") ){
                  return true;
                }
                */
            return false;
          } else {
            return true;
          }
        } catch (e) {
          console.error("Error getting the owners of this AccessPolicy: ", e);
        }
      },

      replaceRightsHolder: function () {
        var owner = this.findWhere({ changePermission: true });

        //Make sure the owner model was found
        if (!owner) {
          return;
        }

        //Set this other owner as the rightsHolder
        this.dataONEObject.set("rightsHolder", owner.get("subject"));

        //Remove them as an AccessRule in the AccessPolicy
        this.remove(owner);
      },
    },
  );

  return AccessPolicy;
});