ЧТМ:Расширения/BatchTools/1.45/0.1: различия между версиями
Новая страница: «<markdown>## [0.1.0] — 2026-05-24 ### Добавлено - Создана служебная страница `Special:BatchTools` (с поддержкой ЧПУ-алиаса «Пакетные инструменты» на русском языке). - Реализована вкладка «Массовое удаление» для быстрой очистки ненужных страниц списком. - Реализована вкладка «...» |
мНет описания правки |
||
| (не показано 15 промежуточных версий 3 участников) | |||
| Строка 1: | Строка 1: | ||
<markdown>## [0.1.0] — 2026-05-24 | <span class="plainlinks">'''{{Size|130|[{{SERVER}}/index.php/Файл:BatchTools-REL1 45-0.1.zip СКАЧАТЬ ZIP]}}'''</span> | ||
<syntaxhighlight lang="markdown">## [0.1.0] — 2026-05-24 | |||
### Добавлено | ### Добавлено | ||
| Строка 12: | Строка 14: | ||
- Реализована защита от межсайтовой подделки запросов (CSRF) с помощью механизма Edit Token ядра MediaWiki. | - Реализована защита от межсайтовой подделки запросов (CSRF) с помощью механизма Edit Token ядра MediaWiki. | ||
- Введено ограничение на размер пакета: за одну операцию допускается обрабатывать не более 500 страниц. | - Введено ограничение на размер пакета: за одну операцию допускается обрабатывать не более 500 страниц. | ||
- Реализовано экранирование имён страниц при выводе ошибок для предотвращения уязвимостей типа XSS.</ | - Реализовано экранирование имён страниц при выводе ошибок для предотвращения уязвимостей типа XSS.</syntaxhighlight> | ||
<pre> | |||
BatchTools/ | |||
├── extension.json | |||
├── i18n/ | |||
│ ├── BatchTools.alias.php | |||
│ ├── en.json | |||
│ └── ru.json | |||
└── includes/ | |||
└── SpecialBatchTools.php | |||
</pre> | |||
=== extension.json === | === extension.json === | ||
<syntaxhighlight lang="json"> | <syntaxhighlight lang="json" line> | ||
{ | { | ||
"name": "BatchTools", | "name": "BatchTools", | ||
| Строка 50: | Строка 61: | ||
== 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-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-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" line> | |||
<?php | |||
namespace MediaWiki\Extension\BatchTools; | |||
use MediaWiki\SpecialPage\SpecialPage; | |||
use MediaWiki\Html\Html; | |||
use MediaWiki\Exception\PermissionsError; | |||
use MediaWiki\MediaWikiServices; | |||
use MediaWiki\Title\Title; | |||
use MediaWiki\Status\Status; | |||
class SpecialBatchTools extends SpecialPage { | |||
// Максимальное количество страниц за одну операцию | |||
private const MAX_BATCH_SIZE = 500; | |||
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(); | |||
if ( !$authority->isAllowed( 'deletebatch' ) && !$authority->isAllowed( 'undeletebatch' ) ) { | |||
throw new PermissionsError( 'deletebatch' ); | |||
} | |||
$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' | |||
] ); | |||
$tabs = []; | |||
if ( $authority->isAllowed( 'deletebatch' ) ) { | |||
$tabs['delete'] = $this->msg( 'batchtools-tab-delete' )->text(); | |||
} | |||
if ( $authority->isAllowed( 'undeletebatch' ) ) { | |||
$tabs['undelete'] = $this->msg( 'batchtools-tab-undelete' )->text(); | |||
} | |||
if ( empty( $tabs ) ) { | |||
$out->addHTML( Html::errorBox( $this->msg( 'batchtools-error-nopermissions' )->text() ) ); | |||
return; | |||
} | |||
$request = $this->getRequest(); | |||
$view = $request->getVal( 'view' ); | |||
if ( $view === null || !array_key_exists( $view, $tabs ) ) { | |||
$view = array_key_first( $tabs ); | |||
} | |||
$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 ( $tabs as $tabKey => $tabName ) { | |||
if ( $view === $tabKey ) { | |||
$tabLinks[] = '<b style="color: #202122; background: #eaecf0; padding: 5px 10px; border-radius: 2px;">' . htmlspecialchars( $tabName ) . '</b>'; | |||
} else { | |||
$url = $this->getPageTitle()->getLocalURL( [ 'view' => $tabKey ] ); | |||
$tabLinks[] = Html::element( 'a', [ 'href' => $url, 'style' => 'padding: 5px 10px;' ], $tabName ); | |||
} | |||
} | |||
$navHtml .= implode( '<span style="color:#a2a9b1;">|</span>', $tabLinks ) . '</div>'; | |||
$out->addHTML( $navHtml ); | |||
// Вывод уведомлений об успехе/ошибках | |||
$this->printSessionResult(); | |||
if ( $view === 'delete' ) { | |||
$this->showMassDeleteTab(); | |||
} elseif ( $view === 'undelete' ) { | |||
$this->showMassRestoreTab(); | |||
} | |||
} | |||
/** | |||
* Выводит результаты выполнения массовых операций из сессии | |||
*/ | |||
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 ) { | |||
// Абсолютная защита от XSS: все имена страниц экранируются здесь | |||
$errHtml .= '<li>' . htmlspecialchars( $err ) . '</li>'; | |||
} | |||
$errHtml .= '</ul>'; | |||
$html .= Html::errorBox( $errHtml ); | |||
} | |||
if ( $html !== '' ) { | |||
$this->getOutput()->addHTML( '<div style="margin-bottom: 20px;">' . $html . '</div>' ); | |||
} | |||
} | |||
} | |||
/** | |||
* Вкладка: Массовое удаление | |||
*/ | |||
private function showMassDeleteTab() { | |||
$out = $this->getOutput(); | |||
$request = $this->getRequest(); | |||
$user = $this->getUser(); | |||
$authority = $this->getAuthority(); | |||
if ( !$authority->isAllowed( 'deletebatch' ) ) { | |||
$out->addHTML( Html::errorBox( $this->msg( 'batchtools-error-nopermissions' )->text() ) ); | |||
return; | |||
} | |||
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; | |||
} | |||
$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 ); | |||
} | |||
/** | |||
* Вкладка: Массовое восстановление | |||
*/ | |||
private function showMassRestoreTab() { | |||
$out = $this->getOutput(); | |||
$request = $this->getRequest(); | |||
$user = $this->getUser(); | |||
$authority = $this->getAuthority(); | |||
if ( !$authority->isAllowed( 'undeletebatch' ) ) { | |||
$out->addHTML( Html::errorBox( $this->msg( 'batchtools-error-nopermissions' )->text() ) ); | |||
return; | |||
} | |||
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; | |||
} | |||
$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 ); | |||
} | |||
private function parseTextareaList( $text ) { | |||
$lines = explode( "\n", $text ); | |||
$result = []; | |||
foreach ( $lines as $line ) { | |||
$line = trim( $line ); | |||
if ( $line !== '' ) { | |||
$result[] = $line; | |||
} | |||
} | |||
return array_unique( $result ); | |||
} | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
{{BatchTools}} | |||
[[Категория:ЧТМ:Расширения/BatchTools/1.45]] | |||