<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

var gadgets = gadgets || {};

/**
 * @fileoverview Provides remote content retrieval facilities.
 *     Available to every gadget.
 */

/**
 * @static
 * @class Provides remote content retrieval functions.
 * @name gadgets.io
 */

gadgets.io = function() {
  /**
   * Holds configuration-related data such as proxy urls.
   */
  var config = {};

  /**
   * Holds state for OAuth.
   */
  var oauthState;

  /**
   * Internal facility to create an xhr request.
   */
  function makeXhr() {
    if (window.XMLHttpRequest) {
      return new XMLHttpRequest();
    } else if (window.ActiveXObject) {
      var x = new ActiveXObject("Msxml2.XMLHTTP");
      if (!x) {
        x = new ActiveXObject("Microsoft.XMLHTTP");
      }
      return x;
    }
  }

  /**
   * Checks the xobj for errors, may call the callback with an error response
   * if the error is fatal.
   *
   * @param {Object} xobj The XHR object to check
   * @param {Function} callback The callback to call if the error is fatal
   * @return true if the xobj is not ready to be processed
   */
  function hadError(xobj, callback) {
    if (xobj.readyState !== 4) {
      return true;
    }
    if (xobj.status !== 200) {
      // TODO Need to work on standardizing errors
      callback({errors : ["Error " + xobj.status]});
      return true;
    }
    return false;
  }

  /**
   * Handles non-proxied XHR callback processing.
   *
   * @param {String} url
   * @param {Function} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processNonProxiedResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var data = {
      body: xobj.responseText
    };
    callback(transformResponseData(params, data));
  }

  var UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";

  /**
   * Handles XHR callback processing.
   *
   * @param {String} url
   * @param {Function} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var txt = xobj.responseText;
    // remove unparseable cruft.
    // TODO: really remove this by eliminating it. It's not any real security
    //    to begin with, and we can solve this problem by using post requests
    //    and / or passing the url in the http headers.
    txt = txt.substr(UNPARSEABLE_CRUFT.length);
    // We are using eval directly here because the outer response comes from a
    // trusted source, and json parsing is slow in IE.
    var data = eval("(" + txt + ")");
    data = data[url];
    // Save off any transient OAuth state the server wants back later.
    if (data.oauthState) {
      oauthState = data.oauthState;
    }
    // Update the security token if the server sent us a new one
    if (data.st) {
      shindig.auth.updateSecurityToken(data.st);
    }
    callback(transformResponseData(params, data));
  }

  function transformResponseData(params, data) {
    var resp = {
     text: data.body,
     approvalUrl: data.approvalUrl,
     errors: []
    };
    if (resp.text) {
      switch (params.CONTENT_TYPE) {
        case "JSON":
        case "FEED":
          resp.data = gadgets.json.parse(resp.text);
          if (!resp.data) {
            resp.errors.push("failed to parse JSON");
            resp.data = null;
          }
          break;
        case "DOM":
          var dom;
          if (window.ActiveXObject) {
            dom = new ActiveXObject("Microsoft.XMLDOM");
            dom.async = false;
            dom.validateOnParse = false;
            dom.resolveExternals = false;
            if (!dom.loadXML(resp.text)) {
              resp.errors.push("failed to parse XML");
            } else {
              resp.data = dom;
            }
          } else {
            var parser = new DOMParser();
            dom = parser.parseFromString(resp.text, "text/xml");
            if ("parsererror" === dom.documentElement.nodeName) {
              resp.errors.push("failed to parse XML");
            } else {
              resp.data = dom;
            }
          }
          break;
        default:
          resp.data = resp.text;
          break;
      }
	}
    return resp;
  }

  /**
   * Sends an XHR post or get request
   *
   * @param realUrl The url to fetch data from that was requested by the gadget
   * @param proxyUrl The url to proxy through
   * @param callback The function to call once the data is fetched
   * @param postData The data to post to the proxyUrl
   * @param params The params to use when processing the response
   * @param processResponseFunction The function that should process the
   *     response from the sever before calling the callback
   */
  function makeXhrRequest(realUrl, proxyUrl, callback, paramData, method,
      params, processResponseFunction) {
    var xhr = makeXhr();

    xhr.open(method, proxyUrl, true);
    if (callback) {
      xhr.onreadystatechange = gadgets.util.makeClosure(
          null, processResponseFunction, realUrl, callback, params, xhr);
    }
    if (paramData != null) {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.send(paramData);
    } else {
        xhr.send(null);
    }
  }

  /**
   * Satisfy a request with data that is prefetched as per the gadget Preload
   * directive. The preloader will only satisfy a request for a specific piece
   * of content once.
   *
   * @param postData The definition of the request to be executed by the proxy
   * @param params The params to use when processing the response
   * @param callback The function to call once the data is fetched
   * @return true if the request can be satisfied by the preloaded content
   *         false otherwise
   */
  function respondWithPreload(postData, params, callback) {
    if (gadgets.io.preloaded_ && gadgets.io.preloaded_[postData.url]) {
      var preload = gadgets.io.preloaded_[postData.url];
      if (postData.httpMethod == "GET") {
        delete gadgets.io.preloaded_[postData.url];
        if (preload.rc !== 200) {
          callback({errors : ["Error " + preload.rc]});
        } else {
          callback(transformResponseData(params, { body: preload.body }));
        }
        return true;
      }
    }
    return false;
  }

  /**
   * @param {Object} configuration Configuration settings
   * @private
   */
  function init (configuration) {
    config = configuration["core.io"];
  }

  var requiredConfig = {
    proxyUrl: new gadgets.config.RegExValidator(/.*%(raw)?url%.*/),
    jsonProxyUrl: gadgets.config.NonEmptyStringValidator
  };
  gadgets.config.register("core.io", requiredConfig, init);

  return /** @scope gadgets.io */ {
    /**
     * Fetches content from the provided URL and feeds that content into the
     * callback function.
     *
     * Example:
     *

     * gadgets.io.makeRequest(url, fn,
     *    {contentType: gadgets.io.ContentType.FEED});
     *

     *
     * @param {String} url The URL where the content is located
     * @param {Function} callback The function to call with the data from the
     *     URL once it is fetched
     * @param {Map.<gadgets.io.RequestParameters, Object>} opt_params
     *     Additional
     *     parameters
     *     to pass to the request
     *
     * @member gadgets.io
     */
    makeRequest : function (url, callback, opt_params) {
      // TODO: This method also needs to respect all members of
      // gadgets.io.RequestParameters, and validate them.

      var params = opt_params || {};

      // Check if authorization is requested
      var auth, st;
      var reqState, oauthService, oauthToken;
      if (params.AUTHORIZATION && params.AUTHORIZATION !== "NONE") {
        auth = params.AUTHORIZATION.toLowerCase();
        st = shindig.auth.getSecurityToken();
        if (params.AUTHORIZATION === "AUTHENTICATED") {
          reqState = oauthState;
          oauthService = params.OAUTH_SERVICE;
          oauthToken = params.OAUTH_TOKEN;
        }
      } else {
        // Non auth'd & non post'd requests are cachable
        if (!params.REFRESH_INTERVAL && !params.POST_DATA) {
          params.REFRESH_INTERVAL = 3600;
         }
      }
      var signOwner = params.OWNER_SIGNED;
      var signViewer = params.VIEWER_SIGNED;

      var headers = params.HEADERS || {};
      if (params.METHOD === "POST" && !headers["Content-Type"]) {
        headers["Content-Type"] = "application/x-www-form-urlencoded";
      }

      var paramData = {
        url: url,
        httpMethod : params.METHOD || "GET",
        headers: gadgets.io.encodeValues(headers, false),
        postData : params.POST_DATA || "",
        authz : auth || "",
        st : st || "",
        oauthState : reqState || "",
        oauthService : oauthService || "",
        oauthToken : oauthToken || "",
        contentType : params.CONTENT_TYPE || "TEXT",
        numEntries : params.NUM_ENTRIES || "3",
        getSummaries : !!params.GET_SUMMARIES,
        signOwner : signOwner || "true",
        signViewer : signViewer || "true",
        gadget : gadgets.util.getUrlParameters()["url"]
      };

      if (!respondWithPreload(paramData, params, callback, processResponse)) {
        var refreshInterval = params.REFRESH_INTERVAL || 0;

        if (refreshInterval > 0) {
          // this content should be cached
          // Add paramData to the URL
          var extraparams = "&refresh=" + refreshInterval + '&'
              + gadgets.io.encodeValues(paramData);

          makeXhrRequest(url, config.jsonProxyUrl + extraparams, callback,
              null, "GET", params, processResponse);

        } else {
          makeXhrRequest(url, config.jsonProxyUrl, callback,
              gadgets.io.encodeValues(paramData), "POST", params,
              processResponse);
        }
      }
    },

    /**
     * @private
     */
    makeNonProxiedRequest : function (relativeUrl, callback, opt_params) {
      var params = opt_params || {};
      makeXhrRequest(relativeUrl, relativeUrl, callback, params.POST_DATA,
          params.METHOD, params, processNonProxiedResponse);
    },

    /**
     * Converts an input object into a URL-encoded data string.
     * (key=value&...)
     *
     * @param {Object} fields The post fields you wish to encode
     * @param {Boolean} opt_noEscaping An optional parameter specifying whether
     *     to turn off escaping of the parameters. Defaults to false.
     * @return {String} The processed post data in www-form-urlencoded format.
     *
     * @member gadgets.io
     */
    encodeValues : function (fields, opt_noEscaping) {
      var escape = !opt_noEscaping;

      var buf = [];
      var first = false;
      for (var i in fields) if (fields.hasOwnProperty(i)) {
        if (!first) {
          first = true;
        } else {
          buf.push("&");
        }
        buf.push(escape ? encodeURIComponent(i) : i);
        buf.push("=");
        buf.push(escape ? encodeURIComponent(fields[i]) : fields[i]);
      }
      return buf.join("");
    },

    /**
     * Gets the proxy version of the passed-in URL.
     *
     * @param {String} url The URL to get the proxy URL for
     * @param {Object} opt_params Optional Parameter Object.
     *     The following properties are supported:
     *       .REFRESH_INTERVAL The number of seconds that this
     *           content should be cached.  Defaults to 3600.
     *
     * @return {String} The proxied version of the URL
     *
     * @member gadgets.io
     */
    getProxyUrl : function (url, opt_params) {
      var params = opt_params || {};
      var refresh = params['REFRESH_INTERVAL'] || '3600';

      return config.proxyUrl.replace("%url%", encodeURIComponent(url)).
          replace("%rawurl%", url).
          replace("%refresh%", encodeURIComponent(refresh));
    }
  };
}();

