增加创建页面的标题有效性校验功能

master
落雨楓 4 months ago
parent dcf17403b9
commit c9e82dab06

@ -25,6 +25,9 @@
"ParserFirstCallInit": "Isekai\\Widgets\\Widgets::onParserSetup", "ParserFirstCallInit": "Isekai\\Widgets\\Widgets::onParserSetup",
"BeforePageDisplay": "Isekai\\Widgets\\Widgets::onLoad" "BeforePageDisplay": "Isekai\\Widgets\\Widgets::onLoad"
}, },
"APIModules": {
"isekaiwidgets": "Isekai\\Widgets\\Api\\ApiIsekaiWidgets"
},
"ResourceModules": { "ResourceModules": {
"ext.isekai.widgets.global": { "ext.isekai.widgets.global": {
"styles": [ "styles": [
@ -56,7 +59,9 @@
"isekai-createpage-create-page-button", "isekai-createpage-create-page-button",
"isekai-createpage-page-exists", "isekai-createpage-page-exists",
"isekai-createpage-title-empty", "isekai-createpage-title-empty",
"isekai-createpage-redirecting" "isekai-createpage-redirecting",
"isekai-createpage-warning-ignore",
"isekai-createpage-ignore-warnings-help"
] ]
}, },
"ext.isekai.discover": { "ext.isekai.discover": {
@ -277,7 +282,7 @@
"IsekaiGlobalWidgets": { "IsekaiGlobalWidgets": {
"value": ["baseWidgets", "offcanvasTOC"] "value": ["baseWidgets", "offcanvasTOC"]
}, },
"IsekaiCreatePageNamespaces": { "IsekaiCreatePageTypes": {
"value": [] "value": []
} }
}, },

@ -2,12 +2,21 @@
"isekai-widgets": "异世界百科 小部件", "isekai-widgets": "异世界百科 小部件",
"isekai-widgets-desc": "在异世界百科上使用的一些小部件", "isekai-widgets-desc": "在异世界百科上使用的一些小部件",
"apihelp-isekaiwidgets-summary": "异世界百科提供的小组件所需的API",
"apihelp-isekaiwidgets-param-method": "操作",
"isekai-createpage-page-title": "页面标题", "isekai-createpage-page-title": "页面标题",
"isekai-createpage-create-page": "新建页面", "isekai-createpage-create-page": "新建页面",
"isekai-createpage-create-page-button": "创建", "isekai-createpage-create-page-button": "创建",
"isekai-createpage-page-exists": "已有相同名字的页面存在,换一个名字吧。", "isekai-createpage-page-exists": "已有相同名字的页面存在,换一个名字吧。",
"isekai-createpage-title-empty": "请填写标题", "isekai-createpage-title-empty": "请填写标题",
"isekai-createpage-redirecting": "正在跳转,请稍后……", "isekai-createpage-redirecting": "正在跳转,请稍后……",
"isekai-createpage-warning-ignore": "忽略警告",
"isekai-createpage-ignore-warnings-help": "如果你确定要以当前页面标题新建页面,可以忽略这些警告,不过新建的页面可能被管理员删除或重命名。",
"apihelp-isekaiwidgets+createpagevalidatetitle-summary": "检测新建页面标题是否有效",
"apihelp-isekaiwidgets+createpagevalidatetitle-param-iwnewpagetitle": "新建页面的标题",
"apihelp-isekaiwidgets+createpagevalidatetitle-param-iwnewpagetype": "新建页面的类型",
"isekai-discover-langcode": "zh", "isekai-discover-langcode": "zh",
"isekai-discover-randompage": "随机页面", "isekai-discover-randompage": "随机页面",

@ -0,0 +1,96 @@
<?php
namespace Isekai\Widgets\Api;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiMain;
use MediaWiki\Api\ApiModuleManager;
use MediaWiki\MediaWikiServices;
use Wikimedia\ParamValidator\ParamValidator;
use Isekai\Widgets\Api\IsekaiWidgets\ApiCreatePageValidateTitle;
class ApiIsekaiWidgets extends ApiBase {
public static $methodModules = [
'createpagevalidatetitle' => [
'class' => ApiCreatePageValidateTitle::class,
],
];
private $mModuleMgr;
private $mParams;
/**
* @param ApiMain $main
* @param string $action
*/
public function __construct( ApiMain $main, $action )
{
parent::__construct($main, $action);
$this->mModuleMgr = new ApiModuleManager(
$this,
MediaWikiServices::getInstance()->getObjectFactory()
);
// Allow custom modules to be added in LocalSettings.php
$config = $this->getConfig();
$this->mModuleMgr->addModules( self::$methodModules, 'method' );
}
/**
* Overrides to return this instance's module manager.
* @return ApiModuleManager
*/
public function getModuleManager() {
return $this->mModuleMgr;
}
public function execute() {
$this->mParams = $this->extractRequestParams();
$modules = [];
$this->instantiateModules( $modules, 'method' );
$this->getResult()->addValue(null, 'modules', array_keys($modules));
foreach ( $modules as $module ) {
$module->execute();
}
}
/**
* Create instances of all modules requested by the client
* @param array &$modules To append instantiated modules to
* @param string $param Parameter name to read modules from
*/
private function instantiateModules( &$modules, $param ) {
$wasPosted = $this->getRequest()->wasPosted();
if ( isset( $this->mParams[$param] ) ) {
foreach ( $this->mParams[$param] as $moduleName ) {
$instance = $this->mModuleMgr->getModule( $moduleName, $param );
if ( $instance === null ) {
ApiBase::dieDebug( __METHOD__, 'Error instantiating module' );
}
if ( !$wasPosted && $instance->mustBePosted() ) {
$this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $moduleName ] );
}
// Ignore duplicates. TODO 2.0: die()?
if ( !array_key_exists( $moduleName, $modules ) ) {
$modules[$moduleName] = $instance;
}
}
}
}
public function getAllowedParams( $flags = 0 ) {
$result = [
'method' => [
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_TYPE => 'submodule',
],
];
return $result;
}
public function isInternal() {
return true;
}
}

