| OLD | NEW | 
|---|
| 1 /* | 1 /* | 
| 2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 2  * This file is part of Adblock Plus <https://adblockplus.org/>, | 
| 3  * Copyright (C) 2006-2016 Eyeo GmbH | 3  * Copyright (C) 2006-2016 Eyeo GmbH | 
| 4  * | 4  * | 
| 5  * Adblock Plus is free software: you can redistribute it and/or modify | 5  * Adblock Plus is free software: you can redistribute it and/or modify | 
| 6  * it under the terms of the GNU General Public License version 3 as | 6  * it under the terms of the GNU General Public License version 3 as | 
| 7  * published by the Free Software Foundation. | 7  * published by the Free Software Foundation. | 
| 8  * | 8  * | 
| 9  * Adblock Plus is distributed in the hope that it will be useful, | 9  * Adblock Plus is distributed in the hope that it will be useful, | 
| 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 50     sendMessage(message, responseCallback) | 50     sendMessage(message, responseCallback) | 
| 51     { | 51     { | 
| 52       chrome.tabs.sendMessage(this.id, message, responseCallback); | 52       chrome.tabs.sendMessage(this.id, message, responseCallback); | 
| 53     } | 53     } | 
| 54   }; | 54   }; | 
| 55 | 55 | 
| 56   ext.getPage = id => new Page({id: parseInt(id, 10)}); | 56   ext.getPage = id => new Page({id: parseInt(id, 10)}); | 
| 57 | 57 | 
| 58   function afterTabLoaded(callback) | 58   function afterTabLoaded(callback) | 
| 59   { | 59   { | 
| 60      return openedTab => | 60     return openedTab => | 
| 61      { | 61     { | 
| 62        let onUpdated = (tabId, changeInfo, tab) => | 62       let onUpdated = (tabId, changeInfo, tab) => | 
| 63        { | 63       { | 
| 64          if (tabId == openedTab.id && changeInfo.status == "complete") | 64         if (tabId == openedTab.id && changeInfo.status == "complete") | 
| 65          { | 65         { | 
| 66            chrome.tabs.onUpdated.removeListener(onUpdated); | 66           chrome.tabs.onUpdated.removeListener(onUpdated); | 
| 67            callback(new Page(openedTab)); | 67           callback(new Page(openedTab)); | 
| 68          } | 68         } | 
| 69        }; | 69       }; | 
| 70        chrome.tabs.onUpdated.addListener(onUpdated); | 70       chrome.tabs.onUpdated.addListener(onUpdated); | 
| 71      }; | 71     }; | 
| 72   } | 72   } | 
| 73 | 73 | 
| 74   ext.pages = { | 74   ext.pages = { | 
| 75     open(url, callback) | 75     open(url, callback) | 
| 76     { | 76     { | 
| 77       chrome.tabs.create({url: url}, callback && afterTabLoaded(callback)); | 77       chrome.tabs.create({url}, callback && afterTabLoaded(callback)); | 
| 78     }, | 78     }, | 
| 79     query(info, callback) | 79     query(info, callback) | 
| 80     { | 80     { | 
| 81       let rawInfo = {}; | 81       let rawInfo = {}; | 
| 82       for (let property in info) | 82       for (let property in info) | 
| 83       { | 83       { | 
| 84         switch (property) | 84         switch (property) | 
| 85         { | 85         { | 
| 86           case "active": | 86           case "active": | 
| 87           case "lastFocusedWindow": | 87           case "lastFocusedWindow": | 
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 124     let frame = createFrame(details.tabId, details.frameId); | 124     let frame = createFrame(details.tabId, details.frameId); | 
| 125     frame.parent = framesOfTabs[details.tabId][details.parentFrameId] || null; | 125     frame.parent = framesOfTabs[details.tabId][details.parentFrameId] || null; | 
| 126   }); | 126   }); | 
| 127 | 127 | 
| 128   let eagerlyUpdatedPages = new ext.PageMap(); | 128   let eagerlyUpdatedPages = new ext.PageMap(); | 
| 129 | 129 | 
| 130   ext._updatePageFrameStructure = (frameId, tabId, url, eager) => | 130   ext._updatePageFrameStructure = (frameId, tabId, url, eager) => | 
| 131   { | 131   { | 
| 132     if (frameId == 0) | 132     if (frameId == 0) | 
| 133     { | 133     { | 
| 134       let page = new Page({id: tabId, url: url}); | 134       let page = new Page({id: tabId, url}); | 
| 135 | 135 | 
| 136       if (eagerlyUpdatedPages.get(page) != url) | 136       if (eagerlyUpdatedPages.get(page) != url) | 
| 137       { | 137       { | 
| 138         ext._removeFromAllPageMaps(tabId); | 138         ext._removeFromAllPageMaps(tabId); | 
| 139 | 139 | 
| 140         // When a sitekey header is received we must immediately update the page | 140         // When a sitekey header is received we must immediately update the page | 
| 141         // structure in order to record and use the key. We want to avoid | 141         // structure in order to record and use the key. We want to avoid | 
| 142         // trashing the page structure if the onCommitted event is then fired | 142         // trashing the page structure if the onCommitted event is then fired | 
| 143         // for the page. | 143         // for the page. | 
| 144         if (eager) | 144         if (eager) | 
| 145           eagerlyUpdatedPages.set(page, url); | 145           eagerlyUpdatedPages.set(page, url); | 
| 146 | 146 | 
| 147         chrome.tabs.get(tabId, () => | 147         chrome.tabs.get(tabId, () => | 
| 148         { | 148         { | 
| 149           // If the tab is prerendered, chrome.tabs.get() sets | 149           // If the tab is prerendered, chrome.tabs.get() sets | 
| 150           // chrome.runtime.lastError and we have to dispatch the onLoading even
     t, | 150           // chrome.runtime.lastError and we have to dispatch the | 
| 151           // since the onUpdated event isn't dispatched for prerendered tabs. | 151           // onLoading event, since the onUpdated event isn't | 
| 152           // However, we have to keep relying on the unUpdated event for tabs th
     at | 152           // dispatched for prerendered tabs.  However, we have to | 
| 153           // are already visible. Otherwise browser action changes get overridde
     n | 153           // keep relying on the unUpdated event for tabs that are | 
| 154           // when Chrome automatically resets them on navigation. | 154           // already visible. Otherwise browser action changes get | 
|  | 155           // overridden when Chrome automatically resets them on | 
|  | 156           // navigation. | 
| 155           if (chrome.runtime.lastError) | 157           if (chrome.runtime.lastError) | 
| 156             ext.pages.onLoading._dispatch(page); | 158             ext.pages.onLoading._dispatch(page); | 
| 157         }); | 159         }); | 
| 158       } | 160       } | 
| 159     } | 161     } | 
| 160 | 162 | 
| 161     // Update frame URL in frame structure | 163     // Update frame URL in frame structure | 
| 162     let frame = createFrame(tabId, frameId); | 164     let frame = createFrame(tabId, frameId); | 
| 163     frame.url = new URL(url); | 165     frame.url = new URL(url); | 
| 164   }; | 166   }; | 
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 227         chrome.browserAction.setBadgeBackgroundColor({ | 229         chrome.browserAction.setBadgeBackgroundColor({ | 
| 228           tabId: this._tabId, | 230           tabId: this._tabId, | 
| 229           color: this._changes.badgeColor | 231           color: this._changes.badgeColor | 
| 230         }); | 232         }); | 
| 231       } | 233       } | 
| 232 | 234 | 
| 233       this._changes = null; | 235       this._changes = null; | 
| 234     }, | 236     }, | 
| 235     _queueChanges() | 237     _queueChanges() | 
| 236     { | 238     { | 
| 237       chrome.tabs.get(this._tabId, function() | 239       chrome.tabs.get(this._tabId, () => | 
| 238       { | 240       { | 
| 239         // If the tab is prerendered, chrome.tabs.get() sets | 241         // If the tab is prerendered, chrome.tabs.get() sets | 
| 240         // chrome.runtime.lastError and we have to delay our changes | 242         // chrome.runtime.lastError and we have to delay our changes | 
| 241         // until the currently visible tab is replaced with the | 243         // until the currently visible tab is replaced with the | 
| 242         // prerendered tab. Otherwise chrome.browserAction.set* fails. | 244         // prerendered tab. Otherwise chrome.browserAction.set* fails. | 
| 243         if (chrome.runtime.lastError) | 245         if (chrome.runtime.lastError) | 
| 244         { | 246         { | 
| 245           let onReplaced = (addedTabId, removedTabId) => | 247           let onReplaced = (addedTabId, removedTabId) => | 
| 246           { | 248           { | 
| 247             if (addedTabId == this._tabId) | 249             if (addedTabId == this._tabId) | 
| 248             { | 250             { | 
| 249               chrome.tabs.onReplaced.removeListener(onReplaced); | 251               chrome.tabs.onReplaced.removeListener(onReplaced); | 
| 250               this._applyChanges(); | 252               this._applyChanges(); | 
| 251             } | 253             } | 
| 252           }; | 254           }; | 
| 253           chrome.tabs.onReplaced.addListener(onReplaced); | 255           chrome.tabs.onReplaced.addListener(onReplaced); | 
| 254         } | 256         } | 
| 255         else | 257         else | 
| 256         { |  | 
| 257           this._applyChanges(); | 258           this._applyChanges(); | 
| 258         } | 259       }); | 
| 259       }.bind(this)); |  | 
| 260     }, | 260     }, | 
| 261     _addChange(name, value) | 261     _addChange(name, value) | 
| 262     { | 262     { | 
| 263       if (!this._changes) | 263       if (!this._changes) | 
| 264       { | 264       { | 
| 265         this._changes = {}; | 265         this._changes = {}; | 
| 266         this._queueChanges(); | 266         this._queueChanges(); | 
| 267       } | 267       } | 
| 268 | 268 | 
| 269       this._changes[name] = value; | 269       this._changes[name] = value; | 
| 270     }, | 270     }, | 
| 271     setIcon(path) | 271     setIcon(path) | 
| 272     { | 272     { | 
| 273       this._addChange("iconPath", path); | 273       this._addChange("iconPath", path); | 
| 274     }, | 274     }, | 
| 275     setBadge(badge) | 275     setBadge(badge) | 
| 276     { | 276     { | 
| 277       if (!badge) | 277       if (!badge) | 
| 278       { |  | 
| 279         this._addChange("badgeText", ""); | 278         this._addChange("badgeText", ""); | 
| 280       } |  | 
| 281       else | 279       else | 
| 282       { | 280       { | 
| 283         if ("number" in badge) | 281         if ("number" in badge) | 
| 284           this._addChange("badgeText", badge.number.toString()); | 282           this._addChange("badgeText", badge.number.toString()); | 
| 285 | 283 | 
| 286         if ("color" in badge) | 284         if ("color" in badge) | 
| 287           this._addChange("badgeColor", badge.color); | 285           this._addChange("badgeColor", badge.color); | 
| 288       } | 286       } | 
| 289     } | 287     } | 
| 290   }; | 288   }; | 
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 371 | 369 | 
| 372   /* Web requests */ | 370   /* Web requests */ | 
| 373 | 371 | 
| 374   let framesOfTabs = Object.create(null); | 372   let framesOfTabs = Object.create(null); | 
| 375 | 373 | 
| 376   ext.getFrame = (tabId, frameId) => | 374   ext.getFrame = (tabId, frameId) => | 
| 377   { | 375   { | 
| 378     return (framesOfTabs[tabId] || {})[frameId]; | 376     return (framesOfTabs[tabId] || {})[frameId]; | 
| 379   }; | 377   }; | 
| 380 | 378 | 
| 381   let handlerBehaviorChangedQuota = chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANG
     ED_CALLS_PER_10_MINUTES; | 379   let handlerBehaviorChangedQuota = | 
