01tonythomas has uploaded a new change for review.
https://gerrit.wikimedia.org/r/304692
Change subject: Introduce ContentHandler on the Newsletter CustomEditpage
......................................................................
Introduce ContentHandler on the Newsletter CustomEditpage
* Added in the contenthandler model for Newsletter namespace
* action=edit, now creates a Newsletter with the Newsletter Contentmodel
Bug: T138462
Change-Id: I89809a4ec1b524148199ff5d11ee4e96ae716919
---
M Newsletter.hooks.php
M extension.json
M includes/Newsletter.php
M includes/NewsletterDb.php
M includes/NewsletterEditPage.php
M includes/NewsletterStore.php
A includes/content/NewsletterContent.php
A includes/content/NewsletterContentHandler.php
8 files changed, 605 insertions(+), 3 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Newsletter
refs/changes/92/304692/1
diff --git a/Newsletter.hooks.php b/Newsletter.hooks.php
index 0f4f747..0076798 100755
--- a/Newsletter.hooks.php
+++ b/Newsletter.hooks.php
@@ -129,6 +129,31 @@
return true;
}
+ /**
+ * @param EditPage $editPage
+ */
+ public static function onAlternateEdit( EditPage $editPage ) {
+ global $wgOut;
+ $title = $editPage->getTitle();
+
+ if ( $title->inNamespace( NS_NEWSLETTER ) ) {
+ if ( $title->hasContentModel( 'NewsletterContent' ) ) {
+ $newsletter = Newsletter::newFromName(
$title->getText() );
+ if ( $newsletter ) {
+ $title = SpecialPage::getTitleFor(
'Newsletter', $newsletter->getId() . '/' .
+ 'manage' );
+ $wgOut->redirect( $title->getFullURL()
);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param Article $article
+ * @param User $user
+ * @return bool
+ * @throws ReadOnlyError
+ */
public static function onCustomEditor( Article $article, User $user ) {
if ( !$article->getTitle()->inNamespace( NS_NEWSLETTER ) ) {
return true;
@@ -136,7 +161,6 @@
$editPage = new NewsletterEditPage( $article->getContext() );
$editPage->edit();
-
return false;
}
}
diff --git a/extension.json b/extension.json
index 775f2ee..383e95d 100644
--- a/extension.json
+++ b/extension.json
@@ -63,6 +63,9 @@
"ExtensionMessagesFiles": {
"NewsletterAlias": "Newsletter.alias.php"
},
+ "ContentHandlers": {
+ "NewsletterContent": "NewsletterContentHandler"
+ },
"AutoloadClasses": {
"Newsletter": "includes/Newsletter.php",
"NewsletterDb": "includes/NewsletterDb.php",
@@ -78,6 +81,8 @@
"NewsletterTablePager":
"includes/specials/pagers/NewsletterTablePager.php",
"ApiNewsletterManage": "includes/api/ApiNewsletterManage.php",
"ApiNewsletterSubscribe":
"includes/api/ApiNewsletterSubscribe.php",
+ "NewsletterContent": "includes/content/NewsletterContent.php",
+ "NewsletterContentHandler":
"includes/content/NewsletterContentHandler.php",
"EchoNewsletterUserLocator":
"includes/Echo/EchoNewsletterUserLocator.php",
"EchoNewsletterFormatter":
"includes/Echo/EchoNewsletterFormatter.php",
"BaseNewsletterPresentationModel":
"includes/Echo/BaseNewsletterPresentationModel.php",
@@ -145,6 +150,9 @@
"UserMergeAccountFields": [
"NewsletterHooks::onUserMergeAccountFields"
],
+ "AlternateEdit": [
+ "NewsletterHooks::onAlternateEdit"
+ ],
"CustomEditor": [
"NewsletterHooks::onCustomEditor"
]
@@ -154,7 +162,8 @@
"id": 5500,
"constant": "NS_NEWSLETTER",
"name": "Newsletter",
- "protection": "newsletter-create"
+ "protection": "newsletter-create",
+ "defaultcontentmodel": "NewsletterContent"
},
{
"id": 5501,
diff --git a/includes/Newsletter.php b/includes/Newsletter.php
index 26dbd74..a630511 100644
--- a/includes/Newsletter.php
+++ b/includes/Newsletter.php
@@ -62,6 +62,16 @@
}
/**
+ * Fetch a new newsletter instance from given name
+ *
+ * @param string $name
+ * @return Newsletter|null
+ */
+ public static function newFromName( $name ) {
+ return
NewsletterStore::getDefaultInstance()->getNewsletterFromName( $name );
+ }
+
+ /**
* @return int|null
*/
public function getId() {
diff --git a/includes/NewsletterDb.php b/includes/NewsletterDb.php
index cac547a..404bcf7 100644
--- a/includes/NewsletterDb.php
+++ b/includes/NewsletterDb.php
@@ -263,6 +263,30 @@
return $this->getNewsletterFromRow( $res->current() );
}
+ /**
+ * Fetch the newsletter matching the given name from the DB
+ *
+ * @param string $name
+ * @return Newsletter|null
+ */
+ public function getNewsletterFromName( $name ) {
+ Assert::parameterType( 'string', $name, '$name' );
+
+ $dbr = $this->lb->getConnection( DB_SLAVE );
+ $res = $dbr->select(
+ 'nl_newsletters',
+ array( 'nl_id', 'nl_name', 'nl_desc', 'nl_main_page_id'
),
+ array( 'nl_name' => $name, 'nl_active' => 1 ),
+ __METHOD__
+ );
+ $this->lb->reuseConnection( $dbr );
+
+ if ( $res->numRows() === 0 ) {
+ return null;
+ }
+
+ return $this->getNewsletterFromRow( $res->current() );
+ }
/**
* @param int $id
diff --git a/includes/NewsletterEditPage.php b/includes/NewsletterEditPage.php
index 43f196c..4077130 100644
--- a/includes/NewsletterEditPage.php
+++ b/includes/NewsletterEditPage.php
@@ -163,8 +163,18 @@
$mainPageId
);
$newsletterCreated = $store->addNewsletter( $this->newsletter );
+ $title = Title::makeTitleSafe( NS_NEWSLETTER, trim(
$data['Name'] ) );
+ $editSummaryMsg = $this->context->msg(
'newsletter-create-editsummary' );
+ $result = NewsletterContentHandler::edit(
+ $title,
+ $data['Description'],
+ $input['mainpage'],
+ $this->user->getName(),
+ $editSummaryMsg->inContentLanguage()->plain(),
+ $this->context
+ );
- if ( $newsletterCreated ) {
+ if ( $newsletterCreated && $result->isGood() ) {
$this->newsletter->subscribe( $this->user );
NewsletterStore::getDefaultInstance()->addPublisher(
$this->newsletter, $this->user );
diff --git a/includes/NewsletterStore.php b/includes/NewsletterStore.php
index 1605fb8..20e2dae 100644
--- a/includes/NewsletterStore.php
+++ b/includes/NewsletterStore.php
@@ -153,6 +153,13 @@
return $this->db->getNewsletter( $id );
}
+ /**
+ * @param string $name
+ * @return Newsletter|null
+ */
+ public function getNewsletterFromName( $name ) {
+ return $this->db->getNewsletterFromName( $name );
+ }
/**
* @param int $id
diff --git a/includes/content/NewsletterContent.php
b/includes/content/NewsletterContent.php
new file mode 100644
index 0000000..27d5a07
--- /dev/null
+++ b/includes/content/NewsletterContent.php
@@ -0,0 +1,426 @@
+<?php
+/**
+ * @license GNU GPL v2+
+ * @author tonythomas
+ */
+
+class NewsletterContent extends JsonContent {
+ /** Subpage actions */
+ const NEWSLETTER_ANNOUNCE = 'announce';
+ const NEWSLETTER_DELETE = 'delete';
+ const NEWSLETTER_MANAGE = 'manage';
+ const NEWSLETTER_SUBSCRIBE = 'subscribe';
+ const NEWSLETTER_UNSUBSCRIBE = 'unsubscribe';
+
+ /**
+ * @var string
+ */
+ private $description;
+
+ /**
+ * @var string
+ */
+ private $mainPage;
+
+ /**
+ * @var Newsletter
+ */
+ private $newsletter;
+
+ /**
+ * @var array
+ */
+ protected $publishers;
+
+ /**
+ * Whether $description and $targets have been populated
+ * @var bool
+ */
+ private $decoded = false;
+
+ /**
+ * @param string $text
+ */
+ public function __construct( $text ) {
+ parent::__construct( $text, 'NewsletterContent' );
+ }
+
+ /**
+ * @return bool
+ */
+ public function isValid() {
+ $this->decode();
+
+ if ( !is_string( $this->description ) || !is_string(
$this->mainPage ) ) {
+ return false;
+ }
+
+ if ( !Title::newFromText( $this->mainPage ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Decode the JSON encoded args
+ */
+ protected function decode() {
+ if ( $this->decoded ) {
+ return true;
+ }
+ $jsonParse = $this->getData();
+ $data = $jsonParse->isGood() ? $jsonParse->getValue() : null;
+ if ( $data ) {
+ $this->description = isset( $data->description ) ?
$data->description : null;
+ $this->mainPage = isset( $data->mainpage ) ?
$data->mainpage : null;
+ }
+
+ $lines = explode( "\n", $data->publishers );
+ // Strip whitespace, then remove blank lines and duplicates
+ $lines = array_unique( array_filter( array_map( 'trim', $lines
) ) );
+
+ // Ask for confirmation before removing all the publishers
+ if ( count( $lines ) === 0 ) {
+ return Status::newFatal(
'newsletter-manage-no-publishers' );
+ }
+
+ $publishers = [];
+ /** @var User[] $newPublishers */
+ foreach ( $lines as $publisherName ) {
+ $user = User::newFromName( $publisherName );
+ if ( !$user || !$user->getId() ) {
+ // Input contains an invalid username
+ return Status::newFatal(
'newsletter-manage-invalid-publisher', $publisherName );
+ }
+ $publishers[] = $user->getId();
+ }
+
+ $this->publishers = UserArray::newFromIDs( $publishers );
+ $this->decoded = true;
+
+ return true;
+ }
+
+ public function onSuccess() {
+ // No-op: We have already redirected.
+ }
+
+ protected function fillParserOutput( Title $title, $revId,
ParserOptions $options, $generateHtml, ParserOutput &$output ) {
+ if ( $generateHtml ) {
+ $this->newsletter = Newsletter::newFromName(
$title->getText() );
+ $user = $options->getUser();
+
+ $newsletterActionButtons = '';
+
+ if ( $user->isLoggedIn() ) {
+ // buttons are only shown for logged-in users
+ $newsletterActionButtons =
$this->getNewsletterActionButtons( $options );
+ }
+
+ $mainTitle = Title::newFromText( $this->mainPage );
+
+ $fields = array(
+ 'name' => array(
+ 'type' => 'info',
+ 'label-message' =>
'newsletter-view-name',
+ 'default' =>
$this->newsletter->getName(),
+ ),
+ 'mainpage' => array(
+ 'type' => 'info',
+ 'label-message' =>
'newsletter-view-mainpage',
+ 'default' => Linker::link( $mainTitle ),
+ 'raw' => true,
+ ),
+ 'description' => array(
+ 'type' => 'info',
+ 'label-message' =>
'newsletter-view-description',
+ 'default' => $this->description,
+ 'rows' => 6,
+ 'readonly' => true,
+ ),
+ 'publishers' => array(
+ 'type' => 'info',
+ 'label' => wfMessage(
'newsletter-view-publishers' )->inLanguage(
+ $options->getUserLangObj() )
+ ->numParams( count(
$this->publishers ) )
+ ->parse(),
+ ),
+ 'subscribers' => array(
+ 'type' => 'info',
+ 'label-message' =>
'newsletter-view-subscriber-count',
+ 'default' =>
$options->getUserLangObj()->formatNum( $this->newsletter->getSubscriberCount()
),
+ ),
+ );
+ if ( count( $this->publishers ) > 0 ) {
+ // Have this here to avoid calling unneeded
functions
+ $this->doLinkCacheQuery( $this->publishers );
+ $fields['publishers']['default'] =
$this->buildUserList( $this->publishers );
+ $fields['publishers']['raw'] = true;
+ } else {
+ // Show a message if there are no publishers
instead of nothing
+ $fields['publishers']['default'] = wfMessage(
'newsletter-view-no-publishers' )
+ ->inLanguage(
$options->getUserLangObj() )
+ ->escaped();
+ }
+ // Show the 10 most recent issues if there have been
announcements
+ $logs = '';
+ $logCount = LogEventsList::showLogExtract(
+ $logs, // by reference
+ 'newsletter',
+ SpecialPage::getTitleFor( 'Newsletter',
$this->newsletter->getId() ),
+ '',
+ array(
+ 'lim' => 10,
+ 'showIfEmpty' => false,
+ 'conds' => array( 'log_action' =>
'issue-added' ),
+ 'extraUrlParams' => array( 'subtype' =>
'issue-added' ),
+ )
+ );
+ if ( $logCount !== 0 ) {
+ $fields['issues'] = array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $logs,
+ 'label' => wfMessage(
'newsletter-view-issues-log' )
+ ->inLanguage(
$options->getUserLangObj() )
+ ->numParams( $logCount
)->escaped(),
+ );
+ }
+ $form = $this->getHTMLForm(
+ $fields,
+ function() {
+ return false;
+ } // nothing to submit - the buttons on this
page are just links
+ );
+
+ $form->suppressDefaultSubmit();
+ $form->prepareForm();
+
+ if ( $options->getUser()->isLoggedIn() ) {
+ $output->setText( $this->getNavigationLinks(
$options ) . $newsletterActionButtons .
+ "<br><br>" . $form->getBody() );
+ } else {
+ $output->setText( $this->getNavigationLinks(
$options ) . $form->getBody() );
+ }
+ return $output;
+ }
+ }
+
+ /**
+ * Create a common HTMLForm which can be used by specific page actions
+ *
+ * @param array $fields array of form fields
+ * @param callback $submit submit callback
+ *
+ * @return HTMLForm
+ */
+ private function getHTMLForm( array $fields, /* callable */ $submit ) {
+ global $wgOut;
+ $form = HTMLForm::factory(
+ 'ooui',
+ $fields,
+ $wgOut->getContext()
+ );
+ $form->setSubmitCallback( $submit );
+ return $form;
+ }
+
+ /**
+ * Build a group of buttons: Delete, Manage, Subscribe|Unsubscribe
+ * Buttons will be showed to the user only if they are relevant to the
current user.
+ *
+ * @return string HTML for the button group
+ */
+ protected function getNewsletterActionButtons( ParserOptions &$options
) {
+ global $wgOut;
+
+ $user = $options->getUser();
+ $id = $this->newsletter->getId();
+ $buttons = array();
+ $wgOut->enableOOUI();
+
+ if ( $this->newsletter->canManage( $user ) ) {
+ $buttons[] = new OOUI\ButtonWidget(
+ array(
+ 'label' => $wgOut->msg(
'newsletter-manage-button' )->escaped(),
+ 'icon' => 'settings',
+ 'href' => SpecialPage::getTitleFor(
'Newsletter', $id. '/' .
+ self::NEWSLETTER_MANAGE
)->getFullURL()
+
+ )
+ );
+ }
+ if ( $this->newsletter->isPublisher( $user ) ) {
+ $buttons[] = new OOUI\ButtonWidget(
+ array(
+ 'label' => $wgOut->msg(
'newsletter-announce-button' )->escaped(),
+ 'icon' => 'comment',
+ 'href' => SpecialPage::getTitleFor(
'Newsletter', $id. '/' .
+ self::NEWSLETTER_ANNOUNCE
)->getFullURL()
+ )
+ );
+ }
+ if ( $this->newsletter->isSubscribed( $user ) ) {
+ $buttons[] = new OOUI\ButtonWidget(
+ array(
+ 'label' => $wgOut->msg(
'newsletter-unsubscribe-button' )->escaped(),
+ 'flags' => array( 'destructive' ),
+ 'href' => SpecialPage::getTitleFor(
'Newsletter', $id. '/' .
+ self::NEWSLETTER_UNSUBSCRIBE
)->getFullURL()
+
+ )
+ );
+ } else {
+ $buttons[] = new OOUI\ButtonWidget(
+ array(
+ 'label' => $wgOut->msg(
'newsletter-subscribe-button' )->escaped(),
+ 'flags' => array( 'constructive' ),
+ 'href' => SpecialPage::getTitleFor(
'Newsletter', $id. '/' .
+ self::NEWSLETTER_SUBSCRIBE
)->getFullURL()
+
+ )
+ );
+ }
+ $widget = new OOUI\ButtonGroupWidget( array( 'items' =>
$buttons ) );
+ return $widget->toString();
+ }
+
+ /**
+ * Batch query to determine whether user pages and user talk pages exist
+ * or not and add them to LinkCache
+ *
+ * @param Iterator $users
+ *
+ * @return string
+ */
+ private function doLinkCacheQuery( Iterator $users ) {
+ $batch = new LinkBatch();
+ foreach ( $users as $user ) {
+ $batch->addObj( $user->getUserPage() );
+ $batch->addObj( $user->getTalkPage() );
+ }
+ $batch->execute();
+ }
+
+ /**
+ * Get a list of users with user-related links next to each username
+ *
+ * @param Iterator $users
+ *
+ * @return string
+ */
+ private function buildUserList( Iterator $users ) {
+ $str = '';
+ foreach ( $users as $user ) {
+ $str .= Html::rawElement(
+ 'li',
+ array(),
+ Linker::userLink( $user->getId(),
$user->getName() ) .
+ Linker::userToolLinks( $user->getId(),
$user->getName() )
+ );
+ }
+ return Html::rawElement( 'ul', array(), $str );
+ }
+
+ protected function getNavigationLinks( ParserOptions $options ) {
+ global $wgOut;
+ $listLink = Linker::linkKnown(
+ SpecialPage::getTitleFor( 'Newsletters' ),
+ wfMessage( 'backlinksubtitle',
+ wfMessage( 'newsletter-subtitlelinks-list'
)->text()
+ )->escaped()
+ );
+
+ $user = $options->getUser();
+ $actions = array();
+ if ( $user->isLoggedIn() ) {
+ $actions[] = $this->newsletter->isSubscribed( $user ) ?
self::NEWSLETTER_UNSUBSCRIBE : self::NEWSLETTER_SUBSCRIBE;
+
+ if ( $this->newsletter->isPublisher( $user ) ) {
+ $actions[] = self::NEWSLETTER_ANNOUNCE;
+ }
+ if ( $this->newsletter->canManage( $user ) ) {
+ $actions[] = self::NEWSLETTER_MANAGE;
+ }
+
+ $links = array();
+ foreach ( $actions as $action ) {
+ $title = SpecialPage::getTitleFor(
'Newsletter', $this->newsletter->getId() . '/' . $action );
+ // Messages used here:
'newsletter-subtitlelinks-announce',
+ // 'newsletter-subtitlelinks-subscribe',
'newsletter-subtitlelinks-unsubscribe'
+ // 'newsletter-subtitlelinks-manage'
+ $msg = wfMessage( 'newsletter-subtitlelinks-' .
$action )->escaped();
+ $links[] = Linker::linkKnown( $title, $msg );
+ }
+
+ $newsletterLinks = Linker::makeSelfLinkObj(
SpecialPage::getTitleFor( 'Newsletter', $this->newsletter->getId() ),
$this->getEscapedName() ) . ' ' . wfMessage( 'parentheses' )->rawParams(
$options->getUserLangObj()->pipeList( $links ) )->escaped();
+ } else {
+ $newsletterLinks = Linker::makeSelfLinkObj(
SpecialPage::getTitleFor( 'Newsletter', $this->newsletter->getId() ),
$this->getEscapedName() );
+ }
+
+ return $wgOut->setSubtitle(
$options->getUserLangObj()->pipeList( array( $listLink, $newsletterLinks ) ) );
+ }
+
+ /**
+ * @param WikiPage $page
+ * @param ParserOutput|null $parserOutput
+ * @return LinksDeletionUpdate[]
+ */
+ public function getDeletionUpdates( WikiPage $page, ParserOutput
$parserOutput = null ) {
+ return array_merge(
+ parent::getDeletionUpdates( $page, $parserOutput ),
+ array( new NewsletterDeletionUpdate(
$page->getTitle()->getText() ) )
+ );
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription() {
+ $this->decode();
+ return $this->description;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMainPage() {
+ $this->decode();
+ return $this->mainPage;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPublishers() {
+ $this->decode();
+ return $this->publishers;
+ }
+
+ /**
+ * We need the escaped newsletter name several times so
+ * extract the method here.
+ *
+ * @return string
+ */
+ protected function getEscapedName() {
+ return htmlspecialchars( $this->newsletter->getName() );
+ }
+
+ /**
+ * Override TextContent::getTextForSummary
+ * @param int $maxLength
+ * @return string
+ */
+ public function getTextForSummary( $maxLength = 250 ) {
+ global $wgContLang;
+
+ $truncatedtext = $wgContLang->truncate(
+ preg_replace( "/[\n\r]/", ' ', $this->getDescription()
)
+ , max( 0, $maxLength )
+ );
+
+ return $truncatedtext;
+ }
+}
\ No newline at end of file
diff --git a/includes/content/NewsletterContentHandler.php
b/includes/content/NewsletterContentHandler.php
new file mode 100644
index 0000000..5acffc2
--- /dev/null
+++ b/includes/content/NewsletterContentHandler.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * @license GNU GPL v2+
+ * @author tonythomas
+ */
+
+class NewsletterContentHandler extends JsonContentHandler {
+ /**
+ * @param string $modelId
+ */
+ public function __construct( $modelId = 'NewsletterContent' ) {
+ parent::__construct( $modelId );
+ }
+
+ /**
+ * @param string $text
+ * @param string $format
+ * @return MassMessageListContent
+ * @throws MWContentSerializationException
+ */
+ public function unserializeContent( $text, $format = null ) {
+ $this->checkFormat( $format );
+ $content = new NewsletterContent( $text );
+ if ( !$content->isValid() ) {
+ throw new MWContentSerializationException( 'The
delivery list content is invalid.' );
+ }
+ return $content;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getContentClass() {
+ return 'NewsletterContent';
+ }
+
+ /**
+ * @return bool
+ */
+ public function isParserCacheSupported() {
+ return false;
+ }
+
+ /**
+ * @param Title $title
+ * @param string $description
+ * @param string $mainPage
+ * @param string $publishers
+ * @param string $summary
+ * @param IContextSource $context
+ * @return Status
+ */
+ public static function edit( Title $title, $description, $mainPage,
$publishers, $summary,
+ IContextSource $context
+ ) {
+ $jsonText = FormatJson::encode(
+ [ 'description' => $description, 'mainpage' =>
$mainPage, 'publishers' => $publishers ]
+ );
+ if ( $jsonText === null ) {
+ return Status::newFatal( 'newsletter-ch-tojsonerror' );
+ }
+
+ // Ensure that a valid context is provided to the API in unit
tests
+ $der = new DerivativeContext( $context );
+ $request = new DerivativeRequest(
+ $context->getRequest(),
+ [
+ 'action' => 'edit',
+ 'title' => $title->getFullText(),
+ 'contentmodel' => 'NewsletterContent',
+ 'text' => $jsonText,
+ 'summary' => $summary,
+ 'token' => $context->getUser()->getEditToken(),
+ ],
+ true // Treat data as POSTed
+ );
+ $der->setRequest( $request );
+
+ try {
+ $api = new ApiMain( $der, true );
+ $api->execute();
+ } catch ( UsageException $e ) {
+ return Status::newFatal( $context->msg(
'newsletter-ch-apierror',
+ $e->getCodeString() ) );
+ }
+ return Status::newGood();
+ }
+
+ protected function getDiffEngineClass() {
+ return 'NewsletterDiffEngine';
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/304692
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I89809a4ec1b524148199ff5d11ee4e96ae716919
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Newsletter
Gerrit-Branch: master
Gerrit-Owner: 01tonythomas <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits