21 Commits

Author SHA1 Message Date
dillard a5f5c5cdb7 Fix code-review findings: ajax error handling, translated comment, docs
- js/index.js: the occ-command ajax call used the wrong jQuery .fail()
  signature ((response, code) instead of (jqXHR, textStatus,
  errorThrown)), so any failure printed the literal string
  "[object Object]" instead of a useful message.
- js/index.js: the initial bootstrap call that builds the terminal had
  no .fail() handler at all - a failure there (expired session/CSRF
  token, network error, rejected admin check) left #app-content
  permanently blank with zero diagnostic information. This is exactly
  what made today's NC34 debugging session harder than it needed to
  be; now it shows the actual HTTP status/reason in the page.
- OccController.php: translated a leftover Dutch comment (missed by
  the earlier Russian->English pass since it wasn't Russian), and
  documented why requireAdmin() is intentional defense-in-depth rather
  than a misunderstanding of the framework: verified against
  Nextcloud's own SecurityMiddleware source that any controller method
  without @NoAdminRequired already rejects non-admins by default, so
  this check (and the original "security fix" commit that added it)
  was addressing a non-issue for THIS controller specifically.
  DbController's equivalent check is still genuinely load-bearing -
  that controller originally shipped with @PublicPage, which does
  bypass the default protection - so it gets a note explaining the
  distinction instead.
- README: added compatibility matrix rows for 0.4.0 (app id rename to
  extended_occweb) and 0.4.1 (these fixes), both verified on NC 34.0.4.

Deferred to when a test instance exists (tracked, not fixed here): no
documented migration path from the old occweb id to extended_occweb
for existing installs, OccController's constructor doing expensive
work before the (redundant) admin check runs, and js/jquery.js loading
unconditionally on NC30-33 where core still provides jQuery.

Version bump to 0.4.1.
2026-09-26 16:04:14 +02:00
dillard 221ea8a249 Shorten navigation menu label to "OCC Web" (was "OCCWeb")
Full app name stays "Extended OCC Web" for the App Store/apps
management page - only the top-nav menu entry needed a shorter,
readable label.
2026-09-26 15:55:10 +02:00
dillard 138ee88486 Rename app id from occweb to extended_occweb, prep for App Store
Nextcloud's app id "occweb" is effectively unavailable for a new store
listing: the original Adphi/occweb still owns that App Store slot
(stale, v0.2.3), and el-choco/occweb_v2 has since published a
same-lineage rewrite under its own id. Rather than fight over the
"occweb" name, publish this fork under its own id, matching the
<name> this app has always had: "Extended OCC Web".

Renames every place the literal app id is used: info.xml <id> and the
auto-generated navigation <route>, script()/style() registrations in
templates/index.php, the TemplateResponse app name in
OccController::index(), the /apps/... base URL in js/index.js, and the
'app' log context tag in DbController.php. The GitHub repo name and
its <website>/<bugs>/<repository>/<screenshot> links are unaffected -
those identify the repo, not the Nextcloud app.

Docs (README, INSTALL-AIO.md, install.sh) updated to clone into and
reference an "extended_occweb" directory, since Nextcloud requires the
app folder name to match its id - a plain `git clone` of this repo no
longer produces a correctly-named install directory on its own.

Version bump to 0.4.0 given the scope (breaking change for anyone who
already deployed under the old "occweb" id - requires disable+remove
of the old app and a fresh enable under the new id, not an in-place
upgrade).
2026-09-26 12:51:21 +02:00
dillard f091cc8c0b Add NC-version compatibility matrix to README 2026-09-26 12:27:49 +02:00
dillard 2872d70653 Fix NC34 compatibility: vendor jQuery, send CSRF token explicitly on ajax calls
Live-tested against nc01 after its auto-update jumped to NC 34.0.4:

- Nextcloud's global jQuery has been deprecated since NC19 and is no
  longer reliably present/first-loaded on NC34 (other apps like
  serverinfo hit the same issue). occweb's index.js and the vendored
  jquery.terminal (term.js) both assume a global jQuery, so ship our
  own copy (js/jquery.js, 3.7.1 from code.jquery.com) and load it
  before them in templates/index.php. Fixes a hard "jQuery is not
  defined" crash that left the terminal blank (black screen).

- With that fixed, every ajax call started failing with HTTP 412
  "CSRF check failed": core only auto-attaches the requesttoken
  header to requests made through ITS OWN globally-loaded jQuery, and
  with occweb now vendoring a separate jQuery instance that no longer
  happens. A global $.ajaxSetup() call turned out unreliable (another
  app's later jQuery load can replace window.jQuery afterwards), so
  each of the three ajax calls (SQL query, initial completion list,
  occ command execution) now sets the requesttoken header explicitly
  via OC.requestToken.

Bump max-version to 34 - actually verified working on NC 34.0.4 now,
not inherited/guessed (see the policy comment in this same file).
Version bump to 0.3.5.
2026-09-26 12:23:28 +02:00
dillard 1a333d161b Replace INSTALL-AIO.md with a generic, English, host-agnostic version
The previous version was in Dutch and hardcoded internal deploy details
(hostname alias, personal local path, private Gitea mirror domain) that
had no business being in a public repo. Replaced with a generic version
using <docker-host> placeholders; the internal nc01-specific copy now
lives at notes/deploy-nc01.md, which is gitignored and stays local-only.

Not rewriting prior history for this (confirmed with user: nothing
credential-level was exposed, just a hostname alias and a path).
2026-09-26 03:59:00 +02:00
dillard 827b2d10dd Revert max-version to 33: only claim what we've actually verified
35 was fanategorius's own tested value, but this is now a modified
fork (CSRF fix, admin checks, translated internals) - inheriting their
compatibility claim isn't the same as having verified it ourselves.
33 is the only version this fork has actually run on (nc01).

An untested max-version that's too high is worse than one that's
accurate: on the next core bump, Nextcloud's own compat check would
keep the app enabled on an unverified major instead of cleanly
disabling it, so a real incompatibility would surface as a runtime
error instead of a safe, expected "needs testing" disable.

Policy going forward (documented inline in info.xml): test against
the next major's beta ahead of time and bump max-version before that
major goes final, so the app is never caught by surprise - never bump
it reactively/speculatively.
2026-09-26 03:53:16 +02:00
dillard 20bdb7dcd6 Fix install/clone instructions to point at our fork, revert max-version to 35
README.md and install.sh's install instructions still told new users to
git clone the abandoned upstream (fanategorius/occweb), which lacks all
of this fork's fixes. Point those at dillardblom/occweb; leave the
Credits section's links to Adphi/fanategorius as-is since those are
deliberate attribution, not install instructions. Also repoint the
README screenshot badge, which was hotlinking Adphi's repo.

composer.json still listed only the original author; add ourselves
alongside, matching the info.xml author field.

Revert max-version 40->35: 35 was fanategorius's own tested release
value (commit 7c09dd2), not a guess. Bumping to 40 was our own
speculative headroom into an untested future major - exactly the
pattern this project's own history already reverted twice, for the
same stated reason ("hasn't been tested against newer Nextcloud
releases"). Getting auto-disabled by the compat check on an untested
major is the correct, safe failure mode; claiming compatibility we
haven't verified is not. 35 still satisfies the Codex finding (no
narrowing below a previously-validated ceiling) without guessing ahead.
2026-09-26 03:50:37 +02:00
dillard c2186ce2ea Give max-version real headroom instead of pinning to nc01's exact core version
Codex review flagged the previous 35->33 change as an unjustified
compatibility regression: nothing in this app's code is actually
incompatible with NC 34/35, so narrowing the ceiling only reflects
nc01's current version, not a real limitation. Worse, nc01's AIO now
auto-updates its core nightly (watchtower-aio), so pinning max-version
to today's exact version would very likely repeat the 2026-08-09
incident (occweb auto-disabled by the compat check) on the very next
core bump. This app only calls a handful of stable, long-lived OCP
interfaces, so a generous ceiling carries little real compatibility
risk compared to that recurring outage.
2026-09-26 03:38:48 +02:00
dillard f4395d8dda Add INSTALL-AIO.md to the repo, rewrite it for the actual AIO/custom_apps workflow
Was untracked locally and stale (referenced apps/ instead of custom_apps/,
git clone inside the container instead of our tar-based deploy). Documents
the two things that just caused a production incident on nc01: files
extracted from a tarball keep the source uid/gid and need an explicit
chown to www-data, and skipping the installed_version sync after bumping
<version> puts the whole instance in "needs upgrade" state, which blocks
not just occ commands but the web login page too.

Link it from README.md's install section.
2026-09-26 03:27:27 +02:00
dillard 97ceaf7c37 Point website/bugs/repository/screenshot at our own fork
Upstream (fanategorius/occweb) is abandoned; this fork is the
maintained one now, so app metadata should point here instead.
2026-09-26 02:01:29 +02:00
dillard 292b8980cb Update app author name to maintainer's own name 2026-09-26 02:00:01 +02:00
dillard faa389c4b1 Update app author email to maintainer's own address
Upstream (fanategorius/occweb) is abandoned; this fork is maintained
independently, so the contact address in info.xml should point here.
2026-09-26 01:59:27 +02:00
dillard 2af32728f7 Correct max-version to 33 (actual nc01 core version, not 36)
occ status on nc01 reports 33.0.8.2, not 36 as assumed — match the
repo's established convention of pinning max-version to the actually
tested/installed core version rather than a guess.
2026-09-26 01:55:56 +02:00
dillard ad0ce047e5 Remove unjustified @NoCSRFRequired from DbController::query(), bump max-version to 36
query() runs arbitrary admin SQL (including DELETE/UPDATE) and was
exempted from CSRF protection — a leftover from the original upstream
code, which had this alongside @PublicPage (no auth at all) before our
admin-check hardening. The client already goes through the standard
$.ajax() flow that auto-attaches Nextcloud's CSRF token, exactly like
OccController::cmd() does without any such exemption, so removing it
has no functional cost.

Also bump nextcloud max-version to 36 to match nc01's current core
version, now that AIO auto-updates the core nightly.

Version bump to 0.3.4.
2026-09-26 00:36:16 +02:00
dillard 7d05643ee6 Translate Russian comments and user-facing strings to English
Covers OccOutput.php, DbController.php and js/index.js: docblocks,
inline comments, the forbidden-construct labels shown to admins in
error responses, and the SQL template descriptions shown in the
terminal (delete-user, list-user, rename-user).
2026-09-26 00:13:03 +02:00
dillard 040743d351 Credit fanategorius and Git-Usr123 forks in README 2026-09-11 17:52:59 +02:00
dillard 85890e12db Update README to reflect active fork status, drop stale Russian translation
Also backport OccController's getConfig() -> IConfig::class fix (deprecated
internal API -> stable OCP interface), spotted while comparing against
Git-Usr123/occweb.
2026-09-11 17:40:12 +02:00
dillard cd01a2fa17 Merge branch 'fix/fetch-return-type-php81' into deploy-nc01 2026-08-09 13:06:32 +02:00
dillard a4a8f9a4eb Fix missing admin check on OccController routes (security)
index(), cmd() and list() were accessible to any logged-in user.
The navigation menu already restricts visibility to admins, but the
routes themselves were unprotected — any authenticated user who knows
the URL could execute occ commands or list all available commands.

Fix: inject IGroupManager + IUserSession, add requireAdmin() helper,
call it at the start of all three public methods. Matches the pattern
already used in DbController.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 22:04:05 +02:00
dillard f681caca35 Fix OccOutput::fetch() return type for PHP 8.1+ compatibility
BufferedOutput::fetch() declares a string return type in Symfony 5+.
PHP 8.1 enforces return type compatibility with parent methods, causing
a fatal 500 error when the occweb page loads.

Fix: add ': string' return type to OccOutput::fetch().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 21:25:56 +02:00
13 changed files with 373 additions and 283 deletions
+3
View File
@@ -30,5 +30,8 @@ js/*.log
.env
*.local
# Internal deploy notes (hostnames, paths) - never publish these
notes/
# Misc logs
*.log
+95
View File
@@ -0,0 +1,95 @@
# Installing/updating occweb on Nextcloud AIO (Docker)
Nextcloud's official All-in-One (AIO) distribution runs core in its own container
(`nextcloud-aio-nextcloud` by convention), so the generic [install instructions](README.md)
(`git clone` straight into the server) don't apply as-is. This covers the differences.
## Where the app belongs
`/var/www/html/custom_apps/` inside the core container — **not** `/var/www/html/apps/`.
AIO's `config.php` sets `apps_paths` with `apps/` as `writable: false` (managed by
Nextcloud's own core/appstore sync, which can overwrite custom code placed there) and
`custom_apps/` as `writable: true` — the intended location for third-party apps like
this one.
## Deploy/update procedure
The AIO container has no working git access to GitHub, so deploy a tarball instead of
`git clone`/`git pull` inside it:
```bash
# 1. On your machine: clean checkout of the branch/tag you want, dev files stripped
STAGE=$(mktemp -d)
git archive main | tar -x -C "$STAGE"
rm -rf "$STAGE"/{tests,.travis.yml,phpunit.xml,phpunit.integration.xml,composer.json,composer.lock,Makefile}
tar -C "$STAGE" -cf /tmp/occweb-deploy.tar .
# 2. Copy it onto the Docker host, then into the container
scp /tmp/occweb-deploy.tar <docker-host>:/tmp/occweb-deploy.tar
ssh <docker-host> "docker cp /tmp/occweb-deploy.tar nextcloud-aio-nextcloud:/tmp/occweb-deploy.tar && \
docker exec nextcloud-aio-nextcloud sh -c 'mkdir -p /var/www/html/custom_apps/extended_occweb && \
tar -xf /tmp/occweb-deploy.tar -C /var/www/html/custom_apps/extended_occweb && rm /tmp/occweb-deploy.tar'"
# 3. Fix ownership - tar preserves the source uid/gid, not www-data
ssh <docker-host> "docker exec nextcloud-aio-nextcloud chown -R www-data:www-data /var/www/html/custom_apps/extended_occweb"
# 4. Fresh install only (skip when updating an already-enabled app):
ssh <docker-host> "docker exec -u www-data nextcloud-aio-nextcloud php occ app:enable extended_occweb"
```
> **Gotcha**: never copy a file into a container with `ssh host "docker exec container bash -c 'cat > file'"`
> piped from local stdin — that produces an empty file. Always `scp` to the host, then
> `docker cp` into the container, as above.
## ⚠️ Critical after bumping `<version>` in `appinfo/info.xml`
This step is **not optional** — skipping it doesn't just block occ commands, it can
block **logging into the web UI entirely** (Nextcloud shows an upgrade screen to every
user instead of the login page).
Nextcloud tracks each app's installed version in the database (`installed_version` in
`oc_appconfig`), separately from `<version>` in `info.xml`. If you replace files in place
(instead of going through the app store or `occ upgrade`), those two fall out of sync,
and Nextcloud puts the whole instance into a "needs upgrade" state until you sync it
manually:
```bash
ssh <docker-host> "docker exec -u www-data nextcloud-aio-nextcloud php occ config:app:set extended_occweb installed_version --value='X.Y.Z'"
```
(use the same `X.Y.Z` as the new `<version>`). Verify with `occ status` that
`needsDbUpgrade: false` again.
## Restart PHP-FPM after every deploy
`occ app:disable`/`app:enable` does not clear PHP's opcache — an update can silently
fail to take effect without a restart (routes 404ing is the typical symptom). Prefer a
graceful reload over a hard restart (keeps in-flight requests alive):
```bash
ssh <docker-host> "docker exec nextcloud-aio-nextcloud ps aux | grep 'php-fpm: master'" # find the PID
ssh <docker-host> "docker exec nextcloud-aio-nextcloud kill -USR2 <master-pid>"
```
`supervisorctl` won't work directly inside this container (its socket isn't at the
default path) — hence the manual PID lookup.
## Removing the app
```bash
ssh <docker-host> "docker exec -u www-data nextcloud-aio-nextcloud php occ app:disable extended_occweb && \
docker exec nextcloud-aio-nextcloud rm -rf /var/www/html/custom_apps/extended_occweb"
```
## `max-version` policy
See the comment next to `<dependencies>` in `appinfo/info.xml`: it must always equal the
highest Nextcloud core version this fork has actually been verified against — never a
value inherited from upstream, and never a guess ahead of what's been tested. Test
against the next major's beta ahead of time and bump it before that major goes final,
rather than reacting after the fact.
## SQL query mode
Type `sql` in the OCCWeb terminal to run raw SQL directly against the Nextcloud
database. Separate statements with `;`, use Shift+Enter for a new line and Enter to run.
+57 -13
View File
@@ -1,26 +1,51 @@
# ⚠️ Deprecated ⚠️ OCCWeb terminal
*Читать на [русском](README.ru.md).*
# OCCWeb terminal (maintained fork)
### A web terminal for admins to launch Nextcloud's occ commands
![occweb](https://github.com/Adphi/OCCWeb/raw/main/appinfo/screenshot.png)
![occweb](https://github.com/dillardblom/occweb/raw/main/appinfo/screenshot.png)
## About this fork
## ⚠️ Deprecated ⚠️
As nextcloudd has no native support for asynchronous operations, due to the use of php, this aplication is deprecated, and will no longer support the Nextcloud' future versions (19+). I did not find a way to implemement true support for interactive and long running occ tasks in a web terminal whitout introducing addtional dependencies (through websockets, for example), the lack of true asynchronous occ operations can lead to serious alterations of voluminous instances.
[This issue](https://github.com/nextcloud/server/issues/16726) may give some hints on why I decided to not support this application anymore.
The original [Adphi/occweb](https://github.com/Adphi/occweb) was marked deprecated by its
author and is unmaintained. This fork tracks [fanategorius/occweb](https://github.com/fanategorius/occweb),
which picked up compatibility maintenance where Adphi left off, and adds SQL query mode,
security hardening (admin checks, CSRF/confirmation flows, transactional batches), and PHP
8.1+ compatibility fixes on top.
### Credits
- [Adphi/occweb](https://github.com/Adphi/occweb) — original author/project.
- [fanategorius/occweb](https://github.com/fanategorius/occweb) — the actively maintained
fork this repo is based on and tracks.
- [Git-Usr123/occweb](https://github.com/Git-Usr123/occweb) — independent fork; the
`OC::$server->getConfig()` → `OC::$server->get(\OCP\IConfig::class)` fix in
`OccController.php` was spotted there and backported here.
**This tool is still not the right place for big/risky operations.** It has no support for
asynchronous or long-running tasks — every command runs synchronously inside a single PHP
web request, so anything that takes a while (`occ files:scan` on a large instance, a Nextcloud
major-version `occ upgrade`, `occ maintenance:mode --on` workflows) can time out mid-operation
with no way to recover cleanly. Use a real shell (SSH + `occ` directly, or the guest-agent/
console) for major upgrades and other long-running or high-blast-radius operations; reserve
this terminal for routine day-to-day occ/SQL admin tasks. [This issue](https://github.com/nextcloud/server/issues/16726)
has background on why Nextcloud has no native async support for occ tasks.
## Install
Running Nextcloud AIO (Docker)? See [INSTALL-AIO.md](INSTALL-AIO.md) instead — the app
path and deploy steps differ from the generic instructions below.
No build step required (plain PHP + vanilla JS). Clone straight into the
target server's `apps/` directory and run the install script:
Nextcloud requires the app's directory name to match its id (`extended_occweb`), so
clone into that name explicitly rather than the repo's own name:
```bash
cd /var/www/nextcloud/apps
git clone https://github.com/fanategorius/occweb.git
bash occweb/install.sh
git clone https://github.com/dillardblom/occweb.git extended_occweb
bash extended_occweb/install.sh
```
`install.sh` removes the dev/CI-only files that don't belong in a running
@@ -31,7 +56,7 @@ server user, and runs `occ app:enable`. It assumes Nextcloud lives at
values as arguments if that's not the case:
```bash
bash occweb/install.sh /path/to/nextcloud custom-web-user
bash extended_occweb/install.sh /path/to/nextcloud custom-web-user
```
`occ app:enable` takes care of registering the app's version correctly, so
@@ -80,7 +105,7 @@ So if your instance is pretty big, commands like `occ files:scan` will time out
## Deploying updates
After pulling/copying new code into `nextcloud/apps/occweb/`, restart PHP so
After pulling/copying new code into `nextcloud/apps/extended_occweb/`, restart PHP so
the changes actually take effect. `occ app:disable`/`app:enable` is **not**
enough — it does not clear PHP's opcode cache, so a stale, cached version of
the code can keep running (routes silently 404ing is a typical symptom).
@@ -102,12 +127,31 @@ CLI-upgrade wizard, even though nothing about Nextcloud core actually
changed. After bumping the version, sync it manually:
```bash
sudo -u www-data php /var/www/nextcloud/occ config:app:set occweb installed_version --value="X.Y.Z"
sudo -u www-data php /var/www/nextcloud/occ config:app:set extended_occweb installed_version --value="X.Y.Z"
```
(use the same `X.Y.Z` as the new `<version>`), then restart PHP as above.
This does not apply on a fresh `occ app:enable` install — that command sets
`installed_version` correctly on its own.
## Compatibility matrix
`max-version` in `appinfo/info.xml` only ever reflects the highest version we've actually
tested (see the policy comment right above it) - this table is the actual history of what
was verified, so a rollback target is obvious if a newer major causes problems.
| occweb version | Tag | NC version tested | Result |
|---|---|---|---|
| 0.3.4 | `v0.3.4` | 33.0.8 | ✅ Verified (nc01, 2026-09-25) |
| 0.3.4 | `v0.3.4` | 34.0.4 | ❌ Blank terminal - global jQuery gone on NC34 (`jQuery is not defined`), then a 412 "CSRF check failed" on every ajax call once jQuery was vendored. Fixed in 0.3.5. |
| 0.3.5 | `v0.3.5-nc34` | 34.0.4 | ✅ Verified (nc01, 2026-09-26) - vendors its own jQuery + sends the CSRF `requesttoken` header explicitly on every ajax call |
| 0.3.5 | `v0.3.5-nc34` | 33.0.8 | ⚠️ Believed compatible (the 0.3.5 change is additive-only, nothing removed that NC30-33 depended on) but not re-verified - nc01 no longer runs NC33 to test against |
| 0.4.0 | (untagged) | 34.0.4 | ✅ Verified (nc01, 2026-09-26) - app id renamed `occweb` → `extended_occweb` (App Store prep), no functional changes to the 0.3.5 fixes otherwise |
| 0.4.1 | `v0.4.1-nc34` | 34.0.4 | ✅ Verified (nc01, 2026-09-26) - code review fixes: correct jQuery `.fail()` signature, added a missing `.fail()` handler on the initial terminal-load request |
| 0.4.1 | `v0.4.1-nc34` | 33.0.8 | ⚠️ Same caveat as 0.3.5/0.4.0 - not re-verified, no NC33 test environment available. Also unresolved: `js/jquery.js` now loads unconditionally on every supported version, even NC30-33 where core still provides jQuery - flagged in code review, needs an NC33 environment to confirm it's harmless there. |
If a future major breaks something, `git checkout v0.3.4` is the last version confirmed
working on NC33 and older.
## TODOs:
See [open issues](https://github.com/Adphi/occweb/issues)
See [open issues](https://github.com/dillardblom/occweb/issues)
-126
View File
@@ -1,126 +0,0 @@
# ⚠️ Устарело ⚠️ OCCWeb terminal
*Read in [English](README.md).*
### Веб-терминал для администраторов для запуска occ-команд Nextcloud
![occweb](https://github.com/Adphi/OCCWeb/raw/main/appinfo/screenshot.png)
## ⚠️ Устарело ⚠️
Поскольку Nextcloud не имеет встроенной поддержки асинхронных операций
(из-за использования PHP), это приложение считается устаревшим и больше не
будет поддерживать будущие версии Nextcloud (19+). Автору не удалось найти
способ реализовать полноценную поддержку интерактивных и долгих occ-задач в
веб-терминале без введения дополнительных зависимостей (например, через
websockets), а отсутствие настоящей асинхронности может привести к серьёзным
проблемам на больших инсталляциях.
[Этот issue](https://github.com/nextcloud/server/issues/16726) объясняет,
почему разработчик решил больше не поддерживать это приложение.
## Установка
Сборка не нужна (чистый PHP + vanilla JS). Клонируйте прямо в `apps/`
целевого сервера и запустите установочный скрипт:
```bash
cd /var/www/nextcloud/apps
git clone https://github.com/fanategorius/occweb.git
bash occweb/install.sh
```
`install.sh` удаляет dev/CI-файлы, не нужные для работающей установки
(`tests/`, `.travis.yml`, `phpunit*.xml`, `composer.json`, `composer.lock`,
`Makefile`), выставляет `chown -R` на пользователя веб-сервера и выполняет
`occ app:enable`. По умолчанию считает, что Nextcloud лежит в
`/var/www/nextcloud`, а пользователь веб-сервера — `www-data`; если у вас
иначе, передайте своими аргументами:
```bash
bash occweb/install.sh /path/to/nextcloud custom-web-user
```
`occ app:enable` сам корректно проставит версию приложения в базе, поэтому
оговорки из раздела «Обновление кода на сервере» ниже к свежей установке не
относятся — они актуальны только когда приложение уже включено и вы
обновляете его на месте.
Перед установкой сверьте версию вашего Nextcloud с диапазоном в
`appinfo/info.xml` (`dependencies/nextcloud`, `min-version`/`max-version`)
— `occ app:enable` откажется включать приложение вне этого диапазона.
Учтите: `install.sh` удаляет файлы, отслеживаемые в git. Если планируете
дальше обновлять эту установку через `git pull`, а не пере-клонированием —
имейте в виду, что будущее изменение одного из удалённых файлов в апстриме
может привести к отказу `git pull` смёржить изменения. Git сам сообщит об
этом, и файл можно вернуть командой `git checkout -- <file>`.
## Режим SQL-запросов
Введите `sql` в терминале, чтобы переключиться в режим SQL-запросов и
выполнять произвольные SQL-запросы напрямую к базе данных Nextcloud (только
для администраторов). Запросы разделяются `;`. Используйте **Shift+Enter**
для перехода на новую строку и **Enter**, чтобы отправить весь блок одним
запросом — это важно для скриптов вида
`SET vars.x = 'value'; SELECT current_setting('vars.x');`, так как все
запросы, отправленные одним блоком, выполняются в рамках одного соединения
(сессии) с базой данных. Введите `occ`, чтобы вернуться в обычный режим
occ-команд.
⚠️ Для `DELETE`/`UPDATE` нет отмены — внимательно проверяйте, что вы
собираетесь выполнить, желательно сначала на некритичной записи/пользователе.
## ⚠️ Предупреждения ⚠️
- Это приложение не является настоящим интерактивным терминалом и не
поддерживает долгие задачи.
Поэтому на крупных инсталляциях команды вроде `occ files:scan` могут
завершаться по таймауту с ошибкой.
- Не используйте `occ maintenance:mode --on`, это очевидно...
- Шаблон `rename-user` — это **best-effort**, а не официально поддерживаемая
Nextcloud операция. Он обновляет только известные таблицы ядра
(`oc_users`, `oc_preferences`, `oc_group_user`, `oc_group_admin`,
`oc_ldap_user_mapping`, `oc_share`, `oc_mounts`, `oc_storages`), но **не**
трогает таблицы сторонних приложений (Talk, Calendar, Contacts, Mail,
двухфакторная аутентификация/WebAuthn и т.д.). Дополнительно нужно вручную
переименовать каталог данных пользователя на диске (`data/<старый> ->
data/<новый>`) при остановленном веб-сервере или в режиме обслуживания, а
затем выполнить `occ files:scan --all`. Сначала сделайте бэкап базы и
протестируйте на некритичном аккаунте.
## Обновление кода на сервере
После того как новый код скопирован/спуллен в `nextcloud/apps/occweb/`,
обязательно перезапустите PHP, иначе изменения не применятся. `occ
app:disable`/`app:enable` **не помогает** — эта команда не сбрасывает
opcode-кэш PHP, из-за чего может продолжать работать старая закэшированная
версия кода (характерный симптом — маршруты внезапно отвечают 404).
Перезапустите сам процесс PHP, например:
```bash
sudo systemctl restart php8.3-fpm # укажите свою версию PHP
sudo systemctl restart apache2 # если PHP работает как модуль Apache
```
### Если меняете `<version>` в `appinfo/info.xml`
Nextcloud хранит установленную версию каждого приложения в базе
(`installed_version` в `oc_appconfig`) отдельно от `<version>` в
`info.xml`. Если вы правите файлы на месте (копированием/`git pull`), а не
через App Store или `occ upgrade`, эти два значения расходятся — и
Nextcloud считает, что «инстансу нужен апгрейд», блокируя большинство
`occ`-команд за CLI-визардом обновления, хотя само ядро Nextcloud никак не
менялось. После бампа версии синхронизируйте её вручную:
```bash
sudo -u www-data php /var/www/nextcloud/occ config:app:set occweb installed_version --value="X.Y.Z"
```
(то же значение `X.Y.Z`, что и в новом `<version>`), затем перезапустите
PHP, как описано выше. На свежую установку через `occ app:enable` это не
распространяется — там версия проставляется корректно автоматически.
## Планы (TODO):
См. [открытые issues](https://github.com/Adphi/occweb/issues)
+15 -10
View File
@@ -1,26 +1,31 @@
<?xml version="1.0"?>
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>occweb</id>
<id>extended_occweb</id>
<name>Extended OCC Web</name>
<summary>OCC Commands in a web terminal</summary>
<description><![CDATA[Run OCC Commands in a web terminal]]></description>
<version>0.3.3</version>
<version>0.4.1</version>
<licence>agpl</licence>
<author mail="ebugaev@itkey.com" >Egor</author>
<author mail="dillard.blom@ensembia.com" >Dillard Blom</author>
<namespace>OCCWeb</namespace>
<category>tools</category>
<website>https://github.com/fanategorius/occweb</website>
<bugs>https://github.com/fanategorius/occweb/issues</bugs>
<repository>https://github.com/fanategorius/occweb</repository>
<screenshot>https://github.com/fanategorius/occweb/raw/main/appinfo/screenshot.png</screenshot>
<website>https://github.com/dillardblom/occweb</website>
<bugs>https://github.com/dillardblom/occweb/issues</bugs>
<repository>https://github.com/dillardblom/occweb</repository>
<screenshot>https://github.com/dillardblom/occweb/raw/main/appinfo/screenshot.png</screenshot>
<dependencies>
<nextcloud min-version="30" max-version="35"/>
<!-- max-version must equal the highest NC core version this fork has
actually been verified on - never a value inherited from upstream
or guessed ahead. Test against the next major's beta and bump
this BEFORE it goes final, so occ:upgrade never has to fall back
to auto-disabling the app. -->
<nextcloud min-version="30" max-version="34"/>
</dependencies>
<navigations>
<navigation role="admin">
<name>OCCWeb</name>
<route>occweb.occ.index</route>
<name>OCC Web</name>
<route>extended_occweb.occ.index</route>
<icon>app.svg</icon>
<type>link</type>
</navigation>
+4
View File
@@ -6,6 +6,10 @@
"authors": [
{
"name": "Adphi"
},
{
"name": "Dillard Blom",
"email": "dillard.blom@ensembia.com"
}
],
"require": {},
+5 -4
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env bash
#
# Post-clone install script for occweb.
# Post-clone install script for occweb (Nextcloud app id: extended_occweb).
#
# Usage (from nextcloud/apps/):
# git clone https://github.com/fanategorius/occweb.git
# bash occweb/install.sh [nextcloud-root] [web-user]
# Usage (from nextcloud/apps/) - clone into "extended_occweb", not the repo's
# own name, since Nextcloud requires the app directory to match its id:
# git clone https://github.com/dillardblom/occweb.git extended_occweb
# bash extended_occweb/install.sh [nextcloud-root] [web-user]
#
# Defaults: nextcloud-root=/var/www/nextcloud, web-user=www-data
#
+56 -35
View File
@@ -1,23 +1,30 @@
(function (OC, window, $, undefined) {
'use strict';
$(function() {
// Nextcloud core only auto-attaches the CSRF requesttoken header to ajax
// calls made through its own globally-loaded jQuery. Since NC34 that
// global jQuery is no longer guaranteed to be the one still active by
// the time our own calls fire (other apps loading their own jQuery can
// replace window.jQuery afterwards - see js/jquery.js), ajaxSetup() here
// isn't reliable; every $.ajax() call below sets the header explicitly.
function scrollToBottom(){
var html = $('html');
html.scrollTop(html.prop('scrollHeight'));
}
var baseUrl = OC.generateUrl('/apps/occweb');
var baseUrl = OC.generateUrl('/apps/extended_occweb');
// Текущий режим терминала: 'occ' (обычные occ-команды) или 'sql'
// (выполнение произвольных SQL-запросов через /db/query).
// Current terminal mode: 'occ' (regular occ commands) or 'sql'
// (running arbitrary SQL queries via /db/query).
var mode = 'occ';
var OCC_PROMPT = 'occ $ ';
var SQL_PROMPT = '[[;#ff5555;]sql]# ';
// Разбивает пачку запросов по ";" с учётом одинарных И двойных кавычек
// (в т.ч. '' / "" как экранированной кавычки внутри строки/идентификатора),
// чтобы ";" в строковом литерале или "квотированном идентификаторе" не
// ломал разбиение. Зеркалит splitStatements() на бэкенде.
// Splits a batch of queries on ";" while respecting single AND double
// quotes (incl. '' / "" as an escaped quote inside a string/identifier),
// so that a ";" inside a string literal or "quoted identifier" doesn't
// break the split. Mirrors splitStatements() on the backend.
function splitStatements(sql) {
var statements = [];
var current = '';
@@ -54,9 +61,9 @@
return statements.filter(function (s) { return s !== ''; });
}
// Быстрая клиентская проверка на DELETE/UPDATE — только для UX (чтобы не
// делать лишний запрос к серверу). Итоговое решение всё равно
// принимает бэкенд (requiresConfirmation), это лишь подсказка.
// Quick client-side check for DELETE/UPDATE — purely for UX (to avoid
// an extra round trip to the server). The backend always makes the
// final call (requiresConfirmation); this is just a hint.
function scriptNeedsConfirmation(sql) {
return splitStatements(sql).some(function (part) {
var normalized = part.replace(/^(\s*--[^\n]*\n)*\s*/, '');
@@ -68,18 +75,18 @@
return String(value).replace(/'/g, "''");
}
// Готовые шаблоны SQL-скриптов. build() возвращает текст скрипта,
// который вставляется в командную строку через term.set_command() —
// ничего не выполняется автоматически, пользователь сам проверяет
// и жмёт Enter (после чего срабатывает обычное подтверждение DELETE).
// Ready-made SQL script templates. build() returns the script text,
// which is inserted into the command line via term.set_command() —
// nothing runs automatically, the user reviews it themselves and
// presses Enter (which then triggers the normal DELETE confirmation).
var TEMPLATES = {
'delete-user': {
args: ['uid'],
description: 'Полностью удалить локального пользователя (oc_preferences, oc_group_user, oc_ldap_user_mapping, oc_users)',
description: 'Completely delete a local user (oc_preferences, oc_group_user, oc_ldap_user_mapping, oc_users)',
build: function (uid) {
// Значение подставляется напрямую в каждый запрос (а не через
// SET+current_setting) — так скрипт остаётся рабочим, даже если
// пользователь скопирует/выполнит только часть строк по отдельности.
// The value is substituted directly into each query (rather than
// via SET+current_setting) — this keeps the script working even
// if the user copies/runs only some of the lines individually.
var v = escapeSqlString(uid);
return [
"SELECT * FROM oc_users WHERE uid = '" + v + "'",
@@ -93,7 +100,7 @@
},
'list-user': {
args: ['uid'],
description: 'Только посмотреть данные пользователя, без удаления (oc_users, oc_preferences, oc_group_user, oc_ldap_user_mapping)',
description: "View a user's data only, without deleting anything (oc_users, oc_preferences, oc_group_user, oc_ldap_user_mapping)",
build: function (uid) {
var v = escapeSqlString(uid);
return [
@@ -106,14 +113,15 @@
},
'rename-user': {
args: ['old_uid', 'new_uid'],
description: 'ВНИМАНИЕ: не официальная операция Nextcloud. Переименовывает uid только в основных таблицах ядра — покрывает не всё, требует ручных доп. шагов (см. предупреждение в самом скрипте)',
description: "WARNING: not an official Nextcloud operation. Renames the uid only in the core tables — doesn't cover everything, requires manual extra steps (see the warning in the script itself)",
build: function (oldUid, newUid) {
var o = escapeSqlString(oldUid);
var n = escapeSqlString(newUid);
// Предупреждение — только однострочные "--"-комментарии без ";" внутри,
// поэтому splitStatements() (парный на бэкенде и здесь) не разобьёт их
// как отдельные запросы, а stripLeadingComments() на бэкенде уберёт
// этот блок перед определением типа самого первого запроса (SELECT).
// The warning consists only of single-line "--" comments with no
// ";" inside them, so splitStatements() (mirrored on the backend
// and here) won't split them into separate statements, and
// stripLeadingComments() on the backend will strip this block
// before determining the type of the very first query (SELECT).
var warning =
"-- WARNING rename is NOT an officially supported Nextcloud operation\n" +
"-- This only updates core tables below - it does NOT cover app-specific\n" +
@@ -169,7 +177,7 @@
term.echo('[[;yellow;]Template inserted into the command line — review it, then press Enter to run.]');
}
// Табличный вывод результатов SELECT в стиле psql.
// Tabular rendering of SELECT results, psql-style.
function renderTable(term, rows) {
if (!rows || !rows.length) {
return;
@@ -178,8 +186,9 @@
function formatCell(v) {
if (v === null || v === undefined) {
// Явная метка, а не пустая строка — иначе NULL неотличим от
// настоящей пустой строки '' в выводе таблицы.
// An explicit label rather than an empty string — otherwise NULL
// would be indistinguishable from an actual empty string '' in
// the table output.
return '[NULL]';
}
if (typeof v === 'object') {
@@ -273,9 +282,9 @@
term.echo('[[;yellow;]Switched back to OCC mode.]');
}
// Таймаут для больших/долгих пачек (например, DELETE по большой таблице
// без индекса): без него зависший запрос молча оставит терминал
// заблокированным (term.pause()) навсегда, если сервер не ответит.
// Timeout for large/long-running batches (e.g. a DELETE on a big table
// without an index): without it, a hung query would silently leave the
// terminal blocked (term.pause()) forever if the server never responds.
var SQL_REQUEST_TIMEOUT_MS = 120000;
function sendSqlQuery(term, sql, confirmed) {
@@ -285,6 +294,7 @@
type: 'POST',
contentType: 'application/json',
timeout: SQL_REQUEST_TIMEOUT_MS,
headers: { requesttoken: OC.requestToken },
data: JSON.stringify({ sql: sql, confirm: !!confirmed })
}).done(function (response) {
if (response && response.requiresConfirmation) {
@@ -317,7 +327,7 @@
});
}
$.get(baseUrl + '/cmd', function(response){
$.ajax({ url: baseUrl + '/cmd', headers: { requesttoken: OC.requestToken } }).done(function(response){
$('#app-content').terminal(function(command, term) {
if (mode === 'sql') {
var trimmed = command.trim();
@@ -373,11 +383,12 @@
url: baseUrl + '/cmd',
type: 'POST',
contentType: 'application/json',
headers: { requesttoken: OC.requestToken },
data: JSON.stringify(occCommand)
}).done(function (response) {
term.echo('\n' + response).resume();
}).fail(function (response, code) {
term.echo('\n' + response).resume();
}).fail(function (xhr, status) {
term.echo('\n[[;#ff5555;]Request failed: ]' + $.terminal.escape_formatting(xhr.status + ' ' + xhr.statusText)).resume();
});
}
}, {
@@ -388,8 +399,8 @@
prompt: OCC_PROMPT,
completion: response,
keydown: function (e) {
// Shift+Enter вставляет перевод строки вместо выполнения команды,
// это позволяет набирать многострочные SQL-скрипты в режиме sql.
// Shift+Enter inserts a newline instead of running the command,
// letting you type multi-line SQL scripts in sql mode.
if (e.shiftKey && e.key === 'Enter') {
this.insert('\n');
return false;
@@ -399,6 +410,16 @@
scrollToBottom()
}
});
}).fail(function (xhr, status) {
// Without this, a failure here (expired session/CSRF token, network
// error, a rejected admin check) leaves #app-content permanently
// empty with no visible error and nothing in the console pointing
// at the cause - exactly what happened during NC34 testing before
// this handler existed.
$('#app-content').text(
'Failed to load the terminal: ' + xhr.status + ' ' + xhr.statusText +
'. Try reloading the page; if that keeps happening, check the server logs.'
);
});
$('html').keypress(function(){
scrollToBottom()
+2
View File
File diff suppressed because one or more lines are too long
+91 -84
View File
@@ -13,28 +13,28 @@ use Psr\Log\LoggerInterface;
class DbController extends Controller
{
/** Максимум строк, возвращаемых на один SELECT (защита от OOM/DoS). */
/** Maximum rows returned per SELECT (protection against OOM/DoS). */
private const MAX_ROWS = 1000;
/** Максимальный размер присланного SQL-текста в байтах (защита от OOM/DoS). */
/** Maximum size of submitted SQL text in bytes (protection against OOM/DoS). */
private const MAX_SQL_BYTES = 1048576; // 1 MB
/**
* Конструкции, дающие доступ к файловой системе сервера или запуску
* внешних программ через SQL. Блокируются полностью, без возможности
* подтверждения — в отличие от DELETE, это не про потерю данных,
* а про потенциальный захват сервера.
* Constructs that give access to the server's filesystem or allow
* launching external programs via SQL. These are blocked entirely,
* with no way to confirm past them — unlike DELETE, this isn't
* about data loss, it's about potential server takeover.
*/
private const FORBIDDEN_PATTERNS = [
'/\bCOPY\b[\s\S]*\bPROGRAM\b/i' => 'COPY ... PROGRAM (запуск внешних команд)',
'/\bCOPY\b[\s\S]*\b(FROM|TO)\b\s*\'/i' => "COPY ... FROM/TO 'file' (доступ к файловой системе сервера)",
'/\bCOPY\b[\s\S]*\bPROGRAM\b/i' => 'COPY ... PROGRAM (runs external commands)',
'/\bCOPY\b[\s\S]*\b(FROM|TO)\b\s*\'/i' => "COPY ... FROM/TO 'file' (access to the server's filesystem)",
'/\bpg_read_binary_file\s*\(/i' => 'pg_read_binary_file()',
'/\bpg_read_file\s*\(/i' => 'pg_read_file()',
'/\bpg_ls_dir\s*\(/i' => 'pg_ls_dir()',
'/\bpg_stat_file\s*\(/i' => 'pg_stat_file()',
'/\blo_import\s*\(/i' => 'lo_import()',
'/\blo_export\s*\(/i' => 'lo_export()',
'/\bdblink(_connect)?\s*\(/i' => 'dblink() (подключение к произвольным БД)',
'/\bdblink(_connect)?\s*\(/i' => 'dblink() (connects to arbitrary databases)',
'/\bLOAD_FILE\s*\(/i' => 'LOAD_FILE()',
'/\bINTO\s+(OUTFILE|DUMPFILE)\b/i' => 'INTO OUTFILE/DUMPFILE',
];
@@ -55,15 +55,15 @@ class DbController extends Controller
$this->db = $db;
$this->groupManager = $groupManager;
$this->userSession = $userSession;
// Через OC::$server, как и в OccController — не добавляем LoggerInterface
// в конструктор, чтобы не менять сигнатуру, резолвящуюся DI-контейнером.
// Via OC::$server, same as in OccController — we don't add LoggerInterface
// to the constructor so as not to change the signature resolved by the DI container.
$this->logger = OC::$server->get(LoggerInterface::class);
}
/**
* Убирает ведущие однострочные комментарии ("-- ...") перед запросом.
* После разбиения пачки такой комментарий может "приклеиться"
* к следующему запросу и помешать определить его тип (SELECT/SET/DELETE).
* Strips leading single-line comments ("-- ...") before a query.
* After splitting a batch, such a comment can end up stuck to the
* next query and interfere with detecting its type (SELECT/SET/DELETE).
*/
private function stripLeadingComments($query)
{
@@ -75,12 +75,12 @@ class DbController extends Controller
}
/**
* Убирает ВСЕ однострочные (-- ...) и блочные C-style комментарии
* из запроса, включая те, что стоят внутри выражения (например, между
* именем функции и открывающей скобкой — иначе так можно спрятать
* запрещённую конструкцию от findForbiddenConstruct). Используется
* только для проверки на запрещённые конструкции — сам запрос на
* выполнение идёт без изменений.
* Strips ALL single-line (-- ...) and block-style C comments from a
* query, including ones sitting inside an expression (e.g. between a
* function name and its opening parenthesis — otherwise a forbidden
* construct could be hidden from findForbiddenConstruct that way).
* Used only for checking against forbidden constructs — the query
* that actually gets executed is left unmodified.
*/
private function removeAllComments($query)
{
@@ -90,10 +90,10 @@ class DbController extends Controller
}
/**
* Разбивает пачку запросов по ";" с учётом одинарных и двойных
* кавычек (строки и экранированные идентификаторы Postgres), чтобы
* точка с запятой внутри 'строки' или "идентификатора" (в том числе
* с '' / "" как экранированной кавычкой) не ломала разбиение.
* Splits a batch of queries on ";" while respecting single and double
* quotes (strings and Postgres-escaped identifiers), so that a
* semicolon inside a 'string' or "identifier" (including '' / "" as
* an escaped quote) doesn't break up the split.
*/
private function splitStatements($sql)
{
@@ -143,10 +143,11 @@ class DbController extends Controller
}
/**
* Возвращает описание найденной запрещённой конструкции (доступ к ФС,
* запуск программ) или null, если запрос безопасен в этом плане.
* Проверяется версия запроса без комментариев — иначе конструкцию
* можно спрятать, вставив комментарий между именем функции и "(".
* Returns a description of the forbidden construct found (filesystem
* access, running programs), or null if the query is safe in that
* regard. The comment-stripped version of the query is checked —
* otherwise the construct could be hidden by inserting a comment
* between the function name and "(".
*/
private function findForbiddenConstruct($query)
{
@@ -159,12 +160,14 @@ class DbController extends Controller
return null;
}
/**
* @NoCSRFRequired
*/
public function query()
{
// Проверка прав администратора
// Deliberate defense-in-depth (see the equivalent check in
// OccController::requireAdmin() for why it's kept even though
// Nextcloud's SecurityMiddleware already enforces this by default
// absent @NoAdminRequired) - this one is also the direct fix for
// this controller having shipped with @PublicPage originally,
// which did bypass that default protection entirely.
$user = $this->userSession->getUser();
if (!$user) {
return new JSONResponse(['error' => 'Not authenticated'], 401);
@@ -188,30 +191,29 @@ class DbController extends Controller
], 413);
}
// Аудит: логируем сам факт попытки выполнения ДО всех проверок,
// чтобы в логе остались и заблокированные/отклонённые запросы,
// а не только успешно выполненные.
$this->logger->warning('[occweb] SQL submitted by {user}: {sql}', [
'app' => 'occweb',
// Audit: log the fact that execution was attempted BEFORE any checks,
// so blocked/rejected queries are recorded in the log too, not just
// the ones that succeeded.
$this->logger->warning('[extended_occweb] SQL submitted by {user}: {sql}', [
'app' => 'extended_occweb',
'user' => $user->getUID(),
'sql' => $sql,
]);
// Разбиваем пачку по ";" с учётом кавычек (см. splitStatements).
// Все запросы выполняются последовательно на одном и том же
// соединении с БД (в рамках одного HTTP-запроса), поэтому SET
// сохраняет своё значение для current_setting() в следующих
// запросах этой же пачки.
// Split the batch on ";" respecting quotes (see splitStatements).
// All queries run sequentially on the same DB connection (within a
// single HTTP request), so SET retains its value for
// current_setting() in the following queries of the same batch.
$queries = $this->splitStatements($sql);
// Доступ к файловой системе сервера / запуск программ через SQL
// блокируется полностью — это не про потерю данных (как DELETE),
// а про потенциальный захват сервера, подтверждением не обходится.
// Access to the server's filesystem / running programs via SQL is
// blocked entirely — this isn't about data loss (like DELETE), it's
// about potential server takeover, and confirmation doesn't bypass it.
foreach ($queries as $query) {
$forbidden = $this->findForbiddenConstruct($query);
if ($forbidden !== null) {
$this->logger->error('[occweb] Blocked forbidden construct ({construct}) from {user}: {sql}', [
'app' => 'occweb',
$this->logger->error('[extended_occweb] Blocked forbidden construct ({construct}) from {user}: {sql}', [
'app' => 'extended_occweb',
'construct' => $forbidden,
'user' => $user->getUID(),
'sql' => $sql,
@@ -223,9 +225,9 @@ class DbController extends Controller
}
}
// DELETE и UPDATE необратимы (или трудно обратимы), поэтому
// требуем явное подтверждение с клиента (confirm=true), прежде
// чем выполнять хоть один запрос из пачки.
// DELETE and UPDATE are irreversible (or hard to reverse), so we
// require explicit confirmation from the client (confirm=true) before
// executing a single query from the batch.
$deleteCount = 0;
$updateCount = 0;
foreach ($queries as $query) {
@@ -254,21 +256,23 @@ class DbController extends Controller
]);
}
// Пачка выполняется в одной транзакции: если один из запросов
// упадёт (например, DELETE на середине серии из-за FK), все уже
// выполненные в этой же пачке изменения откатываются, а не
// остаются частично применёнными. SET (без LOCAL) не транзакционен
// в PostgreSQL, поэтому откат не затрагивает current_setting().
// The batch runs inside a single transaction: if one of the queries
// fails (e.g. a DELETE partway through a series due to an FK), all
// changes already made within this same batch are rolled back instead
// of staying partially applied. SET (without LOCAL) isn't
// transactional in PostgreSQL, so the rollback doesn't affect
// current_setting().
//
// Про DDL: в PostgreSQL (наша БД) DDL — CREATE/ALTER/DROP TABLE и
// т.п. — полностью транзакционен и откатывается вместе с остальными
// изменениями пачки, поэтому здесь для Postgres проблемы нет. Но
// Nextcloud работает и с MySQL/MariaDB через тот же IDBConnection, а
// там DDL делает неявный COMMIT — если эта пачка выполнится на
// MySQL-инстансе, commit()/rollBack() после такого DDL получат
// исключение "no active transaction". Оборачиваем begin/commit/
// rollBack в try/catch, чтобы это не превращалось в необработанное
// исключение и HTTP 500 вместо аккуратного JSON-ответа.
// About DDL: in PostgreSQL (our database) DDL — CREATE/ALTER/DROP
// TABLE etc. — is fully transactional and rolls back together with
// the rest of the batch's changes, so there's no problem here for
// Postgres. But Nextcloud also works with MySQL/MariaDB through the
// same IDBConnection, and there DDL causes an implicit COMMIT — if
// this batch runs on a MySQL instance, commit()/rollBack() after such
// a DDL statement will get a "no active transaction" exception. We
// wrap begin/commit/rollBack in try/catch so this doesn't turn into
// an unhandled exception and an HTTP 500 instead of a clean JSON
// response.
$results = [];
$rolledBack = false;
$rollbackFailed = false;
@@ -278,8 +282,8 @@ class DbController extends Controller
$this->db->beginTransaction();
$transactionStarted = true;
} catch (\Exception $e) {
$this->logger->warning('[occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
'app' => 'occweb',
$this->logger->warning('[extended_occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
'app' => 'extended_occweb',
'error' => $e->getMessage(),
]);
}
@@ -296,9 +300,10 @@ class DbController extends Controller
$stmt->execute();
if ($isSelect) {
// Читаем построчно и останавливаемся на MAX_ROWS, а не
// fetchAll() + array_slice — иначе SELECT без LIMIT на
// огромной таблице всё равно утащит всё в память PHP.
// Read row by row and stop at MAX_ROWS, instead of
// fetchAll() + array_slice — otherwise a SELECT without
// LIMIT on a huge table would still pull everything into
// PHP memory.
$rows = [];
$truncated = false;
while (($row = $stmt->fetch()) !== false) {
@@ -338,22 +343,23 @@ class DbController extends Controller
'type' => 'error',
'error' => $e->getMessage()
];
// Откатываем всю пачку и останавливаемся: не продолжаем
// выполнять оставшиеся запросы (например, серию DELETE),
// если один из предыдущих шагов не выполнился.
// Roll back the whole batch and stop: don't keep executing
// the remaining queries (e.g. a series of DELETEs) if one of
// the preceding steps failed.
if ($transactionStarted) {
try {
$this->db->rollBack();
$rolledBack = true;
} catch (\Exception $rollbackError) {
// Транзакция уже закрыта не нами — например, неявным
// COMMIT-ом от DDL-запроса на MySQL/MariaDB. Всё, что
// выполнилось ДО этой точки в пачке, могло остаться
// применённым навсегда — намеренно НЕ ставим
// rolledBack в true, это было бы неправдой.
// The transaction was already closed by something
// other than us — e.g. an implicit COMMIT from a DDL
// statement on MySQL/MariaDB. Anything that executed
// BEFORE this point in the batch may remain
// permanently applied — we deliberately do NOT set
// rolledBack to true, since that would be untrue.
$rollbackFailed = true;
$this->logger->warning('[occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
'app' => 'occweb',
$this->logger->warning('[extended_occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
'app' => 'extended_occweb',
'error' => $rollbackError->getMessage(),
]);
}
@@ -366,11 +372,12 @@ class DbController extends Controller
try {
$this->db->commit();
} catch (\Exception $e) {
// Транзакция уже закоммичена неявно (например, DDL-запросом
// на MySQL/MariaDB) — эффекты уже сохранены, это не ошибка
// выполнения самой пачки.
$this->logger->info('[occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
'app' => 'occweb',
// The transaction was already committed implicitly (e.g. by
// a DDL statement on MySQL/MariaDB) — the effects are
// already saved, this isn't an error in executing the batch
// itself.
$this->logger->info('[extended_occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
'app' => 'extended_occweb',
'error' => $e->getMessage(),
]);
}
+31 -2
View File
@@ -9,6 +9,7 @@ use OC\MemoryInfo;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Controller;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\OutputInterface;
@@ -31,7 +32,7 @@ class OccController extends Controller
$this->application = new Application(
OC::$server->get(\OCP\ServerVersion::class),
OC::$server->getConfig(),
OC::$server->get(\OCP\IConfig::class),
OC::$server->get(\OCP\EventDispatcher\IEventDispatcher::class),
new FakeRequest(),
$this->logger,
@@ -47,12 +48,38 @@ class OccController extends Controller
$this->symphonyApplication = $reflectionProperty->getValue($this->application);
}
private function requireAdmin(): ?JSONResponse
{
// Deliberate defense-in-depth, not a workaround for a missing framework
// check: Nextcloud's own SecurityMiddleware already rejects non-admins
// here by default (no method below has @NoAdminRequired), so this is
// redundant today. Kept anyway given what this app can do (arbitrary
// occ/SQL execution) - if the framework check ever regresses, this
// still holds. If you ever want to intentionally open one of these
// routes to non-admins, remove the call below AND add
// @NoAdminRequired, or this will keep silently blocking it.
//
// Services via OC::$server so the constructor signature stays unchanged
// and NC's DI container (without an application.php) can resolve the class.
$userSession = OC::$server->get(\OCP\IUserSession::class);
$user = $userSession->getUser();
if ($user === null) {
return new JSONResponse(['error' => 'Not authenticated'], 401);
}
$groupManager = OC::$server->get(\OCP\IGroupManager::class);
if (!$groupManager->isAdmin($user->getUID())) {
return new JSONResponse(['error' => 'Admin privileges required'], 403);
}
return null;
}
/**
* @NoCSRFRequired
*/
public function index()
{
return new TemplateResponse('occweb', 'index');
if ($err = $this->requireAdmin()) return $err;
return new TemplateResponse('extended_occweb', 'index');
}
/**
@@ -76,6 +103,7 @@ class OccController extends Controller
*/
public function cmd($command)
{
if ($err = $this->requireAdmin()) return $err;
$this->logger->debug($command);
$input = new StringInput($command);
$response = $this->run($input);
@@ -84,6 +112,7 @@ class OccController extends Controller
}
public function list() {
if ($err = $this->requireAdmin()) return $err;
$defs = $this->symphonyApplication->all();
$cmds = array();
foreach ($defs as $d) {
+5 -5
View File
@@ -35,19 +35,19 @@ class OccOutput extends BufferedOutput implements ConsoleOutputInterface
);
}
public function fetch()
public function fetch(): string
{
// Получаем основной буфер
// Get the main buffer
$content = parent::fetch();
// Если есть stream от секций, читаем его тоже
// If there's a stream from sections, read that too
if ($this->stream !== null) {
rewind($this->stream);
$streamContent = stream_get_contents($this->stream);
if ($streamContent !== false) {
$content .= $streamContent;
}
// Очищаем stream для следующего вызова
// Clear the stream for the next call
ftruncate($this->stream, 0);
rewind($this->stream);
}
+9 -4
View File
@@ -1,8 +1,13 @@
<?php
script('occweb', 'index');
script('occweb', 'term');
script('occweb', 'unix_formatting');
style('occweb', 'style');
// Nextcloud's global jQuery has been deprecated since NC19 and is no longer
// reliably present on NC34+ (see e.g. serverinfo needing to bundle its own) -
// term.js (jquery.terminal) and index.js both require jQuery to be loaded
// first, so ship and load our own copy rather than relying on core.
script('extended_occweb', 'jquery');
script('extended_occweb', 'index');
script('extended_occweb', 'term');
script('extended_occweb', 'unix_formatting');
style('extended_occweb', 'style');
?>
<div id="app" class="full-width">