/** * User script to stash and replay conflicts. * * The conflicting edit text is stored under a special title like, * Conflict:/<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(); } ); });