/** \file
 * \brief This file contains wrappers for Canvas-related calls
 *
 * \see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
 * \see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
 * \see https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas
 *
 *  \author Copyright (C) 2019  Libor Polcak
 *  \author Copyright (C) 2021  Matus Svancar
 *  \author Copyright (C) 2023  Martin Zmitko
 *
 *  \license SPDX-License-Identifier: GPL-3.0-or-later
 *  \license SPDX-License-Identifier: MPL-2.0
 */
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
//  Alternatively, the contents of this file may be used under the terms
//  of the Mozilla Public License, v. 2.0, as described below:
//
//  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/.
//
//  \copyright Copyright (c) 2020 The Brave Authors.

/** \file
 * \ingroup wrappers
 * This file contains wrappers for calls related to the Canvas API, about which you can read more at MDN:
 *
 *  * [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)
 *  * [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D)
 *  * [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas)
 *
 * The goal is to prevent fingerprinting by modifying the values that can be read from the canvas.
 * So the visual content of wrapped canvases as displayed on the screen is the same as intended.
 *
 * The modified content can be either an empty image or a fake image that is modified according to
 * session and domain keys to be different than the original albeit very similar (i.e. the approach
 * inspired by the algorithms created by [Brave Software](https://brave.com) available [here](https://github.com/brave/brave-core/blob/master/chromium_src/third_party/blink/renderer/core/execution_context/execution_context.cc).
 *
 * Note that both approaches are detectable by a fingerprinter that checks if a predetermined image
 * inserted to the canvas is the same as the read one, see [here](https://arkenfox.github.io/TZP/tests/canvasnoise.html) for an example,
 * Nevertheless, the aim of the wrappers is to limit the finerprintability.
 *
 * Also note that a determined fingerprinter can reveal the modifications and consequently uncover
 * the original image. This can be avoided with the approach that completely clears the data stored
 * in the canvas. Use the modifications based on session and domain keys if you want to provide an
 * image that is similar to the original or if you want to produce a fake image that is not
 * obviously spoofed to a naked eye. Otherwise, use the clearing approach.
 */

/*
 * Create private namespace
 */