gadgets.io.RequestParameters = gadgets.util.makeEnum([
  "METHOD",
  "CONTENT_TYPE",
  "POST_DATA",
  "HEADERS",
  "AUTHORIZATION",
  "NUM_ENTRIES",
  "GET_SUMMARIES",
  "REFRESH_INTERVAL",
  "OAUTH_SERVICE",
  "OAUTH_TOKEN"
]);

// PUT, DELETE, and HEAD not supported currently.
gadgets.io.MethodType = gadgets.util.makeEnum([
  "GET", "POST", "PUT", "DELETE", "HEAD"
]);

gadgets.io.ContentType = gadgets.util.makeEnum([
  "TEXT", "DOM", "JSON", "FEED"
]);

gadgets.io.AuthorizationType = gadgets.util.makeEnum([
  "NONE", "SIGNED", "AUTHENTICATED"
]);
<commit_msg>Fix for query string when makeRequest is performed using GET.<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

var gadgets = gadgets || {};

/**
 * @fileoverview Provides remote content retrieval facilities.
 *     Available to every gadget.
 */

/**
 * @static
 * @class Provides remote content retrieval functions.
 * @name gadgets.io
 */

gadgets.io = function() {
  /**
   * Holds configuration-related data such as proxy urls.
   */
  var config = {};

  /**
   * Holds state for OAuth.
   */
  var oauthState;

  /**
   * Internal facility to create an xhr request.
   */
  function makeXhr() {
    if (window.XMLHttpRequest) {
      return new XMLHttpRequest();
    } else if (window.ActiveXObject) {
      var x = new ActiveXObject("Msxml2.XMLHTTP");
      if (!x) {
        x = new ActiveXObject("Microsoft.XMLHTTP");
      }
      return x;
    }
  }

  /**
   * Checks the xobj for errors, may call the callback with an error response
   * if the error is fatal.
   *
   * @param {Object} xobj The XHR object to check
   * @param {Function} callback The callback to call if the error is fatal
   * @return true if the xobj is not ready to be processed
   */
  function hadError(xobj, callback) {
    if (xobj.readyState !== 4) {
      return true;
    }
    if (xobj.status !== 200) {
      // TODO Need to work on standardizing errors
      callback({errors : ["Error " + xobj.status]});
      return true;
    }
    return false;
  }

  /**
   * Handles non-proxied XHR callback processing.
   *
   * @param {String} url
   * @param {Function} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processNonProxiedResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var data = {
      body: xobj.responseText
    };
    callback(transformResponseData(params, data));
  }

  var UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";

  /**
   * Handles XHR callback processing.
   *
   * @param {String} url
   * @param {Function} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var txt = xobj.responseText;
    // remove unparseable cruft.
    // TODO: really remove this by eliminating it. It's not any real security
    //    to begin with, and we can solve this problem by using post requests
    //    and / or passing the url in the http headers.
    txt = txt.substr(UNPARSEABLE_CRUFT.length);
    // We are using eval directly here because the outer response comes from a
    // trusted source, and json parsing is slow in IE.
    var data = eval("(" + txt + ")");
    data = data[url];
    // Save off any transient OAuth state the server wants back later.
    if (data.oauthState) {
      oauthState = data.oauthState;
    }
    // Update the security token if the server sent us a new one
    if (data.st) {
      shindig.auth.updateSecurityToken(data.st);
    }
    callback(transformResponseData(params, data));
  }

  function transformResponseData(params, data) {
    var resp = {
     text: data.body,
     approvalUrl: data.approvalUrl,
     errors: []
    };
    if (resp.text) {
      switch (params.CONTENT_TYPE) {
        case "JSON":
        case "FEED":
          resp.data = gadgets.json.parse(resp.text);
          if (!resp.data) {
            resp.errors.push("failed to parse JSON");
            resp.data = null;
          }
          break;
        case "DOM":
          var dom;
          if (window.ActiveXObject) {
            dom = new ActiveXObject("Microsoft.XMLDOM");
            dom.async = false;
            dom.validateOnParse = false;
            dom.resolveExternals = false;
            if (!dom.loadXML(resp.text)) {
              resp.errors.push("failed to parse XML");
            } else {
              resp.data = dom;
            }
          } else {
            var parser = new DOMParser();
            dom = parser.parseFromString(resp.text, "text/xml");
            if ("parsererror" === dom.documentElement.nodeName) {
              resp.errors.push("failed to parse XML");
            } else {
              resp.data = dom;
            }
          }
          break;
        default:
          resp.data = resp.text;
          break;
      }
	}
    return resp;
  }

  /**
   * Sends an XHR post or get request
   *
   * @param realUrl The url to fetch data from that was requested by the gadget
   * @param proxyUrl The url to proxy through
   * @param callback The function to call once the data is fetched
   * @param postData The data to post to the proxyUrl
   * @param params The params to use when processing the response
   * @param processResponseFunction The function that should process the
   *     response from the sever before calling the callback
   */
  function makeXhrRequest(realUrl, proxyUrl, callback, paramData, method,
      params, processResponseFunction) {
    var xhr = makeXhr();

    xhr.open(method, proxyUrl, true);
    if (callback) {
      xhr.onreadystatechange = gadgets.util.makeClosure(
          null, processResponseFunction, realUrl, callback, params, xhr);
    }
    if (paramData != null) {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xhr.send(paramData);
    } else {
        xhr.send(null);
    }
  }

  /**
   * Satisfy a request with data that is prefetched as per the gadget Preload
   * directive. The preloader will only satisfy a request for a specific piece
   * of content once.
   *
   * @param postData The definition of the request to be executed by the proxy
   * @param params The params to use when processing the response
   * @param callback The function to call once the data is fetched
   * @return true if the request can be satisfied by the preloaded content
   *         false otherwise
   */
  function respondWithPreload(postData, params, callback) {
    if (gadgets.io.preloaded_ && gadgets.io.preloaded_[postData.url]) {
      var preload = gadgets.io.preloaded_[postData.url];
      if (postData.httpMethod == "GET") {
        delete gadgets.io.preloaded_[postData.url];
        if (preload.rc !== 200) {
          callback({errors : ["Error " + preload.rc]});
        } else {
          callback(transformResponseData(params, { body: preload.body }));
        }
        return true;
      }
    }
    return false;
  }

  /**
   * @param {Object} configuration Configuration settings
   * @private
   */
  function init (configuration) {
    config = configuration["core.io"];
  }

  var requiredConfig = {
    proxyUrl: new gadgets.config.RegExValidator(/.*%(raw)?url%.*/),
    jsonProxyUrl: gadgets.config.NonEmptyStringValidator
  };
  gadgets.config.register("core.io", requiredConfig, init);

  return /** @scope gadgets.io */ {
    /**
     * Fetches content from the provided URL and feeds that content into the
     * callback function.
     *
     * Example:
     *

     * gadgets.io.makeRequest(url, fn,
     *    {contentType: gadgets.io.ContentType.FEED});
     *

     *
     * @param {String} url The URL where the content is located
     * @param {Function} callback The function to call with the data from the
     *     URL once it is fetched
     * @param {Map.<gadgets.io.RequestParameters, Object>} opt_params
     *     Additional
     *     parameters
     *     to pass to the request
     *
     * @member gadgets.io
     */
    makeRequest : function (url, callback, opt_params) {
      // TODO: This method also needs to respect all members of
      // gadgets.io.RequestParameters, and validate them.

      var params = opt_params || {};

      // Check if authorization is requested
      var auth, st;
      var reqState, oauthService, oauthToken;
      if (params.AUTHORIZATION && params.AUTHORIZATION !== "NONE") {
        auth = params.AUTHORIZATION.toLowerCase();
        st = shindig.auth.getSecurityToken();
        if (params.AUTHORIZATION === "AUTHENTICATED") {
          reqState = oauthState;
          oauthService = params.OAUTH_SERVICE;
          oauthToken = params.OAUTH_TOKEN;
        }
      } else {
        // Non auth'd & non post'd requests are cachable
        if (!params.REFRESH_INTERVAL && !params.POST_DATA) {
          params.REFRESH_INTERVAL = 3600;
         }
      }
      var signOwner = params.OWNER_SIGNED;
      var signViewer = params.VIEWER_SIGNED;

      var headers = params.HEADERS || {};
      if (params.METHOD === "POST" && !headers["Content-Type"]) {
        headers["Content-Type"] = "application/x-www-form-urlencoded";
      }

      var paramData = {
        url: url,
        httpMethod : params.METHOD || "GET",
        headers: gadgets.io.encodeValues(headers, false),
        postData : params.POST_DATA || "",
        authz : auth || "",
        st : st || "",
        oauthState : reqState || "",
        oauthService : oauthService || "",
        oauthToken : oauthToken || "",
        contentType : params.CONTENT_TYPE || "TEXT",
        numEntries : params.NUM_ENTRIES || "3",
        getSummaries : !!params.GET_SUMMARIES,
        signOwner : signOwner || "true",
        signViewer : signViewer || "true",
        gadget : gadgets.util.getUrlParameters()["url"]
      };

      if (!respondWithPreload(paramData, params, callback, processResponse)) {
        var refreshInterval = params.REFRESH_INTERVAL || 0;

        if (refreshInterval > 0) {
          // this content should be cached
          // Add paramData to the URL
          var extraparams = "?refresh=" + refreshInterval + '&'
              + gadgets.io.encodeValues(paramData);

          makeXhrRequest(url, config.jsonProxyUrl + extraparams, callback,
              null, "GET", params, processResponse);

        } else {
          makeXhrRequest(url, config.jsonProxyUrl, callback,
              gadgets.io.encodeValues(paramData), "POST", params,
              processResponse);
        }
      }
    },

    /**
     * @private
     */
    makeNonProxiedRequest : function (relativeUrl, callback, opt_params) {
      var params = opt_params || {};
      makeXhrRequest(relativeUrl, relativeUrl, callback, params.POST_DATA,
          params.METHOD, params, processNonProxiedResponse);
    },

    /**
     * Converts an input object into a URL-encoded data string.
     * (key=value&...)
     *
     * @param {Object} fields The post fields you wish to encode
     * @param {Boolean} opt_noEscaping An optional parameter specifying whether
     *     to turn off escaping of the parameters. Defaults to false.
     * @return {String} The processed post data in www-form-urlencoded format.
     *
     * @member gadgets.io
     */
    encodeValues : function (fields, opt_noEscaping) {
      var escape = !opt_noEscaping;

      var buf = [];
      var first = false;
      for (var i in fields) if (fields.hasOwnProperty(i)) {
        if (!first) {
          first = true;
        } else {
          buf.push("&");
        }
        buf.push(escape ? encodeURIComponent(i) : i);
        buf.push("=");
        buf.push(escape ? encodeURIComponent(fields[i]) : fields[i]);
      }
      return buf.join("");
    },

    /**
     * Gets the proxy version of the passed-in URL.
     *
     * @param {String} url The URL to get the proxy URL for
     * @param {Object} opt_params Optional Parameter Object.
     *     The following properties are supported:
     *       .REFRESH_INTERVAL The number of seconds that this
     *           content should be cached.  Defaults to 3600.
     *
     * @return {String} The proxied version of the URL
     *
     * @member gadgets.io
     */
    getProxyUrl : function (url, opt_params) {
      var params = opt_params || {};
      var refresh = params['REFRESH_INTERVAL'] || '3600';

      return config.proxyUrl.replace("%url%", encodeURIComponent(url)).
          replace("%rawurl%", url).
          replace("%refresh%", encodeURIComponent(refresh));
    }
  };
}();

