summaryrefslogtreecommitdiffstats
path: root/src/js/dyna-rules.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 05:47:55 +0000
commit31d6ff6f931696850c348007241195ab3b2eddc7 (patch)
tree615cb1c57ce9f6611bad93326b9105098f379609 /src/js/dyna-rules.js
parentInitial commit. (diff)
downloadublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.tar.xz
ublock-origin-31d6ff6f931696850c348007241195ab3b2eddc7.zip
Adding upstream version 1.55.0+dfsg.upstream/1.55.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/js/dyna-rules.js')
-rw-r--r--src/js/dyna-rules.js678
1 files changed, 678 insertions, 0 deletions
diff --git a/src/js/dyna-rules.js b/src/js/dyna-rules.js
new file mode 100644
index 0000000..ea79742
--- /dev/null
+++ b/src/js/dyna-rules.js
@@ -0,0 +1,678 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2014-present 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 CodeMirror, diff_match_patch, uBlockDashboard */
+
+'use strict';
+
+import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js';
+
+import { hostnameFromURI } from './uri-utils.js';
+import { i18n$ } from './i18n.js';
+import { dom, qs$, qsa$ } from './dom.js';
+
+import './codemirror/ubo-dynamic-filtering.js';
+
+/******************************************************************************/
+
+const hostnameToDomainMap = new Map();
+
+const mergeView = new CodeMirror.MergeView(
+ qs$('.codeMirrorMergeContainer'),
+ {
+ allowEditingOriginals: true,
+ connect: 'align',
+ inputStyle: 'contenteditable',
+ lineNumbers: true,
+ lineWrapping: false,
+ origLeft: '',
+ revertButtons: true,
+ value: '',
+ }
+);
+mergeView.editor().setOption('styleActiveLine', true);
+mergeView.editor().setOption('lineNumbers', false);
+mergeView.leftOriginal().setOption('readOnly', 'nocursor');
+
+uBlockDashboard.patchCodeMirrorEditor(mergeView.editor());
+
+const thePanes = {
+ orig: {
+ doc: mergeView.leftOriginal(),
+ original: [],
+ modified: [],
+ },
+ edit: {
+ doc: mergeView.editor(),
+ original: [],
+ modified: [],
+ },
+};
+
+let cleanEditToken = 0;
+let cleanEditText = '';
+let isCollapsed = false;
+
+/******************************************************************************/
+
+// The following code is to take care of properly internationalizing
+// the tooltips of the arrows used by the CodeMirror merge view. These
+// are hard-coded by CodeMirror ("Push to left", "Push to right"). An
+// observer is necessary because there is no hook for uBO to overwrite
+// reliably the default title attribute assigned by CodeMirror.
+
+{
+ const i18nCommitStr = i18n$('rulesCommit');
+ const i18nRevertStr = i18n$('rulesRevert');
+ const commitArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy-reverse:not([title="' + i18nCommitStr + '"])';
+ const revertArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy:not([title="' + i18nRevertStr + '"])';
+
+ dom.attr('.CodeMirror-merge-scrolllock', 'title', i18n$('genericMergeViewScrollLock'));
+
+ const translate = function() {
+ let elems = qsa$(commitArrowSelector);
+ for ( const elem of elems ) {
+ dom.attr(elem, 'title', i18nCommitStr);
+ }
+ elems = qsa$(revertArrowSelector);
+ for ( const elem of elems ) {
+ dom.attr(elem, 'title', i18nRevertStr);
+ }
+ };
+
+ const mergeGapObserver = new MutationObserver(translate);
+
+ mergeGapObserver.observe(
+ qs$('.CodeMirror-merge-copybuttons-left'),
+ { attributes: true, attributeFilter: [ 'title' ], subtree: true }
+ );
+
+}
+
+/******************************************************************************/
+
+const getDiffer = (( ) => {
+ let differ;
+ return ( ) => {
+ if ( differ === undefined ) { differ = new diff_match_patch(); }
+ return differ;
+ };
+})();
+
+/******************************************************************************/
+
+// Borrowed from...
+// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js#L22
+// ... and modified as needed.
+
+const updateOverlay = (( ) => {
+ let reFilter;
+ const mode = {
+ token: function(stream) {
+ if ( reFilter !== undefined ) {
+ reFilter.lastIndex = stream.pos;
+ let match = reFilter.exec(stream.string);
+ if ( match !== null ) {
+ if ( match.index === stream.pos ) {
+ stream.pos += match[0].length || 1;
+ return 'searching';
+ }
+ stream.pos = match.index;
+ return;
+ }
+ }
+ stream.skipToEnd();
+ }
+ };
+ return function(filter) {
+ reFilter = typeof filter === 'string' && filter !== '' ?
+ new RegExp(filter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi') :
+ undefined;
+ return mode;
+ };
+})();
+
+/******************************************************************************/
+
+// Incrementally update text in a CodeMirror editor for best user experience:
+// - Scroll position preserved
+// - Minimum amount of text updated
+
+const rulesToDoc = function(clearHistory) {
+ const orig = thePanes.orig.doc;
+ const edit = thePanes.edit.doc;
+ orig.startOperation();
+ edit.startOperation();
+
+ for ( const key in thePanes ) {
+ if ( thePanes.hasOwnProperty(key) === false ) { continue; }
+ const doc = thePanes[key].doc;
+ const rules = filterRules(key);
+ if (
+ clearHistory ||
+ doc.lineCount() === 1 && doc.getValue() === '' ||
+ rules.length === 0
+ ) {
+ doc.setValue(rules.length !== 0 ? rules.join('\n') + '\n' : '');
+ continue;
+ }
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/593
+ // Ensure the text content always ends with an empty line to avoid
+ // spurious diff entries.
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/657
+ // Diff against unmodified beforeText so that the last newline can
+ // be reported in the diff and thus appended if needed.
+ let beforeText = doc.getValue();
+ let afterText = rules.join('\n').trim();
+ if ( afterText !== '' ) { afterText += '\n'; }
+ const diffs = getDiffer().diff_main(beforeText, afterText);
+ let i = diffs.length;
+ let iedit = beforeText.length;
+ while ( i-- ) {
+ const diff = diffs[i];
+ if ( diff[0] === 0 ) {
+ iedit -= diff[1].length;
+ continue;
+ }
+ const end = doc.posFromIndex(iedit);
+ if ( diff[0] === 1 ) {
+ doc.replaceRange(diff[1], end, end);
+ continue;
+ }
+ /* diff[0] === -1 */
+ iedit -= diff[1].length;
+ const beg = doc.posFromIndex(iedit);
+ doc.replaceRange('', beg, end);
+ }
+ }
+
+ // Mark ellipses as read-only
+ const marks = edit.getAllMarks();
+ for ( const mark of marks ) {
+ if ( mark.uboEllipsis !== true ) { continue; }
+ mark.clear();
+ }
+ if ( isCollapsed ) {
+ for ( let iline = 0, n = edit.lineCount(); iline < n; iline++ ) {
+ if ( edit.getLine(iline) !== '...' ) { continue; }
+ const mark = edit.markText(
+ { line: iline, ch: 0 },
+ { line: iline + 1, ch: 0 },
+ { atomic: true, readOnly: true }
+ );
+ mark.uboEllipsis = true;
+ }
+ }
+
+ orig.endOperation();
+ edit.endOperation();
+ cleanEditText = mergeView.editor().getValue().trim();
+ cleanEditToken = mergeView.editor().changeGeneration();
+
+ if ( clearHistory !== true ) { return; }
+
+ mergeView.editor().clearHistory();
+ const chunks = mergeView.leftChunks();
+ if ( chunks.length === 0 ) { return; }
+ const ldoc = thePanes.orig.doc;
+ const { clientHeight } = ldoc.getScrollInfo();
+ const line = Math.min(chunks[0].editFrom, chunks[0].origFrom);
+ ldoc.setCursor(line, 0);
+ ldoc.scrollIntoView(
+ { line, ch: 0 },
+ (clientHeight - ldoc.defaultTextHeight()) / 2
+ );
+};
+
+/******************************************************************************/
+
+const filterRules = function(key) {
+ const filter = qs$('#ruleFilter input').value;
+ const rules = thePanes[key].modified;
+ if ( filter === '' ) { return rules; }
+ const out = [];
+ for ( const rule of rules ) {
+ if ( rule.indexOf(filter) === -1 ) { continue; }
+ out.push(rule);
+ }
+ return out;
+};
+
+/******************************************************************************/
+
+const applyDiff = async function(permanent, toAdd, toRemove) {
+ const details = await vAPI.messaging.send('dashboard', {
+ what: 'modifyRuleset',
+ permanent: permanent,
+ toAdd: toAdd,
+ toRemove: toRemove,
+ });
+ thePanes.orig.original = details.permanentRules;
+ thePanes.edit.original = details.sessionRules;
+ onPresentationChanged();
+};
+
+/******************************************************************************/
+
+// CodeMirror quirk: sometimes fromStart.ch and/or toStart.ch is undefined.
+// When this happens, use 0.
+
+mergeView.options.revertChunk = function(
+ mv,
+ from, fromStart, fromEnd,
+ to, toStart, toEnd
+) {
+ // https://github.com/gorhill/uBlock/issues/3611
+ if ( dom.attr(dom.body, 'dir') === 'rtl' ) {
+ let tmp = from; from = to; to = tmp;
+ tmp = fromStart; fromStart = toStart; toStart = tmp;
+ tmp = fromEnd; fromEnd = toEnd; toEnd = tmp;
+ }
+ if ( typeof fromStart.ch !== 'number' ) { fromStart.ch = 0; }
+ if ( fromEnd.ch !== 0 ) { fromEnd.line += 1; }
+ const toAdd = from.getRange(
+ { line: fromStart.line, ch: 0 },
+ { line: fromEnd.line, ch: 0 }
+ );
+ if ( typeof toStart.ch !== 'number' ) { toStart.ch = 0; }
+ if ( toEnd.ch !== 0 ) { toEnd.line += 1; }
+ const toRemove = to.getRange(
+ { line: toStart.line, ch: 0 },
+ { line: toEnd.line, ch: 0 }
+ );
+ applyDiff(from === mv.editor(), toAdd, toRemove);
+};
+
+/******************************************************************************/
+
+function handleImportFilePicker() {
+ const fileReaderOnLoadHandler = function() {
+ if ( typeof this.result !== 'string' || this.result === '' ) { return; }
+ // https://github.com/chrisaljoudi/uBlock/issues/757
+ // Support RequestPolicy rule syntax
+ let result = this.result;
+ let matches = /\[origins-to-destinations\]([^\[]+)/.exec(result);
+ if ( matches && matches.length === 2 ) {
+ result = matches[1].trim()
+ .replace(/\|/g, ' ')
+ .replace(/\n/g, ' * noop\n');
+ }
+ applyDiff(false, result, '');
+ };
+ const file = this.files[0];
+ if ( file === undefined || file.name === '' ) { return; }
+ if ( file.type.indexOf('text') !== 0 ) { return; }
+ const fr = new FileReader();
+ fr.onload = fileReaderOnLoadHandler;
+ fr.readAsText(file);
+}
+
+/******************************************************************************/
+
+const startImportFilePicker = function() {
+ const input = qs$('#importFilePicker');
+ // Reset to empty string, this will ensure an change event is properly
+ // triggered if the user pick a file, even if it is the same as the last
+ // one picked.
+ input.value = '';
+ input.click();
+};
+
+/******************************************************************************/
+
+function exportUserRulesToFile() {
+ const filename = i18n$('rulesDefaultFileName')
+ .replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
+ .replace(/ +/g, '_');
+ vAPI.download({
+ url: 'data:text/plain,' + encodeURIComponent(
+ mergeView.leftOriginal().getValue().trim() + '\n'
+ ),
+ filename: filename,
+ saveAs: true
+ });
+}
+
+/******************************************************************************/
+
+const onFilterChanged = (( ) => {
+ let timer;
+ let overlay = null;
+ let last = '';
+
+ const process = function() {
+ timer = undefined;
+ if ( mergeView.editor().isClean(cleanEditToken) === false ) { return; }
+ const filter = qs$('#ruleFilter input').value;
+ if ( filter === last ) { return; }
+ last = filter;
+ if ( overlay !== null ) {
+ mergeView.leftOriginal().removeOverlay(overlay);
+ mergeView.editor().removeOverlay(overlay);
+ overlay = null;
+ }
+ if ( filter !== '' ) {
+ overlay = updateOverlay(filter);
+ mergeView.leftOriginal().addOverlay(overlay);
+ mergeView.editor().addOverlay(overlay);
+ }
+ rulesToDoc(true);
+ };
+
+ return function() {
+ if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
+ timer = self.requestIdleCallback(process, { timeout: 773 });
+ };
+})();
+
+/******************************************************************************/
+
+const onPresentationChanged = (( ) => {
+ let sortType = 1;
+
+ const reSwRule = /^([^/]+): ([^/ ]+) ([^ ]+)/;
+ const reRule = /^([^ ]+) ([^/ ]+) ([^ ]+ [^ ]+)/;
+ const reUrlRule = /^([^ ]+) ([^ ]+) ([^ ]+ [^ ]+)/;
+
+ const sortNormalizeHn = function(hn) {
+ let domain = hostnameToDomainMap.get(hn);
+ if ( domain === undefined ) {
+ domain = /(\d|\])$/.test(hn)
+ ? hn
+ : publicSuffixList.getDomain(hn);
+ hostnameToDomainMap.set(hn, domain);
+ }
+ let normalized = domain || hn;
+ if ( hn.length !== domain.length ) {
+ const subdomains = hn.slice(0, hn.length - domain.length - 1);
+ normalized += '.' + (
+ subdomains.includes('.')
+ ? subdomains.split('.').reverse().join('.')
+ : subdomains
+ );
+ }
+ return normalized;
+ };
+
+ const slotFromRule = rule => {
+ let type, srcHn, desHn, extra;
+ let match = reSwRule.exec(rule);
+ if ( match !== null ) {
+ type = ' ' + match[1];
+ srcHn = sortNormalizeHn(match[2]);
+ desHn = srcHn;
+ extra = match[3];
+ } else if ( (match = reRule.exec(rule)) !== null ) {
+ type = '\x10FFFE';
+ srcHn = sortNormalizeHn(match[1]);
+ desHn = sortNormalizeHn(match[2]);
+ extra = match[3];
+ } else if ( (match = reUrlRule.exec(rule)) !== null ) {
+ type = '\x10FFFF';
+ srcHn = sortNormalizeHn(match[1]);
+ desHn = sortNormalizeHn(hostnameFromURI(match[2]));
+ extra = match[3];
+ }
+ if ( sortType === 0 ) {
+ return { rule, token: `${type} ${srcHn} ${desHn} ${extra}` };
+ }
+ if ( sortType === 1 ) {
+ return { rule, token: `${srcHn} ${type} ${desHn} ${extra}` };
+ }
+ return { rule, token: `${desHn} ${type} ${srcHn} ${extra}` };
+ };
+
+ const sort = rules => {
+ const slots = [];
+ for ( let i = 0; i < rules.length; i++ ) {
+ slots.push(slotFromRule(rules[i], 1));
+ }
+ slots.sort((a, b) => a.token.localeCompare(b.token));
+ for ( let i = 0; i < rules.length; i++ ) {
+ rules[i] = slots[i].rule;
+ }
+ };
+
+ const collapse = ( ) => {
+ if ( isCollapsed !== true ) { return; }
+ const diffs = getDiffer().diff_main(
+ thePanes.orig.modified.join('\n'),
+ thePanes.edit.modified.join('\n')
+ );
+ const ll = []; let il = 0, lellipsis = false;
+ const rr = []; let ir = 0, rellipsis = false;
+ for ( let i = 0; i < diffs.length; i++ ) {
+ const diff = diffs[i];
+ if ( diff[0] === 0 ) {
+ lellipsis = rellipsis = true;
+ il += 1; ir += 1;
+ continue;
+ }
+ if ( diff[0] < 0 ) {
+ if ( lellipsis ) {
+ ll.push('...');
+ if ( rellipsis ) { rr.push('...'); }
+ lellipsis = rellipsis = false;
+ }
+ ll.push(diff[1].trim());
+ il += 1;
+ continue;
+ }
+ /* diff[0] > 0 */
+ if ( rellipsis ) {
+ rr.push('...');
+ if ( lellipsis ) { ll.push('...'); }
+ lellipsis = rellipsis = false;
+ }
+ rr.push(diff[1].trim());
+ ir += 1;
+ }
+ if ( lellipsis ) { ll.push('...'); }
+ if ( rellipsis ) { rr.push('...'); }
+ thePanes.orig.modified = ll;
+ thePanes.edit.modified = rr;
+ };
+
+ return function(clearHistory) {
+ const origPane = thePanes.orig;
+ const editPane = thePanes.edit;
+ origPane.modified = origPane.original.slice();
+ editPane.modified = editPane.original.slice();
+ const select = qs$('#ruleFilter select');
+ sortType = parseInt(select.value, 10);
+ if ( isNaN(sortType) ) { sortType = 1; }
+ {
+ const mode = origPane.doc.getMode();
+ mode.sortType = sortType;
+ mode.setHostnameToDomainMap(hostnameToDomainMap);
+ mode.setPSL(publicSuffixList);
+ }
+ {
+ const mode = editPane.doc.getMode();
+ mode.sortType = sortType;
+ mode.setHostnameToDomainMap(hostnameToDomainMap);
+ mode.setPSL(publicSuffixList);
+ }
+ sort(origPane.modified);
+ sort(editPane.modified);
+ collapse();
+ rulesToDoc(clearHistory);
+ onTextChanged(clearHistory);
+ };
+})();
+
+/******************************************************************************/
+
+const onTextChanged = (( ) => {
+ let timer;
+
+ const process = details => {
+ timer = undefined;
+ const diff = qs$('#diff');
+ let isClean = mergeView.editor().isClean(cleanEditToken);
+ if (
+ details === undefined &&
+ isClean === false &&
+ mergeView.editor().getValue().trim() === cleanEditText
+ ) {
+ cleanEditToken = mergeView.editor().changeGeneration();
+ isClean = true;
+ }
+ const isDirty = mergeView.leftChunks().length !== 0;
+ dom.cl.toggle(dom.body, 'editing', isClean === false);
+ dom.cl.toggle(diff, 'dirty', isDirty);
+ dom.cl.toggle('#editSaveButton', 'disabled', isClean);
+ dom.cl.toggle('#exportButton,#importButton', 'disabled', isClean === false);
+ dom.cl.toggle('#revertButton,#commitButton', 'disabled', isClean === false || isDirty === false);
+ const input = qs$('#ruleFilter input');
+ if ( isClean ) {
+ dom.attr(input, 'disabled', null);
+ CodeMirror.commands.save = undefined;
+ } else {
+ dom.attr(input, 'disabled', '');
+ CodeMirror.commands.save = editSaveHandler;
+ }
+ };
+
+ return function(now) {
+ if ( timer !== undefined ) { self.cancelIdleCallback(timer); }
+ timer = now ? process() : self.requestIdleCallback(process, { timeout: 57 });
+ };
+})();
+
+/******************************************************************************/
+
+const revertAllHandler = function() {
+ const toAdd = [], toRemove = [];
+ const left = mergeView.leftOriginal();
+ const edit = mergeView.editor();
+ for ( const chunk of mergeView.leftChunks() ) {
+ const addedLines = left.getRange(
+ { line: chunk.origFrom, ch: 0 },
+ { line: chunk.origTo, ch: 0 }
+ );
+ const removedLines = edit.getRange(
+ { line: chunk.editFrom, ch: 0 },
+ { line: chunk.editTo, ch: 0 }
+ );
+ toAdd.push(addedLines.trim());
+ toRemove.push(removedLines.trim());
+ }
+ applyDiff(false, toAdd.join('\n'), toRemove.join('\n'));
+};
+
+/******************************************************************************/
+
+const commitAllHandler = function() {
+ const toAdd = [], toRemove = [];
+ const left = mergeView.leftOriginal();
+ const edit = mergeView.editor();
+ for ( const chunk of mergeView.leftChunks() ) {
+ const addedLines = edit.getRange(
+ { line: chunk.editFrom, ch: 0 },
+ { line: chunk.editTo, ch: 0 }
+ );
+ const removedLines = left.getRange(
+ { line: chunk.origFrom, ch: 0 },
+ { line: chunk.origTo, ch: 0 }
+ );
+ toAdd.push(addedLines.trim());
+ toRemove.push(removedLines.trim());
+ }
+ applyDiff(true, toAdd.join('\n'), toRemove.join('\n'));
+};
+
+/******************************************************************************/
+
+const editSaveHandler = function() {
+ const editor = mergeView.editor();
+ const editText = editor.getValue().trim();
+ if ( editText === cleanEditText ) {
+ onTextChanged(true);
+ return;
+ }
+ const toAdd = [], toRemove = [];
+ const diffs = getDiffer().diff_main(cleanEditText, editText);
+ for ( const diff of diffs ) {
+ if ( diff[0] === 1 ) {
+ toAdd.push(diff[1]);
+ } else if ( diff[0] === -1 ) {
+ toRemove.push(diff[1]);
+ }
+ }
+ applyDiff(false, toAdd.join(''), toRemove.join(''));
+};
+
+/******************************************************************************/
+
+self.cloud.onPush = function() {
+ return thePanes.orig.original.join('\n');
+};
+
+self.cloud.onPull = function(data, append) {
+ if ( typeof data !== 'string' ) { return; }
+ applyDiff(
+ false,
+ data,
+ append ? '' : mergeView.editor().getValue().trim()
+ );
+};
+
+/******************************************************************************/
+
+self.hasUnsavedData = function() {
+ return mergeView.editor().isClean(cleanEditToken) === false;
+};
+
+/******************************************************************************/
+
+vAPI.messaging.send('dashboard', {
+ what: 'getRules',
+}).then(details => {
+ thePanes.orig.original = details.permanentRules;
+ thePanes.edit.original = details.sessionRules;
+ publicSuffixList.fromSelfie(details.pslSelfie);
+ onPresentationChanged(true);
+});
+
+// Handle user interaction
+dom.on('#importButton', 'click', startImportFilePicker);
+dom.on('#importFilePicker', 'change', handleImportFilePicker);
+dom.on('#exportButton', 'click', exportUserRulesToFile);
+dom.on('#revertButton', 'click', revertAllHandler);
+dom.on('#commitButton', 'click', commitAllHandler);
+dom.on('#editSaveButton', 'click', editSaveHandler);
+dom.on('#ruleFilter input', 'input', onFilterChanged);
+dom.on('#ruleFilter select', 'input', ( ) => {
+ onPresentationChanged(true);
+});
+dom.on('#ruleFilter #diffCollapse', 'click', ev => {
+ isCollapsed = dom.cl.toggle(ev.target, 'active');
+ onPresentationChanged(true);
+});
+
+// https://groups.google.com/forum/#!topic/codemirror/UQkTrt078Vs
+mergeView.editor().on('updateDiff', ( ) => {
+ onTextChanged();
+});
+
+/******************************************************************************/
+