
理解 MultiContentSave 钩子
multicontentsave 钩子是mediawiki提供的一个关键扩展点,它在页面内容被保存(无论是新建、编辑还是回退)之后触发。这个钩子为开发者提供了在内容持久化到数据库后,对页面内容进行进一步处理、分析或记录的机会。
钩子的签名如下:
public static function onMultiContentSave(
RenderedRevision $renderedRevision,
UserIdentity $user,
CommentStoreComment $summary,
$flags,
Status $hookStatus
)其中,$renderedRevision 参数是核心,它包含了当前保存的修订版本的所有信息,包括新内容。
获取编辑后的新内容
获取页面编辑后的新内容相对直接。$renderedRevision 对象封装了当前保存的修订版本 (Revision)。我们可以通过它来访问新内容。
use MediaWiki\Revision\RenderedRevision;
use MediaWiki\Revision\RevisionRecord;
use Wikimedia\Content\SlotRecord;
class MyExtensionHooks {
public static function onMultiContentSave(
RenderedRevision $renderedRevision,
UserIdentity $user,
CommentStoreComment $summary,
$flags,
Status $hookStatus
) {
// 获取当前修订版本对象
$revision = $renderedRevision->getRevision();
// 获取页面的 LinkTarget 对象(包含页面标题信息,非直接字符串)
$title = $revision->getPageAsLinkTarget();
// 获取编辑后的新内容
// SlotRecord::MAIN 表示主内容槽
// RevisionRecord::RAW 表示获取原始维基文本内容
$new_content_object = $revision->getContent(SlotRecord::MAIN, RevisionRecord::RAW);
$new_content_text = $new_content_object ? $new_content_object->getNativeData() : '';
// $new_content_text 现在包含了编辑后的页面内容
return true;
}
}上述代码中,$revision->getContent(SlotRecord::MAIN, RevisionRecord::RAW) 返回一个 Content 对象。通过调用 $new_content_object->getNativeData(),我们可以获取到其原始的字符串表示(通常是维基文本)。
获取编辑前的旧内容
获取编辑前的旧内容是本教程的核心挑战。MediaWiki的修订版本系统通过父子关系来跟踪历史。当前修订版本对象 ($revision) 包含了对其父版本(即编辑前的版本)的引用。
以下是获取旧内容的步骤:
-
获取父版本ID: 每个修订版本都有一个父版本ID,指向它所基于的上一版本。我们可以通过 $revision->getParentId() 方法获取这个ID。
- 如果页面是首次创建,getParentId() 将返回 0,表示没有父版本。
- 如果父版本未知或未定义,它可能返回 null。
-
通过ID加载前一个修订版本: 一旦我们有了父版本ID,就可以使用 MediaWiki\Revision\RevisionStore 服务来加载对应的修订版本对象。RevisionStore::getRevisionById() 方法可以根据ID检索修订版本。
- 如果找不到对应的修订版本,此方法将返回 null。
从前一个修订版本中提取内容: 加载到前一个修订版本对象后,获取其内容的方式与获取新内容类似。
下面是获取旧内容的具体实现:
use MediaWiki\Revision\RenderedRevision;
use MediaWiki\WikiPage\Revision\RevisionStore; // 确保引入正确的 RevisionStore
use MediaWiki\Revision\RevisionRecord;
use Wikimedia\Content\SlotRecord;
use MediaWiki\Content\ContentHandler; // 用于将Content对象转换为文本
class MyExtensionHooks {
public static function onMultiContentSave(
RenderedRevision $renderedRevision,
UserIdentity $user,
CommentStoreComment $summary,
$flags,
Status $hookStatus
) {
$revision = $renderedRevision->getRevision();
$title = $revision->getPageAsLinkTarget(); // 获取页面的 LinkTarget 对象
// 获取编辑后的新内容
$new_content_object = $revision->getContent(SlotRecord::MAIN, RevisionRecord::RAW);
$new_content_text = $new_content_object ? $new_content_object->getNativeData() : '';
// --- 获取编辑前的旧内容 ---
$old_content_text = ''; // 初始化旧内容为空字符串
// 1. 获取父版本ID
$parent_id = $revision->getParentId();
// 2. 检查是否存在父版本(即不是首次创建)
if ($parent_id !== 0 && $parent_id !== null) {
// 3. 通过父版本ID加载前一个修订版本
// RevisionStore 是一个服务,通常通过 MediaWiki\MediaWikiServices::getInstance()->getRevisionStore() 获取
// 但在钩子环境中,可以直接访问静态方法 RevisionStore::getRevisionById()
$previous_revision = RevisionStore::getRevisionById($parent_id);
// 4. 检查是否成功加载到前一个修订版本
if ($previous_revision) {
// 5. 从前一个修订版本中提取内容
// Revision::RAW 获取原始内容
$old_content_object = $previous_revision->getContent(SlotRecord::MAIN, RevisionRecord::RAW);
// 6. 将内容对象转换为文本
// ContentHandler::getContentText() 是一个通用的转换方法
$old_content_text = $old_content_object ? ContentHandler::getContentText($old_content_object) : '';
}
}
// $old_content_text 现在包含了编辑前的页面内容
// ... 在这里可以对 $new_content_text 和 $old_content_text 进行比较或处理
return true;
}
}完整示例:比较编辑前后内容
将新旧内容获取逻辑整合在一起,可以方便地进行内容比较,例如实现一个简单的内容差异检测或审计功能。
getRevision();
$pageTitle = $currentRevision->getPageAsLinkTarget()->getText(); // 获取页面标题字符串
// --- 获取编辑后的新内容 ---
$newContentObject = $currentRevision->getContent(SlotRecord::MAIN, RevisionRecord::RAW);
$newContentText = $newContentObject ? ContentHandler::getContentText($newContentObject) : '';
// --- 获取编辑前的旧内容 ---
$oldContentText = ''; // 默认旧内容为空
$parentId = $currentRevision->getParentId();
// 检查是否存在父版本 (即不是首次创建页面)
if ($parentId !== 0 && $parentId !== null) {
// 通过父版本ID加载前一个修订版本
$previousRevision = RevisionStore::getRevisionById($parentId);
if ($previousRevision) {
// 从前一个修订版本中提取内容
$oldContentObject = $previousRevision->getContent(SlotRecord::MAIN, RevisionRecord::RAW);
$oldContentText = $oldContentObject ? ContentHandler::getContentText($oldContentObject) : '';
} else {
// 无法加载到前一个修订版本 (可能ID无效或已删除)
wfDebugLog('ContentComparisonExtension', "Warning: Could not load previous revision with ID $parentId for page $pageTitle.");
}
} else {
// 这是页面的首次创建,没有旧内容
wfDebugLog('ContentComparisonExtension', "Page '$pageTitle' was created. No old content to compare.");
}
// --- 进行内容比较或进一步处理 ---
if ($oldContentText !== $newContentText) {
// 内容发生了变化
wfDebugLog('ContentComparisonExtension', "Page '$pageTitle' content changed.");
// 可以在此处执行差异分析、记录日志、发送通知等操作
// 例如:
// $diff = new \MediaWiki\Diff\TextDiffer();
// $changes = $diff->getDiff($oldContentText, $newContentText);
// ...
} else {
// 内容没有变化 (可能只是保存了空编辑或元数据编辑)
wfDebugLog('ContentComparisonExtension', "Page '$pageTitle' content unchanged.");
}
return true; // 总是返回 true,除非你想阻止保存操作
}
}在 extension.json 中注册钩子:
{
"name": "ContentComparisonExtension",
"version": "1.0.0",
"AutoloadClasses": {
"ContentComparisonExtensionHooks": "ContentComparisonExtensionHooks.php"
},
"Hooks": {
"MultiContentSave": [
"ContentComparisonExtensionHooks::onMultiContentSave"
]
},
"manifest_version": 2
}注意事项与最佳实践
-
错误处理与空值检查:
- $revision->getParentId() 可能返回 0 或 null。在尝试加载前一个修订版本之前,务必进行检查。
- RevisionStore::getRevisionById() 可能返回 null。在尝试访问 null 对象的属性之前,也应进行检查。
- getContent() 方法返回的 Content 对象也可能为 null,因此在调用 getNativeData() 或 ContentHandler::getContentText() 之前进行检查。
-
性能考虑:
- 频繁地加载旧版本内容可能会对大型Wiki的性能产生影响,尤其是在高流量或频繁编辑的页面上。如果只需要简单的标记或审计,可以考虑只存储哈希值进行比较,而不是加载完整内容。
-
内容格式:
- RevisionRecord::RAW 用于获取原始的维基文本。如果页面内容是其他类型(例如JSON或自定义内容模型),可能需要使用不同的 SlotRecord 或 RevisionRecord 常量,并使用相应的 ContentHandler 方法进行处理。
- SlotRecord::MAIN 用于获取页面的主要内容槽。MediaWiki 5.x 版本引入了多内容槽机制,如果你的扩展需要处理非主内容槽的内容,需要指定相应的 SlotRecord 常量。
-
钩子返回值:
- MultiContentSave 钩子通常应返回 true,表示钩子已成功处理且不应阻止后续操作。如果返回 false,可能会中断MediaWiki的正常保存流程。
总结
通过本教程,我们详细学习了如何在MediaWiki扩展的MultiContentSave钩子中,有效地获取页面编辑前后的内容。关键在于利用RenderedRevision对象获取当前修订版本,并通过其getParentId()方法和RevisionStore服务来检索并访问前一个修订版本的内容。掌握这些技术,开发者可以构建出功能强大、能够深入分析和处理页面内容变化的MediaWiki扩展。在实际开发中,务必注意错误处理、性能优化以及内容格式的兼容性。










