/
thumbnail.js
173 lines (156 loc) · 4.82 KB
/
thumbnail.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
/**
* @module thumbnail
*/
import constants from '../constants';
export const SIZES = {
portraitImage: {
h: 250, // Exact height
w: 203 // Max width
},
landscapeImage: {
h: 200, // Max height
w: 320 // Exact Width
}
};
/**
* @typedef {Object} ext.popups.Thumbnail
* @property {JQuery} el
* @property {boolean} isTall Whether or not the thumbnail is portrait
* @property {number} width
* @property {number} height
* @property {boolean} isNarrow whether the thumbnail is portrait and also
* thinner than the default portrait thumbnail width
* (as defined in SIZES.portraitImage.w)
* @property {number} offset in pixels between the thumbnail width and the
* standard portrait thumbnail width (as defined in SIZES.portraitImage.w)
*/
/**
* Creates a thumbnail from the representation of a thumbnail returned by the
* PageImages MediaWiki API query module.
*
* If there's no thumbnail, the thumbnail is too small, or the thumbnail's URL
* contains characters that could be used to perform an
* [XSS attack via CSS](https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)),
* then `null` is returned.
*
* Extracted from `mw.popups.renderer.article.createThumbnail`.
*
* @param {Object} rawThumbnail
* @return {ext.popups.Thumbnail|null}
*/
export function createThumbnail( rawThumbnail ) {
const devicePixelRatio = constants.BRACKETED_DEVICE_PIXEL_RATIO;
if ( !rawThumbnail ) {
return null;
}
const tall = rawThumbnail.width < rawThumbnail.height;
const thumbWidth = rawThumbnail.width / devicePixelRatio;
const thumbHeight = rawThumbnail.height / devicePixelRatio;
if (
// Image too small for landscape display
( !tall && thumbWidth < SIZES.landscapeImage.w ) ||
// Image too small for portrait display
( tall && thumbHeight < SIZES.portraitImage.h ) ||
// These characters in URL that could inject CSS and thus JS
(
rawThumbnail.source.indexOf( '\\' ) > -1 ||
rawThumbnail.source.indexOf( '\'' ) > -1 ||
rawThumbnail.source.indexOf( '"' ) > -1
)
) {
return null;
}
let x, y, width, height;
if ( tall ) {
x = ( thumbWidth > SIZES.portraitImage.w ) ?
( ( thumbWidth - SIZES.portraitImage.w ) / -2 ) :
( SIZES.portraitImage.w - thumbWidth );
y = ( thumbHeight > SIZES.portraitImage.h ) ?
( ( thumbHeight - SIZES.portraitImage.h ) / -2 ) : 0;
width = SIZES.portraitImage.w;
height = SIZES.portraitImage.h;
// Special handling for thin tall images
// https://phabricator.wikimedia.org/T192928#4312088
if ( thumbWidth < width ) {
x = 0;
width = thumbWidth;
}
} else {
x = 0;
y = ( thumbHeight > SIZES.landscapeImage.h ) ?
( ( thumbHeight - SIZES.landscapeImage.h ) / -2 ) : 0;
width = SIZES.landscapeImage.w;
height = ( thumbHeight > SIZES.landscapeImage.h ) ?
SIZES.landscapeImage.h : thumbHeight;
}
const isNarrow = tall && thumbWidth < SIZES.portraitImage.w;
return {
el: createThumbnailElement(
tall ? 'mwe-popups-is-tall' : 'mwe-popups-is-not-tall',
rawThumbnail.source,
x,
y,
thumbWidth,
thumbHeight,
width,
height
),
isTall: tall,
isNarrow,
offset: isNarrow ? SIZES.portraitImage.w - thumbWidth : 0,
width: thumbWidth,
height: thumbHeight
};
}
/**
* Creates the SVG image element that represents the thumbnail.
*
* This function is distinct from `createThumbnail` as it abstracts away some
* browser issues that are uncovered when manipulating elements across
* namespaces.
*
* @param {string} className
* @param {string} url
* @param {number} x
* @param {number} y
* @param {number} thumbnailWidth
* @param {number} thumbnailHeight
* @param {number} width
* @param {number} height
* @return {JQuery}
*/
export function createThumbnailElement(
className, url, x, y, thumbnailWidth, thumbnailHeight, width, height
) {
const nsSvg = 'http://www.w3.org/2000/svg',
nsXlink = 'http://www.w3.org/1999/xlink';
// We want to visually separate the image from the summary
// Given we use an SVG mask, we cannot rely on border to do this
// and instead must insert a polyline element to visually separate
const line = document.createElementNS( nsSvg, 'polyline' );
const isTall = className.indexOf( 'not-tall' ) === -1;
const points = isTall ? [ 0, 0, 0, height ] :
[ 0, height - 1, width, height - 1 ];
line.setAttribute( 'stroke', 'rgba(0,0,0,0.1)' );
line.setAttribute( 'points', points.join( ' ' ) );
line.setAttribute( 'stroke-width', 1 );
const $thumbnailSVGImage = $( document.createElementNS( nsSvg, 'image' ) );
$thumbnailSVGImage[ 0 ].setAttributeNS( nsXlink, 'href', url );
$thumbnailSVGImage
.addClass( className )
.attr( {
x,
y,
width: thumbnailWidth,
height: thumbnailHeight
} );
const $thumbnail = $( document.createElementNS( nsSvg, 'svg' ) )
.attr( {
xmlns: nsSvg,
width,
height
} )
.append( $thumbnailSVGImage );
$thumbnail.append( line );
return $thumbnail;
}