├── l10n ├── .gitkeep ├── km.json ├── bn_BD.json ├── kn.json ├── km.js ├── az.json ├── lb.json ├── nn_NO.json ├── bn_BD.js ├── hy.json ├── ia.json ├── kn.js ├── kab.json ├── az.js ├── lb.js ├── nn_NO.js ├── si.json ├── th.json ├── af.json ├── hy.js ├── ia.js ├── kab.js ├── oc.json ├── si.js ├── th.js ├── af.js ├── mn.json ├── cy_GB.json ├── oc.js ├── mn.js ├── cy_GB.js ├── id.json ├── id.js ├── vi.json ├── vi.js ├── ro.json ├── ro.js ├── sq.json ├── ka_GE.json ├── ka_GE.js ├── sq.js ├── es_419.json ├── es_CL.json ├── es_CO.json ├── es_CR.json ├── es_DO.json ├── es_GT.json ├── es_HN.json ├── es_NI.json ├── es_PA.json ├── es_PE.json ├── es_PR.json ├── es_PY.json ├── es_SV.json ├── es_UY.json ├── br.json ├── es_CL.js ├── es_CO.js ├── es_CR.js ├── es_DO.js ├── es_GT.js ├── es_HN.js ├── es_NI.js ├── es_PA.js ├── es_PE.js ├── es_PR.js ├── es_PY.js ├── es_SV.js ├── es_UY.js ├── es_419.js ├── br.js ├── be.json ├── be.js ├── eo.json ├── he.json ├── eo.js ├── he.js ├── es_AR.json ├── sc.json ├── fi.json ├── es_AR.js ├── sc.js ├── fi.js ├── nl.json ├── nl.js ├── hr.json ├── pt_PT.json ├── hr.js ├── pt_PT.js ├── bg.json ├── bg.js ├── es_EC.json ├── es_EC.js ├── lt_LT.json ├── lt_LT.js ├── mk.json ├── mk.js ├── lv.json ├── lv.js ├── ko.json ├── ko.js ├── sl.json └── sl.js ├── .l10nignore ├── .github ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── reuse.yml │ ├── fixup.yml │ ├── block-unconventional-commits.yml │ ├── lint-php-cs.yml │ ├── block-merge-eol.yml │ ├── dependabot-approve-merge.yml │ ├── block-merge-freeze.yml │ ├── update-nextcloud-ocp-approve-merge.yml │ ├── lint-php.yml │ ├── pr-feedback.yml │ ├── npm-audit-fix.yml │ ├── lint-eslint.yml │ └── psalm-matrix.yml ├── contributing.md └── issue_template.md ├── screenshots └── reader.png ├── css └── logreader-main.css ├── .gitattributes ├── templates └── index.php ├── src ├── utils │ ├── logger.ts │ ├── debounce.ts │ ├── logger.spec.ts │ ├── clipboard.ts │ ├── clipboard.spec.ts │ ├── debounce.spec.ts │ ├── format.ts │ ├── format.spec.ts │ ├── logfile.spec.ts │ ├── splitter.ts │ ├── exception.spec.ts │ └── logfile.ts ├── interfaces │ ├── index.ts │ ├── IAppSettings.ts │ └── ILogEntry.ts ├── index.ts ├── constants.spec.ts ├── components │ ├── exception │ │ ├── StackTrace.vue │ │ ├── TraceLine.vue │ │ └── LogException.vue │ ├── IntersectionObserver.vue │ ├── settings │ │ ├── SettingsLiveView.vue │ │ ├── SettingsSetLogLevel.vue │ │ ├── AppSettingsDialog.vue │ │ ├── SettingsLogLevels.vue │ │ ├── SettingsActions.vue │ │ └── SettingsDatetimeFormat.vue │ ├── LogSearch.vue │ └── table │ │ └── LogTableHeader.vue ├── vue-shim.d.ts ├── constants.ts ├── api.ts └── api.spec.ts ├── tsconfig.json ├── .gitignore ├── .tx └── config ├── AUTHORS.md ├── codecov.yml ├── lib ├── Exception │ └── UnsupportedLogTypeException.php ├── Controller │ ├── PageController.php │ └── SettingsController.php ├── Listener │ └── LogListener.php ├── AppInfo │ └── Application.php ├── Constants.php ├── Settings │ ├── Section.php │ └── Admin.php ├── Log │ ├── LogIteratorFactory.php │ ├── SearchFilter.php │ ├── Console.php │ └── LogIterator.php ├── SetupChecks │ └── LogErrors.php ├── Command │ └── Tail.php └── Service │ └── SettingsService.php ├── img ├── app.svg └── app-dark.svg ├── tests ├── bootstrap.php ├── stubs │ └── stub.phpstub ├── phpunit.xml └── Unit │ └── SetupChecks │ └── LogErrorsTest.php ├── .php-cs-fixer.dist.php ├── appinfo ├── routes.php └── info.xml ├── eslint.config.mjs ├── LICENSES ├── ISC.txt ├── MIT.txt └── BSD-3-Clause.txt ├── vite.config.ts ├── psalm.xml ├── composer.json ├── REUSE.toml ├── package.json ├── Makefile └── README.md /l10n/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.l10nignore: -------------------------------------------------------------------------------- 1 | js/ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /appinfo/info.xml @Antreesy @icewind1991 2 | 3 | -------------------------------------------------------------------------------- /screenshots/reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/logreader/HEAD/screenshots/reader.png -------------------------------------------------------------------------------- /css/logreader-main.css: -------------------------------------------------------------------------------- 1 | /* extracted by css-entry-points-plugin */ 2 | @import './main-BM0QfzKZ.chunk.css'; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | js/* -diff 5 | -------------------------------------------------------------------------------- /l10n/km.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "ម៉ោង", 3 | "Info" : "Info", 4 | "Warning" : "បម្រាម", 5 | "Error" : "កំហុស" 6 | },"pluralForm" :"nplurals=1; plural=0;" 7 | } -------------------------------------------------------------------------------- /l10n/bn_BD.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "সময়", 3 | "Info" : "Info", 4 | "Warning" : "সতর্কবাণী", 5 | "Error" : "সমস্যা" 6 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 7 | } -------------------------------------------------------------------------------- /l10n/kn.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "ಸಮಯ", 3 | "Info" : "Info", 4 | "Warning" : "ಎಚ್ಚರಿಕೆ", 5 | "Error" : "ತಪ್ಪಾಗಿದೆ" 6 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 7 | } -------------------------------------------------------------------------------- /l10n/km.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "ម៉ោង", 5 | "Info" : "Info", 6 | "Warning" : "បម្រាម", 7 | "Error" : "កំហុស" 8 | }, 9 | "nplurals=1; plural=0;"); 10 | -------------------------------------------------------------------------------- /l10n/az.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "Tətbiq", 3 | "Time" : "Vaxt", 4 | "Info" : "Info", 5 | "Warning" : "Xəbərdarlıq", 6 | "Error" : "Səhv" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/lb.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "Zäit", 3 | "Relative" : "Relativ", 4 | "Info" : "Info", 5 | "Warning" : "Warnung", 6 | "Error" : "Fehler" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/nn_NO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "Applikasjon", 3 | "Time" : "Tid", 4 | "Info" : "Info", 5 | "Warning" : "Åtvaring", 6 | "Error" : "Feil" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/bn_BD.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "সময়", 5 | "Info" : "Info", 6 | "Warning" : "সতর্কবাণী", 7 | "Error" : "সমস্যা" 8 | }, 9 | "nplurals=2; plural=(n != 1);"); 10 | -------------------------------------------------------------------------------- /l10n/hy.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "Ժամ", 3 | "Relative" : "Հարաբերական", 4 | "Info" : "Info", 5 | "Warning" : "Զգուշացում", 6 | "Error" : "Սխալ" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/ia.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "Tempore", 3 | "Relative" : "Relative", 4 | "Info" : "Info", 5 | "Warning" : "Aviso", 6 | "Error" : "Error" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/kn.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "ಸಮಯ", 5 | "Info" : "Info", 6 | "Warning" : "ಎಚ್ಚರಿಕೆ", 7 | "Error" : "ತಪ್ಪಾಗಿದೆ" 8 | }, 9 | "nplurals=2; plural=(n > 1);"); 10 | -------------------------------------------------------------------------------- /l10n/kab.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Ctrl" : "Ctrl", 3 | "Message" : "Izen", 4 | "Show details" : "Sken talqayt", 5 | "Warning" : "Alɣu", 6 | "Error" : "Erreur" 7 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 8 | } -------------------------------------------------------------------------------- /l10n/az.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "Tətbiq", 5 | "Time" : "Vaxt", 6 | "Info" : "Info", 7 | "Warning" : "Xəbərdarlıq", 8 | "Error" : "Səhv" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/lb.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "Zäit", 5 | "Relative" : "Relativ", 6 | "Info" : "Info", 7 | "Warning" : "Warnung", 8 | "Error" : "Fehler" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/nn_NO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "Applikasjon", 5 | "Time" : "Tid", 6 | "Info" : "Info", 7 | "Warning" : "Åtvaring", 8 | "Error" : "Feil" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/si.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "යෙදුම", 3 | "Time" : "වේලාව", 4 | "Message" : "පණිවිඩය", 5 | "Info" : "තොරතුරු", 6 | "Warning" : "අවවාදයයි", 7 | "Error" : "දෝෂය" 8 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 9 | } -------------------------------------------------------------------------------- /l10n/th.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "แอป", 3 | "Time" : "เวลา", 4 | "Show details" : "แสดงข้อมูล", 5 | "Info" : "ข้อมูล", 6 | "Warning" : "คำเตือน", 7 | "Error" : "ข้อผิดพลาด" 8 | },"pluralForm" :"nplurals=1; plural=0;" 9 | } -------------------------------------------------------------------------------- /l10n/af.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "Toep", 3 | "Time" : "Tyd", 4 | "Relative" : "Relatief", 5 | "Info" : "Inligting", 6 | "Warning" : "Waarskuwing", 7 | "Error" : "Fout" 8 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 9 | } -------------------------------------------------------------------------------- /l10n/hy.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "Ժամ", 5 | "Relative" : "Հարաբերական", 6 | "Info" : "Info", 7 | "Warning" : "Զգուշացում", 8 | "Error" : "Սխալ" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/ia.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "Tempore", 5 | "Relative" : "Relative", 6 | "Info" : "Info", 7 | "Warning" : "Aviso", 8 | "Error" : "Error" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/kab.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Ctrl" : "Ctrl", 5 | "Message" : "Izen", 6 | "Show details" : "Sken talqayt", 7 | "Warning" : "Alɣu", 8 | "Error" : "Erreur" 9 | }, 10 | "nplurals=2; plural=(n != 1);"); 11 | -------------------------------------------------------------------------------- /l10n/oc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Time" : "Ora", 3 | "Relative" : "Relatiu", 4 | "Show details" : "Mostrar detalhs", 5 | "Info" : "Info", 6 | "Warning" : "Avertiment", 7 | "Error" : "Error" 8 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 9 | } -------------------------------------------------------------------------------- /templates/index.php: -------------------------------------------------------------------------------- 1 | 8 |
9 | -------------------------------------------------------------------------------- /l10n/si.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "යෙදුම", 5 | "Time" : "වේලාව", 6 | "Message" : "පණිවිඩය", 7 | "Info" : "තොරතුරු", 8 | "Warning" : "අවවාදයයි", 9 | "Error" : "දෝෂය" 10 | }, 11 | "nplurals=2; plural=(n != 1);"); 12 | -------------------------------------------------------------------------------- /l10n/th.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "แอป", 5 | "Time" : "เวลา", 6 | "Show details" : "แสดงข้อมูล", 7 | "Info" : "ข้อมูล", 8 | "Warning" : "คำเตือน", 9 | "Error" : "ข้อผิดพลาด" 10 | }, 11 | "nplurals=1; plural=0;"); 12 | -------------------------------------------------------------------------------- /l10n/af.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "Toep", 5 | "Time" : "Tyd", 6 | "Relative" : "Relatief", 7 | "Info" : "Inligting", 8 | "Warning" : "Waarskuwing", 9 | "Error" : "Fout" 10 | }, 11 | "nplurals=2; plural=(n != 1);"); 12 | -------------------------------------------------------------------------------- /l10n/mn.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Level" : "түвшин", 3 | "App" : "Аппликэйшин", 4 | "Time" : "Цаг", 5 | "Relative" : "Хамаарал", 6 | "Info" : "Info", 7 | "Warning" : "Warning", 8 | "Error" : "Алдаа" 9 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 10 | } -------------------------------------------------------------------------------- /l10n/cy_GB.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "Ap", 3 | "Time" : "Amser", 4 | "Relative" : "Perthynas", 5 | "Info" : "Info", 6 | "Warning" : "Rhybudd", 7 | "Error" : "Gwall" 8 | },"pluralForm" :"nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;" 9 | } -------------------------------------------------------------------------------- /l10n/oc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Time" : "Ora", 5 | "Relative" : "Relatiu", 6 | "Show details" : "Mostrar detalhs", 7 | "Info" : "Info", 8 | "Warning" : "Avertiment", 9 | "Error" : "Error" 10 | }, 11 | "nplurals=2; plural=(n > 1);"); 12 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { getLoggerBuilder } from '@nextcloud/logger' 7 | 8 | export const logger = getLoggerBuilder().setApp(appName).build() 9 | -------------------------------------------------------------------------------- /l10n/mn.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Level" : "түвшин", 5 | "App" : "Аппликэйшин", 6 | "Time" : "Цаг", 7 | "Relative" : "Хамаарал", 8 | "Info" : "Info", 9 | "Warning" : "Warning", 10 | "Error" : "Алдаа" 11 | }, 12 | "nplurals=2; plural=(n != 1);"); 13 | -------------------------------------------------------------------------------- /l10n/cy_GB.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "Ap", 5 | "Time" : "Amser", 6 | "Relative" : "Perthynas", 7 | "Info" : "Info", 8 | "Warning" : "Rhybudd", 9 | "Error" : "Gwall" 10 | }, 11 | "nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;"); 12 | -------------------------------------------------------------------------------- /l10n/id.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "App" : "Apl", 3 | "Time" : "Waktu", 4 | "Line {line}" : "Baris {line}", 5 | "Message" : "Pesan", 6 | "Show details" : "Tampilkan detail", 7 | "Info" : "Info", 8 | "Warning" : "Peringatan", 9 | "Error" : "Galat" 10 | },"pluralForm" :"nplurals=1; plural=0;" 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "Bundler", 5 | "outDir": "./js/", 6 | "lib": [ 7 | "DOM", 8 | "ESNext" 9 | ], 10 | "types": [ 11 | "vite/client" 12 | ], 13 | "noImplicitAny": true, 14 | "allowSyntheticDefaultImports": true, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | # Code coverge 5 | .nyc_output/ 6 | coverage/ 7 | 8 | # Test output 9 | cypress/screenshots/ 10 | cypress/videos/ 11 | 12 | # Locally installed packages 13 | node_modules/ 14 | vendor/ 15 | 16 | build/ 17 | *.cache 18 | -------------------------------------------------------------------------------- /l10n/id.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "App" : "Apl", 5 | "Time" : "Waktu", 6 | "Line {line}" : "Baris {line}", 7 | "Message" : "Pesan", 8 | "Show details" : "Tampilkan detail", 9 | "Info" : "Info", 10 | "Warning" : "Peringatan", 11 | "Error" : "Galat" 12 | }, 13 | "nplurals=1; plural=0;"); 14 | -------------------------------------------------------------------------------- /l10n/vi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Level" : "Cấp độ", 3 | "App" : "Ứng dụng", 4 | "Time" : "Thời gian", 5 | "Ctrl" : "Ctrl", 6 | "Relative" : "Liên quan", 7 | "Show details" : "Hiển thị chi tiết", 8 | "Info" : "Thông tin", 9 | "Warning" : "Cảnh báo", 10 | "Error" : "Lỗi" 11 | },"pluralForm" :"nplurals=1; plural=0;" 12 | } -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi, hu_HU: hu, nb_NO: nb, sk_SK: sk 4 | 5 | [o:nextcloud:p:nextcloud:r:logreader] 6 | file_filter = translationfiles//logreader.po 7 | source_file = translationfiles/templates/logreader.pot 8 | source_lang = en 9 | type = PO 10 | 11 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | export type * from './IAppSettings' 7 | export type * from './ILogEntry' 8 | 9 | /** 10 | * Options how to sort rows 11 | */ 12 | export type ISortingOptions = '' | 'ascending' | 'descending' 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { createPinia } from 'pinia' 7 | import { createApp } from 'vue' 8 | import App from './App.vue' 9 | 10 | const pinia = createPinia() 11 | 12 | createApp(App) 13 | .use(pinia) 14 | .mount('#app-content') 15 | -------------------------------------------------------------------------------- /l10n/vi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Level" : "Cấp độ", 5 | "App" : "Ứng dụng", 6 | "Time" : "Thời gian", 7 | "Ctrl" : "Ctrl", 8 | "Relative" : "Liên quan", 9 | "Show details" : "Hiển thị chi tiết", 10 | "Info" : "Thông tin", 11 | "Warning" : "Cảnh báo", 12 | "Error" : "Lỗi" 13 | }, 14 | "nplurals=1; plural=0;"); 15 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 5 | # Authors 6 | 7 | - Carl Schwan 8 | - Côme Chilliet 9 | - Ferdinand Thiessen 10 | - Robin Appelman 11 | - Nextcloud GmbH 12 | - ownCloud, Inc. 13 | -------------------------------------------------------------------------------- /l10n/ro.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Level" : "Nivel", 3 | "App" : "Aplicație", 4 | "Time" : "Timp", 5 | "Relative" : "Relativ", 6 | "Message" : "Mesaj", 7 | "Show details" : "Arată detaliile", 8 | "Info" : "Info", 9 | "Warning" : "Atenție", 10 | "Error" : "Eroare" 11 | },"pluralForm" :"nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));" 12 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | codecov: 4 | branch: master 5 | comment: false 6 | 7 | # Ignore project status check as our CI only runs PHP tests for PHP PRs and JS tests for JS PRs 8 | # Otherwise it will warn about project coverage drops 9 | coverage: 10 | status: 11 | project: off 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: npm 7 | directory: "/" 8 | schedule: 9 | interval: weekly 10 | day: saturday 11 | time: "03:00" 12 | timezone: Europe/Paris 13 | open-pull-requests-limit: 10 14 | versioning-strategy: increase 15 | -------------------------------------------------------------------------------- /src/constants.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { expect, test } from 'vitest' 7 | import { LOGGING_LEVEL, LOGGING_LEVEL_NAMES } from './constants' 8 | 9 | test('constants - every level has a name', () => { 10 | expect(LOGGING_LEVEL.length).toBe(LOGGING_LEVEL_NAMES.length) 11 | }) 12 | -------------------------------------------------------------------------------- /l10n/ro.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Level" : "Nivel", 5 | "App" : "Aplicație", 6 | "Time" : "Timp", 7 | "Relative" : "Relativ", 8 | "Message" : "Mesaj", 9 | "Show details" : "Arată detaliile", 10 | "Info" : "Info", 11 | "Warning" : "Atenție", 12 | "Error" : "Eroare" 13 | }, 14 | "nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));"); 15 | -------------------------------------------------------------------------------- /l10n/sq.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Duke hyrë", 3 | "Log Reader" : "Lexuesi i Regjistrit", 4 | "Level" : "Nivel", 5 | "App" : "Aplikacion", 6 | "Time" : "Kohë", 7 | "Download logs" : "Regjistrimet e shkarkuara", 8 | "Relative" : "Relative", 9 | "Message" : "Mesazh", 10 | "Info" : "Info", 11 | "Warning" : "Kujdes", 12 | "Error" : "Gabim" 13 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 14 | } -------------------------------------------------------------------------------- /l10n/ka_GE.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "იწერება ლოგი", 3 | "Log Reader" : "ლოგის მკითხველი", 4 | "Level" : "დონე", 5 | "App" : "აპლიკაცია", 6 | "Time" : "დრო", 7 | "Download logs" : "ლოგების გადმოწერა", 8 | "Relative" : "ფარდობითი", 9 | "Message" : "წერილი", 10 | "Info" : "ინფორმაცია", 11 | "Warning" : "გაფრთხილება", 12 | "Error" : "შეცდომა" 13 | },"pluralForm" :"nplurals=2; plural=(n!=1);" 14 | } -------------------------------------------------------------------------------- /l10n/ka_GE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "იწერება ლოგი", 5 | "Log Reader" : "ლოგის მკითხველი", 6 | "Level" : "დონე", 7 | "App" : "აპლიკაცია", 8 | "Time" : "დრო", 9 | "Download logs" : "ლოგების გადმოწერა", 10 | "Relative" : "ფარდობითი", 11 | "Message" : "წერილი", 12 | "Info" : "ინფორმაცია", 13 | "Warning" : "გაფრთხილება", 14 | "Error" : "შეცდომა" 15 | }, 16 | "nplurals=2; plural=(n!=1);"); 17 | -------------------------------------------------------------------------------- /l10n/sq.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Duke hyrë", 5 | "Log Reader" : "Lexuesi i Regjistrit", 6 | "Level" : "Nivel", 7 | "App" : "Aplikacion", 8 | "Time" : "Kohë", 9 | "Download logs" : "Regjistrimet e shkarkuara", 10 | "Relative" : "Relative", 11 | "Message" : "Mesazh", 12 | "Info" : "Info", 13 | "Warning" : "Kujdes", 14 | "Error" : "Gabim" 15 | }, 16 | "nplurals=2; plural=(n != 1);"); 17 | -------------------------------------------------------------------------------- /lib/Exception/UnsupportedLogTypeException.php: -------------------------------------------------------------------------------- 1 | 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);" 7 | } -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /l10n/es_CL.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_CO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_CR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_DO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_GT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_HN.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_NI.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_PA.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_PE.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_PR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_PY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_SV.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/es_UY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /l10n/es_419.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar bitácoras", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Info" : "Info", 13 | "Warning" : "Advertencia", 14 | "Error" : "Error" 15 | }, 16 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 17 | -------------------------------------------------------------------------------- /l10n/br.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Show details" : "Diskouel ar munudoù", 5 | "Info" : "Titouroù", 6 | "Warning" : "Kemenadenn", 7 | "Error" : "Fazi" 8 | }, 9 | "nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);"); 10 | -------------------------------------------------------------------------------- /l10n/be.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Level" : "Узровень", 3 | "App" : "Праграма", 4 | "Time" : "Час", 5 | "Ctrl" : "Ctrl", 6 | "Application" : "Праграма", 7 | "Message" : "Паведамленне", 8 | "Show details" : "Паказаць падрабязнасці", 9 | "Info" : "Інфармацыя", 10 | "Warning" : "Папярэджанне", 11 | "Error" : "Памылка" 12 | },"pluralForm" :"nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);" 13 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('Test\\', OC::$SERVERROOT . '/tests/lib/', true); 15 | \OC::$composerAutoloader->addPsr4('Tests\\', OC::$SERVERROOT . '/tests/', true); 16 | 17 | OC_App::loadApp('logreader'); 18 | 19 | OC_Hook::clear(); 20 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | getFinder() 16 | ->ignoreVCSIgnored(true) 17 | ->notPath('build') 18 | ->notPath('l10n') 19 | ->notPath('lib/Vendor') 20 | ->notPath('src') 21 | ->notPath('vendor') 22 | ->in(__DIR__); 23 | return $config; 24 | -------------------------------------------------------------------------------- /l10n/be.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Level" : "Узровень", 5 | "App" : "Праграма", 6 | "Time" : "Час", 7 | "Ctrl" : "Ctrl", 8 | "Application" : "Праграма", 9 | "Message" : "Паведамленне", 10 | "Show details" : "Паказаць падрабязнасці", 11 | "Info" : "Інфармацыя", 12 | "Warning" : "Папярэджанне", 13 | "Error" : "Памылка" 14 | }, 15 | "nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);"); 16 | -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | /** 7 | * Debounce a function call for specified amount of time 8 | * 9 | * @param func The function to debounce 10 | * @param timeout Amount of time (ms) to wait 11 | */ 12 | export function debounce(func: Function, timeout = 300) { 13 | let timer: number 14 | return (...args: unknown[]) => { 15 | clearTimeout(timer) 16 | timer = window.setTimeout(() => { 17 | func.apply(this, args) 18 | }, timeout) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /l10n/eo.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Protokolo", 3 | "Log Reader" : "Protokola legilo", 4 | "A log reader for Nextcloud" : "Protokola legilo por Nextcloud", 5 | "Log reader for Nextcloud" : "Protokola legilo por Nextcloud", 6 | "Level" : "Nivelo", 7 | "App" : "Aplikaĵo", 8 | "Time" : "Dato", 9 | "Download logs" : "Elŝuti prokolon", 10 | "Relative" : "Relative", 11 | "Message" : "Mesaĝo", 12 | "Show details" : "Montri la detalojn", 13 | "Info" : "Info", 14 | "Warning" : "Averto", 15 | "Error" : "Eraro" 16 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 17 | } -------------------------------------------------------------------------------- /l10n/he.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "רישום", 3 | "Log Reader" : "קורא רישומים", 4 | "A log reader for Nextcloud" : "קורא רישומים ל־Nextcloud", 5 | "Log reader for Nextcloud" : "קורא רישומים ל־Nextcloud", 6 | "Level" : "רמה", 7 | "App" : "יישומון", 8 | "Time" : "זמן", 9 | "Download logs" : "הורדת רישומים", 10 | "Relative" : "יחסי", 11 | "Message" : "הודעה", 12 | "Show details" : "הצגת פרטים", 13 | "Info" : "פרטים", 14 | "Warning" : "אזהרה", 15 | "Error" : "שגיאה" 16 | },"pluralForm" :"nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;" 17 | } -------------------------------------------------------------------------------- /l10n/eo.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Protokolo", 5 | "Log Reader" : "Protokola legilo", 6 | "A log reader for Nextcloud" : "Protokola legilo por Nextcloud", 7 | "Log reader for Nextcloud" : "Protokola legilo por Nextcloud", 8 | "Level" : "Nivelo", 9 | "App" : "Aplikaĵo", 10 | "Time" : "Dato", 11 | "Download logs" : "Elŝuti prokolon", 12 | "Relative" : "Relative", 13 | "Message" : "Mesaĝo", 14 | "Show details" : "Montri la detalojn", 15 | "Info" : "Info", 16 | "Warning" : "Averto", 17 | "Error" : "Eraro" 18 | }, 19 | "nplurals=2; plural=(n != 1);"); 20 | -------------------------------------------------------------------------------- /l10n/he.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "רישום", 5 | "Log Reader" : "קורא רישומים", 6 | "A log reader for Nextcloud" : "קורא רישומים ל־Nextcloud", 7 | "Log reader for Nextcloud" : "קורא רישומים ל־Nextcloud", 8 | "Level" : "רמה", 9 | "App" : "יישומון", 10 | "Time" : "זמן", 11 | "Download logs" : "הורדת רישומים", 12 | "Relative" : "יחסי", 13 | "Message" : "הודעה", 14 | "Show details" : "הצגת פרטים", 15 | "Info" : "פרטים", 16 | "Warning" : "אזהרה", 17 | "Error" : "שגיאה" 18 | }, 19 | "nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;"); 20 | -------------------------------------------------------------------------------- /appinfo/routes.php: -------------------------------------------------------------------------------- 1 | [ 9 | // page 10 | ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], 11 | ['name' => 'log#get', 'url' => '/api/log', 'verb' => 'GET'], 12 | ['name' => 'log#poll', 'url' => '/api/poll', 'verb' => 'GET'], 13 | // app settings 14 | ['name' => 'settings#getAppConfig', 'url' => '/api/settings', 'verb' => 'GET'], 15 | ['name' => 'settings#updateAppConfig', 'url' => '/api/settings', 'verb' => 'PUT'] 16 | ]]; 17 | -------------------------------------------------------------------------------- /l10n/es_AR.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Registro", 3 | "Log Reader" : "Lector de registro", 4 | "A log reader for Nextcloud" : "Un lector de registro para Nextcloud", 5 | "Log reader for Nextcloud" : "Lector de registro para Nextcloud", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Download logs" : "Descargar registros", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensaje", 12 | "Show details" : "Mostrar detalles", 13 | "Info" : "Info", 14 | "Warning" : "Advertencia", 15 | "Error" : "Error" 16 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 17 | } -------------------------------------------------------------------------------- /l10n/sc.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Registratzione", 3 | "Log Reader" : "Letore de registros", 4 | "A log reader for Nextcloud" : "Unu letore de registros pro Nextcloud", 5 | "Log reader for Nextcloud" : "Letore de registros pro Nextcloud", 6 | "Level" : "Livellu", 7 | "App" : "Aplicatzione", 8 | "Time" : "Ora", 9 | "Download logs" : "Iscàrriga registros", 10 | "Message" : "Messàgiu", 11 | "Show details" : "Mustra detàllios", 12 | "Debug" : "Currigi", 13 | "Info" : "Informatziones", 14 | "Warning" : "Avisu", 15 | "Error" : "Errore", 16 | "Fatal" : "Fatale" 17 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 18 | } -------------------------------------------------------------------------------- /l10n/fi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Loki", 3 | "Log Reader" : "Lokilukija", 4 | "A log reader for Nextcloud" : "Nextcloudin lokilukija", 5 | "Log reader for Nextcloud" : "Nextcloudin lokilukija", 6 | "Level" : "Taso", 7 | "App" : "Sovellus", 8 | "Time" : "Aika", 9 | "Download logs" : "Lataa lokit", 10 | "Relative" : "Suhteellinen", 11 | "Application" : "Sovellus", 12 | "Message" : "Viesti", 13 | "Show details" : "Näytä lisätiedot", 14 | "Debug" : "Vianjäljitys", 15 | "Info" : "Tietoa", 16 | "Warning" : "Varoitus", 17 | "Error" : "Virhe", 18 | "Fatal" : "Kriittinen" 19 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 20 | } -------------------------------------------------------------------------------- /l10n/es_AR.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registro", 5 | "Log Reader" : "Lector de registro", 6 | "A log reader for Nextcloud" : "Un lector de registro para Nextcloud", 7 | "Log reader for Nextcloud" : "Lector de registro para Nextcloud", 8 | "Level" : "Nivel", 9 | "App" : "Aplicación", 10 | "Time" : "Hora", 11 | "Download logs" : "Descargar registros", 12 | "Relative" : "Relativo", 13 | "Message" : "Mensaje", 14 | "Show details" : "Mostrar detalles", 15 | "Info" : "Info", 16 | "Warning" : "Advertencia", 17 | "Error" : "Error" 18 | }, 19 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 20 | -------------------------------------------------------------------------------- /l10n/sc.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registratzione", 5 | "Log Reader" : "Letore de registros", 6 | "A log reader for Nextcloud" : "Unu letore de registros pro Nextcloud", 7 | "Log reader for Nextcloud" : "Letore de registros pro Nextcloud", 8 | "Level" : "Livellu", 9 | "App" : "Aplicatzione", 10 | "Time" : "Ora", 11 | "Download logs" : "Iscàrriga registros", 12 | "Message" : "Messàgiu", 13 | "Show details" : "Mustra detàllios", 14 | "Debug" : "Currigi", 15 | "Info" : "Informatziones", 16 | "Warning" : "Avisu", 17 | "Error" : "Errore", 18 | "Fatal" : "Fatale" 19 | }, 20 | "nplurals=2; plural=(n != 1);"); 21 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { recommended } from '@nextcloud/eslint-config' 7 | 8 | export default [ 9 | ...recommended, 10 | { 11 | name: 'logreader/disabled-during-migration', 12 | rules: { 13 | '@typescript-eslint/no-unsafe-function-type': 'off', 14 | '@typescript-eslint/no-unused-vars': 'off', 15 | '@typescript-eslint/no-use-before-define': 'off', 16 | 'import-extensions/extensions': 'off', 17 | 'vue/no-boolean-default': 'off', 18 | 'vue/no-required-prop-with-default': 'off', 19 | 'vue/no-unused-properties': 'off', 20 | }, 21 | }, 22 | ] 23 | -------------------------------------------------------------------------------- /l10n/fi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Loki", 5 | "Log Reader" : "Lokilukija", 6 | "A log reader for Nextcloud" : "Nextcloudin lokilukija", 7 | "Log reader for Nextcloud" : "Nextcloudin lokilukija", 8 | "Level" : "Taso", 9 | "App" : "Sovellus", 10 | "Time" : "Aika", 11 | "Download logs" : "Lataa lokit", 12 | "Relative" : "Suhteellinen", 13 | "Application" : "Sovellus", 14 | "Message" : "Viesti", 15 | "Show details" : "Näytä lisätiedot", 16 | "Debug" : "Vianjäljitys", 17 | "Info" : "Tietoa", 18 | "Warning" : "Varoitus", 19 | "Error" : "Virhe", 20 | "Fatal" : "Kriittinen" 21 | }, 22 | "nplurals=2; plural=(n != 1);"); 23 | -------------------------------------------------------------------------------- /src/components/exception/StackTrace.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 22 | 23 | 30 | -------------------------------------------------------------------------------- /l10n/nl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Logs", 3 | "Log Reader" : "Logboek lezer", 4 | "A log reader for Nextcloud" : "Een loglezer voor Nextcloud", 5 | "Log reader for Nextcloud" : "Loglezer voor Nextcloud", 6 | "Level" : "Niveau", 7 | "App" : "App", 8 | "Time" : "Tijd", 9 | "Ctrl" : "Ctrl", 10 | "Download logs" : "Logboek downloaden", 11 | "Relative" : "Relatief", 12 | "Application" : "Applicatie", 13 | "Message" : "Bericht", 14 | "Show details" : "Tonen details", 15 | "Debug" : "Debug", 16 | "Info" : "Info", 17 | "Warning" : "Waarschuwing", 18 | "Error" : "Fout", 19 | "Fatal" : "Fataal" 20 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 21 | } -------------------------------------------------------------------------------- /l10n/nl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Logs", 5 | "Log Reader" : "Logboek lezer", 6 | "A log reader for Nextcloud" : "Een loglezer voor Nextcloud", 7 | "Log reader for Nextcloud" : "Loglezer voor Nextcloud", 8 | "Level" : "Niveau", 9 | "App" : "App", 10 | "Time" : "Tijd", 11 | "Ctrl" : "Ctrl", 12 | "Download logs" : "Logboek downloaden", 13 | "Relative" : "Relatief", 14 | "Application" : "Applicatie", 15 | "Message" : "Bericht", 16 | "Show details" : "Tonen details", 17 | "Debug" : "Debug", 18 | "Info" : "Info", 19 | "Warning" : "Waarschuwing", 20 | "Error" : "Fout", 21 | "Fatal" : "Fataal" 22 | }, 23 | "nplurals=2; plural=(n != 1);"); 24 | -------------------------------------------------------------------------------- /src/utils/logger.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { afterEach, describe, expect, it, vi } from 'vitest' 7 | import { logger } from './logger' 8 | 9 | describe('utils:logger', () => { 10 | afterEach(() => { 11 | vi.restoreAllMocks() 12 | }) 13 | 14 | it('creates a logger instance', () => { 15 | expect(typeof logger).toBe('object') 16 | expect(typeof logger.warn).toBe('function') 17 | 18 | const consoleSpy = vi.spyOn(window.console, 'warn').mockImplementation(() => {}) 19 | logger.warn('foo') 20 | expect(consoleSpy).toBeCalled() 21 | expect(consoleSpy.mock.calls[0][0]).toMatch('[WARN] logreader:') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: Ferdinand Thiessen 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | declare module 'vue-material-design-icons/*.vue' { 7 | import type { DefineComponent } from 'vue' 8 | 9 | const IconVue: DefineComponent<{ 10 | /** @default 24 */ 11 | size?: number 12 | /** @default 'currentColor' */ 13 | fillColor?: string 14 | title?: string 15 | }> 16 | 17 | export default IconVue 18 | } 19 | 20 | declare module '@nextcloud/vue/dist/Components/*.js' { 21 | import Vue from 'vue' 22 | export default Vue 23 | } 24 | 25 | declare module 'json-string-splitter' { 26 | function splitter(input: string): { jsons: string[], remainder: string } 27 | export = splitter 28 | } 29 | -------------------------------------------------------------------------------- /l10n/hr.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Zapisivanje", 3 | "Log Reader" : "Čitač zapisa", 4 | "A log reader for Nextcloud" : "Čitač zapisa za Nextcloud", 5 | "Log reader for Nextcloud" : "Čitač zapisa za Nextcloud", 6 | "Level" : "Razina", 7 | "App" : "Aplikacija", 8 | "Time" : "Vrijeme", 9 | "Download logs" : "Preuzmi zapise", 10 | "Relative" : "Relativno", 11 | "Message" : "Poruka", 12 | "Show details" : "Prikaži pojedinosti", 13 | "Debug" : "Otklanjanje pogrešaka", 14 | "Info" : "Informacije", 15 | "Warning" : "Upozorenje", 16 | "Error" : "Pogreška", 17 | "Fatal" : "Fatalno" 18 | },"pluralForm" :"nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" 19 | } -------------------------------------------------------------------------------- /l10n/pt_PT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Registos", 3 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["%n aviso nos registos desde %s","%n avisos nos registos desde %s","%n avisos nos registos desde %s"], 4 | "Level" : "Nível", 5 | "App" : "Aplicação", 6 | "Time" : "Tempo", 7 | "Download/Upload logs" : "Registos de Transferir/Enviar", 8 | "Ctrl" : "Ctrl", 9 | "Download logs" : "Transferir registos", 10 | "Relative" : "Relativo", 11 | "Message" : "Mensagem", 12 | "Show details" : "Mostrar detalhes", 13 | "Info" : "Informação", 14 | "Warning" : "Aviso", 15 | "Error" : "Erro" 16 | },"pluralForm" :"nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 17 | } -------------------------------------------------------------------------------- /tests/stubs/stub.phpstub: -------------------------------------------------------------------------------- 1 | 6 | * SPDX-License-Identifier: AGPL-3.0-or-later 7 | */ 8 | 9 | namespace OC\Core\Command { 10 | use Symfony\Component\Console\Command\Command; 11 | 12 | class Base extends Command { 13 | protected function abortIfInterrupted(){ 14 | } 15 | } 16 | 17 | class InterruptedException extends \Exception { 18 | } 19 | } 20 | 21 | namespace OC { 22 | class SystemConfig { 23 | } 24 | } 25 | 26 | namespace OC\Log { 27 | use OC\SystemConfig; 28 | class LogDetails { 29 | public function __construct(SystemConfig $config) { 30 | } 31 | public function logDetails(string $app, $message, int $level): array { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /l10n/hr.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Zapisivanje", 5 | "Log Reader" : "Čitač zapisa", 6 | "A log reader for Nextcloud" : "Čitač zapisa za Nextcloud", 7 | "Log reader for Nextcloud" : "Čitač zapisa za Nextcloud", 8 | "Level" : "Razina", 9 | "App" : "Aplikacija", 10 | "Time" : "Vrijeme", 11 | "Download logs" : "Preuzmi zapise", 12 | "Relative" : "Relativno", 13 | "Message" : "Poruka", 14 | "Show details" : "Prikaži pojedinosti", 15 | "Debug" : "Otklanjanje pogrešaka", 16 | "Info" : "Informacije", 17 | "Warning" : "Upozorenje", 18 | "Error" : "Pogreška", 19 | "Fatal" : "Fatalno" 20 | }, 21 | "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;"); 22 | -------------------------------------------------------------------------------- /l10n/pt_PT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registos", 5 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["%n aviso nos registos desde %s","%n avisos nos registos desde %s","%n avisos nos registos desde %s"], 6 | "Level" : "Nível", 7 | "App" : "Aplicação", 8 | "Time" : "Tempo", 9 | "Download/Upload logs" : "Registos de Transferir/Enviar", 10 | "Ctrl" : "Ctrl", 11 | "Download logs" : "Transferir registos", 12 | "Relative" : "Relativo", 13 | "Message" : "Mensagem", 14 | "Show details" : "Mostrar detalhes", 15 | "Info" : "Informação", 16 | "Warning" : "Aviso", 17 | "Error" : "Erro" 18 | }, 19 | "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 20 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { translate as t } from '@nextcloud/l10n' 7 | 8 | /** 9 | * Mapping from numeric log level (0 - 4) to localized names 10 | */ 11 | export const LOGGING_LEVEL_NAMES = [ 12 | t('logreader', 'Debug'), 13 | t('logreader', 'Info'), 14 | t('logreader', 'Warning'), 15 | t('logreader', 'Error'), 16 | t('logreader', 'Fatal'), 17 | ] 18 | 19 | /** 20 | * Mapping from numeric log level to string 21 | */ 22 | export const LOGGING_LEVEL = [ 23 | 'debug', 24 | 'info', 25 | 'warning', 26 | 'error', 27 | 'fatal', 28 | ] as const 29 | 30 | /** 31 | * Interval for polling in ms 32 | */ 33 | export const POLLING_INTERVAL = 10000 34 | -------------------------------------------------------------------------------- /l10n/bg.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Регистри", 3 | "Log Reader" : "Четец на журнал", 4 | "A log reader for Nextcloud" : "Четец на журнал за Nextcloud", 5 | "Log reader for Nextcloud" : "Четец на журнал за Nextcloud", 6 | "Level" : "Ниво", 7 | "App" : "Приложение", 8 | "Time" : "Време", 9 | "Line {line}" : "Ред {line}", 10 | "Ctrl" : "Ctrl /бутон/", 11 | "Download logs" : "Изтегляне на журнали", 12 | "Relative" : "Относително", 13 | "Application" : "Приложение", 14 | "Message" : "Съобщение", 15 | "Show details" : "Показване на подробности", 16 | "Debug" : "Отстраняване на грешки", 17 | "Info" : "Информация", 18 | "Warning" : "Внимание", 19 | "Error" : "Грешка", 20 | "Fatal" : "Фатална" 21 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 22 | } -------------------------------------------------------------------------------- /src/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { translate as t } from '@nextcloud/l10n' 7 | 8 | /** 9 | * Copy text to clipboard if it fails (e.g. not secure context (https, localhost...)) 10 | * a prompt will be opened for the user to copy the text manually 11 | * 12 | * @param text The text to copy 13 | * @return true if automatic copy suceeded, false if prompt was used 14 | */ 15 | export async function copyToCipboard(text: string) { 16 | try { 17 | await window.navigator.clipboard.writeText(text) 18 | return true 19 | } catch (e) { 20 | window.prompt( 21 | t('logreader', 'Could not copy to clipboard, please copy manually:'), 22 | text, 23 | ) 24 | } 25 | return false 26 | } 27 | -------------------------------------------------------------------------------- /LICENSES/ISC.txt: -------------------------------------------------------------------------------- 1 | ISC License: 2 | 3 | Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") 4 | Copyright (c) 1995-2003 by Internet Software Consortium 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 9 | -------------------------------------------------------------------------------- /l10n/bg.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Регистри", 5 | "Log Reader" : "Четец на журнал", 6 | "A log reader for Nextcloud" : "Четец на журнал за Nextcloud", 7 | "Log reader for Nextcloud" : "Четец на журнал за Nextcloud", 8 | "Level" : "Ниво", 9 | "App" : "Приложение", 10 | "Time" : "Време", 11 | "Line {line}" : "Ред {line}", 12 | "Ctrl" : "Ctrl /бутон/", 13 | "Download logs" : "Изтегляне на журнали", 14 | "Relative" : "Относително", 15 | "Application" : "Приложение", 16 | "Message" : "Съобщение", 17 | "Show details" : "Показване на подробности", 18 | "Debug" : "Отстраняване на грешки", 19 | "Info" : "Информация", 20 | "Warning" : "Внимание", 21 | "Error" : "Грешка", 22 | "Fatal" : "Фатална" 23 | }, 24 | "nplurals=2; plural=(n != 1);"); 25 | -------------------------------------------------------------------------------- /l10n/es_EC.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Registrando", 3 | "Log Reader" : "Lector de Bitácoras", 4 | "A log reader for Nextcloud" : "Un lector de registros para Nextcloud.", 5 | "Log reader for Nextcloud" : "Lector de registros para Nextcloud.", 6 | "Level" : "Nivel", 7 | "App" : "Aplicación", 8 | "Time" : "Hora", 9 | "Line {line}" : "Línea {line}", 10 | "Ctrl" : "Ctrl", 11 | "Download logs" : "Descargar bitácoras", 12 | "Relative" : "Relativo", 13 | "Application" : "Aplicación", 14 | "Message" : "Mensaje", 15 | "Show details" : "Mostrar detalles", 16 | "Debug" : "Depuración", 17 | "Info" : "Info", 18 | "Warning" : "Advertencia", 19 | "Error" : "Error", 20 | "Fatal" : "Fatal" 21 | },"pluralForm" :"nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;" 22 | } -------------------------------------------------------------------------------- /tests/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | ./Unit 16 | 17 | 18 | 19 | 20 | ../lib 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: CC0-1.0 4 | */ 5 | 6 | import type { UserConfig } from 'vitest/config' 7 | 8 | import { createAppConfig } from '@nextcloud/vite-config' 9 | 10 | const config = createAppConfig({ 11 | main: 'src/index.ts', 12 | }, { 13 | // Build the css/logreader-style.css instead of inlineing the styles in the js bundle 14 | inlineCSS: false, 15 | assetFileNames: (info) => info.name === 'index.css' ? 'css/logreader-main.css' : undefined, 16 | // Configuration for vitest unit tests 17 | config: { 18 | test: { 19 | coverage: { 20 | include: ['src/**'], 21 | provider: 'istanbul', 22 | reporter: ['lcov', 'text'], 23 | }, 24 | environment: 'happy-dom', 25 | }, 26 | } as UserConfig, 27 | }) 28 | 29 | export default config 30 | -------------------------------------------------------------------------------- /l10n/es_EC.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registrando", 5 | "Log Reader" : "Lector de Bitácoras", 6 | "A log reader for Nextcloud" : "Un lector de registros para Nextcloud.", 7 | "Log reader for Nextcloud" : "Lector de registros para Nextcloud.", 8 | "Level" : "Nivel", 9 | "App" : "Aplicación", 10 | "Time" : "Hora", 11 | "Line {line}" : "Línea {line}", 12 | "Ctrl" : "Ctrl", 13 | "Download logs" : "Descargar bitácoras", 14 | "Relative" : "Relativo", 15 | "Application" : "Aplicación", 16 | "Message" : "Mensaje", 17 | "Show details" : "Mostrar detalles", 18 | "Debug" : "Depuración", 19 | "Info" : "Info", 20 | "Warning" : "Advertencia", 21 | "Error" : "Error", 22 | "Fatal" : "Fatal" 23 | }, 24 | "nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;"); 25 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /l10n/lt_LT.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Registravimas", 3 | "Log Reader" : "Žurnalų skaitytuvė", 4 | "A log reader for Nextcloud" : "Žurnalo skaitytuvė, skirta Nextcloud", 5 | "Log reader for Nextcloud" : "Žurnalo skaitytuvė, skirta Nextcloud", 6 | "Level" : "Lygmuo", 7 | "App" : "Programėlė", 8 | "Time" : "Laikas", 9 | "Ctrl" : "Vald", 10 | "Download logs" : "Atsisiųsti žurnalus", 11 | "Relative" : "Santykinis laikas", 12 | "Message" : "Žinutė", 13 | "Show details" : "Rodyti išsamiau", 14 | "Debug" : "Derinimas", 15 | "Info" : "Informacija", 16 | "Warning" : "Įspėjimas", 17 | "Error" : "Klaida", 18 | "Fatal" : "Lemtinga klaida" 19 | },"pluralForm" :"nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);" 20 | } -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | reuse-compliance-check: 19 | runs-on: ubuntu-latest-low 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 23 | with: 24 | persist-credentials: false 25 | 26 | - name: REUSE Compliance Check 27 | uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 28 | -------------------------------------------------------------------------------- /l10n/lt_LT.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Registravimas", 5 | "Log Reader" : "Žurnalų skaitytuvė", 6 | "A log reader for Nextcloud" : "Žurnalo skaitytuvė, skirta Nextcloud", 7 | "Log reader for Nextcloud" : "Žurnalo skaitytuvė, skirta Nextcloud", 8 | "Level" : "Lygmuo", 9 | "App" : "Programėlė", 10 | "Time" : "Laikas", 11 | "Ctrl" : "Vald", 12 | "Download logs" : "Atsisiųsti žurnalus", 13 | "Relative" : "Santykinis laikas", 14 | "Message" : "Žinutė", 15 | "Show details" : "Rodyti išsamiau", 16 | "Debug" : "Derinimas", 17 | "Info" : "Informacija", 18 | "Warning" : "Įspėjimas", 19 | "Error" : "Klaida", 20 | "Fatal" : "Lemtinga klaida" 21 | }, 22 | "nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);"); 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextcloud/logreader", 3 | "type": "project", 4 | "license": "AGPLv3", 5 | "require-dev": { 6 | "nextcloud/coding-standard": "^1.3", 7 | "nextcloud/ocp": "dev-master", 8 | "phpunit/phpunit": "^9", 9 | "vimeo/psalm": "^5.15" 10 | }, 11 | "config": { 12 | "optimize-autoloader": true, 13 | "classmap-authoritative": true, 14 | "platform": { 15 | "php": "8.1" 16 | }, 17 | "sort-packages": true 18 | }, 19 | "scripts": { 20 | "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -not -path './tests/integration/vendor/*' -print0 | xargs -0 -n1 php -l", 21 | "cs:check": "php-cs-fixer fix --dry-run --diff", 22 | "cs:fix": "php-cs-fixer fix", 23 | "test:unit": "phpunit -c tests/phpunit.xml", 24 | "psalm": "psalm --threads=1" 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "OCP\\": "vendor/nextcloud/ocp/OCP" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/interfaces/IAppSettings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | /** 7 | * Logreader app settings 8 | */ 9 | export interface IAppSettings { 10 | /** 11 | * Logging levels to display messages for 12 | */ 13 | shownLevels: Array<0 | 1 | 2 | 3 | 4> 14 | /** 15 | * How the log time should be displayed 16 | * - `local` Show in client local time format 17 | * - `utc` Same as `local` but in UTC timezone 18 | * - `relative`Same as `local` but show near times as relative (e.g. "2 seconds ago") 19 | * - `raw` The same as in the logfile 20 | */ 21 | dateTimeFormat: 'local' | 'raw' | 'utc' | 'relative' 22 | /** 23 | * Wether the log should be polled 24 | */ 25 | liveLog: boolean 26 | /** 27 | * Wether backend is enable = logging is set to file 28 | */ 29 | enabled: boolean 30 | /** 31 | * The loglevel which is currently set on the server 32 | */ 33 | logLevel: 0 | 1 | 2 | 3 | 4 34 | } 35 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/utils/clipboard.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' 7 | import { copyToCipboard } from './clipboard' 8 | 9 | describe('utils:clipboard', () => { 10 | let originalWindowPrompt: window.prompt 11 | 12 | beforeEach(() => { 13 | window.prompt = vi.fn(() => '') 14 | }) 15 | 16 | afterEach(() => { 17 | window.prompt = originalWindowPrompt 18 | }) 19 | 20 | it('writes to clipboard', () => { 21 | const spy = vi.spyOn(window.navigator.clipboard, 'writeText') 22 | copyToCipboard('foo bar') 23 | expect(spy).toBeCalledWith('foo bar') 24 | expect(window.prompt).not.toBeCalled() 25 | }) 26 | 27 | it('opens promp if not supported clipboard', () => { 28 | window.navigator.clipboard.writeText = vi.fn(() => { 29 | throw Error('no secure context') 30 | }) 31 | copyToCipboard('foo bar') 32 | expect(window.navigator.clipboard.writeText).toBeCalledWith('foo bar') 33 | expect(window.prompt).toBeCalled() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /.github/workflows/fixup.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block fixup and squash commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: fixup-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | commit-message-check: 24 | if: github.event.pull_request.draft == false 25 | 26 | permissions: 27 | pull-requests: write 28 | name: Block fixup and squash commits 29 | 30 | runs-on: ubuntu-latest-low 31 | 32 | steps: 33 | - name: Run check 34 | uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /lib/Controller/PageController.php: -------------------------------------------------------------------------------- 1 | appName, 'logreader-main'); 36 | Util::addStyle($this->appName, 'logreader-main'); 37 | $this->initialState->provideInitialState('settings', $this->settingsService->getAppSettings()); 38 | 39 | return new TemplateResponse($this->appName, 'index'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/block-unconventional-commits.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block unconventional commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: block-unconventional-commits-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | block-unconventional-commits: 24 | name: Block unconventional commits 25 | 26 | runs-on: ubuntu-latest-low 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | with: 32 | persist-credentials: false 33 | 34 | - uses: webiny/action-conventional-commits@8bc41ff4e7d423d56fa4905f6ff79209a78776c7 # v1.3.0 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /l10n/mk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Евидентирање", 3 | "Errors in the log" : "Грешки во записникот", 4 | "No errors in the logs since %s" : "Не постојат грешки во записникот од %s", 5 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["%n грешка во записникот од %s","%n грешки во записникот од %s"], 6 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["%n предупредување во записникот од %s","%n предупредувања во записникот од %s"], 7 | "Log Reader" : "Читач на записи", 8 | "A log reader for Nextcloud" : "Читач на записи со евиденција за Nextcloud", 9 | "Log reader for Nextcloud" : "Читач на записи со евиденција за Nextcloud", 10 | "Log reader" : "Читач на записи", 11 | "Level" : "Ниво", 12 | "App" : "Апликација", 13 | "Time" : "Време", 14 | "Ctrl" : "Ctrl", 15 | "Download logs" : "Преземи записи", 16 | "Relative" : "Релативно", 17 | "Message" : "Порака", 18 | "Show details" : "Прикажи детали", 19 | "Debug" : "Дебаг", 20 | "Info" : "Инфо", 21 | "Warning" : "Предупредување", 22 | "Error" : "Грешка", 23 | "Fatal" : "Фатално" 24 | },"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" 25 | } -------------------------------------------------------------------------------- /l10n/mk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Евидентирање", 5 | "Errors in the log" : "Грешки во записникот", 6 | "No errors in the logs since %s" : "Не постојат грешки во записникот од %s", 7 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["%n грешка во записникот од %s","%n грешки во записникот од %s"], 8 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["%n предупредување во записникот од %s","%n предупредувања во записникот од %s"], 9 | "Log Reader" : "Читач на записи", 10 | "A log reader for Nextcloud" : "Читач на записи со евиденција за Nextcloud", 11 | "Log reader for Nextcloud" : "Читач на записи со евиденција за Nextcloud", 12 | "Log reader" : "Читач на записи", 13 | "Level" : "Ниво", 14 | "App" : "Апликација", 15 | "Time" : "Време", 16 | "Ctrl" : "Ctrl", 17 | "Download logs" : "Преземи записи", 18 | "Relative" : "Релативно", 19 | "Message" : "Порака", 20 | "Show details" : "Прикажи детали", 21 | "Debug" : "Дебаг", 22 | "Info" : "Инфо", 23 | "Warning" : "Предупредување", 24 | "Error" : "Грешка", 25 | "Fatal" : "Фатално" 26 | }, 27 | "nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); 28 | -------------------------------------------------------------------------------- /lib/Listener/LogListener.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class LogListener implements IEventListener { 23 | private ?Console $console; 24 | 25 | public function __construct(Formatter $formatter, SystemConfig $config) { 26 | $level = getenv('OCC_LOG'); 27 | if ($level) { 28 | $terminal = new Terminal(); 29 | $this->console = new Console($formatter, $config, $level, $terminal->getWidth()); 30 | } else { 31 | $this->console = null; 32 | } 33 | } 34 | 35 | 36 | public function handle(Event $event): void { 37 | if (!$event instanceof BeforeMessageLoggedEvent) { 38 | return; 39 | } 40 | 41 | if ($this->console) { 42 | $this->console->log($event->getLevel(), $event->getApp(), $event->getMessage()); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/debounce.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' 7 | import { debounce } from './debounce' 8 | 9 | describe('utils/debounce', () => { 10 | afterAll(() => { 11 | vi.useRealTimers() 12 | }) 13 | beforeAll(() => { 14 | vi.useFakeTimers() 15 | }) 16 | 17 | it('waits for the timeout', () => { 18 | const callback = vi.fn() 19 | const debounced = debounce(callback) 20 | 21 | debounced() 22 | expect(callback).not.toBeCalled() 23 | vi.advanceTimersByTime(300) 24 | expect(callback).toBeCalled() 25 | }) 26 | 27 | it('passes arguments', () => { 28 | const callback = vi.fn() 29 | const debounced = debounce(callback, 50) 30 | 31 | debounced(1, 'two', { three: 3 }) 32 | expect(callback).not.toBeCalled() 33 | vi.advanceTimersByTime(50) 34 | expect(callback).toBeCalledWith(1, 'two', { three: 3 }) 35 | }) 36 | 37 | it('dismisses calls before timeout', () => { 38 | const callback = vi.fn() 39 | const debounced = debounce(callback, 50) 40 | 41 | debounced() 42 | debounced() 43 | expect(callback).not.toBeCalled() 44 | 45 | vi.advanceTimersByTime(50) 46 | expect(callback).toBeCalledTimes(1) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | registerEventListener(BeforeMessageLoggedEvent::class, LogListener::class); 29 | } 30 | $context->registerService(Formatter::class, function (ContainerInterface $c) { 31 | return new Formatter(\OC::$SERVERROOT); 32 | }); 33 | 34 | $context->registerSetupCheck(LogErrors::class); 35 | } 36 | 37 | public function boot(IBootContext $context): void { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/IntersectionObserver.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 58 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLiveView.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) . 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /lib/Constants.php: -------------------------------------------------------------------------------- 1 | { 21 | const dateFormat = Intl.DateTimeFormat(getCanonicalLocale(), { 22 | dateStyle: 'medium', 23 | timeStyle: 'medium', 24 | timeZone: settingsStore.dateTimeFormat === 'utc' ? 'UTC' : undefined, 25 | }) 26 | return dateFormat.format(new Date(time)) 27 | } 28 | /** 29 | * Format a log entry into a human readable text 30 | * 31 | * @param entry The log entry to format 32 | */ 33 | const formatLogEntry = (entry: ILogEntry) => { 34 | return ( 35 | `[${entry.app}] ${LOGGING_LEVEL_NAMES[entry.level]}: ${entry.message}\n` 36 | + (entry.method ? `\t${entry.method} ${entry.url}\n` : '') 37 | + t('logreader', '\tfrom {address} by {user} at {time}\n', { 38 | address: entry.remoteAddr || '?', 39 | user: entry.user || '?', 40 | time: formatTime(entry.time), 41 | }) 42 | ) 43 | } 44 | return { 45 | formatTime, 46 | formatLogEntry, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/Settings/Section.php: -------------------------------------------------------------------------------- 1 | l = $l; 21 | $this->url = $url; 22 | } 23 | 24 | /** 25 | * returns the ID of the section. It is supposed to be a lower case string, 26 | * e.g. 'ldap' 27 | * 28 | * @returns string 29 | */ 30 | public function getID() { 31 | return 'logging'; 32 | } 33 | 34 | /** 35 | * returns the translated name as it should be displayed, e.g. 'LDAP / AD 36 | * integration'. Use the L10N service to translate it. 37 | * 38 | * @return string 39 | */ 40 | public function getName() { 41 | return $this->l->t('Logging'); 42 | } 43 | 44 | /** 45 | * @return int whether the form should be rather on the top or bottom of 46 | * the settings navigation. The sections are arranged in ascending order of 47 | * the priority values. It is required to return a value between 0 and 99. 48 | * 49 | * E.g.: 70 50 | */ 51 | public function getPriority() { 52 | return 90; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getIcon() { 59 | return $this->url->imagePath('logreader', 'app-dark.svg'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 9 | logreader 10 | Log Reader 11 | A log reader for Nextcloud 12 | Log reader for Nextcloud 13 | 6.0.0-dev.0 14 | agpl 15 | Robin Appelman 16 | Ferdinand Thiessen 17 | LogReader 18 | 19 | 20 | 21 | 22 | 23 | 24 | tools 25 | https://github.com/nextcloud/logreader 26 | https://github.com/nextcloud/logreader/issues 27 | https://github.com/nextcloud/logreader 28 | 29 | https://raw.githubusercontent.com/nextcloud/logreader/master/screenshots/reader.png 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | OCA\LogReader\Command\Tail 38 | OCA\LogReader\Command\Watch 39 | 40 | 41 | 42 | OCA\LogReader\Settings\Admin 43 | OCA\LogReader\Settings\Section 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/Log/LogIteratorFactory.php: -------------------------------------------------------------------------------- 1 | config->getSystemValue('logdateformat', \DateTime::ATOM); 30 | $timezone = $this->config->getSystemValue('logtimezone', 'UTC'); 31 | $logType = $this->config->getSystemValue('log_type', 'file'); 32 | if ($logType !== 'file') { 33 | throw new UnsupportedLogTypeException(); 34 | } 35 | $log = $this->logFactory->get('file'); 36 | if ($log instanceof IFileBased) { 37 | $handle = fopen($log->getLogFilePath(), 'rb'); 38 | if ($handle) { 39 | $iterator = new LogIterator($handle, $dateFormat, $timezone); 40 | return new \CallbackFilterIterator($iterator, function ($logItem) use ($levels) { 41 | return $logItem && in_array($logItem['level'], $levels); 42 | }); 43 | } else { 44 | throw new \Exception('Error while opening ' . $log->getLogFilePath()); 45 | } 46 | } 47 | throw new \Exception('Can\'t find log class'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /l10n/lv.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Žurnalēšana", 3 | "No errors in the logs since %s" : "Kopš %s žurnālos nav kļūdu", 4 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["Kopš %s žurnālos ir %n kļūdu","Kopš %s žurnālos ir %n kļūda","Kopš %s žurnālos ir %n kļūdas"], 5 | "Log Reader" : "Žurnālu lasītājs", 6 | "A log reader for Nextcloud" : "Nextcloud žurnālu lasītājs", 7 | "Log reader for Nextcloud" : "Nextcloud žurnālu lasītājs", 8 | "Log reader" : "Žurnālu lasītāj", 9 | "Open log reader settings" : "Atvērt žurnālu lasītāja iestatījumus", 10 | "Log reader settings" : "Žurnālu lasītāja iestatījumi", 11 | "Show server log" : "Parādīt servera žurnālu", 12 | "Level" : "Līmenis", 13 | "App" : "Lietotne", 14 | "Time" : "Laiks", 15 | "Copy raw entry" : "Ievietot starpliktuvē neapstrādātu ierakstu", 16 | "Copy formatted entry" : "Ievietot starpliktuvē formatētu ierakstu", 17 | "Time format" : "Laika formāts", 18 | "Download logs" : "Lejupielādēt žurnālierakstu", 19 | "Relative" : "Relatīvs", 20 | "Application" : "Lietotne", 21 | "Message" : "Ziņojums", 22 | "Collapse row" : "Sakļaut rindu", 23 | "Expand row" : "Izvērst rindu", 24 | "Show details" : "Rādīt informāciju", 25 | "Info" : "Informācija", 26 | "Warning" : "Brīdinājums", 27 | "Error" : "Kļūda", 28 | "Could not load log entries" : "Nevarēja ielādēt žurnāla ierakstus", 29 | "Could not fetch new log entries (server unavailable)" : "Neizdevās izgūt jaunus žurnāla ierakstus (serveris nav pieejams)" 30 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" 31 | } -------------------------------------------------------------------------------- /l10n/lv.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Žurnalēšana", 5 | "No errors in the logs since %s" : "Kopš %s žurnālos nav kļūdu", 6 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["Kopš %s žurnālos ir %n kļūdu","Kopš %s žurnālos ir %n kļūda","Kopš %s žurnālos ir %n kļūdas"], 7 | "Log Reader" : "Žurnālu lasītājs", 8 | "A log reader for Nextcloud" : "Nextcloud žurnālu lasītājs", 9 | "Log reader for Nextcloud" : "Nextcloud žurnālu lasītājs", 10 | "Log reader" : "Žurnālu lasītāj", 11 | "Open log reader settings" : "Atvērt žurnālu lasītāja iestatījumus", 12 | "Log reader settings" : "Žurnālu lasītāja iestatījumi", 13 | "Show server log" : "Parādīt servera žurnālu", 14 | "Level" : "Līmenis", 15 | "App" : "Lietotne", 16 | "Time" : "Laiks", 17 | "Copy raw entry" : "Ievietot starpliktuvē neapstrādātu ierakstu", 18 | "Copy formatted entry" : "Ievietot starpliktuvē formatētu ierakstu", 19 | "Time format" : "Laika formāts", 20 | "Download logs" : "Lejupielādēt žurnālierakstu", 21 | "Relative" : "Relatīvs", 22 | "Application" : "Lietotne", 23 | "Message" : "Ziņojums", 24 | "Collapse row" : "Sakļaut rindu", 25 | "Expand row" : "Izvērst rindu", 26 | "Show details" : "Rādīt informāciju", 27 | "Info" : "Informācija", 28 | "Warning" : "Brīdinājums", 29 | "Error" : "Kļūda", 30 | "Could not load log entries" : "Nevarēja ielādēt žurnāla ierakstus", 31 | "Could not fetch new log entries (server unavailable)" : "Neizdevās izgūt jaunus žurnāla ierakstus (serveris nav pieejams)" 32 | }, 33 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"); 34 | -------------------------------------------------------------------------------- /src/utils/format.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import type { ILogEntry } from '../interfaces' 7 | 8 | import { createTestingPinia } from '@pinia/testing' 9 | import { beforeAll, describe, expect, it, vi } from 'vitest' 10 | import { useLogFormatting } from './format' 11 | 12 | describe('utils:format', () => { 13 | beforeAll(() => { 14 | createTestingPinia({ 15 | fakeApp: true, 16 | createSpy: vi.fn, 17 | }) 18 | }) 19 | 20 | describe('formatLogEntry', () => { 21 | it('formats a log entry', () => { 22 | const { formatLogEntry } = useLogFormatting() 23 | const entry = { app: 'app', level: 1, message: 'msg', time: '2023-07-11T19:08:02.102Z' } 24 | expect(formatLogEntry(entry as ILogEntry)).toBe('[app] Info: msg\n\tfrom ? by ? at Jul 11, 2023, 6:08:02 PM\n') 25 | }) 26 | 27 | it('formats a log entry with user and remoteAddr', () => { 28 | const { formatLogEntry } = useLogFormatting() 29 | const entry = { app: 'app', level: 1, message: 'msg', time: '2023-07-11T19:08:02.102Z', user: 'user1', remoteAddr: '0.0.0.0' } 30 | expect(formatLogEntry(entry as ILogEntry)).toBe('[app] Info: msg\n\tfrom 0.0.0.0 by user1 at Jul 11, 2023, 6:08:02 PM\n') 31 | }) 32 | 33 | it('formats a log entry with url', () => { 34 | const { formatLogEntry } = useLogFormatting() 35 | const entry = { app: 'app', level: 1, message: 'msg', time: '2023-07-11T19:08:02.102Z', method: 'GET', url: 'foo/bar' } 36 | expect(formatLogEntry(entry as ILogEntry)).toBe('[app] Info: msg\n\tGET foo/bar\n\tfrom ? by ? at Jul 11, 2023, 6:08:02 PM\n') 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /lib/Settings/Admin.php: -------------------------------------------------------------------------------- 1 | appName, 'logreader-main'); 30 | Util::addStyle($this->appName, 'logreader-main'); 31 | 32 | $this->initialState->provideInitialState('settings', $this->settingsService->getAppSettings()); 33 | 34 | return new TemplateResponse($this->appName, 'index'); 35 | } 36 | 37 | /** 38 | * @return string the section ID, e.g. 'sharing' 39 | */ 40 | public function getSection() { 41 | return 'logging'; 42 | } 43 | 44 | /** 45 | * @return int whether the form should be rather on the top or bottom of 46 | * the admin section. The forms are arranged in ascending order of the 47 | * priority values. It is required to return a value between 0 and 100. 48 | * 49 | * E.g.: 70 50 | */ 51 | public function getPriority() { 52 | return 90; 53 | } 54 | 55 | public function getName(): ?string { 56 | return null; 57 | } 58 | 59 | public function getAuthorizedAppConfig(): array { 60 | return []; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /l10n/ko.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "로그", 3 | "No errors in the logs since %s" : "%s 이래로 로그에 아무런 오류가 기록되지 않았습니다", 4 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["로그에 %s 이래로 %n개의 오류가 기록되었습니다"], 5 | "Log Reader" : "로그 리더", 6 | "A log reader for Nextcloud" : "Nextcloud 로그 리더", 7 | "Log reader for Nextcloud" : "Nextcloud 로그 리더", 8 | "Log reader" : "로그 리더", 9 | "Open log reader settings" : "로그 리더 설정 열기", 10 | "Log reader settings" : "로그 리더 설정", 11 | "Level" : "단계", 12 | "App" : "앱", 13 | "Time" : "시간", 14 | "Filter log levels" : "로그 단계 필터링", 15 | "Set log level" : "로그 단계 설정", 16 | "Time format" : "시간 형식", 17 | "Live view" : "실시간 보기", 18 | "Download/Upload logs" : "로그 다운로드/업로드", 19 | "Ctrl" : "Ctrl", 20 | "Download logs" : "로그 다운로드", 21 | "Show local log file" : "로컬 로그 파일 보기", 22 | "Upload local log file to be displayed" : "표시할 로컬 로그 파일 업로드", 23 | "Time format used for displaying the timestamp" : "타임스탬프 표시에 사용되는 시간 형식", 24 | "(Local log files only support the \"raw\" time format)" : "(로컬 로그 파일은 \"raw\" 형식만 지원합니다)", 25 | "Could not change date time format." : "시간 형식을 변경할 수 없음", 26 | "Local time" : "로컬 시간 형식", 27 | "UTC time" : "UTC 시간 형식", 28 | "Relative" : "상대적", 29 | "Could not change live view setting." : "실시간 보기 설정을 변경할 수 없음", 30 | "Polling (live view)" : "폴링 (실시간 보기 활성화)", 31 | "Application" : "애플리케이션", 32 | "Message" : "메시지", 33 | "No older log entries available" : "사용 가능한 로그 항목 없음", 34 | "Show details" : "세부 정보", 35 | "Debug" : "디버그", 36 | "Info" : "정보", 37 | "Warning" : "경고", 38 | "Error" : "오류", 39 | "Fatal" : "치명적" 40 | },"pluralForm" :"nplurals=1; plural=0;" 41 | } -------------------------------------------------------------------------------- /l10n/ko.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "로그", 5 | "No errors in the logs since %s" : "%s 이래로 로그에 아무런 오류가 기록되지 않았습니다", 6 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["로그에 %s 이래로 %n개의 오류가 기록되었습니다"], 7 | "Log Reader" : "로그 리더", 8 | "A log reader for Nextcloud" : "Nextcloud 로그 리더", 9 | "Log reader for Nextcloud" : "Nextcloud 로그 리더", 10 | "Log reader" : "로그 리더", 11 | "Open log reader settings" : "로그 리더 설정 열기", 12 | "Log reader settings" : "로그 리더 설정", 13 | "Level" : "단계", 14 | "App" : "앱", 15 | "Time" : "시간", 16 | "Filter log levels" : "로그 단계 필터링", 17 | "Set log level" : "로그 단계 설정", 18 | "Time format" : "시간 형식", 19 | "Live view" : "실시간 보기", 20 | "Download/Upload logs" : "로그 다운로드/업로드", 21 | "Ctrl" : "Ctrl", 22 | "Download logs" : "로그 다운로드", 23 | "Show local log file" : "로컬 로그 파일 보기", 24 | "Upload local log file to be displayed" : "표시할 로컬 로그 파일 업로드", 25 | "Time format used for displaying the timestamp" : "타임스탬프 표시에 사용되는 시간 형식", 26 | "(Local log files only support the \"raw\" time format)" : "(로컬 로그 파일은 \"raw\" 형식만 지원합니다)", 27 | "Could not change date time format." : "시간 형식을 변경할 수 없음", 28 | "Local time" : "로컬 시간 형식", 29 | "UTC time" : "UTC 시간 형식", 30 | "Relative" : "상대적", 31 | "Could not change live view setting." : "실시간 보기 설정을 변경할 수 없음", 32 | "Polling (live view)" : "폴링 (실시간 보기 활성화)", 33 | "Application" : "애플리케이션", 34 | "Message" : "메시지", 35 | "No older log entries available" : "사용 가능한 로그 항목 없음", 36 | "Show details" : "세부 정보", 37 | "Debug" : "디버그", 38 | "Info" : "정보", 39 | "Warning" : "경고", 40 | "Error" : "오류", 41 | "Fatal" : "치명적" 42 | }, 43 | "nplurals=1; plural=0;"); 44 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "logreader" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/logreader" 7 | 8 | [[annotations]] 9 | path = [".github/issue_template.md", ".github/CODEOWNERS", "package-lock.json", "package.json", "**/package.json", "**/package-lock.json", "composer.json", "composer.lock", "**/composer.json", "**/composer.lock", ".jshintrc", ".l10nignore", ".tx/config", "tsconfig.json", "js/vendor.LICENSE.txt", "krankerl.toml", ".nextcloudignore", ".devcontainer/devcontainer.json", ".scrutinizer.yml", ".tx/backport"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "none" 12 | SPDX-License-Identifier = "CC0-1.0" 13 | 14 | [[annotations]] 15 | path = ["l10n/**.js", "l10n/**.json"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2016 Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | [[annotations]] 21 | path = ["js/**.js.map", "js/**.js", "js/**.mjs", "js/**.mjs.map", "js/templates/**.handlebars", "emptyTemplates/**", "css/**.css"] 22 | precedence = "aggregate" 23 | SPDX-FileCopyrightText = "2016 Nextcloud GmbH and Nextcloud contributors" 24 | SPDX-License-Identifier = "AGPL-3.0-or-later" 25 | 26 | [[annotations]] 27 | path = "screenshots/reader.png" 28 | precedence = "aggregate" 29 | SPDX-FileCopyrightText = "2016 Nextcloud GmbH and Nextcloud contributors" 30 | SPDX-License-Identifier = "CC0-1.0" 31 | 32 | [[annotations]] 33 | path = ["img/app-dark.svg", "img/app.svg"] 34 | precedence = "aggregate" 35 | SPDX-FileCopyrightText = "2018-2024 Google LLC" 36 | SPDX-License-Identifier = "Apache-2.0" 37 | -------------------------------------------------------------------------------- /src/components/settings/SettingsSetLogLevel.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logreader", 3 | "version": "6.0.0-dev.0", 4 | "license": "AGPL-3.0-or-later", 5 | "author": "Robin Appelman ", 6 | "type": "module", 7 | "scripts": { 8 | "build": "vite --mode production build", 9 | "dev": "vite --mode development build", 10 | "dev:watch": "vite --mode development build --watch", 11 | "lint": "eslint", 12 | "lint:fix": "eslint --fix", 13 | "test": "TZ='Etc/GMT+1' vitest run", 14 | "test:coverage": "TZ='Etc/GMT+1' vitest run --coverage", 15 | "test:watch": "TZ='Etc/GMT+1' vitest watch" 16 | }, 17 | "browserslist": [ 18 | "extends @nextcloud/browserslist-config" 19 | ], 20 | "dependencies": { 21 | "@highlightjs/vue-plugin": "^2.1.0", 22 | "@mdi/svg": "^7.4.47", 23 | "@nextcloud/axios": "^2.5.2", 24 | "@nextcloud/dialogs": "^7.1.0", 25 | "@nextcloud/initial-state": "^3.0.0", 26 | "@nextcloud/l10n": "^3.4.1", 27 | "@nextcloud/logger": "^3.0.3", 28 | "@nextcloud/router": "^3.1.0", 29 | "@nextcloud/typings": "^1.10.0", 30 | "@nextcloud/vue": "^9.3.0", 31 | "highlight.js": "^11.11.1", 32 | "pinia": "^3.0.4", 33 | "vue": "^3.5.24", 34 | "vue-material-design-icons": "^5.3.1" 35 | }, 36 | "devDependencies": { 37 | "@nextcloud/browserslist-config": "^3.1.2", 38 | "@nextcloud/eslint-config": "^9.0.0-rc.6", 39 | "@nextcloud/vite-config": "^2.5.2", 40 | "@pinia/testing": "^1.0.3", 41 | "@vitest/coverage-istanbul": "^4.0.15", 42 | "@vue/test-utils": "^2.4.6", 43 | "@vue/tsconfig": "^0.8.1", 44 | "browserslist": "^4.28.1", 45 | "happy-dom": "^20.0.11", 46 | "typescript": "^5.9.3", 47 | "vite": "^7.2.7", 48 | "vitest": "^4.0.13" 49 | }, 50 | "engines": { 51 | "node": "^24.0.0", 52 | "npm": "^11.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/exception/TraceLine.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 18 | 47 | 48 | 69 | -------------------------------------------------------------------------------- /lib/Log/SearchFilter.php: -------------------------------------------------------------------------------- 1 | > 12 | */ 13 | class SearchFilter extends \FilterIterator { 14 | /** 15 | * @var string 16 | */ 17 | private $query; 18 | 19 | /** 20 | * @var string[] 21 | */ 22 | private $levels; 23 | 24 | public function __construct(\Iterator $iterator, string $query) { 25 | parent::__construct($iterator); 26 | $this->rewind(); 27 | $this->query = strtolower($query); 28 | $this->levels = ['Debug', 'Info', 'Warning', 'Error', 'Fatal']; 29 | } 30 | 31 | private function formatLevel(int $level): string { 32 | return isset($this->levels[$level]) ? $this->levels[$level] : 'Unknown'; 33 | } 34 | 35 | public function accept(): bool { 36 | if (!$this->query) { 37 | return true; 38 | } 39 | $value = $this->current(); 40 | return $this->inMessage($value['message'] ?? '', $this->query) 41 | || stripos($value['app'] ?? '', $this->query) !== false 42 | || stripos($value['reqId'] ?? '', $this->query) !== false 43 | || stripos($value['user'] ?? '', $this->query) !== false 44 | || stripos($value['url'] ?? '', $this->query) !== false 45 | || stripos($this->formatLevel($value['level'] ?? -1), $this->query) !== false; 46 | } 47 | 48 | private function inMessage($message, string $query): bool { 49 | if (is_string($message)) { 50 | return stripos($message, $query) !== false; 51 | } elseif (isset($message['Exception'])) { 52 | return stripos($message['Exception'], $query) !== false 53 | || stripos($message['Message'] ?? '', $query) !== false; 54 | } 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/lint-php-cs.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php-cs 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-cs-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | 24 | name: php-cs 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 29 | with: 30 | persist-credentials: false 31 | 32 | - name: Get php version 33 | id: versions 34 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 35 | 36 | - name: Set up php${{ steps.versions.outputs.php-min }} 37 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5 38 | with: 39 | php-version: ${{ steps.versions.outputs.php-min }} 40 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 41 | coverage: none 42 | ini-file: development 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer remove nextcloud/ocp --dev --no-scripts 49 | composer i 50 | 51 | - name: Lint 52 | run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) 53 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | ## Submitting issues 2 | 3 | If you have questions about how to install or use Nextcloud, please direct these to our [forum][forum]. We are also available on [IRC][irc]. 4 | 5 | ### Short version 6 | 7 | * The [**issue template can be found here**][template]. Please always use the issue template when reporting issues. 8 | 9 | ### Guidelines 10 | * Please search the existing issues first, it's likely that your issue was already reported or even fixed. 11 | - Go to one of the repositories, click "issues" and type any word in the top search/command bar. 12 | - You can also filter by appending e. g. "state:open" to the search string. 13 | - More info on [search syntax within github](https://help.github.com/articles/searching-issues) 14 | * This repository ([Log reader](https://github.com/nextcloud/logreader/issues)) is *only* for issues within the Log reader code. 15 | * __SECURITY__: Report any potential security bug to us via [our HackerOne page](https://hackerone.com/nextcloud) or security@nextcloud.com following our [security policy](https://nextcloud.com/security/) instead of filing an issue in our bug tracker 16 | * Report the issue using our [template][template], it includes all the information we need to track down the issue. 17 | 18 | Help us to maximize the effort we can spend fixing issues and adding new features, by not reporting duplicate issues. 19 | 20 | [template]: https://raw.github.com/nextcloud/logreader/master/.github/issue_template.md 21 | [forum]: https://help.nextcloud.com/ 22 | [irc]: https://webchat.freenode.net/?channels=nextcloud 23 | 24 | ### Contribute Code and translations 25 | Please check [server's contribution guidelines](https://github.com/nextcloud/server/blob/master/CONTRIBUTING.md#contributing-to-source-code) for further information about contributing code and translations. 26 | 27 | 31 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 8 | ### Steps to reproduce 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ### Expected behaviour 14 | Tell us what should happen 15 | 16 | ### Actual behaviour 17 | Tell us what happens instead 18 | 19 | ### Server configuration 20 | 23 | 24 | **Operating system**: 25 | 26 | **Web server:** 27 | 28 | **Database:** 29 | 30 | **PHP version:** 31 | 32 | **Nextcloud version:** (see Nextcloud admin page) 33 | 34 | **Where did you install Nextcloud from:** 35 | 36 | 37 | **List of activated apps:** 38 | 39 | ``` 40 | If you have access to your command line run e.g.: 41 | sudo -u www-data php occ app:list 42 | from within your Nextcloud installation folder 43 | ``` 44 | 45 | **Nextcloud configuration:** 46 | 47 | ``` 48 | If you have access to your command line run e.g.: 49 | sudo -u www-data php occ config:list system 50 | from within your Nextcloud installation folder 51 | 52 | or 53 | 54 | Insert your config.php content here 55 | Make sure to remove all sensitive content such as passwords. (e.g. database password, passwordsalt, secret, smtp password, …) 56 | ``` 57 | 58 | ### Client configuration 59 | **Browser:** 60 | 61 | **Operating system:** 62 | 63 | ### Logs 64 | 65 | #### Nextcloud log (data/owncloud.log) 66 | ``` 67 | Insert your Nextcloud log here 68 | ``` 69 | 70 | #### Browser log 71 | ``` 72 | Insert your browser log here, this could for example include: 73 | 74 | a) The javascript console log 75 | b) The network log 76 | c) ... 77 | ``` 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc. 3 | # SPDX-License-Identifier: AGPL-3.0-or-later 4 | app_name=logreader 5 | project_dir=$(CURDIR)/../$(app_name) 6 | build_dir=$(project_dir)/build 7 | appstore_dir=$(build_dir)/appstore 8 | package_name=$(app_name) 9 | cert_dir=$(HOME)/.nextcloud/certificates 10 | webpack=node_modules/.bin/webpack 11 | 12 | jssources=$(wildcard js/*) $(wildcard js/*/*) $(wildcard css/*/*) $(wildcard css/*) 13 | othersources=$(wildcard appinfo/*) $(wildcard css/*/*) $(wildcard controller/*/*) $(wildcard templates/*/*) $(wildcard log/*/*) 14 | 15 | all: build/main.js 16 | 17 | clean: 18 | rm -rf $(build_dir) 19 | rm -rf node_modules 20 | 21 | node_modules: package.json 22 | npm install --deps 23 | 24 | build/main.js: node_modules $(jssources) 25 | npm run build 26 | 27 | .PHONY: watch 28 | watch: node_modules 29 | node node_modules/.bin/webpack-dev-server --public localcloud.icewind.me:444 --inline --hot --port 3000 --config webpack/dev.config.js 30 | 31 | appstore: clean build/main.js package 32 | 33 | package: build/appstore/$(package_name).tar.gz 34 | build/appstore/$(package_name).tar.gz: build/main.js $(othersources) 35 | mkdir -p $(appstore_dir) 36 | tar --exclude-vcs \ 37 | --exclude=$(appstore_dir) \ 38 | --exclude=$(project_dir)/node_modules \ 39 | --exclude=$(project_dir)/webpack \ 40 | --exclude=$(project_dir)/.gitattributes \ 41 | --exclude=$(project_dir)/.gitignore \ 42 | --exclude=$(project_dir)/.travis.yml \ 43 | --exclude=$(project_dir)/.tx \ 44 | --exclude=$(project_dir)/.scrutinizer.yml \ 45 | --exclude=$(project_dir)/CONTRIBUTING.md \ 46 | --exclude=$(project_dir)/package.json \ 47 | --exclude=$(project_dir)/screenshots \ 48 | --exclude=$(project_dir)/Makefile \ 49 | -cvzf $(appstore_dir)/$(package_name).tar.gz $(project_dir) 50 | openssl dgst -sha512 -sign $(cert_dir)/$(app_name).key $(appstore_dir)/$(app_name).tar.gz | openssl base64 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/block-merge-eol.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block merges for EOL 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: block-merge-eol-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | block-merges-eol: 22 | name: Block merges for EOL branches 23 | 24 | # Only run on stableXX branches 25 | if: startsWith( github.base_ref, 'stable') 26 | runs-on: ubuntu-latest-low 27 | 28 | steps: 29 | - name: Set server major version environment 30 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 31 | with: 32 | github-token: ${{secrets.GITHUB_TOKEN}} 33 | script: | 34 | const regex = /^stable(\d+)$/ 35 | const baseRef = context.payload.pull_request.base.ref 36 | const match = baseRef.match(regex) 37 | if (match) { 38 | console.log('Setting server_major to ' + match[1]); 39 | core.exportVariable('server_major', match[1]); 40 | console.log('Setting current_day to ' + (new Date()).toISOString().substr(0, 10)); 41 | core.exportVariable('current_day', (new Date()).toISOString().substr(0, 10)); 42 | } 43 | 44 | - name: Checking if server ${{ env.server_major }} is EOL 45 | if: ${{ env.server_major != '' }} 46 | run: | 47 | curl -s https://raw.githubusercontent.com/nextcloud-releases/updater_server/production/config/major_versions.json \ 48 | | jq '.["${{ env.server_major }}"]["eol"] // "9999-99-99" | . >= "${{ env.current_day }}"' \ 49 | | grep -q true 50 | -------------------------------------------------------------------------------- /src/components/settings/AppSettingsDialog.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | 30 | 51 | -------------------------------------------------------------------------------- /src/components/settings/SettingsLogLevels.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | 21 | 57 | 58 | 63 | -------------------------------------------------------------------------------- /lib/Log/Console.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 26 | $this->level = self::parseLogLevel($level); 27 | $this->terminalWidth = $terminalWidth; 28 | } 29 | 30 | public function log(int $level, string $app, array $entry) { 31 | if ($level >= $this->level) { 32 | $messageWidth = $this->terminalWidth - 8 - 18 - 6; 33 | 34 | $entry = $this->logDetails($app, $entry, $level); 35 | 36 | $lines = explode("\n", $this->formatter->formatMessage($entry, $messageWidth)); 37 | $lines[0] = str_pad(Tail::LEVELS[$level], 8) . ' ' . 38 | str_pad(wordwrap($app, 18), 18) . ' ' . 39 | str_pad($lines[0], $messageWidth); 40 | 41 | for ($i = 1; $i < count($lines); $i++) { 42 | $lines[$i] = str_repeat(' ', 8 + 18 + 2) . $lines[$i]; 43 | } 44 | 45 | foreach ($lines as $line) { 46 | fwrite(STDERR, $line . "\n"); 47 | } 48 | fwrite(STDERR, "\n"); 49 | } 50 | } 51 | 52 | private static function parseLogLevel(string $level): int { 53 | if (is_numeric($level)) { 54 | return (int)$level; 55 | } 56 | 57 | switch (strtoupper($level)) { 58 | case 'DEBUG': 59 | return 0; 60 | case 'INFO': 61 | return 1; 62 | case 'WARN': 63 | return 2; 64 | case 'ERROR': 65 | return 3; 66 | case 'FATAL': 67 | return 4; 68 | default: 69 | throw new \Exception("Unknown log level $level"); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/exception/LogException.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 53 | 54 | 75 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-approve-merge.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Auto approve Dependabot PRs 10 | 11 | on: 12 | pull_request_target: # zizmor: ignore[dangerous-triggers] 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | permissions: 19 | contents: read 20 | 21 | concurrency: 22 | group: dependabot-approve-merge-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | auto-approve-merge: 27 | if: github.event.pull_request.user.login == 'dependabot[bot]' || github.event.pull_request.user.login == 'renovate[bot]' 28 | runs-on: ubuntu-latest-low 29 | permissions: 30 | # for hmarr/auto-approve-action to approve PRs 31 | pull-requests: write 32 | # for alexwilson/enable-github-automerge-action to approve PRs 33 | contents: write 34 | 35 | steps: 36 | - name: Disabled on forks 37 | if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} 38 | run: | 39 | echo 'Can not approve PRs from forks' 40 | exit 1 41 | 42 | - uses: mdecoleman/pr-branch-name@55795d86b4566d300d237883103f052125cc7508 # v3.0.0 43 | id: branchname 44 | with: 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | # GitHub actions bot approve 48 | - uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363 # v4.0.0 49 | if: startsWith(steps.branchname.outputs.branch, 'dependabot/') 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | # Enable GitHub auto merge 54 | - name: Auto merge 55 | uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # v2.0.0 56 | if: startsWith(steps.branchname.outputs.branch, 'dependabot/') 57 | with: 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /src/utils/logfile.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { describe, expect, it } from 'vitest' 7 | import { parseLogFile, parseLogString } from './logfile' 8 | 9 | describe('utils:logfile', () => { 10 | it('can parse a file', async () => { 11 | // simply test that files are handled, the inner handling is checked by parseLogString 12 | const file = new File(['{ "app": "myApp" }'], 'log') 13 | expect(await parseLogFile(file)).toEqual([{ app: 'myApp' }]) 14 | }) 15 | 16 | it('can parse server style logs', async () => { 17 | const input = '{ "app": "myApp" }\n{ "app": "myApp2" }' 18 | const expected = [{ app: 'myApp' }, { app: 'myApp2' }] 19 | 20 | const output = await parseLogString(input) 21 | expect(output).toEqual(expected) 22 | }) 23 | 24 | it('can parse logs without newline', async () => { 25 | const input = '{ "app": "myApp" }{ "app": "myApp2" }' 26 | const expected = [{ app: 'myApp' }, { app: 'myApp2' }] 27 | 28 | const output = await parseLogString(input) 29 | expect(output).toEqual(expected) 30 | }) 31 | 32 | it('can parse formatted logs', async () => { 33 | const input = '{\n\t"app": "myApp"\n}{\n\t"app": "myApp2"\n}\n' 34 | const expected = [{ app: 'myApp' }, { app: 'myApp2' }] 35 | 36 | const output = await parseLogString(input) 37 | expect(output).toEqual(expected) 38 | }) 39 | 40 | it('can parse quoted json', async () => { 41 | const input = '"{"app": "myApp"}"' 42 | const expected = [{ app: 'myApp' }] 43 | 44 | const output = await parseLogString(input) 45 | expect(output).toEqual(expected) 46 | }) 47 | 48 | it('can parse quoted and csv escaped json', async () => { 49 | const input = '"{""app"": ""myApp""}"' 50 | const expected = [{ app: 'myApp' }] 51 | 52 | const output = await parseLogString(input) 53 | expect(output).toEqual(expected) 54 | }) 55 | 56 | it('can parse unescaped message', async () => { 57 | const input = '{"app": "myApp", "message":""hello"","level": 1 }' 58 | const expected = [{ app: 'myApp', message: '"hello"', level: 1 }] 59 | 60 | const output = await parseLogString(input) 61 | expect(output).toEqual(expected) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /.github/workflows/block-merge-freeze.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block merges during freezes 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: block-merge-freeze-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | block-merges-during-freeze: 24 | name: Block merges during freezes 25 | 26 | if: github.event.pull_request.draft == false 27 | 28 | runs-on: ubuntu-latest-low 29 | 30 | steps: 31 | - name: Register server reference to fallback to master branch 32 | uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 33 | with: 34 | github-token: ${{secrets.GITHUB_TOKEN}} 35 | script: | 36 | const baseRef = context.payload.pull_request.base.ref 37 | if (baseRef === 'main' || baseRef === 'master') { 38 | core.exportVariable('server_ref', 'master'); 39 | console.log('Setting server_ref to master'); 40 | } else { 41 | const regex = /^stable(\d+)$/ 42 | const match = baseRef.match(regex) 43 | if (match) { 44 | core.exportVariable('server_ref', match[0]); 45 | console.log('Setting server_ref to ' + match[0]); 46 | } else { 47 | console.log('Not based on master/main/stable*, so skipping freeze check'); 48 | } 49 | } 50 | 51 | - name: Download version.php from ${{ env.server_ref }} 52 | if: ${{ env.server_ref != '' }} 53 | run: curl 'https://raw.githubusercontent.com/nextcloud/server/${{ env.server_ref }}/version.php' --output version.php 54 | 55 | - name: Run check 56 | if: ${{ env.server_ref != '' }} 57 | run: cat version.php | grep 'OC_VersionString' | grep -i -v 'RC' 58 | -------------------------------------------------------------------------------- /tests/Unit/SetupChecks/LogErrorsTest.php: -------------------------------------------------------------------------------- 1 | 6 | * SPDX-License-Identifier: AGPL-3.0-or-later 7 | */ 8 | 9 | namespace OCA\LogReader\Tests\Unit\SetupChecks; 10 | 11 | use OCA\LogReader\Log\LogIteratorFactory; 12 | use OCA\LogReader\SetupChecks\LogErrors; 13 | use OCP\IConfig; 14 | use OCP\IDateTimeFormatter; 15 | use OCP\IL10N; 16 | use Test\TestCase; 17 | 18 | class LogErrorsTest extends TestCase { 19 | private IL10N $l10n; 20 | private IConfig $config; 21 | private IDateTimeFormatter $dateFormatter; 22 | private LogIteratorFactory $logIteratorFactory; 23 | private LogErrors $logErrorsCheck; 24 | 25 | protected function setUp(): void { 26 | parent::setUp(); 27 | 28 | $this->l10n = $this->createMock(IL10N::class); 29 | $this->config = $this->createMock(IConfig::class); 30 | $this->dateFormatter = $this->createMock(IDateTimeFormatter::class); 31 | $this->logIteratorFactory = $this->createMock(LogIteratorFactory::class); 32 | $this->logErrorsCheck = new LogErrors( 33 | $this->l10n, 34 | $this->config, 35 | $this->dateFormatter, 36 | $this->logIteratorFactory, 37 | ); 38 | } 39 | 40 | public function logProvider(): array { 41 | $now = (new \DateTime())->format(\DateTime::ATOM); 42 | $tooOld = (new \DateTime('1 month ago'))->format(\DateTime::ATOM); 43 | return [ 44 | [[], 'success'], 45 | [[['level' => 2, 'time' => $now]], 'info'], 46 | [[['level' => 3, 'time' => $now]], 'warning'], 47 | [[['level' => 2, 'time' => $now],['level' => 3, 'time' => $now]], 'warning'], 48 | [[['level' => 2, 'time' => $now],['level' => 3, 'time' => $tooOld]], 'info'], 49 | [[['level' => 2, 'time' => $tooOld],['level' => 3, 'time' => $tooOld]], 'success'], 50 | ]; 51 | } 52 | 53 | /** 54 | * @dataProvider logProvider 55 | */ 56 | public function testSetupCheck(array $logContent, string $severity): void { 57 | $logIterator = new \ArrayIterator($logContent); 58 | $this->logIteratorFactory 59 | ->expects(self::once()) 60 | ->method('getLogIterator') 61 | ->willReturn($logIterator); 62 | 63 | $result = $this->logErrorsCheck->run(); 64 | 65 | $this->assertEquals($severity, $result->getSeverity()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/update-nextcloud-ocp-approve-merge.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Auto approve nextcloud/ocp 10 | 11 | on: 12 | pull_request_target: # zizmor: ignore[dangerous-triggers] 13 | branches: 14 | - main 15 | - master 16 | - stable* 17 | 18 | permissions: 19 | contents: read 20 | 21 | concurrency: 22 | group: update-nextcloud-ocp-approve-merge-${{ github.head_ref || github.run_id }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | auto-approve-merge: 27 | if: github.actor == 'nextcloud-command' 28 | runs-on: ubuntu-latest-low 29 | permissions: 30 | # for hmarr/auto-approve-action to approve PRs 31 | pull-requests: write 32 | # for alexwilson/enable-github-automerge-action to approve PRs 33 | contents: write 34 | 35 | steps: 36 | - name: Disabled on forks 37 | if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} 38 | run: | 39 | echo 'Can not approve PRs from forks' 40 | exit 1 41 | 42 | - uses: mdecoleman/pr-branch-name@55795d86b4566d300d237883103f052125cc7508 # v3.0.0 43 | id: branchname 44 | with: 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | # GitHub actions bot approve 48 | - uses: hmarr/auto-approve-action@b40d6c9ed2fa10c9a2749eca7eb004418a705501 # v2 49 | if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp') 50 | with: 51 | github-token: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | # Enable GitHub auto merge 54 | - name: Auto merge 55 | uses: alexwilson/enable-github-automerge-action@56e3117d1ae1540309dc8f7a9f2825bc3c5f06ff # v2.0.0 56 | if: startsWith(steps.branchname.outputs.branch, 'automated/noid/') && endsWith(steps.branchname.outputs.branch, 'update-nextcloud-ocp') 57 | with: 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | -------------------------------------------------------------------------------- /src/utils/splitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2025 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: LGPL-3.0-or-later 4 | */ 5 | 6 | /** 7 | * Copied from Densaugeo/JSON-String-Splitter 8 | * 9 | * Split concatenated JSON strings 10 | * Accepts a string consisting of one or more valid JSON substrings and splits it. Any remaining string after the end of the last complete JSON substring is returned in the 'remainder' field. 11 | * Passing in invalid JSON can result in garbage output 12 | * 13 | * @param string The string to look for JSON in 14 | * 15 | * @example 16 | * const pieces = splitter('{"foo":"bar"}{"more":"json"}{"partial":"json"') 17 | * 18 | * console.log(pieces.jsons[0]); // '{"foo":"bar"}' 19 | * console.log(pieces.jsons[1]); // '{"more":"json"}' 20 | * console.log(pieces.remainder); // '{"partial":"json"' 21 | */ 22 | export function splitter(string: string): { jsons: string[], remainder: string } { 23 | const START = 0, JSON = 1, STRING = 2, ESCAPE = 3 24 | 25 | let state = START 26 | let nestingLevel = 0 27 | let jsonStart = null 28 | const bounds = [] 29 | 30 | for (let i = 0; i < string.length; ++i) { 31 | switch (state) { 32 | case START: { 33 | switch (string[i]) { 34 | case '{': { 35 | ++nestingLevel 36 | state = JSON 37 | jsonStart = i 38 | break 39 | } 40 | } 41 | break 42 | } 43 | case JSON: { 44 | switch (string[i]) { 45 | case '{': { 46 | ++nestingLevel 47 | break 48 | } 49 | case '}': { 50 | --nestingLevel 51 | if (nestingLevel === 0) { 52 | state = START 53 | bounds.push({ start: jsonStart, end: i + 1 }) 54 | } 55 | break 56 | } 57 | case '"': { 58 | state = STRING 59 | break 60 | } 61 | } 62 | break 63 | } 64 | case STRING: { 65 | switch (string[i]) { 66 | case '"': { 67 | state = JSON 68 | break 69 | } 70 | case '\\': { 71 | state = ESCAPE 72 | break 73 | } 74 | } 75 | break 76 | } 77 | case ESCAPE: { 78 | state = STRING 79 | break 80 | } 81 | } 82 | } 83 | 84 | const result = { 85 | jsons: [], 86 | remainder: string.substring(bounds[bounds.length - 1].end), 87 | } 88 | 89 | bounds.forEach(function(v) { 90 | result.jsons.push(string.substring(v.start, v.end)) 91 | }) 92 | 93 | return result 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/exception.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import type { IException } from '../interfaces/ILogEntry' 7 | 8 | import { describe, expect, it } from 'vitest' 9 | import { parseException } from './exception' 10 | 11 | describe('utils:exception', () => { 12 | describe('parseException', () => { 13 | it('simply returns new style exceptions', () => { 14 | const exception = { 15 | Exception: 'GenericException', 16 | } 17 | 18 | expect(parseException(exception as IException)).toBe(exception) 19 | }) 20 | 21 | it('parses nested json exception', () => { 22 | const exception = { 23 | Exception: 'GenericException', 24 | } 25 | 26 | expect(parseException(JSON.stringify(exception))).toEqual(exception) 27 | }) 28 | 29 | it('parses old style exception', () => { 30 | const exception = { 31 | Exception: 'GenericException', 32 | } 33 | 34 | expect(parseException('Exception: ' + JSON.stringify(exception))).toEqual(exception) 35 | }) 36 | 37 | it('parses old style exception with trace', () => { 38 | const exception = { 39 | Exception: 'GenericException', 40 | // string instead of array 41 | Trace: '1 strstr\n2 /srv/www/index.php(2):main\n3 [builtin]:helper\n4 :function', 42 | } 43 | 44 | const modernException = { 45 | Exception: 'GenericException', 46 | Trace: [ 47 | { 48 | number: '1', 49 | function: 'strstr', 50 | file: false, 51 | }, 52 | { 53 | number: '2', 54 | file: '/srv/www/index.php', 55 | line: 2, 56 | function: 'main', 57 | }, 58 | { 59 | number: '3', 60 | file: '[builtin]', 61 | line: undefined, 62 | function: 'helper', 63 | }, 64 | { 65 | number: '4', 66 | file: '', 67 | line: undefined, 68 | function: 'function', 69 | }, 70 | ], 71 | } 72 | 73 | expect(parseException('Exception: ' + JSON.stringify(exception))).toEqual(modernException) 74 | }) 75 | 76 | it('parses nested json with unescaped newline', () => { 77 | const exception = { 78 | Exception: 'GenericException', 79 | Message: 'With\nnewline', 80 | } 81 | const input = 'Exception: {"Exception":"GenericException", "Message": "With\nnewline"}' 82 | 83 | expect(parseException(input)).toEqual(exception) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /.github/workflows/lint-php.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | matrix: 22 | runs-on: ubuntu-latest-low 23 | outputs: 24 | php-versions: ${{ steps.versions.outputs.php-versions }} 25 | steps: 26 | - name: Checkout app 27 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Get version matrix 32 | id: versions 33 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.0.0 34 | 35 | php-lint: 36 | runs-on: ubuntu-latest 37 | needs: matrix 38 | strategy: 39 | matrix: 40 | php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}} 41 | 42 | name: php-lint 43 | 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 47 | with: 48 | persist-credentials: false 49 | 50 | - name: Set up php ${{ matrix.php-versions }} 51 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5 52 | with: 53 | php-version: ${{ matrix.php-versions }} 54 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 55 | coverage: none 56 | ini-file: development 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | - name: Lint 61 | run: composer run lint 62 | 63 | summary: 64 | permissions: 65 | contents: none 66 | runs-on: ubuntu-latest-low 67 | needs: php-lint 68 | 69 | if: always() 70 | 71 | name: php-lint-summary 72 | 73 | steps: 74 | - name: Summary status 75 | run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi 76 | -------------------------------------------------------------------------------- /src/utils/logfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import type { ILogEntry, IRawLogEntry } from '../interfaces' 7 | 8 | import { parseException } from './exception' 9 | import { logger } from './logger' 10 | import { splitter } from './splitter' 11 | 12 | /** 13 | * Parse a given log file 14 | * 15 | * @param file The log file 16 | */ 17 | export async function parseLogFile(file: File): Promise { 18 | return parseLogString(await file.text()) 19 | } 20 | 21 | /** 22 | * Parse a given log file as string 23 | * 24 | * @param raw The raw log file content 25 | */ 26 | export async function parseLogString(raw: string): Promise { 27 | let entries: IRawLogEntry[] 28 | try { 29 | const lines = raw.split('\n') 30 | entries = lines.map(tryParseJSON) 31 | } catch (e) { 32 | logger.debug('falling back to json splitter') 33 | 34 | // the input might have had its data reformatted, breaking the original newline separated json 35 | const lines = splitter(raw).jsons 36 | entries = lines.map(tryParseJSON) 37 | } 38 | return entries.map(parseRawLogEntry) 39 | } 40 | 41 | /** 42 | * Parse a raw (unknown type of) log entry into a modern log entry 43 | * 44 | * @param entry The raw log entry 45 | */ 46 | export function parseRawLogEntry(entry: IRawLogEntry): ILogEntry { 47 | return { 48 | ...entry, 49 | exception: parseException((entry as ILogEntry).exception || entry.message), 50 | } as ILogEntry 51 | } 52 | 53 | /** 54 | * Try to parse a single log entry 55 | * 56 | * @param json raw log entry 57 | */ 58 | function tryParseJSON(json: string): IRawLogEntry { 59 | try { 60 | return JSON.parse(json) 61 | } catch (e) { 62 | logger.debug('Could not simply parse log entry', { error: e, json }) 63 | 64 | // Handle quoted log entries 65 | if (json.startsWith('"') && json.endsWith('"')) { 66 | let inner = json.substring(1, json.length - 1) 67 | 68 | // csv escaped quotes 69 | if (inner.match(/^\{\s*""/)) { 70 | inner = inner.replace(/""/g, '"') 71 | } 72 | return JSON.parse(inner) 73 | } 74 | 75 | // fix unescaped message json 76 | const startPos = json.indexOf('"message":"') + 11 77 | const endPos = json.lastIndexOf('","level":') 78 | const start = json.substring(0, startPos) 79 | const end = json.substring(endPos) 80 | const message = json.slice(startPos, endPos) 81 | 82 | const escapedMessage = message.replace(/([^\\]|^)["]/g, '$1\\"') 83 | json = start + escapedMessage + end 84 | 85 | return JSON.parse(json) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | # Log Reader 7 | 8 | [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/logreader)](https://api.reuse.software/info/github.com/nextcloud/logreader) 9 | 10 | ![screenshot](https://i.imgur.com/0Y9G8lS.png) 11 | 12 | Log reader for Nextcloud with clean exception display, infinite scrolling and more. 13 | 14 | ## Install instructions 15 | 16 | ### Installed by default 17 | 18 | Log Reader is installed by default in recent versions of Nextcloud so you don't have to do anything else to use the app. 19 | 20 | ### Install the latest stable release manually 21 | 22 | - Download the last pre-build [release](https://github.com/nextcloud/logreader/releases) 23 | - Extract the `tar.gz` into the apps folder 24 | 25 | ### Install from source 26 | 27 | - clone the repo in the apps folder 28 | - Run `make` in the `logreader` folder 29 | 30 | ## Developing 31 | 32 | For building the app `node` and `npm` are required 33 | 34 | ### Building 35 | 36 | Building the app can be done using the `Makefile` 37 | 38 | ``` 39 | make 40 | ``` 41 | 42 | ### Automatic rebuilding during development 43 | 44 | During development the webpack dev server can be used to automatically build the code 45 | for every change. 46 | 47 | Since the compiled source from the webpack dev server need to be injected in the regular Nextcloud 48 | sources a proxy setup is needed to combine things. 49 | 50 | If your local Nextcloud setup runs at http://localcloud an nginx configuration for the proxy 51 | would look like the following: 52 | 53 | ``` 54 | server { 55 | listen 81; 56 | server_name localcloud; 57 | 58 | location /apps/logreader/build/main.js { 59 | proxy_pass http://localhost:3000/build/main.js; 60 | } 61 | location /apps/logreader/build/main.css { 62 | return 404; 63 | } 64 | 65 | location /build { 66 | proxy_pass http://localhost:3000; 67 | } 68 | location /__webpack_hmr { 69 | proxy_set_header Host $host; 70 | proxy_pass http://localhost:3000; 71 | proxy_set_header Connection ''; 72 | proxy_http_version 1.1; 73 | chunked_transfer_encoding off; 74 | proxy_buffering off; 75 | proxy_cache off; 76 | } 77 | 78 | location / { 79 | proxy_set_header Host $host; 80 | proxy_hide_header Content-Security-Policy; 81 | proxy_pass http://localcloud/; 82 | } 83 | } 84 | 85 | ``` 86 | 87 | This will run the proxy at http://localcloud:81/ 88 | 89 | With the proxy configured you can start the webpack dev server and specify where the 90 | Nextcloud proxy is. 91 | 92 | ``` 93 | PROXY_URL="http://localcloud:81/ make watch 94 | ``` 95 | -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-FileCopyrightText: 2023 Marcel Klehr 8 | # SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> 9 | # SPDX-FileCopyrightText: 2023 Daniel Kesselberg 10 | # SPDX-FileCopyrightText: 2023 Florian Steffens 11 | # SPDX-License-Identifier: MIT 12 | 13 | name: 'Ask for feedback on PRs' 14 | on: 15 | schedule: 16 | - cron: '30 1 * * *' 17 | 18 | permissions: 19 | contents: read 20 | pull-requests: write 21 | 22 | jobs: 23 | pr-feedback: 24 | if: ${{ github.repository_owner == 'nextcloud' }} 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: The get-github-handles-from-website action 28 | uses: marcelklehr/get-github-handles-from-website-action@06b2239db0a48fe1484ba0bfd966a3ab81a08308 # v1.0.1 29 | id: scrape 30 | with: 31 | website: 'https://nextcloud.com/team/' 32 | 33 | - name: Get blocklist 34 | id: blocklist 35 | run: | 36 | blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) 37 | echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" 38 | 39 | - uses: nextcloud/pr-feedback-action@f0cab224dea8e1f282f9451de322f323c78fc7a5 # main 40 | with: 41 | feedback-message: | 42 | Hello there, 43 | Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. 44 | 45 | We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. 46 | 47 | Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 48 | 49 | Thank you for contributing to Nextcloud and we hope to hear from you soon! 50 | 51 | (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) 52 | days-before-feedback: 14 53 | start-date: '2025-06-12' 54 | exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' 55 | exempt-bots: true 56 | -------------------------------------------------------------------------------- /lib/SetupChecks/LogErrors.php: -------------------------------------------------------------------------------- 1 | l10n->t('Errors in the log'); 34 | } 35 | 36 | public function getCategory(): string { 37 | return 'system'; 38 | } 39 | 40 | public function run(): SetupResult { 41 | try { 42 | $logIterator = $this->logIteratorFactory->getLogIterator([self::LEVEL_WARNING,self::LEVEL_ERROR,self::LEVEL_FATAL]); 43 | } catch (\Exception $e) { 44 | if ($e instanceof UnsupportedLogTypeException) { 45 | return SetupResult::info($e->getMessage()); 46 | } 47 | 48 | return SetupResult::error( 49 | $this->l10n->t('Failed to get an iterator for log entries: %s', [$e->getMessage()]) 50 | ); 51 | } 52 | $count = [ 53 | self::LEVEL_WARNING => 0, 54 | self::LEVEL_ERROR => 0, 55 | self::LEVEL_FATAL => 0, 56 | ]; 57 | $limit = new \DateTime('7 days ago'); 58 | $startTime = microtime(true); 59 | foreach ($logIterator as $logItem) { 60 | if (!isset($logItem['time'])) { 61 | continue; 62 | } 63 | $time = \DateTime::createFromFormat(\DateTime::ATOM, $logItem['time']); 64 | if ($time < $limit) { 65 | break; 66 | } 67 | $count[$logItem['level']]++; 68 | if (microtime(true) > $startTime + 5) { 69 | $limit = $time; 70 | break; 71 | } 72 | } 73 | if (array_sum($count) === 0) { 74 | return SetupResult::success($this->l10n->t('No errors in the logs since %s', $this->dateFormatter->formatDateTime($limit))); 75 | } elseif ($count[self::LEVEL_ERROR] + $count[self::LEVEL_FATAL] > 0) { 76 | return SetupResult::warning( 77 | $this->l10n->n( 78 | '%n error in the logs since %s', 79 | '%n errors in the logs since %s', 80 | $count[self::LEVEL_ERROR] + $count[self::LEVEL_FATAL], 81 | [$this->dateFormatter->formatDateTime($limit)], 82 | ) 83 | ); 84 | } else { 85 | return SetupResult::info( 86 | $this->l10n->n( 87 | '%n warning in the logs since %s', 88 | '%n warnings in the logs since %s', 89 | $count[self::LEVEL_WARNING], 90 | [$this->dateFormatter->formatDateTime($limit)], 91 | ) 92 | ); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/Command/Tail.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 30 | $this->logIteratorFactory = $logIteratorFactory; 31 | } 32 | 33 | protected function configure() { 34 | $this 35 | ->setName('log:tail') 36 | ->setDescription('Tail the nextcloud logfile') 37 | ->addArgument('lines', InputArgument::OPTIONAL, 'The number of log entries to print', '10') 38 | ->addOption('follow', 'f', InputOption::VALUE_NONE, 'Output new log entries as they appear') 39 | ->addOption('raw', 'r', InputOption::VALUE_NONE, 'Output raw log json instead of formatted log item'); 40 | parent::configure(); 41 | } 42 | 43 | protected function execute(InputInterface $input, OutputInterface $output): int { 44 | $raw = $input->getOption('raw'); 45 | $count = (int)$input->getArgument('lines'); 46 | $io = new SymfonyStyle($input, $output); 47 | $logIterator = $this->logIteratorFactory->getLogIterator(Watch::ALL_LEVELS); 48 | $logIterator = new \LimitIterator($logIterator, 0, $count); 49 | $logItems = iterator_to_array($logIterator); 50 | $logItems = array_reverse($logItems); 51 | 52 | if ($raw) { 53 | foreach ($logItems as $logItem) { 54 | $output->writeln(json_encode($logItem)); 55 | } 56 | } else { 57 | $terminal = new Terminal(); 58 | $totalWidth = $terminal->getWidth(); 59 | // 8 level, 18 for app, 26 for time, 6 for formatting 60 | $messageWidth = $totalWidth - 8 - 18 - 26 - 6; 61 | 62 | $tableItems = array_map(function (array $logItem) use ($messageWidth) { 63 | return [ 64 | self::LEVELS[$logItem['level']], 65 | wordwrap($logItem['app'], 18), 66 | $this->formatter->formatMessage($logItem, $messageWidth) . "\n", 67 | $logItem['time'], 68 | ]; 69 | }, $logItems); 70 | $io->table([ 71 | 'Level', 72 | 'App', 73 | 'Message', 74 | 'Time', 75 | ], $tableItems); 76 | } 77 | 78 | if ($input->getOption('follow')) { 79 | $watch = new Watch($this->formatter, $this->logIteratorFactory); 80 | $watch->watch($raw, $output); 81 | } 82 | 83 | return 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import type { AxiosError, AxiosRequestConfig, AxiosResponse } from '@nextcloud/axios' 7 | import type { IAppSettings, INextcloud22LogEntry } from './interfaces' 8 | 9 | import axios from '@nextcloud/axios' 10 | import { generateUrl } from '@nextcloud/router' 11 | 12 | interface ApiGetLog { 13 | offset?: number 14 | count?: number 15 | query?: string 16 | } 17 | 18 | interface ApiPollLog { 19 | lastReqId: string 20 | } 21 | 22 | interface ApiLogResult { 23 | /** New entries */ 24 | data: readonly INextcloud22LogEntry[] 25 | /** True if more entries are available */ 26 | remain: boolean 27 | } 28 | 29 | type ApiPollLogResult = readonly INextcloud22LogEntry[] 30 | 31 | type IAppSettingsKey = keyof IAppSettings 32 | 33 | interface ApiSetAppSetting { 34 | settingsKey: I 35 | settingsValue: IAppSettings[I] 36 | } 37 | 38 | type ApiGetAppSettings = never 39 | 40 | /** 41 | * Fetch log entries from server 42 | * 43 | * @param data Parameters for request 44 | * @param config Axios config for setting data 45 | * @return Array of fetched log entries 46 | * @throws {AxiosError} with HTTP status 424 if log type is not set to `file` 47 | */ 48 | export const getLog = (data: ApiGetLog, config: AxiosRequestConfig = {}) => axios.get>(generateUrl('apps/logreader/api/log'), { ...config, params: data }) as Promise> 49 | 50 | /** 51 | * Fetch log entries from server 52 | * 53 | * @param data Parameters for request 54 | * @param config Axios config for setting data 55 | * @return Array of fetched log entries 56 | * @throws {AxiosError} with HTTP status 424 if log type is not set to `file` 57 | */ 58 | export const pollLog = (data: ApiPollLog, config: AxiosRequestConfig = {}) => axios.get>(generateUrl('apps/logreader/api/poll'), { ...config, params: data }) as Promise> 59 | 60 | /** 61 | * Change an app setting value 62 | * 63 | * @param data Parameters for request 64 | * @param config Axios config for setting data 65 | * @return 66 | */ 67 | export const setAppSetting = (data: ApiSetAppSetting, config: AxiosRequestConfig> = {}) => axios.put>>(generateUrl('apps/logreader/api/settings'), data, config) 68 | 69 | /** 70 | * Get current app settings 71 | * 72 | * @param data Request parameters 73 | * @param config Optional Axios request config 74 | * @return The current app config 75 | */ 76 | export const getAppSettings = (data?: ApiGetAppSettings, config: AxiosRequestConfig = {}) => axios.get>(generateUrl('apps/logreader/api/settings'), { ...config, params: data }) 77 | -------------------------------------------------------------------------------- /.github/workflows/npm-audit-fix.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Npm audit fix and compile 10 | 11 | on: 12 | workflow_dispatch: 13 | schedule: 14 | # At 2:30 on Sundays 15 | - cron: '30 2 * * 0' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | branches: 28 | - ${{ github.event.repository.default_branch }} 29 | - 'stable32' 30 | - 'stable31' 31 | 32 | name: npm-audit-fix-${{ matrix.branches }} 33 | 34 | steps: 35 | - name: Checkout 36 | id: checkout 37 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 38 | with: 39 | persist-credentials: false 40 | ref: ${{ matrix.branches }} 41 | continue-on-error: true 42 | 43 | - name: Read package.json node and npm engines version 44 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 45 | id: versions 46 | with: 47 | fallbackNode: '^24' 48 | fallbackNpm: '^11.3' 49 | 50 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 51 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 52 | with: 53 | node-version: ${{ steps.versions.outputs.nodeVersion }} 54 | 55 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 56 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 57 | 58 | - name: Fix npm audit 59 | id: npm-audit 60 | uses: nextcloud-libraries/npm-audit-action@1b1728b2b4a7a78d69de65608efcf4db0e3e42d0 # v0.2.0 61 | 62 | - name: Run npm ci and npm run build 63 | if: steps.checkout.outcome == 'success' 64 | env: 65 | CYPRESS_INSTALL_BINARY: 0 66 | run: | 67 | npm ci 68 | npm run build --if-present 69 | 70 | - name: Create Pull Request 71 | if: steps.checkout.outcome == 'success' 72 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 73 | with: 74 | token: ${{ secrets.COMMAND_BOT_PAT }} 75 | commit-message: 'fix(deps): Fix npm audit' 76 | committer: GitHub 77 | author: nextcloud-command 78 | signoff: true 79 | branch: automated/noid/${{ matrix.branches }}-fix-npm-audit 80 | title: '[${{ matrix.branches }}] Fix npm audit' 81 | body: ${{ steps.npm-audit.outputs.markdown }} 82 | labels: | 83 | dependencies 84 | 3. to review 85 | -------------------------------------------------------------------------------- /lib/Service/SettingsService.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | } 19 | 20 | /** 21 | * Load shown levels from app config 22 | */ 23 | public function getShownLevels(): array { 24 | return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_SHOWNLEVELS, '[0,1,2,3,4]'), flags: JSON_THROW_ON_ERROR); 25 | } 26 | 27 | /** 28 | * Load date time format to use for user from app config 29 | */ 30 | public function getDateTimeFormat(): string { 31 | return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_DATETIMEFORMAT, '"local"'), flags: JSON_THROW_ON_ERROR); 32 | } 33 | 34 | /** 35 | * Load app config if dates should be displayed as relative dates 36 | */ 37 | public function getRelativeDates(): bool { 38 | return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_RELATIVEDATES, 'false') ?: 'false', flags: JSON_THROW_ON_ERROR); 39 | } 40 | 41 | /** 42 | * Load app config if log should be updated automatically 43 | */ 44 | public function getLiveLog(): bool { 45 | return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_LIVELOG, 'true'), flags: JSON_THROW_ON_ERROR); 46 | } 47 | 48 | /** 49 | * Get all app settings for displaying the logfiles 50 | */ 51 | public function getAppSettings(): array { 52 | return [ 53 | Constants::CONFIG_KEY_SHOWNLEVELS => $this->getShownLevels(), 54 | Constants::CONFIG_KEY_LOGLEVEL => $this->config->getSystemValueInt('loglevel', 2), 55 | Constants::CONFIG_KEY_DATETIMEFORMAT => $this->getDateTimeFormat(), 56 | Constants::CONFIG_KEY_RELATIVEDATES => $this->getRelativeDates(), 57 | Constants::CONFIG_KEY_LIVELOG => $this->getLiveLog(), 58 | 'enabled' => $this->getLoggingType() === 'file', 59 | ]; 60 | } 61 | 62 | /** 63 | * Get system setting of the logging type 64 | */ 65 | public function getLoggingType(): string { 66 | return $this->config->getSystemValueString('log_type', 'file'); 67 | } 68 | 69 | /** 70 | * Get system setting of the log file name 71 | */ 72 | public function getLoggingFile(): string { 73 | return $this->config->getSystemValueString('logile', ''); 74 | } 75 | 76 | /** 77 | * Get system setting for the log date format 78 | */ 79 | public function getLoggingDateFormat(): string { 80 | // see default: https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/logging_configuration.html#file 81 | return $this->config->getSystemValueString('logdateformat', 'c'); 82 | } 83 | 84 | /** 85 | * Get system setting for the log timezone 86 | */ 87 | public function getLoggingTimezone(): string { 88 | return $this->config->getSystemValueString('logtimezone', 'UTC'); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/LogSearch.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 30 | 31 | 101 | -------------------------------------------------------------------------------- /l10n/sl.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Logging" : "Dnevnik beleženja", 3 | "Errors in the log" : "Napake v dnevniških zapisih", 4 | "No errors in the logs since %s" : "V dnevniških zapisih ni napak od %s", 5 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["V dnevniških zapisih je zabeležena %n napaka (%s je datum prve)","V dnevniških zapisih sta zabeleženi %n napaki (%s je datum prve)","V dnevniških zapisih je zabeležene %n napake (%s je datum prve)","V dnevniških zapisih je zabeleženih %n napak (%s je datum prve)"], 6 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["V dnevniških zapisih je zabeleženo %n opozorilo (%s je datum prvega)","V dnevniških zapisih sta zabeleženi %n opozorili (%s je datum prvega)","V dnevniških zapisih so zabeležena %n opozorila (%s je datum prvega)","V dnevniških zapisih je zabeleženih %n opozoril (%s je datum prvega)"], 7 | "Log Reader" : "Bralnik dnevnikov", 8 | "A log reader for Nextcloud" : "Bralnik dnevnikov za Nextcloud", 9 | "Log reader for Nextcloud" : "Bralnik dnevnikov za Nextcloud", 10 | "Log reader" : "Bralnik dnevnikov", 11 | "Open log reader settings" : "Odpri nastavitve bralnika dnevnikov", 12 | "Log reader settings" : "Nastavitve bralnika dnevnikov", 13 | "Show server log" : "Pokaži dnevnik strežnika", 14 | "Live view is disabled" : "Pogled v živo je onemogočen", 15 | "No log file" : "Ni datoteke nevniškega zapisa", 16 | "Level" : "Raven", 17 | "App" : "Program", 18 | "Time" : "Čas", 19 | "Copy raw entry" : "Kopiraj neoblikovan vpis", 20 | "Copy formatted entry" : "Kopiraj oblikovan vpis", 21 | "Search log entries" : "Preišči dnevniške vnose", 22 | "Line {line}" : "Vrstica {line}", 23 | "Live view" : "Pogled v živo", 24 | "Ctrl" : "CTRL", 25 | "Download logs" : "Prejem dnevnikov", 26 | "Raw data" : "Surovi podatki", 27 | "Local time" : "Krajevni čas", 28 | "UTC time" : "Čas v sistemu UTC", 29 | "Relative" : "Relativno", 30 | "Filter logging levels" : "Filtriraj ravni beleženja", 31 | "Application" : "Program", 32 | "Message" : "Sporočilo", 33 | "Collapse row" : "Zloži vrstico", 34 | "Expand row" : "Razširi vrstico", 35 | "Show details" : "Pokaži podrobnosti", 36 | "Debug" : "Razhroščevanje", 37 | "Info" : "Podrobnosti", 38 | "Warning" : "Opozorilo", 39 | "Error" : "Napaka", 40 | "Fatal" : "Usodna napaka", 41 | "Could not load log entries" : "Ni mogoče naložiti vnosov dnevnika", 42 | "Clipboard" : "Odložišče", 43 | "Could not parse clipboard content" : "Ni mogoče razčleniti vsebine datoteke", 44 | "Could not fetch new log entries (server unavailable)" : "Ni mogoče pridobiti novih vnosov (strežnik ni na voljo)", 45 | "Could not fetch new entries" : "Ni mogoče pridobiti novih vnosov", 46 | "Could not copy to clipboard, please copy manually:" : "Kopiranje v odložišče ni mogoče, poskusite kopirati ročno:", 47 | "\tfrom {address} by {user} at {time}\n" : "\tz naslova {address}, uporabnik {user} ob {time}\n" 48 | },"pluralForm" :"nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" 49 | } -------------------------------------------------------------------------------- /l10n/sl.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "logreader", 3 | { 4 | "Logging" : "Dnevnik beleženja", 5 | "Errors in the log" : "Napake v dnevniških zapisih", 6 | "No errors in the logs since %s" : "V dnevniških zapisih ni napak od %s", 7 | "_%n error in the logs since %s_::_%n errors in the logs since %s_" : ["V dnevniških zapisih je zabeležena %n napaka (%s je datum prve)","V dnevniških zapisih sta zabeleženi %n napaki (%s je datum prve)","V dnevniških zapisih je zabeležene %n napake (%s je datum prve)","V dnevniških zapisih je zabeleženih %n napak (%s je datum prve)"], 8 | "_%n warning in the logs since %s_::_%n warnings in the logs since %s_" : ["V dnevniških zapisih je zabeleženo %n opozorilo (%s je datum prvega)","V dnevniških zapisih sta zabeleženi %n opozorili (%s je datum prvega)","V dnevniških zapisih so zabeležena %n opozorila (%s je datum prvega)","V dnevniških zapisih je zabeleženih %n opozoril (%s je datum prvega)"], 9 | "Log Reader" : "Bralnik dnevnikov", 10 | "A log reader for Nextcloud" : "Bralnik dnevnikov za Nextcloud", 11 | "Log reader for Nextcloud" : "Bralnik dnevnikov za Nextcloud", 12 | "Log reader" : "Bralnik dnevnikov", 13 | "Open log reader settings" : "Odpri nastavitve bralnika dnevnikov", 14 | "Log reader settings" : "Nastavitve bralnika dnevnikov", 15 | "Show server log" : "Pokaži dnevnik strežnika", 16 | "Live view is disabled" : "Pogled v živo je onemogočen", 17 | "No log file" : "Ni datoteke nevniškega zapisa", 18 | "Level" : "Raven", 19 | "App" : "Program", 20 | "Time" : "Čas", 21 | "Copy raw entry" : "Kopiraj neoblikovan vpis", 22 | "Copy formatted entry" : "Kopiraj oblikovan vpis", 23 | "Search log entries" : "Preišči dnevniške vnose", 24 | "Line {line}" : "Vrstica {line}", 25 | "Live view" : "Pogled v živo", 26 | "Ctrl" : "CTRL", 27 | "Download logs" : "Prejem dnevnikov", 28 | "Raw data" : "Surovi podatki", 29 | "Local time" : "Krajevni čas", 30 | "UTC time" : "Čas v sistemu UTC", 31 | "Relative" : "Relativno", 32 | "Filter logging levels" : "Filtriraj ravni beleženja", 33 | "Application" : "Program", 34 | "Message" : "Sporočilo", 35 | "Collapse row" : "Zloži vrstico", 36 | "Expand row" : "Razširi vrstico", 37 | "Show details" : "Pokaži podrobnosti", 38 | "Debug" : "Razhroščevanje", 39 | "Info" : "Podrobnosti", 40 | "Warning" : "Opozorilo", 41 | "Error" : "Napaka", 42 | "Fatal" : "Usodna napaka", 43 | "Could not load log entries" : "Ni mogoče naložiti vnosov dnevnika", 44 | "Clipboard" : "Odložišče", 45 | "Could not parse clipboard content" : "Ni mogoče razčleniti vsebine datoteke", 46 | "Could not fetch new log entries (server unavailable)" : "Ni mogoče pridobiti novih vnosov (strežnik ni na voljo)", 47 | "Could not fetch new entries" : "Ni mogoče pridobiti novih vnosov", 48 | "Could not copy to clipboard, please copy manually:" : "Kopiranje v odložišče ni mogoče, poskusite kopirati ročno:", 49 | "\tfrom {address} by {user} at {time}\n" : "\tz naslova {address}, uporabnik {user} ob {time}\n" 50 | }, 51 | "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"); 52 | -------------------------------------------------------------------------------- /src/components/settings/SettingsActions.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 35 | 36 | 83 | 84 | 95 | -------------------------------------------------------------------------------- /.github/workflows/lint-eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint eslint 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-eslint-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | changes: 22 | runs-on: ubuntu-latest-low 23 | permissions: 24 | contents: read 25 | pull-requests: read 26 | 27 | outputs: 28 | src: ${{ steps.changes.outputs.src}} 29 | 30 | steps: 31 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 32 | id: changes 33 | continue-on-error: true 34 | with: 35 | filters: | 36 | src: 37 | - '.github/workflows/**' 38 | - 'src/**' 39 | - 'appinfo/info.xml' 40 | - 'package.json' 41 | - 'package-lock.json' 42 | - 'tsconfig.json' 43 | - '.eslintrc.*' 44 | - '.eslintignore' 45 | - '**.js' 46 | - '**.ts' 47 | - '**.vue' 48 | 49 | lint: 50 | runs-on: ubuntu-latest 51 | 52 | needs: changes 53 | if: needs.changes.outputs.src != 'false' 54 | 55 | name: NPM lint 56 | 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 60 | with: 61 | persist-credentials: false 62 | 63 | - name: Read package.json node and npm engines version 64 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 65 | id: versions 66 | with: 67 | fallbackNode: '^24' 68 | fallbackNpm: '^11.3' 69 | 70 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 71 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 72 | with: 73 | node-version: ${{ steps.versions.outputs.nodeVersion }} 74 | 75 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 76 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 77 | 78 | - name: Install dependencies 79 | env: 80 | CYPRESS_INSTALL_BINARY: 0 81 | PUPPETEER_SKIP_DOWNLOAD: true 82 | run: npm ci 83 | 84 | - name: Lint 85 | run: npm run lint 86 | 87 | summary: 88 | permissions: 89 | contents: none 90 | runs-on: ubuntu-latest-low 91 | needs: [changes, lint] 92 | 93 | if: always() 94 | 95 | # This is the summary, we just avoid to rename it so that branch protection rules still match 96 | name: eslint 97 | 98 | steps: 99 | - name: Summary status 100 | run: if ${{ needs.changes.outputs.src != 'false' && needs.lint.result != 'success' }}; then exit 1; fi 101 | -------------------------------------------------------------------------------- /src/components/settings/SettingsDatetimeFormat.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 46 | 47 | 94 | 95 | 100 | -------------------------------------------------------------------------------- /src/components/table/LogTableHeader.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 29 | 30 | 108 | 109 | 135 | -------------------------------------------------------------------------------- /src/api.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | import { afterEach, describe, expect, it, vi } from 'vitest' 7 | import { getAppSettings, getLog, pollLog, setAppSetting } from './api' 8 | 9 | const mocks = vi.hoisted(() => { 10 | return { 11 | get: vi.fn(), 12 | put: vi.fn(), 13 | } 14 | }) 15 | 16 | vi.mock('@nextcloud/axios', () => { 17 | return { 18 | default: { 19 | get: mocks.get, 20 | put: mocks.put, 21 | }, 22 | } 23 | }) 24 | 25 | vi.mock('@nextcloud/router', () => ({ 26 | generateUrl: (path: string) => `/index.php/${path}`, 27 | })) 28 | 29 | describe('api', () => { 30 | afterEach(() => { 31 | vi.restoreAllMocks() 32 | }) 33 | 34 | it('getLog', async () => { 35 | const data = { count: 1 } 36 | 37 | await getLog(data) 38 | 39 | expect(mocks.get).toBeCalled() 40 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/log', { params: data }) 41 | }) 42 | 43 | it('getLog with config', async () => { 44 | const data = { count: 1 } 45 | const config = { headers: { Accept: 'application/json' } } 46 | 47 | await getLog(data, config) 48 | 49 | expect(mocks.get).toBeCalled() 50 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/log', { ...config, params: data }) 51 | }) 52 | 53 | it('pollLog', async () => { 54 | const data = { lastReqId: '1234' } 55 | 56 | await pollLog(data) 57 | 58 | expect(mocks.get).toBeCalled() 59 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/poll', { params: data }) 60 | }) 61 | 62 | it('pollLog with config', async () => { 63 | const data = { lastReqId: '1234' } 64 | const config = { headers: { Accept: 'application/json' } } 65 | 66 | await pollLog(data, config) 67 | 68 | expect(mocks.get).toBeCalled() 69 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/poll', { ...config, params: data }) 70 | }) 71 | 72 | it('getAppSettings', async () => { 73 | await getAppSettings() 74 | 75 | expect(mocks.get).toBeCalled() 76 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/settings', {}) 77 | }) 78 | 79 | it('getAppSettings with config', async () => { 80 | const config = { headers: { Accept: 'application/json' } } 81 | 82 | await getAppSettings(undefined, config) 83 | 84 | expect(mocks.get).toBeCalled() 85 | expect(mocks.get).toBeCalledWith('/index.php/apps/logreader/api/settings', { ...config, params: undefined }) 86 | }) 87 | 88 | it('setAppSetting', async () => { 89 | const data = { settingsKey: 'liveLog' as const, settingsValue: true } 90 | 91 | await setAppSetting<'liveLog'>(data) 92 | 93 | expect(mocks.put).toBeCalled() 94 | expect(mocks.put).toBeCalledWith('/index.php/apps/logreader/api/settings', data, {}) 95 | }) 96 | 97 | it('setAppSetting with config', async () => { 98 | const data = { settingsKey: 'liveLog' as const, settingsValue: true } 99 | const config = { headers: { Accept: 'application/json' } } 100 | 101 | await setAppSetting<'liveLog'>(data, config) 102 | 103 | expect(mocks.put).toBeCalled() 104 | expect(mocks.put).toBeCalledWith('/index.php/apps/logreader/api/settings', data, config) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /src/interfaces/ILogEntry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SPDX-FileCopyrightText: 2023 Nextcloud Gmbh and Nextcloud contributors 3 | * SPDX-License-Identifier: AGPL-3.0-or-later 4 | */ 5 | 6 | /** 7 | * Trace entry for exceptions 8 | */ 9 | export interface ITraceLine { 10 | /** 11 | * The file in which the function was called 12 | */ 13 | file: string 14 | /** 15 | * Line that was currently parsed when the exception occured 16 | */ 17 | line?: number 18 | /** 19 | * Called function name 20 | */ 21 | function: string 22 | /** 23 | * Class of the function that was called 24 | */ 25 | class: string 26 | /** 27 | * Function arguments (might be a placeholder when sensitive value) 28 | */ 29 | args?: readonly string[] 30 | /** 31 | * Type of function access, e.g. '->' or '::' 32 | */ 33 | type: string 34 | } 35 | 36 | /** 37 | * Exception entry for log entries 38 | */ 39 | export interface IException { 40 | Code: number 41 | CustomMessage: string 42 | Exception: string 43 | File: string 44 | Line: number 45 | Message: string 46 | Trace: readonly ITraceLine[] 47 | Previous?: this 48 | } 49 | 50 | /** 51 | * Common base format of a Nextcloud log entry 52 | */ 53 | interface IBaseLogEntry { 54 | /** Request ID: any log lines related to a single request have the same value */ 55 | reqId: string 56 | /** Logged incident’s level */ 57 | level: number 58 | /** Affected app */ 59 | app: string 60 | /** Date and time (format and timezone can be configured in config.php) */ 61 | time: string 62 | /** Event information message */ 63 | message: string 64 | /** Nextcloud version at the time of request */ 65 | version: string 66 | /** The IP address of the user (if applicable) */ 67 | remoteAddr?: string 68 | /** Acting user's id (if applicable) */ 69 | user?: string 70 | /** HTTP method, for example GET, POST, PROPFIND, etc. – empty on occ calls */ 71 | method?: string 72 | /** Request path (if applicable – empty on occ calls) */ 73 | url?: string 74 | /** User agent (if applicable – empty on occ calls) */ 75 | userAgent?: string 76 | /** Additional structured data (if applicable) */ 77 | data?: string // if parsed: Record 78 | } 79 | 80 | /** 81 | * Nextcloud 14 version of a log entry, previously the exception was simply a string within the `message` property 82 | */ 83 | export interface INextcloud14LogEntry extends Omit { 84 | /** Event information message or exception */ 85 | message: string | IException 86 | } 87 | 88 | /** 89 | * Format of a log entry introduced with Nextcloud 22 90 | */ 91 | export interface INextcloud22LogEntry extends IBaseLogEntry { 92 | /** Full exception with trace (if applicable) */ 93 | exception?: string | IException 94 | } 95 | 96 | /** 97 | * Raw log entry from log source without fixing the exception 98 | */ 99 | export type IRawLogEntry = INextcloud14LogEntry | INextcloud22LogEntry 100 | 101 | /** 102 | * Fixed version of the log entry where the exception has its own field of type IException 103 | */ 104 | export interface ILogEntry extends Omit { 105 | /** Unique ID, appended to each iterator element (see LogController#poll) */ 106 | id: string 107 | /** Full exception with trace (if applicable) */ 108 | exception?: IException 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/psalm-matrix.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Static analysis 10 | 11 | on: pull_request 12 | 13 | concurrency: 14 | group: psalm-${{ github.head_ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | matrix: 22 | runs-on: ubuntu-latest-low 23 | outputs: 24 | ocp-matrix: ${{ steps.versions.outputs.ocp-matrix }} 25 | steps: 26 | - name: Checkout app 27 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Get version matrix 32 | id: versions 33 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 34 | 35 | - name: Check enforcement of minimum PHP version ${{ steps.versions.outputs.php-min }} in psalm.xml 36 | run: grep 'phpVersion="${{ steps.versions.outputs.php-min }}' psalm.xml 37 | 38 | static-analysis: 39 | runs-on: ubuntu-latest 40 | needs: matrix 41 | strategy: 42 | # do not stop on another job's failure 43 | fail-fast: false 44 | matrix: ${{ fromJson(needs.matrix.outputs.ocp-matrix) }} 45 | 46 | name: static-psalm-analysis ${{ matrix.ocp-version }} 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 50 | with: 51 | persist-credentials: false 52 | 53 | - name: Set up php${{ matrix.php-min }} 54 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # v2.35.5 55 | with: 56 | php-version: ${{ matrix.php-min }} 57 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 58 | coverage: none 59 | ini-file: development 60 | # Temporary workaround for missing pcntl_* in PHP 8.3 61 | ini-values: disable_functions= 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | 65 | - name: Install dependencies 66 | run: | 67 | composer remove nextcloud/ocp --dev --no-scripts 68 | composer i 69 | 70 | - name: Check for vulnerable PHP dependencies 71 | run: composer require --dev roave/security-advisories:dev-latest 72 | 73 | - name: Install dependencies # zizmor: ignore[template-injection] 74 | run: composer require --dev 'nextcloud/ocp:${{ matrix.ocp-version }}' --ignore-platform-reqs --with-dependencies 75 | 76 | - name: Run coding standards check 77 | run: composer run psalm -- --threads=1 --monochrome --no-progress --output-format=github 78 | 79 | summary: 80 | runs-on: ubuntu-latest-low 81 | needs: static-analysis 82 | 83 | if: always() 84 | 85 | name: static-psalm-analysis-summary 86 | 87 | steps: 88 | - name: Summary status 89 | run: if ${{ needs.static-analysis.result != 'success' }}; then exit 1; fi 90 | -------------------------------------------------------------------------------- /lib/Controller/SettingsController.php: -------------------------------------------------------------------------------- 1 | settingsService->getAppSettings()); 40 | } 41 | 42 | /** 43 | * Update values on the app config. 44 | * 45 | * @param string $settingsKey AppConfig Key to store 46 | * @param mixed $settingsValues Corresponding AppConfig Value 47 | * 48 | */ 49 | #[AuthorizedAdminSetting(settings: Admin::class)] 50 | public function updateAppConfig(string $settingsKey, $settingsValue): JSONResponse { 51 | $this->logger->debug('Updating AppConfig: {settingsKey} => {settingsValue}', [ 52 | 'settingsKey' => $settingsKey, 53 | 'settingsValue' => $settingsValue 54 | ]); 55 | 56 | // Check for allowed keys 57 | if (!in_array($settingsKey, Constants::CONFIG_KEYS)) { 58 | $this->logger->debug('Unknown appConfig key: ' . $settingsKey); 59 | return new JSONResponse([], Http::STATUS_BAD_REQUEST); 60 | } 61 | 62 | // Check type of value 63 | if (gettype($settingsValue) !== gettype($this->settingsService->getAppSettings()[$settingsKey])) { 64 | // Invalid type 65 | $this->logger->debug('Incorrect value type for appConfig key', ['settingsKey' => $settingsKey, 'valueType' => gettype($settingsValue)]); 66 | return new JSONResponse([], Http::STATUS_BAD_REQUEST); 67 | } 68 | 69 | if ($settingsKey === Constants::CONFIG_KEY_SHOWNLEVELS) { 70 | foreach ($settingsValue as $value) { 71 | if (!is_integer($value) || !in_array($value, Constants::LOGGING_LEVELS)) { 72 | $this->logger->debug('Invalid logging level given', ['value' => $value ]); 73 | return new JSONResponse([], Http::STATUS_BAD_REQUEST); 74 | } 75 | } 76 | } 77 | 78 | if ($settingsKey === Constants::CONFIG_KEY_LOGLEVEL) { 79 | // Validate loglevel value 80 | if (!is_int($settingsValue) || $settingsValue < 0 || $settingsValue > 4) { 81 | $this->logger->debug('Cannot set {settingsValue} as loglevel', ['settingsValue' => $settingsValue ]); 82 | return new JSONResponse([], Http::STATUS_BAD_REQUEST); 83 | } 84 | // Set backend loglevel directly via system value 85 | $this->config->setSystemValue('loglevel', $settingsValue); 86 | } else { 87 | // Set on DB 88 | $this->config->setAppValue($this->appName, $settingsKey, json_encode($settingsValue)); 89 | } 90 | 91 | return new JSONResponse(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/Log/LogIterator.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class LogIterator implements \Iterator { 14 | /** 15 | * @var resource 16 | */ 17 | private $handle; 18 | private string $dateFormat; 19 | private \DateTimeZone $timezone; 20 | 21 | private string $buffer = ''; 22 | private string $lastLine = ''; 23 | private int $position = 0; 24 | private int $currentKey = -1; 25 | 26 | public const CHUNK_SIZE = 8192; // how many chars do we try at once to find a new line 27 | 28 | /** 29 | * @param resource $handle 30 | * @param string $dateFormat 31 | * @param string $timezone 32 | */ 33 | public function __construct($handle, string $dateFormat, string $timezone) { 34 | $this->handle = $handle; 35 | $this->dateFormat = $dateFormat; 36 | $this->timezone = new \DateTimeZone($timezone); 37 | $this->rewind(); 38 | } 39 | 40 | public function rewind(): void { 41 | fseek($this->handle, 0, SEEK_END); 42 | $this->position = ftell($this->handle); 43 | $this->lastLine = ''; 44 | $this->buffer = ''; 45 | $this->currentKey = -1; 46 | $this->next(); 47 | } 48 | 49 | /** 50 | * @return array 51 | */ 52 | #[\ReturnTypeWillChange] 53 | public function current() { 54 | $entry = json_decode($this->lastLine, true); 55 | if ($this->dateFormat !== \DateTime::ATOM) { 56 | if (isset($entry['time'])) { 57 | $time = \DateTime::createFromFormat($this->dateFormat, $entry['time'], $this->timezone); 58 | if ($time) { 59 | $entry['time'] = $time->format(\DateTime::ATOM); 60 | } 61 | } 62 | } 63 | return $entry; 64 | } 65 | 66 | public function key(): int { 67 | return $this->currentKey; 68 | } 69 | 70 | private function fillBuffer(): void { 71 | $chunkSize = min($this->position, self::CHUNK_SIZE); 72 | $this->position -= $chunkSize; 73 | fseek($this->handle, $this->position); 74 | $chunk = fread($this->handle, $chunkSize); 75 | $this->buffer = $chunk . $this->buffer; 76 | } 77 | 78 | public function next(): void { 79 | // Loop through each character of the file looking for new lines 80 | while ($this->position >= 0) { 81 | $newlinePos = strrpos($this->buffer, "\n"); 82 | if ($newlinePos !== false) { 83 | if ($newlinePos + 1 === strlen($this->buffer)) { 84 | // try again with truncated buffer if it ends with newline, i.e. on first call 85 | $this->buffer = substr($this->buffer, 0, $newlinePos); 86 | continue; 87 | } 88 | $this->lastLine = substr($this->buffer, $newlinePos + 1); 89 | $this->buffer = substr($this->buffer, 0, $newlinePos); 90 | $this->currentKey++; 91 | return; 92 | } elseif ($this->position === 0) { 93 | $this->lastLine = $this->buffer; 94 | $this->buffer = ''; 95 | $this->currentKey++; 96 | return; 97 | } else { 98 | $this->fillBuffer(); 99 | } 100 | } 101 | } 102 | 103 | public function valid(): bool { 104 | if (!is_resource($this->handle)) { 105 | return false; 106 | } 107 | 108 | if ($this->lastLine === '' && $this->position === 0) { 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | } 115 | --------------------------------------------------------------------------------