Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Helper functions for working with Moodle component names, directories, and sources.
18
 *
19
 * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
20
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
21
 */
22
 
23
"use strict";
24
/* eslint-env node */
25
 
26
/** @var {Object} A list of subsystems in Moodle */
27
const componentData = {};
28
 
29
/**
30
 * Load details of all moodle modules.
31
 *
32
 * @returns {object}
33
 */
34
const fetchComponentData = () => {
35
    const fs = require('fs');
36
    const path = require('path');
37
    const glob = require('glob');
38
    const gruntFilePath = process.cwd();
39
 
40
    if (!Object.entries(componentData).length) {
41
        componentData.subsystems = {};
42
        componentData.pathList = [];
43
        componentData.components = {};
44
        componentData.standardComponents = {};
45
 
46
        // Fetch the component definiitions from the distributed JSON file.
47
        const components = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/components.json`));
48
        const pluginData = JSON.parse(fs.readFileSync(`${gruntFilePath}/lib/plugins.json`));
49
 
50
        componentData.pluginTypes = components.plugintypes;
51
 
52
        const standardPlugins = Object.entries(pluginData.standard).map(
53
            ([pluginType, pluginNames]) => {
54
                return pluginNames.map(pluginName => `${pluginType}_${pluginName}`);
55
            }
56
        ).reduce((acc, val) => acc.concat(val), []);
57
 
58
        // Build the list of moodle subsystems.
59
        componentData.subsystems.lib = 'core';
60
        componentData.pathList.push(process.cwd() + path.sep + 'lib');
61
        for (const [component, thisPath] of Object.entries(components.subsystems)) {
62
            if (thisPath) {
63
                // Prefix "core_" to the front of the subsystems.
64
                componentData.subsystems[thisPath] = `core_${component}`;
65
                componentData.pathList.push(process.cwd() + path.sep + thisPath);
66
            }
67
        }
68
 
69
        // The list of components includes the list of subsystems.
70
        componentData.components = {...componentData.subsystems};
71
 
72
        const subpluginAdder = (subpluginType, subpluginTypePath) => {
73
            glob.sync(`${subpluginTypePath}/*/version.php`).forEach(versionPath => {
74
                const componentPath = fs.realpathSync(path.dirname(versionPath));
75
                const componentName = path.basename(componentPath);
76
                const frankenstyleName = `${subpluginType}_${componentName}`;
77
 
78
                componentData.components[`${subpluginTypePath}/${componentName}`] = frankenstyleName;
79
                componentData.pathList.push(componentPath);
80
            });
81
        };
82
 
83
        // Go through each of the plugintypes.
84
        Object.entries(components.plugintypes).forEach(([pluginType, pluginTypePath]) => {
85
            // We don't allow any code in this place..?
86
            glob.sync(`${pluginTypePath}/*/version.php`).forEach(versionPath => {
87
                const componentPath = fs.realpathSync(path.dirname(versionPath));
88
                const componentName = path.basename(componentPath);
89
                const frankenstyleName = `${pluginType}_${componentName}`;
90
                componentData.components[`${pluginTypePath}/${componentName}`] = frankenstyleName;
91
                componentData.pathList.push(componentPath);
92
 
93
                // Look for any subplugins.
94
                const subPluginConfigurationFile = `${componentPath}/db/subplugins.json`;
95
                if (fs.existsSync(subPluginConfigurationFile)) {
96
                    const subpluginList = JSON.parse(fs.readFileSync(fs.realpathSync(subPluginConfigurationFile)));
97
 
98
                    if (subpluginList.subplugintypes) {
99
                        Object.entries(subpluginList.subplugintypes).forEach(([subpluginType, subpluginTypePath]) => {
100
                            subpluginAdder(
101
                                subpluginType,
102
                                `${pluginTypePath}/${componentName}/${subpluginTypePath}`
103
                            );
104
                        });
105
                    } else if (subpluginList.plugintypes) {
106
                        Object.entries(subpluginList.plugintypes).forEach(([subpluginType, subpluginTypePath]) => {
107
                            subpluginAdder(subpluginType, subpluginTypePath);
108
                        });
109
                    }
110
                }
111
            });
112
        });
113
 
114
 
115
        // Create a list of the standard subsystem and plugins.
116
        componentData.standardComponents = Object.fromEntries(
117
            Object.entries(componentData.components).filter(([, name]) => {
118
                if (name === 'core' || name.startsWith('core_')) {
119
                    return true;
120
                }
121
                return standardPlugins.indexOf(name) !== -1;
122
            })
123
        );
124
 
125
        componentData.componentMapping = Object.fromEntries(
126
            Object.entries(componentData.components).map(([path, name]) => [name, path])
127
        );
128
    }
129
 
130
    return componentData;
131
};
132
 
133
/**
134
 * Get the list of component paths.
135
 *
136
 * @param   {string} relativeTo
137
 * @returns {array}
138
 */
139
const getComponentPaths = (relativeTo = '') => fetchComponentData().pathList.map(componentPath => {
140
    return componentPath.replace(relativeTo, '');
141
});
142
 
143
/**
144
 * Get the list of paths to build AMD sources.
145
 *
146
 * @returns {Array}
147
 */
148
const getAmdSrcGlobList = () => {
149
    const globList = [];
150
    fetchComponentData().pathList.forEach(componentPath => {
151
        globList.push(`${componentPath}/amd/src/*.js`);
152
        globList.push(`${componentPath}/amd/src/**/*.js`);
153
    });
154
 
155
    return globList;
156
};
157
 
158
/**
159
 * Get the list of paths to build YUI sources.
160
 *
161
 * @param {String} relativeTo
162
 * @returns {Array}
163
 */
164
const getYuiSrcGlobList = relativeTo => {
165
    const globList = [];
166
    fetchComponentData().pathList.forEach(componentPath => {
167
        const relativeComponentPath = componentPath.replace(relativeTo, '');
168
        globList.push(`${relativeComponentPath}/yui/src/**/*.js`);
169
    });
170
 
171
    return globList;
172
};
173
 
174
/**
175
 * Get the list of paths to thirdpartylibs.xml.
176
 *
177
 * @param {String} relativeTo
178
 * @returns {Array}
179
 */
180
const getThirdPartyLibsList = relativeTo => {
181
    const fs = require('fs');
182
    const path = require('path');
183
 
184
    return fetchComponentData().pathList
185
        .map(componentPath => path.relative(relativeTo, componentPath) + '/thirdpartylibs.xml')
186
        .map(componentPath => componentPath.replace(/\\/g, '/'))
187
        .filter(path => fs.existsSync(path))
188
        .sort();
189
};
190
 
191
/**
192
 * Get the list of thirdparty library paths.
193
 *
194
 * @returns {array}
195
 */
196
const getThirdPartyPaths = () => {
197
    const DOMParser = require('@xmldom/xmldom').DOMParser;
198
    const fs = require('fs');
199
    const path = require('path');
200
    const xpath = require('xpath');
201
 
202
    const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
203
    const libs = ['node_modules/', 'vendor/'];
204
 
205
    const addLibToList = lib => {
206
        if (!lib.match('\\*') && fs.statSync(lib).isDirectory()) {
207
            // Ensure trailing slash on dirs.
208
            lib = lib.replace(/\/?$/, '/');
209
        }
210
 
211
        // Look for duplicate paths before adding to array.
212
        if (libs.indexOf(lib) === -1) {
213
            libs.push(lib);
214
        }
215
    };
216
 
217
    thirdpartyfiles.forEach(function(file) {
218
        const dirname = path.dirname(file);
219
 
220
        const xmlContent = fs.readFileSync(file, 'utf8');
221
        const doc = new DOMParser().parseFromString(xmlContent);
222
        const nodes = xpath.select("/libraries/library/location/text()", doc);
223
 
224
        nodes.forEach(function(node) {
225
            let lib = path.posix.join(dirname, node.toString());
226
            addLibToList(lib);
227
        });
228
    });
229
 
230
    return libs;
231
 
232
};
233
 
234
/**
235
 * Find the name of the component matching the specified path.
236
 *
237
 * @param {String} path
238
 * @returns {String|null} Name of matching component.
239
 */
240
const getComponentFromPath = path => {
241
    const componentList = fetchComponentData().components;
242
 
243
    if (componentList.hasOwnProperty(path)) {
244
        return componentList[path];
245
    }
246
 
247
    return null;
248
};
249
 
250
/**
251
 * Check whether the supplied path, relative to the Gruntfile.js, is in a known component.
252
 *
253
 * @param {String} checkPath The path to check. This can be with either Windows, or Linux directory separators.
254
 * @returns {String|null}
255
 */
256
const getOwningComponentDirectory = checkPath => {
257
    const path = require('path');
258
 
259
    // Fetch all components into a reverse sorted array.
260
    // This ensures that components which are within the directory of another component match first.
261
    const pathList = Object.keys(fetchComponentData().components).sort().reverse();
262
    for (const componentPath of pathList) {
263
        // If the componentPath is the directory being checked, it will be empty.
264
        // If the componentPath is a parent of the directory being checked, the relative directory will not start with ..
265
        if (!path.relative(componentPath, checkPath).startsWith('..')) {
266
            return componentPath;
267
        }
268
    }
269
 
270
    return null;
271
};
272
 
273
/**
274
 * Get the latest tag in a remote GitHub repository.
275
 *
276
 * @param {string} url The remote repository.
277
 * @returns {Array}
278
 */
279
const getRepositoryTags = async(url) => {
280
    const gtr = require('git-tags-remote');
281
    try {
282
        const tags = await gtr.get(url);
283
        if (tags !== undefined) {
284
            return tags;
285
        }
286
    } catch (error) {
287
        return [];
288
    }
289
    return [];
290
};
291
 
292
/**
293
 * Get the list of thirdparty libraries that could be upgraded.
294
 *
295
 * @returns {Array}
296
 */
297
const getThirdPartyLibsUpgradable = async() => {
298
    const libraries = getThirdPartyLibsData().filter((library) => !!library.repository);
299
    const upgradableLibraries = [];
300
    const versionCompare = (a, b) => {
301
        if (a === b) {
302
            return 0;
303
        }
304
 
305
        const aParts = a.split('.');
306
        const bParts = b.split('.');
307
 
308
        for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
309
            const aPart = parseInt(aParts[i], 10);
310
            const bPart = parseInt(bParts[i], 10);
311
            if (aPart > bPart) {
312
                // 1.1.0 > 1.0.9
313
                return 1;
314
            } else if (aPart < bPart) {
315
                // 1.0.9 < 1.1.0
316
                return -1;
317
            } else {
318
                // Same version.
319
                continue;
320
            }
321
        }
322
 
323
        if (aParts.length > bParts.length) {
324
            // 1.0.1 > 1.0
325
            return 1;
326
        }
327
 
328
        // 1.0 < 1.0.1
329
        return -1;
330
    };
331
 
332
    for (let library of libraries) {
333
        upgradableLibraries.push(
334
            getRepositoryTags(library.repository).then((tagMap) => {
335
                library.version = library.version.replace(/^v/, '');
336
                const currentVersion = library.version.replace(/moodle-/, '');
337
                const currentMajorVersion = library.version.split('.')[0];
338
                const tags = [...tagMap]
339
                    .map((tagData) => tagData[0])
340
                    .filter((tag) => !tag.match(/(alpha|beta|rc)/))
341
                    .map((tag) => tag.replace(/^v/, ''))
342
                    .sort((a, b) => versionCompare(b, a));
343
                if (!tags.length) {
344
                    library.warning = "Unable to find any comparable tags.";
345
                    return library;
346
                }
347
 
348
                library.latestVersion = tags[0];
349
                tags.some((tag) => {
350
                    if (!tag) {
351
                        return false;
352
                    }
353
 
354
                    // See if the version part matches.
355
                    const majorVersion = tag.split('.')[0];
356
                    if (majorVersion === currentMajorVersion) {
357
                        library.latestSameMajorVersion = tag;
358
                        return true;
359
                    }
360
                    return false;
361
                });
362
 
363
 
364
                if (versionCompare(currentVersion, library.latestVersion) > 0) {
365
                    // Moodle somehow has a newer version than the latest version.
366
                    library.warning = `Newer version found: ${currentVersion} > ${library.latestVersion} for ${library.name}`;
367
                    return library;
368
                }
369
 
370
 
371
                if (library.version !== library.latestVersion) {
372
                    // Delete version and add it again at the end of the array. That way, current and new will stay closer.
373
                    delete library.version;
374
                    library.version = currentVersion;
375
                    return library;
376
                }
377
                return null;
378
            })
379
        );
380
    }
381
 
382
    return (await Promise.all(upgradableLibraries)).filter((library) => !!library);
383
};
384
 
385
/**
386
 * Get the list of thirdparty libraries.
387
 *
388
 * @returns {Array}
389
 */
390
const getThirdPartyLibsData = () => {
391
    const DOMParser = require('@xmldom/xmldom').DOMParser;
392
    const fs = require('fs');
393
    const xpath = require('xpath');
394
    const path = require('path');
395
 
396
    const libraryList = [];
397
    const libraryFields = [
398
        'location',
399
        'name',
400
        'version',
401
        'repository',
402
    ];
403
 
404
    const thirdpartyfiles = getThirdPartyLibsList(fs.realpathSync('./'));
405
    thirdpartyfiles.forEach(function(libraryPath) {
406
        const xmlContent = fs.readFileSync(libraryPath, 'utf8');
407
        const doc = new DOMParser().parseFromString(xmlContent);
408
        const libraries = xpath.select("/libraries/library", doc);
409
        for (const library of libraries) {
410
            const libraryData = [];
411
            for (const field of libraryFields) {
412
                libraryData[field] = xpath.select(`${field}/text()`, library)?.toString();
413
            }
414
            libraryData.location = path.join(path.dirname(libraryPath), libraryData.location);
415
            libraryList.push(libraryData);
416
        }
417
    });
418
 
419
    return libraryList.sort((a, b) => a.location.localeCompare(b.location));
420
};
421
 
422
module.exports = {
423
    fetchComponentData,
424
    getAmdSrcGlobList,
425
    getComponentFromPath,
426
    getComponentPaths,
427
    getOwningComponentDirectory,
428
    getYuiSrcGlobList,
429
    getThirdPartyLibsList,
430
    getThirdPartyPaths,
431
    getThirdPartyLibsUpgradable,
432
};