Canvas Quiz Untracker

Stop Canvas from logging events such as the user leaving the page to cheat

  1. // ==UserScript==
  2. // @name Canvas Quiz Untracker
  3. // @namespace 7549a111-af08-40ea-a952-539c6d8e2021
  4. // @version 1.0.0
  5. // @description Stop Canvas from logging events such as the user leaving the page to cheat
  6. // @author PrimePlaya24
  7. // @license MIT
  8. // @include /^https:\/\/canvas\.[a-z0-9]*?\.[a-z]*?\/?(.*)?/
  9. // @include /^https:\/\/[a-z0-9]*?\.instructure\.com\/?(.*)?/
  10. // @icon https://canvas.instructure.com/favicon.ico
  11. // @grant none
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. /*
  16.  
  17. Example:
  18.  
  19. {"url":"/api/v1/courses/261055/quizzes/300404/submissions/11199445/events","type":"POST","global":false,"headers":{"Accept":"application/json; charset=UTF-8","Content-Type":"application/json; charset=UTF-8"},"data":"{\"quiz_submission_events\":[]}"}
  20.  
  21. options - object
  22. data - stringified object
  23. quiz_submission_events - array
  24. */
  25.  
  26. const log = (str, ...data) => {
  27. if (isEmptyArray(data)) {
  28. console.log(`[CQU] - ${str}`);
  29. } else {
  30. console.log(`[CQU] - ${str}:`, data);
  31. }
  32. };
  33.  
  34. const get = (selector) =>
  35. selector
  36. .replace(/\[([^\[\]]*)\]/g, ".$1.")
  37. .split(".")
  38. .filter((t) => t !== "")
  39. .reduce((prev, cur) => prev && prev[cur], window);
  40.  
  41. const isEmptyObject = (obj) => JSON.stringify(obj) === "{}";
  42.  
  43. const isEmptyArray = (arr) => !arr.length;
  44.  
  45. const waitUntil = (fn) => {
  46. return new Promise(async (resolve, reject) => {
  47. if (typeof fn !== "function") {
  48. reject();
  49. }
  50. while (Boolean(fn()) !== true) {
  51. await new Promise((r) => setTimeout(r, 10));
  52. }
  53. resolve(true);
  54. });
  55. };
  56.  
  57. const main = () => {
  58. waitUntil(() => get("jQuery") !== undefined).then(() => {
  59. const ajaxOriginal = window.jQuery.ajax;
  60. window.jQuery.ajax = function () {
  61. const ID = (Math.random() * 1e18).toString(36).substring(0, 6);
  62. const ajaxOptions = arguments?.[0];
  63. // log(`[${ID}] ajaxOptions`, ajaxOptions);
  64. if (
  65. ajaxOptions?.url?.search?.("events") > -1 ||
  66. ajaxOptions?.url?.search?.("page_views") > -1
  67. ) {
  68. const moddedData =
  69. typeof ajaxOptions?.data === "string" &&
  70. JSON.parse(ajaxOptions?.data ?? "{}");
  71. if (!isEmptyObject(moddedData)) {
  72. const unfiltered = moddedData.quiz_submission_events;
  73. moddedData.quiz_submission_events =
  74. moddedData?.quiz_submission_events?.filter?.(
  75. (e) => e.event_type.search("page") == -1
  76. );
  77. if (isEmptyArray(moddedData.quiz_submission_events)) {
  78. log(
  79. `[${ID}] Skipped sending an empty log, not sending!`,
  80. moddedData
  81. );
  82. return Promise.resolve();
  83. }
  84. log(
  85. `[${ID}] Cleaned AJAX request, [before, after]`,
  86. unfiltered,
  87. moddedData.quiz_submission_events
  88. );
  89. ajaxOptions.data = JSON.stringify(moddedData);
  90. return ajaxOriginal.apply(this, arguments);
  91. }
  92. log(`[${ID}] Not sending this AJAX request!`, ajaxOptions);
  93. return Promise.resolve();
  94. }
  95. log(`[${ID}] Sending unmodified`, ajaxOptions);
  96. return ajaxOriginal.apply(this, arguments);
  97. };
  98. });
  99. };
  100.  
  101. if (["complete", "interactive"].indexOf(document.readyState) > -1) {
  102. main();
  103. } else {
  104. document.addEventListener("DOMContentLoaded", main, false);
  105. }