User:Kephir/gadgets/cksyntax.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*****
 * THIS SCRIPT IS OBSOLETE
 * 
 * It has been replaced by an update to the CodeEditor extension. This page is kept for historical interest only.
 *****/
mw.loader.using(['ext.wikiEditor'], function () {
"use strict";

if ((wgAction !== 'edit') && (wgAction !== 'submit'))
	return;

var wpTextbox1 = document.getElementById('wpTextbox1');

if ((window.wgPageContentModel === 'javascript') || (window.wgPageContentModel === 'css') || (window.wgPageContentModel === 'lua')) {
	var editCheckboxes = document.getElementsByClassName('editCheckboxes')[0];
	var label = document.createElement('label');
	var inputStrip = document.createElement('input');
	inputStrip.type = 'checkbox';
	inputStrip.checked = (window.wgPageContentModel !== 'lua');
	label.textContent = "Strip trailing whitespace when saving";
	editCheckboxes.appendChild(inputStrip);
	editCheckboxes.appendChild(label);
	inputStrip.id = 'cksyntax-strip-whitespace';
	label.setAttribute('for', inputStrip.id);
}

if (window.wgPageContentModel !== void(0)) {
	if (window.wgPageContentModel !== 'javascript')
		return;
} else { /* fallback check */
	if (((wgNamespaceNumber !== 2) && (wgNamespaceNumber !== 8)) || !/\.js$/.test(wgPageName))
		return;
}

var wpSave = document.getElementById('wpSave');

if (!wpSave || !wpTextbox1)
	return;

var dirty = true;

// is Function(code) equivalent to eval("function () {\n"+code+"\n}")? if yes, we are in trouble
/* Testing results:
 * IE10                   : secure, throws a SyntaxError
 * KJS (Konqueror)        : secure, throws a SyntaxError
 * SpiderMonkey (Mozilla) : secure, throws a SyntaxError
 * Carakan (Opera 12)     : secure, throws a SyntaxError
 * Rhino                  : secure, although allows some invalid inputs, including the first one tested below; silently ignores + and - and any following expression; throws a SyntaxError otherwise (no browser uses Rhino, though)
 * V8 (Chrom*)            : insecure, throws an error if the expression does not evaluate to a function (which is avoided easily)
 * JavaScriptCore         : crashes spectacularly ("WTFCrash"); does not seem to be a security vulnerability otherwise
 */
try {
	new Function('){/*', '*///');

	// if we got here, this is a buggy JavaScriptCore, and the tests that follow may crash the browser.
	// the immediate culprit is at <https://trac.webkit.org/browser/trunk/Source/JavaScriptCore/runtime/CodeCache.cpp?rev=167313#L158>
	// but it seems the fix should be somewhere earlier.

	// even the flawed (see below) versions of V8 will reject the above
	// see <https://code.google.com/p/v8/source/diff?spec=svn13867&r=13867&path=/branches/bleeding_edge/src/v8natives.js>
	if (!confirm('Your browser has a buggy implementation of the Function constructor; a specially crafted invalid script may crash your browser. Perform syntax checks?'))
		return;
} catch (e) {
	// OK, an error is expected here.

	try { // code.google.com/p/v8/issues/detail?id=2470
		new Function("return false; } && void(window._insecure = true) || function () { return true;");
	} catch (ee) {
		// OK, an error is expected here.
	}

	if (window._insecure)
		if (!confirm('Your browser has an insecure implementation of the Function constructor; a specially crafted invalid script may trick your browser into executing arbitrary JavaScript. Perform syntax checks?'))
			return;
}

mw.hook("codeEditor.configure").add(function (sess) {
	sess.on("change", function () {
		dirty = true;
	});
});

wpTextbox1.addEventListener('input', function (ev) {
	dirty = true;
}, false);

wpTextbox1.addEventListener('change', function (ev) {
	dirty = true;
}, false);

function checkSyntax() {
	var fixup = 0;
	// DO NOT prettify. this HAS TO to be on one line (minifiers should be okay).
	try { eval("(") /* xkcd.com/859/ */ } catch (e) { fixup = e.lineNumber }; try { (function($, $, mw, mediaWiki, jQuery, top, self, parent, window, document, navigator, location) { new Function(wpTextbox1.value); }).call({});
	} catch (e) {
		// SpiderMonkey workaround: it sums the line number of the evaluated script
		// with the line number of the Function call in the containing script.
		// this may make sense in some contrived situations, but not here.
		if (fixup)
			e.lineNumber -= fixup - 1;
		else if (!e.line && !e.lineNumber && window.opera) {
			// Carakan reports the line and column when calling eval(), but not Function().
			// since we already know the code is invalid, we can safely eval it again
			// and extract the error info from there.
			try {
				// the \n at the start is necessary to have the message in the format below
				// otherwise we get "at index n"; we can parse that format too, but why bother?

				// also appending an unmatched (, in case for some reason it starts parsing correctly here;
				// the error location will be bogus, but at least we avoided executing a potentially
				// malicious script.
				eval('\n' + wpTextbox1.value + '\n('); 
			} catch (ee) {
				var m = /^at line (\d+), column (\d+)/.exec(ee.message);
				if (m) {
					e.lineNumber = parseInt(m[1], 10) - 1;
					e.columnNumber = parseInt(m[2], 10);
				}
			}
		}

		return e;
	}
	return null;
}

function scrollEditor(line, col) {
	// line is 1-based, while col is 0-based
	var codeEditor = $(wpTextbox1).data('wikiEditorContext').codeEditor;

	if (codeEditor) {
		codeEditor.gotoLine(line, col || 0, true);
		codeEditor.focus();
		return;
	} 

	var position = 0;
	for (var i = 1; i < line; ++i) {
		position = wpTextbox1.value.indexOf('\n', position) + 1;
	}

	var lastcol = wpTextbox1.value.indexOf('\n', position) - position;
	position += col ? (col < lastcol ? col : lastcol) : 0;

	if (wpTextbox1.createTextRange) { // MSIE	
		var range = textBox.createTextRange();
		range.collapse(true);
		range.moveEnd('character', position);
		range.moveStart('character', position);
		range.select();
	} else if (wpTextbox1.setSelectionRange) { // browsers
		wpTextbox1.focus();
		wpTextbox1.setSelectionRange(position, position);
		var phantom = document.createElement('div');
		var style = window.getComputedStyle(wpTextbox1, '');
		phantom.style.padding = '0';
		phantom.style.lineHeight = style.lineHeight;
		phantom.style.fontFamily = style.fontFamily;
		phantom.style.fontSize = style.fontSize;
		phantom.style.fontStyle = style.fontStyle;
		phantom.style.fontVariant = style.fontVariant;
		phantom.style.letterSpacing = style.letterSpacing;
		phantom.style.border = style.border;
		phantom.style.outline = style.outline;
		try { phantom.style.whiteSpace = "-moz-pre-wrap" } catch(e) {}
		try { phantom.style.whiteSpace = "-o-pre-wrap" } catch(e) {}
		try { phantom.style.whiteSpace = "-pre-wrap" } catch(e) {}
		try { phantom.style.whiteSpace = "pre-wrap" } catch(e) {}
		phantom.textContent = wpTextbox1.value.substr(0, position);
		document.body.appendChild(phantom); // XXX: do I need this?
		wpTextbox1.scrollTop = phantom.scrollHeight - (wpTextbox1.clientHeight / 2);
		document.body.removeChild(phantom);
	}
}

function scrollToError(e) {
	if ((typeof e.lineNumber === 'number') && (typeof e.columnNumber === 'number')) { // SpiderMonkey (Firefox) and Opera
		scrollEditor(e.lineNumber, e.columnNumber);
		return "Line: " + e.lineNumber + ", column: " + (e.columnNumber + 1);
	} else if ((typeof e.line === 'number')) { // JavaScriptCore (WebKit)
		scrollEditor(e.line, 1 / 0); // most errors happen on the end of the line
		return "Line: " + e.line;
	}
	// nothing for V8 (Chrom*)
	// unknown whether we can do anything on Trident (MSIE)
	return "Clicking \"Show changes\" may help you locate the error.";
}

wpSave.addEventListener('click', function (ev) {
	if (dirty) {
		$(mw).trigger('LivePreviewPrepare'); // XXX: force CodeEditor (and everything else) to update wpTextbox1.value
		if (inputStrip.checked) {
			wpTextbox1.value = wpTextbox1.value.replace(/[ \t]+$/mg, '');
		}
		var err;

		dirty = false;
		if (err = checkSyntax()) {
			mw.util.jsMessage("There is an error in the script you were trying to save:<br/><br/><b>" + err.toString() + 
				"</b><br/><br/>" + scrollToError(err) + "<br/><br/>If you click the \"Save\" button again, this error will be ignored.");
			ev.preventDefault();
			ev.stopPropagation();
			return false;
		}
	}
}, false);

$(wpTextbox1).wikiEditor('addToToolbar', {
	section: 'main',
	group: 'format',
	tools: {
		cksyntax: {
			label: "Check syntax",
			type: 'button',
			icon: '//upload.wikimedia.org/wikipedia/commons/thumb/1/15/Tick-red.png/22px-Tick-red.png',
			action: {
				type: 'callback',
				execute: function () {
					var err;
					$(mw).trigger('LivePreviewPrepare'); // XXX: force CodeEditor (and everything else) to update wpTextbox1.value
					if (err = checkSyntax()) {
						mw.util.jsMessage("<b>" + err.toString() + "</b><br/><br/>" + scrollToError(err));
						scrollToError(err);
					} else {
						mw.util.jsMessage("No syntax errors found");
					}
				}
			}
		}
	}
});

});