Source: src/dg.js

// Initialize the drupalgap json object.
var drupalgap = drupalgap || drupalgap_init(); // Do not remove this line.

// Init _GET for url path query strings.
var _dg_GET = _dg_GET || {};

/**
 * Initializes the drupalgap json object.
 * @return {Object}
 */
function drupalgap_init() {
    var dg = {
      modules: {
        core: [
           { name: 'comment' },
           { name: 'contact' },
           { name: 'entity' },
           { name: 'field' },
           { name: 'file' },
           { name: 'image' },
           { name: 'menu' },
           { name: 'mvc' },
           { name: 'node' },
           { name: 'search' },
           { name: 'system' },
           { name: 'taxonomy' },
           { name: 'user' },
           { name: 'views' }
         ]
      },
      module_paths: [],
      includes: [
          { name: 'block' },
          { name: 'common' },
          { name: 'form' },
          { name: 'go' },
          { name: 'menu' },
          { name: 'page' },
          { name: 'region' },
          { name: 'theme' },
          { name: 'title' }
      ],
      online: false,
      destination: '',
      api: {},
      back: false, /* moving backwards or not */
      back_path: [], /* paths to move back to */
      blocks: [],
      connected: false, // Becomes true once DrupalGap performs a System Connect call.
      content_types_list: {}, /* holds info about each content type */
      date_formats: { }, /* @see system_get_date_formats() in Drupal core */
      date_types: { }, /* @see system_get_date_types() in Drupal core */
      entity_info: {},
      field_info_fields: {},
      field_info_instances: {},
      field_info_extra_fields: {},
      form_errors: {},
      form_states: [],
      loading: false, /* indicates if the loading message is shown or not */
      loader: 'loading', /* used to determine the jQM loader mode */
      locale: {}, /* holds onto language json objects, keyed by language code */
      messages: [],
      menus: {},
      menu_links: {},
      menu_router: {}, /* @todo - doesn't appear to be used at all, remove it */
      mvc: {
        models: {},
        views: {},
        controllers: {}
      },
      output: '', /* hold output generated by menu_execute_active_handler() */
      page: {
        jqm_events: [],
        title: '',
        variables: {},
        process: true,
        options: {} /* holds the current page's options, eg. reloadPage, etc. */
      },
      pages: [], /* Collection of page ids that are loaded into the DOM. */
      path: '', /* The current menu path. */
      remote_addr: null, /* php's $_SERVER['REMOTE_ADDR'] via system connect */
      router_path: '', /* The current menu router path. */
      services: {},
      sessid: null,
      settings: {},
      site_settings: {}, /* holds variable settings from the Drupal site */
      taxonomy_vocabularies: false, /* holds vocabs from system connect */
      theme_path: '',
      themes: [],
      theme_registry: {},
      toast: {
        shown: false
      },
      views: {
        ids: []
      },
      views_datasource: {}
    };

  // Extend jDrupal as needed...

  // Forms will expire upon install and don't have an expiration time.
  if (!Drupal.cache_expiration) { Drupal.cache_expiration = {}; }
  if (!Drupal.cache_expiration.forms) { Drupal.cache_expiration.forms = {}; }

  // Finally return the JSON object.
  return dg;
}

/**
 * This is called once the <body> element's onload is fired.
 */
function drupalgap_onload() {
  try {

    // At this point, the Drupal object has been initialized by jDrupal and the
    // app/settings.js file was loaded in <head>. Let's add DrupalGap's modules
    // onto the Drupal JSON object. Remember, all of the module source code is
    // included via the makefile's bin generation. However, the core modules
    // hook_install() implementations haven't been called yet, so we add them to
    // the module listing so they can be invoked later on.
    var modules = [
      'drupalgap',
      'block',
      'comment',
      'contact',
      'entity',
      'field',
      'file',
      'image',
      'menu',
      'mvc',
      'node',
      'search',
      'system',
      'taxonomy',
      'user',
      'views'
    ];
    for (var i = 0; i < modules.length; i++) {
      var module = modules[i];
      Drupal.modules.core[module] = module_object_template(module);
    }

    // Depending on the mode, we'll move on to _drupalgap_deviceready()
    // accordingly. By default we'll assume the mode is for phonegap, unless
    // otherwise specified by the settings.js file. If it is for phonegap, we'll
    // attach its device ready listener, otherwise we'll just move on.
    if (typeof drupalgap.settings.mode === 'undefined') {
      drupalgap.settings.mode = 'phonegap';
    }
    switch (drupalgap.settings.mode) {
      case 'phonegap':
        document.addEventListener('deviceready', _drupalgap_deviceready, false);
        break;
      case 'web-app':
        _drupalgap_deviceready();
        break;
      default:
        console.log(
          'drupalgap_onload - unknown mode (' + drupalgap.settings.mode + ')'
        );
        return;
        break;
    }

  }
  catch (error) { console.log('drupalgap_onload - ' + error); }
}

/**
 * Implements PhoneGap's deviceready().
 */
function _drupalgap_deviceready() {
  try {

    // Set some jQM properties to better handle the back button on iOS9.
    if (
      typeof device !== 'undefined' &&
      device.platform === "iOS" &&
      parseInt(device.version) === 9
    ) {
      $.mobile.hashListeningEnabled = false;
      $.mobile.pushStateEnabled = false;
    }

    // The device is now ready, it is now safe for DrupalGap to start...
    drupalgap_bootstrap();

    // Verify site path is set.
    if (!Drupal.settings.site_path || Drupal.settings.site_path == '') {
      var msg = t('No site_path to Drupal set in the app/settings.js file!');
      drupalgap_alert(msg, {
          title: t('Error')
      });
      return;
    }

    // Device is ready, let's call any implementations of hook_deviceready(). If any implementation returns
    // false, that means they would like to take over the rest of the deviceready procedure (aka the System
    // Connect call)
    var proceed = true;
    var invocation_results = module_invoke_all('deviceready');
    if (invocation_results && invocation_results.length > 0) {
      for (var i = 0; i < invocation_results.length; i++) {
        if (!invocation_results[i]) {
          proceed = false;
          break;
        }
      }
    }

    // If the device is offline, warn the user and then go to the offline page, unless someone implemented
    // hook_offline, then let them handle it.
    if (!drupalgap_has_connection()) {
      if (!module_implements('device_offline')) {
        if (drupalgap.settings.offline_message) {
          drupalgap_alert(drupalgap.settings.offline_message, {
            title: t('Offline'),
            alertCallback: function() { drupalgap_goto('offline'); }
          });
        }
        else { drupalgap_goto('offline'); }
      }
      else { setTimeout(function() { module_invoke_all('device_offline'); }, 1); }
    }
    else if (proceed) {

      // Device is online and no one has taken over the deviceready, continue with the System Connect call.
      system_connect(_drupalgap_deviceready_options());

    }

  }
  catch (error) { console.log('_drupalgap_deviceready - ' + error); }
}

