<commit_before>/*global require, exports */

////////////////////////////////////////////////////////////////////////////////
/// @brief ArangoDB Application Launcher Utilities
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2013 triAGENS GmbH, Cologne, Germany
///
/// 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
///
///     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.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Jan Steemann
/// @author Dr. Frank Celler
/// @author Michael Hackstein
/// @author Copyright 2014, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var fs = require("fs");
var arangodb = require("org/arangodb");
var db = arangodb.db;
var download = require("internal").download;

var throwFileNotFound = arangodb.throwFileNotFound;
var throwDownloadError = arangodb.throwDownloadError;
var errors = arangodb.errors;
var ArangoError = arangodb.ArangoError;
var mountRegEx = /^(\/[a-zA-Z0-9_\-%]+)+$/;
var mountAppRegEx = /\/APP(\/|$)/i;

var getStorage = function() {
  "use strict";
  var c = db._apps;
  if (c === undefined) {
    c = db._create("_apps", {isSystem: true});
    c.ensureUniqueConstraint("mount");
  }
  return c;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief comparator for mount points
////////////////////////////////////////////////////////////////////////////////

var compareMounts = function(l, r) {
  "use strict";
  var left = l.mount.toLowerCase();
  var right = r.mount.toLowerCase();

  if (left < right) {
    return -1;
  }
  return 1;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief builds a github repository URL
////////////////////////////////////////////////////////////////////////////////

function buildGithubUrl (repository, version) {
  'use strict';

  if (version === undefined) {
    version = "master";
  }

  return 'https://github.com/' + repository + '/archive/' + version + '.zip';
}

////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a manifest file
////////////////////////////////////////////////////////////////////////////////

function extractNameAndVersionManifest (source, filename) {
  'use strict';

  var manifest = JSON.parse(fs.read(filename));

  source.name = manifest.name;
  source.version = manifest.version;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief processes files in a directory
////////////////////////////////////////////////////////////////////////////////

function processDirectory (source) {
  'use strict';

  var location = source.location;

  if (! fs.exists(location) || ! fs.isDirectory(location)) {
    throwFileNotFound("'" + String(location) + "' is not a directory");
  }

  // .............................................................................
  // extract name and version from manifest
  // .............................................................................

  extractNameAndVersionManifest(source, fs.join(location, "manifest.json"));

  // .............................................................................
  // extract name and version from manifest
  // .............................................................................

  var tree = fs.listTree(location);
  var files = [];
  var i;
  var filename;

  for (i = 0;  i < tree.length;  ++i) {
    filename = fs.join(location, tree[i]);

    if (fs.isFile(filename)) {
      files.push(tree[i]);
    }
  }

  if (files.length === 0) {
    throwFileNotFound("Directory '" + String(location) + "' is empty");
  }

  var tempFile = fs.getTempFile("downloads", false);
  source.filename = tempFile;
  source.removeFile = true;

  fs.zipFile(tempFile, location, files);
  return tempFile;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a zip
////////////////////////////////////////////////////////////////////////////////

function repackZipFile (source) {
  'use strict';

  var i;

  var filename = source.filename;
  var path = fs.getTempFile("zip", false);

  fs.makeDirectory(path);
  fs.unzipFile(filename, path, false, true);

  // .............................................................................
  // locate the manifest file
  // .............................................................................

  var tree = fs.listTree(path).sort(function(a,b) {
    return a.length - b.length;
  });
  var found;
  var mf = "manifest.json";
  var re = /[\/\\\\]manifest\.json$/; // Windows!
  var tf;

  for (i = 0;  i < tree.length && found === undefined;  ++i) {
    tf = tree[i];

    if (re.test(tf) || tf === mf) {
      found = tf;
    }
  }

  if (found === undefined) {
    throwFileNotFound("Cannot find manifest file in zip file '" + filename + "'");
  }

  var mp;

  if (found === mf) {
    mp = ".";
  }
  else {
    mp = found.substr(0, found.length - mf.length - 1);
  }

  // .............................................................................
  // throw away source file if necessary
  // .............................................................................

  if (source.removeFile && source.filename !== '') {
    try {
      fs.remove(source.filename);
    }
    catch (err1) {
      arangodb.printf("Cannot remove temporary file '%s'\n", source.filename);
    }
  }

  delete source.filename;
  delete source.removeFile;

  // .............................................................................
  // repack the zip file
  // .............................................................................

  var newSource = { location: fs.join(path, mp) };

  processDirectory(newSource);

  source.name = newSource.name;
  source.version = newSource.version;
  source.filename = newSource.filename;
  source.removeFile = newSource.removeFile;

  // .............................................................................
  // cleanup temporary paths
  // .............................................................................

  try {
    fs.removeDirectoryRecursive(path);
  }
  catch (err2) {
    arangodb.printf("Cannot remove temporary directory '%s'\n", path);
  }
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes files from a github repository
////////////////////////////////////////////////////////////////////////////////

function processGithubRepository (source) {
  'use strict';

  var url = buildGithubUrl(source.location, source.version);
  var tempFile = fs.getTempFile("downloads", false);

  try {
    var result = download(url, "", {
      method: "get",
      followRedirects: true,
      timeout: 30
    }, tempFile);

    if (result.code >= 200 && result.code <= 299) {
      source.filename = tempFile;
      source.removeFile = true;
    }
    else {
      throwDownloadError("Could not download from repository '" + url + "'");
    }
  }
  catch (err) {
    throwDownloadError("Could not download from repository '" + url + "': " + String(err));
  }

  repackZipFile(source);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief returns all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function listJson (showPrefix, onlyDevelopment) {
  'use strict';

  var mounts = getStorage();
  var cursor;
  if (onlyDevelopment) {
    cursor = mounts.byExample({isDevelopment: true});
  } else {
    cursor = mounts.all();
  }
  var result = [];
  var doc, res;

  while (cursor.hasNext()) {
    doc = cursor.next();

    res = {
      mountId: doc._key,
      mount: doc.mount,
      name: doc.name,
      description: doc.manifest.description,
      author: doc.manifest.author,
      system: doc.isSystem || false,
      development: doc.isDevelopment || false,
      version: doc.version
    };

    if (showPrefix) {
      res.collectionPrefix = doc.options.collectionPrefix;
    }

    result.push(res);
  }

  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief prints all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function list(onlyDevelopment) {
  "use strict";
  var apps = listJson(undefined, onlyDevelopment);

  arangodb.printTable(
    apps.sort(compareMounts),
    [ "mount", "name", "author", "description", "version", "development" ],
    {
      prettyStrings: true,
      totalString: "%s application(s) found",
      emptyString: "no applications found",
      rename: {
        "mount": "Mount",
        "name" : "Name",
        "author" : "Author",
        "description" : "Description",
        "version" : "Version",
        "development" : "Development"
      }
    }
  );
}

////////////////////////////////////////////////////////////////////////////////
/// @brief returns all running Foxx applications in development mode
////////////////////////////////////////////////////////////////////////////////

function listDevelopmentJson (showPrefix) {
  'use strict';
  return listJson(showPrefix, true);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief prints all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function listDevelopment() {
  'use strict';
  return list(true);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief validate the mount point of an app
////////////////////////////////////////////////////////////////////////////////

function validateMount(mount) {
  'use strict';
  if (mount[0] !== "/") {
    throw new Error("Mount point is invalid. It has to start with /.");
  }
  if (mount[1] === "_") {
    throw new Error("Mount point is invalid. /_ apps are reserved for internal use.");
  }
  if (!mountRegEx.test(mount)) {
    throw new Error("Mount point is invalid. It can only contain a-z, A-Z, 0-9 or _.");
  }
  if (mountAppRegEx.test(mount)) {
    throw new Error("Mount point is invalid. It is not allowed to contain /app/.");
  }
  return true;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief validate an app name and fail if it is invalid
////////////////////////////////////////////////////////////////////////////////

function validateAppName (name) {
  'use strict';

  if (typeof name === 'string' && name.length > 0) {
    return;
  }

  throw new ArangoError({
    errorNum: errors.ERROR_APPLICATION_INVALID_NAME.code,
    errorMessage: errors.ERROR_APPLICATION_INVALID_NAME.message
  });
}

////////////////////////////////////////////////////////////////////////////////
/// @brief get the app mounted at this mount point
////////////////////////////////////////////////////////////////////////////////
function mountedApp (mount) {
  "use strict";
  return getStorage().firstExample({mount: mount});
}

////////////////////////////////////////////////////////////////////////////////
/// @brief Update the app mounted at this mountpoint with the new app
////////////////////////////////////////////////////////////////////////////////
function updateApp (mount, update) {
  "use strict";
  return getStorage().updateByExample({mount: mount}, update);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief define regex for parameter types
////////////////////////////////////////////////////////////////////////////////
var typeToRegex = {
  "int": /[0-9]+/,
  "integer": /[0-9]+/,
  "boolean": /(true)|(false)/,
  "bool": /(true)|(false)/,
  "string": /[\w\W]*/
};

////////////////////////////////////////////////////////////////////////////////
/// @brief creates a zip archive of a foxx app. Returns the absolute path
////////////////////////////////////////////////////////////////////////////////
var zipDirectory = function(directory) {
  "use strict";
  if (!fs.isDirectory(directory)) {
    throw directory + " is not a directory.";
  }
  var tempFile = fs.getTempFile("zip", false);

  var tree = fs.listTree(directory);
  var files = [];
  var i;
  var filename;

  for (i = 0;  i < tree.length;  ++i) {
    filename = fs.join(directory, tree[i]);

    if (fs.isFile(filename)) {
      files.push(tree[i]);
    }
  }
  if (files.length === 0) {
    throwFileNotFound("Directory '" + String(directory) + "' is empty");
  }
  fs.zipFile(tempFile, directory, files);
  return tempFile;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief Exports
////////////////////////////////////////////////////////////////////////////////

exports.mountedApp = mountedApp;
exports.updateApp = updateApp;
exports.list = list;
exports.listJson = listJson;
exports.listDevelopment = listDevelopment;
exports.listDevelopmentJson = listDevelopmentJson;
exports.buildGithubUrl = buildGithubUrl;
exports.repackZipFile = repackZipFile;
exports.processDirectory = processDirectory;
exports.processGithubRepository = processGithubRepository;
exports.validateAppName = validateAppName;
exports.validateMount = validateMount;
exports.typeToRegex = typeToRegex;
exports.zipDirectory = zipDirectory;
exports.getStorage = getStorage;

// -----------------------------------------------------------------------------
// --SECTION--                                                       END-OF-FILE
// -----------------------------------------------------------------------------

/// Local Variables:
/// mode: outline-minor
/// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint"
/// End:
<commit_msg>Fixed a bug stopping ArangoDB to drop collections properly due to new foxx behaviour<commit_after>/*global require, exports */

////////////////////////////////////////////////////////////////////////////////
/// @brief ArangoDB Application Launcher Utilities
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2013 triAGENS GmbH, Cologne, Germany
///
/// 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
///
///     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.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Jan Steemann
/// @author Dr. Frank Celler
/// @author Michael Hackstein
/// @author Copyright 2014, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////

var fs = require("fs");
var arangodb = require("org/arangodb");
var db = arangodb.db;
var download = require("internal").download;

var throwFileNotFound = arangodb.throwFileNotFound;
var throwDownloadError = arangodb.throwDownloadError;
var errors = arangodb.errors;
var ArangoError = arangodb.ArangoError;
var mountRegEx = /^(\/[a-zA-Z0-9_\-%]+)+$/;
var mountAppRegEx = /\/APP(\/|$)/i;

var getStorage = function() {
  "use strict";
  var c = db._collection("_apps");
  if (c === null) {
    c = db._create("_apps", {isSystem: true});
    c.ensureUniqueConstraint("mount");
  }
  return c;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief comparator for mount points
////////////////////////////////////////////////////////////////////////////////

var compareMounts = function(l, r) {
  "use strict";
  var left = l.mount.toLowerCase();
  var right = r.mount.toLowerCase();

  if (left < right) {
    return -1;
  }
  return 1;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief builds a github repository URL
////////////////////////////////////////////////////////////////////////////////

function buildGithubUrl (repository, version) {
  'use strict';

  if (version === undefined) {
    version = "master";
  }

  return 'https://github.com/' + repository + '/archive/' + version + '.zip';
}

////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a manifest file
////////////////////////////////////////////////////////////////////////////////

function extractNameAndVersionManifest (source, filename) {
  'use strict';

  var manifest = JSON.parse(fs.read(filename));

  source.name = manifest.name;
  source.version = manifest.version;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief processes files in a directory
////////////////////////////////////////////////////////////////////////////////

function processDirectory (source) {
  'use strict';

  var location = source.location;

  if (! fs.exists(location) || ! fs.isDirectory(location)) {
    throwFileNotFound("'" + String(location) + "' is not a directory");
  }

  // .............................................................................
  // extract name and version from manifest
  // .............................................................................

  extractNameAndVersionManifest(source, fs.join(location, "manifest.json"));

  // .............................................................................
  // extract name and version from manifest
  // .............................................................................

  var tree = fs.listTree(location);
  var files = [];
  var i;
  var filename;

  for (i = 0;  i < tree.length;  ++i) {
    filename = fs.join(location, tree[i]);

    if (fs.isFile(filename)) {
      files.push(tree[i]);
    }
  }

  if (files.length === 0) {
    throwFileNotFound("Directory '" + String(location) + "' is empty");
  }

  var tempFile = fs.getTempFile("downloads", false);
  source.filename = tempFile;
  source.removeFile = true;

  fs.zipFile(tempFile, location, files);
  return tempFile;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief extracts the name and version from a zip
////////////////////////////////////////////////////////////////////////////////

function repackZipFile (source) {
  'use strict';

  var i;

  var filename = source.filename;
  var path = fs.getTempFile("zip", false);

  fs.makeDirectory(path);
  fs.unzipFile(filename, path, false, true);

  // .............................................................................
  // locate the manifest file
  // .............................................................................

  var tree = fs.listTree(path).sort(function(a,b) {
    return a.length - b.length;
  });
  var found;
  var mf = "manifest.json";
  var re = /[\/\\\\]manifest\.json$/; // Windows!
  var tf;

  for (i = 0;  i < tree.length && found === undefined;  ++i) {
    tf = tree[i];

    if (re.test(tf) || tf === mf) {
      found = tf;
    }
  }

  if (found === undefined) {
    throwFileNotFound("Cannot find manifest file in zip file '" + filename + "'");
  }

  var mp;

  if (found === mf) {
    mp = ".";
  }
  else {
    mp = found.substr(0, found.length - mf.length - 1);
  }

  // .............................................................................
  // throw away source file if necessary
  // .............................................................................

  if (source.removeFile && source.filename !== '') {
    try {
      fs.remove(source.filename);
    }
    catch (err1) {
      arangodb.printf("Cannot remove temporary file '%s'\n", source.filename);
    }
  }

  delete source.filename;
  delete source.removeFile;

  // .............................................................................
  // repack the zip file
  // .............................................................................

  var newSource = { location: fs.join(path, mp) };

  processDirectory(newSource);

  source.name = newSource.name;
  source.version = newSource.version;
  source.filename = newSource.filename;
  source.removeFile = newSource.removeFile;

  // .............................................................................
  // cleanup temporary paths
  // .............................................................................

  try {
    fs.removeDirectoryRecursive(path);
  }
  catch (err2) {
    arangodb.printf("Cannot remove temporary directory '%s'\n", path);
  }
}
////////////////////////////////////////////////////////////////////////////////
/// @brief processes files from a github repository
////////////////////////////////////////////////////////////////////////////////

function processGithubRepository (source) {
  'use strict';

  var url = buildGithubUrl(source.location, source.version);
  var tempFile = fs.getTempFile("downloads", false);

  try {
    var result = download(url, "", {
      method: "get",
      followRedirects: true,
      timeout: 30
    }, tempFile);

    if (result.code >= 200 && result.code <= 299) {
      source.filename = tempFile;
      source.removeFile = true;
    }
    else {
      throwDownloadError("Could not download from repository '" + url + "'");
    }
  }
  catch (err) {
    throwDownloadError("Could not download from repository '" + url + "': " + String(err));
  }

  repackZipFile(source);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief returns all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function listJson (showPrefix, onlyDevelopment) {
  'use strict';

  var mounts = getStorage();
  var cursor;
  if (onlyDevelopment) {
    cursor = mounts.byExample({isDevelopment: true});
  } else {
    cursor = mounts.all();
  }
  var result = [];
  var doc, res;
  while (cursor.hasNext()) {
    doc = cursor.next();

    res = {
      mountId: doc._key,
      mount: doc.mount,
      name: doc.name,
      description: doc.manifest.description,
      author: doc.manifest.author,
      system: doc.isSystem || false,
      development: doc.isDevelopment || false,
      version: doc.version
    };

    if (showPrefix) {
      res.collectionPrefix = doc.options.collectionPrefix;
    }

    result.push(res);
  }

  return result;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief prints all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function list(onlyDevelopment) {
  "use strict";
  var apps = listJson(undefined, onlyDevelopment);

  arangodb.printTable(
    apps.sort(compareMounts),
    [ "mount", "name", "author", "description", "version", "development" ],
    {
      prettyStrings: true,
      totalString: "%s application(s) found",
      emptyString: "no applications found",
      rename: {
        "mount": "Mount",
        "name" : "Name",
        "author" : "Author",
        "description" : "Description",
        "version" : "Version",
        "development" : "Development"
      }
    }
  );
}

////////////////////////////////////////////////////////////////////////////////
/// @brief returns all running Foxx applications in development mode
////////////////////////////////////////////////////////////////////////////////

function listDevelopmentJson (showPrefix) {
  'use strict';
  return listJson(showPrefix, true);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief prints all running Foxx applications
////////////////////////////////////////////////////////////////////////////////

function listDevelopment() {
  'use strict';
  return list(true);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief validate the mount point of an app
////////////////////////////////////////////////////////////////////////////////

function validateMount(mount) {
  'use strict';
  if (mount[0] !== "/") {
    throw new Error("Mount point is invalid. It has to start with /.");
  }
  if (mount[1] === "_") {
    throw new Error("Mount point is invalid. /_ apps are reserved for internal use.");
  }
  if (!mountRegEx.test(mount)) {
    throw new Error("Mount point is invalid. It can only contain a-z, A-Z, 0-9 or _.");
  }
  if (mountAppRegEx.test(mount)) {
    throw new Error("Mount point is invalid. It is not allowed to contain /app/.");
  }
  return true;
}

////////////////////////////////////////////////////////////////////////////////
/// @brief validate an app name and fail if it is invalid
////////////////////////////////////////////////////////////////////////////////

function validateAppName (name) {
  'use strict';

  if (typeof name === 'string' && name.length > 0) {
    return;
  }

  throw new ArangoError({
    errorNum: errors.ERROR_APPLICATION_INVALID_NAME.code,
    errorMessage: errors.ERROR_APPLICATION_INVALID_NAME.message
  });
}

////////////////////////////////////////////////////////////////////////////////
/// @brief get the app mounted at this mount point
////////////////////////////////////////////////////////////////////////////////
function mountedApp (mount) {
  "use strict";
  return getStorage().firstExample({mount: mount});
}

////////////////////////////////////////////////////////////////////////////////
/// @brief Update the app mounted at this mountpoint with the new app
////////////////////////////////////////////////////////////////////////////////
function updateApp (mount, update) {
  "use strict";
  return getStorage().updateByExample({mount: mount}, update);
}

////////////////////////////////////////////////////////////////////////////////
/// @brief define regex for parameter types
////////////////////////////////////////////////////////////////////////////////
var typeToRegex = {
  "int": /[0-9]+/,
  "integer": /[0-9]+/,
  "boolean": /(true)|(false)/,
  "bool": /(true)|(false)/,
  "string": /[\w\W]*/
};

////////////////////////////////////////////////////////////////////////////////
/// @brief creates a zip archive of a foxx app. Returns the absolute path
////////////////////////////////////////////////////////////////////////////////
var zipDirectory = function(directory) {
  "use strict";
  if (!fs.isDirectory(directory)) {
    throw directory + " is not a directory.";
  }
  var tempFile = fs.getTempFile("zip", false);

  var tree = fs.listTree(directory);
  var files = [];
  var i;
  var filename;

  for (i = 0;  i < tree.length;  ++i) {
    filename = fs.join(directory, tree[i]);

    if (fs.isFile(filename)) {
      files.push(tree[i]);
    }
  }
  if (files.length === 0) {
    throwFileNotFound("Directory '" + String(directory) + "' is empty");
  }
  fs.zipFile(tempFile, directory, files);
  return tempFile;
};

////////////////////////////////////////////////////////////////////////////////
/// @brief Exports
////////////////////////////////////////////////////////////////////////////////

exports.mountedApp = mountedApp;
exports.updateApp = updateApp;
exports.list = list;
exports.listJson = listJson;
exports.listDevelopment = listDevelopment;
exports.listDevelopmentJson = listDevelopmentJson;
exports.buildGithubUrl = buildGithubUrl;
exports.repackZipFile = repackZipFile;
exports.processDirectory = processDirectory;
exports.processGithubRepository = processGithubRepository;
exports.validateAppName = validateAppName;
exports.validateMount = validateMount;
exports.typeToRegex = typeToRegex;
exports.zipDirectory = zipDirectory;
exports.getStorage = getStorage;

// -----------------------------------------------------------------------------
// --SECTION--                                                       END-OF-FILE
// -----------------------------------------------------------------------------

/// Local Variables:
/// mode: outline-minor
/// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint"
/// End:
<|endoftext|>


<commit_before>function gameOne(state = {
  gameNumber:1,
  gameName:'Identify Errors',
  canGoBack:false,
  loading:true,
  aapb_link:null,
  media_url: null,
  phrases: [],
  metadata:{},
  currentTime:0,
  startTime:0,
  isPlaying:false,
  segment:0,
  endSegment:0,
  endOfRound:false,
  startSegment:0,
  gameScore:0,
  waiting:false,
  inGameTip:true
}, action) {
  switch(action.type){
    case 'GET_GAMEONE':
      return {...state,
        loading:true
      }
    case 'GET_GAMEONE_SUCCESS':
      return {...state,
        loading: false,
        aapb_link:action.data.aapb_link,
        media_url:action.data.media_url,
        phrases:action.data.phrases,
        metadata:action.data.metadata,
      }
    case 'SET_PHRASE_LIST':
      return {...state,
        phrases:action.newPhrases
      }
    case 'SET_CURRENTTIME':
      return {...state,
        currentTime:action.currentTime
      }
    case 'SET_STARTTIME':
      return {...state,
        startTime:action.startTime
      }
    case 'SET_ISPLAYING':
      return {...state,
        isPlaying:action.isPlaying
      }
    case 'UPDATE_GAME_SCORE':
      return {...state,
        gameScore:state.gameScore + action.amount
      }
    case 'RESET_GAME_SCORE':
      return {...state,
        gameScore:action.amount
      }
    case 'SET_SEGMENT_START':
      return {...state,
        startSegment:action.segmentStart
      }
    case 'SET_SEGMENT_END':
      return {...state,
        endSegment:action.segmentEnd
      }
    case 'ADVANCE_SEGMENT':
      return  {...state,
        segment:state.segment + action.progress
      }
    case 'WAITING_UPDATE':
      return  {...state,
        waiting:action.waiting
      }
    case 'SET_END_ROUND':
      return  {...state,
        endOfRound:action.endOfRound
      }
    case 'GOBACK_ROUND':
      return  {...state,
        segment:state.segment - action.progress
      }
    case 'RESET_ROUND':
      return  {...state,
        segment:action.progress
      }
    case 'MARK_INCORRECT':
      return  {...state.wrongPhrases,
        wrongPhrases:action.phrase
      }
    case 'UNMARK_PHRASE':
      return  {...state,
        wrongPhrases:action.phrase
      }
    case 'DISMISS_TIP_ONE':
      return  {...state,
        inGameTip:action.bool
      }
    default:
      return state;
  }
}
export default gameOne;<commit_msg>changed segment to start at 1<commit_after>function gameOne(state = {
  gameNumber:1,
  gameName:'Identify Errors',
  canGoBack:false,
  loading:true,
  aapb_link:null,
  media_url: null,
  phrases: [],
  metadata:{},
  currentTime:0,
  startTime:0,
  isPlaying:false,
  segment:1,
  endSegment:0,
  endOfRound:false,
  startSegment:0,
  gameScore:0,
  waiting:false,
  inGameTip:true
}, action) {
  switch(action.type){
    case 'GET_GAMEONE':
      return {...state,
        loading:true
      }
    case 'GET_GAMEONE_SUCCESS':
      return {...state,
        loading: false,
        aapb_link:action.data.aapb_link,
        media_url:action.data.media_url,
        phrases:action.data.phrases,
        metadata:action.data.metadata,
      }
    case 'SET_PHRASE_LIST':
      return {...state,
        phrases:action.newPhrases
      }
    case 'SET_CURRENTTIME':
      return {...state,
        currentTime:action.currentTime
      }
    case 'SET_STARTTIME':
      return {...state,
        startTime:action.startTime
      }
    case 'SET_ISPLAYING':
      return {...state,
        isPlaying:action.isPlaying
      }
    case 'UPDATE_GAME_SCORE':
      return {...state,
        gameScore:state.gameScore + action.amount
      }
    case 'RESET_GAME_SCORE':
      return {...state,
        gameScore:action.amount
      }
    case 'SET_SEGMENT_START':
      return {...state,
        startSegment:action.segmentStart
      }
    case 'SET_SEGMENT_END':
      return {...state,
        endSegment:action.segmentEnd
      }
    case 'ADVANCE_SEGMENT':
      return  {...state,
        segment:state.segment + action.progress
      }
    case 'WAITING_UPDATE':
      return  {...state,
        waiting:action.waiting
      }
    case 'SET_END_ROUND':
      return  {...state,
        endOfRound:action.endOfRound
      }
    case 'GOBACK_ROUND':
      return  {...state,
        segment:state.segment - action.progress
      }
    case 'RESET_ROUND':
      return  {...state,
        segment:action.progress
      }
    case 'MARK_INCORRECT':
      return  {...state.wrongPhrases,
        wrongPhrases:action.phrase
      }
    case 'UNMARK_PHRASE':
      return  {...state,
        wrongPhrases:action.phrase
      }
    case 'DISMISS_TIP_ONE':
      return  {...state,
        inGameTip:action.bool
      }
    default:
      return state;
  }
}
export default gameOne;<|endoftext|>


<commit_before>module.exports = {
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    [
      // Update versions in sub-packages dependencies
      "@google/semantic-release-replace-plugin",
      {
        "replacements": [
          {
            "files": ["packages/*/package.json"],
            "from": "\"mongodb-memory-server\": \".*\"",
            "to": "\"mongodb-memory-server\": \"${nextRelease.version}\"",
          },
          {
            "files": ["packages/*/package.json"],
            "from": "\"mongodb-memory-server-core\": \".*\"",
            "to": "\"mongodb-memory-server-core\": \"${nextRelease.version}\"",
          }
        ]
      }
    ],
    "@semantic-release/changelog",
    ["@semantic-release/git", {
      "assets": ["packages/*/package.json", "CHANGELOG.md"],
      "message": "chore: release v${nextRelease.version}"
    }],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-core',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-3.6',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-4.2',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-4.4',
      },
    ],
    "@semantic-release/github"
  ],
};
<commit_msg>chore(releaserc): move "@semantic-release/git" after all "npm"<commit_after>module.exports = {
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    [
      // Update versions in sub-packages dependencies
      "@google/semantic-release-replace-plugin",
      {
        "replacements": [
          {
            "files": ["packages/*/package.json"],
            "from": "\"mongodb-memory-server\": \".*\"",
            "to": "\"mongodb-memory-server\": \"${nextRelease.version}\"",
          },
          {
            "files": ["packages/*/package.json"],
            "from": "\"mongodb-memory-server-core\": \".*\"",
            "to": "\"mongodb-memory-server-core\": \"${nextRelease.version}\"",
          }
        ]
      }
    ],
    "@semantic-release/changelog",
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-core',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-3.6',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-4.2',
      },
    ],
    [
      '@semantic-release/npm',
      {
        npmPublish: true,
        pkgRoot: './packages/mongodb-memory-server-global-4.4',
      },
    ],
    ["@semantic-release/git", {
      "assets": ["packages/*/package.json", "CHANGELOG.md"],
      "message": "chore: release v${nextRelease.version}"
    }],
    "@semantic-release/github"
  ],
};
<|endoftext|>


<commit_before>'use strict';

/**
 * @ngdoc service
 * @name vegalite-ui.logger
 * @description
 * # logger
 * Service in the vegalite-ui.
 */
angular.module('vleApp')
  .service('Logger', function ($location, $window, $webSql, consts, Papa) {

    var service = {};

    // get user id once in the beginning
    var user = $location.search().user;

    service.db = $webSql.openDatabase('logs', '1.0', 'Logs', 2 * 1024 * 1024);

    service.tableName = 'log_' + consts.appId;

    service.actions = {
      UNDO: 'UNDO',
      REDO: 'REDO',
      DATASET_CHANGE: 'DATASET_CHANGE',
      CHART_MOUSEOVER: 'CHART_MOUSEOVER',
      CHART_MOUSEOUT: 'CHART_MOUSEOUT',
      CHART_RENDER: 'CHART_RENDER',
      TOOLTIP: 'TOOLTIP',
      BOOKMARK_ADD: 'BOOKMARK_ADD',
      BOOKMARK_REMOVE: 'BOOKMARK_REMOVE',
      BOOKMARKS_CLEAR: 'BOOKMARKS_CLEAR',
      NULL_FILTER_TOGGLE: 'NULL_FILTER_TOGGLE',
      TRANSPOSE_TOGGLE: 'TRANSPOSE_TOGGLE',
      SORT_TOGGLE: 'SORT_TOGGLE',
      // Polestar only
      SPEC_CHANGE: 'SPEC_CHANGE',
      // Voyager only
      FIELDS_CHANGE: 'FIELDS_CHANGE',
      FIELDS_RESET: 'FIELDS_RESET',
      CLUSTER_EXPAND: 'CLUSTER_EXPAND',
      CLUSTER_SELECT: 'CLUSTER_SELECT',
      LOAD_MORE: 'LOAD_MORE'
    }

    service.createTableIfNotExists = function() {
      service.db.createTable(service.tableName, {
        'userid':{
          'type': 'INTEGER',
          'null': 'NOT NULL'
        },
        'time':{
          'type': 'TIMESTAMP',
          'null': 'NOT NULL',
          'default': 'CURRENT_TIMESTAMP'
        },
        'action':{
          'type': 'TEXT',
          'null': 'NOT NULL'
        },
        'data': {
          'type': 'TEXT'
        },
        'diff': {
          'type': 'TEXT'
        }
      });
    }

    service.clear = function() {
      var r = $window.confirm('Really clear the logs?');
      if (r == true) {
        service.db.dropTable(service.tableName);
        service.createTableIfNotExists();
      }
    };

    service.export = function() {
      var data = service.db.selectAll(service.tableName).then(function(results) {
        if (results.rows.length === 0) {
          console.warn('No logs');
          return;
        }

        var rows = [];

        for(var i=0; i < results.rows.length; i++) {
          rows.push(results.rows.item(i));
        }
        var csv = Papa.unparse(rows);

        var element = angular.element('<a/>');
        element.attr({
          href: 'data:attachment/csv;charset=utf-8,' + encodeURI(csv),
          target: '_blank',
          download: service.tableName + '.csv'
        })[0].click();
      });
    }

    service.logInteraction = function(action, data, diff) {
      if (!consts.logging)
        return;

      console.log('[Logging] ', action, data);

      var row = {userid: user, action: action};
      if (data !== undefined) {
        row.data = JSON.stringify(data);
      }

      if (diff !== undefined) {
        row.diff = JSON.stringify(diff);
      }

      service.db.insert(service.tableName, row).then(function(results) {});
    };

    return service;
  });
<commit_msg>Update logger service<commit_after>'use strict';

/**
 * @ngdoc service
 * @name vegalite-ui.logger
 * @description
 * # logger
 * Service in the vegalite-ui.
 */
angular.module('vleApp')
  .service('Logger', function ($location, $window, $webSql, consts, Papa) {

    var service = {};

    // get user id once in the beginning
    var user = $location.search().user;

    service.db = $webSql.openDatabase('logs', '1.0', 'Logs', 2 * 1024 * 1024);

    service.tableName = 'log_' + consts.appId;

    service.actions = {
      UNDO: 'UNDO',
      REDO: 'REDO',
      DATASET_CHANGE: 'DATASET_CHANGE',
      CHART_MOUSEOVER: 'CHART_MOUSEOVER',
      CHART_MOUSEOUT: 'CHART_MOUSEOUT',
      CHART_RENDER: 'CHART_RENDER',
      CHART_EXPOSE: 'CHART_EXPOSE',
      CHART_TOOLTIP: 'CHART_TOOLTIP',
      BOOKMARK_ADD: 'BOOKMARK_ADD',
      BOOKMARK_REMOVE: 'BOOKMARK_REMOVE',
      BOOKMARKS_CLEAR: 'BOOKMARKS_CLEAR',

      NULL_FILTER_TOGGLE: 'NULL_FILTER_TOGGLE',
      TRANSPOSE_TOGGLE: 'TRANSPOSE_TOGGLE',
      SORT_TOGGLE: 'SORT_TOGGLE',
      MARKTYPE_TOGGLE: 'MARKTYPE_TOGGLE',
      // Polestar only
      SPEC_CHANGE: 'SPEC_CHANGE',
      // Voyager only
      FIELDS_CHANGE: 'FIELDS_CHANGE',
      FIELDS_RESET: 'FIELDS_RESET',
      DRILL_DOWN_OPEN: 'DRILL_DOWN_OPEN',
      DRILL_DOWN_CLOSE: 'DRILL_DOWN_CLOSE',
      CLUSTER_SELECT: 'CLUSTER_SELECT',
      LOAD_MORE: 'LOAD_MORE'
    };

    service.createTableIfNotExists = function() {
      service.db.createTable(service.tableName, {
        'userid':{
          'type': 'INTEGER',
          'null': 'NOT NULL'
        },
        'time':{
          'type': 'TIMESTAMP',
          'null': 'NOT NULL',
          'default': 'CURRENT_TIMESTAMP'
        },
        'action':{
          'type': 'TEXT',
          'null': 'NOT NULL'
        },
        'data': {
          'type': 'TEXT'
        },
        'diff': {
          'type': 'TEXT'
        }
      });
    };

    service.clear = function() {
      var r = $window.confirm('Really clear the logs?');
      if (r === true) {
        service.db.dropTable(service.tableName);
        service.createTableIfNotExists();
      }
    };

    service.export = function() {
      service.db.selectAll(service.tableName).then(function(results) {
        if (results.rows.length === 0) {
          console.warn('No logs');
          return;
        }

        var rows = [];

        for(var i=0; i < results.rows.length; i++) {
          rows.push(results.rows.item(i));
        }
        var csv = Papa.unparse(rows);

        var element = angular.element('<a/>');
        element.attr({
          href: 'data:attachment/csv;charset=utf-8,' + encodeURI(csv),
          target: '_blank',
          download: service.tableName + '.csv'
        })[0].click();
      });
    };

    service.logInteraction = function(action, data, diff) {
      if (!consts.logging) {
        return;
      }

      console.log('[Logging] ', action, data);

      var row = {userid: user, action: action};
      if (data !== undefined) {
        row.data = JSON.stringify(data);
      }

      if (diff !== undefined) {
        row.diff = JSON.stringify(diff);
      }

      service.db.insert(service.tableName, row).then(function(/*results*/) {});
    };

    return service;
  });
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2016 Glyptodon LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * Overrides $templateRequest such that HTML patches defined within Guacamole
 * extensions are automatically applied to template contents.
 */
angular.module('index').config(['$provide', function($provide) {
    $provide.decorator('$templateRequest', ['$delegate', '$injector',
            function decorateTemplateRequest($delegate, $injector) {

        // Required services
        var $q = $injector.get('$q');

        /**
         * A map of previously-returned promises from past calls to
         * $templateRequest(). Future calls to $templateRequest() will return
         * new promises chained to the first promise returned for a given URL,
         * rather than redo patch processing for every request.
         *
         * @type Object.<String, Promise.<String>>
         */
        var promiseCache = {};

        /**
         * Array of the raw HTML of all patches which should be applied to the
         * HTML of retrieved templates.
         *
         * @type String[]
         */
        var patches = [
            '<meta name="before" content="a">
HELLO BEFORE
',
            '<meta name="after" content="a">
HELLO AFTER
',
            '<meta name="replace" content="div.protocol">
:-)
'
        ];

        /**
         * Represents a single HTML patching operation which will be applied
         * to the raw HTML of a template. The name of the patching operation
         * MUST be one of the valid names defined within
         * PatchOperation.Operations.
         *
         * @constructor
         * @param {String} name
         *     The name of the patching operation that will be applied. Valid
         *     names are defined within PatchOperation.Operations.
         *
         * @param {String} selector
         *     The CSS selector which determines which elements within a
         *     template will be affected by the patch operation.
         */
        var PatchOperation = function PatchOperation(name, selector) {

            /**
             * Applies this patch operation to the template defined by the
             * given root element, which must be a single element wrapped by
             * JQuery.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template to which
             *     this patch operation should be applied.
             *
             * @param {Element[]} elements
             *     The elements which should be applied by the patch
             *     operation. For example, if the patch operation is inserting
             *     elements, these are the elements that will be inserted.
             */
            this.apply = function apply(root, elements) {
                PatchOperation.Operations[name](root, selector, elements);
            };

        };

        /**
         * Mapping of all valid patch operation names to their corresponding
         * implementations. Each implementation accepts the same three
         * parameters: the root element of the template being patched, the CSS
         * selector determining which elements within the template are patched,
         * and an array of elements which make up the body of the patch.
         *
         * @type Object.<String, Function>
         */
        PatchOperation.Operations = {

            /**
             * Inserts the given elements before the elements matched by the
             * provided CSS selector.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'before' : function before(root, selector, elements) {
                root.find(selector).before(elements);
            },

            /**
             * Inserts the given elements after the elements matched by the
             * provided CSS selector.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'after' : function after(root, selector, elements) {
                root.find(selector).after(elements);
            },

            /**
             * Replaces the elements matched by the provided CSS selector with
             * the given elements.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'replace' : function replace(root, selector, elements) {
                root.find(selector).replaceWith(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, before any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'before-children' : function beforeChildren(root, selector, elements) {
                root.find(selector).prepend(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, after any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'after-children' : function afterChildren(root, selector, elements) {
                root.find(selector).append(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, replacing any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'replace-children' : function replaceChildren(root, selector, elements) {
                root.find(selector).empty().append(elements);
            }

        };

        /**
         * Invokes $templateRequest() with all arguments exactly as provided,
         * applying all HTML patches from any installed Guacamole extensions
         * to the HTML of the requested template.
         *
         * @param {String} url
         *     The URL of the template being requested.
         *
         * @returns {Promise.<String>}
         *     A Promise which resolves with the patched HTML contents of the
         *     requested template if retrieval of the template is successful.
         */
        var decoratedTemplateRequest = function decoratedTemplateRequest(url) {

            var deferred = $q.defer();

            // Chain to cached promise if it already exists
            var cachedPromise = promiseCache[url];
            if (cachedPromise) {
                cachedPromise.then(deferred.resolve, deferred.reject);
                return deferred.promise;
            }

            // Resolve promise with patched template HTML
            $delegate.apply(this, arguments).then(function patchTemplate(data) {

                // Parse HTML into DOM tree
                var root = $('

').html(data);

                // Apply all defined patches
                angular.forEach(patches, function applyPatch(patch) {

                    var elements = $(patch);

                    // Filter out and parse all applicable meta tags
                    var operations = [];
                    elements = elements.filter(function filterMetaTags(index, element) {

                        // Leave non-meta tags untouched
                        if (element.tagName !== 'META')
                            return true;

                        // Only meta tags having a valid "name" attribute need
                        // to be filtered
                        var name = element.getAttribute('name');
                        if (!name || !(name in PatchOperation.Operations))
                            return true;

                        // The "content" attribute must be present for any
                        // valid "name" meta tag
                        var content = element.getAttribute('content');
                        if (!content)
                            return true;

                        // Filter out and parse meta tag
                        operations.push(new PatchOperation(name, content));
                        return false;

                    });

                    // Apply each operation implied by the meta tags
                    angular.forEach(operations, function applyOperation(operation) {
                        operation.apply(root, elements);
                    });

                });

                // Transform back into HTML
                deferred.resolve.call(this, root.html());

            }, deferred.reject);

            // Cache this promise for future results
            promiseCache[url] = deferred.promise;
            return deferred.promise;

        };

        return decoratedTemplateRequest;

    }]);
}]);<commit_msg>GUAC-1378: Evict cache of $templateRequest results when $templateCache is modified externally.<commit_after>/*
 * Copyright (C) 2016 Glyptodon LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * Overrides $templateRequest such that HTML patches defined within Guacamole
 * extensions are automatically applied to template contents. As the results of
 * $templateRequest are cached internally, $templateCache is also overridden to
 * update the internal cache as necessary.
 */
angular.module('index').config(['$provide', function($provide) {

    /**
     * A map of previously-returned promises from past calls to
     * $templateRequest(). Future calls to $templateRequest() will return
     * new promises chained to the first promise returned for a given URL,
     * rather than redo patch processing for every request.
     *
     * @type Object.<String, Promise.<String>>
     */
    var promiseCache = {};

    // Decorate $templateCache such that promiseCache is updated if a template
    // is modified within $templateCache at runtime
    $provide.decorator('$templateCache', ['$delegate',
            function decorateTemplateCache($delegate) {

        // Create shallow copy of original $templateCache which we can safely
        // override
        var decoratedTemplateCache = angular.extend({}, $delegate);

        /**
         * Overridden version of $templateCache.put() which automatically
         * invalidates the cached $templateRequest result when used. Only the
         * URL parameter is defined, as all other arguments, if any, will be
         * passed through to the original $templateCache.put() when the actual
         * put() operation is performed.
         *
         * @param {String} url
         *     The URL of the template whose entry is being updated.
         *
         * @return {Object}
         *     The value returned by $templateCache.put(), which is defined as
         *     being the value that was just stored at the provided URL.
         */
        decoratedTemplateCache.put = function put(url) {

            // Evict old cached $templateRequest() result
            delete promiseCache[url];

            // Continue with originally-requested put() operation
            return $delegate.put.apply(this, arguments);

        };

        return decoratedTemplateCache;

    }]);

    // Decorate $templateRequest such that it automatically applies any HTML
    // patches associated with installed Guacamole extensions
    $provide.decorator('$templateRequest', ['$delegate', '$injector',
            function decorateTemplateRequest($delegate, $injector) {

        // Required services
        var $q = $injector.get('$q');

        /**
         * Array of the raw HTML of all patches which should be applied to the
         * HTML of retrieved templates.
         *
         * @type String[]
         */
        var patches = [
            '<meta name="before" content="a">
HELLO BEFORE
',
            '<meta name="after" content="a">
HELLO AFTER
',
            '<meta name="replace" content="div.protocol">
:-)
'
        ];

        /**
         * Represents a single HTML patching operation which will be applied
         * to the raw HTML of a template. The name of the patching operation
         * MUST be one of the valid names defined within
         * PatchOperation.Operations.
         *
         * @constructor
         * @param {String} name
         *     The name of the patching operation that will be applied. Valid
         *     names are defined within PatchOperation.Operations.
         *
         * @param {String} selector
         *     The CSS selector which determines which elements within a
         *     template will be affected by the patch operation.
         */
        var PatchOperation = function PatchOperation(name, selector) {

            /**
             * Applies this patch operation to the template defined by the
             * given root element, which must be a single element wrapped by
             * JQuery.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template to which
             *     this patch operation should be applied.
             *
             * @param {Element[]} elements
             *     The elements which should be applied by the patch
             *     operation. For example, if the patch operation is inserting
             *     elements, these are the elements that will be inserted.
             */
            this.apply = function apply(root, elements) {
                PatchOperation.Operations[name](root, selector, elements);
            };

        };

        /**
         * Mapping of all valid patch operation names to their corresponding
         * implementations. Each implementation accepts the same three
         * parameters: the root element of the template being patched, the CSS
         * selector determining which elements within the template are patched,
         * and an array of elements which make up the body of the patch.
         *
         * @type Object.<String, Function>
         */
        PatchOperation.Operations = {

            /**
             * Inserts the given elements before the elements matched by the
             * provided CSS selector.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'before' : function before(root, selector, elements) {
                root.find(selector).before(elements);
            },

            /**
             * Inserts the given elements after the elements matched by the
             * provided CSS selector.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'after' : function after(root, selector, elements) {
                root.find(selector).after(elements);
            },

            /**
             * Replaces the elements matched by the provided CSS selector with
             * the given elements.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'replace' : function replace(root, selector, elements) {
                root.find(selector).replaceWith(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, before any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'before-children' : function beforeChildren(root, selector, elements) {
                root.find(selector).prepend(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, after any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'after-children' : function afterChildren(root, selector, elements) {
                root.find(selector).append(elements);
            },

            /**
             * Inserts the given elements within the elements matched by the
             * provided CSS selector, replacing any existing children.
             *
             * @param {Element[]} root
             *     The JQuery-wrapped root element of the template being
             *     patched.
             *
             * @param {String} selector
             *     The CSS selector which determines where this patch operation
             *     should be applied within the template defined by root.
             *
             * @param {Element[]} elements
             *     The contents of the patch which should be applied to the
             *     template defined by root at the locations selected by the
             *     given CSS selector.
             */
            'replace-children' : function replaceChildren(root, selector, elements) {
                root.find(selector).empty().append(elements);
            }

        };

        /**
         * Invokes $templateRequest() with all arguments exactly as provided,
         * applying all HTML patches from any installed Guacamole extensions
         * to the HTML of the requested template.
         *
         * @param {String} url
         *     The URL of the template being requested.
         *
         * @returns {Promise.<String>}
         *     A Promise which resolves with the patched HTML contents of the
         *     requested template if retrieval of the template is successful.
         */
        var decoratedTemplateRequest = function decoratedTemplateRequest(url) {

            var deferred = $q.defer();

            // Chain to cached promise if it already exists
            var cachedPromise = promiseCache[url];
            if (cachedPromise) {
                cachedPromise.then(deferred.resolve, deferred.reject);
                return deferred.promise;
            }

            // Resolve promise with patched template HTML
            $delegate.apply(this, arguments).then(function patchTemplate(data) {

                // Parse HTML into DOM tree
                var root = $('

').html(data);

                // Apply all defined patches
                angular.forEach(patches, function applyPatch(patch) {

                    var elements = $(patch);

                    // Filter out and parse all applicable meta tags
                    var operations = [];
                    elements = elements.filter(function filterMetaTags(index, element) {

                        // Leave non-meta tags untouched
                        if (element.tagName !== 'META')
                            return true;

                        // Only meta tags having a valid "name" attribute need
                        // to be filtered
                        var name = element.getAttribute('name');
                        if (!name || !(name in PatchOperation.Operations))
                            return true;

                        // The "content" attribute must be present for any
                        // valid "name" meta tag
                        var content = element.getAttribute('content');
                        if (!content)
                            return true;

                        // Filter out and parse meta tag
                        operations.push(new PatchOperation(name, content));
                        return false;

                    });

                    // Apply each operation implied by the meta tags
                    angular.forEach(operations, function applyOperation(operation) {
                        operation.apply(root, elements);
                    });

                });

                // Transform back into HTML
                deferred.resolve.call(this, root.html());

            }, deferred.reject);

            // Cache this promise for future results
            promiseCache[url] = deferred.promise;
            return deferred.promise;

        };

        return decoratedTemplateRequest;

    }]);
}]);<|endoftext|>


<commit_before>import fallback from '../system/fallback';
import isFunction from '../common/isFunction';
import isUndefined from '../common/isUndefined';
import {sendException} from './fshGa';
import {getLength, pop, push} from './sch';

var paused = true;
var message = 'fshMessage';

function taskRunner() {
  if (getLength() === 0) {
    paused = true;
  } else {
    paused = false;
    window.postMessage(message, '*');
  }
}

function devLog(args) {
  if (args && !Array.isArray(args)) {
    // eslint-disable-next-line no-console
    console.log('addTask Array.isArray(args)', Array.isArray(args));
  }
}

export default function add(priority, fn, args, scope) {
  //#if _DEV  //  Not sending args as Array
  devLog(args);
  //#endif
  if (isFunction(fn)) {
    var _scope = fallback(scope, window);
    var _args = fallback(args, []);
    push(fn.bind.apply(fn, [_scope].concat(_args)), priority);
    if (paused) {taskRunner();}
  }
}

function parseStack(e) {
  var concatStack = e.stack.replace(/\n +/g, '|');
  if (e.stack.includes(e.message)) {
    return concatStack;
  }
  return e.message + '|' + concatStack;
}

function parseError(e) {
  if (e.stack) {return parseStack(e);}
  return e.message;
}

function popError(fn) {
  if (!isUndefined(fn)) {
    sendException('pop() was not a function', false);
  }
}

function testPop() {
  var testFn = pop();
  if (isFunction(testFn)) {
    testFn();
  } else {popError(testFn);}
}

function asyncTask() {
  try {
    testPop();
  } catch (e) {
    sendException(parseError(e), false);
  } finally {
    taskRunner();
  }
}

function callback(event) {
  var key = event.data;
  if (typeof key === 'string' && key.indexOf(message) === 0) {
    asyncTask();
  }
}

window.addEventListener('message', callback);
<commit_msg>investigate pop() error<commit_after>import fallback from '../system/fallback';
import isFunction from '../common/isFunction';
import isUndefined from '../common/isUndefined';
import {sendException} from './fshGa';
import {getLength, pop, push} from './sch';

var paused = true;
var message = 'fshMessage';

function taskRunner() {
  if (getLength() === 0) {
    paused = true;
  } else {
    paused = false;
    window.postMessage(message, '*');
  }
}

function devLog(args) {
  if (args && !Array.isArray(args)) {
    // eslint-disable-next-line no-console
    console.log('addTask Array.isArray(args)', Array.isArray(args));
  }
}

export default function add(priority, fn, args, scope) {
  //#if _DEV  //  Not sending args as Array
  devLog(args);
  //#endif
  if (isFunction(fn)) {
    var _scope = fallback(scope, window);
    var _args = fallback(args, []);
    push(fn.bind.apply(fn, [_scope].concat(_args)), priority);
    if (paused) {taskRunner();}
  }
}

function parseStack(e) {
  var concatStack = e.stack.replace(/\n +/g, '|');
  if (e.stack.includes(e.message)) {
    return concatStack;
  }
  return e.message + '|' + concatStack;
}

function parseError(e) {
  if (e.stack) {return parseStack(e);}
  return e.message;
}

function popError(fn) {
  if (!isUndefined(fn)) {
    sendException('pop() was not a function (' + typeof fn + ')', false);
  }
}

function testPop() {
  var testFn = pop();
  if (isFunction(testFn)) {
    testFn();
  } else {popError(testFn);}
}

function asyncTask() {
  try {
    testPop();
  } catch (e) {
    sendException(parseError(e), false);
  } finally {
    taskRunner();
  }
}

function callback(event) {
  var key = event.data;
  if (typeof key === 'string' && key.indexOf(message) === 0) {
    asyncTask();
  }
}

window.addEventListener('message', callback);
<|endoftext|>