diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/amperize.js b/lib/amperize.js index 1651831..68cb15b 100644 --- a/lib/amperize.js +++ b/lib/amperize.js @@ -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 = { @@ -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; } @@ -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; @@ -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 @@ -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); diff --git a/package.json b/package.json index 819e970..19d433a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/amperize.test.js b/test/amperize.test.js index b50cb63..8aef893 100644 --- a/test/amperize.test.js +++ b/test/amperize.test.js @@ -81,6 +81,7 @@ describe('Amperize', function () { afterEach(function () { sinon.restore(); + Amperize.__set__('called', false); }); it('throws an error if no callback provided', function () { @@ -95,7 +96,7 @@ describe('Amperize', function () { sizeOfMock = nock('http://static.wixstatic.com') .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') .reply(200, { - data: '' + body: '' }); sizeOfStub.returns({width: 50, height: 50, type: 'jpg'}); @@ -118,7 +119,7 @@ describe('Amperize', function () { sizeOfMock = nock('http://static.wixstatic.com') .get('/media/355241_d31358572a2542c5a44738ddcb59e7ea.jpg_256') .reply(200, { - data: '' + body: '' }); sizeOfStub.returns({width: 350, height: 200, type: 'jpg'}); @@ -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: '' + }); + + 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('', function (error, result) { + expect(result).to.exist; + expect(Amperize.__get__('called')).to.be.equal(false); + expect(result).to.contain(''); + done(); + }); + }); + + it('transforms .gif with only height property into with full dimensions by overriding them', function (done) { sizeOfMock = nock('https://media.giphy.com') .get('/media/l46CtzgjhTm29Cbjq/giphy.gif') .reply(200, { - data: '' + body: '' }); sizeOfStub.returns({width: 800, height: 600, type: 'gif'}); @@ -230,6 +264,15 @@ describe('Amperize', function () { }); }); + it('can handle invalid URLs', function (done) { + amperize.parse('', function (error, result) { + expect(result).to.exist; + expect(Amperize.__get__('called')).to.be.equal(true); + expect(result).to.be.equal(''); + done(); + }); + }); + it('can handle