From de9775ce6908e749084eaf41726570956f99d016 Mon Sep 17 00:00:00 2001 From: Paul Holden Date: Fri, 3 Oct 2025 12:17:44 +0100 Subject: [PATCH] MDL-81441 tiny_media: broaden content search for used draft files. Changes in 47f9106c (upon which this code is based) assumed used files were always enclosed within quote marks. However this isn't the case for embedded H5P files which output the filepath directly and rely on subsequent filtering to turn into appropriate markup. --- .../editor/tiny/plugins/media/amd/build/usedfiles.min.js | 2 +- .../tiny/plugins/media/amd/build/usedfiles.min.js.map | 2 +- .../lib/editor/tiny/plugins/media/amd/src/usedfiles.js | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js b/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js index c20a7f5e956..e1e953e1abd 100644 --- a/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js +++ b/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js @@ -5,6 +5,6 @@ define("tiny_media/usedfiles",["exports","core/templates","core/config"],(functi * @module tiny_media/usedfiles * @copyright 2022, Stevani Andolo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(userContext,itemId,elementId){this.files=this.getFiles(),this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"']"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?.+?)[\\?\"']","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_media/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}getFiles(){const files={};return document.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((input=>{files[input.dataset.filename]=input.dataset.filehash})),files}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}})); + */(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(userContext,itemId,elementId){this.files=this.getFiles(),this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"'\\s>]"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?.+?)[\\?\"'\\s<]","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_media/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}getFiles(){const files={};return document.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((input=>{files[input.dataset.filename]=input.dataset.filehash})),files}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}})); //# sourceMappingURL=usedfiles.min.js.map \ No newline at end of file diff --git a/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js.map b/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js.map index cd3c083c514..10e1d654d53 100644 --- a/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js.map +++ b/public/lib/editor/tiny/plugins/media/amd/build/usedfiles.min.js.map @@ -1 +1 @@ -{"version":3,"file":"usedfiles.min.js","sources":["../src/usedfiles.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media Manager usedfiles.\n *\n * @module tiny_media/usedfiles\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Templates from 'core/templates';\nimport Config from 'core/config';\n\nclass UsedFileManager {\n constructor(userContext, itemId, elementId) {\n this.files = this.getFiles();\n this.userContext = userContext;\n this.itemId = itemId;\n this.elementId = elementId;\n }\n\n getElementId() {\n return this.elementId;\n }\n\n getUsedFiles() {\n const editor = window.parent.tinymce.EditorManager.get(this.getElementId());\n if (!editor) {\n window.console.error(`Editor not found for ${this.getElementId()}`);\n return [];\n }\n const content = editor.getContent();\n const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`;\n const pattern = new RegExp(\"[\\\"']\" + baseUrl.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&') + \"(?.+?)[\\\\?\\\"']\", 'gm');\n\n const usedFiles = [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename));\n\n return usedFiles;\n }\n\n // Return an array of unused files.\n findUnusedFiles(usedFiles) {\n return Object.entries(this.files)\n .filter(([filename]) => !usedFiles.includes(filename))\n .map(([filename]) => filename);\n }\n\n // Return an array of missing files.\n findMissingFiles(usedFiles) {\n return usedFiles.filter((filename) => !this.files.hasOwnProperty(filename));\n }\n\n updateFiles() {\n const form = document.querySelector('form');\n const usedFiles = this.getUsedFiles();\n const unusedFiles = this.findUnusedFiles(usedFiles);\n const missingFiles = this.findMissingFiles(usedFiles);\n\n form.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach((checkbox) => {\n if (!unusedFiles.includes(checkbox.dataset.filename)) {\n checkbox.closest('.fitem').remove();\n }\n });\n\n form.classList.toggle('has-missing-files', !!missingFiles.length);\n form.classList.toggle('has-unused-files', !!unusedFiles.length);\n\n return Templates.renderForPromise('tiny_media/missingfiles', {\n missingFiles,\n }).then(({html, js}) => {\n Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);\n return;\n });\n }\n\n /**\n * Retrieves a list of existing files selected for deletion.\n *\n * @returns {Object} An object where the keys are filenames and the values are file hashes.\n *\n */\n getFiles() {\n const files = {};\n document.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach(input => {\n files[input.dataset.filename] = input.dataset.filehash;\n });\n return files;\n }\n}\n\nexport const init = (files, usercontext, itemid, elementid) => {\n const manager = new UsedFileManager(files, usercontext, itemid, elementid);\n manager.updateFiles();\n\n return manager;\n};\n"],"names":["UsedFileManager","constructor","userContext","itemId","elementId","files","this","getFiles","getElementId","getUsedFiles","editor","window","parent","tinymce","EditorManager","get","console","error","content","getContent","baseUrl","Config","wwwroot","pattern","RegExp","replace","matchAll","map","match","decodeURIComponent","groups","filename","findUnusedFiles","usedFiles","Object","entries","filter","_ref","includes","_ref2","findMissingFiles","hasOwnProperty","updateFiles","form","document","querySelector","unusedFiles","missingFiles","querySelectorAll","forEach","checkbox","dataset","closest","remove","classList","toggle","length","Templates","renderForPromise","then","_ref3","html","js","replaceNodeContents","input","filehash","usercontext","itemid","elementid","manager"],"mappings":";;;;;;;+EA0BMA,gBACFC,YAAYC,YAAaC,OAAQC,gBACxBC,MAAQC,KAAKC,gBACbL,YAAcA,iBACdC,OAASA,YACTC,UAAYA,UAGrBI,sBACWF,KAAKF,UAGhBK,qBACUC,OAASC,OAAOC,OAAOC,QAAQC,cAAcC,IAAIT,KAAKE,oBACvDE,cACDC,OAAOK,QAAQC,qCAA8BX,KAAKE,iBAC3C,SAELU,QAAUR,OAAOS,aACjBC,kBAAaC,gBAAOC,kCAAyBhB,KAAKJ,mCAA0BI,KAAKH,YACjFoB,QAAU,IAAIC,OAAO,QAAUJ,QAAQK,QAAQ,wBAAyB,QAAU,2BAA4B,YAElG,IAAIP,QAAQQ,SAASH,UAAUI,KAAKC,OAAUC,mBAAmBD,MAAME,OAAOC,YAMpGC,gBAAgBC,kBACLC,OAAOC,QAAQ7B,KAAKD,OACtB+B,QAAOC,WAAEN,sBAAeE,UAAUK,SAASP,aAC3CJ,KAAIY,YAAER,uBAAcA,YAI7BS,iBAAiBP,kBACNA,UAAUG,QAAQL,WAAczB,KAAKD,MAAMoC,eAAeV,YAGrEW,oBACUC,KAAOC,SAASC,cAAc,QAC9BZ,UAAY3B,KAAKG,eACjBqC,YAAcxC,KAAK0B,gBAAgBC,WACnCc,aAAezC,KAAKkC,iBAAiBP,kBAE3CU,KAAKK,iBAAiB,4CAA4CC,SAASC,WAClEJ,YAAYR,SAASY,SAASC,QAAQpB,WACvCmB,SAASE,QAAQ,UAAUC,YAInCV,KAAKW,UAAUC,OAAO,sBAAuBR,aAAaS,QAC1Db,KAAKW,UAAUC,OAAO,qBAAsBT,YAAYU,QAEjDC,UAAUC,iBAAiB,0BAA2B,CACzDX,aAAAA,eACDY,MAAKC,YAACC,KAACA,KAADC,GAAOA,UACZL,UAAUM,oBAAoBpB,KAAKE,cAAc,kBAAmBgB,KAAMC,OAWjFvD,iBACSF,MAAQ,UACduC,SAASI,iBAAiB,4CAA4CC,SAAQe,QAC1E3D,MAAM2D,MAAMb,QAAQpB,UAAYiC,MAAMb,QAAQc,YAE3C5D,qBAIK,CAACA,MAAO6D,YAAaC,OAAQC,mBACvCC,QAAU,IAAIrE,gBAAgBK,MAAO6D,YAAaC,OAAQC,kBAChEC,QAAQ3B,cAED2B"} \ No newline at end of file +{"version":3,"file":"usedfiles.min.js","sources":["../src/usedfiles.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media Manager usedfiles.\n *\n * @module tiny_media/usedfiles\n * @copyright 2022, Stevani Andolo \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Templates from 'core/templates';\nimport Config from 'core/config';\n\nclass UsedFileManager {\n constructor(userContext, itemId, elementId) {\n this.files = this.getFiles();\n this.userContext = userContext;\n this.itemId = itemId;\n this.elementId = elementId;\n }\n\n getElementId() {\n return this.elementId;\n }\n\n getUsedFiles() {\n const editor = window.parent.tinymce.EditorManager.get(this.getElementId());\n if (!editor) {\n window.console.error(`Editor not found for ${this.getElementId()}`);\n return [];\n }\n const content = editor.getContent();\n const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`;\n\n // Match any draft file contained within quotes, whitespace, or between html elements.\n const pattern = new RegExp(\n \"[\\\"'\\\\s>]\" + baseUrl.replace(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&') + \"(?.+?)[\\\\?\\\"'\\\\s<]\",\n 'gm'\n );\n\n return [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename));\n }\n\n // Return an array of unused files.\n findUnusedFiles(usedFiles) {\n return Object.entries(this.files)\n .filter(([filename]) => !usedFiles.includes(filename))\n .map(([filename]) => filename);\n }\n\n // Return an array of missing files.\n findMissingFiles(usedFiles) {\n return usedFiles.filter((filename) => !this.files.hasOwnProperty(filename));\n }\n\n updateFiles() {\n const form = document.querySelector('form');\n const usedFiles = this.getUsedFiles();\n const unusedFiles = this.findUnusedFiles(usedFiles);\n const missingFiles = this.findMissingFiles(usedFiles);\n\n form.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach((checkbox) => {\n if (!unusedFiles.includes(checkbox.dataset.filename)) {\n checkbox.closest('.fitem').remove();\n }\n });\n\n form.classList.toggle('has-missing-files', !!missingFiles.length);\n form.classList.toggle('has-unused-files', !!unusedFiles.length);\n\n return Templates.renderForPromise('tiny_media/missingfiles', {\n missingFiles,\n }).then(({html, js}) => {\n Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);\n return;\n });\n }\n\n /**\n * Retrieves a list of existing files selected for deletion.\n *\n * @returns {Object} An object where the keys are filenames and the values are file hashes.\n *\n */\n getFiles() {\n const files = {};\n document.querySelectorAll('input[type=checkbox][name^=\"deletefile\"]').forEach(input => {\n files[input.dataset.filename] = input.dataset.filehash;\n });\n return files;\n }\n}\n\nexport const init = (files, usercontext, itemid, elementid) => {\n const manager = new UsedFileManager(files, usercontext, itemid, elementid);\n manager.updateFiles();\n\n return manager;\n};\n"],"names":["UsedFileManager","constructor","userContext","itemId","elementId","files","this","getFiles","getElementId","getUsedFiles","editor","window","parent","tinymce","EditorManager","get","console","error","content","getContent","baseUrl","Config","wwwroot","pattern","RegExp","replace","matchAll","map","match","decodeURIComponent","groups","filename","findUnusedFiles","usedFiles","Object","entries","filter","_ref","includes","_ref2","findMissingFiles","hasOwnProperty","updateFiles","form","document","querySelector","unusedFiles","missingFiles","querySelectorAll","forEach","checkbox","dataset","closest","remove","classList","toggle","length","Templates","renderForPromise","then","_ref3","html","js","replaceNodeContents","input","filehash","usercontext","itemid","elementid","manager"],"mappings":";;;;;;;+EA0BMA,gBACFC,YAAYC,YAAaC,OAAQC,gBACxBC,MAAQC,KAAKC,gBACbL,YAAcA,iBACdC,OAASA,YACTC,UAAYA,UAGrBI,sBACWF,KAAKF,UAGhBK,qBACUC,OAASC,OAAOC,OAAOC,QAAQC,cAAcC,IAAIT,KAAKE,oBACvDE,cACDC,OAAOK,QAAQC,qCAA8BX,KAAKE,iBAC3C,SAELU,QAAUR,OAAOS,aACjBC,kBAAaC,gBAAOC,kCAAyBhB,KAAKJ,mCAA0BI,KAAKH,YAGjFoB,QAAU,IAAIC,OAChB,YAAcJ,QAAQK,QAAQ,wBAAyB,QAAU,+BACjE,YAGG,IAAIP,QAAQQ,SAASH,UAAUI,KAAKC,OAAUC,mBAAmBD,MAAME,OAAOC,YAIzFC,gBAAgBC,kBACLC,OAAOC,QAAQ7B,KAAKD,OACtB+B,QAAOC,WAAEN,sBAAeE,UAAUK,SAASP,aAC3CJ,KAAIY,YAAER,uBAAcA,YAI7BS,iBAAiBP,kBACNA,UAAUG,QAAQL,WAAczB,KAAKD,MAAMoC,eAAeV,YAGrEW,oBACUC,KAAOC,SAASC,cAAc,QAC9BZ,UAAY3B,KAAKG,eACjBqC,YAAcxC,KAAK0B,gBAAgBC,WACnCc,aAAezC,KAAKkC,iBAAiBP,kBAE3CU,KAAKK,iBAAiB,4CAA4CC,SAASC,WAClEJ,YAAYR,SAASY,SAASC,QAAQpB,WACvCmB,SAASE,QAAQ,UAAUC,YAInCV,KAAKW,UAAUC,OAAO,sBAAuBR,aAAaS,QAC1Db,KAAKW,UAAUC,OAAO,qBAAsBT,YAAYU,QAEjDC,UAAUC,iBAAiB,0BAA2B,CACzDX,aAAAA,eACDY,MAAKC,YAACC,KAACA,KAADC,GAAOA,UACZL,UAAUM,oBAAoBpB,KAAKE,cAAc,kBAAmBgB,KAAMC,OAWjFvD,iBACSF,MAAQ,UACduC,SAASI,iBAAiB,4CAA4CC,SAAQe,QAC1E3D,MAAM2D,MAAMb,QAAQpB,UAAYiC,MAAMb,QAAQc,YAE3C5D,qBAIK,CAACA,MAAO6D,YAAaC,OAAQC,mBACvCC,QAAU,IAAIrE,gBAAgBK,MAAO6D,YAAaC,OAAQC,kBAChEC,QAAQ3B,cAED2B"} \ No newline at end of file diff --git a/public/lib/editor/tiny/plugins/media/amd/src/usedfiles.js b/public/lib/editor/tiny/plugins/media/amd/src/usedfiles.js index 0d23f6a72cc..050fcca22f3 100644 --- a/public/lib/editor/tiny/plugins/media/amd/src/usedfiles.js +++ b/public/lib/editor/tiny/plugins/media/amd/src/usedfiles.js @@ -44,11 +44,14 @@ class UsedFileManager { } const content = editor.getContent(); const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`; - const pattern = new RegExp("[\"']" + baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + "(?.+?)[\\?\"']", 'gm'); - const usedFiles = [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename)); + // Match any draft file contained within quotes, whitespace, or between html elements. + const pattern = new RegExp( + "[\"'\\s>]" + baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + "(?.+?)[\\?\"'\\s<]", + 'gm' + ); - return usedFiles; + return [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename)); } // Return an array of unused files.