/* Copyright (c) 2013, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://yuilibrary.com/license/ */ var UNKNOWN = 'UNKNOWN'; var UNLICENSED = 'UNLICENSED'; var fs = require('fs'); var path = require('path'); var read = require('read-installed'); var chalk = require('chalk'); var treeify = require('treeify'); var license = require('./license'); var licenseFiles = require('./license-files'); var debug = require('debug'); var mkdirp = require('mkdirp'); var spdxSatisfies = require('spdx-satisfies'); var spdxCorrect =require('spdx-correct'); // Set up debug logging // https://www.npmjs.com/package/debug#stderr-vs-stdout var debugError = debug('license-checker:error'); var debugLog = debug('license-checker:log'); debugLog.log = console.log.bind(console); var flatten = function(options) { var moduleInfo = { licenses: UNKNOWN }, json = options.deps, data = options.data, key = json.name + '@' + json.version, colorize = options.color, unknown = options.unknown, readmeFile, licenseData, dirFiles, files = [], noticeFiles = [], licenseFile; if (json.private) { moduleInfo.private = true; } // If we have processed this key already, just return the data object. // This was added so that we don't recurse forever if there was a circular // dependency in the dependency tree. /*istanbul ignore next*/ if (data[key]) { return data; } if ((options.production && json.extraneous) || (options.development && !json.extraneous && !json.root)) { return data; } data[key] = moduleInfo; // Include property in output unless custom format has set property to false. var include = function(property) { return (options.customFormat === undefined || options.customFormat[property] !== false); }; if (include("repository") && json.repository) { /*istanbul ignore else*/ if (typeof json.repository === 'object' && typeof json.repository.url === 'string') { moduleInfo.repository = json.repository.url.replace('git+ssh://git@', 'git://'); moduleInfo.repository = moduleInfo.repository.replace('git+https://github.com', 'https://github.com'); moduleInfo.repository = moduleInfo.repository.replace('git://github.com', 'https://github.com'); moduleInfo.repository = moduleInfo.repository.replace('git@github.com:', 'https://github.com/'); moduleInfo.repository = moduleInfo.repository.replace(/\.git$/, ''); } } if (include("url") && json.url) { /*istanbul ignore next*/ if (typeof json.url === 'object') { moduleInfo.url = json.url.web; } } if (json.author && typeof json.author === 'object') { /*istanbul ignore else - This should always be there*/ if (include("publisher") && json.author.name) { moduleInfo.publisher = json.author.name; } if (include("email") && json.author.email) { moduleInfo.email = json.author.email; } if (include("url") && json.author.url) { moduleInfo.url = json.author.url; } } /*istanbul ignore next*/ if (unknown) { moduleInfo.dependencyPath = json.path; } /*istanbul ignore next*/ if (options.customFormat) { Object.keys(options.customFormat).forEach(function forEachCallback(item) { if (include(item) && json[item]) { //For now, we only support strings, not JSON objects if (typeof json[item] === 'string') { moduleInfo[item] = json[item]; } } else if (include(item)) { moduleInfo[item] = options.customFormat[item]; } }); } if (include("path") && json.path && typeof json.path === 'string') { moduleInfo.path = json.path; } licenseData = json.license || json.licenses || undefined; if (json.path && (!json.readme || json.readme.toLowerCase().indexOf('no readme data found') > -1)) { readmeFile = path.join(json.path, 'README.md'); /*istanbul ignore if*/ if (fs.existsSync(readmeFile)) { json.readme = fs.readFileSync(readmeFile, 'utf8').toString(); } } if (licenseData) { /*istanbul ignore else*/ if (Array.isArray(licenseData) && licenseData.length > 0) { moduleInfo.licenses = licenseData.map(function(license){ /*istanbul ignore else*/ if (typeof license === 'object') { /*istanbul ignore next*/ return license.type || license.name; } else if (typeof license === 'string') { return license; } }); } else if (typeof licenseData === 'object' && (licenseData.type || licenseData.name)) { moduleInfo.licenses = license(licenseData.type || licenseData.name); } else if (typeof licenseData === 'string') { moduleInfo.licenses = license(licenseData); } } else if (license(json.readme)) { moduleInfo.licenses = license(json.readme); } if (Array.isArray(moduleInfo.licenses)) { /*istanbul ignore else*/ if (moduleInfo.licenses.length === 1) { moduleInfo.licenses = moduleInfo.licenses[0]; } } /*istanbul ignore else*/ if (json.path && fs.existsSync(json.path)) { dirFiles = fs.readdirSync(json.path); files = licenseFiles(dirFiles); noticeFiles = dirFiles.filter(function(filename) { filename = filename.toUpperCase(); var name = path.basename(filename).replace(path.extname(filename), ''); return name === 'NOTICE'; }); } files.forEach(function(filename, index) { licenseFile = path.join(json.path, filename); // Checking that the file is in fact a normal file and not a directory for example. /*istanbul ignore else*/ if (fs.lstatSync(licenseFile).isFile()) { var content; if (!moduleInfo.licenses || moduleInfo.licenses.indexOf(UNKNOWN) > -1 || moduleInfo.licenses.indexOf('Custom:') === 0) { //Only re-check the license if we didn't get it from elsewhere content = fs.readFileSync(licenseFile, { encoding: 'utf8' }); moduleInfo.licenses = license(content); } if (index === 0) { // Treat the file with the highest precedence as licenseFile /*istanbul ignore else*/ if (include("licenseFile")) { moduleInfo.licenseFile = options.basePath ? path.relative(options.basePath, licenseFile) : licenseFile; } if (include("licenseText") && options.customFormat) { if (!content) { content = fs.readFileSync(licenseFile, { encoding: 'utf8' }); } /*istanbul ignore else*/ if (options._args && !options._args.csv) { moduleInfo.licenseText = content.trim(); } else { moduleInfo.licenseText = content.replace(/"/g, '\'').replace(/\r?\n|\r/g, " ").trim(); } } if(include('copyright') && options.customFormat) { if (!content) { content = fs.readFileSync(licenseFile, { encoding: 'utf8' }); } var linesWithCopyright = content .replace(/\r\n/g, '\n') .split('\n\n') .filter(function selectCopyRightStatements(value) { return value.startsWith('opyright', 1) && // include copyright statements !value.startsWith('opyright notice', 1) && // exclude lines from from license text !value.startsWith('opyright and related rights', 1); }) .filter(function removeDuplicates(value, index, list) { return index === 0 || value !== list[0]; }); if(linesWithCopyright.length > 0) { moduleInfo.copyright = linesWithCopyright[0] .replace(/\n/g, '. ') .trim(); } // Mark files with multiple copyright statements. This might be // an indicator to take a closer look at the LICENSE file. if(linesWithCopyright.length > 1) { moduleInfo.copyright += '*'; } } } } }); noticeFiles.forEach(function(filename) { var file = path.join(json.path, filename); /*istanbul ignore else*/ if (fs.lstatSync(file).isFile()) { moduleInfo.noticeFile = options.basePath ? path.relative(options.basePath, file) : file; } }); /*istanbul ignore else*/ if (json.dependencies) { Object.keys(json.dependencies).forEach(function(name) { var childDependency = json.dependencies[name], dependencyId = childDependency.name + '@' + childDependency.version; if (data[dependencyId]) { // already exists return; } data = flatten({ deps: childDependency, data: data, color: colorize, unknown: unknown, customFormat: options.customFormat, production: options.production, development: options.development, basePath: options.basePath, _args: options._args }); }); } if (!json.name || !json.version) { delete data[key]; } return data; }; exports.init = function(options, callback) { debugLog('scanning %s', options.start); if (options.customPath) { options.customFormat = this.parseJson(options.customPath); } var opts = { dev: true, log: debugLog, depth: options.direct }; if (options.production || options.development) { opts.dev = false; } var toCheckforFailOn = []; var toCheckforOnlyAllow = []; var checker, pusher; if (options.onlyAllow) { checker = options.onlyAllow; pusher = toCheckforOnlyAllow; } if (options.failOn) { checker = options.failOn; pusher = toCheckforFailOn; } if (checker && pusher) { checker.split(';').forEach(function(license) { var trimmed = license.trim(); /*istanbul ignore else*/ if (trimmed.length > 0) { pusher.push(trimmed); } }); } read(options.start, opts, function(err, json) { var data = flatten({ deps: json, data: {}, color: options.color, unknown: options.unknown, customFormat: options.customFormat, production: options.production, development: options.development, basePath: options.relativeLicensePath ? json.path : null, _args: options }), colorize = options.color, sorted = {}, filtered = {}, exclude = options.exclude && options.exclude.match(/([^\\\][^,]|\\,)+/g).map(function(license) { return license.replace(/\\,/g, ',').replace(/^\s+|\s+$/g, ''); }), inputError = null; var colorizeString = function(string) { /*istanbul ignore next*/ return colorize ? chalk.bold.red(string) : string; }; Object.keys(data).sort().forEach(function(item) { if (data[item].private) { data[item].licenses = colorizeString(UNLICENSED); } /*istanbul ignore next*/ if (!data[item].licenses) { data[item].licenses = colorizeString(UNKNOWN); } if (options.unknown) { /*istanbul ignore else*/ if (data[item].licenses && data[item].licenses !== UNKNOWN) { if (data[item].licenses.indexOf('*') > -1) { /*istanbul ignore if*/ data[item].licenses = colorizeString(UNKNOWN); } } } /*istanbul ignore else*/ if (data[item]) { if (options.onlyunknown) { if (data[item].licenses.indexOf('*') > -1 || data[item].licenses.indexOf(UNKNOWN) > -1) { sorted[item] = data[item]; } } else { sorted[item] = data[item]; } } }); if (!Object.keys(sorted).length) { err = new Error('No packages found in this path..'); } if (exclude) { var transformBSD = function(spdx) { return spdx === 'BSD' ? '(0BSD OR BSD-2-Clause OR BSD-3-Clause OR BSD-4-Clause)' : spdx; }; var invert = function(fn) { return function(spdx) { return !fn(spdx);};}; var spdxIsValid = function(spdx) { return spdxCorrect(spdx) === spdx; }; var validSPDXLicenses = exclude.map(transformBSD).filter(spdxIsValid); var invalidSPDXLicenses = exclude.map(transformBSD).filter(invert(spdxIsValid)); var spdxExcluder = '( ' + validSPDXLicenses.join(' OR ') + ' )'; Object.keys(sorted).forEach(function(item) { var licenses = sorted[item].licenses; /*istanbul ignore if - just for protection*/ if(!licenses) { filtered[item] = sorted[item]; } else { licenses = [].concat(licenses); var licenseMatch = false; licenses.forEach(function(license) { /*istanbul ignore if - just for protection*/ if (license.indexOf(UNKNOWN) >= 0) { // necessary due to colorization filtered[item] = sorted[item]; } else { if(license.indexOf('*') >= 0) { license = license.substring(0, license.length - 1); } if(license === 'BSD') { license = '(0BSD OR BSD-2-Clause OR BSD-3-Clause OR BSD-4-Clause)'; } if (invalidSPDXLicenses.indexOf(license) >= 0) { licenseMatch = true; } else if (spdxCorrect(license) && spdxSatisfies(spdxCorrect(license), spdxExcluder)) { licenseMatch = true; } } }); if(!licenseMatch) { filtered[item] = sorted[item]; } } }); } else { filtered = sorted; } var restricted = filtered; // package whitelist if (options.packages) { var packages = options.packages.split(';'); restricted = {}; Object.keys(filtered).map(function(key) { if (packages.includes(key)) { restricted[key] = filtered[key]; } }); } // package blacklist if (options.excludePackages) { var excludedPackages = options.excludePackages.split(';'); restricted = {}; Object.keys(filtered).map(function(key) { if (!excludedPackages.includes(key)) { restricted[key] = filtered[key]; } }); } if (options.excludePrivatePackages) { Object.keys(filtered).forEach(function(key) { /*istanbul ignore next - I don't have access to private packages to test */ if (restricted[key] && restricted[key].private) { delete restricted[key]; } }); } Object.keys(restricted).forEach(function(item) { if (toCheckforFailOn.length > 0) { if (toCheckforFailOn.indexOf(restricted[item].licenses) > -1) { console.error('Found license defined by the --failOn flag: "' + restricted[item].licenses + '". Exiting.'); process.exit(1); } } if (toCheckforOnlyAllow.length > 0) { var good = false; toCheckforOnlyAllow.forEach(function(k) { if (restricted[item].licenses.indexOf(k) === -1 && !good) { good = false; } else { good = true; } }); if (!good) { console.error('Package "' + item + '" is licensed under "' + restricted[item].licenses + '" which is not permitted by the --onlyAllow flag. Exiting.'); process.exit(1); } } }); /*istanbul ignore next*/ if (err) { debugError(err); inputError = err; } //Return the callback and variables nicely callback(inputError, restricted); }); }; exports.print = function(sorted) { console.log(exports.asTree(sorted)); }; exports.asTree = function(sorted) { return treeify.asTree(sorted, true); }; exports.asSummary = function(sorted) { var licenseCountObj = {}; var licenceCountArray = []; var sortedLicenseCountObj = {}; Object.keys(sorted).forEach(function(key) { /*istanbul ignore else*/ if (sorted[key].licenses) { licenseCountObj[sorted[key].licenses] = licenseCountObj[sorted[key].licenses] || 0; licenseCountObj[sorted[key].licenses]++; } }); Object.keys(licenseCountObj).forEach(function(license) { licenceCountArray.push({ license: license, count: licenseCountObj[license] }); }); /*istanbul ignore next*/ licenceCountArray.sort(function(a, b) { return b['count'] - a['count']; }); licenceCountArray.forEach(function(licenseObj) { sortedLicenseCountObj[licenseObj.license] = licenseObj.count; }); return treeify.asTree(sortedLicenseCountObj, true); }; exports.asCSV = function(sorted, customFormat, csvComponentPrefix) { var text = [], textArr = [], lineArr = []; var prefixName = '"component"'; var prefix = csvComponentPrefix; if (customFormat && Object.keys(customFormat).length > 0) { textArr = []; if (csvComponentPrefix) { textArr.push(prefixName); } textArr.push('"module name"'); Object.keys(customFormat).forEach(function forEachCallback(item) { textArr.push('"' + item + '"'); }); text.push(textArr.join(',')); } else { textArr = []; /*istanbul ignore next*/ if (csvComponentPrefix) { textArr.push(prefixName); } ['"module name"','"license"','"repository"'].forEach(function(item) { textArr.push(item); }); text.push(textArr.join(',')); } Object.keys(sorted).forEach(function(key) { var module = sorted[key], line = ''; lineArr = []; //Grab the custom keys from the custom format if (customFormat && Object.keys(customFormat).length > 0) { if (csvComponentPrefix) { lineArr.push('"'+prefix+'"'); } lineArr.push('"' + key + '"'); Object.keys(customFormat).forEach(function forEachCallback(item) { lineArr.push('"' + module[item] + '"'); }); line = lineArr.join(','); } else { /*istanbul ignore next*/ if (csvComponentPrefix) { lineArr.push('"'+prefix+'"'); } lineArr.push([ '"' + key + '"', '"' + (module.licenses || '') + '"', '"' + (module.repository || '') + '"' ]); line = lineArr.join(','); } text.push(line); }); return text.join('\n'); }; /** * Exports data as markdown (*.md) file which has it's own syntax. * @method * @param {JSON} sorted The sorted JSON data from all packages. * @param {JSON} customFormat The custom format with information about the needed keys. * @return {String} The returning plain text. */ exports.asMarkDown = function(sorted, customFormat) { var text = []; if (customFormat && Object.keys(customFormat).length > 0) { Object.keys(sorted).forEach(function sortedCallback(sortedItem) { text.push(' - **[' + sortedItem + '](' + sorted[sortedItem].repository + ')**'); Object.keys(customFormat).forEach(function customCallback(customItem) { text.push(' - ' + customItem + ': ' + sorted[sortedItem][customItem]); }); }); text = text.join('\n'); } else { Object.keys(sorted).forEach(function(key) { var module = sorted[key]; text.push('[' + key + '](' + module.repository + ') - ' + module.licenses); }); text = text.join('\n'); } return text; }; exports.parseJson = function(jsonPath) { if (typeof jsonPath !== 'string') { return new Error('did not specify a path'); } var jsonFileContents = '', result = { }; try { jsonFileContents = fs.readFileSync(jsonPath, { encoding: 'utf8' }); result = JSON.parse(jsonFileContents); } catch (err) { result = err; } return result; }; exports.asFiles = function(json, outDir) { mkdirp.sync(outDir); Object.keys(json).forEach(function(moduleName) { var licenseFile = json[moduleName].licenseFile, fileContents, outFileName, outPath, baseDir; if (licenseFile && fs.existsSync(licenseFile)) { fileContents = fs.readFileSync(licenseFile); outFileName = moduleName + "-LICENSE.txt"; outPath = path.join(outDir, outFileName); baseDir = path.dirname(outPath); mkdirp.sync(baseDir); fs.writeFileSync(outPath, fileContents, "utf8"); } else { console.warn("no license file found for: " + moduleName); } }); };