User:JJPMaster/pageswap.js

From Uncyclopedia, the content-free encyclopedia
Jump to navigation Jump to search

Note: After saving, you have to bypass your browser's cache to see the changes.

  • Internet Explorer: hold down the Ctrl key and click the Refresh or Reload button, or press Ctrl+F5.
  • Firefox: hold down the Shift key while clicking Reload; alternatively press Ctrl+F5 or Ctrl-Shift-R.
  • Opera, Konqueror and Safari users can just click the Reload button.
  • Chrome: press Ctrl+F5 or Shift+F5
// <syntaxhighlight lang="javascript">
// [[WP:PMRC#4]] round-robin history swap
// by [[User:Andy M. Wang]]
// 1.6.1.2018.0920

$(document).ready(function() {
mw.loader.using( [
    'mediawiki.api',
    'mediawiki.util',
] ).then( function() {
    "use strict";

/**
 * If user is able to perform swaps
 */
function checkUserPermissions() {
    var ret = {};
    ret.canSwap = true;
    var reslt = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) {
            alert("Swapping pages unavailable."); return ret; },
        data: { action:'query', format:'json', meta:'userinfo', uiprop:'rights' }
    }).responseText).query.userinfo;

    // check userrights for suppressredirect
    var rightslist = reslt.rights;
    ret.canSwap =
            $.inArray('suppressredirect', rightslist) > -1
    ret.allowSwapTemplates =
            $.inArray('templateeditor', rightslist) > -1;

    return ret;
}

/**
 * Given namespace data, title, title namespace, returns expected title of page
 * Along with title without prefix
 * Precondition, title, titleNs is a subject page!
 */
function getTalkPageName(nsData, title, titleNs) {
    var ret = {};
    var prefixLength = nsData['' + titleNs]['*'].length === 0
        ? 0 : nsData['' + titleNs]['*'].length + 1;
    ret.titleWithoutPrefix = title.substring(prefixLength, title.length);
    ret.talkTitle = nsData['' + (titleNs + 1)]['*'] + ':'
        + ret.titleWithoutPrefix;
    return ret;
}

/**
 * Given two (normalized) titles, find their namespaces, if they are redirects,
 * if have a talk page, whether the current user can move the pages, suggests
 * whether movesubpages should be allowed, whether talk pages need to be checked
 */