/**
 * Builds the default system connect options.
 * @return {Object}
 */
function _drupalgap_deviceready_options() {
  try {
    var pageOptions = arguments[0] ? arguments[0] : {};
    return {
      success: function(result) {

        // Set the connection and invoke hook_device_connected().
        drupalgap.connected = true;
        module_invoke_all('device_connected');

        // If there is a hash url present and it can be routed go directly to that page,
        // otherwise go to the app's front page.
        var path = '';
        var hash = window.location.hash;
        if (hash.indexOf('#') != -1) {
          hash = hash.replace('#', '');
          _drupalgap_goto_prepare_path(hash, true);
          var routedPath = drupalgap_get_path_from_page_id(hash);
          if (routedPath) { path = routedPath; }
        }
        drupalgap_goto(path, pageOptions);

      },
      error: function(jqXHR, textStatus, errorThrown) {

        // Build an informative error message and display it.
        var msg = t('Failed connection to') + ' ' + Drupal.settings.site_path;
        if (errorThrown != '') { msg += ' - ' + errorThrown; }
        msg += ' - ' + t('Check your device\'s connection and check that') +
          ' ' + Drupal.settings.site_path + ' ' + t('is online.');
       drupalgap_alert(msg, {
           title: t('Unable to Connect'),
           alertCallback: function() { drupalgap_goto('offline'); }
       });
      }
    };
  }
  catch (error) { console.log('_drupalgap_deviceready_options - ' + error); }
}

/**
 * Loads up all necessary assets to make DrupalGap ready.
 */
function drupalgap_bootstrap() {
  try {
    // Load up any contrib and/or custom modules (the DG core moodules have
    // already been loaded at this point), load the theme and all blocks. Then
    // build the menu router, load the menus, and build the theme registry.
    drupalgap_load_modules();
    drupalgap_load_theme();
    drupalgap_load_blocks();
    drupalgap_load_locales();
    menu_router_build();
    drupalgap_menus_load();
    drupalgap_theme_registry_build();

    // Attach device back button handler (Android).
    document.addEventListener('backbutton', drupalgap_back, false);
  }
  catch (error) { console.log('drupalgap_bootstrap - ' + error); }
}

/**
 * Loads any contrib or custom modules specifed in the settings.js file. Then
 * invoke hook_install() on all modules, including core.
 */
function drupalgap_load_modules() {
  try {
    var module_types = ['contrib', 'custom'];
    // We only need to load contrib and custom modules because core modules are
    // already included in the binary.
    for (var index in module_types) {
        if (!module_types.hasOwnProperty(index)) { continue; }
        var bundle = module_types[index];
        // Let's be nice and try to load any old drupalgap.modules declarations
        // in developers settings.js files for a while, but throw a warning to
        // encourage them to update. This code can be removed after a few
        // releases to help developers get caught up without angering them.
        if (
          drupalgap.modules &&
          drupalgap.modules[bundle] &&
          drupalgap.modules[bundle].length != 0
        ) {
          for (var index in drupalgap.modules[bundle]) {
              if (!drupalgap.modules[bundle].hasOwnProperty(index)) { continue; }
              var module = drupalgap.modules[bundle][index];
              if (module.name) {
                var msg = 'WARNING: The module "' + module.name + '" defined ' +
                  'in settings.js needs to be added to ' +
                  'Drupal.modules[\'' + bundle + '\'] instead! See ' +
                  'default.settings.js for examples on the new syntax!';
                console.log(msg);
                Drupal.modules[bundle][module.name] = module;
              }
          }
        }
        for (var module_name in Drupal.modules[bundle]) {
            if (!Drupal.modules[bundle].hasOwnProperty(module_name)) { continue; }
            var module = Drupal.modules[bundle][module_name];
            // If the module object is empty, initialize a module object.
            if ($.isEmptyObject(module)) {
              Drupal.modules[bundle][module_name] =
                module_object_template(module_name);
              module = Drupal.modules[bundle][module_name];
            }
            // If the module's name isn't set, set it.
            if (!module.name) {
              Drupal.modules[bundle][module_name].name = module_name;
              module = Drupal.modules[bundle][module_name];
            }

          // If the module is already loaded via the index.html file, just continue.
          if (typeof module.loaded !== 'undefined' && module.loaded) { continue; }

            // Determine module directory.
            var dir = drupalgap_modules_get_bundle_directory(bundle);
            module_base_path = dir + '/' + module.name;
            // Add module .js file to array of paths to load.
            var extension = module.minified ? '.min.js' : '.js';
            module_path = module_base_path + '/' + module.name + extension;
            modules_paths = [module_path];
            // If there are any includes with this module, add them to the
            // list of paths to include.
            if (module.includes != null && module.includes.length != 0) {
              for (var include_index in module.includes) {
                if (!module.includes.hasOwnProperty(include_index)) { continue; }
                var include_object = module.includes[include_index];
                modules_paths.push(
                  module_base_path + '/' + include_object.name + '.js'
                );
              }
            }
            // Now load all the paths for this module.
            for (var modules_paths_index in modules_paths) {
                if (!modules_paths.hasOwnProperty(modules_paths_index)) { continue; }
                var modules_paths_object = modules_paths[modules_paths_index];
                jQuery.ajax({
                    async: false,
                    type: 'GET',
                    url: modules_paths_object,
                    data: null,
                    success: function() {
                      if (Drupal.settings.debug) { console.log(modules_paths_object); }
                    },
                    dataType: 'script',
                    error: function(xhr, textStatus, errorThrown) {
                      console.log(
                          t('Failed to load module!') + ' (' + module.name + ')',
                          textStatus,
                          errorThrown.message
                      );
                    }
                });
            }
        }
    }
    // Now invoke hook_install on all modules, including core.
    module_invoke_all('install');
  }
  catch (error) { console.log('drupalgap_load_modules - ' + error); }
}

