forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			888 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			888 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| /* globals process, require */
 | |
| 
 | |
| this.shot = (function () {
 | |
|   let exports = {}; // Note: in this library we can't use any "system" dependencies because this can be used from multiple
 | |
|   // environments
 | |
| 
 | |
|   const isNode =
 | |
|     typeof process !== "undefined" &&
 | |
|     Object.prototype.toString.call(process) === "[object process]";
 | |
|   const URL = (isNode && require("url").URL) || window.URL;
 | |
| 
 | |
|   /** 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);
 | |
|     throw new Error(`Failed assertion: ${args.join(" ")}`);
 | |
|   }
 | |
| 
 | |
|   /** True if `url` is a valid URL */
 | |
|   function isUrl(url) {
 | |
|     try {
 | |
|       const parsed = new URL(url);
 | |
| 
 | |
|       if (parsed.protocol === "view-source:") {
 | |
|         return isUrl(url.substr("view-source:".length));
 | |
|       }
 | |
| 
 | |
|       return true;
 | |
|     } catch (e) {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function isValidClipImageUrl(url) {
 | |
|     return isUrl(url) && !(url.indexOf(")") > -1);
 | |
|   }
 | |
| 
 | |
|   function assertUrl(url) {
 | |
|     if (!url) {
 | |
|       throw new Error("Empty value is not URL");
 | |
|     }
 | |
|     if (!isUrl(url)) {
 | |
|       const exc = new Error("Not a URL");
 | |
|       exc.scheme = url.split(":")[0];
 | |
|       throw exc;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function isSecureWebUri(url) {
 | |
|     return isUrl(url) && url.toLowerCase().startsWith("https");
 | |
|   }
 | |
| 
 | |
|   function assertOrigin(url) {
 | |
|     assertUrl(url);
 | |
|     if (url.search(/^https?:/i) !== -1) {
 | |
|       let newUrl = new URL(url);
 | |
|       if (newUrl.pathname != "/") {
 | |
|         throw new Error("Bad origin, might include path");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function originFromUrl(url) {
 | |
|     if (!url) {
 | |
|       return null;
 | |
|     }
 | |
|     if (url.search(/^https?:/i) === -1) {
 | |
|       // Non-HTTP URLs don't have an origin
 | |
|       return null;
 | |
|     }
 | |
|     try {
 | |
|       let tryUrl = new URL(url);
 | |
|       return tryUrl.origin;
 | |
|     } catch {
 | |
|       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) {
 | |
|     if (typeof obj !== "object" || obj === null) {
 | |
|       throw new Error(
 | |
|         "Cannot check non-object: " +
 | |
|           typeof obj +
 | |
|           " that is " +
 | |
|           JSON.stringify(obj)
 | |
|       );
 | |
|     }
 | |
|     required = required || [];
 | |
|     for (const attr of required) {
 | |
|       if (!(attr in obj)) {
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     optional = optional || [];
 | |
|     for (const attr in obj) {
 | |
|       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 || [];
 | |
|     const result = {};
 | |
|     for (const attr of required) {
 | |
|       result[attr] = obj[attr];
 | |
|     }
 | |
|     optional = optional || [];
 | |
|     for (const attr of optional) {
 | |
|       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;
 | |
|     }
 | |
|     if (typeof a !== "object" || typeof b !== "object") {
 | |
|       return a === b;
 | |
|     }
 | |
|     if (Array.isArray(a)) {
 | |
|       if (!Array.isArray(b)) {
 | |
|         return false;
 | |
|       }
 | |
|       if (a.length !== b.length) {
 | |
|         return false;
 | |
|       }
 | |
|       for (let i = 0; i < a.length; i++) {
 | |
|         if (!deepEqual(a[i], b[i])) {
 | |
|           return false;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     if (Array.isArray(b)) {
 | |
|       return false;
 | |
|     }
 | |
|     const seen = new Set();
 | |
|     for (const attr of Object.keys(a)) {
 | |
|       if (!deepEqual(a[attr], b[attr])) {
 | |
|         return false;
 | |
|       }
 | |
|       seen.add(attr);
 | |
|     }
 | |
|     for (const attr of Object.keys(b)) {
 | |
|       if (!seen.has(attr)) {
 | |
|         if (!deepEqual(a[attr], b[attr])) {
 | |
|           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;
 | |
|       if (!id) {
 | |
|         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 || {};
 | |
|       assert(
 | |
|         /^[a-zA-Z0-9]{1,4000}\/[a-z0-9._-]{1,4000}$/.test(id),
 | |
|         "Bad ID (should be alphanumeric):",
 | |
|         JSON.stringify(id)
 | |
|       );
 | |
|       this._backend = backend;
 | |
|       this._id = id;
 | |
|       this.origin = attrs.origin || null;
 | |
|       this.fullUrl = attrs.fullUrl || null;
 | |
|       if (!attrs.fullUrl && attrs.url) {
 | |
|         console.warn("Received deprecated attribute .url");
 | |
|         this.fullUrl = attrs.url;
 | |
|       }
 | |
|       if (this.origin && !isSecureWebUri(this.origin)) {
 | |
|         this.origin = "";
 | |
|       }
 | |
|       if (this.fullUrl && !isSecureWebUri(this.fullUrl)) {
 | |
|         this.fullUrl = "";
 | |
|       }
 | |
|       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;
 | |
|       this.thumbnail = attrs.thumbnail || null;
 | |
|       this.abTests = attrs.abTests || null;
 | |
|       this.firefoxChannel = attrs.firefoxChannel || null;
 | |
|       this._clips = {};
 | |
|       if (attrs.clips) {
 | |
|         for (const clipId in attrs.clips) {
 | |
|           const clip = attrs.clips[clipId];
 | |
|           this._clips[clipId] = new this.Clip(this, clipId, clip);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       const isProd =
 | |
|         typeof process !== "undefined" && process.env.NODE_ENV === "production";
 | |
| 
 | |
|       for (const attr in attrs) {
 | |
|         if (
 | |
|           attr !== "clips" &&
 | |
|           attr !== "id" &&
 | |
|           !this.REGULAR_ATTRS.includes(attr) &&
 | |
|           !this.DEPRECATED_ATTRS.includes(attr)
 | |
|         ) {
 | |
|           if (isProd) {
 | |
|             console.warn("Unexpected attribute: " + attr);
 | |
|           } else {
 | |
|             throw new Error("Unexpected attribute: " + attr);
 | |
|           }
 | |
|         } 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) {
 | |
|       const ALL_ATTRS = ["clips"].concat(this.REGULAR_ATTRS);
 | |
|       assert(
 | |
|         checkObject(json, [], ALL_ATTRS),
 | |
|         "Bad attr to new Shot():",
 | |
|         Object.keys(json)
 | |
|       );
 | |
|       for (const attr in json) {
 | |
|         if (attr === "clips") {
 | |
|           continue;
 | |
|         }
 | |
|         if (
 | |
|           typeof json[attr] === "object" &&
 | |
|           typeof this[attr] === "object" &&
 | |
|           this[attr] !== null
 | |
|         ) {
 | |
|           let val = this[attr];
 | |
|           if (val.toJSON) {
 | |
|             val = val.toJSON();
 | |
|           }
 | |
|           if (!deepEqual(json[attr], val)) {
 | |
|             this[attr] = json[attr];
 | |
|           }
 | |
|         } else if (json[attr] !== this[attr] && (json[attr] || this[attr])) {
 | |
|           this[attr] = json[attr];
 | |
|         }
 | |
|       }
 | |
|       if (json.clips) {
 | |
|         for (const clipId in json.clips) {
 | |
|           if (!json.clips[clipId]) {
 | |
|             this.delClip(clipId);
 | |
|           } else if (!this.getClip(clipId)) {
 | |
|             this.setClip(clipId, json.clips[clipId]);
 | |
|           } else if (
 | |
|             !deepEqual(this.getClip(clipId).toJSON(), json.clips[clipId])
 | |
|           ) {
 | |
|             this.setClip(clipId, json.clips[clipId]);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     /** Returns a JSON version of this shot */
 | |
|     toJSON() {
 | |
|       const result = {};
 | |
|       for (const attr of this.REGULAR_ATTRS) {
 | |
|         let val = this[attr];
 | |
|         if (val && val.toJSON) {
 | |
|           val = val.toJSON();
 | |
|         }
 | |
|         result[attr] = val;
 | |
|       }
 | |
|       result.clips = {};
 | |
|       for (const attr in this._clips) {
 | |
|         result.clips[attr] = this._clips[attr].toJSON();
 | |
|       }
 | |
|       return result;
 | |
|     }
 | |
| 
 | |
|     /** A more minimal JSON representation for creating indexes of shots */
 | |
|     asRecallJson() {
 | |
|       const result = { clips: {} };
 | |
|       for (const attr of this.RECALL_ATTRS) {
 | |
|         let val = this[attr];
 | |
|         if (val && val.toJSON) {
 | |
|           val = val.toJSON();
 | |
|         }
 | |
|         result[attr] = val;
 | |
|       }
 | |
|       for (const name of this.clipNames()) {
 | |
|         result.clips[name] = this.getClip(name).toJSON();
 | |
|       }
 | |
|       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;
 | |
|     }
 | |
| 
 | |
|     get isOwner() {
 | |
|       return this._isOwner;
 | |
|     }
 | |
| 
 | |
|     set isOwner(val) {
 | |
|       this._isOwner = val || undefined;
 | |
|     }
 | |
| 
 | |
|     get filename() {
 | |
|       let filenameTitle = this.title;
 | |
|       const date = new Date(this.createdDate);
 | |
|       /* eslint-disable no-control-regex */
 | |
|       filenameTitle = filenameTitle
 | |
|         .replace(/[\\/]/g, "_")
 | |
|         .replace(/[\u200e\u200f\u202a-\u202e]/g, "")
 | |
|         .replace(/[\x00-\x1f\x7f-\x9f:*?|"<>;,+=\[\]]+/g, " ")
 | |
|         .replace(/^[\s\u180e.]+|[\s\u180e.]+$/g, "");
 | |
|       /* eslint-enable no-control-regex */
 | |
|       filenameTitle = filenameTitle.replace(/\s{1,4000}/g, " ");
 | |
|       const currentDateTime = new Date(
 | |
|         date.getTime() - date.getTimezoneOffset() * 60 * 1000
 | |
|       ).toISOString();
 | |
|       const filenameDate = currentDateTime.substring(0, 10);
 | |
|       const filenameTime = currentDateTime.substring(11, 19).replace(/:/g, "-");
 | |
|       let clipFilename = `Screenshot ${filenameDate} at ${filenameTime} ${filenameTitle}`;
 | |
| 
 | |
|       // Crop the filename size at less than 246 bytes, so as to leave
 | |
|       // room for the extension and an ellipsis [...]. Note that JS
 | |
|       // strings are UTF16 but the filename will be converted to UTF8
 | |
|       // when saving which could take up more space, and we want a
 | |
|       // maximum of 255 bytes (not characters). Here, we iterate
 | |
|       // and crop at shorter and shorter points until we fit into
 | |
|       // 255 bytes.
 | |
|       let suffix = "";
 | |
|       for (let cropSize = 246; cropSize >= 0; cropSize -= 32) {
 | |
|         if (new Blob([clipFilename]).size > 246) {
 | |
|           clipFilename = clipFilename.substring(0, cropSize);
 | |
|           suffix = "[...]";
 | |
|         } else {
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       clipFilename += suffix;
 | |
| 
 | |
|       const clip = this.getClip(this.clipNames()[0]);
 | |
|       let extension = ".png";
 | |
|       if (clip && clip.image && clip.image.type) {
 | |
|         if (clip.image.type === "jpeg") {
 | |
|           extension = ".jpg";
 | |
|         }
 | |
|       }
 | |
|       return clipFilename + extension;
 | |
|     }
 | |
| 
 | |
|     get urlDisplay() {
 | |
|       if (!this.url) {
 | |
|         return null;
 | |
|       }
 | |
|       if (/^https?:\/\//i.test(this.url)) {
 | |
|         let txt = this.url;
 | |
|         txt = txt.replace(/^[a-z]{1,4000}:\/\//i, "");
 | |
|         txt = txt.replace(/\/.{0,4000}/, "");
 | |
|         txt = txt.replace(/^www\./i, "");
 | |
|         return txt;
 | |
|       } else if (this.url.startsWith("data:")) {
 | |
|         return "data:url";
 | |
|       }
 | |
|       let txt = this.url;
 | |
|       txt = txt.replace(/\?.{0,4000}/, "");
 | |
|       return txt;
 | |
|     }
 | |
| 
 | |
|     get viewUrl() {
 | |
|       const url = this.backend + "/" + this.id;
 | |
|       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) {
 | |
|       assert(val === null || typeof val === "string", "Bad docTitle:", val);
 | |
|       this._title = val;
 | |
|     }
 | |
| 
 | |
|     get openGraph() {
 | |
|       return this._openGraph || null;
 | |
|     }
 | |
|     set openGraph(val) {
 | |
|       assert(val === null || typeof val === "object", "Bad openGraph:", val);
 | |
|       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) {
 | |
|       assert(val === null || typeof val === "object", "Bad twitterCard:", val);
 | |
|       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) {
 | |
|       assert(val === null || typeof val === "string", "Bad userTitle:", val);
 | |
|       this._userTitle = val;
 | |
|     }
 | |
| 
 | |
|     get title() {
 | |
|       // FIXME: we shouldn't support both openGraph.title and ogTitle
 | |
|       const ogTitle = this.openGraph && this.openGraph.title;
 | |
|       const twitterTitle = this.twitterCard && this.twitterCard.title;
 | |
|       let title =
 | |
|         this.userTitle || ogTitle || twitterTitle || this.docTitle || this.url;
 | |
|       if (Array.isArray(title)) {
 | |
|         title = title[0];
 | |
|       }
 | |
|       if (!title) {
 | |
|         title = "Screenshot";
 | |
|       }
 | |
|       return title;
 | |
|     }
 | |
| 
 | |
|     get createdDate() {
 | |
|       return this._createdDate;
 | |
|     }
 | |
|     set createdDate(val) {
 | |
|       assert(val === null || typeof val === "number", "Bad createdDate:", val);
 | |
|       this._createdDate = val;
 | |
|     }
 | |
| 
 | |
|     clipNames() {
 | |
|       const names = Object.getOwnPropertyNames(this._clips);
 | |
|       names.sort(function (a, b) {
 | |
|         return a.sortOrder < b.sortOrder ? 1 : 0;
 | |
|       });
 | |
|       return names;
 | |
|     }
 | |
|     getClip(name) {
 | |
|       return this._clips[name];
 | |
|     }
 | |
|     addClip(val) {
 | |
|       const name = makeRandomId();
 | |
|       this.setClip(name, val);
 | |
|       return name;
 | |
|     }
 | |
|     setClip(name, val) {
 | |
|       const clip = new this.Clip(this, name, val);
 | |
|       this._clips[name] = clip;
 | |
|     }
 | |
|     delClip(name) {
 | |
|       if (!this._clips[name]) {
 | |
|         throw new Error("No existing clip with id: " + name);
 | |
|       }
 | |
|       delete this._clips[name];
 | |
|     }
 | |
|     delAllClips() {
 | |
|       this._clips = {};
 | |
|     }
 | |
|     biggestClipSortOrder() {
 | |
|       let biggest = 0;
 | |
|       for (const clipId in this._clips) {
 | |
|         biggest = Math.max(biggest, this._clips[clipId].sortOrder);
 | |
|       }
 | |
|       return biggest;
 | |
|     }
 | |
|     updateClipUrl(clipId, clipUrl) {
 | |
|       const clip = this.getClip(clipId);
 | |
|       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) {
 | |
|       assert(typeof val === "string" || !val);
 | |
|       this._siteName = val;
 | |
|     }
 | |
| 
 | |
|     get documentSize() {
 | |
|       return this._documentSize;
 | |
|     }
 | |
|     set documentSize(val) {
 | |
|       assert(typeof val === "object" || !val);
 | |
|       if (val) {
 | |
|         assert(
 | |
|           checkObject(
 | |
|             val,
 | |
|             ["height", "width"],
 | |
|             "Bad attr to documentSize:",
 | |
|             Object.keys(val)
 | |
|           )
 | |
|         );
 | |
|         assert(typeof val.height === "number");
 | |
|         assert(typeof val.width === "number");
 | |
|         this._documentSize = val;
 | |
|       } else {
 | |
|         this._documentSize = null;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get thumbnail() {
 | |
|       return this._thumbnail;
 | |
|     }
 | |
|     set thumbnail(val) {
 | |
|       assert(typeof val === "string" || !val);
 | |
|       if (val) {
 | |
|         assert(isUrl(val));
 | |
|         this._thumbnail = val;
 | |
|       } else {
 | |
|         this._thumbnail = null;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     get abTests() {
 | |
|       return this._abTests;
 | |
|     }
 | |
|     set abTests(val) {
 | |
|       if (val === null || val === undefined) {
 | |
|         this._abTests = null;
 | |
|         return;
 | |
|       }
 | |
|       assert(
 | |
|         typeof val === "object",
 | |
|         "abTests should be an object, not:",
 | |
|         typeof val
 | |
|       );
 | |
|       assert(!Array.isArray(val), "abTests should not be an Array");
 | |
|       for (const name in val) {
 | |
|         assert(
 | |
|           val[name] && typeof val[name] === "string",
 | |
|           `abTests.${name} should be a string:`,
 | |
|           typeof val[name]
 | |
|         );
 | |
|       }
 | |
|       this._abTests = val;
 | |
|     }
 | |
| 
 | |
|     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;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   AbstractShot.prototype.REGULAR_ATTRS = `
 | |
| origin fullUrl docTitle userTitle createdDate images
 | |
| siteName openGraph twitterCard documentSize
 | |
| thumbnail abTests firefoxChannel
 | |
| `.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 url
 | |
| fullScreenThumbnail favicon
 | |
| `.split(/\s+/g);
 | |
| 
 | |
|   AbstractShot.prototype.RECALL_ATTRS = `
 | |
| url docTitle userTitle createdDate openGraph twitterCard images thumbnail
 | |
| `.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;
 | |
|       assert(
 | |
|         !json.dimensions ||
 | |
|           (typeof json.dimensions.x === "number" &&
 | |
|             typeof json.dimensions.y === "number"),
 | |
|         "Bad Image dimensions:",
 | |
|         json.dimensions
 | |
|       );
 | |
|       this.dimensions = json.dimensions;
 | |
|       assert(
 | |
|         typeof json.title === "string" || !json.title,
 | |
|         "Bad Image title:",
 | |
|         json.title
 | |
|       );
 | |
|       this.title = json.title;
 | |
|       assert(
 | |
|         typeof json.alt === "string" || !json.alt,
 | |
|         "Bad Image alt:",
 | |
|         json.alt
 | |
|       );
 | |
|       this.alt = json.alt;
 | |
|     }
 | |
| 
 | |
|     toJSON() {
 | |
|       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)
 | |
|       );
 | |
|       assert(typeof id === "string" && id, "Bad Clip id:", id);
 | |
|       this._id = id;
 | |
|       this.createdDate = json.createdDate;
 | |
|       if ("sortOrder" in json) {
 | |
|         assert(
 | |
|           typeof json.sortOrder === "number" || !json.sortOrder,
 | |
|           "Bad Clip sortOrder:",
 | |
|           json.sortOrder
 | |
|         );
 | |
|       }
 | |
|       if ("sortOrder" in json) {
 | |
|         this.sortOrder = json.sortOrder;
 | |
|       } else {
 | |
|         const biggestOrder = shot.biggestClipSortOrder();
 | |
|         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}]`;
 | |
|     }
 | |
| 
 | |
|     toJSON() {
 | |
|       return jsonify(this, ["createdDate"], ["sortOrder", "image"]);
 | |
|     }
 | |
| 
 | |
|     get id() {
 | |
|       return this._id;
 | |
|     }
 | |
| 
 | |
|     get createdDate() {
 | |
|       return this._createdDate;
 | |
|     }
 | |
|     set createdDate(val) {
 | |
|       assert(typeof val === "number" || !val, "Bad Clip createdDate:", val);
 | |
|       this._createdDate = val;
 | |
|     }
 | |
| 
 | |
|     get image() {
 | |
|       return this._image;
 | |
|     }
 | |
|     set image(image) {
 | |
|       if (!image) {
 | |
|         this._image = undefined;
 | |
|         return;
 | |
|       }
 | |
|       assert(
 | |
|         checkObject(
 | |
|           image,
 | |
|           ["url"],
 | |
|           ["dimensions", "text", "location", "captureType", "type"]
 | |
|         ),
 | |
|         "Bad attrs for Clip Image:",
 | |
|         Object.keys(image)
 | |
|       );
 | |
|       assert(isValidClipImageUrl(image.url), "Bad Clip image URL:", image.url);
 | |
|       assert(
 | |
|         image.captureType === "madeSelection" ||
 | |
|           image.captureType === "selection" ||
 | |
|           image.captureType === "visible" ||
 | |
|           image.captureType === "auto" ||
 | |
|           image.captureType === "fullPage" ||
 | |
|           image.captureType === "fullPageTruncated" ||
 | |
|           !image.captureType,
 | |
|         "Bad image.captureType:",
 | |
|         image.captureType
 | |
|       );
 | |
|       assert(
 | |
|         typeof image.text === "string" || !image.text,
 | |
|         "Bad Clip image text:",
 | |
|         image.text
 | |
|       );
 | |
|       if (image.dimensions) {
 | |
|         assert(
 | |
|           typeof image.dimensions.x === "number" &&
 | |
|             typeof image.dimensions.y === "number",
 | |
|           "Bad Clip image dimensions:",
 | |
|           image.dimensions
 | |
|         );
 | |
|       }
 | |
|       if (image.type) {
 | |
|         assert(
 | |
|           image.type === "png" || image.type === "jpeg",
 | |
|           "Unexpected image type:",
 | |
|           image.type
 | |
|         );
 | |
|       }
 | |
|       assert(
 | |
|         image.location &&
 | |
|           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
 | |
|       );
 | |
|       if (
 | |
|         image.location.topLeftElement ||
 | |
|         image.location.topLeftOffset ||
 | |
|         image.location.bottomRightElement ||
 | |
|         image.location.bottomRightOffset
 | |
|       ) {
 | |
|         assert(
 | |
|           typeof image.location.topLeftElement === "string" &&
 | |
|             image.location.topLeftOffset &&
 | |
|             typeof image.location.topLeftOffset.x === "number" &&
 | |
|             typeof image.location.topLeftOffset.y === "number" &&
 | |
|             typeof image.location.bottomRightElement === "string" &&
 | |
|             image.location.bottomRightOffset &&
 | |
|             typeof image.location.bottomRightOffset.x === "number" &&
 | |
|             typeof image.location.bottomRightOffset.y === "number",
 | |
|           "Bad Clip image element location:",
 | |
|           image.location
 | |
|         );
 | |
|       }
 | |
|       this._image = image;
 | |
|     }
 | |
| 
 | |
|     isDataUrl() {
 | |
|       if (this.image) {
 | |
|         return this.image.url.startsWith("data:");
 | |
|       }
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     get sortOrder() {
 | |
|       return this._sortOrder || null;
 | |
|     }
 | |
|     set sortOrder(val) {
 | |
|       assert(typeof val === "number" || !val, "Bad Clip sortOrder:", val);
 | |
|       this._sortOrder = val;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   AbstractShot.prototype.Clip = _Clip;
 | |
| 
 | |
|   if (typeof exports !== "undefined") {
 | |
|     exports.AbstractShot = AbstractShot;
 | |
|     exports.originFromUrl = originFromUrl;
 | |
|     exports.isValidClipImageUrl = isValidClipImageUrl;
 | |
|   }
 | |
| 
 | |
|   return exports;
 | |
| })();
 | |
| null;
 | 
