MediaWiki:Gadget-DiscutiRevisioneBlocco.js

Da Wikipedia, l'enciclopedia libera.
Vai alla navigazione Vai alla ricerca

Questa pagina definisce alcuni parametri di aspetto e comportamento generale di tutte le pagine. Per personalizzarli vedi Aiuto:Stile utente.


Nota: dopo aver salvato è necessario pulire la cache del proprio browser per vedere i cambiamenti (per le pagine globali è comunque necessario attendere qualche minuto). Per Mozilla / Firefox / Safari: fare clic su Ricarica tenendo premuto il tasto delle maiuscole, oppure premere Ctrl-F5 o Ctrl-R (Command-R su Mac); per Chrome: premere Ctrl-Shift-R (Command-Shift-R su un Mac); per Konqueror: premere il pulsante Ricarica o il tasto F5; per Opera può essere necessario svuotare completamente la cache dal menù Strumenti → Preferenze; per Internet Explorer: mantenere premuto il tasto Ctrl mentre si preme il pulsante Aggiorna o premere Ctrl-F5.

/**
 * Questo accessorio automatizza tutte le operazioni necessarie per avviare la
 * discussione sulla revisione di un blocco.
 * Quando l'utente clicca il pulsante "Crea la pagina di discussione" del
 * template:Revisione blocco, visualizza una richiesta di conferma e poi
 * riceve riscontro delle operazioni in corso in una finestra di dialogo.
 *
 * @author https://it.wikipedia.org/wiki/Utente:Sakretsu
 */
/* global mediaWiki, jQuery, OO */