/**
 * Load the theme specified by drupalgap.settings.theme into drupalgap.theme
 * Returns true on success, false if it fails.
 * @return {Boolean}
 */
function drupalgap_load_theme() {
  try {
    if (!drupalgap.settings.theme) {
      var msg = 'drupalgap_load_theme - ' +
        t('no theme specified in settings.js');
      drupalgap_alert(msg);
    }
    else {
      // Pull the theme name from the settings.js file.
      var theme_name = drupalgap.settings.theme;
      var theme_path = 'themes/' + theme_name + '/' + theme_name + '.js';
      if (theme_name != 'easystreet3' && theme_name != 'ava') {
        theme_path = 'app/themes/' + theme_name + '/' + theme_name + '.js';
      }
      if (!drupalgap_file_exists(theme_path)) {
        var error_msg = 'drupalgap_theme_load - ' + t('Failed to load theme!') +
          ' ' + t('The theme\'s JS file does not exist') + ': ' + theme_path;
        drupalgap_alert(error_msg);
        return false;
      }
      // We found the theme's js file, add it to the page.
      drupalgap_add_js(theme_path);
      // Call the theme's template_info implementation.
      var template_info_function = theme_name + '_info';
      if (function_exists(template_info_function)) {
        var fn = window[template_info_function];
        drupalgap.theme = fn();
        // For each region in the name, set the 'name' value on the region JSON.
        for (var name in drupalgap.theme.regions) {
            if (!drupalgap.theme.regions.hasOwnProperty(name)) { continue; }
            var region = drupalgap.theme.regions[name];
            drupalgap.theme.regions[name].name = name;
        }
        // Make sure the theme implements the required regions.
        var regions = system_regions_list();
        for (var i = 0; i < regions.length; i++) {
          var region = regions[i];
          if (typeof drupalgap.theme.regions[region] === 'undefined') {
            console.log('WARNING: drupalgap_load_theme() - The "' +
                        theme_name + '" theme does not have the "' + region +
                        '" region specified in "' + theme_name + '_info()."');
          }
        }
        // Theme loaded successfully! Set the drupalgap.theme_path and return
        // true.
        drupalgap.theme_path = theme_path.replace('/' + theme_name + '.js', '');
        return true;
      }
      else {
        var error_msg = 'drupalgap_load_theme() - ' + t('failed') + ' - ' +
          template_info_function + '() ' + t('does not exist!');
        drupalgap_alert(error_msg);
      }
    }
    return false;
  }
  catch (error) { console.log('drupalgap_load_theme - ' + error); }
}

/**
 * Given a path to a javascript file relative to the app's www directory,
 * this will load the javascript file so it will be available in scope.
 */
function drupalgap_add_js() {
  try {
    var data;
    if (arguments[0]) { data = arguments[0]; }
    jQuery.ajax({
      async: false,
      type: 'GET',
      url: data,
      data: null,
      success: function() {
        if (Drupal.settings.debug) {
          // Print the js path to the console.
          console.log(data);
        }
      },
      dataType: 'script',
      error: function(xhr, textStatus, errorThrown) {
        console.log(
          'drupalgap_add_js - error - (' +
            data + ' : ' + textStatus +
          ') ' + errorThrown
        );
      }
    });
  }
  catch (error) {
    console.log('drupalgap_add_js - ' + error);
  }
}

/**
 * Given a path to a css file relative to the app's www directory, this will
 * attempt to load the css file so it will be available in scope.
 */
function drupalgap_add_css() {
  try {
    var data;
    if (arguments[0]) { data = arguments[0]; }
    $('<link/>', {rel: 'stylesheet', href: data}).appendTo('head');
  }
  catch (error) { console.log('drupalgap_add_css - ' + error); }
}

/**
 * Rounds up all blocks defined by hook_block_info and places them in the
 * drupalgap.blocks array.
 */
function drupalgap_load_blocks() {
  try {
    drupalgap.blocks = module_invoke_all('block_info');
  }
  catch (error) { console.log('drupalgap_load_blocks - ' + error); }
}

/**
 * Loads language files.
 */
function drupalgap_load_locales() {
  try {

    // Load any drupalgap.settings.locale specified language files.
    if (typeof drupalgap.settings.locale === 'undefined') { return; }
    for (var language_code in drupalgap.settings.locale) {
      if (!drupalgap.settings.locale.hasOwnProperty(language_code)) {
        continue;
      }
      var language = drupalgap.settings.locale[language_code];
      var file_path = 'locale/' + language_code + '.json';
      if (!drupalgap_file_exists(file_path)) { continue; }
      drupalgap.locale[language_code] = drupalgap_file_get_contents(
        file_path,
        { dataType: 'json' }
      );
    }

    // Load any language files specified by modules, and merge them into the
    // global language file (or create a new one if it doesn't exist).
    var modules = module_implements('locale');
    for (var i = 0; i < modules.length; i++) {
      var module = modules[i];
      var fn = window[module + '_locale'];
      var languages = fn();
      for (var j = 0; j < languages.length; j++) {
        var language_code = languages[j];
        var file_path = drupalgap_get_path('module', module) + '/locale/' + language_code + '.json';
        var translations = drupalgap_file_get_contents(
          file_path,
          { dataType: 'json' }
        );
        if (typeof drupalgap.locale[language_code] === 'undefined') {
          drupalgap.locale[language_code] = translations;
        }
        else {
          $.extend(
            drupalgap.locale[language_code],
            drupalgap.locale[language_code],
            translations
          );
        }
      }
    }

  }
  catch (error) { console.log('drupalgap_load_locales - ' + error); }
}

