User:Syunsyunminmin/script-installer-core.js: Difference between revisions

From Test Wiki
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
 
(3 intermediate revisions by the same user not shown)
Line 8: Line 8:


// How many scripts do we need before we show the quick filter?
// How many scripts do we need before we show the quick filter?
var NUM_SCRIPTS_FOR_SEARCH = 5;
var NUM_SCRIPTS_FOR_SEARCH = 1;


// The master import list, keyed by target. (A "target" is a user JS subpage
// The master import list, keyed by target. (A "target" is a user JS subpage
Line 46: Line 46:
panelHeader: "現在、以下のスクリプトがインストールされています。",
panelHeader: "現在、以下のスクリプトがインストールされています。",
cannotInstall: "インストール出来ません",
cannotInstall: "インストール出来ません",
cannotInstallSkin: "このページはあなたの利用者カスタムページの1つで、すでにページの読み込み時に実行されているかもしれません(common.jsの場合は実行されています)。",
cannotInstallSkin: "This page is one of your user customization pages, and may (will, if common.js) already run on each page load.",
cannotInstallContentModel: "ページのコンテンツモデルは$1です。'javascript'ではありません。",
cannotInstallContentModel: "ページのコンテンツモデルは$1です。'javascript'ではありません。",
insecure: "(insecure)", // used at the end of some messages
insecure: "(危険)", // used at the end of some messages
notJavaScript: "not JavaScript",
notJavaScript: "JavaScriptではない",
installViaPreferences: "Install via preferences",
installViaPreferences: "個人設定からインストール",
showNormalizeLinks: '"標準化"リンクを表示しますか?',
showNormalizeLinks: '"標準化"リンクを表示しますか?',
showMoveLinks: '"移動"リンクを表示しますか?',
showMoveLinks: '"移動"リンクを表示しますか?',
quickFilter: "Quick filter:",
quickFilter: "簡易フィルター:",
tempWarning: "利用者サブページまたはMediaWiki名前空間以外で保護されているページからのインストールは一時的なものであり、いつか削除される可能性があります。",
tempWarning: "利用者サブページまたはMediaWiki名前空間以外で保護されているページからのインストールは一時的なものであり、いつか削除される可能性があります。",
badPageError: "ページは User: または MediaWiki: ではなく、保護されていません",
badPageError: "ページは User: または MediaWiki: ではなく、保護されていません",
Line 399: Line 399:
$.each( imports, function ( targetName, targetImports ) {
$.each( imports, function ( targetName, targetImports ) {
var fmtTargetName = ( targetName === "common"
var fmtTargetName = ( targetName === "common"
? "common (applies to all skins)"
? "common (全ての外装に適用)"
: targetName );
: targetName );
if( targetImports.length ) {
if( targetImports.length ) {

Latest revision as of 14:21, 4 February 2023

// <nowiki>
( function () {
    // An mw.Api object
    var api;

    // Keep "common" at beginning
    var SKINS = [ "common", "monobook", "minerva", "vector", "vector-2022", "timeless" ];

    // How many scripts do we need before we show the quick filter?
    var NUM_SCRIPTS_FOR_SEARCH = 1;

    // The master import list, keyed by target. (A "target" is a user JS subpage
    // where the script is imported, like "common" or "vector".) Set in buildImportList
    var imports = {};

    // Local scripts, keyed on name; value will be the target. Set in buildImportList.
    var localScriptsByName = {};

    // How many scripts are installed?
    var scriptCount = 0;

    // Goes on the end of edit summaries
    var ADVERT = " ([[User:Enterprisey/script-installer|script-installer]])";

    /**
     * Strings, for translation
     */
    var STRINGS = {
        installSummary: "$1をインストール",
        installLinkText: "インストール",
        installProgressMsg: "インストール中...",
        uninstallSummary: "$1をアンインストール",
        uninstallLinkText: "アンインストール",
        uninstallProgressMsg: "アンインストール中...",
        disableSummary: "$1を無効化",
        disableLinkText: "無効化",
        disableProgressMsg: "無効化中...",
        enableSummary: "$1を有効化",
        enableLinkText: "有効化",
        enableProgressMsg: "有効化中...",
        moveLinkText: "移動",
        moveProgressMsg: "移動中...",
        movePrompt: "移動先は? 次の中から一つ入力してください:", // followed by the names of skins
        normalizeSummary: "スクリプトのインストールを標準化",
        remoteUrlDesc: "$1, $2からの読み込み",
        panelHeader: "現在、以下のスクリプトがインストールされています。",
        cannotInstall: "インストール出来ません",
        cannotInstallSkin: "このページはあなたの利用者カスタムページの1つで、すでにページの読み込み時に実行されているかもしれません(common.jsの場合は実行されています)。",
        cannotInstallContentModel: "ページのコンテンツモデルは$1です。'javascript'ではありません。",
        insecure: "(危険)", // used at the end of some messages
        notJavaScript: "JavaScriptではない",
        installViaPreferences: "個人設定からインストール",
        showNormalizeLinks: '"標準化"リンクを表示しますか?',
        showMoveLinks: '"移動"リンクを表示しますか?',
        quickFilter: "簡易フィルター:",
        tempWarning: "利用者サブページまたはMediaWiki名前空間以外で保護されているページからのインストールは一時的なものであり、いつか削除される可能性があります。",
        badPageError: "ページは User: または MediaWiki: ではなく、保護されていません",
        manageUserScripts: "ユーザースクリプトの管理",
        bigSecurityWarning: "警告!$1すべてのユーザースクリプトには、あなたのアカウントを侵害する悪質な内容が含まれている可能性があります。スクリプトをインストールするということは、他の人によってアカウントが乗っ取られる可能性があるということです。インストールする前に作者を信頼できるか確認してください。スクリプトが安全かどうかわからない場合は、プロジェクト:ウィキ技術部で確認してください。このスクリプトをインストールしますか? (次からこのダイアログを非表示にするにはcommon.jsに sciNoConfirm=true; を記述してください)",
        securityWarningSection: " $1を信頼しますか?"
    };

    var USER_NAMESPACE_NAME = mw.config.get( "wgFormattedNamespaces" )[2];

    /**
     * Constructs an Import. An Import is a line in a JS file that imports a
     * user script. Properties:
     *
     *  - "page" is a page name, such as "User:Foo/Bar.js".
     *  - "wiki" is a wiki from which the script is loaded, such as
     *    "en.wikipedia". If null, the script is local, on the user's
     *    wiki.
     *  - "url" is a URL that can be passed into mw.loader.load.
     *  - "target" is the title of the user subpage where the script is,
     *    without the .js ending: for example, "common".
     *  - "disabled" is whether this import is commented out.
     *  - "type" is 0 if local, 1 if remotely loaded, and 2 if URL.
     *
     * EXACTLY one of "page" or "url" are null for every Import. This
     * constructor should not be used directly; use the factory
     * functions (Import.ofLocal, Import.ofUrl, Import.fromJs) instead.
     */
    function Import( page, wiki, url, target, disabled ) {
        this.page = page;
        this.wiki = wiki;
        this.url = url;
        this.target = target;
        this.disabled = disabled;
        this.type = this.url ? 2 : ( this.wiki ? 1 : 0 );
    }

    Import.ofLocal = function ( page, target, disabled ) {
        if( disabled === undefined ) disabled = false;
        return new Import( page, null, null, target, disabled );
    }

    /** URL to Import. Assumes wgScriptPath is "/w" */
    Import.ofUrl = function ( url, target, disabled ) {
        if( disabled === undefined ) disabled = false;
        var URL_RGX = /^(?:https?:)?\/\/(.+?)\.org\/w\/index\.php\?.*?title=(.+?(?:&|$))/;
        var match;
        if( match = URL_RGX.exec( url ) ) {
            var title = decodeURIComponent( match[2].replace( /&$/, "" ) ),
                wiki = decodeURIComponent( match[1] );
            return new Import( title, wiki, null, target, disabled );
        }
        return new Import( null, null, url, target, disabled );
    }

    Import.fromJs = function ( line, target ) {
        var IMPORT_RGX = /^\s*(\/\/)?\s*importScript\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/;
        var match;
        if( match = IMPORT_RGX.exec( line ) ) {
            return Import.ofLocal( unescapeForJsString( match[2] ), target, !!match[1] );
        }

        var LOADER_RGX = /^\s*(\/\/)?\s*mw\.loader\.load\s*\(\s*(?:"|')(.+?)(?:"|')\s*\)/;
        if( match = LOADER_RGX.exec( line ) ) {
            return Import.ofUrl( unescapeForJsString( match[2] ), target, !!match[1] );
        }
    }

    Import.prototype.getDescription = function ( useWikitext ) {
        switch( this.type ) {
            case 0: return useWikitext ? ( "[[" + this.page + "]]" ) : this.page;
            case 1: return STRINGS.remoteUrlDesc.replace( "$1", this.page ).replace( "$2", this.wiki );
            case 2: return this.url;
        }
    }

    /**
     * Human-readable (NOT necessarily suitable for ResourceLoader) URL.
     */
    Import.prototype.getHumanUrl = function () {
        switch( this.type ) {
            case 0: return "/wiki/" + encodeURI( this.page );
            case 1: return "//" + this.wiki + ".org/wiki/" + encodeURI( this.page );
            case 2: return this.url;
        }
    }

    Import.prototype.toJs = function () {
        var dis = this.disabled ? "//" : "",
            url = this.url;
        switch( this.type ) {
            case 0: return dis + "importScript('" + escapeForJsString( this.page ) + "'); // Backlink: [[" + escapeForJsComment( this.page ) + "]]";
            case 1: url = "//" + encodeURIComponent( this.wiki ) + ".org/w/index.php?title=" +
                            encodeURIComponent( this.page ) + "&action=raw&ctype=text/javascript"; 
                    /* FALL THROUGH */
            case 2: return dis + "mw.loader.load('" + escapeForJsString( url ) + "');";
        }
    }

    /**
     * Installs the import.
     */
    Import.prototype.install = function () {
        return api.postWithEditToken( {
            action: "edit",
            title: getFullTarget( this.target ),
            summary: STRINGS.installSummary.replace( "$1", this.getDescription( /* useWikitext */ true ) ) + ADVERT,
            appendtext: "\n" + this.toJs()
        } );
    }

    /**
     * Get all line numbers from the target page that mention
     * the specified script.
     */
    Import.prototype.getLineNums = function ( targetWikitext ) {
        function quoted( s ) {
            return new RegExp( "(['\"])" + escapeForRegex( s ) + "\\1" );
        }
        var toFind;
        switch( this.type ) {
            case 0: toFind = quoted( escapeForJsString( this.page ) ); break;
            case 1: toFind = new RegExp( escapeForRegex( encodeURIComponent( this.wiki ) ) + ".*?" +
                            escapeForRegex( encodeURIComponent( this.page ) ) ); break;
            case 2: toFind = quoted( escapeForJsString( this.url ) ); break;
        }
        var lineNums = [], lines = targetWikitext.split( "\n" );
        for( var i = 0; i < lines.length; i++ ) {
            if( toFind.test( lines[i] ) ) {
                lineNums.push( i );
            }
        }
        return lineNums;
    }

    /**
     * Uninstalls the given import. That is, delete all lines from the
     * target page that import the specified script.
     */
    Import.prototype.uninstall = function () {
        var that = this;
        return getWikitext( getFullTarget( this.target ) ).then( function ( wikitext ) {
            var lineNums = that.getLineNums( wikitext ),
                newWikitext = wikitext.split( "\n" ).filter( function ( _, idx ) {
                    return lineNums.indexOf( idx ) < 0;
                } ).join( "\n" );
            return api.postWithEditToken( {
                action: "edit",
                title: getFullTarget( that.target ),
                summary: STRINGS.uninstallSummary.replace( "$1", that.getDescription( /* useWikitext */ true ) ) + ADVERT,
                text: newWikitext
            } );
        } );
    }

    /**
     * Sets whether the given import is disabled, based on the provided
     * boolean value.
     */
    Import.prototype.setDisabled = function ( disabled ) {
        var that = this;
        this.disabled = disabled;
        return getWikitext( getFullTarget( this.target ) ).then( function ( wikitext ) {
            var lineNums = that.getLineNums( wikitext ),
                newWikitextLines = wikitext.split( "\n" );

            if( disabled ) {
                lineNums.forEach( function ( lineNum ) {
                    if( newWikitextLines[lineNum].trim().indexOf( "//" ) != 0 ) {
                        newWikitextLines[lineNum] = "//" + newWikitextLines[lineNum].trim();
                    }
                } );
            } else {
                lineNums.forEach( function ( lineNum ) {
                    if( newWikitextLines[lineNum].trim().indexOf( "//" ) == 0 ) {
                        newWikitextLines[lineNum] = newWikitextLines[lineNum].replace( /^\s*\/\/\s*/, "" );
                    }
                } );
            }

            var summary = ( disabled ? STRINGS.disableSummary : STRINGS.enableSummary )
                    .replace( "$1", that.getDescription( /* useWikitext */ true ) ) + ADVERT;
            return api.postWithEditToken( {
                action: "edit",
                title: getFullTarget( that.target ),
                summary: summary,
                text: newWikitextLines.join( "\n" )
            } );
        } );
    }

    Import.prototype.toggleDisabled = function () {
        this.disabled = !this.disabled;
        return this.setDisabled( this.disabled );
    }

    /**
     * Move this import to another file.
     */
    Import.prototype.move = function ( newTarget ) {
        if( this.target === newTarget ) return;
        var old = new Import( this.page, this.wiki, this.url, this.target, this.disabled );
        this.target = newTarget;
        return $.when( old.uninstall(), this.install() );
    }

    function getAllTargetWikitexts() {
        return $.getJSON(
            mw.util.wikiScript( "api" ),
            {
                format: "json",
                action: "query",
                prop: "revisions",
                rvprop: "content",
                rvslots: "main",
                titles: SKINS.map( getFullTarget ).join( "|" )
            }
        ).then( function ( data ) {
            if( data && data.query && data.query.pages ) {
                var result = {};
                    prefixLength = mw.config.get( "wgUserName" ).length + 6;
                Object.values( data.query.pages ).forEach( function ( moreData ) {
                    var nameWithoutExtension = new mw.Title( moreData.title ).getNameText();
                    var targetName = nameWithoutExtension.substring( nameWithoutExtension.indexOf( "/" ) + 1 );
                    result[targetName] = moreData.revisions ? moreData.revisions[0].slots.main["*"] : null;
                } );
                return result;
            }
        } );
    }

    function buildImportList() {
        return getAllTargetWikitexts().then( function ( wikitexts ) {
            Object.keys( wikitexts ).forEach( function ( targetName ) {
                var targetImports = [];
                if( wikitexts[ targetName ] ) {
                    var lines = wikitexts[ targetName ].split( "\n" );
                    var currImport;
                    for( var i = 0; i < lines.length; i++ ) {
                        if( currImport = Import.fromJs( lines[i], targetName ) ) {
                            targetImports.push( currImport );
                            scriptCount++;
                            if( currImport.type === 0 ) {
                                if( !localScriptsByName[ currImport.page ] )
                                    localScriptsByName[ currImport.page ] = [];
                                localScriptsByName[ currImport.page ].push( currImport.target );
                            }
                        }
                    }
                }
                imports[ targetName ] = targetImports;
            } );
        } );
    }


    /*
     * "Normalizes" (standardizes the format of) lines in the given
     * config page.
     */
    function normalize( target ) {
        return getWikitext( getFullTarget( target ) ).then( function ( wikitext ) {
            var lines = wikitext.split( "\n" ),
                newLines = Array( lines.length ),
                currImport;
            for( var i = 0; i < lines.length; i++ ) {
                if( currImport = Import.fromJs( lines[i], target ) ) {
                    newLines[i] = currImport.toJs();
                } else {
                    newLines[i] = lines[i];
                }
            }
            return api.postWithEditToken( {
                action: "edit",
                title: getFullTarget( target ),
                summary: STRINGS.normalizeSummary,
                text: newLines.join( "\n" )
            } );
        } );
    }

    function conditionalReload( openPanel ) {
        if( window.scriptInstallerAutoReload ) {
            if( openPanel ) document.cookie = "open_script_installer=yes";
            window.location.reload( true );
        }
    }

    /********************************************
     *
     * UI code
     *
     ********************************************/
    function makePanel() {
        var list = $( "<div>" ).attr( "id", "script-installer-panel" )
            .append( $( "<header>" ).text( STRINGS.panelHeader ) );
        var container = $( "<div>" ).addClass( "container" ).appendTo( list );
        
        // Container for checkboxes
        container.append( $( "<div>" )
            .attr( "class", "checkbox-container" )
            .append(
                $( "<input>" )
                    .attr( { "id": "siNormalize", "type": "checkbox" } )
                    .click( function () {
                        $( ".normalize-wrapper" ).toggle( 0 )
                    } ),
                $( "<label>" )
                    .attr( "for", "siNormalize" )
                    .text( STRINGS.showNormalizeLinks ),
                $( "<input>" )
                    .attr( { "id": "siMove", "type": "checkbox" } )
                    .click( function () {
                        $( ".move-wrapper" ).toggle( 0 )
                    } ),
                $( "<label>" )
                    .attr( "for", "siMove" )
                    .text( STRINGS.showMoveLinks ) ) );
        if( scriptCount > NUM_SCRIPTS_FOR_SEARCH ) {
            container.append( $( "<div>" )
                .attr( "class", "filter-container" )
                .append(
                    $( "<label>" )
                        .attr( "for", "siQuickFilter" )
                        .text( STRINGS.quickFilter ),
                    $( "<input>" )
                        .attr( { "id": "siQuickFilter", "type": "text" } )
                        .on( "input", function () {
                            var filterString = $( this ).val();
                            if( filterString ) {
                                var sel = "#script-installer-panel li[name*='" +
                                        $.escapeSelector( $( this ).val() ) + "']";
                                $( "#script-installer-panel li.script" ).toggle( false );
                                $( sel ).toggle( true );
                            } else {
                                $( "#script-installer-panel li.script" ).toggle( true );
                            }
                        } )
                ) );

            // Now, get the checkboxes out of the way
            container.find( ".checkbox-container" )
                .css( "float", "right" );
        }
        $.each( imports, function ( targetName, targetImports ) {
            var fmtTargetName = ( targetName === "common"
                ? "common (全ての外装に適用)"
                : targetName );
                if( targetImports.length ) {
                container.append(
                    $( "<h2>" ).append(
                        fmtTargetName,
                        $( "<span>" )
                        .addClass( "normalize-wrapper" )
                        .append( 
                            " (",
                            $( "<a>" )
                                .text( "normalize" )
                                .click( function () {
                                    normalize( targetName ).done( function () {
                                        conditionalReload( true );
                                    } );
                                 } ),
                            ")" )
                            .hide() ),
                        $( "<ul>" ).append(
                            targetImports.map( function ( anImport ) {
                                return $( "<li>" )
                                    .addClass( "script" )
                                    .attr( "name", anImport.getDescription() )
                                    .append(
                                        $( "<a>" )
                                            .text( anImport.getDescription() )
                                            .addClass( "script" )
                                            .attr( "href", anImport.getHumanUrl() ),
                                        " (",
                                        $( "<a>" )
                                            .text( STRINGS.uninstallLinkText )
                                            .click( function () {
                                                $( this ).text( STRINGS.uninstallProgressMsg );
                                                anImport.uninstall().done( function () {
                                                    conditionalReload( true );
                                                } );
                                            } ),
                                        " | ",
                                        $( "<a>" )
                                            .text( anImport.disabled ? STRINGS.enableLinkText : STRINGS.disableLinkText )
                                            .click( function () {
                                                $( this ).text( anImport.disabled ? STRINGS.enableProgressMsg : STRINGS.disableProgressMsg );
                                                anImport.toggleDisabled().done( function () {
                                                    $( this ).toggleClass( "disabled" );
                                                    conditionalReload( true );
                                                } );
                                            } ),
                                        $( "<span>" )
                                            .addClass( "move-wrapper" )
                                            .append(
                                            " | ",
                                            $( "<a>" )
                                                .text( STRINGS.moveLinkText )
                                                .click( function () {
                                                    var dest = null;
                                                    var PROMPT = STRINGS.movePrompt + " " + SKINS.join( ", " );
                                                    do {
                                                        dest = ( window.prompt( PROMPT ) || "" ).toLowerCase();
                                                    } while( dest && SKINS.indexOf( dest ) < 0 )
                                                    if( !dest ) return;
                                                    $( this ).text( STRINGS.moveProgressMsg );
                                                    anImport.move( dest ).done( function () {
                                                        conditionalReload( true );
                                                    } );
                                                } )
                                            )
                                            .hide(),
                                        ")" )
                                .toggleClass( "disabled", anImport.disabled );
                                } ) ) );
                }
        } );
        return list;
    }

    function buildCurrentPageInstallElement() {
        var addingInstallLink = false; // will we be adding a legitimate install link?
        var installElement = $( "<span>" ); // only used if addingInstallLink is set to true

        var namespaceNumber = mw.config.get( "wgNamespaceNumber" );
        var pageName = mw.config.get( "wgPageName" );

        // Namespace 2 is User
        if( namespaceNumber === 2 &&
                pageName.indexOf( "/" ) > 0 ) {
            var contentModel = mw.config.get( "wgPageContentModel" );
            if( contentModel === "javascript" ) {
                var prefixLength = mw.config.get( "wgUserName" ).length + 6;
                if( pageName.indexOf( USER_NAMESPACE_NAME + ":" + mw.config.get( "wgUserName" ) ) === 0 ) {
                    var skinIndex = SKINS.indexOf( pageName.substring( prefixLength ).slice( 0, -3 ) );
                    if( skinIndex >= 0 ) {
                        return $( "<abbr>" ).text( STRINGS.cannotInstall )
                                .attr( "title", STRINGS.cannotInstallSkin );
                    }
                }
                addingInstallLink = true;
            } else {
                return $( "<abbr>" ).text( STRINGS.cannotInstall + " (" + STRINGS.notJavaScript + ")" )
                        .attr( "title", STRINGS.cannotInstallContentModel.replace( "$1", contentModel ) );
            }
        }

        // Namespace 8 is MediaWiki
        if( namespaceNumber === 8 ) {
            return $( "<a>" ).text( STRINGS.installViaPreferences )
                    .attr( "href", mw.util.getUrl( "Special:Preferences" ) + "#mw-prefsection-gadgets" );
        }

        var editRestriction = mw.config.get( "wgRestrictionEdit" ) || [];
        if( ( namespaceNumber !== 2 && namespaceNumber !== 8 ) &&
            ( editRestriction.indexOf( "sysop" ) >= 0 ||
                editRestriction.indexOf( "editprotected" ) >= 0 ) ) {
            installElement.append( " ",
                $( "<abbr>" ).append(
                    $( "<img>" ).attr( "src", "https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Achtung-yellow.svg/20px-Achtung-yellow.svg.png" ).addClass( "warning" ),
                    STRINGS.insecure )
                .attr( "title", STRINGS.tempWarning ) );
            addingInstallLink = true;
        }

        if( addingInstallLink ) {
            var fixedPageName = mw.config.get( "wgPageName" ).replace( /_/g, " " );
            installElement.prepend( $( "<a>" )
                    .attr( "id", "script-installer-main-install" )
                    .text( localScriptsByName[ fixedPageName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
                    .click( makeLocalInstallClickHandler( fixedPageName ) ) );

            // If the script is installed but disabled, allow the user to enable it
            var allScriptsInTarget = imports[ localScriptsByName[ fixedPageName ] ];
            var importObj = allScriptsInTarget && allScriptsInTarget.find( function ( anImport ) { return anImport.page === fixedPageName; } );
            if( importObj && importObj.disabled ) {
                installElement.append( " | ",
                    $( "<a>" )
                        .attr( "id", "script-installer-main-enable" )
                        .text( STRINGS.enableLinkText )
                        .click( function () {
                            $( this ).text( STRINGS.enableProgressMsg );
                            importObj.setDisabled( false ).done( function () {
                                conditionalReload( false );
                            } );
                        } ) );
            }
            return installElement;
        }

        return $( "<abbr>" ).text( STRINGS.cannotInstall + " " + STRINGS.insecure )
                .attr( "title", STRINGS.badPageError );
    }

    function showUi() {
        var fixedPageName = mw.config.get( "wgPageName" ).replace( /_/g, " " );
        $( "#firstHeading" ).append( $( "<span>" )
            .attr( "id", "script-installer-top-container" )
            .append(
                buildCurrentPageInstallElement(),
                " | ",
                $( "<a>" )
                    .text( STRINGS.manageUserScripts ).click( function () {
                        if( !document.getElementById( "script-installer-panel" ) ) {
                            $( "#mw-content-text" ).before( makePanel() );
                        } else {
                            $( "#script-installer-panel" ).remove();
                        }
                     } ) ) );
    }

    function attachInstallLinks() {
        // At the end of each {{Userscript}} transclusion, there is
        // <span id='User:Foo/Bar.js' class='scriptInstallerLink'></span>
        $( "span.scriptInstallerLink" ).each( function () {
            var scriptName = this.id;
            $( this ).append( " | ", $( "<a>" )
                    .text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
                    .click( makeLocalInstallClickHandler( scriptName ) ) );
        } );

        $( "table.infobox-user-script" ).each( function () {
            var scriptName = $( this ).find( "th:contains('Source')" ).next().text() ||
                    mw.config.get( "wgPageName" );
            scriptName = /user:.+?\/.+?.js/i.exec( scriptName )[0];
            $( this ).children( "tbody" ).append( $( "<tr>" ).append( $( "<td>" )
                    .attr( "colspan", "2" )
                    .addClass( "script-installer-ibx" )
                    .append( $( "<button>" )
                        .addClass( "mw-ui-button mw-ui-progressive mw-ui-big" )
                        .text( localScriptsByName[ scriptName ] ? STRINGS.uninstallLinkText : STRINGS.installLinkText )
                        .click( makeLocalInstallClickHandler( scriptName ) ) ) ) );
        } );
    }

    function makeLocalInstallClickHandler( scriptName ) {
        return function () {
            var $this = $( this );
            if( $this.text() === STRINGS.installLinkText ) {
                var bigSecurityWarning = STRINGS.bigSecurityWarning;
                if( scriptName.indexOf( '/' ) >= 0 ) {
                    bigSecurityWarning = bigSecurityWarning.replace( '$1', STRINGS.securityWarningSection.replace( '$1', scriptName.substring( 0, scriptName.indexOf( '/' ) ) ) );
                } else {
                    bigSecurityWarning = bigSecurityWarning.replace( '$1', '' );
                }
                var okay = window.sciNoConfirm || window.confirm( bigSecurityWarning );
                if( okay ) {
                    $( this ).text( STRINGS.installProgressMsg )
                    Import.ofLocal( scriptName, window.scriptInstallerInstallTarget ).install().done( function () {
                        $( this ).text( STRINGS.uninstallLinkText );
                        conditionalReload( false );
                    }.bind( this ) );
                }
            } else {
                $( this ).text( STRINGS.uninstallProgressMsg )
                var uninstalls = uniques( localScriptsByName[ scriptName ] )
                        .map( function ( target ) { return Import.ofLocal( scriptName, target ).uninstall(); } )
                $.when.apply( $, uninstalls ).then( function () {
                    $( this ).text( STRINGS.installLinkText );
                    conditionalReload( false );
                }.bind( this ) );
            }
         };
    }

    function addCss() {
        mw.util.addCSS(
            "#script-installer-panel li.disabled a.script { "+
              "text-decoration: line-through; font-style: italic; }"+
            "#script-installer-panel { width:60%; border:solid lightgray 1px; "+
              "padding:0; margin-left: auto; "+
              "margin-right: auto; margin-bottom: 15px; overflow: auto; "+
              "box-shadow: 5px 5px 5px #999; background-color: #fff; z-index:50; }"+
            "#script-installer-panel header { background-color:#CAE1FF; display:block;"+
              "padding:5px; font-size:1.1em; font-weight:bold; text-align:left; }"+
            "#script-installer-panel .checkbox-container input { margin-left: 1.5em; }"+
            "#script-installer-panel .filter-container { margin-bottom: -0.75em; }"+
            "#script-installer-panel .filter-container label { margin-right: 0.35em; }"+
            "#script-installer-panel .container { padding: 0.75em; }"+
            "#script-installer-panel .container h2 { margin-top: 0.75em; }"+
            "#script-installer-panel a { cursor: pointer; }"+
            "#script-installer-main-install { font-weight: bold; }"+
            "#script-installer-top-container { bottom: 5px; font-size: 70%; margin-left: 1em }"+
            "body.skin-modern #script-installer-top-container a { color: inherit; cursor: pointer }"+
            "body.skin-timeless #script-installer-top-container a,body.skin-cologneblue #script-installer-top-container a { cursor: pointer }"+
            "#script-installer-top-container img.warning { position: relative; top: -2px; margin-right: 3px }"+
            "td.script-installer-ibx { text-align: center }"
        );
    }

    /********************************************
     *
     * Utility functions
     *
     ********************************************/

    /**
     * Gets the wikitext of a page with the given title (namespace required).
     */
    function getWikitext( title ) {
        return $.getJSON(
            mw.util.wikiScript( "api" ),
            {
                format: "json",
                action: "query",
                prop: "revisions",
                rvprop: "content",
                rvslots: "main",
                rvlimit: 1,
                titles: title
            }
        ).then( function ( data ) {
            var pageId = Object.keys( data.query.pages )[0];
            if( data.query.pages[pageId].revisions ) {
                return data.query.pages[pageId].revisions[0].slots.main["*"];
            }
            return "";
        } );
    }

    function escapeForRegex( s ) {
        return s.replace( /[-\/\\^$*+?.()|[\]{}]/g, '\\$&' );
    }

    /**
    * Escape a string for use in a JavaScript string literal.
    * This function is adapted from
    * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js
    * (released under the MIT licence).
    */
    function escapeForJsString( s ) {
        return s.replace( /["'\\\n\r\u2028\u2029]/g, function ( character ) {
            // Escape all characters not included in SingleStringCharacters and
            // DoubleStringCharacters on
            // http://www.ecma-international.org/ecma-262/5.1/#sec-7.8.4
            switch ( character ) {
                case '"':
                case "'":
                case '\\':
                    return '\\' + character;
                // Four possible LineTerminator characters need to be escaped:
                case '\n':
                    return '\\n';
                case '\r':
                    return '\\r';
                case '\u2028':
                    return '\\u2028';
                case '\u2029':
                    return '\\u2029';
            }
        } );
    }

    /**
    * Escape a string for use in an inline JavaScript comment (comments that
    * start with two slashes "//").
    * This function is adapted from
    * https://github.com/joliss/js-string-escape/blob/6887a69003555edf5c6caaa75f2592228558c595/index.js
    * (released under the MIT licence).
    */
    function escapeForJsComment( s ) {
        return s.replace( /[\n\r\u2028\u2029]/g, function ( character ) {
            switch ( character ) {
                // Escape possible LineTerminator characters
                case '\n':
                    return '\\n';
                case '\r':
                    return '\\r';
                case '\u2028':
                    return '\\u2028';
                case '\u2029':
                    return '\\u2029';
            }
        } );
    }

    /**
    * Unescape a JavaScript string literal.
    *
    * This is the inverse of escapeForJsString.
    */
    function unescapeForJsString( s ) {
        return s.replace( /\\"|\\'|\\\\|\\n|\\r|\\u2028|\\u2029/g, function ( substring ) {
            switch ( substring ) {
                case '\\"':
                    return '"';
                case "\\'":
                    return "'";
                case "\\\\":
                    return "\\";
                case "\\r":
                    return "\r";
                case "\\n":
                    return "\n";
                case "\\u2028":
                    return "\u2028";
                case "\\u2029":
                    return "\u2029";
            }
        } );
    }

    function getFullTarget ( target ) {
        return USER_NAMESPACE_NAME + ":" + mw.config.get( "wgUserName" ) + "/" + 
                target + ".js";
    }

    // From https://stackoverflow.com/a/10192255
    function uniques( array ){
        return array.filter( function( el, index, arr ) {
            return index === arr.indexOf( el );
        });
    }

    if( window.scriptInstallerAutoReload === undefined ) {
        window.scriptInstallerAutoReload = true;
    }

    if( window.scriptInstallerInstallTarget === undefined ) {
        window.scriptInstallerInstallTarget = "common"; // by default, install things to the user's common.js
    }

    var jsPage = mw.config.get( "wgPageName" ).slice( -3 ) === ".js" ||
        mw.config.get( "wgPageContentModel" ) === "javascript";
    $.when(
        $.ready,
        mw.loader.using( [ "mediawiki.api", "mediawiki.util" ] )
    ).then( function () {
        api = new mw.Api();
        addCss();
        buildImportList().then( function () {
            attachInstallLinks();
            if( jsPage ) showUi();

            // Auto-open the panel if we set the cookie to do so (see `conditionalReload()`)
            if( document.cookie.indexOf( "open_script_installer=yes" ) >= 0 ) {
                document.cookie = "open_script_installer=; expires=Thu, 01 Jan 1970 00:00:01 GMT";
                $( "#script-installer-top-container a:contains('Manage')" ).trigger( "click" );
            }
        } );
    } );
} )();
// </nowiki>