|
|
|
/*******************************************************************************
|
|
|
|
|
|
|
|
µMatrix - a Chromium browser extension to black/white list requests.
|
|
|
|
Copyright (C) 2014 Raymond Hill
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
|
|
|
|
Home: https://github.com/gorhill/uMatrix
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* global objectAssign, punycode, publicSuffixList */
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.getBytesInUse = function() {
|
|
|
|
var µm = this;
|
|
|
|
var getBytesInUseHandler = function(bytesInUse) {
|
|
|
|
µm.storageUsed = bytesInUse;
|
|
|
|
};
|
|
|
|
// Not all WebExtension implementations support getBytesInUse().
|
|
|
|
if ( typeof vAPI.storage.getBytesInUse === 'function' ) {
|
|
|
|
vAPI.storage.getBytesInUse(null, getBytesInUseHandler);
|
|
|
|
} else {
|
|
|
|
µm.storageUsed = undefined;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.saveUserSettings = function() {
|
|
|
|
this.XAL.keyvalSetMany(
|
|
|
|
this.userSettings,
|
|
|
|
this.getBytesInUse.bind(this)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadUserSettings = function(callback) {
|
|
|
|
var µm = this;
|
|
|
|
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
var settingsLoaded = function(store) {
|
|
|
|
// console.log('storage.js > loaded user settings');
|
|
|
|
|
|
|
|
µm.userSettings = store;
|
|
|
|
|
|
|
|
callback(µm.userSettings);
|
|
|
|
};
|
|
|
|
|
|
|
|
vAPI.storage.get(this.userSettings, settingsLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
// save white/blacklist
|
|
|
|
µMatrix.saveMatrix = function() {
|
|
|
|
µMatrix.XAL.keyvalSetOne('userMatrix', this.pMatrix.toString());
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadMatrix = function(callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
var µm = this;
|
|
|
|
var onLoaded = function(bin) {
|
|
|
|
if ( bin.hasOwnProperty('userMatrix') ) {
|
|
|
|
µm.pMatrix.fromString(bin.userMatrix);
|
|
|
|
µm.tMatrix.assign(µm.pMatrix);
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
this.XAL.keyvalGetOne('userMatrix', onLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.listKeysFromCustomHostsFiles = function(raw) {
|
|
|
|
var out = new Set(),
|
|
|
|
reIgnore = /^[!#]/,
|
|
|
|
reValid = /^[a-z-]+:\/\/\S+/,
|
|
|
|
lineIter = new this.LineIterator(raw),
|
|
|
|
location;
|
|
|
|
while ( lineIter.eot() === false ) {
|
|
|
|
location = lineIter.next().trim();
|
|
|
|
if ( reIgnore.test(location) || !reValid.test(location) ) { continue; }
|
|
|
|
out.add(location);
|
|
|
|
}
|
|
|
|
return this.setToArray(out);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.getAvailableHostsFiles = function(callback) {
|
|
|
|
var µm = this,
|
|
|
|
availableHostsFiles = {};
|
|
|
|
|
|
|
|
// Custom filter lists.
|
|
|
|
var importedListKeys = this.listKeysFromCustomHostsFiles(µm.userSettings.externalHostsFiles),
|
|
|
|
i = importedListKeys.length,
|
|
|
|
listKey, entry;
|
|
|
|
while ( i-- ) {
|
|
|
|
listKey = importedListKeys[i];
|
|
|
|
entry = {
|
|
|
|
content: 'filters',
|
|
|
|
contentURL: listKey,
|
|
|
|
external: true,
|
|
|
|
submitter: 'user',
|
|
|
|
title: listKey
|
|
|
|
};
|
|
|
|
availableHostsFiles[listKey] = entry;
|
|
|
|
this.assets.registerAssetSource(listKey, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
// selected lists
|
|
|
|
var onSelectedHostsFilesLoaded = function(bin) {
|
|
|
|
// Now get user's selection of lists
|
|
|
|
for ( var assetKey in bin.liveHostsFiles ) {
|
|
|
|
var availableEntry = availableHostsFiles[assetKey];
|
|
|
|
if ( availableEntry === undefined ) { continue; }
|
|
|
|
var liveEntry = bin.liveHostsFiles[assetKey];
|
|
|
|
availableEntry.off = liveEntry.off || false;
|
|
|
|
if ( liveEntry.entryCount !== undefined ) {
|
|
|
|
availableEntry.entryCount = liveEntry.entryCount;
|
|
|
|
}
|
|
|
|
if ( liveEntry.entryUsedCount !== undefined ) {
|
|
|
|
availableEntry.entryUsedCount = liveEntry.entryUsedCount;
|
|
|
|
}
|
|
|
|
// This may happen if the list name was pulled from the list content
|
|
|
|
if ( availableEntry.title === '' && liveEntry.title !== undefined ) {
|
|
|
|
availableEntry.title = liveEntry.title;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove unreferenced imported filter lists.
|
|
|
|
var dict = new Set(importedListKeys);
|
|
|
|
for ( assetKey in availableHostsFiles ) {
|
|
|
|
var entry = availableHostsFiles[assetKey];
|
|
|
|
if ( entry.submitter !== 'user' ) { continue; }
|
|
|
|
if ( dict.has(assetKey) ) { continue; }
|
|
|
|
delete availableHostsFiles[assetKey];
|
|
|
|
µm.assets.unregisterAssetSource(assetKey);
|
|
|
|
µm.assets.remove(assetKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(availableHostsFiles);
|
|
|
|
};
|
|
|
|
|
|
|
|
// built-in lists
|
|
|
|
var onBuiltinHostsFilesLoaded = function(entries) {
|
|
|
|
for ( var assetKey in entries ) {
|
|
|
|
if ( entries.hasOwnProperty(assetKey) === false ) { continue; }
|
|
|
|
entry = entries[assetKey];
|
|
|
|
if ( entry.content !== 'filters' ) { continue; }
|
|
|
|
availableHostsFiles[assetKey] = objectAssign({}, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now get user's selection of lists
|
|
|
|
vAPI.storage.get(
|
|
|
|
{ 'liveHostsFiles': availableHostsFiles },
|
|
|
|
onSelectedHostsFilesLoaded
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
this.assets.metadata(onBuiltinHostsFilesLoaded);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadHostsFiles = function(callback) {
|
|
|
|
var µm = µMatrix;
|
|
|
|
var hostsFileLoadCount;
|
|
|
|
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
var loadHostsFilesEnd = function() {
|
|
|
|
µm.ubiquitousBlacklist.freeze();
|
|
|
|
vAPI.storage.set({ 'liveHostsFiles': µm.liveHostsFiles });
|
|
|
|
vAPI.messaging.broadcast({ what: 'loadHostsFilesCompleted' });
|
|
|
|
µm.getBytesInUse();
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
var mergeHostsFile = function(details) {
|
|
|
|
µm.mergeHostsFile(details);
|
|
|
|
hostsFileLoadCount -= 1;
|
|
|
|
if ( hostsFileLoadCount === 0 ) {
|
|
|
|
loadHostsFilesEnd();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var loadHostsFilesStart = function(hostsFiles) {
|
|
|
|
µm.liveHostsFiles = hostsFiles;
|
|
|
|
µm.ubiquitousBlacklist.reset();
|
|
|
|
var locations = Object.keys(hostsFiles);
|
|
|
|
hostsFileLoadCount = locations.length;
|
|
|
|
|
|
|
|
// Load all hosts file which are not disabled.
|
|
|
|
var location;
|
|
|
|
while ( (location = locations.pop()) ) {
|
|
|
|
if ( hostsFiles[location].off ) {
|
|
|
|
hostsFileLoadCount -= 1;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
µm.assets.get(location, mergeHostsFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://github.com/gorhill/uMatrix/issues/2
|
|
|
|
if ( hostsFileLoadCount === 0 ) {
|
|
|
|
loadHostsFilesEnd();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.getAvailableHostsFiles(loadHostsFilesStart);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.mergeHostsFile = function(details) {
|
|
|
|
var usedCount = this.ubiquitousBlacklist.count;
|
|
|
|
var duplicateCount = this.ubiquitousBlacklist.duplicateCount;
|
|
|
|
|
|
|
|
this.mergeHostsFileContent(details.content);
|
|
|
|
|
|
|
|
usedCount = this.ubiquitousBlacklist.count - usedCount;
|
|
|
|
duplicateCount = this.ubiquitousBlacklist.duplicateCount - duplicateCount;
|
|
|
|
|
|
|
|
var hostsFilesMeta = this.liveHostsFiles[details.assetKey];
|
|
|
|
hostsFilesMeta.entryCount = usedCount + duplicateCount;
|
|
|
|
hostsFilesMeta.entryUsedCount = usedCount;
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.mergeHostsFileContent = function(rawText) {
|
|
|
|
var rawEnd = rawText.length;
|
|
|
|
var ubiquitousBlacklist = this.ubiquitousBlacklist;
|
|
|
|
var reLocalhost = /(^|\s)(localhost\.localdomain|localhost|local|broadcasthost|0\.0\.0\.0|127\.0\.0\.1|::1|fe80::1%lo0)(?=\s|$)/g;
|
|
|
|
var reAsciiSegment = /^[\x21-\x7e]+$/;
|
|
|
|
var matches;
|
|
|
|
var lineBeg = 0, lineEnd;
|
|
|
|
var line;
|
|
|
|
|
|
|
|
while ( lineBeg < rawEnd ) {
|
|
|
|
lineEnd = rawText.indexOf('\n', lineBeg);
|
|
|
|
if ( lineEnd < 0 ) {
|
|
|
|
lineEnd = rawText.indexOf('\r', lineBeg);
|
|
|
|
if ( lineEnd < 0 ) {
|
|
|
|
lineEnd = rawEnd;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// rhill 2014-04-18: The trim is important here, as without it there
|
|
|
|
// could be a lingering `\r` which would cause problems in the
|
|
|
|
// following parsing code.
|
|
|
|
line = rawText.slice(lineBeg, lineEnd).trim();
|
|
|
|
lineBeg = lineEnd + 1;
|
|
|
|
|
|
|
|
// https://github.com/gorhill/httpswitchboard/issues/15
|
|
|
|
// Ensure localhost et al. don't end up in the ubiquitous blacklist.
|
|
|
|
line = line
|
|
|
|
.replace(/#.*$/, '')
|
|
|
|
.toLowerCase()
|
|
|
|
.replace(reLocalhost, '')
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
// The filter is whatever sequence of printable ascii character without
|
|
|
|
// whitespaces
|
|
|
|
matches = reAsciiSegment.exec(line);
|
|
|
|
if ( !matches || matches.length === 0 ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bypass anomalies
|
|
|
|
// For example, when a filter contains whitespace characters, or
|
|
|
|
// whatever else outside the range of printable ascii characters.
|
|
|
|
if ( matches[0] !== line ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
line = matches[0];
|
|
|
|
if ( line === '' ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
ubiquitousBlacklist.add(line);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
// `switches` contains the filter lists for which the switch must be revisited.
|
|
|
|
|
|
|
|
µMatrix.selectHostsFiles = function(details, callback) {
|
|
|
|
var µm = this,
|
|
|
|
externalHostsFiles = this.userSettings.externalHostsFiles,
|
|
|
|
i, n, assetKey;
|
|
|
|
|
|
|
|
// Hosts file to select
|
|
|
|
if ( Array.isArray(details.toSelect) ) {
|
|
|
|
for ( assetKey in this.liveHostsFiles ) {
|
|
|
|
if ( this.liveHostsFiles.hasOwnProperty(assetKey) === false ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( details.toSelect.indexOf(assetKey) !== -1 ) {
|
|
|
|
this.liveHostsFiles[assetKey].off = false;
|
|
|
|
} else if ( details.merge !== true ) {
|
|
|
|
this.liveHostsFiles[assetKey].off = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Imported hosts files to remove
|
|
|
|
if ( Array.isArray(details.toRemove) ) {
|
|
|
|
var removeURLFromHaystack = function(haystack, needle) {
|
|
|
|
return haystack.replace(
|
|
|
|
new RegExp(
|
|
|
|
'(^|\\n)' +
|
|
|
|
needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
|
|
'(\\n|$)', 'g'),
|
|
|
|
'\n'
|
|
|
|
).trim();
|
|
|
|
};
|
|
|
|
for ( i = 0, n = details.toRemove.length; i < n; i++ ) {
|
|
|
|
assetKey = details.toRemove[i];
|
|
|
|
delete this.liveHostsFiles[assetKey];
|
|
|
|
externalHostsFiles = removeURLFromHaystack(externalHostsFiles, assetKey);
|
|
|
|
this.assets.remove(assetKey);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hosts file to import
|
|
|
|
if ( typeof details.toImport === 'string' ) {
|
|
|
|
// https://github.com/gorhill/uBlock/issues/1181
|
|
|
|
// Try mapping the URL of an imported filter list to the assetKey of an
|
|
|
|
// existing stock list.
|
|
|
|
var assetKeyFromURL = function(url) {
|
|
|
|
var needle = url.replace(/^https?:/, '');
|
|
|
|
var assets = µm.liveHostsFiles, asset;
|
|
|
|
for ( var assetKey in assets ) {
|
|
|
|
asset = assets[assetKey];
|
|
|
|
if ( asset.content !== 'filters' ) { continue; }
|
|
|
|
if ( typeof asset.contentURL === 'string' ) {
|
|
|
|
if ( asset.contentURL.endsWith(needle) ) { return assetKey; }
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ( Array.isArray(asset.contentURL) === false ) { continue; }
|
|
|
|
for ( i = 0, n = asset.contentURL.length; i < n; i++ ) {
|
|
|
|
if ( asset.contentURL[i].endsWith(needle) ) {
|
|
|
|
return assetKey;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return url;
|
|
|
|
};
|
|
|
|
var importedSet = new Set(this.listKeysFromCustomHostsFiles(externalHostsFiles)),
|
|
|
|
toImportSet = new Set(this.listKeysFromCustomHostsFiles(details.toImport)),
|
|
|
|
iter = toImportSet.values();
|
|
|
|
for (;;) {
|
|
|
|
var entry = iter.next();
|
|
|
|
if ( entry.done ) { break; }
|
|
|
|
if ( importedSet.has(entry.value) ) { continue; }
|
|
|
|
assetKey = assetKeyFromURL(entry.value);
|
|
|
|
if ( assetKey === entry.value ) {
|
|
|
|
importedSet.add(entry.value);
|
|
|
|
}
|
|
|
|
this.liveHostsFiles[assetKey] = {
|
|
|
|
content: 'filters',
|
|
|
|
contentURL: [ assetKey ],
|
|
|
|
title: assetKey
|
|
|
|
};
|
|
|
|
}
|
|
|
|
externalHostsFiles = this.setToArray(importedSet).sort().join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( externalHostsFiles !== this.userSettings.externalHostsFiles ) {
|
|
|
|
this.userSettings.externalHostsFiles = externalHostsFiles;
|
|
|
|
vAPI.storage.set({ externalHostsFiles: externalHostsFiles });
|
|
|
|
}
|
|
|
|
vAPI.storage.set({ 'liveHostsFiles': this.liveHostsFiles });
|
|
|
|
|
|
|
|
if ( typeof callback === 'function' ) {
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
// `switches` contains the preset blacklists for which the switch must be
|
|
|
|
// revisited.
|
|
|
|
|
|
|
|
µMatrix.reloadHostsFiles = function() {
|
|
|
|
this.loadHostsFiles();
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.loadPublicSuffixList = function(callback) {
|
|
|
|
if ( typeof callback !== 'function' ) {
|
|
|
|
callback = this.noopFunc;
|
|
|
|
}
|
|
|
|
|
|
|
|
var applyPublicSuffixList = function(details) {
|
|
|
|
if ( !details.error ) {
|
|
|
|
publicSuffixList.parse(details.content, punycode.toASCII);
|
|
|
|
}
|
|
|
|
callback();
|
|
|
|
};
|
|
|
|
|
|
|
|
this.assets.get(this.pslAssetKey, applyPublicSuffixList);
|
|
|
|
};
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.scheduleAssetUpdater = (function() {
|
|
|
|
var timer, next = 0;
|
|
|
|
return function(updateDelay) {
|
|
|
|
if ( timer ) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
timer = undefined;
|
|
|
|
}
|
|
|
|
if ( updateDelay === 0 ) {
|
|
|
|
next = 0;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var now = Date.now();
|
|
|
|
// Use the new schedule if and only if it is earlier than the previous
|
|
|
|
// one.
|
|
|
|
if ( next !== 0 ) {
|
|
|
|
updateDelay = Math.min(updateDelay, Math.max(next - now, 0));
|
|
|
|
}
|
|
|
|
next = now + updateDelay;
|
|
|
|
timer = vAPI.setTimeout(function() {
|
|
|
|
timer = undefined;
|
|
|
|
next = 0;
|
|
|
|
µMatrix.assets.updateStart({ delay: 120000 });
|
|
|
|
}, updateDelay);
|
|
|
|
};
|
|
|
|
})();
|
|
|
|
|
|
|
|
/******************************************************************************/
|
|
|
|
|
|
|
|
µMatrix.assetObserver = function(topic, details) {
|
|
|
|
// Do not update filter list if not in use.
|
|
|
|
if ( topic === 'before-asset-updated' ) {
|
|
|
|
if (
|
|
|
|
this.liveHostsFiles.hasOwnProperty(details.assetKey) === false ||
|
|
|
|
this.liveHostsFiles[details.assetKey].off === true
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( topic === 'after-asset-updated' ) {
|
|
|
|
vAPI.messaging.broadcast({
|
|
|
|
what: 'assetUpdated',
|
|
|
|
key: details.assetKey,
|
|
|
|
cached: true
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update failed.
|
|
|
|
if ( topic === 'asset-update-failed' ) {
|
|
|
|
vAPI.messaging.broadcast({
|
|
|
|
what: 'assetUpdated',
|
|
|
|
key: details.assetKey,
|
|
|
|
failed: true
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reload all filter lists if needed.
|
|
|
|
if ( topic === 'after-assets-updated' ) {
|
|
|
|
if ( details.assetKeys.length !== 0 ) {
|
|
|
|
this.loadHostsFiles();
|
|
|
|
}
|
|
|
|
if ( this.userSettings.autoUpdate ) {
|
|
|
|
this.scheduleAssetUpdater(25200000);
|
|
|
|
} else {
|
|
|
|
this.scheduleAssetUpdater(0);
|
|
|
|
}
|
|
|
|
vAPI.messaging.broadcast({
|
|
|
|
what: 'assetsUpdated',
|
|
|
|
assetKeys: details.assetKeys
|
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// New asset source became available, if it's a filter list, should we
|
|
|
|
// auto-select it?
|
|
|
|
if ( topic === 'builtin-asset-source-added' ) {
|
|
|
|
if ( details.entry.content === 'filters' ) {
|
|
|
|
if ( details.entry.off !== true ) {
|
|
|
|
this.saveSelectedFilterLists([ details.assetKey ], true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|