ЧТМ:Расширения/BatchTools/1.45/0.3: различия между версиями

Материал из ЧТМ
Перейти к навигации Перейти к поиску
м Diman Russkov переименовал страницу ЧТМ:Расширения/BatchTools/0.3 в ЧТМ:Расширения/BatchTools/1.45/0.3
мНет описания правки
 
(не показано 9 промежуточных версий 2 участников)
Строка 1: Строка 1:
<span class="plainlinks">'''{{Size|130|[{{SERVER}}/index.php/Файл:BatchTools-REL1 45-0.3.zip СКАЧАТЬ ZIP]}}'''</span>


== ROOT ==
<syntaxhighlight lang="markdown">
## [0.3.0] — 2026-05-24
 
### Изменено
- **Архитектурный рефакторинг**: Основной класс служебной страницы `SpecialBatchTools` разделён на изолированные компоненты. Логика работы с кодом перенесена во вложенное пространство имён `Handlers`.
- Создан абстрактный класс `BatchToolHandler`, определяющий структуру для всех будущих пакетных инструментов и предоставляющий удобные методы-хелперы для работы с контекстом MediaWiki.
- Логика массового удаления вынесена в самостоятельный класс `MassDeleteHandler`.
- Логика массового восстановления вынесена в самостоятельный класс `MassUndeleteHandler`.
- Код `SpecialBatchTools` существенно сокращён и переведён на декларативный роутинг вкладок. Это упростит добавление новых пакетных инструментов в будущем.
</syntaxhighlight>
 
<pre>
BatchTools/
├── extension.json
├── i18n/
│  ├── BatchTools.alias.php
│  ├── en.json
│  └── ru.json
└── includes/
    ├── SpecialBatchTools.php
    └── Handlers/
        ├── BatchToolHandler.php
        ├── MassDeleteHandler.php
        └── MassUndeleteHandler.php
</pre>


=== extension.json ===
=== extension.json ===
<syntaxhighlight lang="json">
<syntaxhighlight lang="json" line>
{
"name": "BatchTools",
"version": "0.3",
"author": "Diman Russkov",
"descriptionmsg": "batchtools-desc",
"type": "specialpage",
"manifest_version": 2,
"requires": {
"MediaWiki": ">= 1.45.0"
},
"AvailableRights": [
"deletebatch",
"undeletebatch"
],
"SpecialPages": {
"BatchTools": "MediaWiki\\Extension\\BatchTools\\SpecialBatchTools"
},
"AutoloadNamespaces": {
"MediaWiki\\Extension\\BatchTools\\": "includes/"
},
"MessagesDirs": {
"BatchTools": [
"i18n"
]
},
"ExtensionMessagesFiles": {
"BatchToolsAlias": "i18n/BatchTools.alias.php"
}
}
</syntaxhighlight>


</syntaxhighlight>


== i18n ==
== i18n ==
=== BatchTools.alias.php ===
<syntaxhighlight lang="php" line>
<?php
/**
* Aliases for special pages of the BatchTools extension
*/
$specialPageAliases = [];
/** English (English) */
$specialPageAliases['en'] = [
'BatchTools' => [ 'BatchTools' ],
];
/** Russian (Русский) */
$specialPageAliases['ru'] = [
'BatchTools' => [ 'BatchTools', 'Пакетные_инструменты' ],
];
</syntaxhighlight>


=== en.json ===
=== en.json ===
<syntaxhighlight lang="json">
<syntaxhighlight lang="json" line>
 
{
"@metadata": {
"authors": []
},
"batchtools-desc": "Provides tools for batch operations (mass delete, mass undelete, etc.)",
"batchtools": "Batch tools",
"right-deletebatch": "Mass delete pages",
"action-deletebatch": "mass delete pages",
"right-undeletebatch": "Mass undelete pages",
"action-undeletebatch": "mass undelete pages",
"batchtools-tab-delete": "Mass Delete",
"batchtools-tab-undelete": "Mass Restore",
"batchtools-error-nopermissions": "You do not have permission to access any of the batch tools.",
"batchtools-error-cannot-read": "You do not have permission to read this page.",
"batchtools-error-cannot-delete": "You do not have permission to delete this page.",
"batchtools-error-cannot-restore": "You do not have permission to restore this page.",
"batchtools-massdelete-pages": "List of pages (one per line):",
"batchtools-reason": "Reason:",
"batchtools-massdelete-submit": "Delete pages",
"batchtools-massdelete-success": "Successfully deleted $1 pages.",
"batchtools-massdelete-errors": "Failed to delete the following pages:",
"batchtools-massdelete-empty": "The page list is empty or contains invalid names.",
"batchtools-massrestore-pages": "List of pages to restore (one per line):",
"batchtools-massrestore-submit": "Restore pages",
"batchtools-massrestore-success": "Successfully restored $1 pages.",
"batchtools-massrestore-errors": "Failed to restore the following pages:",
"batchtools-massrestore-empty": "The page list is empty or contains invalid names.",
"batchtools-massrestore-norevisions": "The page was not deleted or has already been restored.",
"batchtools-error-limit": "Limit exceeded! Please process no more than {{PLURAL:$1|page|pages|pages}} at a time."
}
</syntaxhighlight>
</syntaxhighlight>


