Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
013d22e
Add embedded static editor support
erseco Mar 7, 2026
9ba212b
Fix code
erseco Mar 9, 2026
8393540
Use embedded editor
erseco Mar 18, 2026
5bcdc5f
Add playground link
erseco Mar 18, 2026
13a9ced
Add playground link
erseco Mar 18, 2026
c943e30
Add playground link
erseco Mar 18, 2026
380b653
Use action-moodle-playground-pr-preview for PR previews
erseco Mar 28, 2026
246884e
Use pull_request trigger instead of pull_request_target
erseco Mar 28, 2026
83102b8
Simplify PR preview workflow using blueprint-file input
erseco Mar 28, 2026
3de27c1
Use Moodle Playground SVG button in README
erseco Mar 28, 2026
694d6fa
Skip PR preview on cross-fork PRs
erseco Mar 28, 2026
63bc20d
Add embedded editor management and local install support
erseco Mar 31, 2026
e60d4a1
Build PR playground blueprint from head branch
erseco Mar 31, 2026
bc02906
Use github-proxy on playground
erseco Mar 31, 2026
1b2b92d
Use github-proxy on playground
erseco Mar 31, 2026
8a1588f
Use MOODLE_PROXY_URL on playground
erseco Apr 1, 2026
d5b7637
Use MOODLE_PROXY_URL on playground
erseco Apr 1, 2026
37888bb
Better ajax refresh
erseco Apr 1, 2026
03c1e9b
Removed PROXY
erseco Apr 1, 2026
d5af988
Refresh box info
erseco Apr 1, 2026
60a3644
Clarified comments
erseco Apr 1, 2026
d0a7de1
Add example activity and student user to blueprint
erseco Apr 1, 2026
d12024f
Add extra-text warning, append-to-description mode, and sample ELPX c…
erseco Apr 2, 2026
d563e82
Simplify blueprint with addModule files support and append-to-descrip…
erseco Apr 2, 2026
6524b18
Set plugin display defaults in blueprint via setConfigs
erseco Apr 2, 2026
f9d43db
Add display field to blueprint addModule steps
erseco Apr 2, 2026
30e4c05
Show admin-aware error when embedded editor is not installed
erseco Apr 2, 2026
8bf39e3
Add target=_top to settings link so it opens outside the editor iframe
erseco Apr 2, 2026
d958e2f
Fix settings link opening inside editor iframe
erseco Apr 2, 2026
f5ba2dc
Simplify admin error to text-only message without link
erseco Apr 2, 2026
82b125d
Clean up: remove unused $cmid param, unused base string, fix formatting
erseco Apr 2, 2026
b253eb2
Replace progress bar with spinner and downloading message
erseco Apr 2, 2026
65c99c1
Fix trusted origins for postMessage in editor embedding config
erseco Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .distignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.git
.github
.gitignore
.aider*
.DS_Store
.distignore
.env
exelearning/
vendor/
node_modules/
phpmd-rules.xml
phpmd.xml
Makefile
docker-compose.yml
Dockerfile
composer.json
composer.lock
composer.phar
CLAUDE.md
AGENTS.md
mod_exeweb-*.zip
10 changes: 9 additions & 1 deletion .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ XDEBUG_CONFIG="client_host=host.docker.internal"

EXELEARNING_WEB_SOURCECODE_PATH=
EXELEARNING_WEB_CONTAINER_TAG=latest
EXELEARNING_EDITOR_REPO_URL=https://github.com/exelearning/exelearning.git
EXELEARNING_EDITOR_DEFAULT_BRANCH=main
EXELEARNING_EDITOR_REF=
EXELEARNING_EDITOR_REF_TYPE=auto

# To use a specific Moodle version, set MOODLE_VERSION to git release tag.
# You can find the list of available tags at:
# https://api.github.com/repos/moodle/moodle/tags
MOODLE_VERSION=v5.0.5

# Test user data
TEST_USER_EMAIL=user@exelearning.net
TEST_USER_USERNAME=user
TEST_USER_PASSWORD=1234

95 changes: 95 additions & 0 deletions .github/workflows/check-editor-releases.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: Check Editor Releases

on:
schedule:
- cron: "0 8 * * *" # Daily at 8:00 UTC
workflow_dispatch:

permissions:
contents: write
actions: write

jobs:
check_and_build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Get latest exelearning release
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Fetch latest release from exelearning/exelearning
LATEST=$(gh api repos/exelearning/exelearning/releases/latest --jq '.tag_name' 2>/dev/null || echo "")
if [ -z "$LATEST" ]; then
echo "No release found"
echo "found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Latest editor release: $LATEST"
echo "tag=$LATEST" >> $GITHUB_OUTPUT

# Check if we already built this version
MARKER_FILE=".editor-version"
CURRENT=""
if [ -f "$MARKER_FILE" ]; then
CURRENT=$(cat "$MARKER_FILE")
fi
echo "Current built version: $CURRENT"