/**
 * Checks for an Internet connection, returns true if connected, false otherwise.
 * @returns {boolean}
 */
function drupalgap_has_connection() {
  try {
    drupalgap_check_connection();
    module_invoke_all('device_connection');
    return drupalgap.online;
  }
  catch (error) { console.log('drupalgap_has_connection - ' + error); }
}

/**
 * Checks the devices connection and sets drupalgap.online to true if the
 * device has a connection, false otherwise.
 * @return {String}
 *   A string indicating the type of connection according to PhoneGap.
 */
function drupalgap_check_connection() {
  try {
    // If we're not in phonegap, just use the navigator.onLine value.
    if (drupalgap.settings.mode != 'phonegap' || typeof parent.window.ripple === 'function' ) {
      drupalgap.online = navigator.onLine;
      return 'Ethernet connection'; // @TODO detect real connection type.
    }

    // Determine what connection phonegap has.
    var networkState = navigator.connection.type;
    var states = {};
    states[Connection.UNKNOWN] = 'Unknown connection';
    states[Connection.ETHERNET] = 'Ethernet connection';
    states[Connection.WIFI] = 'WiFi connection';
    states[Connection.CELL_2G] = 'Cell 2G connection';
    states[Connection.CELL_3G] = 'Cell 3G connection';
    states[Connection.CELL_4G] = 'Cell 4G connection';
    states[Connection.NONE] = 'No network connection';
    drupalgap.online = states[networkState] != 'No network connection';
    return states[networkState];
  }
  catch (error) { console.log('drupalgap_check_connection - ' + error); }
}

/**
 * @deprecated Use empty() instead.
 * Returns true if given value is empty. A generic way to test for emptiness.
 * @param {*} value
 * @return {Boolean}
 */
function drupalgap_empty(value) {
  try {
    console.log(
      'WARNING: drupalgap_empty() is deprecated! ' +
      'Use empty() instead.'
    );
    return empty(value);
  }
  catch (error) { console.log('drupalgap_empty - ' + error); }
}

/**
 * Checks if a given file exists, returns true or false.
 * @param  {string} path
 *   A path to a file
 * @return {bool}
 *   True if file exists, else false.
 */
function drupalgap_file_exists(path) {
  try {
    var file_exists = false;
    jQuery.ajax({
      async: false,
      type: 'HEAD',
      dataType: 'text',
      url: path,
      success: function() { file_exists = true; },
      error: function(xhr, textStatus, errorThrown) { }
    });
    return file_exists;
  }
  catch (error) { console.log('drupalgap_file_exists - ' + error); }
}

/**
 * Reads entire file into a string and returns the string. Returns false if
 * it fails.
 * @param {String} path
 * @param {Object} options
 * @return {String}
 */
function drupalgap_file_get_contents(path, options) {
  try {
    var file = false;
    var default_options = {
      type: 'GET',
      url: path,
      dataType: 'html',
      data: null,
      async: false,
      success: function(data) { file = data; },
      error: function(xhr, textStatus, errorThrown) {
        console.log(
          'drupalgap_file_get_contents - failed to load file (' + path + ')'
        );
      }
    };
    $.extend(default_options, options);
    jQuery.ajax(default_options);
    return file;
  }
  catch (error) { console.log('drupalgap_file_get_contents - ' + error); }
}

/**
 * @see https://api.drupal.org/api/drupal/includes!common.inc/function/format_interval/7
 * @param {Number} interval The length of the interval in seconds.
 * @return {String}
 */
function drupalgap_format_interval(interval) {
  try {
    // @TODO - deprecate this and move it to jDrupal as format_interval().
    var granularity = 2; if (arguments[1]) { granularity = arguments[1]; }
    var langcode = null; if (arguments[2]) { langcode = langcode[2]; }
    var units = {
      '1 year|@count years': 31536000,
      '1 month|@count months': 2592000,
      '1 week|@count weeks': 604800,
      '1 day|@count days': 86400,
      '1 hour|@count hours': 3600,
      '1 min|@count min': 60,
      '1 sec|@count sec': 1
    };
    var output = '';
    for (var key in units) {
      if (!units.hasOwnProperty(key)) { continue; }
      var value = units[key];
      var key = key.split('|');
      if (interval >= value) {
        var count = Math.floor(interval / value);
        output +=
          (output ? ' ' : '') +
          drupalgap_format_plural(
            count,
            key[0],
            key[1]
          );
        if (output.indexOf('@count') != -1) {
          output = output.replace('@count', count);
        }
        interval %= value;
        granularity--;
      }
      if (granularity == 0) { break; }
    }
    return output ? output : '0 sec';
  }
  catch (error) { console.log('drupalgap_format_interval - ' + error); }
}

/**
 * @see http://api.drupal.org/api/drupal/includes%21common.inc/function/format_plural/7
 * @param {Number} count
 * @param {String} singular
 * @param {String} plural
 * @return {String}
 */
function drupalgap_format_plural(count, singular, plural) {
    // @TODO - deprecate this and move it to jDrupal as format_plural().
    if (count == 1) { return singular; }
    return plural;
}

/**
 * @deprecated - Use function_exists() instead.
 * @param {String} name
 * @return {Boolean}
 */
function drupalgap_function_exists(name) {
  try {
    console.log('WARNING - drupalgap_function_exists() is deprecated. ' +
      'Use function_exists() instead!');
    return function_exists(name);
  }
  catch (error) { console.log('drupalgap_function_exists - ' + error); }
}

/**
 * Given an html string from a *.tpl.html file, this will extract all of the
 * placeholders names and return them in an array. Returns false otherwise.
 * @param {String} html
 * @return {*}
 */
function drupalgap_get_placeholders_from_html(html) {
  try {
    var placeholders = false;
    if (html) {
      placeholders = html.match(/(?!{:)([\w]+)(?=:})/g);
    }
    return placeholders;
  }
  catch (error) {
    console.log('drupalgap_get_placeholders_from_html - ' + error);
  }
}

/**
 * Returns the current page's title.
 * @return {String}
 */
