Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/*
* Adds custom module to the Wikia Rail
*/
(function () {
/*
* @returns validated and normalized list of rail module configurations
*/
function getConfiguredMods() {
var numPrependedMods = 0;
return (window.AddRailModule || (function () {
// Interpret deprecated settings (for backwards compatibility)
var usesDeprecatedARMModules = ((typeof(window.ARMModules) !== 'undefined') &&
$.isArray(window.ARMModules) &&
window.ARMModules.every(function (e) { return (typeof(e) === 'string'); })),
usesDeprecatedARMPrepend = (typeof(window.ARMPrepend) !== 'undefined');
if (usesDeprecatedARMModules && usesDeprecatedARMPrepend) {
return window.ARMModules.map(function (e) { return {page: e, prepend: Boolean(window.ARMPrepend)}; });
} else if (usesDeprecatedARMModules) {
return window.ARMModules;
} else if (usesDeprecatedARMPrepend) {
return [{prepend: Boolean(window.ARMPrepend)}];
}
}()) || [{}]).map(function (mod) {
// Expand shorthand syntax and validate config
if (typeof(mod) === 'string') {
mod = {page: mod};
}
return {
page: (typeof(mod.page) === 'string') ? mod.page : 'Template:RailModule',
prepend: (typeof(mod.prepend) === 'boolean') ? (mod.prepend && (++numPrependedMods <= 2)) : false,
maxAge: (typeof(mod.maxAge) === 'number') ? Math.min(Math.max(0, Math.round(mod.maxAge)), 86400) : 300,
};
});
}
function fetchPages(context, pages) {
var deferred = $.Deferred(),
mangledIdsToPage = {},
pagesToHTML = {},
markup = '';
pages.forEach(function (page) {
var mangledId = 'arm:' + btoa(encodeURIComponent(page)).replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
mangledIdsToPage[mangledId] = page;
// Set a default if (for some reason) we fail to fetch/parse HTML for this page
pagesToHTML[page] = '';
});
// We don't want the order in which pages are specified to affect the "canonical" form of the `text` param.
Object.keys(mangledIdsToPage).sort().forEach(function (mangledId) {
markup += '<div id="' + mangledId + '">{' + '{' + mangledIdsToPage[mangledId] + '}}</div>';
});
$.getJSON(mw.util.wikiScript('api'), {
format: 'json',
action: 'parse',
title: context.mwConfig.wgPageName,
text: markup,
prop: 'text',
uselang: context.mwConfig.wgUserLanguage,
disablepp: '',
// Cache rail module contents on the CDN for 10 minutes for anonymous users
maxage: 600,
smaxage: 600
}).done(function (response) {
var tempContainer = document.createElement('div');
tempContainer.innerHTML = response.parse.text['*'];
var $tempContainer = $(tempContainer);
if ($tempContainer.children().length === 1) {
var $child = $($tempContainer.children()[0]);
if ($child.hasClass('mw-parser-output')) {
$tempContainer = $child;
}
}
var $renderedMods = $tempContainer.children().filter('div[id^="arm:"]');
$renderedMods.each(function (i, elem) {
if (mangledIdsToPage.hasOwnProperty(elem.id)) {
pagesToHTML[mangledIdsToPage[elem.id]] = elem.innerHTML;
}
});
deferred.resolve(pagesToHTML);
}).fail(deferred.reject.bind(deferred));
return deferred;
}
function getPages(context, pagesToMaxAge) {
// Out of Wikia's set of supported browsers, only IE11 doesn't support the Cache API.
// Since the Cache API is built on promises, we won't explicitly test for `'Promise' in window`.
if (!('caches' in window)) {
return fetchPages(context, Object.keys(pagesToMaxAge));
}
var deferred = $.Deferred();
function fallback() {
fetchPages(context, Object.keys(pagesToMaxAge))
.done(deferred.resolve.bind(deferred))
.fail(deferred.reject.bind(deferred));
caches['delete']('AddRailModule-v0');
}
caches.open('AddRailModule-v0').then(function (cache) {
var pagesToHTML = {},
pagesToFetch = [];
function toCacheRequest(context, page) {
return new Request(mw.util.wikiScript('AddRailModule') + '?' + $.param({
text: '{' + '{' + page + '}}',
uselang: context.mwConfig.wgUserLanguage,
user: context.mwConfig.wgUserName,
}));
}
function purgeCache() {
// Purge stale or invalid entries
return cache.keys().then(function (requests) {
return Promise.all(requests.map(function (request) {
if (!request.url.startsWith(window.location.origin)) {
return cache['delete'](request);
}
return cache.match(request).then(function (response) {
if (!(response && response.ok && (Date.parse(response.headers.get('Expires')) > context.now))) {
return cache['delete'](request);
}
});
}));
});
}
function fetchFromCache() {
// Populate results from cache
return Promise.all(Object.keys(pagesToMaxAge).map(function (page) {
return cache.match(toCacheRequest(context, page)).then(function (response) {
if (response) {
return response.text().then(function (html) {
pagesToHTML[page] = html;
});
} else {
pagesToFetch.push(page);
}
});
}));
}
function fetchMissingFromOriginAndResolve() {
if (!pagesToFetch.length) {
deferred.resolve(pagesToHTML);
} else {
// Fetch cache misses
fetchPages(context, pagesToFetch).done(function (fetchedPagesToHTML) {
$.each(fetchedPagesToHTML, function (page, html) {
pagesToHTML[page] = html;
if (pagesToMaxAge[page]) {
cache.put(toCacheRequest(context, page), new Response(html, {headers: {
'Expires': (new Date(context.now + (pagesToMaxAge[page] * 1000))).toUTCString(),
}}));
}
});
deferred.resolve(pagesToHTML);
}).fail(deferred.reject.bind(deferred));
}
}
purgeCache()
.then(fetchFromCache, fallback)
.then(fetchMissingFromOriginAndResolve, fallback);
}, fallback);
return deferred;
}
function prepareMods(context, mods) {
var deferred = $.Deferred(),
pagesToMods = {},
pagesToMaxAge = {};
mods.forEach(function (mod) {
if (!pagesToMods.hasOwnProperty(mod.page)) {
// Account for the remote possibility that we'll encounter multiple rail modules backed by the same page.
pagesToMods[mod.page] = [mod];
pagesToMaxAge[mod.page] = mod.maxAge;
} else {
pagesToMods[mod.page].push(mod);
if (mod.maxAge < pagesToMaxAge[mod.page]) {
pagesToMaxAge[mod.page] = mod.maxAge;
}
}
});
getPages(context, pagesToMaxAge).done(function (pagesToHTML) {
$.each(pagesToHTML, function (page, html) {
pagesToMods[page].forEach(function (mod) {
mod.section = document.createElement('section');
mod.section.className = 'railModule rail-module';
mod.section.dataset.addRailModulePage = mod.page;
mod.section.innerHTML = html;
// Per <https://html.spec.whatwg.org/commit-snapshots/f476180797e6124074b3cfeaf1973ea39eb6c499/#the-script-element>,
// script tags generated via innerHTML don't execute when inserted into the DOM.
// Since we want these inline scripts (generated by parser tags like gallery/poll/rss/tabview
// and typically comprising invocations of `JSSnippetsStack.push`) to execute, we'll have to
// swap out the tainted script tags with pristine ones. Note that we're trusting no other scripts
// have maliciously tampered with our cache.
var taintedScripts = mod.section.getElementsByTagName('script');
for (var i = 0; i < taintedScripts.length; ++i) {
var taintedScript = taintedScripts[i];
var untaintedScript = document.createElement('script');
untaintedScript.innerHTML = taintedScript.innerHTML;
taintedScript.parentNode.replaceChild(untaintedScript, taintedScript);
}
});
});
deferred.resolve();
}).fail(deferred.reject.bind(deferred));
return deferred;
}
function attachMods(mods, attachFragment) {
var fragment = document.createDocumentFragment();
mods.forEach(function (mod) { fragment.appendChild(mod.section); });
attachFragment(fragment);
mods.forEach(function (mod) {
var $section = $(mod.section);
mw.hook('wikipage.content').fire($section);
mw.hook('AddRailModule.module').fire(mod.page, $section);
});
}
function bootstrap() {
var $rail, railLoaded,
mods, modsToPrepend, modsToAppend,
context, modsPrepared;
$rail = $('#WikiaRail');
if (!$rail[0]) { return; }
railLoaded = $.Deferred();
if ($rail.filter('.loaded, .is-ready')[0]) {
railLoaded.resolve();
} else {
$rail.on('afterLoad.rail', function () { railLoaded.resolve(); });
}
mods = getConfiguredMods();
if (!mods.length) { return; }
modsToPrepend = [];
modsToAppend = [];
mods.forEach(function (mod) {
(mod.prepend ? modsToPrepend : modsToAppend).push(mod);
});
context = {
mwConfig: mw.config.get([
'wgPageName',
'wgUserLanguage',
'wgUserName',
]),
now: Date.now(),
};
modsPrepared = prepareMods(context, mods);
// In Fandom's infinite wisdom, `#WikiaRail` is now a React root for the recently-introduced "Recent Images" rail module.
// To prevent our custom rail modules from getting clobbered, we'll insert them before and after `#WikiaRail` instead.
// This has been reported through Zendesk as #1270911.
$.when(railLoaded, modsPrepared).done(function () {
var $railWrapper = $rail.parent('.right-rail-wrapper');
if (!$railWrapper[0]) { return; }
var $ads;
if (modsToPrepend.length) {
// Top ads live in `#rail-boxad-wrapper`, which is `#WikiaRail`'s previous sibling.
$ads = $railWrapper.children('#rail-boxad-wrapper').last();
attachMods(modsToPrepend, $ads[0] ? function (fragment) {
$railWrapper[0].insertBefore(fragment, $ads[0].nextSibling);
} : function (fragment) {
$railWrapper[0].insertBefore(fragment, $rail[0]);
});
}
if (modsToAppend.length) {
// Bottom ads live in `.sticky-modules-wrapper > #WikiaAdInContentPlaceHolder`, which is `#WikiaRail`'s sibling.
$ads = $railWrapper.children('.sticky-modules-wrapper').first();
attachMods(modsToAppend, $ads[0] ? function (fragment) {
$railWrapper[0].insertBefore(fragment, $ads[0]);
} : function (fragment) {
$railWrapper[0].insertBefore(fragment, $rail[0].nextSibling);
});
}
});
}
bootstrap();
}());