gadgets.io.RequestParameters = gadgets.util.makeEnum([
  "METHOD",
  "CONTENT_TYPE",
  "POST_DATA",
  "HEADERS",
  "AUTHORIZATION",
  "NUM_ENTRIES",
  "GET_SUMMARIES",
  "REFRESH_INTERVAL",
  "OAUTH_SERVICE",
  "OAUTH_TOKEN"
]);

// PUT, DELETE, and HEAD not supported currently.
gadgets.io.MethodType = gadgets.util.makeEnum([
  "GET", "POST", "PUT", "DELETE", "HEAD"
]);

gadgets.io.ContentType = gadgets.util.makeEnum([
  "TEXT", "DOM", "JSON", "FEED"
]);

gadgets.io.AuthorizationType = gadgets.util.makeEnum([
  "NONE", "SIGNED", "AUTHENTICATED"
]);
<|endoftext|>


<commit_before>import P from 'bluebird'
import find from 'lodash.find'
import get from 'lodash.get'
import * as util from 'src/util'

import { firebase, geofire } from 'src/firebase'

import {
  registerGeoquery,
  removeBeacon,
  foundBeacon,
  setConnecting,
} from '../index'

import {
  geolocationSuccess,
  geolocationError,
  geolocationDenied,
} from '../../location'

import * as notificationActions from 'src/core/notifications'

// The main exported function for
export default function beginConnectingAsync() {
  return (dispatch, getState) => {
    dispatch(setConnecting(true))

    return getGeolocation()
      .catch(err => dispatch(handleGeolocationErrorAsync(err)))
      .then(location => {
        if (!location) {
          return
        }

        const { connect } = getState()

        // Check to ensure the connect action has not been canceled
        if (!connect.isConnecting) {
          return
        }

        const { coords } = location
        dispatch(geolocationSuccess(coords))

        return P.props({
          createOwnBeacon: dispatch(createOwnBeaconAsync(location)),
          findBeacons: dispatch(findBeaconsAsync(location)),
        })
      })
  }
}

function getGeolocation() {
  if (navigator && navigator.geolocation) {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject)
    })
  } else {
    return Promise.reject()
  }
}

