diff --git a/.grunt/components.js b/.grunt/components.js
index 9f1651e94ab..a520198c075 100644
--- a/.grunt/components.js
+++ b/.grunt/components.js
@@ -232,6 +232,155 @@ const getOwningComponentDirectory = checkPath => {
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').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,
@@ -241,4 +390,5 @@ module.exports = {
getYuiSrcGlobList,
getThirdPartyLibsList,
getThirdPartyPaths,
+ getThirdPartyLibsUpgradable,
};
diff --git a/.grunt/tasks/upgradablelibs.js b/.grunt/tasks/upgradablelibs.js
new file mode 100644
index 00000000000..be1f19860ff
--- /dev/null
+++ b/.grunt/tasks/upgradablelibs.js
@@ -0,0 +1,42 @@
+// 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 .
+/* jshint node: true, browser: false */
+/* eslint-env node */
+
+/**
+ * @copyright 2023 Sara Arjona
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+module.exports = grunt => {
+ /**
+ * Generate upgradable third-party libraries (utilising thirdpartylibs.xml data)
+ */
+ grunt.registerTask('upgradablelibs', 'Generate upgradable third-party libraries', async function() {
+ const done = this.async();
+
+ const path = require('path');
+ const ComponentList = require(path.join(process.cwd(), '.grunt', 'components.js'));
+
+ // An array of third party libraries that have a newer version in their repositories.
+ const thirdPartyLibs = await ComponentList.getThirdPartyLibsUpgradable({progress: true});
+ for (let library of thirdPartyLibs) {
+ grunt.log.ok(JSON.stringify(Object.assign({}, library), null, 4));
+ }
+
+ done();
+ });
+
+};
diff --git a/Gruntfile.js b/Gruntfile.js
index b600f6db6ee..7cc31e33a39 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -260,6 +260,8 @@ module.exports = function(grunt) {
addTask('watch', grunt);
addTask('startup', grunt);
+ addTask('upgradablelibs', grunt);
+
// Register the default task.
grunt.registerTask('default', ['startup']);
};
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 5d85b1306e4..d9b8f2fa1a4 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -79,6 +79,7 @@ information provided here is intended especially for developers.
Course formats using components will be allowed to use one level indentation only.
* The method `flexible_table::set_columnsattributes` now can be used with 'class' key to add custom classes to the DOM.
* The editor_tinymce plugin has been removed from core.
+* A new grunt task, upgradablelibs, has been added to get the list of libraries that have a newer version in their repositories.
=== 4.1 ===
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 3c53398cf18..fdae64ae012 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -29,6 +29,7 @@
"eslint-plugin-promise": "6.0.0",
"fb-watchman": "2.0.1",
"gherkin-lint": "^4.2.2",
+ "git-tags-remote": "^1.0.5",
"glob": "7.2.0",
"grunt": "^1.4.1",
"grunt-contrib-uglify": "5.0.1",
@@ -6372,6 +6373,15 @@
"node": ">= 6"
}
},
+ "node_modules/git-tags-remote": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/git-tags-remote/-/git-tags-remote-1.0.5.tgz",
+ "integrity": "sha512-BMPL7t5XWDTD1AAyc+0rtq5zAE6e6QPE8KYu1nLPI0+JZztmCWhUNzNLF3P8vPSvJ1YupCL9NYiM6OQevJYY1g==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.3.2"
+ }
+ },
"node_modules/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
@@ -18989,6 +18999,15 @@
}
}
},
+ "git-tags-remote": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/git-tags-remote/-/git-tags-remote-1.0.5.tgz",
+ "integrity": "sha512-BMPL7t5XWDTD1AAyc+0rtq5zAE6e6QPE8KYu1nLPI0+JZztmCWhUNzNLF3P8vPSvJ1YupCL9NYiM6OQevJYY1g==",
+ "dev": true,
+ "requires": {
+ "semver": "^7.3.2"
+ }
+ },
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
diff --git a/package.json b/package.json
index 5baef68aab9..80d38a53073 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"eslint-plugin-promise": "6.0.0",
"fb-watchman": "2.0.1",
"gherkin-lint": "^4.2.2",
+ "git-tags-remote": "^1.0.5",
"glob": "7.2.0",
"grunt": "^1.4.1",
"grunt-contrib-uglify": "5.0.1",