Security update for permissions_by_term
[yaffs-website] / node_modules / uncss / src / phantom.js
1 'use strict';
2
3 /* eslint-env phantomjs */
4
5 /* globals document: true */
6
7 var path = require('path'),
8     phridge = require('phridge'),
9     promise = require('bluebird'),
10     utility = require('./utility'),
11     fs = require('fs'),
12     _ = require('lodash');
13
14 var phantom;
15
16 /**
17  * Create the PhantomJS instances, or use the given one.
18  * @param  {Object}  instance   The instance to use, if there is one
19  * @return {promise}
20  */
21 function init(instance) {
22     if (instance) {
23         phantom = instance;
24         return null;
25     }
26
27     // Convert to bluebird promise
28     return new promise(function (resolve) {
29         resolve(phridge.spawn({
30             ignoreSslErrors: 'yes',
31             sslProtocol: 'any'
32         }));
33     }).then(function (ph) {
34         /* Phridge outputs everything to stdout by default */
35         ph.childProcess.cleanStdout.unpipe();
36         ph.childProcess.cleanStdout.pipe(process.stderr);
37         phantom = ph;
38     }).disposer(phridge.disposeAll);
39 }
40
41 function cleanupAll() {
42     phridge.disposeAll();
43 }
44
45 /**
46  * This function is called whenever a resource is requested by PhantomJS.
47  * If we are loading either raw HTML or a local page, PhantomJS needs to be able to find the
48  *   resource with an absolute path.
49  * There are two possible cases:
50  *   - 'file://': This might be either a protocol-less URL or a relative path. Since we
51  *                can't handle both, we choose to handle the former.
52  *   - 'file:///': This is an absolute path. If options.htmlroot is specified, we have a chance to
53  *                 redirect the request to the correct location.
54  */
55 function ResourceHandler(htmlroot, isWindows, resolve) {
56     var ignoredExtensions = ['\\.css', '\\.png', '\\.gif', '\\.jpg', '\\.jpeg', ''],
57         ignoredEndpoints = ['fonts\\.googleapis'];
58
59     var ignoreRequests = new RegExp(ignoredExtensions.join('$|') + ignoredEndpoints.join('|'));
60
61     this.onResourceRequested = function (requestData, networkRequest) {
62         var originalUrl = requestData.url,
63             url = originalUrl.split('?')[0].split('#')[0];
64
65         if (url.substr(-3) === '.js' && url.substr(0, 7) === 'file://') {
66             /* Try and match protocol-less URLs and absolute ones.
67              * Relative URLs will still not load.
68              */
69             if (url.substr(5, 3) === '///') {
70                 /* Absolute URL
71                  * Retry loading the resource appending the htmlroot option
72                  */
73                 if (isWindows) {
74                     /* Do not strip leading '/' */
75                     url = originalUrl.substr(0, 8) + htmlroot + originalUrl.substr(7);
76                 } else {
77                     url = originalUrl.substr(0, 7) + htmlroot + originalUrl.substr(7);
78                 }
79             } else {
80                 /* Protocol-less URL */
81                 url = 'http://' + originalUrl.substr(7);
82             }
83             networkRequest.changeUrl(url);
84         } else if (ignoreRequests.test(url)) {
85             networkRequest.abort();
86         }
87     };
88     resolve();
89 }
90
91 /**
92  * Helper for fromRaw, fromLocal, fromRemote;
93  * return the phantom page after the timeout
94  * has elapsed
95  * @param  {phantom} page    Page created by phantom
96  * @param  {Object} options
97  * @return {promise}
98  */
99 function resolveWithPage(page, options) {
100     return function () {
101         return new promise(function (resolve) {
102             setTimeout(function () {
103                 return resolve(page);
104             }, options.timeout);
105         });
106     };
107 }
108
109 /**
110  * Load a page given an HTML string.
111  * @param  {String}  html
112  * @param  {Object}  options
113  * @return {promise}
114  */
115 function fromRaw(html, options) {
116     var page = phantom.createPage(),
117         htmlroot = path.join(process.cwd(), options.htmlroot || '');
118
119     return promise.resolve(page.run(htmlroot, utility.isWindows(), ResourceHandler).then(function () {
120         return page.run(html, function (raw) {
121             this.setContent(raw, 'local');
122         });
123     }).then(resolveWithPage(page, options)));
124 }
125
126 /**
127  * Open a page given a filename.
128  * @param  {String}  filename
129  * @param  {Object}  options
130  * @return {promise}
131  */
132 function fromLocal(filename, options) {
133     return promise.promisify(fs.readFile)(filename, 'utf-8').then(function (html) {
134         return fromRaw(html, options);
135     });
136 }
137
138 /**
139  * Open a page given a URL.
140  * @param  {String}  url
141  * @param  {Object}  options
142  * @return {promise}
143  */
144 function fromRemote(url, options) {
145     /* If the protocol is unspecified, default to HTTP */
146     if (!/^http/.test(url)) {
147         url = 'http:' + url;
148     }
149
150     return promise.resolve(phantom.openPage(url).then(function (page) {
151         return resolveWithPage(page, options)();
152     }));
153 }
154
155 /**
156  * Extract stylesheets' hrefs from dom
157  * @param  {Object}  page       A PhantomJS page
158  * @param  {Object}  options    Options, as passed to UnCSS
159  * @return {promise}
160  */
161 function getStylesheets(page, options) {
162     if (_.isArray(options.media) === false) {
163         options.media = [options.media];
164     }
165     var media = _.union(['', 'all', 'screen'], options.media);
166     return page.run(function () {
167         return this.evaluate(function () {
168             return Array.prototype.map.call(document.querySelectorAll('link[rel="stylesheet"]'), function (link) {
169                 return {
170                     href: link.href,
171                     media: link.media
172                 };
173             });
174         });
175     }).then(function (stylesheets) {
176         stylesheets = _
177             .toArray(stylesheets)
178             /* Match only specified media attributes, plus defaults */
179             .filter(function (sheet) {
180                 return media.indexOf(sheet.media) !== -1;
181             })
182             .map(function (sheet) {
183                 return sheet.href;
184             });
185         return stylesheets;
186     });
187 }
188
189 /**
190  * Filter unused selectors.
191  * @param  {Object}  page   A PhantomJS page
192  * @param  {Array}   sels   List of selectors to be filtered
193  * @return {promise}
194  */
195 function findAll(page, sels) {
196     return page.run(sels, function (args) {
197         return this.evaluate(function (selectors) {
198             // Unwrap noscript elements
199             Array.prototype.forEach.call(document.getElementsByTagName('noscript'), function (ns) {
200                 var wrapper = document.createElement('div');
201                 wrapper.innerHTML = ns.innerText;
202                 // Insert each child of the <noscript> as its sibling
203                 Array.prototype.forEach.call(wrapper.children, function (child) {
204                     ns.parentNode.insertBefore(child, ns);
205                 });
206             });
207             // Do the filtering
208             selectors = selectors.filter(function (selector) {
209                 try {
210                     if (document.querySelector(selector)) {
211                         return true;
212                     }
213                 } catch (e) {
214                     return true;
215                 }
216                 return false;
217             });
218             return {
219                 selectors: selectors
220             };
221         }, args);
222     }).then(function (res) {
223         if (res === null) {
224             return [];
225         }
226         return res.selectors;
227     });
228 }
229
230 module.exports = {
231     init: init,
232     cleanupAll: cleanupAll,
233     fromLocal: fromLocal,
234     fromRaw: fromRaw,
235     fromRemote: fromRemote,
236     findAll: findAll,
237     getStylesheets: getStylesheets
238 };