mirror of
https://github.com/moodle/moodle.git
synced 2025-02-22 19:06:51 +01:00
420 lines
14 KiB
JavaScript
420 lines
14 KiB
JavaScript
// This file is part of Moodle - http://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
/**
|
|
* Helper functions for working with Moodle component names, directories, and sources.
|
|
*
|
|
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
|
|
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
*/
|
|
|
|
"use strict";
|
|
/* eslint-env node */
|
|
|
|
/** @var {Object} A list of subsystems in Moodle */
|
|
const componentData = {};
|
|
|
|
/**
|
|
* Load details of all moodle modules.
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
const fetchComponentData = () => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const glob = require('glob');
|
|
const gruntFilePath = process.cwd();
|
|
|
|
if (!Object.entries(componentData).length) {
|
|
componentData.subsystems = {};
|
|
componentData.pathList = [];
|
|
componentData.components = {};
|
|
componentData.standardComponents = {};
|
|
|
|
// Fetch the component definiitions from the distributed JSON file.
|
|
const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
|
|
const pluginData = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/plugins.json`));
|
|
|
|
componentData.pluginTypes = components.plugintypes;
|
|
|
|
const standardPlugins = Object.entries(pluginData.standard).map(
|
|
([pluginType, pluginNames]) => {
|
|
return pluginNames.map(pluginName => `${pluginType}_${pluginName}`);
|
|
}
|
|
).reduce((acc, val) => acc.concat(val), []);
|
|
|
|
// Build the list of moodle subsystems.
|
|
componentData.subsystems.lib = 'core';
|
|
componentData.pathList.push(process.cwd() + path.sep + 'lib');
|
|
for (const [component, thisPath] of Object.entries(components.subsystems)) {
|
|
if (thisPath) {
|
|
// Prefix "core_" to the front of the subsystems.
|
|
componentData.subsystems[thisPath] = `core_${component}`;
|
|
componentData.pathList.push(process.cwd() + path.sep + thisPath);
|
|
}
|
|
}
|
|
|
|
// The list of components includes the list of subsystems.
|
|
componentData.components = {...componentData.subsystems};
|
|
|
|
// Go through each of the plugintypes.
|
|
Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => {
|
|
// We don't allow any code in this place..?
|
|
glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => {
|
|
const componentPath = fs.realpathSync(path.dirname(versionPath));
|
|
const componentName = path.basename(componentPath);
|
|
const frankenstyleName = `${pluginType}_${componentName}`;
|
|
componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName;
|
|
componentData.pathList.push(componentPath);
|
|
|
|
// Look for any subplugins.
|
|
const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`;
|
|
if (fs.existsSync(subPluginConfigurationFile)) {
|
|
const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile)));
|
|
|
|
Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => {
|
|
glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => {
|
|
const componentPath = fs.realpathSync(path.dirname(versionPath));
|
|
const componentName = path.basename(componentPath);
|
|
const frankenstyleName = `${subpluginType}_${componentName}`;
|
|
|
|
componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName;
|
|
componentData.pathList.push(componentPath);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// Create a list of the standard subsystem and plugins.
|
|
componentData.standardComponents = Object.fromEntries(
|
|
Object.entries(componentData.components).filter(([, name]) => {
|
|
if (name === 'core' || name.startsWith('core_')) {
|
|
return true;
|
|
}
|
|
return standardPlugins.indexOf(name) !== -1;
|
|
})
|
|
);
|
|
|
|
componentData.componentMapping = Object.fromEntries(
|
|
Object.entries(componentData.components).map(([path, name]) => [name, path])
|
|
);
|
|
}
|
|
|
|
return componentData;
|
|
};
|
|
|
|
/**
|
|
* Get the list of component paths.
|
|
*
|
|
* @param {string} relativeTo
|
|
* @returns {array}
|
|
*/
|
|
const getComponentPaths = (relativeTo = '') => fetchComponentData().pathList.map(componentPath => {
|
|
return componentPath.replace(relativeTo, '');
|
|
});
|
|
|
|
/**
|
|
* Get the list of paths to build AMD sources.
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
const getAmdSrcGlobList = () => {
|
|
const globList = [];
|
|
fetchComponentData().pathList.forEach(componentPath => {
|
|
globList.push(`${componentPath}/amd/src/*.js`);
|
|
globList.push(`${componentPath}/amd/src/**/*.js`);
|
|
});
|
|
|
|
return globList;
|
|
};
|
|
|
|
/**
|
|
* Get the list of paths to build YUI sources.
|
|
*
|
|
* @param {String} relativeTo
|
|
* @returns {Array}
|
|
*/
|
|
const getYuiSrcGlobList = relativeTo => {
|
|
const globList = [];
|
|
fetchComponentData().pathList.forEach(componentPath => {
|
|
const relativeComponentPath = componentPath.replace(relativeTo, '');
|
|
globList.push(`${relativeComponentPath}/yui/src/**/*.js`);
|
|
});
|
|
|
|
return globList;
|
|
};
|
|
|
|
/**
|
|
* Get the list of paths to thirdpartylibs.xml.
|
|
*
|
|
* @param {String} relativeTo
|
|
* @returns {Array}
|
|
*/
|
|
const getThirdPartyLibsList = relativeTo => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
return fetchComponentData().pathList
|
|
.map(componentPath => path.relative(relativeTo, componentPath) + '/thirdpartylibs.xml')
|
|
.map(componentPath => componentPath.replace(/\\/g, '/'))
|
|
.filter(path => fs.existsSync(path))
|
|
.sort();
|
|
};
|
|
|
|
/**
|
|
* Get the list of thirdparty library paths.
|
|
*
|
|
* @returns {array}
|
|
*/
|
|
const getThirdPartyPaths = () => {
|
|
const DOMParser = require('@xmldom/xmldom').DOMParser;
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const xpath = require('xpath');
|
|
|
|
const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
|
|
const libs = ['node_modules/', 'vendor/'];
|
|
|
|
const addLibToList = lib => {
|
|
if (!lib.match('\\*') && fs.statSync(lib).isDirectory()) {
|
|
// Ensure trailing slash on dirs.
|
|
lib = lib.replace(/\/?$/, '/');
|
|
}
|
|
|
|
// Look for duplicate paths before adding to array.
|
|
if (libs.indexOf(lib) === -1) {
|
|
libs.push(lib);
|
|
}
|
|
};
|
|
|
|
thirdpartyfiles.forEach(function(file) {
|
|
const dirname = path.dirname(file);
|
|
|
|
const xmlContent = fs.readFileSync(file, 'utf8');
|
|
const doc = new DOMParser().parseFromString(xmlContent);
|
|
const nodes = xpath.select("/libraries/library/location/text()", doc);
|
|
|
|
nodes.forEach(function(node) {
|
|
let lib = path.posix.join(dirname, node.toString());
|
|
addLibToList(lib);
|
|
});
|
|
});
|
|
|
|
return libs;
|
|
|
|
};
|
|
|
|
/**
|
|
* Find the name of the component matching the specified path.
|
|
*
|
|
* @param {String} path
|
|
* @returns {String|null} Name of matching component.
|
|
*/
|
|
const getComponentFromPath = path => {
|
|
const componentList = fetchComponentData().components;
|
|
|
|
if (componentList.hasOwnProperty(path)) {
|
|
return componentList[path];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
|
|
*
|
|
* @param {String} checkPath The path to check. This can be with either Windows, or Linux directory separators.
|
|
* @returns {String|null}
|
|
*/
|
|
const getOwningComponentDirectory = checkPath => {
|
|
const path = require('path');
|
|
|
|
// Fetch all components into a reverse sorted array.
|
|
// This ensures that components which are within the directory of another component match first.
|
|
const pathList = Object.keys(fetchComponentData().components).sort().reverse();
|
|
for (const componentPath of pathList) {
|
|
// If the componentPath is the directory being checked, it will be empty.
|
|
// If the componentPath is a parent of the directory being checked, the relative directory will not start with ..
|
|
if (!path.relative(componentPath, checkPath).startsWith('..')) {
|
|
return componentPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the latest tag in a remote GitHub repository.
|
|
*
|
|
* @param {string} url The remote repository.
|
|
* @returns {Array}
|
|
*/
|
|
const getRepositoryTags = async(url) => {
|
|
const gtr = require('git-tags-remote');
|
|
try {
|
|
const tags = await gtr.get(url);
|
|
if (tags !== undefined) {
|
|
return tags;
|
|
}
|
|
} catch (error) {
|
|
return [];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
/**
|
|
* Get the list of thirdparty libraries that could be upgraded.
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
const getThirdPartyLibsUpgradable = async() => {
|
|
const libraries = getThirdPartyLibsData().filter((library) => !!library.repository);
|
|
const upgradableLibraries = [];
|
|
const versionCompare = (a, b) => {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
|
|
const aParts = a.split('.');
|
|
const bParts = b.split('.');
|
|
|
|
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
|
const aPart = parseInt(aParts[i], 10);
|
|
const bPart = parseInt(bParts[i], 10);
|
|
if (aPart > bPart) {
|
|
// 1.1.0 > 1.0.9
|
|
return 1;
|
|
} else if (aPart < bPart) {
|
|
// 1.0.9 < 1.1.0
|
|
return -1;
|
|
} else {
|
|
// Same version.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (aParts.length > bParts.length) {
|
|
// 1.0.1 > 1.0
|
|
return 1;
|
|
}
|
|
|
|
// 1.0 < 1.0.1
|
|
return -1;
|
|
};
|
|
|
|
for (let library of libraries) {
|
|
upgradableLibraries.push(
|
|
getRepositoryTags(library.repository).then((tagMap) => {
|
|
library.version = library.version.replace(/^v/, '');
|
|
const currentVersion = library.version.replace(/moodle-/, '');
|
|
const currentMajorVersion = library.version.split('.')[0];
|
|
const tags = [...tagMap]
|
|
.map((tagData) => tagData[0])
|
|
.filter((tag) => !tag.match(/(alpha|beta|rc)/))
|
|
.map((tag) => tag.replace(/^v/, ''))
|
|
.sort((a, b) => versionCompare(b, a));
|
|
if (!tags.length) {
|
|
library.warning = "Unable to find any comparable tags.";
|
|
return library;
|
|
}
|
|
|
|
library.latestVersion = tags[0];
|
|
tags.some((tag) => {
|
|
if (!tag) {
|
|
return false;
|
|
}
|
|
|
|
// See if the version part matches.
|
|
const majorVersion = tag.split('.')[0];
|
|
if (majorVersion === currentMajorVersion) {
|
|
library.latestSameMajorVersion = tag;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
|
|
if (versionCompare(currentVersion, library.latestVersion) > 0) {
|
|
// Moodle somehow has a newer version than the latest version.
|
|
library.warning = `Newer version found: ${currentVersion} > ${library.latestVersion} for ${library.name}`;
|
|
return library;
|
|
}
|
|
|
|
|
|
if (library.version !== library.latestVersion) {
|
|
// Delete version and add it again at the end of the array. That way, current and new will stay closer.
|
|
delete library.version;
|
|
library.version = currentVersion;
|
|
return library;
|
|
}
|
|
return null;
|
|
})
|
|
);
|
|
}
|
|
|
|
return (await Promise.all(upgradableLibraries)).filter((library) => !!library);
|
|
};
|
|
|
|
/**
|
|
* Get the list of thirdparty libraries.
|
|
*
|
|
* @returns {Array}
|
|
*/
|
|
const getThirdPartyLibsData = () => {
|
|
const DOMParser = require('@xmldom/xmldom').DOMParser;
|
|
const fs = require('fs');
|
|
const xpath = require('xpath');
|
|
const path = require('path');
|
|
|
|
const libraryList = [];
|
|
const libraryFields = [
|
|
'location',
|
|
'name',
|
|
'version',
|
|
'repository',
|
|
];
|
|
|
|
const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
|
|
thirdpartyfiles.forEach(function(libraryPath) {
|
|
const xmlContent = fs.readFileSync(libraryPath, 'utf8');
|
|
const doc = new DOMParser().parseFromString(xmlContent);
|
|
const libraries = xpath.select("/libraries/library", doc);
|
|
for (const library of libraries) {
|
|
const libraryData = [];
|
|
for (const field of libraryFields) {
|
|
libraryData[field] = xpath.select(`${field}/text()`, library)?.toString();
|
|
}
|
|
libraryData.location = path.join(path.dirname(libraryPath), libraryData.location);
|
|
libraryList.push(libraryData);
|
|
}
|
|
});
|
|
|
|
return libraryList.sort((a, b) => a.location.localeCompare(b.location));
|
|
};
|
|
|
|
module.exports = {
|
|
fetchComponentData,
|
|
getAmdSrcGlobList,
|
|
getComponentFromPath,
|
|
getComponentPaths,
|
|
getOwningComponentDirectory,
|
|
getYuiSrcGlobList,
|
|
getThirdPartyLibsList,
|
|
getThirdPartyPaths,
|
|
getThirdPartyLibsUpgradable,
|
|
};
|