=== ru.json ===
=== ru.json ===
<syntaxhighlight lang="json">
<syntaxhighlight lang="json" line>
{
"@metadata": {
"authors": []
},
"batchtools-desc": "Предоставляет инструменты для массовых операций",
"batchtools": "Массовые инструменты",
"right-deletebatch": "Массовое удаление страниц",
"action-deletebatch": "массово удалять страницы",
"right-undeletebatch": "Массовое восстановление страниц",
"action-undeletebatch": "массово восстанавливать страницы",
"batchtools-tab-delete": "Массовое удаление",
"batchtools-tab-undelete": "Массовое восстановление",
"batchtools-error-nopermissions": "У вас нет прав для доступа к массовым инструментам.",
"batchtools-error-cannot-read": "У вас нет прав для чтения этой страницы.",
"batchtools-error-cannot-delete": "У вас нет прав для удаления этой страницы.",
"batchtools-error-cannot-restore": "У вас нет прав для восстановления этой страницы.",
"batchtools-massdelete-pages": "Список страниц (по одной на строке):",
"batchtools-reason": "Причина:",
"batchtools-massdelete-submit": "Удалить страницы",
"batchtools-massdelete-success": "Успешно удалено страниц: $1.",
"batchtools-massdelete-errors": "Не удалось удалить следующие страницы:",
"batchtools-massdelete-empty": "Список страниц пуст или содержит недопустимые имена.",
"batchtools-massrestore-pages": "Список страниц для восстановления (по одной на строке):",
"batchtools-massrestore-submit": "Восстановить страницы",
"batchtools-massrestore-success": "Успешно восстановлено страниц: $1.",
"batchtools-massrestore-errors": "Не удалось восстановить следующие страницы:",
"batchtools-massrestore-empty": "Список страниц пуст или содержит недопустимые имена.",
"batchtools-massrestore-norevisions": "Страница не была удалена или уже восстановлена.",
"batchtools-error-limit": "Превышен лимит! Пожалуйста, обрабатывайте не более $1 {{PLURAL:$1|страницы|страниц|страниц}} за один раз."
}
</syntaxhighlight>


</syntaxhighlight>


== includes ==
== includes ==


=== SpecialBatchTools.php ===
=== SpecialBatchTools.php ===
<syntaxhighlight lang="php">
<syntaxhighlight lang="php" line>
<?php
 
namespace MediaWiki\Extension\BatchTools;
 
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Html\Html;
use MediaWiki\Exception\PermissionsError;
use MediaWiki\Extension\BatchTools\Handlers\MassDeleteHandler;
use MediaWiki\Extension\BatchTools\Handlers\MassUndeleteHandler;
 
class SpecialBatchTools extends SpecialPage {
 
    public function __construct() {
        parent::__construct( 'BatchTools' );
    }
 
    public function isListed() {
        $authority = $this->getAuthority();
        // Пока что оставляем эти права, потом добавим новые (масс-блок, переименование и т.д.)
        return $authority->isAllowed( 'deletebatch' ) || $authority->isAllowed( 'undeletebatch' );
    }
 
    public function execute( $par ) {
        $authority = $this->getAuthority();
 
        $this->setHeaders();
        $this->checkReadOnly();
 
        $out = $this->getOutput();
        $out->setPageTitle( $this->msg( 'batchtools' )->text() );
 
        $out->enableOOUI();
        $out->addModules( [
            'oojs-ui-core',
            'oojs-ui-widgets',
            'mediawiki.widgets'
        ] );
 
        // 1. ДИНАМИЧЕСКИЙ СПИСОК ДОСТУПНЫХ ВКЛАДОК (Хэндлеров)
        $handlers = [];
        if ( $authority->isAllowed( 'deletebatch' ) ) {
            $handlers['delete'] = [
                'class' => MassDeleteHandler::class,
                'label' => $this->msg( 'batchtools-tab-delete' )->text()
            ];
        }
        if ( $authority->isAllowed( 'undeletebatch' ) ) {
            $handlers['undelete'] = [
                'class' => MassUndeleteHandler::class,
                'label' => $this->msg( 'batchtools-tab-undelete' )->text()
            ];
        }
 
        if ( empty( $handlers ) ) {
            throw new PermissionsError( 'deletebatch' );
        }
 
        // 2. ОПРЕДЕЛЯЕМ АКТИВНУЮ ВКЛАДКУ
        $request = $this->getRequest();
        $view = $request->getVal( 'view' );
 
        // Если вкладка не указана или у участника нет на неё прав — кидаем на первую доступную
        if ( $view === null || !array_key_exists( $view, $handlers ) ) {
            $view = array_key_first( $handlers );
        }
 
        // 3. ОТРИСОВКА МЕНЮ
        $navHtml = '<div style="margin-bottom: 25px; font-size: 1.1em; border-bottom: 2px solid #a2a9b1; padding-bottom: 10px; display: flex; flex-wrap: wrap; gap: 10px;">';
        $tabLinks = [];
        foreach ( $handlers as $tabKey => $tabData ) {
            if ( $view === $tabKey ) {
                $tabLinks[] = '<b style="color: #202122; background: #eaecf0; padding: 5px 10px; border-radius: 2px;">' . htmlspecialchars( $tabData['label'] ) . '</b>';
            } else {
                $url = $this->getPageTitle()->getLocalURL( [ 'view' => $tabKey ] );
                $tabLinks[] = Html::element( 'a', [ 'href' => $url, 'style' => 'padding: 5px 10px;' ], $tabData['label'] );
            }
        }
        $navHtml .= implode( '<span style="color:#a2a9b1;">|</span>', $tabLinks ) . '</div>';
        $out->addHTML( $navHtml );
 
        // Вывод общих уведомлений об успехе/ошибках
        $this->printSessionResult();
 
        // 4. ЗАПУСК НУЖНОГО ХЭНДЛЕРА
        $handlerClass = $handlers[$view]['class'];
        /** @var Handlers\BatchToolHandler $handlerObj */
        $handlerObj = new $handlerClass( $this );
        $handlerObj->execute();
    }
 
