Skip to content

Commit 2cf4414

Browse files
committed
Add embedded static eXeLearning editor support
Add a fully integrated embedded eXeLearning editor that runs inside Moodle, allowing teachers to create and edit web content without leaving the platform. Key changes: - Editor bootstrap (editor/index.php, editor/static.php, editor/save.php) that loads the static editor build inside an iframe with postMessage bridge for save/load communication - Admin settings widget to install/update/uninstall the editor from GitHub releases, with spinner UX and multilingual status messages - External API (manage_embedded_editor) for install/status AJAX actions - Source resolver with moodledata > bundled precedence policy - PostMessage bridge (moodle_exe_bridge.js) handling OPEN_FILE, REQUEST_EXPORT, and document change tracking via yjs - Admin-aware error messages: admins see "install from plugin settings", non-admins see "contact your administrator" - Fix trusted origins for postMessage (use scheme+host only) - Blueprint with all default settings and sample activities - Translations for en, es, ca, eu, gl - Capability mod/exeweb:manageembeddededitor - CI: Playground PR preview support
1 parent a9ecb24 commit 2cf4414

57 files changed

Lines changed: 7353 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.distignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.git
2+
.github
3+
.gitignore
4+
.aider*
5+
.DS_Store
6+
.distignore
7+
.env
8+
exelearning/
9+
vendor/
10+
node_modules/
11+
phpmd-rules.xml
12+
phpmd.xml
13+
Makefile
14+
docker-compose.yml
15+
Dockerfile
16+
composer.json
17+
composer.lock
18+
composer.phar
19+
CLAUDE.md
20+
AGENTS.md
21+
mod_exeweb-*.zip

.env.dist

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ XDEBUG_CONFIG="client_host=host.docker.internal"
1313

1414
EXELEARNING_WEB_SOURCECODE_PATH=
1515
EXELEARNING_WEB_CONTAINER_TAG=latest
16+
EXELEARNING_EDITOR_REPO_URL=https://github.com/exelearning/exelearning.git
17+
EXELEARNING_EDITOR_DEFAULT_BRANCH=main
18+
EXELEARNING_EDITOR_REF=
19+
EXELEARNING_EDITOR_REF_TYPE=auto
20+
21+
# To use a specific Moodle version, set MOODLE_VERSION to git release tag.
22+
# You can find the list of available tags at:
23+
# https://api.github.com/repos/moodle/moodle/tags
24+
MOODLE_VERSION=v5.0.5
1625

