diff --git a/.gitignore b/.gitignore index c2aea7406..fdb55f0da 100644 --- a/.gitignore +++ b/.gitignore @@ -219,3 +219,7 @@ _codeql_detected_source_root .grepai/ .codegraph/ *.bak + +# audioMotion-analyzer is AGPL-3.0-or-later and is installed by the +# administrator rather than shipped; see its src/help.txt. +web/skins/*/assets/audioMotion-analyzer/src/audioMotion-analyzer.js diff --git a/tests/js/audiomotion-paths.test.js b/tests/js/audiomotion-paths.test.js new file mode 100644 index 000000000..9a43296d0 --- /dev/null +++ b/tests/js/audiomotion-paths.test.js @@ -0,0 +1,150 @@ +'use strict'; + +// The audioMotion-analyzer library is not shipped with ZoneMinder (it is +// AGPL-3.0-or-later), so the admin installs it by hand following instructions +// that live in four places: the skin's PHP feature probe, the dynamic import in +// the skin JS, the help.txt beside the install location, and the translated +// help/strings. They drifted apart once already, sending people to a URL that +// serves a different build of the library than the one the import expects. +// These tests pin them to each other. + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const root = path.join(__dirname, '../..'); +const read = (p) => fs.readFileSync(path.join(root, p), 'utf8'); + +const INSTALL_PATH = 'assets/audioMotion-analyzer/src/audioMotion-analyzer.js'; + +const functionsPhp = read('web/skins/classic/includes/functions.php'); +const analyzerJs = read('web/skins/classic/js/audioMotionAnalyzer.js'); +const helpTxt = read('web/skins/classic/' + INSTALL_PATH.replace(/[^/]+$/, 'help.txt')); +const assetsVersion = read('web/skins/classic/assets/version'); +const enGb = read('web/lang/en_gb.php'); +const ruRu = read('web/lang/ru_ru.php'); + +let passed = 0; +let failed = 0; +function test(name, fn) { + try { + fn(); + console.log(' ok ' + name); + passed++; + } catch (e) { + console.error(' FAIL ' + name); + console.error(' ' + e.message); + failed++; + } +} + +console.log('install path agreement'); + +test('AUDIO_MOTION_ENABLED probes the path the skin JS imports', () => { + const probe = functionsPhp.match(/define\("AUDIO_MOTION_ENABLED",\s*file_exists\("([^"]+)"\)\)/); + assert.ok(probe, 'AUDIO_MOTION_ENABLED define not found in functions.php'); + // skins/$skin/assets/... -> assets/... + const probed = probe[1].replace(/^skins\/\$skin\//, ''); + assert.strictEqual(probed, INSTALL_PATH); + + const imported = analyzerJs.match(/import\('([^']+audioMotion-analyzer\.js)'\)/); + assert.ok(imported, 'dynamic import of the library not found in audioMotionAnalyzer.js'); + // The import is relative to skins//js/, so ../assets/... is assets/... + assert.strictEqual(imported[1].replace(/^\.\.\//, ''), INSTALL_PATH); +}); + +test('help.txt tells the admin the path the code actually looks at', () => { + assert.ok( + helpTxt.includes('skins/classic/' + INSTALL_PATH), + 'help.txt does not name skins/classic/' + INSTALL_PATH); +}); + +test('the help text does not use a placeholder skin name', () => { + for (const [name, text] of [['help.txt', helpTxt], ['en_gb.php', enGb], ['ru_ru.php', ruRu]]) { + assert.ok(!/MySkin/.test(text), name + ' still refers to the "MySkin" placeholder'); + } +}); + +console.log('download instructions'); + +// The package root serves "main", the minified UMD bundle in dist/. Only the +// explicit /src/ path serves the ES module that the dynamic import needs, so +// the bare URL may appear only on a line that warns against using it. +const LINK = /https:\/\/cdn\.jsdelivr\.net\/npm\/audiomotion-analyzer@[^\s'",)]*/g; +// The Russian phrase is written with \u escapes so this file stays ASCII +// and passes utils/check-homoglyphs.py. +const RU_DO_NOT_USE = '\u041d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435'; +const isWarning = (line) => /\bbare\b|Do not use/i.test(line) || line.includes(RU_DO_NOT_USE); + +test('every jsDelivr link offered for download points at src/audioMotion-analyzer.js', () => { + for (const [name, text] of [['help.txt', helpTxt], ['en_gb.php', enGb], ['ru_ru.php', ruRu], ['assets/version', assetsVersion]]) { + let offered = 0; + for (const line of text.split('\n')) { + for (const match of line.match(LINK) || []) { + if (isWarning(line)) continue; + // "~~" is the lang catalogues' line-break marker, not part of the URL. + const link = match.replace(/~~$/, ''); + offered++; + assert.ok( + link.endsWith('/src/audioMotion-analyzer.js'), + name + ' offers ' + link + ', which serves the UMD bundle, not the ES module'); + } + } + assert.ok(offered, name + ' offers no jsDelivr download link'); + } +}); + +test('isWarning only exempts lines that tell you not to use the URL', () => { + assert.ok(isWarning('Do not use the bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL')); + assert.ok(!isWarning('https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js')); +}); + +console.log('version agreement'); + +const supported = analyzerJs.match(/SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION\s*=\s*'([^']+)'/); + +test('audioMotionAnalyzer.js declares the supported version', () => { + assert.ok(supported, 'SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION not found'); + assert.match(supported[1], /^\d+\.\d+\.\d+$/); +}); + +test('assets/version documents the same version the code requires', () => { + const entry = assetsVersion.match(/audioMotion-analyzer - (\d+\.\d+\.\d+)/); + assert.ok(entry, 'assets/version has no audioMotion-analyzer entry'); + assert.strictEqual(entry[1], supported[1]); +}); + +test('help.txt does not hard-code a second copy of the version', () => { + assert.ok( + !helpTxt.includes(supported[1]), + 'help.txt repeats the version ' + supported[1] + '; it should point at ' + + 'SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION instead'); +}); + +console.log('translation keys'); + +test('en_gb and ru_ru define the same AudioMotion keys', () => { + const keys = (text) => (text.match(/'(AudioMotion\w+|RequiresAudioMotionEnabled)'\s*=>/g) || []) + .map((m) => m.match(/'([^']+)'/)[1]).sort(); + assert.deepStrictEqual(keys(enGb), keys(ruRu)); + assert.ok(keys(enGb).length >= 4, 'expected the AudioMotion* strings to be present'); +}); + +test('the version-check strings use the placeholders monitor.js substitutes', () => { + const monitorJs = read('web/skins/classic/views/js/monitor.js'); + const substituted = (monitorJs.match(/replaceAll\('\{(\w+)\}'/g) || []) + .map((m) => m.match(/\{(\w+)\}/)[1]); + assert.ok(substituted.length, 'monitor.js substitutes no AudioMotion placeholders'); + for (const [name, text] of [['en_gb.php', enGb], ['ru_ru.php', ruRu]]) { + const used = new Set((text.match(/\{(AudioMotionVersion\w+)\}/g) || []).map((m) => m.slice(1, -1))); + for (const ph of used) { + assert.ok( + substituted.includes(ph), + name + ' uses {' + ph + '}, which monitor.js never substitutes'); + } + } +}); + +console.log(''); +console.log(passed + ' passed, ' + failed + ' failed'); +process.exit(failed ? 1 : 0); diff --git a/web/lang/en_gb.php b/web/lang/en_gb.php index c3a64187f..4f40dad70 100644 --- a/web/lang/en_gb.php +++ b/web/lang/en_gb.php @@ -144,8 +144,8 @@ $SLANG = array( 'AttrStartWeekday' => 'Start Weekday', 'AttrEndWeekday' => 'End Weekday', 'AudioMotionVersionOK' => 'Correct version installed "{AudioMotionVersionInstalled}"', - 'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', - 'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have "{AudioMotionVersionInstalled}" installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionNotInstalled' => 'Requires audio motion analyzer version "{AudioMotionVersionRequired}" to be installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionWrongVersion' => 'The required analyzer version is "{AudioMotionVersionRequired}", but you have "{AudioMotionVersionInstalled}" installed~~Download link: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js or https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', 'Auth' => 'Authentication', 'AutoStopTimeout' => 'Auto Stop Timeout', 'AvgBrScore' => 'Avg.
Score', @@ -702,7 +702,7 @@ $SLANG = array( 'ReplaySingle' => 'Single Event', 'ReportEventAudit' => 'Audit Events Report', 'RequestMissing' => 'The request is missing', - 'RequiresAudioMotionEnabled' => 'Requires installation of the file "audioMotion-analyzer.js"', + 'RequiresAudioMotionEnabled' => 'Requires installation of the file "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js"', 'ResetEventCounts' => 'Reset Event Counts', 'RestrictedCameraIds' => 'Restricted Camera Ids', 'RestrictedMonitors' => 'Restricted Monitors', @@ -1167,10 +1167,12 @@ Always: A zmc process will run and immediately connect and stay connected.~~~~ 'OPTIONS_WHATTODISPLAY' => array( 'Help' => ' On the Watch, Montage, Event page, you can display either a video stream, or an audio stream visualization, or both a video stream and an audio visualization.~~ - To display the audio motion visualization, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~ - This file can be downloaded from the following links:~~ - https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number~~ - https://github.com/hvianna/audioMotion-analyzer/releases + The visualization is drawn by the audioMotion-analyzer library, which is licensed AGPL-3.0-or-later and so is not shipped with ZoneMinder. To display it, install the library as "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js" under your web directory, substituting your own skin name for "classic" if you use a different skin.~~ + Download it from one of the following, where X.X.X is the required version:~~ + https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js~~ + https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X (the file is under src/ in the source tarball)~~ + Do not use the bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL: it serves the minified UMD bundle rather than the ES module this path expects.~~ + The required version is reported below this setting, and is set by SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in skins/classic/js/audioMotionAnalyzer.js. ', ), 'FUNCTION_ANALYSIS_ENABLED' => array( diff --git a/web/lang/ru_ru.php b/web/lang/ru_ru.php index 4b7881546..496ec7caf 100644 --- a/web/lang/ru_ru.php +++ b/web/lang/ru_ru.php @@ -139,8 +139,8 @@ $SLANG = array( 'AttrSystemLoad' => 'Нагрузка проц.', 'AttrTotalScore' => 'Сумм. оценка', 'AudioMotionVersionOK' => 'Установлена корректная версия "{AudioMotionVersionInstalled}"', - 'AudioMotionVersionNotInstalled' => 'Требуется установка audio motion analyzer версии "{AudioMotionVersionRequired}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', - 'AudioMotionVersionWrongVersion' => 'Требуется версия анализатора "{AudioMotionVersionRequired}", но у Вас установлена "{AudioMotionVersionInstalled}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired} или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionNotInstalled' => 'Требуется установка audio motion analyzer версии "{AudioMotionVersionRequired}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', + 'AudioMotionVersionWrongVersion' => 'Требуется версия анализатора "{AudioMotionVersionRequired}", но у Вас установлена "{AudioMotionVersionInstalled}"~~Ссылка для загрузки: https://cdn.jsdelivr.net/npm/audiomotion-analyzer@{AudioMotionVersionRequired}/src/audioMotion-analyzer.js или https://github.com/hvianna/audioMotion-analyzer/releases/tag/{AudioMotionVersionRequired}', 'Auth' => 'Авторизация', 'Auto' => 'Авто', 'AutoStopTimeout' => 'Тайм-аут автоостановки', @@ -685,7 +685,7 @@ $SLANG = array( 'ReportEventAudit' => 'Отчёт о событиях аудита', // Edited - 2019-03-24 'Reports' => 'Отчеты', 'RequestMissing' => 'В запросе отсутствует', - 'RequiresAudioMotionEnabled' => 'Требуется установка файла "audioMotion-analyzer.js"', + 'RequiresAudioMotionEnabled' => 'Требуется установка файла "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js"', 'Reset' => 'Сбросить', 'ResetEventCounts' => 'Обнулить счетчик событий', 'Restart' => 'Перезапустить', @@ -1232,10 +1232,12 @@ $OLANG = array( 'OPTIONS_WHATTODISPLAY' => array( 'Help' => ' На страницах живого просмотра, монтажа и просмотра события возможно отображение движения аудио.~~ - Для этого необходимо установить файл "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js".~~ - Указанный файл можно скачать по ссылкам:~~ - https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X где X.X.X это номер версии~~ - https://github.com/hvianna/audioMotion-analyzer/releases + Библиотека audioMotion-analyzer распространяется под лицензией AGPL-3.0-or-later и не входит в состав ZoneMinder. Для отображения визуализации необходимо установить файл "skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js" в веб-каталоге, заменив "classic" на имя используемого скина.~~ + Указанный файл можно скачать по ссылкам, где X.X.X это номер требуемой версии:~~ + https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js~~ + https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X (файл находится в каталоге src/ архива с исходным кодом)~~ + Не используйте ссылку https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X без пути: по ней отдаётся минифицированная UMD-сборка, а не ES-модуль, который ожидается по этому пути.~~ + Требуемая версия указана ниже, под этой настройкой, и задаётся константой SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION в файле skins/classic/js/audioMotionAnalyzer.js. ' ), 'FUNCTION_ANALYSIS_ENABLED' => array( diff --git a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt index b3a0eb5d7..85447bffe 100644 --- a/web/skins/classic/assets/audioMotion-analyzer/src/help.txt +++ b/web/skins/classic/assets/audioMotion-analyzer/src/help.txt @@ -1,5 +1,30 @@ Audio motion visualization can be displayed on the Montage, Watch, and Event pages. -To do this, install the file "/skins/MySkin/assets/audioMotion-analyzer/src/audioMotion-analyzer.js". -This file can be downloaded from the following links: -https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X where X.X.X is the version number -https://github.com/hvianna/audioMotion-analyzer/releases + +It is drawn by the audioMotion-analyzer library, which ZoneMinder does not ship: +it is licensed AGPL-3.0-or-later, so it has to be installed by hand. Drop the +library next to this file, keeping the file name exactly as it is upstream: + + skins/classic/assets/audioMotion-analyzer/src/audioMotion-analyzer.js + +(substitute your own skin name for "classic" if you are not using that skin.) + +Download it with, where X.X.X is the required version: + + curl -L -o audioMotion-analyzer.js \ + https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X/src/audioMotion-analyzer.js + +Mind the "/src/audioMotion-analyzer.js" on the end of that URL. Do not use the +bare https://cdn.jsdelivr.net/npm/audiomotion-analyzer@X.X.X URL: it serves the +package's "main" entry point, which is the minified UMD bundle (dist/index.js), +not the ES module this path expects. + +The same file is in the upstream source tarball under src/: + https://github.com/hvianna/audioMotion-analyzer/releases/tag/X.X.X + +The required version is not repeated here, to keep it in one place. It is +SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in skins/classic/js/audioMotionAnalyzer.js, +and the monitor edit page reports, under "Show", which version is required and +whether the installed one matches. + +Note that the npm package is named "audiomotion-analyzer" (all lower case) while +the project, this directory and the file itself are "audioMotion-analyzer". diff --git a/web/skins/classic/assets/version b/web/skins/classic/assets/version index 1585d4ae2..8ef47f7be 100644 --- a/web/skins/classic/assets/version +++ b/web/skins/classic/assets/version @@ -14,3 +14,10 @@ https://github.com/john-doherty/swiped-events *********************************** mb.extruder Oct 24, 2018 with magor changes IgorA100 https://github.com/pupunzi/jquery.mb.extruder +*********************************** +audioMotion-analyzer - 4.5.4 (required version, see +SUPPORTED_AUDIO_MOTION_ANALYZER_VERSION in ../js/audioMotionAnalyzer.js) +NOT BUNDLED - AGPL-3.0-or-later, installed by the administrator. +See audioMotion-analyzer/src/help.txt +https://github.com/hvianna/audioMotion-analyzer +https://cdn.jsdelivr.net/npm/audiomotion-analyzer@4.5.4/src/audioMotion-analyzer.js