Greasy Fork

Ultra Popup Blocker

Configurable popup blocker that blocks all popup windows by default, with whitelist and blacklist support.

当前为 2025-06-26 提交的版本,查看 最新版本

// ==UserScript==
// @name         Ultra Popup Blocker
// @description  Configurable popup blocker that blocks all popup windows by default, with whitelist and blacklist support.
// @namespace    eskander.github.io
// @author       Eskander & 1Td
// @version      5.0
// @include      *
// @license      MIT
// @homepage     https://github.com/Eskander/ultra-popup-blocker
// @supportURL   https://github.com/Eskander/ultra-popup-blocker/issues/new
// @compatible   firefox Tampermonkey / Violentmonkey
// @compatible   chrome Tampermonkey / Violentmonkey
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM.registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    /* Constants and Globals */
    const CONSTANTS = {
        TIMEOUT_SECONDS: 15,
        TRUNCATE_LENGTH: 50,
        MODAL_WIDTH: '450px'
    }

    const STYLES = {
        modal: `position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; background-color: #ffffff !important; color: #000000 !important; width: ${CONSTANTS.MODAL_WIDTH} !important; border: 1px solid #000000 !important; z-index: 2147483647 !important; box-shadow: 0 2px 10px rgba(0,0,0,0.5) !important; margin: 0 !important; padding: 0 !important; font-family: Arial !important; font-size: 14px !important; line-height: 1.5 !important; box-sizing: border-box !important;`,
        modalHeader: `background-color: #000000 !important; padding: 20px 30px !important; color: #ffffff !important; text-align: center !important; margin: 0 !important; font-size: inherit !important; line-height: inherit !important;`,
        modalFooter: `background-color: #000000 !important; padding: 5px 30px !important; color: #ffffff !important; text-align: center !important; margin: 0 !important;`,
        button: `margin-right: 10px !important; padding: 5px !important; cursor: pointer !important; font-family: inherit !important; font-size: inherit !important; line-height: inherit !important; border: 1px solid #000000 !important; background: #ffffff !important; color: #000000 !important; border-radius: 3px !important;`,
        notificationBar: `position: fixed !important; bottom: 0 !important; left: 0 !important; z-index: 2147483646 !important; width: 100% !important; padding: 5px !important; font-family: Arial !important; font-size: 14px !important; line-height: 1.5 !important; background-color: #000000 !important; color: #ffffff !important; display: none !important; margin: 0 !important; box-sizing: border-box !important;`,
        listItem: `padding: 12px 8px 12px 40px !important; font-size: 16px !important; background-color: #ffffff !important; color: #000000 !important; border-bottom: 1px solid #dddddd !important; position: relative !important; transition: 0.2s !important; margin: 0 !important;`,
        removeButton: `cursor: pointer !important; position: absolute !important; right: 0 !important; top: 0 !important; padding: 12px 16px !important; background: transparent !important; border: none !important; color: #000000 !important; font-size: 20px !important;`
    }

    const global = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
    global.upbCounter = 0

    const realWindowOpen = global.open

    const FakeWindow = {
        blur: () => false,
        focus: () => false
    }

    /* Domain Management */
    class DomainManager {
        static async getCurrentTopDomain () {
            const hostname = document.location.hostname;
            const matches = hostname.match(/[^.]+\.[^.]+$/);
            return matches ? matches[0] : hostname;
        }
        static async getPermissionStatus (domain) {
            return await GM.getValue(domain, 'ask');
        }
        static async setPermission (domain, status) {
            await GM.setValue(domain, status);
        }
        static async removePermission (domain) {
            await GM.deleteValue(domain);
        }
        static async getAllPermissions () {
            const keys = await GM.listValues();
            const permissions = [];
            for (const key of keys) {
                const status = await GM.getValue(key);
                if (status === 'allowed' || status === 'denied') {
                    permissions.push({ domain: key, status: status });
                }
            }
            return permissions;
        }
    }

    /* UI Components */
    class UIComponents {
        static createButton (text, id, clickHandler, color) {
            const button = document.createElement('button')
            button.id = `upb-${id}`
            button.innerHTML = text
            button.style.cssText = `${STYLES.button} color: ${color} !important;`
            button.addEventListener('click', clickHandler)
            return button
        }
        static createNotificationBar () {
            const bar = document.createElement('div')
            bar.id = 'upb-notification-bar'
            bar.style.cssText = STYLES.notificationBar
            document.body.appendChild(bar);
            return bar
        }
        static createModalElement () {
            const modal = document.createElement('div')
            modal.id = 'upb-trusted-domains-modal'
            modal.style.cssText = STYLES.modal
            document.body.appendChild(modal);
            return modal
        }
        static updateDenyButtonText (button, timeLeft) {
            if (button) {
                button.innerHTML = `🔴 Deny (${timeLeft})`
            }
        }
    }

    /* Notification Bar */
    class NotificationBar {
        constructor () {
            this.element = null
            this.timeLeft = CONSTANTS.TIMEOUT_SECONDS
            this.denyTimeoutId = null
            this.denyButton = null
        }
        createElement () {
            if (!this.element) {
                this.element = UIComponents.createNotificationBar()
            }
            return this.element
        }
        show (url) {
            if (!this.element) {
                this.createElement()
            }
            this.element.style.display = 'block'
            this.setMessage(url)
            this.addButtons(url)
            this.startDenyTimeout()
        }
        hide () {
            if (this.element) {
                this.element.style.display = 'none'
                if (this.element.parentNode) {
                    this.element.parentNode.removeChild(this.element)
                }
                this.element = null
            }
            global.upbCounter = 0
            this.clearDenyTimeout()
        }
        clearDenyTimeout () {
            if (this.denyTimeoutId) {
                clearInterval(this.denyTimeoutId)
                this.denyTimeoutId = null
            }
        }
        setMessage (url) {
            const truncatedUrl = url.length > CONSTANTS.TRUNCATE_LENGTH
            ? `${url.substring(0, CONSTANTS.TRUNCATE_LENGTH)}..`
            : url
            this.element.innerHTML = `
              Ultra Popup Blocker: This site is attempting to open <b>${global.upbCounter}</b> popup(s).
              <a href="${url}" style="color:yellow;" target="_blank">${truncatedUrl}</a>
            `
        }
        async addButtons (url) {
            const currentDomain = await DomainManager.getCurrentTopDomain()
            this.element.appendChild(
                UIComponents.createButton('🟢 Allow Once', 'allow', () => {
                    realWindowOpen(url, '_blank')
                    this.hide()
                }, 'green')
            )
            this.element.appendChild(
                UIComponents.createButton('🔵 Always Allow', 'trust', async () => {
                    await DomainManager.setPermission(currentDomain, 'allowed')
                    realWindowOpen(url, '_blank')
                    this.hide()
                    await PopupBlocker.initialize()
                }, 'blue')
            )
            this.element.appendChild(
                UIComponents.createButton('🚫 Always Deny', 'deny-always', async () => {
                    await DomainManager.setPermission(currentDomain, 'denied');
                    this.hide();
                    await PopupBlocker.initialize();
                }, '#333')
            );
            this.denyButton = UIComponents.createButton(`🔴 Deny (${CONSTANTS.TIMEOUT_SECONDS})`, 'deny-once', () => {
                this.hide()
            }, 'red')
            this.element.appendChild(this.denyButton)
            const configButton = UIComponents.createButton('🟠 Config', 'config', () => {
                new TrustedDomainsModal().show()
            }, 'orange')
            configButton.style.float = 'right'
            this.element.appendChild(configButton)
        }
        startDenyTimeout () {
            this.timeLeft = CONSTANTS.TIMEOUT_SECONDS
            this.clearDenyTimeout()
            UIComponents.updateDenyButtonText(this.denyButton, this.timeLeft)
            this.denyTimeoutId = setInterval(() => {
                this.timeLeft--
                UIComponents.updateDenyButtonText(this.denyButton, this.timeLeft)
                if (this.timeLeft <= 0) {
                    this.clearDenyTimeout()
                    this.hide()
                }
            }, 1000)
        }
    }

    /* MODIFIED Trusted Domains Modal */
    class TrustedDomainsModal {
        constructor () {
            this.element = document.getElementById('upb-trusted-domains-modal') || this.createElement()
        }
        createElement () {
            const modal = UIComponents.createModalElement()
            const header = document.createElement('div')
            header.style.cssText = STYLES.modalHeader
            header.innerHTML = `<h2 style="color:white !important; margin:0; padding:0;">Ultra Popup Blocker</h2>`
            modal.appendChild(header)
            const listsContainer = document.createElement('div');
            listsContainer.id = 'upb-lists-container';
            listsContainer.style.cssText = 'max-height: 400px; overflow-y: auto;';
            const allowedHeader = document.createElement('h4');
            allowedHeader.innerText = '✅ Allowed Websites (Whitelist)';
            allowedHeader.style.cssText = 'padding: 10px 20px 5px; margin: 0; background: #f0f0f0; color: black;';
            const allowedList = document.createElement('ul');
            allowedList.id = 'upb-allowed-list';
            allowedList.style.cssText = 'margin:0;padding:0;list-style-type:none;';
            const blockedHeader = document.createElement('h4');
            blockedHeader.innerText = '🚫 Denied Websites (Blacklist)';
            blockedHeader.style.cssText = 'padding: 10px 20px 5px; margin: 0; background: #f0f0f0; color: black; border-top: 1px solid #ccc;';
            const deniedList = document.createElement('ul');
            deniedList.id = 'upb-denied-list';
            deniedList.style.cssText = 'margin:0;padding:0;list-style-type:none;';
            listsContainer.appendChild(allowedHeader);
            listsContainer.appendChild(allowedList);
            listsContainer.appendChild(blockedHeader);
            listsContainer.appendChild(deniedList);
            modal.appendChild(listsContainer);
            const footer = document.createElement('div')
            footer.style.cssText = STYLES.modalFooter
            const closeButton = document.createElement('button')
            closeButton.innerText = 'Close'
            closeButton.style.cssText = `background-color:#4CAF50;color:#ffffff;border:none;padding:10px 20px;cursor:pointer;border-radius:3px;`
            closeButton.onclick = () => this.hide()
            footer.appendChild(closeButton)
            modal.appendChild(footer)
            return modal
        }
        show () {
            this.refreshDomainsList()
            this.element.style.display = 'block'
        }
        hide () {
            this.element.style.display = 'none'
        }
        
        async refreshDomainsList () {
            const allowedList = document.getElementById('upb-allowed-list');
            const deniedList = document.getElementById('upb-denied-list');
            allowedList.innerHTML = '';
            deniedList.innerHTML = '';

            const permissions = await DomainManager.getAllPermissions();

            let allowedCount = 0;
            let deniedCount = 0;

            for (const perm of permissions) {
                if (perm.status === 'allowed') {
                    await this.addDomainListItem(allowedList, perm.domain);
                    allowedCount++;
                } else if (perm.status === 'denied') {
                    await this.addDomainListItem(deniedList, perm.domain);
                    deniedCount++;
                }
            }

            if (allowedCount === 0) {
                const message = document.createElement('p');
                message.style.cssText = 'padding: 20px; color: #555; text-align: center;';
                message.innerText = 'No allowed websites.';
                allowedList.appendChild(message);
            }

            if (deniedCount === 0) {
                const message = document.createElement('p');
                message.style.cssText = 'padding: 20px; color: #555; text-align: center;';
                message.innerText = 'No denied websites.';
                deniedList.appendChild(message);
            }
        }

        async addDomainListItem (list, domain) {
            const item = document.createElement('li')
            item.style.cssText = STYLES.listItem
            item.innerText = domain
            item.addEventListener('mouseover', () => { item.style.backgroundColor = '#f1f1f1' })
            item.addEventListener('mouseout', () => { item.style.backgroundColor = 'white' })
            const removeButton = document.createElement('span')
            removeButton.style.cssText = STYLES.removeButton
            removeButton.innerHTML = '&times;'
            removeButton.addEventListener('mouseover', () => { removeButton.style.backgroundColor = '#f44336'; removeButton.style.color = 'white'; })
            removeButton.addEventListener('mouseout', () => { removeButton.style.backgroundColor = 'transparent'; removeButton.style.color = 'black'; })
            removeButton.addEventListener('click', async () => {
                await DomainManager.removePermission(domain);
                await this.refreshDomainsList();
                await PopupBlocker.initialize();
            })
            item.appendChild(removeButton)
            list.appendChild(item)
        }
    }

    class PopupBlocker {
        static clickHandler = null;

        static async initialize () {
            global.open = realWindowOpen;
            if (this.clickHandler) {
                document.removeEventListener('click', this.clickHandler, true);
                this.clickHandler = null;
            }

            const currentDomain = await DomainManager.getCurrentTopDomain();
            const permission = await DomainManager.getPermissionStatus(currentDomain);

            if (permission === 'allowed') {
                console.log(`[UPB] Whitelisted domain: ${currentDomain}. Popups are allowed.`);
                return;
            }

            const notificationBar = new NotificationBar();

            const windowOpenHandler = (url) => {
                global.upbCounter++;
                console.log(`[UPB] Intercepted window.open popup: ${url}`);
                notificationBar.show(url);
                return FakeWindow;
            };
            const silentWindowOpenHandler = (url) => {
                console.log(`[UPB] Silently blocked window.open on denied domain: ${url}`);
                return FakeWindow;
            };
            
            const clickHandler = (event) => {
                let target = event.target;
                while (target && target.tagName !== 'A') {
                    target = target.parentNode;
                }

                if (target && target.target === '_blank' && target.href) {
                    event.preventDefault();
                    event.stopPropagation();
                    global.upbCounter++;
                    console.log(`[UPB] Intercepted click-based popup: ${target.href}`);
                    notificationBar.show(target.href);
                }
            };
            const silentClickHandler = (event) => {
                 let target = event.target;
                while (target && target.tagName !== 'A') {
                    target = target.parentNode;
                }
                if (target && target.target === '_blank' && target.href) {
                    event.preventDefault();
                    event.stopPropagation();
                    console.log(`[UPB] Silently blocked click-based popup on denied domain: ${target.href}`);
                }
            };

            if (permission === 'denied') {
                console.log(`[UPB] Denied domain: ${currentDomain}. All popups will be silently blocked.`);
                global.open = silentWindowOpenHandler;
                this.clickHandler = silentClickHandler;
                document.addEventListener('click', this.clickHandler, true);
                return;
            }

            console.log(`[UPB] Active on: ${currentDomain}. New popups will trigger a notification.`);
            global.open = windowOpenHandler;
            this.clickHandler = clickHandler;
            document.addEventListener('click', this.clickHandler, true);
        }
    }

    /* Initialize */
    (async function() {
        if (document.body) {
            await PopupBlocker.initialize();
            GM.registerMenuCommand('Ultra Popup Blocker: Manage Permissions', () => {
                new TrustedDomainsModal().show();
            });
        } else {
            window.addEventListener('load', async () => {
                await PopupBlocker.initialize();
                GM.registerMenuCommand('Ultra Popup Blocker: Manage Permissions', () => {
                    new TrustedDomainsModal().show();
                });
            });
        }
    })();

})();