function handleGeolocationErrorAsync(err) {
  return (dispatch) => {
    dispatch(notificationActions.addNotification({
      message: 'You must enable geolocation to use the GLARE app',
      kind: 'danger',
      dismissAfter: 5000,
    }))

    if (err.code === 1 || err.message === 'User denied Geolocation') {
      dispatch(geolocationDenied())
    } else {
      dispatch(geolocationError(err))
    }
  }
}

function createOwnBeaconAsync({ coords, timestamp }) {
  const { latitude, longitude } = coords
  return (dispatch, getState) => {
    const { auth, connect } = getState()

    // Check to ensure the connect action has not been canceled
    if (!connect.isConnecting) {
      return
    }

    return P.props({
      // Set the beacon location and the corresponding listener for removing on disconnect
      beaconlocation: geofire.beaconLocations.set(auth.id, [latitude, longitude])
        .then(() => firebase.database().ref().child(`beaconLocations/${auth.id}`).onDisconnect().remove()),

      // Set the beacon data and the corresponding listener for removing on disconnect
      beacon: firebase.database().ref().child(`beacons/${auth.id}`)
        .set({ latitude, longitude, timestamp })
        .then(() => firebase.database().ref().child(`beacons/${auth.id}`).onDisconnect().remove())
    })
  }
}

function findBeaconsAsync({ coords }) {
  const { latitude, longitude } = coords
  return (dispatch, getState) => {
    const { connect } = getState()

    // Check to ensure the connect action has not been canceled
    if (!connect.isConnecting) {
      return
    }

    const geoquery = geofire.beaco
    nLocations.query({
      center: [latitude, longitude],
      radius: 300,
    })

    // Store the geoquery object so that we can cancel it on connect success or cancel
    dispatch(registerGeoquery(geoquery))

    // Register event listener for finding a beacon by the geoquery
    geoquery.on('key_entered', (key) => {
      const { auth, user } = getState()
      const isConnected = get(user, ['connections', key])

      // Ignore the user's beacon and any beacons of users the user is already
      // connected to
      if (key === auth.id || isConnected) {
        return
      }

      // Get the user record corresponding to the geokey
      firebase.database().ref().child(`users/${key}`).once('value', snapshot => {
        const otherUser = util.recordFromSnapshot(snapshot)
        const { user, connect } = getState()

        // Ignore found beacons that are already in the list
        const beaconUserIds = connect.beacons.map(user => user.id)
        const existsInBeacons = find(beaconUserIds, otherUser.id)
        // Ignore found beacons that the user is already connected to
        const existsInConnections = get(user, ['connections', otherUser.id])
        if (existsInBeacons || existsInConnections) {
          return
        }

        // If the current user has access, add this beacon to the list of available
        // users to connect with only if the other user does not
        if (user.hasAccess && !otherUser.hasAccess) {
          return dispatch(foundBeacon(otherUser))
        }

        // Otherwise, only add the beacon to the list if the other user has access
        if (!user.hasAccess && otherUser.hasAccess) {
          return dispatch(foundBeacon(otherUser))
        }
      })
    })

    // Register event listeners for the beacon being removed
    geoquery.on('key_exited', (key) => {
      const { auth } = getState()
      // Ignore the removed beacon if it's the users (this is handled in app)
      if (key === auth.id) {
        return
      }

      // Remove the beacon from the list
      dispatch(removeBeacon(key))
    })
  }
}
<commit_msg>Fix dummy<commit_after>import P from 'bluebird'
import find from 'lodash.find'
import get from 'lodash.get'
import * as util from 'src/util'

import { firebase, geofire } from 'src/firebase'

import {
  registerGeoquery,
  removeBeacon,
  foundBeacon,
  setConnecting,
} from '../index'

import {
  geolocationSuccess,
  geolocationError,
  geolocationDenied,
} from '../../location'

import * as notificationActions from 'src/core/notifications'

// The main exported function for
export default function beginConnectingAsync() {
  return (dispatch, getState) => {
    dispatch(setConnecting(true))

    return getGeolocation()
      .catch(err => dispatch(handleGeolocationErrorAsync(err)))
      .then(location => {
        if (!location) {
          return
        }

        const { connect } = getState()

        // Check to ensure the connect action has not been canceled
        if (!connect.isConnecting) {
          return
        }

        const { coords } = location
        dispatch(geolocationSuccess(coords))

        return P.props({
          createOwnBeacon: dispatch(createOwnBeaconAsync(location)),
          findBeacons: dispatch(findBeaconsAsync(location)),
        })
      })
  }
}

function getGeolocation() {
  if (navigator && navigator.geolocation) {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject)
    })
  } else {
    return Promise.reject()
  }
}

function handleGeolocationErrorAsync(err) {
  return (dispatch) => {
    dispatch(notificationActions.addNotification({
      message: 'You must enable geolocation to use the GLARE app',
      kind: 'danger',
      dismissAfter: 5000,
    }))

    if (err.code === 1 || err.message === 'User denied Geolocation') {
      dispatch(geolocationDenied())
    } else {
      dispatch(geolocationError(err))
    }
  }
}

function createOwnBeaconAsync({ coords, timestamp }) {
  const { latitude, longitude } = coords
  return (dispatch, getState) => {
    const { auth, connect } = getState()

    // Check to ensure the connect action has not been canceled
    if (!connect.isConnecting) {
      return
    }

    return P.props({
      // Set the beacon location and the corresponding listener for removing on disconnect
      beaconlocation: geofire.beaconLocations.set(auth.id, [latitude, longitude])
        .then(() => firebase.database().ref().child(`beaconLocations/${auth.id}`).onDisconnect().remove()),

      // Set the beacon data and the corresponding listener for removing on disconnect
      beacon: firebase.database().ref().child(`beacons/${auth.id}`)
        .set({ latitude, longitude, timestamp })
        .then(() => firebase.database().ref().child(`beacons/${auth.id}`).onDisconnect().remove())
    })
  }
}

function findBeaconsAsync({ coords }) {
  const { latitude, longitude } = coords
  return (dispatch, getState) => {
    const { connect } = getState()

    // Check to ensure the connect action has not been canceled
    if (!connect.isConnecting) {
      return
    }

    const geoquery = geofire.beaconLocations.query({
      center: [latitude, longitude],
      radius: 300,
    })

    // Store the geoquery object so that we can cancel it on connect success or cancel
    dispatch(registerGeoquery(geoquery))

    // Register event listener for finding a beacon by the geoquery
    geoquery.on('key_entered', (key) => {
      const { auth, user } = getState()
      const isConnected = get(user, ['connections', key])

      // Ignore the user's beacon and any beacons of users the user is already
      // connected to
      if (key === auth.id || isConnected) {
        return
      }

      // Get the user record corresponding to the geokey
      firebase.database().ref().child(`users/${key}`).once('value', snapshot => {
        const otherUser = util.recordFromSnapshot(snapshot)
        const { user, connect } = getState()

        // Ignore found beacons that are already in the list
        const beaconUserIds = connect.beacons.map(user => user.id)
        const existsInBeacons = find(beaconUserIds, otherUser.id)
        // Ignore found beacons that the user is already connected to
        const existsInConnections = get(user, ['connections', otherUser.id])
        if (existsInBeacons || existsInConnections) {
          return
        }

        // If the current user has access, add this beacon to the list of available
        // users to connect with only if the other user does not
        if (user.hasAccess && !otherUser.hasAccess) {
          return dispatch(foundBeacon(otherUser))
        }

        // Otherwise, only add the beacon to the list if the other user has access
        if (!user.hasAccess && otherUser.hasAccess) {
          return dispatch(foundBeacon(otherUser))
        }
      })
    })

    // Register event listeners for the beacon being removed
    geoquery.on('key_exited', (key) => {
      const { auth } = getState()
      // Ignore the removed beacon if it's the users (this is handled in app)
      if (key === auth.id) {
        return
      }

      // Remove the beacon from the list
      dispatch(removeBeacon(key))
    })
  }
}
<|endoftext|>


<commit_before>/**
 * Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You
 * may not use this file except in compliance with the License. A copy of
 * the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file 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.
 */

var AWS = require('./core');
var inherit = AWS.util.inherit;