1726
# Test user data
1827
TEST_USER_EMAIL=user@exelearning.net
1928
TEST_USER_USERNAME=user
2029
TEST_USER_PASSWORD=1234
21-
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
name: Check Editor Releases
3+
4+
on:
5+
schedule:
6+
- cron: "0 8 * * *" # Daily at 8:00 UTC
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
actions: write
12+
13+
jobs:
14+
check_and_build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Get latest exelearning release
20+
id: check
21+
env:
22+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23+
run: |
24+
# Fetch latest release from exelearning/exelearning
25+
LATEST=$(gh api repos/exelearning/exelearning/releases/latest --jq '.tag_name' 2>/dev/null || echo "")
26+
if [ -z "$LATEST" ]; then
27+
echo "No release found"
28+
echo "found=false" >> $GITHUB_OUTPUT
29+
exit 0
30+
fi
31+
echo "Latest editor release: $LATEST"
32+
echo "tag=$LATEST" >> $GITHUB_OUTPUT
33+
34+
# Check if we already built this version
35+
MARKER_FILE=".editor-version"
36+
CURRENT=""
37+
if [ -f "$MARKER_FILE" ]; then
38+
CURRENT=$(cat "$MARKER_FILE")
39+
fi
40+
echo "Current built version: $CURRENT"
41+
42+
if [ "$LATEST" = "$CURRENT" ]; then
43+
echo "Already up to date"
44+
echo "found=false" >> $GITHUB_OUTPUT
45+
else
46+
echo "New version available"
47+
echo "found=true" >> $GITHUB_OUTPUT
48+
fi
49+
50+
- name: Setup Bun
51+
if: steps.check.outputs.found == 'true'
52+
uses: oven-sh/setup-bun@v2
53+
54+
- name: Build static editor
55+
if: steps.check.outputs.found == 'true'
56+
env:
57+
EXELEARNING_EDITOR_REPO_URL: https://github.com/exelearning/exelearning.git
58+
EXELEARNING_EDITOR_REF: ${{ steps.check.outputs.tag }}
59+
EXELEARNING_EDITOR_REF_TYPE: tag
60+
run: make build-editor
61+
62+
- name: Compute version
63+
if: steps.check.outputs.found == 'true'
64+
id: version
65+
run: |
66+
TAG="${{ steps.check.outputs.tag }}"
67+
VERSION="${TAG#v}"
68+
echo "version=$VERSION" >> $GITHUB_OUTPUT
69+
echo "tag=$TAG" >> $GITHUB_OUTPUT
70+
71+
- name: Create package
72+
if: steps.check.outputs.found == 'true'
73+
run: make package RELEASE=${{ steps.version.outputs.version }}
74+
75+
- name: Update editor version marker
76+
if: steps.check.outputs.found == 'true'
77+
run: |
78+
echo "${{ steps.check.outputs.tag }}" > .editor-version
79+
git config user.name "github-actions[bot]"
80+
git config user.email "github-actions[bot]@users.noreply.github.com"
81+
git add .editor-version
82+
git commit -m "Update editor version to ${{ steps.check.outputs.tag }}"
83+
git push
84+
85+
- name: Create GitHub Release
86+
if: steps.check.outputs.found == 'true'
87+
uses: softprops/action-gh-release@v2
88+
with:
89+
tag_name: ${{ steps.version.outputs.tag }}
90+
name: "${{ steps.version.outputs.tag }}"
91+
body: |
92+
Automated build with eXeLearning editor ${{ steps.version.outputs.tag }}.
93+
files: mod_exeweb-${{ steps.version.outputs.version }}.zip
94+
env:
95+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
name: PR Playground Preview
3+
4+
on:
5+
pull_request:
6+
types: [opened, synchronize, reopened]
7+
8+
concurrency:
9+
group: playground-${{ github.event.pull_request.number }}
10+
cancel-in-progress: true
11+
12+
permissions:
13+
contents: read
14+
pull-requests: write
15+
16+
jobs:
17+
playground-preview:
18+
if: github.event.pull_request.head.repo.full_name == github.repository
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 2
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
ref: ${{ github.event.pull_request.head.sha }}
25+
26+
- name: Build PR blueprint
27+
env:
28+
PLUGIN_REPO_OWNER: ${{ github.event.pull_request.head.repo.owner.login }}
29+
PLUGIN_REPO_NAME: ${{ github.event.pull_request.head.repo.name }}
30+
PLUGIN_BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
31+
run: |
32+
plugin_url="https://github.com/${PLUGIN_REPO_OWNER}/${PLUGIN_REPO_NAME}/archive/refs/heads/${PLUGIN_BRANCH_NAME}.zip"
33+
sed "s|https://github.com/exelearning/mod_exeweb/archive/refs/heads/main.zip|${plugin_url}|" blueprint.json > blueprint.pr.json
34+
35+
- uses: ateeducacion/action-moodle-playground-pr-preview@main
36+
with:
37+
blueprint-file: blueprint.pr.json
38+
mode: append-to-description
39+
github-token: ${{ secrets.GITHUB_TOKEN }}
40+
extra-text: >
41+
⚠️ The embedded eXeLearning editor is not included in this preview.
42+
You can install it from **Modules > eXeLearning Web > Configure**
43+
using the "Download & Install Editor" button.
44+
All other module features (ELPX upload, viewer, preview) work normally.