function drupalgap_get_title() {
  try {
    return drupalgap.page.title;
  }
  catch (error) { console.log('drupalgap_get_title - ' + error); }
}

/**
 * Returns the IP Address of the current user as reported by PHP via the last
 * System Connect call's $_SERVER['REMOTE_ADDR'] value.
 * @return {String|Null}
 */
function drupalgap_get_ip() {
  try {
    return drupalgap.remote_addr;
  }
  catch (error) { console.log('drupalgap_get_ip - ' + error); }
}

/**
 * Given a router path, this will return an array containing the indexes of
 * where the wildcards (%) are present in the router path. Returns false if
 * there are no wildcards present.
 * @param {String} router_path
 * @return {Boolean}
 */
function drupalgap_get_wildcards_from_router_path(router_path) {
    // @todo - Is this function even used? Doesn't look like it.
    var wildcards = false;
    return wildcards;
}


/**
 * Given a drupal image file uri, this will return the path to the image on the
 * Drupal site.
 * @param {String} uri
 * @return {String}
 */
function drupalgap_image_path(uri) {
  try {
    var altered = false;
    // If any modules want to alter the path, let them do it.
    var modules = module_implements('image_path_alter');
    if (modules) {
      for (var index in modules) {
          if (!modules.hasOwnProperty(index)) { continue; }
          var module = modules[index];
          var result = module_invoke(module, 'image_path_alter', uri);
          if (result) {
            altered = true;
            uri = result;
            break;
          }
      }
    }
    if (!altered) {
      // No one modified the image path, we'll use the default approach to
      // generating the image src path.
      var src = Drupal.settings.site_path + Drupal.settings.base_path + uri;
      if (src.indexOf('public://') != -1) {
        src = src.replace('public://', Drupal.settings.file_public_path + '/');
      }
      else if (src.indexOf('private://') != -1) {
        src = src.replace(
          'private://',
          Drupal.settings.file_private_path + '/'
        );
      }
      return src;
    }
    else { return uri; }
  }
  catch (error) { console.log('drupalgap_image_path - ' + error); }
}

/**
 * @deprecated - This is no longer needed since the includes are built via the
 * makefile. Loads the js files in includes specified by drupalgap.includes.
 */
function drupalgap_includes_load() {
  try {
    if (drupalgap.includes != null && drupalgap.includes.length != 0) {
      for (var index in drupalgap.includes) {
          if (!drupalgap.includes.hasOwnProperty(index)) { continue; }
          var include = drupalgap.includes[index];
          var include_path = 'includes/' + include.name + '.inc.js';
          jQuery.ajax({
              async: false,
              type: 'GET',
              url: include_path,
              data: null,
              success: function() {
                if (Drupal.settings.debug) {
                  // Print the include path to the console.
                  dpm(include_path);
                }
              },
              dataType: 'script',
              error: function(xhr, textStatus, errorThrown) {
                console.log(errorThrown);
              }
          });
      }
    }
  }
  catch (error) { console.log('drupalgap_includes_load - ' + error); }
}

/**
 * Given an html list element id and an array of items, this will clear the
 * list, populate it with the items, and then refresh the list.
 * @param {String} list_css_selector
 * @param {Array} items
 */
function drupalgap_item_list_populate(list_css_selector, items) {
  try {
    // @todo - This could use some validation and alerts for improper input.
    $(list_css_selector).html('');
    for (var i = 0; i < items.length; i++) {
      $(list_css_selector).append($('<li></li>', { html: items[i] }));
    }
    $(list_css_selector).listview('refresh').listview();
  }
  catch (error) { console.log('drupalgap_item_list_populate - ' + error); }
}

/**
 * Given an html table element id and an array of rows, this will clear the
 * table, populate it with the rows, and then refresh the table.
 * @param {String} table_css_selector
 * @param {Array} rows
 * rows follow the.
 */
function drupalgap_table_populate(table_css_selector, rows) {
  try {
    // Select only the body. Other things are already setup
    table_css_selector = table_css_selector + '> tbody ';
    $(table_css_selector).html('');
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var rowhtml = '';
      for (var j = 0; j < row.length; j++) {
          rowhtml = rowhtml + '<td>' + row[j] + '</td>';
      }
      $('<tr>').html(rowhtml).appendTo($(table_css_selector));
    }
    $(table_css_selector).rebuild();
  }
  catch (error) { console.log('drupalgap_table_populate - ' + error); }
}

/**
 * Given a jQM page event, and the corresponding callback function name that
 * handles the event, this function will call the callback function, if it has
 * not already been called on the current page. This really is only used by
 * menu_execute_active_handler() to prevent jQM from firing inline page event
 * handlers more than once. You may optionally pass in a 4th argument, a string,
 * to append to the suffix of the unique key of recorded fired page events.
 * @param {String} event
 * @param {String} callback
 * @param {*} page_arguments
 */
function drupalgap_jqm_page_event_fire(event, callback, page_arguments) {
  try {
    // Concatenate the event name and the callback name together into a unique
    // key so multiple callbacks can handle the same event.
    var key = event + '-' + callback;
    // Is there an optional 4th argument coming in (the suffix)?
    if (typeof arguments[3] !== 'undefined') {
      if (arguments[3]) { key += '-' + arguments[3]; }
    }
    if ($.inArray(key, drupalgap.page.jqm_events) == -1 &&
      function_exists(callback)) {
      drupalgap.page.jqm_events.push(key);
      var fn = window[callback];
      if (page_arguments) {
        // If the page arguments aren't an array, place them into an array so
        // they can be applied to the callback function.
        if (!$.isArray(page_arguments)) { page_arguments = [page_arguments]; }
        fn.apply(null, Array.prototype.slice.call(page_arguments));
      }
      else { fn(); }
    }
  }
  catch (error) { console.log('drupalgap_jqm_page_event_fire - ' + error); }
}

/**
 * Returns array of jQM Page event names.
 * @return {Array}
 * @see http://api.jquerymobile.com/category/events/
 */
function drupalgap_jqm_page_events() {
  return [
    'pagebeforechange',
    'pagebeforecreate',
    'pagebeforehide',
    'pagebeforeload',
    'pagebeforeshow',
    'pagechange',
    'pagechangefailed',
    'pagecreate',
    'pagehide',
    'pageinit',
    'pageload',
    'pageloadfailed',
    'pageremove',
    'pageshow'
  ];
}

/**
 * Given a JSON object with a page id, a jQM page event name, a callback
 * function to handle the jQM page event and any page arguments (as a JSON
 * string), this function will return the inline JS code needed to handle the
 * event. You may optionally pass in a unique second argument (string) to
 * allow the same page event handler to be fired more than once on a page.
 * @param {Object} options
 * @return {String}
 */
function drupalgap_jqm_page_event_script_code(options) {
  try {
    if (!options.page_id) { options.page_id = drupalgap_get_page_id(); }
    if (!options.jqm_page_event) { options.jqm_page_event = 'pageshow'; }
    // Build the arguments to send to the event fire handler.
    var event_fire_args = '"' + options.jqm_page_event + '", "' +
      options.jqm_page_event_callback + '", ' +
      options.jqm_page_event_args;
    if (arguments[1]) { event_fire_args += ', "' + arguments[1] + '"'; }
    // Build the inline JS and return it.
    return '<script type="text/javascript">' +
      '$("#' + options.page_id + '").on("' +
        options.jqm_page_event + '", drupalgap_jqm_page_event_fire(' +
        event_fire_args +
      '));' +
    '</script>';
  }
  catch (error) {
    console.log('drupalgap_jqm_page_event_script_code - ' + error);
  }
}

/**
 * Returns the suggested max width for elements within the content area.
 * @return {Number}
 */
function drupalgap_max_width() {
  try {
    var padding = parseInt($('.ui-content').css('padding'));
    if (isNaN(padding)) { padding = 16; } // use a 16px default if needed
    return $(document).width() - padding * 2;
  }
  catch (error) { console.log('drupalgap_max_width - ' + error); }
}

/**
 * Checks to see if the current user has access to the given path. Returns true
 * if the user has access, false otherwise. You may optionally pass in a user
 * account object as the second argument to check access on a specific user.
 * Also, you may optionally pass in an entity object as the third argument, if
 * that entity needs to be passed along to an 'access_callback' handler.
 * @param {String} routerPath The router path of the destination.
 * @param {String} path The destination path.
 * @return {Boolean}
 */
function drupalgap_menu_access(routerPath, path) {
  try {

    // User #1 is allowed to do anything, I mean anything.
    if (Drupal.user.uid == 1) { return true; }
    // Everybody else will not have access unless we prove otherwise.
    var access = false;
    if (drupalgap.menu_links[routerPath]) {
      // Check to see if there is an access callback specified with the menu
      // link.
      if (typeof drupalgap.menu_links[routerPath].access_callback === 'undefined') {
        // No access call back specified, if there are any access arguments
        // on the menu link, then it is assumed they are user permission machine
        // names, so check that user account's role(s) for that permission to
        // grant access.
        if (drupalgap.menu_links[routerPath].access_arguments) {
          if ($.isArray(drupalgap.menu_links[routerPath].access_arguments)) {
            for (var index in drupalgap.menu_links[routerPath].access_arguments) {
              if (!drupalgap.menu_links[routerPath].access_arguments.hasOwnProperty(index)) { continue; }
              var permission = drupalgap.menu_links[routerPath].access_arguments[index];
              access = user_access(permission);
              if (access) { break; }
            }
          }
        }
        else {
          // There is no access callback and no access arguments specified with
          // the menu link, so we'll assume everyone has access.
          access = true;
        }
      }
      else {

        // An access callback function is specified for this routerPath...
        var function_name = drupalgap.menu_links[routerPath].access_callback;
        if (function_exists(function_name)) {
          // Grab the access callback function. If there are any access args
          // send them along, or just call the function directly.
          // access arguments.
          var fn = window[function_name];
          if (drupalgap.menu_links[routerPath].access_arguments) {
            var access_arguments =
              drupalgap.menu_links[routerPath].access_arguments.slice(0);
            // If we have an entity loaded, replace the first integer we find
            // in the page arguments with the loaded entity.
            if (arguments[2]) {
              var entity = arguments[2];
              for (var index in access_arguments) {
                  if (!access_arguments.hasOwnProperty(index)) { continue; }
                  var page_argument = access_arguments[index];
                  if (is_int(parseInt(page_argument))) {
                    access_arguments[index] = entity;
                    break;
                  }
              }
            }
            else {
              // Replace any integer arguments with the corresponding path argument.
              for (var i = 0; i < access_arguments.length; i++) {
                if (is_int(access_arguments[i])) { access_arguments[i] = arg(i, path); }
              }
            }
            return fn.apply(null, Array.prototype.slice.call(access_arguments));
          }
          else { return fn(); }
        }
        else {
          console.log('drupalgap_menu_access - access call back (' +
            function_name + ') does not exist'
          );
        }
      }
    }
    else {
      console.log('drupalgap_menu_access - routerPath (' + routerPath + ') does not exist');
    }
    return access;
  }
  catch (error) { console.log('drupalgap_menu_access - ' + error); }
}

/**
 * @deprecated Use module_load() instead.
 * @param {String} name
 * @return {Object}
 */
function drupalgap_module_load(name) {
  try {
    return module_load(name);
  }
  catch (error) { console.log('drupalgap_module_load - ' + error); }
}

/**
 * Given a module bundle type, this will return the path to that module bundle's
 * directory.
 * @param {String} bundle
 * @return {String}
 */
function drupalgap_modules_get_bundle_directory(bundle) {
  try {
    dir = '';
    if (bundle == 'core') { dir = 'modules'; }
    else if (bundle == 'contrib') { dir = 'app/modules'; }
    else if (bundle == 'custom') { dir = 'app/modules/custom'; }
    return dir;
  }
  catch (error) {
    console.log('drupalgap_modules_get_bundle_directory - ' + error);
  }
}

/**
 * Given a router path (and optional path, defaults to current drupalgap path if
 * one isn't provided), this takes the path's arguments and replaces any
 * wildcards (%) in the router path with the corresponding path argument(s). It
 * then returns the assembled path. Returns false otherwise.
 * @param {String} input_path
 * @return {*}
 */
