gecko/browser/components/preferences/aboutPermissions.js

835 lines
27 KiB
JavaScript

/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is about:permissions code.
*
* The Initial Developer of the Original Code is
* the Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Margaret Leibovic <margaret.leibovic@gmail.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
let Ci = Components.interfaces;
let Cc = Components.classes;
let Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/DownloadUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
let gFaviconService = Cc["@mozilla.org/browser/favicon-service;1"].
getService(Ci.nsIFaviconService);
let gPlacesDatabase = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsPIPlacesDatabase).
DBConnection.
clone(true);
let gSitesStmt = gPlacesDatabase.createAsyncStatement(
"SELECT get_unreversed_host(rev_host) AS host " +
"FROM moz_places " +
"WHERE rev_host > '.' " +
"AND visit_count > 0 " +
"GROUP BY rev_host " +
"ORDER BY MAX(frecency) DESC " +
"LIMIT :limit");
let gVisitStmt = gPlacesDatabase.createAsyncStatement(
"SELECT SUM(visit_count) AS count " +
"FROM moz_places " +
"WHERE rev_host = :rev_host");
/**
* Permission types that should be tested with testExactPermission, as opposed
* to testPermission. This is based on what consumers use to test these permissions.
*/
let TEST_EXACT_PERM_TYPES = ["geo"];
/**
* Site object represents a single site, uniquely identified by a host.
*/
function Site(host) {
this.host = host;
this.listitem = null;
this.httpURI = NetUtil.newURI("http://" + this.host);
this.httpsURI = NetUtil.newURI("https://" + this.host);
}
Site.prototype = {
/**
* Gets the favicon to use for the site. The callback only gets called if
* a favicon is found for either the http URI or the https URI.
*
* @param aCallback
* A callback function that takes a favicon image URL as a parameter.
*/
getFavicon: function Site_getFavicon(aCallback) {
function faviconDataCallback(aURI, aDataLen, aData, aMimeType) {
try {
aCallback(aURI.spec);
} catch (e) {
Cu.reportError("AboutPermissions: " + e);
}
}
// Try to find favicion for both URIs. Callback will only be called if a
// favicon URI is found, so this means we'll always prefer the https favicon.
gFaviconService.getFaviconURLForPage(this.httpURI, faviconDataCallback);
gFaviconService.getFaviconURLForPage(this.httpsURI, faviconDataCallback);
},
/**
* Gets the number of history visits for the site.
*
* @param aCallback
* A function that takes the visit count (a number) as a parameter.
*/
getVisitCount: function Site_getVisitCount(aCallback) {
let rev_host = this.host.split("").reverse().join("") + ".";
gVisitStmt.params.rev_host = rev_host;
gVisitStmt.executeAsync({
handleResult: function(aResults) {
let row = aResults.getNextRow();
let count = row.getResultByName("count") || 0;
try {
aCallback(count);
} catch (e) {
Cu.reportError("AboutPermissions: " + e);
}
},
handleError: function(aError) {
Cu.reportError("AboutPermissions: " + aError);
},
handleCompletion: function(aReason) {
}
});
},
/**
* Gets the permission value stored for a specified permission type.
*
* @param aType
* The permission type string stored in permission manager.
* e.g. "cookie", "geo", "indexedDB", "popup", "image"
* @param aResultObj
* An object that stores the permission value set for aType.
*
* @return A boolean indicating whether or not a permission is set.
*/
getPermission: function Site_getPermission(aType, aResultObj) {
// Password saving isn't a nsIPermissionManager permission type, so handle
// it seperately.
if (aType == "password") {
aResultObj.value = this.loginSavingEnabled ?
Ci.nsIPermissionManager.ALLOW_ACTION :
Ci.nsIPermissionManager.DENY_ACTION;
return true;
}
let permissionValue;
if (TEST_EXACT_PERM_TYPES.indexOf(aType) == -1) {
permissionValue = Services.perms.testPermission(this.httpURI, aType);
} else {
permissionValue = Services.perms.testExactPermission(this.httpURI, aType);
}
aResultObj.value = permissionValue;
return permissionValue != Ci.nsIPermissionManager.UNKNOWN_ACTION;
},
/**
* Sets a permission for the site given a permission type and value.
*
* @param aType
* The permission type string stored in permission manager.
* e.g. "cookie", "geo", "indexedDB", "popup", "image"
* @param aPerm
* The permission value to set for the permission type. This should
* be one of the constants defined in nsIPermissionManager.
*/
setPermission: function Site_setPermission(aType, aPerm) {
// Password saving isn't a nsIPermissionManager permission type, so handle
// it seperately.
if (aType == "password") {
this.loginSavingEnabled = aPerm == Ci.nsIPermissionManager.ALLOW_ACTION;
return;
}
// Using httpURI is kind of bogus, but the permission manager stores the
// permission for the host, so the right thing happens in the end.
Services.perms.add(this.httpURI, aType, aPerm);
},
/**
* Clears a user-set permission value for the site given a permission type.
*
* @param aType
* The permission type string stored in permission manager.
* e.g. "cookie", "geo", "indexedDB", "popup", "image"
*/
clearPermission: function Site_clearPermission(aType) {
Services.perms.remove(this.host, aType);
},
/**
* Gets cookies stored for the site. This does not return cookies stored
* for the base domain, only the exact hostname stored for the site.
*
* @return An array of the cookies set for the site.
*/
get cookies() {
let cookies = [];
let enumerator = Services.cookies.getCookiesFromHost(this.host);
while (enumerator.hasMoreElements()) {
let cookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
// getCookiesFromHost returns cookies for base domain, but we only want
// the cookies for the exact domain.
if (cookie.rawHost == this.host) {
cookies.push(cookie);
}
}
return cookies;
},
/**
* Removes a set of specific cookies from the browser.
*/
clearCookies: function Site_clearCookies() {
this.cookies.forEach(function(aCookie) {
Services.cookies.remove(aCookie.host, aCookie.name, aCookie.path, false);
});
},
/**
* Gets logins stored for the site.
*
* @return An array of the logins stored for the site.
*/
get logins() {
// There could be more logins for different schemes/ports, but this covers
// the vast majority of cases.
let httpLogins = Services.logins.findLogins({}, this.httpURI.prePath, "", null);
let httpsLogins = Services.logins.findLogins({}, this.httpsURI.prePath, "", null);
return httpLogins.concat(httpsLogins);
},
get loginSavingEnabled() {
// Only say that login saving is blocked if it is blocked for both http and https.
return Services.logins.getLoginSavingEnabled(this.httpURI.prePath) &&
Services.logins.getLoginSavingEnabled(this.httpsURI.prePath);
},
set loginSavingEnabled(isEnabled) {
Services.logins.setLoginSavingEnabled(this.httpURI.prePath, isEnabled);
Services.logins.setLoginSavingEnabled(this.httpsURI.prePath, isEnabled);
},
/**
* Removes all data from the browser corresponding to the site.
*/
forgetSite: function Site_forgetSite() {
let pb = Cc["@mozilla.org/privatebrowsing;1"].
getService(Ci.nsIPrivateBrowsingService);
pb.removeDataFromDomain(this.host);
}
}
/**
* PermissionDefaults object keeps track of default permissions for sites based
* on global preferences.
*
* Inspired by pageinfo/permissions.js
*/
let PermissionDefaults = {
UNKNOWN: Ci.nsIPermissionManager.UNKNOWN_ACTION, // 0
ALLOW: Ci.nsIPermissionManager.ALLOW_ACTION, // 1
DENY: Ci.nsIPermissionManager.DENY_ACTION, // 2
SESSION: Ci.nsICookiePermission.ACCESS_SESSION, // 8
get password() {
if (Services.prefs.getBoolPref("signon.rememberSignons")) {
return this.ALLOW;
}
return this.DENY;
},
set password(aValue) {
let value = (aValue != this.DENY);
Services.prefs.setBoolPref("signon.rememberSignons", value);
},
// For use with network.cookie.* prefs.
COOKIE_ACCEPT: 0,
COOKIE_DENY: 2,
COOKIE_NORMAL: 0,
COOKIE_SESSION: 2,
get cookie() {
if (Services.prefs.getIntPref("network.cookie.cookieBehavior") == this.COOKIE_DENY) {
return this.DENY;
}
if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == this.COOKIE_SESSION) {
return this.SESSION;
}
return this.ALLOW;
},
set cookie(aValue) {
let value = (aValue == this.DENY) ? this.COOKIE_DENY : this.COOKIE_ACCEPT;
Services.prefs.setIntPref("network.cookie.cookieBehavior", value);
let lifetimeValue = aValue == this.SESSION ? this.COOKIE_SESSION :
this.COOKIE_NORMAL;
Services.prefs.setIntPref("network.cookie.lifetimePolicy", lifetimeValue);
},
get geo() {
if (!Services.prefs.getBoolPref("geo.enabled")) {
return this.DENY;
}
// We always ask for permission to share location with a specific site, so
// there is no global ALLOW.
return this.UNKNOWN;
},
set geo(aValue) {
let value = (aValue != this.DENY);
Services.prefs.setBoolPref("geo.enabled", value);
},
get indexedDB() {
if (!Services.prefs.getBoolPref("dom.indexedDB.enabled")) {
return this.DENY;
}
// We always ask for permission to enable indexedDB storage for a specific
// site, so there is no global ALLOW.
return this.UNKNOWN;
},
set indexedDB(aValue) {
let value = (aValue != this.DENY);
Services.prefs.setBoolPref("dom.indexedDB.enabled", value);
},
get popup() {
if (Services.prefs.getBoolPref("dom.disable_open_during_load")) {
return this.DENY;
}
return this.ALLOW;
},
set popup(aValue) {
let value = (aValue == this.DENY);
Services.prefs.setBoolPref("dom.disable_open_during_load", value);
}
}
/**
* AboutPermissions manages the about:permissions page.
*/
let AboutPermissions = {
/**
* Number of sites to return from the places database.
*/
PLACES_SITES_LIMIT: 50,
/**
* Stores a mapping of host strings to Site objects.
*/
_sites: {},
sitesList: null,
_selectedSite: null,
/**
* This reflects the permissions that we expose in the UI. These correspond
* to permission type strings in the permission manager, PermissionDefaults,
* and element ids in aboutPermissions.xul.
*
* Potential future additions: "sts/use", "sts/subd"
*/
_supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup"],
/**
* Permissions that don't have a global "Allow" option.
*/
_noGlobalAllow: ["geo", "indexedDB"],
_stringBundle: Services.strings.
createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
/**
* Called on page load.
*/
init: function() {
this.sitesList = document.getElementById("sites-list");
this.getSitesFromPlaces();
this.enumerateServices();
// Attach observers in case data changes while the page is open.
Services.prefs.addObserver("signon.rememberSignons", this, false);
Services.prefs.addObserver("network.cookie.", this, false);
Services.prefs.addObserver("geo.enabled", this, false);
Services.prefs.addObserver("dom.indexedDB.enabled", this, false);
Services.prefs.addObserver("dom.disable_open_during_load", this, false);
Services.obs.addObserver(this, "perm-changed", false);
Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
Services.obs.addObserver(this, "cookie-changed", false);
Services.obs.addObserver(this, "browser:purge-domain-data", false);
this._observersInitialized = true;
},
/**
* Called on page unload.
*/
cleanUp: function() {
if (this._observersInitialized) {
Services.prefs.removeObserver("signon.rememberSignons", this, false);
Services.prefs.removeObserver("network.cookie.", this, false);
Services.prefs.removeObserver("geo.enabled", this, false);
Services.prefs.removeObserver("dom.indexedDB.enabled", this, false);
Services.prefs.removeObserver("dom.disable_open_during_load", this, false);
Services.obs.removeObserver(this, "perm-changed", false);
Services.obs.removeObserver(this, "passwordmgr-storage-changed", false);
Services.obs.removeObserver(this, "cookie-changed", false);
Services.obs.removeObserver(this, "browser:purge-domain-data", false);
}
gSitesStmt.finalize();
gVisitStmt.finalize();
gPlacesDatabase.asyncClose(null);
},
observe: function (aSubject, aTopic, aData) {
switch(aTopic) {
case "perm-changed":
// Permissions changes only affect individual sites.
if (!this._selectedSite) {
break;
}
// aSubject is null when nsIPermisionManager::removeAll() is called.
if (!aSubject) {
this._supportedPermissions.forEach(function(aType){
this.updatePermission(aType);
}, this);
break;
}
let permission = aSubject.QueryInterface(Ci.nsIPermission);
// We can't compare selectedSite.host and permission.host here because
// we need to handle the case where a parent domain was changed in a
// way that affects the subdomain.
if (this._supportedPermissions.indexOf(permission.type) != -1) {
this.updatePermission(permission.type);
}
break;
case "nsPref:changed":
this._supportedPermissions.forEach(function(aType){
this.updatePermission(aType);
}, this);
break;
case "passwordmgr-storage-changed":
this.updatePermission("password");
if (this._selectedSite) {
this.updatePasswordsCount();
}
break;
case "cookie-changed":
if (this._selectedSite) {
this.updateCookiesCount();
}
break;
case "browser:purge-domain-data":
this.deleteFromSitesList(aData);
break;
}
},
/**
* Creates Site objects for the top-frecency sites in the places database and stores
* them in _sites. The number of sites created is controlled by PLACES_SITES_LIMIT.
*/
getSitesFromPlaces: function() {
gSitesStmt.params.limit = this.PLACES_SITES_LIMIT;
gSitesStmt.executeAsync({
handleResult: function(aResults) {
AboutPermissions.startSitesListBatch();
let row;
while (row = aResults.getNextRow()) {
let host = row.getResultByName("host");
AboutPermissions.addHost(host);
}
AboutPermissions.endSitesListBatch();
},
handleError: function(aError) {
Cu.reportError("AboutPermissions: " + aError);
},
handleCompletion: function(aReason) {
// Notify oberservers for testing purposes.
Services.obs.notifyObservers(null, "browser-permissions-initialized", null);
}
});
},
/**
* Finds sites that have non-default permissions and creates Site objects for
* them if they are not already stored in _sites.
*/
enumerateServices: function() {
this.startSitesListBatch();
let logins = Services.logins.getAllLogins();
logins.forEach(function(aLogin) {
try {
// aLogin.hostname is a string in origin URL format (e.g. "http://foo.com")
let uri = NetUtil.newURI(aLogin.hostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
}
}, this);
let disabledHosts = Services.logins.getAllDisabledHosts();
disabledHosts.forEach(function(aHostname) {
try {
// aHostname is a string in origin URL format (e.g. "http://foo.com")
let uri = NetUtil.newURI(aHostname);
this.addHost(uri.host);
} catch (e) {
// newURI will throw for add-ons logins stored in chrome:// URIs
}
}, this);
let (enumerator = Services.perms.enumerator) {
while (enumerator.hasMoreElements()) {
let permission = enumerator.getNext().QueryInterface(Ci.nsIPermission);
// Only include sites with exceptions set for supported permission types.
if (this._supportedPermissions.indexOf(permission.type) != -1) {
this.addHost(permission.host);
}
}
}
this.endSitesListBatch();
},
/**
* Creates a new Site and adds it to _sites if it's not already there.
*
* @param aHost
* A host string.
*/
addHost: function(aHost) {
if (aHost in this._sites) {
return;
}
let site = new Site(aHost);
this._sites[aHost] = site;
this.addToSitesList(site);
},
/**
* Populates sites-list richlistbox with data from Site object.
*
* @param aSite
* A Site object.
*/
addToSitesList: function(aSite) {
let item = document.createElement("richlistitem");
item.setAttribute("class", "site");
item.setAttribute("value", aSite.host);
aSite.getFavicon(function(aURL) {
item.setAttribute("favicon", aURL);
});
aSite.listitem = item;
(this._listFragment || this.sitesList).appendChild(item);
},
startSitesListBatch: function () {
if (!this._listFragment)
this._listFragment = document.createDocumentFragment();
},
endSitesListBatch: function () {
if (this._listFragment) {
this.sitesList.appendChild(this._listFragment);
this._listFragment = null;
}
},
/**
* Hides sites in richlistbox based on search text in sites-filter textbox.
*/
filterSitesList: function() {
let siteItems = this.sitesList.children;
let filterValue = document.getElementById("sites-filter").value.toLowerCase();
if (filterValue == "") {
for (let i = 0; i < siteItems.length; i++) {
siteItems[i].collapsed = false;
}
return;
}
for (let i = 0; i < siteItems.length; i++) {
let siteValue = siteItems[i].value.toLowerCase();
siteItems[i].collapsed = siteValue.indexOf(filterValue) == -1;
}
},
/**
* Removes all evidence of the selected site. The "forget this site" observer
* will call deleteFromSitesList to update the UI.
*/
forgetSite: function() {
this._selectedSite.forgetSite();
},
/**
* Deletes sites for a host and all of its sub-domains. Removes these sites
* from _sites and removes their corresponding elements from the DOM.
*
* @param aHost
* The host string corresponding to the site to delete.
*/
deleteFromSitesList: function(aHost) {
for each (let site in this._sites) {
if (site.host.hasRootDomain(aHost)) {
if (site == this._selectedSite) {
// Replace site-specific interface with "All Sites" interface.
this.sitesList.selectedItem = document.getElementById("all-sites-item");
}
this.sitesList.removeChild(site.listitem);
delete this._sites[site.host];
}
}
},
/**
* Shows interface for managing site-specific permissions.
*/
onSitesListSelect: function(event) {
if (event.target.selectedItem.id == "all-sites-item") {
// Clear the header label value from the previously selected site.
document.getElementById("site-label").value = "";
this.manageDefaultPermissions();
return;
}
let host = event.target.value;
let site = this._selectedSite = this._sites[host];
document.getElementById("site-label").value = host;
document.getElementById("header-deck").selectedPanel =
document.getElementById("site-header");
this.updateVisitCount();
this.updatePermissionsBox();
},
/**
* Shows interface for managing default permissions. This corresponds to
* the "All Sites" list item.
*/
manageDefaultPermissions: function() {
this._selectedSite = null;
document.getElementById("header-deck").selectedPanel =
document.getElementById("defaults-header");
this.updatePermissionsBox();
},
/**
* Updates permissions interface based on selected site.
*/
updatePermissionsBox: function() {
this._supportedPermissions.forEach(function(aType){
this.updatePermission(aType);
}, this);
this.updatePasswordsCount();
this.updateCookiesCount();
},
/**
* Sets menulist for a given permission to the correct state, based on the
* stored permission.
*
* @param aType
* The permission type string stored in permission manager.
* e.g. "cookie", "geo", "indexedDB", "popup", "image"
*/
updatePermission: function(aType) {
let allowItem = document.getElementById(aType + "-" + PermissionDefaults.ALLOW);
allowItem.hidden = !this._selectedSite &&
this._noGlobalAllow.indexOf(aType) != -1;
let permissionMenulist = document.getElementById(aType + "-menulist");
let permissionValue;
if (!this._selectedSite) {
// If there is no selected site, we are updating the default permissions interface.
permissionValue = PermissionDefaults[aType];
} else {
let result = {};
permissionValue = this._selectedSite.getPermission(aType, result) ?
result.value : PermissionDefaults[aType];
}
permissionMenulist.selectedItem = document.getElementById(aType + "-" + permissionValue);
},
onPermissionCommand: function(event) {
let permissionType = event.currentTarget.getAttribute("type");
let permissionValue = event.target.value;
if (!this._selectedSite) {
// If there is no selected site, we are setting the default permission.
PermissionDefaults[permissionType] = permissionValue;
} else {
this._selectedSite.setPermission(permissionType, permissionValue);
}
},
updateVisitCount: function() {
this._selectedSite.getVisitCount(function(aCount) {
let visitForm = AboutPermissions._stringBundle.GetStringFromName("visitCount");
let visitLabel = PluralForm.get(aCount, visitForm)
.replace("#1", aCount);
document.getElementById("site-visit-count").value = visitLabel;
});
},
updatePasswordsCount: function() {
if (!this._selectedSite) {
document.getElementById("passwords-count").hidden = true;
document.getElementById("passwords-manage-all-button").hidden = false;
return;
}
let passwordsCount = this._selectedSite.logins.length;
let passwordsForm = this._stringBundle.GetStringFromName("passwordsCount");
let passwordsLabel = PluralForm.get(passwordsCount, passwordsForm)
.replace("#1", passwordsCount);
document.getElementById("passwords-label").value = passwordsLabel;
document.getElementById("passwords-manage-button").disabled = (passwordsCount < 1);
document.getElementById("passwords-manage-all-button").hidden = true;
document.getElementById("passwords-count").hidden = false;
},
/**
* Opens password manager dialog.
*/
managePasswords: function() {
let selectedHost = "";
if (this._selectedSite) {
selectedHost = this._selectedSite.host;
}
let win = Services.wm.getMostRecentWindow("Toolkit:PasswordManager");
if (win) {
win.setFilter(selectedHost);
win.focus();
} else {
window.openDialog("chrome://passwordmgr/content/passwordManager.xul",
"Toolkit:PasswordManager", "", {filterString : selectedHost});
}
},
updateCookiesCount: function() {
if (!this._selectedSite) {
document.getElementById("cookies-count").hidden = true;
document.getElementById("cookies-clear-all-button").hidden = false;
document.getElementById("cookies-manage-all-button").hidden = false;
return;
}
let cookiesCount = this._selectedSite.cookies.length;
let cookiesForm = this._stringBundle.GetStringFromName("cookiesCount");
let cookiesLabel = PluralForm.get(cookiesCount, cookiesForm)
.replace("#1", cookiesCount);
document.getElementById("cookies-label").value = cookiesLabel;
document.getElementById("cookies-clear-button").disabled = (cookiesCount < 1);
document.getElementById("cookies-manage-button").disabled = (cookiesCount < 1);
document.getElementById("cookies-clear-all-button").hidden = true;
document.getElementById("cookies-manage-all-button").hidden = true;
document.getElementById("cookies-count").hidden = false;
},
/**
* Clears cookies for the selected site.
*/
clearCookies: function() {
if (!this._selectedSite) {
return;
}
let site = this._selectedSite;
site.clearCookies(site.cookies);
this.updateCookiesCount();
},
/**
* Opens cookie manager dialog.
*/
manageCookies: function() {
let selectedHost = "";
if (this._selectedSite) {
selectedHost = this._selectedSite.host;
}
let win = Services.wm.getMostRecentWindow("Browser:Cookies");
if (win) {
win.gCookiesWindow.setFilter(selectedHost);
win.focus();
} else {
window.openDialog("chrome://browser/content/preferences/cookies.xul",
"Browser:Cookies", "", {filterString : selectedHost});
}
}
}
// See nsPrivateBrowsingService.js
String.prototype.hasRootDomain = function hasRootDomain(aDomain) {
let index = this.indexOf(aDomain);
if (index == -1)
return false;
if (this == aDomain)
return true;
let prevChar = this[index - 1];
return (index == (this.length - aDomain.length)) &&
(prevChar == "." || prevChar == "/");
}