AWS.Endpoint = inherit({

  constructor: function Endpoint(endpoint, config) {
    if (!endpoint.match(/^http/)) {
      var useSSL = config && config.sslEnabled !== undefined ?
        config.sslEnabled : AWS.config.sslEnabled;
      endpoint = (useSSL ? 'https' : 'http') + '://' + endpoint;
    }

    AWS.util.update(this, AWS.util.urlParse(endpoint));

    // Ensure the port property is set as an integer
    if (this.port) {
      this.port = parseInt(this.port, 10);
    } else {
      this.port = this.protocol === 'https:' ? 443 : 80;
    }
  }

});

AWS.HttpRequest = inherit({

  constructor: function HttpRequest() {
    this.method = 'POST';
    this.path = '/';
    this.headers = {};
    this.headers['User-Agent'] = AWS.util.userAgent();
    this.body = undefined;
    this.endpoint = undefined;
    this.region = undefined;
  },

  pathname: function pathname() {
    return this.path.split('?', 1)[0];
  },

  search: function search() {
    return this.path.split('?', 2)[1] || '';
  }

});

AWS.HttpResponse = inherit({
  constructor: function HttpResponse() {
    this.statusCode = null;
    this.headers = {};
    this.body = null;
  }
});

/**
 * @api private
 */
AWS.RequestHandler = inherit({

  constructor: function RequestHandler(awsRequest) {
    this.awsRequest = awsRequest;
    this.awsResponse = awsRequest.awsResponse;
    this.client = this.awsResponse.client;
    this.method = this.awsResponse.method;
    this.params = this.awsResponse.params;
  },

  makeRequest: function makeRequest() {
    var httpRequest = this.client.buildRequest(this.method, this.params);
    httpRequest.sign();
    this.sendRequest(httpRequest);
  },

  sendRequest: function sendRequest(httpRequest) {

    var httpResponse = new AWS.HttpResponse();

    this.awsResponse.httpResponse = httpResponse;
    this.awsResponse.httpRequest = httpRequest;

    var self = this;
    AWS.HttpClient.getInstance().handleRequest(httpRequest, {

      onHeaders: function onHeaders(statusCode, headers) {
        httpResponse.statusCode = statusCode;
        httpResponse.headers = headers;
      },

      onData: function onData(data) {
        if (httpResponse.body === null) {
          httpResponse.body = data;
        } else {
          httpResponse.body += data;
        }
        self.handleHttpData(httpResponse);
      },

      onEnd: function onEnd() {

        if (httpResponse.body) {
          httpResponse.body = httpResponse.body.toString();
        }

        if (httpResponse.statusCode == 307) {
          self.redirect(httpRequest, httpResponse);
        } else {
          self.handleHttpResponse(httpResponse);
        }

      },

      onError: function onError(error) {
        self.retryRequest(error);
      }

    });

  },

  handleHttpData: function handleHttpData(httpResponse) {
    if (this.awsRequest.callbacks.data.length === 0) return;
    this.awsRequest.notifyData(httpResponse.body);
    httpResponse.body = null;
  },

  // Called when the http client returns a response.  Successfull http requests
  // may still contain an error (e.g. 400, 500, etc)
  redirect: function redirect(httpRequest, httpResponse) {
    /*jshint sub:true */
    httpRequest.endpoint = new AWS.Endpoint(httpResponse.headers['location']);
    this.sendRequest(httpRequest);
  },

  handleHttpResponse: function handleHttpResponse(httpResponse) {
    var self = this;
    this.client.parseResponse(httpResponse, this.method, function (error, data) {
      if (error)
        self.handleError(error);
      else
        self.awsRequest.notifyDone(data);
    });
  },

  handleError: function handleError(error) {
    if (error.retryable) {
      this.retryRequest(error);
    } else {
      this.awsRequest.notifyFail(error);
    }
  },

  retryRequest: function retryRequest(error) {

    var delays = this.client.retryDelays();
    var delay = delays[this.awsResponse.retryCount];

    this.awsResponse.retryCount += 1;

    if (delay !== undefined) {

      var self = this;
      setTimeout(function delayRetry() {
        self.makeRequest();
      }, delay);

    } else {

      this.awsRequest.notifyFail(error); // retried too many times

    }
  }
});

/**
 * @api private
 */
AWS.NodeHttpClient = inherit({

  constructor: function NodeHttpClient() {},

  handleRequest: function handleRequest(request, callbacks) {
    var options = {
      host: request.endpoint.hostname,
      port: request.endpoint.port,
      method: request.method,
      headers: request.headers,
      path: request.path
    };
    var useSSL = request.endpoint.protocol === 'https:';
    var client = useSSL ? require('https') : require('http');

    if (useSSL) {
      options.rejectUnauthorized = true;
      options.cert = AWS.util.readFileSync(__dirname + '/../ca-bundle.crt');
      options.agent = new client.Agent(options);
    }

    var req = client.request(options, function onResponse(resp) {
      callbacks.onHeaders(resp.statusCode, resp.headers);
      resp.on('data', callbacks.onData);
      resp.on('end', callbacks.onEnd);
    });

    if (request.body) req.write(request.body);

    req.end();
    req.on('error', function (e) {
      callbacks.onError({ code: 'NetworkingError', message: e.toString() });
    });
  }

});

/**
 * @api private
 */
AWS.HttpClient = AWS.NodeHttpClient;

/**
 * @api private
 */
AWS.HttpClient.getInstance = function getInstance() {
  /*jshint newcap:false */
  if (this.singleton === undefined) {
    this.singleton = new this();
  }
  return this.singleton;
};
<commit_msg>Document AWS.Endpoint, AWS.HttpRequest, and AWS.HttpResponse<commit_after>/**
 * Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"). You
 * may not use this file except in compliance with the License. A copy of
 * the License is located at
 *
 *     http://aws.amazon.com/apache2.0/
 *
 * or in the "license" file accompanying this file. This file 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.
 */

var AWS = require('./core');
var inherit = AWS.util.inherit;

/**
 * The endpoint that a service will talk to, for example,
 * <tt>https://ec2.ap-southeast-1.amazonaws.com</tt>. If
 * you need to override an endpoint for a service, you can
 * set the endpoint on a service by passing the endpoint
 * object with the +endpoint+ option key:
 *
 *   var ep = new AWS.Endpoint('awsproxy.example.com');
 *   var s3 = new AWS.S3({endpoint: ep});
 *   s3.client.endpoint.hostname == 'awsproxy.example.com'
 *
 * Note that if you do not specify a protocol, the protocol will
 * be selected based on your current {AWS.config} configuration.
 *
 * @!attribute protocol
 *   @return [String] the protocol (http or https) of the endpoint
 *     URL
 * @!attribute hostname
 *   @return [String] the host portion of the endpoint, e.g.,
 *     example.com
 * @!attribute host
 *   @return [String] the host portion of the endpoint including
 *     the port, e.g., example.com:80
 * @!attribute port
 *   @return [Integer] the port of the endpoint
 * @!attribute href
 *   @return [String] the full URL of the endpoint
 */
AWS.Endpoint = inherit({

  /**
   * @overload Endpoint(endpoint)
   *   Constructs a new endpoint given an endpoint URL. If the
   *   URL omits a protocol (http or https), the default protocol
   *   set in the global {AWS.config} will be used.
   *   @param endpoint [String] the URL to construct an endpoint from
   */
  constructor: function Endpoint(endpoint, config) {
    if (!endpoint.match(/^http/)) {
      var useSSL = config && config.sslEnabled !== undefined ?
        config.sslEnabled : AWS.config.sslEnabled;
      endpoint = (useSSL ? 'https' : 'http') + '://' + endpoint;
    }

    AWS.util.update(this, AWS.util.urlParse(endpoint));

    // Ensure the port property is set as an integer
    if (this.port) {
      this.port = parseInt(this.port, 10);
    } else {
      this.port = this.protocol === 'https:' ? 443 : 80;
    }
  }

});

