LinkHandlerParent.sys.mjs (7050B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { 6 TYPE_ICO, 7 SVG_DATA_URI_PREFIX, 8 TRUSTED_FAVICON_SCHEMES, 9 blobAsDataURL, 10 } from "moz-src:///browser/modules/FaviconUtils.sys.mjs"; 11 12 const lazy = {}; 13 14 ChromeUtils.defineESModuleGetters(lazy, { 15 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 16 OpenSearchManager: 17 "moz-src:///browser/components/search/OpenSearchManager.sys.mjs", 18 }); 19 20 let gTestListeners = new Set(); 21 22 async function drawImageOnCanvas(canvas, image) { 23 let data = await image.blob.bytes(); 24 let frame = new VideoFrame(data, { 25 timestamp: 0, 26 format: image.format, 27 codedWidth: image.displayWidth, 28 codedHeight: image.displayHeight, 29 }); 30 31 canvas.width = frame.displayWidth; 32 canvas.height = frame.displayHeight; 33 let ctx = canvas.getContext("2d"); 34 ctx.drawImage(frame, 0, 0); 35 } 36 37 // Re-construct the ICO file with different sized PNG images. 38 // See https://en.wikipedia.org/wiki/ICO_(file_format). 39 function createICO(images) { 40 const ICO_HEADER_SIZE = 6; 41 const ICO_DIR_ENTRY_SIZE = 16; 42 43 const metadataSize = ICO_HEADER_SIZE + ICO_DIR_ENTRY_SIZE * images.length; 44 const size = 45 metadataSize + images.reduce((acc, image) => acc + image.byteLength, 0); 46 47 let buffer = new ArrayBuffer(size); 48 let u8 = new Uint8Array(buffer); 49 let view = new DataView(buffer); 50 51 view.setUint16(0, 0, true); // idReserved 52 view.setUint16(2, 1, true); // idType (1 = ICO) 53 view.setUint16(4, images.length, true); // idCount 54 55 let dataOffset = metadataSize; // Append image data directly after the meta data. 56 for (let i = 0; i < images.length; i++) { 57 const off = ICO_HEADER_SIZE + ICO_DIR_ENTRY_SIZE * i; 58 59 // We use a zero width and height because we always use compressed PNGs, 60 // which require this and have their own width/height information. 61 view.setUint8(off, 0); // bWidth 62 view.setUint8(off + 1, 0); // bHeight 63 view.setUint8(off + 2, 0); // bColorCount 64 view.setUint8(off + 3, 0); // bReserved 65 view.setUint16(off + 4, 1, true); // wPlanes 66 view.setUint16(off + 6, 32, true); // wBitCount 67 view.setUint32(off + 8, images[i].byteLength, true); // dwBytesInRes 68 view.setUint32(off + 12, dataOffset, true); // dwImageOffset 69 70 // Copy the image's bytes into the ICO buffer. 71 u8.set(images[i], dataOffset); 72 73 dataOffset += images[i].byteLength; 74 } 75 76 return buffer; 77 } 78 79 export class LinkHandlerParent extends JSWindowActorParent { 80 static addListenerForTests(listener) { 81 gTestListeners.add(listener); 82 } 83 84 static removeListenerForTests(listener) { 85 gTestListeners.delete(listener); 86 } 87 88 receiveMessage(aMsg) { 89 let browser = this.browsingContext.top.embedderElement; 90 if (!browser) { 91 return; 92 } 93 94 let win = browser.ownerGlobal; 95 96 let gBrowser = win.gBrowser; 97 98 switch (aMsg.name) { 99 case "Link:LoadingIcon": 100 if (!gBrowser) { 101 return; 102 } 103 104 if (!aMsg.data.isRichIcon) { 105 let tab = gBrowser.getTabForBrowser(browser); 106 if (tab.hasAttribute("busy")) { 107 tab.setAttribute("pendingicon", "true"); 108 } 109 } 110 111 this.notifyTestListeners("LoadingIcon", aMsg.data); 112 break; 113 114 case "Link:SetIcon": 115 if (!gBrowser) { 116 return; 117 } 118 119 this.setIconFromLink(gBrowser, browser, aMsg.data); 120 121 this.notifyTestListeners("SetIcon", aMsg.data); 122 break; 123 124 case "Link:SetFailedIcon": 125 if (!gBrowser) { 126 return; 127 } 128 129 if (!aMsg.data.isRichIcon) { 130 this.clearPendingIcon(gBrowser, browser); 131 } 132 133 this.notifyTestListeners("SetFailedIcon", aMsg.data); 134 break; 135 136 case "Link:AddSearch": { 137 if (!gBrowser) { 138 return; 139 } 140 141 let tab = gBrowser.getTabForBrowser(browser); 142 if (!tab) { 143 break; 144 } 145 146 lazy.OpenSearchManager.addEngine(browser, aMsg.data.engine); 147 break; 148 } 149 } 150 } 151 152 notifyTestListeners(name, data) { 153 for (let listener of gTestListeners) { 154 listener(name, data); 155 } 156 } 157 158 clearPendingIcon(gBrowser, aBrowser) { 159 let tab = gBrowser.getTabForBrowser(aBrowser); 160 tab.removeAttribute("pendingicon"); 161 } 162 163 async setIconFromLink( 164 gBrowser, 165 browser, 166 { 167 pageURL, 168 originalURL, 169 expiration, 170 iconURL, 171 images, 172 canStoreIcon, 173 beforePageShow, 174 isRichIcon, 175 } 176 ) { 177 let tab = gBrowser.getTabForBrowser(browser); 178 if (!tab) { 179 return; 180 } 181 182 if (images) { 183 let canvas = tab.ownerDocument.createElement("canvas"); 184 185 // We have multiple images, need to create an ICO file to collect them. 186 if (images.length > 1) { 187 // Convert all images to PNG bytes. 188 let blobs = []; 189 for (let image of images) { 190 await drawImageOnCanvas(canvas, image); 191 blobs.push(await new Promise(resolve => canvas.toBlob(resolve))); 192 } 193 let buffers = await Promise.all(blobs.map(blob => blob.bytes())); 194 195 // Create an ICO "file" containing all the PNGs. 196 let ico = createICO(buffers); 197 198 // Convert the ICO bytes to a data URL. 199 iconURL = await blobAsDataURL(new Blob([ico], { type: TYPE_ICO })); 200 } else { 201 await drawImageOnCanvas(canvas, images[0]); 202 iconURL = canvas.toDataURL(); 203 } 204 } 205 206 // The browser might have gone away during `await` above. 207 if (!gBrowser.getBrowserForTab(tab)) { 208 return; 209 } 210 211 if (!isRichIcon) { 212 this.clearPendingIcon(gBrowser, browser); 213 } 214 215 let iconURI; 216 try { 217 iconURI = Services.io.newURI(iconURL); 218 } catch (ex) { 219 console.error(ex); 220 return; 221 } 222 223 // The content process should send decoded images for all schemes except for trusted schemes and SVGs, which should not be rasterized. 224 if ( 225 !images && 226 !TRUSTED_FAVICON_SCHEMES.includes(iconURI.scheme) && 227 !iconURL.startsWith(SVG_DATA_URI_PREFIX) 228 ) { 229 console.error( 230 `Not allowed to set favicon "${iconURL}" with that scheme!` 231 ); 232 return; 233 } 234 235 if (!iconURI.schemeIs("data")) { 236 try { 237 Services.scriptSecurityManager.checkLoadURIWithPrincipal( 238 browser.contentPrincipal, 239 iconURI, 240 Services.scriptSecurityManager.ALLOW_CHROME 241 ); 242 } catch (ex) { 243 return; 244 } 245 } 246 if (canStoreIcon) { 247 try { 248 lazy.PlacesUtils.favicons 249 .setFaviconForPage( 250 Services.io.newURI(pageURL), 251 Services.io.newURI(originalURL), 252 iconURI, 253 expiration && lazy.PlacesUtils.toPRTime(expiration), 254 isRichIcon 255 ) 256 .catch(console.error); 257 } catch (ex) { 258 console.error(ex); 259 } 260 } 261 262 if (!isRichIcon) { 263 gBrowser.setIcon(tab, iconURL, originalURL, beforePageShow); 264 } 265 } 266 }