User:Former User aDB0haVymg/Gadgets/Close-DRV.js

维基百科,自由的百科全书

注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google ChromeFirefoxMicrosoft EdgeSafari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。

/**
 * <nowiki>
 * Close-DRV.js
 * A simple userscript for Chinese Wikipedia
 *
 * ----
 *
 * @author  User:Classy_Melissa @ zh.100ke.info
 * @licence unlicense
 * @version pre-release
 *
 */

// wrap in big function to prevent scope leak
"use strict";

function launchCloseDRVTool() {

    if (mw.config.get("wgPageName") !== "Wikipedia:存廢覆核請求") {
        // ineligible. Silently return.
        return;
    }

    // script configs and constants
    const SCRIPT_IDENT = "[[User:Former User aDB0haVymg/Gadgets/Close-DRV.js|Close-DRV.js]]";  // used in summaries

// tuneable constants

// global variables
    let apiEndpoint;
    const currentPageTitle = mw.config.get("wgPageName");
    let rawWikitext;

// SimpleMWAPI Shim library

    class SMAPIError extends Error {
        constructor(message) {
            super(message);
        }
    }

    class SimpleMWApi {

        /**
         * Constructs a new instance of SimpleMWApi
         * Remember: the MediaWiki core mw.Api is already defined
         */
        constructor() {
            this.apiInterface = new mw.Api({ajax: {headers: {'Api-User-Agent': 'w:zh:User:ClassyMelissa/Gadgets/Close-DRV.js'}}});
        }

        /**
         * Gets the unparsed content of a given page
         *
         * @param {String} title    the title of the page to request
         * @returns {String}        a string with the complete content of the title
         *
         * @throws  ReferenceError  if there is any API error (e.g. page does not exist)
         *
         */
        async readPage(title) {

            // construct the object
            const reqParams = {
                "action": "parse",
                "format": "json",
                "page": title,
                "prop": "wikitext"
            };

            // send the request
            const response = (await this.apiInterface.get(reqParams)).parse;

            // set a trap to detect any error
            if (response.error) {
                // there is an error!
                throw new SMAPIError(response.error.toString());
            }

            // there is no error.
            // read the text out
            return response.wikitext['*'];

        }

        /**
         * Writes wikitext content to a page. Will overwrite existing data.
         * If the page doesn't exist yet, this will create the page.
         *
         * @param {String} title    the title of the page to write.
         * @param {String} content  the wikitext content to write
         * @param {String} summary  the edit summary. optional.
         *
         * @throws  {SMAPIError}    if anything goes wrong
         * @returns {Number}        the new revid
         *
         */
        async writePage(title, content, summary = "") {

            const reqParams = {
                "action": "edit",
                "format": "json",
                "title": title,
                "text": content,
                "summary": summary,
            };

            const response = (await this.apiInterface.postWithEditToken(reqParams)).edit;

            // detect error
            if (response.result.toLowerCase() === "success") {
                return response.newrevid;
            } else {
                throw new SMAPIError("Edit failed.");
            }

        }

    }

// re-initialise the global apiEndpoint
    apiEndpoint = new SimpleMWApi();// UI Controllers
// QA Exploratory Testing Passed 2020-07-16

    /**
     * Opens a jQuery dialog to perform the function
     * @param title
     * @param indexOfSection
     */
    function openJQDialog(title, indexOfSection) {

        const element = getDialogUIElement();

        $(element).dialog({
            title: `正在關閉:${title}`,
            minWidth: 600,
            minHeight: 300,
            buttons: [
                {
                    text: '確定',
                    click: () => openButtonOnClick(element, title, indexOfSection)  // fancy bind
                },
                {
                    text: '取消',
                    click: function () {
                        $(this).dialog('close');
                    }
                }
            ]
        });

    }

    /**
     * Generate the DOM element of what's supposed to be inside the dialog.
     * @param title {string} the title of the section to close
     * @returns {HTMLDivElement}
     */
    function getDialogUIElement(title) {

        const divElement = document.createElement("div");
        divElement.id = "closeDRV-dialog-div";

        // ⚠ XSS Hot Spot
        divElement.innerHTML = `

        <br />
        <strong>將{{status}}模板狀態改為:</strong><br />
        <select id="status-dropdown-select">
            <option value="done">完成</option>
            <option value="not done">未完成</option>
            <option value="on hold">等待中</option>
            <option value="">(無)</option>
        </select><br />
        
        <br />
        自訂狀態文字:<br />
        <input type="text" id="status-comments" style="width: 100%" />
           
        <br /><hr /><br />
        
        處理結果 wikitext (不需簽名):<br />
        <input type="text" id="outcome-wikitext" style="width: 100%" />
        
        <br /><hr /><br />
        
        執行操作: <br />
        <label><input type="radio" name="next-step" id="next-step-noop" checked />什麼都不做</label> <br />
        <label><input type="radio" name="next-step" id="next-step-undelete-all" />還原頁面所有版本</label><br />
        <label><input type="radio" name="next-step" id="next-step-open-special" />開啟Special:Undelete以執行進一步操作</label><br />
        
    `;

        return divElement;

    }

    /**
     * A simple handler function
     * @param divSection
     * @param title
     * @param indexOfSelection
     * @returns {Promise<void>}
     */
    function openButtonOnClick(divSection, title, indexOfSelection) {

        // shorthand: safe querySelector on divSection
        const $ef = (selector) => {

            const result = divSection.querySelector(selector);
            if (!result) {
                // missing elements. Probably already finalised
                // Just close the dialog.
                $(divSection).dialog("close");
            }

            return result;

        };

		let newStatus, newStatusComments, outcomeWikitext;
		try {
	        // extract all form variables
	        newStatus = $ef('#status-dropdown-select').value;
	        newStatusComments = $ef("#status-comments").value;
	    	outcomeWikitext = $ef("#outcome-wikitext").value;
		} catch (e) {
			return;
		}

        // figure out the next step
        let nextOp = "noop";
        if ($ef("#next-step-undelete-all").checked) {
            nextOp = "undelete";
        }
        if ($ef("#next-step-open-special").checked) {
            nextOp = "special";
        }

        return performCloseAction(divSection, title, indexOfSelection,
            newStatus, newStatusComments, outcomeWikitext, nextOp);

    }// Driver

    /**
     * An abstract wrapper function for performing user requested action.
     * @param dialogElement     {HTMLDivElement}
     * @param titleToClose      {string}
     * @param indexOfSelection  {number}
     * @param newStatus         {string}
     * @param newStatusComments {string}
     * @param outcomeWikitext   {string}
     * @param nextOp            {string}
     * @returns {Promise<void>}
     */
    async function performCloseAction(dialogElement, titleToClose, indexOfSelection,
                                      newStatus, newStatusComments, outcomeWikitext, nextOp) {

        // clear out the dialog for status report
        dialogElement.innerHTML = "";
        const $upd = (string) => {
            dialogElement.innerText += string;
        };

        try {

            // get the raw wikitext of this page
            $upd("取得頁面原始碼... ");
            const oldWikitext = await apiEndpoint.readPage(currentPageTitle);
            $upd("取得成功\n")

            const newWikitext = doReplaceWikitext(oldWikitext, indexOfSelection, newStatus, newStatusComments, outcomeWikitext);

            $upd("應用變更... ");
            await apiEndpoint.writePage(currentPageTitle, newWikitext, `/* ${titleToClose} */ 關閉請求 (${SCRIPT_IDENT}) `);
            $upd("完成. \n")

            if (nextOp === "undelete") {
                $upd("還原所有版本... ");
                await undeleteAllRevisions(titleToClose);
                $upd("完成. \n");
            }

            if (nextOp === "special") {
                $upd("正在開啟還原頁面...");
                _redirectToSpecialUndelete(titleToClose);
                return;
            }

            $upd("作業完成,即將重新整理。");
            location.reload();

        } catch (e) {

            $upd("\n錯誤 -- " + e.toString());

        }


    }

// Exploratory QA Passed 2020-07-16
    function _redirectToSpecialUndelete(pageTitle) {

        const fullTargetTitle = `Special:Undelete/${pageTitle}`;

        const relativeURL = mw.config.get("wgArticlePath").replace("$1", fullTargetTitle);
        const fullURL = new URL(relativeURL, location);

        location.href = fullURL.toString();

    }

    /**
     * Send a request to the API to undelete all revisions of a page.
     * @param pageTitle
     * @returns {Promise<void>}
     */
    async function undeleteAllRevisions(pageTitle) {

        const queryParams = {
            "action": "undelete",
            "format": "json",
            "title": pageTitle,
            "reason": `存廢覆核還原 (${SCRIPT_IDENT})`,
            "utf8": 1
        }

        const newApiEndpoint = new mw.Api();
        await newApiEndpoint.postWithEditToken(queryParams);

    }// DOM Helpers
// Unit QA Passed 2020-07-16

    /**
     * Inserts all "close" buttons onto the DOM
     */
    function insertAllCloseButtons() {

        const allH2s = _getAllRelevantH2s();
        allH2s.forEach(insertOneCloseButton);

    }

    /**
     * Returns a list of all <h2> elements in the document
     * @returns {Array}
     * @private
     */
    function _getAllRelevantH2s() {

        let result = Array.from(document.querySelectorAll("#bodyContent h2"));

        // remove the TOC, if presenting
        result = result.filter((node) => node.id !== "mw-toc-heading");

        return result;

    }

    /**
     * Inserts a close button to one specific <h2>
     * @param destinationH2Node {Element}
     * @param indexOfButton {number} starting from 0, the index of the button
     */
    function insertOneCloseButton(destinationH2Node, indexOfButton) {

        const newElement = _generateCloseButtonElement(indexOfButton);
        destinationH2Node.insertAdjacentElement("beforeend", newElement);

    }

    /**
     * Gets an HTML element of the close button
     * @param indexOfButton {number} starting from 0, the index of the button
     * @returns {Element}
     * @private
     */
    function _generateCloseButtonElement(indexOfButton) {

        // generate a specific handler function
        const handlerFunction = (event) => closeButtonOnClick(event, indexOfButton);

        // generate element itself
        const newElement = document.createElement("span");
        const $ns = newElement.style;
        newElement.classList.add("clamel-close-drv-h2-button");

        $ns.marginLeft = "1em";
        $ns.fontSize = "75%";
        $ns.verticalAlign = "middle";

        // ⚠ XSS Hot Spot
        newElement.innerHTML = "<span class='mw-ui-button mw-ui-destructive'>關閉段落</span>";

        newElement.addEventListener("click", handlerFunction);
        return newElement;

    }

    /**
     * Extracts the title of an H2 mw-headline element, nonwithstanding the edit button and close button
     * @param targetH2 {Element}
     * @returns {string} title
     */
    function extractH2Title(targetH2) {
        cdAssert(targetH2.tagName === "H2");
        return targetH2.querySelector(".mw-headline").innerText;
    }

// wikitext processors

    /**
     * Calculates new wikitext from old wikitext, adding the user requested actions
     * @param oldWikitext       {string}
     * @param indexToReplace    {number}
     * @param newStatus         {string}
     * @param newStatusComments {string}
     * @param outcomeWikitext   {string}
     * @returns {string} the new wikitext
     */
    function doReplaceWikitext(oldWikitext, indexToReplace, newStatus, newStatusComments, outcomeWikitext) {

        // split old wikitext into h2 tokens
        const splitToken = new RegExp("^==(?!=)", "mgi");
        const allSections = _losslessSplit(oldWikitext, splitToken);

        // remove the first section, because it is {{/header}}
        allSections.shift();

        const oldSectionText = allSections[indexToReplace];
        const newSectionText = transformSectionWikitext(oldSectionText, newStatus, newStatusComments, outcomeWikitext);

        return oldWikitext.replace(oldSectionText, newSectionText);

    }

    /**
     * Constructs a new status template
     * @param newStatus
     * @param newStatusComments
     * @returns {string}
     * @private
     */
    function _constructStatusWikitext(newStatus, newStatusComments) {

        let newStatusTemplate = "{{Status";
        if (newStatus) {
            newStatusTemplate += `|1=${newStatus}`;
        }
        if (newStatusComments) {
            newStatusTemplate += `|2=${newStatusComments}`;
        }
        newStatusTemplate += "}}";
        return newStatusTemplate;

    }

    function transformSectionWikitext(oldSectionWikitext, newStatus, newStatusComments, outcomeWikitext) {

        let result;
        const statusTemplatePattern = /{{status(.+)}}/i;
        const outcomeInsertionPoint = /\*處理結果:<!-- 請勿編輯本行並留待管理員填寫更改 -->/gi;

        // sanity check
        if (!oldSectionWikitext.match(statusTemplatePattern) || !oldSectionWikitext.match(outcomeInsertionPoint)) {
            throw new Error("此章節似乎未使用標準模板格式,或者已經被關閉了;請手動操作。");
        }

        // newStatus + newStatusComments
        const newStatusTemplate = _constructStatusWikitext(newStatus, newStatusComments);
        result = oldSectionWikitext.replace(statusTemplatePattern, newStatusTemplate);

        // outcomeWikitext
        result = result.replace(outcomeInsertionPoint, `*處理結果:${outcomeWikitext} --~~~~`);

        return result;

    }

// Exploratory QA passed 2020-07-16
    function _losslessSplit(string, splitter) {

        // insert "SPLITTOKEN" before all splitters
        const splitToken = "98764burcgbckgbrcxeroqcdonsathbsn";
        const stringWithSplitToken = string.replace(splitter, match => `${splitToken}${match}`);

        return stringWithSplitToken.split(splitToken);

    }// Other main app files

    async function ignite() {

        // store the raw wikitext
        rawWikitext = await apiEndpoint.readPage(currentPageTitle);

        // insert (close) buttons on all HTML H2s
        insertAllCloseButtons();

    }

    /**
     * Close button on click handler function
     * NOTE - must be bound to a specific indexOfButton value
     * @param event {Event} default dom event
     * @param indexOfButton {number} starting from 0, the index of the button clicked
     */
    function closeButtonOnClick(event, indexOfButton) {

        // extract the title of the target h2
        const title = extractH2Title(event.target.parentElement.parentElement);

        mw.loader.using(['jquery.ui'], openJQDialog.bind(null, title, indexOfButton));

    }

    function handleError(error) {

    }

    function cdAssert(conditional) {
        if (!conditional) {
            throw new Error("Internal error");
        }
    }

    return ignite();

}

launchCloseDRVTool();
// </nowiki>