В рунете я почти не встречал материалов о том, как писать расширения для MediaWIki (платформы, на которой работает Википедия). Основной стартовой точкой при написании расширений был и остается официальный сайт платформы, но там процесс расписан не очень дружелюбно по отношению к новичкам. Попробуем же это исправить.
В этой статье я покажу, как написать простейшее расширение для Медиавики, включающее в себя новый метод API, расширение парсера и немного js/css для фронтенда. А чтобы не было скучно, приплетем сюда работу с Google Knowledge Graph.

Расширения MediaWiki
MediaWiki — модульная платформа, куда можно устанавливать расширения для добавления самого разного функционала. Помимо того, что расширения могут реализовывать какой-то свой независимый функционал (например, добавлять какие-нибудь виджеты), оно также может и модифицировать функциональность платформы: например, менять принцип работы поиска или модифицировать внешний вид платформы. Посмотреть примеры расширений можно на официальном сайте платформы.
Пишутся расширения, как правило, на php+jQuery. Возможность встраиваться в код ядра MediaWiki (или в код других расширений) реализована через т.н. хуки. Хуки позволяют вызывать дополнительный код по заданным событиям. Примерами таких событий могут быть: сохранение страницы, вызов поиска по сайту, открытие страницы на редактирование и так далее.
Расширения MediaWiki позволяют делать что угодно: работать напрямую с базой, модифицировать вики-движок, работать с файловой системой и так далее. С одной стороны, это позволяет добавлять какой угодно функционал, но с другой — накладывает на вас большую ответственность при установке новых сторонних расширений. Впрочем, довольно лирики, приступим к написанию своего расширения.
Что будем писать?
Готовое расширение можно взять тут:
https://github.com/Griboedow/GoogleKnowledgeGraph
Давайте развлечемся и напишем что-нибудь бесполезное. Скажем, расширение, которое будет вытаскивать описания с Google Knowledge Graph.
Т.е. расширение будет вот это:
Код этого приложения прост и изящен как <GoogleKnowledgeGraph query="Мэльхэнанвенанхытбельхын"/>
Превращать в это:

