/**
* @module gateway/rest
*/
import { createModel } from '../preview/model';
import { abortablePromise } from './index.js';
const RESTBASE_PROFILE = 'https://www.mediawiki.org/wiki/Specs/Summary/1.2.0';
/** @typedef {function(JQuery.AjaxSettings=): JQuery.jqXHR} Ajax */
/**
* Creates an instance of the RESTBase gateway.
*
* This gateway differs from the {@link MediaWikiGateway MediaWiki gateway} in
* that it fetches page data from [the RESTBase page summary endpoint][0].
*
* [0]: https://en.wikipedia.org/api/rest_v1/#!/Page_content/get_page_summary_title
*
* @param {Ajax} ajax A function with the same signature as `jQuery.ajax`
* @param {Object} config Configuration that affects the major behavior of the
* gateway.
* @param {Function} extractParser A function that takes response and returns
* parsed extract
* @return {Gateway}
*/
export default function createRESTBaseGateway( ajax, config, extractParser ) {
/**
* Fetches page data from [the RESTBase page summary endpoint][0].
*
* [0]: https://en.wikipedia.org/api/rest_v1/#!/Page_content/get_page_summary_title
*
* @method
* @name RESTBaseGateway#fetch
* @param {string} title
* @return {jQuery.jqXHR}
*/
function fetch( title ) {
const endpoint = config.endpoint;
return ajax( {
url: endpoint + encodeURIComponent( title ),
headers: {
Accept: `application/json; charset=utf-8; profile="${ RESTBASE_PROFILE }"`,
'Accept-Language': config.acceptLanguage
}
} );
}
/**
* @param {mw.Title} title
* @return {AbortPromise<PagePreviewModel>}
*/
function fetchPreviewForTitle( title ) {
const titleText = title.getPrefixedDb(),
xhr = fetch( titleText );
return abortablePromise( xhr.then( ( page ) => {
// Endpoint response may be empty or simply missing a title.
page = page || {};
page.title = page.title || titleText;
// And extract may be omitted if empty string
page.extract = page.extract || '';
return convertPageToModel(
page, config.THUMBNAIL_SIZE, extractParser
);
} ).catch( ( jqXHR, textStatus, errorThrown ) => {
// The client will choose how to handle these errors which may include
// those due to HTTP 4xx and 5xx status. The rejection typing matches
// fetch failures.
return Promise.reject( 'http', {
xhr: jqXHR,
textStatus,
exception: errorThrown
} );
} ), () => xhr.abort() );
}
return {
fetch,
convertPageToModel,
fetchPreviewForTitle
};
}
/**
* Checks whether the `originalImage` property contains an image
* format that's safe to render.
* https://www.mediawiki.org/wiki/Help:Images#Supported_media_types_for_images
*
* @param {string} filename
*
* @return {boolean}
*/
function isSafeImgFormat( filename ) {
const safeImage = new RegExp( /\.(jpg|jpeg|png|gif)$/i );
return safeImage.test( filename );
}
/**
* Resizes the thumbnail to the requested width, preserving its aspect ratio.
*
* The requested width is limited to that of the original image unless the image
* is an SVG, which can be scaled infinitely.
*
* This function is only intended to mangle the pretty thumbnail URLs used on
* Wikimedia Commons. Once [an official thumb API](https://phabricator.wikimedia.org/T66214)
* is fully specified and implemented, this function can be made more general.
*
* @param {Object} thumbnail The thumbnail image
* @param {Object} original The original image
* @param {number} thumbSize The requested size
* @return {{source: string, width: number, height: number}|undefined}
*/
function generateThumbnailData( thumbnail, original, thumbSize ) {
const parts = thumbnail.source.split( '/' ),
lastPart = parts[ parts.length - 1 ],
originalIsSafe = isSafeImgFormat( original.source ) || undefined;
// The last part, the thumbnail's full filename, is in the following form:
// ${width}px-${filename}.${extension}. Splitting the thumbnail's filename
// makes this function resilient to the thumbnail not having the same
// extension as the original image, which is definitely the case for SVG's
// where the thumbnail's extension is .svg.png.
const filenamePxIndex = lastPart.indexOf( 'px-' );
if ( filenamePxIndex === -1 ) {
// The thumbnail size is not customizable. Presumably, RESTBase requested a
// width greater than the original and so MediaWiki returned the original's
// URL instead of a thumbnail compatible URL. An original URL does not have
// a "thumb" path, e.g.:
//
// https://upload.wikimedia.org/wikipedia/commons/a/aa/Red_Giant_Earth_warm.jpg
//
// Instead of:
//
// https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Red_Giant_Earth_warm.jpg/512px-Red_Giant_Earth_warm.jpg
//
// Use the original if it's a supported image format.
return originalIsSafe && original;
}
const filename = lastPart.slice( filenamePxIndex + 3 );
// Scale the thumbnail's largest dimension.
let width, height;
if ( thumbnail.width > thumbnail.height ) {
width = thumbSize;
height = Math.floor( ( thumbSize / thumbnail.width ) * thumbnail.height );
} else {
width = Math.floor( ( thumbSize / thumbnail.height ) * thumbnail.width );
height = thumbSize;
}
// If the image isn't an SVG, then it shouldn't be scaled past its original
// dimensions.
if ( width >= original.width && filename.indexOf( '.svg' ) === -1 ) {
// if the image format is not supported, it shouldn't be rendered.
return originalIsSafe && original;
}
parts[ parts.length - 1 ] = `${ width }px-${ filename }`;
return {
source: parts.join( '/' ),
width,
height
};
}
/**
* Converts the API response to a preview model.
*
* @method
* @name RESTBaseGateway#convertPageToModel
* @param {Object} page
* @param {number} thumbSize
* @param {Function} extractParser
* @return {PagePreviewModel}
*/
export function convertPageToModel( page, thumbSize, extractParser ) {
return createModel(
page.title,
new mw.Title( page.title ).getUrl(),
page.lang,
page.dir,
extractParser( page ),
page.type,
page.thumbnail ?
generateThumbnailData(
page.thumbnail, page.originalimage, thumbSize
) : undefined,
page.pageid
);
}