@ -0,0 +1,100 @@
<?php
namespace Isekai\Widgets\Api\IsekaiWidgets;
use Isekai\Widgets\Api\ApiIsekaiWidgets;
use Isekai\Widgets\Utils\NewPageTitleValidationResult;
use MediaWiki\Api\ApiBase;
use MediaWiki\MediaWikiServices;
use MediaWiki\Title\Title;
use Wikimedia\ParamValidator\ParamValidator;
class ApiCreatePageValidateTitle extends ApiBase {
/** @var MediaWikiServices */
private $services;
/** @var ApiIsekaiWidgets */
private $mParent;
public function __construct(ApiIsekaiWidgets $main, $method) {
parent::__construct($main->getMain(), $method);
$this->mParent = $main;
$this->services = MediaWikiServices::getInstance();
}
public function execute() {
$newPageTitle = $this->getParameter('iwnewpagetitle');
$newPageType = $this->getParameter('iwnewpagetype');
$config = $this->services->getMainConfig();
$pageTypes = $config->get('IsekaiCreatePageTypes');
$newPageTypePrefix = "";
foreach ($pageTypes as $item) {
if ($item['id'] === $newPageType) {
$newPageTypePrefix = $item['prefix'] ?? '';
break;
}
}
$newPageTitle = $newPageTypePrefix . $newPageTitle;
$user = $this->getUser();
$permissionManager = $this->services->getPermissionManager();
$result = $this->getResult();
$validationResult = new NewPageTitleValidationResult();
/*if ($permissionManager->userHasRight($user, 'editprotected')) {
// 允许编辑受保护页面的用户,不进行标题检查
$result->addValue(['isekaiwidget'], $this->getModuleName(), $validationResult->toArray());
return true;
}*/
// 检查页面是否已经存在
try {
$title = Title::newFromText($newPageTitle);
if ($title && $title->exists()) {
$validationResult->addError('pageexists', wfMessage('isekai-createpage-page-exists')->text());
}
} catch (\Exception $e) {
wfLogWarning('Failed to validate new page title: ' . $e->getMessage());
}
// 调用Hook检查标题是否正确
$hookContainer = $this->services->getHookContainer();
$hookContainer->run('IsekaiWidgets::CreatePageValidateTitle', [
$validationResult,
$user,
$newPageTitle,
$newPageType,
]);
$result->addValue(['isekaiwidget'], $this->getModuleName(), $validationResult->toArray());
return true;
}
public function getAllowedParams($flags = 0) {
return [
'iwnewpagetitle' => [
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_DEFAULT => null,
ParamValidator::PARAM_REQUIRED => true,
],
'iwnewpagetype' => [
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_DEFAULT => 'default',
ParamValidator::PARAM_REQUIRED => false,
],
];
}
public function getCacheMode($params) {
return 'private';
}
public function getParent() {
return $this->mParent;
}
}

@ -18,9 +18,9 @@ class CreatePageWidget {
public static function create($text, $params, Parser $parser, PPFrame $frame) { public static function create($text, $params, Parser $parser, PPFrame $frame) {
$config = MediaWikiServices::getInstance()->getMainConfig(); $config = MediaWikiServices::getInstance()->getMainConfig();
$configCreatePageNamespaces = $config->get('IsekaiCreatePageNamespaces'); $configCreatePageTypes = $config->get('IsekaiCreatePageTypes');
$parser->getOutput()->setJsConfigVar('wgIsekaiCreatePageNamespaces', $configCreatePageNamespaces); $parser->getOutput()->setJsConfigVar('wgIsekaiCreatePageTypes', $configCreatePageTypes);
$parser->getOutput()->addModules(['ext.isekai.createPage']); $parser->getOutput()->addModules(['ext.isekai.createPage']);
return self::getHtml(); return self::getHtml();

@ -0,0 +1,46 @@
<?php
namespace Isekai\Widgets\Utils;
class NewPageTitleValidationResult {
private $hasError = false;
private $hasWarning = false;
private $warnings = [];
private $errors = [];
public function toArray(): array {
return [
'hasError' => $this->hasError ? 1 : 0,
'hasWarning' => $this->hasWarning ? 1 : 0,
'warnings' => $this->warnings,
'errors' => $this->errors,
];
}
public function __serialize(): array {
return $this->toArray();
}
public function __unserialize(array $data): void {
$this->hasError = (bool)$data['hasError'] ?? false;
$this->hasWarning = (bool)$data['hasWarning'] ?? false;
$this->warnings = $data['warnings'] ?? [];
$this->errors = $data['errors'] ?? [];
}
public function addError(string $errorCode, string $errorMessage): void {
$this->hasError = true;
$this->errors[] = [
'code' => $errorCode,
'message' => $errorMessage,
];
}
public function addWarning(string $warningCode, string $warningMessage): void {
$this->hasWarning = true;
$this->warnings[] = [
'code' => $warningCode,
'message' => $warningMessage,
];
}
}

@ -23,6 +23,23 @@
display: none; display: none;
} }
.isekai-createpage-ignore-warnings-field {
margin-top: 0.5em;
.oo-ui-fieldLayout-header {
display: block;
.oo-ui-labelElement-label {
line-height: 2.4em;
font-size: var(--font-size-medium);
}
.oo-ui-labelWidget.oo-ui-inline-help {
font-size: var(--font-size-small);
line-height: 1.42857143em;
}
}
}
.oo-ui-fieldLayout-messages { .oo-ui-fieldLayout-messages {
margin: 0.5em 0 0 0.5em; margin: 0.5em 0 0 0.5em;
} }
@ -46,6 +63,14 @@
flex-shrink: 1; flex-shrink: 1;
} }
} }
.errors-container:not(:empty) {
padding-top: 0.5rem;
}
.errors-container > * {
margin-top: 0.5em;
}
} }
} }
} }

File diff suppressed because one or more lines are too long