.github/workflows/release.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
name: Release
3+
4+
on:
5+
release:
6+
types: [published]
7+
workflow_dispatch:
8+
inputs:
9+
release_tag:
10+
description: "Release label for package name (e.g. 1.2.3 or 1.2.3-beta)"
11+
required: false
12+
default: ""
13+
editor_repo_url:
14+
description: "Editor source repository URL"
15+
required: false
16+
default: "https://github.com/exelearning/exelearning.git"
17+
editor_ref:
18+
description: "Editor ref value (main, branch name, or tag)"
19+
required: false
20+
default: "main"
21+
editor_ref_type:
22+
description: "Type of editor ref"
23+
required: false
24+
default: "auto"
25+
type: choice
26+
options:
27+
- auto
28+
- branch
29+
- tag
30+
31+
permissions:
32+
contents: write
33+
34+
jobs:
35+
build_and_upload:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Setup Bun
41+
uses: oven-sh/setup-bun@v2
42+
43+
- name: Set environment variables
44+
run: |
45+
if [ "${{ github.event_name }}" = "release" ]; then
46+
RAW_TAG="${GITHUB_REF##*/}"
47+
VERSION_TAG="${RAW_TAG#v}"
48+
echo "RELEASE_TAG=${VERSION_TAG}" >> $GITHUB_ENV
49+
echo "EXELEARNING_EDITOR_REPO_URL=https://github.com/exelearning/exelearning.git" >> $GITHUB_ENV
50+
echo "EXELEARNING_EDITOR_REF=main" >> $GITHUB_ENV
51+
echo "EXELEARNING_EDITOR_REF_TYPE=branch" >> $GITHUB_ENV
52+
else
53+
INPUT_RELEASE="${{ github.event.inputs.release_tag }}"
54+
if [ -z "$INPUT_RELEASE" ]; then
55+
INPUT_RELEASE="manual-$(date +%Y%m%d)-${GITHUB_SHA::7}"
56+
fi
57+
echo "RELEASE_TAG=${INPUT_RELEASE}" >> $GITHUB_ENV
58+
echo "EXELEARNING_EDITOR_REPO_URL=${{ github.event.inputs.editor_repo_url }}" >> $GITHUB_ENV
59+
echo "EXELEARNING_EDITOR_REF=${{ github.event.inputs.editor_ref }}" >> $GITHUB_ENV
60+
echo "EXELEARNING_EDITOR_REF_TYPE=${{ github.event.inputs.editor_ref_type }}" >> $GITHUB_ENV
61+
fi
62+
63+
- name: Build static editor
64+
run: make build-editor
65+
66+
- name: Create package
67+
run: make package RELEASE=${RELEASE_TAG}
68+
69+
- name: Upload ZIP as workflow artifact
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: mod_exeweb-${{ env.RELEASE_TAG }}
73+
path: mod_exeweb-${{ env.RELEASE_TAG }}.zip
74+
75+
- name: Upload ZIP to release
76+
if: github.event_name == 'release'
77+
uses: softprops/action-gh-release@v2
78+
with:
79+
files: mod_exeweb-${{ env.RELEASE_TAG }}.zip
80+
env:
81+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,11 @@ phpmd-rules.xml
77
# Composer ignores
88
/vendor/
99
/composer.lock
10-
/composer.phar
10+
/composer.phar
11+
12+
# Built static editor files
13+
dist/static/
14+
15+
# Local editor checkout fetched during build
16+
exelearning/
17+
.omc/