/**
 * The low level HTTP request object, encapsulating all HTTP header
 * and body data sent by a service request.
 *
 * @!attribute method
 *   @return [String] the HTTP method of the request
 * @!attribute path
 *   @return [String] the path portion of the URI, e.g.,
 *     "/list/?start=5&num=10"
 * @!attribute headers
 *   @return [map<String,String>]
 *     a map of header keys and their respective values
 * @!attribute body
 *   @return [String] the request body payload
 * @!attribute endpoint
 *   @return [AWS.Endpoint] the endpoint for the request
 * @!attribute region
 *   @api private
 *   @return [String] the region, for signing purposes only.
 */
AWS.HttpRequest = inherit({

  /**
   * @api private
   */
  constructor: function HttpRequest() {
    this.method = 'POST';
    this.path = '/';
    this.headers = {};
    this.headers['User-Agent'] = AWS.util.userAgent();
    this.body = undefined;
    this.endpoint = undefined;
    this.region = undefined;
  },

  /**
   * @return [String] the part of the {path} excluding the
   *   query string
   */
  pathname: function pathname() {
    return this.path.split('?', 1)[0];
  },

  /**
   * @return [String] the query string portion of the {path}
   */
  search: function search() {
    return this.path.split('?', 2)[1] || '';
  }

});

/**
 * The low level HTTP response object, encapsulating all HTTP header
 * and body data returned from the request.
 *
 * @!attribute statusCode
 *   @return [Integer] the HTTP status code of the response (e.g., 200, 404)
 * @!attribute headers
 *   @return [map<String,String>]
 *      a map of response header keys and their respective values
 * @!attribute body
 *   @return [String] the response body payload
 */
AWS.HttpResponse = inherit({
  constructor: function HttpResponse() {
    this.statusCode = null;
    this.headers = {};
    this.body = null;
  }
});

/**
 * @api private
 */
AWS.RequestHandler = inherit({

  constructor: function RequestHandler(awsRequest) {
    this.awsRequest = awsRequest;
    this.awsResponse = awsRequest.awsResponse;
    this.client = this.awsResponse.client;
    this.method = this.awsResponse.method;
    this.params = this.awsResponse.params;
  },

  makeRequest: function makeRequest() {
    var httpRequest = this.client.buildRequest(this.method, this.params);
    httpRequest.sign();
    this.sendRequest(httpRequest);
  },

  sendRequest: function sendRequest(httpRequest) {

    var httpResponse = new AWS.HttpResponse();

    this.awsResponse.httpResponse = httpResponse;
    this.awsResponse.httpRequest = httpRequest;

    var self = this;
    AWS.HttpClient.getInstance().handleRequest(httpRequest, {

      onHeaders: function onHeaders(statusCode, headers) {
        httpResponse.statusCode = statusCode;
        httpResponse.headers = headers;
      },

      onData: function onData(data) {
        if (httpResponse.body === null) {
          httpResponse.body = data;
        } else {
          httpResponse.body += data;
        }
        self.handleHttpData(httpResponse);
      },

      onEnd: function onEnd() {

        if (httpResponse.body) {
          httpResponse.body = httpResponse.body.toString();
        }

        if (httpResponse.statusCode == 307) {
          self.redirect(httpRequest, httpResponse);
        } else {
          self.handleHttpResponse(httpResponse);
        }

      },

      onError: function onError(error) {
        self.retryRequest(error);
      }

    });

  },

  handleHttpData: function handleHttpData(httpResponse) {
    if (this.awsRequest.callbacks.data.length === 0) return;
    this.awsRequest.notifyData(httpResponse.body);
    httpResponse.body = null;
  },

  // Called when the http client returns a response.  Successfull http requests
  // may still contain an error (e.g. 400, 500, etc)
  redirect: function redirect(httpRequest, httpResponse) {
    /*jshint sub:true */
    httpRequest.endpoint = new AWS.Endpoint(httpResponse.headers['location']);
    this.sendRequest(httpRequest);
  },

  handleHttpResponse: function handleHttpResponse(httpResponse) {
    var self = this;
    this.client.parseResponse(httpResponse, this.method, function (error, data) {
      if (error)
        self.handleError(error);
      else
        self.awsRequest.notifyDone(data);
    });
  },

  handleError: function handleError(error) {
    if (error.retryable) {
      this.retryRequest(error);
    } else {
      this.awsRequest.notifyFail(error);
    }
  },

  retryRequest: function retryRequest(error) {

    var delays = this.client.retryDelays();
    var delay = delays[this.awsResponse.retryCount];

    this.awsResponse.retryCount += 1;

    if (delay !== undefined) {

      var self = this;
      setTimeout(function delayRetry() {
        self.makeRequest();
      }, delay);

    } else {

      this.awsRequest.notifyFail(error); // retried too many times

    }
  }
});

/**
 * @api private
 */
AWS.NodeHttpClient = inherit({

  constructor: function NodeHttpClient() {},

  handleRequest: function handleRequest(request, callbacks) {
    var options = {
      host: request.endpoint.hostname,
      port: request.endpoint.port,
      method: request.method,
      headers: request.headers,
      path: request.path
    };
    var useSSL = request.endpoint.protocol === 'https:';
    var client = useSSL ? require('https') : require('http');

    if (useSSL) {
      options.rejectUnauthorized = true;
      options.cert = AWS.util.readFileSync(__dirname + '/../ca-bundle.crt');
      options.agent = new client.Agent(options);
    }

    var req = client.request(options, function onResponse(resp) {
      callbacks.onHeaders(resp.statusCode, resp.headers);
      resp.on('data', callbacks.onData);
      resp.on('end', callbacks.onEnd);
    });

    if (request.body) req.write(request.body);

    req.end();
    req.on('error', function (e) {
      callbacks.onError({ code: 'NetworkingError', message: e.toString() });
    });
  }

});

/**
 * @api private
 */
AWS.HttpClient = AWS.NodeHttpClient;

/**
 * @api private
 */
AWS.HttpClient.getInstance = function getInstance() {
  /*jshint newcap:false */
  if (this.singleton === undefined) {
    this.singleton = new this();
  }
  return this.singleton;
};
<|endoftext|>


