Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a607426a9c | |||
| 35e1f5df32 | |||
| 4b84be3cb7 | |||
| 93244a0d4d | |||
| 8fc4942517 | |||
| 95cdd06426 | |||
| 6016d0916c | |||
| 422748cace | |||
| a5f5c5cdb7 | |||
| 221ea8a249 | |||
| 138ee88486 | |||
| f091cc8c0b | |||
| 2872d70653 | |||
| 1a333d161b | |||
| 827b2d10dd | |||
| 20bdb7dcd6 | |||
| c2186ce2ea | |||
| f4395d8dda |
@@ -0,0 +1,11 @@
|
|||||||
|
# Files excluded from `git archive` output - used by `make appstore` (and by
|
||||||
|
# our own nc01 deploy tarballs) to strip dev/CI-only files that don't belong
|
||||||
|
# in a running install, without needing them repeated as tar --exclude flags.
|
||||||
|
tests export-ignore
|
||||||
|
.travis.yml export-ignore
|
||||||
|
phpunit.xml export-ignore
|
||||||
|
phpunit.integration.xml export-ignore
|
||||||
|
composer.json export-ignore
|
||||||
|
composer.lock export-ignore
|
||||||
|
Makefile export-ignore
|
||||||
|
.gitattributes export-ignore
|
||||||
@@ -30,5 +30,11 @@ js/*.log
|
|||||||
.env
|
.env
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Internal deploy notes (hostnames, paths) - never publish these
|
||||||
|
notes/
|
||||||
|
|
||||||
|
# Local compat-test docker rigs (throwaway, not part of the app release)
|
||||||
|
testbak/
|
||||||
|
|
||||||
# Misc logs
|
# Misc logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -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 HEAD | 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.
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
# "build": "node node_modules/gulp-cli/bin/gulp.js"
|
# "build": "node node_modules/gulp-cli/bin/gulp.js"
|
||||||
# },
|
# },
|
||||||
|
|
||||||
app_name=$(notdir $(CURDIR))
|
app_name=$(shell xpath -q -e "//info/id/text()" appinfo/info.xml)
|
||||||
build_tools_directory=$(CURDIR)/build/tools
|
build_tools_directory=$(CURDIR)/build/tools
|
||||||
source_build_directory=$(CURDIR)/build/artifacts/source
|
source_build_directory=$(CURDIR)/build/artifacts/source
|
||||||
source_package_name=$(source_build_directory)/$(app_name)
|
source_package_name=$(source_build_directory)/$(app_name)
|
||||||
@@ -110,48 +110,41 @@ dist:
|
|||||||
make source
|
make source
|
||||||
make appstore
|
make appstore
|
||||||
|
|
||||||
# Builds the source package
|
# Both targets package via `git archive` rather than tarring the checkout
|
||||||
|
# directory directly: it only ever includes tracked files (so untracked/
|
||||||
|
# gitignored content - build/, notes/, any stray .env - can never leak into
|
||||||
|
# a release archive), it sets the correct top-level folder name via
|
||||||
|
# --prefix regardless of what this checkout is actually called (this repo
|
||||||
|
# is "occweb", the app id is "extended_occweb" - no symlink trick needed),
|
||||||
|
# and dev/CI-only files (tests/, Makefile, composer.*, phpunit*.xml,
|
||||||
|
# .travis.yml) are stripped via the `export-ignore` entries in
|
||||||
|
# .gitattributes instead of a long, easy-to-miss list of tar --exclude
|
||||||
|
# flags repeated per target. That also means source and appstore now
|
||||||
|
# produce the same archive; both targets are kept for compatibility with
|
||||||
|
# anything that invokes one of them by name.
|
||||||
|
#
|
||||||
|
# app_name comes from `xpath` (see its definition above) - if that binary
|
||||||
|
# is missing, $(shell ...) silently returns an empty string rather than
|
||||||
|
# failing the build, which would turn --prefix=$(app_name)/ below into
|
||||||
|
# --prefix=/ and produce a broken, unusably-rooted archive with no
|
||||||
|
# indication anything went wrong. Fail loudly instead.
|
||||||
|
.PHONY: check-app-name
|
||||||
|
check-app-name:
|
||||||
|
@test -n "$(app_name)" || \
|
||||||
|
(echo "app_name is empty - is 'xpath' (libxml2-utils/perl-XML-XPath) installed?" >&2; exit 1)
|
||||||
|
|
||||||
.PHONY: source
|
.PHONY: source
|
||||||
source:
|
source: check-app-name check-clean-tree
|
||||||
rm -rf $(source_build_directory)
|
rm -rf $(source_build_directory)
|
||||||
mkdir -p $(source_build_directory)
|
mkdir -p $(source_build_directory)
|
||||||
tar \
|
git archive --format=tar --prefix=$(app_name)/ HEAD | gzip > $(source_package_name).tar.gz
|
||||||
--exclude-vcs \
|
|
||||||
--exclude="../$(app_name)/build" \
|
|
||||||
--exclude="../$(app_name)/js/node_modules" \
|
|
||||||
--exclude="../$(app_name)/node_modules" \
|
|
||||||
--exclude="../$(app_name)/*.log" \
|
|
||||||
--exclude="../$(app_name)/js/*.log" \
|
|
||||||
-cvzf $(source_package_name).tar.gz ../$(app_name)
|
|
||||||
|
|
||||||
# Builds the source package for the app store, ignores php and js tests
|
# Builds the source package for the app store, ignores php and js tests
|
||||||
.PHONY: appstore
|
.PHONY: appstore
|
||||||
appstore:
|
appstore: check-app-name check-clean-tree
|
||||||
rm -rf $(appstore_build_directory)
|
rm -rf $(appstore_build_directory)
|
||||||
mkdir -p $(appstore_build_directory)
|
mkdir -p $(appstore_build_directory)
|
||||||
tar \
|
git archive --format=tar --prefix=$(app_name)/ HEAD | gzip > $(appstore_package_name).tar.gz
|
||||||
--exclude-vcs \
|
|
||||||
--exclude="../$(app_name)/build" \
|
|
||||||
--exclude="../$(app_name)/tests" \
|
|
||||||
--exclude="../$(app_name)/Makefile" \
|
|
||||||
--exclude="../$(app_name)/*.log" \
|
|
||||||
--exclude="../$(app_name)/phpunit*xml" \
|
|
||||||
--exclude="../$(app_name)/composer.*" \
|
|
||||||
--exclude="../$(app_name)/js/node_modules" \
|
|
||||||
--exclude="../$(app_name)/js/tests" \
|
|
||||||
--exclude="../$(app_name)/js/test" \
|
|
||||||
--exclude="../$(app_name)/js/*.log" \
|
|
||||||
--exclude="../$(app_name)/js/package.json" \
|
|
||||||
--exclude="../$(app_name)/js/bower.json" \
|
|
||||||
--exclude="../$(app_name)/js/karma.*" \
|
|
||||||
--exclude="../$(app_name)/js/protractor.*" \
|
|
||||||
--exclude="../$(app_name)/package.json" \
|
|
||||||
--exclude="../$(app_name)/bower.json" \
|
|
||||||
--exclude="../$(app_name)/karma.*" \
|
|
||||||
--exclude="../$(app_name)/protractor\.*" \
|
|
||||||
--exclude="../$(app_name)/.*" \
|
|
||||||
--exclude="../$(app_name)/js/.*" \
|
|
||||||
-cvzf $(appstore_package_name).tar.gz ../$(app_name)
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: composer
|
test: composer
|
||||||
@@ -159,8 +152,8 @@ test: composer
|
|||||||
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
|
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml
|
||||||
|
|
||||||
.PHONY: sign
|
.PHONY: sign
|
||||||
sign:
|
sign: check-app-name
|
||||||
@openssl dgst -sha512 -sign ~/.nextcloud/certificates/occweb.key build/artifacts/appstore/occweb.tar.gz |openssl base64
|
@openssl dgst -sha512 -sign ~/.nextcloud/certificates/$(app_name).key $(appstore_package_name).tar.gz |openssl base64
|
||||||
|
|
||||||
VERSION := $(shell xpath -q -e "//info/version/text()" appinfo/info.xml)
|
VERSION := $(shell xpath -q -e "//info/version/text()" appinfo/info.xml)
|
||||||
|
|
||||||
@@ -168,11 +161,28 @@ VERSION := $(shell xpath -q -e "//info/version/text()" appinfo/info.xml)
|
|||||||
show-version:
|
show-version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
|
# Defaults to "origin" - correct for a plain clone of this repo, where
|
||||||
|
# "origin" is dillardblom/occweb. Override for a checkout where "origin"
|
||||||
|
# means something else (e.g. this maintainer's own machine, where the repo
|
||||||
|
# was originally forked from upstream and "origin" still points there):
|
||||||
|
# make version RELEASE_REMOTE=fork
|
||||||
|
RELEASE_REMOTE ?= origin
|
||||||
|
|
||||||
|
# VERSION/app_name are read live from the working tree's appinfo/info.xml,
|
||||||
|
# but the archive this tags/packages/signs is git archive HEAD - i.e. the
|
||||||
|
# last COMMITTED state. Without this check, bumping <version> without
|
||||||
|
# committing first would tag and sign a package named/keyed after the new
|
||||||
|
# version while its actual contents are still the previous commit's.
|
||||||
|
.PHONY: check-clean-tree
|
||||||
|
check-clean-tree:
|
||||||
|
@git diff --quiet HEAD -- || \
|
||||||
|
(echo "Uncommitted changes to tracked files - commit before releasing (see 'git status')." >&2; exit 1)
|
||||||
|
|
||||||
.PHONY: version
|
.PHONY: version
|
||||||
version:
|
version: check-clean-tree
|
||||||
@echo "Creating version v$(VERSION)"
|
@echo "Creating version v$(VERSION)"
|
||||||
@git tag v$(VERSION)
|
@git tag v$(VERSION)
|
||||||
@git push origin v$(VERSION)
|
@git push $(RELEASE_REMOTE) v$(VERSION)
|
||||||
@make dist
|
@make dist
|
||||||
@echo "\nRelease Signature: \n"
|
@echo "\nRelease Signature: \n"
|
||||||
@make sign
|
@make sign
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
### A web terminal for admins to launch Nextcloud's occ commands
|
### A web terminal for admins to launch Nextcloud's occ commands
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## About this fork
|
## About this fork
|
||||||
|
|
||||||
@@ -33,13 +33,19 @@ has background on why Nextcloud has no native async support for occ tasks.
|
|||||||
|
|
||||||
## Install
|
## 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
|
No build step required (plain PHP + vanilla JS). Clone straight into the
|
||||||
target server's `apps/` directory and run the install script:
|
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
|
```bash
|
||||||
cd /var/www/nextcloud/apps
|
cd /var/www/nextcloud/apps
|
||||||
git clone https://github.com/fanategorius/occweb.git
|
git clone https://github.com/dillardblom/occweb.git extended_occweb
|
||||||
bash occweb/install.sh
|
bash extended_occweb/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
`install.sh` removes the dev/CI-only files that don't belong in a running
|
`install.sh` removes the dev/CI-only files that don't belong in a running
|
||||||
@@ -50,7 +56,7 @@ server user, and runs `occ app:enable`. It assumes Nextcloud lives at
|
|||||||
values as arguments if that's not the case:
|
values as arguments if that's not the case:
|
||||||
|
|
||||||
```bash
|
```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
|
`occ app:enable` takes care of registering the app's version correctly, so
|
||||||
@@ -99,7 +105,7 @@ So if your instance is pretty big, commands like `occ files:scan` will time out
|
|||||||
|
|
||||||
## Deploying updates
|
## 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**
|
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
|
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).
|
the code can keep running (routes silently 404ing is a typical symptom).
|
||||||
@@ -121,12 +127,43 @@ CLI-upgrade wizard, even though nothing about Nextcloud core actually
|
|||||||
changed. After bumping the version, sync it manually:
|
changed. After bumping the version, sync it manually:
|
||||||
|
|
||||||
```bash
|
```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.
|
(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
|
This does not apply on a fresh `occ app:enable` install — that command sets
|
||||||
`installed_version` correctly on its own.
|
`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. |
|
||||||
|
| 0.4.2 | (untagged) | 32.0.15 | ✅ Verified (`testbak/`, 2026-09-27) - terminal loads and runs commands correctly. Confirms the suspected jQuery issue above: core still exposes a read-only `window.$` getter here, so our vendored `js/jquery.js` throws `TypeError: Cannot set property $ of #<Window> which has only a getter` on load - a real, uncaught console exception, but harmless in practice since the app attaches via `window.jQuery` (still writable), not `window.$`. Fixed in 0.4.3. |
|
||||||
|
| 0.4.2 | (untagged) | 33.0.9 | ✅ Verified (`testbak/`, 2026-09-27) - same result as 32.0.15: functional, same harmless-but-real `window.$` console exception. Fixed in 0.4.3. |
|
||||||
|
| 0.4.2 | (untagged) | 35.0.1 | ✅ Verified (`testbak/`, 2026-09-27) after a real fix: the bootstrap request 500'd with a PHP fatal, `Declaration of OccOutput::setErrorOutput() must be compatible with ConsoleOutputInterface::setErrorOutput(): void` - NC35 bundles a Symfony Console version whose interface now declares that return type. Fixed by adding `: void` to `OccOutput::setErrorOutput()` (`lib/Controller/OccOutput.php`), which is compatible with every earlier Symfony/NC version too. Confirmed clean afterwards: no console errors, no CSRF issues, terminal fully functional. |
|
||||||
|
| 0.4.3 | (untagged) | 32.0.15, 34.0.4 | ✅ Verified (`testbak/`, 2026-09-27) - `templates/index.php` now only loads `js/jquery.js` when `\OCP\ServerVersion::getMajorVersion() >= 34`, since core still provides a working jQuery on 32/33. Re-tested both sides of the boundary: NC32 loads no `jquery.js` request at all and has zero console errors; NC34 still loads it and still works, confirming no regression. |
|
||||||
|
|
||||||
|
`min-version`/`max-version` in `appinfo/info.xml` were updated from `30`/`34` to `32`/`35` on 2026-09-27 based on the above: 30/31 have never actually been tested (not even before the app-id rename) so claiming them was never justified once the `testbak/`-based verification policy existed, and 35 is now a genuinely verified (and fixed) target rather than a guess. 30/31 are also past Nextcloud's own currently-supported boundary as of 2026-09-18 (32-35 supported), which independently confirms 32 as the right floor.
|
||||||
|
|
||||||
|
If a future major breaks something, `v0.3.4` is the last version confirmed working on NC33
|
||||||
|
and older - but a plain `git checkout v0.3.4` is **not** a working rollback by itself. That
|
||||||
|
tag predates the app id rename (it still declares `<id>occweb</id>`), so checking it out
|
||||||
|
into an `extended_occweb`-named install just produces a broken id/directory mismatch. A
|
||||||
|
real rollback means disabling/removing `extended_occweb` and deploying `v0.3.4` fresh under
|
||||||
|
an `occweb`-named directory instead, same as the original install steps above. This is one
|
||||||
|
of the items still deferred to when a proper test instance exists (see the code review
|
||||||
|
notes in git history around 0.4.1) - not yet written up in full.
|
||||||
|
|
||||||
## TODOs:
|
## TODOs:
|
||||||
See [open issues](https://github.com/dillardblom/occweb/issues)
|
See [open issues](https://github.com/dillardblom/occweb/issues)
|
||||||
|
|||||||
+14
-5
@@ -1,11 +1,11 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
|
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||||
<id>occweb</id>
|
<id>extended_occweb</id>
|
||||||
<name>Extended OCC Web</name>
|
<name>Extended OCC Web</name>
|
||||||
<summary>OCC Commands in a web terminal</summary>
|
<summary>OCC Commands in a web terminal</summary>
|
||||||
<description><![CDATA[Run OCC Commands in a web terminal]]></description>
|
<description><![CDATA[Run OCC Commands in a web terminal]]></description>
|
||||||
<version>0.3.4</version>
|
<version>0.4.3</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author mail="dillard.blom@ensembia.com" >Dillard Blom</author>
|
<author mail="dillard.blom@ensembia.com" >Dillard Blom</author>
|
||||||
<namespace>OCCWeb</namespace>
|
<namespace>OCCWeb</namespace>
|
||||||
@@ -15,12 +15,21 @@
|
|||||||
<repository>https://github.com/dillardblom/occweb</repository>
|
<repository>https://github.com/dillardblom/occweb</repository>
|
||||||
<screenshot>https://github.com/dillardblom/occweb/raw/main/appinfo/screenshot.png</screenshot>
|
<screenshot>https://github.com/dillardblom/occweb/raw/main/appinfo/screenshot.png</screenshot>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<nextcloud min-version="30" max-version="33"/>
|
<!-- min-version and max-version must equal the lowest/highest NC core
|
||||||
|
versions this fork has actually been verified on (via testbak/,
|
||||||
|
see its README) - never inherited from upstream or guessed at
|
||||||
|
either end. min-version was 30 pre-testbak, unverified since the
|
||||||
|
app-id rename/jQuery fixes - narrowed to 32 (2026-09-27) once 30/31
|
||||||
|
turned out to never have been re-tested and 32 was confirmed
|
||||||
|
working. Test against the next major's beta and bump max-version
|
||||||
|
BEFORE it goes final, so occ:upgrade never has to fall back to
|
||||||
|
auto-disabling the app. -->
|
||||||
|
<nextcloud min-version="32" max-version="35"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<navigations>
|
<navigations>
|
||||||
<navigation role="admin">
|
<navigation role="admin">
|
||||||
<name>OCCWeb</name>
|
<name>OCC Web</name>
|
||||||
<route>occweb.occ.index</route>
|
<route>extended_occweb.occ.index</route>
|
||||||
<icon>app.svg</icon>
|
<icon>app.svg</icon>
|
||||||
<type>link</type>
|
<type>link</type>
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Adphi"
|
"name": "Adphi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dillard Blom",
|
||||||
|
"email": "dillard.blom@ensembia.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {},
|
"require": {},
|
||||||
|
|||||||
+5
-4
@@ -1,10 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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/):
|
# Usage (from nextcloud/apps/) - clone into "extended_occweb", not the repo's
|
||||||
# git clone https://github.com/fanategorius/occweb.git
|
# own name, since Nextcloud requires the app directory to match its id:
|
||||||
# bash occweb/install.sh [nextcloud-root] [web-user]
|
# 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
|
# Defaults: nextcloud-root=/var/www/nextcloud, web-user=www-data
|
||||||
#
|
#
|
||||||
|
|||||||
+23
-4
@@ -1,11 +1,18 @@
|
|||||||
(function (OC, window, $, undefined) {
|
(function (OC, window, $, undefined) {
|
||||||
'use strict';
|
'use strict';
|
||||||
$(function() {
|
$(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(){
|
function scrollToBottom(){
|
||||||
var html = $('html');
|
var html = $('html');
|
||||||
html.scrollTop(html.prop('scrollHeight'));
|
html.scrollTop(html.prop('scrollHeight'));
|
||||||
}
|
}
|
||||||
var baseUrl = OC.generateUrl('/apps/occweb');
|
var baseUrl = OC.generateUrl('/apps/extended_occweb');
|
||||||
|
|
||||||
// Current terminal mode: 'occ' (regular occ commands) or 'sql'
|
// Current terminal mode: 'occ' (regular occ commands) or 'sql'
|
||||||
// (running arbitrary SQL queries via /db/query).
|
// (running arbitrary SQL queries via /db/query).
|
||||||
@@ -287,6 +294,7 @@
|
|||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
timeout: SQL_REQUEST_TIMEOUT_MS,
|
timeout: SQL_REQUEST_TIMEOUT_MS,
|
||||||
|
headers: { requesttoken: OC.requestToken },
|
||||||
data: JSON.stringify({ sql: sql, confirm: !!confirmed })
|
data: JSON.stringify({ sql: sql, confirm: !!confirmed })
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
if (response && response.requiresConfirmation) {
|
if (response && response.requiresConfirmation) {
|
||||||
@@ -319,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) {
|
$('#app-content').terminal(function(command, term) {
|
||||||
if (mode === 'sql') {
|
if (mode === 'sql') {
|
||||||
var trimmed = command.trim();
|
var trimmed = command.trim();
|
||||||
@@ -375,11 +383,12 @@
|
|||||||
url: baseUrl + '/cmd',
|
url: baseUrl + '/cmd',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
|
headers: { requesttoken: OC.requestToken },
|
||||||
data: JSON.stringify(occCommand)
|
data: JSON.stringify(occCommand)
|
||||||
}).done(function (response) {
|
}).done(function (response) {
|
||||||
term.echo('\n' + response).resume();
|
term.echo('\n' + response).resume();
|
||||||
}).fail(function (response, code) {
|
}).fail(function (xhr, status) {
|
||||||
term.echo('\n' + response).resume();
|
term.echo('\n[[;#ff5555;]Request failed: ]' + $.terminal.escape_formatting(xhr.status + ' ' + xhr.statusText)).resume();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@@ -401,6 +410,16 @@
|
|||||||
scrollToBottom()
|
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(){
|
$('html').keypress(function(){
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|||||||
Vendored
+2
File diff suppressed because one or more lines are too long
@@ -162,7 +162,12 @@ class DbController extends Controller
|
|||||||
|
|
||||||
public function query()
|
public function query()
|
||||||
{
|
{
|
||||||
// Check admin privileges
|
// 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();
|
$user = $this->userSession->getUser();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return new JSONResponse(['error' => 'Not authenticated'], 401);
|
return new JSONResponse(['error' => 'Not authenticated'], 401);
|
||||||
@@ -189,8 +194,8 @@ class DbController extends Controller
|
|||||||
// Audit: log the fact that execution was attempted BEFORE any checks,
|
// Audit: log the fact that execution was attempted BEFORE any checks,
|
||||||
// so blocked/rejected queries are recorded in the log too, not just
|
// so blocked/rejected queries are recorded in the log too, not just
|
||||||
// the ones that succeeded.
|
// the ones that succeeded.
|
||||||
$this->logger->warning('[occweb] SQL submitted by {user}: {sql}', [
|
$this->logger->warning('[extended_occweb] SQL submitted by {user}: {sql}', [
|
||||||
'app' => 'occweb',
|
'app' => 'extended_occweb',
|
||||||
'user' => $user->getUID(),
|
'user' => $user->getUID(),
|
||||||
'sql' => $sql,
|
'sql' => $sql,
|
||||||
]);
|
]);
|
||||||
@@ -207,8 +212,8 @@ class DbController extends Controller
|
|||||||
foreach ($queries as $query) {
|
foreach ($queries as $query) {
|
||||||
$forbidden = $this->findForbiddenConstruct($query);
|
$forbidden = $this->findForbiddenConstruct($query);
|
||||||
if ($forbidden !== null) {
|
if ($forbidden !== null) {
|
||||||
$this->logger->error('[occweb] Blocked forbidden construct ({construct}) from {user}: {sql}', [
|
$this->logger->error('[extended_occweb] Blocked forbidden construct ({construct}) from {user}: {sql}', [
|
||||||
'app' => 'occweb',
|
'app' => 'extended_occweb',
|
||||||
'construct' => $forbidden,
|
'construct' => $forbidden,
|
||||||
'user' => $user->getUID(),
|
'user' => $user->getUID(),
|
||||||
'sql' => $sql,
|
'sql' => $sql,
|
||||||
@@ -277,8 +282,8 @@ class DbController extends Controller
|
|||||||
$this->db->beginTransaction();
|
$this->db->beginTransaction();
|
||||||
$transactionStarted = true;
|
$transactionStarted = true;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->warning('[occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
|
$this->logger->warning('[extended_occweb] beginTransaction() failed, proceeding without an explicit transaction: {error}', [
|
||||||
'app' => 'occweb',
|
'app' => 'extended_occweb',
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -353,8 +358,8 @@ class DbController extends Controller
|
|||||||
// permanently applied — we deliberately do NOT set
|
// permanently applied — we deliberately do NOT set
|
||||||
// rolledBack to true, since that would be untrue.
|
// rolledBack to true, since that would be untrue.
|
||||||
$rollbackFailed = true;
|
$rollbackFailed = true;
|
||||||
$this->logger->warning('[occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
|
$this->logger->warning('[extended_occweb] rollBack() failed after an error — earlier statements in this batch may already be permanently applied: {error}', [
|
||||||
'app' => 'occweb',
|
'app' => 'extended_occweb',
|
||||||
'error' => $rollbackError->getMessage(),
|
'error' => $rollbackError->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -371,8 +376,8 @@ class DbController extends Controller
|
|||||||
// a DDL statement on MySQL/MariaDB) — the effects are
|
// a DDL statement on MySQL/MariaDB) — the effects are
|
||||||
// already saved, this isn't an error in executing the batch
|
// already saved, this isn't an error in executing the batch
|
||||||
// itself.
|
// itself.
|
||||||
$this->logger->info('[occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
|
$this->logger->info('[extended_occweb] commit() had nothing to commit (likely auto-committed by a DDL statement): {error}', [
|
||||||
'app' => 'occweb',
|
'app' => 'extended_occweb',
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,17 @@ class OccController extends Controller
|
|||||||
|
|
||||||
private function requireAdmin(): ?JSONResponse
|
private function requireAdmin(): ?JSONResponse
|
||||||
{
|
{
|
||||||
// Services via OC::$server zodat de constructor-signatuur ongewijzigd blijft
|
// Deliberate defense-in-depth, not a workaround for a missing framework
|
||||||
// en NC's DI-container (zonder application.php) de class kan resolven.
|
// 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);
|
$userSession = OC::$server->get(\OCP\IUserSession::class);
|
||||||
$user = $userSession->getUser();
|
$user = $userSession->getUser();
|
||||||
if ($user === null) {
|
if ($user === null) {
|
||||||
@@ -70,7 +79,7 @@ class OccController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
if ($err = $this->requireAdmin()) return $err;
|
if ($err = $this->requireAdmin()) return $err;
|
||||||
return new TemplateResponse('occweb', 'index');
|
return new TemplateResponse('extended_occweb', 'index');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ class OccOutput extends BufferedOutput implements ConsoleOutputInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setErrorOutput(OutputInterface $error)
|
// NC35 bundles a Symfony Console version whose ConsoleOutputInterface
|
||||||
|
// declares this method `: void` - PHP 8 fatals ("Declaration must be
|
||||||
|
// compatible") without a matching return type here. Declaring `: void`
|
||||||
|
// is a valid narrowing of the untyped signature older Symfony/NC
|
||||||
|
// versions have, so this stays compatible with NC30-34 too.
|
||||||
|
public function setErrorOutput(OutputInterface $error): void
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-4
@@ -1,8 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
script('occweb', 'index');
|
// Nextcloud's global jQuery has been deprecated since NC19 and is no longer
|
||||||
script('occweb', 'term');
|
// reliably present on NC34+ (see e.g. serverinfo needing to bundle its own) -
|
||||||
script('occweb', 'unix_formatting');
|
// term.js (jquery.terminal) and index.js both require jQuery to be loaded
|
||||||
style('occweb', 'style');
|
// first, so ship and load our own copy where core no longer provides one.
|
||||||
|
//
|
||||||
|
// Only do this on NC34+: on NC32/33 core still exposes a working jQuery, but
|
||||||
|
// via a read-only `window.$` getter - loading our vendored copy there too
|
||||||
|
// makes its UMD wrapper throw trying to reassign `window.$` (confirmed via
|
||||||
|
// testbak/, see the README compat matrix). Harmless in practice (this app
|
||||||
|
// attaches via window.jQuery, not window.$) but a real, avoidable console
|
||||||
|
// exception, so skip the extra load where core's own jQuery already works.
|
||||||
|
if (\OCP\Server::get(\OCP\ServerVersion::class)->getMajorVersion() >= 34) {
|
||||||
|
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">
|
<div id="app" class="full-width">
|
||||||
|
|||||||
Reference in New Issue
Block a user