function drupalgap_place_args_in_path(input_path) {
  try {
    var assembled_path = false;
    if (input_path) {

      // Determine path to use and break it up into its args.
      var path = drupalgap_path_get();
      if (arguments[1]) { path = arguments[1]; }
      var path_args = arg(null, path);

      // Grab wild cards from router path then replace each wild card with
      // the corresponding path arg.
      var wildcards;
      var input_path_args = arg(null, input_path);
      if (input_path_args && input_path_args.length > 0) {
        for (var index in input_path_args) {
            if (!input_path_args.hasOwnProperty(index)) { continue; }
            var _arg = input_path_args[index];
            if (_arg == '%') {
              if (!wildcards) { wildcards = []; }
              wildcards.push(index);
            }
        }
        if (wildcards && wildcards.length > 0) {
          for (var index in wildcards) {
              if (!wildcards.hasOwnProperty(index)) { continue; }
              var wildcard = wildcards[index];
              if (path_args[wildcard]) {
                input_path_args[wildcard] = path_args[wildcard];
              }
          }
          assembled_path = input_path_args.join('/');
        }
      }
    }
    return assembled_path;
  }
  catch (error) {
    console.log('drupalgap_place_args_in_path - ' + error);
  }
}

/**
 * Given an args array, this returns true if the path in the array will have an
 * entity (id) present in it.
 * @param {Array} args
 * @return {Boolean}
 */
function drupalgap_path_has_entity_arg(args) {
  try {
    if (args.length > 1 &&
        (
          args[0] == 'comment' ||
          args[0] == 'file' ||
          args[0] == 'node' ||
          (args[0] == 'taxonomy' &&
            (args[1] == 'vocabulary' || args[1] == 'term')
          ) ||
          args[0] == 'user' ||
          args[0] == 'item'
        )
    ) { return true; }
    return false;
  }
  catch (error) { console.log('drupalgap_path_has_entity_arg - ' + error); }
}

/**
 * Given a page id, this will remove it from the DOM.
 * @param {String} page_id
 */
function drupalgap_remove_page_from_dom(page_id) {
  try {
    $('#' + page_id).empty().remove();
  }
  catch (error) { console.log('drupalgap_remove_page_from_dom - ' + error); }
}

/**
 * Restart the app.
 */
function drupalgap_restart() {
  try {
    location.reload();
  }
  catch (error) { console.log('drupalgap_restart - ' + error); }
}

/**
 * Implementation of drupal_set_title().
 * @param {String} title
 */
function drupalgap_set_title(title) {
  try {
    if (title) { drupalgap.page.title = title; }
  }
  catch (error) { console.log('drupalgap_set_title - ' + error); }
}

/**
 * Returns true if the loader spinner is enabled, false otherwise. Defaults to true if no config for it is present.
 * @returns {Boolean}
 */
function drupalgap_loader_enabled() {
  if (!drupalgap.settings.loader) { drupalgap.settings.loader = {}; }
  return typeof drupalgap.settings.loader.enabled !== 'undefined' ?
      drupalgap.settings.loader.enabled : true;
}

/**
 * Toggle on or off the loader spinner, send true to turn it on, false to turn it off.
 * @param {Boolean} enable
 */
function drupalgap_loader_enable(enable) {
  drupalgap.settings.loader.enabled = enable;
}

/**
 * Implements hook_services_preprocess().
 * @param {Object} options
 */
function drupalgap_services_preprocess(options) {
  if (drupalgap_loader_enabled()) { drupalgap_loading_message_show(); }
}

/**
 * Implements hook_services_postprocess().
 * @param {Object} options
 * @param {Object} result
 */
function drupalgap_services_postprocess(options, result) {
  if (drupalgap_loader_enabled()) { drupalgap_loading_message_hide(); }
}

/**
 * Implements hook_services_request_pre_postprocess_alter().
 * @param {Object} options
 * @param {*} result
 */
function drupalgap_services_request_pre_postprocess_alter(options, result) {
  try {
    // Extract drupalgap system connect service resource results.
    if (options.service == 'system' && options.resource == 'connect') {
      drupalgap.remote_addr = result.remote_addr;
      drupalgap.entity_info = result.entity_info;
      drupalgap.field_info_instances = result.field_info_instances;
      drupalgap.field_info_fields = result.field_info_fields;
      drupalgap.field_info_extra_fields = result.field_info_extra_fields;
      drupalgap.taxonomy_vocabularies =
        drupalgap_taxonomy_vocabularies_extract(
          result.taxonomy_vocabularies
        );
      drupalgap_service_resource_extract_results({
        service: options.service,
        resource: options.resource,
        data: result
      });
    }
    // Whenever a user logs in, out or registers, remove all pages from the DOM.
    else if (options.service == 'user' &&
      (options.resource == 'logout' || options.resource == 'login' ||
        options.resource == 'register')) {
      drupalgap_remove_pages_from_dom();
    }
    // Whenever an entity is created, updated or deleted, remove the
    // corresponing DrupalGap core page(s) from the DOM so the pages will be
    // rebuilt properly next time they are loaded.
    else if (
      in_array(options.resource, ['create', 'update', 'delete']) &&
      in_array(options.service, entity_types())
    ) {
      var entity_type = options.entity_type;
      var entity_id = options.entity_id;
      var bundle = options.bundle || null;
      var paths = [];
      if (options.resource != 'create') {
        var prefix = entity_type;
        if (in_array(entity_type, ['taxonomy_vocabulary', 'taxonomy_term'])) {
          prefix = prefix.replace('_', '/', prefix);
        }
        paths.push(prefix + '/' + entity_id);
        paths.push(prefix + '/' + entity_id + '/view');
        // @todo This page won't get removed since it is the current page...
        // maybe when an entity is updated or deleted, we need a transitional
        // page that says "Deleting [entity]..." that way we can remove this
        // page (since it is not possible to remove the current page in jQM).
        // Actually now that I think about, we should have a confirmation page
        // when deleting an entity just like Drupal, and that should take care
        // of the deletion case. Not sure what to do about the update case...
        // maybe some type of pageshow handler on entity view pages that can
        // remove the edit form for the entity.
        paths.push(prefix + '/' + entity_id + '/edit');
      }
      else {
        switch (entity_type) {
          case 'node':
            // @todo This page won't get removed since it is the current page...
            paths.push('node/add/' + bundle);
            break;
        }
      }
      // Add extras depending on the entity type.
      switch (entity_type) {
        case 'node': paths.push('node'); break;
        case 'taxonomy_vocabulary': paths.push('taxonomy/vocabularies'); break;
        case 'user': paths.push('user-listing'); break;
      }
      // Convert the paths to page ids, then remove them from the DOM.
      var pages = [];
      for (var index in paths) {
          if (!paths.hasOwnProperty(index)) { continue; }
          var path = paths[index];
          pages.push(drupalgap_get_page_id(path));
      }
      for (var index in pages) {
          if (!pages.hasOwnProperty(index)) { continue; }
          var page_id = pages[index];
          drupalgap_remove_page_from_dom(page_id);
      }
    }
  }
  catch (error) {
    console.log('drupalgap_services_request_pre_postprocess_alter - ' + error);
  }
}

