Skip to content

Commit

Permalink
✨ Replace http and https with got
Browse files Browse the repository at this point in the history
closes #99

- Used [`got`](https://github.com/sindresorhus/got) for image-size requests, which is more lightweight and has the huge advantage of following redirects
- Added and updated test
- indents
- Added dependencies:
    - [validator](https://github.com/chriso/validator.js)
    - [lodash](https://github.com/lodash/lodash)
    - [got](https://github.com/sindresorhus/got)
  • Loading branch information
aileen committed Sep 13, 2017
1 parent 94ac029 commit 77b5880
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 63 deletions.
Binary file added lib/.DS_Store
Binary file not shown.
123 changes: 69 additions & 54 deletions lib/amperize.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ var merge = require('lodash.merge')
, uuid = require('uuid')
, async = require('async')
, url = require('url')
, http = require('http')
, https = require('https')
, got = require('got')
, _ = require('lodash')
, sizeOf = require('image-size')
, validator = require('validator')
, helpers = require('./helpers');

var DEFAULTS = {
Expand Down Expand Up @@ -129,21 +130,21 @@ Amperize.prototype.traverse = function traverse(data, html, done) {
}

function useSecureSchema(element) {
if (element.attribs && element.attribs.src) {
// Every src attribute must be with 'https' protocol otherwise it will not get validated by AMP.
// If we're unable to replace it, we will deal with the valitation error, but at least
// we tried.
if (element.attribs.src.indexOf('https://') === -1) {
if (element.attribs.src.indexOf('http://') === 0) {
// Replace 'http' with 'https', so the validation passes
element.attribs.src = element.attribs.src.replace(/^http:\/\//i, 'https://');
} else if (element.attribs.src.indexOf('//') === 0) {
// Giphy embedded iFrames are without protocol and start with '//', so at least
// we can fix those cases.
element.attribs.src = 'https:' + element.attribs.src;
}
}
if (element.attribs && element.attribs.src) {
// Every src attribute must be with 'https' protocol otherwise it will not get validated by AMP.
// If we're unable to replace it, we will deal with the valitation error, but at least
// we tried.
if (element.attribs.src.indexOf('https://') === -1) {
if (element.attribs.src.indexOf('http://') === 0) {
// Replace 'http' with 'https', so the validation passes
element.attribs.src = element.attribs.src.replace(/^http:\/\//i, 'https://');
} else if (element.attribs.src.indexOf('//') === 0) {
// Giphy embedded iFrames are without protocol and start with '//', so at least
// we can fix those cases.
element.attribs.src = 'https:' + element.attribs.src;
}
}
}

return;
}
Expand All @@ -170,46 +171,60 @@ Amperize.prototype.traverse = function traverse(data, html, done) {
* @return {Object} element incl. width and height
*/
function getImageSize(element) {
var options = url.parse(element.attribs.src),
timeout = 5000,
request = element.attribs.src.indexOf('https') === 0 ? https : http;
var imagePath = url.parse(element.attribs.src),
requestOptions,
timeout = 5000;

called = false;

if (!validator.isURL(imagePath.href)) {
if (called) return;
called = true;

// revert this element, do not show
element.name = 'img';

return enter();
}

// We need the user-agent, otherwise some https request may fail (e. g. cloudfare)
options.headers = { 'User-Agent': 'Mozilla/5.0' };

return request.get(options, function (response) {
var chunks = [];
response.on('data', function (chunk) {
chunks.push(chunk);
}).on('end', function () {
try {
var dimensions = sizeOf(Buffer.concat(chunks));
element.attribs.width = dimensions.width;
element.attribs.height = dimensions.height;

return getLayoutAttribute(element);
} catch (err) {
if (called) return;
called = true;

// revert this element, do not show
element.name = 'img';
return enter();
}
});
}).on('socket', function (socket) {
socket.setTimeout(timeout);
socket.on('timeout', function () {
if (called) return;
called = true;

// revert this element, do not show
element.name = 'img';
return enter();
});
}).on('error', function () {
requestOptions = {
headers: {
'User-Agent': 'Mozilla/5.0'
},
timeout: timeout,
encoding: null
};

return got (
imagePath.href,
requestOptions
).then(function (response) {
try {
// Using the Buffer rather than an URL requires to use sizeOf synchronously.
// See https://github.com/image-size/image-size#asynchronous
var dimensions = sizeOf(response.body);

// CASE: `.ico` files might have multiple images and therefore multiple sizes.
// We return the largest size found (image-size default is the first size found)
if (dimensions.images) {
dimensions.width = _.maxBy(dimensions.images, function (w) {return w.width;}).width;
dimensions.height = _.maxBy(dimensions.images, function (h) {return h.height;}).height;
}

element.attribs.width = dimensions.width;
element.attribs.height = dimensions.height;

return getLayoutAttribute(element);
} catch (err) {
if (called) return;
called = true;

// revert this element, do not show
element.name = 'img';
return enter();
}
}).catch(function (err) {
if (called) return;
called = true;

Expand All @@ -229,7 +244,7 @@ Amperize.prototype.traverse = function traverse(data, html, done) {

if (!element.attribs.width || !element.attribs.height || !element.attribs.layout) {
if (element.attribs.src.indexOf('http') === 0) {
return getImageSize(element);
return getImageSize(element);
}
}
// Fallback to default values for a local image
Expand All @@ -254,7 +269,7 @@ Amperize.prototype.traverse = function traverse(data, html, done) {
}

if (element.name === 'audio') {
element.name = 'amp-audio';
element.name = 'amp-audio';
}

useSecureSchema(element);
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@
"dependencies": {
"async": "2.1.4",
"emits": "3.0.0",
"got": "7.1.0",
"htmlparser2": "3.9.2",
"image-size": "0.5.1",
"lodash": "4.17.4",
"lodash.merge": "4.6.0",
"nock": "^9.0.2",
"rewire": "^2.5.2",
"uuid": "^3.0.0"
"uuid": "^3.0.0",
"validator": "8.2.0"
},
"devDependencies": {
"chai": "3.5.0",
Expand Down
90 changes: 82 additions & 8 deletions test/amperize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ describe('Amperize', function () {

afterEach(function () {
sinon.restore();
Amperize.__set__('called', false);
});

it('throws an error if no callback provided', function () {
Expand All @@ -95,7 +96,7 @@ describe('Amperize', function () {
sizeOfMock = nock('http://static.wixstatic.com')
.get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256')
.reply(200, {
data: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

sizeOfStub.returns({width: 50, height: 50, type: 'jpg'});
Expand All @@ -118,7 +119,7 @@ describe('Amperize', function () {
sizeOfMock = nock('http://static.wixstatic.com')
.get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256')
.reply(200, {
data: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

sizeOfStub.returns({width: 350, height: 200, type: 'jpg'});
Expand All @@ -137,11 +138,44 @@ describe('Amperize', function () {
});
});

it('returns largest image value for .ico files', function (done) {
sizeOfMock = nock('https://somewebsite.com')
.get('/favicon.ico')
.reply(200, {
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

sizeOfStub.returns({
width: 32,
height: 32,
type: 'ico',
images: [
{width: 48, height: 48},
{width: 32, height: 32},
{width: 16, height: 16}
]
});
Amperize.__set__('sizeOf', sizeOfStub);

amperize.parse('<img src="https://somewebsite.com/favicon.ico">', function (error, result) {
expect(result).to.exist;
expect(Amperize.__get__('called')).to.be.equal(false);
expect(result).to.contain('<amp-img');
expect(result).to.contain('src="https://somewebsite.com/favicon.ico"');
expect(result).to.contain('layout="fixed"');
expect(result).to.contain('width="48"');
expect(result).to.contain('height="48"');
expect(result).to.contain('</amp-img>');
done();
});
});


it('transforms .gif <img> with only height property into <amp-anim></amp-anim> with full dimensions by overriding them', function (done) {
sizeOfMock = nock('https://media.giphy.com')
.get('/media/l46CtzgjhTm29Cbjq/giphy.gif')
.reply(200, {
data: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

sizeOfStub.returns({width: 800, height: 600, type: 'gif'});
Expand Down Expand Up @@ -230,6 +264,15 @@ describe('Amperize', function () {
});
});

it('can handle invalid URLs', function (done) {
amperize.parse('<img src="http:not-a-website">', function (error, result) {
expect(result).to.exist;
expect(Amperize.__get__('called')).to.be.equal(true);
expect(result).to.be.equal('<img src="http:not-a-website">');
done();
});
});

it('can handle <iframe> tag without src and does not transform it', function (done) {
amperize.parse('<iframe>', function (error, result) {
expect(result).to.exist;
Expand Down Expand Up @@ -274,11 +317,42 @@ describe('Amperize', function () {
});
});

it('can handle redirects', function (done) {
var secondSizeOfMock;

sizeOfMock = nock('http://noimagehere.com')
.get('/files/f/feedough/x/11/1540353_20925115.jpg')
.reply(301, {
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
},
{
location: 'http://someredirectedurl.com/files/f/feedough/x/11/1540353_20925115.jpg'
});

secondSizeOfMock = nock('http://someredirectedurl.com')
.get('/files/f/feedough/x/11/1540353_20925115.jpg')
.reply(200, {
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

sizeOfStub.returns({width: 100, height: 100, type: 'jpg'});
Amperize.__set__('sizeOf', sizeOfStub);


amperize.parse('<img src="http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg">', function (error, result) {
expect(sizeOfMock.isDone()).to.be.equal(true);
expect(secondSizeOfMock.isDone()).to.be.equal(true);
expect(Amperize.__get__('called')).to.be.equal(false);
expect(error).to.be.null;
expect(result).to.contain('<amp-img src="http://noimagehere.com/files/f/feedough/x/11/1540353_20925115.jpg" width="100" height="100" layout="fixed"></amp-img>');
done();
});
});

it('can handle request errors', function (done) {
var callCounts;
sizeOfMock = nock('http://example.com')
.get('/images/IMG_xyz.jpg')
.replyWithError('something awful happened');
.reply(404, {message: 'something awful happened', code: 'AWFUL_ERROR'});

amperize.parse('<img src="http://example.com/images/IMG_xyz.jpg">', function (error, result) {
expect(Amperize.__get__('called')).to.be.equal(true);
Expand All @@ -292,9 +366,9 @@ describe('Amperize', function () {
sizeOfMock = nock('http://example.com')
.get('/images/IMG_xyz.jpg')
.reply(200, {
data: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});
sizeOfStub.throws('error');
sizeOfStub.throws({error: 'image-size could not find dimensions'});
Amperize.__set__('sizeOf', sizeOfStub);

amperize.parse('<img src="http://example.com/images/IMG_xyz.jpg">', function (error, result) {
Expand All @@ -310,7 +384,7 @@ describe('Amperize', function () {
.get('/images/IMG_xyz.jpg')
.socketDelay(5500)
.reply(200, {
data: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
body: '<Buffer 2c be a4 40 f7 87 73 1e 57 2c c1 e4 0d 79 03 95 42 f0 42 2e 41 95 27 c9 5c 35 a7 71 2c 09 5a 57 d3 04 1e 83 03 28 07 96 b0 c8 88 65 07 7a d1 d6 63 50>'
});

amperize.parse('<img src="http://example.com/images/IMG_xyz.jpg">', function (error, result) {
Expand Down

0 comments on commit 77b5880

Please sign in to comment.