function swapValidate(titleOne, titleTwo, pagesData, nsData, uPerms) {
    var ret = {};
    ret.valid = true;
    if (titleOne === null || titleTwo === null || pagesData === null) {
        ret.valid = false;
        ret.invalidReason = "Unable to validate swap.";
        return ret;
    }

    ret.allowMoveSubpages = true;
    ret.checkTalk = true;
    var count = 0;
    for (var k in pagesData) {
        ++count;
        if (k == "-1" || pagesData[k].ns < 0) {
            ret.valid = false;
            ret.invalidReason = ("Page " + pagesData[k].title + " does not exist.");
            return ret;
        }
        // enable only in ns 0..5,12,13,118,119 (Main,Talk,U,UT,WP,WT,H,HT,D,DT)
        if ((pagesData[k].ns >= 6 && pagesData[k].ns <= 9)
         || (pagesData[k].ns >= 10 && pagesData[k].ns <= 11 && !uPerms.allowSwapTemplates)
         || (pagesData[k].ns >= 14 && pagesData[k].ns <= 117)
         || (pagesData[k].ns >= 120)) {
            ret.valid = false;
            ret.invalidReason = ("Namespace of " + pagesData[k].title + " ("
                + pagesData[k].ns + ") not supported.\n\nLikely reasons:\n"
                + "- Names of pages in this namespace relies on other pages\n"
                + "- Namespace features heavily-transcluded pages\n"
                + "- Namespace involves subpages: swaps produce many redlinks\n"
                + "\n\nIf the move is legitimate, consider a careful manual swap.");
            return ret;
        }
        if (titleOne == pagesData[k].title) {
            ret.currTitle   = pagesData[k].title;
            ret.currNs      = pagesData[k].ns;
            ret.currTalkId  = pagesData[k].talkid; // could be undefined
            ret.currCanMove = pagesData[k].actions.move === '';
            ret.currIsRedir = pagesData[k].redirect === '';
        }
        if (titleTwo == pagesData[k].title) {
            ret.destTitle   = pagesData[k].title;
            ret.destNs      = pagesData[k].ns;
            ret.destTalkId  = pagesData[k].talkid; // could be undefined
            ret.destCanMove = pagesData[k].actions.move === '';
            ret.destIsRedir = pagesData[k].redirect === '';
        }
    }

    if (!ret.valid) return ret;
    if (!ret.currCanMove) {
        ret.valid = false;
        ret.invalidReason = ('' + ret.currTitle + " is immovable. Aborting");
        return ret;
    }
    if (!ret.destCanMove) {
        ret.valid = false;
        ret.invalidReason = ('' + ret.destTitle + " is immovable. Aborting");
        return ret;
    }
    if (ret.currNs % 2 !== ret.destNs % 2) {
        ret.valid = false;
        ret.invalidReason = "Namespaces don't match: one is a talk page.";
        return ret;
    }
    if (count !== 2) {
        ret.valid = false;
        ret.invalidReason = "Pages have the same title. Aborting.";
        return ret;
    }
    ret.currNsAllowSubpages = nsData['' + ret.currNs].subpages !== '';
    ret.destNsAllowSubpages = nsData['' + ret.destNs].subpages !== '';

    // if same namespace (subpages allowed), if one is subpage of another,
    // disallow movesubpages
    if (ret.currTitle.startsWith(ret.destTitle + '/')
            || ret.destTitle.startsWith(ret.currTitle + '/')) {
        if (ret.currNs !== ret.destNs) {
            ret.valid = false;
            ret.invalidReason = "Strange.\n" + ret.currTitle + " in ns "
                + ret.currNs + "\n" + ret.destTitle + " in ns " + ret.destNs
                + ". Disallowing.";
            return ret;
        }

        ret.allowMoveSubpages = ret.currNsAllowSubpages;
        if (!ret.allowMoveSubpages)
            ret.addlInfo = "One page is a subpage. Disallowing move-subpages";
    }

    if (ret.currNs % 2 === 1) {
        ret.checkTalk = false; // no need to check talks, already talk pages
    } else { // ret.checkTalk = true;
        var currTPData = getTalkPageName(nsData, ret.currTitle, ret.currNs);
        ret.currTitleWithoutPrefix = currTPData.titleWithoutPrefix;
        ret.currTalkName = currTPData.talkTitle;
        var destTPData = getTalkPageName(nsData, ret.destTitle, ret.destNs);
        ret.destTitleWithoutPrefix = destTPData.titleWithoutPrefix;
        ret.destTalkName = destTPData.talkTitle;
        // possible: ret.currTalkId undefined, but subject page has talk subpages
    }

    return ret;
}

/**
 * Given two talk page titles (may be undefined), retrieves their pages for comparison
 * Assumes that talk pages always have subpages enabled.
 * Assumes that pages are not identical (subject pages were already verified)
 * Assumes namespaces are okay (subject pages already checked)
 * (Currently) assumes that the malicious case of subject pages
 *   not detected as subpages and the talk pages ARE subpages
 *   (i.e. A and A/B vs. Talk:A and Talk:A/B) does not happen / does not handle
 * Returns structure indicating whether move talk should be allowed
 */
function talkValidate(checkTalk, talk1, talk2) {
    var ret = {};
    ret.allowMoveTalk = true;
    if (!checkTalk) { return ret; } // currTitle destTitle already talk pages
    if (talk1 === undefined || talk2 === undefined) {
        alert("Unable to validate talk. Disallowing movetalk to be safe");
        ret.allowMoveTalk = false;
        return ret;
    }
    ret.currTDNE = true;
    ret.destTDNE = true;
    ret.currTCanCreate = true;
    ret.destTCanCreate = true;
    var talkTitleArr = [talk1, talk2];
    if (talkTitleArr.length !== 0) {
        var talkData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
            error: function (jsondata) {
                alert("Unable to get info on talk pages."); return ret; },
            data: { action:'query', format:'json', prop:'info',
                intestactions:'move|create', titles:talkTitleArr.join('|') }
        }).responseText).query.pages;
        for (var id in talkData) {
            if (talkData[id].title === talk1) {
                ret.currTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
                ret.currTTitle = talkData[id].title;
                ret.currTCanMove = talkData[id].actions.move === '';
                ret.currTCanCreate = talkData[id].actions.create === '';
                ret.currTalkIsRedir = talkData[id].redirect === '';
            } else if (talkData[id].title === talk2) {
                ret.destTDNE = talkData[id].invalid === '' || talkData[id].missing === '';
                ret.destTTitle = talkData[id].title;
                ret.destTCanMove = talkData[id].actions.move === '';
                ret.destTCanCreate = talkData[id].actions.create === '';
                ret.destTalkIsRedir = talkData[id].redirect === '';
            } else {
                alert("Found pageid not matching given ids."); return {};
            }
        }
    }

    ret.allowMoveTalk = (ret.currTCanCreate && ret.currTCanMove)
        && (ret.destTCanCreate && ret.destTCanMove);
    return ret;
}