/**
 * @deprecated - Loads the settings specified in app/settings.js into the app.
 */
function drupalgap_settings_load() {
  try {
    console.log('WARNING: drupalgap_settings_load() is deprecated!');
    //drupal_settings_load();
  }
  catch (error) {
    console.log('drupalgap_settings_load - ' + error);
  }
}

/**
 * This calls all implementations of hook_theme and builds the DrupalGap theme
 * registry.
 */
function drupalgap_theme_registry_build() {
  try {
    var modules = module_implements('theme');
    for (var index in modules) {
        if (!modules.hasOwnProperty(index)) { continue; }
        var module = modules[index];
        var function_name = module + '_theme';
        var fn = window[function_name];
        var hook_theme = fn();
        for (var element in hook_theme) {
            if (!hook_theme.hasOwnProperty(element)) { continue; }
            var variables = hook_theme[element];
            variables.path = drupalgap_get_path(
              'theme',
              drupalgap.settings.theme
            );
            drupalgap.theme_registry[element] = variables;
        }
    }
  }
  catch (error) { console.log('drupalgap_theme_registry_build - ' + error); }
}

/**
 * Given a variable name and value, this will save the value to local storage,
 * keyed by its name.
 * @param {String} name
 * @param {*} value
 * @return {*}
 */
function variable_set(name, value) {
  try {
    if (!value) { value = ' '; } // store null values as a single space*
    else if (is_int(value)) { value = value.toString(); }
    else if (typeof value === 'object') { value = JSON.stringify(value); }
    return window.localStorage.setItem(name, value);
    // * phonegap won't store an empty string in local storage
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Given a variable name and a default value, this will first attempt to load
 * the variable from local storage, if it can't then the default value will be
 * returned.
 * @param {String} name
 * @param {*} default_value
 * @return {*}
 */
function variable_get(name, default_value) {
  try {
    var value = window.localStorage.getItem(name);
    if (!value) { value = default_value; }
    if (value == ' ') { value = ''; } // Convert single spaces to empty strings.
    return value;
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Given a variable name, this will remove the value from local storage.
 * @param {String} name
 * @return {*}
 */
function variable_del(name) {
  try {
    return window.localStorage.removeItem(name);
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Returns the current time as a string with the format: "yyyy-mm-dd hh:mm:ss".
 * @return {String}
 */
function date_yyyy_mm_dd_hh_mm_ss() {
  try {
    var result;
    if (arguments[0]) { result = arguments[0]; }
    else { result = date_yyyy_mm_dd_hh_mm_ss_parts(); }
    return result['year'] + '-' + result['month'] + '-' + result['day'] + ' ' +
           result['hour'] + ':' + result['minute'] + ':' + result['second'];
  }
  catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss - ' + error); }
}

/**
 * Returns an array with the parts for the current time. You may optionally
 * pass in a JS date object to use that date instead.
 * @return {Array}
 */
function date_yyyy_mm_dd_hh_mm_ss_parts() {
  try {
    var result = [];
    var now = null;
    if (arguments[0]) { now = arguments[0]; }
    else { now = new Date(); }
    var year = '' + now.getFullYear();
    var month = '' + (now.getMonth() + 1);
    if (month.length == 1) { month = '0' + month; }
    var day = '' + now.getDate();
    if (day.length == 1) { day = '0' + day; }
    var hour = '' + now.getHours();
    if (hour.length == 1) { hour = '0' + hour; }
    var minute = '' + now.getMinutes();
    if (minute.length == 1) { minute = '0' + minute; }
    var second = '' + now.getSeconds();
    if (second.length == 1) { second = '0' + second; }
    result['year'] = year;
    result['month'] = month;
    result['day'] = day;
    result['hour'] = hour;
    result['minute'] = minute;
    result['second'] = second;
    return result;
  }
  catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss_parts - ' + error); }
}

/**
 * Given a year and month (0-11), this will return the number of days in that
 * month.
 * @see http://stackoverflow.com/a/1810990/763010
 * @param {Number} year
 * @param {Number} month
 * @return {Number}
 */
function date_number_of_days_in_month(year, month) {
  try {
    var d = new Date(year, month, 0);
    return d.getDate();
  }
  catch (error) { console.log('date_number_of_days_in_month - ' + error); }
}

/**
 * @see http://www.dconnell.co.uk/blog/index.php/2012/03/12/scroll-to-any-element-using-jquery/
 */
function scrollToElement(selector, time, verticalOffset) {
  try {
    time = typeof(time) != 'undefined' ? time : 1000;
    verticalOffset = typeof(verticalOffset) != 'undefined' ? verticalOffset : 0;
    element = $(selector);
    offset = element.offset();
    offsetTop = offset.top + verticalOffset;
    $('html, body').animate({
        scrollTop: offsetTop
    }, time);
  }
  catch (error) { console.log('scrollToElement - ' + error); }
}