User:Chlod/Scripts/InfringementAssistant.js

/*
 * Infringement Assistant
 *
 * More information on the userscript itself can be found at [[User:Chlod/IA]].
 */
// <nowiki>
mw.loader.using([
    "oojs-ui-core",
    "oojs-ui-windows",
    "oojs-ui-widgets",
    "mediawiki.api",
    "mediawiki.util"
], async function() {

    // =============================== STYLES =================================

    mw.util.addCSS(`
        .ia-submit {
            margin-left: auto;
            margin-top: 16px;
        }
    `);

    // ============================== CONSTANTS ===============================

    /**
     * Debug mode will redirect CP listings to Special:MyPage/sandbox/{date} and
     * replace the copyvio template with a soft link (using {{T}}).
     * @type {boolean}
     */
    const debug = false;

    const advert = "([[User:Chlod/IA|InfringementAssistant]])";
    /**
     * Using a fixed set of months since `mw.language.months` changes depending
     * on `?uselang` even if we're still on the English Wikipedia.
     * @type {string[]}
     */
    const months = [
        "January", "February", "March", "April", "May", "June", "July",
        "August", "September", "October", "November", "December"
    ];

    // =========================== HELPER FUNCTIONS ===========================

    function getListingDate(header = false) {
        const today = new Date();
        return `${
            header ? today.getUTCDate() : today.getUTCFullYear()
        } ${
            months[today.getUTCMonth()]
        } ${
            header ? today.getUTCFullYear() : today.getUTCDate()
        }`;
    }

    /**
     * Gets the title of today's copyright problems page.
     * @returns {string}
     */
    function getListingPage() {
        return (debug ? `User:${mw.config.get("wgUserName")}/sandbox/` : "Wikipedia:Copyright problems/")
            + getListingDate();
    }

    /**
     * Ask for confirmation before unloading.
     * @param {BeforeUnloadEvent} event
     */
    function exitBlock(event) {
        event.preventDefault();
        return event.returnValue = undefined;
    }

    // ============================== SINGLETONS ==============================

    /**
     * The WindowManager for this userscript.
     */
    const windowManager = new OO.ui.WindowManager();
    document.body.appendChild(windowManager.$element[0]);

    /**
     * MediaWiki API class.
     * @type {mw.Api}
     */
    const api = new mw.Api();
    const pageName = mw.config.get("wgPageName").replace(/_/g, " ");

    // =========================== PROCESS FUNCTIONS ==========================

    async function shadowPage(options) {
        let summary = `Hiding ${
            options.fullPage ? "the page" : `/* ${options.sectionName} */`
        } due to a suspected/complicated copyright violation (see [[${
            getListingPage()}#${pageName
        }]]) ${
            advert
        }`;

        if (options.fullPage) {
            return api.postWithEditToken({
                action: "edit",
                title: pageName,
                prependtext: `{{${debug ? "T|" : ""}subst:copyvio|${options.fromText}|fullpage=yes}}\n`,
                nocreate: true,
                summary: summary
            });
        } else if (options.section) {
            return api.postWithEditToken({
                action: "edit",
                title: pageName,
                section: options.section,
                prependtext: `{{${debug ? "T|" : ""}subst:copyvio|${options.fromText}}}\n`,
                appendtext: `\n{{${debug ? "T|" : ""}Copyvio/bottom}}`,
                nocreate: true,
                summary: summary
            });
        } else {
            throw "Illegal state.";
        }
    }

    async function addListing(options) {
        const listingPage = getListingPage();
        const pageExists = (await api.get({
            "action": "query",
            "titles": listingPage
        }))["query"]["pages"]["-1"] == null;

        let summary = `${
            pageExists ? "A" : "Created page and a"
        }dded listing for [[${
            pageName
        }${
            !options.fullPage && options.sectionName ? `#${options.sectionName}|${pageName} § ${
                options.sectionName
            }` : ""
        }]] ${
            advert
        }`;

        const listingText = `\n* {{subst:article-cv|${pageName}}} from ${
            options.fromText
        }.${
            options.additionalNotes ? ` ${options.additionalNotes}` : ""
        } ~~~~`;

        if (pageExists) {
            return api.postWithEditToken({
                action: "edit",
                title: listingPage,
                appendtext: listingText,
                recreate: true,
                summary: summary
            });
        } else {
            const listingHeader = `==== [[${listingPage}|${getListingDate(true)}]] ====\n`;
            return api.postWithEditToken({
                action: "edit",
                title: listingPage,
                text: `${listingHeader}${listingText}`,
                recreate: true,
                summary: summary
            });
        }
    }

    // =============================== PANELS =================================

    function SuspectedInfringementPanel(config) {
        SuspectedInfringementPanel.super.call( this, name, config );

        this.inputs = {
            fullPage: new OO.ui.CheckboxInputWidget({ selected: true }),
            section: new OO.ui.DropdownInputWidget({
                disabled: true,
                options: config.context["sections"].length > 0 ? [
                    { data: "0", label: "0: Lead" },
                    ...config.context["sections"].map(
                        (d) => { return { data: d.index, label: `${d.number}: ${d.line}` }; }
                    )
                ] : null,
                placeholder: "Select section to hide"
            }),
            fromURL: new OO.ui.CheckboxInputWidget({ selected: true }),
            urls: new OO.ui.MenuTagMultiselectWidget({
                allowArbitrary: true,
                inputPosition: "outline",
                indicators: [ "required" ],
                placeholder: "Add URL",
                options: config.context["externallinks"].length > 0 ? config.context["externallinks"].map(
                    (d) => { return { data: d, label: d }; }
                ) : null
            }),
            rawFrom: new OO.ui.MultilineTextInputWidget({
                autosize: true,
                maxRows: 2
            }),
            additionalNotes: new OO.ui.MultilineTextInputWidget({
                autosize: true,
                maxRows: 2
            })
        };
        this.fields = {
            fullPage: new OO.ui.FieldLayout(this.inputs.fullPage, {
                align: "inline",
                label: "Hide the entire page"
            }),
            section: new OO.ui.FieldLayout(this.inputs.section, {
                align: "top",
                label: "Section"
            }),
            fromURL: new OO.ui.FieldLayout(this.inputs.fromURL, {
                $overlay: config.dialog.$overlay,
                align: "inline",
                label: "Use URLs for the origin",
                help: "URLs will automatically be wrapped with brackets to shorten the external link. " +
                    "Disabling this option will present the text as is."
            }),
            urls: new OO.ui.FieldLayout(this.inputs.urls, {
                align: "top",
                label: "Source of copied content"
            }),
            rawFrom: new OO.ui.FieldLayout(this.inputs.rawFrom, {
                align: "top",
                label: "Source of copied content"
            }),
            additionalNotes: new OO.ui.FieldLayout(this.inputs.additionalNotes, {
                align: "top",
                label: "Additional notes"
            })
        }

        this.fields.rawFrom.toggle(false);

        this.inputs.fromURL.on("change", (selected) => {
            this.fields.rawFrom.toggle(!selected);
            this.fields.urls.toggle(selected);
        });

        this.inputs.fullPage.on("change", (selected) => {
            this.inputs.section.setDisabled(selected);
        });

        this.urls = [];
        this.inputs.urls.on("change", (items) => {
            this.urls = items.map(i => i.data);
        });

        for (const field of Object.values(this.fields)) {
            /** @var $element */
            this.$element.append(field.$element);
        }

        const submit = new OO.ui.ButtonWidget({
            label: "Submit",
            flags: [ "primary", "progressive" ],
            classes: [ "ia-submit" ]
        });
        const submitContainer = document.createElement("div");
        submitContainer.style.textAlign = "right";
        submitContainer.appendChild(submit.$element[0]);

        submit.on("click", () => {
            const panel = this;
            config.dialog.setCompletionFunction(async () => {
                const options = {
                    fullPage: panel.inputs.fullPage.isSelected(),
                    additionalNotes: panel.inputs.additionalNotes.getValue()
                };
                if (!options.fullPage) {
                    options.section = +panel.inputs.section.getValue();
                    options.sectionName = panel.inputs.section.dropdownWidget.label.replace(/^[0-9.]+: /g, "");
                }
                if (panel.inputs.fromURL.isSelected()) {
                    options.urls = panel.urls;
                    options.fromText = panel.urls.map(u => `[${
                        encodeURI(u)
                    }]`).join(", ")
                } else {
                    options.fromText = panel.inputs.rawFrom.getValue();
                }
                return addListing(options).then(() => shadowPage(options));
            });
            config.dialog.executeAction("execute");
        });

        /** @var $element */
        this.$element.append(submitContainer);
    }
    OO.inheritClass(SuspectedInfringementPanel, OO.ui.TabPanelLayout);
    // noinspection JSUnusedGlobalSymbols
    SuspectedInfringementPanel.prototype.setupTabItem = function () {
        /** @var tabItem */
        this.tabItem.setLabel("Suspected or complicated");
    };

    // =============================== DIALOGS ================================

    function InfringementAssistantDialog(config) {
        InfringementAssistantDialog.super.call(this, config);
        if (config.context == null)
            throw "Context was not provided.";
        else
            this.context = config.context;
    }
    OO.inheritClass(InfringementAssistantDialog, OO.ui.ProcessDialog);

    InfringementAssistantDialog.static.name = "infringementAssistantDialog";
    InfringementAssistantDialog.static.title = "Infringement Assistant";
    InfringementAssistantDialog.static.size = "medium";
    InfringementAssistantDialog.static.actions = [
        {
            flags: ["safe", "close"],
            icon: "close",
            label: "Close",
            title: "Close",
            invisibleLabel: true,
            action: "close"
        }
    ];

    // noinspection JSUnusedGlobalSymbols
    InfringementAssistantDialog.prototype.getBodyHeight = function () {
        return 470;
    };

    InfringementAssistantDialog.prototype.initialize = function () {
        InfringementAssistantDialog.super.prototype.initialize.apply(this, arguments);

        this.indexLayout = new OO.ui.IndexLayout({
            expanded: true
        });
        this.panelLayout = new OO.ui.PanelLayout({
            expanded: true,
            framed: true,
            content: [ this.indexLayout ]
        });

        this.indexLayout.addTabPanels([
            new SuspectedInfringementPanel({
                dialog: this,
                context: this.context
            })
        ]);

        /** @var $content */
        this.$body.append(this.panelLayout.$element);
    }

    InfringementAssistantDialog.prototype.setCompletionFunction = function (process) {
        this.completionFunction = process;
    }

    InfringementAssistantDialog.prototype.getSetupProcess = function (data) {
        const process = InfringementAssistantDialog.super.prototype.getSetupProcess.call(this, data);

        process.next(() => {
            window.addEventListener("beforeunload", exitBlock);
        });

        return process;
    }

    InfringementAssistantDialog.prototype.getActionProcess = function (action) {
        const process = InfringementAssistantDialog.super.prototype.getActionProcess.call(this, action);

        if (action === "execute") {
            process.first(this.completionFunction);
        }
        process.next(function () {
            this.close({ action: action });
        }, this);

        return process;
    }

    InfringementAssistantDialog.prototype.getTeardownProcess = function (data) {
        window.removeEventListener("beforeunload", exitBlock);
        /** @var any */
        return InfringementAssistantDialog.super.prototype.getTeardownProcess.call(this, data);
    }

    // ============================== INITIALIZE ==============================

    function openDialog() {
        api.get({
            "action": "parse",
            "page": pageName,
            "prop": "externallinks|sections"
        }).then((data) => {
            const dialog = new InfringementAssistantDialog({
                context: data["parse"]
            });
            windowManager.addWindows([ dialog ]);
            windowManager.openWindow(dialog);
        }).catch((error) => {
            if (error === "missingtitle")
                OO.ui.alert("Cannot open Infringement Assistant: The page does not exist.");
            else
                OO.ui.alert(`Cannot open Infringement Assistant: ${error}`);
        });
    }

    if (document.getElementById("pt-ia") == null && mw.config.get("wgNamespaceNumber") >= 0) {
        mw.util.addPortletLink(
            "p-tb",
            "javascript:void(0)",
            "Infringement Assistant",
            "pt-ia"
        ).addEventListener("click", function() {
            openDialog();
        });
    }

    // Query parameter-based autostart
    if (/[?&]ia-autostart(=(1|yes|true|on)?(&|$)|$)/.test(window.location.search)) {
        openDialog();
    }

    if (debug) {
        mw.notify("Debug mode has been enabled.", { title: "Infringement Assistant" });
    }

    document.dispatchEvent(new Event("ia:load"));

});
// </nowiki>
/*
 * Copyright 2021 Chlod
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Also licensed under the Creative Commons Attribution-ShareAlike 3.0
 * Unported License, a copy of which is available at
 *
 *     https://creativecommons.org/licenses/by-sa/3.0
 *
 */

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.