    /**
    * Выводит результаты выполнения массовых операций из сессии
    */
    private function printSessionResult() {
        $session = $this->getRequest()->getSession();
        $result = $session->get( 'batchtools_result' );
 
        if ( $result ) {
            $session->remove( 'batchtools_result' );
            $html = '';
 
            $action = $result['action'] ?? 'delete';
            $msgSuccess = $action === 'delete' ? 'batchtools-massdelete-success' : 'batchtools-massrestore-success';
            $msgErrors = $action === 'delete' ? 'batchtools-massdelete-errors' : 'batchtools-massrestore-errors';
 
            if ( !empty( $result['success_count'] ) ) {
                $html .= Html::successBox( $this->msg( $msgSuccess, $result['success_count'] )->text() );
            }


            if ( !empty( $result['errors'] ) ) {
                $errHtml = '<b>' . $this->msg( $msgErrors )->text() . '</b><ul style="margin-top: 5px; margin-bottom: 0;">';
                foreach ( $result['errors'] as $err ) {
                    $errHtml .= '<li>' . htmlspecialchars( $err ) . '</li>';
                }
                $errHtml .= '</ul>';
                $html .= Html::errorBox( $errHtml );
            }
            if ( $html !== '' ) {
                $this->getOutput()->addHTML( '<div style="margin-bottom: 20px;">' . $html . '</div>' );
            }
        }
    }
}
</syntaxhighlight>
</syntaxhighlight>
=== Handlers ===
==== BatchToolHandler.php ====
<syntaxhighlight lang="php" line>
<?php
namespace MediaWiki\Extension\BatchTools\Handlers;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Title\Title;
/**
* Базовый класс для всех массовых инструментов.
* Содержит общие помощники и определяет структуру хэндлера.
*/
abstract class BatchToolHandler {
    protected SpecialPage $specialPage; // в новых версиях php это не ошибка
    public function __construct( SpecialPage $specialPage ) {
        $this->specialPage = $specialPage;
    }
    // Удобные геттеры для доступа к контексту страницы
    protected function getRequest() { return $this->specialPage->getRequest(); }
    protected function getOutput() { return $this->specialPage->getOutput(); }
    protected function getUser() { return $this->specialPage->getUser(); }
    protected function getAuthority() { return $this->specialPage->getAuthority(); }
    protected function msg( $key, ...$params ) { return $this->specialPage->msg( $key, ...$params ); }
    protected function getPageTitle() { return $this->specialPage->getPageTitle(); }
    /**
    * Главный метод, который отрисовывает и обрабатывает вкладку
    */
    abstract public function execute();
    /**
    * Парсит текст из Textarea, удаляет пустые строки и дубли
    */
    protected function parseTextareaList( $text ) {
        $lines = explode( "\n", $text );
        $result = [];
        foreach ( $lines as $line ) {
            $line = trim( $line );
            if ( $line !== '' ) {
                $result[] = $line;
            }
        }
        return array_unique( $result );
    }
    /**
    * Проверяет права доступа к конкретной странице (для действия).
    */
    protected function checkPagePermissions( string $action, Title $title ): ?string {
        $authority = $this->getAuthority();
        // Защита от обхода Lockdown
        if ( !$authority->definitelyCan( 'read', $title ) ) {
            return 'batchtools-error-cannot-read';
        }
        if ( !$authority->definitelyCan( $action, $title ) ) {
            if ( $action === 'undelete' ) {
                return 'batchtools-error-cannot-restore';
            }
            return 'batchtools-error-cannot-' . $action;
        }
        return null;
    }
}
</syntaxhighlight>
==== MassDeleteHandler.php ====
<syntaxhighlight lang="php" line>
<?php
namespace MediaWiki\Extension\BatchTools\Handlers;
use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Status\Status;
class MassDeleteHandler extends BatchToolHandler {
    private const MAX_BATCH_SIZE = 500;
    public function execute() {
        $out = $this->getOutput();
        $request = $this->getRequest();
        $user = $this->getUser();
        $authority = $this->getAuthority();
        if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) && $request->getVal( 'action_mass_delete' ) ) {
            $pagesText = $request->getVal( 'pages_list', '' );
            $reason = trim( $request->getVal( 'delete_reason', '' ) );
            $pages = $this->parseTextareaList( $pagesText );
            if ( empty( $pages ) ) {
                $out->addHTML( Html::errorBox( $this->msg( 'batchtools-massdelete-empty' )->text() ) );
            } elseif ( count( $pages ) > self::MAX_BATCH_SIZE ) {
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'delete',
                    'success_count' => 0,
                    'errors' => [ $this->msg( 'batchtools-error-limit', self::MAX_BATCH_SIZE )->text() ]
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'delete' ] ) );
                return;
            } else {
                $successCount = 0;
                $errors = [];
                $deletePageFactory = MediaWikiServices::getInstance()->getDeletePageFactory();
                foreach ( $pages as $pageName ) {
                    $title = Title::newFromText( $pageName );
                    if ( !$title || !$title->exists() ) {
                        $errors[] = "$pageName — страница не существует или недопустимое имя.";
                        continue;
                    }
                    $errKey = $this->checkPagePermissions( 'delete', $title );
                    if ( $errKey !== null ) {
                        $errors[] = "$pageName — " . $this->msg( $errKey )->text();
                        continue;
                    }
                    $deletePage = $deletePageFactory->newDeletePage( $title->toPageIdentity(), $authority );
                    $status = $deletePage->deleteIfAllowed( $reason );
                    if ( $status->isOK() ) {
                        $successCount++;
                    } else {
                        $statusObj = Status::wrap( $status );
                        $errors[] = "$pageName — " . $statusObj->getMessage()->text();
                    }
                }
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'delete',
                    'success_count' => $successCount,
                    'errors' => $errors
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'delete' ] ) );
                return;
            }
        }
        $pagesInput = new \OOUI\MultilineTextInputWidget([
            'name' => 'pages_list', 'rows' => 10, 'infusable' => true
        ]);
        $reasonInput = new \OOUI\TextInputWidget([
            'name' => 'delete_reason', 'infusable' => true
        ]);
        $submitBtn = new \OOUI\ButtonInputWidget([
            'type' => 'submit', 'name' => 'action_mass_delete', 'value' => '1',
            'label' => $this->msg( 'batchtools-massdelete-submit' )->text(),
            'flags' => [ 'primary', 'destructive' ], 'infusable' => true
        ]);
        $formAction = $this->getPageTitle()->getLocalURL( [ 'view' => 'delete' ] );
        $html = '<form method="post" action="' . htmlspecialchars( $formAction ) . '">';
        $html .= Html::hidden( 'wpEditToken', $user->getEditToken() );
        $html .= '<div style="background:#eaecf0; padding:20px; border:1px solid #c8ccd1; border-radius: 2px; max-width: 600px;">';
        $html .= '<div style="margin-bottom: 15px;"><b>' . $this->msg( 'batchtools-massdelete-pages' )->text() . '</b><br>' . $pagesInput->toString() . '</div>';
        $html .= '<div style="margin-bottom: 20px;"><b>' . $this->msg( 'batchtools-reason' )->text() . '</b><br>' . $reasonInput->toString() . '</div>';
        $html .= '<div>' . $submitBtn->toString() . '</div></div></form>';
        $out->addHTML( $html );
    }
}
</syntaxhighlight>
==== MassUndeleteHandler.php ====
<syntaxhighlight lang="php" line>
<?php
namespace MediaWiki\Extension\BatchTools\Handlers;
use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Status\Status;
class MassUndeleteHandler extends BatchToolHandler {
    private const MAX_BATCH_SIZE = 500;
    public function execute() {
        $out = $this->getOutput();
        $request = $this->getRequest();
        $user = $this->getUser();
        $authority = $this->getAuthority();
        if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) && $request->getVal( 'action_mass_restore' ) ) {
            $pagesText = $request->getVal( 'pages_list', '' );
            $reason = trim( $request->getVal( 'restore_reason', '' ) );
            $pages = $this->parseTextareaList( $pagesText );
            if ( empty( $pages ) ) {
                $out->addHTML( Html::errorBox( $this->msg( 'batchtools-massrestore-empty' )->text() ) );
            } elseif ( count( $pages ) > self::MAX_BATCH_SIZE ) {
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'restore',
                    'success_count' => 0,
                    'errors' => [ $this->msg( 'batchtools-error-limit', self::MAX_BATCH_SIZE )->text() ]
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'undelete' ] ) );
                return;
            } else {
                $successCount = 0;
                $errors = [];
                $undeletePageFactory = MediaWikiServices::getInstance()->getUndeletePageFactory();
                foreach ( $pages as $pageName ) {
                    $title = Title::newFromText( $pageName );
                    if ( !$title || !$title->canExist() ) {
                        $errors[] = "$pageName — недопустимое имя страницы.";
                        continue;
                    }
                    $errKey = $this->checkPagePermissions( 'undelete', $title );
                    if ( $errKey !== null ) {
                        $errors[] = "$pageName — " . $this->msg( $errKey )->text();
                        continue;
                    }
                    $undeletePage = $undeletePageFactory->newUndeletePage( $title->toPageIdentity(), $authority );
                    $status = $undeletePage->undeleteIfAllowed( $reason );
                    if ( $status->isOK() ) {
                        $val = $status->getValue();
                        $revs = $val['revs'] ?? 0;
                        $files = $val['files'] ?? 0;
                        if ( $revs > 0 || $files > 0 ) {
                            $successCount++;
                        } else {
                            $errors[] = "$pageName — " . $this->msg( 'batchtools-massrestore-norevisions' )->text();
                        }
                    } else {
                        $statusObj = Status::wrap( $status );
                        $errors[] = "$pageName — " . $statusObj->getMessage()->text();
                    }
                }
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'restore',
                    'success_count' => $successCount,
                    'errors' => $errors
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'undelete' ] ) );
                return;
            }
        }
        $pagesInput = new \OOUI\MultilineTextInputWidget([
            'name' => 'pages_list', 'rows' => 10, 'infusable' => true
        ]);
        $reasonInput = new \OOUI\TextInputWidget([
            'name' => 'restore_reason', 'infusable' => true
        ]);
        $submitBtn = new \OOUI\ButtonInputWidget([
            'type' => 'submit', 'name' => 'action_mass_restore', 'value' => '1',
            'label' => $this->msg( 'batchtools-massrestore-submit' )->text(),
            'flags' => [ 'primary', 'progressive' ], 'infusable' => true
        ]);
        $formAction = $this->getPageTitle()->getLocalURL( [ 'view' => 'undelete' ] );
        $html = '<form method="post" action="' . htmlspecialchars( $formAction ) . '">';
        $html .= Html::hidden( 'wpEditToken', $user->getEditToken() );
        $html .= '<div style="background:#eaf3ff; padding:20px; border:1px solid #36c; border-radius: 2px; max-width: 600px;">';
        $html .= '<div style="margin-bottom: 15px;"><b>' . $this->msg( 'batchtools-massrestore-pages' )->text() . '</b><br>' . $pagesInput->toString() . '</div>';
        $html .= '<div style="margin-bottom: 20px;"><b>' . $this->msg( 'batchtools-reason' )->text() . '</b><br>' . $reasonInput->toString() . '</div>';
        $html .= '<div>' . $submitBtn->toString() . '</div></div></form>';
        $out->addHTML( $html );
    }
}
</syntaxhighlight>
{{BatchTools}}
[[Категория:ЧТМ:Расширения/BatchTools/1.45]]

