diff --git a/.gitignore b/.gitignore index 0eae1ca082..689569b31b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,6 @@ package-lock.json /plugins/content/menu/versions/ /plugins/content/bower/bowercache/ -/routes/lang/en.json - /temp /test/.testcache /tmp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bdc24a2fc..1e3e4a0631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to the Adapt authoring tool are documented in this file. **IMPORTANT**: For information on how to **correctly and safely** update your installation, please consult **INSTALL.md**.
_Note that we adhere to the [semantic versioning](http://semver.org/) scheme for release numbering._ +## [0.10.1] - 2019-10-22 + +Bugfix release. + +### Fixed +- Courses dashboard: A-Z/Z-A sort is case sensitive ([#2325](https://github.com/adaptlearning/adapt_authoring/issues/2325)) +- Item copy can result in broken courseassets in other items ([#2347](https://github.com/adaptlearning/adapt_authoring/issues/2347)) +- Allow non-interactive install and upgrade scripts ([#2407](https://github.com/adaptlearning/adapt_authoring/issues/2407)) +- installation: missing translation key for app.productname ([#2410](https://github.com/adaptlearning/adapt_authoring/issues/2410)) +- Fix reading of asset type from schema ([#2416](https://github.com/adaptlearning/adapt_authoring/issues/2416)) +- Grunt tasks do not process symlinks ([#2428](https://github.com/adaptlearning/adapt_authoring/issues/2428)) +- Importing plugin with existing targetAttribute causes error when retrieving plugin schemas ([#2433](https://github.com/adaptlearning/adapt_authoring/issues/2433)) +- Support Node 12 ([#2437](https://github.com/adaptlearning/adapt_authoring/issues/2437)) +- Asset tags are not preserved on import ([#2439](https://github.com/adaptlearning/adapt_authoring/issues/2439)) + +### Added +- skip-version check should be passed as cli argument ([#2005](https://github.com/adaptlearning/adapt_authoring/issues/2005)) +- Plugin upload failed modal should be more descriptive ([#2444](https://github.com/adaptlearning/adapt_authoring/issues/2444)) + ## [0.10.0] - 2019-08-29 Adds ability to import courses with an older framework version, and latest bugfixes. @@ -640,6 +659,7 @@ Initial release. - Loading screen of death - Session cookie security issues +[0.10.1]: https://github.com/adaptlearning/adapt_authoring/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.9.0...v0.10.0 [0.9.0]: https://github.com/adaptlearning/adapt_authoring/compare/v0.8.1...v0.9.0 [0.8.1]: https://github.com/adaptlearning/adapt_authoring/compare/v0.8.0...v0.8.1 diff --git a/Gruntfile.js b/Gruntfile.js index cf3cd18a2d..e6ea02b031 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,13 +4,14 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), - "merge-json": { - en: { - src: [ - 'routes/lang/en-application.json', - 'frontend/src/**/lang/en.json' - ], - dest: 'routes/lang/en.json' + 'generate-lang-json': { + options: { + langFileExt: '.json', + src: { + backend: 'routes/lang', + frontend: 'frontend/src/**/lang' + }, + dest: 'temp/lang' } }, copy: { @@ -87,13 +88,17 @@ module.exports = function(grunt) { partialRegex: /^part_/, partialsPathRegex: /\/partials\// }, - files: { - "frontend/src/templates/templates.js": [ - "frontend/src/core/**/*.hbs", - "frontend/src/modules/**/*.hbs", - "frontend/src/plugins/**/*.hbs" - ] - } + files: [ + { + follow: true, + src: [ + 'frontend/src/core/**/*.hbs', + 'frontend/src/modules/**/*.hbs', + 'frontend/src/plugins/**/*.hbs' + ], + dest: 'frontend/src/templates/templates.js' + } + ] } }, requirejs: { @@ -217,7 +222,10 @@ module.exports = function(grunt) { var ret = ''; for (var i = 0, l = src.length; i < l; i++) { - grunt.file.expand({ filter: options.filter }, src[i]).forEach(function(lessPath) { + grunt.file.expand({ + filter: options.filter, + follow: true + }, src[i]).forEach(function(lessPath) { ret += '@import \'' + path.normalize(lessPath) + '\';\n'; }); } @@ -262,6 +270,30 @@ module.exports = function(grunt) { done(); } }); + grunt.registerTask('generate-lang-json', function() { + const fs = require('fs-extra'); + const path = require('path'); + + const options = this.options(); + const backendGlob = path.join(options.src.backend, `*${options.langFileExt}`); + const dest = options.dest; + // load each route lang file + /** + * NOTE there must be a file in routes/lang for the language to be loaded, + * won't work if you've only got lang files in frontend + */ + grunt.file.expand({}, path.join(backendGlob)).forEach(backendPath => { + const basename = path.basename(backendPath); + const frontendGlob = path.join(options.src.frontend, basename); + let data = { ...fs.readJSONSync(backendPath) }; + // load all matching frontend lang files + grunt.file.expand({}, frontendGlob).forEach(frontendPath => { + data = { ...data, ...fs.readJSONSync(frontendPath) }; + }); + fs.ensureDirSync(dest); + fs.writeJSONSync(path.join(dest, basename), data, { spaces: 2 }); + }); + }); grunt.registerTask('default', ['build:dev']); grunt.registerTask('test', ['mochaTest']); @@ -283,7 +315,7 @@ module.exports = function(grunt) { config.isProduction = isProduction; grunt.file.write(configFile, JSON.stringify(config, null, 2)); // run the task - grunt.task.run(['migration-conf', 'requireBundle', 'merge-json', 'copy', 'less:' + compilation, 'handlebars', 'requirejs:'+ compilation]); + grunt.task.run(['migration-conf', 'requireBundle', 'generate-lang-json', 'copy', 'less:' + compilation, 'handlebars', 'requirejs:'+ compilation]); } catch(e) { grunt.task.run(['requireBundle', 'copy', 'less:' + compilation, 'handlebars', 'requirejs:' + compilation]); diff --git a/frontend/src/core/helpers.js b/frontend/src/core/helpers.js index 619de89ed7..5c8269b45c 100644 --- a/frontend/src/core/helpers.js +++ b/frontend/src/core/helpers.js @@ -172,12 +172,6 @@ define(function(require){ return new Handlebars.SafeString(html + ''); }, - decodeHTML: function(html) { - var el = document.createElement('div'); - el.innerHTML = html; - return el.childNodes.length === 0 ? "" : el.childNodes[0].nodeValue; - }, - ifHasPermissions: function(permissions, block) { var hasPermission = Origin.permissions.hasPermissions(permissions.split(',')); return hasPermission ? block.fn(this) : block.inverse(this); @@ -342,6 +336,27 @@ define(function(require){ '', Origin.l10n.t('app.maxfileuploadsize', {size: Origin.constants.humanMaxFileUploadSize}), ''].join('')) + }, + + flattenNestedProperties: function(properties) { + if (!properties) return {}; + var flatProperties = {}; + for (var key in properties) { + // Check for nested properties + if (typeof properties[key] === 'object') { + for (var innerKey in properties[key]) { + // Check if key already exists + if (flatProperties[innerKey]) { + flatProperties[key+'.'+innerKey] = properties[key][innerKey]; + } else { + flatProperties[innerKey] = properties[key][innerKey]; + } + } + } else { + flatProperties[key] = properties[key]; + } + } + return flatProperties; } }; diff --git a/frontend/src/libraries/handlebars.js b/frontend/src/libraries/handlebars.js index 6fc8bcb6eb..f3d6a8eabe 100644 --- a/frontend/src/libraries/handlebars.js +++ b/frontend/src/libraries/handlebars.js @@ -1,7 +1,7 @@ /**! @license - handlebars v4.0.11 + handlebars v4.4.0 Copyright (C) 2011-2017 by Yehuda Katz @@ -275,11 +275,13 @@ return /******/ (function(modules) { // webpackBootstrap var _logger2 = _interopRequireDefault(_logger); - var VERSION = '4.0.11'; + var VERSION = '4.4.0'; exports.VERSION = VERSION; - var COMPILER_REVISION = 7; - + var COMPILER_REVISION = 8; exports.COMPILER_REVISION = COMPILER_REVISION; + var LAST_COMPATIBLE_COMPILER_REVISION = 7; + + exports.LAST_COMPATIBLE_COMPILER_REVISION = LAST_COMPATIBLE_COMPILER_REVISION; var REVISION_CHANGES = { 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it 2: '== 1.0.0-rc.3', @@ -287,7 +289,8 @@ return /******/ (function(modules) { // webpackBootstrap 4: '== 1.x.x', 5: '== 2.0.0-alpha.x', 6: '>= 2.0.0-beta.1', - 7: '>= 4.0.0' + 7: '>= 4.0.0 <4.3.0', + 8: '>= 4.3.0' }; exports.REVISION_CHANGES = REVISION_CHANGES; @@ -371,6 +374,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.createFrame = createFrame; exports.blockParams = blockParams; exports.appendContextPath = appendContextPath; + var escape = { '&': '&', '<': '<', @@ -588,6 +592,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.__esModule = true; exports.registerDefaultHelpers = registerDefaultHelpers; + exports.moveHelperToHooks = moveHelperToHooks; var _helpersBlockHelperMissing = __webpack_require__(11); @@ -627,6 +632,15 @@ return /******/ (function(modules) { // webpackBootstrap _helpersWith2['default'](instance); } + function moveHelperToHooks(instance, helperName, keepHelper) { + if (instance.helpers[helperName]) { + instance.hooks[helperName] = instance.helpers[helperName]; + if (!keepHelper) { + delete instance.helpers[helperName]; + } + } + } + /***/ }), /* 11 */ /***/ (function(module, exports, __webpack_require__) { @@ -674,7 +688,7 @@ return /******/ (function(modules) { // webpackBootstrap /* 12 */ /***/ (function(module, exports, __webpack_require__) { - 'use strict'; + /* WEBPACK VAR INJECTION */(function(global) {'use strict'; var _interopRequireDefault = __webpack_require__(1)['default']; @@ -736,6 +750,16 @@ return /******/ (function(modules) { // webpackBootstrap execIteration(i, i, i === context.length - 1); } } + } else if (global.Symbol && context[global.Symbol.iterator]) { + var newContext = []; + var iterator = context[global.Symbol.iterator](); + for (var it = iterator.next(); !it.done; it = iterator.next()) { + newContext.push(it.value); + } + context = newContext; + for (var j = context.length; i < j; i++) { + execIteration(i, i, i === context.length - 1); + } } else { var priorKey = undefined; @@ -766,6 +790,7 @@ return /******/ (function(modules) { // webpackBootstrap }; module.exports = exports['default']; + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) /***/ }), /* 13 */ @@ -868,7 +893,13 @@ return /******/ (function(modules) { // webpackBootstrap exports['default'] = function (instance) { instance.registerHelper('lookup', function (obj, field) { - return obj && obj[field]; + if (!obj) { + return obj; + } + if (field === 'constructor' && !obj.propertyIsEnumerable(field)) { + return undefined; + } + return obj[field]; }); }; @@ -1063,23 +1094,28 @@ return /******/ (function(modules) { // webpackBootstrap var _base = __webpack_require__(4); + var _helpers = __webpack_require__(10); + function checkRevision(compilerInfo) { var compilerRevision = compilerInfo && compilerInfo[0] || 1, currentRevision = _base.COMPILER_REVISION; - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = _base.REVISION_CHANGES[currentRevision], - compilerVersions = _base.REVISION_CHANGES[compilerRevision]; - throw new _exception2['default']('Template was precompiled with an older version of Handlebars than the current runtime. ' + 'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').'); - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw new _exception2['default']('Template was precompiled with a newer version of Handlebars than the current runtime. ' + 'Please update your runtime to a newer version (' + compilerInfo[1] + ').'); - } + if (compilerRevision >= _base.LAST_COMPATIBLE_COMPILER_REVISION && compilerRevision <= _base.COMPILER_REVISION) { + return; + } + + if (compilerRevision < _base.LAST_COMPATIBLE_COMPILER_REVISION) { + var runtimeVersions = _base.REVISION_CHANGES[currentRevision], + compilerVersions = _base.REVISION_CHANGES[compilerRevision]; + throw new _exception2['default']('Template was precompiled with an older version of Handlebars than the current runtime. ' + 'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').'); + } else { + // Use the embedded version info since the runtime doesn't know about this revision yet + throw new _exception2['default']('Template was precompiled with a newer version of Handlebars than the current runtime. ' + 'Please update your runtime to a newer version (' + compilerInfo[1] + ').'); } } function template(templateSpec, env) { + /* istanbul ignore next */ if (!env) { throw new _exception2['default']('No environment passed to template'); @@ -1091,9 +1127,12 @@ return /******/ (function(modules) { // webpackBootstrap templateSpec.main.decorator = templateSpec.main_d; // Note: Using env.VM references rather than local var references throughout this section to allow - // for external users to override these as psuedo-supported APIs. + // for external users to override these as pseudo-supported APIs. env.VM.checkRevision(templateSpec.compiler); + // backwards compatibility for precompiled templates with compiler-version 7 (<4.3.0) + var templateWasPrecompiledWithCompilerV7 = templateSpec.compiler && templateSpec.compiler[0] === 7; + function invokePartialWrapper(partial, context, options) { if (options.hash) { context = Utils.extend({}, context, options.hash); @@ -1101,13 +1140,15 @@ return /******/ (function(modules) { // webpackBootstrap options.ids[0] = true; } } - partial = env.VM.resolvePartial.call(this, partial, context, options); - var result = env.VM.invokePartial.call(this, partial, context, options); + + var optionsWithHooks = Utils.extend({}, options, { hooks: this.hooks }); + + var result = env.VM.invokePartial.call(this, partial, context, optionsWithHooks); if (result == null && env.compile) { options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); - result = options.partials[options.name](context, options); + result = options.partials[options.name](context, optionsWithHooks); } if (result != null) { if (options.indent) { @@ -1174,15 +1215,6 @@ return /******/ (function(modules) { // webpackBootstrap } return value; }, - merge: function merge(param, common) { - var obj = param || common; - - if (param && common && param !== common) { - obj = Utils.extend({}, common, param); - } - - return obj; - }, // An empty object to use as replacement for null-contexts nullContext: _Object$seal({}), @@ -1219,18 +1251,25 @@ return /******/ (function(modules) { // webpackBootstrap ret._setup = function (options) { if (!options.partial) { - container.helpers = container.merge(options.helpers, env.helpers); + container.helpers = Utils.extend({}, env.helpers, options.helpers); if (templateSpec.usePartial) { - container.partials = container.merge(options.partials, env.partials); + container.partials = Utils.extend({}, env.partials, options.partials); } if (templateSpec.usePartial || templateSpec.useDecorators) { - container.decorators = container.merge(options.decorators, env.decorators); + container.decorators = Utils.extend({}, env.decorators, options.decorators); } + + container.hooks = {}; + + var keepHelperInHelpers = options.allowCallsToHelperMissing || templateWasPrecompiledWithCompilerV7; + _helpers.moveHelperToHooks(container, 'helperMissing', keepHelperInHelpers); + _helpers.moveHelperToHooks(container, 'blockHelperMissing', keepHelperInHelpers); } else { container.helpers = options.helpers; container.partials = options.partials; container.decorators = options.decorators; + container.hooks = options.hooks; } }; @@ -1267,6 +1306,10 @@ return /******/ (function(modules) { // webpackBootstrap return prog; } + /** + * This is currently part of the official API, therefore implementation details should not be changed. + */ + function resolvePartial(partial, context, options) { if (!partial) { if (options.name === '@partial-block') { @@ -1629,8 +1672,7 @@ return /******/ (function(modules) { // webpackBootstrap symbols_: { "error": 2, "root": 3, "program": 4, "EOF": 5, "program_repetition0": 6, "statement": 7, "mustache": 8, "block": 9, "rawBlock": 10, "partial": 11, "partialBlock": 12, "content": 13, "COMMENT": 14, "CONTENT": 15, "openRawBlock": 16, "rawBlock_repetition_plus0": 17, "END_RAW_BLOCK": 18, "OPEN_RAW_BLOCK": 19, "helperName": 20, "openRawBlock_repetition0": 21, "openRawBlock_option0": 22, "CLOSE_RAW_BLOCK": 23, "openBlock": 24, "block_option0": 25, "closeBlock": 26, "openInverse": 27, "block_option1": 28, "OPEN_BLOCK": 29, "openBlock_repetition0": 30, "openBlock_option0": 31, "openBlock_option1": 32, "CLOSE": 33, "OPEN_INVERSE": 34, "openInverse_repetition0": 35, "openInverse_option0": 36, "openInverse_option1": 37, "openInverseChain": 38, "OPEN_INVERSE_CHAIN": 39, "openInverseChain_repetition0": 40, "openInverseChain_option0": 41, "openInverseChain_option1": 42, "inverseAndProgram": 43, "INVERSE": 44, "inverseChain": 45, "inverseChain_option0": 46, "OPEN_ENDBLOCK": 47, "OPEN": 48, "mustache_repetition0": 49, "mustache_option0": 50, "OPEN_UNESCAPED": 51, "mustache_repetition1": 52, "mustache_option1": 53, "CLOSE_UNESCAPED": 54, "OPEN_PARTIAL": 55, "partialName": 56, "partial_repetition0": 57, "partial_option0": 58, "openPartialBlock": 59, "OPEN_PARTIAL_BLOCK": 60, "openPartialBlock_repetition0": 61, "openPartialBlock_option0": 62, "param": 63, "sexpr": 64, "OPEN_SEXPR": 65, "sexpr_repetition0": 66, "sexpr_option0": 67, "CLOSE_SEXPR": 68, "hash": 69, "hash_repetition_plus0": 70, "hashSegment": 71, "ID": 72, "EQUALS": 73, "blockParams": 74, "OPEN_BLOCK_PARAMS": 75, "blockParams_repetition_plus0": 76, "CLOSE_BLOCK_PARAMS": 77, "path": 78, "dataName": 79, "STRING": 80, "NUMBER": 81, "BOOLEAN": 82, "UNDEFINED": 83, "NULL": 84, "DATA": 85, "pathSegments": 86, "SEP": 87, "$accept": 0, "$end": 1 }, terminals_: { 2: "error", 5: "EOF", 14: "COMMENT", 15: "CONTENT", 18: "END_RAW_BLOCK", 19: "OPEN_RAW_BLOCK", 23: "CLOSE_RAW_BLOCK", 29: "OPEN_BLOCK", 33: "CLOSE", 34: "OPEN_INVERSE", 39: "OPEN_INVERSE_CHAIN", 44: "INVERSE", 47: "OPEN_ENDBLOCK", 48: "OPEN", 51: "OPEN_UNESCAPED", 54: "CLOSE_UNESCAPED", 55: "OPEN_PARTIAL", 60: "OPEN_PARTIAL_BLOCK", 65: "OPEN_SEXPR", 68: "CLOSE_SEXPR", 72: "ID", 73: "EQUALS", 75: "OPEN_BLOCK_PARAMS", 77: "CLOSE_BLOCK_PARAMS", 80: "STRING", 81: "NUMBER", 82: "BOOLEAN", 83: "UNDEFINED", 84: "NULL", 85: "DATA", 87: "SEP" }, productions_: [0, [3, 2], [4, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [13, 1], [10, 3], [16, 5], [9, 4], [9, 4], [24, 6], [27, 6], [38, 6], [43, 2], [45, 3], [45, 1], [26, 3], [8, 5], [8, 5], [11, 5], [12, 3], [59, 5], [63, 1], [63, 1], [64, 5], [69, 1], [71, 3], [74, 3], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [56, 1], [56, 1], [79, 2], [78, 1], [86, 3], [86, 1], [6, 0], [6, 2], [17, 1], [17, 2], [21, 0], [21, 2], [22, 0], [22, 1], [25, 0], [25, 1], [28, 0], [28, 1], [30, 0], [30, 2], [31, 0], [31, 1], [32, 0], [32, 1], [35, 0], [35, 2], [36, 0], [36, 1], [37, 0], [37, 1], [40, 0], [40, 2], [41, 0], [41, 1], [42, 0], [42, 1], [46, 0], [46, 1], [49, 0], [49, 2], [50, 0], [50, 1], [52, 0], [52, 2], [53, 0], [53, 1], [57, 0], [57, 2], [58, 0], [58, 1], [61, 0], [61, 2], [62, 0], [62, 1], [66, 0], [66, 2], [67, 0], [67, 1], [70, 1], [70, 2], [76, 1], [76, 2]], - performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$ - /**/) { + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$) { var $0 = $$.length - 1; switch (yystate) { @@ -2167,11 +2209,10 @@ return /******/ (function(modules) { // webpackBootstrap this.begin(condition); } }; lexer.options = {}; - lexer.performAction = function anonymous(yy, yy_, $avoiding_name_collisions, YY_START - /**/) { + lexer.performAction = function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { function strip(start, end) { - return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng - end); + return yy_.yytext = yy_.yytext.substring(start, yy_.yyleng - end + start); } var YYSTATE = YY_START; @@ -2208,7 +2249,7 @@ return /******/ (function(modules) { // webpackBootstrap if (this.conditionStack[this.conditionStack.length - 1] === 'raw') { return 15; } else { - yy_.yytext = yy_.yytext.substr(5, yy_.yyleng - 9); + strip(5, 9); return 'END_RAW_BLOCK'; } @@ -2571,7 +2612,7 @@ return /******/ (function(modules) { // webpackBootstrap return; } - // We omit the last node if it's whitespace only and not preceeded by a non-content node. + // We omit the last node if it's whitespace only and not preceded by a non-content node. var original = current.value; current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, ''); current.leftStripped = current.value !== original; @@ -2772,7 +2813,7 @@ return /******/ (function(modules) { // webpackBootstrap function id(token) { if (/^\[.*\]$/.test(token)) { - return token.substr(1, token.length - 2); + return token.substring(1, token.length - 1); } else { return token; } @@ -2786,7 +2827,7 @@ return /******/ (function(modules) { // webpackBootstrap } function stripComment(comment) { - return comment.replace(/^\{\{~?\!-?-?/, '').replace(/-?-?~?\}\}$/, ''); + return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); } function preparePath(data, parts, loc) { @@ -2794,8 +2835,7 @@ return /******/ (function(modules) { // webpackBootstrap var original = data ? '@' : '', dig = [], - depth = 0, - depthString = ''; + depth = 0; for (var i = 0, l = parts.length; i < l; i++) { var part = parts[i].part, @@ -2810,7 +2850,6 @@ return /******/ (function(modules) { // webpackBootstrap throw new _exception2['default']('Invalid path: ' + original, { loc: loc }); } else if (part === '..') { depth++; - depthString += '../'; } } else { dig.push(part); @@ -3045,11 +3084,11 @@ return /******/ (function(modules) { // webpackBootstrap 'lookup': true }; if (knownHelpers) { + // the next line should use "Object.keys", but the code has been like this a long time and changing it, might + // cause backwards-compatibility issues... It's an old library... + // eslint-disable-next-line guard-for-in for (var _name in knownHelpers) { - /* istanbul ignore else */ - if (_name in knownHelpers) { - this.options.knownHelpers[_name] = knownHelpers[_name]; - } + this.options.knownHelpers[_name] = knownHelpers[_name]; } } @@ -3565,10 +3604,19 @@ return /******/ (function(modules) { // webpackBootstrap // PUBLIC API: You can override these methods in a subclass to provide // alternative compiled forms for name lookup and buffering semantics nameLookup: function nameLookup(parent, name /* , type*/) { - if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return [parent, '.', name]; - } else { - return [parent, '[', JSON.stringify(name), ']']; + var isEnumerable = [this.aliasable('container.propertyIsEnumerable'), '.call(', parent, ',"constructor")']; + + if (name === 'constructor') { + return ['(', isEnumerable, '?', _actualLookup(), ' : undefined)']; + } + return _actualLookup(); + + function _actualLookup() { + if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { + return [parent, '.', name]; + } else { + return [parent, '[', JSON.stringify(name), ']']; + } } }, depthedLookup: function depthedLookup(name) { @@ -3767,7 +3815,6 @@ return /******/ (function(modules) { // webpackBootstrap for (var alias in this.aliases) { // eslint-disable-line guard-for-in var node = this.aliases[alias]; - if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { varDeclarations += ', alias' + ++aliasCount + '=' + alias; node.children[0] = 'alias' + aliasCount; @@ -3862,7 +3909,7 @@ return /******/ (function(modules) { // webpackBootstrap // replace it on the stack with the result of properly // invoking blockHelperMissing. blockValue: function blockValue(name) { - var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + var blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'), params = [this.contextName(0)]; this.setupHelperArgs(name, 0, params); @@ -3880,7 +3927,7 @@ return /******/ (function(modules) { // webpackBootstrap // On stack, after, if lastHelper: value ambiguousBlockValue: function ambiguousBlockValue() { // We're being a bit cheeky and reusing the options value from the prior exec - var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), + var blockHelperMissing = this.aliasable('container.hooks.blockHelperMissing'), params = [this.contextName(0)]; this.setupHelperArgs('', 0, params, true); @@ -4171,18 +4218,33 @@ return /******/ (function(modules) { // webpackBootstrap // If the helper is not found, `helperMissing` is called. invokeHelper: function invokeHelper(paramSize, name, isSimple) { var nonHelper = this.popStack(), - helper = this.setupHelper(paramSize, name), - simple = isSimple ? [helper.name, ' || '] : ''; + helper = this.setupHelper(paramSize, name); + + var possibleFunctionCalls = []; - var lookup = ['('].concat(simple, nonHelper); + if (isSimple) { + // direct call to helper + possibleFunctionCalls.push(helper.name); + } + // call a function from the input object + possibleFunctionCalls.push(nonHelper); if (!this.options.strict) { - lookup.push(' || ', this.aliasable('helpers.helperMissing')); + possibleFunctionCalls.push(this.aliasable('container.hooks.helperMissing')); } - lookup.push(')'); - this.push(this.source.functionCall(lookup, 'call', helper.callParams)); + var functionLookupCode = ['(', this.itemsSeparatedBy(possibleFunctionCalls, '||'), ')']; + var functionCall = this.source.functionCall(functionLookupCode, 'call', helper.callParams); + this.push(functionCall); }, + itemsSeparatedBy: function itemsSeparatedBy(items, separator) { + var result = []; + result.push(items[0]); + for (var i = 1; i < items.length; i++) { + result.push(separator, items[i]); + } + return result; + }, // [invokeKnownHelper] // // On stack, before: hash, inverse, program, params..., ... @@ -4220,7 +4282,7 @@ return /******/ (function(modules) { // webpackBootstrap var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')']; if (!this.options.strict) { lookup[0] = '(helper = '; - lookup.push(' != null ? helper : ', this.aliasable('helpers.helperMissing')); + lookup.push(' != null ? helper : ', this.aliasable('container.hooks.helperMissing')); } this.push(['(', lookup, helper.paramsInit ? ['),(', helper.paramsInit] : [], '),', '(typeof helper === ', this.aliasable('"function"'), ' ? ', this.source.functionCall('helper', 'call', helper.callParams), ' : helper))']); diff --git a/frontend/src/modules/assetManagement/views/assetManagementModalView.js b/frontend/src/modules/assetManagement/views/assetManagementModalView.js index 6885f17775..2494e66615 100644 --- a/frontend/src/modules/assetManagement/views/assetManagementModalView.js +++ b/frontend/src/modules/assetManagement/views/assetManagementModalView.js @@ -16,7 +16,7 @@ define(function(require) { postRender: function() { this.setupSubViews(); this.setupFilterAndSearchView(); - if (this.options.assetType === "Asset:image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { + if (this.options.assetType === "image" && Origin.scaffold.getCurrentModel().get('_component') === 'graphic') { this.setupImageAutofillButton(); } this.resizePanels(); @@ -24,8 +24,8 @@ define(function(require) { setupSubViews: function() { this.search = {}; - // Replace Asset and : so we can have both filtered and all asset types - var assetType = this.options.assetType.replace('Asset', '').replace(':', ''); + + var assetType = this.options.assetType; if (assetType) { var filters = [assetType]; diff --git a/frontend/src/modules/editor/themeEditor/views/editorThemingView.js b/frontend/src/modules/editor/themeEditor/views/editorThemingView.js index 93f8c8f551..789c31d85b 100644 --- a/frontend/src/modules/editor/themeEditor/views/editorThemingView.js +++ b/frontend/src/modules/editor/themeEditor/views/editorThemingView.js @@ -212,9 +212,10 @@ define(function(require) { if (!fieldView) { return; } - if (fieldView.schema.inputType === 'ColourPicker') { + var inputType = fieldView.schema.inputType.type || fieldView.schema.inputType; + if (inputType === 'ColourPicker') { fieldView.setValue(value); - } else if (typeof fieldView.schema.inputType === 'string' && fieldView.schema.inputType.indexOf('Asset:') > -1) { + } else if (inputType.indexOf('Asset') > -1) { fieldView.setValue(value); fieldView.render(); $('div[data-editor-id*="' + key + '"]').append(fieldView.editor.$el); diff --git a/frontend/src/modules/pluginManagement/views/pluginManagementUploadView.js b/frontend/src/modules/pluginManagement/views/pluginManagementUploadView.js index 31f6265d2f..de940f5549 100644 --- a/frontend/src/modules/pluginManagement/views/pluginManagementUploadView.js +++ b/frontend/src/modules/pluginManagement/views/pluginManagementUploadView.js @@ -61,7 +61,7 @@ define(function(require){ Origin.Notify.alert({ type: 'error', title: Origin.l10n.t('app.uploadpluginerror'), - text: Helpers.decodeHTML(message) + text: message }); Origin.router.navigateTo('pluginManagement/upload'); }, diff --git a/frontend/src/modules/projects/views/projectsView.js b/frontend/src/modules/projects/views/projectsView.js index 2b92e5e513..8c14b4e5da 100644 --- a/frontend/src/modules/projects/views/projectsView.js +++ b/frontend/src/modules/projects/views/projectsView.js @@ -133,7 +133,8 @@ define(function(require){ operators : { skip: this.fetchCount, limit: this.pageSize, - sort: this.sort + sort: this.sort, + collation: { locale: navigator.language.substring(0, 2) } } }, success: function(collection, response) { diff --git a/frontend/src/modules/scaffold/views/scaffoldAssetItemView.js b/frontend/src/modules/scaffold/views/scaffoldAssetItemView.js index 13341d0cb5..9c5a25f98f 100644 --- a/frontend/src/modules/scaffold/views/scaffoldAssetItemView.js +++ b/frontend/src/modules/scaffold/views/scaffoldAssetItemView.js @@ -174,7 +174,7 @@ define([ Origin.trigger('modal:open', AssetManagementModalView, { collection: new AssetCollection, - assetType: 'Asset:image', + assetType: 'image', _shouldShowScrollbar: false, onUpdate: function(data) { if (!data) return; diff --git a/frontend/src/modules/scaffold/views/scaffoldAssetView.js b/frontend/src/modules/scaffold/views/scaffoldAssetView.js index 7084f709ce..2371d4cfe6 100644 --- a/frontend/src/modules/scaffold/views/scaffoldAssetView.js +++ b/frontend/src/modules/scaffold/views/scaffoldAssetView.js @@ -10,6 +10,8 @@ define([ var ScaffoldAssetView = Backbone.Form.editors.Base.extend({ + assetType: null, + events: { 'change input': function() { this.trigger('change', this); }, 'focus input': function() { this.trigger('focus', this); }, @@ -52,9 +54,13 @@ define([ var inputType = this.schema.inputType; var dataUrl = Helpers.isAssetExternal(this.value) ? this.value : ''; + this.assetType = typeof inputType === 'string' ? + inputType.replace(/Asset|:/g, '') : + inputType.media; + this.$el.html(Handlebars.templates[this.constructor.template]({ value: this.value, - type: inputType.media || inputType.replace('Asset:', ''), + type: this.assetType, url: id ? 'api/asset/serve/' + id : dataUrl, thumbUrl: id ? 'api/asset/thumb/' + id : dataUrl })); @@ -200,7 +206,7 @@ define([ Origin.trigger('modal:open', AssetManagementModalView, { collection: new AssetCollection, - assetType: this.schema.inputType, + assetType: this.assetType, _shouldShowScrollbar: false, onUpdate: function(data) { if (!data) return; diff --git a/frontend/src/modules/scaffold/views/scaffoldListView.js b/frontend/src/modules/scaffold/views/scaffoldListView.js index 6d198905bc..7e3909d658 100644 --- a/frontend/src/modules/scaffold/views/scaffoldListView.js +++ b/frontend/src/modules/scaffold/views/scaffoldListView.js @@ -1,8 +1,10 @@ define([ 'core/origin', + 'core/helpers', + 'core/models/courseAssetModel', 'backbone-forms', 'backbone-forms-lists' -], function(Origin, BackboneForms) { +], function(Origin, Helpers, CourseAssetModel, BackboneForms) { var ScaffoldListView = Backbone.Form.editors.List.extend({ defaultValue: [], @@ -92,9 +94,42 @@ define([ }, cloneItem: function(event) { + var flatItem = Helpers.flattenNestedProperties(this.editor.value); + var itemValues = _.values(flatItem); + var parentAttributes = Origin.scaffold.getCurrentModel().attributes; + itemValues.forEach(function(item) { + if (typeof item !== 'string' || item.indexOf('course/assets') === -1) return; + + var itemFileName = item.substring(item.lastIndexOf('/')+1); + $.ajax({ + url: 'api/asset/query', + type:'GET', + data: {search: { filename: itemFileName }}, + success: function (result) { + (new CourseAssetModel()).save({ + _courseId : Origin.editor.data.course.get('_id'), + _contentType : parentAttributes._type, + _contentTypeId : parentAttributes._id, + _fieldName : itemFileName, + _assetId : result[0]._id, + _contentTypeParentId: parentAttributes._parentId + }, { + error: function(error) { + Origin.Notify.alert({ + type: 'error', + text: Origin.l10n.t('app.errorsaveasset') + }); + } + }); + }, + error: function() { + Origin.Notify.alert({ type: 'error', text: Origin.l10n.t('app.errorduplication') }); + } + }); + }); + this.list.addItem(this.editor.value, true); } - }); Origin.on('origin:dataReady', function() { diff --git a/install.js b/install.js index 9bdf1cca98..25e3543f97 100644 --- a/install.js +++ b/install.js @@ -261,6 +261,9 @@ installHelpers.checkPrimaryDependencies(function(error) { ] }; if(!IS_INTERACTIVE) { + if (installHelpers.inputHelpers.toBoolean(optimist.argv.useJSON)) { + USE_CONFIG = true; + } return start(); } console.log(''); @@ -282,7 +285,8 @@ installHelpers.checkPrimaryDependencies(function(error) { function generatePromptOverrides() { if(USE_CONFIG) { var configJson = require('./conf/config.json'); - var configData = JSON.parse(JSON.stringify(configJson).replace(/true/g, '"y"').replace(/false/g, '"n"')); + var configData = JSON.parse(JSON.stringify(configJson).replace(/:true/g, ':"y"').replace(/:false/g, ':"n"')); + addConfig(configData); configData.install = 'y'; } const sessionSecret = USE_CONFIG && configData.sessionSecret || crypto.randomBytes(64).toString('hex'); @@ -345,7 +349,7 @@ function configureDatabase(callback) { installHelpers.getInput(inputData.database.dbConfig, function(result) { addConfig(result); - var isStandard = !result.useConnectionUri || USE_CONFIG && configResults.useConnectionUri !== 'y'; + var isStandard = !installHelpers.inputHelpers.toBoolean(result.useConnectionUri); var config = inputData.database[isStandard ? 'configureStandard' : 'configureUri']; installHelpers.getInput(config, function(result) { @@ -360,14 +364,14 @@ function configureFeatures(callback) { function smtp(cb) { installHelpers.getInput(inputData.features.smtp.confirm, function(result) { addConfig(result); - if(!result.useSmtp || USE_CONFIG && configResults.useSmtp !== 'y') { + if (!installHelpers.inputHelpers.toBoolean(result.useSmtp)) { return cb(); } // prompt user if custom connection url or well-known-service should be used installHelpers.getInput(inputData.features.smtp.confirmConnectionUrl, function(result) { addConfig(result); var smtpConfig; - if (result.useSmtpConnectionUrl === true) { + if (installHelpers.inputHelpers.toBoolean(result.useSmtpConnectionUrl)) { smtpConfig = inputData.features.smtp.configure.concat(inputData.features.smtp.configureConnectionUrl); } else { smtpConfig = inputData.features.smtp.configure.concat(inputData.features.smtp.configureService); @@ -478,7 +482,7 @@ function createSuperUser(callback) { var onError = function(error) { handleError(error, 1, 'Failed to create admin user account. Please check the console output.'); }; - console.log(`\nNow we need to set up a 'Super Admin' account. This account can be used to manage everything on your ${app.polyglot.t('app.productname')} instance.`); + console.log(`\nNow we need to set up a 'Super Admin' account. This account can be used to manage everything on your authoring tool instance.`); installHelpers.getInput(inputData.superUser, function(result) { console.log(''); app.usermanager.deleteUser({ email: result.suEmail }, function(error, userRec) { diff --git a/lib/application.js b/lib/application.js index 039b277451..c868c28073 100644 --- a/lib/application.js +++ b/lib/application.js @@ -328,14 +328,13 @@ Origin.prototype.createServer = function (options, cb) { Origin.prototype.startServer = function (options) { var app = this; - // Ensure that the options object is set. - if(typeof options === 'undefined') { - options = { - skipDependencyCheck: false, - skipVersionCheck: false, - skipStartLog: false - }; - } + + options = { ...{ + skipDependencyCheck: false, + skipVersionCheck: false, + skipStartLog: false + }, ...options }; + checkPrerequisites(options, function(err, result) { if(err) { logger.log('error', chalk.red(err.message)); diff --git a/lib/dml/mongoose/index.js b/lib/dml/mongoose/index.js index 87bc1a1135..3e119f34a2 100644 --- a/lib/dml/mongoose/index.js +++ b/lib/dml/mongoose/index.js @@ -7,8 +7,8 @@ var Database = require('../../database').Database, fs = require('fs'), path = require('path'), mongoose = require('mongoose'), - mongoUri = require('mongodb-uri'); - _ = require('underscore'); + mongoUri = require('mongodb-uri'), + semver = require('semver'); mongoose.Promise = global.Promise; mongoose.set('useCreateIndex', true); @@ -32,6 +32,7 @@ function MongooseDB() { this.conn = false; this._models = false; this.createdAt = new Date(); + this.mongoVersion = null; } /** @@ -69,9 +70,11 @@ MongooseDB.prototype.connect = function(db) { dbPass = configuration.getConfig('dbPass'); dbReplicaset = configuration.getConfig('dbReplicaset'); dbConnectionUri = configuration.getConfig('dbConnectionUri'); - options = configuration.getConfig('dbOptions') || {}; - options.domainsEnabled = true; - options.useNewUrlParser = true; + options = { ...configuration.getConfig('dbOptions'), ...{ + domainsEnabled: true, + useNewUrlParser: true, + useUnifiedTopology: true + }}; // Construct the authentication part of the connection string. authenticationString = dbUser && dbPass ? dbUser + ':' + dbPass + '@' : ''; @@ -109,6 +112,13 @@ MongooseDB.prototype.connect = function(db) { this.conn.once('error', function(){ logger.log('error', 'Database Connection failed, please check your database'); }); //added to give console notification of the problem this.updatedAt = new Date(); this._models = {}; + this.conn.db.command({ buildInfo: 1 }, (error, info) => { + if (error) { + logger.log('error', error); + } else { + this.mongoVersion = info.version; + } + }); }.bind(this)); }; @@ -298,6 +308,7 @@ MongooseDB.prototype.retrieve = function(objectType, search, options, callback) var limit = false; var distinct = false; var elemMatch = false; + var collation = operators && operators.collation; var jsonOnly = options && options.jsonOnly ? true : false; @@ -336,7 +347,10 @@ MongooseDB.prototype.retrieve = function(objectType, search, options, callback) } // apply any query operators - // do sort first + if (collation && semver.satisfies(this.mongoVersion, '>=3.4')) { + query.collation(collation); + } + if (sort && !distinct) { query.sort(sort); } diff --git a/lib/installHelpers.js b/lib/installHelpers.js index 9264310dc1..a9830dc705 100644 --- a/lib/installHelpers.js +++ b/lib/installHelpers.js @@ -41,6 +41,21 @@ var inputHelpers = { */ readline.moveCursor(process.stdout, 0, -1); return v; + }, + isFalsyString: function(v) { + if (typeof v !== 'string') return false; + switch (v.trim()) { + case '': + case 'N': + case 'n': + case 'false': + case 'null': + case 'undefined': + case '0': + return true; + default: + return false; + } } }; @@ -444,7 +459,7 @@ function installFramework(opts, callback) { if(fs.existsSync(opts.directory) && !opts.force) { return updateFramework(opts, callback); } - async.applyEachSeries([cloneRepo, updateFramework], opts, callback); + async.applyEachSeries([ cloneRepo, updateFramework ], opts)(callback); } function updateFramework(opts, callback) { @@ -456,7 +471,7 @@ function updateFramework(opts, callback) { installDependencies, purgeCourseFolder, updateFrameworkPlugins - ], opts, callback); + ], opts)(callback); } function checkOptions(opts, action, callback) { diff --git a/lib/outputmanager.js b/lib/outputmanager.js index c6fa7a5175..c879da9f94 100644 --- a/lib/outputmanager.js +++ b/lib/outputmanager.js @@ -607,11 +607,13 @@ OutputPlugin.prototype.writeThemeVariables = function(courseId, theme, themeVari function(seriesCallback) { // Create LESS for properties async.each(Object.keys(props), function(prop, innerCallback) { + var themeProperty = theme.properties.variables[prop]; + var inputType = themeProperty.inputType; // Check if the user has customised any properties - if (savedSettings.hasOwnProperty(prop) && theme.properties.variables[prop].default !== savedSettings[prop]) { + if (savedSettings.hasOwnProperty(prop) && themeProperty.default !== savedSettings[prop]) { // The user has customised this property // Check if processing an image asset - if (theme.properties.variables[prop].inputType === 'Asset:image') { + if (inputType.media === 'image' || inputType === 'Asset:image') { // Split the path so we can process the filename var assetPathArray = savedSettings[prop].split('/'); // Encode the filename (removing spaces, etc.) diff --git a/lib/permissions.js b/lib/permissions.js index 74e1eeef8c..37f6c4935b 100644 --- a/lib/permissions.js +++ b/lib/permissions.js @@ -1,5 +1,6 @@ // LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE -var database = require('./database'), +var _ = require('underscore'), + database = require('./database'), async = require('async'), configuration = require('./configuration'); diff --git a/lib/tenantmanager.js b/lib/tenantmanager.js index 6726971ac8..d0733d9188 100644 --- a/lib/tenantmanager.js +++ b/lib/tenantmanager.js @@ -2,6 +2,7 @@ /** * Tenant management module */ +var _ = require('underscore'); var async = require('async'); var fs = require('fs-extra'); var path = require('path'); diff --git a/package.json b/package.json index f841f857b1..70da2e1951 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "adapt_authoring", - "version": "0.10.0", + "version": "0.10.1", "license": "GPL-3.0", "description": "A server-based user interface for authoring eLearning courses using the Adapt Framework.", "keywords": [ @@ -14,80 +14,79 @@ "framework": "2", "main": "index", "engines": { - "node": "8 || 10" + "node": "10 || 12" }, "scripts": { "test": "grunt test", "migrate": "migrate --config conf/migrate.json --template-file migrations/helpers/template.js" }, "dependencies": { - "archiver": "^2.1.1", - "async": "^2.6.1", + "archiver": "^3.1.1", + "async": "^3.1.0", "bcrypt-nodejs": "0.0.3", - "body-parser": "^1.18.3", - "bower": "^1.8.4", - "bytes": "^3.0.0", - "chalk": "^2.4.1", - "compression": "^1.7.2", - "connect-mongodb-session": "^2.0.5", + "body-parser": "^1.19.0", + "bower": "^1.8.8", + "bytes": "^3.1.0", + "chalk": "^2.4.2", + "compression": "^1.7.4", + "connect-mongodb-session": "^2.2.0", "consolidate": "^0.15.1", - "cookie-parser": "^1.4.3", - "email-templates": "^4.0.0", - "errorhandler": "^1.5.0", - "express": "^4.16.3", - "express-session": "^1.15.6", - "ffmpeg-static": "^2.4.0", + "cookie-parser": "^1.4.4", + "email-templates": "^6.0.2", + "errorhandler": "^1.5.1", + "express": "^4.17.1", + "express-session": "^1.16.2", + "ffmpeg-static": "^2.6.0", "ffprobe": "^1.1.0", "ffprobe-static": "^3.0.0", "fluent-ffmpeg": "^2.1.2", "formidable": "^1.2.1", - "fs-extra": "^6.0.1", - "glob": "^7.1.2", - "grunt": "^1.0.2", + "fs-extra": "^8.1.0", + "glob": "^7.1.4", + "grunt": "^1.0.4", "grunt-contrib-copy": "^1.0.0", - "grunt-contrib-handlebars": "^1.0.0", + "grunt-contrib-handlebars": "^2.0.0", "grunt-contrib-requirejs": "^1.0.0", - "grunt-merge-json": "^0.9.7", "grunt-mocha-test": "^0.13.3", - "handlebars": "^4.0.11", - "handlebars-form-helpers": "^0.1.3", - "hbs": "^4.0.1", + "handlebars": "^4.4.0", + "handlebars-form-helpers": "^0.1.4", + "hbs": "^4.0.5", "jshint-stylish": "^2.2.1", "json-schema-mapper": "0.0.2", - "junk": "^2.1.0", - "less": "^3.0.4", - "log-update": "^2.3.0", + "junk": "^3.1.0", + "less": "^3.10.3", + "log-update": "^3.3.0", "matchdep": "^2.0.0", - "method-override": "^2.3.10", - "migrate-mongoose": "^3.2.2", - "mime": "^2.3.1", - "moment": "^2.22.1", + "method-override": "^3.0.0", + "migrate-mongoose": "^4.0.0", + "mime": "^2.4.4", + "moment": "^2.24.0", "mongodb-uri": "^0.9.7", - "mongoose": "^5.5.8", - "morgan": "^1.9.0", - "multer": "^1.3.0", - "needle": "^2.2.1", - "node-polyglot": "^2.2.2", - "nodemailer": "^4.6.4", + "mongoose": "^5.7.3", + "morgan": "^1.9.1", + "multer": "^1.4.2", + "needle": "^2.4.0", + "node-polyglot": "^2.4.0", + "nodemailer": "^6.3.0", "optimist": "^0.6.1", "passport": "^0.4.0", "prompt": "^1.0.0", - "request": "^2.87.0", - "semver": "^5.5.0", + "request": "^2.88.0", + "semver": "^6.3.0", "serve-favicon": "^2.5.0", "traverse": "^0.6.6", - "underscore": "^1.9.0", - "unzip": "^0.1.11", - "validator": "^10.2.0", - "winston": "3", - "yauzl": "^2.9.1" + "underscore": "^1.9.1", + "unzipper": "^0.10.5", + "validator": "^11.1.0", + "winston": "^3.2.1", + "yauzl": "^2.10.0" }, "devDependencies": { - "mocha": "^5.2.0", - "mocha-multi": "^1.0.1", + "mocha": "^6.2.1", + "mocha-multi": "^1.1.3", "mocha-simple-html-reporter": "^1.1.0", - "mongodb": "^3.0.10", - "should": "^13.2.1", - "supertest": "^3.1.0" + "mongodb": "^3.3.2", + "should": "^13.2.3", + "supertest": "^4.0.2" } } diff --git a/plugins/content/bower/index.js b/plugins/content/bower/index.js index 4f8cc970b8..ecc4accec8 100644 --- a/plugins/content/bower/index.js +++ b/plugins/content/bower/index.js @@ -27,7 +27,7 @@ var origin = require('../../../'), _ = require('underscore'), util = require('util'), path = require('path'), - unzip = require('unzip'), + unzip = require('unzipper'), exec = require('child_process').exec, IncomingForm = require('formidable').IncomingForm, installHelpers = require('../../../lib/installHelpers'), @@ -803,93 +803,114 @@ function addPackage (plugin, packageInfo, options, cb) { return addCb(err); } - // don't duplicate component.name, component.version - db.retrieve(plugin.type, { name: package.name, version: package.version }, function (err, results) { + const targetAttribute = pkgMeta.targetAttribute; + + async.some([ 'componenttype', 'extensiontype', 'menutype', 'themetype' ], (type, asyncCallback) => { + if (!targetAttribute) return asyncCallback(); + + db.retrieve(type, { targetAttribute: targetAttribute }, (err, results) => { + asyncCallback(err, results && results.length); + }); + }, (err, targetAttributeExists) => { if (err) { logger.log('error', err); return addCb(err); } - if (results && 0 !== results.length) { - // don't add duplicate - if (options.strict) { - return addCb(new PluginPackageError("Can't add plugin: plugin already exists!")); - } - return addCb(null); + if (targetAttributeExists) { + return addCb(new PluginPackageError(app.polyglot.t('app.targetattributeexists', { + targetAttribute + }))); } - db.create(plugin.type, package, function (err, newPlugin) { + // don't duplicate component.name, component.version + db.retrieve(plugin.type, { name: package.name, version: package.version }, function (err, results) { if (err) { + logger.log('error', err); + return addCb(err); + } + + if (results && 0 !== results.length) { + // don't add duplicate if (options.strict) { - return addCb(err); + return addCb(new PluginPackageError(app.polyglot.t('app.versionexists'))); } - - logger.log('error', 'Failed to add package: ' + package.name, err); return addCb(null); } - logger.log('info', 'Added package: ' + package.name); - - // #509 update content targeted by previous versions of this package - logger.log('info', 'searching old package types ... '); - db.retrieve(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err, results) { - + db.create(plugin.type, package, function (err, newPlugin) { if (err) { - // strictness doesn't matter at this point - logger.log('error', 'Failed to retrieve previous packages: ' + err.message, err); + if (options.strict) { + return addCb(err); + } + + logger.log('error', 'Failed to add package: ' + package.name, err); + return addCb(null); } - if (results && results.length) { - // found previous versions to update - // only update content using the id of the most recent version - var oldPlugin = false; - results.forEach(function (item) { - if (!oldPlugin) { - oldPlugin = item; - } else if (semver.gt(item.version, oldPlugin.version)) { - oldPlugin = item; - } - }); + logger.log('info', 'Added package: ' + package.name); - // Persist the _isAvailableInEditor flag. - db.update(plugin.type, {_id: newPlugin._id}, {_isAvailableInEditor: oldPlugin._isAvailableInEditor}, function(err, results) { - if (err) { - logger.log('error', err); - return addCb(err); - } + // #509 update content targeted by previous versions of this package + logger.log('info', 'searching old package types ... '); + db.retrieve(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err, results) { + + if (err) { + // strictness doesn't matter at this point + logger.log('error', 'Failed to retrieve previous packages: ' + err.message, err); + } + + if (results && results.length) { + // found previous versions to update + // only update content using the id of the most recent version + var oldPlugin = false; + results.forEach(function (item) { + if (!oldPlugin) { + oldPlugin = item; + } else if (semver.gt(item.version, oldPlugin.version)) { + oldPlugin = item; + } + }); - plugin.updateLegacyContent(newPlugin, oldPlugin, function (err) { + // Persist the _isAvailableInEditor flag. + db.update(plugin.type, {_id: newPlugin._id}, {_isAvailableInEditor: oldPlugin._isAvailableInEditor}, function(err, results) { if (err) { logger.log('error', err); return addCb(err); } - // Remove older versions of this plugin - db.destroy(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err) { + plugin.updateLegacyContent(newPlugin, oldPlugin, function (err) { if (err) { logger.log('error', err); return addCb(err); } - logger.log('info', 'Successfully removed versions of ' + package.name + '(' + plugin.type + ') older than ' + newPlugin.version); - return addCb(null, newPlugin); + // Remove older versions of this plugin + db.destroy(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err) { + if (err) { + logger.log('error', err); + return addCb(err); + } + + logger.log('info', 'Successfully removed versions of ' + package.name + '(' + plugin.type + ') older than ' + newPlugin.version); + return addCb(null, newPlugin); + }); }); }); - }); - } else { - // nothing to do! - // Remove older versions of this plugin - db.destroy(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err) { - if (err) { - logger.log('error', err); - return addCb(err); - } + } else { + // nothing to do! + // Remove older versions of this plugin + db.destroy(plugin.type, { name: package.name, version: { $ne: newPlugin.version } }, function (err) { + if (err) { + logger.log('error', err); + return addCb(err); + } - logger.log('info', 'Successfully removed versions of ' + package.name + '(' + plugin.type + ') older than ' + newPlugin.version); + logger.log('info', 'Successfully removed versions of ' + package.name + '(' + plugin.type + ') older than ' + newPlugin.version); - return addCb(null, newPlugin); - }); - } + return addCb(null, newPlugin); + }); + } + }); }); }); }); @@ -1058,7 +1079,7 @@ function handleUploadedPlugin (req, res, next) { var file = files.file; if (!file || !file.path) { - return next(new PluginPackageError('File upload failed!')); + return next(new PluginPackageError(app.polyglot.t('app.fileuploaderror'))); } // try unzipping @@ -1104,17 +1125,19 @@ function handleUploadedPlugin (req, res, next) { }, function(hasResults) { if (!hasResults) { - return next(new PluginPackageError('Cannot find expected bower.json file in the plugin root, please check the structure of your zip file and try again.')); + return next(app.polyglot.t('app.cannotfindbower')); } if (!packageJson) { - return next(new PluginPackageError('Unrecognized plugin - a plugin should have a bower.json file')); + return next(app.polyglot.t('app.unrecognisedplugin')); } // extract the plugin type from the package var pluginType = extractPluginType(packageJson); if (!pluginType) { - return next(new PluginPackageError('Unrecognized plugin type for package ' + packageJson.name)); + return next(new PluginPackageError(app.polyglot.t('app.unrecognisedpluginforpackage', { + package: packageJson.name + }))); } // mark as a locally installed package @@ -1132,7 +1155,9 @@ function handleUploadedPlugin (req, res, next) { } // Check if the framework has been defined on the plugin and that it's not compatible if (packageInfo.pkgMeta.framework && !semver.satisfies(semver.clean(frameworkVersion), packageInfo.pkgMeta.framework, { includePrerelease: true })) { - return next(new PluginPackageError('This plugin is incompatible with version ' + frameworkVersion + ' of the Adapt framework')); + return next(new PluginPackageError(app.polyglot.t('app.incompatibleframework', { + framework: frameworkVersion + }))); } app.contentmanager.getContentPlugin(pluginType, function (error, contentPlugin) { if (error) { @@ -1145,10 +1170,9 @@ function handleUploadedPlugin (req, res, next) { function sendResponse() { res.statusCode = 200; - return res.json({ - success: true, - pluginType: pluginType, - message: 'successfully added new plugin' + return res.json({ + success: true, + pluginType: pluginType }); } diff --git a/plugins/filestorage/localfs/index.js b/plugins/filestorage/localfs/index.js index c340a741fb..f4d8abbc1b 100644 --- a/plugins/filestorage/localfs/index.js +++ b/plugins/filestorage/localfs/index.js @@ -232,7 +232,7 @@ LocalFileStorage.prototype.processFileUpload = function (file, newPath, options, if (options.createMetadata) { return self.inspectFile(newPath, file.type, function (err, withMeta) { if (withMeta) { - data = _.extend(data, withMeta); + Object.assign(data, withMeta); } nextFunc(); }); diff --git a/plugins/output/adapt/helpers.js b/plugins/output/adapt/helpers.js index 004981a4ab..ba5c3a5573 100644 --- a/plugins/output/adapt/helpers.js +++ b/plugins/output/adapt/helpers.js @@ -167,11 +167,6 @@ function importAsset(fileMetadata, metadata, assetImported) { } var asset = _.extend(fileMetadata, storedFile); - _.each(asset.tags, function iterator(tag, index) { - if (metadata.idMap[tag]) { - asset.tags[index] = metadata.idMap[tag]; - } - }); origin.assetmanager.createAsset(asset, function onAssetCreated(createError, assetRec) { if (createError) { diff --git a/plugins/output/adapt/importsource.js b/plugins/output/adapt/importsource.js index e415f01fdc..2e5b0a9e5a 100644 --- a/plugins/output/adapt/importsource.js +++ b/plugins/output/adapt/importsource.js @@ -169,14 +169,26 @@ function ImportSource(req, done) { var fileStat = fs.statSync(assetPath); var assetTitle = assetName; var assetDescription = assetName; - var tags = formTags.slice(); + var assetJson = assetsJson[assetName]; + var tags = []; - if (assetsJson[assetName]) { - assetTitle = assetsJson[assetName].title; - assetDescription = assetsJson[assetName].description; + if (assetJson) { + assetTitle = assetJson.title; + assetDescription = assetJson.description; - assetsJson[assetName].tags.forEach(function(tag) { - tags.push(tag._id); + assetJson.tags.forEach(function(tag) { + const tagTitle = tag.title; + const warn = (error) => logger.log('warn', `Failed to create asset tag '${tagTitle}' ${error}`); + + if(!tagTitle) return warn(new Error('Tag has no title')); + + app.contentmanager.getContentPlugin('tag', function(error, plugin) { + if(error) return warn(error); + plugin.create({ title: tagTitle }, function(error, record) { // @note retrieves if tag already exists + if(error) return warn(error); + tags.push(record._id); + }); + }); }); } var fileMeta = { diff --git a/routes/lang/en-application.json b/routes/lang/en.json similarity index 97% rename from routes/lang/en-application.json rename to routes/lang/en.json index dc5608859f..0fb8d29ad4 100644 --- a/routes/lang/en-application.json +++ b/routes/lang/en.json @@ -410,5 +410,12 @@ "app.searchByMail": "Search by email", "app.maxfileuploadsize": "Maximum upload file size: %{size}.", "app.uploadsizeerror": "File size limit exceeded. Expected no more than %{max}, received %{size}.", + "app.fileuploaderror": "File upload failed.", + "app.cannotfindbower": "Cannot find expected bower.json file in the plugin root, please check the structure of your zip file and try again.", + "app.unrecognisedplugin": "Unrecognised plugin - a plugin should have a bower.json file.", + "app.unrecognisedpluginforpackage": "Unrecognised plugin type for package %{package}.", + "app.incompatibleframework": "This plugin is incompatible with version %{framework} of the Adapt framework.", + "app.targetattributeexists": "There is a plugin already installed with a target attribute of '%{targetAttribute}'.", + "app.versionexists": "You already have this version of the plugin installed.", "app.unknownuser": "Unknown User" } diff --git a/routes/lang/es-application.json b/routes/lang/es.json similarity index 100% rename from routes/lang/es-application.json rename to routes/lang/es.json diff --git a/routes/lang/index.js b/routes/lang/index.js index 3452a3022a..0c9e2d3f34 100644 --- a/routes/lang/index.js +++ b/routes/lang/index.js @@ -1,18 +1,19 @@ var express = require('express'); var path = require('path'); var fs = require('fs'); -var server = module.exports = express(); -server.get('/lang/:lang', function (req, res, next) { - var lang = req.params.lang; // ie 'en' for /lang/en - var filename = path.join(__dirname, lang) + '.json'; - var file; +var configuration = require('../../lib/configuration'); +var Constants = require('../../lib/outputmanager').Constants; - fs.exists(filename, function(exists) { - file = exists - ? require(filename) - : require(path.join(__dirname, 'en') + '.json'); +var server = module.exports = express(); - return res.json(file); - }); +server.get('/lang/:lang', function (req, res, next) { + var lang = req.params.lang; // ie 'en' for /lang/en + var filename = path.join(configuration.serverRoot, Constants.Folders.Temp, 'lang', lang + '.json'); + fs.exists(filename, function(exists) { + if(!exists) { + return res.status(404).end(); + } + return res.json(require(filename)); + }); }); diff --git a/server.js b/server.js index b9cb5f5099..1433d60580 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ -// LICENCE https://github.com/adaptlearning/adapt_authoring/blob/master/LICENSE -var app = require('./lib/application')(); -app.run(); +const app = require('./lib/application')(); +const argv = require('optimist').argv; + +app.run(argv); diff --git a/test/entry.js b/test/entry.js index a736337799..1e6dfb879c 100644 --- a/test/entry.js +++ b/test/entry.js @@ -112,7 +112,8 @@ function removeTestData(done) { var connStr = 'mongodb://' + testConfig.dbHost + ':' + testConfig.dbPort + '/' + testConfig.dbName; MongoClient.connect(connStr, { domainsEnabled: true, - useNewUrlParser: true + useNewUrlParser: true, + useUnifiedTopology: true }, function(error, client) { if(error) return cb(error); diff --git a/upgrade.js b/upgrade.js index 4c88195e04..a6adfb316b 100644 --- a/upgrade.js +++ b/upgrade.js @@ -142,7 +142,7 @@ function checkForUpdates(callback) { function doUpdate(data) { async.series([ function upgradeAuthoring(cb) { - if(!data.adapt_authoring) { + if (installHelpers.inputHelpers.isFalsyString(data.adapt_authoring)) { return cb(); } installHelpers.updateAuthoring({ @@ -159,7 +159,7 @@ function doUpdate(data) { }); }, function upgradeFramework(cb) { - if(!data.adapt_framework) { + if (installHelpers.inputHelpers.isFalsyString(data.adapt_framework)) { return cb(); } var dir = path.join(configuration.tempDir, configuration.getConfig('masterTenantID'), OutputConstants.Folders.Framework);