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 = {

/**	 * Div created by Module:Synchronizer that will hold the Synchronizer table * @type {Object} jQuery object containing the div */	$div: null,

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

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

Synchronizer.initDiffDialog; },

load: function {

// Do basic checks var hash = location.hash; if ( !hash ) { return Synchronizer.error( 'Hash not found.'); }		var $div = $( hash ); if ( !$div.length ) { return Synchronizer.error( 'Div with ID ' + hash + ' not found.'); }		Synchronizer.$div = $div; var page = $div.data( 'page' ).trim; if ( !page ) { return Synchronizer.error( 'Page not set.' ); }		$div.text( 'Loading...' );

// Set master wiki var master = $div.data( 'master' ); if ( !master ) { master = mw.config.get( 'wgWikiID' ); }

// Set basic master data Synchronizer.master = { wiki: master, page: page, status: 'Master', };

// Get wikidata data Synchronizer.getWikidataData; },

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

getMasterData: function { var api = new mw.ForeignApi( Synchronizer.master.api ); api.get( {			formatversion: 2,			action: 'query',			prop: 'revisions',			rvprop: 'timestamp|sha1|content',			rvslots: 'main',			titles: Synchronizer.master.page		} ).done( function ( data ) {			var revision = data.query.pages[0].revisions[0];			Synchronizer.master.time = new Date( revision.timestamp );			Synchronizer.master.sha1 = revision.sha1;			Synchronizer.master.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 api.get( {				formatversion: 2,				action: 'query',				prop: 'revisions',				rvprop: 'sha1|ids',				rvlimit: 'max',				titles: Synchronizer.master.page			} ).done( function ( data ) {				var page = data.query.pages[0];				Synchronizer.master.revisions = page.revisions;				var sitelink;				for ( var key in Synchronizer.master.sitelinks ) {					sitelink = Synchronizer.master.sitelinks[ key ];					var remote = {						wiki: sitelink.site,						page: sitelink.title,						url: sitelink.url,						api: sitelink.url.replace( /\/wiki\/.+/, '/w/api.php' )					};					Synchronizer.updateStatus( remote );				}			} ); } );	},

updateStatus: function ( remote ) { new mw.ForeignApi( remote.api ).get( {			formatversion: 2,			action: 'query',			prop: 'revisions|info',			rvprop: 'timestamp|sha1',			inprop: 'protection',			meta: 'sitenifo',			siprop: 'namespaces', // @todo Use this to show lock icons for protected namespaces			titles: remote.page		} ).done( function ( data ) {			//console.log( data );			var page = data.query.pages[0];			var revision = page.revisions[0];			remote.sha1 = revision.sha1;			remote.timestamp = revision.timestamp;			var hashes = Synchronizer.master.revisions.map( function ( revision ) { return revision.sha1; } );			if ( remote.sha1 === Synchronizer.master.sha1 ) {				remote.status = 'Updated';			} else if ( hashes.includes( remote.sha1 ) ) {				remote.status = 'Outdated';			} else {				remote.status = 'Forked';			}			if ( page.protection.length ) {				remote.protection = page.protection;			}			Synchronizer.updateRow( remote ); } );	},

updateRow: function ( branch ) { var color, title, $button; switch ( branch.status ) { case 'Updated': color = '#afa'; title = 'The code of this module is the same as the master branch.'; break; case 'Outdated': color = '#ffa'; title = 'The code of this module is behind that of the master branch.'; $button = $( ' ' ).text( 'Update' ).click( function  {					$( this ).replaceWith( 'Loading diff...' );					Synchronizer.showDiff( branch );				} ); break; case 'Forked': color = '#faa'; title = 'The code of this module has diverged from the master branch.'; $button = $( ' ' ).text( 'Update' ).click( function  {					$( this ).replaceWith( 'Loading diff...' );					Synchronizer.showDiff( branch );				} ); break; }		var masterName = Synchronizer.master.page.substring( Synchronizer.master.page.indexOf( ':' ) + 1 ); var branchName = branch.page.substring( branch.page.indexOf( ':' ) + 1 ); var $alert; if ( masterName !== branchName ) { $alert = new OO.ui.IconWidget( {				icon: 'alert',				title: "This module is called '" + branchName + "' rather than '" + masterName + "'. This may break dependencies between synchronized modules."			} ).$element; $alert.css( {				'cursor': 'help',				'margin-left': '.5em',				'vertical-align': 'top'			} ); }		var $lock; if ( branch.protection ) { var level; for ( var protection of branch.protection ) { if ( protection.type === 'edit' ) { level = protection.level; }			}			if ( level ) { $lock = new OO.ui.IconWidget( {					icon: 'lock',					title: "This page is protected. Only '" + level + "' users may edit it."				} ).$element; $lock.css( {					'cursor': 'help',					'margin-left': '.5em',					'vertical-align': 'top'				} ); }		}		var $link = $( '' ).text( branch.page ).attr( 'href', branch.url ); var $td1 = $( ' ' ).text( branch.wiki ); var $td2 = $( ' ' ).append( $link, $lock, $alert ); var $td3 = $( ' ' ).text( branch.status ).attr( 'title', title ).css( { 'background-color': color, 'cursor': 'help' } ); var $td4 = $( ' ' ).append( $button ); var $div = Synchronizer.$div; var $row = $div.find( '.' + branch.wiki ); $row.empty.append( $td1, $td2, $td3, $td4 ); },

showDiff: function ( branch ) { new mw.ForeignApi( branch.api ).post( {			'formatversion': 2,			'action': 'compare',			'fromtitle': branch.page,			'toslots': 'main',			'totext-main': Synchronizer.master.content		} ).done( function ( data ) {			var diff = data.compare.body;			branch.diff = diff;

var windowManager = new OO.ui.WindowManager; $( document.body ).append( windowManager.$element ); var dialog = new Synchronizer.DiffDialog( { data: branch } ); windowManager.addWindows( [ dialog ] ); windowManager.openWindow( dialog ); } );	},

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.master.page;			var summary = 'Update from master using Synchronizer';			return {				text: Synchronizer.master.content,				summary: summary,				assert: 'user'			};		} ).done( function ( data ) {			//console.log( data );			remote.status = 'Updated';			remote.content = Synchronizer.master.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 ); } );	},

makeTable: function { var $table = $( ' ';		   Synchronizer.DiffDialog.super.prototype.initialize.apply( this, arguments );		    this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );		    this.content.$element.append( table );		    this.$body.append( this.content.$element );		};		Synchronizer.DiffDialog.prototype.getActionProcess = function ( action ) {		    var dialog = this;		    if ( action ) {		        return new OO.ui.Process( function  { Synchronizer.update( dialog.data ); dialog.close( { action: action } ); } );		   }		    return Synchronizer.DiffDialog.super.prototype.getActionProcess.call( this, action );		};	},

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

mw.loader.using( [	'oojs-ui-core',	'oojs-ui-widgets',	'oojs-ui-windows',	'oojs-ui.styles.icons-alerts',	'oojs-ui.styles.icons-moderation',	'mediawiki.diff.styles',	'mediawiki.ForeignApi', ], Synchronizer.init );