AGENTS.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Moodle activity module (`mod_exeweb`) for creating and editing web sites with eXeLearning. Teachers can author educational content via three modes: local upload, eXeLearning Online (remote editor), or an embedded static editor.
8+
9+
**Component**: `mod_exeweb`
10+
**Moodle compatibility**: 4.2+
11+
**License**: GNU GPL v3+
12+
13+
## Common Commands
14+
15+
```bash
16+
# Development environment (Docker-based)
17+
make up # Start containers interactively
18+
make upd # Start containers in background
19+
make down # Stop containers
20+
make shell # Shell into Moodle container
21+
make clean # Stop containers + remove volumes
22+
23+
# PHP dependencies
24+
make install-deps # composer install
25+
26+
# Code quality
27+
make lint # PHP CodeSniffer (Moodle standard)
28+
make fix # Auto-fix CodeSniffer violations
29+
make phpmd # PHP Mess Detector
30+
31+
# Testing
32+
make test # PHPUnit tests
33+
make behat # Behat BDD tests
34+
35+
# Embedded editor
36+
make build-editor # Fetch exelearning source + build to dist/static/
37+
make clean-editor # Remove built editor artifacts
38+
39+
# Packaging
40+
make package RELEASE=1.2.3 # Create distributable ZIP
41+
```
42+
43+
Lint/test/phpmd/behat all delegate to `composer` scripts defined in `composer.json`. Run `make install-deps` first.
44+
45+
## Architecture
46+
47+
### Three Content Origins
48+
49+
| Constant | Mode | How it works |
50+
|----------|------|-------------|
51+
| `EXEWEB_ORIGIN_LOCAL` | Upload | ZIP package uploaded via form |
52+
| `EXEWEB_ORIGIN_EXEONLINE` | Online | Redirects to remote eXeLearning; JWT-authenticated callbacks (`get_ode.php`/`set_ode.php`) sync the package |
53+
| `EXEWEB_ORIGIN_EMBEDDED` | Embedded | Static HTML5 editor in iframe from `dist/static/`; bridge script + postMessage for Moodle ↔ editor communication |
54+
55+
### Key File Groups
56+
57+
- **Core Moodle hooks**: `lib.php`, `locallib.php` (display/rendering), `mod_form.php` (activity form), `settings.php` (admin config)
58+
- **Editor integration**: `editor/index.php` (bootstrap), `editor/static.php` (asset server), `editor/save.php` (AJAX save)
59+
- **Online editor**: `get_ode.php`, `set_ode.php`, `classes/exeonline/` (redirector, JWT token manager)
60+
- **Frontend JS** (AMD): `amd/src/editor_modal.js` (fullscreen overlay), `amd/src/admin_embedded_editor.js` (settings page AJAX + progress), `amd/src/moodle_exe_bridge.js` (runs inside editor iframe, raw — not AMD)
61+
- **Package handling**: `classes/exeweb_package.php` (validation, extraction)
62+
- **Database**: `db/install.xml` (schema), `db/access.php` (capabilities), `db/upgrade.php` (migrations)
63+
- **Backup/restore**: `backup/moodle2/`
64+
65+
### Embedded Editor: Hybrid Source Model
66+
67+
The embedded editor uses a **hybrid source model** with two possible locations:
68+
69+
1. **Bundled**: `dist/static/` inside the plugin (from release ZIP or `make build-editor`)
70+
2. **Admin-installed**: `$CFG->dataroot/mod_exeweb/embedded_editor/` (downloaded from GitHub Releases via the admin management page)
71+
72+
**Source precedence**: moodledata (admin-installed) → bundled → not available
73+
74+
Key classes:
75+
- `classes/local/embedded_editor_source_resolver.php` — single source of truth for which editor source is active
76+
- `classes/local/embedded_editor_installer.php` — download/install pipeline from GitHub Releases
77+
78+
The resolver is used by `lib.php` helper functions (`exeweb_get_embedded_editor_local_static_dir()`, `exeweb_embedded_editor_uses_local_assets()`, `exeweb_get_embedded_editor_index_source()`), which in turn are used by `editor/static.php` (asset proxy) and `editor/index.php` (bootstrap). This makes the source transparent to the rest of the codebase.
79+
80+
**Admin management (inline settings widget)**: Editor management is integrated directly into the plugin's admin settings page via a custom `admin_setting` subclass — no separate page. Actions (install/update/repair/uninstall) use AJAX external functions; a JS AMD module handles progress display and timeout resilience.
81+
82+
Key files:
83+
- `classes/external/manage_embedded_editor.php` — AJAX external functions (`execute_action` + `get_status`), modern `\core_external\external_api` pattern (Moodle 4.2+, PSR-4 namespaced `\mod_exeweb\external\manage_embedded_editor`)
84+
- `classes/admin/admin_setting_embeddededitor.php` — Custom `admin_setting` subclass (PSR-4 namespaced `\mod_exeweb\admin\admin_setting_embeddededitor`), renders inline widget in settings page. `get_setting()`/`write_setting()` return empty string (display-only, no config stored).
85+
- `templates/admin_embedded_editor.mustache` — Inline widget template (status card, action buttons, progress bar, result area)
86+
- `amd/src/admin_embedded_editor.js` — AMD module: calls `get_status(checklatest=true)` on page load (async GitHub API check), handles action AJAX calls with 120s JS timeout, falls back to status polling every 10s using `CONFIG_INSTALLING` lock, polling capped at 5 minutes
87+
88+
**Design decisions & rationale**:
89+
- **Why AJAX instead of synchronous POST**: Keeps user on settings page; the old `manage_embedded_editor.php` navigated away. POST with `NO_OUTPUT_BUFFERING` progress bar was considered but rejected because it still navigates away.
90+
- **Why indeterminate progress bar**: The installer doesn't expose byte-level progress. Operation typically takes 10-30s. Animated Bootstrap stripes are sufficient.
91+
- **Why 120s JS timeout + polling**: Reverse proxies (nginx default 60s, Apache 60-120s) may kill long AJAX requests for ~50MB ZIP downloads. The existing `CONFIG_INSTALLING` lock (300s TTL) serves as a "still running" signal. JS polls `get_status` after timeout to distinguish "still running" / "completed" / "failed". Stale lock (>300s) is detected and reported.
92+
- **Why GitHub API only from JS**: `discover_latest_version()` hits GitHub API (60 req/hr unauthenticated). Calling it on PHP render would fire on every settings page visit. JS calls it once async after page load.
93+
- **Why modern \core_external\external_api**: Plugin minimum is Moodle 4.2+ (`version.php`). Uses PSR-4 namespaced classes at `classes/external/`. The legacy `classes/external.php` (Frankenstyle) remains for existing external functions but new code uses the modern pattern.
94+
- **Both capabilities required**: `moodle/site:config` AND `mod/exeweb:manageembeddededitor`, matching the original `admin_externalpage` registration.
95+
96+
### Embedded Editor Build (Development)
97+
98+
`dist/static/` is **not committed**. It's built from the `exelearning/exelearning` repo:
99+
- `make build-editor` shallow-clones the source (configured via `.env` or env vars: `EXELEARNING_EDITOR_REPO_URL`, `EXELEARNING_EDITOR_REF`, `EXELEARNING_EDITOR_REF_TYPE`)
100+
- Builds with Bun (`bun install && bun run build:static`)
101+
- Copies output to `dist/static/`
102+
103+
### File Storage
104+
105+
Uses Moodle's file API — packages stored in `mod_exeweb/package` filearea, expanded content in `mod_exeweb/content`. Revision number in URLs for cache busting. Never serve files directly from disk.
106+
107+
### Capabilities
108+
109+
`mod/exeweb:view`, `mod/exeweb:addinstance`, `mod/exeweb:manageembeddededitor`
110+
111+
## Code Standards
112+
113+
- **PHP**: Moodle coding standard enforced via PHP CodeSniffer (`make lint`/`make fix`)
114+
- **Strings**: All UI strings in `lang/{ca,en,es,eu,gl}/exeweb.php` — use `get_string('key', 'mod_exeweb')`
115+
- **JS**: AMD modules in `amd/src/`, compiled to `amd/build/` (exception: `moodle_exe_bridge.js` loads raw in editor iframe)
116+
117+
## Packaging & Release
118+
119+
- `make package RELEASE=X.Y.Z` updates `version.php`, creates ZIP excluding files in `.distignore`, then restores dev values
120+
- GitHub Actions `release.yml` triggers on git tags: fetches editor, builds, packages, uploads to GitHub Release
121+
- `check-editor-releases.yml` runs daily to auto-release when new editor versions appear

0 commit comments

Comments
 (0)