Штука довольно бесполезная, но она послужит хорошей иллюстрацией. Еще и с графом знаний Гугла поиграемся!
Расширение сделано исключительно в учебных целях, не рекомендую его использовать на настоящих вики. Гугл предоставляет 100 000 бесплатных запросов в день. Для небольших вики это не проблема, но на серьезных сайтах ресурс будет исчерпан очень быстро.
Как оно будет работать
Примерный принцип работы расширения выглядит так:
-
Пользователь сохраняет страницу, где в тексте присутствуют теги
<GoogleKnowledgeGraph query="Ричард Докинз">.-
MediaWIki позволяет использовать не только формат тега, но и формат функции парсера <link>:
{{#GoogleKnowledgeGraph||query=Ричард Докинз}}.
-
-
Расширение функции парсера превращает тег в html код
<span class="googleKnowledgeGraph">Ричард Докинз</span> -
JS код при загрузке страницы идет по всем элементам
.googleKnowledgeGraphи запрашивает через API нашего же расширения описания терминов, подставляя их в title. -
API нашего расширения будет максимально примитивным: он будет передавать запросы от фронтенда на Google API, чистить ответ от всего лишнего и передавать очищенное описание сущности на фронт.
В целом, можно было бы обойтись и без фронтенда, но запросы на внешние сайты могут проходить не очень быстро. Лучше показать основной контент страницы пораньше, и в фоне догрузить необязательный контент. К тому же мы тут учимся, а не пишем серьезный код, верно?
Итого, нам потребуется:
-
Определить манифест нашего расширения.
-
Расширить MediaWIki API, добавив запрос на получение описания из Google Knowledge Graph
-
Расширить парсер MediaWiki, добавив обработку нового тега.
-
Добавить JS код, который будет выполняться по загрузке страницы
-
Подгрузить наше расширение в MediaWiki
-
Поделиться результатом наших трудов с сообществом.
А еще перед началом работы вам потребуется токен для работы с Google Knowledge Graph API. Сгенерировать его можно тут.
Создаем структуру расширения
Типичная иерархия файлов и папок для MediaWIki расширения выглядит так:
extensions <-- Папка всех расширений MediaWiki └── GoogleKnowledgeGraph <-- Подпапка с нашим расширением ├── extension.json <-- Манифест нашего расширения ├── i18n <-- Каталог с используемыми строками для разных языков │ ├── en.json <-- Строки на английском │ ├── qqq.json <-- Описания строк для облегчения жизни переводчиков │ └── ru.json <-- Строки на русском ├── includes <-- PHP код │ ├── ApiGoogleKnowledgeGraph.php <-- Расширение API │ └── GoogleKnowledgeGraph.hooks.php <-- Расширение парсера и другие хуки └── modules <-- Папка с JS модулями └── ext.GoogleKnowledgeGraph <-- В нашем случае модуль только 1 ├── ext.GoogleKnowledgeGraph.css <-- CSS стили нашего модуля └── ext.GoogleKnowledgeGraph.js <-- JS код нашего модуля
Разберем содержимое всех файлов по порядку, и начнем с самого простого.
Интернационализация (i18n)
Для того, чтобы нашим расширением было удобно пользоваться на всех языках, можно воспользоваться стандартной системой интернационализации banana-i18n. Помимо облегчения интернационализации, эта система также позволяет хранить все тексты в одном месте (а не раскиданными по коду). Выглядит это примерно так:
qqq.json
{ "@metadata": { "authors": [ "Developer Name" ] }, "googleknowledgegraph-description": "Description of the extension, to be show in Special:Vesion.", "apihelp-askgoogleknowledgegraph-summary" : "Help string for 'askgoogleknowledgegraph' API request", "apihelp-askgoogleknowledgegraph-param-query": "Help string for 'query' parameter of API request 'askgoogleknowledgegraph'" }
en.json
{ "@metadata": { "authors": [ "Nikolai Kochkin" ] }, "googleknowledgegraph-description": "The extension gets brief description from Google Knowledge Graph", "apihelp-askgoogleknowledgegraph-summary" : "API to get description from Google Knowledge Graph", "apihelp-askgoogleknowledgegraph-param-query": "String to ask from Google Knowledge Graph" }
Создаем манифест расширения (extension.json)
Для начала разберемся, как нам сообщить MediaWiki, что нужно загрузить то или иное расширение. Путей на самом деле два:
-
Использовать
require_once( '/path/to/file.php' ). Этот метод считается устаревшим, так что мы его подробно не будем рассматривать. -
Использовать функцию
wfLoadExtension('ExtensionName'). Сейчас этот способ считается основным, так что на нем и остановимся. https://habr.com/ru/company/veeam/blog/544534
Второй способ подразумевает наличие в папке файла extension.json с описанием манифеста приложения (как оно называется, из чего состоит, какие хуки использует и так далее).
Определяем манифест (файл extension.json):
{ "name": "GoogleKnowledgeGraph", "version": "0.1.0", "author": [ "Nikolai Kochkin" ], "url": "https://habr.com/ru/company/veeam/blog/544534/", "descriptionmsg": "googleknowledgegraph-description", "license-name": "GPL-2.0-or-later", "type": "parserhook", "requires": { "MediaWiki": ">= 1.29.0" }, "MessagesDirs": { "GoogleKnowledgeGraph": [ "i18n" ] }, "AutoloadClasses": { "GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php", "ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php" }, "APIModules": { "askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph" }, "Hooks": { "OutputPageParserOutput": "GoogleKnowledgeGraphHooks::onBeforeHtmlAddedToOutput", "ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup" }, "ResourceFileModulePaths": { "localBasePath": "modules", "remoteExtPath": "GoogleKnowledgeGraph/modules" }, "ResourceModules": { "ext.GoogleKnowledgeGraph": { "localBasePath": "modules/ext.GoogleKnowledgeGraph", "remoteExtPath": "GoogleKnowledgeGraph/modules/ext.GoogleKnowledgeGraph", "scripts": [ "ext.GoogleKnowledgeGraph.js" ], "styles": [ "ext.GoogleKnowledgeGraph.css" ] } }, "config": { "GoogleApiLanguage": { "value": "ru", "path": false, "description": "In which language you want to get result from the Knowledge Graph", "public": true }, "GoogleApiToken": { "value": "", "path": false, "description": "API token to be used with Google API", "public": false } }, "ConfigRegistry": { "GoogleKnowledgeGraph": "GlobalVarConfig::newInstance" }, "manifest_version": 2 }
Разбираем extension.json по частям
Первая часть файла определяет то, что пользователь увидит в описании расширения на странице Special:Version
"name": "GoogleKnowledgeGraph", "version": "0.1.0", "author": [ "Nikolai Kochkin" ], "url": "https://habr.com/ru/company/veeam/blog/544534/", "descriptionmsg": "googleknowledgegraph-description", "license-name": "GPL-2.0-or-later", "type": "parserhook",

Далее мы указываем зависимости нашего расширения: с какими версиями MediaWIki расширение может работать, какие версии php требуются, какие расширения должны быть уже установлены и так далее.
"requires": { "MediaWiki": ">= 1.29.0" },
Затем мы указываем, где искать файлы со строками i18n
"MessagesDirs": { "GoogleKnowledgeGraph": [ "i18n" ] },
И сообщаем, в каких файлах искать классы для автоподгрузки. Подробнее тут.
"AutoloadClasses": { "GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php", "ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php" },
Заявляем, что мы реализовываем API метод askgoogleknowledgegraph в классе ApiAskGoogleKnowledgeGraph
"APIModules": { "askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph" },
Перечисляем, какие коллбеки для каких хуков у нас реализованы
"Hooks": { "BeforePageDisplay": "GoogleKnowledgeGraphHooks::onBeforePageDisplay", "ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup" },
Сообщаем, что модули наши лежат в папке modules
"ResourceFileModulePaths": { "localBasePath": "modules", "remoteExtPath": "GoogleKnowledgeGraph/modules" },
И определяем наш фронтенд модуль с js и css. Когда модулей несколько, можно указать в коде зависимости между ними.
"ResourceModules": { "ext.GoogleKnowledgeGraph": { "localBasePath": "modules/ext.GoogleKnowledgeGraph", "remoteExtPath": "GoogleKnowledgeGraph/modules/ext.GoogleKnowledgeGraph", "scripts": [ "ext.GoogleKnowledgeGraph.js" ], "styles": [ "ext.GoogleKnowledgeGraph.css" ] } },
И, наконец, задаем дополнительные параметры конфигурации нашего расширения
"config": { "GoogleApiLanguage": { "value": "ru", "path": false, "description": "In which language you want to get result from the Knowledge Graph", "public": true }, "GoogleApiToken": { "value": "", "path": false, "description": "API token to be used with Google API", "public": false } }, "ConfigRegistry": { "GoogleKnowledgeGraph": "GlobalVarConfig::newInstance" },
В LocalSettings.php опции будут иметь стандартный префикс wg
$wgGoogleApiToken = 'your-google-token'; $wgGoogleApiLanguage = 'ru';
И, наконец, задаем версию схемы манифеста
"manifest_version": 2
Мы используем лишь небольшой список поддерживаемых полей манифеста. Почитать обо всех полях можно тут.
Расширяем API
Для начала реализуем API.
В extension.json мы заявили, что у нас будет метод askgoogleknowledgegraph, реализованный в классе ApiAskGoogleKnowledgeGraph из файла includes/ApiAskGoogleKnowledgeGraph.php:
// extension.json fragment "AutoloadClasses": { <...> "ApiAskGoogleKnowledgeGraph": "includes/ApiAskGoogleKnowledgeGraph.php" }, "APIModules": { "askgoogleknowledgegraph": "ApiAskGoogleKnowledgeGraph" },
Теперь реализуем наш метод. Файл includes/ApiAskGoogleKnowledgeGraph.php:
<?php /** * Класс включает в себя реализацию и описание API метода askgoogleknowledgegraph * Для простоты я не реализую кеширование, любопытные могут подсмотреть реализацию тут: * https://github.com/wikimedia/mediawiki-extensions-TextExtracts/blob/master/includes/ApiQueryExtracts.php */ use MediaWiki\MediaWikiServices; class ApiAskGoogleKnowledgeGraph extends ApiBase { public function execute() { $params = $this->extractRequestParams(); // query - обязательный параметр, так что $params['query'] всегда определен $description = ApiAskGoogleKnowledgeGraph::getGknDescription( $params['query'] ); /** * Определяем результат для Get запроса. * На самом деле Post запрос отработает с тем же успехом, * если специально не отслеживать тип запроса ¯\_(ツ)_/¯. */ $this->getResult()->addValue( null, "description", $description ); } /** * Список поддерживаемых параметров метода */ public function getAllowedParams() { return [ 'query' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ] ]; } /** * Получаем данные из Google Knowledge Graph, * предполагая, что самый первый результат и есть верный. */ private static function getGknDescription( $query ) { /** * Вытаскиваем параметры языка и токен. * Все параметры в LocalSettings.php имеют префикс wg, например: wgGoogleApiToken. * Здесь же мы их указываем без префикса */ $config = MediaWikiServices::getInstance()->getConfigFactory()->makeConfig( 'GoogleKnowledgeGraph' ); $gkgToken = $config->get( 'GoogleApiToken' ); $gkgLang = $config->get( 'GoogleApiLanguage' ); $service_url = 'https://kgsearch.googleapis.com/v1/entities:search'; $params = [ 'query' => $query , 'limit' => 1, 'languages' => $gkgLang, 'indent' => TRUE, 'key' => $gkgToken, ]; $url = $service_url . '?' . http_build_query( $params ); $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $url) ; curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); $response = json_decode( curl_exec( $ch ), true ); curl_close( $ch ); if( count( $response['itemListElement'] ) == 0 ){ return "Nothing found by your request \"$query\""; } if( !isset( $response['itemListElement'][0]['result'] ) ){ return "Unknown GKG result format for request \"$query\""; } if( !isset($response['itemListElement'][0]['result']['detailedDescription'] ) ){ return "detailedDescription was not provided by GKG for request \"$query\""; } if( !isset( $response['itemListElement'][0]['result']['detailedDescription']['articleBody'] ) ){ return "articleBody was not provided by GKG for request \"$query\""; } return $response['itemListElement'][0]['result']['detailedDescription']['articleBody']; } }
Теперь мы можем обращаться по апи к нашей вики:
Get /api.php?action=askgoogleknowledgegraph&query=Выхухоль&format=json Response body: { "description": "Вы́хухоль, или русская выхухоль, или хоху́ля, — вид млекопитающих отряда насекомоядных из трибы Desmanini подсемейства Talpinae семейства кротовых. Один из двух видов трибы; вторым видом является пиренейская выхухоль." }
Расширяем парсер и используем прочие хуки
// Фрагмент файла extension.json "AutoloadClasses": { "GoogleKnowledgeGraphHooks": "includes/GoogleKnowledgeGraph.hooks.php", <...> }, "Hooks": { "BeforePageDisplay": "GoogleKnowledgeGraphHooks::onBeforePageDisplay", "ParserFirstCallInit": "GoogleKnowledgeGraphHooks::onParserSetup" },
В extension.json мы заявили, что в классе GoogleKnowledgeGraphHooks из файла includes/GoogleKnowledgeGraph.hooks.php реализуем расширения для хуков:
-
OutputPageParserOutput в методе
onBeforeHtmlAddedToOutput; -
ParserFirstCallInit в методе
onParserSetup
Немножко про используемые хуки:
-
OutputPageParserOutput позволяет выполнить какой-то код после того, как парсер закончил формировать html, но перед тем, как html был добавлен к аутпуту. Здесь мы, например, можем подгрузить фронтенд. Фронтенд мы целиком расположили в модуле
ext.GoogleKnowledgeGraph, так что достаточно будет подгрузить его. -
ParserFirstCallInit позволяет расширить парсер дополнительными методами. Мы добавим в парсер обработку тега
<GoogleKnowledgeGraph>.
Итак, реализация (файл includes/GoogleKnowledgeGraph.hooks.php):
<?php /** * Хуки расширения GoogleKnowledgeGraph */ class GoogleKnowledgeGraphHooks { /** * Сработает хук после окончания работы парсера, но перед выводом html. * Детали тут: https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput */ public static function onBeforeHtmlAddedToOutput( OutputPage &$out, ParserOutput $parserOutput ) { // Добавляем подгрузку модуля фронтенда для всех страниц, его определение ищи в extension.json $out->addModules( 'ext.GoogleKnowledgeGraph' ); return true; } /** * Расширяем парсер, добавляя обработку тега <GoogleKnowledgeGraphHooks> */ public static function onParserSetup( Parser $parser ) { $parser->setHook( 'GoogleKnowledgeGraph', 'GoogleKnowledgeGraphHooks::processGoogleKnowledgeGraphTag' ); return true; } /** * Реализация обработки тега <GoogleKnowledgeGraph> */ public static function processGoogleKnowledgeGraphTag( $input, array $args, Parser $parser, PPFrame $frame ) { // Парсим аргументы, переданные в формате <GoogleKnowledgeGraph arg1="val1" arg2="val2" ...> if( isset( $args['query'] ) ){ $query = $args['query']; } else{ // В тег не был передан аргумент query, так что и выводить нам нечего return ''; } return '<span class="googleKnowledgeGraph">' . htmlspecialchars( $query ) . '</span>'; } }
Добавляем фронтенд
Фронтенд свяжет воедино все, что мы реализовали выше.
// Фрагмент файла extension.json "ResourceModules": { "ext.GoogleKnowledgeGraph": { "localBasePath": "modules", "remoteExtPath": "GoogleKnowledgeGraph/modules", "scripts": [ "ext.GoogleKnowledgeGraph.js" ], "styles": [ "ext.GoogleKnowledgeGraph.css" ], "dependencies": [ ] } }, "ResourceFileModulePaths": { "localBasePath": "modules", "remoteExtPath": "GoogleKnowledgeGraph/modules" },
В extension.json мы заявили, что у нас есть один модуль ext.GoogleKnowledgeGraph, который находится в папке modules и состоит из двух файлов:
-
modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.js
-
modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.css
Загрузку модуля мы реализовали чуть раньше в методе onBeforeHtmlAddedToOutput. Определим теперь и сам код модуля.
Для начала зададим стили
(файл modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.css):
.googleKnowledgeGraph{ border-bottom: 1px dotted #000; text-decoration: none; }
А теперь возьмемся за JS
(файл modules/ext.GoogleKnowledgeGraph/ext.GoogleKnowledgeGraph.js):
( function ( mw, $ ) { /** * Ищем все элементы с <span class="googleKnowledgeGraph">MyText</span>, * вытаскиваем MyText и отправляем запрос * /api.php?action=askgoogleknowledgegraph&query=MyText * После чего добавляем результат в 'title'. */ $( ".googleKnowledgeGraph" ).each( function( index, element ) { $.ajax({ type: "GET", url: mw.util.wikiScript( 'api' ), data: { action: 'askgoogleknowledgegraph', query: $( element ).text(), format: 'json', }, dataType: 'json', success: function( jsondata ){ $( element ).prop( 'title', jsondata.description ); } }); }); }( mediaWiki, jQuery ) );
JS код довольно прост. jQuery нам достался даром, поскольку MediaWiki подгружает его автоматически.
Подгружаем наше расширение и радуемся
Для загрузки расширения, как мы уже обсуждали, потребуется поправить файл LocalSettings.php. Добавляем в самый конец:
// Фрагмент файла LocalSettings.php <?php <...> wfLoadExtension( 'GoogleKnowledgeGraph' ); $wgGoogleApiToken = "your-google-token"; $wgGoogleApiLanguage = 'ru';
Можно пробовать! Добавим на страницу что-нибудь эдакое:
Даже <GoogleKnowledgeGraph query="прикольный флот"/> может стать отстойным.
И получим:

Делимся с сообществом
Если есть возможность поделиться расширением с общественностью, то можно создать страницу на MediaWiki с кратким описанием, что ваше расширение может сделать (не забудьте скриншоты: лучше один раз увидеть, чем сто раз прочитать). На страницы с описаниями расширений обычно добавляют шаблон Extension, поля которого хорошо задокументированы. Если же возникнут сложности, всегда можно скопировать его с другой страницы расширений и подправить отличающиеся поля.
Заключение
В статье был описан случай довольно простого расширения, но, на самом деле, такие расширения как iFrame, CategoryTree, Drawio и многие другие не очень далеко ушли по сложности.
За скобками остались такие вещи, как работа с базой, кэширование, OOUI и много-многое другое. Все ж я вас не напугать хотел, а как раз наоборот — показать, что писать расширения под вики на самом деле совсем не сложно и не страшно.
Ссылки
-
Страница помощи разработчику расширений MediaWIki
-
Example extension — расширение с пачкой примеров на все случаи жизни
-
banana-i18n (как работает интернационализация)
-
Схема extension.json (файл поддерживает много дополнительных полей)
ссылка на оригинал статьи https://habr.com/ru/company/veeam/blog/544534/
Добавить комментарий