/**
 * Given existing title (not prefixed with "/"), optionally searching for talk,
 *   finds subpages (incl. those that are redirs) and whether limits are exceeded
 * As of 2016-08, uses 2 api get calls to get needed details:
 *   whether the page can be moved, whether the page is a redirect
 */
function getSubpages(nsData, title, titleNs, isTalk) {
    if ((!isTalk) && nsData['' + titleNs].subpages !== '') { return { data:[] }; }
    var titlePageData = getTalkPageName(nsData, title, titleNs);
    var subpages = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) {
            return { error:"Unable to search for subpages. They may exist" }; },
        data: { action:'query', format:'json', list:'allpages',
            apnamespace:(isTalk ? (titleNs + 1) : titleNs),
            apfrom:(titlePageData.titleWithoutPrefix + '/'),
            apto:(titlePageData.titleWithoutPrefix + '0'),
            aplimit:101 }
    }).responseText).query.allpages;

    // put first 50 in first arr (need 2 queries due to api limits)
    var subpageids = [[],[]];
    for (var idx in subpages) {
        subpageids[idx < 50 ? 0 : 1].push( subpages[idx].pageid );
    }

    if (subpageids[0].length === 0) { return { data:[] }; }
    if (subpageids[1].length === 51) { return { error:"100+ subpages. Aborting" }; }
    var dataret = [];
    var subpageData0 = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) {
            return { error:"Unable to fetch subpage data." }; },
        data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
            pageids:subpageids[0].join('|') }
    }).responseText).query.pages;
    for (var k0 in subpageData0) {
        dataret.push({
            title:subpageData0[k0].title,
            isRedir:subpageData0[k0].redirect === '',
            canMove:subpageData0[k0].actions.move === ''
        });
    }

    if (subpageids[1].length === 0) { return { data:dataret }; }
    var subpageData1 = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) {
            return { error:"Unable to fetch subpage data." }; },
        data: { action:'query', format:'json', prop:'info', intestactions:'move|create',
            pageids:subpageids[1].join('|') }
    }).responseText).query.pages;
    for (var k1 in subpageData1) {
        dataret.push({
            title:subpageData1[k1].title,
            isRedir:subpageData1[k1].redirect === '',
            canMove:subpageData1[k1].actions.move === ''
        });
    }
    return { data:dataret };
}

/**
 * Prints subpage data given retrieved subpage information returned by getSubpages
 * Returns a suggestion whether movesubpages should be allowed
 */
function printSubpageInfo(basepage, currSp) {
    var ret = {};
    var currSpArr = [];
    var currSpCannotMove = [];
    var redirCount = 0;
    for (var kcs in currSp.data) {
        if (!currSp.data[kcs].canMove) {
            currSpCannotMove.push(currSp.data[kcs].title);
        }
        currSpArr.push((currSp.data[kcs].isRedir ? "(R) " : "  ")
            + currSp.data[kcs].title);
        if (currSp.data[kcs].isRedir)
            redirCount++;
    }

    if (currSpArr.length > 0) {
        alert((currSpCannotMove.length > 0
                ? "Disabling move-subpages.\n"
                + "The following " + currSpCannotMove.length + " (of "
                + currSpArr.length + ") total subpages of "
                + basepage + " CANNOT be moved:\n\n  "
                + currSpCannotMove.join("\n  ") + '\n\n'
            : (currSpArr.length + " total subpages of " + basepage + ".\n"
            + (redirCount !== 0 ? ('' + redirCount + " redirects, labeled (R)\n") : '')
            + '\n' + currSpArr.join('\n'))));
    }

    ret.allowMoveSubpages = currSpCannotMove.length === 0;
    ret.noNeed = currSpArr.length === 0;
    return ret;
}

/**
 * After successful page swap, post-move cleanup:
 * Make talk page redirect
 * TODO more reasonable cleanup/reporting as necessary
 * vData.(curr|dest)IsRedir
 */