(function() {

	const DEF_CANVAS_COPY = `
		let canvasCopy = ctx => {
			let {width, height} = ctx.canvas;
			let fake = document.createElement("canvas");
			fake.setAttribute("width", width);
			fake.setAttribute("height", height);
			let stx = fake.getContext("2d");
			let imageData = window.CanvasRenderingContext2D.prototype.getImageData.call(ctx, 0, 0, width, height);
			stx.putImageData(imageData, 0, 0);
			return fake;
		};
	`;

	/** @var String helping_code.
	 * Selects if the canvas should be cleared (1) or a fake image should be created based on session
	 * and domain keys (0).
	 */
	var helping_code = `var approach = args[0];`;
	var wrappers = [{
			parent_object: "HTMLCanvasElement.prototype",
			parent_object_property: "toDataURL",
			wrapped_objects: [{
				original_name: "HTMLCanvasElement.prototype.toDataURL",
				wrapped_name: "origToDataURL",
			}],
			helping_code: helping_code,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.HTMLCanvasElement.prototype.toDataURL",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake HTMLCanvasElement.prototype.toDataURL
			 * \brief Returns fake canvas content, see CanvasRenderingContext2D.prototype for more details.
			 *
			 * Internally creates a fake canvas of the same height and width as the original and calls
			 * CanvasRenderingContext2D.getImageData() that determines the result. If canvas uses WebGLRenderingContext
			 * the content is copied to new canvas using CanvasRenderingContext2D and function toDataURL is called on it.
			 */
			wrapping_function_body: `
				var ctx = this.getContext("2d");
				if(ctx){
					${DEF_CANVAS_COPY}
					return origToDataURL.call(canvasCopy(ctx), ...args);
				}
				else {
					var ctx = this.getContext("webgl2", {preserveDrawingBuffer: true}) ||
					  this.getContext("experimental-webgl2", {preserveDrawingBuffer: true}) ||
					  this.getContext("webgl", {preserveDrawingBuffer: true}) ||
					  this.getContext("experimental-webgl", {preserveDrawingBuffer: true}) ||
					  this.getContext("moz-webgl", {preserveDrawingBuffer: true});
					if(ctx){
					  var fake = document.createElement("canvas");
					  fake.setAttribute("width", this.width);
					  fake.setAttribute("height", this.height);
					  var stx = fake.getContext("2d");
					  stx.drawImage(ctx.canvas, 0, 0);
					  return HTMLCanvasElement.prototype.toDataURL.call(fake);
          }
				}
				`,
		},
		{
			parent_object: "CanvasRenderingContext2D.prototype",
			parent_object_property: "getImageData",
			wrapped_objects: [{
				original_name: "CanvasRenderingContext2D.prototype.getImageData",
				wrapped_name: "origGetImageData",
			}],
			helping_code: helping_code + farbleCanvasDataBrave.toString() + `
				var farble = function(context, fake) {
					if(approach === 1){
						fake.fillStyle = "white";
						fake.fillRect(0, 0, context.canvas.width, context.canvas.height);
						return;
					}
					else if(approach === 0){
						const width = context.canvas.width;
						const height = context.canvas.height;
						const imageData = origGetImageData.call(context, 0, 0, width, height);
						const len = imageData.data.length;
						if (wasm.ready && wasm.grow(len)) {
							try {
								farblePixelsWASM();
							} catch (e) {
								console.error("WebAssembly optimized farbling failed, falling back to JavaScript implementation", e);
								farblePixelsJS();
							}
						} else {
							farblePixelsJS();
						}
						// Do not modify the original canvas, always modify the fake canvas.
						// Always farble the whole image so that the farbled data do not depend
						// on the page-specified extraction data rectangle.
						fake.putImageData(imageData, 0, 0);
						
						function farblePixelsWASM() {
							wasm.set(imageData.data);
							const crc = wasm.crc16(len);
							const mash = new Mash();
							mash.addData(' ');
							mash.addData(domainHash);
							mash.addData("CanvasFarbling");
							mash.addData(crc);
							wasm.farbleBytes(len, mash.n | 0, true);
							imageData.data.set(wasm.get(len));
						}
						
						function farblePixelsJS() {
							const BYTES_PER_ROW = width * 4;
							farbleCanvasDataBrave(function*() {
								let data = imageData.data;
								let offset = 0;
								while (offset < len) {
									yield data.subarray(offset, offset + BYTES_PER_ROW);
									offset += BYTES_PER_ROW;
								}
							}, width);
						}
					}
				};`,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.CanvasRenderingContext2D.prototype.getImageData",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake CanvasRenderingContext2D.prototype.getImageData
			 * \brief Returns a fake image data of the same height and width as stored in the original canvas.
			 *
			 * Internally calls the farbling that select the output which can be either an empty image or
			 * a fake image that is modified according to session and domain keys to be different than the
			 * original albeit very similar.
			 */
			wrapping_function_body: `
				var fake = document.createElement("canvas");
				fake.setAttribute("width", this.canvas.width);
				fake.setAttribute("height", this.canvas.height);
				var stx = fake.getContext("2d");
				farble(this,stx);
				return origGetImageData.call(stx, ...args);
			`,
		},
		{
			parent_object: "HTMLCanvasElement.prototype",
			parent_object_property: "toBlob",
			wrapped_objects: [{
				original_name: "HTMLCanvasElement.prototype.toBlob",
				wrapped_name: "origToBlob",
			}],
			helping_code: ``,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.HTMLCanvasElement.prototype.toBlob",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake HTMLCanvasElement.prototype.toBlob
			 * \brief Returns fake canvas content, see CanvasRenderingContext2D.prototype for more details.
			 *
			 * Internally creates a fake canvas of the same height and width as the original and calls
			 * CanvasRenderingContext2D.getImageData() that detemines the result.
			 */
			wrapping_function_body: `
				${DEF_CANVAS_COPY}
				return origToBlob.call(canvasCopy(this.getContext("2d")), ...args);
			`,
		},
		{
			parent_object: "OffscreenCanvas.prototype",
			parent_object_property: "convertToBlob",
			wrapped_objects: [{
				original_name: "OffscreenCanvas.prototype.convertToBlob",
				wrapped_name: "origConvertToBlob",
			}],
			helping_code: ``,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.OffscreenCanvas.prototype.convertToBlob",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake OffscreenCanvas.prototype.convertToBlob
			 * \brief Returns fake canvas content, see CanvasRenderingContext2D.prototype for more details.
			 *
			 * Internally creates a fake canvas of the same height and width as the original and calls
			 * CanvasRenderingContext2D.getImageData() that detemines the result.
			 */
			wrapping_function_body: `
			${DEF_CANVAS_COPY}
			return origConvertToBlob.call(canvasCopy(this.getContext("2d")), ...args);
			`,
		},
		{
			parent_object: "CanvasRenderingContext2D.prototype",
			parent_object_property: "isPointInPath",
			wrapped_objects: [{
				original_name: "CanvasRenderingContext2D.prototype.isPointInPath",
				wrapped_name: "origIsPointInPath",
			}],
			helping_code: helping_code + `
				function farbleIsPointInPath(ctx, ...args){
					if(approach === 0){
						var ret = origIsPointInPath.call(ctx, ...args);
						return (ret && ((prng()*20) > 1));
					}
					else if(approach === 1){
						return false;
					}
				};
			`,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.CanvasRenderingContext2D.prototype.isPointInPath",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake CanvasRenderingContext2D.prototype.isPointInPath
			 * \brief Returns modified result
			 *
			 * Either returns false or original function return value which is changed to false with 1/20 probability
			 *
			 * \bug Changing value with probability has some issues:
			 * * multiple calls with the same pixel can return different values
			 * * inconsistencies among adjacent pixels
			 */
			wrapping_function_body: `
				return farbleIsPointInPath(this, ...args);
			`,
		},
		{
			parent_object: "CanvasRenderingContext2D.prototype",
			parent_object_property: "isPointInStroke",
			wrapped_objects: [{
				original_name: "CanvasRenderingContext2D.prototype.isPointInStroke",
				wrapped_name: "origIsPointInStroke",
			}],
			helping_code: helping_code + `
				function farbleIsPointInStroke(ctx, ...args){
					if(approach === 0){
						var ret = origIsPointInStroke.call(ctx, ...args);
						return (ret && ((prng()*20) > 1));
					}
					else if(approach === 1){
						return false;
					}
				};
			`,
			wrapping_code_function_name: "wrapping",
			wrapping_code_function_params: "parent",
			wrapping_code_function_call_window: true,
			original_function: "parent.CanvasRenderingContext2D.prototype.isPointInStroke",
			replace_original_function: true,
			wrapping_function_args: "...args",
			/** \fn fake CanvasRenderingContext2D.prototype.isPointInStroke
			 * \brief Returns modified result
			 *
			 * Either returns false or original function return value which is changed to false with 1/20 probability
			 *
			 * \bug Changing value with probability has some issues:
			 * * multiple calls with the same pixel can return different values
			 * * inconsistencies among adjacent pixels
			 */
			wrapping_function_body: `
				return farbleIsPointInStroke(this, ...args);
			`,
		},
	]
	add_wrappers(wrappers);
})();
