2019-07-10 15:03:37 +03:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
2017-06-12 21:38:25 +03:00
|
|
|
this.shot = (function () {let exports={}; // Note: in this library we can't use any "system" dependencies because this can be used from multiple
|
2017-04-13 11:49:17 +03:00
|
|
|
// environments
|
|
|
|
|
2018-04-20 00:10:10 +03:00
|
|
|
const isNode = typeof process !== "undefined" && Object.prototype.toString.call(process) === "[object process]";
|
|
|
|
const URL = (isNode && require("url").URL) || window.URL;
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
/** Throws an error if the condition isn't true. Any extra arguments after the condition
|
|
|
|
are used as console.error() arguments. */
|
|
|
|
function assert(condition, ...args) {
|
|
|
|
if (condition) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
console.error("Failed assertion", ...args);
|
2017-05-26 21:48:44 +03:00
|
|
|
throw new Error(`Failed assertion: ${args.join(" ")}`);
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/** True if `url` is a valid URL */
|
|
|
|
function isUrl(url) {
|
2018-04-20 00:10:10 +03:00
|
|
|
try {
|
|
|
|
const parsed = new URL(url);
|
|
|
|
|
|
|
|
if (parsed.protocol === "view-source:") {
|
|
|
|
return isUrl(url.substr("view-source:".length));
|
|
|
|
}
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
return true;
|
2018-04-20 00:10:10 +03:00
|
|
|
} catch (e) {
|
|
|
|
return false;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-13 22:40:39 +03:00
|
|
|
function isValidClipImageUrl(url) {
|
2018-02-23 03:57:01 +03:00
|
|
|
return isUrl(url) && !(url.indexOf(")") > -1);
|
2017-09-13 22:40:39 +03:00
|
|
|
}
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
function assertUrl(url) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!url) {
|
2017-04-13 11:49:17 +03:00
|
|
|
throw new Error("Empty value is not URL");
|
|
|
|
}
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!isUrl(url)) {
|
2018-02-23 03:57:01 +03:00
|
|
|
const exc = new Error("Not a URL");
|
2017-04-13 11:49:17 +03:00
|
|
|
exc.scheme = url.split(":")[0];
|
|
|
|
throw exc;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-13 22:40:39 +03:00
|
|
|
function isSecureWebUri(url) {
|
2018-04-20 00:10:10 +03:00
|
|
|
return isUrl(url) && url.toLowerCase().startsWith("https");
|
2017-09-13 22:40:39 +03:00
|
|
|
}
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
function assertOrigin(url) {
|
|
|
|
assertUrl(url);
|
2018-02-23 03:57:01 +03:00
|
|
|
if (url.search(/^https?:/i) !== -1) {
|
|
|
|
const match = (/^https?:\/\/[^/:]{1,4000}\/?$/i).exec(url);
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!match) {
|
2017-04-13 11:49:17 +03:00
|
|
|
throw new Error("Bad origin, might include path");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function originFromUrl(url) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!url) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return null;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
if (url.search(/^https?:/i) === -1) {
|
2017-04-13 11:49:17 +03:00
|
|
|
// Non-HTTP URLs don't have an origin
|
|
|
|
return null;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
const match = (/^https?:\/\/[^/:]{1,4000}/i).exec(url);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (match) {
|
|
|
|
return match[0];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Check if the given object has all of the required attributes, and no extra
|
|
|
|
attributes exception those in optional */
|
|
|
|
function checkObject(obj, required, optional) {
|
2018-02-23 03:57:01 +03:00
|
|
|
if (typeof obj !== "object" || obj === null) {
|
2017-04-13 11:49:17 +03:00
|
|
|
throw new Error("Cannot check non-object: " + (typeof obj) + " that is " + JSON.stringify(obj));
|
|
|
|
}
|
|
|
|
required = required || [];
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr of required) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!(attr in obj)) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
optional = optional || [];
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr in obj) {
|
2017-04-13 11:49:17 +03:00
|
|
|
if (!required.includes(attr) && !optional.includes(attr)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Create a JSON object from a normal object, given the required and optional
|
|
|
|
attributes (filtering out any other attributes). Optional attributes are
|
|
|
|
only kept when they are truthy. */
|
|
|
|
function jsonify(obj, required, optional) {
|
|
|
|
required = required || [];
|
2018-02-23 03:57:01 +03:00
|
|
|
const result = {};
|
|
|
|
for (const attr of required) {
|
2017-04-13 11:49:17 +03:00
|
|
|
result[attr] = obj[attr];
|
|
|
|
}
|
|
|
|
optional = optional || [];
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr of optional) {
|
2017-04-13 11:49:17 +03:00
|
|
|
if (obj[attr]) {
|
|
|
|
result[attr] = obj[attr];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** True if the two objects look alike. Null, undefined, and absent properties
|
|
|
|
are all treated as equivalent. Traverses objects and arrays */
|
|
|
|
function deepEqual(a, b) {
|
|
|
|
if ((a === null || a === undefined) && (b === null || b === undefined)) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
if (typeof a !== "object" || typeof b !== "object") {
|
2017-04-13 11:49:17 +03:00
|
|
|
return a === b;
|
|
|
|
}
|
|
|
|
if (Array.isArray(a)) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!Array.isArray(b)) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
if (a.length !== b.length) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
2017-05-02 02:58:23 +03:00
|
|
|
for (let i = 0; i < a.length; i++) {
|
|
|
|
if (!deepEqual(a[i], b[i])) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (Array.isArray(b)) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
const seen = new Set();
|
|
|
|
for (const attr of Object.keys(a)) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!deepEqual(a[attr], b[attr])) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
seen.add(attr);
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr of Object.keys(b)) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!seen.has(attr)) {
|
|
|
|
if (!deepEqual(a[attr], b[attr])) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeRandomId() {
|
|
|
|
// Note: this isn't for secure contexts, only for non-conflicting IDs
|
|
|
|
let id = "";
|
|
|
|
while (id.length < 12) {
|
|
|
|
let num;
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!id) {
|
2017-04-13 11:49:17 +03:00
|
|
|
num = Date.now() % Math.pow(36, 3);
|
|
|
|
} else {
|
|
|
|
num = Math.floor(Math.random() * Math.pow(36, 3));
|
|
|
|
}
|
|
|
|
id += num.toString(36);
|
|
|
|
}
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
|
|
|
class AbstractShot {
|
|
|
|
|
|
|
|
constructor(backend, id, attrs) {
|
|
|
|
attrs = attrs || {};
|
2017-11-20 22:06:02 +03:00
|
|
|
assert((/^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/).test(id), "Bad ID (should be alphanumeric):", JSON.stringify(id));
|
2017-04-13 11:49:17 +03:00
|
|
|
this._backend = backend;
|
|
|
|
this._id = id;
|
|
|
|
this.origin = attrs.origin || null;
|
|
|
|
this.fullUrl = attrs.fullUrl || null;
|
2017-05-02 02:58:23 +03:00
|
|
|
if ((!attrs.fullUrl) && attrs.url) {
|
2017-04-13 11:49:17 +03:00
|
|
|
console.warn("Received deprecated attribute .url");
|
|
|
|
this.fullUrl = attrs.url;
|
|
|
|
}
|
2017-09-13 22:40:39 +03:00
|
|
|
if (this.origin && !isSecureWebUri(this.origin)) {
|
|
|
|
this.origin = "";
|
|
|
|
}
|
|
|
|
if (this.fullUrl && !isSecureWebUri(this.fullUrl)) {
|
|
|
|
this.fullUrl = "";
|
|
|
|
}
|
2017-04-13 11:49:17 +03:00
|
|
|
this.docTitle = attrs.docTitle || null;
|
|
|
|
this.userTitle = attrs.userTitle || null;
|
|
|
|
this.createdDate = attrs.createdDate || Date.now();
|
|
|
|
this.siteName = attrs.siteName || null;
|
|
|
|
this.images = [];
|
|
|
|
if (attrs.images) {
|
|
|
|
this.images = attrs.images.map(
|
|
|
|
(json) => new this.Image(json));
|
|
|
|
}
|
|
|
|
this.openGraph = attrs.openGraph || null;
|
|
|
|
this.twitterCard = attrs.twitterCard || null;
|
|
|
|
this.documentSize = attrs.documentSize || null;
|
2018-02-09 02:54:36 +03:00
|
|
|
this.thumbnail = attrs.thumbnail || null;
|
2017-04-13 11:49:17 +03:00
|
|
|
this.abTests = attrs.abTests || null;
|
2018-10-15 23:10:31 +03:00
|
|
|
this.firefoxChannel = attrs.firefoxChannel || null;
|
2017-04-13 11:49:17 +03:00
|
|
|
this._clips = {};
|
|
|
|
if (attrs.clips) {
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const clipId in attrs.clips) {
|
|
|
|
const clip = attrs.clips[clipId];
|
2017-04-13 11:49:17 +03:00
|
|
|
this._clips[clipId] = new this.Clip(this, clipId, clip);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-23 03:57:01 +03:00
|
|
|
const isProd = typeof process !== "undefined" && process.env.NODE_ENV === "production";
|
2018-02-09 02:54:36 +03:00
|
|
|
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr in attrs) {
|
2017-04-13 11:49:17 +03:00
|
|
|
if (attr !== "clips" && attr !== "id" && !this.REGULAR_ATTRS.includes(attr) && !this.DEPRECATED_ATTRS.includes(attr)) {
|
2018-02-09 02:54:36 +03:00
|
|
|
if (isProd) {
|
|
|
|
console.warn("Unexpected attribute: " + attr);
|
|
|
|
} else {
|
|
|
|
throw new Error("Unexpected attribute: " + attr);
|
|
|
|
}
|
2017-04-13 11:49:17 +03:00
|
|
|
} else if (attr === "id") {
|
|
|
|
console.warn("passing id in attrs in AbstractShot constructor");
|
|
|
|
console.trace();
|
|
|
|
assert(attrs.id === this.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Update any and all attributes in the json object, with deep updating
|
|
|
|
of `json.clips` */
|
|
|
|
update(json) {
|
2018-02-23 03:57:01 +03:00
|
|
|
const ALL_ATTRS = ["clips"].concat(this.REGULAR_ATTRS);
|
2017-04-13 11:49:17 +03:00
|
|
|
assert(checkObject(json, [], ALL_ATTRS), "Bad attr to new Shot():", Object.keys(json));
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr in json) {
|
|
|
|
if (attr === "clips") {
|
2017-04-13 11:49:17 +03:00
|
|
|
continue;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
if (typeof json[attr] === "object" && typeof this[attr] === "object" && this[attr] !== null) {
|
2017-04-13 11:49:17 +03:00
|
|
|
let val = this[attr];
|
2018-04-20 00:10:10 +03:00
|
|
|
if (val.toJSON) {
|
|
|
|
val = val.toJSON();
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!deepEqual(json[attr], val)) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this[attr] = json[attr];
|
|
|
|
}
|
|
|
|
} else if (json[attr] !== this[attr] &&
|
|
|
|
(json[attr] || this[attr])) {
|
|
|
|
this[attr] = json[attr];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (json.clips) {
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const clipId in json.clips) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!json.clips[clipId]) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this.delClip(clipId);
|
2017-05-02 02:58:23 +03:00
|
|
|
} else if (!this.getClip(clipId)) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this.setClip(clipId, json.clips[clipId]);
|
2018-04-20 00:10:10 +03:00
|
|
|
} else if (!deepEqual(this.getClip(clipId).toJSON(), json.clips[clipId])) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this.setClip(clipId, json.clips[clipId]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns a JSON version of this shot */
|
2018-04-20 00:10:10 +03:00
|
|
|
toJSON() {
|
2018-02-23 03:57:01 +03:00
|
|
|
const result = {};
|
|
|
|
for (const attr of this.REGULAR_ATTRS) {
|
|
|
|
let val = this[attr];
|
2018-04-20 00:10:10 +03:00
|
|
|
if (val && val.toJSON) {
|
|
|
|
val = val.toJSON();
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
result[attr] = val;
|
|
|
|
}
|
|
|
|
result.clips = {};
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const attr in this._clips) {
|
2018-04-20 00:10:10 +03:00
|
|
|
result.clips[attr] = this._clips[attr].toJSON();
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** A more minimal JSON representation for creating indexes of shots */
|
|
|
|
asRecallJson() {
|
2018-02-23 03:57:01 +03:00
|
|
|
const result = {clips: {}};
|
|
|
|
for (const attr of this.RECALL_ATTRS) {
|
|
|
|
let val = this[attr];
|
2018-04-20 00:10:10 +03:00
|
|
|
if (val && val.toJSON) {
|
|
|
|
val = val.toJSON();
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
result[attr] = val;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const name of this.clipNames()) {
|
2018-04-20 00:10:10 +03:00
|
|
|
result.clips[name] = this.getClip(name).toJSON();
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
get backend() {
|
|
|
|
return this._backend;
|
|
|
|
}
|
|
|
|
|
|
|
|
get id() {
|
|
|
|
return this._id;
|
|
|
|
}
|
|
|
|
|
|
|
|
get url() {
|
|
|
|
return this.fullUrl || this.origin;
|
|
|
|
}
|
|
|
|
set url(val) {
|
|
|
|
throw new Error(".url is read-only");
|
|
|
|
}
|
|
|
|
|
|
|
|
get fullUrl() {
|
|
|
|
return this._fullUrl;
|
|
|
|
}
|
|
|
|
set fullUrl(val) {
|
|
|
|
if (val) {
|
|
|
|
assertUrl(val);
|
|
|
|
}
|
|
|
|
this._fullUrl = val || undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
get origin() {
|
|
|
|
return this._origin;
|
|
|
|
}
|
|
|
|
set origin(val) {
|
|
|
|
if (val) {
|
|
|
|
assertOrigin(val);
|
|
|
|
}
|
|
|
|
this._origin = val || undefined;
|
|
|
|
}
|
|
|
|
|
2019-02-05 23:33:45 +03:00
|
|
|
get isOwner() {
|
|
|
|
return this._isOwner;
|
|
|
|
}
|
|
|
|
|
|
|
|
set isOwner(val) {
|
|
|
|
this._isOwner = val || undefined;
|
|
|
|
}
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
get filename() {
|
|
|
|
let filenameTitle = this.title;
|
2018-02-23 03:57:01 +03:00
|
|
|
const date = new Date(this.createdDate);
|
2017-10-27 21:32:59 +03:00
|
|
|
// eslint-disable-next-line no-control-regex
|
|
|
|
filenameTitle = filenameTitle.replace(/[:\\<>/!@&?"*.|\x00-\x1F]/g, " ");
|
2017-06-12 21:38:25 +03:00
|
|
|
filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
|
2018-04-20 00:10:10 +03:00
|
|
|
const filenameDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000).toISOString().substring(0, 10);
|
|
|
|
let clipFilename = `Screenshot_${filenameDate} ${filenameTitle}`;
|
2017-04-13 11:49:17 +03:00
|
|
|
const clipFilenameBytesSize = clipFilename.length * 2; // JS STrings are UTF-16
|
|
|
|
if (clipFilenameBytesSize > 251) { // 255 bytes (Usual filesystems max) - 4 for the ".png" file extension string
|
|
|
|
const excedingchars = (clipFilenameBytesSize - 246) / 2; // 251 - 5 for ellipsis "[...]"
|
|
|
|
clipFilename = clipFilename.substring(0, clipFilename.length - excedingchars);
|
2018-02-23 03:57:01 +03:00
|
|
|
clipFilename = clipFilename + "[...]";
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
const clip = this.getClip(this.clipNames()[0]);
|
2017-09-25 21:17:43 +03:00
|
|
|
let extension = ".png";
|
|
|
|
if (clip && clip.image && clip.image.type) {
|
2018-02-23 03:57:01 +03:00
|
|
|
if (clip.image.type === "jpeg") {
|
2017-09-25 21:17:43 +03:00
|
|
|
extension = ".jpg";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return clipFilename + extension;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get urlDisplay() {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!this.url) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return null;
|
|
|
|
}
|
2017-11-20 22:06:02 +03:00
|
|
|
if (/^https?:\/\//i.test(this.url)) {
|
2017-04-13 11:49:17 +03:00
|
|
|
let txt = this.url;
|
2017-06-12 21:38:25 +03:00
|
|
|
txt = txt.replace(/^[a-z]{1,4000}:\/\//i, "");
|
|
|
|
txt = txt.replace(/\/.{0,4000}/, "");
|
2017-04-13 11:49:17 +03:00
|
|
|
txt = txt.replace(/^www\./i, "");
|
|
|
|
return txt;
|
|
|
|
} else if (this.url.startsWith("data:")) {
|
|
|
|
return "data:url";
|
|
|
|
}
|
2017-05-02 02:58:23 +03:00
|
|
|
let txt = this.url;
|
2017-06-12 21:38:25 +03:00
|
|
|
txt = txt.replace(/\?.{0,4000}/, "");
|
2017-05-02 02:58:23 +03:00
|
|
|
return txt;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get viewUrl() {
|
2018-02-23 03:57:01 +03:00
|
|
|
const url = this.backend + "/" + this.id;
|
2017-04-13 11:49:17 +03:00
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
get creatingUrl() {
|
|
|
|
let url = `${this.backend}/creating/${this.id}`;
|
|
|
|
url += `?title=${encodeURIComponent(this.title || "")}`;
|
|
|
|
url += `&url=${encodeURIComponent(this.url)}`;
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
|
|
|
get jsonUrl() {
|
|
|
|
return this.backend + "/data/" + this.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
get oembedUrl() {
|
|
|
|
return this.backend + "/oembed?url=" + encodeURIComponent(this.viewUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
get docTitle() {
|
|
|
|
return this._title;
|
|
|
|
}
|
|
|
|
set docTitle(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(val === null || typeof val === "string", "Bad docTitle:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._title = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get openGraph() {
|
|
|
|
return this._openGraph || null;
|
|
|
|
}
|
|
|
|
set openGraph(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(val === null || typeof val === "object", "Bad openGraph:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (val) {
|
|
|
|
assert(checkObject(val, [], this._OPENGRAPH_PROPERTIES), "Bad attr to openGraph:", Object.keys(val));
|
|
|
|
this._openGraph = val;
|
|
|
|
} else {
|
|
|
|
this._openGraph = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get twitterCard() {
|
|
|
|
return this._twitterCard || null;
|
|
|
|
}
|
|
|
|
set twitterCard(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(val === null || typeof val === "object", "Bad twitterCard:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (val) {
|
|
|
|
assert(checkObject(val, [], this._TWITTERCARD_PROPERTIES), "Bad attr to twitterCard:", Object.keys(val));
|
|
|
|
this._twitterCard = val;
|
|
|
|
} else {
|
|
|
|
this._twitterCard = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get userTitle() {
|
|
|
|
return this._userTitle;
|
|
|
|
}
|
|
|
|
set userTitle(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(val === null || typeof val === "string", "Bad userTitle:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._userTitle = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get title() {
|
|
|
|
// FIXME: we shouldn't support both openGraph.title and ogTitle
|
2018-02-23 03:57:01 +03:00
|
|
|
const ogTitle = this.openGraph && this.openGraph.title;
|
|
|
|
const twitterTitle = this.twitterCard && this.twitterCard.title;
|
2017-04-13 11:49:17 +03:00
|
|
|
let title = this.userTitle || ogTitle || twitterTitle || this.docTitle || this.url;
|
|
|
|
if (Array.isArray(title)) {
|
|
|
|
title = title[0];
|
|
|
|
}
|
2017-06-12 21:38:25 +03:00
|
|
|
if (!title) {
|
|
|
|
title = "Screenshot";
|
|
|
|
}
|
2017-04-13 11:49:17 +03:00
|
|
|
return title;
|
|
|
|
}
|
|
|
|
|
|
|
|
get createdDate() {
|
|
|
|
return this._createdDate;
|
|
|
|
}
|
|
|
|
set createdDate(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(val === null || typeof val === "number", "Bad createdDate:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._createdDate = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
clipNames() {
|
2018-02-23 03:57:01 +03:00
|
|
|
const names = Object.getOwnPropertyNames(this._clips);
|
2017-05-02 02:58:23 +03:00
|
|
|
names.sort(function(a, b) {
|
2017-04-13 11:49:17 +03:00
|
|
|
return a.sortOrder < b.sortOrder ? 1 : 0;
|
|
|
|
});
|
|
|
|
return names;
|
|
|
|
}
|
|
|
|
getClip(name) {
|
|
|
|
return this._clips[name];
|
|
|
|
}
|
|
|
|
addClip(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
const name = makeRandomId();
|
2017-04-13 11:49:17 +03:00
|
|
|
this.setClip(name, val);
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
setClip(name, val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
const clip = new this.Clip(this, name, val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._clips[name] = clip;
|
|
|
|
}
|
|
|
|
delClip(name) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!this._clips[name]) {
|
2017-04-13 11:49:17 +03:00
|
|
|
throw new Error("No existing clip with id: " + name);
|
|
|
|
}
|
|
|
|
delete this._clips[name];
|
|
|
|
}
|
2017-06-06 01:11:19 +03:00
|
|
|
delAllClips() {
|
|
|
|
this._clips = {};
|
|
|
|
}
|
2017-04-13 11:49:17 +03:00
|
|
|
biggestClipSortOrder() {
|
|
|
|
let biggest = 0;
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const clipId in this._clips) {
|
2017-04-13 11:49:17 +03:00
|
|
|
biggest = Math.max(biggest, this._clips[clipId].sortOrder);
|
|
|
|
}
|
|
|
|
return biggest;
|
|
|
|
}
|
|
|
|
updateClipUrl(clipId, clipUrl) {
|
2018-02-23 03:57:01 +03:00
|
|
|
const clip = this.getClip(clipId);
|
2017-04-13 11:49:17 +03:00
|
|
|
if ( clip && clip.image ) {
|
|
|
|
clip.image.url = clipUrl;
|
|
|
|
} else {
|
|
|
|
console.warn("Tried to update the url of a clip with no image:", clip);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get siteName() {
|
|
|
|
return this._siteName || null;
|
|
|
|
}
|
|
|
|
set siteName(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "string" || !val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._siteName = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get documentSize() {
|
|
|
|
return this._documentSize;
|
|
|
|
}
|
|
|
|
set documentSize(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "object" || !val);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (val) {
|
|
|
|
assert(checkObject(val, ["height", "width"], "Bad attr to documentSize:", Object.keys(val)));
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val.height === "number");
|
|
|
|
assert(typeof val.width === "number");
|
2017-04-13 11:49:17 +03:00
|
|
|
this._documentSize = val;
|
|
|
|
} else {
|
|
|
|
this._documentSize = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-09 02:54:36 +03:00
|
|
|
get thumbnail() {
|
|
|
|
return this._thumbnail;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
2018-02-09 02:54:36 +03:00
|
|
|
set thumbnail(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "string" || !val);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (val) {
|
|
|
|
assert(isUrl(val));
|
2018-02-09 02:54:36 +03:00
|
|
|
this._thumbnail = val;
|
2017-04-13 11:49:17 +03:00
|
|
|
} else {
|
2018-02-09 02:54:36 +03:00
|
|
|
this._thumbnail = null;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get abTests() {
|
|
|
|
return this._abTests;
|
|
|
|
}
|
|
|
|
set abTests(val) {
|
|
|
|
if (val === null || val === undefined) {
|
|
|
|
this._abTests = null;
|
|
|
|
return;
|
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "object", "abTests should be an object, not:", typeof val);
|
2017-05-02 02:58:23 +03:00
|
|
|
assert(!Array.isArray(val), "abTests should not be an Array");
|
2018-02-23 03:57:01 +03:00
|
|
|
for (const name in val) {
|
|
|
|
assert(val[name] && typeof val[name] === "string", `abTests.${name} should be a string:`, typeof val[name]);
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
this._abTests = val;
|
|
|
|
}
|
|
|
|
|
2018-10-15 23:10:31 +03:00
|
|
|
get firefoxChannel() {
|
|
|
|
return this._firefoxChannel;
|
|
|
|
}
|
|
|
|
set firefoxChannel(val) {
|
|
|
|
if (val === null || val === undefined) {
|
|
|
|
this._firefoxChannel = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
assert(typeof val === "string", "firefoxChannel should be a string, not:", typeof val);
|
|
|
|
this._firefoxChannel = val;
|
|
|
|
}
|
|
|
|
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
AbstractShot.prototype.REGULAR_ATTRS = (`
|
2018-10-15 23:10:31 +03:00
|
|
|
origin fullUrl docTitle userTitle createdDate images
|
2017-04-13 11:49:17 +03:00
|
|
|
siteName openGraph twitterCard documentSize
|
2018-10-15 23:10:31 +03:00
|
|
|
thumbnail abTests firefoxChannel
|
2017-04-13 11:49:17 +03:00
|
|
|
`).split(/\s+/g);
|
|
|
|
|
|
|
|
// Attributes that will be accepted in the constructor, but ignored/dropped
|
|
|
|
AbstractShot.prototype.DEPRECATED_ATTRS = (`
|
|
|
|
microdata history ogTitle createdDevice head body htmlAttrs bodyAttrs headAttrs
|
|
|
|
readable hashtags comments showPage isPublic resources deviceId url
|
2018-10-15 23:10:31 +03:00
|
|
|
fullScreenThumbnail favicon
|
2017-04-13 11:49:17 +03:00
|
|
|
`).split(/\s+/g);
|
|
|
|
|
|
|
|
AbstractShot.prototype.RECALL_ATTRS = (`
|
2018-10-15 23:10:31 +03:00
|
|
|
url docTitle userTitle createdDate openGraph twitterCard images thumbnail
|
2017-04-13 11:49:17 +03:00
|
|
|
`).split(/\s+/g);
|
|
|
|
|
|
|
|
AbstractShot.prototype._OPENGRAPH_PROPERTIES = (`
|
|
|
|
title type url image audio description determiner locale site_name video
|
|
|
|
image:secure_url image:type image:width image:height
|
|
|
|
video:secure_url video:type video:width image:height
|
|
|
|
audio:secure_url audio:type
|
|
|
|
article:published_time article:modified_time article:expiration_time article:author article:section article:tag
|
|
|
|
book:author book:isbn book:release_date book:tag
|
|
|
|
profile:first_name profile:last_name profile:username profile:gender
|
|
|
|
`).split(/\s+/g);
|
|
|
|
|
|
|
|
AbstractShot.prototype._TWITTERCARD_PROPERTIES = (`
|
|
|
|
card site title description image
|
|
|
|
player player:width player:height player:stream player:stream:content_type
|
|
|
|
`).split(/\s+/g);
|
|
|
|
|
|
|
|
/** Represents one found image in the document (not a clip) */
|
|
|
|
class _Image {
|
|
|
|
// FIXME: either we have to notify the shot of updates, or make
|
|
|
|
// this read-only
|
|
|
|
constructor(json) {
|
|
|
|
assert(typeof json === "object", "Clip Image given a non-object", json);
|
|
|
|
assert(checkObject(json, ["url"], ["dimensions", "title", "alt"]), "Bad attrs for Image:", Object.keys(json));
|
|
|
|
assert(isUrl(json.url), "Bad Image url:", json.url);
|
|
|
|
this.url = json.url;
|
2017-05-02 02:58:23 +03:00
|
|
|
assert((!json.dimensions) ||
|
2018-02-23 03:57:01 +03:00
|
|
|
(typeof json.dimensions.x === "number" && typeof json.dimensions.y === "number"),
|
2017-04-13 11:49:17 +03:00
|
|
|
"Bad Image dimensions:", json.dimensions);
|
|
|
|
this.dimensions = json.dimensions;
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof json.title === "string" || !json.title, "Bad Image title:", json.title);
|
2017-04-13 11:49:17 +03:00
|
|
|
this.title = json.title;
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof json.alt === "string" || !json.alt, "Bad Image alt:", json.alt);
|
2017-04-13 11:49:17 +03:00
|
|
|
this.alt = json.alt;
|
|
|
|
}
|
|
|
|
|
2018-04-20 00:10:10 +03:00
|
|
|
toJSON() {
|
2017-04-13 11:49:17 +03:00
|
|
|
return jsonify(this, ["url"], ["dimensions"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AbstractShot.prototype.Image = _Image;
|
|
|
|
|
|
|
|
/** Represents a clip, either a text or image clip */
|
|
|
|
class _Clip {
|
|
|
|
constructor(shot, id, json) {
|
|
|
|
this._shot = shot;
|
|
|
|
assert(checkObject(json, ["createdDate", "image"], ["sortOrder"]), "Bad attrs for Clip:", Object.keys(json));
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof id === "string" && id, "Bad Clip id:", id);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._id = id;
|
|
|
|
this.createdDate = json.createdDate;
|
2018-02-23 03:57:01 +03:00
|
|
|
if ("sortOrder" in json) {
|
|
|
|
assert(typeof json.sortOrder === "number" || !json.sortOrder, "Bad Clip sortOrder:", json.sortOrder);
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
2018-02-23 03:57:01 +03:00
|
|
|
if ("sortOrder" in json) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this.sortOrder = json.sortOrder;
|
|
|
|
} else {
|
2018-02-23 03:57:01 +03:00
|
|
|
const biggestOrder = shot.biggestClipSortOrder();
|
2017-04-13 11:49:17 +03:00
|
|
|
this.sortOrder = biggestOrder + 100;
|
|
|
|
}
|
|
|
|
this.image = json.image;
|
|
|
|
}
|
|
|
|
|
|
|
|
toString() {
|
|
|
|
return `[Shot Clip id=${this.id} sortOrder=${this.sortOrder} image ${this.image.dimensions.x}x${this.image.dimensions.y}]`;
|
|
|
|
}
|
|
|
|
|
2018-04-20 00:10:10 +03:00
|
|
|
toJSON() {
|
2017-04-13 11:49:17 +03:00
|
|
|
return jsonify(this, ["createdDate"], ["sortOrder", "image"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
get id() {
|
|
|
|
return this._id;
|
|
|
|
}
|
|
|
|
|
|
|
|
get createdDate() {
|
|
|
|
return this._createdDate;
|
|
|
|
}
|
|
|
|
set createdDate(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "number" || !val, "Bad Clip createdDate:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._createdDate = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get image() {
|
|
|
|
return this._image;
|
|
|
|
}
|
|
|
|
set image(image) {
|
2017-05-02 02:58:23 +03:00
|
|
|
if (!image) {
|
2017-04-13 11:49:17 +03:00
|
|
|
this._image = undefined;
|
|
|
|
return;
|
|
|
|
}
|
2017-09-25 21:17:43 +03:00
|
|
|
assert(checkObject(image, ["url"], ["dimensions", "text", "location", "captureType", "type"]), "Bad attrs for Clip Image:", Object.keys(image));
|
2017-09-13 22:40:39 +03:00
|
|
|
assert(isValidClipImageUrl(image.url), "Bad Clip image URL:", image.url);
|
2017-09-28 23:18:35 +03:00
|
|
|
assert(
|
2018-02-23 03:57:01 +03:00
|
|
|
image.captureType === "madeSelection" ||
|
|
|
|
image.captureType === "selection" ||
|
|
|
|
image.captureType === "visible" ||
|
|
|
|
image.captureType === "auto" ||
|
|
|
|
image.captureType === "fullPage" ||
|
|
|
|
image.captureType === "fullPageTruncated" ||
|
2017-09-28 23:18:35 +03:00
|
|
|
!image.captureType, "Bad image.captureType:", image.captureType);
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof image.text === "string" || !image.text, "Bad Clip image text:", image.text);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (image.dimensions) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof image.dimensions.x === "number" && typeof image.dimensions.y === "number", "Bad Clip image dimensions:", image.dimensions);
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
2017-09-25 21:17:43 +03:00
|
|
|
if (image.type) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(image.type === "png" || image.type === "jpeg", "Unexpected image type:", image.type);
|
2017-09-25 21:17:43 +03:00
|
|
|
}
|
2017-04-13 11:49:17 +03:00
|
|
|
assert(image.location &&
|
2018-02-23 03:57:01 +03:00
|
|
|
typeof image.location.left === "number" &&
|
|
|
|
typeof image.location.right === "number" &&
|
|
|
|
typeof image.location.top === "number" &&
|
|
|
|
typeof image.location.bottom === "number", "Bad Clip image pixel location:", image.location);
|
2017-04-13 11:49:17 +03:00
|
|
|
if (image.location.topLeftElement || image.location.topLeftOffset ||
|
|
|
|
image.location.bottomRightElement || image.location.bottomRightOffset) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof image.location.topLeftElement === "string" &&
|
2017-04-13 11:49:17 +03:00
|
|
|
image.location.topLeftOffset &&
|
2018-02-23 03:57:01 +03:00
|
|
|
typeof image.location.topLeftOffset.x === "number" &&
|
|
|
|
typeof image.location.topLeftOffset.y === "number" &&
|
|
|
|
typeof image.location.bottomRightElement === "string" &&
|
2017-04-13 11:49:17 +03:00
|
|
|
image.location.bottomRightOffset &&
|
2018-02-23 03:57:01 +03:00
|
|
|
typeof image.location.bottomRightOffset.x === "number" &&
|
|
|
|
typeof image.location.bottomRightOffset.y === "number",
|
2017-04-13 11:49:17 +03:00
|
|
|
"Bad Clip image element location:", image.location);
|
|
|
|
}
|
|
|
|
this._image = image;
|
|
|
|
}
|
|
|
|
|
|
|
|
isDataUrl() {
|
|
|
|
if (this.image) {
|
|
|
|
return this.image.url.startsWith("data:");
|
|
|
|
}
|
2017-05-02 02:58:23 +03:00
|
|
|
return false;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
get sortOrder() {
|
|
|
|
return this._sortOrder || null;
|
|
|
|
}
|
|
|
|
set sortOrder(val) {
|
2018-02-23 03:57:01 +03:00
|
|
|
assert(typeof val === "number" || !val, "Bad Clip sortOrder:", val);
|
2017-04-13 11:49:17 +03:00
|
|
|
this._sortOrder = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
AbstractShot.prototype.Clip = _Clip;
|
|
|
|
|
2018-02-23 03:57:01 +03:00
|
|
|
if (typeof exports !== "undefined") {
|
2017-04-13 11:49:17 +03:00
|
|
|
exports.AbstractShot = AbstractShot;
|
|
|
|
exports.originFromUrl = originFromUrl;
|
2017-09-13 22:40:39 +03:00
|
|
|
exports.isValidClipImageUrl = isValidClipImageUrl;
|
2017-04-13 11:49:17 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return exports;
|
|
|
|
})();
|
|
|
|
null;
|
|
|
|
|