Jforrester has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/340330 )

Change subject: Fix security/general concerns
......................................................................

Fix security/general concerns

I skipped two concerns, namely

* "threed" as being confused with "thread" is probably not worth fixing now
* three.js is unwieldy, but I'm not sure there's a "good" way to fix that

Bug: T157077
Change-Id: Ifc716148ae40e87c8e9d5b650e069dee9e09d43d
---
M ThreeDHandler.php
D ThreeDHooks.php
M extension.json
M i18n/en.json
M i18n/qqq.json
M modules/mmv.3d.js
D modules/three/AMFLoader.js
D tests/3d.test.js
D tests/3d.test.php
9 files changed, 32 insertions(+), 579 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/3D 
refs/changes/30/340330/1

diff --git a/ThreeDHandler.php b/ThreeDHandler.php
index 7676111..4b10c84 100644
--- a/ThreeDHandler.php
+++ b/ThreeDHandler.php
@@ -41,7 +41,7 @@
                        return false;
                }
 
-               # Don't make an image bigger than wgMaxSVGSize on the smaller 
side
+               // Don't make an image bigger than wgMaxSVGSize on the smaller 
side
                if ( $params['physicalWidth'] <= $params['physicalHeight'] ) {
                        if ( $params['physicalWidth'] > $wgSVGMaxSize ) {
                                $srcWidth = $image->getWidth();
@@ -70,7 +70,7 @@
         * @return 
MediaTransformError|MediaTransformOutput|ThumbnailImage|TransformParameterError
         */
        function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
-               global $wg3dProcessor, $wg3dProcessEnviron;
+               global $wg3dProcessor, $wg3dProcessEnviron, $wgMaxShellMemory;
 
                // Impose an aspect ratio
                $params['height'] = round( $params['width'] / ( 640 / 480 ) );
@@ -79,12 +79,12 @@
                        return new ThumbnailImage( $image, $dstUrl, $dstPath, 
$params );
                }
 
+               $width = $params['width'];
+               $height = $params['height'];
+
                if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) 
{
                        return $this->doThumbError( $width, $height, 
'thumbnail_dest_directory' );
                }
-
-               $width = $params['width'];
-               $height = $params['height'];
 
                $srcPath = $image->getLocalRefPath();
 
@@ -101,7 +101,7 @@
                $err = wfShellExecWithStderr( $cmd, $retval, 
$wg3dProcessEnviron, [ 'memory' => '10000000' ] );
                wfProfileOut( 'ThreeDHandler' );
 
-               if ( $retval != 0 || $removed ) {
+               if ( $retval != 0 ) {
                        wfDebugLog( 'thumbnail',
                                sprintf( 'thumbnail failed on %s: error %d "%s" 
from "%s"',
                                wfHostname(), $retval, trim( $err ), $cmd ) );
diff --git a/ThreeDHooks.php b/ThreeDHooks.php
deleted file mode 100644
index c0dc520..0000000
--- a/ThreeDHooks.php
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/**
- * 3d extension hooks
- *
- * @file
- * @ingroup Extensions
- * @license MIT
- */
-class ThreeDHooks {
-
-       public static function onResourceLoaderTestModules(
-               array &$testModules,
-               ResourceLoader &$resourceLoader
-       ) {
-
-       }
-
-}
diff --git a/extension.json b/extension.json
index b9ff0a1..b2e04ef 100644
--- a/extension.json
+++ b/extension.json
@@ -15,11 +15,6 @@
             "i18n"
         ]
     },
