В рунете я почти не встречал материалов о том, как писать расширения для 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')
. Сейчас этот способ считается основным, так что на нем и остановимся. http://personeltest.ru/aways/habr.com/ru/company/veeam/blog/544534
Второй способ подразумевает наличие в папке файла extension.json с описанием манифеста приложения (как оно называется, из чего состоит, какие хуки использует и так далее).
Определяем манифест (файл extension.json):
{"name": "GoogleKnowledgeGraph","version": "0.1.0","author": ["Nikolai Kochkin"],"url": "http://personeltest.ru/aways/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": "http://personeltest.ru/aways/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=jsonResponse 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 (файл поддерживает много дополнительных полей)