// <nowiki>
// === Compiled with Novem Linguae's publish.php script ======================
$(async function() {
// === main.js ======================================================
/*
Forked from [[User:Enterprisey/unblock-review.js]] on Oct 31, 2024.
Many additional bugs fixed.
*/
/* global importStylesheet */
//
( function () {
const UNBLOCK_REQ_COLOR = 'rgb(235, 244, 255)';
const DEFAULT_DECLINE_REASON = '{{subst:Decline reason here}}';
const ADVERT = ' ([[User:Novem Linguae/Scripts/UnblockReview.js|unblock-review]])';
function execute() {
const userTalkNamespace = 3;
if ( mw.config.get( 'wgNamespaceNumber' ) !== userTalkNamespace ) {
return;
}
$.when( $.ready, mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ) ).then( () => {
// add styles
mw.util.addCSS(
'.unblock-review td { padding: 0 }' +
'td.reason-container { padding-right: 1em; width: 30em }' +
'.unblock-review-reason { height: 5em }' );
importStylesheet( 'User:Enterprisey/mw-ui-button.css' );
importStylesheet( 'User:Enterprisey/mw-ui-input.css' );
// look for user-block HTML class, which will correspond to {{Unblock}} requests
const userBlockBoxes = document.querySelectorAll( 'div.user-block' );
for ( let i = 0, n = userBlockBoxes.length; i < n; i++ ) {
if ( userBlockBoxes[ i ].style[ 'background-color' ] !== UNBLOCK_REQ_COLOR ) {
continue;
}
// We now have a pending unblock request - add UI
const unblockDiv = userBlockBoxes[ i ];
const [ container, hrEl ] = addTextBoxAndButtons( unblockDiv );
listenForAcceptAndDecline( container, hrEl );
}
} );
}
function addTextBoxAndButtons( unblockDiv ) {
const container = document.createElement( 'table' );
container.className = 'unblock-review';
// Note: The innerHtml of the button is sensitive. Is used to figure out which accept/decline wikitext to use. Don't add whitespace to it.
container.innerHTML = `
<tr>
<td class='reason-container' rowspan='2'>
<textarea class='unblock-review-reason mw-ui-input' placeholder='Reason for accepting/declining here'>${ DEFAULT_DECLINE_REASON }</textarea>
</td>
<td>
<button class='unblock-review-accept mw-ui-button mw-ui-progressive'>Accept</button>
</td>
</tr>
<tr>
<td>
<button class='unblock-review-decline mw-ui-button mw-ui-destructive'>Decline</button>
</td>
</tr>`;
const hrEl = unblockDiv.querySelector( 'hr' );
unblockDiv.insertBefore( container, hrEl.previousElementSibling );
return [ container, hrEl ];
}
function listenForAcceptAndDecline( container, hrEl ) {
const reasonArea = container.querySelector( 'textarea' );
$( container ).find( 'button' ).on( 'click', function () {
// look at the innerHtml of the button to see if it says "Accept" or "Decline"
const acceptOrDecline = $( this ).text().toLowerCase();
const appealReason = hrEl.nextElementSibling.nextElementSibling.childNodes[ 0 ].textContent;
// FIXME: should handle this case (|reason=\nText, https://github.com/NovemLinguae/UserScripts/issues/240) instead of throwing an error
if ( appealReason === '\n' ) {
mw.notify( 'UnblockReview error 1: unable to find decline reason by scanning HTML', { type: 'error' } );
return;
}
$.getJSON(
mw.util.wikiScript( 'api' ),
{
format: 'json',
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
titles: mw.config.get( 'wgPageName' )
}
).done( ( data ) => {
// Extract wikitext from API response
const pageId = Object.keys( data.query.pages )[ 0 ];
const wikitext = data.query.pages[ pageId ].revisions[ 0 ][ '*' ];
// change wikitext
// eslint-disable-next-line no-undef
const unblockReview = new UnblockReview();
const acceptDeclineReason = reasonArea.value;
const wikitext2 = unblockReview.processAcceptOrDecline( wikitext, appealReason, acceptDeclineReason, DEFAULT_DECLINE_REASON, acceptOrDecline );
if ( wikitext === wikitext2 ) {
mw.notify( 'UnblockReview error 2: unable to determine write location.', { type: 'error' } );
return;
}
// build edit summary
const acceptingOrDeclining = ( acceptOrDecline === 'accept' ? 'Accepting' : 'Declining' );
const summary = acceptingOrDeclining + ' unblock request' + ADVERT;
// make edit
( new mw.Api() ).postWithToken( 'csrf', {
action: 'edit',
title: mw.config.get( 'wgPageName' ),
summary: summary,
text: wikitext2
} ).done( ( data ) => {
if ( data && data.edit && data.edit.result && data.edit.result === 'Success' ) {
window.location.reload( true );
} else {
console.log( data );
}
} );
} );
} );
}
execute();
}() );
//
// === modules/UnblockReview.js ======================================================
class UnblockReview {
constructor() {
this.SIGNATURE = '~~~~';
}
processAcceptOrDecline( wikitext, appealReason, acceptDeclineReason, DEFAULT_DECLINE_REASON, acceptOrDecline ) {
// HTML does one line break and wikitext does 2ish. Cut off all text after the first line break to avoid breaking our search algorithm.
appealReason = appealReason.split( '\n' )[ 0 ];
let initialText = '';
// Special case: If the user didn't provide a reason, the template will display "Please provide a reason as to why you should be unblocked", and this will be detected as the appealReason.
const reasonProvided = !appealReason.startsWith( 'Please provide a reason as to why you should be unblocked' );
if ( !reasonProvided ) {
initialText = wikitext.match( /(\{\{Unblock)\}\}/i )[ 1 ];
appealReason = '';
} else {
initialText = this.getLeftHalfOfUnblockTemplate( wikitext, appealReason );
}
if ( !acceptDeclineReason.trim() ) {
acceptDeclineReason = DEFAULT_DECLINE_REASON + ' ' + this.SIGNATURE;
} else if ( !this.hasSignature( acceptDeclineReason ) ) {
acceptDeclineReason = acceptDeclineReason + ' ' + this.SIGNATURE;
}
// eslint-disable-next-line no-useless-concat
const negativeLookbehinds = '(?<!<' + 'nowiki>)';
const regEx = new RegExp( negativeLookbehinds + this.escapeRegExp( initialText + appealReason ), 'g' );
let wikitext2 = wikitext.replace(
regEx,
'{{unblock reviewed|' + acceptOrDecline + '=' + acceptDeclineReason + '|1=' + appealReason
);
if ( wikitext === wikitext2 ) {
throw new Error( 'Replacing text with unblock message failed!' );
}
// get rid of any [#*:] in front of {{unblock X}} templates. indentation messes up the background color and border of the unblock template.
wikitext2 = wikitext2.replace( /^[#*: ]{1,}(\{\{\s*unblock)/mi, '$1' );
return wikitext2;
}
/**
* Given the wikitext of an entire page, and the |reason= parameter of one of the many unblock templates (e.g. {{Unblock}}, {{Unblock-un}}, {{Unblock-auto}}, {{Unblock-bot}}, etc.), return the wikitext of just the beginning of the template.
*
* For example, "Test {{unblock|reason=Your reason here [[User:Filipe46]]}} Test" as the wikitext and "Your reason here" as the appealReason will return "{{unblock|reason=".
*
* This can also handle 1=, and no parameter at all (just a pipe)
*/
getLeftHalfOfUnblockTemplate( wikitext, appealReason ) {
// Isolate the reason, stripping out all template syntax. So `{{Unblock|reason=ABC}}` becomes matches = [ 'ABC ']
// eslint-disable-next-line no-useless-concat
const negativeLookbehinds = '(?<!<' + 'nowiki>{{unblock\\|reason=)(?<!reviewed ?\\|1=)';
const regEx = new RegExp( negativeLookbehinds + this.escapeRegExp( appealReason ), 'g' );
let matches = wikitext.matchAll( regEx );
matches = [ ...matches ];
if ( matches.length === 0 ) {
throw new Error( 'Searching for target text failed!' );
}
// Loop through all the potential matches, and peek at the characters in front of the match. Eliminate false positives ({{tlx|unblock}}, the same text not anywhere near an {{unblock}} template, etc.). If a true positive, return the beginning of the template.
for ( const match of matches ) {
// The position of the match within the wikicode.
const MatchPos = match.index;
// The position of the unblock template of that match within the wikicode. Set them equal initially. Will be adjusted below.
let UnblockTemplateStartPos = MatchPos;
// check for {{tlx|unblock. if found, this isn't what we want, skip.
const startOfSplice = UnblockTemplateStartPos - 50 < 0 ? 0 : UnblockTemplateStartPos - 50;
const chunkFiftyCharactersWide = wikitext.slice( startOfSplice, UnblockTemplateStartPos );
if ( /\{\{\s*tlx\s*\|\s*unblock/i.test( chunkFiftyCharactersWide ) ) {
continue;
}
// Scan backwards from the match until we find {{
let i = 0;
while ( wikitext[ UnblockTemplateStartPos ] !== '{' && i < 50 ) {
UnblockTemplateStartPos--;
i++;
}
// If the above scan couldn't find the beginning of the template within 50 characters of the match, then that wasn't it. Check the next match.
if ( i === 50 ) {
continue;
}
// The above scan stopped at {Unblock. Subtract one so it's {{Unblock
UnblockTemplateStartPos--;
const initialText = wikitext.slice( UnblockTemplateStartPos, MatchPos );
return initialText;
}
throw new Error( 'Searching backwards failed!' );
}
/**
* @copyright coolaj86, CC BY-SA 4.0, https://stackoverflow.com/a/6969486/3480193
*/
escapeRegExp( string ) {
// $& means the whole matched string
return string.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
}
/**
* Is there a signature (four tildes) present in the given text, outside of a nowiki element?
*/
hasSignature( text ) {
// no literal signature?
if ( text.indexOf( this.SIGNATURE ) < 0 ) {
return false;
}
// if there's a literal signature and no nowiki elements,
// there must be a real signature
if ( text.indexOf( '' ) < 0 ) {
return true;
}
// Save all nowiki spans
const nowikiSpanStarts = []; // list of ignored span beginnings
const nowikiSpanLengths = []; // list of ignored span lengths
const NOWIKI_RE = /.*?<\/nowiki>/g;
let spanMatch;
do {
spanMatch = NOWIKI_RE.exec( text );
if ( spanMatch ) {
nowikiSpanStarts.push( spanMatch.index );
nowikiSpanLengths.push( spanMatch[ 0 ].length );
}
} while ( spanMatch );
// So that we don't check every ignore span every time
let nowikiSpanStartIdx = 0;
const SIG_RE = new RegExp( this.SIGNATURE, 'g' );
let sigMatch;
matchLoop:
do {
sigMatch = SIG_RE.exec( text );
if ( sigMatch ) {
// Check that we're not inside a nowiki
for ( let nwIdx = nowikiSpanStartIdx; nwIdx <
nowikiSpanStarts.length; nwIdx++ ) {
if ( sigMatch.index > nowikiSpanStarts[ nwIdx ] ) {
if ( sigMatch.index + sigMatch[ 0 ].length <=
nowikiSpanStarts[ nwIdx ] + nowikiSpanLengths[ nwIdx ] ) {
// Invalid sig
continue matchLoop;
} else {
// We'll never encounter this span again, since
// headers only get later and later in the wikitext
nowikiSpanStartIdx = nwIdx;
}
}
}
// We aren't inside a nowiki
return true;
}
} while ( sigMatch );
return false;
}
}
});
// </nowiki>