ЧТМ:Расширения/BatchTools/1.45/0.3: различия между версиями
Перейти к навигации
Перейти к поиску
Нет описания правки |
мНет описания правки |
||
| (не показано 8 промежуточных версий 2 участников) | |||
| Строка 1: | Строка 1: | ||
<span class="plainlinks">'''{{Size|130|[{{SERVER}}/index.php/Файл:BatchTools-REL1 45-0.3.zip СКАЧАТЬ ZIP]}}'''</span> | |||
= | <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> | |||
== 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> | |||
== includes == | == includes == | ||
=== | === 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 ==== | ==== BatchToolHandler.php ==== | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php" line> | ||
<?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; | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
==== MassDeleteHandler.php ==== | ==== MassDeleteHandler.php ==== | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php" line> | ||
<?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 ); | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
==== MassUndeleteHandler.php ==== | ==== MassUndeleteHandler.php ==== | ||
<syntaxhighlight lang="php"> | <syntaxhighlight lang="php" line> | ||
<?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 ); | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
{{BatchTools}} | |||
[[Категория:ЧТМ:Расширения/BatchTools/1.45]] | |||
Текущая версия от 05:13, 3 июня 2026
## [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"
}
}
i18n
[править код]BatchTools.alias.php
[править код]<?php
/**
* Aliases for special pages of the BatchTools extension
*/
$specialPageAliases = [];
/** English (English) */
$specialPageAliases['en'] = [
'BatchTools' => [ 'BatchTools' ],
];
/** Russian (Русский) */
$specialPageAliases['ru'] = [
'BatchTools' => [ 'BatchTools', 'Пакетные_инструменты' ],
];
en.json
[править код]{
"@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."
}
ru.json
[править код]{
"@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|страницы|страниц|страниц}} за один раз."
}
includes
[править код]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>' );
}
}
}
}
Handlers
[править код]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 | |||
|---|---|---|---|
|
|||