function doPostMoveCleanup(movedTalk, movedSubpages, vData, vTData) {
    if (movedTalk && vTData.currTDNE && confirm("Create redirect "
            + vData.currTalkName + " → " + vData.destTalkName + " if possible?")) {
        // means that destination talk now is redlinked TODO
    } else if (movedTalk && vTData.destTDNE && confirm("Create redirect "
            + vData.destTalkName + " → " + vData.currTalkName + " if possible?")) {
        // curr talk now is redlinked TODO
    }
}


/**
 * Swaps the two pages (given all prerequisite checks)
 * Optionally moves talk pages and subpages
 */
function swapPages(titleOne, titleTwo, moveReason, intermediateTitlePrefix,
        moveTalk, moveSubpages, vData, vTData) {
    if (titleOne === null || titleTwo === null
            || moveReason === null || moveReason === '') {
        alert("Titles are null, or move reason given was empty. Swap not done");
        return false;
    }

    var intermediateTitle = intermediateTitlePrefix + titleOne;
    var pOne = { action:'move', from:titleTwo, to:intermediateTitle,
        reason:"[[w:WP:PMRC#4|Round-robin history swap]] step 1 using [[User:JJPMaster/pageswap.js|pageswap]]",
        watchlist:"unwatch", noredirect:1 };
    var pTwo = { action:'move', from:titleOne, to:titleTwo,
        reason:moveReason,
        watchlist:"unwatch", noredirect:1 };
    var pTre = { action:'move', from:intermediateTitle, to:titleOne,
        reason:"[[w:WP:PMRC#4|Round-robin history swap]] step 3 using [[User:JJPMaster/pageswap.js|pageswap]]",
        watchlist:"unwatch", noredirect:1 };
    if (moveTalk) {
        pOne.movetalk = 1; pTwo.movetalk = 1; pTre.movetalk = 1;
    }
    if (moveSubpages) {
        pOne.movesubpages = 1; pTwo.movesubpages = 1; pTre.movesubpages = 1;
    }

    new mw.Api().postWithToken("csrf", pOne).done(function (reslt1) {
    new mw.Api().postWithToken("csrf", pTwo).done(function (reslt2) {
    new mw.Api().postWithToken("csrf", pTre).done(function (reslt3) {
        alert("Moves completed successfully.\n"
            + "Please create new red-linked talk pages/subpages if there are incoming links\n"
            + "  (check your contribs for \"Talk:\" redlinks),\n"
            + "  correct any moved redirects, and do post-move cleanup if necessary.");
        //doPostMoveCleanup(moveTalk, moveSubpages, vData, vTData);
    }).fail(function (reslt3) {
        alert("Fail on third move " + intermediateTitle + " → " + titleOne);
    });
    }).fail(function (reslt2) {
        alert("Fail on second move " + titleOne + " → " + titleTwo);
    });
    }).fail(function (reslt1) {
        alert("Fail on first move " + titleTwo + " → " + intermediateTitle);
    });
}

/**
 * Given two titles, normalizes, does prerequisite checks for talk/subpages,
 * prompts user for config before swapping the titles
 */