if [ "$LATEST" = "$CURRENT" ]; then
echo "Already up to date"
echo "found=false" >> $GITHUB_OUTPUT
else
echo "New version available"
echo "found=true" >> $GITHUB_OUTPUT
fi

- name: Setup Bun
if: steps.check.outputs.found == 'true'
uses: oven-sh/setup-bun@v2

- name: Build static editor
if: steps.check.outputs.found == 'true'
env:
EXELEARNING_EDITOR_REPO_URL: https://github.com/exelearning/exelearning.git
EXELEARNING_EDITOR_REF: ${{ steps.check.outputs.tag }}
EXELEARNING_EDITOR_REF_TYPE: tag
run: make build-editor

- name: Compute version
if: steps.check.outputs.found == 'true'
id: version
run: |
TAG="${{ steps.check.outputs.tag }}"
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT

- name: Create package
if: steps.check.outputs.found == 'true'
run: make package RELEASE=${{ steps.version.outputs.version }}

- name: Update editor version marker
if: steps.check.outputs.found == 'true'
run: |
echo "${{ steps.check.outputs.tag }}" > .editor-version
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .editor-version
git commit -m "Update editor version to ${{ steps.check.outputs.tag }}"
git push

- name: Create GitHub Release
if: steps.check.outputs.found == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag }}
name: "${{ steps.version.outputs.tag }}"
body: |
Automated build with eXeLearning editor ${{ steps.version.outputs.tag }}.
files: mod_exeweb-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 changes: 44 additions & 0 deletions .github/workflows/pr-playground-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
name: PR Playground Preview

on:
pull_request:
types: [opened, synchronize, reopened]

concurrency:
group: playground-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
playground-preview:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Build PR blueprint
env:
PLUGIN_REPO_OWNER: ${{ github.event.pull_request.head.repo.owner.login }}
PLUGIN_REPO_NAME: ${{ github.event.pull_request.head.repo.name }}
PLUGIN_BRANCH_NAME: ${{ github.event.pull_request.head.ref }}
run: |
plugin_url="https://github.com/${PLUGIN_REPO_OWNER}/${PLUGIN_REPO_NAME}/archive/refs/heads/${PLUGIN_BRANCH_NAME}.zip"
sed "s|https://github.com/exelearning/mod_exeweb/archive/refs/heads/main.zip|${plugin_url}|" blueprint.json > blueprint.pr.json

- uses: ateeducacion/action-moodle-playground-pr-preview@main
with:
blueprint-file: blueprint.pr.json
mode: append-to-description
github-token: ${{ secrets.GITHUB_TOKEN }}
extra-text: >
⚠️ The embedded eXeLearning editor is not included in this preview.
You can install it from **Modules > eXeLearning Web > Configure**
using the "Download & Install Editor" button.
All other module features (ELPX upload, viewer, preview) work normally.
81 changes: 81 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: Release

on:
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: "Release label for package name (e.g. 1.2.3 or 1.2.3-beta)"
required: false
default: ""
editor_repo_url:
description: "Editor source repository URL"
required: false
default: "https://github.com/exelearning/exelearning.git"
editor_ref:
description: "Editor ref value (main, branch name, or tag)"
required: false
default: "main"
editor_ref_type:
description: "Type of editor ref"
required: false
default: "auto"
type: choice
options:
- auto
- branch
- tag

permissions:
contents: write

jobs:
build_and_upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2

- name: Set environment variables
run: |
if [ "${{ github.event_name }}" = "release" ]; then
RAW_TAG="${GITHUB_REF##*/}"
VERSION_TAG="${RAW_TAG#v}"
echo "RELEASE_TAG=${VERSION_TAG}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REPO_URL=https://github.com/exelearning/exelearning.git" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF=main" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF_TYPE=branch" >> $GITHUB_ENV
else
INPUT_RELEASE="${{ github.event.inputs.release_tag }}"
if [ -z "$INPUT_RELEASE" ]; then
INPUT_RELEASE="manual-$(date +%Y%m%d)-${GITHUB_SHA::7}"
fi
echo "RELEASE_TAG=${INPUT_RELEASE}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REPO_URL=${{ github.event.inputs.editor_repo_url }}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF=${{ github.event.inputs.editor_ref }}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF_TYPE=${{ github.event.inputs.editor_ref_type }}" >> $GITHUB_ENV
fi

- name: Build static editor
run: make build-editor

- name: Create package
run: make package RELEASE=${RELEASE_TAG}

- name: Upload ZIP as workflow artifact
uses: actions/upload-artifact@v4
with:
name: mod_exeweb-${{ env.RELEASE_TAG }}
path: mod_exeweb-${{ env.RELEASE_TAG }}.zip