( function ( mw, $ ) {
	'use strict';

	const conf = mw.config.get( [
		'wgArticleId',
		'wgCurRevisionId',
		'wgNamespaceNumber',
		'wgRelevantUserName',
		'wgRevisionId',
		'wgUserGroups',
		'wgUserName'
	] );
	const dependencies = [
		'ext.gadget.CommentWidget',
		'mediawiki.api',
		'mediawiki.util',
		'oojs-ui-core',
		'oojs-ui-widgets',
		'oojs-ui-windows'
	];
	const monthNames = [
		'gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno',
		'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'
	];
	const rfcRootPage = conf.wgRelevantUserName === conf.wgUserName ?
		`Utente:${ conf.wgUserName }/Sandbox/Test revisione del blocco` :
		'Wikipedia:Richieste di revisione del blocco';
	const rfcTemplate = 'Wikipedia:Richieste di revisione del blocco/ModelloDiscussione';

	/**
	 * Cerca le parti commentate e le parti fra i tag nowiki nel wikitesto
	 * passato in input.
	 * Restituisce un array di tuple contenenti la posizione di inizio e fine di
	 * ciascuna parte disabilitata trovata (oppure soltanto la posizione delle
	 * parti commentate, se così richiesto).
	 * 
	 * @param {string} wikitext - Il wikitesto in cui effettuare la ricerca
	 * @param {boolean} commentedPartsOnly - Indica se bisogna restituire solo le
	 *  parti totalmente disabilitate, ovvero quelle commentate con <!-- e -->
	 * @return {object}
	 */
	function findDisabledParts( wikitext, commentedPartsOnly ) {
		const disabledParts = [];
		// regex per trovare sia le parti commentate sia le parti fra i tag nowiki
		// (quelle che iniziano con '<!--' possono anche non essere chiuse)
		const regex = /(<!--[\s\S]*?(-->|$)|<nowiki(\s*|\s[^>]*)>[\s\S]*?<\/nowiki\s*>)/g;

		// ciclo che si ripete per ogni match trovato
		while ( true ) {
			const match = regex.exec( wikitext );

			if ( !match ) break;

			// se commentedPartsOnly è true salta le parti fra i tag nowiki,
			// comprese le parti fra <!-- e --> che sono annidate al loro interno
			if ( commentedPartsOnly && match[ 0 ].startsWith( '<nowiki' ) ) {
				continue;
			}

			// memorizza la posizione di inizio e fine della parte disabilitata
			disabledParts.push( [ match.index, regex.lastIndex ] );
		}

		return disabledParts;
	}

	/**
	 * Verifica se la posizione passata in input si trova in una delle parti di
	 * testo che risultano disabilitate.
	 * Assume che le parti di testo disabilitate siano state ricavate con la
	 * funzione findDisabledParts.
	 * 
	 * @param {int} index - La posizione da verificare
	 * @param {object} disabledParts - L'array delle tuple contenenti la posizione
	 *  di inizio e fine delle parti di testo disabilitate
	 * @return {boolean}
	 */
	function isInDisabledPart( index, disabledParts ) {
		return disabledParts.some( ( [ start, end ] ) => index >= start && index < end );
	}

	/**
	 * Verifica se è stato compilato solo il parametro 'motivo' del
	 * template:Revisione blocco passato in input.
	 * 
	 * @param {string} templateText - Il testo del template
	 * @return {boolean}
	 */
	function isAppealNew( templateText ) {
		const commentedParts = findDisabledParts( templateText, true );

		// rimuove le parti commentate
		for ( let i = commentedParts.length - 1; i >= 0; i-- ) {
			const [ start, end ] = commentedParts[ i ];
			templateText = templateText.substring( 0, start ) + templateText.substring( end );
		}

		return !/\|\s*(esito|link discussione)\s*=\s*[^\s\|\}]/.test( templateText ) &&
			/\|\s*motivo\s*=\s*[^\s\|\}]/.test( templateText );
	}

	/**
	 * Cerca i template:Revisione blocco nel wikitesto passato in input.
	 * Restituisce il testo del primo template che corrisponde a una nuova richiesta.
	 * 
	 * @param {string} wikitext - Il wikitesto in cui effettuare la ricerca
	 * @return {string}
	 */
	function findNewAppeal( wikitext ) {
		let newAppeal;
		const disabledParts = findDisabledParts( wikitext, false );
		const templateNameRegex = isOwnSandbox() ?
			/\{\{\s*[Rr]evisione *blocco(\/Sandbox)?\s*\|/g :
			/\{\{\s*[Rr]evisione *blocco\s*\|/g;

		// ciclo che si ripete per ogni match di inizio template:Revisione blocco
		// trovato (es. '{{Revisione blocco' o '{{revisione blocco')
		while ( true ) {
			const templateNameMatch = templateNameRegex.exec( wikitext );

			if ( !templateNameMatch ) break;

			// salta i match di inizio template trovati nelle parti di testo disabilitate
			if ( isInDisabledPart( templateNameMatch.index, disabledParts ) ) {
				continue;
			}

			// mantiene il conto delle parentesi graffe aperte trovate
			let unclosedBracketCount = 2;
			const bracketRegex = /(\{\{ *(?=[^ \{])|\}\})/g;
			const stringAfterTemplateNameMatch = wikitext.substring(
				templateNameRegex.lastIndex
			);

			// ciclo che si ripete per ogni match di doppie parantesi graffe (sia
			// aperte sia chiuse) trovate nel testo che viene dopo le graffe aperte
			// matchate nel ciclo esteriore
			while ( true ) {
				const bracketMatch = bracketRegex.exec( stringAfterTemplateNameMatch );

				if ( !bracketMatch ) break;

				const bracketMatchStart = templateNameRegex.lastIndex + bracketMatch.index;
				const bracketMatchEnd = templateNameRegex.lastIndex + bracketRegex.lastIndex;

				// controlla che le doppie parentesi graffe trovate non siano
				// disabilitate e aggiorna il conto delle graffe aperte
				if ( isInDisabledPart( bracketMatchStart, disabledParts ) ) {
					continue;
				} else if ( bracketMatch[ 0 ] === '{{' ) {
					unclosedBracketCount += 2;
				} else if ( bracketMatch[ 0 ] === '}}' ) {
					unclosedBracketCount -= 2;
				}

				// estrae il testo del template:Revisione blocco appena il conto
				// delle parentesi graffe aperte si azzera
				if ( unclosedBracketCount === 0 ) {
					const templateText = wikitext.substring(
						templateNameMatch.index,
						bracketMatchEnd
					);

					// verifica che il template estratto sia quello cercato
					if ( isAppealNew( templateText ) ) {
						newAppeal = templateText;
					}

					break;
				}
			}

			if ( newAppeal ) break;
		}

		return newAppeal;
	}

	/**
	 * Restituisce una data in formato d mmmm yyyy.
	 * 
	 * @param {object} date - L'oggetto che rappresenta la data
	 * @return {string}
	 */
	function formatDate( date ) {
		return date
			.toLocaleDateString( 'it-IT', { timeZone: 'Europe/Berlin' } )
			.replace(
				/\/\d\d?\//,
				match => ` ${ monthNames[ match.replaceAll( '/', '' ) - 1 ] } `
			);
	}

	/**
	 * Restituisce il link a una pagina wiki.
	 * 
	 * @param {string} title - Il titolo della pagina da linkare
	 * @return {string}
	 */
	function link( title ) {
		return `<a href=${ mw.util.getUrl( title ) }>${ title }</a>`;
	}

	/**
	 * Effettua una chiamata API per chiedere la modifica di una pagina.
	 * 
	 * @param {object} customParams - I parametri della chiamata che si
	 *  integrano con quelli precompilati (o li sovrascrivono)
	 * @return {jQuery.Promise}
	 */
	function editContent( customParams ) {
		return new mw.Api( {
				parameters: {
					action: 'edit',
					format: 'json',
					watchlist: 'nochange'
				}
			} ).postWithToken( 'csrf', customParams );
	}

	/**
	 * Effettua una chiamata API per ottenere il contenuto di una pagina.
	 * 
	 * @param {object} customParams - I parametri della chiamata che si
	 *  integrano con quelli precompilati (o li sovrascrivono)
	 * @return {jQuery.Promise}
	 */
	function getContent( customParams ) {
		return new mw.Api( {
				parameters: {
					action: 'query',
					format: 'json',
					formatversion: 2,
					prop: 'revisions',
					rvprop: [ 'ids', 'content', 'timestamp' ],
					rvslots: 'main'
				}
			} ).get( customParams );
	}

	/**
	 * Crea la discussione sulla revisione richiesta dall'utente bloccato e
	 * la linka o include in tutte le pagine correlate.
	 * 
	 * @param {string} reviewingAdminComment - Il parere dell'admin che crea la discussione.
	 * @param {function} progressMsgHandler - La funzione per mostrare progressi.
	 * @param {function} errorMsgHandler - La funzione per mostrare errori.
	 * @param {function} successMsgHandler - La funzione per mostrare successo.
	 * @return {jQuery.Promise}
	 */
	function createRequestForComment(
		reviewingAdminComment, progressMsgHandler, errorMsgHandler, successMsgHandler
	) {
		const currentDate = formatDate( new Date() );
		const rfcTitle = `${ rfcRootPage }/${ conf.wgRelevantUserName }/${ currentDate }`;
		let currentRevision, newAppeal;

		// ottiene il wikitesto della talk visualizzata e cerca al suo interno
		// il template della richiesta di revisione aperta
		progressMsgHandler( mw.msg( 'fetchingTalkPage' ) );

		return getContent( { pageids: conf.wgArticleId } ).then( result => {
			currentRevision = result.query.pages[ 0 ].revisions[ 0 ];
			newAppeal = findNewAppeal( currentRevision.slots.main.content );

			if ( !newAppeal ) {
				return $.Deferred().reject( 'templatemissing' );
			}

			// compila il modello predefinito della discussione (il motivo della
			// richiesta di revisione è ottenuto tramite subst del t:Revisione blocco)
			const text = `{{subst:${ rfcTemplate }` +
				'|nome richiedente=' + conf.wgRelevantUserName +
				'|motivo richiesta=' + newAppeal.replace( '{{', '{{subst:' ) +
				'|oldid motivo=' + currentRevision.revid +
				'|data apertura discussione=' + currentDate +
				'|primo parere=' + reviewingAdminComment +
				'}}';

			// crea la pagina dedicata alla discussione sulla revisione del blocco
			progressMsgHandler( mw.msg( 'creatingRfC', link( rfcTitle ) ) );

			return editContent( {
				title: rfcTitle,
				text: text,
				summary: mw.msg( 'rfcEditSummary' ),
				createonly: 1,
				watchlist: 'watch'
			} );
		} ).then( () => {
			const archivePage = `${ rfcRootPage }/${ conf.wgRelevantUserName }`;

			// linka la pagina di discussione nell'archivio delle discussioni che
			// si sono tenute sui blocchi dello stesso utente
			progressMsgHandler( mw.msg( 'updatingArchive', link( archivePage ) ) );

			return editContent( {
				title: archivePage,
				appendtext: `\n# [[${ rfcTitle }]]`,
				summary: mw.msg( 'archiveEditSummary', currentDate )
			} );
		} ).then( () => {
			// include la pagina di discussione nella pagina di servizio dove sono
			// elencate le discussioni sulle revisioni in corso
			progressMsgHandler( mw.msg( 'transcludingRfC', link( rfcRootPage ) ) );

			return editContent( {
				title: rfcRootPage,
				appendtext: `\n\n{{${ rfcTitle }}}`,
				summary: mw.msg( 'rfcRootPageEditSummary', conf.wgRelevantUserName )
			} );
		} ).then( () => {
			// aggiorna il template:Revisione blocco nella talk dell'utente
			// compilando il parametro "link discussione" col nome della pagina
			// di discussione appena creata
			progressMsgHandler( mw.msg( 'notifyingUser' ) );

			const text = currentRevision.slots.main.content.replace(
				newAppeal,
				newAppeal.replace( /(\|\s*link discussione\s*= ?|(?=\n? *\}\}$))/, match => {
					if ( match.includes( 'link discussione' ) ) {
						return match + rfcTitle;
					} else {
						const pre = /\n *\|/.test( newAppeal ) ? '\n' : '';
						return `${ pre }|link discussione=${ rfcTitle }`;
					}
				} )
			);

			return editContent( {
				pageid: conf.wgArticleId,
				text: text,
				summary: mw.msg( 'talkPageEditSummary', rfcTitle ),
				starttimestamp: currentRevision.timestamp,
				baserevid: currentRevision.revid
			} );
		} ).done( () => {
			successMsgHandler( mw.msg( 'success' ) );
		} ).fail( code => {
			let errorText = mw.msg( 'errorOccurred' ) + ' ';

			switch( code ) {
				case 'articleexists':
				case 'templatemissing':
					errorText += mw.msg( code );
					break;
				default:
					errorText += mw.msg( 'unknownError', code );
			}

			errorMsgHandler( errorText );
		} );
	}

	/**
	 * Crea una finestra di dialogo che mostra gli aggiornamenti sulle
	 * operazioni in corso.
	 * 
	 * @return {object} - L'oggetto che rappresenta la finestra
	 */
	function createProgressDialog() {
		function ProgressDialog() {
			ProgressDialog.super.call( this,  { size: 'large' } );
		}

		OO.inheritClass( ProgressDialog, OO.ui.Dialog );

		ProgressDialog.static.name = 'progressDialog';
		ProgressDialog.static.title = mw.msg( 'dialogTitle' );

		ProgressDialog.prototype.initialize = function () {
			ProgressDialog.super.prototype.initialize.call( this );

			this.content = new OO.ui.PanelLayout( {
				padded: true,
				expanded: false
			} );
			this.message = new OO.ui.LabelWidget();
			this.closeButton = new OO.ui.ButtonWidget( {
				label: mw.msg( 'closeButtonLabel' )
			} );

			this.content.$element.css( {
				'min-height': '240px'
			} );
			this.title.$element.css( {
				'display': 'block',
				'font-size': '1.5em',
				'padding-bottom': '.25em',
				'text-align': 'center'
			} );
			this.message.$element.css( {
				'display': 'block',
				'font-size': '1.1em'
			} );
			this.closeButton.toggle( false );

			this.content.$element.append(
				this.title.$element,
				this.message.$element,
				this.closeButton.$element
			);
			this.$body.append( this.content.$element );

			this.closeButton.connect( this, { click: 'close' } );
		};

		ProgressDialog.prototype.appendMsg = function ( text, styles ) {
			this.message.$element.append( $( '<p>' ).css( styles ).html( text ) );
			this.updateSize();
		};

		ProgressDialog.prototype.appendProgressMsg = function ( text ) {
			this.appendMsg( `\u2022 ${ text }`, { 'color': '#202122' } );
		};

		ProgressDialog.prototype.appendErrorMsg = function ( text ) {
			this.appendMsg( text, {
				'color': 'red',
				'padding': '.15em 0'
			} );
		};

		ProgressDialog.prototype.appendSuccessMsg = function ( text ) {
			this.appendMsg( text, {
				'font-size': 'large',
				'padding-top': '.25em',
				'text-align': 'center'
			} );
		};

		ProgressDialog.prototype.showCloseButton = function () {
			this.closeButton.toggle( true );
			this.updateSize();
		};

		return new ProgressDialog();
	}

	/**
	 * Verifica se l'utente che sta usando l'accessorio è un amministratore.
	 * 
	 * @return {boolean}
	 */
	function isUserAdmin() {
		return conf.wgUserGroups.includes( 'sysop' );
	}

	/**
	 * Verifica se la pagina visualizzata è una sandbox dell'utente che sta
	 * usando l'accessorio.
	 * 
	 * @return {boolean}
	 */
	function isOwnSandbox() {
		return conf.wgNamespaceNumber === 2 &&
			conf.wgRelevantUserName === conf.wgUserName;
	}

	/**
	 * Verifica se la pagina visualizzata rientra fra quelle dove è previsto che
	 * sia eseguito l'accessorio.
	 * 
	 * @return {boolean}
	 */
	function isPageValid() {
		// consente di testare l'accessorio nelle proprie sandbox
		if ( isOwnSandbox() ) {
			return true;
		}

		return conf.wgNamespaceNumber === 3 &&
			conf.wgRelevantUserName !== null &&
			conf.wgArticleId !== 0 &&
			conf.wgCurRevisionId === conf.wgRevisionId;
	}

	$( () => {
		const newRfCButton = $( '.pulsante-discuti-revisione-blocco' );

		// termina l'esecuzione se riscontra anomalie
		if ( !isPageValid() || !isUserAdmin() || !newRfCButton.length ) return;

		// aspetta il corretto caricamento delle dipendenze prima di procedere
		mw.loader.using( dependencies ).done( () => {
			let windowManager, commentWidget;

			// carica i messaggi di sistema dell'accessorio
			mw.messages.set( require( './DiscutiRevisioneBlocco-Messages.json' ) );

			// modifica il comportamento del pulsante "Crea la pagina di discussione"
			newRfCButton.on( 'click', () => {
				event.preventDefault();

				// avvisa l'utente che non può esserci più di una richiesta aperta
				if ( newRfCButton.length > 1 ) {
					OO.ui.alert( mw.msg( 'toomanyappeals' ), {
						title: mw.msg( 'errorOccurred' )
					} );
					return;
				}

				if ( !commentWidget ) {
					const CommentWidget = require( 'ext.gadget.CommentWidget' );

					// crea un'area di testo dove l'utente può inserire il suo
					// parere sulla revisione del blocco e confermare
					commentWidget = new CommentWidget( {
						commentIndent: '*',
						placeholder: mw.msg( 'commentBodyPlaceholder' ),
						storageId: conf.wgArticleId
					} ).on( 'detach', () => {
						newRfCButton.find( 'input' ).addClass( 'mw-ui-progressive' );
					} ).on( 'confirm', data => {
						if ( !windowManager ) {
							commentWidget.setReadOnly( true );
							commentWidget.confirmButton
								.on( 'click', () => commentWidget.emit( 'confirm' ) )
								.disconnect( commentWidget, { click: 'onConfirmClick' } );

							// crea la finestra che mostra le operazioni in corso
							const progressDialog = createProgressDialog();
							windowManager = new OO.ui.WindowManager();

							$( 'body' ).append( windowManager.$element );
							windowManager.addWindows( [ progressDialog ] );

							// esegue le operazioni tenendo aggiornata la finestra
							createRequestForComment(
								data.value,
								text => progressDialog.appendProgressMsg( text ),
								text => progressDialog.appendErrorMsg( text ),
								text => progressDialog.appendSuccessMsg( text )
							).done(
								() => commentWidget.clearStorage()
							).always(
								() => progressDialog.showCloseButton()
							);
						}

						windowManager.openWindow( 'progressDialog' );
					} );
					commentWidget.disconnect( commentWidget, { confirm: 'teardown' } );
					commentWidget.cancelButton
						.connect( commentWidget, { click: 'detach' } )
						.disconnect( commentWidget, { click: 'tryTeardown' } );
				}

				if ( !commentWidget.isElementAttached() ) {
					newRfCButton.find( 'input' ).removeClass( 'mw-ui-progressive' );
					newRfCButton.after( commentWidget.$element );
				}

				commentWidget.focus().scrollElementIntoView();
			} );
		} );
	} );
}( mediaWiki, jQuery ) );