jenkins-bot has submitted this change and it was merged. Change subject: Support wikitabular graph protocol ......................................................................
Support wikitabular graph protocol Bug: T149713 Depends-On: I6b5f189690b52fc3b523a4087ba8d1e48755a879 Change-Id: I6eb35ea739f5827a14f85758748c313701734880 --- M lib/graph2.compiled.js M modules/graph2.js 2 files changed, 239 insertions(+), 180 deletions(-) Approvals: MaxSem: Looks good to me, approved jenkins-bot: Verified diff --git a/lib/graph2.compiled.js b/lib/graph2.compiled.js index e7abd5e..bd95538 100644 --- a/lib/graph2.compiled.js +++ b/lib/graph2.compiled.js @@ -1,107 +1,4 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ -( function ( $, mw, vg ) { - - 'use strict'; - /* global require */ - - var wrapper, - VegaWrapper = require( 'graph-shared' ); - - wrapper = new VegaWrapper( - vg.util, true, - mw.config.get( 'wgGraphIsTrusted' ), - mw.config.get( 'wgGraphAllowedDomains' ), - false, - function ( warning ) { - mw.log.warn( warning ); - }, function ( opt ) { - // Parse URL - var uri = new mw.Uri( opt.url ); - // reduce confusion, only keep expected values - if ( uri.port ) { - uri.host += ':' + uri.port; - delete uri.port; - } - // If url begins with protocol:///... mark it as having relative host - if ( /^[a-z]+:\/\/\//.test( opt.url ) ) { - uri.isRelativeHost = true; - } - if ( uri.protocol ) { - // All other libs use trailing colon in the protocol field - uri.protocol += ':'; - } - // Node's path includes the query, whereas pathname is without the query - // Standardizing on pathname - uri.pathname = uri.path; - delete uri.path; - return uri; - }, function ( uri, opt ) { - // Format URL back into a string - // Revert path into pathname - uri.path = uri.pathname; - delete uri.pathname; - - if ( location.host.toLowerCase() === uri.host.toLowerCase() ) { - if ( !mw.config.get( 'wgGraphIsTrusted' ) ) { - // Only send this header when hostname is the same. - // This is broader than the same-origin policy, - // but playing on the safer side. - opt.headers = { 'Treat-as-Untrusted': 1 }; - } - } else if ( opt.addCorsOrigin ) { - // All CORS api calls require origin parameter. - // It would be better to use location.origin, - // but apparently it's not universal yet. - uri.query.origin = location.protocol + '//' + location.host; - } - - if ( uri.protocol[ uri.protocol.length - 1 ] === ':' ) { - uri.protocol = uri.protocol.substring( 0, uri.protocol.length - 1 ); - } - - return uri.toString(); - } ); - - /** - * Set up drawing canvas inside the given element and draw graph data - * - * @param {HTMLElement} element - * @param {Object|string} data graph spec - * @param {Function} [callback] function(error) called when drawing is done - */ - mw.drawVegaGraph = function ( element, data, callback ) { - vg.parse.spec( data, function ( error, chart ) { - if ( !error ) { - chart( { el: element } ).update(); - } - if ( callback ) { - callback( error ); - } - } ); - }; - - mw.hook( 'wikipage.content' ).add( function ( $content ) { - var specs = mw.config.get( 'wgGraphSpecs' ); - if ( !specs ) { - return; - } - $content.find( '.mw-graph.mw-graph-always' ).each( function () { - var graphId = $( this ).data( 'graph-id' ); - if ( !specs.hasOwnProperty( graphId ) ) { - mw.log.warn( graphId ); - } else { - mw.drawVegaGraph( this, specs[ graphId ], function ( error ) { - if ( error ) { - mw.log.warn( error ); - } - } ); - } - } ); - } ); - -}( jQuery, mediaWiki, vg ) ); - -},{"graph-shared":4}],2:[function(require,module,exports){ 'use strict'; /** @@ -128,7 +25,7 @@ .join('|') + ')$', 'i'); }; -},{}],3:[function(require,module,exports){ +},{}],2:[function(require,module,exports){ 'use strict'; /* global module */ @@ -195,7 +92,7 @@ } -},{}],4:[function(require,module,exports){ +},{}],3:[function(require,module,exports){ 'use strict'; /* global module */ @@ -203,34 +100,43 @@ parseWikidataValue = require('wd-type-parser'); module.exports = VegaWrapper; +module.exports.removeColon = removeColon; + +/** + * Utility function to remove trailing colon from a protocol + * @param {string} protocol + * @return {string} + */ +function removeColon(protocol) { + return protocol && protocol.length && protocol[protocol.length - 1] === ':' + ? protocol.substring(0, protocol.length - 1) : protocol; +} /** * Shared library to wrap around vega code - * @param {Object} datalib Vega's datalib object - * @param {Object} datalib.load Vega's data loader - * @param {Function} datalib.load.loader Vega's data loader function - * @param {Function} datalib.extend similar to jquery's extend() - * @param {boolean} useXhr true if we should use XHR, false for node.js http loading - * @param {boolean} isTrusted true if the graph spec can be trusted - * @param {Object} domains allowed protocols and a list of their domains - * @param {Object} domainMap domain remapping - * @param {Function} logger - * @param {Function} parseUrl - * @param {Function} formatUrl + * @param {Object} wrapperOpts Configuration options + * @param {Object} wrapperOpts.datalib Vega's datalib object + * @param {Object} wrapperOpts.datalib.load Vega's data loader + * @param {Function} wrapperOpts.datalib.load.loader Vega's data loader function + * @param {Function} wrapperOpts.datalib.extend similar to jquery's extend() + * @param {boolean} wrapperOpts.useXhr true if we should use XHR, false for node.js http loading + * @param {boolean} wrapperOpts.isTrusted true if the graph spec can be trusted + * @param {Object} wrapperOpts.domains allowed protocols and a list of their domains + * @param {Object} wrapperOpts.domainMap domain remapping + * @param {Function} wrapperOpts.logger + * @param {Function} wrapperOpts.parseUrl + * @param {Function} wrapperOpts.formatUrl + * @param {string} [wrapperOpts.languageCode] * @constructor */ -function VegaWrapper(datalib, useXhr, isTrusted, domains, domainMap, logger, parseUrl, formatUrl) { +function VegaWrapper(wrapperOpts) { var self = this; - self.isTrusted = isTrusted; - self.domains = domains; - self.domainMap = domainMap; - self.logger = logger; - self.objExtender = datalib.extend; - self.parseUrl = parseUrl; - self.formatUrl = formatUrl; + // Copy all options into this object + self.objExtender = wrapperOpts.datalib.extend; + self.objExtender(self, wrapperOpts); self.validators = {}; - datalib.load.loader = function (opt, callback) { + self.datalib.load.loader = function (opt, callback) { var error = callback || function (e) { throw e; }, url; try { @@ -245,21 +151,21 @@ return self.dataParser(error, data, opt, callback); }; - if (useXhr) { - return datalib.load.xhr(url, opt, cb); + if (self.useXhr) { + return self.datalib.load.xhr(url, opt, cb); } else { - return datalib.load.http(url, opt, cb); + return self.datalib.load.http(url, opt, cb); } }; - datalib.load.sanitizeUrl = self.sanitizeUrl.bind(self); + self.datalib.load.sanitizeUrl = self.sanitizeUrl.bind(self); // Prevent accidental use - datalib.load.file = alwaysFail; - if (useXhr) { - datalib.load.http = alwaysFail; + self.datalib.load.file = alwaysFail; + if (self.useXhr) { + self.datalib.load.http = alwaysFail; } else { - datalib.load.xhr = alwaysFail; + self.datalib.load.xhr = alwaysFail; } } @@ -306,15 +212,10 @@ * @private */ VegaWrapper.prototype._getProtocolDomains = function _getProtocolDomains(protocol) { - return this.domains[protocol] || this.domains[this.removeColon(protocol)]; + return this.domains[protocol] || this.domains[removeColon(protocol)]; }; -VegaWrapper.prototype.removeColon = function removeColon(protocol) { - return protocol && protocol.length && protocol[protocol.length - 1] === ':' - ? protocol.substring(0, protocol.length - 1) : protocol; -} - -/**this +/** * Validate and update urlObj to be safe for client-side and server-side usage * @param {Object} opt passed by the vega loader, and will add 'graphProtocol' param * @returns {boolean} true on success @@ -414,24 +315,39 @@ break; case 'wikiraw:': + case 'tabular:': + case 'tabularinfo:': // wikiraw:///MyPage/data - // Get raw content of a wiki page, where the path is the title + // Get content of a wiki page, where the path is the title // of the page with an additional leading '/' which gets removed. // Uses mediawiki api, and extract the content after the request // Query value must be a valid MediaWiki title string, but we only ensure - // there is no pipe symbol, the rest is handlered by the api. + // there is no pipe symbol, the rest is handled by the api. decodedPathname = decodeURIComponent(urlParts.pathname); if (!/^\/[^|]+$/.test(decodedPathname)) { - throw new Error('wikiraw: invalid title'); + throw new Error(urlParts.protocol + ' invalid title'); } - urlParts.query = { - format: 'json', - formatversion: '2', - action: 'query', - prop: 'revisions', - rvprop: 'content', - titles: decodedPathname.substring(1) - }; + if (urlParts.protocol === 'wikiraw:') { + urlParts.query = { + format: 'json', + formatversion: '2', + action: 'query', + prop: 'revisions', + rvprop: 'content', + titles: decodedPathname.substring(1) + }; + } else { + urlParts.query = { + format: 'json', + formatversion: '2', + action: 'jsondata', + title: decodedPathname.substring(1) + }; + if (this.languageCode) { + urlParts.query.uselang = this.languageCode; + } + } + urlParts.pathname = '/w/api.php'; urlParts.protocol = sanitizedHost.protocol; opt.addCorsOrigin = true; @@ -479,7 +395,7 @@ throw new Error(opt.graphProtocol + ' missing ids or query parameter in: ' + opt.url); } // the query object is not modified - urlParts.pathname = '/' + this.removeColon(opt.graphProtocol); + urlParts.pathname = '/' + removeColon(opt.graphProtocol); break; case 'mapsnapshot:': @@ -562,29 +478,63 @@ }; /** + * Parses the response from MW Api, throwing an error or logging warnings + */ +VegaWrapper.prototype.parseMWApiResponse = function parseMWApiResponse(data) { + data = JSON.parse(data); + if (data.error) { + throw new Error('API error: ' + JSON.stringify(data.error)); + } + if (data.warnings) { + this.logger('API warnings: ' + JSON.stringify(data.warnings)); + } + return data; +}; + +/** * Performs post-processing of the data requested by the graph's spec, and throw on error */ VegaWrapper.prototype.parseDataOrThrow = function parseDataOrThrow(data, opt) { + var result; switch (opt.graphProtocol) { case 'wikiapi:': + data = this.parseMWApiResponse(data); + break; case 'wikiraw:': - // This was an API call - check for errors - data = JSON.parse(data); - if (data.error) { - throw new Error('API error: ' + JSON.stringify(data.error)); - } - if (data.warnings) { - this.logger('API warnings: ' + JSON.stringify(data.warnings)); - } - if (opt.graphProtocol === 'wikiraw:') { - try { - data = data.query.pages[0].revisions[0].content; - } catch (e) { - throw new Error('Page content not available ' + opt.url); - } + data = this.parseMWApiResponse(data); + try { + data = data.query.pages[0].revisions[0].content; + } catch (e) { + throw new Error('Page content not available ' + opt.url); } break; - + case 'tabular:': + data = this.parseMWApiResponse(data).jsondata; + result = []; + data.rows.forEach(function(v) { + var row = {}; + for (var i = 0; i < data.headers.length; i++) { + row[data.headers[i]] = v[i]; + } + result.push(row); + }); + data = result; + break; + case 'tabularinfo:': + data = this.parseMWApiResponse(data).jsondata; + result = { + license: data.license, + info: data.info, + types: {}, + titles: {}, + count: data.rows ? data.rows.length : 0 + }; + for (var i = 0; i < data.headers.length; i++) { + result.types[data.headers[i]] = data.types[i]; + result.titles[data.headers[i]] = data.titles[i]; + } + data = result; + break; case 'wikidatasparql:': data = JSON.parse(data); if (!data.results || !Array.isArray(data.results.bindings)) { @@ -612,4 +562,110 @@ throw new Error('Disabled'); } -},{"domain-validator":2,"wd-type-parser":3}]},{},[1]); +},{"domain-validator":1,"wd-type-parser":2}],4:[function(require,module,exports){ +( function ( $, mw, vg ) { + + 'use strict'; + /* global require */ + + var wrapper, + VegaWrapper = require( 'graph-shared' ); + + wrapper = new VegaWrapper( { + datalib: vg.util, + useXhr: true, + isTrusted: mw.config.get( 'wgGraphIsTrusted' ), + domains: mw.config.get( 'wgGraphAllowedDomains' ), + domainMap: false, + logger: function ( warning ) { + mw.log.warn( warning ); + }, + parseUrl: function ( opt ) { + // Parse URL + var uri = new mw.Uri( opt.url ); + // reduce confusion, only keep expected values + if ( uri.port ) { + uri.host += ':' + uri.port; + delete uri.port; + } + // If url begins with protocol:///... mark it as having relative host + if ( /^[a-z]+:\/\/\//.test( opt.url ) ) { + uri.isRelativeHost = true; + } + if ( uri.protocol ) { + // All other libs use trailing colon in the protocol field + uri.protocol += ':'; + } + // Node's path includes the query, whereas pathname is without the query + // Standardizing on pathname + uri.pathname = uri.path; + delete uri.path; + return uri; + }, + formatUrl: function ( uri, opt ) { + // Format URL back into a string + // Revert path into pathname + uri.path = uri.pathname; + delete uri.pathname; + + if ( location.host.toLowerCase() === uri.host.toLowerCase() ) { + if ( !mw.config.get( 'wgGraphIsTrusted' ) ) { + // Only send this header when hostname is the same. + // This is broader than the same-origin policy, + // but playing on the safer side. + opt.headers = { 'Treat-as-Untrusted': 1 }; + } + } else if ( opt.addCorsOrigin ) { + // All CORS api calls require origin parameter. + // It would be better to use location.origin, + // but apparently it's not universal yet. + uri.query.origin = location.protocol + '//' + location.host; + } + + uri.protocol = VegaWrapper.removeColon( uri.protocol ); + + return uri.toString(); + }, + languageCode: mw.config.get( 'wgUserLanguage' ) + } ); + + /** + * Set up drawing canvas inside the given element and draw graph data + * + * @param {HTMLElement} element + * @param {Object|string} data graph spec + * @param {Function} [callback] function(error) called when drawing is done + */ + mw.drawVegaGraph = function ( element, data, callback ) { + vg.parse.spec( data, function ( error, chart ) { + if ( !error ) { + chart( { el: element } ).update(); + } + if ( callback ) { + callback( error ); + } + } ); + }; + + mw.hook( 'wikipage.content' ).add( function ( $content ) { + var specs = mw.config.get( 'wgGraphSpecs' ); + if ( !specs ) { + return; + } + $content.find( '.mw-graph.mw-graph-always' ).each( function () { + var graphId = $( this ).data( 'graph-id' ); + if ( !specs.hasOwnProperty( graphId ) ) { + mw.log.warn( graphId ); + } else { + mw.drawVegaGraph( this, specs[ graphId ], function ( error ) { + if ( error ) { + mw.log.warn( error ); + } + } ); + } + } ); + } ); + +}( jQuery, mediaWiki, vg ) ); + +},{"graph-shared":3}]},{},[4]); diff --git a/modules/graph2.js b/modules/graph2.js index 3a81d11..ea72eb5 100644 --- a/modules/graph2.js +++ b/modules/graph2.js @@ -6,14 +6,16 @@ var wrapper, VegaWrapper = require( 'graph-shared' ); - wrapper = new VegaWrapper( - vg.util, true, - mw.config.get( 'wgGraphIsTrusted' ), - mw.config.get( 'wgGraphAllowedDomains' ), - false, - function ( warning ) { + wrapper = new VegaWrapper( { + datalib: vg.util, + useXhr: true, + isTrusted: mw.config.get( 'wgGraphIsTrusted' ), + domains: mw.config.get( 'wgGraphAllowedDomains' ), + domainMap: false, + logger: function ( warning ) { mw.log.warn( warning ); - }, function ( opt ) { + }, + parseUrl: function ( opt ) { // Parse URL var uri = new mw.Uri( opt.url ); // reduce confusion, only keep expected values @@ -34,7 +36,8 @@ uri.pathname = uri.path; delete uri.path; return uri; - }, function ( uri, opt ) { + }, + formatUrl: function ( uri, opt ) { // Format URL back into a string // Revert path into pathname uri.path = uri.pathname; @@ -54,12 +57,12 @@ uri.query.origin = location.protocol + '//' + location.host; } - if ( uri.protocol[ uri.protocol.length - 1 ] === ':' ) { - uri.protocol = uri.protocol.substring( 0, uri.protocol.length - 1 ); - } + uri.protocol = VegaWrapper.removeColon( uri.protocol ); return uri.toString(); - } ); + }, + languageCode: mw.config.get( 'wgUserLanguage' ) + } ); /** * Set up drawing canvas inside the given element and draw graph data -- To view, visit https://gerrit.wikimedia.org/r/281975 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I6eb35ea739f5827a14f85758748c313701734880 Gerrit-PatchSet: 15 Gerrit-Project: mediawiki/extensions/Graph Gerrit-Branch: master Gerrit-Owner: Yurik <yu...@wikimedia.org> Gerrit-Reviewer: MaxSem <maxsem.w...@gmail.com> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits