<commit_before>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community 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.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var fs = require('fs');
var readdirp = require('readdirp');
var watch = require('watch');

var IO = require('oae-util/lib/io');
var log = require('oae-logger').logger('static-files');
var Validator = require('oae-util/lib/validator').Validator

// Path to the 3akai-ux repository
var uiDirectory = null;

// A dictionary that will hold the content for each file. This will be lazy filled. The first
// time a particular file is requested, it will be cached. After that, the cached version will
// be used
var staticFileCache = {};

// A dictionary that will hold the widget configs for all widgets. This will be filled upon
// initialization.
var widgetConfigCache = {};


////////////////////
// Initialization //
////////////////////

/**
 * This will find all of the widget config files in the UI repository and cache. For development, 
 * it will also put watches on the files in the UI repository in order to invalidate the cache
 * when a file has been changed/removed.
 *
 * @param  {String}     uiDir       The path to the directory containing the UI repository.
 */
var init = module.exports.init = function(uiDir) {
    // Cache the ui directory path and make sure we have the absolute path
    uiDirectory = fs.realpathSync(uiDir);

    // Cache all of the widget config files
    cacheWidgetConfigs();

    // Monitor the UI repository for changes and refresh the cache. 
    // This will only be done in development mode
    if (process.env['NODE_ENV'] !== 'production') {
        watch.createMonitor(uiDirectory, {'ignoreDotFiles': true}, function(monitor) {
            monitor.on('created', updateFileCaches);
            monitor.on('changed', updateFileCaches);
            monitor.on('removed', updateFileCaches);
        });
    }
};

/**
 * When a file in the UI repository has changed, we update the static file cache by deleting
 * the record for that file if there is one. The next time the updated/created file will be
 * re-requested, caching will be attempted again. We also check if the changed file is a 
 * widget config file, and refresh the widget config cache if that's the case
 * 
 * @param  {String}     filename        The absolute path to the file that has been added/updated/deleted
 * @api private
 */
var updateFileCaches = function(filename) {
    filename = filename.replace(uiDirectory, '');
    // Delete the file from the static file cache, for it to be re-cached
    // when it is requested again
    delete staticFileCache[filename];
    // If the changed file is a widget config file, we re-cache the widget
    // config files
    if (/^\/node_modules\/(.*?)\/config.json$/.test(filename)) {
        cacheWidgetConfigs();
    }
};

///////////////////////////////
// Widget config aggregation //
///////////////////////////////

/**
 * Get the aggregated list of widget configs.
 * 
 * @return {Object}     An object where each key is the widget id and the value is the widget config.
 */
var getWidgetConfigs = module.exports.getWidgetConfigs = function() {
    return widgetConfigCache;
};

/**
 * Cache all widget configs under the UI's node_modules directory
 * 
 * @api private
 */
var cacheWidgetConfigs = function() {
    widgetConfigCache = {};
    // Cache all of the widget config files under node_modules
    readdirp({ 'root': uiDirectory + '/node_modules', 'fileFilter': 'config.json' })
      .on('data', function (entry) {
          var widgetId = entry.parentDir.split('/')[entry.parentDir.split('/').length - 1];
          widgetConfigCache[widgetId] = require(entry.fullPath);
          widgetConfigCache[widgetId].id = widgetId;
          widgetConfigCache[widgetId].path = entry.parentDir;
      })
      .on('warn', function (err) { log().error({'err': err}, 'A non-fatal error occured whilst caching the config schema'); })
      .on('error', function (err) { log().error({'err': err}, 'A fatal error occured whilst caching the config schema'); })
}

//////////////////
// Static batch //
//////////////////

/**
 * Get the content of a set of static files. The returned data is an object where the key is the requested URL and
 * the values are the static file contents. In case the file couldn't be found, null will be returned.
 * 
 * @param {String[]}    files           An array of file paths relative to the UI repository.
 * @param {Function}    callback        Standard callback method.
 * @param {Object}      callback.err    An error object (if any)
 * @param {Object}      callback.data   JSON Object representing the retrieved files.
 */
var getStaticBatch = module.exports.getStaticBatch = function(files, callback) {
    var validator = new Validator();
    validator.check(files, {'code': 400, 'msg': 'The files parameter must be an array'}).isArray();
    // Filter out the duplicate ones
    files = _.uniq(files);
    // Make sure that all provided filenames are real strings
    for (var i = 0; i < files.length; i++) {
        validator.check(files[i], {'code': 400, 'msg': 'A valid file path needs to be provided'}).notEmpty();
        // Make sure that only absolute paths are allowed. All paths that contain a '../' have the potential of<commit_msg>Followed up on code review<commit_after>/*!
 * Copyright 2012 Sakai Foundation (SF) Licensed under the
 * Educational Community 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.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

var _ = require('underscore');
var fs = require('fs');
var readdirp = require('readdirp');
var watch = require('watch');

var IO = require('oae-util/lib/io');
var log = require('oae-logger').logger('static-files');
var Validator = require('oae-util/lib/validator').Validator

// Path to the 3akai-ux repository
var uiDirectory = null;

// A dictionary that will hold the content for each file. This will be lazy filled. The first
// time a particular file is requested, it will be cached. After that, the cached version will
// be used
var staticFileCache = {};

// A dictionary that will hold the widget configs for all widgets. This will be filled upon
// initialization.
var widgetConfigCache = {};


////////////////////
// Initialization //
////////////////////

/**
 * This will find all of the widget config files in the UI repository and cache. For development, 
 * it will also put watches on the files in the UI repository in order to invalidate the cache
 * when a file has been changed/removed.
 *
 * @param  {String}     uiDir       The path to the directory containing the UI repository.
 */
var init = module.exports.init = function(uiDir) {
    // Cache the ui directory path and make sure we have the absolute path
    uiDirectory = fs.realpathSync(uiDir);

    // Cache all of the widget config files
    cacheWidgetConfigs();

    // Monitor the UI repository for changes and refresh the cache. 
    // This will only be done in development mode
    if (process.env['NODE_ENV'] !== 'production') {
        watch.createMonitor(uiDirectory, {'ignoreDotFiles': true}, function(monitor) {
            monitor.on('created', updateFileCaches);
            monitor.on('changed', updateFileCaches);
            monitor.on('removed', updateFileCaches);
        });
    }
};

/**
 * When a file in the UI repository has changed, we update the static file cache by deleting
 * the record for that file if there is one. The next time the updated/created file will be
 * re-requested, caching will be attempted again. We also check if the changed file is a 
 * widget config file, and refresh the widget config cache if that's the case
 * 
 * @param  {String}     filename        The absolute path to the file that has been added/updated/deleted
 * @api private
 */
var updateFileCaches = function(filename) {
    filename = filename.replace(uiDirectory, '');
    // Delete the file from the static file cache, for it to be re-cached
    // when it is requested again
    delete staticFileCache[filename];
    // If the changed file is a widget config file, we re-cache the widget
    // config files
    if (/^\/node_modules\/(.*?)\/config.json$/.test(filename)) {
        cacheWidgetConfigs();
    }
};

///////////////////////////////
// Widget config aggregation //
///////////////////////////////

/**
 * Get the aggregated list of widget configs.
 * 
 * @return {Object}     An object where each key is the widget id and the value is the widget config.
 */
var getWidgetConfigs = module.exports.getWidgetConfigs = function() {
    return widgetConfigCache;
};

/**
 * Cache all widget configs under the UI's node_modules directory
 * 
 * @api private
 */
var cacheWidgetConfigs = function() {
    widgetConfigCache = {};
    // Cache all of the widget config files under node_modules
    readdirp({ 'root': uiDirectory + '/node_modules', 'fileFilter': 'config.json' })
      .on('data', function (entry) {
          var widgetId = entry.parentDir.split('/')[entry.parentDir.split('/').length - 1];
          widgetConfigCache[widgetId] = require(entry.fullPath);
          widgetConfigCache[widgetId].id = widgetId;
          widgetConfigCache[widgetId].path = entry.parentDir;
      })
      .on('warn', function (err) { log().warn({'err': err}, 'A non-fatal error occured whilst caching the config schema'); })
      .on('error', function (err) { log().error({'err': err}, 'A fatal error occured whilst caching the config schema'); })
}

//////////////////
// Static batch //
//////////////////

/**
 * Get the content of a set of static files. The returned data is an object where the key is the requested URL and
 * the values are the static file contents. In case the file couldn't be found, null will be returned.
 * 
 * @param {String[]}    files           An array of file paths relative to the UI repository.
 * @param {Function}    callback        Standard callback method.
 * @param {Object}      callback.err    An error object (if any)
 * @param {Object}      callback.data   JSON Object representing the retrieved files.
 */
var getStaticBatch = module.exports.getStaticBatch = function(files, callback) {
    var validator = new Validator();
    validator.check(files, {'code': 400, 'msg': 'The files parameter must be an array'}).isArray();
    // Filter out the duplicate ones
    files = _.uniq(files);
    // Make sure that all provided filenames are real strings
    for (var i = 0; i < files.length; i++) {
        validator.check(files[i], {'code': 400, 'msg': 'A valid file path needs to be provided'}).notEmpty();
        // Make sure that only absolute paths are allowed. All paths that contain a '../' have the potential of
{'fraction_non_alphanumeric': 0.12601136168015148, 'fraction_numerical': 0.002926493372353245, 'mean_word_length': 2.926664413653261, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7196305', 'n_tokens_mistral': 3064, 'n_tokens_neox': 2993, 'n_words': 1317}