<commit_before>/**
 * `core/modules` data store: module sharing settings
 *
 * Site Kit by Google, Copyright 2022 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';
import isEqual from 'lodash/isEqual';
import pick from 'lodash/pick';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { createFetchStore } from '../../data/create-fetch-store';
import { CORE_MODULES } from './constants';

const { createRegistrySelector } = Data;

// Actions
const SET_SHARING_MANAGEMENT = 'SET_SHARING_MANAGEMENT';
const SET_SHARED_ROLES = 'SET_SHARED_ROLES';
const RECEIVE_GET_SHARING_SETTINGS = 'RECEIVE_GET_SHARING_SETTINGS';
const RECEIVE_SHAREABLE_ROLES = 'RECEIVE_SHAREABLE_ROLES';

const baseInitialState = {
	sharingSettings: undefined,
	savedSharingSettings: undefined,
	shareableRoles: undefined,
};

const fetchSaveSharingSettingsStore = createFetchStore( {
	baseName: 'saveSharingSettings',
	controlCallback: ( { savedSharingSettings } ) => {
		return API.set(
			'core',
			'modules',
			'sharing-settings',
			savedSharingSettings,
			{
				useCache: false,
			}
		);
	},
	reducerCallback: ( state, { settings } ) => {
		return {
			...state,
			savedSharingSettings: settings,
			sharingSettings: settings,
		};
	},
	argsToParams: ( savedSharingSettings ) => ( { savedSharingSettings } ),
	validateParams: ( { savedSharingSettings } = {} ) => {
		invariant( savedSharingSettings, 'savedSharingSettings is required.' );
	},
} );

const baseActions = {
	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string} moduleSlug Module slug.
	 * @param {string} management New management role for module, one of: all_admins, owner.
	 * @return {Object} Action for SET_SHARING_MANAGEMENT.
	 */
	setSharingManagement( moduleSlug, management ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		const managementRoles = [ 'all_admins', 'owner' ];
		invariant(
			managementRoles.includes( management ),
			`management must be one of: ${ managementRoles.join( ', ' ) }.`
		);
		return {
			payload: {
				moduleSlug,
				management,
			},
			type: SET_SHARING_MANAGEMENT,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string}   moduleSlug Module slug.
	 * @param {string[]} roles      List of roles the module is shared with.
	 * @return {Object} Action for SET_SHARED_ROLES.
	 */
	setSharedRoles( moduleSlug, roles ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		invariant(
			Array.isArray( roles ) &&
				roles.every( ( role ) => typeof role === 'string' ),
			'roles must be an array of strings.'
		);
		return {
			payload: {
				moduleSlug,
				roles,
			},
			type: SET_SHARED_ROLES,
		};
	},

	/**
	 * Saves sharingSettings for dashboard sharing.
	 *
	 * Save sharingSettings for dashboard sharing.
	 * Update ownerID from the response for the modules in the sharingSettings state.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Object with `{response, error}`.
	 */
	*saveSharingSettings() {
		const registry = yield Data.commonActions.getRegistry();

		const sharingSettings = registry
			.select( CORE_MODULES )
			.getSharingSettings();

		const {
			response,
			error,
		} = yield fetchSaveSharingSettingsStore.actions.fetchSaveSharingSettings(
			sharingSettings
		);

		// Update module owner IDs in the sharing settings modules.
		if ( Object.keys( response?.newOwnerIDs ).length ) {
			for ( const [ slug, ownerID ] of Object.entries(
				response.newOwnerIDs
			) ) {
				const storeName = registry
					.select( CORE_MODULES )
					.getModuleStoreName( slug );

				yield registry.dispatch( storeName ).setOwnerID( ownerID );
			}
		}

		return { response, error };
	},

	/**
	 * Receives sharingSettings for dashboard sharing.
	 * Stores sharingSettings in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} sharingSettings Sharing settings for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_GET_SHARING_SETTINGS.
	 */
	receiveGetSharingSettings( sharingSettings ) {
		invariant( sharingSettings, 'sharingSettings is required.' );
		return {
			payload: { sharingSettings },
			type: RECEIVE_GET_SHARING_SETTINGS,
		};
	},

	/**
	 * Receives shareableRoles for dashboard sharing.
	 * Stores shareableRoles in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} shareableRoles Shareable Roles for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_SHAREABLE_ROLES.
	 */
	receiveShareableRoles( shareableRoles ) {
		invariant( shareableRoles, 'shareableRoles is required.' );
		return {
			payload: { shareableRoles },
			type: RECEIVE_SHAREABLE_ROLES,
		};
	},
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case SET_SHARING_MANAGEMENT: {
			const { moduleSlug, management } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						management,
					},
				},
			};
		}

		case SET_SHARED_ROLES: {
			const { moduleSlug, roles } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
			};
		}

		case RECEIVE_GET_SHARING_SETTINGS: {
			const { sharingSettings } = payload;

			return {
				...state,
				sharingSettings,
				savedSharingSettings: sharingSettings,
			};
		}

		case RECEIVE_SHAREABLE_ROLES: {
			const { shareableRoles } = payload;

			return {
				...state,
				shareableRoles,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getSharingSettings() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_MODULES ).getSharingSettings() ) {
			return;
		}

		if ( ! global._googlesitekitDashboardSharingData ) {
			global.console.error(
				'Could not load core/modules dashboard sharing settings.'
			);
			return;
		}

		const { settings } = global._googlesitekitDashboardSharingData;
		yield actions.receiveGetSharingSettings( settings );
	},

	*getShareableRoles() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_MODULES ).getShareableRoles() ) {
			return;
		}

		if ( ! global._googlesitekitDashboardSharingData ) {
			global.console.error(
				'Could not load core/modules dashboard sharing roles.'
			);
			return;
		}

		const { roles } = global._googlesitekitDashboardSharingData;
		yield actions.receiveShareableRoles( roles );
	},
};

const baseSelectors = {
	/**
	 * Gets the current dashboard sharing settings.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Sharing Settings object. Returns undefined if it is not loaded yet.
	 */
	getSharingSettings( state ) {
		const { sharingSettings } = state;
		return sharingSettings;
	},

	/**
	 * Gets the current dashboard shareable roles.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Shareable Roles object. Returns undefined if it is not loaded yet.
	 */
	getShareableRoles( state ) {
		const { shareableRoles } = state;
		return shareableRoles;
	},

	/**
	 * Gets the dashboard sharing management for the given module.
	 *
	 * Returns the module's sharing management string.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug.
	 * @return {(string|null|undefined)} The module's sharing management string, null if there is none,
	 *                                   undefined if not loaded yet.
	 */
	getSharingManagement: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			invariant( moduleSlug, 'moduleSlug is required.' );
			const sharingSettings = select( CORE_MODULES ).getSharingSettings();

			if ( sharingSettings === undefined ) {
				return undefined;
			}
			return sharingSettings[ moduleSlug ]?.management || null;
		}
	),

	/**
	 * Gets the shared roles for the given module.
	 *
	 * Returns the module's shared roles list.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug.
	 * @return {(Array|null|undefined)} The module's shared roles array, null if there is none,
	 *                                   undefined if not loaded yet.
	 */
	getSharedRoles: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			invariant( moduleSlug, 'moduleSlug is required.' );
			const sharingSettings = select( CORE_MODULES ).getSharingSettings();

			if ( sharingSettings === undefined ) {
				return undefined;
			}
			return sharingSettings[ moduleSlug ]?.sharedRoles || null;
		}
	),

	/**
	 * Indicates whether the current sharing settings have changed from what is saved.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object}     state Data store's state.
	 * @param {Array|null} keys  Sharing Settings keys to check; if not provided, all sharing settings are checked.
	 * @return {boolean} True if the sharing settings have changed, false otherwise.
	 */
	haveSharingSettingsChanged( state, keys = null ) {
		const { sharingSettings, savedSharingSettings } = state;

		if ( keys ) {
			return ! isEqual(
				pick( sharingSettings, keys ),
				pick( savedSharingSettings, keys )
			);
		}

		return ! isEqual( sharingSettings, savedSharingSettings );
	},
};

