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 */ window.Synchronizer = {

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

initTable: function { var id = location.hash; if ( !id ) { return console.log( 'Hash in URL not found.'); }		var $div = $( id ); if ( !$div.length ) { return console.log( 'Div with ID ' + id + ' not found.'); }		var table = new Synchronizer.Table( id ); table.init; },

Table: function ( id ) {

/**		 * Wrapper div created by Module:Synchronizer that will hold the table * @type {Object} jQuery object containing the wrapper div */		this.$div = $( id );

/**		 * Data about the master module * @type {Object} Map from data key to data value */		this.master = {};

this.init = function { var table = this;

table.$div.text( 'Loading...' );

// Get master page var page = table.$div.data( 'page' ).trim; if ( !page ) { return table.error( 'Page not set.' ); }

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

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

// Get wikidata data table.getWikidataData; };

this.getWikidataData = function { var table = this; new mw.ForeignApi( '//www.wikidata.org/w/api.php' ).get( {				action: 'wbgetentities',				sites: table.master.wiki,				props: 'info|sitelinks/urls',				normalize: 1,				titles: table.master.page			} ).done( function ( data ) {				var entity = Object.values( data.entities )[0];				if ( entity.hasOwnProperty( 'missing' ) ) {					return table.error( 'No Wikidata item associated to ' + table.master.page );				}				var sitelink = entity.sitelinks[ table.master.wiki ]; // Save the master sitelink				delete entity.sitelinks[ table.master.wiki ]; // Then remove it				if ( !entity.sitelinks ) {					return table.error( 'No page associated to ' + table.master.page );				}				table.master.url = sitelink.url;				table.master.api = sitelink.url.replace( /\/wiki\/.+/, '/w/api.php' );				table.master.item = entity.id;				table.master.sitelinks = entity.sitelinks;				table.makeTable;				table.getMasterData; } );		};		this.getMasterData = function {			var table = this;			var api = new mw.ForeignApi( table.master.api );			api.get( { formatversion: 2, action: 'query', prop: 'revisions', rvprop: 'sha1|content', rvslots: 'main', titles: table.master.page } ).done( function ( data ) { var revision = data.query.pages[0].revisions[0]; table.master.sha1 = revision.sha1; table.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: table.master.page				} ).done( function ( data ) {					var page = data.query.pages[0];					table.master.revisions = page.revisions;					var sitelink;					for ( var key in table.master.sitelinks ) {						sitelink = table.master.sitelinks[ key ];						var module = {							wiki: sitelink.site,							page: sitelink.title,							url: sitelink.url,							api: sitelink.url.replace( /\/wiki\/.+/, '/w/api.php' )						};						table.updateStatus( module );					}				} ); } );		},		this.updateStatus = function ( module ) {			var table = this;			new mw.ForeignApi( module.api ).get( { formatversion: 2, action: 'query', prop: 'revisions|info', rvprop: 'sha1', inprop: 'protection', meta: 'sitenifo', siprop: 'namespaces', // @todo Use this to show lock icons for protected namespaces titles: module.page } ).done( function ( data ) { //console.log( data ); var page = data.query.pages[0]; var revision = page.revisions[0]; module.sha1 = revision.sha1; var hashes = table.master.revisions.map( function ( revision ) { return revision.sha1; } ); if ( module.sha1 === table.master.sha1 ) { module.status = 'Updated'; } else { table.master.revisions.forEach( function ( revision, key ) {						if ( revision.sha1 === module.sha1 ) {							module.status = key + ' revision' + ( key === 1 ? '' : 's' ) + ' behind';							return;						}					} ); if ( !module.status ) { module.status = 'Forked'; }				}				if ( page.protection.length ) { module.protection = page.protection; }				table.updateRow( module ); } );		};		this.updateRow = function ( module ) {			var table = this;			var color, title, $button;			if ( module.status === 'Updated' ) {				color = '#afa';				title = 'The code of this module is the same as the master module.';			} else if ( module.status.includes( 'behind' ) ) {				color = '#ffa';				title = 'The code of this module is behind that of the master module.';				$button = $( ' ' ).text( 'Update' ).click( function  { $( this ).replaceWith( 'Loading diff...' ); table.showDiff( module ); } );			} else if ( module.status === 'Forked' ) {				color = '#faa';				title = 'The code of this module has diverged from the master module.';				$button = $( ' ' ).text( 'Update' ).click( function  { $( this ).replaceWith( 'Loading diff...' ); table.showDiff( module ); } );			}			var masterName = table.master.page.substring( table.master.page.indexOf( ':' ) + 1 );			var moduleName = module.page.substring( module.page.indexOf( ':' ) + 1 );			var $alert;			if ( masterName !== moduleName ) {				$alert = new OO.ui.IconWidget( { icon: 'alert', title: "This module is called '" + moduleName + "' rather than '" + masterName + "'. This may break dependencies between synchronized modules." } ).$element;				$alert.css( { 'cursor': 'help', 'margin-left': '.5em', 'vertical-align': 'top' } );			}			var $lock;			if ( module.protection ) {				var level;				for ( var protection of module.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( module.page ).attr( 'href', module.url );			var $td1 = $( ' ' ).text( module.wiki );			var $td2 = $( '  ' ).append( $link, $lock, $alert );			var $td3 = $( '  ' ).text( module.status ).attr( 'title', title ).css( { 'background-color': color, 'cursor': 'help' } );			var $td4 = $( '  ' ).append( $button );			var $row = table.$div.find( '.' + module.wiki );			$row.empty.append( $td1, $td2, $td3, $td4 );		};		this.showDiff = function ( module ) {			var table = this;			new mw.ForeignApi( module.api ).post( { 'formatversion': 2, 'action': 'compare', 'fromtitle': module.page, 'toslots': 'main', 'totext-main': table.master.content } ).done( function ( data ) { var diff = data.compare.body; module.diff = diff; var windowManager = new OO.ui.WindowManager; $( document.body ).append( windowManager.$element ); var dialog = new Synchronizer.DiffDialog( { data: {					'table': table,					'module': module,				} } ); windowManager.addWindows( [ dialog ] ); windowManager.openWindow( dialog ); } );		};		this.update = function ( module ) {			var table = this;			module.status = 'Updating...';			table.updateRow( module );			return new mw.ForeignApi( module.api ).edit( module.page, function ( revision ) { var master = 'd:Special:GoToLinkedPage/' + table.master.wiki + '/' + table.master.item; var summary = 'Update from master using Synchronizer #synchronizer'; return { text: table.master.content, summary: summary, assert: 'user' };			} ).done( function ( data ) { //console.log( data ); module.status = 'Updated'; module.content = table.master.content; table.updateRow( module ); } ).fail( function ( error ) { //console.log( error ); switch ( error ) { case 'protectednamespace-interface': case 'protectednamespace': case 'customcssjsprotected': case 'cascadeprotected': case 'protectedpage': case 'permissiondenied': module.status = 'No permission'; break; case 'assertuserfailed': module.status = 'Not logged-in'; break; default: module.status = 'Failed'; break; }				table.updateRow( module ); } );		};		this.makeTable = function {			var table = this;			var $table = $( ' ' );		    this.$body.append( this.content.$element );		};		Synchronizer.DiffDialog.prototype.getActionProcess = function ( action ) {		    if ( action ) {			    var dialog = this;				var table = dialog.data.table;				var module = dialog.data.module;			    if ( action === 'save' ) {		    		table.update( module );			    }			    if ( action === 'cancel' ) {		    		table.updateRow( module );			    }		        return new OO.ui.Process( function  { dialog.close; } );		   }		    return Synchronizer.DiffDialog.super.prototype.getActionProcess.call( this, action );		};	} };

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 );