privly/privly-applications

View on GitHub
shared/javascripts/network_service.js

Summary

Maintainability
F
3 days
Test Coverage
/**
 * @fileOverview Interfaces the application with its current architecture.
 * This could be an extension, mobile app, or a hosted web server.
 *
 **/
 
/**
 * @namespace
 */
var privlyNetworkService = {
  
  /**
   * Permissions the user may have on creating and updating content.
   * Each of these permissions are associated with a network request.
   * They should only be set to "true" if the associated get/post/delete
   * request could be made without harming the remote server. You can usually
   * establish that the request will not harm data stored remotely by 
   * doing a get request that affirms the user's right to perform certain
   * operations.
   */
  permissions: {
    canCreate: false,
    canDestroy: false,
    canUpdate: false,
    canShare: false,
    canShow: false
  },
  
  /**
   * If this variable is assigned, it will be appended as a get parameter
   * on all requests, eg, `?auth_token=AUTH_TOKEN_HERE`. This should never
   * be referenced by anything but the auth token setters.
   */
  authToken: "",
  
  /**
   * Set the authentication token if a parameter is supplied, or the
   * current platform has a setter function. This may be called from the
   * context of mobile applications or browser extensions.
   */
  setAuthTokenString: function(authTokenString) {
    if (authTokenString !== undefined) {
      privlyNetworkService.authToken = "auth_token=" + authTokenString;
    } else if(privlyNetworkService.platformName() === "ANDROID") {
      privlyNetworkService.authToken = "auth_token=" + 
                                              androidJsBridge.fetchAuthToken();
    }
  },
  
  /**
   * Adds the auth token to the current URL. If the auth token has not been
   * assigned, nothing is added. If the auth token is assigned, it is added
   * as a get parameter.
   *
   * @param string url the URL that may need an auth token added.
   *
   */
  getAuthenticatedUrl: function(url) {
    
    // get the parameter string for the auth token
    privlyNetworkService.setAuthTokenString();
    
    // Don't change the URL if there is no auth token, or the URL
    // is not for the content server
    if( privlyNetworkService.authToken === "" || 
      url.indexOf(privlyNetworkService.contentServerDomain()) !== 0 ) {
      return url;
    }
    
    // if query parameters already exist on the URL
    if (url.indexOf("?") >= 0 && (url.indexOf("?") < url.indexOf("#") ||
      url.indexOf("#") === -1)) {
      return url.replace("?", "?" + privlyNetworkService.authToken + "&");
      
    // else if there is an anchor
    } else if(url.indexOf("#") >= 0) {
      return url.replace("#", "?" + privlyNetworkService.authToken + "#");
    } else {
      return url + "?" + privlyNetworkService.authToken;
    }
  },
  
  /**
   * @deprecated Use Privly.message.currentAdapter.getPlatformName() instead.
   */
  platformName: function() {
    if (navigator.userAgent.indexOf("iPhone") >= 0 || 
      navigator.userAgent.indexOf("iPad") >= 0) {
      if( navigator.userAgent.indexOf("Safari") >= 0 ) { return "HOSTED"; }
        return "IOS";
    } else if(typeof androidJsBridge !== "undefined") {
      return "ANDROID";
    } else if (typeof chrome !== "undefined" && typeof chrome.extension !== "undefined") {
      return "CHROME";
    } else if (window.location.href.indexOf("chrome://") === 0) {
      return "FIREFOX";
    } else if (window.location.href.indexOf("safari-extension://") === 0) {
      return "SAFARI";
    } else {
      return "HOSTED";
    }
  },
  
  /**
   * Helper for determining the protocol+domain of a
   * URL.
   *
   * @param {string} url the URL for which we want the domain and protocl
   * @return {string} The string of the protocol and domain.
   */
  getProtocolAndDomain: function(url) {
    var domainGrabber = document.createElement("a");
    domainGrabber.href = url;
    return domainGrabber.protocol + "//" + domainGrabber.host;
  },
  
  /**
   * The cross-site request forgery tokens for servers initialized via 
   * initPrivlyService.
   */
  csrfTokens: {},
  
  /**
   * Get the CSRF token (if it exists) for a domain. CSRF tokens should be
   * initialized before requesting them here.
   * @param {string} url a URL for which we need to lookup its CSRF token
   */
  getCSRFToken: function(url) {
    return privlyNetworkService.csrfTokens[privlyNetworkService.getProtocolAndDomain(url)];
  },
  
  /**
   * This function is specific to the privly content server available on GitHub.
   * It initializes a CSRF token, and checks whether the user is logged in.
   *
   * @param loggedInCallback function the function to execute when
   * initialization is successful.
   *
   * @param loginCallback function the function to execute if the user is 
   * not logged in.
   *
   * @param errorCallback function the function to execute if the remote 
   * server is not available.
   */
  initPrivlyService: function(url, loggedInCallback, loginCallback, 
    errorCallback) {
    
    var csrfTokenAddress = privlyNetworkService.getProtocolAndDomain(url) + 
                           "/posts/user_account_data";
    
    csrfTokenAddress =  privlyNetworkService.getAuthenticatedUrl(csrfTokenAddress);
    $.ajax({
      url: csrfTokenAddress,
      dataType: "json",
      headers: { 
              Accept: "application/json",
              "X-Requested-With": "XMLHttpRequest"
          },
      success: function (json, textStatus, jqXHR) {
        // set the CSRF
        privlyNetworkService.csrfTokens[privlyNetworkService.getProtocolAndDomain(url)] = json.csrf;
        if(json.signedIn) {
          loggedInCallback(json, textStatus, jqXHR);
        } else {
          loginCallback(json, textStatus, jqXHR);
        }
      },
      error: function (jqXHR, textStatus, errorThrown) {
        errorCallback(jqXHR, textStatus, errorThrown);
      }
    });
    
  },
  
  /**
   * Determines network policy for request. Since network requests can be 
   * used to track when a user is reading Privly content (emails for instance),
   * requests must be checked for whitelisting status when the content is 
   * injected.
   *
   * @param {string} url The URL that may be able to track the user. Must
   * specify the http or https protocols.
   *
   */
  isWhitelistedDomain: function(url) {
    
    // Chrome maintains an explicit whitelist in local storage
    if( privlyNetworkService.platformName() === "CHROME" || 
      privlyNetworkService.platformName() === "FIREFOX") {
      
      // get the user defined whitelist and add in the default whitelist
      var whitelist = Privly.options.getWhitelistDomains();
      
      whitelist.push("priv.ly");
      whitelist.push("dev.privly.org");
      whitelist.push("localhost");
      whitelist.push("privlyalpha.org");
      whitelist.push("privlybeta.org");
      whitelist.push("localhost:3000");
      whitelist.push("localhost:4000");
      
      // See if the domain is in the whitelist
      for(var i = 0; i < whitelist.length; i++) {
        if( url.indexOf(whitelist[i]) > 0) {
          var url_split = url.split("/");
          if(url_split[2] === whitelist[i] &&
             url_split[1] === "" && 
             (url_split[0] === "http:" || url_split[0] === "https:") ) {
                return true;
          }
        }
      }
    } else {
      //Hosted, Android, and iOS don't have the same privacy concerns
      return true;
    }
    return false;
  },
  
  /**
   * Gives the domain for the user's trusted content server.
   * 
   * @return {string} The domain content is associated with.
   */
  contentServerDomain: function() {
    var protocolDomainPort = location.protocol + 
                             '//'+location.hostname + 
                             (location.port ? ':'+location.port: '');

    var platformName = privlyNetworkService.platformName();
    if (platformName === "HOSTED") {
      return protocolDomainPort;
    } else if (platformName === "CHROME" ||
               platformName === "FIREFOX" ||
               platformName === "SAFARI" ||
               platformName === "IOS") {
      return Privly.options.getServerUrl();
    } else if (platformName === "ANDROID") {
      return androidJsBridge.fetchDomainName();
    } else {
      return protocolDomainPort;
    }
  },
  
  /**
   * Make a same-origin get request for content.
   *
   * This request should always proceed a post request to ensure the post
   * endpoint is expecting integration with the extension.
   *
   * @param {string} url The URL to make a cross domain request for.
   * @param {function} callback The function to execute after the response
   * returns.
   */
  sameOriginGetRequest: function(url, callback) {
    
    // Add the auth token if the get request is on the user's content
    // server
    if( url.indexOf(privlyNetworkService.contentServerDomain()) === 0 ) {
      url = privlyNetworkService.getAuthenticatedUrl(url);
    }
    
    return $.ajax({
      url: url,
      dataType: "json",
      headers: { 
              Accept: "application/json",
              "X-Requested-With": "XMLHttpRequest"
          },
      success: function (json, textStatus, jqXHR) {
        callback({json: json, textStatus: textStatus, jqXHR: jqXHR});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        callback({json: {}, textStatus: textStatus, jqXHR: jqXHR});
      }
    });
  },
  
  /**
   * Make a same-origin post request to the specified server.
   *
   * Warning: Do not use this function without first checking the data returned
   * by a get request for conformance with the application's signature.
   * Basically, you need to see whether the data at the URL expects to interface
   * with your application. The easiest way to do this is to have a version
   * string for your application in the JSON stored on the content server.
   * This prevents some rather nasty cross-site-scripting attacks.
   *
   * @param {string} url The url to make a cross domain request for.
   * @param {function} callback The function to execute after the response
   * returns.
   * @param {object} data The data to be sent with the post request.
   */
  sameOriginPostRequest: function(url, callback, data) {
    url = privlyNetworkService.getAuthenticatedUrl(url);
    return $.ajax({
      url: url,
      cache: false,
      type: "POST",
      data: data,
      dataType: "json",
      headers: { 
              Accept: "application/json",
              "X-CSRF-Token": privlyNetworkService.getCSRFToken(url),
              "X-Requested-With": "XMLHttpRequest"
          },
      success: function (json, textStatus, jqXHR) {
        callback({json: json, textStatus: textStatus, jqXHR: jqXHR});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        callback({json: {}, textStatus: textStatus, jqXHR: jqXHR});
      }
    });
  },
  
  /**
   * Make a same-origin put request to the specified server.
   *
   * Warning: Do not use this function without first checking the data returned
   * by a get request for conformance with the application's signature.
   * Basically, you need to see whether the data at the URL expects to interface
   * with your application. The easiest way to do this is to have a version
   * string for your application in the JSON stored on the content server.
   * This prevents some rather nasty cross-site-scripting attacks.
   *
   * @param {string} url The url to make a cross domain request for.
   * @param {function} callback The function to execute after the response
   * returns.
   * @param {object} data The data to be sent with the post request.
   */
  sameOriginPutRequest: function(url, callback, data) {
    url = privlyNetworkService.getAuthenticatedUrl(url);
    return $.ajax({
      url: url,
      cache: false,
      type: "PUT",
      data: data,
      dataType: "json",
      headers: { 
              Accept: "application/json",
              "X-CSRF-Token": privlyNetworkService.getCSRFToken(url),
              "X-Requested-With": "XMLHttpRequest"
          },
      success: function (json, textStatus, jqXHR) {
        callback({json: json, textStatus: textStatus, jqXHR: jqXHR});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        callback({json: {}, textStatus: textStatus, jqXHR: jqXHR});
      }
    });
  },
  
  /**
   * Make a same-origin delete request to the specified server.
   *
   * Warning: Do not use this function without first checking the data returned
   * by a get request for conformance with the application's signature.
   * Basically, you need to see whether the data at the URL expects to interface
   * with your application. The easiest way to do this is to have a version
   * string for your application in the JSON stored on the content server.
   * This prevents some rather nasty cross-site-scripting attacks.
   *
   * @param {string} url The url to make a cross domain request for.
   * @param {function} callback The function to execute after the response
   * returns.
   * @param {object} data The data to be sent with the post request.
   */
  sameOriginDeleteRequest: function(url, callback, data) {
    url = privlyNetworkService.getAuthenticatedUrl(url);
    return $.ajax({
      url: url,
      cache: false,
      type: "DELETE",
      data: data,
      dataType: "json",
      headers: { 
              Accept: "application/json",
              "X-CSRF-Token": privlyNetworkService.getCSRFToken(url),
              "X-Requested-With": "XMLHttpRequest"
          },
      success: function (json, textStatus, jqXHR) {
        callback({json: json, textStatus: textStatus, jqXHR: jqXHR});
      },
      error: function (jqXHR, textStatus, errorThrown) {
        callback({json: {}, textStatus: textStatus, jqXHR: jqXHR});
      }
    });
  },
  
  /**
   * Hide all the elements not required by mobile and adjust the CSS appropriately.
   * Elements will only be modified for mobile apps.
   */
  mobileHide: function() {
    if( privlyNetworkService.platformName() === "IOS" ||
        privlyNetworkService.platformName() === "ANDROID" ) {
      $(".mobile_hide").hide();
      $("body").css("padding-top", "0px");
    }
  },

  /**
   * Assign the href attribute of navigation links appropriately.
   */
  initializeNavigation: function() {
    var domain = privlyNetworkService.contentServerDomain();
    $(".home_domain").text(domain.split("/")[2]);
    $(".account_url").attr("href", domain + "/pages/account");
    $(".legal_nav").attr("href", domain + "/pages/privacy");
    document.getElementById("logout_link").addEventListener('click', function(){
      var url = domain + "/users/sign_out";
      $.ajax({
        url: url,
        type: "POST",
        data: "_method=delete",
        headers: {
          Accept: "application/html",
          "X-CSRF-Token": privlyNetworkService.getCSRFToken(url)
        },
        success: function (json, textStatus, jqXHR) {
          window.location = "../Login/new.html";
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $("#messages").text("We are unable to verify that this computer has " +
            "been logged out from the server. Please refresh the page and try again.");
        }
      });


    });
    
    // Change the target property to be self if the application is hosted
    // by a remote server.
    if( privlyNetworkService.platformName() === "HOSTED" ) {
      $(".home_domain").attr("href", domain);
      $(".home_domain").attr("target", "_self");
      $(".account_url").attr("target", "_self");
      $(".legal_nav").attr("target", "_self");
    }

    privlyNetworkService.mobileHide();
  },

  /**
   * Show/hide the appropriate navigation items for when the user is logged out.
   */
  showLoggedOutNav: function() {
    if ( privlyNetworkService.platformName() === "ANDROID" ) {
      androidJsBridge.showLoginActivity();
      privlyNetworkService.mobileHide();
    } else if( typeof privlyHostPage === "undefined" || ! privlyHostPage.isInjected() ) {
      $(".logged_in_nav").hide();
      $(".logged_out_nav").show();
      $(".injected_hide").show();
    }
  },

  /**
   * Show/hide the appropriate navigation items for when the user is logged in.
   */
  showLoggedInNav: function() {
    if( typeof privlyHostPage === "undefined" || ! privlyHostPage.isInjected() ) {
      $(".logged_out_nav").hide();
      $(".injected_hide").show();
      $(".logged_in_nav").show();
      privlyNetworkService.mobileHide();
    }
  }
};