const store = Data.combineStores( fetchSaveSharingSettingsStore, {
	initialState: baseInitialState,
	actions: baseActions,
	selectors: baseSelectors,
	reducer: baseReducer,
	resolvers: baseResolvers,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const selectors = store.selectors;
export const reducer = store.reducer;

export default store;
<commit_msg>Add canSubmitSharingChanges selector.<commit_after>/**
 * `core/modules` data store: module sharing settings
 *
 * Site Kit by Google, Copyright 2022 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';
import isEqual from 'lodash/isEqual';
import pick from 'lodash/pick';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { createFetchStore } from '../../data/create-fetch-store';
import { CORE_MODULES } from './constants';

const { createRegistrySelector } = Data;

// Actions
const SET_SHARING_MANAGEMENT = 'SET_SHARING_MANAGEMENT';
const SET_SHARED_ROLES = 'SET_SHARED_ROLES';
const RECEIVE_GET_SHARING_SETTINGS = 'RECEIVE_GET_SHARING_SETTINGS';
const RECEIVE_SHAREABLE_ROLES = 'RECEIVE_SHAREABLE_ROLES';

const baseInitialState = {
	sharingSettings: undefined,
	savedSharingSettings: undefined,
	shareableRoles: undefined,
};

const fetchSaveSharingSettingsStore = createFetchStore( {
	baseName: 'saveSharingSettings',
	controlCallback: ( { savedSharingSettings } ) => {
		return API.set(
			'core',
			'modules',
			'sharing-settings',
			savedSharingSettings,
			{
				useCache: false,
			}
		);
	},
	reducerCallback: ( state, { settings } ) => {
		return {
			...state,
			savedSharingSettings: settings,
			sharingSettings: settings,
		};
	},
	argsToParams: ( savedSharingSettings ) => ( { savedSharingSettings } ),
	validateParams: ( { savedSharingSettings } = {} ) => {
		invariant( savedSharingSettings, 'savedSharingSettings is required.' );
	},
} );

const baseActions = {
	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string} moduleSlug Module slug.
	 * @param {string} management New management role for module, one of: all_admins, owner.
	 * @return {Object} Action for SET_SHARING_MANAGEMENT.
	 */
	setSharingManagement( moduleSlug, management ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		const managementRoles = [ 'all_admins', 'owner' ];
		invariant(
			managementRoles.includes( management ),
			`management must be one of: ${ managementRoles.join( ', ' ) }.`
		);
		return {
			payload: {
				moduleSlug,
				management,
			},
			type: SET_SHARING_MANAGEMENT,
		};
	},

	/**
	 * Sets the sharing settings management role of given module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string}   moduleSlug Module slug.
	 * @param {string[]} roles      List of roles the module is shared with.
	 * @return {Object} Action for SET_SHARED_ROLES.
	 */
	setSharedRoles( moduleSlug, roles ) {
		invariant( moduleSlug, 'moduleSlug is required.' );
		invariant(
			Array.isArray( roles ) &&
				roles.every( ( role ) => typeof role === 'string' ),
			'roles must be an array of strings.'
		);
		return {
			payload: {
				moduleSlug,
				roles,
			},
			type: SET_SHARED_ROLES,
		};
	},

	/**
	 * Saves sharingSettings for dashboard sharing.
	 *
	 * Save sharingSettings for dashboard sharing.
	 * Update ownerID from the response for the modules in the sharingSettings state.
	 *
	 * @since n.e.x.t
	 *
	 * @return {Object} Object with `{response, error}`.
	 */
	*saveSharingSettings() {
		const registry = yield Data.commonActions.getRegistry();

		const sharingSettings = registry
			.select( CORE_MODULES )
			.getSharingSettings();

		const {
			response,
			error,
		} = yield fetchSaveSharingSettingsStore.actions.fetchSaveSharingSettings(
			sharingSettings
		);

		// Update module owner IDs in the sharing settings modules.
		if ( Object.keys( response?.newOwnerIDs ).length ) {
			for ( const [ slug, ownerID ] of Object.entries(
				response.newOwnerIDs
			) ) {
				const storeName = registry
					.select( CORE_MODULES )
					.getModuleStoreName( slug );

				yield registry.dispatch( storeName ).setOwnerID( ownerID );
			}
		}

		return { response, error };
	},

	/**
	 * Receives sharingSettings for dashboard sharing.
	 * Stores sharingSettings in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} sharingSettings Sharing settings for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_GET_SHARING_SETTINGS.
	 */
	receiveGetSharingSettings( sharingSettings ) {
		invariant( sharingSettings, 'sharingSettings is required.' );
		return {
			payload: { sharingSettings },
			type: RECEIVE_GET_SHARING_SETTINGS,
		};
	},

	/**
	 * Receives shareableRoles for dashboard sharing.
	 * Stores shareableRoles in the datastore.
	 *
	 * Because this is frequently-accessed data, this is usually sourced
	 * from a global variable (`_googlesitekitDashboardSharingData`), set by PHP
	 * in the `before_print` callback for `googlesitekit-datastore-site`.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} shareableRoles Shareable Roles for modules with `management` and `sharedRoles` properties.
	 * @return {Object} Action for RECEIVE_SHAREABLE_ROLES.
	 */
	receiveShareableRoles( shareableRoles ) {
		invariant( shareableRoles, 'shareableRoles is required.' );
		return {
			payload: { shareableRoles },
			type: RECEIVE_SHAREABLE_ROLES,
		};
	},
};

const baseReducer = ( state, { type, payload } ) => {
	switch ( type ) {
		case SET_SHARING_MANAGEMENT: {
			const { moduleSlug, management } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						management,
					},
				},
			};
		}

		case SET_SHARED_ROLES: {
			const { moduleSlug, roles } = payload;

			return {
				...state,
				sharingSettings: {
					...state.sharingSettings,
					[ moduleSlug ]: {
						...state.sharingSettings[ moduleSlug ],
						sharedRoles: roles,
					},
				},
			};
		}

		case RECEIVE_GET_SHARING_SETTINGS: {
			const { sharingSettings } = payload;

			return {
				...state,
				sharingSettings,
				savedSharingSettings: sharingSettings,
			};
		}

		case RECEIVE_SHAREABLE_ROLES: {
			const { shareableRoles } = payload;

			return {
				...state,
				shareableRoles,
			};
		}

		default: {
			return state;
		}
	}
};

const baseResolvers = {
	*getSharingSettings() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_MODULES ).getSharingSettings() ) {
			return;
		}

		if ( ! global._googlesitekitDashboardSharingData ) {
			global.console.error(
				'Could not load core/modules dashboard sharing settings.'
			);
			return;
		}

		const { settings } = global._googlesitekitDashboardSharingData;
		yield actions.receiveGetSharingSettings( settings );
	},

	*getShareableRoles() {
		const registry = yield Data.commonActions.getRegistry();

		if ( registry.select( CORE_MODULES ).getShareableRoles() ) {
			return;
		}

		if ( ! global._googlesitekitDashboardSharingData ) {
			global.console.error(
				'Could not load core/modules dashboard sharing roles.'
			);
			return;
		}

		const { roles } = global._googlesitekitDashboardSharingData;
		yield actions.receiveShareableRoles( roles );
	},
};

const baseSelectors = {
	/**
	 * Gets the current dashboard sharing settings.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Sharing Settings object. Returns undefined if it is not loaded yet.
	 */
	getSharingSettings( state ) {
		const { sharingSettings } = state;
		return sharingSettings;
	},

	/**
	 * Gets the current dashboard shareable roles.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state Data store's state.
	 * @return {(Object|undefined)} Shareable Roles object. Returns undefined if it is not loaded yet.
	 */
	getShareableRoles( state ) {
		const { shareableRoles } = state;
		return shareableRoles;
	},

	/**
	 * Gets the dashboard sharing management for the given module.
	 *
	 * Returns the module's sharing management string.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug.
	 * @return {(string|null|undefined)} The module's sharing management string, null if there is none,
	 *                                   undefined if not loaded yet.
	 */
	getSharingManagement: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			invariant( moduleSlug, 'moduleSlug is required.' );
			const sharingSettings = select( CORE_MODULES ).getSharingSettings();

			if ( sharingSettings === undefined ) {
				return undefined;
			}
			return sharingSettings[ moduleSlug ]?.management || null;
		}
	),

	/**
	 * Gets the shared roles for the given module.
	 *
	 * Returns the module's shared roles list.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug.
	 * @return {(Array|null|undefined)} The module's shared roles array, null if there is none,
	 *                                   undefined if not loaded yet.
	 */
	getSharedRoles: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			invariant( moduleSlug, 'moduleSlug is required.' );
			const sharingSettings = select( CORE_MODULES ).getSharingSettings();

			if ( sharingSettings === undefined ) {
				return undefined;
			}
			return sharingSettings[ moduleSlug ]?.sharedRoles || null;
		}
	),

	/**
	 * Indicates whether the current sharing settings have changed from what is saved.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object}     state Data store's state.
	 * @param {Array|null} keys  Sharing Settings keys to check; if not provided, all sharing settings are checked.
	 * @return {boolean} True if the sharing settings have changed, false otherwise.
	 */
	haveSharingSettingsChanged( state, keys = null ) {
		const { sharingSettings, savedSharingSettings } = state;

		if ( keys ) {
			return ! isEqual(
				pick( sharingSettings, keys ),
				pick( savedSharingSettings, keys )
			);
		}

		return ! isEqual( sharingSettings, savedSharingSettings );
	},

	/**
	 * Checks whether we can submit sharing settings changes for a module.
	 *
	 * @since n.e.x.t
	 *
	 * @param {Object} state      Data store's state.
	 * @param {string} moduleSlug Module slug.
	 * @return {boolean} Whether the module supports submitting sharing settings changes.
	 */
	canSubmitSharingChanges: createRegistrySelector(
		( select ) => ( state, moduleSlug ) => {
			invariant( moduleSlug, 'moduleSlug is required.' );
			const storeName = select( CORE_MODULES ).getModuleStoreName(
				moduleSlug
			);
			return !! select( storeName )?.canSubmitSharingChanges?.();
		}
	),
};

const store = Data.combineStores( fetchSaveSharingSettingsStore, {
	initialState: baseInitialState,
	actions: baseActions,
	selectors: baseSelectors,
	reducer: baseReducer,
	resolvers: baseResolvers,
} );

export const initialState = store.initialState;
export const actions = store.actions;
export const selectors = store.selectors;
export const reducer = store.reducer;

export default store;
<|endoftext|>