Текущая версия от 05:13, 3 июня 2026

45-0.3.zip СКАЧАТЬ ZIP

## [0.3.0] — 2026-05-24

### Изменено
- **Архитектурный рефакторинг**: Основной класс служебной страницы `SpecialBatchTools` разделён на изолированные компоненты. Логика работы с кодом перенесена во вложенное пространство имён `Handlers`.
- Создан абстрактный класс `BatchToolHandler`, определяющий структуру для всех будущих пакетных инструментов и предоставляющий удобные методы-хелперы для работы с контекстом MediaWiki.
- Логика массового удаления вынесена в самостоятельный класс `MassDeleteHandler`.
- Логика массового восстановления вынесена в самостоятельный класс `MassUndeleteHandler`.
- Код `SpecialBatchTools` существенно сокращён и переведён на декларативный роутинг вкладок. Это упростит добавление новых пакетных инструментов в будущем.
BatchTools/
├── extension.json
├── i18n/
│   ├── BatchTools.alias.php
│   ├── en.json
│   └── ru.json
└── includes/
    ├── SpecialBatchTools.php
    └── Handlers/
        ├── BatchToolHandler.php
        ├── MassDeleteHandler.php
        └── MassUndeleteHandler.php

extension.json

[править код]
{
	"name": "BatchTools",
	"version": "0.3",
	"author": "Diman Russkov",
	"descriptionmsg": "batchtools-desc",
	"type": "specialpage",
	"manifest_version": 2,
	"requires": {
		"MediaWiki": ">= 1.45.0"
	},
	"AvailableRights": [
		"deletebatch",
		"undeletebatch"
	],
	"SpecialPages": {
		"BatchTools": "MediaWiki\\Extension\\BatchTools\\SpecialBatchTools"
	},
	"AutoloadNamespaces": {
		"MediaWiki\\Extension\\BatchTools\\": "includes/"
	},
	"MessagesDirs": {
		"BatchTools": [
			"i18n"
		]
	},
	"ExtensionMessagesFiles": {
		"BatchToolsAlias": "i18n/BatchTools.alias.php"
	}
}


