User:Gary/script installer source.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
Documentation for this user script can be added at User:Gary/script installer source. |
// ====================================================================================================
// ============================================== api.js ==============================================
// ====================================================================================================
/**
* The callback response that retrieves a script's "associated script".
*/
associatedScriptCallback = function(response)
{
if (!response['query'] || !response['query']['pageids'] || response['query']['pageids'][0] == -1) return false;
markAssociatedScript(otherPage);
// set cookie
setConvertedCookie('associated-scripts', [ pageName, otherPage ]);
};
/**
* Gets the edit token for the INSTALL_PAGE, then sends return data to another function for processing.
*/
beginScriptInstallation = function(pageToInstallTo, scriptName)
{
var api = sajax_init_object();
api.open('GET', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?format=json&action=query&prop=info&indexpageids=1&intoken=edit&titles=' + pageToInstallTo, true);
// now that we used GET, get the token from the results
api.onreadystatechange = function()
{
if (api.readyState == 4)
{
if (api.status == 200)
{
var response = eval('(' + api.responseText + ')');
var tokenPage = response['query']['pages'][response['query']['pageids'][0]];
sendScriptInstallation(pageToInstallTo, tokenPage['edittoken'], scriptName);
}
else
errorMessage('EXT_TOK');
}
};
api.send(null);
};
/**
* Gets the backlinks to a page, which we later process and then call "installations".
*/
function getInstallations(callback, page, blcontinue, printToLog)
{
wikiApi(callback, 'action=query&rawcontinue=&list=backlinks&bltitle=' + page + '&bllimit=500&blfilterredir=nonredirects&blnamespace=2' + (blcontinue ? '&blcontinue=' + blcontinue : ''), printToLog);
};
/**
* Get the user's page where their scripts are installed to.
*/
getInstallPage = function(scriptName, skinPage, markInstalled, functionToRun)
{
if (skinPage == scriptName)
{
// current script is the script where we install to
var install = document.getElementById('install-this-script');
var node = document.createElement('span');
node.className = 'si-heading';
node.id = 'install-this-script';
node.innerHTML = '<a href="' + createLink(libraryPage) + '">Your scripts</a> are installed here';
return install.parentNode.replaceChild(node, install);
}
var api = sajax_init_object();
api.open('GET', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?format=json&action=query&prop=revisions&titles=' + skinPage + '&rvprop=content&indexpageids=1', true);
api.onreadystatechange = function()
{
if (api.readyState == 4)
{
if (api.status == 200)
{
var response = eval('(' + api.responseText + ')');
var pageToInstallTo = response['query']['pages'][response['query']['pageids'][0]];
if (typeof pageToInstallTo['revisions'] == 'undefined')
{
errorMessage('NO_REV');
return;
}
existingScriptContent = pageToInstallTo['revisions'][0]['*'];
if (!functionToRun)
indicateScriptInstalled(scriptName, pageToInstallTo['title'], existingScriptContent, markInstalled);
else
functionToRun(pageToInstallTo['title'], existingScriptContent);
}
else if (typeof(failedToConnectToAPI) == 'function')
failedToConnectToAPI();
}
};
api.send(null);
};
/**
* The callback used for the Script Library to get all of a user's scripts.
*
* Also, the callback for finding how many users have a script installed
* with blcontinue (for when we reached the 500 max limit on results)
*/
scriptInstallationsCallback = function(response)
{
if (!response['query'] || !response['query']['backlinks']) return;
var userCount = countUsersFromJSON(response['query']['backlinks']).length;
doScriptInstallation(userCount, (response['query-continue'] ? formatBlContinue(response['query-continue']['backlinks']['blcontinue']) : ''));
};
/**
* With the token, use POST to submit a page edit.
*/
function sendScriptInstallation(pageToInstallTo, token, scriptName)
{
// Script is already in script page
if (findExistingScript(scriptName, existingScriptContent) && siSettings['checkIfScriptIsInstalled'])
{
alert('The script is already installed.');
return;
}
else
indicateScriptIsInstalling();
var api = sajax_init_object();
var text = existingScriptContent + (existingScriptContent ? '\n' : '') + importScriptTypes[scriptType] + '(\'' + scriptName + '\'); // [[' + scriptName + ']]';
// FIXME Indicate "Installing stylesheet" if that is the case.
var editSummary = siEditSummary('Installing script [[' + scriptName + ']]');
var parameters = 'action=edit&title=' + encodeURIComponent(pageToInstallTo) + '&text=' + encodeURIComponent(text) + '&token=' + encodeURIComponent(token) + '&summary=' + encodeURIComponent(editSummary) + '&format=json';
api.open('POST', mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php', true);
// tell the user what has happened; either success or failure
api.onreadystatechange = function()
{
if (api.readyState == 4)
{
if (api.status == 200)
{
// TODO Update the Script Library cookie by adding this script.
alert('The script "' + scriptName + '" was installed successfully to "' + pageToInstallTo + '".');
location.reload(true);
}
else
errorMessage('AL_RES_STAT');
}
};
api.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
api.setRequestHeader('Connection', 'keep-alive');
api.setRequestHeader('Content-length', parameters.length);
api.send(parameters);
};
/**
* Callback response for number of backlinks to a single script.
*/
singleScriptInstalls = function(response)
{
if (!response['query'] || !response['query']['backlinks']) return;
var userCount = countUsersFromJSON(response['query']['backlinks']).length;
doSingleScriptInstalls(userCount, (response['query-continue'] ? formatBlContinue(response['query-continue']['backlinks']['blcontinue']) : ''));
};
/**
* Wrapper for Wikipedia's API.
*/
function wikiApi(callback, parameters, printToLog)
{
var url = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?callback=' + callback + '&format=json&' + parameters;
importScriptURI(url);
if (printToLog) siLog(url);
};
// ====================================================================================================
// ============================================ cookies.js ============================================
// ====================================================================================================
/**
* Create a cookie.
*/
function createCookie(name, value, days)
{
// override the "days" parameter (for now)
var days = ageOfCookies;
// convert 'value' hash to string
if (value instanceof Array)
{
var newValues = [];
for (var i = 0; i < value.length; i++)
{
if (value[i] instanceof Array)
newValues.push(value[i].join(','));
}
var value = newValues.join(encodeURIComponent(';'));
}
if (days)
{
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = '; expires=' + date.toGMTString();
}
else
var expires = '';
// don't set the cookie if it has the same value as the one that already exists
if (readCookie('si-' + name) == value) return false;
var name = 'si-' + name;
document.cookie = name + '=' + value + expires + '; path=/';
};
/**
* Delete a cookie.
*/
function deleteCookie(name)
{
createCookie(name, '', -1);
};
/**
* Get a cookie, then convert it to a hash.
*
* @param {string} name Name of cookie, without the preceding "si-".
* @param {boolean} asArray Return an array instead.
* @param {boolean} fromSetter True if retrieved from the cookie setter function;
* necessary so that that function can still work properly when SI cookies are disabled.
* @return {object} Returns the cookie laid out as an associative array.
*/
function getConvertedCookie(name, asArray, fromSetter)
{
var cookieName = 'si-' + name;
var cookie = readCookie(cookieName);
if (!cookie || (!siSettings['enableSICookies'] && !fromSetter)) return [];
var allowed = ['associated-scripts', 'installed-by', 'script-library'];
if (!has(allowed, name)) return (asArray ? [] : {});
var scripts = decodeURIComponent(cookie).split(';');
if (asArray) var result = [];
else var result = {};
var length = 0;
for (var i = 0; i < scripts.length; i++)
{
var script = scripts[i].split(',');
if (name == 'script-library')
{
if (asArray) result.push([script[0], script[1], script[2]]);
else result[script[0]] = [script[1], script[2]];
}
else
{
if (asArray) result.push([script[0], script[1]]);
else result[script[0]] = script[1];
}
length++;
}
if (typeof(result) == 'object')
{
// include the length
result['length'] = length;
}
return result;
};
/**
* Reads a cookie.
*/
function readCookie(name)
{
var nameEQ = name + '=';
var ca = document.cookie.split(';');
for(var i = 0; i < ca.length; i++)
{
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
};
/**
* Sets a converted cookie.
*
* @param {string} name Name of cookie
* @param {array} value The item to add to the existing cookie.
*/
function setConvertedCookie(name, value)
{
// get the cookie first, only keep maximum 50 items in the cookie
var convertedCookie = getConvertedCookie(name, true, true);
var convertedCookieHash = getConvertedCookie(name, false, true);
// FIXME Don't set this if old script library cookie is the same as the new one.
// Set a cookie every time the Script Library loads
if (name == 'script-library')
{
return createCookie(name, (convertedCookie.length > 0 ? convertedCookie : value));
}
// if new cookie already exists in existing convertedCookie
// AND the values are the same, then skip this
if (convertedCookieHash[value[0]] && convertedCookieHash[value[0]] == value[1]) return true;
// if the new cookie already exists, then remove it first
if (name == 'installed-by')
{
for (var i = 0; i < convertedCookie.length; i++)
{
if (convertedCookie[i][0] == value[0])
{
convertedCookie.splice(i, 1);
break;
}
}
}
if (convertedCookie.length >= 50)
{
// remove the first item
convertedCookie.shift();
// add newest item at the end
convertedCookie.push(value);
}
else
{
// add newest item at the end
convertedCookie.push(value);
}
return createCookie(name, convertedCookie);
};
// ====================================================================================================
// ============================================= doers.js =============================================
// ====================================================================================================
addTitleTooltips = function()
{
if (typeof(tooltipText) == 'undefined')
return false;
// apply TITLE attribute to the ID as well
for (var name in tooltipText)
{
var id = document.getElementById('si-' + name);
if (id) id.title = stripHTML(tooltipText[name].replace(/<br \/>/g, ' '));
}
};
/**
* Create the list of scripts installed for a user's Script Library
*/
function buildUserLibrary(title, content)
{
var scripts = installedScripts(content);
doUserLibrary(scripts);
setConvertedCookie('script-library', scripts);
};
function countUsersFromJSON(users)
{
// TODO +1 user if current user is not included.
// remove the forced = 1 from other functions then, but keep the link unlinking.
var newUsers = [];
for (var i = 0; i < users.length; i++)
{
// determine if the title ends in .js and is a skin page
// strip .js first
var title = users[i]['title'];
var parts = title.split('/');
if (parts.length != 2) continue;
title = parts[1];
if (!title.indexOf('.')) continue;
title = title.substring(0, title.indexOf('.'));
if (allSkinsHash[title]) newUsers.push(users[i]);
}
return newUsers;
};
/**
* Used on the Script Library.
* Executed directly when a cookie is found.
* Otherwise, executed after contacting API and after scriptInstallationsCallback.
* Determines which node to update with the help of the "callback-counter" node.
*
* @param {integer} userCount Number of users.
* @param {object} blcontinue An object containing blcontinue information, if available.
*/
function doScriptInstallation(userCount, blcontinue)
{
if (userCount == 0) userCount = 1;
// get the current counter
var counter = document.getElementById('callback-counter');
var currentCount = counter.firstChild.nodeValue;
// insert the number of installations
var installs = document.getElementById('installation-' + currentCount);
// check if "installs" exists; if not, then fall back to script counter
if (!installs)
{
var scriptCounter = document.getElementById('si-script-counter');
var scriptNode = document.getElementById(scriptCounter.removeChild(scriptCounter.firstChild).firstChild.nodeValue);
currentCount = scriptNode.getElementsByClassName('si-script-counter')[0].firstChild.nodeValue;
installs = document.getElementById('installation-' + currentCount);
}
else
{
// replace the counter
counter.replaceChild(document.createTextNode(parseInt(currentCount) + 1), counter.firstChild);
}
var script = installs.parentNode.parentNode.id;
// insert the new number of users
var currentUsers = installs.firstChild.nodeValue;
userCount = userCount + (isNaN(parseInt(currentUsers)) ? 0 : parseInt(currentUsers));
var numberOfUsers = document.createTextNode(userCount);
installs.replaceChild(numberOfUsers, installs.firstChild);
// pluralize if necessary
var plural = document.getElementById('installation-plural-' + currentCount);
if (userCount == 1)
{
plural.replaceChild(document.createTextNode(' installation'), plural.firstChild);
// unlink installation-link-i since we are forcing the number
var span = convertIdToSpan('installation-link-' + currentCount);
span.title = 'Only YOU have this script installed';
}
// connect to API again to get more backlinks on the same script
if (blcontinue)
{
var scriptCounter = document.getElementById('si-script-counter');
var newScriptCounter = scriptCounter.appendChild(document.createElement('div'));
newScriptCounter.className = 'si-hidden';
newScriptCounter.appendChild(document.createTextNode(wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title']));
// get API
getInstallations('scriptInstallationsCallback', wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title'], blcontinue['full']);
}
else
{
// update the cookie
setConvertedCookie('installed-by', [script, userCount]);
}
};
function doSingleScriptInstalls(numberOfUsers, blcontinue)
{
// TODO Do some "faking" by adding 1 if the user has this script installed
// AND the number of installs is 0? Then remove the link to backlinks?
var Xpeople = document.getElementById('installed-by-X-people');
// delink the link to the script's backlinks
if (numberOfUsers == 0)
var span = convertIdToSpan('installed-by-link');
// singularize "people" to "person" for 1 person
if (numberOfUsers == 1)
document.getElementById('people-plural').replaceChild(document.createTextNode(' person'), document.getElementById('people-plural').firstChild);
// calculate number of users by adding it with existing number
var currentNumberOfUsers = Xpeople.firstChild.nodeValue;
numberOfUsers = numberOfUsers + (isNaN(parseInt(currentNumberOfUsers)) ? 0 : parseInt(currentNumberOfUsers));
// insert the number of users
Xpeople.replaceChild(document.createTextNode(numberOfUsers), Xpeople.firstChild);
// get API
if (blcontinue)
getInstallations('singleScriptInstalls', wgFormattedNamespaces[blcontinue['namespace']] + ':' + blcontinue['title'], blcontinue['full']);
// update the cookie
else
setConvertedCookie('installed-by', [(scriptData && scriptData['page'] ? scriptData['page'] : pageName ), numberOfUsers]);
};
function failedToConnectToAPI()
{
// Mark "Checking script" as failed
var installThisScript = document.getElementById('install-this-script');
if (installThisScript)
installThisScript.replaceChild(document.createTextNode('Failed to connect to the API'), installThisScript.firstChild);
};
/**
* Formats blcontinue tokens
*/
function formatBlContinue(origBlcontinue)
{
var blcontinue = origBlcontinue.split('|');
return { 'namespace': blcontinue[0], 'title': blcontinue[1], 'id': blcontinue[2], 'full': origBlcontinue };
};
hideTooltip = function(tooltip)
{
if (typeof timerIsRunning != 'undefined' && timerIsRunning === true) timerIsRunning = false;
var tooltip = document.getElementById('tooltip-' + tooltip);
if (tooltip) return tooltip.parentNode.removeChild(tooltip);
};
function indicateScriptInstalled(scriptName, pageToInstallTo, content, markInstalled)
{
var install = document.getElementById('install-this-script');
// current script is already installed
// TODO This should use a cookie for checking if script is already installed or not.
if (findExistingScript(scriptName, content) && markInstalled !== false)
{
var node = document.createElement('span');
node.className = 'si-heading';
node.id = 'install-this-script';
node.innerHTML = 'You already <a href="' + createLink(libraryPage) + '">installed this ' + scriptType + '</a>';
}
// current script is not yet installed
else
{
var node = document.createElement('a');
node.className = 'si-heading';
node.href = 'javascript:beginScriptInstallation(\'' + pageToInstallTo + '\', \'' + scriptName + '\');';
node.id = 'install-this-script';
node.title = 'This will install the script to [[' + pageToInstallTo + ']]';
node.appendChild(document.createTextNode('Install this ' + scriptType));
}
install.parentNode.replaceChild(node, install);
};
function indicateScriptIsInstalling()
{
// replace "Install this script" with "Installing..."
var span = document.createElement('span');
span.className = 'si-heading';
span.id = 'install-this-script';
// FIXME Indicate "Installing stylesheet" if that is the case.
span.appendChild(document.createTextNode('Installing script...'));
var install = document.getElementById('install-this-script');
install.parentNode.replaceChild(span, install);
};
function markAssociatedScript(otherPage)
{
var relatedPage = document.getElementById('si-related-page');
var a = document.createElement('a');
a.href = createLink(otherPage);
a.id = 'si-related-page';
a.appendChild(document.createTextNode('Related ' + oppScriptType));
return relatedPage.parentNode.replaceChild(a, relatedPage);
};
function parseScriptData()
{
var scriptDataId = document.getElementById('script-data');
if (!scriptDataId) return false;
var data = {};
for (var i = 0; i < documentationData.length; i++)
{
var docData = document.getElementById('script-data-' + documentationData[i]);
if (docData && docData.firstChild && docData.firstChild.nodeValue) data[documentationData[i]] = docData.firstChild.nodeValue.trim();
}
return data;
};
showTooltip = function(event, tooltip, extras)
{
// if tooltip already exists due to a previous hover, then don't create it again
if (document.getElementById('tooltip-' + tooltip))
{
hideTooltip(tooltip);
return;
}
if (extras) extras = eval(extras);
else extras = [];
var coords = getMouseCoordinates(event);
var text = tooltipText[tooltip];
if (!text) text = '';
// apply extras
if (tooltip == 'verified')
{
if (extras[0] === true) text = text.replace('<b>Verified:</b>', '<b class="highlight">Verified:</b>');
else if (extras[0] === false) text = text.replace('<b>Unverified:</b>', '<b class="highlight">Unverified:</b>');
}
var div = document.createElement('div');
div.id = 'tooltip-' + tooltip;
div.className = 'si-tooltip';
div.style.left = (coords[0] + 5) + 'px';
div.style.top = (coords[1] + 5) + 'px';
// add the close button
div.innerHTML = '<div class="si-close-tooltip" title="Close this box">[<a href="#" onclick="hideTooltip(\'' + tooltip + '\'); return false;">X</a>]</div>';
div.innerHTML += text;
body.appendChild(div);
};
function sortScripts(first, second)
{
if (first[1]) var a = first[1];
else var a = getBasePage(first[0]).capitalize();
if (second[1]) var b = second[1];
else var b = getBasePage(second[0]).capitalize();
if (a < b) return -1;
else if (a > b) return 1;
else return 0;
};
// ====================================================================================================
// ============================================ general.js ============================================
// ====================================================================================================
function addClass(element, newClass)
{
if (element.className)
{
var classes = element.className.split(' ');
classes.push(newClass);
return element.className = classes.join(' ');
}
else return element.className = newClass;
};
String.prototype.capitalize = function(string)
{
return this.replace(/(^|\s)([a-z])/g, function(m, p1, p2) { return p1 + p2.toUpperCase(); } );
};;
function checkURLParam(param)
{
if (!document.location.search || document.location.search.length == 0) return false;
var params = document.location.search.substr(1).split('&');
for (var i = 0; i < params.length; i++)
{
var paramParts = params[i].split('=');
if (paramParts[0] == param) return true;
}
return false;
};
function convertArrayToHash(array)
{
var hash = {};
for (var i = 0; i < array.length; i++) hash[array[i]] = true;
return hash;
};
function convertIdToSpan(id)
{
var node = document.getElementById(id);
var span = document.createElement('span');
span.id = id;
var children = node.childNodes;
for (var i = children.length - 1; i >= 0; i--)
span.insertBefore(children[i], span.firstChild);
node.parentNode.replaceChild(span, node);
return span;
};
function createLink(title)
{
var title = title.trim();
if (title.indexOf('http://') == 0) return title;
else return wgScript + '?title=' + encodeURIComponent(title);
};
function createLinkForBacklinks(script)
{
return mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=Special:WhatLinksHere/' + script + '&namespace=2&hideredirs=1&hidetrans=1&limit=50';
};
function errorMessage(code)
{
alert(standardErrorMessage + ' (' + code.toUpperCase() + ')');
};
function generateTooltip(name, extras)
{
return '<span class="si-question-mark">(<a href="#" onclick="showTooltip(event, \'' + name + '\', \'' + (extras ? extras : '') + '\'); return false;">?</a>)</span>';
};
function has(haystack, needle)
{
if (haystack instanceof Array)
{
for (var i = 0; i < haystack.length; i++)
{
if (haystack[i] == needle)
return true;
}
}
return false;
};
function hasClass(element, classToCheck)
{
if (typeof(element) == 'undefined' || !element.className) return false;
var classes = element.className.split(' ');
for (var i = 0; i < classes.length; i++)
if (classes[i] == classToCheck) return true;
return false;
};
function isANumber(number)
{
return !isNaN(parseInt(number));
};
function isUnsafe()
{
if (typeof(unsafeWindow) != 'undefined') return true;
else return false;
};
function siLog(message, alert)
{
if (typeof(console) != 'undefined' && alert !== true) console.log(message);
else window.alert(message);
};
String.prototype.ltrim = function(stringToTrim)
{
return this.replace(/^[\s|\n]+/, '');
};
function objectLength(obj)
{
var size = 0, key;
for (key in obj) if (obj.hasOwnProperty(key)) size++;
return size;
};
String.prototype.pluralize = function(count, plural)
{
if (plural == null) var plural = this + 's';
return (count == 1 ? this : plural)
};
function removeClass(element, oldClass)
{
if (!element.className) return false;
var classes = element.className.split(' ');
var newClasses = [];
for (var i = 0; i < classes.length; i++)
{
if (classes[i] != oldClass)
newClasses.push(classes[i]);
}
return element.className = newClasses;
};
function reverseHash(oldHash)
{
var newHash = {};
for (var key in oldHash) newHash[oldHash[key]] = key;
return newHash;
};
String.prototype.rtrim = function(stringToTrim)
{
return this.replace(/[\s|\n]+$/, '');
};
function sanitizeHTML(data)
{
return data.replace(/</g, '<').replace(/>/g, '>');
};
function stripHTML(html)
{
var tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent||tmp.innerText;
};
String.prototype.trim = function(stringToTrim)
{
return this.replace(/^[\s|\n]+|[\s|\n]+$/g, '');
};
String.prototype.truncate = function(maxLength, truncateFromStart)
{
if (this.length <= maxLength) return this;
if (truncateFromStart)
return '...' + this.substring(this.length - maxLength + 3, this.length);
else
return this.substring(0, maxLength - 3) + '...';
};
// ====================================================================================================
// ============================================ getters.js ============================================
// ====================================================================================================
function findAssociatedScript()
{
var relatedPage = document.getElementById('si-related-page');
if (!scriptData['page'] || !relatedPage) return false;
var split = scriptData['page'].split('/');
var last = split[split.length - 1];
if (last.indexOf('.') == -1 || !linkExtensions[last.substring(last.indexOf('.') + 1, last.length)]) return false;
otherPage = scriptData['page'].substring(0, scriptData['page'].indexOf('.', scriptData['page'].indexOf(last)) + 1) + reverseLinkExtensions[oppScriptType];
var associatedScripts = getConvertedCookie('associated-scripts');
if (associatedScripts[pageName]) markAssociatedScript(otherPage);
else wikiApi('associatedScriptCallback', 'action=query&prop=info&indexpageids=1&titles=' + otherPage.replace(/ /g, '_'));
};
function findExistingScript(name, content)
{
// FIXME This function doesn't actually find importScript; only the script name itself. Not necessarily a problem, though.
var searchedText = ('\n' + content + '\n').replace(/\\n/g, '\n');
if (searchedText.indexOf(name) == -1) return false; // didn't find the script
else if (searchedText.search(new RegExp('\n(.*?)//(.*?)' + name)) != -1) // found it commented out
{
searchedText = searchedText.replace(new RegExp('\n(.*?)//(.*?)' + name + '(.*?)\n', 'g'), '\n$1\n'); // remove the commented out versions
if (searchedText.search(new RegExp('\n(.*?)' + name)) == -1) return false; // we can no longer find it
else return true; // we found it even though the commented out copies are removed, so it's definitely installed
}
else return true; // it's found and it's not commented out
};
function getBasePage(page)
{
var pages = page.split('/');
if (!pages[1]) return false;
var hasExt = pages[pages.length - 1].indexOf('.');
if (hasExt == -1) return false;
return pages[pages.length - 1].substring(0, hasExt);
};
function getIsViewingSkin()
{
var basePage = getBasePage(pageName);
for (var i = 0; i < allSkins.length; i++)
if (allSkins[i] == basePage) return true;
return false;
};
function getMouseCoordinates(e)
{
var posx = 0;
var posy = 0;
if (!e) var e = window.event;
if (e.pageX || e.pageY)
{
posx = e.pageX;
posy = e.pageY;
}
else if (e.clientX || e.clientY)
{
posx = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
posy = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return [posx, posy];
};
function getOppScriptType()
{
if (scriptType == 'stylesheet') return 'script';
else return 'stylesheet';
};
function getScriptMetadata(maxLength)
{
// get comments classes
var classes = {};
classes['multi'] = content.getElementsByClassName('coMULTI');
classes['single'] = content.getElementsByClassName('co1');
var existingMatches = {};
for (var commentClass in classes)
{
if (classes[commentClass].length > 100) var shorterLength = 100;
else var shorterLength = classes[commentClass].length;
for (var i = 0; i < shorterLength; i++)
{
var singleClass = classes[commentClass][i];
var value = singleClass.firstChild.nodeValue;
// find metadata in the element
existingMatches = hasScriptMetadata(value, existingMatches, maxLength);
if (existingMatches['done']) break;
}
if (existingMatches['done']) break;
}
delete existingMatches['done'];
return existingMatches;
};
function getScriptType(data)
{
if (data['type'])
{
for (var name in scriptClasses)
if (scriptClasses[name] == data['type']) return data['type'];
}
var pre = document.getElementsByTagName('pre')[0];
if (!pre) return false;
var classes = pre.className.split(' ');
var className = classes[0];
return scriptClasses[className]
};
function hasAssociatedScript(findScript)
{
var script = document.getElementById('mw-script-doc');
if (!script) return false;
var links = script.getElementsByTagName('a');
if (links.length >= 3) return links[2].href;
else if (isViewingSkin && links.length >= 2 && !hasClass(links[1], 'new')) return links[1].href;
else return false;
};
function hasDocumentation()
{
var docs = document.getElementById('mw-script-doc');
if (!docs) return false;
if (docs.getElementsByClassName('new').length > 0) return false;
// get the documentation page
else if (!isViewingSkin) return docs.getElementsByTagName('a')[1].href;
};
function hasScriptMetadata(string, existingMatches, maxLength)
{
var string = ('\n' + string + '\n').replace(/\\n/g, '\n');
for (var i = 0; i < singleScriptData.length; i++)
{
if (existingMatches[singleScriptData[i]]) continue;
var matches = string.match(new RegExp('@' + singleScriptData[i] + '\\s+(.*?)\n'));
if (matches) existingMatches[singleScriptData[i]] = sanitizeHTML(matches[1].trim());
else continue;
if (isANumber(maxLength)) existingMatches[singleScriptData[i]] = existingMatches[singleScriptData[i]].truncate(maxLength);
}
if (singleScriptData.length == objectLength(existingMatches)) existingMatches['done'] = true;
return existingMatches;
};
function installedScripts(content)
{
var searchedText = ('\n' + content + '\n').replace(/\\n/g, '\n');
// remove comments with an importScript type
for (var type in importScriptTypes)
searchedText = searchedText.replace(new RegExp('\n(.*?)//(.*?)' + importScriptTypes[type] + '(.*?)\n', 'g'), '\n$1\n');
// collect importScript types, line by line
var matches = [];
var lines = searchedText.split('\n');
for (var type in importScriptTypes)
{
if (type == 'external') continue;
for (var i = 0; i < lines.length; i++)
{
var pattern = new RegExp('(' + importScriptTypes[type] + '\\s*\\(\\s*[\'|"].*?[\'|"]\\s*\\))');
var match = lines[i].match(pattern);
if (match)
{
match = match[1].replace(/.*['|"](.*?)['|"].*/, '$1');
var verified = verifiedScripts[match];
if (verified) matches.push([match, verified['name'], verified['documentation']]);
else matches.push([match]);
}
}
}
return matches.sort(sortScripts);
};
function isSiInProduction()
{
for (var setting in siSettings)
{
if (!siSettings[setting])
return false;
}
return true;
};
// ====================================================================================================
// ============================================== init.js =============================================
// ====================================================================================================
/**
* @fileoverview Initiating the script
*/
/*
@name Installer or Script Installer or Easy Script Installer?
@description Simplifies the installation and configuration process for user scripts.
*/
if (typeof(isUnsafe) == 'function' && isUnsafe())
{
addPortletLink = unsafeWindow.addPortletLink;
appendCSS = unsafeWindow.appendCSS;
importScriptURI = unsafeWindow.importScriptURI;
sajax_init_object = unsafeWindow.sajax_init_object;
skin = unsafeWindow.skin;
wgCanonicalNamespace = unsafeWindow.wgCanonicalNamespace;
wgPageName = unsafeWindow.wgPageName;
wgScript = unsafeWindow.wgScript;
wgScriptPath = unsafeWindow.wgScriptPath;
wgServer = unsafeWindow.wgServer;
wgUserName = unsafeWindow.wgUserName;
wgFormattedNamespaces = unsafeWindow.wgFormattedNamespaces;
};
if (typeof(ScriptInstaller) == 'undefined') ScriptInstaller = {};
/**
* This is the only function loaded in ONLOAD.
* This script is intended to work on two pages: script pages (ending in .js) and script documentation pages.
* Primarily these two, anyway.
*/
function scriptInstaller()
{
if (typeof(isSiInProduction) != 'function' || typeof(doDocumentationPage) != 'function')
return false;
/*
Useful param disablers:
?installations: does not retrieve number of installations for scripts
*/
// Settings. These all default to TRUE.
// FIXME The following settings should be different when this script is in beta, and especially production mode.
siSettings = {};
siSettings['checkIfScriptIsInstalled'] = 1; // If false, don't check if a script is installed
// FIXME When this is true, the installations for the Script Library are in the wrong order.
siSettings['enableSICookies'] = 0; // If false, then cookies are disabled, so no cached data is retrieved. Data is still cached, however.
siSettings['useRealLibraryLink'] = 1; // If false, links to the sandbox
siSettings['useSkinPageForInstalls'] = 0; // If false, uses test page instead
siIsInProduction = isSiInProduction();
siScriptName = 'Script Installer'; // the current script's name
maxLengthForScriptMetadata = 250; // maximum length for script metadata on single script pages
ageOfCookies = 365; // age for cookies; set to 365, but in reality we still update cookies behind the scenes when they are different from newly acquired data
// tooltip text
tooltipText =
{
'installed-by': 'Only users that install this script on a <a href="' + createLink('Wikipedia:Skin#Customisation (advanced users)') + '">personal JavaScript</a> page are counted (including yourself).',
'metadata': '<b>Script metadata:</b> Information about the script that is provided by the script itself.', // Hence, <a href="' + createLink('Metadata') + '">metadata</a>.
'number-of-scripts': '<b>Scripts installed here:</b> The number of scripts found on this page that are imported with importScript().<br /><br />Does not include scripts installed using importScriptURI(), which are typically scripts not located on the <a href="' + createLink('English Wikipedia') + '">English Wikipedia</a> and are therefore limited in the amount of information that can be obtained about them.',
'personal-user-script': '<b>Personal user script:</b> A script that contains all the scripts installed by a given user. Installing this will install all scripts that the user has installed themselves.',
'verified': '<b>Verified:</b> The script has been tested. It is known to work.<br /><b>Unverified:</b> The script has <em>not</em> been tested.'
};
// useful variables
installPage = (siSettings['useSkinPageForInstalls'] || (!siSettings['useSkinPageForInstalls'] && wgUserName != 'Gary King') ? 'User:' + wgUserName + '/' + skin + '.js' : 'User:Gary King/scripts.js');
// 'class used in PRE': 'what to call the current script\'s type'
scriptClasses = { 'css': 'stylesheet', 'javascript': 'script' };
importScriptTypes = { 'script': 'importScript', 'stylesheet': 'importStylesheet', 'external': 'importScriptURI' };
body = document.getElementsByTagName('body')[0];
content = document.getElementById('content');
if (!content) return;
// the following are used for highestBox, in this order
jsfile = document.getElementById('jsfile');
jsWarning = document.getElementById('jswarning');
clearCache = document.getElementById('clearprefcache');
standardErrorMessage = 'An error occurred. Please try again later.';
pageName = wgPageName.replace(/_/g, ' ');
documentationData = ['page', 'type'];
scriptData = parseScriptData();
scriptType = getScriptType(scriptData);
oppScriptType = getOppScriptType();
allSkins = ['chick', 'standard', 'cologneblue', 'modern', 'monobook', 'myskin', 'nostalgia', 'simple', 'vector'];
allSkinsHash = convertArrayToHash(allSkins);
isViewingSkin = getIsViewingSkin();
libraryPage = (siSettings['useRealLibraryLink'] ? 'Wikipedia:Script Library' : 'User:Gary King/My Scripts');
linkExtensions = {'css': 'stylesheet', 'js': 'script'};
reverseLinkExtensions = reverseHash(linkExtensions);
singleScriptData = ['name', 'description'];
jumpToNav = document.getElementById('jump-to-nav');
usersWithBetaAccess = [/*'Gary King', 'Gary Queen'*/];
// only allow verified users to use this script, for now
if (!siIsInProduction && usersWithBetaAccess.length > 0)
{
var hasAccess = checkIfUserCanAccessSI();
if (!hasAccess)
{
if (wgUserName != null)
alert('You do not have access to the \"' + siScriptName + '\" script while it is in beta mode.');
return false;
}
}
// TODO Disable this script with a config setting, for myself only so I can test this script in Greasemonkey while it still exists in my skin.js.
// add settings, if different from default, to siteSub. Only on Script Library and single script pages, and only if still in testing mode.
if (!siIsInProduction && (wgUserName == 'Gary King') && (pageName == libraryPage || (clearCache && scriptType))) addSISettingsToPage();
// add portlet link for script library
if (addPortletLink) mw.util.addPortletLink('p-tb', createLink(libraryPage), 'Script Library', 't-script-library', 'Go to the Script Library');
// identify documentation pages
if (wgCanonicalNamespace != '' && wgCanonicalNamespace != 'Template') doDocumentationPage();
// replace imported scripts using importScript and importScriptURI with clickable links for easier access to them
// this works on not only monobook.js, but on script documentation pages, etc. as well
if (wgCanonicalNamespace != '') linksReplaced = replaceImportedScriptsWithLinks();
// do script library stuff if viewing the library page
if (pageName == libraryPage) doScriptLibrary();
// we are viewing an actual script page
if (clearCache && scriptType)
{
// a global variable; "currentScript' should eventually be replaced with 'pageName'
currentScript = pageName;
// now that we know we are viewing a script, add the install message box
addInstallMessageBox(pageName);
// check if script is already installed or not, and replace text appropriately
getInstallPage(pageName, installPage, (siSettings['checkIfScriptIsInstalled'] ? true : false), false);
}
};
if (typeof(isUnsafe) == 'function' && isUnsafe())
{
unsafeWindow.addTitleTooltips = addTitleTooltips;
unsafeWindow.associatedScriptCallback = associatedScriptCallback;
unsafeWindow.beginScriptInstallation = beginScriptInstallation;
unsafeWindow.hideTooltip = hideTooltip;
unsafeWindow.scriptInstallationsCallback = scriptInstallationsCallback;
unsafeWindow.sendScriptInstallation = sendScriptInstallation;
unsafeWindow.showTooltip = showTooltip;
unsafeWindow.singleScriptInstalls = singleScriptInstalls;
};
if (typeof(isUnsafe) == 'function' && typeof(isSiInProduction) == 'function'/* && typeof(verifiedScripts) != 'undefined'*/)
{
// On wiki
if (typeof(addOnloadHook) != 'undefined')
{
if (typeof(siSettings) == 'undefined')
{
addOnloadHook(scriptInstaller);
addOnloadHook(addTitleTooltips);
}
}
// Off wiki
else
{
if (typeof(siSettings) == 'undefined')
{
scriptInstaller();
addTitleTooltips();
}
}
}// ====================================================================================================
// ============================================= layout.js ============================================
// ====================================================================================================
/**
* Add the install message box to a single script page
*/
function addInstallMessageBox(currentScript)
{
var boxes = [jsfile, jsWarning, clearCache, jumpToNav.nextSibling];
for (var i = 0; i < boxes.length; i++)
{
if (boxes[i])
{
var highestBox = boxes[i];
break;
}
}
if (!highestBox) return false;
var div = document.createElement('div');
div.id = 'si-message-box';
var checking = document.createElement('span');
checking.className = 'si-heading si-loading';
checking.id = 'install-this-script';
checking.appendChild(document.createTextNode('Checking if script is already installed...'));
// if viewing a user's personal script, then indicate so
var isViewingSkinNode = document.createElement('div');
isViewingSkinNode.className = 'si-text';
isViewingSkinNode.id = 'is-viewing-skin';
if (isViewingSkin && currentScript != installPage) isViewingSkinNode.innerHTML = '<span id="si-careful">Careful!</span> This is a <a href="' + createLink('Wikipedia:Skin#Customisation (advanced users)') + '">personal user script</a> ' + generateTooltip('personal-user-script') + '.';
// if there are scripts installed on this page, then indicate how many there are
var replaced = document.createElement('div');
replaced.id = 'si-number-of-scripts';
// FIXME This says "X scripts installed here" even though they could be stylesheets installed here, too.
if (typeof(linksReplaced) == 'object' && linksReplaced.length > 0)
replaced.innerHTML = '<b>' + linksReplaced.length + ' ' + 'script'.pluralize(linksReplaced.length) + '</b> installed here ' + generateTooltip('number-of-scripts');
// FIXME Don't show the following if we're viewing the user's skin.js
var verified = document.createElement('div');
verified.id = 'si-verified';
verified.className = 'si-verification-status ' + (verifiedScripts[currentScript] ? 'si-verified' : 'si-not-verified');
verified.innerHTML = (verifiedScripts[currentScript] ? 'Verified' : 'Unverified') + ' ' + generateTooltip('verified', '[verifiedScripts[currentScript] ? true : false]');
var installedByCookie = getConvertedCookie('installed-by');
if (installedByCookie[currentScript]) var numberOfInstallers = installedByCookie[currentScript];
else var numberOfInstallers = '';
// FIXME Don't show the following if we're viewing the user's skin.js
var installedBy = document.createElement('div');
installedBy.id = 'si-installed-by';
installedBy.className = 'si-installed-by';
installedBy.innerHTML = ' Installed by <a href="' + createLinkForBacklinks(currentScript) + '" id="installed-by-link"><span class="unknown" id="installed-by-X-people">?</span> <span id="people-plural" class="si-people">people</span></a> ' + generateTooltip('installed-by');
// also insert other text, below the heading created above
var text = document.createElement('div');
text.id = 'script-description';
text.className = 'si-text';
// create menu
var menuItems = [];
// find documentation
var documentation = hasDocumentation();
if (documentation) menuItems.push('<a href="' + documentation + '" id="si-documentation">Documentation</a>');
else if (!scriptData) menuItems.push('<span id="si-documentation">No documentation</span>');
// find stylesheet
var script = hasAssociatedScript();
if (script) menuItems.push('<a href="' + script + '" id="si-related-page">Related ' + oppScriptType + '</a>');
else menuItems.push('<span id="si-related-page">No ' + oppScriptType + '</span>');
text.innerHTML = menuItems.join(' · ');
// get the script's metadata
var metadata = getScriptMetadata(maxLengthForScriptMetadata);
var metadataArray = [];
for (var data in metadata) metadataArray.push('<span class="si-metadata-name">' + data.capitalize() + '</span>: <span class="si-metadata-data">' + metadata[data] + '</span>');
// add script metadata
var metadata = document.createElement('div');
metadata.id = 'si-metadata';
if (metadataArray.length > 0) metadata.innerHTML = '<b>Script info ' + generateTooltip('metadata') + ':</b> ' + metadataArray.join(' · ');
// order of the message box parts
var parts = [checking, isViewingSkinNode, replaced, verified, installedBy, text, metadata];
var partsWithNoFollowingBullet = [checking, isViewingSkinNode, text, metadata];
for (var i = 0; i < parts.length; i++)
{
if (!parts[i].firstChild) continue;
div.appendChild(parts[i]);
var addBullet = true;
for (var j = 0; j < partsWithNoFollowingBullet.length; j++)
{
var noBullets = partsWithNoFollowingBullet[j];
if (noBullets == parts[i])
{
addBullet = false;
break;
}
}
if (addBullet) div.appendChild(document.createTextNode(' · '));
}
// insert the message box into the page
highestBox.parentNode.insertBefore(div, highestBox);
// get number of installations for this script
if (numberOfInstallers)
doSingleScriptInstalls(numberOfInstallers);
else
getInstallations('singleScriptInstalls', currentScript);
// check if associated script exists via a callback, only for documentation pages
findAssociatedScript();
};
/**
* Adds the script's settings, if different from default, to siteSub
*/
function addSISettingsToPage()
{
// Disabled settings in $siScriptName: ...
var disabledSettings = [];
for (var setting in siSettings)
{
if (!siSettings[setting])
disabledSettings.push('<u>' + setting + '</u>');
}
var string = '<strong>Disabled settings</strong> in <em>' + siScriptName + '</em>: ' + disabledSettings.join(', ') + '. ';
document.getElementById('siteSub').innerHTML = string + document.getElementById('siteSub').innerHTML + '.';
};
/**
* Check if the user has access to this script while its in beta mode
*/
function checkIfUserCanAccessSI()
{
for (var i = 0; i < usersWithBetaAccess.length; i++)
{
if (usersWithBetaAccess[i] == wgUserName)
return true;
}
return false;
};
function doDocumentationPage()
{
if (!scriptData) return false;
addInstallMessageBox(scriptData['page']);
getInstallPage(scriptData['page'], installPage, (siSettings['checkIfScriptIsInstalled'] ? true : false), false);
};
/**
* Draw the Script Library.
*/
function doScriptLibrary()
{
// we are viewing the script library page
var myLib = document.getElementById('my-scripts');
if (!myLib) return false;
// heading
var heading = document.createElement('h2');
heading.className = 'installed-scripts-heading';
heading.innerHTML = 'My Scripts';
myLib.appendChild(heading);
// descriptive text
var text = document.createElement('span');
text.className = 'installed-scripts-description';
text.innerHTML = 'Your <span id="si-number-of-scripts">scripts</span> ' + generateTooltip('number-of-scripts') + ' are installed at <strong><a href="' + createLink(installPage) + '">' + installPage + '</a></strong>. <span id="si-verified">Script names in <strong>bold</strong> have been <strong>verified</strong> ' + generateTooltip('verified') + '.</span><br />' + tooltipText['installed-by'] + ' You have the following scripts installed:<br />';
myLib.appendChild(text);
// installed scripts
var scripts = document.createElement('div');
scripts.id = 'si-installed-scripts';
var loading = document.createElement('span');
loading.id = 'loading';
loading.className = 'si-scripts-are-loading si-loading';
loading.appendChild(document.createTextNode('Loading...'));
scripts.appendChild(loading);
myLib.appendChild(scripts);
var scriptLibraryCookie = getConvertedCookie('script-library', true);
// create the list of scripts
if (scriptLibraryCookie.length > 0)
// Script Library cookie already exists
{
doUserLibrary(scriptLibraryCookie);
// FIXME Should be updating the cookie when the script library contents have changed.
// Is it comparing the old list of scripts with the one existing in the cookie?
var scripts = installedScripts(content);
setConvertedCookie('script-library', scripts);
}
else
// Script Library cookie does not exist, so create it.
{
getInstallPage(false, installPage, false, buildUserLibrary);
}
// script gallery
var galleryHeading = document.createElement('h2');
galleryHeading.className = 'si-script-gallery';
galleryHeading.innerHTML = 'Script Gallery';
myLib.appendChild(galleryHeading);
};
function doUserLibrary(scripts)
{
var installed = document.getElementById('si-installed-scripts');
if (!installed) return;
// remove loading text
installed.removeChild(document.getElementById('loading'));
var numberOfScriptsInstalled = scripts.length;
var counter = document.createElement('span');
counter.className = 'si-hidden';
counter.id = 'callback-counter';
counter.appendChild(document.createTextNode(0));
installed.appendChild(counter);
// create counter with a script name
var scriptCounter = document.createElement('span');
scriptCounter.id = 'si-script-counter';
scriptCounter.className = 'si-hidden';
installed.appendChild(scriptCounter);
for (var i = 0; i < scripts.length; i++)
{
var script = scripts[i][0];
var scriptName = scripts[i][1];
var docs = scripts[i][2] ? scripts[i][2] : '';
var node = document.createElement('div');
node.className = 'script-library-item';
node.id = script.replace(/ /g, '_');
// truncate the script name from the beginning if it's too long
var scriptLink = '<a href="' + createLink(script) + '">' + (scriptName ? scriptName : script).truncate(50, true) + '</a>';
// bold the script's name if it's verified
if (scriptName) scriptLink = '<strong>' + scriptLink + '</strong>';
if (docs) var documentation = '(<a class="si-documentation-link" href="' + createLink(docs) + '">documentation</a>)';
else var documentation = '';
var installations = '<a class="script-installations" href="' + createLinkForBacklinks(script) + '" id="installation-link-' + i + '"><span id="installation-' + i + '">?</span> <span id="installation-plural-' + i + '">installations</span></a>';
node.innerHTML = '<span class="script-name">' + scriptLink + ' ' + documentation + '</span>' + installations + '<span class="si-hidden si-script-name" id="si-script-name-' + i + '">' + script + '</span><span class="si-script-counter si-hidden">' + i + '</span>';
installed.appendChild(node);
// get number of installations
if (checkURLParam('installations')) continue;
var installedByCookie = getConvertedCookie('installed-by');
if (installedByCookie[script]) doScriptInstallation(installedByCookie[script]);
else getInstallations('scriptInstallationsCallback', script);
}
var numberOfScripts = document.getElementById('si-number-of-scripts');
numberOfScripts.replaceChild(document.createTextNode(numberOfScriptsInstalled + ' scripts'), numberOfScripts.firstChild);
};
function replaceImportedScriptsWithLinks()
{
// find .js links in code
var scriptLinks = content.getElementsByClassName('st0');
var replacedLinks = [];
for (var i = 0; i < (scriptLinks.length > 250 ? 250 : scriptLinks.length); i++)
{
// trim the script text to get just the actual name
var sL = scriptLinks[i];
if (sL.childNodes.length > 1 || !sL.firstChild.nodeValue) continue;
var nodeValue = sL.firstChild.nodeValue;
var open = nodeValue.trim().replace(/^(['|"]).*/g, '$1').trim();
var close = nodeValue.trim().replace(/^['|"].*(['|"])/g, '$1').trim();
var title = nodeValue.trim().replace(/^['|"]|['|"]$/g, '').trim();
// check if this is an imported script
var prevSibling = sL.previousSibling.previousSibling;
if (prevSibling && prevSibling.nodeValue) var functionName = prevSibling.nodeValue.trim();
else var functionName = '';
var isLegit = false;
for (var importType in importScriptTypes)
{
if (importType == 'external') continue;
if (functionName == importScriptTypes[importType])
isLegit = true;
}
if (!isLegit) continue;
// create the link to replace the existing text with
var a = document.createElement('a');
a.href = createLink(title);
a.appendChild(document.createTextNode(title));
var span = document.createElement('span');
span.appendChild(document.createTextNode(open));
span.appendChild(a);
span.appendChild(document.createTextNode(close));
sL.replaceChild(span, sL.firstChild);
replacedLinks.push(title);
}
return replacedLinks;
};
// ====================================================================================================
// ============================================== misc.js =============================================
// ====================================================================================================
/**
* Create an edit summary
*/
function siEditSummary(text)
{
return text + ' with [[WP:Script Installer]]';
};