Skip to content

Commit 95413ac

Browse files
committed
Improve favicon parsing:
- Attempt to load all found links + default and use first one that works - Include Promise.any polyfill for older browsers
1 parent 54410bf commit 95413ac

File tree

1 file changed

+84
-37
lines changed

1 file changed

+84
-37
lines changed

src/scripts/libs/favicon.js

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@
44
*/
55
define([], function () {
66
async function checkFavicon(source) {
7+
if (typeof Promise.any !== 'function') {
8+
Promise.any = (promises) => {
9+
return new Promise((resolve, reject) => {
10+
let hasResolved = false;
11+
let processedPromises = 0;
12+
const rejectionReasons = [];
13+
const resolveOnce = (value) => {
14+
if (!hasResolved) {
15+
hasResolved = true;
16+
resolve(value);
17+
}
18+
};
19+
const rejectionCheck = (reason) => {
20+
rejectionReasons.push(reason);
21+
if (rejectionReasons.length >= processedPromises) {
22+
reject(rejectionReasons);
23+
}
24+
};
25+
for (const promise of promises) {
26+
processedPromises++;
27+
promise.then((result) => {
28+
resolveOnce(result);
29+
}).catch((reason) => {
30+
rejectionCheck(reason);
31+
});
32+
}
33+
});
34+
};
35+
}
36+
737
return new Promise((resolve, reject) => {
838
const baseUrl = new URL(source.get('base'));
939

@@ -24,60 +54,72 @@ define([], function () {
2454
reject('timeout');
2555
};
2656
xhr.onloadend = () => {
27-
if (xhr.readyState !== 4) {
57+
if (xhr.readyState !== XMLHttpRequest.DONE) {
2858
reject('network error');
2959
return;
3060
}
3161
if (xhr.status !== 200) {
32-
reject('non-200');
62+
reject('Encountered non-200 response trying to parse ' + baseUrl.origin);
3363
return;
3464
}
3565
const baseDocumentContents = xhr.responseText.replace(/<body(.*?)<\/body>/gm, '');
3666
const baseDocument = new DOMParser().parseFromString(baseDocumentContents, 'text/html');
37-
const iconLinks = [...baseDocument.querySelectorAll('link[rel*="icon"][href]')];
67+
const linkElements = [...baseDocument.querySelectorAll('link[rel*="icon"][href]')];
68+
69+
const links = [];
3870

39-
iconLinks.some((link) => {
40-
favicon = link.getAttribute('href');
71+
linkElements.forEach((link) => {
72+
const favicon = link.getAttribute('href');
4173
if (!favicon) {
42-
return false;
74+
return;
4375
}
4476
if (favicon.includes('svg')) {
45-
return false;
77+
return;
4678
}
47-
if (favicon.startsWith('/')) {
48-
const prefix = favicon.startsWith('//') ? 'https:' : baseUrl.origin;
49-
resolve(prefix + favicon);
50-
return true;
79+
if (favicon.startsWith('http')) {
80+
links.push(favicon);
5181
}
52-
if (!favicon.startsWith('http')) {
53-
resolve(baseUrl.origin + '/' + favicon);
54-
return true;
82+
if (favicon.startsWith('//')) {
83+
links.push(baseUrl.protocol + favicon);
84+
return;
85+
}
86+
if (favicon.startsWith('data')) {
87+
links.push(favicon);
88+
return;
89+
}
90+
if (favicon.startsWith('/')) {
91+
links.push(baseUrl.origin + favicon);
92+
return;
5593
}
5694

57-
resolve(favicon);
95+
links.push(baseUrl.origin + '/' + favicon);
5896
});
5997

60-
resolve(baseUrl.origin + '/favicon.ico');
98+
links.push(baseUrl.origin + '/favicon.ico');
99+
100+
resolve(links);
61101
};
62102

63-
xhr.open('GET', baseUrl);
103+
xhr.open('GET', baseUrl.origin);
64104
xhr.timeout = 1000 * 30;
65105
xhr.send();
66106
});
67107
}
68108

69109
getFaviconAddress(baseUrl)
70-
.then((faviconAddress) => {
71-
toDataURI(faviconAddress)
72-
.then(response => {
73-
resolve(response);
74-
})
75-
.catch(() => {
76-
reject();
77-
});
110+
.then((faviconAddresses) => {
111+
const promises = faviconAddresses.map((favicon) => {
112+
return toDataURI(favicon);
113+
});
114+
Promise.any(promises)
115+
.then((dataURI) => {
116+
resolve(dataURI);
117+
}).catch((errors) => {
118+
reject(errors);
119+
});
78120
})
79-
.catch(() => {
80-
reject();
121+
.catch((error) => {
122+
reject(error);
81123
});
82124
});
83125
}
@@ -89,32 +131,38 @@ define([], function () {
89131
// * @constructor
90132
// * @extends Object
91133
// */
92-
function toDataURI(url) {
93-
return new Promise(function (resolve, reject) {
134+
function toDataURI(favicon) {
135+
return new Promise((resolve, reject) => {
136+
if (favicon.startsWith('data')) {
137+
resolve(favicon);
138+
}
94139
const xhr = new window.XMLHttpRequest();
95140
xhr.responseType = 'arraybuffer';
96-
xhr.onerror = function () {
97-
reject('[modules/toDataURI] XMLHttpRequest error on', url);
141+
xhr.onerror = () => {
142+
reject('[modules/toDataURI] error on: ' + favicon);
98143
};
99144
xhr.ontimeout = () => {
100145
reject('timeout');
101146
};
102147
xhr.onloadend = function () {
103148
if (xhr.readyState !== XMLHttpRequest.DONE) {
104-
reject('[modules/toDataURI] XMLHttpRequest error on', url);
149+
reject('[modules/toDataURI] network error on: ' + favicon);
105150
return;
106151
}
107152
if (xhr.status !== 200) {
108-
reject('[modules/toDataURI] XMLHttpRequest error on', url);
153+
reject('[modules/toDataURI] non-200 on: ' + favicon);
109154
return;
110155
}
111156
const type = xhr.getResponseHeader('content-type');
112157
if (!~type.indexOf('image') || xhr.response.byteLength < 10) {
113-
reject('[modules/toDataURI] Not an image on', url);
158+
reject('[modules/toDataURI] Not an image on: ' + favicon);
114159
return;
115160
}
116-
let expires = 0;
161+
const imgData = 'data:' + type + ';base64,' + AB2B64(xhr.response);
162+
117163
const expiresHeader = xhr.getResponseHeader('expires');
164+
165+
let expires = 0;
118166
if (expiresHeader) {
119167
expires = parseInt(Math.round((new Date(expiresHeader)).getTime() / 1000));
120168
} else {
@@ -127,10 +175,9 @@ define([], function () {
127175
expires = parseInt(Math.round((new Date()).getTime() / 1000)) + maxAge;
128176
}
129177

130-
const imgData = 'data:' + type + ';base64,' + AB2B64(xhr.response);
131178
resolve({favicon: imgData, faviconExpires: expires});
132179
};
133-
xhr.open('GET', url);
180+
xhr.open('GET', favicon);
134181
xhr.timeout = 1000 * 30;
135182
xhr.send();
136183
});

0 commit comments

Comments
 (0)