fix blob: URLS, corractly calculate image preview viwport size
This commit is contained in:
+5
-1
@@ -1238,8 +1238,12 @@ i.search {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#image-preview-container {
|
#image-preview-container {
|
||||||
margin: auto;
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-preview-footer {
|
#image-preview-footer {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+26
-1
@@ -91,7 +91,7 @@ export function isUrlRelative(url) {
|
|||||||
return !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
return !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure URL is valid and does not present an XSS risk. Optional allowedSchemes may contain an array of
|
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
|
||||||
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
|
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
|
||||||
export function sanitizeUrl(url, allowedSchemes) {
|
export function sanitizeUrl(url, allowedSchemes) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -105,6 +105,10 @@ export function sanitizeUrl(url, allowedSchemes) {
|
|||||||
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
|
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
// Blob URLs are safe.
|
||||||
|
if (/^blob:http/.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
// Absolute URL. Accept only safe schemes, or no scheme.
|
// Absolute URL. Accept only safe schemes, or no scheme.
|
||||||
const schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
const schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
||||||
@@ -115,3 +119,24 @@ export function sanitizeUrl(url, allowedSchemes) {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
|
||||||
|
// have http:, https:, or data: scheme. In case of data: scheme, the URL must start with
|
||||||
|
// a 'data:image/XXXX;base64,'.
|
||||||
|
export function sanitizeImageUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedUrl = sanitizeUrl(url);
|
||||||
|
if (sanitizedUrl) {
|
||||||
|
return sanitizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this a data: URL of an image?
|
||||||
|
if (/data:image\/[a-z0-9.-]+;base64,/i.test(url.trim())) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -496,7 +496,6 @@ class TinodeWeb extends React.Component {
|
|||||||
credCode: undefined,
|
credCode: undefined,
|
||||||
myUserId: this.tinode.getCurrentUserID()
|
myUserId: this.tinode.getCurrentUserID()
|
||||||
});
|
});
|
||||||
console.log("handleLoginSuccessful");
|
|
||||||
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
|
||||||
me.subscribe(
|
me.subscribe(
|
||||||
me.startMetaQuery().
|
me.startMetaQuery().
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Drafty } from 'tinode-sdk'
|
|||||||
import Attachment from './attachment.jsx';
|
import Attachment from './attachment.jsx';
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
import ReceivedMarker from './received-marker.jsx'
|
import ReceivedMarker from './received-marker.jsx'
|
||||||
import { sanitizeUrl } from '../lib/utils.js';
|
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class ChatMessage extends React.Component {
|
export default class ChatMessage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -164,7 +164,7 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
||||||
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||||
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
||||||
attr.src = sanitizeUrl(attr.src);
|
attr.src = sanitizeImageUrl(attr.src);
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.onClick = this.handlePreviewImage;
|
attr.onClick = this.handlePreviewImage;
|
||||||
attr.className += ' image-clickable';
|
attr.className += ' image-clickable';
|
||||||
|
|||||||
@@ -9,21 +9,27 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {};
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
assignWidth(node) {
|
||||||
this.setState({
|
if (node && !this.state.width) {
|
||||||
width: this.container.clientWidth,
|
const bounds = node.getBoundingClientRect();
|
||||||
height: this.container.clientHeight
|
this.setState({
|
||||||
});
|
width: bounds.width | 0,
|
||||||
|
height: bounds.height | 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.content) {
|
if (!this.props.content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const instance = this;
|
|
||||||
const dim = fitImageSize(this.props.content.width, this.props.content.height,
|
const dim = fitImageSize(this.props.content.width, this.props.content.height,
|
||||||
this.state.width, this.state.height, false);
|
this.state.width, this.state.height, false);
|
||||||
const size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
|
const size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
|
||||||
@@ -32,9 +38,10 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
size.maxHeight = '100%';
|
size.maxHeight = '100%';
|
||||||
|
|
||||||
let filename = this.props.content.filename;
|
let filename = this.props.content.filename;
|
||||||
const maxlength = (this.props.content.width / REM_SIZE) | 0;
|
// Averate font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||||
|
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
|
||||||
if (filename.length > maxlength) {
|
if (filename.length > maxlength) {
|
||||||
filename = filename.slice(0, maxlength-2) + '...' + filename.slice(2-maxlength);
|
filename = filename.slice(0, maxlength/2 - 1) + '...' + filename.slice(1 - maxlength/2);
|
||||||
}
|
}
|
||||||
const width = this.props.content.width || '-';
|
const width = this.props.content.width || '-';
|
||||||
const height = this.props.content.height || '-';
|
const height = this.props.content.height || '-';
|
||||||
@@ -47,7 +54,7 @@ export default class ImagePreview extends React.PureComponent {
|
|||||||
</a>
|
</a>
|
||||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||||
</div>
|
</div>
|
||||||
<div id="image-preview-container" ref={function(ref) {instance.container = ref;}}>
|
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
|
||||||
<img src={this.props.content.url} style={size} />
|
<img src={this.props.content.url} style={size} />
|
||||||
</div>
|
</div>
|
||||||
<div id="image-preview-footer">
|
<div id="image-preview-footer">
|
||||||
|
|||||||
+41
-15
@@ -744,7 +744,7 @@ function stringHash(value) {
|
|||||||
/*!**************************!*\
|
/*!**************************!*\
|
||||||
!*** ./src/lib/utils.js ***!
|
!*** ./src/lib/utils.js ***!
|
||||||
\**************************/
|
\**************************/
|
||||||
/*! exports provided: updateFavicon, vcard, arrayEqual, asPhone, asEmail, isUrlRelative, sanitizeUrl */
|
/*! exports provided: updateFavicon, vcard, arrayEqual, asPhone, asEmail, isUrlRelative, sanitizeUrl, sanitizeImageUrl */
|
||||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -756,6 +756,7 @@ __webpack_require__.r(__webpack_exports__);
|
|||||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "asEmail", function() { return asEmail; });
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "asEmail", function() { return asEmail; });
|
||||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isUrlRelative", function() { return isUrlRelative; });
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isUrlRelative", function() { return isUrlRelative; });
|
||||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sanitizeUrl", function() { return sanitizeUrl; });
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sanitizeUrl", function() { return sanitizeUrl; });
|
||||||
|
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sanitizeImageUrl", function() { return sanitizeImageUrl; });
|
||||||
function updateFavicon(count) {
|
function updateFavicon(count) {
|
||||||
var oldIcon = document.getElementById('shortcut-icon');
|
var oldIcon = document.getElementById('shortcut-icon');
|
||||||
var head = document.head || document.getElementsByTagName('head')[0];
|
var head = document.head || document.getElementsByTagName('head')[0];
|
||||||
@@ -849,6 +850,10 @@ function sanitizeUrl(url, allowedSchemes) {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/^blob:http/.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
var schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
var schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
||||||
var re = new RegExp('^((' + schemes + '):|//)', 'i');
|
var re = new RegExp('^((' + schemes + '):|//)', 'i');
|
||||||
|
|
||||||
@@ -858,6 +863,23 @@ function sanitizeUrl(url, allowedSchemes) {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
function sanitizeImageUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitizedUrl = sanitizeUrl(url);
|
||||||
|
|
||||||
|
if (sanitizedUrl) {
|
||||||
|
return sanitizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/data:image\/[a-z0-9.-]+;base64,/i.test(url.trim())) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
@@ -4775,7 +4797,6 @@ var TinodeWeb = function (_React$Component) {
|
|||||||
credCode: undefined,
|
credCode: undefined,
|
||||||
myUserId: this.tinode.getCurrentUserID()
|
myUserId: this.tinode.getCurrentUserID()
|
||||||
});
|
});
|
||||||
console.log("handleLoginSuccessful");
|
|
||||||
me.subscribe(me.startMetaQuery().withLaterSub().withDesc().withTags().withCred().build())["catch"](function (err) {
|
me.subscribe(me.startMetaQuery().withLaterSub().withDesc().withTags().withCred().build())["catch"](function (err) {
|
||||||
localStorage.removeItem('auth-token');
|
localStorage.removeItem('auth-token');
|
||||||
|
|
||||||
@@ -6646,7 +6667,7 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
width: dim.dstWidth + 'px',
|
width: dim.dstWidth + 'px',
|
||||||
height: dim.dstHeight + 'px'
|
height: dim.dstHeight + 'px'
|
||||||
};
|
};
|
||||||
attr.src = Object(_lib_utils_js__WEBPACK_IMPORTED_MODULE_6__["sanitizeUrl"])(attr.src);
|
attr.src = Object(_lib_utils_js__WEBPACK_IMPORTED_MODULE_6__["sanitizeImageUrl"])(attr.src);
|
||||||
|
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.onClick = this.handlePreviewImage;
|
attr.onClick = this.handlePreviewImage;
|
||||||
@@ -8644,17 +8665,23 @@ var ImagePreview = function (_React$PureComponent) {
|
|||||||
_classCallCheck(this, ImagePreview);
|
_classCallCheck(this, ImagePreview);
|
||||||
|
|
||||||
_this = _possibleConstructorReturn(this, _getPrototypeOf(ImagePreview).call(this, props));
|
_this = _possibleConstructorReturn(this, _getPrototypeOf(ImagePreview).call(this, props));
|
||||||
_this.state = {};
|
_this.state = {
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
return _this;
|
return _this;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createClass(ImagePreview, [{
|
_createClass(ImagePreview, [{
|
||||||
key: "componentDidMount",
|
key: "assignWidth",
|
||||||
value: function componentDidMount() {
|
value: function assignWidth(node) {
|
||||||
this.setState({
|
if (node && !this.state.width) {
|
||||||
width: this.container.clientWidth,
|
var bounds = node.getBoundingClientRect();
|
||||||
height: this.container.clientHeight
|
this.setState({
|
||||||
});
|
width: bounds.width | 0,
|
||||||
|
height: bounds.height | 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
key: "render",
|
key: "render",
|
||||||
@@ -8665,7 +8692,6 @@ var ImagePreview = function (_React$PureComponent) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = this;
|
|
||||||
var dim = Object(_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__["fitImageSize"])(this.props.content.width, this.props.content.height, this.state.width, this.state.height, false);
|
var dim = Object(_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__["fitImageSize"])(this.props.content.width, this.props.content.height, this.state.width, this.state.height, false);
|
||||||
var size = dim ? {
|
var size = dim ? {
|
||||||
width: dim.dstWidth + 'px',
|
width: dim.dstWidth + 'px',
|
||||||
@@ -8678,10 +8704,10 @@ var ImagePreview = function (_React$PureComponent) {
|
|||||||
size.maxWidth = '100%';
|
size.maxWidth = '100%';
|
||||||
size.maxHeight = '100%';
|
size.maxHeight = '100%';
|
||||||
var filename = this.props.content.filename;
|
var filename = this.props.content.filename;
|
||||||
var maxlength = this.props.content.width / _config_js__WEBPACK_IMPORTED_MODULE_2__["REM_SIZE"] | 0;
|
var maxlength = Math.max((this.state.width / _config_js__WEBPACK_IMPORTED_MODULE_2__["REM_SIZE"] / 1.5 | 0) - 2, 12);
|
||||||
|
|
||||||
if (filename.length > maxlength) {
|
if (filename.length > maxlength) {
|
||||||
filename = filename.slice(0, maxlength - 2) + '...' + filename.slice(2 - maxlength);
|
filename = filename.slice(0, maxlength / 2 - 1) + '...' + filename.slice(1 - maxlength / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
var width = this.props.content.width || '-';
|
var width = this.props.content.width || '-';
|
||||||
@@ -8710,8 +8736,8 @@ var ImagePreview = function (_React$PureComponent) {
|
|||||||
className: "material-icons gray"
|
className: "material-icons gray"
|
||||||
}, "close"))), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
|
}, "close"))), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
|
||||||
id: "image-preview-container",
|
id: "image-preview-container",
|
||||||
ref: function ref(_ref) {
|
ref: function ref(node) {
|
||||||
instance.container = _ref;
|
return _this2.assignWidth(node);
|
||||||
}
|
}
|
||||||
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
|
}, react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("img", {
|
||||||
src: this.props.content.url,
|
src: this.props.content.url,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user