BatchTools.alias.php

[править код]
<?php
/**
 * Aliases for special pages of the BatchTools extension
 */

$specialPageAliases = [];

/** English (English) */
$specialPageAliases['en'] = [
	'BatchTools' => [ 'BatchTools' ],
];

/** Russian (Русский) */
$specialPageAliases['ru'] = [
	'BatchTools' => [ 'BatchTools', 'Пакетные_инструменты' ],
];
{
	"@metadata": {
		"authors": []
	},
	"batchtools-desc": "Provides tools for batch operations (mass delete, mass undelete, etc.)",
	"batchtools": "Batch tools",
	"right-deletebatch": "Mass delete pages",
	"action-deletebatch": "mass delete pages",
	"right-undeletebatch": "Mass undelete pages",
	"action-undeletebatch": "mass undelete pages",
	"batchtools-tab-delete": "Mass Delete",
	"batchtools-tab-undelete": "Mass Restore",
	"batchtools-error-nopermissions": "You do not have permission to access any of the batch tools.",
	"batchtools-error-cannot-read": "You do not have permission to read this page.",
	"batchtools-error-cannot-delete": "You do not have permission to delete this page.",
	"batchtools-error-cannot-restore": "You do not have permission to restore this page.",
	"batchtools-massdelete-pages": "List of pages (one per line):",
	"batchtools-reason": "Reason:",
	"batchtools-massdelete-submit": "Delete pages",
	"batchtools-massdelete-success": "Successfully deleted $1 pages.",
	"batchtools-massdelete-errors": "Failed to delete the following pages:",
	"batchtools-massdelete-empty": "The page list is empty or contains invalid names.",
	"batchtools-massrestore-pages": "List of pages to restore (one per line):",
	"batchtools-massrestore-submit": "Restore pages",
	"batchtools-massrestore-success": "Successfully restored $1 pages.",
	"batchtools-massrestore-errors": "Failed to restore the following pages:",
	"batchtools-massrestore-empty": "The page list is empty or contains invalid names.",
	"batchtools-massrestore-norevisions": "The page was not deleted or has already been restored.",
	"batchtools-error-limit": "Limit exceeded! Please process no more than {{PLURAL:$1|page|pages|pages}} at a time."
}
{
	"@metadata": {
		"authors": []
	},
	"batchtools-desc": "Предоставляет инструменты для массовых операций",
	"batchtools": "Массовые инструменты",
	"right-deletebatch": "Массовое удаление страниц",
	"action-deletebatch": "массово удалять страницы",
	"right-undeletebatch": "Массовое восстановление страниц",
	"action-undeletebatch": "массово восстанавливать страницы",
	"batchtools-tab-delete": "Массовое удаление",
	"batchtools-tab-undelete": "Массовое восстановление",
	"batchtools-error-nopermissions": "У вас нет прав для доступа к массовым инструментам.",
	"batchtools-error-cannot-read": "У вас нет прав для чтения этой страницы.",
	"batchtools-error-cannot-delete": "У вас нет прав для удаления этой страницы.",
	"batchtools-error-cannot-restore": "У вас нет прав для восстановления этой страницы.",
	"batchtools-massdelete-pages": "Список страниц (по одной на строке):",
	"batchtools-reason": "Причина:",
	"batchtools-massdelete-submit": "Удалить страницы",
	"batchtools-massdelete-success": "Успешно удалено страниц: $1.",
	"batchtools-massdelete-errors": "Не удалось удалить следующие страницы:",
	"batchtools-massdelete-empty": "Список страниц пуст или содержит недопустимые имена.",
	"batchtools-massrestore-pages": "Список страниц для восстановления (по одной на строке):",
	"batchtools-massrestore-submit": "Восстановить страницы",
	"batchtools-massrestore-success": "Успешно восстановлено страниц: $1.",
	"batchtools-massrestore-errors": "Не удалось восстановить следующие страницы:",
	"batchtools-massrestore-empty": "Список страниц пуст или содержит недопустимые имена.",
	"batchtools-massrestore-norevisions": "Страница не была удалена или уже восстановлена.",
	"batchtools-error-limit": "Превышен лимит! Пожалуйста, обрабатывайте не более $1 {{PLURAL:$1|страницы|страниц|страниц}} за один раз."
}


