MediaWiki:Synchronizer.js

/** * Synchronizer is a tool for synchronizing Lua modules and other pages across Wikimedia wikis * Documentation: https://www.mediawiki.org/wiki/Synchronizer * Author: User:Sophivorus * License: CC-BY-SA-3.0 */ var Synchronizer = {

/**	 * tag that will hold the HTML we will generate * @type {Object} jQuery object containing the */	$span: null,

/**	 * Data about the local page we will synchronize * @type {Object} Map from data key to data value */	local: null,

init: function { Synchronizer.realInit; $( window ).on( 'hashchange', Synchronizer.realInit ); },

realInit: function {

// Do basic checks var id = location.hash.replace( ':', '\\:' ).replace( '.', '\\.' ); // Escape special characters if ( !id ) { return Synchronizer.error( 'ID not found.'); }		var $span = $( id ); if ( !$span.length ) { return Synchronizer.error( 'Span with ID ' + id + ' not found.'); }		Synchronizer.$span = $span; var page = $span.data( 'page' ).trim; if ( !page ) { return Synchronizer.error( 'Page not set.' ); }		$span.text( 'Loading...' );

// Set basic local data Synchronizer.local = { wiki: mw.config.get( 'wgWikiID' ), page: page, status: 'Local', api: new mw.Api };

// Get wikidata data Synchronizer.getWikidataData; },

getWikidataData: function { new mw.ForeignApi( '//www.wikidata.org/w/api.php' ).get( {			action: 'wbgetentities',			sites: 'mediawikiwiki',			props: 'info|sitelinks/urls',			normalize: 1,			titles: Synchronizer.local.page		} ).done( function ( data ) {			var entity = Object.values( data.entities )[0];			if ( entity.hasOwnProperty( 'missing' ) ) {				return Synchronizer.error( 'No Wikidata item associated to ' + Synchronizer.local.page );			}			var sitelink = entity.sitelinks[ Synchronizer.local.wiki ]; // Save the local sitelink			delete entity.sitelinks[ Synchronizer.local.wiki ]; // Then remove it			if ( !entity.sitelinks ) {				return Synchronizer.error( 'No page associated to ' + Synchronizer.local.page );			}			Synchronizer.local.url = sitelink.url;			Synchronizer.local.item = entity.id;			Synchronizer.local.sitelinks = entity.sitelinks;			Synchronizer.makeTable;			Synchronizer.getLocalData;		} ); },

getLocalData: function { Synchronizer.local.api.get( {			formatversion: 2,			action: 'query',			prop: 'revisions',			rvprop: 'timestamp|sha1|content',			rvslots: 'main',			titles: Synchronizer.local.page		} ).done( function ( data ) {			var revision = data.query.pages[0].revisions[0];			Synchronizer.local.time = new Date( revision.timestamp );			Synchronizer.local.sha1 = revision.sha1;			Synchronizer.local.content = revision.slots.main.content;

// We need to do a separate API call to get 500 revisions // because "content" is considered an "expensive" property // so if we request all together we only get 50 revisions Synchronizer.local.api.get( {				formatversion: 2,				action: 'query',				prop: 'revisions',				rvprop: 'sha1',				rvlimit: 'max',				titles: Synchronizer.local.page			} ).done( function ( data ) {				var page = data.query.pages[0];				Synchronizer.local.hashes = page.revisions.map( function ( revision ) { return revision.sha1; } );				var sitelink;				for ( var key in Synchronizer.local.sitelinks ) {					sitelink = Synchronizer.local.sitelinks[ key ];					var remote = {						wiki: sitelink.site,						page: sitelink.title,						url: sitelink.url,						api: sitelink.url.replace( /\/wiki\/.+/, '/w/api.php' ) // Hacky but efficient					};					Synchronizer.updateStatus( remote );				}			} ); } );	},

updateStatus: function ( remote ) { new mw.ForeignApi( remote.api ).get( {			formatversion: 2,			action: 'query',			prop: 'revisions|info',			rvprop: 'timestamp|sha1',			inprop: 'protection',			titles: remote.page		} ).done( function ( data ) {			//console.log( data );			var page = data.query.pages[0];			var revision = page.revisions[0];			var sha1 = revision.sha1;			var time = new Date( revision.timestamp );			if ( sha1 === Synchronizer.local.sha1 ) {				remote.status = 'Updated';			} else if ( Synchronizer.local.hashes.includes( sha1 ) ) {				remote.status = 'Outdated';			} else {				remote.status = 'Forked';			}			if ( page.protection.length ) {				remote.protection = page.protection;			}			Synchronizer.updateRow( remote );		} ); },

update: function ( remote ) { remote.status = 'Updating...'; Synchronizer.updateRow( remote ); return new mw.ForeignApi( remote.api ).edit( remote.page, function ( revision ) {			var prefix = Synchronizer.getInterwikiPrefix( remote );			var master = prefix + ':' + Synchronizer.local.page;			var summary = 'Update from master using Synchronizer';			return {				text: Synchronizer.local.content,				summary: summary,				assert: 'user'			};		} ).done( function ( data ) {			//console.log( data );			remote.status = 'Updated';			remote.content = Synchronizer.local.content;			Synchronizer.updateRow( remote );		} ).fail( function ( error ) {			//console.log( error );			switch ( error ) {				case 'protectednamespace-interface':				case 'protectednamespace':				case 'customcssjsprotected':				case 'cascadeprotected':				case 'protectedpage':				case 'permissiondenied':					remote.status = 'No permission';					break;				case 'assertuserfailed':					remote.status = 'Not logged-in'; break; default: remote.status = 'Failed'; break; }			Synchronizer.updateRow( remote ); } );	},

updateRow: function ( remote ) { var color, $button, $icon; switch ( remote.status ) { case 'Updated': color = '#afa'; break; case 'Outdated': color = '#ffa'; $button = $( ' ' ).text( 'Update' ).click( function  {					Synchronizer.update( remote );				} ); break; case 'Forked': color = '#faa'; $button = $( ' ' ).text( 'Update' ).click( function  {					OO.ui.confirm( 'Carelessly updating a forked page may break things. Are you sure you want to continue?' ).done( function ( confirmed ) { if ( confirmed ) { Synchronizer.update( remote ); }					} );				} );				break; }		if ( remote.protection ) { $icon = $( ' 🔒 ' ).css( 'cursor', 'pointer' ).click( function {				var level;				for ( var protection of remote.protection ) {					if ( protection.type === 'edit' ) {						level = protection.level;					}				}				if ( level ) {					OO.ui.alert( "This page is protected. Only '" + level + "' users may edit it." );				}			} );		}		var $td3 = $( ' ' ).text( remote.status ).append( $icon ).css( 'background-color', color ); var $td4 = $( ' ' ).append( $button ); var $span = Synchronizer.$span; var $row = $span.find( '.' + remote.wiki ); $row.children( 'td' ).eq( 2 ).replaceWith( $td3 ); $row.children( 'td' ).eq( 3 ).replaceWith( $td4 ); },

makeTable: function { var $table = $( ' ' );

// Make header var $row = $( ' ' ); var $th1 = $( ' ' ).text( 'Wiki' ); var $th2 = $( ' ' ).text( 'Title' ); var $th3 = $( ' ' ).text( 'Status' ); var $th4 = $( ' ' ).text( 'Action' ); $row.append( $th1, $th2, $th3, $th4 ); $table.append( $row );

// Make local row $row = $( ' ' ); var $link = $( '' ).text( Synchronizer.local.page ).attr( 'href', Synchronizer.local.url ); var $td1 = $( ' ' ).text( Synchronizer.local.wiki ); var $td2 = $( ' ' ).html( $link ); var $td3 = $( ' ' ).text( 'Local' ).css( 'background-color', '#aff' ); var $td4 = $( ' ' ); $row.append( $td1, $td2, $td3, $td4 ); $table.append( $row );

// Make remote rows var sitelink; for ( var key in Synchronizer.local.sitelinks ) { sitelink = Synchronizer.local.sitelinks[ key ]; $row = $( ' ' ).addClass( sitelink.site ); $link = $( '' ).text( sitelink.title ).attr( 'href', sitelink.url ); $td1 = $( ' ' ).text( sitelink.site ); $td2 = $( ' ' ).html( $link ); $td3 = $( ' ' ).text( 'Loading...' ); $td4 = $( ' ' ); $row.append( $td1, $td2, $td3, $td4 ); $table.append( $row ); }

// Add to DOM Synchronizer.$span.html( $table ); },

getInterwikiPrefix: function ( remote ) { var prefix = Synchronizer.local.wiki; var localHost = location.host; var localParts = localHost.split( '.' ); var localLanguage = localParts[0]; var localProject = localParts[1]; var projects = [ 'wikipedia', 'wiktionary', 'wikinews', 'wikibooks', 'wikisource', 'wikiversity', 'wikiquote', 'wikivoyage', ];		if ( projects.includes( localProject ) ) { var remoteURL = new URL( remote.url ); var remoteHost = remoteURL.host; var remoteParts = remoteHost.split( '.' ); var remoteProject = remoteParts[1]; if ( remoteProject === localProject ) { prefix = localLanguage; } else { prefix = localProject + ':' + localLanguage; }		}		return prefix; },

/**	 * Make error message */	error: function ( message ) { if ( Synchronizer.$span && Synchronizer.$span.length ) { Synchronizer.$span.addClass( 'error' ).text( message ); } else { console.log( message ); }	} };

mw.loader.using( 'mediawiki.ForeignApi', Synchronizer.init );