User:Infinityboy7/AjaxBlock.js

From Test Wiki
Jump to navigation Jump to search

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

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/*
 * AjaxBlock ([[User:Infinityboy7/AjaxBlock]])
 *
 * @author:infinityboy7
(https://testwiki.wiki/wiki/User:Infinityboy7)
 * @scope: Personal use
 * @description: Allows user blocking without leaving the current page.
 * @update 2016-05-04 Doru: Now detects block and unblock links that were added after window onload.
 * @update 2016-05-22 Doru: Now supports unblocking IDs + a few minor changes.
 * @update 2016-06-03 Doru: Now supports the incredibly rare and useless Special:Block?wpTarget=<user> links,
 *                          This script will not run if it was initialized already, or if you right click the link,
 *                          You now have retry, unblock, and re-block links on banner notifications,
 *                          Fixed bug while blocking IDs.
 * @update 2017-09-01 Doru: Adding i18n, option group support, and some code clean-up.
 * @update 2020-10-20 Thundercraft5: Adding UCP support, some new features, as well as general improvements.
 *
 * @update 2020-10-25 Doru: Rewrite, remove badly thought out features, remove QDModal in favor of Modal-js, fix bugs, implement new loader
 */

(function() {
    if (window.AjaxBlock && window.AjaxBlock.loaded) return;

    var ui;

    window.AjaxBlock = $.extend({
        loaded: true,

        // Config options and defaults
        expiryTimes: null,
        blockReasons: null,
        unblockReasons: null,
        check: {
            talk: false,
            autoBlock: false,
            override: true,
            noCreate: true
        },

        // Globals
        wg: mw.config.get([
            'wgUserGroups',
            'wgNamespaceIds',
            'wgCanonicalSpecialPageName',
            'wgArticlePath'
        ]),
        $currentModal: null,

        // Resource management
        loading: [
            'css',
            'api',
            'i18n',
            'i18n-js',
            'scm',
            'banners',
            'dorui',
            'expiry-times',
            'block-reasons',
            'aliases'
        ],
        onload: function(key, arg) {
            switch (key) {
                case 'i18n-js':
                    arg.loadMessages('AjaxBlock').then(this.onload.bind(this, 'i18n'));
                    break;
                case 'i18n':
                    this.i18n = arg;
                    break;
                case 'api':
                    this.api = new mw.Api();
                    this.ensureBlockSelects();
                    this.loadSpecialPageAliases();
                    break;
                case 'dorui':
                    ui = arg;
                    break;
                case 'banners':
                    this.BannerNotification = arg;
                    break;
            }

            var index = this.loading.indexOf(key);
            if (index === -1) throw new Error('Unregistered dependency loaded: ' + key);

            this.loading.splice(index, 1);

            if (this.loading.length !== 0) return;

            this.init();
        },
        canRun: function() {
            return this.hasRights([
                'sysop',
                'bureaucrate',
                'Stewards'
            ]);
        },
        hasRights: function(rights) {
            var len = rights.length;
            while (len--) {
                if (this.wg.wgUserGroups.indexOf(rights[len]) !== -1) return true;
            }

            return false;
        },
        preload: function() {
            // Styles
            importArticle({
                type: 'style',
                article: 'User:Infinityboy7:AjaxBlock.css'
            }).then(this.onload.bind(this, 'css'));

            // Libraries
            importArticles({
                type: 'script',
                articles: [
                    'User:Infinityboy7/I18n-js/code.js',
                    'User:Infinityboy7/ShowCustomModal.js',
                    'User:Infinityboy7/BannerNotification.js',
                    'User:Infinityboy7/Dorui.js',
                ]
            });

            mw.hook('dev.showCustomModal').add(this.onload.bind(this, 'scm'));
            mw.hook('dev.banners').add(this.onload.bind(this, 'banners'));
            mw.hook('dev.i18n').add(this.onload.bind(this, 'i18n-js'));
            mw.hook('doru.ui').add(this.onload.bind(this, 'dorui'));

            // Loader modules
            mw.loader.using('mediawiki.api').then(this.onload.bind(this, 'api'));
        },
        ensureBlockSelects: function() {
            var pagesToLoad = [];

            if (this.expiryTimes === null) {
                pagesToLoad.push('Ipboptions');
            } else {
                this.onload('expiry-times');
            }

            if (this.blockReasons === null) {
                pagesToLoad.push('Ipbreason-dropdown');
            } else {
                this.onload('block-reasons');
            }

            if (pagesToLoad.length === 0) return;

            this.api.get({
                action: 'query',
                meta: 'allmessages',
                ammessages: pagesToLoad.join('|')
            }).then(function(data) {
                data.query.allmessages.forEach(function(message) {
                    var wikitext = message['*'];

                    switch (message.name) {
                        case 'Ipboptions':
                            this.expiryTimes = this.parseExpiryTimes(wikitext);
                            this.onload('expiry-times');
                            break;
                        case 'Ipbreason-dropdown':
                            this.blockReasons = this.parseBlockReasons(wikitext);
                            this.onload('block-reasons');
                            break;
                    }
                }.bind(this));
            }.bind(this));
        },
        parseExpiryTimes: function(wikitext) {
            var expiryTimes = {};

            wikitext.split(',').forEach(function(item) {
                var pair = item.split(':');
                var label = pair[0];
                var value = pair[1];

                expiryTimes[value] = label;
            });

            return expiryTimes;
        },
        parseBlockReasons: function(wikitext) {
            var lines = wikitext.split('\n');
            var reasons = {};
            var currentGroup = null;

            for (var i = 0; i < lines.length; i++) {
                var line = lines[i];

                if (line.trim() === '') continue;

                var type = '';

                if (line.charAt(0) === '*') {
                    if (line.charAt(1) === '*') {
                        type = 'sub';
                    } else {
                        type = 'group';
                    }
                } else {
                    type = 'reason';
                }

                switch (type) {
                    case 'reason':
                        if (currentGroup !== null && !this.isEmptyObject(currentGroup.reasons)) {
                            reasons[currentGroup.label] = currentGroup.reasons;
                        }

                        currentGroup = null;

                        var value = line.trim();
                        reasons[value] = value;
                        break;
                    case 'group':
                        if (currentGroup !== null && !this.isEmptyObject(currentGroup.reasons)) {
                            reasons[currentGroup.label] = currentGroup.reasons;
                        }

                        var label = line.slice(1).trim();

                        currentGroup = {
                            label: label,
                            reasons: {}
                        };
                        break;
                    case 'sub':
                        var value = line.slice(2).trim();

                        if (currentGroup !== null) {
                            currentGroup.reasons[value] = value;
                        } else {
                            reasons[value] = value;
                        }
                        break;
                }
            }

            if (currentGroup !== null && !this.isEmptyObject(currentGroup.reasons)) {
                reasons[currentGroup.label] = currentGroup.reasons;
            }

            return reasons;
        },
        isEmptyObject: function(obj) {
            for (var _ in obj) {
                return false;
            }

            return true;
        },
        loadSpecialPageAliases: function() {
            this.api.get({
                action: 'query',
                meta: 'siteinfo',
                siprop: 'specialpagealiases'
            }).then(function(data) {
                this.blockAliases = this.getPageAliases(data.query.specialpagealiases, 'Block');
                this.unblockAliases = this.getPageAliases(data.query.specialpagealiases, 'Unblock');

                this.onload('aliases');
            }.bind(this));
        },
        getPageAliases: function(aliases, name) {
            return aliases.find(function(page) {
                return page.realname === name;
            }).aliases.map(function(alias) {
                return alias.toLowerCase();
            });
        },
        onDocumentClick: function(e) {
            // Left click
            if (e.which !== 1) return;

            // Bail out of meta keys
            if (e.ctrlKey || e.shiftKey) return;

            // Only if a link was clicked
            var anchor = e.target.closest('a[href]');
            if (anchor === null) return;

            var uri = new mw.Uri(anchor.href);
            var target = this.getPageName(uri.path);
            if (target === '') return;

            var specialNamespace = this.specialNamespaceAliases.find(function(alias) {
                return target.slice(0, alias.length + 1).toLowerCase() == alias + ':';
            });
            if (specialNamespace === undefined) return;

            var title = target.slice(specialNamespace.length + 1);
            var lowerTitle = title.toLowerCase();
            if (this.wg.wgCanonicalSpecialPageName && this.isRootPage(lowerTitle, this.wg.wgCanonicalSpecialPageName.toLowerCase())) return;

            var isBlocking = this.blockAliases.some(this.isRootPage.bind(this, lowerTitle));
            var isUnblocking = this.unblockAliases.some(this.isRootPage.bind(this, lowerTitle));

            if (!isBlocking && !isUnblocking) return;

            var userTarget = this.getBlockTarget(uri, title);
            // It was a regular Special:Block link, ignore it
            if (!userTarget) return;

            e.preventDefault();

            if (isBlocking) {
                this.showBlockModal(userTarget);
            } else {
                this.showUnblockModal(userTarget);
            }
        },
        getPageName: function(path) {
            var root = this.wg.wgArticlePath.replace('$1', '');
            if (path.slice(0, root.length) !== root) return '';

            return path.slice(root.length);
        },
        isRootPage: function(title, alias) {
            // Special:Block
            if (title === alias) return true;

            // Special:Block/Something
            if (title.slice(0, alias.length + 1) === alias + '/') return true;

            return false;
        },
        getBlockTarget: function(uri, title) {
            // wpTarget query parameter takes priority
            if (uri.query.wpTarget) {
                return uri.query.wpTarget;
            }

            var parts = title.split('/');

            // Block link with no target
            if (parts.length === 1) {
                return undefined;
            }

            return decodeURIComponent(parts[1]).replace(/_/g, ' ');
        },
        buildSelectChildren: function(data) {
            var children = [
                ui.option({
                    value: 'other',
                    text: this.i18n.msg('other').plain()
                })
            ];

            for (var key in data) {
                var value = data[key];

                if (typeof value === 'string') {
                    children.push(
                        ui.option({
                            value: key,
                            text: value
                        })
                    );
                } else {
                    var subChildren = [];

                    for (var subKey in value) {
                        var subValue = value[subKey];

                        subChildren.push(
                            ui.option({
                                value: subKey,
                                text: subValue
                            })
                        );
                    }

                    var group = ui.optgroup({
                        label: key,
                        children: subChildren
                    });

                    children.push(group);
                }
            }

            return children;
        },
        buildCheckbox: function(data) {
            var attrs = {
                type: 'checkbox',
                id: data.id
            };

            if (data.checked) {
                attrs.checked = 'checked';
            }

            return ui.div({
                children: [
                    ui.input({
                        attrs: attrs
                    }),
                    ui.label({
                        'for': data.id,
                        text: data.label
                    })
                ]
            });
        },
        showBlockModal: function(username) {
            var $modal = this.showModal(this.i18n.msg('block-title', username).escape(), {
                id: 'BlockModal',
                content: ui.div({
                    id: 'AjaxBlockModalContent',
                    children: [
                        ui.div({
                            id: 'AjaxBlockExpiryWrapper',
                            children: [
                                this.i18n.msg('expiry').plain(),
                                ui.select({
                                    id: 'AjaxBlockExpirySelect',
                                    children: this.buildSelectChildren(this.expiryTimes)
                                }),
                                ui.input({
                                    id: 'AjaxBlockExpiryInput'
                                })
                            ]
                        }),
                        ui.div({
                            id: 'AjaxBlockReasonWrapper',
                            children: [
                                this.i18n.msg('reason').plain(),
                                ui.select({
                                    id: 'AjaxBlockReasonSelect',
                                    children: this.buildSelectChildren(this.blockReasons)
                                }),
                                ui.input({
                                    id: 'AjaxBlockReasonInput'
                                })
                            ]
                        }),
                        ui.div({
                            id: 'AjaxBlockCheckers',
                            children: [
                                this.buildCheckbox({
                                    id: 'AjaxBlockDisableWall',
                                    checked: this.check.talk,
                                    label: this.i18n.msg('label-disable-wall').plain()
                                }),
                                this.buildCheckbox({
                                    id: 'AjaxBlockAutoBlock',
                                    checked: this.check.autoblock || this.check.autoBlock,
                                    label: this.i18n.msg('label-auto-block').plain()
                                }),
                                this.buildCheckbox({
                                    id: 'AjaxBlockDisableAccount',
                                    checked: this.check.nocreate || this.check.noCreate,
                                    label: this.i18n.msg('label-no-create').plain()
                                }),
                                this.buildCheckbox({
                                    id: 'AjaxBlockOverrideBlock',
                                    checked: this.check.override,
                                    label: this.i18n.msg('label-override').plain()
                                })
                            ]
                        })
                    ]
                }),
                buttons: [
                    {
                        defaultButton: true,
                        message: this.i18n.msg('block-button').escape(),
                        handler: function() {
                            var state = this.getModalState(username);

                            if (state.expiry === '') {
                                this.notify({
                                    type: 'warn',
                                    text: this.i18n.msg('error-no-expiry').plain()
                                });
                                return;
                            }

                            this.block(state).then(function(data) {
                                if (data.error) {
                                    this.notify({
                                        type: 'error',
                                        text: this.i18n.msg('error-block', username, data.error.info).plain()
                                    });
                                } else {
                                    this.notify({
                                        type: 'confirm',
                                        text: this.i18n.msg('success-block', username).plain()
                                    });

                                    setTimeout(function() {
                                        dev.showCustomModal.closeModal($modal);
                                    }, 3000);
                                }
                            }.bind(this)).fail(function(code, data) {
                                var error = data.error && data.error.info || code;

                                this.notify({
                                    type: 'error',
                                    text: this.i18n.msg('error-block', username, error).plain()
                                });
                            }.bind(this));
                        }.bind(this)
                    },
                    {
                        message: this.i18n.msg('cancel-button').escape(),
                        handler: function() {
                            dev.showCustomModal.closeModal($modal);
                        }
                    }
                ]
            });
        },
        showUnblockModal: function(username) {
            var $modal = this.showModal(this.i18n.msg('unblock-title', username).escape(), {
                id: 'UnblockModal',
                content: ui.div({
                    id: 'AjaxUnblockModalContent',
                    children: [
                        ui.div({
                            id: 'AjaxBlockReasonWrapper',
                            children: [
                                this.i18n.msg('reason').plain(),
                                this.unblockReasons && ui.select({
                                    id: 'AjaxUnblockReasonSelect',
                                    children: this.buildSelectChildren(this.unblockReasons)
                                }),
                                ui.input({
                                    id: 'AjaxUnblockReasonInput'
                                })
                            ]
                        })
                    ]
                }),
                buttons: [
                    {
                        defaultButton: true,
                        message: this.i18n.msg('unblock-button').escape(),
                        handler: function() {
                            var state = this.getModalState(username);

                            this.unblock(state).then(function(data) {

                                if (data.error) {
                                    this.notify({
                                        type: 'error',
                                        text: this.i18n.msg('error-unblock', username, data.error.info).plain()
                                    });
                                } else {
                                    this.notify({
                                        type: 'confirm',
                                        text: this.i18n.msg('success-unblock', username).plain()
                                    });

                                    setTimeout(function() {
                                        dev.showCustomModal.closeModal($modal);
                                    }, 3000);
                                }
                            }.bind(this)).fail(function(code, data) {
                                var error = data.error && data.error.info || code;

                                this.notify({
                                    type: 'error',
                                    text: this.i18n.msg('error-unblock', username, error).plain()
                                });
                            }.bind(this));
                        }.bind(this)
                    },
                    {
                        message: this.i18n.msg('cancel-button').escape(),
                        handler: function() {
                            dev.showCustomModal.closeModal($modal);
                        }
                    }
                ]
            });
        },
        showModal: function(title, options) {
            var $modal = dev.showCustomModal(title, options);

            this.$currentModal = $modal;

            return $modal;
        },
        getModalState: function(username) {
            var $modal = this.$currentModal;
            var $reason = $modal.find('#AjaxBlockReasonWrapper');
            var $expiry = $modal.find('#AjaxBlockExpiryWrapper');
            var $checkboxes = $modal.find('input[type="checkbox"]');
            var data = {
                username: username,
                checkboxes: {}
            };

            if ($reason.length) {
                var $select = $reason.find('select');
                var $input = $reason.find('input');

                if ($select.length === 0 || $select.val() === 'other') {
                    data.reason = $input.val();
                } else {
                    if ($input.val().trim() === '') {
                        data.reason = $select.val();
                    } else {
                        data.reason = $select.val() + ': ' + $input.val().trim();
                    }
                }
            }

            if ($expiry.length) {
                var $select = $expiry.find('select');
                var $input = $expiry.find('input');
                data.expiry = $select.val() === 'other'
                    ? $input.val()
                    : $select.val();
            }

            $checkboxes.each(function() {
                data.checkboxes[this.id] = this.checked;
            });

            return data;
        },
        notify: function(data) {
            new this.BannerNotification(data.text, data.type).show();
        },
        block: function(data) {
            var query = {
                action: 'block',
                user: data.username,
                expiry: data.expiry,
                reason: data.reason,
                token: mw.user.tokens.get('csrfToken'),
            };

            if (!data.checkboxes.AjaxBlockDisableWall) {
                query.allowusertalk = true;
            }

            if (data.checkboxes.AjaxBlockAutoBlock) {
                query.autoblock = true;
            }

            if (data.checkboxes.AjaxBlockOverrideBlock) {
                query.reblock = true;
            }

            if (data.checkboxes.AjaxBlockDisableAccount) {
                query.nocreate = true;
            }

            if (!data.checkboxes.AjaxBlockAutoBlock) {
                query.anononly = true;
            }

            return this.api.post(query);
        },
        unblock: function(data) {
            var query = {
                action: 'unblock',
                reason: data.reason,
                token: mw.user.tokens.get('csrfToken')
            };

            if (data.username.charAt(0) == '#') {
                query.id = data.username.slice(1);
            } else {
                query.user = data.username;
            }

            return this.api.post(query);
        },

        // Entrypoint
        init: function() {
            this.specialNamespaceAliases = Object.keys(this.wg.wgNamespaceIds).filter(function(key) {
                return this.wg.wgNamespaceIds[key] === -1;
            }.bind(this));

            document.addEventListener('click', this.onDocumentClick.bind(this));
        }
    }, window.AjaxBlock);

    if (!AjaxBlock.canRun()) return;

    AjaxBlock.preload();
})();