SpecialBatchTools.php

[править код]
<?php

namespace MediaWiki\Extension\BatchTools;

use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Html\Html;
use MediaWiki\Exception\PermissionsError;
use MediaWiki\Extension\BatchTools\Handlers\MassDeleteHandler;
use MediaWiki\Extension\BatchTools\Handlers\MassUndeleteHandler;

class SpecialBatchTools extends SpecialPage {

    public function __construct() {
        parent::__construct( 'BatchTools' );
    }

    public function isListed() {
        $authority = $this->getAuthority();
        // Пока что оставляем эти права, потом добавим новые (масс-блок, переименование и т.д.)
        return $authority->isAllowed( 'deletebatch' ) || $authority->isAllowed( 'undeletebatch' );
    }

    public function execute( $par ) {
        $authority = $this->getAuthority();

        $this->setHeaders();
        $this->checkReadOnly();

        $out = $this->getOutput();
        $out->setPageTitle( $this->msg( 'batchtools' )->text() );

        $out->enableOOUI();
        $out->addModules( [
            'oojs-ui-core',
            'oojs-ui-widgets',
            'mediawiki.widgets'
        ] );

        // 1. ДИНАМИЧЕСКИЙ СПИСОК ДОСТУПНЫХ ВКЛАДОК (Хэндлеров)
        $handlers = [];
        if ( $authority->isAllowed( 'deletebatch' ) ) {
            $handlers['delete'] = [
                'class' => MassDeleteHandler::class,
                'label' => $this->msg( 'batchtools-tab-delete' )->text()
            ];
        }
        if ( $authority->isAllowed( 'undeletebatch' ) ) {
            $handlers['undelete'] = [
                'class' => MassUndeleteHandler::class,
                'label' => $this->msg( 'batchtools-tab-undelete' )->text()
            ];
        }

        if ( empty( $handlers ) ) {
            throw new PermissionsError( 'deletebatch' );
        }

        // 2. ОПРЕДЕЛЯЕМ АКТИВНУЮ ВКЛАДКУ
        $request = $this->getRequest();
        $view = $request->getVal( 'view' );

        // Если вкладка не указана или у участника нет на неё прав — кидаем на первую доступную
        if ( $view === null || !array_key_exists( $view, $handlers ) ) {
            $view = array_key_first( $handlers );
        }

        // 3. ОТРИСОВКА МЕНЮ
        $navHtml = '<div style="margin-bottom: 25px; font-size: 1.1em; border-bottom: 2px solid #a2a9b1; padding-bottom: 10px; display: flex; flex-wrap: wrap; gap: 10px;">';
        $tabLinks = [];
        foreach ( $handlers as $tabKey => $tabData ) {
            if ( $view === $tabKey ) {
                $tabLinks[] = '<b style="color: #202122; background: #eaecf0; padding: 5px 10px; border-radius: 2px;">' . htmlspecialchars( $tabData['label'] ) . '</b>';
            } else {
                $url = $this->getPageTitle()->getLocalURL( [ 'view' => $tabKey ] );
                $tabLinks[] = Html::element( 'a', [ 'href' => $url, 'style' => 'padding: 5px 10px;' ], $tabData['label'] );
            }
        }
        $navHtml .= implode( '<span style="color:#a2a9b1;">|</span>', $tabLinks ) . '</div>';
        $out->addHTML( $navHtml );

        // Вывод общих уведомлений об успехе/ошибках
        $this->printSessionResult();

        // 4. ЗАПУСК НУЖНОГО ХЭНДЛЕРА
        $handlerClass = $handlers[$view]['class'];
        /** @var Handlers\BatchToolHandler $handlerObj */
        $handlerObj = new $handlerClass( $this );
        $handlerObj->execute();
    }