@ -6,29 +6,33 @@ class CreatePageWidget {
this.pageUrl = null; this.pageUrl = null;
this.api = new mw.Api(); this.api = new mw.Api();
this.hasError = false; this.hasErrors = false;
this.hasWarnings = false;
this.createButtonEnabled = true;
this.initDom(); this.initDom();
} }
initDom() { initDom() {
let namespaces = mw.config.get('wgIsekaiCreatePageNamespaces'); let namespaces = mw.config.get('wgIsekaiCreatePageTypes');
if (!Array.isArray(namespaces) || namespaces.length === 0) { if (!Array.isArray(namespaces) || namespaces.length === 0) {
namespaces = [ namespaces = [
{ {
type: 'default',
name: 'Main', name: 'Main',
prefix: '', prefix: '',
} }
]; ];
} }
this.selectNamespaceDropdown = new OO.ui.DropdownInputWidget({ this.selectPageTypeDropdown = new OO.ui.DropdownInputWidget({
options: namespaces.map((nsInfo) => ({ options: namespaces.map((nsInfo) => ({
data: nsInfo.prefix, data: JSON.stringify(nsInfo),
label: nsInfo.name, label: nsInfo.name,
})), })),
}); });
this.selectNamespaceDropdown.on('change', this.onNamespaceChange.bind(this)); this.selectPageTypeDropdown.on('change', this.onPageTypeChange.bind(this));
this.pageNameInput = new isekai.ui.CreatePageInputWidget({ this.pageNameInput = new isekai.ui.CreatePageInputWidget({
placeholder: mw.message('isekai-createpage-page-title').parse(), placeholder: mw.message('isekai-createpage-page-title').parse(),
@ -46,49 +50,78 @@ class CreatePageWidget {
this.createButton.on('click', this.createPage.bind(this)); this.createButton.on('click', this.createPage.bind(this));
this.inputGroup = new OO.ui.ActionFieldLayout(this.pageNameInput, this.createButton, { this.inputGroup = new OO.ui.ActionFieldLayout(this.pageNameInput, this.createButton, {
align: 'top' align: 'center'
}); });
this.formGroup = new OO.ui.HorizontalLayout({ this.formGroup = new OO.ui.HorizontalLayout({
items: [ items: [
this.selectNamespaceDropdown, this.selectPageTypeDropdown,
this.inputGroup this.inputGroup
] ]
}); });
this.baseDom.find('.card-body .card-content').append(this.formGroup.$element); this.errorsContainer = $('<div class="errors-container">');
this.ignoreWarningsCheckbox = new OO.ui.CheckboxInputWidget({
selected: false
});
this.ignoreWarningsCheckbox.on('change', this.refreshCreatePageBtnEnabled.bind(this));
this.ignoreWarningsCheckboxField = new OO.ui.FieldLayout(this.ignoreWarningsCheckbox, {
align: 'inline',
label: mw.message('isekai-createpage-warning-ignore').parse(),
helpInline: true,
help: mw.message('isekai-createpage-ignore-warnings-help').parse(),
});
this.ignoreWarningsCheckboxField.$element
.addClass('isekai-createpage-ignore-warnings-field')
.hide();
this.cardContent = this.baseDom.find('.card-body .card-content');
this.cardContent.append(this.formGroup.$element);
this.cardContent.append(this.errorsContainer);
this.cardContent.append(this.ignoreWarningsCheckboxField.$element);
} }
createPage() { createPage() {
let title = this.selectNamespaceDropdown.getValue() + this.pageNameInput.getValue(); let title = this.pageNameInput.getValue();
console.log(title); let pageTypeConfig = this.selectPageTypeDropdown.getValue();
if (this.hasError) { pageTypeConfig = JSON.parse(pageTypeConfig || '{}');
this.clearError(); //清除errors
} let pageTypeId = pageTypeConfig.id || 'default';
let fullTitle = (pageTypeConfig.prefix || '') + title;
if (title.trim().length > 0) { if (title.trim().length > 0) {
this.createButton.setDisabled(true); this.createButton.setDisabled(true);
this.pageExists(title).then((exists) => {
if (exists) { if (this.hasWarnings && this.ignoreWarningsCheckbox.isSelected()) {
this.createButton.setDisabled(false); // 已忽略警告
this.setError(mw.message('isekai-createpage-page-exists').parse()); //提示页面已经存在 this._doCreatePage(fullTitle);
} else { } else {
let targetUrl = mw.util.getUrl(title, { veaction: 'edit' }); this.validateNewPageTitle(title, pageTypeId).then((valid) => {
this.inputGroup.setSuccess([ if (valid) {
mw.message('isekai-createpage-redirecting').parse() this._doCreatePage(fullTitle);
]); //提示正在跳转
location.href = targetUrl;
} }
}); });
}
} else { } else {
this.setError(mw.message('isekai-createpage-title-empty').parse()); this.setErrors([
mw.message('isekai-createpage-title-empty').parse()
]);
} }
} }
onPageNameChange() { _doCreatePage(fullTitle) {
if (this.hasError) { let targetUrl = mw.util.getUrl(fullTitle, { veaction: 'edit' });
this.clearError(); window.open(targetUrl, '_blank');
this.pageNameInput.setValue('');
this.clearErrorWarnings();
} }
onPageNameChange() {
this.clearErrorWarnings();
let value = this.pageNameInput.getValue(); let value = this.pageNameInput.getValue();
if (value.indexOf('') !== -1 || value.indexOf('`') !== -1) { if (value.indexOf('') !== -1 || value.indexOf('`') !== -1) {
let range = this.pageNameInput.getRange(); let range = this.pageNameInput.getRange();
@ -98,42 +131,118 @@ class CreatePageWidget {
} }
} }
onNamespaceChange() { onPageTypeChange() {
this.pageNameInput.setPagePrefix(this.selectNamespaceDropdown.getValue()); let pageTypeConfig = this.selectPageTypeDropdown.getValue();
this.pageNameInput.setPagePrefix(pageTypeConfig.prefix);
this.clearErrorWarnings();
} }
setError(msg) { makeMessageRaw(kind, html) {
this.inputGroup.setErrors([msg]); //提示页面已经存在 let $el = new OO.ui.MessageWidget({
this.hasError = true; type: kind,
inline: true,
label: html,
}).$element;
let $label = $el.find('.oo-ui-labelElement-label');
$label.html(html);
return $el;
} }
clearError() { setErrors(errorMessages) {
this.inputGroup.setErrors([]); errorMessages.forEach((errorMessage) => {
this.hasError = false; let $message = this.makeMessageRaw('error', errorMessage);
this.errorsContainer.append($message);
});
this.hasErrors = true;
this.refreshCreatePageBtnEnabled();
this.refreshIgnoreWarningsCheckbox();
} }
pageExists(title) { setWarnings(warningMessages) {
warningMessages.forEach((warningMessage) => {
let $message = this.makeMessageRaw('warning', warningMessage);
this.errorsContainer.append($message);
});
this.hasWarnings = true;
this.refreshCreatePageBtnEnabled();
this.refreshIgnoreWarningsCheckbox();
}
clearErrorWarnings() {
if (this.hasErrors || this.hasWarnings) {
this.errorsContainer.empty();
this.hasErrors = false;
this.hasWarnings = false;
this.refreshCreatePageBtnEnabled();
this.refreshIgnoreWarningsCheckbox();
}
}
refreshCreatePageBtnEnabled() {
this.createButtonEnabled = (() => {
if (this.hasErrors) {
return false;
}
if (this.hasWarnings) {
// 检查是否忽略警告
return this.ignoreWarningsCheckbox.isSelected();
}
return true;
})();
this.createButton.setDisabled(!this.createButtonEnabled);
}
refreshIgnoreWarningsCheckbox() {
if (this.hasWarnings && !this.hasErrors) {
this.ignoreWarningsCheckboxField.$element.show();
} else {
this.ignoreWarningsCheckboxField.$element.hide();
}
if (!this.hasWarnings && !this.hasErrors) {
// 没有警告和错误时,重置忽略警告复选框
this.ignoreWarningsCheckbox.setSelected(false);
}
}
validateNewPageTitle(title, pageType = 'default') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.api.get({ this.api.get({
action: 'query', action: 'isekaiwidgets',
titles: title, method: 'createpagevalidatetitle',
iwnewpagetitle: title,
iwnewpagetype: pageType,
}).done((data) => { }).done((data) => {
if (data.query && data.query.pages) { if (data.isekaiwidget && data.isekaiwidget.createpagevalidatetitle) {
if (data.query.pages["-1"]) { let result = data.isekaiwidget.createpagevalidatetitle;
resolve(false);
} else { if (!result.hasError && !result.hasWarning) {
resolve(true); resolve(true);
return;
} }
if (result.hasError) {
this.setErrors(result.errors.map(error => error.message));
}
if (result.hasWarning) {
this.setWarnings(result.warnings.map(warning => warning.message));
}
resolve(false);
} else { } else {
resolve(false); resolve(false);
} }
}).fail(reject); }).fail(reject);
}); });
} }
setTitle(title) {
this.title.text(title);
}
} }
registerModule('ui.CreatePageWidget', CreatePageWidget); registerModule('ui.CreatePageWidget', CreatePageWidget);
Loading…
Cancel
Save