-    "Hooks": {
-        "ResourceLoaderTestModules": [
-            "ThreeDHooks::onResourceLoaderTestModules"
-        ]
-    },
     "ResourceModules": {
         "ext.3d": {
             "scripts": [
@@ -35,7 +30,6 @@
             "scripts": [
                 "mmv.3d.js",
                 "three/three.js",
-                "three/AMFLoader.js",
                 "three/STLLoader.js",
                 "three/OrbitControls.js"
             ],
@@ -52,11 +46,10 @@
         "remoteExtPath": "3d/modules"
     },
     "AutoloadClasses": {
-        "ThreeDHooks": "ThreeDHooks.php",
         "ThreeDHandler": "ThreeDHandler.php"
     },
     "MediaHandlers": {
         "application/x-amf": "ThreeDHandler",
         "application/sla": "ThreeDHandler"
     }
-}
\ No newline at end of file
+}
diff --git a/i18n/en.json b/i18n/en.json
index 291e8db..9ae453e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1,9 +1,9 @@
 {
-    "@metadata": {
-        "authors": [
-            "Gilles Dubuc"
-        ]
-    },
-    "3d": "3d",
-    "3d-desc": "Provides support for 3d file formats."
-}
\ No newline at end of file
+       "@metadata": {
+               "authors": [
+                       "Gilles Dubuc"
+               ]
+       },
+       "3d": "3d",
+       "3d-desc": "Provides support for 3d file formats."
+}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 8880d4b..89dbb4c 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -1,9 +1,9 @@
 {
-    "@metadata": {
-        "authors": [
-            "Gilles Dubuc"
-        ]
-    },
-    "3d": "The name of the extension",
-    "3d-desc": 
"{{desc|name=3d|url=https://www.mediawiki.org/wiki/Extension:3d}}";
-}
\ No newline at end of file
+       "@metadata": {
+               "authors": [
+                       "Gilles Dubuc"
+               ]
+       },
+       "3d": "The name of the extension",
+       "3d-desc": 
"{{desc|name=3d|url=https://www.mediawiki.org/wiki/Extension:3d}}";
+}
diff --git a/modules/mmv.3d.js b/modules/mmv.3d.js
index b1e8bf7..a166604 100644
--- a/modules/mmv.3d.js
+++ b/modules/mmv.3d.js
@@ -1,18 +1,18 @@
 /*
- * This file is part of the MediaWiki extension MultimediaViewer.
+ * This file is part of the MediaWiki extension 3D.
  *
- * MultimediaViewer is free software: you can redistribute it and/or modify
+ * The 3D extension is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation, either version 2 of the License, or
  * (at your option) any later version.
  *
- * MultimediaViewer is distributed in the hope that it will be useful,
+ * The 3D extension is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with MultimediaViewer.  If not, see <http://www.gnu.org/licenses/>.
+ * along with The 3D extension. If not, see <http://www.gnu.org/licenses/>.
  */
 
 ( function ( mw, $ ) {
@@ -59,9 +59,9 @@
        TD.center = function ( object ) {
                var bbox, bboxWidth, bboxHeight, bboxDepth,
                        camerax, cameray, cameraz;
-               if ( object.type == 'Group' ) {
+               if ( object.type === 'Group' ) {
                        this.center( object.children[ 0 ] );
-               } else if ( object.type == 'Mesh' ) {
+               } else if ( object.type === 'Mesh' ) {
                        object.geometry.center();
                        object.geometry.computeBoundingBox();
 
@@ -89,16 +89,14 @@
                        request,
                        loader;
 
-               if ( extension == 'stl' ) {
+               if ( extension === 'stl' ) {
                        loader = new THREE.STLLoader( this.manager );
-               } else if ( extension == 'amf' ) {
-                       loader = new THREE.AMFLoader( this.manager );
                }
 
                request = loader.load( url, function ( data ) {
                        var object = data;
 
-                       if ( extension == 'stl' ) {
+                       if ( extension === 'stl' ) {
                                object = threed.geometryToObject( data );
                        }
 
@@ -195,7 +193,7 @@
                var extension = e.image.filePageTitle.ext;
 
                // Ignore events from formats that we don't care about
-               if ( $.inArray( extension, [ 'amf', 'stl' ] ) == -1 ) {
+               if ( $.inArray( extension, [ 'amf', 'stl' ] ) === -1 ) {
                        return;
                }
 
diff --git a/modules/three/AMFLoader.js b/modules/three/AMFLoader.js
deleted file mode 100644
index ade823c..0000000
--- a/modules/three/AMFLoader.js
+++ /dev/null
@@ -1,501 +0,0 @@
-/*
- * @author tamarintech / https://tamarintech.com
- *
- * Description: Early release of an AMF Loader following the pattern of the
- * example loaders in the three.js project.
- *
- * More information about the AMF format: http://amf.wikispaces.com
- *
- * Usage:
- *     var loader = new AMFLoader();
- *     loader.load('/path/to/project.amf', function(objecttree) {
- *             scene.add(objecttree);
- *     });
- *
- * Materials now supported, material colors supported
- * Zip support, requires jszip
- * TextDecoder polyfill required by some browsers (particularly IE, Edge)
- * No constellation support (yet)!
- *
- */
-
-THREE.AMFLoader = function ( manager ) {
-
-       this.manager = ( manager !== undefined ) ? manager : 
THREE.DefaultLoadingManager;
-
-};
-
-THREE.AMFLoader.prototype = {
-
-       constructor: THREE.AMFLoader,
-
-       load: function ( url, onLoad, onProgress, onError ) {
-
-               var scope = this;
-
-               var loader = new THREE.XHRLoader( scope.manager );
-               loader.setResponseType( 'arraybuffer' );
-               return loader.load( url, function( text ) {
-
-                       onLoad( scope.parse( text ) );
-
-               }, onProgress, onError );
-
-       },
-
-       parse: function ( data ) {
-
-               function loadDocument( data ) {
-
-                       var view = new DataView( data );
-                       var magic = String.fromCharCode( view.getUint8( 0 ), 
view.getUint8( 1 ) );
-
-                       if ( magic === "PK" ) {
-
-                               var zip = null;
-                               var file = null;
-
-                               console.log( "Loading Zip" );
-
-                               try {
-
-                                       zip = new JSZip( data );
-
-                               } catch ( e ) {
-
-                                       if ( e instanceof ReferenceError ) {
-
-                                               console.log( "  jszip missing 
and file is compressed." );
-                                               return null;
-
-                                       }
-
-                               }
-
-                               for ( file in zip.files ) {
-
-                                       if ( file.toLowerCase().substr( - 4 ) 
=== '.amf' ) {
-
-                                               break;
-
-                                       }
-
-                               }
-
-                               console.log( "  Trying to load file asset: " + 
file );
-                               view = new DataView( zip.file( file 
).asArrayBuffer() );
-
-                       }
-
-                       if ( TextDecoder === undefined ) {
-
-                               console.log( "  TextDecoder not present.        
Please use TextDecoder polyfill." );
-                               return null;
-
-                       }
-
-                       var fileText = new TextDecoder( 'utf-8' ).decode( view 
);
-                       var xmlData = new DOMParser().parseFromString( 
fileText, 'application/xml' );
-
-                       if ( xmlData.documentElement.nodeName.toLowerCase() !== 
"amf" ) {
-
-                               console.log( "  Error loading AMF - no AMF 
document found." );
-                               return null;
-
-                       }
-
-                       return xmlData;
-
-               }
-
-               function loadDocumentScale( node ) {
-
-                       var scale = 1.0;
-                       var unit = 'millimeter';
-
-                       if ( node.documentElement.attributes[ 'unit' ] !== 
undefined ) {
-
-                               unit = node.documentElement.attributes[ 'unit' 
].value.toLowerCase();
-
-                       }
-
-                       var scaleUnits = {
-                               'millimeter': 1.0,
-                               'inch': 25.4,
-                               'feet': 304.8,
-                               'meter': 1000.0,
-                               'micron': 0.001
-                       };
-
-                       if ( scaleUnits[ unit ] !== undefined ) {
-
-                               scale = scaleUnits[ unit ];
-
-                       }
-
-                       console.log( "  Unit scale: " + scale );
-                       return scale;
-
-               }
-
-               function loadMaterials( node ) {
-
-                       var matName = "AMF Material";
-                       var matId = node.attributes[ 'id' ].textContent;
-                       var color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
-
-                       var loadedMaterial = null;
-
-                       for ( var i = 0; i < node.children.length; i ++ ) {
-
-                               var matChildEl = node.children[ i ];
-
-                               if ( matChildEl.nodeName === "metadata" && 
matChildEl.attributes[ 'type' ] !== undefined ) {
-
-                                       if ( matChildEl.attributes[ 'type' 
].value === 'name' ) {
-
-                                               matname = 
matChildEl.textContent;
-
-                                       }
-
-                               } else if ( matChildEl.nodeName === 'color' ) {
-
-                                       color = loadColor( matChildEl );
-
-                               }
-
-                       }
-
-                       loadedMaterial = new THREE.MeshPhongMaterial( {
-                               shading: THREE.FlatShading,
-                               color: new THREE.Color( color.r, color.g, 
color.b ),
-                               name: matName
-                       } );
-
-                       if ( color.a !== 1.0 ) {
-
-                               loadedMaterial.transparent = true;
-                               loadedMaterial.opacity = color.a;
-
-                       }
-
-                       return { 'id': matId, 'material': loadedMaterial };
-
-               }
-
-               function loadColor( node ) {
-
-                       var color = { 'r': 1.0, 'g': 1.0, 'b': 1.0, 'a': 1.0 };
-
-                       for ( var i = 0; i < node.children.length; i ++ ) {
-
-                               var matColor = node.children[ i ];
-
-                               if ( matColor.nodeName === 'r' ) {
-
-                                       color.r = matColor.textContent;
-
-                               } else if ( matColor.nodeName === 'g' ) {
-
-                                       color.g = matColor.textContent;
-
-                               } else if ( matColor.nodeName === 'b' ) {
-
-                                       color.b = matColor.textContent;
-
-                               } else if ( matColor.nodeName === 'a' ) {
-
-                                       color.a = matColor.textContent;
-
-                               }
-
-                       }
-
-                       return color;
-
-               }
-
-               function loadMeshVolume( node ) {
-
-                       var volume = { "name": "", "triangles": [], 
"materialid": null };
-
-                       var currVolumeNode = node.firstElementChild;
-
-                       if ( node.attributes[ 'materialid' ] !== undefined ) {
-
-                               volume.materialId = node.attributes[ 
'materialid' ].nodeValue;
-
-                       }
-
-                       while ( currVolumeNode ) {
-
-                               if ( currVolumeNode.nodeName === "metadata" ) {
-
-                                       if ( currVolumeNode.attributes[ 'type' 
] !== undefined ) {
-
-                                               if ( currVolumeNode.attributes[ 
'type' ].value === 'name' ) {
-
-                                                       volume.name = 
currVolumeNode.textContent;
-
-                                               }
-
-                                       }
-
-                               } else if ( currVolumeNode.nodeName === 
"triangle" ) {
-
-                                       var v1 = 
currVolumeNode.getElementsByTagName("v1")[0].textContent;
-                                       var v2 = 
currVolumeNode.getElementsByTagName("v2")[0].textContent;
-                                       var v3 = 
currVolumeNode.getElementsByTagName("v3")[0].textContent;
-
-                                       volume.triangles.push( v1 );
-                                       volume.triangles.push( v2 );
-                                       volume.triangles.push( v3 );
-
-                               }
-
-                               currVolumeNode = 
currVolumeNode.nextElementSibling;
-
-                       }
-
-                       return volume;
-
-               }
-
-               function loadMeshVertices( node ) {
-
-                       var vertArray = [];
-                       var normalArray = [];
-                       var currVerticesNode = node.firstElementChild;
-
-                       while ( currVerticesNode ) {
-
-                               if ( currVerticesNode.nodeName === "vertex" ) {
-
-                                       var vNode = 
currVerticesNode.firstElementChild;
-
-                                       while ( vNode ) {
-
-                                               if ( vNode.nodeName === 
"coordinates" ) {
-
-                                                       var x = 
vNode.getElementsByTagName("x")[0].textContent;
-                                                       var y = 
vNode.getElementsByTagName("y")[0].textContent;
-                                                       var z = 
vNode.getElementsByTagName("z")[0].textContent;
-
-                                                       vertArray.push(x);
-                                                       vertArray.push(y);
-                                                       vertArray.push(z);
-
-                                               } else if ( vNode.nodeName === 
"normal" ) {
-
-                                                       var nx = 
vNode.getElementsByTagName("nx")[0].textContent;
-                                                       var ny = 
vNode.getElementsByTagName("ny")[0].textContent;
-                                                       var nz = 
vNode.getElementsByTagName("nz")[0].textContent;
-
-                                                       normalArray.push(nx);
-                                                       normalArray.push(ny);
-                                                       normalArray.push(nz);
-
-                                               }
-
-                                               vNode = 
vNode.nextElementSibling;
-
-                                       }
-
-                               }
-                               currVerticesNode = 
currVerticesNode.nextElementSibling;
-
-                       }
-
-                       return { "vertices": vertArray, "normals": normalArray 
};
-
-               }
-
-               function loadObject( node ) {
-
-                       var objId = node.attributes[ 'id' ].textContent;
-                       var loadedObject = { "name": "amfobject", "meshes": [] 
};
-                       var currColor = null;
-                       var currObjNode = node.firstElementChild;
-
-                       while ( currObjNode ) {
-
-                               if ( currObjNode.nodeName === "metadata" ) {
-
-                                       if ( currObjNode.attributes[ 'type' ] 
!== undefined ) {
-
-                                               if ( currObjNode.attributes[ 
'type' ].value === 'name' ) {
-
-                                                       loadedObject.name = 
currObjNode.textContent;
-
-                                               }
-
-                                       }
-
-                               } else if ( currObjNode.nodeName === "color" ) {
-
-                                       currColor = loadColor( currObjNode );
-
-                               } else if ( currObjNode.nodeName === "mesh" ) {
-
-                                       var currMeshNode = 
currObjNode.firstElementChild;
-                                       var mesh = { "vertices": [], "normals": 
[], "volumes": [], "color": currColor };
-
-                                       while ( currMeshNode ) {
-
-                                               if ( currMeshNode.nodeName === 
"vertices" ) {
-
-                                                       var loadedVertices = 
loadMeshVertices( currMeshNode );
-
-                                                       mesh.normals = 
mesh.normals.concat( loadedVertices.normals );
-                                                       mesh.vertices = 
mesh.vertices.concat( loadedVertices.vertices );
-
-                                               } else if ( 
currMeshNode.nodeName === "volume" ) {
-
-                                                       mesh.volumes.push( 
loadMeshVolume( currMeshNode ) );
-
-                                               }
-
-                                               currMeshNode = 
currMeshNode.nextElementSibling;
-
-                                       }
-
-                                       loadedObject.meshes.push( mesh );
-
-                               }
-
-                               currObjNode = currObjNode.nextElementSibling;
-
-                       }
-
-                       return { 'id': objId, 'obj': loadedObject };
-
-               }
-
-               var xmlData = loadDocument( data );
-               var amfName = "";
-               var amfAuthor = "";
-               var amfScale = loadDocumentScale( xmlData );
-               var amfMaterials = {};
-               var amfObjects = {};
-               var children = xmlData.documentElement.children;
-
-               for ( var i = 0; i < children.length; i ++ ) {
-
-                       var child = children[ i ];
-
-                       if ( child.nodeName === 'metadata' ) {
-
-                               if ( child.attributes[ 'type' ] !== undefined ) 
{
-
-                                       if ( child.attributes[ 'type' ].value 
=== 'name' ) {
-
-                                               amfName = child.textContent;
-
-                                       } else if ( child.attributes[ 'type' 
].value === 'author' ) {
-
-                                               amfAuthor = child.textContent;
-
-                                       }
-
-                               }
-
-                       } else if ( child.nodeName === 'material' ) {
-
-                               var loadedMaterial = loadMaterials( child );
-
-                               amfMaterials[ loadedMaterial.id ] = 
loadedMaterial.material;
-
-                       } else if ( child.nodeName === 'object' ) {
-
-                               var loadedObject = loadObject( child );
-
-                               amfObjects[ loadedObject.id ] = 
loadedObject.obj;
-
-                       }
-
-               }
-
-               var sceneObject = new THREE.Group();
-               var defaultMaterial = new THREE.MeshPhongMaterial( { color: 
0xaaaaff, shading: THREE.FlatShading } );
-
-               sceneObject.name = amfName;
-               sceneObject.userData.author = amfAuthor;
-               sceneObject.userData.loader = "AMF";
-
-               for ( var id in amfObjects ) {
-
-                       var meshes = amfObjects[ id ].meshes;
-                       var newObject = new THREE.Group();
-
-                       for ( var i = 0; i < meshes.length; i ++ ) {
-
-                               var objDefaultMaterial = defaultMaterial;
-                               var mesh = meshes[ i ];
-                               var meshVertices = Float32Array.from( 
mesh.vertices );
-                               var vertices = new THREE.BufferAttribute( 
Float32Array.from( meshVertices ), 3 );
-                               var meshNormals = null;
-                               var normals = null;
-
-                               if ( mesh.normals.length ) {
-
-                                       meshNormals = Float32Array.from( 
mesh.normals );
-                                       normals = new THREE.BufferAttribute( 
Float32Array.from( meshNormals ), 3 );
-
-                               }
-
-                               if ( mesh.color ) {
-
-                                       var color = mesh.color;
-
-                                       objDefaultMaterial = 
defaultMaterial.clone();
-                                       objDefaultMaterial.color = new 
THREE.Color( color.r, color.g, color.b );
-
-                                       if ( color.a !== 1.0 ) {
-
-                                               objDefaultMaterial.transparent 
= true;
-                                               objDefaultMaterial.opacity = 
color.a;
-
-                                       }
-
-                               }
-
-                               var volumes = mesh.volumes;
-
-                               for ( var j = 0; j < volumes.length; j ++ ) {
-
-                                       var volume = volumes[ j ];
-                                       var newGeometry = new 
THREE.BufferGeometry();
-                                       var indexes = Uint32Array.from( 
volume.triangles );
-                                       var material = objDefaultMaterial;
-
-                                       newGeometry.setIndex( new 
THREE.BufferAttribute( indexes, 1 ) );
-                                       newGeometry.addAttribute( 'position', 
vertices.clone() );
-
-                                       if( normals ) {
-
-                                               newGeometry.addAttribute( 
'normal', normals.clone() );
-
-                                       }
-
-                                       if ( amfMaterials[ volume.materialId ] 
!== undefined ) {
-
-                                               material = amfMaterials[ 
volume.materialId ];
-
-                                       }
-
-                                       newGeometry.scale( amfScale, amfScale, 
amfScale );
-                                       newObject.add( new THREE.Mesh( 
newGeometry, material.clone() ) );
-
-                               }
-
-                       }
-
-                       sceneObject.add( newObject );
-
-               }
-
-               return sceneObject;
-
-       }
-
-};
diff --git a/tests/3d.test.js b/tests/3d.test.js
deleted file mode 100644
index c2f70c6..0000000
--- a/tests/3d.test.js
+++ /dev/null
@@ -1,12 +0,0 @@
-( function ( mw, $ ) {
-       QUnit.module( 'ext.3d' );
-
-       /**
-        * Write your QUnit tests here. For more information on
-        * how to write proper JavaScript QUnit tests for
-        * MediaWiki extension development, please read
-        * the manual:
-        * 
https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing#Write_a_unit_test
-        */
-
-} )( mediaWiki, jQuery );
diff --git a/tests/3d.test.php b/tests/3d.test.php
deleted file mode 100644
index b7a9013..0000000
--- a/tests/3d.test.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-
-/**
- * For more information on how to create PHPUnit tests
- * for your extension, visit the documentation page:
- * https://www.mediawiki.org/wiki/Manual:PHP_unit_testing/Writing_unit_tests
- */

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ifc716148ae40e87c8e9d5b650e069dee9e09d43d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/3D
Gerrit-Branch: master
Gerrit-Owner: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: MarkTraceur <mholmqu...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to