// ====================================================================================================
// ============================================== 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]]';
};