jenkins-bot has submitted this change and it was merged.
Change subject: Split parsoidService into more manageable files
......................................................................
Split parsoidService into more manageable files
Change-Id: I9a2a573e6afe14c911d42d4d5ecd048eff7f1865
---
M api/ParsoidService.js
A api/routes.js
A api/utils.js
3 files changed, 776 insertions(+), 728 deletions(-)
Approvals:
Subramanya Sastry: Looks good to me, approved
jenkins-bot: Verified
diff --git a/api/ParsoidService.js b/api/ParsoidService.js
index 903b281..bf6c2ba 100644
--- a/api/ParsoidService.js
+++ b/api/ParsoidService.js
@@ -1,309 +1,34 @@
-/*
+/**
* Simple Parsoid web service.
*/
"use strict";
require('../lib/core-upgrade.js');
-/**
- * @class ParserServiceModule
- * @singleton
- * @private
- */
-
// global includes
var express = require('express'),
- domino = require('domino'),
hbs = require('handlebars'),
- childProc = require('child_process'),
cluster = require('cluster'),
- fs = require('fs'),
- path = require('path'),
- url = require('url'),
- util = require('util'),
- pkg = require('../package.json'),
- Diff = require('../lib/mediawiki.Diff.js').Diff,
- LogData = require('../lib/LogData.js').LogData;
+ path = require('path');
-// local includes
-var mp = '../lib/';
+// relative includes
+var mp = '../lib/',
+ ParsoidConfig = require( mp + 'mediawiki.ParsoidConfig' ).ParsoidConfig;
function ParsoidService(options) {
- /**
- * The name of this instance.
- * @property {string}
- */
- var instanceName = cluster.isWorker ? 'worker(' + process.pid + ')' :
'master';
- console.log( ' - ' + instanceName + ' loading...' );
+ var instanceName = cluster.isWorker
+ ? 'worker(' + process.pid + ')'
+ : 'master';
- var WikitextSerializer = require(mp +
'mediawiki.WikitextSerializer.js').WikitextSerializer,
- SelectiveSerializer = require( mp +
'mediawiki.SelectiveSerializer.js' ).SelectiveSerializer,
- Util = require( mp + 'mediawiki.Util.js' ).Util,
- DU = require( mp + 'mediawiki.DOMUtils.js' ).DOMUtils,
- libtr = require(mp + 'mediawiki.ApiRequest.js'),
- ParsoidConfig = require( mp + 'mediawiki.ParsoidConfig'
).ParsoidConfig,
- MWParserEnvironment = require( mp +
'mediawiki.parser.environment.js' ).MWParserEnvironment,
- TemplateRequest = libtr.TemplateRequest;
+ console.log( " - %s loading ...", instanceName );
-
-/**
- * Set header, but only if response hasn't been sent.
- *
- * @method
- * @param {MWParserEnvironment} env
- * @param {Response} res The response object from our routing function.
- * @property {Function} Serializer
- */
- function setHeader (res, env) {
- if (env.responseSent) {
- return;
- } else {
- res.setHeader.apply(res,
Array.prototype.slice.call(arguments, 2));
- }
- }
-
- /**
- * End response, but only if response hasn't been sent.
- *
- * @method
- * @param {MWParserEnvironment} env
- * @param {Response} res The response object from our routing function.
- * @property {Function} Serializer
- */
- function endResponse(res, env) {
- if (env.responseSent) {
- return;
- } else {
- env.responseSent = true;
- res.end.apply(res,
Array.prototype.slice.call(arguments, 2));
- env.log("end/response");
- }
- }
-
- /**
- * Send response, but only if response hasn't been sent.
- *
- * @method
- * @param {MWParserEnvironment} env
- * @param {Response} res The response object from our routing function.
- * @property {Function} Serializer
- */
- function sendResponse(res, env) {
- if (env.responseSent) {
- return;
- } else {
- env.responseSent = true;
- res.send.apply(res,
Array.prototype.slice.call(arguments, 2));
- }
- }
-
- /**
- * Render response, but only if response hasn't been sent.
- */
-
- function renderResponse(res, env) {
- if (env.responseSent) {
- return;
- } else {
- env.responseSent = true;
- res.render.apply(res,
Array.prototype.slice.call(arguments, 2));
- }
- }
-
- function jsonResponse(res, env) {
- if (env.responseSent) {
- return;
- } else {
- env.responseSent = true;
- res.json.apply(res,
Array.prototype.slice.call(arguments, 2));
- }
- }
-
- /**
- * The global parsoid configuration object.
- * @property {ParsoidConfig}
- */
+ // The global parsoid configuration object.
var parsoidConfig = new ParsoidConfig( options, null );
- /**
- * The serializer to use for the web requests.
- * @property {Function} Serializer
- */
- var Serializer = parsoidConfig.useSelser ? SelectiveSerializer :
WikitextSerializer;
-
- /**
- * Get the interwiki regexp.
- *
- * @method
- * @returns {RegExp} The regular expression that matches to all
interwikis accepted by the API.
- */
- var interwikiRE;
- function getInterwikiRE() {
- // this RE won't change -- so, cache it
- if (!interwikiRE) {
- interwikiRE = parsoidConfig.interwikiRegexp;
- }
- return interwikiRE;
- }
-
- var htmlSpecialChars = function ( s ) {
- return s.replace(/&/g,'&')
- .replace(/</g,'<')
- .replace(/"/g,'"')
- .replace(/'/g,''');
- };
-
- var roundTripDiff = function ( selser, req, res, env, document ) {
- var out = [];
-
- var finalCB = function () {
- var i;
- out = out.join('');
-
- // Strip selser trigger comment
- out = out.replace(/<!--rtSelserEditTestComment-->\n*$/,
'');
-
- // Emit base href so all relative urls resolve properly
- var hNodes = document.body.firstChild.childNodes;
- var headNodes = "";
- for (i = 0; i < hNodes.length; i++) {
- if (hNodes[i].nodeName.toLowerCase() ===
'base') {
- headNodes +=
DU.serializeNode(hNodes[i]);
- break;
- }
- }
-
- var bNodes = document.body.childNodes;
- var bodyNodes = "";
- for (i = 0; i < bNodes.length; i++) {
- bodyNodes += DU.serializeNode(bNodes[i]);
- }
-
- var htmlSpeChars = htmlSpecialChars(out);
-
- var src = env.page.src.replace(/\n(?=\n)/g, '\n ');
- out = out.replace(/\n(?=\n)/g, '\n ');
-
- var patch = Diff.convertChangesToXML(
Diff.diffLines(src, out) );
-
- setHeader(res, env, 'X-Parsoid-Performance',
env.getPerformanceHeader());
-
- renderResponse(res, env, "roundtrip", {
- headers: headNodes,
- bodyNodes: bodyNodes,
- htmlSpeChars: htmlSpeChars,
- patch: patch,
- reqUrl: req.url
- });
-
- env.log("info", "completed parsing in",
env.performance.duration, "ms");
- };
-
- // Re-parse the HTML to uncover foster-parenting issues
- document = domino.createDocument(document.outerHTML);
-
- var Serializer = selser ? SelectiveSerializer :
WikitextSerializer;
- new Serializer({ env: env }).serializeDOM(
- document.body,
- function( chunk ) { out.push(chunk); },
- finalCB
- );
- };
-
- function handleCacheRequest( env, req, res, cb, src, cacheErr, cacheSrc
) {
- var errorHandlingCB = function ( src, err, doc ) {
- if ( err ) {
- env.log("fatal/request", err);
- return;
- }
- cb( req, res, src, doc );
- };
-
- if ( cacheErr ) {
- // No luck with the cache request, just proceed as
normal.
- Util.parse(env, errorHandlingCB, null, src);
- return;
- }
- // Extract transclusion and extension content from the DOM
- var expansions = DU.extractExpansions(DU.parseHTML(cacheSrc));
-
- // Figure out what we can reuse
- var parsoidHeader = JSON.parse(req.headers['x-parsoid'] ||
'{}');
- if (parsoidHeader.cacheID) {
- if (parsoidHeader.mode === 'templates') {
- // Transclusions need to be updated, so don't
reuse them.
- expansions.transclusions = {};
- } else if (parsoidHeader.mode === 'files') {
- // Files need to be updated, so don't reuse
them.
- expansions.files = {};
- }
- }
-
- // pass those expansions into Util.parse to prime the caches.
- //console.log('expansions:', expansions);
- Util.parse(env, errorHandlingCB, null, src, expansions);
- }
-
- var parse = function ( env, req, res, cb, err, src_and_metadata ) {
- if ( err ) {
- env.log("fatal/request", err);
- return;
- }
-
- // Set the source
- env.setPageSrcInfo( src_and_metadata );
-
- // Now env.page.meta.title has the canonical title, and
- // env.page.meta.revision.parentid has the predecessor oldid
-
- // See if we can reuse transclusion or extension expansions.
- if (env.conf.parsoid.parsoidCacheURI &&
- // And don't parse twice for recursive parsoid
requests
- ! req.headers['x-parsoid-request'])
- {
- // Try to retrieve a cached copy of the content so that
we can recycle
- // template and / or extension expansions.
- var parsoidHeader = JSON.parse(req.headers['x-parsoid']
|| '{}'),
- // If we get a prevID passed in in X-Parsoid
(from our PHP
- // extension), use that explicitly. Otherwise
default to the
- // parentID.
- cacheID = parsoidHeader.cacheID ||
- env.page.meta.revision.parentid,
- cacheRequest = new
libtr.ParsoidCacheRequest(env,
- env.page.meta.title, cacheID);
- cacheRequest.once('src',
- handleCacheRequest.bind(null, env, req,
res, cb, env.page.src));
- } else {
- handleCacheRequest(env, req, res, cb, env.page.src,
"Recursive request", null);
- }
- };
-
-
- /**
- * Send a redirect response with optional code and a relative URL
- *
- * (Returns if a response has already been sent.)
- * This is not strictly HTTP spec conformant, but works in most
clients. More
- * importantly, it works both behind proxies and on the internal
network.
- */
- function relativeRedirect(args) {
- if (!args.code) {
- args.code = 302; // moved temporarily
- }
-
- if (args.res && args.env && args.env.responseSent ) {
- return;
- } else {
- args.res.writeHead(args.code, {
- 'Location': args.path
- });
- args.res.end();
- }
- }
-
- /* -------------------- web app access points below
--------------------- */
+ // Load routes
+ var routes = require('./routes')( parsoidConfig );
var app = express.createServer();
@@ -324,7 +49,7 @@
app.use(express.favicon(path.join(__dirname, "favicon.ico")));
// Increase the form field size limit from the 2M default.
- app.use(express.bodyParser({maxFieldsSize: 15 * 1024 * 1024}));
+ app.use(express.bodyParser({ maxFieldsSize: 15 * 1024 * 1024 }));
// Support gzip / deflate transfer-encoding
app.use(express.compress());
@@ -332,441 +57,8 @@
// limit upload file size
app.use(express.limit('15mb'));
- app.get('/', function(req, res){
- res.render('home');
- });
-
- function interParams( req, res, next ) {
- res.local('iwp', req.params[0] || parsoidConfig.defaultWiki ||
'');
- res.local('pageName', req.params[1] || '');
- res.local('oldid', req.query.oldid || null);
- next();
- }
-
- function parserEnvMw( req, res, next ) {
- MWParserEnvironment.getParserEnv( parsoidConfig, null,
res.local('iwp'), res.local('pageName'), req.headers.cookie, function ( err,
env ) {
-
- function errCB ( res, env, logData, callback ) {
- try {
- if ( !env.responseSent ) {
- setHeader(res, env,
'Content-Type', 'text/plain; charset=UTF-8' );
- sendResponse(res, env,
logData.fullMsg(), logData.code || 500);
- if ( typeof callback ===
'function' ) {
- res.on('finish',
callback);
- }
- return;
- }
- } catch (e) {
- console.log( e.stack || e );
- res.end();
- }
- if ( typeof callback === 'function' ) {
- callback();
- }
- }
-
- if ( err ) {
- return errCB(res, {}, new LogData(null,
"error", err));
- }
-
- env.logger.registerBackend(/fatal(\/.*)?/,
errCB.bind(this, res, env));
- res.local("env", env);
- next();
- });
- }
-
- // robots.txt: no indexing.
- app.get(/^\/robots.txt$/, function ( req, res ) {
- res.end("User-agent: *\nDisallow: /\n" );
- });
-
- // Redirects for old-style URL compatibility
- app.get( new RegExp( '^/((?:_rt|_rtve)/)?(' + getInterwikiRE() +
- '):(.*)$' ), function ( req, res ) {
- if ( req.params[0] ) {
- relativeRedirect({"path" : '/' + req.params[0] +
req.params[1] + '/' + req.params[2], "res" : res, "code" : 301});
- } else {
- relativeRedirect({"path" : '/' + req.params[1] + '/' +
req.params[2], "res" : res, "code": 301});
- }
- res.end();
- });
-
- function action( res ) {
- return [ "", res.local('iwp'), res.local('pageName') ].join(
"/" );
- }
-
- // Form-based HTML DOM -> wikitext interface for manual testing
- app.get( new RegExp('/_html/(?:(' + getInterwikiRE() + ')/(.*))?'),
interParams, parserEnvMw, function ( req, res ) {
- var env = res.local('env');
- renderResponse(res, env, "form", {
- title: "Your HTML DOM:",
- action: action(res),
- name: "html"
- });
- });
-
- // Form-based wikitext -> HTML DOM interface for manual testing
- app.get( new RegExp('/_wikitext/(?:(' + getInterwikiRE() + ')/(.*))?'),
interParams, parserEnvMw, function ( req, res ) {
- var env = res.local('env');
- renderResponse(res, env, "form", {
- title: "Your wikitext:",
- action: action(res),
- name: "wt"
- });
- });
-
- // Round-trip article testing
- app.get( new RegExp('/_rt/(?:(' + getInterwikiRE() + ')/(.*))?'),
interParams, parserEnvMw, function(req, res) {
- var env = res.local('env');
- var target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
-
- req.connection.setTimeout(300 * 1000);
- env.log('info', 'starting parsing');
-
- var oldid = null;
- if ( req.query.oldid ) {
- oldid = req.query.oldid;
- }
- var tpr = new TemplateRequest( env, target, oldid );
- tpr.once('src', parse.bind( tpr, env, req, res,
roundTripDiff.bind( null, false ) ));
- });
-
- // Round-trip article testing with newline stripping for editor-created
HTML
- // simulation
- app.get( new RegExp('/_rtve/(' + getInterwikiRE() + ')/(.*)'),
interParams, parserEnvMw, function(req, res) {
- var env = res.local('env');
- var target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
-
- env.log('info', 'starting parsing');
- var oldid = null;
- if ( req.query.oldid ) {
- oldid = req.query.oldid;
- }
- var tpr = new TemplateRequest( env, target, oldid ),
- cb = function ( req, res, src, document ) {
- // strip newlines from the html
- var html =
document.innerHTML.replace(/[\r\n]/g, ''),
- newDocument = DU.parseHTML(html);
- roundTripDiff( false, req, res, src,
newDocument );
- };
-
- tpr.once('src', parse.bind( tpr, env, req, res, cb ));
- });
-
- // Round-trip article testing with selser over re-parsed HTML.
- app.get( new RegExp('/_rtselser/(' + getInterwikiRE() + ')/(.*)'),
interParams, parserEnvMw, function (req, res) {
- var env = res.local('env');
- var target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
-
- env.log('info', 'starting parsing');
- var oldid = null;
- if ( req.query.oldid ) {
- oldid = req.query.oldid;
- }
- var tpr = new TemplateRequest( env, target, oldid ),
- tprCb = function ( req, res, src, document ) {
- var newDocument = DU.parseHTML(
DU.serializeNode(document) ),
- newNode =
newDocument.createComment('rtSelserEditTestComment');
- newDocument.body.appendChild(newNode);
- roundTripDiff( true, req, res, src, newDocument
);
- };
-
- tpr.once( 'src', parse.bind( tpr, env, req, res, tprCb ) );
- });
-
- // Form-based round-tripping for manual testing
- app.get( new RegExp('/_rtform/(?:(' + getInterwikiRE() + ')/(.*))?'),
interParams, parserEnvMw, function ( req, res ) {
- var env = res.local('env');
- renderResponse(res, env, "form", {
- title: "Your wikitext:",
- action: "/_rtform/" + res.local('pageName'),
- name: "content"
- });
- });
-
- app.post( new RegExp('/_rtform/(?:(' + getInterwikiRE() + ')/(.*))?'),
interParams, parserEnvMw, function ( req, res ) {
- var env = res.local('env');
- setHeader(res, env, 'Content-Type', 'text/html; charset=UTF-8');
- // we don't care about \r, and normalize everything to \n
- parse( env, req, res, roundTripDiff.bind( null, false ), null, {
- revision: { '*': req.body.content.replace(/\r/g, '') }
- });
- });
-
- function html2wt( req, res, html ) {
- var env = res.local('env');
- env.page.id = req.body.oldid || null;
-
- if ( env.conf.parsoid.allowCORS ) {
- // allow cross-domain requests (CORS) so that parsoid
service
- // can be used by third-party sites
- setHeader(res, env, 'Access-Control-Allow-Origin',
- env.conf.parsoid.allowCORS );
- }
-
- var html2wtCb = function () {
- var doc;
- try {
- doc = DU.parseHTML( html.replace( /\r/g, '' ) );
- } catch ( e ) {
- env.log("fatal", e, "There was an error in the
HTML5 parser!");
- return;
- }
-
- try {
- var out = [];
- new Serializer( { env: env, oldid: env.page.id
} ).serializeDOM(
- doc.body,
- function ( chunk ) {
- out.push( chunk );
- }, function () {
- setHeader(res, env,
'Content-Type', 'text/x-mediawiki; charset=UTF-8' );
- setHeader(res, env,
'X-Parsoid-Performance', env.getPerformanceHeader() );
- endResponse(res, env,
out.join( '' ) );
- } );
- } catch ( e ) {
- env.log("fatal", e);
- return;
- }
- };
-
- if ( env.conf.parsoid.fetchWT ) {
- var target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
- var tpr = new TemplateRequest( env, target, env.page.id
);
- tpr.once( 'src', function ( err, src_and_metadata ) {
- if ( err ) {
- env.log("error", "There was an error
fetching the original wikitext for", target, err);
- } else {
- env.setPageSrcInfo( src_and_metadata );
- }
- html2wtCb();
- } );
- } else {
- html2wtCb();
- }
- }
-
- function wt2html( req, res, wt, v2 ) {
- var env = res.local('env'),
- prefix = res.local('iwp'),
- oldid = res.local('oldid'),
- target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
-
- // Set the timeout to 600 seconds..
- req.connection.setTimeout( 600 * 1000 );
-
- if ( env.conf.parsoid.allowCORS ) {
- // allow cross-domain requests (CORS) so that parsoid
service
- // can be used by third-party sites
- setHeader( res, env, 'Access-Control-Allow-Origin',
env.conf.parsoid.allowCORS );
- }
-
- var tmpCb;
- if ( wt ) {
- wt = wt.replace( /\r/g, '' );
- env.log('info', 'starting parsing');
-
- // clear default page name
- if ( !res.local('pageName') ) {
- env.page.name = '';
- }
-
- var parser =
env.pipelineFactory.getPipeline('text/x-mediawiki/full');
- parser.once( 'document', function ( document ) {
- // Don't cache requests when wt is set in case
somebody uses
- // GET for wikitext parsing
- setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
- sendRes( req.body.body ? document.body :
document );
- });
-
- tmpCb = function ( err, src_and_metadata ) {
- if ( err ) {
- env.log("fatal/request", err);
- return;
- }
-
- // Set the source
- env.setPageSrcInfo( src_and_metadata );
-
- try {
- parser.processToplevelDoc( wt );
- } catch ( e ) {
- env.log("fatal", e);
- return;
- }
- };
-
- if ( !res.local('pageName') || !oldid ) {
- // no pageName supplied; don't fetch the page
source
- tmpCb( null, wt );
- return;
- }
-
- } else {
- if ( oldid ) {
- env.log('info', 'starting parsing');
-
- if ( !req.headers.cookie ) {
- setHeader(res, env, 'Cache-Control',
's-maxage=2592000' );
- } else {
- // Don't cache requests with a session
- setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
- }
-
- // Indicate the MediaWiki revision in a header
as well for
- // ease of extraction in clients.
- setHeader(res, env, 'content-revision-id',
oldid);
-
- tmpCb = parse.bind( null, env, req, res,
function ( req, res, src, doc ) {
- sendRes( doc.documentElement );
- });
- } else {
- // Don't cache requests with no oldid
- setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
- tmpCb = function ( err, src_and_metadata ) {
- if ( err ) {
- env.log("fatal/request", err);
- return;
- }
-
- // Set the source
- env.setPageSrcInfo( src_and_metadata );
- oldid = env.page.meta.revision.revid;
-
- var path = "/";
- if ( v2 ) {
- path += [
- "v2",
- url.parse(
env.conf.parsoid.interwikiMap.get( prefix ) ).host,
- encodeURIComponent(
target ),
- v2.format,
- oldid
- ].join("/");
- } else {
- path += [
- prefix,
- encodeURIComponent(
target ) + "?oldid=" + oldid
- ].join("/");
- }
-
- // Redirect to oldid
- relativeRedirect({ "path": path, "res":
res, "env": env });
- env.log("info", "redirected to
revision", env.page.meta.revision.revid);
- };
- }
- }
-
- var tpr = new TemplateRequest( env, target, oldid );
- tpr.once( 'src', tmpCb );
-
- function sendRes( doc ) {
- try {
- setHeader(res, env, 'X-Parsoid-Performance',
env.getPerformanceHeader());
- if ( v2 && v2.format === "pagebundle" ) {
- var dp =
doc.ownerDocument.getElementById('mw-data-parsoid');
- dp.parentNode.removeChild(dp);
- jsonResponse(res, env, {
- html: DU.serializeNode( doc ),
- "data-parsoid":
JSON.parse(dp.text)
- });
- } else {
- setHeader(res, env, 'Content-Type',
'text/html; charset=UTF-8');
- endResponse(res, env,
DU.serializeNode( doc ));
- }
- env.log("info", "completed parsing in",
env.performance.duration, "ms");
- } catch (e) {
- env.log("fatal/request", e);
- }
- }
- }
-
- // Attempt to define a new version of the API
- // /v2/{domain}/{title}/{format}/{revision}
-
- var supportedFormats = new Set([ "pagebundle", "html" ]);
-
- function v2Middle( req, res, next ) {
- function errOut(err) {
- // FIXME: provide more consistent error handling.
- sendResponse( res, {}, err, 404 );
- }
-
- var iwp = parsoidConfig.reverseIWMap.get( req.params.domain );
- if ( !iwp ) {
- return errOut("Invalid domain.");
- }
- res.local('iwp', iwp);
-
- res.local('format', req.params.format || "html");
- if ( !supportedFormats.has( res.local('format') ) ) {
- return errOut("Invalid format.");
- }
-
- res.local('pageName', req.params.title);
- res.local('oldid', req.params.revision || null);
- next();
- }
-
- app.get('/v2/:domain/:title/:format?/:revision?', v2Middle,
parserEnvMw, function(req, res) {
- var v2 = { format: res.local("format") };
- if ( v2.format === "pagebundle" ) {
- res.local('env').conf.parsoid.storeDataParsoid = true;
- }
- wt2html( req, res, null, v2 );
- });
-
- // Regular article parsing
- app.get(new RegExp('/(' + getInterwikiRE() + ')/(.*)'), interParams,
parserEnvMw, function(req, res) {
- wt2html( req, res );
- });
-
- // Regular article serialization using POST
- app.post(new RegExp('/(' + getInterwikiRE() + ')/(.*)'), interParams,
parserEnvMw, function(req, res) {
- // parse html or wt
- if ( req.body.wt ) {
- wt2html( req, res, req.body.wt );
- } else {
- html2wt( req, res, req.body.html || req.body.content ||
'' );
- }
- });
-
- var version = {
- name: pkg.name,
- version: pkg.version
- };
-
- function gitVersion( cb ) {
- fs.exists( path.join( __dirname, '/../.git' ), function (
exists ) {
- if ( !exists ) {
- cb();
- return;
- }
- childProc.exec(
- 'git rev-parse HEAD',
- function ( error, stdout, stderr ) {
- if ( !error ) {
- version.sha = stdout.slice(0,
-1);
- }
- cb();
- });
- });
- }
-
- /**
- * Return Parsoid version based on package.json + git sha1 if available
- */
- app.get( "/_version", function ( req, res ) {
- res.json( version );
- });
-
- // Get host and port from the environment, if available
- // VCAP_APP_PORT is for appfog.com support
- var port = parsoidConfig.serverPort ||
- process.env.VCAP_APP_PORT || process.env.PORT || 8000;
- // default bind all
- var host = parsoidConfig.serverInterface || process.env.INTERFACE;
-
- app.on( 'error', function( err ) {
+ // Catch errors
+ app.on('error', function( err ) {
if ( err.errno === "EADDRINUSE" ) {
console.error( "Port %d is already in use. Exiting.",
port );
cluster.worker.disconnect();
@@ -775,12 +67,49 @@
}
});
- gitVersion( function () {
- app.listen( port, host );
- console.log( ' - ' + instanceName + ' ready on ' +
- (host||'') + ':' + port );
- });
+ // Routes
+
+ var i = routes.interParams,
+ p = routes.parserEnvMw,
+ v = routes.v2Middle;
+
+ function re(str) { return new RegExp(str); }
+
+ // Regexp that matches to all interwikis accepted by the API.
+ var iwRe = parsoidConfig.interwikiRegexp;
+
+ app.get( '/', routes.home );
+ app.get( "/_version", routes.version );
+ app.get( /^\/robots.txt$/, routes.robots );
+
+ app.get( re( '^/((?:_rt|_rtve)/)?(' + iwRe + '):(.*)$' ),
routes.redirectOldStyle );
+ app.get( re('/_html/(?:(' + iwRe + ')/(.*))?'), i, p,
routes.html2wtForm );
+ app.get( re('/_wikitext/(?:(' + iwRe + ')/(.*))?'), i, p,
routes.wt2htmlForm );
+ app.get( re('/_rt/(?:(' + iwRe + ')/(.*))?'), i, p,
routes.roundtripTesting );
+ app.get( re('/_rtve/(' + iwRe + ')/(.*)'), i, p,
routes.roundtripTestingNL );
+ app.get( re('/_rtselser/(' + iwRe + ')/(.*)'), i, p,
routes.roundtripSelser );
+ app.get( re('/_rtform/(?:(' + iwRe + ')/(.*))?'), i, p,
routes.get_rtForm );
+ app.post( re('/_rtform/(?:(' + iwRe + ')/(.*))?'), i, p,
routes.post_rtForm );
+ app.get( re('/(' + iwRe + ')/(.*)'), i, p, routes.wt2html );
+ app.post( re('/(' + iwRe + ')/(.*)'), i, p, routes.html2wt );
+
+ // Attempt to define a new version of the API
+ // /v2/{domain}/{title}/{format}/{revision}
+
+ app.get('/v2/:domain/:title/:format?/:revision?', v, p,
routes.v2_wt2html );
+
+
+ // Get host and port from the environment, if available
+ // VCAP_APP_PORT is for appfog.com support
+ var port = parsoidConfig.serverPort ||
+ process.env.VCAP_APP_PORT || process.env.PORT || 8000;
+ // default bind all
+ var host = parsoidConfig.serverInterface || process.env.INTERFACE;
+
+ app.listen( port, host );
+
+ console.log( " - %s ready on %s:%s", instanceName, host || "", port );
}
module.exports = {
diff --git a/api/routes.js b/api/routes.js
new file mode 100644
index 0000000..f2c9ddb
--- /dev/null
+++ b/api/routes.js
@@ -0,0 +1,614 @@
+"use strict";
+
+require('es6-shim');
+
+var path = require('path'),
+ fs = require('fs'),
+ url = require('url'),
+ util = require('util'),
+ childProc = require('child_process'),
+ domino = require('domino'),
+ pkg = require('../package.json'),
+ apiUtils = require('./utils');
+
+
+// relative includes
+var mp = '../lib/';
+
+var MWParserEnvironment = require( mp + 'mediawiki.parser.environment.js'
).MWParserEnvironment,
+ WikitextSerializer = require( mp + 'mediawiki.WikitextSerializer.js'
).WikitextSerializer,
+ SelectiveSerializer = require( mp + 'mediawiki.SelectiveSerializer.js'
).SelectiveSerializer,
+ LogData = require( mp + 'LogData.js' ).LogData,
+ Util = require( mp + 'mediawiki.Util.js' ).Util,
+ DU = require( mp + 'mediawiki.DOMUtils.js' ).DOMUtils,
+ ApiRequest = require( mp + 'mediawiki.ApiRequest.js' ),
+ Diff = require( mp + 'mediawiki.Diff.js' ).Diff;
+
+var ParsoidCacheRequest = ApiRequest.ParsoidCacheRequest,
+ TemplateRequest = ApiRequest.TemplateRequest;
+
+module.exports = function( parsoidConfig ) {
+
+var routes = {};
+
+
+// Helpers
+
+var Serializer = parsoidConfig.useSelser ? SelectiveSerializer :
WikitextSerializer;
+
+var supportedFormats = new Set([ "pagebundle", "html" ]);
+
+function action( res ) {
+ return [ "", res.local('iwp'), res.local('pageName') ].join( "/" );
+}
+
+var roundTripDiff = function( selser, req, res, env, document ) {
+ var out = [];
+
+ var finalCB = function () {
+ var i;
+ out = out.join('');
+
+ // Strip selser trigger comment
+ out = out.replace(/<!--rtSelserEditTestComment-->\n*$/, '');
+
+ // Emit base href so all relative urls resolve properly
+ var hNodes = document.body.firstChild.childNodes;
+ var headNodes = "";
+ for (i = 0; i < hNodes.length; i++) {
+ if (hNodes[i].nodeName.toLowerCase() === 'base') {
+ headNodes += DU.serializeNode(hNodes[i]);
+ break;
+ }
+ }
+
+ var bNodes = document.body.childNodes;
+ var bodyNodes = "";
+ for (i = 0; i < bNodes.length; i++) {
+ bodyNodes += DU.serializeNode(bNodes[i]);
+ }
+
+ var htmlSpeChars = apiUtils.htmlSpecialChars(out);
+
+ var src = env.page.src.replace(/\n(?=\n)/g, '\n ');
+ out = out.replace(/\n(?=\n)/g, '\n ');
+
+ var patch = Diff.convertChangesToXML( Diff.diffLines(src, out)
);
+
+ apiUtils.setHeader(res, env, 'X-Parsoid-Performance',
env.getPerformanceHeader());
+
+ apiUtils.renderResponse(res, env, "roundtrip", {
+ headers: headNodes,
+ bodyNodes: bodyNodes,
+ htmlSpeChars: htmlSpeChars,
+ patch: patch,
+ reqUrl: req.url
+ });
+
+ env.log("info", "completed parsing in",
env.performance.duration, "ms");
+ };
+
+ // Re-parse the HTML to uncover foster-parenting issues
+ document = domino.createDocument(document.outerHTML);
+
+ var Serializer = selser ? SelectiveSerializer : WikitextSerializer;
+ new Serializer({ env: env }).serializeDOM(
+ document.body,
+ function( chunk ) { out.push(chunk); },
+ finalCB
+ );
+};
+
+function handleCacheRequest( env, req, res, cb, src, cacheErr, cacheSrc ) {
+ var errorHandlingCB = function ( src, err, doc ) {
+ if ( err ) {
+ env.log("fatal/request", err);
+ return;
+ }
+ cb( req, res, src, doc );
+ };
+
+ if ( cacheErr ) {
+ // No luck with the cache request, just proceed as normal.
+ Util.parse(env, errorHandlingCB, null, src);
+ return;
+ }
+ // Extract transclusion and extension content from the DOM
+ var expansions = DU.extractExpansions(DU.parseHTML(cacheSrc));
+
+ // Figure out what we can reuse
+ var parsoidHeader = JSON.parse(req.headers['x-parsoid'] || '{}');
+ if (parsoidHeader.cacheID) {
+ if (parsoidHeader.mode === 'templates') {
+ // Transclusions need to be updated, so don't reuse
them.
+ expansions.transclusions = {};
+ } else if (parsoidHeader.mode === 'files') {
+ // Files need to be updated, so don't reuse them.
+ expansions.files = {};
+ }
+ }
+
+ // pass those expansions into Util.parse to prime the caches.
+ //console.log('expansions:', expansions);
+ Util.parse(env, errorHandlingCB, null, src, expansions);
+}
+
+var parse = function ( env, req, res, cb, err, src_and_metadata ) {
+ if ( err ) {
+ env.log("fatal/request", err);
+ return;
+ }
+
+ // Set the source
+ env.setPageSrcInfo( src_and_metadata );
+
+ // Now env.page.meta.title has the canonical title, and
+ // env.page.meta.revision.parentid has the predecessor oldid
+
+ // See if we can reuse transclusion or extension expansions.
+ if (env.conf.parsoid.parsoidCacheURI &&
+ // And don't parse twice for recursive parsoid requests
+ ! req.headers['x-parsoid-request'])
+ {
+ // Try to retrieve a cached copy of the content so that we can
recycle
+ // template and / or extension expansions.
+ var parsoidHeader = JSON.parse(req.headers['x-parsoid'] ||
'{}'),
+ // If we get a prevID passed in in X-Parsoid (from our
PHP
+ // extension), use that explicitly. Otherwise default
to the
+ // parentID.
+ cacheID = parsoidHeader.cacheID ||
+ env.page.meta.revision.parentid,
+ cacheRequest = new ParsoidCacheRequest(env,
+ env.page.meta.title, cacheID);
+ cacheRequest.once('src',
+ handleCacheRequest.bind(null, env, req, res,
cb, env.page.src));
+ } else {
+ handleCacheRequest(env, req, res, cb, env.page.src, "Recursive
request", null);
+ }
+};
+
+function html2wt( req, res, html ) {
+ var env = res.local('env');
+ env.page.id = req.body.oldid || null;
+
+ if ( env.conf.parsoid.allowCORS ) {
+ // allow cross-domain requests (CORS) so that parsoid service
+ // can be used by third-party sites
+ apiUtils.setHeader(res, env, 'Access-Control-Allow-Origin',
+ env.conf.parsoid.allowCORS );
+ }
+
+ var html2wtCb = function () {
+ var doc;
+ try {
+ doc = DU.parseHTML( html.replace( /\r/g, '' ) );
+ } catch ( e ) {
+ env.log("fatal", e, "There was an error in the HTML5
parser!");
+ return;
+ }
+
+ try {
+ var out = [];
+ new Serializer( { env: env, oldid: env.page.id }
).serializeDOM(
+ doc.body,
+ function ( chunk ) {
+ out.push( chunk );
+ }, function () {
+ apiUtils.setHeader(res, env,
'Content-Type', 'text/x-mediawiki; charset=UTF-8' );
+ apiUtils.setHeader(res, env,
'X-Parsoid-Performance', env.getPerformanceHeader() );
+ apiUtils.endResponse(res, env,
out.join( '' ) );
+ } );
+ } catch ( e ) {
+ env.log("fatal", e);
+ return;
+ }
+ };
+
+ if ( env.conf.parsoid.fetchWT ) {
+ var target = env.resolveTitle( env.normalizeTitle(
env.page.name ), '' );
+ var tpr = new TemplateRequest( env, target, env.page.id );
+ tpr.once( 'src', function ( err, src_and_metadata ) {
+ if ( err ) {
+ env.log("error", "There was an error fetching
the original wikitext for", target, err);
+ } else {
+ env.setPageSrcInfo( src_and_metadata );
+ }
+ html2wtCb();
+ } );
+ } else {
+ html2wtCb();
+ }
+}
+
+function wt2html( req, res, wt, v2 ) {
+ var env = res.local('env'),
+ prefix = res.local('iwp'),
+ oldid = res.local('oldid'),
+ target = env.resolveTitle( env.normalizeTitle( env.page.name ),
'' );
+
+ // Set the timeout to 600 seconds..
+ req.connection.setTimeout( 600 * 1000 );
+
+ if ( env.conf.parsoid.allowCORS ) {
+ // allow cross-domain requests (CORS) so that parsoid service
+ // can be used by third-party sites
+ apiUtils.setHeader( res, env, 'Access-Control-Allow-Origin',
env.conf.parsoid.allowCORS );
+ }
+
+ var tmpCb;
+ if ( wt ) {
+ wt = wt.replace( /\r/g, '' );
+ env.log('info', 'starting parsing');
+
+ // clear default page name
+ if ( !res.local('pageName') ) {
+ env.page.name = '';
+ }
+
+ var parser =
env.pipelineFactory.getPipeline('text/x-mediawiki/full');
+ parser.once( 'document', function ( document ) {
+ // Don't cache requests when wt is set in case somebody
uses
+ // GET for wikitext parsing
+ apiUtils.setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
+ sendRes( req.body.body ? document.body : document );
+ });
+
+ tmpCb = function ( err, src_and_metadata ) {
+ if ( err ) {
+ env.log("fatal/request", err);
+ return;
+ }
+
+ // Set the source
+ env.setPageSrcInfo( src_and_metadata );
+
+ try {
+ parser.processToplevelDoc( wt );
+ } catch ( e ) {
+ env.log("fatal", e);
+ return;
+ }
+ };
+
+ if ( !res.local('pageName') || !oldid ) {
+ // no pageName supplied; don't fetch the page source
+ tmpCb( null, wt );
+ return;
+ }
+
+ } else {
+ if ( oldid ) {
+ env.log('info', 'starting parsing');
+
+ if ( !req.headers.cookie ) {
+ apiUtils.setHeader(res, env, 'Cache-Control',
's-maxage=2592000' );
+ } else {
+ // Don't cache requests with a session
+ apiUtils.setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
+ }
+
+ // Indicate the MediaWiki revision in a header as well
for
+ // ease of extraction in clients.
+ apiUtils.setHeader(res, env, 'content-revision-id',
oldid);
+
+ tmpCb = parse.bind( null, env, req, res, function (
req, res, src, doc ) {
+ sendRes( doc.documentElement );
+ });
+ } else {
+ // Don't cache requests with no oldid
+ apiUtils.setHeader(res, env, 'Cache-Control',
'private,no-cache,s-maxage=0' );
+ tmpCb = function ( err, src_and_metadata ) {
+ if ( err ) {
+ env.log("fatal/request", err);
+ return;
+ }
+
+ // Set the source
+ env.setPageSrcInfo( src_and_metadata );
+ oldid = env.page.meta.revision.revid;
+
+ var path = "/";
+ if ( v2 ) {
+ path += [
+ "v2",
+ url.parse(
env.conf.parsoid.interwikiMap.get( prefix ) ).host,
+ encodeURIComponent( target ),
+ v2.format,
+ oldid
+ ].join("/");
+ } else {
+ path += [
+ prefix,
+ encodeURIComponent( target ) +
"?oldid=" + oldid
+ ].join("/");
+ }
+
+ // Redirect to oldid
+ apiUtils.relativeRedirect({
+ "path": path,
+ "res": res,
+ "env": env
+ });
+ env.log("info", "redirected to revision",
env.page.meta.revision.revid);
+ };
+ }
+ }
+
+ var tpr = new TemplateRequest( env, target, oldid );
+ tpr.once( 'src', tmpCb );
+
+ function sendRes( doc ) {
+ try {
+ apiUtils.setHeader(res, env, 'X-Parsoid-Performance',
env.getPerformanceHeader());
+ if ( v2 && v2.format === "pagebundle" ) {
+ var dp =
doc.ownerDocument.getElementById('mw-data-parsoid');
+ dp.parentNode.removeChild(dp);
+ apiUtils.jsonResponse(res, env, {
+ html: DU.serializeNode( doc ),
+ "data-parsoid": JSON.parse(dp.text)
+ });
+ } else {
+ apiUtils.setHeader(res, env, 'Content-Type',
'text/html; charset=UTF-8');
+ apiUtils.endResponse(res, env,
DU.serializeNode( doc ));
+ }
+ env.log("info", "completed parsing in",
env.performance.duration, "ms");
+ } catch (e) {
+ env.log("fatal/request", e);
+ }
+ }
+}
+
+// Middlewares
+
+routes.interParams = function( req, res, next ) {
+ res.local('iwp', req.params[0] || parsoidConfig.defaultWiki || '');
+ res.local('pageName', req.params[1] || '');
+ res.local('oldid', req.query.oldid || null);
+ next();
+};
+
+routes.parserEnvMw = function( req, res, next ) {
+ MWParserEnvironment.getParserEnv( parsoidConfig, null,
res.local('iwp'), res.local('pageName'), req.headers.cookie, function ( err,
env ) {
+ function errCB( res, env, logData, callback ) {
+ try {
+ if ( !env.responseSent ) {
+ apiUtils.setHeader(res, env,
'Content-Type', 'text/plain; charset=UTF-8' );
+ apiUtils.sendResponse(res, env,
logData.fullMsg(), logData.code || 500);
+ if ( typeof callback === 'function' ) {
+ res.on('finish', callback);
+ }
+ return;
+ }
+ } catch (e) {
+ console.log( e.stack || e );
+ res.end();
+ }
+ if ( typeof callback === 'function' ) {
+ callback();
+ }
+ }
+
+ if ( err ) {
+ return errCB(res, {}, new LogData(null, "error", err));
+ }
+
+ env.logger.registerBackend(/fatal(\/.*)?/, errCB.bind(this,
res, env));
+ res.local("env", env);
+ next();
+ });
+};
+
+routes.v2Middle = function( req, res, next ) {
+ function errOut(err) {
+ // FIXME: provide more consistent error handling.
+ apiUtils.sendResponse( res, {}, err, 404 );
+ }
+
+ var iwp = parsoidConfig.reverseIWMap.get( req.params.domain );
+ if ( !iwp ) {
+ return errOut("Invalid domain.");
+ }
+ res.local('iwp', iwp);
+
+ res.local('format', req.params.format || "html");
+ if ( !supportedFormats.has( res.local('format') ) ) {
+ return errOut("Invalid format.");
+ }
+
+ res.local('pageName', req.params.title);
+ res.local('oldid', req.params.revision || null);
+ next();
+};
+
+// Routes
+
+routes.home = function( req, res ){
+ res.render('home');
+};
+
+// robots.txt: no indexing.
+routes.robots = function ( req, res ) {
+ res.end("User-agent: *\nDisallow: /\n");
+};
+
+function gitVersion( cb ) {
+ fs.exists( path.join( __dirname, '/../.git' ), function ( exists ) {
+ if ( !exists ) {
+ cb();
+ return;
+ }
+ childProc.exec(
+ 'git rev-parse HEAD',
+ function ( error, stdout, stderr ) {
+ if ( !error ) {
+ versionCache.sha = stdout.slice(0, -1);
+ }
+ cb();
+ });
+ });
+}
+
+// Return Parsoid version based on package.json + git sha1 if available
+var versionCache;
+routes.version = function( req, res ) {
+ if ( versionCache ) {
+ return res.json( versionCache );
+ }
+ versionCache = {
+ name: pkg.name,
+ version: pkg.version
+ };
+ gitVersion(function() {
+ res.json( versionCache );
+ });
+};
+
+// Redirects for old-style URL compatibility
+routes.redirectOldStyle = function( req, res ) {
+ if ( req.params[0] ) {
+ apiUtils.relativeRedirect({
+ "path" : '/' + req.params[0] + req.params[1] + '/' +
req.params[2],
+ "res" : res,
+ "code" : 301
+ });
+ } else {
+ apiUtils.relativeRedirect({
+ "path" : '/' + req.params[1] + '/' + req.params[2],
+ "res" : res,
+ "code": 301
+ });
+ }
+ res.end();
+};
+
+// Form-based HTML DOM -> wikitext interface for manual testing
+routes.html2wtForm = function( req, res ) {
+ var env = res.local('env');
+ apiUtils.renderResponse(res, env, "form", {
+ title: "Your HTML DOM:",
+ action: action(res),
+ name: "html"
+ });
+};
+
+// Form-based wikitext -> HTML DOM interface for manual testing
+routes.wt2htmlForm = function( req, res ) {
+ var env = res.local('env');
+ apiUtils.renderResponse(res, env, "form", {
+ title: "Your wikitext:",
+ action: action(res),
+ name: "wt"
+ });
+};
+
+// Round-trip article testing
+routes.roundtripTesting = function(req, res) {
+ var env = res.local('env');
+ var target = env.resolveTitle( env.normalizeTitle( env.page.name ), ''
);
+
+ req.connection.setTimeout(300 * 1000);
+ env.log('info', 'starting parsing');
+
+ var oldid = null;
+ if ( req.query.oldid ) {
+ oldid = req.query.oldid;
+ }
+ var tpr = new TemplateRequest( env, target, oldid );
+ tpr.once('src', parse.bind( tpr, env, req, res, roundTripDiff.bind(
null, false ) ));
+};
+
+// Round-trip article testing with newline stripping for editor-created HTML
+// simulation
+routes.roundtripTestingNL = function(req, res) {
+ var env = res.local('env');
+ var target = env.resolveTitle( env.normalizeTitle( env.page.name ), ''
);
+
+ env.log('info', 'starting parsing');
+ var oldid = null;
+ if ( req.query.oldid ) {
+ oldid = req.query.oldid;
+ }
+ var tpr = new TemplateRequest( env, target, oldid ),
+ cb = function ( req, res, src, document ) {
+ // strip newlines from the html
+ var html = document.innerHTML.replace(/[\r\n]/g, ''),
+ newDocument = DU.parseHTML(html);
+ roundTripDiff( false, req, res, src, newDocument );
+ };
+
+ tpr.once('src', parse.bind( tpr, env, req, res, cb ));
+};
+
+// Round-trip article testing with selser over re-parsed HTML.
+routes.roundtripSelser = function(req, res) {
+ var env = res.local('env');
+ var target = env.resolveTitle( env.normalizeTitle( env.page.name ), ''
);
+
+ env.log('info', 'starting parsing');
+ var oldid = null;
+ if ( req.query.oldid ) {
+ oldid = req.query.oldid;
+ }
+ var tpr = new TemplateRequest( env, target, oldid ),
+ tprCb = function ( req, res, src, document ) {
+ var newDocument = DU.parseHTML(
DU.serializeNode(document) ),
+ newNode =
newDocument.createComment('rtSelserEditTestComment');
+ newDocument.body.appendChild(newNode);
+ roundTripDiff( true, req, res, src, newDocument );
+ };
+
+ tpr.once( 'src', parse.bind( tpr, env, req, res, tprCb ) );
+};
+
+// Form-based round-tripping for manual testing
+routes.get_rtForm = function( req, res ) {
+ var env = res.local('env');
+ apiUtils.renderResponse(res, env, "form", {
+ title: "Your wikitext:",
+ action: "/_rtform/" + res.local('pageName'),
+ name: "content"
+ });
+};
+
+// Form-based round-tripping for manual testing
+routes.post_rtForm = function( req, res ) {
+ var env = res.local('env');
+ apiUtils.setHeader(res, env, 'Content-Type', 'text/html;
charset=UTF-8');
+ // we don't care about \r, and normalize everything to \n
+ parse( env, req, res, roundTripDiff.bind( null, false ), null, {
+ revision: { '*': req.body.content.replace(/\r/g, '') }
+ });
+};
+
+// Regular article parsing
+routes.wt2html = function(req, res) {
+ wt2html( req, res );
+};
+
+// Regular article serialization using POST
+routes.html2wt = function(req, res) {
+ // parse html or wt
+ if ( req.body.wt ) {
+ wt2html( req, res, req.body.wt );
+ } else {
+ html2wt( req, res, req.body.html || req.body.content || '' );
+ }
+};
+
+
+// v2 Routes
+
+
+// Regular article parsing
+routes.v2_wt2html = function(req, res) {
+ var v2 = { format: res.local("format") };
+ if ( v2.format === "pagebundle" ) {
+ res.local('env').conf.parsoid.storeDataParsoid = true;
+ }
+ wt2html( req, res, null, v2 );
+};
+
+
+return routes;
+
+};
diff --git a/api/utils.js b/api/utils.js
new file mode 100644
index 0000000..d46a737
--- /dev/null
+++ b/api/utils.js
@@ -0,0 +1,105 @@
+"use strict";
+
+var apiUtils = module.exports = {};
+
+
+/**
+ * Send a redirect response with optional code and a relative URL
+ *
+ * (Returns if a response has already been sent.)
+ * This is not strictly HTTP spec conformant, but works in most clients. More
+ * importantly, it works both behind proxies and on the internal network.
+ */
+apiUtils.relativeRedirect = function(args) {
+ if (!args.code) {
+ args.code = 302; // moved temporarily
+ }
+
+ if (args.res && args.env && args.env.responseSent ) {
+ return;
+ } else {
+ args.res.writeHead(args.code, {
+ 'Location': args.path
+ });
+ args.res.end();
+ }
+};
+
+/**
+ * Set header, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.setHeader = function(res, env) {
+ if (env.responseSent) {
+ return;
+ } else {
+ res.setHeader.apply(res, Array.prototype.slice.call(arguments,
2));
+ }
+};
+
+/**
+ * End response, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.endResponse = function(res, env) {
+ if (env.responseSent) {
+ return;
+ } else {
+ env.responseSent = true;
+ res.end.apply(res, Array.prototype.slice.call(arguments, 2));
+ env.log("end/response");
+ }
+};
+
+/**
+ * Send response, but only if response hasn't been sent.
+ *
+ * @method
+ * @param {MWParserEnvironment} env
+ * @param {Response} res The response object from our routing function.
+ * @property {Function} Serializer
+ */
+apiUtils.sendResponse = function(res, env) {
+ if (env.responseSent) {
+ return;
+ } else {
+ env.responseSent = true;
+ res.send.apply(res, Array.prototype.slice.call(arguments, 2));
+ }
+};
+
+/**
+ * Render response, but only if response hasn't been sent.
+ */
+apiUtils.renderResponse = function(res, env) {
+ if (env.responseSent) {
+ return;
+ } else {
+ env.responseSent = true;
+ res.render.apply(res, Array.prototype.slice.call(arguments, 2));
+ }
+};
+
+apiUtils.jsonResponse = function(res, env) {
+ if (env.responseSent) {
+ return;
+ } else {
+ env.responseSent = true;
+ res.json.apply(res, Array.prototype.slice.call(arguments, 2));
+ }
+};
+
+apiUtils.htmlSpecialChars = function( s ) {
+ return s.replace(/&/g,'&')
+ .replace(/</g,'<')
+ .replace(/"/g,'"')
+ .replace(/'/g,''');
+};
--
To view, visit https://gerrit.wikimedia.org/r/164365
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I9a2a573e6afe14c911d42d4d5ecd048eff7f1865
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/services/parsoid
Gerrit-Branch: master
Gerrit-Owner: Arlolra <[email protected]>
Gerrit-Reviewer: Cscott <[email protected]>
Gerrit-Reviewer: Marcoil <[email protected]>
Gerrit-Reviewer: Subramanya Sastry <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits