Quellcode durchsuchen

Merge pull request #147 from gmetais/develop

v1.10.0
Gaël Métais vor 9 Jahren
Ursprung
Commit
51e4265109

+ 13 - 4
.travis.yml

@@ -1,9 +1,18 @@
 language: node_js
 sudo: false
 node_js:
-    - "0.12.4"
+  - "5.8"
+  - "0.12"
+env:
+  - CXX=g++-4.8
+addons:
+  apt:
+    sources:
+      - ubuntu-toolchain-r-test
+    packages:
+      - g++-4.8
 before_install:
-    - "npm install -g npm"
-    - "npm install -g grunt-cli"
+  - "npm install -g npm"
+  - "npm install -g grunt-cli"
 install:
-    - "npm install"
+  - "npm install"

+ 3 - 8
bin/cli.js

@@ -3,7 +3,7 @@
 var debug       = require('debug')('ylt:cli');
 var meow        = require('meow');
 var path        = require('path');
-var jstoxml     = require('jstoxml');
+var EasyXml     = require('easyxml');
 
 var ylt         = require('../lib/index');
 
@@ -15,7 +15,6 @@ var cli = meow({
         'Options:',
         '  --device             Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
         '  --screenshot         Will take a screenshot and use this value as the output path. It needs to end with ".png".',
-        '  --js-deep-analysis   When activated, the javascriptExecutionTree will contain sub-requests.',
         '  --wait-for-selector  Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
         '  --cookie             Adds a cookie on the main domain.',
         '  --auth-user          Basic HTTP authentication username.',
@@ -51,11 +50,6 @@ if (screenshot) {
     options.screenshot = cli.flags.screenshot;
 }
 
-// Deep JS analysis option
-if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
-    options.jsDeepAnalysis = true;
-}
-
 // Device simulation
 options.device = cli.flags.device || 'desktop';
 
@@ -86,7 +80,8 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
             debug('Success');
             switch(cli.flags.reporter) {
                 case 'xml':
-                    console.log(jstoxml.toXML(data, {indent: '  '}));
+                    var serializer = new EasyXml();
+                    console.log(serializer.render(data));
                     break;
                 default:
                     console.log(JSON.stringify(data, null, 2));

+ 2 - 1
front/src/css/index.css

@@ -34,7 +34,8 @@
   font-size: 1em;
 }
 .settings input[type=text],
-.settings input[type=password] {
+.settings input[type=password],
+.settings textarea {
   width: 100%;
   min-width: 4em;
 }

+ 25 - 13
front/src/js/services/apiService.js

@@ -9,25 +9,26 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
                 url: url,
                 waitForResponse: false,
                 screenshot: true,
-                jsTimeline: true,
                 device: settings.device,
                 waitForSelector: settings.waitForSelector,
                 cookie: settings.cookie,
                 authUser: settings.authUser,
-                authPass: settings.authPass
+                authPass: settings.authPass,
+                blockDomain: settings.blockDomain,
+                allowedDomains: settings.allowedDomains,
+                noExternals: settings.noExternals
             };
 
-            if (settings.waitForSelector && settings.waitForSelector !== '') {
-                runObject.waitForSelector = settings.waitForSelector;
-            }
-
-            if (settings.cookie && settings.cookie !== '') {
-                runObject.cookie = settings.cookie;
-            }
-
-            if (settings.authUser && settings.authUser !== '' && settings.authPass && settings.authPass !== '') {
-                runObject.authUser = settings.authUser;
-                runObject.authPass = settings.authPass;
+            
+            if (settings.domainsBlackOrWhite === 'black') {
+                runObject.blockDomain = this.parseDomains(settings.domains);
+            } else if (settings.domainsBlackOrWhite === 'white') {
+                var allowedDomains = this.parseDomains(settings.domains);
+                if (allowedDomains.length > 0) {
+                    runObject.allowDomain = allowedDomains;
+                } else {
+                    runObject.noExternals = true;
+                }
             }
 
             Runs.save(runObject, function(data) {
@@ -43,6 +44,17 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
 
         relaunchTest: function(result) {
             this.launchTest(result.params.url, result.params.options);
+        },
+
+        parseDomains: function(textareaContent) {
+            var lines = textareaContent.split('\n');
+            
+            function removeEmptyLines (line) {
+                return line.trim() !== '';
+            }
+
+            // Remove empty lines
+            return lines.filter(removeEmptyLines).join(',');
         }
     };
 

+ 1 - 1
front/src/less/index.less

@@ -38,7 +38,7 @@
         font-size: 1em;
     }
 
-    input[type=text], input[type=password] {
+    input[type=text], input[type=password], textarea {
         width: 100%;
         min-width: 4em;
     }

+ 7 - 0
front/src/views/dashboard.html

@@ -1,6 +1,13 @@
 <div ng-include="'views/resultSubHeader.html'"></div>
 <div class="summary board">
     
+    <div ng-if="result.blockedRequests">
+        <b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
+        <div ng-repeat="request in result.blockedRequests">
+            {{request}}
+        </div>
+    </div>
+
     <div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
         <div>
             <h2>Global score</h2>

+ 12 - 5
front/src/views/index.html

@@ -61,15 +61,22 @@
                     </div>
                 </div>
             </div>
-            <!--<div>
-                <div class="label">Blocked domains</div>
+            <div>
+                <div class="label">
+                    Block domains
+                    <span class="settingsTooltip">
+                        <span class="icon-question"></span>
+                        <div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
+                    </span>
+                </div>
                 <div>
                     <div>
-                        Blacklist / Whitelist
+                        <input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
+                        <input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
                     </div>
-                    <textarea name=""></textarea>
+                    <textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
                 </div>
-            </div>-->
+            </div>
         </div>
     </div>
 </form>

+ 18 - 0
front/src/views/rule.html

@@ -96,6 +96,12 @@
                         </span>
                     </div>
 
+                    <div ng-if="policyName === 'cssRules'">
+                        <span ng-if="offender.file === 'inline CSS'">inline CSS</span>
+                        <span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
+                        : <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
+                    </div>
+
                     <div ng-if="policyName === 'similarColors'">
                         <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
                     </div>
@@ -369,6 +375,18 @@
         </div>
     </div>
 
+    <div ng-if="policyName === 'http2'">
+        <h3>Protocols advertised by the server</h3>
+        <div class="offendersTable">
+            <div ng-repeat="protocol in rule.offendersObj.list">
+                <div>{{protocol}}</div>
+            </div>
+            <div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
+                <div>none</div>
+            </div>
+        </div>
+    </div>
+
     <div ng-if="!rule && rule !== null" class="notFound">
         <h2>404</h2>
         Rule "{{policyName}}"" not found

+ 81 - 15
lib/metadata/policies.js

@@ -267,7 +267,7 @@ var policies = {
     },
     "jQueryVersion": {
         "label": "jQuery version",
-        "message": "<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
+        "message": "<p>Current latest versions of jQuery are 1.12 (with support for old IE versions) and 2.2 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
         "hasOffenders": false,
         "scoreFn": function(data) {
             var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
@@ -279,27 +279,31 @@ var policies = {
                 var value = data.toolsResults.phantomas.metrics.jQueryVersion;
                 var score;
 
-                if (value.indexOf('1.11.') === 0 ||
-                    value.indexOf('1.12.') === 0 ||
-                    value.indexOf('2.1.') === 0 ||
+                if (value.indexOf('1.12.') === 0 ||
                     value.indexOf('2.2.') === 0 ||
-                    value.indexOf('3.0.') === 0) {
+                    value.indexOf('1.13.') === 0 ||
+                    value.indexOf('2.3.') === 0 ||
+                    value.indexOf('3.0.') === 0 ||
+                    value.indexOf('3.1.') === 0) {
                     score = 100;
+                } else if (value.indexOf('1.11.') === 0 ||
+                           value.indexOf('2.1.') === 0) {
+                    score = 90;
                 } else if (value.indexOf('1.10.') === 0 ||
                            value.indexOf('2.0.') === 0) {
-                    score = 90;
-                } else if (value.indexOf('1.9.') === 0) {
                     score = 70;
-                } else if (value.indexOf('1.8.') === 0) {
+                } else if (value.indexOf('1.9.') === 0) {
                     score = 50;
-                } else if (value.indexOf('1.7') === 0) {
+                } else if (value.indexOf('1.8.') === 0) {
                     score = 40;
-                } else if (value.indexOf('1.6') === 0) {
+                } else if (value.indexOf('1.7') === 0) {
                     score = 30;
-                } else if (value.indexOf('1.5') === 0) {
+                } else if (value.indexOf('1.6') === 0) {
                     score = 20;
-                } else if (value.indexOf('1.4') === 0) {
+                } else if (value.indexOf('1.5') === 0) {
                     score = 10;
+                } else if (value.indexOf('1.4') === 0) {
+                    score = 0;
                 } else if (value.indexOf('1.3') === 0) {
                     score = 0;
                 } else if (value.indexOf('1.2') === 0) {
@@ -420,7 +424,40 @@ var policies = {
         "isOkThreshold": 750,
         "isBadThreshold": 3000,
         "isAbnormalThreshold": 4500,
-        "hasOffenders": false
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            var hasInline = false;
+            var inlineCount = 0;
+            var files = [];
+
+            offenders.forEach(function(line) {
+                if (line.indexOf('[inline CSS]: ') === 0) {
+                    hasInline = true;
+                    inlineCount += parseInt(line.substr(14));
+                } else {
+                    var parts = /^<(.*)>: (\d+)$/.exec(line);
+
+                    if (parts) {
+                        files.push({
+                            file: parts[1],
+                            rules: parseInt(parts[2], 10)
+                        });
+                    }
+                }
+            });
+
+            if (hasInline) {
+                files.push({
+                    file: 'inline CSS',
+                    rules: inlineCount
+                });
+            }
+
+            return {
+                count: files.length,
+                list: files
+            };
+        }
     },
     "cssComplexSelectors": {
         "tool": "phantomas",
@@ -925,7 +962,7 @@ var policies = {
     "imageOptimization": {
         "tool": "weightChecker",
         "label": "Image optimization",
-        "message": "<p>This metric mesures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
+        "message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
         "isOkThreshold": 10240,
         "isBadThreshold": 122880,
         "isAbnormalThreshold": 307200,
@@ -935,7 +972,7 @@ var policies = {
     "gzipCompression": {
         "tool": "weightChecker",
         "label": "Gzip compression",
-        "message": "<p>Mesures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
+        "message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
         "isOkThreshold": 5125,
         "isBadThreshold": 81920,
         "isAbnormalThreshold": 153600,
@@ -1044,6 +1081,35 @@ var policies = {
         "isAbnormalThreshold": 30,
         "hasOffenders": true
     },
+    "http2": {
+        "label": "HTTP/2 or SPDY",
+        "message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
+        "hasOffenders": true,
+        "scoreFn": function(data) {
+            if (!data.toolsResults.http2) {
+                return null;
+            }
+
+            var isHttp2 = data.toolsResults.http2.metrics.http2;
+
+            var result = {
+                value: isHttp2 ? 'Yes' : 'No',
+                score: isHttp2 ? 100 : 0,
+                bad: !isHttp2,
+                abnormal: false,
+                abnormalityScore: 0
+            };
+
+            if (data.toolsResults.http2.offenders) {
+                result.offendersObj = {
+                    count: data.toolsResults.http2.offenders.http2.length,
+                    list: data.toolsResults.http2.offenders.http2
+                };
+            }
+
+            return result;
+        }
+    },
     "cachingDisabled": {
         "tool": "phantomas",
         "label": "Caching disabled",

+ 1 - 0
lib/metadata/scoreProfileGeneric.json

@@ -96,6 +96,7 @@
         "serverConfig": {
             "label": "Server config",
             "policies": {
+                "http2": 2,
                 "closedConnections": 2,
                 "cachingNotSpecified": 1,
                 "cachingDisabled": 1,

+ 12 - 1
lib/runner.js

@@ -5,6 +5,7 @@ var phantomasWrapper        = require('./tools/phantomas/phantomasWrapper');
 var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
 var colorDiff               = require('./tools/colorDiff');
 var mediaQueriesChecker     = require('./tools/mediaQueriesChecker');
+var isHttp2                 = require('./tools/isHttp2');
 var weightChecker           = require('./tools/weightChecker/weightChecker');
 var rulesChecker            = require('./rulesChecker');
 var scoreCalculator         = require('./scoreCalculator');
@@ -41,6 +42,11 @@ var Runner = function(params) {
 
     })
 
+    .then(function(data) {
+        // Check if HTTP2
+        return isHttp2.check(data);
+    })
+
     .then(function(data) {
 
         // Rules checker
@@ -60,9 +66,14 @@ var Runner = function(params) {
         delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
         delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
 
+
+        if (data.toolsResults.phantomas.offenders.blockedRequests) {
+            data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
+        }
+
+
         // Finished!
         deferred.resolve(data);
-
     })
 
     .fail(function(err) {

+ 9 - 11
lib/server/controllers/apiController.js

@@ -34,12 +34,14 @@ var ApiController = function(app) {
                 waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
                 partialResult: req.body.partialResult || null,
                 screenshot: req.body.screenshot || false,
-                jsTimeline: req.body.jsTimeline || false,
                 device: req.body.device || 'desktop',
                 waitForSelector: req.body.waitForSelector || null,
                 cookie: req.body.cookie || null,
                 authUser: req.body.authUser || null,
-                authPass: req.body.authPass || null
+                authPass: req.body.authPass || null,
+                blockDomain: req.body.blockDomain || null,
+                allowDomain: req.body.allowDomain || null,
+                noExternals: req.body.noExternals || false
             }
         };
 
@@ -71,12 +73,14 @@ var ApiController = function(app) {
 
             var runOptions = {
                 screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
-                jsDeepAnalysis: run.params.jsTimeline,
                 device: run.params.device,
                 waitForSelector: run.params.waitForSelector,
                 cookie: run.params.cookie,
                 authUser: run.params.authUser,
                 authPass: run.params.authPass,
+                blockDomain: run.params.blockDomain,
+                allowDomain: run.params.allowDomain,
+                noExternals: run.params.noExternals,
                 phantomasEngine: serverSettings.phantomasEngine
             };
 
@@ -96,7 +100,7 @@ var ApiController = function(app) {
             if (run.params.screenshot) {
                 
                 // Replace the empty promise created earlier with Q.resolve()
-                screenshotPromise = screenshot.toThumbnail(400)
+                screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
                 
                     // Read screenshot
                     .then(function(screenshotBuffer) {
@@ -129,13 +133,7 @@ var ApiController = function(app) {
                     // Remove uneeded temp screenshot path
                     delete data.params.options.screenshot;
 
-                    // Empty javascriptExecutionTree if not needed
-                    if (!run.params.jsTimeline) {
-                        data.javascriptExecutionTree = {};
-                        data.scrollExecutionTree = {};
-                    }
-
-                    // Remove tools results if not needed
+                    // Here we can remove tools results if not needed
 
                     return resultsDatastore.saveResult(data);
                 })

+ 105 - 0
lib/tools/isHttp2.js

@@ -0,0 +1,105 @@
+var debug   = require('debug')('ylt:isHttp2');
+var url     = require('url');
+var Q       = require('q');
+var http2   = require('is-http2');
+
+var isHttp2 = function() {
+    'use strict';
+
+    this.check = function(data) {
+        debug('Starting to check for HTTP2 support...');
+
+        return this.checkHttp2(data)
+
+            .then(function(result) {
+
+                if (result.isHttp) {
+                    debug('The website is not even in HTTPS');
+
+                    data.toolsResults.http2 = {
+                        metrics: {
+                            http2: false
+                        }
+                    };
+
+                } else if (result.isHttp2) {
+                    debug('HTTP/2 (or SPDY) is supported');
+
+                    data.toolsResults.http2 = {
+                        metrics: {
+                            http2: true
+                        }
+                    };
+                
+                } else {
+                    debug('HTTP/2 is not supported');
+                    
+                    data.toolsResults.http2 = {
+                        metrics: {
+                            http2: false
+                        }
+                    };
+                }
+
+                // Add the supported protocols as offenders
+                if (result.supportedProtocols) {
+                    debug('Supported protocols: ' + result.supportedProtocols.join(' '));
+                    data.toolsResults.http2.offenders = {
+                        http2: result.supportedProtocols
+                    };
+                }
+
+                debug('End of HTTP2 support check');
+
+                return data;
+            })
+
+            .fail(function() {
+                return data;
+            });
+    };
+
+    this.getParsedUrl = function(data) {
+        return url.parse(data.toolsResults.phantomas.url);
+    };
+
+    this.getProtocol = function(data) {
+        return this.getParsedUrl(data).protocol;
+    };
+
+    this.getDomain = function(data) {
+        return this.getParsedUrl(data).hostname;
+    };
+
+    this.checkHttp2 = function(data) {
+        var deferred = Q.defer();
+
+        // Check if it's HTTPS first
+        if (this.getProtocol(data) === 'http:') {
+            
+            deferred.resolve({
+                isHttp: true
+            });
+
+        } else {
+
+            // To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
+            http2(this.getDomain(data), {includeSpdy: true})
+
+                .then(function(result) {
+                    deferred.resolve(result);
+                })
+                
+                .catch(function(error) {
+                    debug('Error while checking HTTP2 support:');
+                    debug(error);
+                    deferred.reject('Error while checking for HTTP2 support');
+                });
+
+        }
+
+        return deferred.promise;
+    };
+};
+
+module.exports = new isHttp2();

+ 15 - 25
lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js

@@ -18,8 +18,8 @@ exports.module = function(phantomas) {
         responseEndTime = Date.now();
     });
 
-    phantomas.once('init', function() {
-        phantomas.evaluate(function(responseEndTime, deepAnalysis) {
+    phantomas.on('init', function() {
+        phantomas.evaluate(function(responseEndTime) {
             (function(phantomas) {
     
                 // Overwritting phantomas spy function
@@ -106,9 +106,6 @@ exports.module = function(phantomas) {
                     var currentContext = root;
                     var depth = 0;
 
-                    if (deepAnalysis) {
-                        phantomas.log('Entering deep Javascript analysis mode');
-                    }
 
                     // Add a child but don't enter its context
                     function pushContext(data) {
@@ -129,9 +126,7 @@ exports.module = function(phantomas) {
                             }
                         }
 
-                        if (depth === 0 || deepAnalysis) {
-                            currentContext.addChild(data);
-                        }
+                        currentContext.addChild(data);
                     }
                     
                     // Add a child to the current context and enter its context
@@ -153,9 +148,7 @@ exports.module = function(phantomas) {
                             }
                         }
 
-                        if (depth === 0 || deepAnalysis) {
-                            currentContext = currentContext.addChild(data);
-                        }
+                        currentContext = currentContext.addChild(data);
 
                         depth ++;
                     }
@@ -178,21 +171,18 @@ exports.module = function(phantomas) {
                             }
                         }
 
-                        if (depth === 1 || deepAnalysis) {
-
-                            // Merge previous data with moreData (ovewrites if exists)
-                            if (moreData) {
-                                for (var key in moreData) {
-                                    currentContext.data[key] = moreData[key];
-                                }
+                        // Merge previous data with moreData (ovewrites if exists)
+                        if (moreData) {
+                            for (var key in moreData) {
+                                currentContext.data[key] = moreData[key];
                             }
+                        }
 
-                            var parent = currentContext.parent;
-                            if (parent === null) {
-                                console.error('Error: trying to close root context in ContextTree');
-                            } else {
-                                currentContext = parent;
-                            }
+                        var parent = currentContext.parent;
+                        if (parent === null) {
+                            console.error('Error: trying to close root context in ContextTree');
+                        } else {
+                            currentContext = parent;
                         }
 
                         depth --;
@@ -255,6 +245,6 @@ exports.module = function(phantomas) {
                 })();
 
             })(window.__phantomas);
-        }, responseEndTime, phantomas.getParam('js-deep-analysis'));
+        }, responseEndTime);
     });
 };

+ 1 - 1
lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js

@@ -19,7 +19,7 @@ exports.module = function(phantomas) {
     phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
 
     // fake native DOM functions
-    phantomas.once('init', function() {
+    phantomas.on('init', function() {
         phantomas.evaluate(function() {
             (function(phantomas) {
                 function querySpy(type, query, fnName, context, hasNoResults) {

+ 1 - 1
lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js

@@ -12,7 +12,7 @@ exports.module = function(phantomas) {
     phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
     phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
 
-    phantomas.once('init', function() {
+    phantomas.on('init', function() {
         phantomas.evaluate(function() {
             (function(phantomas) {
                 // spy calls to EventTarget.addEventListener

+ 1 - 1
lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js

@@ -138,7 +138,7 @@ exports.module = function(phantomas) {
     jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
 
     // spy calls to jQuery functions
-    phantomas.once('init', function() {
+    phantomas.on('init', function() {
         phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
             (function(phantomas) {
                 var oldJQuery;

+ 1 - 1
lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js

@@ -23,7 +23,7 @@ exports.module = function(phantomas) {
         phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
     }
 
-    phantomas.once('init', function() {
+    phantomas.on('init', function() {
         phantomas.evaluate(function(spyEval) {
             (function(phantomas) {
                 function report(msg, caller, backtrace, metric) {

+ 8 - 7
lib/tools/phantomas/phantomasWrapper.js

@@ -22,7 +22,6 @@ var PhantomasWrapper = function() {
             // Cusomizable options
             'engine': task.options.phantomasEngine || 'webkit',
             'timeout': task.options.timeout || 30,
-            'js-deep-analysis': task.options.jsDeepAnalysis || false,
             'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
             'tablet': (task.options.device === 'tablet'),
             'phone': (task.options.device === 'phone'),
@@ -31,12 +30,14 @@ var PhantomasWrapper = function() {
             'cookie': task.options.cookie,
             'auth-user': task.options.authUser,
             'auth-pass': task.options.authPass,
+            'block-domain': task.options.blockDomain,
+            'allow-domain': task.options.allowDomain,
+            'no-externals': task.options.noExternals,
 
             // Mandatory
             'reporter': 'json:pretty',
             'analyze-css': true,
             'skip-modules': [
-                'blockDomains', // not needed
                 'domHiddenContent', // overriden
                 'domMutations', // not compatible with webkit
                 'domQueries', // overriden
@@ -118,11 +119,6 @@ var PhantomasWrapper = function() {
         async.retry(triesNumber, function(cb) {
 
             currentTry ++;
-            // Fix for https://github.com/gmetais/YellowLabTools/issues/114
-            if (currentTry === 2 && options.engine === 'webkit2') {
-                debug('Launching a second try with the old webkit v1 engine');
-                options.engine = 'webkit';
-            }
 
             var process = phantomas(task.url, options, function(err, json, results) {
                 var errorCode = err ? parseInt(err.message, 10) : null;
@@ -155,6 +151,11 @@ var PhantomasWrapper = function() {
                 }
 
                 cb(errorCode, json);
+            
+            }).fail(function() {
+                // This function is useless, but the failing promise needs to be handled,
+                // otherwise the module meow writes in the console in case of a timeout (error code 252).
+                debug('Failing promise handled');
             });
             
             phantomasPid = process.pid;

+ 35 - 34
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yellowlabtools",
-  "version": "1.9.3",
+  "version": "1.10.0",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "license": "GPL-2.0",
   "author": {
@@ -20,51 +20,52 @@
   },
   "main": "./lib/index.js",
   "dependencies": {
-    "angular": "1.4.8",
-    "angular-animate": "1.4.8",
-    "angular-chart.js": "0.8.8",
-    "angular-local-storage": "0.2.2",
-    "angular-resource": "1.4.8",
-    "angular-route": "1.4.8",
-    "angular-sanitize": "1.4.8",
-    "async": "1.5.0",
-    "body-parser": "1.14.2",
+    "angular": "1.5.2",
+    "angular-animate": "1.5.2",
+    "angular-chart.js": "0.9.0",
+    "angular-local-storage": "0.2.7",
+    "angular-resource": "1.5.2",
+    "angular-route": "1.5.2",
+    "angular-sanitize": "1.5.2",
+    "async": "1.5.2",
+    "body-parser": "1.15.0",
     "chart.js": "1.0.2",
-    "clean-css": "3.4.8",
-    "color-diff": "0.1.7",
-    "compression": "1.6.0",
+    "clean-css": "3.4.10",
+    "color-diff": "1.0.0",
+    "compression": "1.6.1",
     "cors": "2.7.1",
     "css-mq-parser": "0.0.3",
     "debug": "2.2.0",
-    "express": "4.13.3",
+    "easyxml": "2.0.1",
+    "express": "4.13.4",
     "imagemin": "4.0.0",
     "imagemin-jpegoptim": "4.1.0",
-    "jstoxml": "0.2.3",
+    "is-http2": "1.0.4",
     "lwip": "0.0.8",
-    "meow": "3.6.0",
-    "minimize": "1.7.4",
+    "meow": "3.7.0",
+    "minimize": "1.8.1",
     "parse-color": "1.0.0",
-    "phantomas": "1.13.0",
+    "phantomas": "1.15.1",
     "ps-node": "0.0.5",
     "q": "1.4.1",
-    "request": "2.67.0",
-    "rimraf": "2.4.4",
+    "request": "2.69.0",
+    "rimraf": "2.5.2",
     "temporary": "0.0.8",
     "try-thread-sleep": "1.0.0",
-    "uglify-js": "2.6.1"
+    "uglify-js": "2.6.2"
   },
   "devDependencies": {
-    "chai": "~3.4.0",
+    "chai": "~3.5.0",
     "grunt": "~0.4.5",
     "grunt-blanket": "~0.0.10",
-    "grunt-contrib-clean": "~0.7.0",
-    "grunt-contrib-concat": "~0.5.1",
-    "grunt-contrib-copy": "~0.8.2",
-    "grunt-contrib-cssmin": "~0.14.0",
-    "grunt-contrib-htmlmin": "~0.6.0",
-    "grunt-contrib-jshint": "~0.11.3",
-    "grunt-contrib-less": "~1.1.0",
-    "grunt-contrib-uglify": "~0.11.0",
+    "grunt-contrib-clean": "~1.0.0",
+    "grunt-contrib-concat": "~1.0.0",
+    "grunt-contrib-copy": "~1.0.0",
+    "grunt-contrib-cssmin": "~1.0.1",
+    "grunt-contrib-htmlmin": "~1.1.0",
+    "grunt-contrib-jshint": "~1.0.0",
+    "grunt-contrib-less": "~1.2.0",
+    "grunt-contrib-uglify": "~1.0.1",
     "grunt-env": "~0.4.4",
     "grunt-express": "~1.4.1",
     "grunt-filerev": "~2.3.1",
@@ -73,10 +74,10 @@
     "grunt-mocha-test": "~0.12.7",
     "grunt-replace": "~0.11.0",
     "grunt-usemin": "~3.1.1",
-    "grunt-webfont": "~1.1.0",
-    "matchdep": "~1.0.0",
-    "mocha": "~2.3.2",
-    "sinon": "~1.17.2",
+    "grunt-webfont": "~1.2.0",
+    "matchdep": "~1.0.1",
+    "mocha": "~2.4.5",
+    "sinon": "~1.17.3",
     "sinon-chai": "~2.8.0"
   },
   "scripts": {

+ 1 - 0
server_config/settings-prod.json

@@ -2,6 +2,7 @@
     "serverPort": 80,
     "phantomasEngine": "webkit",
     "googleAnalyticsId": "",
+    "screenshotWidth": 400,
 
     "authorizedKeys": {
         

+ 1 - 0
server_config/settings.json

@@ -2,6 +2,7 @@
     "serverPort": 8383,
     "phantomasEngine": "webkit",
     "googleAnalyticsId": "",
+    "screenshotWidth": 400,
 
     "authorizedKeys": {
         

+ 10 - 4
test/api/apiTest.js

@@ -31,6 +31,7 @@ describe('api', function() {
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': 'invalid'
             }
         }, function(error, response, body) {
@@ -53,6 +54,7 @@ describe('api', function() {
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {
@@ -76,6 +78,7 @@ describe('api', function() {
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {
@@ -99,12 +102,13 @@ describe('api', function() {
                 screenshot: true,
                 device: 'tablet',
                 //waitForSelector: '*',
-                cookie: 'foo=bar',
+                cookie: 'foo=bar;domain=google.com',
                 authUser: 'joe',
                 authPass: 'secret'
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {
@@ -133,6 +137,7 @@ describe('api', function() {
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {
@@ -165,14 +170,13 @@ describe('api', function() {
                 body.should.have.a.property('rules').that.is.an('object');
                 body.should.have.a.property('toolsResults').that.is.an('object');
 
-                // javascriptExecutionTree should only be filled if option jsTimeline is true
                 body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
-                body.javascriptExecutionTree.should.deep.equal({});
+                body.javascriptExecutionTree.should.not.deep.equal({});
 
                 // Check if settings are correctly sent and retrieved
                 body.params.options.should.have.a.property('device').that.equals('tablet');
                 //body.params.options.should.have.a.property('waitForSelector').that.equals('*');
-                body.params.options.should.have.a.property('cookie').that.equals('foo=bar');
+                body.params.options.should.have.a.property('cookie').that.equals('foo=bar;domain=google.com');
                 body.params.options.should.have.a.property('authUser').that.equals('joe');
                 body.params.options.should.have.a.property('authPass').that.equals('secret');
 
@@ -208,6 +212,7 @@ describe('api', function() {
             },
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {
@@ -232,6 +237,7 @@ describe('api', function() {
             url: serverUrl + '/api/runs/' + asyncRunId,
             json: true,
             headers: {
+                'Content-Type': 'application/json',
                 'X-Api-Key': Object.keys(config.authorizedKeys)[0]
             }
         }, function(error, response, body) {

+ 13 - 12
test/core/customPoliciesTest.js

@@ -183,18 +183,19 @@ describe('customPolicies', function() {
 
         var versions = {
             '1.2.9': 0,
-            '1.3.9': 0,
-            '1.4.4': 10,
-            '1.5.0': 20,
-            '1.6.3': 30,
-            '1.7.0': 40,
-            '1.8.3a': 50,
-            '1.9.2': 70,
-            '1.10.1': 90,
-            '2.0.0-rc1': 90,
-            '1.11.1': 100,
-            '2.1.1-beta1': 100,
-            '3.0.0': 100
+            '1.4.4': 0,
+            '1.5.0': 10,
+            '1.6.3': 20,
+            '1.7.0': 30,
+            '1.8.3a': 40,
+            '1.9.2': 50,
+            '1.10.1': 70,
+            '2.0.0-rc1': 70,
+            '1.11.1': 90,
+            '2.1.1-beta1': 90,
+            '1.12.1': 100,
+            '2.3.1': 100,
+            '3.1.0': 100
         };
 
         for (var version in versions) {

+ 48 - 0
test/core/isHttp2Test.js

@@ -0,0 +1,48 @@
+var should = require('chai').should();
+var isHttp2 = require('../../lib/tools/isHttp2');
+
+describe('isHttp2', function() {
+    
+    it('should parse the protocol correctly', function() {
+        isHttp2.getProtocol({
+            toolsResults: {
+                phantomas: {
+                    url: 'http://www.yahoo.com'
+                }
+            }
+        }).should.equal('http:');
+
+
+        isHttp2.getProtocol({
+            toolsResults: {
+                phantomas: {
+                    url: 'https://www.yahoo.com'
+                }
+            }
+        }).should.equal('https:');
+    });
+
+    it('should parse the domain correctly', function() {
+        isHttp2.getDomain({
+            toolsResults: {
+                phantomas: {
+                    url: 'http://www.yahoo.com'
+                }
+            }
+        }).should.equal('www.yahoo.com');
+
+
+        isHttp2.getDomain({
+            toolsResults: {
+                phantomas: {
+                    url: 'https://www.yahoo.com'
+                }
+            }
+        }).should.equal('www.yahoo.com');
+    });
+
+    it('should have a function checkHttp2', function() {
+        isHttp2.should.have.a.property('checkHttp2').that.is.a('function');
+    });
+
+});

+ 1 - 0
test/fixtures/settings.json

@@ -2,6 +2,7 @@
     "serverPort": "8387",
     "phantomasEngine": "webkit",
     "googleAnalyticsId": "",
+    "screenshotWidth": 400,
     
     "authorizedKeys": {
         "1234567890": "contact@gaelmetais.com"