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.

/**
 * Aggiunge un link nella barra degli strumenti laterale
 * per segnalare rapidamente un'utenza o un IP tra i vandalismi in corso.
 *
 * @author https://it.wiki.x.io/wiki/Utente:Titore and contributors
 * @date 2019-09-17
 * @license Dual-licensed under CC BY-SA 3.0 and GPL 3.0 (or any later version)
 */
(function (mw, $) {
    'use strict';

    var windowManager, dialog;

    // API MediaWiki
    const api = new mw.Api();

    // Variabili MediaWiki
    const conf = mw.config.get([
        'wgRelevantUserName', // Presente in pagine attinenti all'utente (es. contributi, discussioni, pagina utente)
        'wgUserName',
        'wgArticlePath',
    ]);

    // Utente da segnalare
    const utente = conf.wgRelevantUserName;

    // Utilizzatore dello script
    const currentUser = conf.wgUserName;

    // Pagina di servizio
    const paginaSegnalazioni = 'Wikipedia:Vandalismi in corso';

    // Campo oggetto
    const oggetto = `[[Utente:Titore/VC|VC.js]]: segnalazione [[Speciale:Contributi/${utente}|${utente}]]`; //TODO: o tutto qui o tutto lì (inviaSegnalazione -> summary:)

    // Motivazioni frequenti
    const motivazioniFrequenti = [
        'Reiterati vandalismi.',
        'Avvisato, insiste.',
        'Continua dopo il giallo.',
        'Recidivo.',
        'Evasione del blocco: ',
        'Rimozione contenuti.',
        'Utenza creata al solo scopo di vandalizzare.',
    ];

    // Numero massimo di segnalazioni da elaborare per ricerca duplicati
    var maxSegnalazioni = 5;

    // Aggiunge link nella barra degli strumenti laterale
    if (utente && utente!=currentUser) { // Evita di segnalare se stessi
        var link = mw.util.addPortletLink(
            'p-tb',
            '#',
            'Segnala vandalo',
            't-segnalavandalo',
            `Segnala ${utente} tra i vandalismi in corso`,
            getAvailableAccessKey('v', 'i', 'c')); // Tasto di scelta rapida (es. Alt + Maiusc + v) e alternative in caso sia già in uso
        $(link).click((event) => {
            event.preventDefault();
            avviaScript();
        });
    }

    /**
     * Avvia lo script.
     */
    function avviaScript() {
        mw.loader.using('oojs-ui-windows')
            .done(() => finestraDiDialogo())
            .fail(() => console.error('Impossibile avviare lo script VC.js.'));
    }

    /**
     * Finestra di dialogo
     */
    function finestraDiDialogo() {

        // Subclass di ProcessDialog 
        function Dialog(config) {
            Dialog.super.call(this, config);
        }
        OO.inheritClass(Dialog, OO.ui.ProcessDialog);

        // Nome, titolo, action set
        Dialog.static.name = 'dialogo';
        Dialog.static.title = 'Nuova segnalazione';
        Dialog.static.actions = [
            {
                flags: ['primary', 'progressive'], // Azione primaria, blu
                label: 'Invia',
                action: 'save',
                disabled: true, // Disabilito finché non pronto //TODO: usare setAbilities()
            },
            {
                flags: ['safe', 'close'],
                label: 'Annulla',
            }
        ];

        // initialize(): aggiungo contenuto e layout
        Dialog.prototype.initialize = function () {
            Dialog.super.prototype.initialize.call(this);

            this.panel = new OO.ui.PanelLayout({
                padded: true,
                expanded: false,
            });
            this.content = new OO.ui.FieldsetLayout();
            this.vandaloInput = new OO.ui.TextInputWidget({ //this.vandaloInput = new mw.widgets.UserInputWidget({ //non funziona da mobile
                readOnly: true,
            });
            var style = ".vertical textarea{ resize: vertical; }"; // Solo resize verticale
            mw.util.addCSS(style);
            this.motivoInput = new OO.ui.MultilineTextInputWidget({
                placeholder: 'Descrivi brevemente il tipo di vandalismo in corso.\nLa firma è automatica.',
                rows: 3,
                classes: ['vertical'],
                /*multiline: true,
                autosize: true,
                maxRows: 3,
                indicator: 'required',*/
            });

            // Dropdown 
            // TODO: meglio in una funzione? https://en.wiki.x.io/w/index.php?title=MediaWiki:Gadget-defaultsummaries.js#L-66
            var itemSelected; //<--- per strict
            var self = this; //TODO: bind "this" to the function call instead
            //      https://stackoverflow.com/questions/337878/var-self-this#comment14371472_338106
            //      https://stackoverflow.com/a/10115970/8762088
            this.dropDown = new OO.ui.DropdownWidget({
                label: 'Motivazioni frequenti'
            }),
                // Evento che inserirà nel campo motivo l'opzione selezionata dal menu a tendina
                itemSelected = function () { //TODO: inserire direttamente come anon func?
                    var motivazioneFrequente = self.dropDown.getMenu().findSelectedItem().getLabel();
                    self.motivoInput.setValue(motivazioneFrequente);
                };
            popolaDropdown(this.dropDown, motivazioniFrequenti);

            // Fieldset
            var fieldsetLayout = new OO.ui.FieldsetLayout({
                items: [
                    new OO.ui.FieldLayout(this.vandaloInput, {
                        label: 'Vandalo:',
                        align: 'top',
                    }),
                    new OO.ui.FieldLayout(this.motivoInput, {
                        label: 'Motivazione:',
                        align: 'top',
                        /*$overlay: this.$overlay,
                        help: 'Firma aggiunta automaticamente',
                        helpInline: true,*/
                    }),
                    new OO.ui.FieldLayout(this.dropDown, {
                        align: 'inline',
                    }),
                ]
            });

            this.content.addItems([fieldsetLayout]);
            this.panel.$element.append(this.content.$element);
            /* // Disclaimer
            this.content.$element.append( '<small><p>Accertarsi che sia chiaro cosa si intende per <a href="/wiki/Wikipedia:Vandalismo" title="Wikipedia:Vandalismo">vandalismo</a>, che il vandalo sia stato avvertito e che i vandalismi siano recenti.</small>' );*/
            this.$body.append(this.panel.$element);
            this.dropDown.getMenu().on('select', itemSelected); // Quando viene scelta un'opzione dal menu
            this.motivoInput.connect(this, { 'change': 'onMotivoInputChange' }); // Alla modifica del campo motivo
        };

        // Impedisce motivazione vuota
        Dialog.prototype.onMotivoInputChange = function (value) {
            this.actions.setAbilities({
                save: !!value.length
            });
        };

        // getSetupProcess(): passa dei dati all'apertura della finestra
        Dialog.prototype.getSetupProcess = function (data) {
            data = data || {};
            return Dialog.super.prototype.getSetupProcess.call(this, data)
                .next(function () {
                    this.vandaloInput.setValue(data.vandalo);
                    this.primoTentativo = true;
                    segnalatoDiRecente(paginaSegnalazioni).then(result => {
                        result ? console.log(`${utente} già segnalato.`) : console.log(`${utente} non ancora segnalato.`);
                    });

                }, this);
        };

        // getActionProcess(): gestione delle azioni (es. 'save').
        Dialog.prototype.getActionProcess = function (action) {
            var parent = this; // Fallback to parent handler // TODO: <-------
            const url = {
                vc: conf.wgArticlePath.replace('$1', paginaSegnalazioni),
                contribs: conf.wgArticlePath.replace('$1', `Special:Contribs/${this.vandaloInput.getValue()}`)
            };
            const err = {
                giaBloccato: $(`<span>${this.vandaloInput.getValue()} <a href="${url.contribs}">risulta già bloccato</a>. Inviare comunque?</span>`),
                giaSegnalato: $(`<span>${this.vandaloInput.getValue()} è già stato segnalato <a href="${url.vc}#footer">di recente</a>.</span>`)
            };

            return Dialog.super.prototype.getActionProcess.call(this, action)
                // Impedisce se segnalato di recente
                .next(function () {
                    // Alla pressione del tasto "Invia"
                    if (action === 'save') {
                        var promise = $.Deferred();
                        segnalatoDiRecente(paginaSegnalazioni).then(result => {
                            if (result) {
                                promise.reject(new OO.ui.Error(err.giaSegnalato, { recoverable: false }));
                            } else {
                                promise.resolve();
                            }
                        });
                        return promise;
                    }
                }, this)

                // Avvisa se già bloccato
                .next(function () {
                    if (action === 'save') {
                        var promise = $.Deferred();
                        controllaBlocco(this.vandaloInput.getValue(), blocco => {
                            if (blocco && this.primoTentativo) { // Mostra il warning solo la prima volta
                                this.primoTentativo = false;
                                promise.reject(new OO.ui.Error(err.giaBloccato, { warning: true }));
                                console.log(`Segnalazione non inviata: ${parent.vandaloInput.getValue()} già bloccato.\n` +
                                    `Il testo era: "${parent.motivoInput.getValue()}"`);
                            } else {
                                promise.resolve();
                            }
                        });
                        return promise;
                    }
                }, this)

                // Invia la segnalazione
                .next(function () {
                    if (action === 'save') {
                        var promise = $.Deferred();
                        inviaSegnalazione(parent.vandaloInput.getValue(), parent.motivoInput.getValue())
                            .done(() => windowManager.closeWindow(dialog))
                            .fail((code) => { promise.reject(new OO.ui.Error(code)); console.error(`Segnalazione non inviata: ${code}`); });
                        return promise;
                    }
                }, this);
        };

        //if (!windowManager) { // per non perdere contenuto alla chiusura della finestra //FIXME: cleanup dopo invio

        // Crea e aggiunge nuovo window manager
        windowManager = new OO.ui.WindowManager();
        $(document.body).append(windowManager.$element);

        // Crea finestra di dialogo
        dialog = new Dialog();

        // Aggiunge la finestra al window manager
        windowManager.addWindows([dialog]);
        //}

        // Apre la finestra
        windowManager.openWindow(dialog, { vandalo: utente })
            // Codice da eseguire dopo che la finestra è stata aperta    
            .opened.then(function () {
                // Focus all'input per la motivazione
                dialog.motivoInput.focus();
            });
    }

    /**
     * Invia la segnalazione.
     * 
     * @param {string} utente - Vandalo da segnalare
     * @param {string} motivo - Contenuto segnalazione
     * @returns {object} jQuery.Promise
     */
    function inviaSegnalazione(utente, motivo) {

        const segnalazione = `{{vandalo|${utente}}} ${motivo} --~~\~~`;

        // Data odierna, ordinale per il primo del mese
        const dataOdierna = new Date()
            .toLocaleDateString("it-IT", { day: 'numeric', month: 'long' })
            .replace(/\b[1] /, '1º ');

        // URL per link in notifica
        const url = conf.wgArticlePath.replace('$1', paginaSegnalazioni);

        // Per aggiungere sottosezione con data se non già esistente
        return titoloUltimaSezione(paginaSegnalazioni)
            .then(titoloUltimaSezione => {
                return api.postWithEditToken({
                    action: 'edit',
                    title: paginaSegnalazioni,
                    appendtext: (dataOdierna == titoloUltimaSezione) ? ('\n\n' + segnalazione) :
                        ('\n\n' + `=== ${dataOdierna} ===` + '\n' + segnalazione),
                    summary: `/* ${dataOdierna} */ ${oggetto}`
                }).done(data => {
                    mw.notify($(`<span>${utente} è stato segnalato tra i <a href="${url}#footer">vandalismi in corso</a>.</span>`), {
                        title: 'Segnalato',
                        tag: 'notif'
                    });
                    console.log(`L'utente ha scritto "${motivo}" e ha fatto clic su "OK".`);
                    console.log(data);
                });
            });
    }

    /**
     * Ottiene titoli e numeri sezioni.
     * 
     * @param {string} pagina - Titolo della pagina di servizio 
     * @returns {object} jQuery.Promise - Sezioni
     */
    function parsing(pagina) {
        return api.get({
            page: pagina,
            action: 'parse',
            prop: 'sections'
        });
    }

    /**
     * Ottiene il titolo dell'ultima sezione.
     * 
     * @param {string} pagina - Titolo della pagina di servizio
     * @returns {object} jQuery.Promise - Titolo dell'ultima sezione
     */
    function titoloUltimaSezione(pagina) {
        return parsing(pagina).then(data => {
            return data.parse.sections[data.parse.sections.length - 1].line
                // ordinale per il primo del mese
                .replace(/\b[1] /, '1º ')
                .replace('1° ', '1º ');
        });
    }

    /**
    * Ottiene l'ultimo numero di sezione della pagina.
    * 
    * @returns {Promise<string>} Numero dell'ultima sezione
    */
    function indexUltimaSezione(pagina) {
        return parsing(pagina).then(data => data.parse.sections[data.parse.sections.length - 1].index);
    }

    /**
     * Ottiene il wikitesto a partire dal numero della sezione.
     * 
     * @param index Numero di sezione
     * @returns {Promise<string>} wikitesto
     */
    function wikitestoSezione(index, pagina) {
        return api.get({
            page: pagina,
            action: 'parse',
            prop: 'wikitext',
            section: index,
            formatversion: 2 //'cci tua
        }).then(data => data.parse.wikitext);
        //return data.parse.wikitext['*'] //https://stackoverflow.com/q/7610191/8762088; mw:API:JSON_version_2#Changes_to_JSON_output_format
    }

    /**
     * Ottiene il wikitesto delle ultime due sezioni.
     * 
     * @returns {Promise<array>} Array di sezioni
     */
    function ultimeSezioni(pagina) {
        return Promise.all([
            // Wikitesto penultima sezione
            indexUltimaSezione(pagina).then(index => wikitestoSezione(index - 1, pagina)),
            // Wikitesto ultima sezione
            indexUltimaSezione(pagina).then(index => wikitestoSezione(index, pagina))
        ]);
    }

    /**
     * Controlla se il vandalo è già stato segnalato di recente.
     * Solo ultime 2 sezioni e non oltre limite massimo.

     * @param {string} pagina - Titolo della pagina di servizio
     * @returns {Promise<boolean>} bool - Se è già stato segnalato
     */
    function segnalatoDiRecente(pagina) {
        return ultimeSezioni(pagina)
            .then(wikitesto => {
                var regex = /{{[\s\t\n\r]*vandalo[\s\t\n\r]*\|[\s\t\n\r]*([^}]*)\b/gi;
                var segnalati = [];

                var match = regex.exec(wikitesto);
                while (match !== null) {
                    segnalati.push(match[1]);
                    match = regex.exec(wikitesto);
                }

                // Mi accerto che il limite massimo non superi il numero di segnalazioni
                if (maxSegnalazioni > segnalati.length) {
                    maxSegnalazioni = segnalati.length;
                }
                //var segnalatiDiRecente = []
                console.log(`Ultime segnalazioni: ${segnalati.slice(-maxSegnalazioni).join(", ")}.`);
                for (var i = segnalati.length - maxSegnalazioni; i < segnalati.length; i++) {
                    // Se il vandalo è già presente tra le ultime segnalazioni
                    if (utente.toLowerCase() == segnalati[i].toLowerCase()) {
                        return true;
                    }
                    //segnalatiDiRecente.push(segnalati[i])
                }
            });
    }

    /**
     * Controlla se l'utente è bloccato.
     * @param {string} utente - Utente da controllare
     * @param {function} blocco - Risultato
     */
    function controllaBlocco(utente, blocco) {
        api.get({
            action: 'query',
            list: 'blocks',
            bkusers: utente,
            bklimit: '1',
            bkprop: 'by',
            format: 'json'
        }).done(data => {
            blocco(data.query.blocks[0]);
        });
    }

    /**
     * Popola il menu a tendina.
     * 
     * @param dropdown - Dropdown widget
     * @param {string[]} opzioni - Scelte
     */
    function popolaDropdown(dropdown, opzioni) {
        dropdown.menu.addItems(opzioni.map((opzione) => {
            return new OO.ui.MenuOptionWidget({ label: opzione });
        }));
    }

    /**
     * Controlla se un tasto di scelta rapida è disponibile.
     * 
     * @param {string} key - Tasto da provare
     * @returns {boolean} Se disponibile o meno
     */
    function isAccessKeyAvailable(key) {
        //$('[accesskey=v]')[0]
        if (!$(`[accesskey=${key}]`)[0]) { // se non già in uso
            return true;
        }
    }

    /**
     * Trova il primo tasto di scelta rapida disponibile tra quelli indicati
     * 
     * @param {string[]} accessKeys - Tasti da provare
     * @returns {string} Il primo tasto disponibile
     */
    function getAvailableAccessKey(...accessKeys) {
        for (var accessKey of accessKeys) {
            if (isAccessKeyAvailable(accessKey)) {
                return accessKey;
            }
        }
    }

}(mediaWiki, jQuery));

// FIXME:
// Escludere se stessi può rendere difficile il debug in commons.js

// TODO:
// se già bloccato (es. allungare blocco filtro):
//     chiedere conferma anche prima del clic a 'invia'
//     visualizzare durata e autore blocco
// controllare che vandalo sia stato avvisato
// permettere di scegliere utente
//     di selezionare più utenti
//     controllare se utente esiste
// aggiungere anche mese se non presente
// motivazioni personalizzabili (tramite variabile da inserire in user script, JSON o option api)