User:DieBuche/delete.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
// Ajax-based replacement for [[MediaWiki:Quick-delete-code.js]]
// *** EXPERIMENTAL ***
// TODO: more reliable localization loading
// DONE: use prependtext/appendtext API parameters for more reliable editing
// DONE: display detailed progress; 
// TODO: better error handling/reporting
// TODO: allow user to choose which uploaders to notify
// TODO: (somehow) detect bots, don't notify them
// TODO: try to find an actual user to notify for bot-uploaded files
// TODO: follow talk page redirects (including {{softredirect}})
// DONE: Add the ability to be extended by more userdefined buttons
// <source lang="JavaScript">

if (typeof (AjaxQuickDelete) == 'undefined' && wgNamespaceNumber >= 0) {




if (window.QuickDeleteEnhanced) {
var insertTagButtons = [
{
    'label': 'No source',
    'tag': '{\{subst:nsd}}',
    'talk_tag': '{\{subst:image source|1=%FILE%}}',
    'img_summary': 'File has no source',
    'talk_summary': '%FILE% does not have a source'
 
},
 
{
    'label': 'No permission',
    'tag': '{\{subst:npd}}',
    'talk_tag': '{\{subst:image permission|1=%FILE%}}',
    'img_summary': 'Missing permission',
    'talk_summary': 'Please send a permission for %FILE% to [[COM:OTRS|OTRS]]'
 
},
{
    'label': 'No license',
    'tag': '{\{subst:nld}}',
    'talk_tag': '{\{subst:image license|1=%FILE%}}',
    'img_summary': 'Missing license',
    'talk_summary': '%FILE% does not have a license'
 
}
];
} else {
var insertTagButtons = [];
}
var AjaxQuickDelete = {
    /**
     * Set up the AjaxQuickDelete object and add the toolbox link.  Called via
     * addOnloadHook() during page loading.
     */
    install : function () {
        // abort if AJAX is not supported
        if (!wfSupportsAjax || !wfSupportsAjax()) return;

        // abort if page seems to be already nominated for deletion
        if (document.getElementById("nuke")) return;

        // remove old toolbox link if called twice
        var tool = document.getElementById('t-ajaxquickdelete');
        if (tool) tool.parentNode.removeChild(tool);

        // set up toolbox link
        //For each in etc.
        if (skin == 'vector') {
            mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();',
                       this.i18n.toolboxLink + " (Ajax)", 't-ajaxquickdelete', null);
        } else {
            addToolLink('p-tb', 'javascript:AjaxQuickDelete.nominateForDeletion();',
                       this.i18n.toolboxLink + " (Ajax)", 't-ajaxquickdelete', null);

        }
        for (var i = 0; i < insertTagButtons.length; i++) {
            var inb = insertTagButtons[i];
            if (skin == 'vector') {
                    mw.util.addPortletLink('p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + inb['tag'] + '","' + inb['img_summary'] + '","' + inb['talk_tag'] + '","' + inb['talk_summary'] + '");', inb['label']);
                
            } else {
                    addToolLink('p-tb', 'javascript:AjaxQuickDelete.insertTagOnPage("' + inb['tag'] + '","' + inb['img_summary'] + '","' + inb['talk_tag'] + '","' + inb['talk_summary'] + '");', inb['label']);
                
            }
        }
    },

    /**
     * This is the main entry point which actually starts the nomination process.
     * Takes as an optional parameter a string used as the deletion reason; if none
     * is given, prompts the user for one first.
     */
     
     
     insertTagOnPage : function (tag, img_summary, talk_tag, talk_summary) {
         //Todo: Allow customization of question..
         this.tag=tag + '\n';
         this.img_summary=img_summary;
         
         if(tag.indexOf("%PARAMETER%") != -1){
             reason = prompt( this.i18n.reasonForDeletion );
             if (!reason) return;
             this.tag=tag.replace('%PARAMETER%', reason);
             this.img_summary=this.img_summary.replace('%PARAMETER%', reason);
             this.img_summary=this.img_summary.replace('%PARAMETER-LINKED%', '[[:'+reason+']]');

         } 
         
         // first schedule some API queries to fetch the info we need...
         this.tasks = [];  // reset task list in case an earlier error left it non-empty
         if (talk_tag != "undefined") {
             
             this.talk_tag=talk_tag.replace('%FILE%', wgPageName);
             this.talk_summary=talk_summary.replace('%FILE%', '[[:'+wgPageName+']]');
             
             this.addTask( 'findUploaders' );
         }
         
         //get token
         this.addTask( 'loadPages' );
         


         // ...then schedule the actual edits
         this.addTask( 'prependAnyTemplate' );
         this.addTask( 'notifyUploaders' );

         // finally reload the page to show the deletion tag
         this.addTask( 'reloadPage' );

         // now go do all the stuff we just scheduled!
         document.body.style.cursor = 'wait';


         bodycontent = document.getElementById('bodyContent');
         this.feedbackDiv = document.createElement('div');
         this.feedbackDiv.setAttribute("class", 'ajaxDeleteFeedback');

         //The following will be moved to common.css later
         this.feedbackDiv.style.border = '1px #A9DE16 solid';
         this.feedbackDiv.style.background = '#EAF2CB url(http://bits.wikimedia.org/skins-1.5/common/images/ajax-loader.gif) no-repeat 8px 14px';
         this.feedbackDiv.style.padding = '1em 1em 1em 2.5em';
         this.feedbackDiv.style.position = 'fixed';
         this.feedbackDiv.style.top = '50%';
         this.feedbackDiv.style.left = '50%';
         this.feedbackDiv.style.zIndex = '100';
         bodycontent.parentNode.insertBefore(this.feedbackDiv, bodycontent); 



         this.nextTask();
     }, 

    nominateForDeletion : function (reason) {
        this.startDate = new Date ();

        // prompt for reason if necessary
        // TODO: replace this with a nice textbox / drop menu entry form like in TWINKLE
        if (!reason) reason = prompt( this.i18n.reasonForDeletion );
        if (!reason) return;
        this.reason = reason;

        // set up some page names we'll need later
        this.requestPage = this.requestPagePrefix + wgPageName;
        this.dailyLogPage = this.requestPagePrefix + this.formatDate( "YYYY/MM/DD" );

        // first schedule some API queries to fetch the info we need...
        this.tasks = [];  // reset task list in case an earlier error left it non-empty
        this.addTask( 'findUploaders' );  // TODO: allow user to edit list
        this.addTask( 'loadPages' );

        // ...then schedule the actual edits
        this.addTask( 'prependDeletionTemplate' );
        this.addTask( 'createRequestSubpage' );
        this.addTask( 'listRequestSubpage' );
        this.addTask( 'notifyUploaders' );

        // finally reload the page to show the deletion tag
        this.addTask( 'reloadPage' );

        // now go do all the stuff we just scheduled!
        document.body.style.cursor = 'wait';
        
        
        bodycontent = document.getElementById('bodyContent');
        this.feedbackDiv = document.createElement('div');
        this.feedbackDiv.setAttribute("class", 'ajaxDeleteFeedback');
        
        //The following will be moved to common.css later
        this.feedbackDiv.style.border = '1px #A9DE16 solid';
        this.feedbackDiv.style.background = '#EAF2CB url(http://bits.wikimedia.org/skins-1.5/common/images/ajax-loader.gif) no-repeat 8px 14px';
        this.feedbackDiv.style.padding = '1em 1em 1em 2.5em';
        this.feedbackDiv.style.position = 'fixed';
        this.feedbackDiv.style.top = '50%';
        this.feedbackDiv.style.left = '50%';
        this.feedbackDiv.style.zIndex = '100';
        bodycontent.parentNode.insertBefore(this.feedbackDiv, bodycontent); 

        
        
        this.nextTask();
    },

    /**
     * Edit the current page to add the {{delete}} tag.  Assumes that the page hasn't
     * been tagged yet; if it is, a duplicate tag will be added.
     */
    prependDeletionTemplate : function () {
        var page = this.pages[ wgPageName ];
        page.text = "{{delete|reason=" + this.reason + this.formatDate("|year=YYYY|month=MON|day=DAY}}\n");
        
        
        page.editType = 'prependtext';
        
        //Update status
        this.feedbackDiv.innerHTML = this.i18n.addingTemplate;
        
        this.savePage( page, "Nominating for deletion", 'nextTask' );
        
    },
    
    prependAnyTemplate : function () {
        var page = this.pages[ wgPageName ];
        
        page.text = this.tag;
        page.editType = 'prependtext';
        
        //Update status
        this.feedbackDiv.innerHTML = this.i18n.addingTemplate;
        
        this.savePage( page, this.img_summary, 'nextTask' );
        
    },

    /**
     * Create the DR subpage (or append a new request to an existing subpage).  The request
     * page will always be watchlisted.
     * TODO: if the page exists, check that any earlier nomination has been closed already
     */
    createRequestSubpage : function () {
        this.templateAdded = true;  // we've got this far; if something fails, user can follow instructions on template to finish
        var page = this.pages[ this.requestPage ];
        page.text = "\n\n=== [[:" + wgPageName + "]] ===\n" + this.reason + " --~~"+"~~\n";
        page.watchlist = 'watch';
        page.editType = 'appendtext';
        
        //Update status
        this.feedbackDiv.innerHTML = this.i18n.creatingNomination;
        
        this.savePage( page, "Starting deletion request", 'nextTask' );
    },

    /**
     * Transclude the nomination page onto today's DR log page, creating it if necessary.
     * The log page will never be watchlisted (unless the user is already watching it).
     */
    listRequestSubpage : function () {
        var page = this.pages[ this.dailyLogPage ];
        if (!page.text) page.text = "{{"+"subst:" + this.requestPagePrefix + "newday}}";  // add header to new log pages
        page.text = "\n{{" + this.requestPage + "}}\n";
        page.watchlist = 'nochange';
        page.editType = 'appendtext';
        
        //Update status
        this.feedbackDiv.innerHTML = this.i18n.listingNomination;
        
        this.savePage( page, "Listing [[" + this.requestPage + "]]", 'nextTask' );
    },

    /**
     * Notify any uploaders/creators of this page using {{idw}}.
     * TODO: follow talk page redirects (including {{softredirect}})
     * TODO: obey {{nobots}} and/or other opt-out mechanisms
     */
    notifyUploaders : function () {
        this.uploadersToNotify = 0;
        for (var user in this.uploaders) {
            if (this.tag){
                var page = this.pages[ this.userTalkPrefix + user ];
                page.text = "\n"+ this.talk_tag + "\n";
                page.editType = 'appendtext';
                this.savePage( page, this.talk_summary, 'uploaderNotified' );
                //Update status
                this.feedbackDiv.innerHTML = this.i18n.notifyingUploader.replace('%USER%', user);
            
                this.uploadersToNotify++;
            }else{
                var page = this.pages[ this.userTalkPrefix + user ];
                page.text = "\n{{"+"subst:idw|" + wgPageName + "}}\n";
                page.editType = 'appendtext';
                this.savePage( page, "[[:" + wgPageName + "]] has been nominated for deletion", 'uploaderNotified' );
                //Update status
                this.feedbackDiv.innerHTML = this.i18n.notifyingUploader.replace('%USER%', user);
            
                this.uploadersToNotify++;
                
            }
            
        }
        
        if (this.uploadersToNotify == 0) this.nextTask();
    },

    uploaderNotified : function () {
        this.uploadersToNotify--;
        if (this.uploadersToNotify == 0) this.nextTask();
    },

    /**
     * Compile a list of uploaders to notify.  Users who have only reverted the file to an
     * earlier version will not be notified.
     * TODO: notify creator of non-file pages
     * TODO: handle continuations if there are more than 50 revisions
     * TODO: don't notify bots, try to figure out a real human user to notify instead
     * TODO: allow nominator to choose which users to actually notify
     */
    findUploaders : function () {
        var query = {
            action : 'query',
            prop : 'imageinfo',
            iiprop : 'user|sha1',
            iilimit : 50,
            titles : wgPageName };
        this.doAPICall( query, 'findUploadersCB' );
    },
    findUploadersCB : function (result) {
        this.uploaders = {};
        var pages = result.query.pages;
        for (var id in pages) {  // there should be only one, but we don't know its ID
            var info = pages[id].imageinfo;
            if (!info) continue;  // not a file?
            var seenHashes = {};
            for (var i = info.length-1; i >= 0; i--) {  // iterate in reverse order
                if (info[i].sha1 && seenHashes[ info[i].sha1 ]) continue;  // skip reverts
                this.uploaders[ info[i].user ] = true;
            }
            // TODO: improve handling of bot uploads
        }
        //Update status
        this.feedbackDiv.innerHTML = this.i18n.preparingToEdit.replace('%COUNT%', this.uploaders.length);
        
        this.nextTask();
    },

    /**
     * Fetch page content for editing.  Currently we do it all in one batch; if we'd
     * like to fetch more pages later, this code would need some redesign.
     * TODO: follow redirects?
     */

     
    loadPages : function () {
        var pages = [ wgPageName, this.requestPage, this.dailyLogPage ];
        
        if (this.tag) var pages = [ wgPageName ];
        
        for (var user in this.uploaders) pages.push( this.userTalkPrefix + user );
        var query = {
            action : 'query',
            prop : 'info|revisions',
            intoken : 'edit',
            //If we use append/prependtext, we won't need the content
            rvprop : 'timestamp',
            titles : pages.join('|') };
        this.doAPICall( query, 'loadPagesCB' );
    },
    loadPagesCB : function (result) {
        // build a denormalization map so that we can keep using the same (possibly non-canonical) page names as we originally queried:
        var denorm = {};
        var norm = result.query.normalized || [];
        for (var i = 0; i < norm.length; i++) {
            denorm[ norm[i].to ] = norm[i].from;
        }
        // save results:
        this.pages = {};
        var pages = result.query.pages;
        for (var id in pages) {
            var page = pages[id];
            var name = denorm[ page.title ] || page.title;
            //Won't need this
            //page.text = ((page.revisions || [])[0] || {})['*'] || "";  // copy of revision text for editing
            this.pages[ name ] = page;
        }
        this.nextTask();
    },

    /**
     * Submit an edited page.
     */
    savePage : function (page, summary, callback) {
        var edit = {
            action : 'edit',
            summary : summary,
            watchlist : (page.watchlist || 'preferences'),
            title : page.title,
            token : page.edittoken,
            //If we use append/prepend, there'll never be editconflicts
            //starttimestamp : page.starttimestamp 
            };
        
        //Is there a way to declare this inside the brackets?
        edit[page.editType]=page.text;
        
        if (page.revisions && page.revisions.length) {
            edit.basetimestamp = page.revisions[0].timestamp;
            edit.nocreate = 1;
        } else {
            edit.createonly = 1;
        }

        this.doAPICall( edit, callback );
    },

    /**
     * Does a MediaWiki API request and passes the result to the supplied callback (method name).
     * Uses POST requests for everything for simplicity.
     * TODO: better error handling
     */
    doAPICall : function ( params, callback ) {
        var query = [ "format=json" ];
        for (var name in params) {
            query.push( encodeURIComponent(name) + "=" + encodeURIComponent(params[name]) );
        }
        query = query.sort().join("&");  // conveniently, "text" sorts before "token"

        var o = this;
        var x = sajax_init_object();
        x.open( 'POST', this.apiURL, true );
        x.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
        x.onreadystatechange = function () {
            if (x.readyState != 4) return;
            if (x.status >= 300) return o.fail( "API request returned code " + x.status + " " + x.statusText );
            try {
                var result = eval( "(" + x.responseText + ")" );
            } catch (e) {
                return o.fail( "Error evaluating API result: " + e + "\n\nResponse content:\n" + x.responseText );
            }
            if (!result) return o.fail( "Receive empty API response:\n" + x.responseText );
            if (result.error) return o.fail( "API request failed (" + result.error.code + "): " + result.error.info );
            try { o[callback](result); } catch (e) { return o.fail(e); }
        };
	x.send(query);
    },

    /**
     * Simple task queue.  addTask() adds a new task to the queue, nextTask() executes
     * the next scheduled task.  Tasks are specified as method names to call.
     */
    tasks : [],  // list of pending tasks
    currentTask : '',  // current task, for error reporting
    addTask : function ( task ) {
        this.tasks.push( task );
    },
    nextTask : function () {
        var task = this.currentTask = this.tasks.shift();
        try { this[task](); } catch (e) { this.fail(e); }
    },

    /**
     * Once we're all done, reload the page.
     */
    reloadPage : function () {
        if (wgAction == 'view') location.reload();
        else location.href = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace("$1", encodeURIComponent(mw.config.get('wgPageName')));
    },

    /**
     * Crude error handler. Just throws an alert at the user and (if we managed to
     * add the {{delete}} tag) reloads the page.
     */
    fail : function ( err ) {
        var msg = this.i18n.taskFailure[this.currentTask] || this.i18n.genericFailure;
        var fix = (this.templateAdded ? this.i18n.completeRequestByHand : this.i18n.addTemplateByHand );
        alert( msg + " " + fix + "\n\n" + this.i18n.errorDetails + "\n" + err );
        if (this.templateAdded) this.reloadPage();
        else document.body.style.cursor = 'default';
    },

    /**
     * Very simple date formatter.  Replaces the substrings "YYYY", "MM" and "DD" in a
     * given string with the UTC year, month and day numbers respectively.  Also
     * replaces "MON" with the English full month name and "DAY" with the unpadded day. 
     */
    formatDate : function ( fmt, date ) {
        var pad0 = function ( s ) { s = "" + s; return (s.length > 1 ? s : "0" + s); };  // zero-pad to two digits
        if (!date) date = this.startDate;
        fmt = fmt.replace( /YYYY/g, date.getUTCFullYear() );
        fmt = fmt.replace( /MM/g, pad0( date.getUTCMonth()+1 ) );
        fmt = fmt.replace( /DD/g, pad0( date.getUTCDate() ) );
        fmt = fmt.replace( /MON/g, this.months[ date.getUTCMonth() ] );
        fmt = fmt.replace( /DAY/g, date.getUTCDate() );
        return fmt;
    },
    months : "January February March April May June July August September October November December".split(" "),  // srsly? I have to do this myself?? wtf?

    // Constants
    requestPagePrefix : "Commons:Deletion requests/",  // DR subpage prefix
    userTalkPrefix : wgFormattedNamespaces[3] + ":",   // user talk page prefix
    apiURL : mw.config.get('wgServer') + mw.config.get('wgScriptPath') + "/api.php",     // MediaWiki API script URL

    // Translatable strings (many unused for now)
    i18n : {
        toolboxLink : "Nominate for deletion",

        // GUI reason prompt form (mostly unused)
        reasonForDeletion : "Reason for deletion:",
        notifyFollowingUsers : "Notify the following users:",
        submitButtonLabel : "Nominate",
        cancelButtonLabel : "Cancel",

        // GUI progress messages (unused)
        preparingToEdit : "Preparing to edit %COUNT% pages... ",
        creatingNomination : "Creating nomination page... ",
        listingNomination : "Adding nomination page to daily list... ",
        addingTemplate : "Adding deletion template to file description page... ",
        notifyingUploader : "Notifying %USER%... ",

        // GUI results (unused)
        operationSucceeded : "done",
        operationFailed : "ERROR",

        // Errors
        genericFailure : "An error occurred while nominating this "+(6==wgNamespaceNumber?"file":"page")+" for deletion.",
        taskFailure : {
            listUploaders : "An error occurred while determining the " + (6==wgNamespaceNumber ? "uploader(s) of this file" : "creator of this page") + ".",
            loadPages : "An error occurred while preparing to nominate this "+(6==wgNamespaceNumber?"file":"page")+" for deletion.",
            prependDeletionTemplate : "An error occurred while adding the {{delete}} template to this "+(6==wgNamespaceNumber?"file":"page")+".",
            createRequestSubpage : "An error occurred while creating the request subpage.",
            listRequestSubpage : "An error occurred while adding the deletion request to today's log.",
            notifyUploaders : "An error occurred while notifying the " + (6==wgNamespaceNumber ? "uploader(s) of this file" : "creator of this page") + ".",
            dummy : ""  // IE doesn't like trailing commas
        },
        addTemplateByHand : "To nominate this "+(6==wgNamespaceNumber?"file":"page")+" for deletion, please edit the page to add the {{delete}} template and follow the instructions shown on it.",
        completeRequestByHand : "Please follow the instructions on the deletion notice to complete the request.",
        errorDetails : "A detailed description of the error is shown below:",

        dummy : ""  // IE doesn't like trailing commas
    }
};

if (!/^en\b/.test(wgUserLanguage)) importScript( 'User:Ilmari_Karonen/ajax_quick_delete.js/' + wgUserLanguage.replace(/-.*/, "") );

$ (function () { AjaxQuickDelete.install(); });

} // end if (guard)
 
// </source>