|  | 380     chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES; | 
| 382 | 381 | 
| 383   function propagateHandlerBehaviorChange() | 382   function propagateHandlerBehaviorChange() | 
| 384   { | 383   { | 
| 385     // Make sure to not call handlerBehaviorChanged() more often than allowed | 384     // Make sure to not call handlerBehaviorChanged() more often than allowed | 
| 386     // by chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES. | 385     // by chrome.webRequest.MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES. | 
| 387     // Otherwise Chrome notifies the user that this extension is causing issues. | 386     // Otherwise Chrome notifies the user that this extension is causing issues. | 
| 388     if (handlerBehaviorChangedQuota > 0) | 387     if (handlerBehaviorChangedQuota > 0) | 
| 389     { | 388     { | 
| 390       chrome.webNavigation.onBeforeNavigate.removeListener(propagateHandlerBehav
     iorChange); | 389       chrome.webNavigation.onBeforeNavigate.removeListener( | 
|  | 390         propagateHandlerBehaviorChange | 
|  | 391       ); | 
| 391       chrome.webRequest.handlerBehaviorChanged(); | 392       chrome.webRequest.handlerBehaviorChanged(); | 
| 392 | 393 | 
| 393       handlerBehaviorChangedQuota--; | 394       handlerBehaviorChangedQuota--; | 
| 394       setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); | 395       setTimeout(() => { handlerBehaviorChangedQuota++; }, 600000); | 
| 395     } | 396     } | 
| 396   } | 397   } | 
| 397 | 398 | 
| 398   ext.webRequest = { | 399   ext.webRequest = { | 
| 399     onBeforeRequest: new ext._EventTarget(), | 400     onBeforeRequest: new ext._EventTarget(), | 
| 400     handlerBehaviorChanged() | 401     handlerBehaviorChanged() | 
| 401     { | 402     { | 
| 402       // Defer handlerBehaviorChanged() until navigation occurs. | 403       // Defer handlerBehaviorChanged() until navigation occurs. | 
| 403       // There wouldn't be any visible effect when calling it earlier, | 404       // There wouldn't be any visible effect when calling it earlier, | 
| 404       // but it's an expensive operation and that way we avoid to call | 405       // but it's an expensive operation and that way we avoid to call | 
| 405       // it multiple times, if multiple filters are added/removed. | 406       // it multiple times, if multiple filters are added/removed. | 
| 406       let onBeforeNavigate = chrome.webNavigation.onBeforeNavigate; | 407       let {onBeforeNavigate} = chrome.webNavigation; | 
| 407       if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) | 408       if (!onBeforeNavigate.hasListener(propagateHandlerBehaviorChange)) | 
| 408         onBeforeNavigate.addListener(propagateHandlerBehaviorChange); | 409         onBeforeNavigate.addListener(propagateHandlerBehaviorChange); | 
| 409     } | 410     } | 
| 410   }; | 411   }; | 
| 411 | 412 | 
| 412   chrome.tabs.query({}, tabs => | 413   chrome.tabs.query({}, tabs => | 
| 413   { | 414   { | 
| 414     tabs.forEach(tab => | 415     tabs.forEach(tab => | 
| 415     { | 416     { | 
| 416       chrome.webNavigation.getAllFrames({tabId: tab.id}, details => | 417       chrome.webNavigation.getAllFrames({tabId: tab.id}, details => | 
| 417       { | 418       { | 
| 418         if (details && details.length > 0) | 419         if (details && details.length > 0) | 
| 419         { | 420         { | 
| 420           let frames = framesOfTabs[tab.id] = Object.create(null); | 421           let frames = framesOfTabs[tab.id] = Object.create(null); | 
| 421 | 422 | 
| 422           for (let i = 0; i < details.length; i++) | 423           for (let i = 0; i < details.length; i++) | 
| 423             frames[details[i].frameId] = {url: new URL(details[i].url), parent: 
     null}; | 424           { | 
|  | 425             frames[details[i].frameId] = { | 
|  | 426               url: new URL(details[i].url), | 
|  | 427               parent: null | 
|  | 428             }; | 
|  | 429           } | 
| 424 | 430 | 
| 425           for (let i = 0; i < details.length; i++) | 431           for (let i = 0; i < details.length; i++) | 
| 426           { | 432           { | 
| 427             let parentFrameId = details[i].parentFrameId; | 433             let {parentFrameId} = details[i]; | 
| 428 | 434 | 
| 429             if (parentFrameId != -1) | 435             if (parentFrameId != -1) | 
| 430               frames[details[i].frameId].parent = frames[parentFrameId]; | 436               frames[details[i].frameId].parent = frames[parentFrameId]; | 
| 431           } | 437           } | 
| 432         } | 438         } | 
| 433       }); | 439       }); | 
| 434     }); | 440     }); | 
| 435   }); | 441   }); | 
| 436 | 442 | 
| 437   chrome.webRequest.onBeforeRequest.addListener(details => | 443   chrome.webRequest.onBeforeRequest.addListener(details => | 
| 438   { | 444   { | 
| 439     // The high-level code isn't interested in requests that aren't | 445     // The high-level code isn't interested in requests that aren't | 
| 440     // related to a tab or requests loading a top-level document, | 446     // related to a tab or requests loading a top-level document, | 
| 441     // those should never be blocked. | 447     // those should never be blocked. | 
| 442     if (details.tabId == -1 || details.type == "main_frame") | 448     if (details.tabId == -1 || details.type == "main_frame") | 
| 443       return; | 449       return; | 
| 444 | 450 | 
| 445     // We are looking for the frame that contains the element which | 451     // We are looking for the frame that contains the element which | 
| 446     // has triggered this request. For most requests (e.g. images) we | 452     // has triggered this request. For most requests (e.g. images) we | 
| 447     // can just use the request's frame ID, but for subdocument requests | 453     // can just use the request's frame ID, but for subdocument requests | 
| 448     // (e.g. iframes) we must instead use the request's parent frame ID. | 454     // (e.g. iframes) we must instead use the request's parent frame ID. | 
| 449     let frameId; | 455     let {frameId, type} = details; | 
| 450     let requestType; | 456     if (type == "sub_frame") | 
| 451     if (details.type == "sub_frame") |  | 
| 452     { | 457     { | 
| 453       frameId = details.parentFrameId; | 458       frameId = details.parentFrameId; | 
| 454       requestType = "SUBDOCUMENT"; | 459       type = "SUBDOCUMENT"; | 
| 455     } |  | 
| 456     else |  | 
| 457     { |  | 
| 458       frameId = details.frameId; |  | 
| 459       requestType = details.type.toUpperCase(); |  | 
| 460     } | 460     } | 
| 461 | 461 | 
| 462     let frame = ext.getFrame(details.tabId, frameId); | 462     let frame = ext.getFrame(details.tabId, frameId); | 
| 463     if (frame) | 463     if (frame) | 
| 464     { | 464     { | 
| 465       let results = ext.webRequest.onBeforeRequest._dispatch( | 465       let results = ext.webRequest.onBeforeRequest._dispatch( | 
| 466         new URL(details.url), | 466         new URL(details.url), | 
| 467         requestType, | 467         type.toUpperCase(), | 
| 468         new Page({id: details.tabId}), | 468         new Page({id: details.tabId}), | 
| 469         frame | 469         frame | 
| 470       ); | 470       ); | 
| 471 | 471 | 
| 472       if (results.indexOf(false) != -1) | 472       if (results.indexOf(false) != -1) | 
| 473         return {cancel: true}; | 473         return {cancel: true}; | 
| 474     } | 474     } | 
| 475   }, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); | 475   }, {urls: ["http://*/*", "https://*/*"]}, ["blocking"]); | 
| 476 | 476 | 
| 477 | 477 | 
| (...skipping 19 matching lines...) Expand all  Loading... | 
| 497 | 497 | 
| 498           let frame = frames[rawSender.frameId]; | 498           let frame = frames[rawSender.frameId]; | 
| 499           if (frame) | 499           if (frame) | 
| 500             return frame.parent; | 500             return frame.parent; | 
| 501 | 501 | 
| 502           return frames[0]; | 502           return frames[0]; | 
| 503         } | 503         } | 
| 504       }; | 504       }; | 
| 505     } | 505     } | 
| 506 | 506 | 
| 507     return ext.onMessage._dispatch(message, sender, sendResponse).indexOf(true) 
     != -1; | 507     return ext.onMessage._dispatch( | 
|  | 508       message, sender, sendResponse | 
|  | 509     ).indexOf(true) != -1; | 
| 508   }); | 510   }); | 
| 509 | 511 | 
| 510 | 512 | 
| 511   /* Storage */ | 513   /* Storage */ | 
| 512 | 514 | 
| 513   ext.storage = { | 515   ext.storage = { | 
| 514     get(keys, callback) | 516     get(keys, callback) | 
| 515     { | 517     { | 
| 516       chrome.storage.local.get(keys, callback); | 518       chrome.storage.local.get(keys, callback); | 
| 517     }, | 519     }, | 
| (...skipping 10 matching lines...) Expand all  Loading... | 
| 528     onChanged: chrome.storage.onChanged | 530     onChanged: chrome.storage.onChanged | 
| 529   }; | 531   }; | 
| 530 | 532 | 
| 531   /* Options */ | 533   /* Options */ | 
| 532 | 534 | 
| 533   if ("openOptionsPage" in chrome.runtime) | 535   if ("openOptionsPage" in chrome.runtime) | 
| 534   { | 536   { | 
| 535     ext.showOptions = callback => | 537     ext.showOptions = callback => | 
| 536     { | 538     { | 
| 537       if (!callback) | 539       if (!callback) | 
| 538       { |  | 
| 539         chrome.runtime.openOptionsPage(); | 540         chrome.runtime.openOptionsPage(); | 
| 540       } |  | 
| 541       else | 541       else | 
| 542       { | 542       { | 
| 543         chrome.runtime.openOptionsPage(() => | 543         chrome.runtime.openOptionsPage(() => | 
| 544         { | 544         { | 
| 545           if (chrome.runtime.lastError) | 545           if (chrome.runtime.lastError) | 
| 546             return; | 546             return; | 
| 547 | 547 | 
| 548           chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | 548           chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => | 
| 549           { | 549           { | 
| 550             if (tabs.length > 0) | 550             if (tabs.length > 0) | 
| (...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after  Loading... | 
| 582           { | 582           { | 
| 583             let tab = tabs[0]; | 583             let tab = tabs[0]; | 
| 584 | 584 | 
| 585             chrome.windows.update(tab.windowId, {focused: true}); | 585             chrome.windows.update(tab.windowId, {focused: true}); | 
| 586             chrome.tabs.update(tab.id, {active: true}); | 586             chrome.tabs.update(tab.id, {active: true}); | 
| 587 | 587 | 
| 588             if (callback) | 588             if (callback) | 
| 589               callback(new Page(tab)); | 589               callback(new Page(tab)); | 
| 590           } | 590           } | 
| 591           else | 591           else | 
| 592           { |  | 
| 593             ext.pages.open(optionsUrl, callback); | 592             ext.pages.open(optionsUrl, callback); | 
| 594           } |  | 
| 595         }); | 593         }); | 
| 596       }); | 594       }); | 
| 597     }; | 595     }; | 
| 598   } | 596   } | 
| 599 | 597 | 
| 600   /* Windows */ | 598   /* Windows */ | 
| 601   ext.windows = { | 599   ext.windows = { | 
| 602     create(createData, callback) | 600     create(createData, callback) | 
| 603     { | 601     { | 
| 604       chrome.windows.create(createData, createdWindow => | 602       chrome.windows.create(createData, createdWindow => | 
| 605       { | 603       { | 
| 606         afterTabLoaded(callback)(createdWindow.tabs[0]); | 604         afterTabLoaded(callback)(createdWindow.tabs[0]); | 
| 607       }); | 605       }); | 
| 608     } | 606     } | 
| 609   }; | 607   }; | 
| 610 } | 608 } | 
| OLD | NEW | 
|---|