Adamw has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/61929


Change subject: WIP: banner DonationForm backend support
......................................................................

WIP: banner DonationForm backend support

Change-Id: I212499e904210222eeee6cfb2a9b2c618c4d3b18
---
A mixins/DonationForm/AskString.js
A mixins/DonationForm/DonationForm.php
A mixins/DonationForm/FormController.js
A mixins/DonationForm/PaymentMethods.js
A mixins/FundraisingBannerMixins.php
A mixins/Thermometer/Thermometer.php
6 files changed, 589 insertions(+), 0 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/DonationInterface 
refs/changes/29/61929/1

diff --git a/mixins/DonationForm/AskString.js b/mixins/DonationForm/AskString.js
new file mode 100644
index 0000000..3338bf1
--- /dev/null
+++ b/mixins/DonationForm/AskString.js
@@ -0,0 +1,254 @@
+function convertAsk(amount, currency, country) {
+    // given an amount in USD, find an "equivalent" amount for the given 
currency and country
+
+    var usdamount = parseInt(amount, 10);
+    if(isNaN(usdamount)){
+        return 0;
+    }
+ 
+    var usdbase = [3, 5, 10, 20, 30, 50, 100];
+ 
+    if(currency === 'USD'){
+        if(country === 'US'){
+            return usdamount;
+        }
+    }
+ 
+    var other = {
+        'EUR' : {
+            'default' : [3, 5, 10, 20, 30, 50, 100]
+        },
+        'GBP' : [3, 5, 10, 20, 30, 50, 100],
+        'CAD' : [3, 5, 10, 20, 30, 50, 100],
+        'AUD' : [3, 5, 10, 20, 30, 50, 100],
+        'NZD' : [3, 5, 10, 20, 30, 50, 100],
+        
+        'INR' : [100, 200, 300, 500, 1000, 2000, 3000],
+        'JPY' : [1000, 1500, 2000, 3000, 5000, 10000, 15000],
+        'RUB' : [100, 150, 200, 500, 1000, 2000, 3000],
+        'BRL' : [10, 20, 30, 50, 100, 250, 500],
+        'SEK' : [50, 100, 150, 250, 500, 750, 1000],
+        'NOK' : [50, 100, 150, 250, 500, 750, 1000],
+        'ILS' : [25, 50, 100, 200, 300, 500, 1000],
+        'DKK' : [50, 100, 150, 200, 300, 500, 750],
+        'SGD' : [5, 20, 50, 75, 100, 150, 300],
+        'HKD' : [50, 200, 300, 500, 1000, 1500, 2000],
+        'TWD' : [150, 300, 500, 1000, 1500, 2000, 5000],
+        'CNY' : [50, 75, 100, 300, 500, 1000, 1500],
+        'PLN' : [20, 35, 50, 100, 150, 300],
+        'CZK' : [100, 150, 300, 1000, 1500, 3000,],
+        'ZAR' : [25, 50, 100, 300, 500, 750, 1000],
+        'KRW' : [5000, 10000, 20000, 30000, 50000, 75000, 100000],
+        'TRY' : [10, 25, 50, 100, 150, 200, 500],
+        'SAR' : [50, 100, 200, 500, 750, 1000, 1500],
+        'THB' : [100, 250, 500, 750, 1000, 2000, 3000],
+        'HUF' : [1000, 2000, 5000, 10000, 20000, 50000, 100000],
+        'IDR' : [50000, 75000, 100000, 150000, 200000, 500000, 1000000],
+        'ARS' : [20, 50, 100, 200, 500, 750, 1000],
+        'PHP' : [200, 500, 750, 1000, 2000, 3000, 5000],
+        'CLP' : [2500, 5000, 10000, 20000, 30000],
+        'MXN' : [50, 100, 200, 500, 750, 1000, 2000],
+        'UYU' : [100, 200, 400, 1000, 1500, 1950, 5000],
+        'COP' : [5000, 10000, 20000, 50000, 100000, 150000, 200000],
+        'NIO' : [100, 250, 500, 1000, 1500, 2500, 5500],
+        'DOP' : [200, 500, 1000, 2000, 5000, 7500, 10000],
+        'UAH' : [50, 75, 150, 300, 500, 750, 1000],
+        'MYR' : [20, 30, 50, 100, 200, 300, 500],
+        'RON' : [25, 50, 75, 100, 200, 300, 500],
+        'BGN' : [10, 25, 50, 75, 100, 150, 200],
+        'HRK' : [35, 50, 100, 250, 500, 1000, 1500],
+        'QAR' : [20, 50, 75, 185, 250, 350, 1000],
+        'KWD' : [2, 5, 10,15, 25, 30, 75],
+        'LTL' : [15, 25, 50, 100, 200, 250, 600],
+        'KZT' : [750, 1500, 3000, 7500, 12000, 15000, 35000],
+        'LVL' : [5, 10, 20, 30, 40, 50, 75],
+        'CRC' : [2500, 5000, 10000, 20000, 50000, 75000, 100000],
+        'VEF' : [5, 10, 20, 50, 75, 100, 250],
+        'BHD' : [4, 5, 10, 25, 50, 100],
+        'PEN' : [15, 30, 50, 150, 200, 275, 700],
+        // the following USD ask strings are for countries in which we 
fundraise
+        // in USD, but that need a difference ask string than in the US
+        'USD' : {
+            'default' : usdbase
+        }
+    };
+ 
+ 
+    if (other[currency] == null) {
+        return usdamount;
+    }
+    var index = $.inArray(usdamount, usdbase);
+    if (index == -1) {
+        // the amount is not in the USD ask array, find a near neighbor
+        index = 0;
+        while (usdbase[index+1] < usdamount && index < usdbase.length + 1) {
+            index++;
+        }
+    }
+ 
+    if ( other[currency] instanceof Array ) { // simplest case, just one array 
for this currency
+        return other[currency][index];
+    } else { // arrays for multiple countries
+        if (other[currency][country] != null) {
+            return other[currency][country][index];
+        } else {
+            return other[currency]['default'][index];
+        }
+    }
+    
+};
+
+function getAverage(currency, country, language) {
+    // return a string of the "average" donation amount in the given currency 
& country for use in the banner text
+     var usdaverage = 30;
+
+     var average = {
+        'EUR' : {
+            'default' : 30
+        },
+        'GBP' : 20,
+        'CAD' : 30,
+        'AUD' : 30,
+        'NZD' : 30,
+
+        'INR' : 1500,
+        'JPY' : 3000,
+        'RUB' : 1000,
+        'BRL' : 60,
+        'SEK' : 200,
+        'NOK' : 150,
+        'ILS' : 100,
+        'DKK' : 150,
+        'SGD' : 30,
+        'HKD' : 200,
+        'TWD' : 1000,
+        'CNY' : 200,
+        'PLN' : 100,
+        'CZK' : 500,
+        'ZAR' : 300,
+        'KRW' : 30000,
+        'TRY' : 50,
+        'SAR' : 100,
+        'THB' : 1000,
+        'HUF' : 5000,
+        'IDR' : 300000,
+        'ARS' : 150,
+        'ISK' : 4000,
+        'PHP' : 1500,
+        'CLP' : 20000,
+        'MXN' : 300,
+        'UYU' : 750,
+        'COP' : 30000,
+        'NIO' : 750,
+        'DOP' : 1500,
+        'UAH' : 300,
+        'MYR' : 150,
+        'RON' : 200,
+        'BGN' : 75,
+        'HRK' : 250,
+        'QAR' : 185,
+        'KWD' : 15,
+        'LTL' : 100,
+        'KZT' : 5000,
+        'LVL' : 30,
+        'CRC' : 20000,
+        'VEF' : 50,
+        'BHD' : 25,
+        'PEN' : 100,
+       'USD' : {
+            'default' : usdaverage
+        }
+    };
+
+    if (average[currency] == null) {
+        return currencyLocalize('USD', usdaverage, language); // fallback to 
$30 if not defined
+    }
+
+    if ( typeof(average[currency]) === 'number' ) { // simplest case, single 
average for currency
+        return currencyLocalize(currency, average[currency], language);
+    } else { // different averages for different countries
+        if (average[currency][country] != null) {
+            return currencyLocalize(currency, average[currency][country], 
language);
+        } else {
+            return currencyLocalize(currency, average[currency]['default'], 
language);
+        }
+    }
+
+};
+
+function getMinimumString(currency, country, language) {
+    // return a string of the "minimum" donation amount in the given currency 
& country for use in the banner text
+    var usdminimum = 3;
+
+    var minimum = {
+        'EUR' : {
+            'default' : 3
+        },
+        'GBP' : 3,
+        'CAD' : 3, 
+        'AUD' : 3, 
+        'NZD' : 3, 
+        
+        'INR' : 100,
+        'JPY' : 1000, 
+        'RUB' : 100, 
+        'BRL' : 10, 
+        'SEK' : 50, 
+        'NOK' : 50, 
+        'ILS' : 25, 
+        'DKK' : 50,
+        'SGD' : 5, 
+        'HKD' : 50, 
+        'TWD' : 150,
+        'CNY' : 50, 
+        'PLN' : 20,
+        'CZK' : 100,
+        'ZAR' : 25,
+        'KRW' : 5000,
+        'TRY' : 10,
+        'SAR' : 50,
+        'THB' : 100,
+        'HUF' : 1000,
+        'IDR' : 50000,
+        'ARS' : 20,
+        'PHP' : 200,
+        'CLP' : 2500,
+        'MXN' : 50,
+        'UYU' : 100,
+        'COP' : 5000,
+        'NIO' : 100,
+        'DIO' : 200,
+        'UAH' : 50,
+        'MYR' : 20,
+        'RON' : 25,
+        'BGN' : 10,
+        'HRK' : 25,
+        'QAR' : 20,
+        'KWD' : 1,
+        'LTL' : 15,
+        'KZT' : 750,
+        'LVL' : 5,
+        'CRC' : 2500,
+        'VEF' : 5,
+        'BHD' : 4,
+        'PEN' : 15,
+       'USD' : {
+            'default' : usdminimum
+        }
+    };
+
+    if (minimum[currency] == null) {
+        return currencyLocalize('USD', usdminimum, language); // fallback to 
$30 if not defined
+    }
+
+    if ( typeof(minimum[currency]) === 'number' ) { // simplest case, single 
minimum for currency
+        return currencyLocalize(currency, minimum[currency], language);
+    } else { // different minimums for different countries
+        if (minimum[currency][country] != null) {
+            return currencyLocalize(currency, minimum[currency][country], 
language);
+        } else {
+            return currencyLocalize(currency, minimum[currency]['default'], 
language);
+        }
+    }
+
+}
diff --git a/mixins/DonationForm/DonationForm.php 
b/mixins/DonationForm/DonationForm.php
new file mode 100644
index 0000000..b2af1fa
--- /dev/null
+++ b/mixins/DonationForm/DonationForm.php
@@ -0,0 +1,39 @@
+<?php
+/**
+ * This is a WIP, do not use.
+ */
+class DonationForm implements IBannerMixin {
+       protected $controller;
+
+       function register( MixinController $controller ) {
+               $this->controller = $controller;
+
+               $controller->registerMagicWord( 'amount', array( $this, 
'getLocalizedAmount' ) );
+               $controller->registerMagicWord( 'currency', array( $this, 
'getCurrency' ) );
+               $controller->registerMagicWord( 'minimum-amount', array( $this, 
'getMinimumAmount' ) );
+               $controller->registerMagicWord( "ask-amount", array( $this, 
'getAskAmount' ) );
+               $controller->registerMagicWord( "ask-value", array( $this, 
'getAskValue' ) );
+       }
+
+       function getCurrency() {
+               return $this->controller->getFrContext()->getCountry() . "$";
+       }
+
+       function getMinimumAmount() {
+               $currency = $this->getCurrency();
+               //FIXME: round
+               return 1.0 / ExchangeRates::getConversion( $currency );
+       }
+
+       function getLocalizedAmount( $amount ) {
+               return "$amount ZROW";
+       }
+
+       function getAskAmount( $level ) {
+               return $this->getLocalizedAmount( $this->getAskValue( $level ) 
);
+       }
+
+       function getAskValue( $level ) {
+               return $level * 10;
+       }
+}
diff --git a/mixins/DonationForm/FormController.js 
b/mixins/DonationForm/FormController.js
new file mode 100644
index 0000000..501620a
--- /dev/null
+++ b/mixins/DonationForm/FormController.js
@@ -0,0 +1,169 @@
+function validateForm(form) {
+    var error = true;
+
+    // Get amount selection
+    var amount = null;
+    for (var i = 0; i < form.amount.length; i++) {
+        if (form.amount[i].checked) {
+            amount = form.amount[i].value;
+        }
+    }
+    if (form.amountGiven.value != '') {
+        var otherAmount = form.amountGiven.value;
+        otherAmount = otherAmount.replace(/[,.](\d)$/, '\:$10');
+        otherAmount = otherAmount.replace(/[,.](\d)(\d)$/, '\:$1$2');
+        otherAmount = otherAmount.replace(/[\$,.]/g, '');
+        otherAmount = otherAmount.replace(/:/, '.');
+        form.amountGiven.value = otherAmount;
+        amount = otherAmount;
+    }
+    // Check amount is a real number
+    error = ( amount == null || isNaN(amount) || amount.value <= 0 );
+    // Check amount is at least the minimum
+    var currency = form.currency_code.value;
+    if (amount < getMinimum(currency) || error) {
+        
alert('{{{validation-error-minimum|{{int:fr2013-dropdown-smallamount-error}}}}}'.replace('$1',
 getMinimum(currency) + ' ' + currency));
+        error = true;
+    }
+    return !error;
+}
+
+function redirectPayment(paymentMethod, paymentSubMethod) {
+    if (typeof paymentSubMethod == 'undefined'){
+        paymentSubMethod = '';
+    }
+    var form = document.paypalcontribution; // we should really change this 
some day
+    var language = $("input[name='language']").val();
+
+    var paymentsURL = 
'https://payments.wikimedia.org/index.php/Special:GatewayFormChooser';
+    var paypalURL = 
'https://wikimediafoundation.org/wiki/Special:ContributionTracking/' + language;
+
+    var params = {
+        'uselang' : language,
+        'language' : language,
+        'currency' : $("input[name='currency_code']").val(),
+        'country' : $("input[name='country']").val(),
+        'paymentmethod' : paymentMethod
+    };
+    if( paymentSubMethod != '' ){
+        params['submethod'] = paymentSubMethod;
+    }
+
+    var frequency = $("input[name='frequency']:checked").val();
+    if( frequency !== 'monthly' ){
+        frequency = 'onetime';
+    } else {
+        params['recurring'] = 'true';
+        // the following is only for contribution_tracking, do not submit 'r' 
to payments
+        paymentMethod = 'r' + paymentMethod;
+    }
+
+    form.action = paymentsURL + '?' + $.param(params);
+    form.utm_source.value = '{{{banner}}}.no-LP' + '.' + paymentMethod;
+    form.payment_method.value = paymentMethod;
+    if( paymentSubMethod != '' ){
+        form.payment_method.value = form.payment_method.value + + '.' + 
paymentSubMethod;
+    }
+
+    if (validateForm(document.paypalcontribution)) {
+        form.submit();
+    }
+}
+
+function toggleMonthly( monthly ){
+    if( monthly.type === 'checkbox' ){
+        monthly = monthly.checked;
+    }
+
+    var onetimeonly = $(".no-monthly");
+
+    if( monthly ){
+        onetimeonly.css("display", "none");
+    } else {
+        onetimeonly.css("display", "");
+    }
+}
+
+$(document).ready( function () {
+    if ( wgCanonicalSpecialPageName != "CentralNotice" && 
wgCanonicalSpecialPageName != "NoticeTemplate" ){
+        // append the banner count in utm-key
+        var cookieName = 'centralnotice_bannercount_fr12';
+        var count = $.cookie(cookieName);
+        $('[name="paypalcontribution"]').append(
+            $('<input type="hidden" name="utm_key" />').attr('value', count));
+
+        var currency = getCurrency(Geo.country);
+        var language = mw.config.get('wgUserLanguage');
+
+        // hide CC or PP buttons anywhere we need to
+        var noCC = [];
+        if ($.inArray(Geo.country, noCC) != -1) {
+            $(".paymentmethod-cc").remove();
+        }
+        var noPP = ['RU'];
+        if ($.inArray(Geo.country, noPP) != -1){
+            $(".paymentmethod-pp").remove();
+        }
+
+        // can't do monthly credit card in India
+        if (Geo.country === 'IN') {
+            $(".paymentmethod-cc").addClass("no-monthly");
+        }
+
+        // show any extra local payment methods, or remove them if not needed
+        var extrapaymentmethods = {
+            'amazon' : ['US'],
+            'bpay' : ['AU'],
+            'ideal' : ['NL'],
+            'yandex' : ['RU'],
+            'webmoney' : ['RU'],
+            'sofort' : ['AT', 'BE', 'CH', 'DE'],
+            'dd' : ['AT', 'DE', 'ES', 'NL'],
+            'boletos' : ['BR']
+        };
+
+        for (var method in extrapaymentmethods) {
+            var $methodbutton = $('.paymentmethod-' + method);
+
+            if ($.inArray(Geo.country, extrapaymentmethods[method]) != -1) { 
// country is in the list
+                $methodbutton.show();
+            } else {
+                $methodbutton.remove();
+            }
+        }
+
+        // set the form fields
+        $("input[name='country']").val(Geo.country);
+        $("input[name='currency_code']").val(currency);
+        $("input[name='language']").val(mw.config.get('wgUserLanguage'));
+        $("input[name='return_to']").val("Thank_You/" + 
mw.config.get('wgUserLanguage'));
+
+        // do fun things to localize currency in the banner and form
+        $("input[name='amount']").each(function(index){
+            var id = $(this).attr("id");
+            var label = $("label[for='" + id + "']");
+            if(id.indexOf("other") == -1){
+                var amount = convertAsk($(this).val(), currency, Geo.country);
+                $(this).val(amount);
+                label.text(currencyLocalize(currency, amount, language));
+            } else {
+                // simply replace the currency symbol
+                label.text(label.text().replace(/\$/,  
currencyLocalize(currency, "", language)));
+            }
+        });
+
+        // handle pressing Enter on "Other" field
+        $('input[name="amountGiven"]').keydown(function(e){
+            if (e.keyCode == 13) {
+                e.preventDefault();
+                redirectPayment('cc'); // use credit card by default. Might be 
nice to have different defaults for some countries, but this will do for now.
+                return false;
+            }
+        });
+
+        // if there are no recurring payment methods available, hide the 
"monthly" radio button.
+        if ( !$('form[name="paypalcontribution"] 
button[class^="paymentmethod-"]:not(.no-monthly)').length ) {
+            $('#frequency_monthly').prop('disabled', 'disabled');
+        }
+    }
+});
diff --git a/mixins/DonationForm/PaymentMethods.js 
b/mixins/DonationForm/PaymentMethods.js
new file mode 100644
index 0000000..a8310d9
--- /dev/null
+++ b/mixins/DonationForm/PaymentMethods.js
@@ -0,0 +1 @@
+// Define payment gateway capabilities
diff --git a/mixins/FundraisingBannerMixins.php 
b/mixins/FundraisingBannerMixins.php
new file mode 100644
index 0000000..bdb384d
--- /dev/null
+++ b/mixins/FundraisingBannerMixins.php
@@ -0,0 +1,19 @@
+<?php
+
+$wgResourceModules[ 'ext.donationInterface.mixins.DonationForm' ] = array(
+       'localBasePath' => __DIR__ . '/DonationForm',
+       'remoteExtPath' => 'DonationInterface/mixins/DonationForm',
+       'scripts' => array(
+               'PaymentMethods.js',
+               'FormController.js',
+       ),
+       'position' => 'top',
+       'targets' => array( 'desktop' ),
+);
+
+$wgNoticeMixins['DonationForm'] = array(
+       'localBasePath' => __DIR__ . "/DonationForm",
+
+       'php' => "DonationForm.php",
+       'resourceLoader' => "ext.donationInterface.mixins.DonationForm",
+);
diff --git a/mixins/Thermometer/Thermometer.php 
b/mixins/Thermometer/Thermometer.php
new file mode 100644
index 0000000..6897674
--- /dev/null
+++ b/mixins/Thermometer/Thermometer.php
@@ -0,0 +1,107 @@
+<?php //namespace CentralNotice\Mixins;
+
+class Thermometer implements IBannerMixin {
+       protected $context = null;
+
+       function register( MixinController $controller ) {
+               $this->context = $controller->getContext();
+
+               $controller->registerMagicWord( 'cumulative-amount', array( 
$this, "getDonationAmount" ) );
+               $controller->registerMagicWord( 'daily-amount', array( $this, 
"getDailyDonationAmount" ) );
+       }
+
+       /**
+        * Pull the current amount raised during a fundraiser
+        * @throws SpecialBannerLoaderException
+        */
+       function getDonationAmount( $arg1 ) {
+               return "fake-out!: $arg1";
+
+               global $wgNoticeCounterSource, $wgMemc;
+               // Pull short-cached amount
+               $count = intval( $wgMemc->get( wfMemcKey( 'centralnotice', 
'counter' ) ) );
+               if ( !$count ) {
+                       // Pull from dynamic counter -- WHAT
+                       $counter_value = Http::get( $wgNoticeCounterSource );
+                       if( !$counter_value ) {
+                               throw new RemoteServerProblemException();
+                       }
+                       $count = intval( $counter_value );
+                       if ( !$count ) {
+                               // Pull long-cached amount
+                               $count = intval( $wgMemc->get(
+                                       wfMemcKey( 'centralnotice', 'counter', 
'fallback' ) ) );
+                               if ( !$count ) {
+                                       throw new 
DonationAmountUnknownException();
+                               }
+                       }
+                       // Expire in 60 seconds
+                       $wgMemc->set( wfMemcKey( 'centralnotice', 'counter' ), 
$count, 60 );
+                       // No expiration
+                       $wgMemc->set( wfMemcKey( 'centralnotice', 'counter', 
'fallback' ), $count );
+               }
+
+               $num = $this->toMillions( $count );
+               return "{{{amount|$num}}}";
+       }
+
+       /**
+        * Pull the amount raised so far today during a fundraiser
+        * @throws SpecialBannerLoaderException
+        */
+       function getDailyDonationAmount() {
+               return "fake-out!";
+
+               global $wgNoticeDailyCounterSource, $wgMemc;
+               // Pull short-cached amount
+               $count = intval( $wgMemc->get( wfMemcKey( 'centralnotice', 
'dailycounter' ) ) );
+               if ( !$count ) {
+                       // Pull from dynamic counter
+                       $counter_value = Http::get( $wgNoticeDailyCounterSource 
);
+                       if( !$counter_value ) {
+                               throw new RemoteServerProblemException();
+                       }
+                       $count = intval( $counter_value );
+                       if ( !$count ) {
+                               // Pull long-cached amount
+                               $count = intval( $wgMemc->get(
+                                       wfMemcKey( 'centralnotice', 
'dailycounter', 'fallback' ) ) );
+                               if ( !$count ) {
+                                       throw new 
DonationAmountUnknownException();
+                               }
+                       }
+                       // Expire in 60 seconds
+                       $wgMemc->set( wfMemcKey( 'centralnotice', 
'dailycounter' ), $count, 60 );
+                       // No expiration
+                       $wgMemc->set( wfMemcKey( 'centralnotice', 
'dailycounter', 'fallback' ), $count );
+               }
+
+               $num = $this->toThousands( $count );
+               return "{{{daily-amount|$num}}}";
+       }
+
+       /**
+        * Convert number of dollars to millions of dollars
+        */
+       protected function toMillions( $num ) {
+               $num = sprintf( "%.1f", $num / 1e6 );
+               if ( substr( $num, - 2 ) == '.0' ) {
+                       $num = substr( $num, 0, - 2 );
+               }
+               return $this->context->getLanguage()->formatNum( $num );
+       }
+
+       /**
+        * Convert number of dollars to thousands of dollars
+        */
+       protected function toThousands( $num ) {
+               $num = sprintf( "%d", $num / 1000 );
+               return $this->context->getLanguage()->formatNum( $num );
+       }
+}
+
+class RemoteServerProblemException extends BannerLoaderException {
+}
+
+class DonationAmountUnknownException extends BannerLoaderException {
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/61929
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I212499e904210222eeee6cfb2a9b2c618c4d3b18
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/DonationInterface
Gerrit-Branch: master
Gerrit-Owner: Adamw <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to