    /**
     * Выводит результаты выполнения массовых операций из сессии
     */
    private function printSessionResult() {
        $session = $this->getRequest()->getSession();
        $result = $session->get( 'batchtools_result' );

        if ( $result ) {
            $session->remove( 'batchtools_result' );
            $html = '';

            $action = $result['action'] ?? 'delete';
            $msgSuccess = $action === 'delete' ? 'batchtools-massdelete-success' : 'batchtools-massrestore-success';
            $msgErrors = $action === 'delete' ? 'batchtools-massdelete-errors' : 'batchtools-massrestore-errors';

            if ( !empty( $result['success_count'] ) ) {
                $html .= Html::successBox( $this->msg( $msgSuccess, $result['success_count'] )->text() );
            }

            if ( !empty( $result['errors'] ) ) {
                $errHtml = '<b>' . $this->msg( $msgErrors )->text() . '</b><ul style="margin-top: 5px; margin-bottom: 0;">';
                foreach ( $result['errors'] as $err ) {
                    $errHtml .= '<li>' . htmlspecialchars( $err ) . '</li>';
                }
                $errHtml .= '</ul>';
                $html .= Html::errorBox( $errHtml );
            }

            if ( $html !== '' ) {
                $this->getOutput()->addHTML( '<div style="margin-bottom: 20px;">' . $html . '</div>' );
            }
        }
    }
}


BatchToolHandler.php

[править код]
<?php

namespace MediaWiki\Extension\BatchTools\Handlers;

use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Title\Title;

/**
 * Базовый класс для всех массовых инструментов.
 * Содержит общие помощники и определяет структуру хэндлера.
 */
abstract class BatchToolHandler {

    protected SpecialPage $specialPage; // в новых версиях php это не ошибка

    public function __construct( SpecialPage $specialPage ) {
        $this->specialPage = $specialPage;
    }

    // Удобные геттеры для доступа к контексту страницы
    protected function getRequest() { return $this->specialPage->getRequest(); }
    protected function getOutput() { return $this->specialPage->getOutput(); }
    protected function getUser() { return $this->specialPage->getUser(); }
    protected function getAuthority() { return $this->specialPage->getAuthority(); }
    protected function msg( $key, ...$params ) { return $this->specialPage->msg( $key, ...$params ); }
    protected function getPageTitle() { return $this->specialPage->getPageTitle(); }

    /**
     * Главный метод, который отрисовывает и обрабатывает вкладку
     */
    abstract public function execute();

    /**
     * Парсит текст из Textarea, удаляет пустые строки и дубли
     */
    protected function parseTextareaList( $text ) {
        $lines = explode( "\n", $text );
        $result = [];
        foreach ( $lines as $line ) {
            $line = trim( $line );
            if ( $line !== '' ) {
                $result[] = $line;
            }
        }
        return array_unique( $result );
    }

    /**
     * Проверяет права доступа к конкретной странице (для действия).
     */
    protected function checkPagePermissions( string $action, Title $title ): ?string {
        $authority = $this->getAuthority();

        // Защита от обхода Lockdown
        if ( !$authority->definitelyCan( 'read', $title ) ) {
            return 'batchtools-error-cannot-read';
        }

        if ( !$authority->definitelyCan( $action, $title ) ) {
            if ( $action === 'undelete' ) {
                return 'batchtools-error-cannot-restore';
            }
            return 'batchtools-error-cannot-' . $action;
        }

        return null;
    }
}

MassDeleteHandler.php

[править код]
<?php

namespace MediaWiki\Extension\BatchTools\Handlers;

use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Status\Status;

class MassDeleteHandler extends BatchToolHandler {

    private const MAX_BATCH_SIZE = 500;

    public function execute() {
        $out = $this->getOutput();
        $request = $this->getRequest();
        $user = $this->getUser();
        $authority = $this->getAuthority();

        if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) && $request->getVal( 'action_mass_delete' ) ) {
            $pagesText = $request->getVal( 'pages_list', '' );
            $reason = trim( $request->getVal( 'delete_reason', '' ) );
            $pages = $this->parseTextareaList( $pagesText );

            if ( empty( $pages ) ) {
                $out->addHTML( Html::errorBox( $this->msg( 'batchtools-massdelete-empty' )->text() ) );
            } elseif ( count( $pages ) > self::MAX_BATCH_SIZE ) {
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'delete',
                    'success_count' => 0,
                    'errors' => [ $this->msg( 'batchtools-error-limit', self::MAX_BATCH_SIZE )->text() ]
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'delete' ] ) );
                return;
            } else {
                $successCount = 0;
                $errors = [];
                $deletePageFactory = MediaWikiServices::getInstance()->getDeletePageFactory();

                foreach ( $pages as $pageName ) {
                    $title = Title::newFromText( $pageName );

                    if ( !$title || !$title->exists() ) {
                        $errors[] = "$pageName — страница не существует или недопустимое имя.";
                        continue;
                    }

                    $errKey = $this->checkPagePermissions( 'delete', $title );
                    if ( $errKey !== null ) {
                        $errors[] = "$pageName — " . $this->msg( $errKey )->text();
                        continue;
                    }

                    $deletePage = $deletePageFactory->newDeletePage( $title->toPageIdentity(), $authority );
                    $status = $deletePage->deleteIfAllowed( $reason );

                    if ( $status->isOK() ) {
                        $successCount++;
                    } else {
                        $statusObj = Status::wrap( $status );
                        $errors[] = "$pageName — " . $statusObj->getMessage()->text();
                    }
                }

                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'delete',
                    'success_count' => $successCount,
                    'errors' => $errors
                ] );

                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'delete' ] ) );
                return;
            }
        }

        $pagesInput = new \OOUI\MultilineTextInputWidget([
            'name' => 'pages_list', 'rows' => 10, 'infusable' => true
        ]);
        $reasonInput = new \OOUI\TextInputWidget([
            'name' => 'delete_reason', 'infusable' => true
        ]);
        $submitBtn = new \OOUI\ButtonInputWidget([
            'type' => 'submit', 'name' => 'action_mass_delete', 'value' => '1',
            'label' => $this->msg( 'batchtools-massdelete-submit' )->text(),
            'flags' => [ 'primary', 'destructive' ], 'infusable' => true
        ]);

        $formAction = $this->getPageTitle()->getLocalURL( [ 'view' => 'delete' ] );
        $html = '<form method="post" action="' . htmlspecialchars( $formAction ) . '">';
        $html .= Html::hidden( 'wpEditToken', $user->getEditToken() );
        $html .= '<div style="background:#eaecf0; padding:20px; border:1px solid #c8ccd1; border-radius: 2px; max-width: 600px;">';
        $html .= '<div style="margin-bottom: 15px;"><b>' . $this->msg( 'batchtools-massdelete-pages' )->text() . '</b><br>' . $pagesInput->toString() . '</div>';
        $html .= '<div style="margin-bottom: 20px;"><b>' . $this->msg( 'batchtools-reason' )->text() . '</b><br>' . $reasonInput->toString() . '</div>';
        $html .= '<div>' . $submitBtn->toString() . '</div></div></form>';

        $out->addHTML( $html );
    }
}

MassUndeleteHandler.php

[править код]
<?php

namespace MediaWiki\Extension\BatchTools\Handlers;

use MediaWiki\Html\Html;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use MediaWiki\Status\Status;

class MassUndeleteHandler extends BatchToolHandler {

    private const MAX_BATCH_SIZE = 500;

    public function execute() {
        $out = $this->getOutput();
        $request = $this->getRequest();
        $user = $this->getUser();
        $authority = $this->getAuthority();

        if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ) ) && $request->getVal( 'action_mass_restore' ) ) {
            $pagesText = $request->getVal( 'pages_list', '' );
            $reason = trim( $request->getVal( 'restore_reason', '' ) );
            $pages = $this->parseTextareaList( $pagesText );

            if ( empty( $pages ) ) {
                $out->addHTML( Html::errorBox( $this->msg( 'batchtools-massrestore-empty' )->text() ) );
            } elseif ( count( $pages ) > self::MAX_BATCH_SIZE ) {
                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'restore',
                    'success_count' => 0,
                    'errors' => [ $this->msg( 'batchtools-error-limit', self::MAX_BATCH_SIZE )->text() ]
                ] );
                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'undelete' ] ) );
                return;
            } else {
                $successCount = 0;
                $errors = [];
                $undeletePageFactory = MediaWikiServices::getInstance()->getUndeletePageFactory();

                foreach ( $pages as $pageName ) {
                    $title = Title::newFromText( $pageName );

                    if ( !$title || !$title->canExist() ) {
                        $errors[] = "$pageName — недопустимое имя страницы.";
                        continue;
                    }

                    $errKey = $this->checkPagePermissions( 'undelete', $title );
                    if ( $errKey !== null ) {
                        $errors[] = "$pageName — " . $this->msg( $errKey )->text();
                        continue;
                    }

                    $undeletePage = $undeletePageFactory->newUndeletePage( $title->toPageIdentity(), $authority );
                    $status = $undeletePage->undeleteIfAllowed( $reason );

                    if ( $status->isOK() ) {
                        $val = $status->getValue();
                        $revs = $val['revs'] ?? 0;
                        $files = $val['files'] ?? 0;

                        if ( $revs > 0 || $files > 0 ) {
                            $successCount++;
                        } else {
                            $errors[] = "$pageName — " . $this->msg( 'batchtools-massrestore-norevisions' )->text();
                        }
                    } else {
                        $statusObj = Status::wrap( $status );
                        $errors[] = "$pageName — " . $statusObj->getMessage()->text();
                    }
                }

                $request->getSession()->set( 'batchtools_result', [
                    'action' => 'restore',
                    'success_count' => $successCount,
                    'errors' => $errors
                ] );

                $out->redirect( $this->getPageTitle()->getFullURL( [ 'view' => 'undelete' ] ) );
                return;
            }
        }

        $pagesInput = new \OOUI\MultilineTextInputWidget([
            'name' => 'pages_list', 'rows' => 10, 'infusable' => true
        ]);
        $reasonInput = new \OOUI\TextInputWidget([
            'name' => 'restore_reason', 'infusable' => true
        ]);
        $submitBtn = new \OOUI\ButtonInputWidget([
            'type' => 'submit', 'name' => 'action_mass_restore', 'value' => '1',
            'label' => $this->msg( 'batchtools-massrestore-submit' )->text(),
            'flags' => [ 'primary', 'progressive' ], 'infusable' => true
        ]);

        $formAction = $this->getPageTitle()->getLocalURL( [ 'view' => 'undelete' ] );
        $html = '<form method="post" action="' . htmlspecialchars( $formAction ) . '">';
        $html .= Html::hidden( 'wpEditToken', $user->getEditToken() );
        $html .= '<div style="background:#eaf3ff; padding:20px; border:1px solid #36c; border-radius: 2px; max-width: 600px;">';
        $html .= '<div style="margin-bottom: 15px;"><b>' . $this->msg( 'batchtools-massrestore-pages' )->text() . '</b><br>' . $pagesInput->toString() . '</div>';
        $html .= '<div style="margin-bottom: 20px;"><b>' . $this->msg( 'batchtools-reason' )->text() . '</b><br>' . $reasonInput->toString() . '</div>';
        $html .= '<div>' . $submitBtn->toString() . '</div></div></form>';

        $out->addHTML( $html );
    }
}
BatchTools
Mediawiki 1.45 0.9 • 0.8 • 0.7 • 0.6 • 0.5 • 0.4 • 0.3 • 0.2 • 0.1