User:Adamw/replay conflicts.js

From Wikipedia, the free encyclopedia
Template:Script doc autoNote: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: hold down the Ctrl key and click the Refresh or Reload button. Firefox: hold down the Shift key while clicking Reload (or press Ctrl-Shift-R). Google Chrome and Safari users can just click the Reload button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * User script to stash and replay conflicts.
 *
 * The conflicting edit text is stored under a special title like,
 *   Conflict:<title>/<base_revision_id>/<conflict_sequence>
 * Where the placeholders are, the page title where the conflict occurred, the base revision ID that was being edited,
 * and a 1-based sequence which is ignored for now, and lets multiple conflicts share the same base revision.
 *
 * TODO:
 *   - Take edit summary from the saved conflict content edit summary?
 *   - Controls to do advanced things like tweaking the branchpoint and choosing an "other" edit other than
 *     the target page's latest revision.
 *   - Proper namespace for these pages.
 *   - UI labels from messages.
 */
mw.loader.using( [
	'mediawiki.api',
	'moment',
	'oojs-ui-core'
] ).done( function () {
	var api = new mw.Api();

	function fetchMyRevisionContent() {
		return api.get( {
			action: 'query',
			prop: 'revisions',
			revids: mw.config.get( 'wgCurRevisionId' ),
			rvprop: 'content'
		} ).then(
			function ( data ) {
				for ( var pageId in data.query.pages ) {
					for ( var index in data.query.pages[pageId].revisions ) {
						return data.query.pages[pageId].revisions[index];
					}
				}
			}
		);
	}

	/**
	 * Get timestamp of the original base revision, to convince EditPage we need to conflict.
	 * Edit time turns out to be important, conflict cannot be triggered without it!
	 */
	function fetchBaseRevisionTimestamp( baseRevisionId ) {
		return api.get( {
			action: 'query',
			prop: 'revisions',
			revids: baseRevisionId,
			rvprop: 'timestamp'
		} ).then(
			function ( data ) {
				for ( var pageId in data.query.pages ) {
					for ( var index in data.query.pages[pageId].revisions ) {
						var revision = data.query.pages[pageId].revisions[index];
						return moment( revision.timestamp ).utc().format( 'YYYYMMDDHHmmss' );
					}
				}
			}
		);
	}

	function buildConflictNamespaceForm( baseTitle, postFields ) {
		var form = new OO.ui.FormLayout( {
				// TODO: urlencode the title, or build the query using a helper.
				action: mw.config.get('wgScript' ) + '?title=' + baseTitle + '&action=submit',
				method: 'post'
			} ),
			recreateButton = new OO.ui.FieldsetLayout( {
				label: 'Conflict',
				items: [
					new OO.ui.ButtonWidget( {
						label: 'Recreate conflict',
						title: 'Submit',
					} ).on( 'click', function () {
						// TODO: clean up binding
						form.$element.submit();
					} )
				]
			} ),
			keys = Object.keys( postFields );

		form.addItems( [ recreateButton ] );

		for ( var i = 0; i < keys.length; i++ ) {
			form.addItems( [
				new OO.ui.HiddenInputWidget( {
					name: keys[i],
					value: postFields[keys[i]],
				} )
			] );
		}

		return form;
	}

	function initializeConflictNamespaceActions() {
		// TODO: The regex is missing the start anchor "^" so that it can be used as a subpage in various places,
		//  until the Conflict namespace exists.
		var conflictTitleParams = /Conflict:(.*)\/(\d+)\/(\d+)$/.exec( mw.config.get('wgPageName' ) );
		if ( conflictTitleParams !== null ) {
			var baseTitle = conflictTitleParams[1],
				baseRevisionId = conflictTitleParams[2],
				conflictSequence = conflictTitleParams[3];

			$.when(
				api.getEditToken(),
				fetchMyRevisionContent(),
				fetchBaseRevisionTimestamp( baseRevisionId )
			).done(
				// TODO: error handling
				function ( editToken, myRevision, editTime ) {
					var postFields = {
							wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',
							editRevId: baseRevisionId,
							parentRevId: baseRevisionId,
							// TODO: stored / input / automatic summary field.
							//wpSummary: '',
							wpSave: 'Save changes',
							mode: 'text',
							wpEditToken: editToken,
							wpTextbox1: myRevision['*'],
							format: myRevision.contentformat,
							model: myRevision.contentmodel,
							wpEdittime: editTime,
							wpUltimateParam: '1',
						},
						form = buildConflictNamespaceForm( baseTitle, postFields );

					$( '#bodyContent' ).prepend( form.$element );
				}
			);
		}
	}

	function handlePostpone() {
		var baseTitle = mw.config.get( 'wgPageName' ),
			baseRevisionId = $( '#editform input[name="parentRevId"]' ).val(),
			// TODO: Detect existing, deduplicate, and autoincrement.
			conflictSequenceId = 1,
			// TODO: Preference to put here or global "Conflict:"
			userName = mw.config.get( 'wgUserName' ),
			conflictTitle = 'User:' + userName + '/Conflict:' + baseTitle + '/' + baseRevisionId + '/' + conflictSequenceId,
			conflictUrl = mw.config.get( 'wgArticlePath' ).replace( '$1', conflictTitle ),
			// TODO: Use TwoColConflict merger to build potentially edited content, also compat with legacy workflow.
			content = $( '#wpTextbox2' ).val() ||
				$( '#editform input[name="mw-twocolconflict-your-text"]' ).val();

		api.create(
			conflictTitle,
			{
				// TODO: Take from attempted edit.
				summary: 'Created by conflict userscript',
			},
			content
		).then( function () {
			window.location.href = conflictUrl;
		} );
	}

	function buildStashButton() {
		return new OO.ui.ButtonWidget( {
			label: 'Postpone resolution',
		} ).on( 'click', handlePostpone );
	}

	function initializeConflictWorkflowActions() {
		if ( mw.config.get( 'wgEditMessage' ) !== 'editconflict' ) {
			return;
		}

		$( '.cancelLink' ).prepend( buildStashButton().$element );
	}

	$( function () {
		initializeConflictNamespaceActions();
		initializeConflictWorkflowActions();
	} );
});