- name: Upload ZIP to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: mod_exeweb-${{ env.RELEASE_TAG }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ phpmd-rules.xml
# Composer ignores
/vendor/
/composer.lock
/composer.phar
/composer.phar

# Built static editor files
dist/static/

# Local editor checkout fetched during build
exelearning/
.omc/
121 changes: 121 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

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.

**Component**: `mod_exeweb`
**Moodle compatibility**: 4.2+
**License**: GNU GPL v3+

## Common Commands

```bash
# Development environment (Docker-based)
make up # Start containers interactively
make upd # Start containers in background
make down # Stop containers
make shell # Shell into Moodle container
make clean # Stop containers + remove volumes

# PHP dependencies
make install-deps # composer install

# Code quality
make lint # PHP CodeSniffer (Moodle standard)
make fix # Auto-fix CodeSniffer violations
make phpmd # PHP Mess Detector

# Testing
make test # PHPUnit tests
make behat # Behat BDD tests

# Embedded editor
make build-editor # Fetch exelearning source + build to dist/static/
make clean-editor # Remove built editor artifacts

# Packaging
make package RELEASE=1.2.3 # Create distributable ZIP
```

Lint/test/phpmd/behat all delegate to `composer` scripts defined in `composer.json`. Run `make install-deps` first.

## Architecture

### Three Content Origins

| Constant | Mode | How it works |
|----------|------|-------------|
| `EXEWEB_ORIGIN_LOCAL` | Upload | ZIP package uploaded via form |
| `EXEWEB_ORIGIN_EXEONLINE` | Online | Redirects to remote eXeLearning; JWT-authenticated callbacks (`get_ode.php`/`set_ode.php`) sync the package |
| `EXEWEB_ORIGIN_EMBEDDED` | Embedded | Static HTML5 editor in iframe from `dist/static/`; bridge script + postMessage for Moodle ↔ editor communication |

### Key File Groups

- **Core Moodle hooks**: `lib.php`, `locallib.php` (display/rendering), `mod_form.php` (activity form), `settings.php` (admin config)
- **Editor integration**: `editor/index.php` (bootstrap), `editor/static.php` (asset server), `editor/save.php` (AJAX save)
- **Online editor**: `get_ode.php`, `set_ode.php`, `classes/exeonline/` (redirector, JWT token manager)
- **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)
- **Package handling**: `classes/exeweb_package.php` (validation, extraction)
- **Database**: `db/install.xml` (schema), `db/access.php` (capabilities), `db/upgrade.php` (migrations)
- **Backup/restore**: `backup/moodle2/`

### Embedded Editor: Hybrid Source Model

The embedded editor uses a **hybrid source model** with two possible locations:

1. **Bundled**: `dist/static/` inside the plugin (from release ZIP or `make build-editor`)
2. **Admin-installed**: `$CFG->dataroot/mod_exeweb/embedded_editor/` (downloaded from GitHub Releases via the admin management page)

**Source precedence**: moodledata (admin-installed) → bundled → not available

Key classes:
- `classes/local/embedded_editor_source_resolver.php` — single source of truth for which editor source is active
- `classes/local/embedded_editor_installer.php` — download/install pipeline from GitHub Releases

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.

**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.

Key files:
- `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`)
- `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).
- `templates/admin_embedded_editor.mustache` — Inline widget template (status card, action buttons, progress bar, result area)
- `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

**Design decisions & rationale**:
- **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.
- **Why indeterminate progress bar**: The installer doesn't expose byte-level progress. Operation typically takes 10-30s. Animated Bootstrap stripes are sufficient.
- **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.
- **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.
- **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.
- **Both capabilities required**: `moodle/site:config` AND `mod/exeweb:manageembeddededitor`, matching the original `admin_externalpage` registration.

### Embedded Editor Build (Development)

`dist/static/` is **not committed**. It's built from the `exelearning/exelearning` repo:
- `make build-editor` shallow-clones the source (configured via `.env` or env vars: `EXELEARNING_EDITOR_REPO_URL`, `EXELEARNING_EDITOR_REF`, `EXELEARNING_EDITOR_REF_TYPE`)
- Builds with Bun (`bun install && bun run build:static`)
- Copies output to `dist/static/`

### File Storage

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.

### Capabilities

`mod/exeweb:view`, `mod/exeweb:addinstance`, `mod/exeweb:manageembeddededitor`

## Code Standards

- **PHP**: Moodle coding standard enforced via PHP CodeSniffer (`make lint`/`make fix`)
- **Strings**: All UI strings in `lang/{ca,en,es,eu,gl}/exeweb.php` — use `get_string('key', 'mod_exeweb')`
- **JS**: AMD modules in `amd/src/`, compiled to `amd/build/` (exception: `moodle_exe_bridge.js` loads raw in editor iframe)

## Packaging & Release

- `make package RELEASE=X.Y.Z` updates `version.php`, creates ZIP excluding files in `.distignore`, then restores dev values
- GitHub Actions `release.yml` triggers on git tags: fetches editor, builds, packages, uploads to GitHub Release
- `check-editor-releases.yml` runs daily to auto-release when new editor versions appear
Loading