function roundrobin(uPerms, currNs, currTitle, destTitle, intermediateTitlePrefix) {
    // get ns info (nsData.query.namespaces)
    var nsData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) { alert("Unable to get info about namespaces"); },
        data: { action:'query', format:'json', meta:'siteinfo', siprop:'namespaces' }
    }).responseText).query.namespaces;

    // get page data, normalize titles
    var relevantTitles = currTitle + "|" + destTitle;
    var pagesData = JSON.parse($.ajax({ url:mw.util.wikiScript('api'), async:false,
        error: function (jsondata) {
            alert("Unable to get info about " + currTitle + " or " + destTitle);
        },
        data: { action:'query', format:'json', prop:'info', inprop:'talkid',
            intestactions:'move|create', titles:relevantTitles }
    }).responseText).query;

    for (var kp in pagesData.normalized) {
        if (currTitle == pagesData.normalized[kp].from) { currTitle = pagesData.normalized[kp].to; }
        if (destTitle == pagesData.normalized[kp].from) { destTitle = pagesData.normalized[kp].to; }
    }
    // validate namespaces, not identical, can move
    var vData = swapValidate(currTitle, destTitle, pagesData.pages, nsData, uPerms);
    if (!vData.valid) { alert(vData.invalidReason); return; }
    if (vData.addlInfo !== undefined) { alert(vData.addlInfo); }

    // subj subpages
    var currSp = getSubpages(nsData, vData.currTitle, vData.currNs, false);
    if (currSp.error !== undefined) { alert(currSp.error); return; }
    var currSpFlags = printSubpageInfo(vData.currTitle, currSp);
    var destSp = getSubpages(nsData, vData.destTitle, vData.destNs, false);
    if (destSp.error !== undefined) { alert(destSp.error); return; }
    var destSpFlags = printSubpageInfo(vData.destTitle, destSp);

    var vTData = talkValidate(vData.checkTalk, vData.currTalkName, vData.destTalkName);

    // future goal: check empty subpage DESTINATIONS on both sides (subj, talk)
    //   for create protection. disallow move-subpages if any destination is salted
    var currTSp = getSubpages(nsData, vData.currTitle, vData.currNs, true);
    if (currTSp.error !== undefined) { alert(currTSp.error); return; }
    var currTSpFlags = printSubpageInfo(vData.currTalkName, currTSp);
    var destTSp = getSubpages(nsData, vData.destTitle, vData.destNs, true);
    if (destTSp.error !== undefined) { alert(destTSp.error); return; }
    var destTSpFlags = printSubpageInfo(vData.destTalkName, destTSp);

    var noSubpages = currSpFlags.noNeed && destSpFlags.noNeed
        && currTSpFlags.noNeed && destTSpFlags.noNeed;
    // If one ns disables subpages, other enables subpages, AND HAS subpages,
    //   consider abort. Assume talk pages always safe (TODO fix)
    var subpageCollision = (vData.currNsAllowSubpages && !destSpFlags.noNeed)
        || (vData.destNsAllowSubpages && !currSpFlags.noNeed);

    var moveTalk = false;
    // TODO: count subpages and make restrictions?
    if (vData.checkTalk && vTData.allowMoveTalk) {
        moveTalk = confirm("Move talk page(s)? (OK for yes, Cancel for no)");
    } else if (vData.checkTalk) {
        alert("Disallowing moving talk. "
            + (!vTData.currTCanCreate ? (vData.currTalkName + " is create-protected")
            : (!vTData.destTCanCreate ? (vData.destTalkName + " is create-protected")
            : "Talk page is immovable")));
    }

    var moveSubpages = false;
    // TODO future: currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages
    // needs to be separate check. If talk subpages immovable, should not affect subjspace
    if (!subpageCollision && !noSubpages && vData.allowMoveSubpages
            && (currSpFlags.allowMoveSubpages && destSpFlags.allowMoveSubpages)
            && (currTSpFlags.allowMoveSubpages && destTSpFlags.allowMoveSubpages)) {
        moveSubpages = confirm("Move subpages? (OK for yes, Cancel for no)");
    } else if (subpageCollision) {
        alert("One namespace does not have subpages enabled. Disallowing move subpages");
    }

    var moveReason = '';
    if (typeof moveReasonDefault === 'string') {
        moveReason = prompt("Move reason:", moveReasonDefault);
    } else {
        moveReason = prompt("Move reason:");
    }

    var confirmString = "Round-robin configuration:\n  "
        + currTitle + " → " + destTitle + "\n    : " + moveReason
        + "\n      with movetalk:" + moveTalk + ", movesubpages:" + moveSubpages
        + "\n\nProceed? (Cancel to abort)";

    if (confirm(confirmString)) {
        swapPages(currTitle, destTitle, moveReason, intermediateTitlePrefix,
            moveTalk, moveSubpages, vData, vTData);
    }
}

    var currNs = mw.config.get("wgNamespaceNumber");
    if (currNs < 0 || currNs >= 120
            || (currNs >=  6 && currNs <= 9)
            || (currNs >= 14 && currNs <= 99))
        return; // special/other page

    var portletLink = mw.util.addPortletLink("p-cactions", "#", "Swap",
        "ca-swappages", "Perform a revision history swap / round-robin move");
    $( portletLink ).click(function(e) {
        e.preventDefault();
        var userPermissions = checkUserPermissions();
        if (!userPermissions.canSwap) {
            alert("User rights insufficient for action."); return;
        }
        var currTitle = mw.config.get("wgPageName");
        var destTitle = prompt("Swap \"" + (currTitle.replace(/_/g, ' ')) + "\" with:");

        return roundrobin(userPermissions, currNs, currTitle, destTitle, "Draft:Move/");
    });
});
});
// </syntaxhighlight>