[suggest] Badge suggestions show incorrect license (#1159)

Fix #821
This commit is contained in:
Marcin Mielnicki
2017-10-12 21:54:34 +02:00
committed by Paul Melnikow
parent 71531b81ef
commit a7ed4b244f
2 changed files with 110 additions and 160 deletions

View File

@@ -2,7 +2,9 @@
const nodeUrl = require('url');
const request = require('request');
const serverSecrets = require('./server-secrets');
const githubAuth = require('./github-auth');
const githubApiUrl = process.env.GITHUB_URL || 'https://api.github.com';
// data: {url}, JSON-serializable object.
// end: function(json), with json of the form:
@@ -57,7 +59,7 @@ function twitterPage (url) {
const path = url.path;
return Promise.resolve({
name: 'Twitter',
link: 'https://twitter.com/intent/tweet?text=Wow:&url=' + encodeURIComponent(url),
link: 'https://twitter.com/intent/tweet?text=Wow:&url=' + encodeURIComponent(url.href),
badge: 'https://img.shields.io/twitter/url/' + schema + '/' + host + path + '.svg?style=social',
});
}
@@ -89,169 +91,35 @@ function githubStars (user, repo) {
});
}
// user: eg, qubyte
// repo: eg, rubidium
// returns a promise of {link, badge, name}
function githubLicense (user, repo) {
return new Promise(function(resolve, reject) {
// Step 1: Get the repo's default branch.
let apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '';
// Using our OAuth App secret grants us 5000 req/hour
// instead of the standard 60 req/hour.
if (serverSecrets) {
apiUrl += '?client_id=' + serverSecrets.gh_client_id
+ '&client_secret=' + serverSecrets.gh_client_secret;
}
const badgeData = {text: ['license',''], colorscheme: 'blue'};
// A special User-Agent is required:
// http://developer.github.com/v3/#user-agent-required
request(apiUrl, { headers: { 'User-Agent': 'Shields.io' } }, function(err, res, buffer) {
if (err != null) { resolve(null); return; }
return new Promise((resolve) => {
const apiUrl = `${githubApiUrl}/repos/${user}/${repo}/license`;
githubAuth.request(request, apiUrl, {}, function(err, res, buffer) {
if (err !== null) {
resolve(null);
return;
}
const defaultBadge = {
name: 'GitHub license',
link: `https://github.com/${user}/${repo}`,
badge: `https://img.shields.io/github/license/${user}/${repo}.svg`
};
if (res.statusCode !== 200) {
resolve(defaultBadge);
}
try {
if ((+res.headers['x-ratelimit-remaining']) === 0) { resolve(null); return; }
const data = JSON.parse(buffer);
const defaultBranch = data.default_branch;
// Step 2: Get the SHA-1 hash of the branch tip.
let apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/branches/' + defaultBranch;
if (serverSecrets) {
apiUrl += '?client_id=' + serverSecrets.gh_client_id
+ '&client_secret=' + serverSecrets.gh_client_secret;
if (data.html_url) {
defaultBadge.link = data.html_url;
resolve(defaultBadge);
} else {
resolve(defaultBadge);
}
request(apiUrl, { headers: { 'User-Agent': 'Shields.io' } }, function(err, res, buffer) {
if (err != null) { resolve(null); return; }
try {
if ((+res.headers['x-ratelimit-remaining']) === 0) { resolve(null); return; }
const data = JSON.parse(buffer);
const branchTip = data.commit.sha;
// Step 3: Get the tree at the commit.
let apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/git/trees/' + branchTip;
if (serverSecrets) {
apiUrl += '?client_id=' + serverSecrets.gh_client_id
+ '&client_secret=' + serverSecrets.gh_client_secret;
}
request(apiUrl, { headers: { 'User-Agent': 'Shields.io' } }, function(err, res, buffer) {
if (err != null) { resolve(null); return; }
try {
if ((+res.headers['x-ratelimit-remaining']) === 0) { resolve(null); return; }
const data = JSON.parse(buffer);
const treeArray = data.tree;
let licenseBlob;
let licenseFilename;
// Crawl each file in the root directory
for (let i = 0; i < treeArray.length; i++) {
if (treeArray[i].type !== 'blob') {
continue;
}
if (treeArray[i].path.match(/(LICENSE|COPYING|COPYRIGHT).*/i)) {
licenseBlob = treeArray[i].sha;
licenseFilename = treeArray[i].path;
break;
}
}
// Could not find license file
if (!licenseBlob) { resolve(null); return; }
// Step 4: Get the license blob.
let apiUrl = 'https://api.github.com/repos/' + user + '/' + repo + '/git/blobs/' + licenseBlob;
const link = 'https://raw.githubusercontent.com/' +
[user, repo, defaultBranch, licenseFilename].join('/');
if (serverSecrets) {
apiUrl += '?client_id=' + serverSecrets.gh_client_id
+ '&client_secret=' + serverSecrets.gh_client_secret;
}
// Get the raw blob instead of JSON
// https://developer.github.com/v3/media/
request(apiUrl, { headers: { 'User-Agent': 'Shields.io', 'Accept': 'appplication/vnd.github.raw' } },
function(err, res, buffer) {
if (err != null) { resolve(null); return; }
try {
if ((+res.headers['x-ratelimit-remaining']) === 0) { resolve(null); return; }
const license = guessLicense(buffer);
if (license) {
badgeData.text[1] = license;
resolve({
link: link,
badge: shieldsBadge(badgeData),
name: 'GitHub license'
});
return;
} else {
// Not a recognized license
resolve(null);
return;
}
} catch(e) { reject(e); }
});
} catch(e) { reject(e); }
});
} catch(e) { reject(e); }
});
} catch(e) { reject(e); }
});
} catch(e) {
resolve(defaultBadge);
}
})
});
}
// Key phrases for common licenses
const licensePhrases = {
'Apache 1.1': 'apache (software )?license,? (version)? 1\\.1',
'Apache 2': 'apache (software )?license,? (version)? 2',
'Original BSD': 'all advertising materials mentioning features or use of this software must display the following acknowledgement',
'New BSD': 'may be used to endorse or promote products derived from this software without specific prior written permission',
'BSD': 'redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met',
'AGPLv1': 'affero general public license,? version 1',
'AGPLv3': 'affero general public license,? version 3',
'AGPL': 'affero general public license',
'GPLv2': 'gnu general public license,? version 2',
'GPLv3': 'gnu general public license,? version 3',
'GPL': 'gnu general public license',
'LGPLv2.0': 'gnu library general public license,? version 2',
'LGPLv2.1': 'gnu lesser general public license,? version 2\\.1',
'LGPLv3': 'gnu lesser general public license,? version 3',
'LGPL': 'gnu (library|lesser) general public license',
'MIT': '\\(?(mit|expat|x11)\\)? license|permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files',
'MPL 1.1': 'mozilla public license,? (\\(MPL\\) )?(version |v|v\\.)?1\\.1',
'MPL 2': 'mozilla public license,? (\\(MPL\\) )?(version |v|v\\.)?2',
'MPL': 'mozilla public license',
'CDDL': 'common development and distribution license',
'Eclipse': 'eclipse public license',
'Artistic': 'artistic license',
'zlib': 'the origin of this software must not be misrepresented',
'ISC': 'permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted',
'CC0': 'cc0',
'Unlicense': 'this is free and unencumbered software released into the public domain',
};
const licenseCodes = Object.keys(licensePhrases);
const spaceMetaRegex = new RegExp(' ', 'g');
// Spaces can be any whitespace
for (let i = 0; i < licenseCodes.length; i++) {
licensePhrases[licenseCodes[i]] = licensePhrases[licenseCodes[i]].replace(spaceMetaRegex, '\\s+');
}
// Try to guess the license based on the text and return an abbreviated name (or null if not recognized).
function guessLicense (text) {
let licenseRegex;
for (let i = 0; i < licenseCodes.length; i++) {
licenseRegex = licensePhrases[licenseCodes[i]];
if (text.match(new RegExp(licenseRegex, 'i'))) {
return licenseCodes[i];
}
}
// Not a recognized license
return null;
}
function shieldsBadge (badgeData) {
return ('https://img.shields.io/badge/'
+ escapeField(badgeData.text[0])
+ '-' + escapeField(badgeData.text[1])
+ '-' + badgeData.colorscheme + '.svg');
}
function escapeField (s) {
return encodeURIComponent(s.replace(/-/g, '--').replace(/_/g, '__'));
}
module.exports = suggest;

82
service-tests/suggest.js Normal file
View File

@@ -0,0 +1,82 @@
'use strict';
const ServiceTester = require('./runner/service-tester');
const t = new ServiceTester({ id: 'suggest', title: 'suggest', pathPrefix: '/$suggest' });
module.exports = t;
t.create('issues, forks, stars and twitter')
.get('/v1?url=' + encodeURIComponent('https://github.com/atom/atom'))
// suggest resource requires this header value
.addHeader('origin', 'https://shields.io')
.expectJSON('badges.?', {
name: 'GitHub issues',
link: 'https://github.com/atom/atom/issues',
badge: 'https://img.shields.io/github/issues/atom/atom.svg'
})
.expectJSON('badges.?', {
name: 'GitHub forks',
link: 'https://github.com/atom/atom/network',
badge: 'https://img.shields.io/github/forks/atom/atom.svg'
})
.expectJSON('badges.?', {
name: 'GitHub stars',
link: 'https://github.com/atom/atom/stargazers',
badge: 'https://img.shields.io/github/stars/atom/atom.svg'
})
.expectJSON('badges.?', {
name: 'Twitter',
link: 'https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fatom%2Fatom',
badge: 'https://img.shields.io/twitter/url/https/github.com/atom/atom.svg?style=social'
});
t.create('license')
.get('/v1?url=' + encodeURIComponent('https://github.com/atom/atom'))
.addHeader('origin', 'https://shields.io')
.expectJSON('badges.?', {
name: 'GitHub license',
link: 'https://github.com/atom/atom/blob/master/LICENSE.md',
badge: 'https://img.shields.io/github/license/atom/atom.svg'
});
t.create('license for non-existing project')
.get('/v1?url=' + encodeURIComponent('https://github.com/atom/atom'))
.addHeader('origin', 'https://shields.io')
.intercept(nock => nock('https://api.github.com')
.get(/\/repos\/atom\/atom\/license/)
.reply(404))
.expectJSON('badges.?', {
name: 'GitHub license',
link: 'https://github.com/atom/atom',
badge: 'https://img.shields.io/github/license/atom/atom.svg'
});
t.create('license when json response is invalid')
.get('/v1?url=' + encodeURIComponent('https://github.com/atom/atom'))
.addHeader('origin', 'https://shields.io')
.intercept(nock => nock('https://api.github.com')
.get(/\/repos\/atom\/atom\/license/)
.reply(200, 'invalid json'), {
'Content-Type': 'application/json;charset=UTF-8'
})
.expectJSON('badges.?', {
name: 'GitHub license',
link: 'https://github.com/atom/atom',
badge: 'https://img.shields.io/github/license/atom/atom.svg'
});
t.create('license when html_url not found in GitHub api response')
.get('/v1?url=' + encodeURIComponent('https://github.com/atom/atom'))
.addHeader('origin', 'https://shields.io')
.intercept(nock => nock('https://api.github.com')
.get(/\/repos\/atom\/atom\/license/)
.reply(200, {
license: 'MIT'
}))
.expectJSON('badges.?', {
name: 'GitHub license',
link: 'https://github.com/atom/atom',
badge: 'https://img.shields.io/github/license/atom/atom.svg'
});