mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-19 14:04:06 -04:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd099426bd | |||
| d02dfcf7d6 | |||
| 8a59eb6977 | |||
| 7ee720fee4 | |||
| 5e83b9f43a | |||
| 9da6a85ad8 | |||
| 51b1097509 | |||
| 95644dc961 | |||
| a3fe375a83 | |||
| 777b06bbeb | |||
| e89c4a3c85 | |||
| 7a02d25d0b | |||
| c7a673bbbf | |||
| b0c0d9a83f | |||
| ae63a5af64 | |||
| 98d5d6af8b | |||
| 9302559d4b | |||
| 279cccb980 | |||
| 25e91e9d5e | |||
| d6a11514e5 | |||
| c7a1d60d73 | |||
| 6ee5bef6ce | |||
| 2e01000559 | |||
| 4f11addcfd | |||
| b11f2f561c | |||
| b6da9c4662 | |||
| 10c0782fe0 | |||
| 318dad0e5d | |||
| 09ead87884 | |||
| 559977f9bc | |||
| 35de017562 | |||
| c19c5207e9 |
@@ -24,7 +24,6 @@ If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu Linux]
|
||||
- Installation method: [Docker install, or single server install]
|
||||
- Browser, if applicable
|
||||
|
||||
**Additional context**
|
||||
|
||||
@@ -37,3 +37,5 @@ frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
|
||||
static/chapters_editor/videos/sample-video.mp3
|
||||
static/video_editor/videos/sample-video.mp3
|
||||
templates/todo-MS4.md
|
||||
.secret_key
|
||||
.secret_key.lock
|
||||
|
||||
@@ -1,5 +1,95 @@
|
||||
# Changelog
|
||||
|
||||
## [8.3.0](https://github.com/mediacms-io/mediacms/compare/v8.2.1...v8.3.0) (2026-06-19)
|
||||
|
||||
### Features
|
||||
|
||||
* split moodle plugin into 2 ([d02dfcf](https://github.com/mediacms-io/mediacms/commit/d02dfcf7d60c4c26ce222b58cd2a178e12f6b78a))
|
||||
|
||||
### Documentation
|
||||
|
||||
* Moodle plugin doc ([#1539](https://github.com/mediacms-io/mediacms/issues/1539)) ([8a59eb6](https://github.com/mediacms-io/mediacms/commit/8a59eb6977c48c0c63d586332db4921fc67dba18))
|
||||
* Moodle Plugin Documentation ([#1537](https://github.com/mediacms-io/mediacms/issues/1537)) ([7ee720f](https://github.com/mediacms-io/mediacms/commit/7ee720fee4ac1d416bf1614b7447a987a018bb01))
|
||||
|
||||
## [8.2.1](https://github.com/mediacms-io/mediacms/compare/v8.2.0...v8.2.1) (2026-06-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SAML provider add guard to skip empty mappings before iterating ([#1536](https://github.com/mediacms-io/mediacms/issues/1536)) ([9da6a85](https://github.com/mediacms-io/mediacms/commit/9da6a85ad86f5092edb96495eeb1cca22d5334bf))
|
||||
|
||||
## [8.2.0](https://github.com/mediacms-io/mediacms/compare/v8.1.3...v8.2.0) (2026-05-31)
|
||||
|
||||
### Features
|
||||
|
||||
* configure SP certificate and private key via SAMLConfiguration ([#1531](https://github.com/mediacms-io/mediacms/issues/1531)) ([95644dc](https://github.com/mediacms-io/mediacms/commit/95644dc9615f428191d9fda0847c1b91a0b094a5))
|
||||
|
||||
## [8.1.3](https://github.com/mediacms-io/mediacms/compare/v8.1.2...v8.1.3) (2026-05-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* django connection settings ([#1529](https://github.com/mediacms-io/mediacms/issues/1529)) ([e89c4a3](https://github.com/mediacms-io/mediacms/commit/e89c4a3c8523574b5852a434ed67e281b6290584))
|
||||
* prestart.sh loaddata re-runs on every container restart ([#1502](https://github.com/mediacms-io/mediacms/issues/1502)) ([777b06b](https://github.com/mediacms-io/mediacms/commit/777b06bbebf141e5b1cb27e17533fe65d57eb6cd))
|
||||
|
||||
## [8.1.2](https://github.com/mediacms-io/mediacms/compare/v8.1.1...v8.1.2) (2026-05-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove redundant check ([#1528](https://github.com/mediacms-io/mediacms/issues/1528)) ([c7a673b](https://github.com/mediacms-io/mediacms/commit/c7a673bbbf46efc37621dc4a5109a85fc10e1317))
|
||||
|
||||
## [8.1.1](https://github.com/mediacms-io/mediacms/compare/v8.1.0...v8.1.1) (2026-05-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* x-accell headers on uploaded poster ([#1526](https://github.com/mediacms-io/mediacms/issues/1526)) ([ae63a5a](https://github.com/mediacms-io/mediacms/commit/ae63a5af647c8865b96e6e50dda1ea9d29b5bd0b))
|
||||
|
||||
## [8.1.0](https://github.com/mediacms-io/mediacms/compare/v8.0.8...v8.1.0) (2026-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
* introduce x-accell headers ([9302559](https://github.com/mediacms-io/mediacms/commit/9302559d4bb3e4d0adb299ed37438b04c39e1864))
|
||||
|
||||
## [8.0.8](https://github.com/mediacms-io/mediacms/compare/v8.0.7...v8.0.8) (2026-05-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update documentation and fix smaller issues ([#1520](https://github.com/mediacms-io/mediacms/issues/1520)) ([d6a1151](https://github.com/mediacms-io/mediacms/commit/d6a11514e54b9341ec8a306a259adce6b4199d42))
|
||||
|
||||
## [8.0.7](https://github.com/mediacms-io/mediacms/compare/v8.0.6...v8.0.7) (2026-05-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bring related items back ([#1515](https://github.com/mediacms-io/mediacms/issues/1515)) ([6ee5bef](https://github.com/mediacms-io/mediacms/commit/6ee5bef6ce31cf849941f65d0817e53b8f03362f))
|
||||
|
||||
## [8.0.6](https://github.com/mediacms-io/mediacms/compare/v8.0.5...v8.0.6) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* better place secret key settings ([4f11add](https://github.com/mediacms-io/mediacms/commit/4f11addcfd6657e7e63eed0570b1d4d9bca75698))
|
||||
|
||||
## [8.0.5](https://github.com/mediacms-io/mediacms/compare/v8.0.4...v8.0.5) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add secret key to gitignore ([b6da9c4](https://github.com/mediacms-io/mediacms/commit/b6da9c4662b3fba234b8dc69700ffa44fced7482))
|
||||
|
||||
## [8.0.4](https://github.com/mediacms-io/mediacms/compare/v8.0.3...v8.0.4) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* docker compose settings, provide key ([318dad0](https://github.com/mediacms-io/mediacms/commit/318dad0e5d2512d68068c019eb87f942f83318e9))
|
||||
|
||||
## [8.0.3](https://github.com/mediacms-io/mediacms/compare/v8.0.2...v8.0.3) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* secret key ([559977f](https://github.com/mediacms-io/mediacms/commit/559977f9bc74412739784926862b94a558e6fd84))
|
||||
|
||||
## [8.0.2](https://github.com/mediacms-io/mediacms/compare/v8.0.1...v8.0.2) (2026-05-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* provide Bento4 url ([c19c520](https://github.com/mediacms-io/mediacms/commit/c19c5207e907cbd7d0c968d8cd8caaec20706277))
|
||||
|
||||
## [7.7.0](https://github.com/mediacms-io/mediacms/compare/v7.6.0...v7.7.0) (2026-05-11)
|
||||
|
||||
### Features
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ RUN mkdir -p ffmpeg-tmp && \
|
||||
|
||||
# Install Bento4 in the specified location
|
||||
RUN mkdir -p /home/mediacms.io/bento4 && \
|
||||
wget -q http://zebulon.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||
wget -q --tries=5 --waitretry=10 --timeout=30 https://www.bok.net/Bento4/binaries/Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip && \
|
||||
unzip Bento4-SDK-1-6-0-637.x86_64-unknown-linux.zip -d /home/mediacms.io/bento4 && \
|
||||
mv /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/* /home/mediacms.io/bento4/ && \
|
||||
rm -rf /home/mediacms.io/bento4/Bento4-SDK-1-6-0-637.x86_64-unknown-linux && \
|
||||
|
||||
@@ -43,7 +43,7 @@ A demo is available at https://demo.mediacms.io
|
||||
- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9)
|
||||
- **Adaptive video streaming**: possible through HLS protocol
|
||||
- **Subtitles/CC**: support for multilingual subtitle files
|
||||
- **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers
|
||||
- **Scalable transcoding**: transcoding through priorities.
|
||||
- **Chunked file uploads**: for pausable/resumable upload of content
|
||||
- **REST API**: Documented through Swagger
|
||||
- **Translation**: Most of the CMS is translated to a number of languages
|
||||
@@ -91,7 +91,6 @@ In order to support automatic transcriptions through Whisper, consider more CPUs
|
||||
|
||||
There are two ways to run MediaCMS, through Docker Compose and through installing it on a server via an automation script that installs and configures all needed services. Find the related pages:
|
||||
|
||||
- [Single Server](docs/admins_docs.md#2-server-installation) page
|
||||
- [Docker Compose](docs/admins_docs.md#3-docker-installation) page
|
||||
|
||||
A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b).
|
||||
@@ -105,6 +104,7 @@ There are two ways to run MediaCMS, through Docker Compose and through installin
|
||||
* [Transcoding](docs/transcoding.md) page
|
||||
* [Developer Experience](docs/dev_exp.md) page
|
||||
* [Media Permissions](docs/media_permissions.md) page
|
||||
* [Moodle Plugin](docs/moodle_plugin.md) page
|
||||
|
||||
|
||||
## Technology
|
||||
|
||||
@@ -3,7 +3,9 @@ from __future__ import absolute_import
|
||||
import os
|
||||
|
||||
from celery import Celery
|
||||
from celery.signals import worker_process_init
|
||||
from django.conf import settings
|
||||
from django.db import connections
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cms.settings")
|
||||
app = Celery("cms")
|
||||
@@ -20,3 +22,13 @@ app.conf.task_always_eager = settings.CELERY_TASK_ALWAYS_EAGER
|
||||
|
||||
|
||||
app.conf.worker_prefetch_multiplier = 1
|
||||
|
||||
|
||||
@worker_process_init.connect
|
||||
def close_db_pool_on_fork(**_):
|
||||
# psycopg3's ConnectionPool is not fork-safe: children inherit dead sockets
|
||||
# from the parent's pool and block on getconn() until PoolTimeout. Dispose
|
||||
# the inherited pool here so each prefork child opens its own on first use.
|
||||
# NB: plain conn.close() would only putconn() back into the broken pool.
|
||||
for conn in connections.all():
|
||||
conn.close_pool()
|
||||
|
||||
+41
-18
@@ -171,8 +171,19 @@ REST_FRAMEWORK = {
|
||||
}
|
||||
|
||||
|
||||
SECRET_KEY = "2dii4cog7k=5n37$fz)8dst)kg(s3&10)^qa*gv(kk+nv-z&cu"
|
||||
# TODO: this needs to be changed!
|
||||
# In docker, deploy/docker/entrypoint.sh ensures the SECRET_KEY env var is
|
||||
# set (generating .secret_key once on first start if needed). Outside docker,
|
||||
# either set SECRET_KEY in the environment or create a .secret_key file at the
|
||||
# project root, e.g.:
|
||||
# python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > .secret_key
|
||||
SECRET_KEY = os.getenv('SECRET_KEY')
|
||||
if not SECRET_KEY:
|
||||
_secret_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.secret_key')
|
||||
if os.path.exists(_secret_path):
|
||||
with open(_secret_path) as _f:
|
||||
SECRET_KEY = _f.read().strip()
|
||||
if not SECRET_KEY:
|
||||
raise RuntimeError("SECRET_KEY is not set. Set the SECRET_KEY env var or create a .secret_key file at the project root.")
|
||||
|
||||
TEMP_DIRECTORY = "/tmp" # Don't use a temp directory inside BASE_DIR!!!
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -191,6 +202,15 @@ THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
|
||||
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
||||
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
||||
|
||||
# Protect media files via nginx auth_request
|
||||
# When True, nginx delegates authorization for /media/<protected>/... to a
|
||||
# Django endpoint that checks the Media's state and the user's access.
|
||||
USE_X_ACCEL_REDIRECT = True
|
||||
# Subdirectories of MEDIA_ROOT that should be gated. "chunks" is intentionally
|
||||
# omitted (upload state, not playback).
|
||||
X_ACCEL_PROTECTED_PATHS = ["encoded", "hls", "original"]
|
||||
X_ACCEL_AUTH_CACHE_SECONDS = 300
|
||||
|
||||
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
||||
FFPROBE_COMMAND = "ffprobe" # this is the path
|
||||
MP4HLS = "mp4hls"
|
||||
@@ -255,21 +275,6 @@ CANNOT_ADD_MEDIA_MESSAGE = "User cannot add media, or maximum number of media up
|
||||
# mp4hls command, part of Bento4
|
||||
MP4HLS_COMMAND = "/home/mediacms.io/mediacms/Bento4-SDK-1-6-0-637.x86_64-unknown-linux/bin/mp4hls"
|
||||
|
||||
# highly experimental, related with remote workers
|
||||
ADMIN_TOKEN = ""
|
||||
# this is used by remote workers to push
|
||||
# encodings once they are done
|
||||
# USE_BASIC_HTTP = True
|
||||
# BASIC_HTTP_USER_PAIR = ('user', 'password')
|
||||
# specify basic auth user/password pair for use with the
|
||||
# remote workers, if nginx basic auth is setup
|
||||
# apache2-utils need be installed
|
||||
# then run
|
||||
# htpasswd -c /home/mediacms.io/mediacms/deploy/.htpasswd user
|
||||
# and set a password
|
||||
# edit /etc/nginx/sites-enabled/mediacms.io and
|
||||
# uncomment the two lines related to htpasswd
|
||||
|
||||
|
||||
AUTH_USER_MODEL = "users.User"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
@@ -402,7 +407,25 @@ LOGGING = {
|
||||
},
|
||||
}
|
||||
|
||||
DATABASES = {"default": {"ENGINE": "django.db.backends.postgresql", "NAME": "mediacms", "HOST": "127.0.0.1", "PORT": "5432", "USER": "mediacms", "PASSWORD": "mediacms", "OPTIONS": {'pool': True}}}
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": "mediacms",
|
||||
"HOST": "127.0.0.1",
|
||||
"PORT": "5432",
|
||||
"USER": "mediacms",
|
||||
"PASSWORD": "mediacms",
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": 2,
|
||||
"max_size": 8,
|
||||
"timeout": 10,
|
||||
"max_lifetime": 30 * 60,
|
||||
"max_idle": 10 * 60,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
REDIS_LOCATION = "redis://127.0.0.1:6379/1"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "8.0.1"
|
||||
VERSION = "8.3.0"
|
||||
|
||||
@@ -35,4 +35,29 @@ find /home/mediacms.io/mediacms ! \( -path "*.git*" -o -name "package-lock.json"
|
||||
|
||||
chmod +x /home/mediacms.io/mediacms/deploy/docker/start.sh /home/mediacms.io/mediacms/deploy/docker/prestart.sh
|
||||
|
||||
# Generate or read SECRET_KEY once, shared across all containers via the
|
||||
# host-mounted project volume. Atomic create-or-read so parallel container
|
||||
# starts (web + celery_worker + celery_beat + migrations) can't race.
|
||||
# Uses `mkdir` as the lock primitive (POSIX-atomic, no dependency on flock).
|
||||
SECRET_KEY_FILE="${SECRET_KEY_FILE:-/home/mediacms.io/mediacms/.secret_key}"
|
||||
SECRET_KEY_LOCK="${SECRET_KEY_FILE}.lock"
|
||||
|
||||
if [ -z "${SECRET_KEY:-}" ]; then
|
||||
if [ ! -s "$SECRET_KEY_FILE" ]; then
|
||||
# Spin-acquire the lock. mkdir is atomic; first caller wins, others retry.
|
||||
while ! mkdir "$SECRET_KEY_LOCK" 2>/dev/null; do
|
||||
sleep 0.2
|
||||
done
|
||||
# Re-check inside the lock: another container may have just written it.
|
||||
if [ ! -s "$SECRET_KEY_FILE" ]; then
|
||||
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' > "$SECRET_KEY_FILE"
|
||||
chown www-data:www-data "$SECRET_KEY_FILE"
|
||||
chmod 600 "$SECRET_KEY_FILE"
|
||||
echo "entrypoint.sh: generated new SECRET_KEY at $SECRET_KEY_FILE"
|
||||
fi
|
||||
rmdir "$SECRET_KEY_LOCK"
|
||||
fi
|
||||
export SECRET_KEY="$(cat "$SECRET_KEY_FILE")"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
||||
@@ -2,7 +2,6 @@ import os
|
||||
|
||||
FRONTEND_HOST = os.getenv('FRONTEND_HOST', 'http://localhost')
|
||||
PORTAL_NAME = os.getenv('PORTAL_NAME', 'MediaCMS')
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'ma!s3^b-cw!f#7s6s0m3*jx77a@riw(7701**(r=ww%w!2+yk2')
|
||||
REDIS_LOCATION = os.getenv('REDIS_LOCATION', 'redis://redis:6379/1')
|
||||
|
||||
DATABASES = {
|
||||
@@ -13,7 +12,15 @@ DATABASES = {
|
||||
"PORT": os.getenv('POSTGRES_PORT', '5432'),
|
||||
"USER": os.getenv('POSTGRES_USER', 'mediacms'),
|
||||
"PASSWORD": os.getenv('POSTGRES_PASSWORD', 'mediacms'),
|
||||
"OPTIONS": {'pool': True},
|
||||
"OPTIONS": {
|
||||
"pool": {
|
||||
"min_size": 2,
|
||||
"max_size": 8,
|
||||
"timeout": 10,
|
||||
"max_lifetime": 30 * 60,
|
||||
"max_idle": 10 * 60,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,27 @@ server {
|
||||
location /static {
|
||||
alias /home/mediacms.io/mediacms/static ;
|
||||
}
|
||||
location = /_media_auth {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:9000/api/v1/media-auth;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
}
|
||||
|
||||
location /media/original {
|
||||
alias /home/mediacms.io/mediacms/media_files/original;
|
||||
location ~ ^/media/(encoded|hls|original)/(.*)$ {
|
||||
auth_request /_media_auth;
|
||||
alias /home/mediacms.io/mediacms/media_files/$1/$2;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
|
||||
location /media {
|
||||
|
||||
@@ -6,7 +6,7 @@ ADMIN_PASSWORD=${ADMIN_PASSWORD:-$RANDOM_ADMIN_PASS}
|
||||
if [ X"$ENABLE_MIGRATIONS" = X"yes" ]; then
|
||||
echo "Running migrations service"
|
||||
python manage.py migrate
|
||||
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell`
|
||||
EXISTING_INSTALLATION=`echo "from users.models import User; print(User.objects.exists())" |python manage.py shell 2>/dev/null | tail -1`
|
||||
if [ "$EXISTING_INSTALLATION" = "True" ]; then
|
||||
echo "Loaddata has already run"
|
||||
else
|
||||
|
||||
+3
-65
@@ -2,7 +2,6 @@
|
||||
|
||||
## Table of contents
|
||||
- [1. Welcome](#1-welcome)
|
||||
- [2. Single Server Installaton](#2-single-server-installation)
|
||||
- [3. Docker Installation](#3-docker-installation)
|
||||
- [4. Docker Deployment options](#4-docker-deployment-options)
|
||||
- [5. Configuration](#5-configuration)
|
||||
@@ -34,58 +33,6 @@
|
||||
## 1. Welcome
|
||||
This page is created for MediaCMS administrators that are responsible for setting up the software, maintaining it and making modifications.
|
||||
|
||||
## 2. Single Server Installation
|
||||
|
||||
The core dependencies are python3, Django, celery, PostgreSQL, redis, ffmpeg. Any system that can have these dependencies installed, can run MediaCMS. But the install.sh is only tested in Linux Ubuntu 24 and 22 versions.
|
||||
|
||||
Installation on an Ubuntu 22/24 system with git utility installed should be completed in a few minutes with the following steps.
|
||||
Make sure you run it as user root, on a clear system, since the automatic script will install and configure the following services: Celery/PostgreSQL/Redis/Nginx and will override any existing settings.
|
||||
|
||||
|
||||
|
||||
```bash
|
||||
mkdir /home/mediacms.io && cd /home/mediacms.io/
|
||||
git clone https://github.com/mediacms-io/mediacms
|
||||
cd /home/mediacms.io/mediacms/ && bash ./install.sh
|
||||
```
|
||||
|
||||
The script will ask if you have a URL where you want to deploy MediaCMS, otherwise it will use localhost. If you provide a URL, it will use Let's Encrypt service to install a valid ssl certificate.
|
||||
|
||||
|
||||
### Update
|
||||
|
||||
If you've used the above way to install MediaCMS, update with the following:
|
||||
|
||||
```bash
|
||||
cd /home/mediacms.io/mediacms # enter mediacms directory
|
||||
source /home/mediacms.io/bin/activate # use virtualenv
|
||||
git pull # update code
|
||||
pip install -r requirements.txt -U # run pip install to update
|
||||
python manage.py migrate # run Django migrations
|
||||
sudo systemctl restart mediacms celery_long celery_short # restart services
|
||||
```
|
||||
|
||||
### Update from version 2 to version 3
|
||||
Version 3 is using Django 4 and Celery 5, and needs a recent Python 3.x version. If you are updating from an older version, make sure Python is updated first. Version 2 could run on Python 3.6, but version 3 needs Python3.8 and higher.
|
||||
The syntax for starting Celery has also changed, so you have to copy the celery related systemctl files and restart
|
||||
|
||||
```
|
||||
# cp deploy/local_install/celery_long.service /etc/systemd/system/celery_long.service
|
||||
# cp deploy/local_install/celery_short.service /etc/systemd/system/celery_short.service
|
||||
# cp deploy/local_install/celery_beat.service /etc/systemd/system/celery_beat.service
|
||||
# systemctl daemon-reload
|
||||
# systemctl start celery_long celery_short celery_beat
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Configuration
|
||||
Checkout the configuration section here.
|
||||
|
||||
|
||||
### Maintenance
|
||||
Database can be backed up with pg_dump and media_files on /home/mediacms.io/mediacms/media_files include original files and encoded/transcoded versions
|
||||
|
||||
|
||||
## 3. Docker Installation
|
||||
|
||||
@@ -220,14 +167,10 @@ Several options are available on `cms/settings.py`, most of the things that are
|
||||
|
||||
It is advisable to override any of them by adding it to `local_settings.py` .
|
||||
|
||||
In case of a the single server installation, add to `cms/local_settings.py` .
|
||||
|
||||
In case of a docker compose installation, add to `deploy/docker/local_settings.py` . This will automatically overwrite `cms/local_settings.py` .
|
||||
|
||||
Any change needs restart of MediaCMS in order to take effect.
|
||||
|
||||
Single server installation: edit `cms/local_settings.py`, make a change and restart MediaCMS
|
||||
|
||||
```bash
|
||||
#systemctl restart mediacms
|
||||
```
|
||||
@@ -795,14 +738,7 @@ Instructions contributed by @alberto98fx
|
||||
On the [Configuration](https://github.com/mediacms-io/mediacms/blob/main/docs/admins_docs.md#5-configuration) section of this guide we've see how to edit the email settings.
|
||||
In case we are yet unable to receive email from MediaCMS, the following may help us debug the issue - in most cases it is an issue of setting the correct username, password or TLS option
|
||||
|
||||
Enter the Django shell, example if you're using the Single Server installation:
|
||||
|
||||
```bash
|
||||
source /home/mediacms.io/bin/activate
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
and inside the shell
|
||||
Enter the Django shell and inside the shell
|
||||
|
||||
```bash
|
||||
from django.core.mail import EmailMessage
|
||||
@@ -1011,6 +947,8 @@ Select the SAML Configurations tab, create a new one and set:
|
||||
3. **SSO URL**:
|
||||
4. **SLO URL**:
|
||||
5. **SP Metadata URL**: The metadata URL that the IDP will utilize. This can be https://{portal}/saml/metadata and is autogenerated by MediaCMS
|
||||
6. **SP Certificate** (optional): SP x509 certificate (PEM). Enables encrypted/signed SAML communication. If set, the SP Private Key must also be provided, and the certificate is published in the SP metadata so the IDP can encrypt assertions to MediaCMS.
|
||||
7. **SP Private Key** (optional): SP private key (PEM). Used to sign AuthnRequests/LogoutRequests and to decrypt assertions encrypted by the IDP. Required if SP Certificate is provided.
|
||||
|
||||
- Step 3: Set other Options
|
||||
1. **Email Settings**:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
@@ -0,0 +1,277 @@
|
||||
# MediaCMS plugin for Moodle
|
||||
|
||||
## Documentation and user guides
|
||||
|
||||
The MediaCMS plugin brings a fully featured video content management system directly into Moodle, empowering institutions and their users with powerful media tools — all within a familiar learning environment.
|
||||
|
||||
At its core, MediaCMS offers a robust transcoding, transcription, and translation engine, alongside a comprehensive toolbox tailored for administrators, lecturers, students, and staff alike. Its intuitive interface and streamlined workflows mean that most media files can be uploaded, reviewed, edited, and published quickly — with no transcoding delays and no steep learning curve.
|
||||
|
||||
This guide covers everything needed to get started: from installation and configuration to everyday use — with dedicated sections for administrators, lecturers, and students.
|
||||
|
||||

|
||||
|
||||
## 1. For Administrators
|
||||
|
||||
Administrators: Installation and configuration of MediaCMS plugin for Moodle
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
The MediaCMS integration in Moodle should use the same domain as the Moodle portal (further explanation in note 1 below)
|
||||
|
||||
E.g. `moodle.organisation.org` versus `mediacms.organisation.org`
|
||||
|
||||
### 1.1 Configure MediaCMS `local_settings.py` settings
|
||||
|
||||
```
|
||||
USE_IDENTITY_PROVIDERS = True
|
||||
USE_RBAC = True
|
||||
USE_LTI = True
|
||||
```
|
||||
|
||||
Restart MediaCMS services.
|
||||
|
||||
### 1.2 Moodle: Add External Tool
|
||||
|
||||
Moodle > Site Administration > Plugins > Activity Modules > External tools > Manage Tools > Configure a Tool Manually:
|
||||
|
||||
**Tool Settings**
|
||||
- **Name:** MediaCMS
|
||||
- **Tool URL:** MediaCMS issuer URL + `/lti/launch/` (e.g. `https://mediacms.organisation.org/lti/launch/`)
|
||||
- **LTI version:** LTI 1.3
|
||||
- **Client ID:** will be provided once the tool is saved
|
||||
- **Public key type:** Keyset URL
|
||||
- **Public keyset:** MediaCMS issuer URL + `/lti/jwks/` (e.g. `https://mediacms.organisation.org/lti/jwks/`)
|
||||
- **Initiate login URL:** MediaCMS issuer URL + `/lti/oidc/login/` (e.g. `https://mediacms.organisation.org/lti/oidc/login/`)
|
||||
- **Redirection URI(s):** MediaCMS issuer URL + `/lti/launch/` (e.g. `https://mediacms.organisation.org/lti/launch/`)
|
||||
- **Tool configuration usage:** Show in activity chooser and as a preconfigured tool
|
||||
- **Default launch container:** Embed, without blocks + Supports Deep Linking
|
||||
|
||||
**Services**
|
||||
- **IMS LTI Assignment and Grade Services:** Do not use this service
|
||||
- **IMS LTI Names and Role Provisioning:** Use this service to retrieve members' information as per privacy settings
|
||||
- **Tool Settings:** Use this service
|
||||
|
||||
**Privacy**
|
||||
- **Share launcher's name with tool:** Always
|
||||
- **Share launcher's email with tool:** Always
|
||||
- **Accept grades from the tool:** As specified in Deep Linking definition or Delegate to teacher
|
||||
|
||||
Save Changes.
|
||||
|
||||
### 1.3 Moodle: Installation and configuration of MediaCMS plugin in Moodle
|
||||
|
||||
#### 1.3.1 Download plugin and install MediaCMS as an LTI tool
|
||||
|
||||
The plugin is available in the `lms-plugins/mediacms-moodle/dist/` directory of this repository. It will also be published to the [Moodle Plugins directory](https://moodle.org/plugins) shortly.
|
||||
|
||||
Two methods can be used:
|
||||
|
||||
- **A. Copy files from ZIP to the Moodle folder structure.** Benefit: This method can be automated.
|
||||
- Place the zip file in `/var/www/moodle/public` and unzip (depending on your Moodle installation directory).
|
||||
- Open Moodle and the installation should start automatically.
|
||||
- **B. Install plugin via** Moodle > Site Administration > Install plugins. Benefit: Easy manual workflow.
|
||||
|
||||
#### 1.3.2 Configuration of plugin: Site Administration > ...
|
||||
|
||||
- [ ] Plugins > Filters > Manage Filters > MediaCMS > Enable, and place in top of list
|
||||
- [ ] Plugins > Filters > MediaCMS >
|
||||
- [ ] My Media link placement: Top Navigation or User Navigation (further explanation in note 2 below)
|
||||
- [ ] MediaCMS URL: e.g. `mediacms.organisation.org`
|
||||
- [ ] LTI Tool: e.g. `mediacms.organisation.org/lti/launch/`
|
||||
- [ ] Share Embedded Media (whether embedded media can be accessed via My Media > Shared with Me) (note 3 below)
|
||||
- [ ] Plugins > Text editors > TinyMCE > MediaCMS > sets default embed options for users
|
||||
- [ ] Show video title (media title option on top of video)
|
||||
- [ ] Link video title (media title option on top of video linking to full media playback options: download, comments etc.)
|
||||
- [ ] Show user avatar (show user's icon picture on top of video)
|
||||
|
||||
**Notes:**
|
||||
|
||||
**Note 1.** As of Q2 2026 browser providers increasingly implement measures to prevent cross-site tracking, which also set limitations on how users can view embedded content from a system X (e.g. MediaCMS), that has been embedded in system Y (e.g. Moodle). To avoid these limitations, it is recommended to use the same domain for the configuration of MediaCMS as is used for Moodle.
|
||||
|
||||
**Note 2.** User Navigation is the user's icon in top right corner of Moodle interface, where user profile and preferences are found.
|
||||
|
||||
**Note 3.** "Share Embedded Media" configuration explained:
|
||||
|
||||
Users are automatically assigned permissions to embedded media in Moodle Activity/Resource, if they have access to a particular Moodle Activity / Resource. Publish State of the media is set to Private and Shared, whereby media cannot be shared outside Moodle by e.g. copying a link from Moodle and using it outside Moodle. These permissions are not automatically removed, if access to the Moodle Activity/Resource is removed.
|
||||
|
||||
Users listed as sharing partners can be removed manually via My Media > Bulk Actions > Course Cleanup > select course > "Remove present course permissions for all course members" > Proceed. Users can still access the media in Moodle Activities / Resources, if that access is given in Moodle, whereby user will again become sharing partner, if making use of that access.
|
||||
|
||||
**Option 1: Share Embedded Media = True**
|
||||
|
||||
With this configuration users, who have accessed embedded media, can also access this media via My Media > Shared with Me. This is possible in My Media in Moodle as well as My Media in the MediaCMS video portal.
|
||||
|
||||
**Option 2: Share Embedded Media = False**
|
||||
|
||||
Select this option, if users should not have access to embedded content under My Media > Shared with Me. Select this option, if strict access control should be handled for embedded content, and not rely on e.g. manual Course Cleanup procedures.
|
||||
|
||||
### 1.4 MediaCMS Administration: Add Moodle as an LTI platform
|
||||
|
||||
In MediaCMS, go to Administration > LTI 1.3 Integration > LTI Platforms > Add LTI Platform > add:
|
||||
|
||||
**Basic Information**
|
||||
- **Name:** What makes sense in your context
|
||||
- **Platform ID:** Moodle platform's issuer URL (iss claim, e.g. `https://moodle.organisation.org`)
|
||||
- **Client ID:** get it from Moodle > Site Administration > Plugins > Activity Modules > External tools > Manage Tools > MediaCMS > Edit > Client ID
|
||||
|
||||
**OIDC Endpoints**
|
||||
- **Auth login URL:** Moodle issuer URL + `/filter/mediacms/lti_auth.php` (e.g. `https://moodle.organisation.org/filter/mediacms/lti_auth.php`)
|
||||
- **Auth token URL:** Moodle issuer URL + `/mod/lti/token.php` (e.g. `https://moodle.organisation.org/mod/lti/token.php`)
|
||||
- **Auth audience:** Moodle issuer URL + `/mod/lti/certs.php` (e.g. `https://moodle.organisation.org/mod/lti/certs.php`)
|
||||
|
||||
**JWK Configuration**
|
||||
- **Key set URL:** Issuer URL + `/mod/lti/certs.php` (e.g. `https://moodle.organisation.org/mod/lti/certs.php`)
|
||||
|
||||
**Deployment & Features**
|
||||
- **Deployment IDs:** get it from Moodle > Site Administration > Plugins > Activity Modules > External tools > Manage Tools > MediaCMS > View configuration details
|
||||
- **Enable NRPS:** yes
|
||||
- **Enable deep linking:** yes
|
||||
|
||||
**Auto-Provision Settings**
|
||||
- **Remove from groups on unenroll:** yes
|
||||
|
||||
Done. Installation can now be tested with the role as an administrator, lecturer, student or any other role in Moodle.
|
||||
|
||||
|
||||
## 2. For Lecturers
|
||||
|
||||
Lecturers: Moodle workflows covered in the MediaCMS integration
|
||||
|
||||
### 1. Upload media to your My Media repository
|
||||
|
||||
Moodle > My Media > camera icon to the right (Add Media) > Upload > Drag and drop files / Browse your files
|
||||
|
||||
Video, audio, pictures and PDF files can be uploaded and shared with the course members, but only video and audio files can be embedded in Moodle Activities / Resources.
|
||||
|
||||
### 2. Record media to your My Media repository
|
||||
|
||||
Make simple recordings via your browser, and add them directly to your My Media repository:
|
||||
|
||||
Moodle > My Media > camera icon to the right (Add Media) > Record > Record Screen with Audio (computers) / Record Video (mobiles)
|
||||
|
||||
Apple iPhone iOS only supports 10 minutes recording in 480p, whereas Android based systems have fewer restrictions.
|
||||
|
||||
### 3. Edit media
|
||||
|
||||
Moodle > My Media > particular media > Edit Media (pencil icon on top of media thumbnail) > ...
|
||||
|
||||
- **3.1 Metadata:** Edit title and description, add tags, set date, poster image, thumbnail image, enable comments
|
||||
- **3.2 Trim:** Remove part of media, or split media into several new media files
|
||||
- **3.3 Captions:** Automatically create transcriptions and/or English translations, or manually upload transcription files (`.srt` / `.vtt`)
|
||||
- **3.4 Chapters:** Create chapters in media, which are displayed in the media player
|
||||
- **3.5 Publish:** Share the media with courses, and/or make the media unlisted for publishing outside Moodle
|
||||
- **3.6 Replace:** Add new media to the media instance, but retain all metadata, captions, chapters etc.
|
||||
|
||||
### 4. Share media with other users in Moodle or MediaCMS
|
||||
|
||||
Moodle > My Media > select media > Bulk Actions > Share with 1. Co-Viewers OR 2. Co-Editors OR 3. Co-Owners OR 4. Course Members
|
||||
|
||||
- **Note 1:** Select one or several users, with whom you want to share media, giving them viewer permissions
|
||||
- **Note 2:** Similar, but giving users Editor permissions (they can edit the media and media's metadata)
|
||||
- **Note 3:** Similar, but giving users Co-editor permissions (they can do everything the owner can do, except delete the media)
|
||||
- **Note 4:** Select courses with which members you want to share media, giving them permissions according to permissions in the course (Students become Viewers, and Lecturers and similar roles become Co-Owners)
|
||||
|
||||
Alternatively, use My Media > individual media > Publish > select courses, which will have the same effect as Bulk Actions > Share with Course Members (Note 4).
|
||||
|
||||
Sharing media with Course Members will automatically list the media under that Course. A link to all listed course media can be accessed under the media's full viewing page. Users will only see course listings for courses, where they are members.
|
||||
|
||||
### 5. Embed media in Moodle course activities / resources
|
||||
|
||||
Two basic workflows are supported:
|
||||
|
||||
**Workflow A.** In the context of the media player, copy URL from the viewing page and paste it into the editing area of the text editor:
|
||||
|
||||
Moodle > My Media > click to view media > right click in viewing area > Copy Video URL > go to course activity / resource > activity edit mode > in the Content area > paste URL into the text editor > media will be displayed > click Edit to adjust settings for media > Insert > Save and Display
|
||||
|
||||
**Workflow B.** In the context of a course activity / resource, use the text editor to embed MediaCMS media:
|
||||
|
||||
Any course activity / resource > activate Editing > in the Content area > text editor > click green play icon: Insert MediaCMS media > select media > adjust parameters > Insert > Save and Display
|
||||
|
||||
If adjustments need to be applied, click edit over the media when in edit mode of the Moodle activity / resource. For media inserted via a text link, edit mode can be accessed by clicking the media link when in edit mode of the Moodle activity / resource, followed by clicking the green play icon in the editor "Insert MediaCMS media".
|
||||
|
||||
**Embedding parameters:**
|
||||
|
||||
- **Show title:** A title will be displayed on top of media
|
||||
- **Show user avatar:** Shows user's icon picture on top of video
|
||||
- **Link title:** Users can click on the title text over the media to get access to full media viewing, e.g. download, commenting, bookmarking (playlists), if these features have been activated by owner. Media will open in a new browser tab.
|
||||
- **Insert text link only:** A link to the media will be displayed in page, instead of the media being visually embedded. User gets access to the same media viewing page, in a new tab, as described under 3.5.
|
||||
- **Dimensions:** User can set maximum dimensions, which will automatically adapt to the size of the browser window / device display.
|
||||
|
||||
Relevant embedding parameters will be saved in a browser cookie, so that the configuration is remembered for next time a media is embedded.
|
||||
|
||||
Media will get the Publish state: Shared, when inserted in the activity / resource, and media will automatically be shared with specific users, when they access the Moodle activity / resource.
|
||||
|
||||
If administrator has activated a specific configuration (Share Embedded Media), users will be able to view the embedded shared media content under My Media > Shared with Me.
|
||||
|
||||
### 6. Link to, or embed media outside Moodle
|
||||
|
||||
To publish an external link to the media: My Media > click to view the media > right-click the viewing window > copy URL or embed code > add this to your external portal or client.
|
||||
|
||||
### 7. Bookmark and collect video in Playlists
|
||||
|
||||
Full media viewing page > Save > Save to existing Playlist, or create new Playlist for the bookmarked media.
|
||||
|
||||
Access your Playlists under My Media > Playlists.
|
||||
|
||||
### 8. Bulk Actions
|
||||
|
||||
Other actions available for all users:
|
||||
|
||||
Handle media settings for many media items in one go: Enable, disable, delete Comment and Enable or Disable Download. Manage media, such as change Publish State (Private, Shared, Unlisted), change ownership of media, or copy or delete media.
|
||||
|
||||
### 9. Course Cleanup
|
||||
|
||||
Remove existing permissions and delete comments under media in course:
|
||||
|
||||
Moodle > My Media > Bulk Actions > Course Cleanup
|
||||
|
||||
Administrators and lecturers can make use of the Bulk Action > Course Cleanup without selecting any media, whereby the cleanup will apply to all media embedded in course activities / resources, and media Shared with Course Members / Publish to the course. If only selecting specific media, the cleanup will only apply to the selected media.
|
||||
|
||||
#### 9.1 Moodle > My Media > Bulk Actions > Course Cleanup > select course > Remove present course permissions for all course members
|
||||
|
||||
If selecting this option, all users that have accessed the embedded course media will no longer be listed as sharing partners. Similarly, if media has been shared via Shared with Course Members / Publish to Course, these users will also be removed as sharing partners.
|
||||
|
||||
Be aware that this action does not remove user's access to media, if users still have access to the Moodle activity / resource, and the media is still embedded in that activity / resource.
|
||||
|
||||
#### 9.2 Moodle > My Media > Bulk Actions > Course Cleanup > select course > Remove Comments
|
||||
|
||||
If selecting this option, all comments added to the embedded course media, or media shared with course members, will be deleted.
|
||||
|
||||
Course Cleanup can ideally be used after a term, where a course has ended, and a clean sheet will provide better overview. E.g. media can be reused from course to course, without comments from previous courses getting in the way.
|
||||
|
||||
### 10. Provisioning of courses and users explained
|
||||
|
||||
A course in Moodle corresponds to a category in MediaCMS, and Moodle course roles are mapped to MediaCMS category roles:
|
||||
|
||||
- Student → Viewer
|
||||
- Teacher → Manager
|
||||
|
||||
A course category in MediaCMS is created along these workflows:
|
||||
|
||||
- Moodle > My Media > Bulk Actions > Share with Course Members > course is added > clicking Proceed
|
||||
- Moodle > My Media > media > Publish > course is added to list > clicking Publish Media
|
||||
- Moodle > Course > course element (e.g. page) > Edit > TinyMCE editor > clicking Insert MediaCMS Media
|
||||
|
||||
Users are added individually to the course category group in MediaCMS when accessing embedded media in Moodle element or My Media in Moodle, or when content is shared with course members.
|
||||
|
||||
Automatic continuous synchronisation of courses and users has not yet been established, e.g. via NRPS. A future version may include this. Instead, ad hoc course membership and role synchronisation is happening for a specific user, when the user clicks on My Media.
|
||||
|
||||
As such, if the user is removed from a course, or gets a different role, this is synced to course category group in MediaCMS, when user clicks on My Media in Moodle.
|
||||
|
||||
|
||||
## 3. For Students
|
||||
|
||||
Students: Moodle workflows covered with the MediaCMS integration
|
||||
|
||||
Almost all the workflows supported for lecturers are also supported for students. This goes for the following workflows described above for lecturers:
|
||||
|
||||
1. Upload Media
|
||||
2. Edit Media
|
||||
3. Share media with other users
|
||||
4. Embed media in Moodle course activities / resources
|
||||
5. Link to, or embed media outside Moodle
|
||||
6. Bookmark and collect media in Playlists
|
||||
7. Bulk Actions
|
||||
|
||||
**Note 4:** Students can only embed media in Moodle activities / resources, where they have the permissions to do so via permissions set in Moodle.
|
||||
|
||||
**Note 7:** With regard to Bulk Actions, students do not have access to Course Cleanup.
|
||||
@@ -120,9 +120,11 @@ class MediaList(APIView):
|
||||
operation_description='Delete media for MediaCMS managers and reviewers',
|
||||
)
|
||||
def delete(self, request, format=None):
|
||||
if not is_mediacms_manager(request.user):
|
||||
return Response({"detail": "bad permissions"}, status=status.HTTP_403_FORBIDDEN)
|
||||
tokens = request.GET.get("tokens")
|
||||
if tokens:
|
||||
tokens = tokens.split(",")
|
||||
tokens = [t for t in tokens.split(",") if t][:50]
|
||||
Media.objects.filter(friendly_token__in=tokens).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -177,7 +179,7 @@ class CommentList(APIView):
|
||||
def delete(self, request, format=None):
|
||||
comment_ids = request.GET.get("comment_ids")
|
||||
if comment_ids:
|
||||
comments = comment_ids.split(",")
|
||||
comments = [c for c in comment_ids.split(",") if c][:50]
|
||||
Comment.objects.filter(uid__in=comments).delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
+13
-9
@@ -453,17 +453,21 @@ def kill_ffmpeg_process(filepath):
|
||||
filepath: Path to the file being processed by ffmpeg
|
||||
|
||||
Returns:
|
||||
subprocess.CompletedProcess: Result of the kill command
|
||||
bool: True if the lookup ran, False if input was unusable
|
||||
"""
|
||||
if not filepath:
|
||||
if not filepath or not isinstance(filepath, str):
|
||||
return False
|
||||
cmd = "ps aux|grep 'ffmpeg'|grep %s|grep -v grep |awk '{print $2}'" % filepath
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||
pid = result.stdout.decode("utf-8").strip()
|
||||
if pid:
|
||||
cmd = "kill -9 %s" % pid
|
||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, shell=True)
|
||||
return result
|
||||
try:
|
||||
ps = subprocess.run(["ps", "aux"], stdout=subprocess.PIPE, check=False)
|
||||
except OSError:
|
||||
return False
|
||||
for line in ps.stdout.decode("utf-8", "replace").splitlines():
|
||||
if "ffmpeg" not in line or filepath not in line or "grep" in line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
subprocess.run(["kill", "-9", parts[1]], check=False)
|
||||
return True
|
||||
|
||||
|
||||
def copy_video(original_media, copy_encodings=True, title_suffix="(Trimmed)"):
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
|
||||
from .. import helpers
|
||||
from .utils import (
|
||||
@@ -136,9 +135,6 @@ class Encoding(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.profile.name}-{self.media.title}"
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("api_get_encoding", kwargs={"encoding_id": self.id})
|
||||
|
||||
|
||||
@receiver(post_save, sender=Encoding)
|
||||
def encoding_file_save(sender, instance, created, **kwargs):
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.conf import settings
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.core.files import File
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Func, Value
|
||||
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
@@ -536,7 +536,9 @@ class Media(models.Model):
|
||||
|
||||
from .. import tasks
|
||||
|
||||
tasks.produce_sprite_from_video.delay(self.friendly_token)
|
||||
# Defer until the surrounding transaction commits so the worker can
|
||||
# actually find the Media row. Runs immediately if not in a tx.
|
||||
transaction.on_commit(lambda token=self.friendly_token: tasks.produce_sprite_from_video.delay(token))
|
||||
return True
|
||||
|
||||
def encode(self, profiles=[], force=True, chunkize=True):
|
||||
@@ -559,9 +561,8 @@ class Media(models.Model):
|
||||
profiles.remove(profile)
|
||||
encoding = Encoding(media=self, profile=profile)
|
||||
encoding.save()
|
||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
||||
tasks.encode_media.apply_async(
|
||||
args=[self.friendly_token, profile.id, encoding.id, enc_url],
|
||||
args=[self.friendly_token, profile.id, encoding.id],
|
||||
kwargs={"force": force},
|
||||
priority=0,
|
||||
)
|
||||
@@ -575,13 +576,12 @@ class Media(models.Model):
|
||||
continue
|
||||
encoding = Encoding(media=self, profile=profile)
|
||||
encoding.save()
|
||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
||||
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||
priority = 9
|
||||
else:
|
||||
priority = 0
|
||||
tasks.encode_media.apply_async(
|
||||
args=[self.friendly_token, profile.id, encoding.id, enc_url],
|
||||
args=[self.friendly_token, profile.id, encoding.id],
|
||||
kwargs={"force": force},
|
||||
priority=priority,
|
||||
)
|
||||
|
||||
+2
-5
@@ -171,8 +171,7 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
||||
continue
|
||||
encoding = Encoding(media=media, profile=profile)
|
||||
encoding.save()
|
||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
||||
encode_media.delay(friendly_token, profile.id, encoding.id, enc_url, force=force)
|
||||
encode_media.delay(friendly_token, profile.id, encoding.id, force=force)
|
||||
return False
|
||||
|
||||
chunks = [os.path.join(cwd, ch) for ch in chunks]
|
||||
@@ -202,13 +201,12 @@ def chunkize_media(self, friendly_token, profiles, force=True):
|
||||
)
|
||||
|
||||
encoding.save()
|
||||
enc_url = settings.SSL_FRONTEND_HOST + encoding.get_absolute_url()
|
||||
if profile.resolution in settings.MINIMUM_RESOLUTIONS_TO_ENCODE:
|
||||
priority = 0
|
||||
else:
|
||||
priority = 9
|
||||
encode_media.apply_async(
|
||||
args=[friendly_token, profile.id, encoding.id, enc_url],
|
||||
args=[friendly_token, profile.id, encoding.id],
|
||||
kwargs={"force": force, "chunk": True, "chunk_file_path": chunk},
|
||||
priority=priority,
|
||||
)
|
||||
@@ -246,7 +244,6 @@ def encode_media(
|
||||
friendly_token,
|
||||
profile_id,
|
||||
encoding_id,
|
||||
encoding_url,
|
||||
force=True,
|
||||
chunk=False,
|
||||
chunk_file_path="",
|
||||
|
||||
+1
-5
@@ -61,11 +61,6 @@ urlpatterns = [
|
||||
views.MediaDetail.as_view(),
|
||||
name="api_get_media",
|
||||
),
|
||||
re_path(
|
||||
r"^api/v1/media/encoding/(?P<encoding_id>[\w]*)$",
|
||||
views.EncodingDetail.as_view(),
|
||||
name="api_get_encoding",
|
||||
),
|
||||
re_path(r"^api/v1/search$", views.MediaSearch.as_view()),
|
||||
re_path(
|
||||
rf"^api/v1/media/{friendly_token}/share$",
|
||||
@@ -111,6 +106,7 @@ urlpatterns = [
|
||||
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
||||
re_path(r"^api/v1/media-auth$", views.media_auth, name="media_auth"),
|
||||
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
||||
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
from .auth import custom_login_view, saml_metadata # noqa: F401
|
||||
from .categories import CategoryList, CategoryListContributor, TagList # noqa: F401
|
||||
from .comments import CommentDetail, CommentList # noqa: F401
|
||||
from .encoding import EncodeProfileList, EncodingDetail # noqa: F401
|
||||
from .encoding import EncodeProfileList # noqa: F401
|
||||
from .media import MediaActions # noqa: F401
|
||||
from .media import MediaBulkUserActions # noqa: F401
|
||||
from .media import MediaDetail # noqa: F401
|
||||
from .media import MediaList # noqa: F401
|
||||
from .media import MediaSearch # noqa: F401
|
||||
from .media import media_share # noqa: F401
|
||||
from .media_auth import media_auth # noqa: F401
|
||||
from .pages import about # noqa: F401
|
||||
from .pages import add_subtitle # noqa: F401
|
||||
from .pages import approval_required # noqa: F401
|
||||
|
||||
+1
-158
@@ -1,168 +1,11 @@
|
||||
from django.conf import settings
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.parsers import (
|
||||
FileUploadParser,
|
||||
FormParser,
|
||||
JSONParser,
|
||||
MultiPartParser,
|
||||
)
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from ..helpers import produce_ffmpeg_commands
|
||||
from ..models import EncodeProfile, Encoding
|
||||
from ..models import EncodeProfile
|
||||
from ..serializers import EncodeProfileSerializer
|
||||
|
||||
|
||||
class EncodingDetail(APIView):
|
||||
"""Experimental. This View is used by remote workers
|
||||
Needs heavy testing and documentation.
|
||||
"""
|
||||
|
||||
permission_classes = (permissions.IsAdminUser,)
|
||||
parser_classes = (JSONParser, MultiPartParser, FormParser, FileUploadParser)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def post(self, request, encoding_id):
|
||||
ret = {}
|
||||
force = request.data.get("force", False)
|
||||
task_id = request.data.get("task_id", False)
|
||||
action = request.data.get("action", "")
|
||||
chunk = request.data.get("chunk", False)
|
||||
chunk_file_path = request.data.get("chunk_file_path", "")
|
||||
|
||||
encoding_status = request.data.get("status", "")
|
||||
progress = request.data.get("progress", "")
|
||||
commands = request.data.get("commands", "")
|
||||
logs = request.data.get("logs", "")
|
||||
retries = request.data.get("retries", "")
|
||||
worker = request.data.get("worker", "")
|
||||
temp_file = request.data.get("temp_file", "")
|
||||
total_run_time = request.data.get("total_run_time", "")
|
||||
if action == "start":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
media = encoding.media
|
||||
profile = encoding.profile
|
||||
except BaseException:
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
# TODO: break chunk True/False logic here
|
||||
if (
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).count()
|
||||
> 1 # noqa
|
||||
and force is False # noqa
|
||||
):
|
||||
Encoding.objects.filter(id=encoding_id).delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
Encoding.objects.filter(
|
||||
media=media,
|
||||
profile=profile,
|
||||
chunk=chunk,
|
||||
chunk_file_path=chunk_file_path,
|
||||
).exclude(id=encoding.id).delete()
|
||||
|
||||
encoding.status = "running"
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
|
||||
encoding.save()
|
||||
if chunk:
|
||||
original_media_path = chunk_file_path
|
||||
original_media_md5sum = encoding.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + encoding.media_chunk_url
|
||||
else:
|
||||
original_media_path = media.media_file.path
|
||||
original_media_md5sum = media.md5sum
|
||||
original_media_url = settings.SSL_FRONTEND_HOST + media.original_media_url
|
||||
|
||||
ret["original_media_url"] = original_media_url
|
||||
ret["original_media_path"] = original_media_path
|
||||
ret["original_media_md5sum"] = original_media_md5sum
|
||||
|
||||
# generating the commands here, and will replace these with temporary
|
||||
# files created on the remote server
|
||||
tf = "TEMP_FILE_REPLACE"
|
||||
tfpass = "TEMP_FPASS_FILE_REPLACE"
|
||||
ffmpeg_commands = produce_ffmpeg_commands(
|
||||
original_media_path,
|
||||
media.media_info,
|
||||
resolution=profile.resolution,
|
||||
codec=profile.codec,
|
||||
output_filename=tf,
|
||||
pass_file=tfpass,
|
||||
chunk=chunk,
|
||||
)
|
||||
if not ffmpeg_commands:
|
||||
encoding.delete()
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
ret["duration"] = media.duration
|
||||
ret["ffmpeg_commands"] = ffmpeg_commands
|
||||
ret["profile_extension"] = profile.extension
|
||||
return Response(ret, status=status.HTTP_201_CREATED)
|
||||
elif action == "update_fields":
|
||||
try:
|
||||
encoding = Encoding.objects.get(id=encoding_id)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
to_update = ["size", "update_date"]
|
||||
if encoding_status:
|
||||
encoding.status = encoding_status
|
||||
to_update.append("status")
|
||||
if progress:
|
||||
encoding.progress = progress
|
||||
to_update.append("progress")
|
||||
if logs:
|
||||
encoding.logs = logs
|
||||
to_update.append("logs")
|
||||
if commands:
|
||||
encoding.commands = commands
|
||||
to_update.append("commands")
|
||||
if task_id:
|
||||
encoding.task_id = task_id
|
||||
to_update.append("task_id")
|
||||
if total_run_time:
|
||||
encoding.total_run_time = total_run_time
|
||||
to_update.append("total_run_time")
|
||||
if worker:
|
||||
encoding.worker = worker
|
||||
to_update.append("worker")
|
||||
if temp_file:
|
||||
encoding.temp_file = temp_file
|
||||
to_update.append("temp_file")
|
||||
|
||||
if retries:
|
||||
encoding.retries = retries
|
||||
to_update.append("retries")
|
||||
|
||||
try:
|
||||
encoding.save(update_fields=to_update)
|
||||
except BaseException:
|
||||
return Response({"status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"status": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@swagger_auto_schema(auto_schema=None)
|
||||
def put(self, request, encoding_id, format=None):
|
||||
encoding_file = request.data["file"]
|
||||
encoding = Encoding.objects.filter(id=encoding_id).first()
|
||||
if not encoding:
|
||||
return Response(
|
||||
{"detail": "encoding does not exist"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
encoding.media_file = encoding_file
|
||||
encoding.save()
|
||||
return Response({"detail": "ok"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class EncodeProfileList(APIView):
|
||||
"""List encode profiles"""
|
||||
|
||||
|
||||
@@ -660,7 +660,9 @@ class MediaBulkUserActions(APIView):
|
||||
|
||||
# Prioritize category_uids
|
||||
if category_uids:
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
requested = Category.objects.filter(uid__in=category_uids)
|
||||
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
|
||||
categories = Category.objects.filter(id__in=allowed_ids)
|
||||
elif lti_context_id:
|
||||
# Filter categories by lti_context_id and ensure they ARE RBAC categories
|
||||
potential_categories = Category.objects.filter(lti_context_id=lti_context_id, is_rbac_category=True)
|
||||
@@ -691,9 +693,11 @@ class MediaBulkUserActions(APIView):
|
||||
if not category_uids:
|
||||
return Response({"detail": "category_uids is required for remove_from_category action"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
categories = Category.objects.filter(uid__in=category_uids)
|
||||
requested = Category.objects.filter(uid__in=category_uids)
|
||||
allowed_ids = [cat.id for cat in requested if not cat.is_rbac_category or request.user.has_contributor_access_to_category(cat)]
|
||||
categories = Category.objects.filter(id__in=allowed_ids)
|
||||
if not categories:
|
||||
return Response({"detail": "No matching categories found"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({"detail": "No matching categories found or access denied"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
removed_count = 0
|
||||
for category in categories:
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from ..methods import is_mediacms_editor
|
||||
from ..models import Media
|
||||
|
||||
UID_RE = re.compile(r"[0-9a-f]{32}")
|
||||
THUMBNAILS_PREFIX = "original/thumbnails/"
|
||||
|
||||
|
||||
def _ttl():
|
||||
return getattr(settings, "X_ACCEL_AUTH_CACHE_SECONDS", 300)
|
||||
|
||||
|
||||
def _extract_uid(uri):
|
||||
if not uri:
|
||||
return None
|
||||
match = UID_RE.search(uri)
|
||||
return match.group(0) if match else None
|
||||
|
||||
|
||||
def _relpath_from_uri(uri):
|
||||
path = unquote(uri.split("?", 1)[0])
|
||||
media_url = settings.MEDIA_URL
|
||||
if path.startswith(media_url):
|
||||
return path[len(media_url) :]
|
||||
return None
|
||||
|
||||
|
||||
def _lookup_uid_by_path(relpath):
|
||||
path_key = f"xaccel:path:{relpath}"
|
||||
cached = cache.get(path_key)
|
||||
if cached is not None:
|
||||
return cached or None
|
||||
|
||||
parts = relpath.split("/", 4)
|
||||
if len(parts) < 5 or parts[2] != "user":
|
||||
cache.set(path_key, "", _ttl())
|
||||
return None
|
||||
username = parts[3]
|
||||
|
||||
row = Media.objects.filter(user__username=username).filter(Q(uploaded_thumbnail=relpath) | Q(uploaded_poster=relpath)).values("uid").first()
|
||||
uid_hex = row["uid"].hex if row else ""
|
||||
cache.set(path_key, uid_hex, _ttl())
|
||||
return uid_hex or None
|
||||
|
||||
|
||||
def _lookup_state(uid):
|
||||
"""Return (state, owner_id) for a uid, or (None, None) if missing.
|
||||
|
||||
Cached on uid alone since state/ownership do not depend on the requester.
|
||||
Uses .values() rather than .only() because Media.__init__ touches deferred
|
||||
file fields, which would otherwise recurse via refresh_from_db.
|
||||
"""
|
||||
state_key = f"xaccel:state:{uid}"
|
||||
cached = cache.get(state_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
row = Media.objects.filter(uid=uid).values("state", "user_id").first()
|
||||
value = (row["state"], row["user_id"]) if row else (None, None)
|
||||
cache.set(state_key, value, _ttl())
|
||||
return value
|
||||
|
||||
|
||||
def _decide(uid, user):
|
||||
state, owner_id = _lookup_state(uid)
|
||||
if state is None:
|
||||
return False
|
||||
if state in ("public", "unlisted"):
|
||||
return True
|
||||
# private
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if owner_id == user.id:
|
||||
return True
|
||||
if is_mediacms_editor(user):
|
||||
return True
|
||||
# RBAC / MediaPermission path needs a full Media instance.
|
||||
try:
|
||||
media = Media.objects.get(uid=uid)
|
||||
except Media.DoesNotExist:
|
||||
return False
|
||||
return user.has_member_access_to_media(media)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_GET
|
||||
def media_auth(request):
|
||||
"""Authorize a protected media request from nginx auth_request.
|
||||
|
||||
nginx passes the original request URI in the X-Original-URI header. The
|
||||
Media.uid (32 hex chars, no dashes) is embedded somewhere in that URI for
|
||||
every protected path. No uid => deny. Unknown uid => deny.
|
||||
"""
|
||||
if not getattr(settings, "USE_X_ACCEL_REDIRECT", True):
|
||||
return HttpResponse(status=204)
|
||||
|
||||
uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
|
||||
uid = _extract_uid(uri)
|
||||
if not uid:
|
||||
# User-uploaded thumbnails/posters don't have the uid in the filename.
|
||||
# Fall back to a per-path lookup, scoped to /original/thumbnails/.
|
||||
relpath = _relpath_from_uri(uri)
|
||||
if relpath and relpath.startswith(THUMBNAILS_PREFIX):
|
||||
uid = _lookup_uid_by_path(relpath)
|
||||
if not uid:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
user = request.user
|
||||
cache_key = f"xaccel:auth:{uid}:{user.id if user.is_authenticated else 'anon'}"
|
||||
cached = cache.get(cache_key)
|
||||
if cached is None:
|
||||
allowed = _decide(uid, user)
|
||||
cache.set(cache_key, allowed, _ttl())
|
||||
else:
|
||||
allowed = cached
|
||||
|
||||
return HttpResponse(status=204 if allowed else 403)
|
||||
@@ -1,4 +1,7 @@
|
||||
import React from 'react';
|
||||
import { MediaPageStore } from '../../utils/stores/';
|
||||
import { AutoPlay } from './AutoPlay';
|
||||
import { RelatedMedia } from './RelatedMedia';
|
||||
import PlaylistView from './PlaylistView';
|
||||
|
||||
export default class ViewerSidebar extends React.PureComponent {
|
||||
@@ -9,6 +12,7 @@ export default class ViewerSidebar extends React.PureComponent {
|
||||
playlistData: props.playlistData,
|
||||
isPlaylistPage: !!props.playlistData,
|
||||
activeItem: 0,
|
||||
mediaType: MediaPageStore.get('media-type'),
|
||||
};
|
||||
|
||||
if (props.playlistData) {
|
||||
@@ -23,6 +27,21 @@ export default class ViewerSidebar extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.onMediaLoad = this.onMediaLoad.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MediaPageStore.on('loaded_media_data', this.onMediaLoad);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MediaPageStore.removeListener('loaded_media_data', this.onMediaLoad);
|
||||
}
|
||||
|
||||
onMediaLoad() {
|
||||
this.setState({
|
||||
mediaType: MediaPageStore.get('media-type'),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -30,7 +49,10 @@ export default class ViewerSidebar extends React.PureComponent {
|
||||
<div className="viewer-sidebar">
|
||||
{this.state.isPlaylistPage ? (
|
||||
<PlaylistView activeItem={this.state.activeItem} playlistData={this.props.playlistData} />
|
||||
) : 'video' === this.state.mediaType || 'audio' === this.state.mediaType ? (
|
||||
<AutoPlay />
|
||||
) : null}
|
||||
<RelatedMedia hideFirst={!this.state.isPlaylistPage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// check templates/config/installation/translations.html for more
|
||||
|
||||
export function translateString(str) {
|
||||
return window.TRANSLATION?.[str] || str;
|
||||
return window.TRANSLATION?.[str] ?? str;
|
||||
}
|
||||
|
||||
@@ -10,13 +10,33 @@ Requirements
|
||||
- MediaCMS set as External Tool
|
||||
|
||||
|
||||
Installation
|
||||
------------------
|
||||
The suite consists of two plugins that are distributed as separate ZIP files:
|
||||
|
||||
1. Extract zip file to Moodle root public directory:
|
||||
- filter_mediacms-v1.0.0.zip (Text filter - install FIRST)
|
||||
- tiny_mediacms-v1.0.0.zip (TinyMCE editor button - requires the filter)
|
||||
|
||||
tiny_mediacms declares a dependency on filter_mediacms, so Moodle will not
|
||||
let you install the editor plugin without the filter being present.
|
||||
|
||||
|
||||
Installation - Option A: Upload through Moodle (recommended)
|
||||
-------------------------------------------------------------
|
||||
|
||||
1. Log in as Administrator
|
||||
2. Go to: Site Administration → Plugins → Install plugins
|
||||
3. Upload filter_mediacms-v1.0.0.zip and complete the installation
|
||||
4. Upload tiny_mediacms-v1.0.0.zip and complete the installation
|
||||
5. Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
|
||||
Installation - Option B: Extract manually
|
||||
-----------------------------------------
|
||||
|
||||
1. Extract the zip files to the Moodle root public directory:
|
||||
|
||||
cd /var/www/moodle/public
|
||||
unzip mediacms-moodle-v1.0.0.zip
|
||||
unzip filter_mediacms-v1.0.0.zip -d filter/
|
||||
unzip tiny_mediacms-v1.0.0.zip -d lib/editor/tiny/plugins/
|
||||
|
||||
This will place files in:
|
||||
- filter/mediacms/
|
||||
@@ -33,7 +53,10 @@ Installation
|
||||
- Both plugins will be installed automatically
|
||||
- Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
4. Make sure Filter is enabled
|
||||
After installation (both options)
|
||||
---------------------------------
|
||||
|
||||
Make sure the Filter is enabled
|
||||
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
|
||||
Then place it at the top of the filter. This is important, otherwise embeds won't load.
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Version: 1.0.0, tested on Moodle 5
|
||||
|
||||
> **Moodle plugins directory**
|
||||
> - Filter plugin: https://moodle.org/plugins/filter_mediacms
|
||||
> - TinyMCE editor plugin: https://moodle.org/plugins/tiny_mediacms
|
||||
|
||||
This plugin provides complete MediaCMS integration for Moodle, consisting of two plugins that work together with unified settings:
|
||||
|
||||
1. **Filter Plugin (filter_mediacms):**
|
||||
@@ -19,8 +23,37 @@ This plugin provides complete MediaCMS integration for Moodle, consisting of two
|
||||
|
||||
## Installation
|
||||
|
||||
Upload the plugin in Moodle's public directory and unzip
|
||||
# cd /var/www/moodle/public ; cp /root/mediacms-moodle-v1.0.0.zip . && unzip mediacms-moodle-v1.0.0.zip
|
||||
The suite ships as **two separate ZIP packages**, one per plugin:
|
||||
|
||||
| Order | Package | Plugin | Notes |
|
||||
|-------|---------|--------|-------|
|
||||
| 1 | `filter_mediacms-v1.0.0.zip` | `filter_mediacms` | Install **first** — provides the shared core settings |
|
||||
| 2 | `tiny_mediacms-v1.0.0.zip` | `tiny_mediacms` | Depends on `filter_mediacms`; Moodle will block install until the filter is present |
|
||||
|
||||
`tiny_mediacms` declares a hard dependency on `filter_mediacms` in its `version.php`, so the two are always installed together — the editor plugin cannot be installed on its own.
|
||||
|
||||
Build both packages with:
|
||||
|
||||
```bash
|
||||
./build.sh # produces dist/filter_mediacms-v1.0.0.zip and dist/tiny_mediacms-v1.0.0.zip
|
||||
```
|
||||
|
||||
### Option A — Upload through Moodle (recommended)
|
||||
|
||||
1. Log in to Moodle as an Administrator.
|
||||
2. Go to **Site administration > Plugins > Install plugins**.
|
||||
3. Upload `filter_mediacms-v1.0.0.zip` and complete the install.
|
||||
4. Upload `tiny_mediacms-v1.0.0.zip` and complete the install.
|
||||
|
||||
### Option B — Extract manually
|
||||
|
||||
Upload both packages to Moodle's public directory and unzip each to its plugin type folder:
|
||||
|
||||
```bash
|
||||
cd /var/www/moodle/public
|
||||
unzip filter_mediacms-v1.0.0.zip -d filter/
|
||||
unzip tiny_mediacms-v1.0.0.zip -d lib/editor/tiny/plugins/
|
||||
```
|
||||
|
||||
Ensure the web server user (typically `www-data`) has ownership of the new directories:
|
||||
|
||||
@@ -32,11 +65,7 @@ chmod -R 755 /var/www/moodle/public/filter/mediacms
|
||||
chmod -R 755 /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
|
||||
```
|
||||
|
||||
### 3. Install Plugins
|
||||
|
||||
1. Log in to Moodle as an Administrator.
|
||||
2. Go to **Site administration > Notifications**.
|
||||
3. Follow the prompts to upgrade the database and install the new plugins.
|
||||
Then log in as an Administrator, go to **Site administration > Notifications**, and follow the prompts to upgrade the database and install both plugins.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# MediaCMS Moodle Plugin Suite - Build Script
|
||||
# Creates distributable ZIP package
|
||||
# Creates two distributable ZIP packages, one per plugin, as required by the
|
||||
# moodle.org plugins directory (one plugin per ZIP, root folder named after
|
||||
# the plugin).
|
||||
#
|
||||
# Packages:
|
||||
# - filter_mediacms-v<VERSION>.zip (install first)
|
||||
# - tiny_mediacms-v<VERSION>.zip (declares a dependency on filter_mediacms)
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
@@ -19,71 +25,73 @@ echo
|
||||
# Configuration
|
||||
VERSION="1.0.0"
|
||||
BUILD_DATE=$(date +%Y%m%d)
|
||||
PACKAGE_NAME="mediacms-moodle-v${VERSION}"
|
||||
DIST_DIR="dist"
|
||||
BUILD_DIR="${DIST_DIR}/${PACKAGE_NAME}"
|
||||
|
||||
# Create clean dist directory
|
||||
echo -e "${YELLOW}→${NC} Cleaning dist directory..."
|
||||
rm -rf "${DIST_DIR}"
|
||||
mkdir -p "${BUILD_DIR}"
|
||||
mkdir -p "${DIST_DIR}"
|
||||
|
||||
# Copy filter plugin
|
||||
echo -e "${YELLOW}→${NC} Copying filter plugin..."
|
||||
mkdir -p "${BUILD_DIR}/filter"
|
||||
cp -r filter/mediacms "${BUILD_DIR}/filter/"
|
||||
# Remove development files from a staged plugin directory
|
||||
clean_plugin_dir() {
|
||||
local dir="$1"
|
||||
find "${dir}" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${dir}" -type f -name ".DS_Store" -delete 2>/dev/null || true
|
||||
find "${dir}" -type f -name "*.log" -delete 2>/dev/null || true
|
||||
find "${dir}" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${dir}" -type f -name ".gitignore" -delete 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Copy TinyMCE plugin
|
||||
echo -e "${YELLOW}→${NC} Copying TinyMCE plugin..."
|
||||
mkdir -p "${BUILD_DIR}/lib/editor/tiny/plugins"
|
||||
cp -r tiny/mediacms "${BUILD_DIR}/lib/editor/tiny/plugins/"
|
||||
# Stage, zip and checksum a single plugin.
|
||||
# The ZIP root folder must be the plugin folder name ("mediacms"), as the
|
||||
# moodle.org plugins directory expects.
|
||||
build_plugin() {
|
||||
local component="$1" # e.g. filter_mediacms
|
||||
local source_dir="$2" # e.g. filter/mediacms
|
||||
local package_name="${component}-v${VERSION}"
|
||||
local stage_dir="${DIST_DIR}/${package_name}"
|
||||
|
||||
# Copy documentation
|
||||
echo -e "${YELLOW}→${NC} Copying documentation..."
|
||||
cp README.md "${BUILD_DIR}/filter/mediacms/"
|
||||
cp INSTALL.txt "${BUILD_DIR}/filter/mediacms/"
|
||||
echo -e "${YELLOW}→${NC} Packaging ${component}..."
|
||||
mkdir -p "${stage_dir}"
|
||||
cp -r "${source_dir}" "${stage_dir}/mediacms"
|
||||
clean_plugin_dir "${stage_dir}/mediacms"
|
||||
|
||||
# Clean up development files
|
||||
echo -e "${YELLOW}→${NC} Removing development files..."
|
||||
find "${BUILD_DIR}" -type d -name "node_modules" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name ".DS_Store" -delete 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name "*.log" -delete 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
|
||||
find "${BUILD_DIR}" -type f -name ".gitignore" -delete 2>/dev/null || true
|
||||
(cd "${stage_dir}" && zip -r "../${package_name}.zip" mediacms -q)
|
||||
(cd "${DIST_DIR}" && sha256sum "${package_name}.zip" > "${package_name}.zip.sha256")
|
||||
}
|
||||
|
||||
# Remove AMD source files (keep only built versions)
|
||||
echo -e "${YELLOW}→${NC} Cleaning AMD source files..."
|
||||
find "${BUILD_DIR}/lib/editor/tiny/plugins/mediacms/amd" -type f -name "*.js" ! -name "*-lazy.js" ! -path "*/build/*" -delete 2>/dev/null || true
|
||||
# Build filter_mediacms (base plugin - install first)
|
||||
build_plugin "filter_mediacms" "filter/mediacms"
|
||||
|
||||
# Create ZIP archive
|
||||
echo -e "${YELLOW}→${NC} Creating ZIP archive..."
|
||||
cd "${BUILD_DIR}"
|
||||
zip -r "../${PACKAGE_NAME}.zip" . -q
|
||||
cd ../..
|
||||
# Include suite documentation in the filter package
|
||||
echo -e "${YELLOW}→${NC} Copying documentation into filter_mediacms..."
|
||||
cp README.md INSTALL.txt "${DIST_DIR}/filter_mediacms-v${VERSION}/mediacms/"
|
||||
(cd "${DIST_DIR}/filter_mediacms-v${VERSION}" && zip -r "../filter_mediacms-v${VERSION}.zip" mediacms -q)
|
||||
(cd "${DIST_DIR}" && sha256sum "filter_mediacms-v${VERSION}.zip" > "filter_mediacms-v${VERSION}.zip.sha256")
|
||||
|
||||
# Create checksum
|
||||
echo -e "${YELLOW}→${NC} Generating checksum..."
|
||||
cd "${DIST_DIR}"
|
||||
sha256sum "${PACKAGE_NAME}.zip" > "${PACKAGE_NAME}.zip.sha256"
|
||||
cd ..
|
||||
# Build tiny_mediacms (depends on filter_mediacms)
|
||||
build_plugin "tiny_mediacms" "tiny/mediacms"
|
||||
cp INSTALL.txt "${DIST_DIR}/tiny_mediacms-v${VERSION}/mediacms/"
|
||||
(cd "${DIST_DIR}/tiny_mediacms-v${VERSION}" && zip -r "../tiny_mediacms-v${VERSION}.zip" mediacms -q)
|
||||
(cd "${DIST_DIR}" && sha256sum "tiny_mediacms-v${VERSION}.zip" > "tiny_mediacms-v${VERSION}.zip.sha256")
|
||||
|
||||
# Display results
|
||||
ZIP_SIZE=$(du -h "${DIST_DIR}/${PACKAGE_NAME}.zip" | cut -f1)
|
||||
echo
|
||||
echo -e "${GREEN}✓ Build complete!${NC}"
|
||||
echo
|
||||
echo "Package: ${DIST_DIR}/${PACKAGE_NAME}.zip"
|
||||
echo "Size: ${ZIP_SIZE}"
|
||||
echo "Checksum: ${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
|
||||
for component in filter_mediacms tiny_mediacms; do
|
||||
package="${DIST_DIR}/${component}-v${VERSION}.zip"
|
||||
echo "Package: ${package} ($(du -h "${package}" | cut -f1))"
|
||||
done
|
||||
echo
|
||||
echo -e "${YELLOW}Contents:${NC}"
|
||||
echo " - filter/mediacms/ (includes docs)"
|
||||
echo " - lib/editor/tiny/plugins/mediacms/"
|
||||
echo -e "${YELLOW}Install order:${NC}"
|
||||
echo " 1. filter_mediacms-v${VERSION}.zip"
|
||||
echo " 2. tiny_mediacms-v${VERSION}.zip (requires filter_mediacms)"
|
||||
echo
|
||||
echo -e "${GREEN}Ready for distribution!${NC}"
|
||||
echo
|
||||
|
||||
# Show checksum
|
||||
echo -e "${YELLOW}SHA256 Checksum:${NC}"
|
||||
cat "${DIST_DIR}/${PACKAGE_NAME}.zip.sha256"
|
||||
# Show checksums
|
||||
echo -e "${YELLOW}SHA256 Checksums:${NC}"
|
||||
cat "${DIST_DIR}"/*.sha256
|
||||
echo
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
46d5fcfb7a6f75c527fef5672b49cba923b677a19920c6b115b76496492428b5 filter_mediacms-v1.0.0.zip
|
||||
@@ -0,0 +1,77 @@
|
||||
================================================================================
|
||||
MediaCMS Moodle Plugin Suite v1.0.0
|
||||
Installation Guide
|
||||
================================================================================
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Moodle 4.5 or later
|
||||
- MediaCMS instance
|
||||
- MediaCMS set as External Tool
|
||||
|
||||
|
||||
The suite consists of two plugins that are distributed as separate ZIP files:
|
||||
|
||||
- filter_mediacms-v1.0.0.zip (Text filter - install FIRST)
|
||||
- tiny_mediacms-v1.0.0.zip (TinyMCE editor button - requires the filter)
|
||||
|
||||
tiny_mediacms declares a dependency on filter_mediacms, so Moodle will not
|
||||
let you install the editor plugin without the filter being present.
|
||||
|
||||
|
||||
Installation - Option A: Upload through Moodle (recommended)
|
||||
-------------------------------------------------------------
|
||||
|
||||
1. Log in as Administrator
|
||||
2. Go to: Site Administration → Plugins → Install plugins
|
||||
3. Upload filter_mediacms-v1.0.0.zip and complete the installation
|
||||
4. Upload tiny_mediacms-v1.0.0.zip and complete the installation
|
||||
5. Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
|
||||
Installation - Option B: Extract manually
|
||||
-----------------------------------------
|
||||
|
||||
1. Extract the zip files to the Moodle root public directory:
|
||||
|
||||
cd /var/www/moodle/public
|
||||
unzip filter_mediacms-v1.0.0.zip -d filter/
|
||||
unzip tiny_mediacms-v1.0.0.zip -d lib/editor/tiny/plugins/
|
||||
|
||||
This will place files in:
|
||||
- filter/mediacms/
|
||||
- lib/editor/tiny/plugins/mediacms/
|
||||
|
||||
2. Set permissions
|
||||
chown -R www-data:www-data filter/mediacms
|
||||
chown -R www-data:www-data lib/editor/tiny/plugins/mediacms
|
||||
|
||||
3. Install through Moodle
|
||||
- Log in as Administrator
|
||||
- Go to: Site Administration → Notifications
|
||||
- Click "Upgrade Moodle database now"
|
||||
- Both plugins will be installed automatically
|
||||
- Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
After installation (both options)
|
||||
---------------------------------
|
||||
|
||||
Make sure the Filter is enabled
|
||||
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
|
||||
Then place it at the top of the filter. This is important, otherwise embeds won't load.
|
||||
|
||||
|
||||
What to expect
|
||||
-------
|
||||
|
||||
1. Create a test course
|
||||
2. Add a page or label
|
||||
3. Click MediaCMS button in TinyMCE editor
|
||||
4. Try inserting from video library or pasting a URL
|
||||
|
||||
SUPPORT
|
||||
-------
|
||||
Issues: https://github.com/mediacms-io/mediacms/issues
|
||||
Docs: https://docs.mediacms.io
|
||||
|
||||
================================================================================
|
||||
@@ -0,0 +1,135 @@
|
||||
# MediaCMS for Moodle
|
||||
|
||||
Version: 1.0.0, tested on Moodle 5
|
||||
|
||||
This plugin provides complete MediaCMS integration for Moodle, consisting of two plugins that work together with unified settings:
|
||||
|
||||
1. **Filter Plugin (filter_mediacms):**
|
||||
* Handles LTI 1.3 authentication and secure video launches
|
||||
* Auto-converts MediaCMS URLs to embedded players
|
||||
* **Provides core settings** (MediaCMS URL, LTI Tool ID) used by both plugins
|
||||
* **Location:** Admin, Plugins, Manage filters, MediaCMS
|
||||
|
||||
2. **Editor Plugin (tiny_mediacms):**
|
||||
* Adds MediaCMS button to TinyMCE editor
|
||||
* Browse authenticated video library via LTI Deep Linking
|
||||
* Configure embed options (dimensions, display, start time)
|
||||
* **Reads core settings** from filter plugin
|
||||
* **Location:** Admin, Plugins, TinyMCE, MediaCMS
|
||||
|
||||
## Installation
|
||||
|
||||
The suite ships as **two separate ZIP packages**, one per plugin:
|
||||
|
||||
| Order | Package | Plugin | Notes |
|
||||
|-------|---------|--------|-------|
|
||||
| 1 | `filter_mediacms-v1.0.0.zip` | `filter_mediacms` | Install **first** — provides the shared core settings |
|
||||
| 2 | `tiny_mediacms-v1.0.0.zip` | `tiny_mediacms` | Depends on `filter_mediacms`; Moodle will block install until the filter is present |
|
||||
|
||||
`tiny_mediacms` declares a hard dependency on `filter_mediacms` in its `version.php`, so the two are always installed together — the editor plugin cannot be installed on its own.
|
||||
|
||||
Build both packages with:
|
||||
|
||||
```bash
|
||||
./build.sh # produces dist/filter_mediacms-v1.0.0.zip and dist/tiny_mediacms-v1.0.0.zip
|
||||
```
|
||||
|
||||
### Option A — Upload through Moodle (recommended)
|
||||
|
||||
1. Log in to Moodle as an Administrator.
|
||||
2. Go to **Site administration > Plugins > Install plugins**.
|
||||
3. Upload `filter_mediacms-v1.0.0.zip` and complete the install.
|
||||
4. Upload `tiny_mediacms-v1.0.0.zip` and complete the install.
|
||||
|
||||
### Option B — Extract manually
|
||||
|
||||
Upload both packages to Moodle's public directory and unzip each to its plugin type folder:
|
||||
|
||||
```bash
|
||||
cd /var/www/moodle/public
|
||||
unzip filter_mediacms-v1.0.0.zip -d filter/
|
||||
unzip tiny_mediacms-v1.0.0.zip -d lib/editor/tiny/plugins/
|
||||
```
|
||||
|
||||
Ensure the web server user (typically `www-data`) has ownership of the new directories:
|
||||
|
||||
```bash
|
||||
# Example for Ubuntu/Debian systems
|
||||
chown -R www-data:www-data /var/www/moodle/public/filter/mediacms
|
||||
chown -R www-data:www-data /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
|
||||
chmod -R 755 /var/www/moodle/public/filter/mediacms
|
||||
chmod -R 755 /var/www/moodle/public/lib/editor/tiny/plugins/mediacms
|
||||
```
|
||||
|
||||
Then log in as an Administrator, go to **Site administration > Notifications**, and follow the prompts to upgrade the database and install both plugins.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Step 1: Core Settings (Required) - Configure Once
|
||||
|
||||
Go to **Site administration > Plugins > Filters > MediaCMS** (Settings)
|
||||
|
||||
* **MediaCMS URL:** Enter your MediaCMS instance URL (e.g., `https://lti.mediacms.io`)
|
||||
* **LTI Tool:** Select the External Tool configuration for MediaCMS
|
||||
* *First create an LTI 1.3 tool at: Site administration > Plugins > Activity modules > External tool > Manage tools*
|
||||
|
||||
> **✨ Note:** These core settings are automatically used by **both** the filter and TinyMCE editor plugin.
|
||||
|
||||
### Step 2: Enable Filter
|
||||
|
||||
1. Go to **Site administration > Plugins > Filters > Manage filters**
|
||||
2. Set **MediaCMS** to "On"
|
||||
|
||||
### Step 3: Configure Auto-convert Defaults (Optional)
|
||||
|
||||
Go to **Site administration > Plugins > Text editors > TinyMCE editor > MediaCMS settings**
|
||||
|
||||
Configure default display options for auto-converted URLs:
|
||||
* Show video title
|
||||
* Link video title
|
||||
* Show related videos
|
||||
* Show user avatar
|
||||
|
||||
> **Note:** The core settings (URL, LTI Tool) are managed in the filter plugin settings.
|
||||
|
||||
## Usage
|
||||
|
||||
### For Teachers (Editor)
|
||||
|
||||
1. In any text editor (TinyMCE), click the **MediaCMS** icon (or "Insert MediaCMS Media" from the Insert menu).
|
||||
2. You can:
|
||||
* **Paste a URL:** Paste a View or Embed URL.
|
||||
* **Video Library:** Click the "Video Library" tab to browse and select videos (requires LTI Deep Linking configuration).
|
||||
3. The video will appear as a placeholder or iframe in the editor.
|
||||
|
||||
### For Students (Display)
|
||||
|
||||
When content is viewed, the Filter will ensure the video is loaded securely via LTI 1.3, authenticating the user with MediaCMS automatically.
|
||||
|
||||
|
||||
|
||||
## Build instructions / Developing with the plugin
|
||||
|
||||
two types of changes: php (no build), js (build with npx grunt amd)
|
||||
|
||||
needs moodle/
|
||||
npx version, dependencies etc
|
||||
|
||||
1. make changes here in lms-plugins/mediacms-moodle
|
||||
2. copy to moodle
|
||||
3. run `npx grunt amd` in moodle to build the JS files
|
||||
4. from moodle copy back
|
||||
sudo cp -r ~/mediacms/lms-plugins/mediacms-moodle/tiny/mediacms/ -r ~/mediacms/moodle/public/lib/editor/tiny/plugins/
|
||||
|
||||
5. cd ~/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms/
|
||||
|
||||
npx grunt amd
|
||||
6.
|
||||
cp files back...
|
||||
sudo cp -r /home/user/mediacms/moodle/public/lib/editor/tiny/plugins/mediacms /home/user/mediacms/lms-plugins/mediacms-moodle/tiny/
|
||||
|
||||
php admin/cli/purge_caches.php after
|
||||
|
||||
|
||||
### Troubleshooting
|
||||
Admin, advanced theme settings, add `My Media|/filter/mediacms/my_media.php` in case the position is not workin
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/**
|
||||
* Hook callbacks for filter_mediacms.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace filter_mediacms;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
class hooks {
|
||||
/**
|
||||
* Appends the My Media link to the custom menus after configuration is loaded.
|
||||
*
|
||||
* @param \core\hook\after_config $hook
|
||||
*/
|
||||
public static function append_my_media_link(\core\hook\after_config $hook): void {
|
||||
global $CFG;
|
||||
|
||||
$navposition = get_config('filter_mediacms', 'mymedia_nav_position');
|
||||
|
||||
// MEDIACMS_NAV_PLACEMENT_NONE = 2
|
||||
if ($navposition == 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!get_string_manager()->string_exists('mymedia', 'filter_mediacms')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$linktext = get_string('mymedia', 'filter_mediacms');
|
||||
$linkurl = '/filter/mediacms/my_media.php';
|
||||
$menuitem = "\n{$linktext}|{$linkurl}";
|
||||
|
||||
// MEDIACMS_NAV_PLACEMENT_PROFILE = 1
|
||||
if ($navposition == 1) {
|
||||
// Add to User Profile Menu
|
||||
if (!isset($CFG->customusermenuitems)) {
|
||||
$CFG->customusermenuitems = '';
|
||||
}
|
||||
if (strpos($CFG->customusermenuitems, $linktext) === false) {
|
||||
$CFG->customusermenuitems .= $menuitem;
|
||||
}
|
||||
} else {
|
||||
// Default to Top Navigation Menu (MEDIACMS_NAV_PLACEMENT_TOP = 0)
|
||||
if (!isset($CFG->custommenuitems)) {
|
||||
$CFG->custommenuitems = '';
|
||||
}
|
||||
if (strpos($CFG->custommenuitems, $linktext) === false) {
|
||||
$CFG->custommenuitems .= $menuitem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace filter_mediacms\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace filter_mediacms;
|
||||
|
||||
use moodle_url;
|
||||
use html_writer;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* MediaCMS text filter.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class text_filter extends \core_filters\text_filter {
|
||||
|
||||
/**
|
||||
* Filter method.
|
||||
*
|
||||
* @param string $text The text to filter.
|
||||
* @param array $options Filter options.
|
||||
* @return string The filtered text.
|
||||
*/
|
||||
public function filter($text, array $options = array()) {
|
||||
if (!is_string($text) or empty($text)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
if (empty($mediacmsurl)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$newtext = $text;
|
||||
|
||||
// 1. Handle [mediacms:TOKEN] tag
|
||||
$pattern_tag = '/\[mediacms:([a-zA-Z0-9]+)\]/';
|
||||
$newtext = preg_replace_callback($pattern_tag, [$this, 'callback_tag'], $newtext);
|
||||
|
||||
// 2a. Convert MediaCMS URLs that are already inside <iframe src="..."> attributes
|
||||
// (saved by the TinyMCE plugin) to launch.php URLs, preserving all other iframe
|
||||
// attributes. This must run before 2b so the URL pattern below does not try to
|
||||
// replace just the URL string and produce broken HTML inside the src attribute.
|
||||
$iframe_src_pattern = '/(<iframe\b[^>]*?\s)src=(["\'])('
|
||||
. $scheme . ':\/\/' . $host . $path_prefix
|
||||
. '\/(view|embed)\?m=([a-zA-Z0-9]+)[^"\']*)\2/is';
|
||||
$newtext = preg_replace_callback($iframe_src_pattern, [$this, 'callback_iframe_src'], $newtext);
|
||||
|
||||
// 2b. Auto-convert plain-text MediaCMS URLs to embedded players.
|
||||
// First, protect text-only links from being converted
|
||||
// by temporarily replacing them with placeholders.
|
||||
$textlink_placeholders = [];
|
||||
$textlink_pattern = '/<a\s+[^>]*data-mediacms-textlink=["\']true["\'][^>]*>.*?<\/a>/is';
|
||||
|
||||
$newtext = preg_replace_callback($textlink_pattern, function($matches) use (&$textlink_placeholders) {
|
||||
$placeholder = '###MEDIACMS_TEXTLINK_' . count($textlink_placeholders) . '###';
|
||||
$textlink_placeholders[$placeholder] = $matches[0];
|
||||
return $placeholder;
|
||||
}, $newtext);
|
||||
|
||||
// Regex for plain-text MediaCMS view/embed URLs (not inside iframe src="" — those
|
||||
// were already handled by 2a above).
|
||||
$parsed_url = parse_url($mediacmsurl);
|
||||
$host = preg_quote($parsed_url['host'] ?? '', '/');
|
||||
$scheme = preg_quote($parsed_url['scheme'] ?? 'https', '/');
|
||||
|
||||
// Allow http or https, and optional path prefix
|
||||
$path_prefix = preg_quote(rtrim($parsed_url['path'] ?? '', '/'), '/');
|
||||
|
||||
// Pattern: https://HOST/PREFIX/view?m=TOKEN
|
||||
// Also handle /embed?m=TOKEN
|
||||
$pattern_url = '/(' . $scheme . ':\/\/' . $host . $path_prefix . '\/(view|embed)\?m=([a-zA-Z0-9]+)(?:&[^\s<]*)?)/';
|
||||
|
||||
$newtext = preg_replace_callback($pattern_url, [$this, 'callback_url'], $newtext);
|
||||
|
||||
// Restore protected text-only links as modal launchers
|
||||
foreach ($textlink_placeholders as $placeholder => $original) {
|
||||
$newtext = str_replace($placeholder, $this->transform_textlink($original), $newtext);
|
||||
}
|
||||
|
||||
return $newtext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for MediaCMS URLs found inside existing <iframe src="..."> attributes.
|
||||
* Replaces only the src value with a launch.php URL so the rest of the iframe
|
||||
* attributes (width, height, style, etc.) are preserved unchanged.
|
||||
*
|
||||
* $matches[1] — everything in the opening tag before `src=`
|
||||
* $matches[2] — the quote character (" or ')
|
||||
* $matches[3] — the full MediaCMS URL
|
||||
* $matches[4] — "view" or "embed"
|
||||
* $matches[5] — the media friendly_token
|
||||
*/
|
||||
public function callback_iframe_src($matches) {
|
||||
global $COURSE;
|
||||
|
||||
$full_url = $matches[3];
|
||||
$token = $matches[5];
|
||||
$before_src = $matches[1]; // e.g. '<iframe style="..." '
|
||||
$quote = $matches[2];
|
||||
|
||||
// Extract embed params from the original URL.
|
||||
$embed_params = [];
|
||||
$parsed_qs = parse_url($full_url);
|
||||
if (isset($parsed_qs['query'])) {
|
||||
// The saved URL may have HTML-entity-encoded ampersands.
|
||||
$raw_query = html_entity_decode($parsed_qs['query'], ENT_QUOTES | ENT_HTML5);
|
||||
parse_str($raw_query, $query_params);
|
||||
foreach (['showTitle', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'] as $p) {
|
||||
if (isset($query_params[$p])) {
|
||||
$embed_params[$p] = $query_params[$p];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$launch_params = array_merge(
|
||||
['token' => $token, 'courseid' => $COURSE->id ?? 0],
|
||||
$embed_params
|
||||
);
|
||||
|
||||
$launch_url = (new moodle_url('/filter/mediacms/launch.php', $launch_params))->out(false);
|
||||
|
||||
// Reconstruct the opening iframe tag with the new src, keeping all other attributes.
|
||||
return $before_src . 'src=' . $quote . $launch_url . $quote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for [mediacms:TOKEN]
|
||||
*/
|
||||
public function callback_tag($matches) {
|
||||
return $this->generate_iframe($matches[1], []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for URLs
|
||||
*/
|
||||
public function callback_url($matches) {
|
||||
// matches[0] is the full matched string
|
||||
// matches[1] is full URL, matches[3] is token
|
||||
|
||||
// Check if this URL is inside a text-only link
|
||||
// by looking at the context around the match
|
||||
$fullmatch = $matches[0];
|
||||
|
||||
// If this is already inside an <a> tag with data-mediacms-textlink="true",
|
||||
// return the original URL unchanged
|
||||
// We'll check this in the main filter method instead
|
||||
|
||||
$token = $matches[3];
|
||||
|
||||
// Extract additional embed parameters from the URL
|
||||
$embed_params = [];
|
||||
$full_url = $matches[1];
|
||||
|
||||
// Decode HTML entities (& -> &) before parsing
|
||||
$full_url = html_entity_decode($full_url, ENT_QUOTES | ENT_HTML5);
|
||||
|
||||
$parsed_url = parse_url($full_url);
|
||||
|
||||
if (isset($parsed_url['query'])) {
|
||||
parse_str($parsed_url['query'], $query_params);
|
||||
|
||||
// Extract embed-related parameters
|
||||
$supported_params = ['showTitle', 'showUserAvatar', 'linkTitle', 't', 'width', 'height'];
|
||||
foreach ($supported_params as $param) {
|
||||
if (isset($query_params[$param])) {
|
||||
$embed_params[$param] = $query_params[$param];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->generate_iframe($token, $embed_params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the Iframe pointing to launch.php
|
||||
*/
|
||||
private function generate_iframe($token, $embed_params = []) {
|
||||
global $CFG, $COURSE;
|
||||
|
||||
// Use width/height from embed params if provided, no defaults
|
||||
$width = isset($embed_params['width']) ? $embed_params['width'] : null;
|
||||
$height = isset($embed_params['height']) ? $embed_params['height'] : null;
|
||||
$courseid = $COURSE->id ?? 0;
|
||||
|
||||
// Build launch URL parameters
|
||||
$launch_params = [
|
||||
'token' => $token,
|
||||
'courseid' => $courseid
|
||||
];
|
||||
|
||||
// Add width/height only if provided
|
||||
if ($width !== null) {
|
||||
$launch_params['width'] = $width;
|
||||
}
|
||||
if ($height !== null) {
|
||||
$launch_params['height'] = $height;
|
||||
}
|
||||
|
||||
// Add other embed parameters if provided (excluding width/height as they're already handled)
|
||||
foreach ($embed_params as $key => $value) {
|
||||
if ($key !== 'width' && $key !== 'height') {
|
||||
$launch_params[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$launchurl = new moodle_url('/filter/mediacms/launch.php', $launch_params);
|
||||
|
||||
// Build responsive CSS
|
||||
$max_width = ($width !== null) ? (int)$width : 640;
|
||||
if ($width !== null && $height !== null && (int)$height > 0) {
|
||||
$aspect_ratio_css = (int)$width . ' / ' . (int)$height;
|
||||
} else {
|
||||
$aspect_ratio_css = '16 / 9';
|
||||
}
|
||||
$style = 'width:100%;max-width:' . $max_width . 'px;aspect-ratio:' . $aspect_ratio_css
|
||||
. ';display:block;margin:0 auto;border:0;';
|
||||
|
||||
$iframe_attrs = [
|
||||
'src' => $launchurl->out(false),
|
||||
'style' => $style,
|
||||
'frameborder' => '0',
|
||||
'allowfullscreen' => 'allowfullscreen',
|
||||
'title' => 'MediaCMS Video',
|
||||
];
|
||||
|
||||
$iframe = html_writer::tag('iframe', '', $iframe_attrs);
|
||||
|
||||
return $iframe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a text-only link into a link that replaces itself with an inline iframe on click.
|
||||
*
|
||||
* @param string $anchor_html Original <a ...>...</a> HTML
|
||||
* @return string Transformed HTML (or original if token cannot be extracted)
|
||||
*/
|
||||
private function transform_textlink($anchor_html) {
|
||||
global $COURSE;
|
||||
|
||||
// Extract href.
|
||||
if (!preg_match('/href=["\']([^"\']+)["\']/', $anchor_html, $href_matches)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
$href = html_entity_decode($href_matches[1], ENT_QUOTES | ENT_HTML5);
|
||||
|
||||
// Extract ?m=TOKEN and optional ?t=seconds.
|
||||
parse_str(parse_url($href, PHP_URL_QUERY) ?? '', $query_params);
|
||||
$token = $query_params['m'] ?? null;
|
||||
if (!$token || !preg_match('/^[a-zA-Z0-9]+$/', $token)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
$start_time = isset($query_params['t']) ? (int)$query_params['t'] : null;
|
||||
|
||||
// Extract inner link text.
|
||||
if (!preg_match('/<a[^>]*>(.*?)<\/a>/is', $anchor_html, $text_matches)) {
|
||||
return $anchor_html;
|
||||
}
|
||||
|
||||
$courseid = isset($COURSE->id) ? (int)$COURSE->id : 0;
|
||||
|
||||
$view_params = ['token' => $token, 'courseid' => $courseid];
|
||||
if ($start_time !== null && $start_time > 0) {
|
||||
$view_params['t'] = $start_time;
|
||||
}
|
||||
|
||||
$view_url = new moodle_url('/filter/mediacms/my_media.php', $view_params);
|
||||
|
||||
return html_writer::tag('a', $text_matches[1], [
|
||||
'href' => $view_url->out(false),
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Hook registrations for filter_mediacms.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$callbacks = [
|
||||
[
|
||||
'hook' => \core\hook\after_config::class,
|
||||
'callback' => [\filter_mediacms\hooks::class, 'append_my_media_link'],
|
||||
'priority' => 100,
|
||||
],
|
||||
];
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Post-installation hook.
|
||||
*/
|
||||
function xmldb_filter_mediacms_install() {
|
||||
global $CFG, $DB;
|
||||
require_once($CFG->libdir . '/filterlib.php');
|
||||
|
||||
// Enable the filter globally.
|
||||
filter_set_global_state('filter_mediacms', TEXTFILTER_ON);
|
||||
|
||||
// Move to top priority (lowest sortorder).
|
||||
$syscontextid = context_system::instance()->id;
|
||||
$filters = $DB->get_records('filter_active', ['contextid' => $syscontextid], 'sortorder ASC');
|
||||
|
||||
if (empty($filters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Separate mediacms from other filters by inspecting the record property,
|
||||
// not the array key (get_records indexes by id, not by filter name).
|
||||
$mediacmsrecord = null;
|
||||
$otherrecords = [];
|
||||
foreach ($filters as $record) {
|
||||
if ($record->filter === 'filter_mediacms') {
|
||||
$mediacmsrecord = $record;
|
||||
} else {
|
||||
$otherrecords[] = $record;
|
||||
}
|
||||
}
|
||||
|
||||
// Reassign sortorders: mediacms first, then everyone else.
|
||||
$sortorder = 1;
|
||||
if ($mediacmsrecord) {
|
||||
$mediacmsrecord->sortorder = $sortorder++;
|
||||
$DB->update_record('filter_active', $mediacmsrecord);
|
||||
}
|
||||
foreach ($otherrecords as $record) {
|
||||
$record->sortorder = $sortorder++;
|
||||
$DB->update_record('filter_active', $record);
|
||||
}
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['filtername'] = 'MediaCMS';
|
||||
$string['pluginname'] = 'MediaCMS';
|
||||
$string['coresettings'] = 'Core MediaCMS Settings';
|
||||
$string['coresettings_desc'] = 'These settings are shared with the TinyMCE MediaCMS editor plugin.';
|
||||
$string['mediacmsurl'] = 'MediaCMS URL';
|
||||
$string['mediacmsurl_desc'] = 'The base URL of your MediaCMS instance (e.g., https://lti.mediacms.io). This setting is used by both the filter and the TinyMCE editor plugin.';
|
||||
$string['ltitoolid'] = 'LTI Tool';
|
||||
$string['ltitoolid_desc'] = 'Select the External Tool configuration for MediaCMS. This enables the video library in the TinyMCE editor and LTI authentication. To set up an LTI tool, go to Site Administration > Plugins > Activity modules > External tool > Manage tools.';
|
||||
$string['noltitoolsfound'] = 'No LTI tools found';
|
||||
$string['iframewidth'] = 'Default Width';
|
||||
$string['iframewidth_desc'] = 'Default width for embedded videos (pixels).';
|
||||
$string['iframeheight'] = 'Default Height';
|
||||
$string['iframeheight_desc'] = 'Default height for embedded videos (pixels).';
|
||||
$string['enableautoconvert'] = 'Auto-convert URLs';
|
||||
$string['enableautoconvert_desc'] = 'Automatically convert MediaCMS URLs (e.g., /view?m=xyz) in text to embedded players.';
|
||||
$string['privacy:metadata'] = 'The MediaCMS filter does not store any personal data.';
|
||||
|
||||
$string['mymedia'] = 'My Media';
|
||||
$string['notconfigured'] = 'MediaCMS is not fully configured. Please set the MediaCMS URL and LTI Tool in Site Administration → Plugins → Filters → MediaCMS.';
|
||||
$string['ltitoolnotfound'] = 'The configured LTI tool could not be found. Please check the MediaCMS filter settings.';
|
||||
$string['cannotcreatedummyactivity'] = 'Could not create the MediaCMS launcher activity. Please check course permissions.';
|
||||
|
||||
$string['mymediaposition'] = 'My Media Link Position';
|
||||
$string['mymediaposition_desc'] = 'Select where the "My Media" link should appear in the Moodle interface.';
|
||||
$string['pos_topbar'] = 'Top Navigation Bar';
|
||||
$string['pos_userdrop'] = 'User Profile Dropdown';
|
||||
$string['pos_none'] = 'None (Do not display)';
|
||||
|
||||
$string['shareembeddedmedia'] = 'Share Embedded Media';
|
||||
$string['shareembeddedmedia_desc'] = 'When enabled, a student viewing embedded media is automatically granted viewer permission on that media (it appears under "Shared with me"). Disable this to allow viewing without creating a sharing record.';
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
/**
|
||||
* LTI Launch for MediaCMS Filter
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
|
||||
global $SITE, $DB, $PAGE, $OUTPUT, $CFG, $SESSION;
|
||||
|
||||
require_login();
|
||||
|
||||
$mediatoken = required_param('token', PARAM_ALPHANUMEXT);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
$height = optional_param('height', 0, PARAM_INT);
|
||||
$width = optional_param('width', 0, PARAM_INT);
|
||||
|
||||
// Extract embed parameters
|
||||
$showTitle = optional_param('showTitle', '', PARAM_TEXT);
|
||||
$showUserAvatar = optional_param('showUserAvatar', '', PARAM_TEXT);
|
||||
$linkTitle = optional_param('linkTitle', '', PARAM_TEXT);
|
||||
$startTime = optional_param('t', '', PARAM_TEXT);
|
||||
$show_media_page = optional_param('show_media_page', '', PARAM_TEXT);
|
||||
|
||||
// Get configuration
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
$share_raw = get_config('filter_mediacms', 'share_embedded_media');
|
||||
$share_embedded_media = ($share_raw === false) ? 1 : (int)(bool)$share_raw;
|
||||
|
||||
if (empty($mediacmsurl)) {
|
||||
die('MediaCMS URL not configured');
|
||||
}
|
||||
|
||||
$type = false;
|
||||
if (!empty($ltitoolid)) {
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
}
|
||||
if (!$type) {
|
||||
die('LTI tool not found or not configured.');
|
||||
}
|
||||
|
||||
// Set up context
|
||||
if ($courseid && $courseid != SITEID) {
|
||||
$context = context_course::instance($courseid);
|
||||
$course = get_course($courseid);
|
||||
} else {
|
||||
$context = context_system::instance();
|
||||
$course = $SITE;
|
||||
}
|
||||
|
||||
// Build custom params for this video embed.
|
||||
$custom_params = ["media_friendly_token=" . $mediatoken];
|
||||
|
||||
if ($showTitle !== '') {
|
||||
$custom_params[] = "embed_show_title=" . $showTitle;
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$custom_params[] = "embed_show_user_avatar=" . $showUserAvatar;
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$custom_params[] = "embed_link_title=" . $linkTitle;
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$custom_params[] = "embed_start_time=" . $startTime;
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$custom_params[] = "show_media_page=true";
|
||||
}
|
||||
$custom_params[] = "embed_share_media=" . $share_embedded_media;
|
||||
|
||||
// Pass the My Media base URL so MediaCMS can navigate the parent frame back into Moodle
|
||||
// when the user clicks a media title inside the embed player (see parent_media_base in embeddedApp.ts).
|
||||
$my_media_base = (new moodle_url('/filter/mediacms/my_media.php'))->out(false);
|
||||
if ($courseid) {
|
||||
$my_media_base .= '?courseid=' . intval($courseid);
|
||||
}
|
||||
$custom_params[] = "parent_media_base=" . $my_media_base;
|
||||
|
||||
// Set up page
|
||||
$page_params = [
|
||||
'token' => $mediatoken,
|
||||
'courseid' => $courseid,
|
||||
'width' => $width,
|
||||
'height' => $height
|
||||
];
|
||||
|
||||
if ($showTitle !== '') {
|
||||
$page_params['showTitle'] = $showTitle;
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$page_params['showUserAvatar'] = $showUserAvatar;
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$page_params['linkTitle'] = $linkTitle;
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$page_params['t'] = $startTime;
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$page_params['show_media_page'] = 'true';
|
||||
}
|
||||
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/launch.php', $page_params));
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('embedded');
|
||||
$PAGE->set_title('MediaCMS');
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
|
||||
// Build the OIDC login request params directly so we can capture the launchid.
|
||||
// This avoids a shared SESSION key, which would cause a race condition when
|
||||
// multiple videos are embedded on the same page and load simultaneously.
|
||||
$oidc_params = lti_build_login_request($course->id, 0, null, $typeconfig, null, 0, 'MediaCMS Video');
|
||||
|
||||
// Key the custom params by launchid — lti_auth.php retrieves them the same way.
|
||||
$hint = json_decode($oidc_params['lti_message_hint']);
|
||||
$SESSION->{'mediacms_cp_' . $hint->launchid} = implode("\n", $custom_params);
|
||||
|
||||
// Build the fallback hidden fields (MediaCMS encodes them in state as a secondary mechanism).
|
||||
$hidden_fields = '<input type="hidden" name="media_token" value="' . htmlspecialchars($mediatoken, ENT_QUOTES) . '" />';
|
||||
|
||||
if ($showTitle !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_show_title" value="' . htmlspecialchars($showTitle, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($showUserAvatar !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_show_user_avatar" value="' . htmlspecialchars($showUserAvatar, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($linkTitle !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_link_title" value="' . htmlspecialchars($linkTitle, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($startTime !== '') {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_start_time" value="' . htmlspecialchars($startTime, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($show_media_page === 'true') {
|
||||
$hidden_fields .= '<input type="hidden" name="show_media_page" value="true" />';
|
||||
}
|
||||
if ($width) {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_width" value="' . htmlspecialchars($width, ENT_QUOTES) . '" />';
|
||||
}
|
||||
if ($height) {
|
||||
$hidden_fields .= '<input type="hidden" name="embed_height" value="' . htmlspecialchars($height, ENT_QUOTES) . '" />';
|
||||
}
|
||||
|
||||
// Produce the OIDC login form (mirrors lti_initiate_login output).
|
||||
$content = '<form action="' . htmlspecialchars($typeconfig->lti_initiatelogin, ENT_COMPAT)
|
||||
. '" name="ltiInitiateLoginForm" id="ltiInitiateLoginForm"'
|
||||
. ' method="post" encType="application/x-www-form-urlencoded">' . "\n";
|
||||
foreach ($oidc_params as $key => $value) {
|
||||
$key = htmlspecialchars($key, ENT_COMPAT);
|
||||
$value = htmlspecialchars($value, ENT_COMPAT);
|
||||
$content .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
|
||||
}
|
||||
$content .= $hidden_fields . "\n";
|
||||
$content .= "</form>\n";
|
||||
$content .= "<script type=\"text/javascript\">\n"
|
||||
. "//<![CDATA[\n"
|
||||
. "document.ltiInitiateLoginForm.submit();\n"
|
||||
. "//]]>\n"
|
||||
. "</script>\n";
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $content;
|
||||
echo $OUTPUT->footer();
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
define('MEDIACMS_NAV_PLACEMENT_TOP', 0);
|
||||
define('MEDIACMS_NAV_PLACEMENT_PROFILE', 1);
|
||||
define('MEDIACMS_NAV_PLACEMENT_NONE', 2);
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Local helper functions for filter_mediacms
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* MediaCMS custom LTI 1.3 auth endpoint.
|
||||
*
|
||||
* Functionally identical to /mod/lti/auth.php except the no-activity (id=0)
|
||||
* branch only requires the user to be logged in — not moodle/course:manageactivities.
|
||||
*
|
||||
* Custom params (publishdata, redirect_path) are read from the PHP session
|
||||
* where lti_launch.php / select_media_picker.php store them before starting
|
||||
* the OIDC flow.
|
||||
*
|
||||
* Setup required:
|
||||
* 1. MediaCMS admin → LTI Platforms → edit Moodle record:
|
||||
* set "Auth login url" to https://YOUR_MOODLE/filter/mediacms/lti_auth.php
|
||||
* 2. Moodle admin → Site admin → Plugins → Activity modules → External tool
|
||||
* → Manage tools → MediaCMS tool → edit → add to "Redirection URIs":
|
||||
* https://YOUR_MOODLE/filter/mediacms/lti_auth.php
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
global $_POST, $_SERVER, $SESSION;
|
||||
|
||||
if (!isloggedin() && empty($_POST['repost'])) {
|
||||
header_remove("Set-Cookie");
|
||||
$PAGE->set_pagelayout('popup');
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$output = $PAGE->get_renderer('mod_lti');
|
||||
$page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST);
|
||||
echo $output->header();
|
||||
echo $output->render($page);
|
||||
echo $output->footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope = optional_param('scope', '', PARAM_TEXT);
|
||||
$responsetype = optional_param('response_type', '', PARAM_TEXT);
|
||||
$clientid = optional_param('client_id', '', PARAM_TEXT);
|
||||
$redirecturi = optional_param('redirect_uri', '', PARAM_URL);
|
||||
$loginhint = optional_param('login_hint', '', PARAM_TEXT);
|
||||
$ltimessagehintenc = optional_param('lti_message_hint', '', PARAM_TEXT);
|
||||
$state = optional_param('state', '', PARAM_TEXT);
|
||||
$responsemode = optional_param('response_mode', '', PARAM_TEXT);
|
||||
$nonce = optional_param('nonce', '', PARAM_TEXT);
|
||||
$prompt = optional_param('prompt', '', PARAM_TEXT);
|
||||
|
||||
$ok = !empty($scope) && !empty($responsetype) && !empty($clientid) &&
|
||||
!empty($redirecturi) && !empty($loginhint) && !empty($nonce);
|
||||
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
}
|
||||
$ltimessagehint = json_decode($ltimessagehintenc);
|
||||
$ok = $ok && isset($ltimessagehint->launchid);
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
$desc = 'No launch id in LTI hint';
|
||||
}
|
||||
if ($ok && ($scope !== 'openid')) {
|
||||
$ok = false;
|
||||
$error = 'invalid_scope';
|
||||
}
|
||||
if ($ok && ($responsetype !== 'id_token')) {
|
||||
$ok = false;
|
||||
$error = 'unsupported_response_type';
|
||||
}
|
||||
if ($ok) {
|
||||
$launchid = $ltimessagehint->launchid;
|
||||
list($courseid, $typeid, $id, $messagetype, $foruserid, $titleb64, $textb64) =
|
||||
explode(',', $SESSION->$launchid, 7);
|
||||
unset($SESSION->$launchid);
|
||||
$config = lti_get_type_type_config($typeid);
|
||||
$ok = ($clientid === $config->lti_clientid);
|
||||
if (!$ok) {
|
||||
$error = 'unauthorized_client';
|
||||
}
|
||||
}
|
||||
if ($ok && ($loginhint !== $USER->id)) {
|
||||
$ok = false;
|
||||
$error = 'access_denied';
|
||||
}
|
||||
|
||||
if (empty($config)) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
} else {
|
||||
$uris = array_map('trim', explode("\n", $config->lti_redirectionuris));
|
||||
if (!in_array($redirecturi, $uris)) {
|
||||
throw new moodle_exception('invalidrequest', 'error');
|
||||
}
|
||||
}
|
||||
if ($ok) {
|
||||
if (isset($responsemode)) {
|
||||
$ok = ($responsemode === 'form_post');
|
||||
if (!$ok) {
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Invalid response_mode';
|
||||
}
|
||||
} else {
|
||||
$ok = false;
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Missing response_mode';
|
||||
}
|
||||
}
|
||||
if ($ok && !empty($prompt) && ($prompt !== 'none')) {
|
||||
$ok = false;
|
||||
$error = 'invalid_request';
|
||||
$desc = 'Invalid prompt';
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$course = $DB->get_record('course', ['id' => $courseid], '*', MUST_EXIST);
|
||||
|
||||
if ($id) {
|
||||
// Activity-based launch — identical to auth.php's if ($id) branch.
|
||||
$cm = get_coursemodule_from_id('lti', $id, 0, false, MUST_EXIST);
|
||||
$context = context_module::instance($cm->id);
|
||||
require_login($course, true, $cm);
|
||||
require_capability('mod/lti:view', $context);
|
||||
$lti = $DB->get_record('lti', ['id' => $cm->instance], '*', MUST_EXIST);
|
||||
$lti->cmid = $cm->id;
|
||||
list($endpoint, $params) = lti_get_launch_data($lti, $nonce, $messagetype, $foruserid);
|
||||
} else {
|
||||
// No-activity launch — student-accessible.
|
||||
// Custom params (publishdata / redirect_path) were stored in the session
|
||||
// by lti_launch.php or select_media_picker.php before initiating the OIDC flow.
|
||||
require_login($course);
|
||||
|
||||
// launch.php keys params by launchid (safe for concurrent embeds on one page).
|
||||
// lti_launch.php and select_media_picker.php use the fixed key (single-use pages).
|
||||
$customparams = '';
|
||||
$cpkey = 'mediacms_cp_' . $launchid;
|
||||
if (!empty($SESSION->$cpkey)) {
|
||||
$customparams = $SESSION->$cpkey;
|
||||
unset($SESSION->$cpkey);
|
||||
} elseif (!empty($SESSION->mediacms_launch_customparams)) {
|
||||
$customparams = $SESSION->mediacms_launch_customparams;
|
||||
unset($SESSION->mediacms_launch_customparams);
|
||||
}
|
||||
|
||||
// Minimal LTI instance object — enough for lti_get_launch_data to sign the JWT.
|
||||
$lti = new stdClass();
|
||||
$lti->id = 0;
|
||||
$lti->typeid = (int) $typeid;
|
||||
$lti->course = (int) $courseid;
|
||||
$lti->cmid = 0;
|
||||
$lti->name = 'MediaCMS';
|
||||
$lti->toolurl = '';
|
||||
$lti->securetoolurl = '';
|
||||
$lti->instructorcustomparameters = $customparams;
|
||||
$lti->instructorchoicesendname = LTI_SETTING_ALWAYS;
|
||||
$lti->instructorchoicesendemailaddr = LTI_SETTING_ALWAYS;
|
||||
$lti->instructorchoiceacceptgrades = LTI_SETTING_NEVER;
|
||||
$lti->instructorchoiceallowroster = null;
|
||||
$lti->launchcontainer = LTI_LAUNCH_CONTAINER_EMBED_NO_BLOCKS;
|
||||
$lti->resourcekey = '';
|
||||
$lti->password = '';
|
||||
$lti->servicesalt = '';
|
||||
$lti->resource_link_id = 'mediacms_' . $typeid;
|
||||
|
||||
list($endpoint, $params) = lti_get_launch_data(
|
||||
$lti,
|
||||
$nonce,
|
||||
$messagetype ?: 'basic-lti-launch-request',
|
||||
$foruserid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$params['error'] = $error;
|
||||
if (!empty($desc)) {
|
||||
$params['error_description'] = $desc;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($state)) {
|
||||
$params['state'] = $state;
|
||||
}
|
||||
unset($SESSION->lti_message_hint);
|
||||
|
||||
$r = '<form action="' . $redirecturi . "\" name=\"ltiAuthForm\" id=\"ltiAuthForm\" " .
|
||||
"method=\"post\" enctype=\"application/x-www-form-urlencoded\">\n";
|
||||
foreach ($params as $key => $value) {
|
||||
$key = htmlspecialchars($key, ENT_COMPAT);
|
||||
$value = htmlspecialchars($value, ENT_COMPAT);
|
||||
$r .= " <input type=\"hidden\" name=\"{$key}\" value=\"{$value}\"/>\n";
|
||||
}
|
||||
$r .= "</form>\n";
|
||||
$r .= "<script type=\"text/javascript\">\n" .
|
||||
"//<![CDATA[\n" .
|
||||
"document.ltiAuthForm.submit();\n" .
|
||||
"//]]>\n" .
|
||||
"</script>\n";
|
||||
echo $r;
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* My Media LTI launch page — runs inside the iframe from my_media.php.
|
||||
*
|
||||
* Builds custom_publishdata (enrolled courses + roles) and initiates
|
||||
* the LTI 1.3 OIDC login flow, outputting the auto-submit form directly.
|
||||
*
|
||||
* No dummy LTI activity is created. The publishdata is stored in the PHP
|
||||
* session and picked up by lti_auth.php during the OIDC callback.
|
||||
*
|
||||
* Edge case: if the user is not enrolled in any course the launch still
|
||||
* proceeds using the site course (SITEID). MediaCMS will receive an empty
|
||||
* publishdata array and can decide how to handle it (e.g. show a message).
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
|
||||
global $SITE, $DB, $CFG, $USER, $SESSION;
|
||||
|
||||
require_login();
|
||||
|
||||
$mediacmsurl = get_config('filter_mediacms', 'mediacmsurl');
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
$share_raw = get_config('filter_mediacms', 'share_embedded_media');
|
||||
$share_embedded_media = ($share_raw === false) ? 1 : (int)(bool)$share_raw;
|
||||
|
||||
if (empty($mediacmsurl) || empty($ltitoolid)) {
|
||||
throw new moodle_exception('notconfigured', 'filter_mediacms');
|
||||
}
|
||||
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
if (!$type) {
|
||||
throw new moodle_exception('ltitoolnotfound', 'filter_mediacms');
|
||||
}
|
||||
|
||||
// Build publishdata: all courses the user is enrolled in + role.
|
||||
$enrolled_courses = enrol_get_users_courses($USER->id, true, ['id', 'shortname', 'fullname']);
|
||||
|
||||
$publish_data = [];
|
||||
foreach ($enrolled_courses as $enrolled_course) {
|
||||
if ((int)$enrolled_course->id === SITEID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$course_context = context_course::instance($enrolled_course->id);
|
||||
$roles = get_user_roles($course_context, $USER->id, false);
|
||||
|
||||
$role_shortname = 'student';
|
||||
if (!empty($roles)) {
|
||||
$role = reset($roles);
|
||||
$role_shortname = $role->shortname;
|
||||
}
|
||||
|
||||
$publish_data[] = [
|
||||
'id' => (int)$enrolled_course->id,
|
||||
'shortname' => $enrolled_course->shortname,
|
||||
'fullname' => $enrolled_course->fullname,
|
||||
'role' => $role_shortname,
|
||||
];
|
||||
}
|
||||
|
||||
$publishdata_b64 = base64_encode(json_encode($publish_data));
|
||||
|
||||
// Use a course the user is actually enrolled in so they pass require_login
|
||||
// in lti_auth.php. Fall back to SITEID for admins with no course enrolments.
|
||||
$launch_courseid = SITEID;
|
||||
$launch_course = $SITE;
|
||||
foreach ($enrolled_courses as $ec) {
|
||||
if ((int)$ec->id !== SITEID) {
|
||||
$launch_courseid = (int)$ec->id;
|
||||
$launch_course = get_course($launch_courseid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Store publishdata in session — lti_auth.php picks it up after the OIDC roundtrip.
|
||||
$SESSION->mediacms_launch_customparams = 'publishdata=' . $publishdata_b64 . "\nembed_share_media=" . $share_embedded_media;
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
$content = lti_initiate_login($launch_courseid, 0, null, $typeconfig, null, 'MediaCMS My Media');
|
||||
|
||||
echo $content;
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
/**
|
||||
* My Media page — renders the Moodle shell with an LTI iframe.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
|
||||
global $SITE, $PAGE, $OUTPUT, $USER, $COURSE;
|
||||
|
||||
require_login();
|
||||
|
||||
$token = optional_param('token', '', PARAM_ALPHANUMEXT);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
$start_time = optional_param('t', 0, PARAM_INT);
|
||||
|
||||
$context = context_system::instance();
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_course($SITE);
|
||||
$PAGE->set_pagelayout('mydashboard');
|
||||
|
||||
if ($token) {
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php', ['token' => $token]));
|
||||
$PAGE->set_title('MediaCMS');
|
||||
$PAGE->set_heading('MediaCMS');
|
||||
|
||||
$launch_params = [
|
||||
'token' => $token,
|
||||
'courseid' => $courseid ?: ($COURSE->id ?? 0),
|
||||
'show_media_page' => 'true',
|
||||
];
|
||||
if ($start_time > 0) {
|
||||
$launch_params['t'] = $start_time;
|
||||
}
|
||||
$src = (new moodle_url('/filter/mediacms/launch.php', $launch_params))->out(false);
|
||||
} else {
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/my_media.php'));
|
||||
$PAGE->set_title(get_string('mymedia', 'filter_mediacms'));
|
||||
$PAGE->set_heading(get_string('mymedia', 'filter_mediacms'));
|
||||
|
||||
$src = (new moodle_url('/filter/mediacms/lti_launch.php'))->out(false);
|
||||
}
|
||||
|
||||
echo $OUTPUT->header();
|
||||
|
||||
echo html_writer::tag('iframe', '', [
|
||||
'id' => 'contentframe',
|
||||
'src' => $src,
|
||||
'allowfullscreen' => 'true',
|
||||
'allow' => 'autoplay *; fullscreen *; encrypted-media *; camera *; microphone *; display-capture *;',
|
||||
'style' => 'border:none;display:block;width:100%;',
|
||||
]);
|
||||
|
||||
// Fill the iframe to the remaining viewport height and suppress the outer
|
||||
// page scrollbar. Uses requestAnimationFrame so it runs after Moodle theme
|
||||
// JS has finished shifting the layout, and re-fires on window load + resize.
|
||||
echo html_writer::script("
|
||||
(function () {
|
||||
var iframe = document.getElementById('contentframe');
|
||||
|
||||
function resizeIframe() {
|
||||
var top = iframe.getBoundingClientRect().top + window.scrollY;
|
||||
var h = window.innerHeight - iframe.getBoundingClientRect().top;
|
||||
iframe.style.height = Math.max(h, 100) + 'px';
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function schedule() { requestAnimationFrame(resizeIframe); }
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', schedule);
|
||||
} else {
|
||||
schedule();
|
||||
}
|
||||
window.addEventListener('load', schedule);
|
||||
window.addEventListener('resize', schedule);
|
||||
})();
|
||||
");
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Student-accessible MediaCMS media picker launcher.
|
||||
*
|
||||
* Initiates a no-activity LTI 1.3 OIDC login that routes to MediaCMS's
|
||||
* /lti/select-media/ UI. No LTI activity is created in the course.
|
||||
*
|
||||
* The redirect_path custom param is stored in the PHP session and injected
|
||||
* by lti_auth.php during the OIDC callback, so MediaCMS routes to the
|
||||
* media-picker rather than the default My Media page.
|
||||
*
|
||||
* Flow:
|
||||
* 1. TinyMCE plugin opens this URL in an iframe (contentItemUrl).
|
||||
* 2. We store redirect_path in session and start the OIDC flow.
|
||||
* 3. lti_auth.php processes the OIDC callback (no manageactivities check).
|
||||
* 4. MediaCMS receives redirect_path=/lti/select-media/?mode=lms_embed_mode.
|
||||
* 5. User picks a video; MediaCMS sends postMessage({type:'videoSelected',...})
|
||||
* which iframeembed.js already handles.
|
||||
*
|
||||
* @package filter_mediacms
|
||||
* @copyright 2026 MediaCMS
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/lib.php');
|
||||
require_once($CFG->dirroot . '/mod/lti/locallib.php');
|
||||
|
||||
global $DB, $PAGE, $OUTPUT, $SITE, $USER, $SESSION;
|
||||
|
||||
require_login();
|
||||
|
||||
$courseid = required_param('courseid', PARAM_INT);
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
$ltitoolid = get_config('filter_mediacms', 'ltitoolid');
|
||||
|
||||
if (empty($ltitoolid)) {
|
||||
die('MediaCMS LTI tool not configured.');
|
||||
}
|
||||
|
||||
$type = $DB->get_record('lti_types', ['id' => $ltitoolid]);
|
||||
if (!$type) {
|
||||
die('LTI tool not found.');
|
||||
}
|
||||
|
||||
// Resolve course — fall back to the user's first enrolled course if needed.
|
||||
if ($courseid && $courseid != SITEID) {
|
||||
$course = get_course($courseid);
|
||||
$context = context_course::instance($courseid);
|
||||
} else {
|
||||
$course = $SITE;
|
||||
$context = context_system::instance();
|
||||
foreach (enrol_get_users_courses($USER->id, true, ['id']) as $ec) {
|
||||
if ((int)$ec->id !== SITEID) {
|
||||
$course = get_course($ec->id);
|
||||
$context = context_course::instance($ec->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_login($course);
|
||||
|
||||
$PAGE->set_url(new moodle_url('/filter/mediacms/select_media_picker.php', ['courseid' => $course->id]));
|
||||
$PAGE->set_context($context);
|
||||
$PAGE->set_pagelayout('embedded');
|
||||
$PAGE->set_title('MediaCMS Select Media');
|
||||
|
||||
$typeconfig = lti_get_type_type_config($type->id);
|
||||
|
||||
// Store redirect_path in session — lti_auth.php picks it up after the OIDC roundtrip.
|
||||
if ($action === 'upload') {
|
||||
$SESSION->mediacms_launch_customparams = 'redirect_path=/upload?action=select_media';
|
||||
} else {
|
||||
$SESSION->mediacms_launch_customparams = 'redirect_path=/lti/select-media/?mode=lms_embed_mode';
|
||||
}
|
||||
|
||||
$content = lti_initiate_login($course->id, 0, null, $typeconfig, null, 'MediaCMS Select Media');
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $content;
|
||||
echo $OUTPUT->footer();
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
require_once($CFG->dirroot . '/filter/mediacms/lib.php');
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
$settings->add(new admin_setting_heading(
|
||||
'filter_mediacms/coresettings',
|
||||
get_string('coresettings', 'filter_mediacms'),
|
||||
get_string('coresettings_desc', 'filter_mediacms')
|
||||
));
|
||||
|
||||
$settings->add(new admin_setting_configselect(
|
||||
'filter_mediacms/mymedia_nav_position',
|
||||
get_string('mymediaposition', 'filter_mediacms'),
|
||||
get_string('mymediaposition_desc', 'filter_mediacms'),
|
||||
MEDIACMS_NAV_PLACEMENT_TOP,
|
||||
array(
|
||||
MEDIACMS_NAV_PLACEMENT_TOP => 'Top Navigation Bar',
|
||||
MEDIACMS_NAV_PLACEMENT_PROFILE => 'User Profile Dropdown',
|
||||
MEDIACMS_NAV_PLACEMENT_NONE => 'None (Do not display)'
|
||||
)
|
||||
));
|
||||
|
||||
$settings->add(new admin_setting_configtext(
|
||||
'filter_mediacms/mediacmsurl',
|
||||
get_string('mediacmsurl', 'filter_mediacms'),
|
||||
get_string('mediacmsurl_desc', 'filter_mediacms'),
|
||||
'https://lti.mediacms.io',
|
||||
PARAM_URL
|
||||
));
|
||||
|
||||
$ltioptions = [0 => get_string('noltitoolsfound', 'filter_mediacms')];
|
||||
try {
|
||||
$tools = $DB->get_records('lti_types', null, 'name ASC', 'id, name, baseurl');
|
||||
if (!empty($tools)) {
|
||||
$ltioptions = [0 => get_string('choose')];
|
||||
foreach ($tools as $tool) {
|
||||
$ltioptions[$tool->id] = $tool->name . ' (' . $tool->baseurl . ')';
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
$settings->add(new admin_setting_configselect(
|
||||
'filter_mediacms/ltitoolid',
|
||||
get_string('ltitoolid', 'filter_mediacms'),
|
||||
get_string('ltitoolid_desc', 'filter_mediacms'),
|
||||
0,
|
||||
$ltioptions
|
||||
));
|
||||
|
||||
$settings->add(new admin_setting_configcheckbox(
|
||||
'filter_mediacms/share_embedded_media',
|
||||
get_string('shareembeddedmedia', 'filter_mediacms'),
|
||||
get_string('shareembeddedmedia_desc', 'filter_mediacms'),
|
||||
1
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2026051100; // 2026-05-11
|
||||
$plugin->requires = 2024100700; // Requires Moodle 4.5+
|
||||
$plugin->component = 'filter_mediacms';
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
$plugin->release = 'v1.0.0';
|
||||
@@ -1 +0,0 @@
|
||||
df913eb7ba0420001b21a522b0841d99209e636073e950f88c93fed4d7e05008 mediacms-moodle-v1.0.0.zip
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
eb3e54e5ea813b3287c438cced6fb6ad57cc8ae68547b1a8b598ffc61d02d826 tiny_mediacms-v1.0.0.zip
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# MediaCMS URL Auto-Convert Feature
|
||||
|
||||
This feature automatically converts pasted MediaCMS video URLs into embedded video players within the TinyMCE editor.
|
||||
|
||||
## Overview
|
||||
|
||||
When a user pastes a MediaCMS video URL like:
|
||||
```
|
||||
https://deic.mediacms.io/view?m=JpBd1Zvdl
|
||||
```
|
||||
|
||||
It is automatically converted to an embedded video player:
|
||||
```html
|
||||
<div class="tiny-iframe-responsive" contenteditable="false">
|
||||
<iframe
|
||||
style="width: 100%; max-width: calc(100vh * 16 / 9); aspect-ratio: 16 / 9; display: block; margin: auto; border: 0;"
|
||||
src="https://deic.mediacms.io/embed?m=JpBd1Zvdl&showTitle=1&showRelated=1&showUserAvatar=1&linkTitle=1"
|
||||
allowfullscreen="allowfullscreen">
|
||||
</iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Supported URL Formats
|
||||
|
||||
The auto-convert feature recognizes MediaCMS view URLs in this format:
|
||||
- `https://[domain]/view?m=[VIDEO_ID]`
|
||||
|
||||
Examples:
|
||||
- `https://deic.mediacms.io/view?m=JpBd1Zvdl`
|
||||
- `https://your-mediacms-instance.com/view?m=abc123`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Accessing Settings
|
||||
|
||||
1. Log in to Moodle as an administrator
|
||||
2. Navigate to: **Site administration** → **Plugins** → **Text editors** → **TinyMCE editor** → **MediaCMS**
|
||||
3. Scroll to the **Auto-convert MediaCMS URLs** section
|
||||
|
||||
### Available Settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| **Enable auto-convert** | Turn the auto-convert feature on or off | Enabled |
|
||||
| **MediaCMS base URL** | Restrict auto-conversion to a specific MediaCMS domain | Empty (allow all) |
|
||||
| **Show video title** | Display the video title in the embedded player | Enabled |
|
||||
| **Link video title** | Make the video title clickable, linking to the original video page | Enabled |
|
||||
| **Show related videos** | Display related videos after the current video ends | Enabled |
|
||||
| **Show user avatar** | Display the uploader's avatar in the embedded player | Enabled |
|
||||
|
||||
### Settings Location in Moodle
|
||||
|
||||
The settings are stored in the Moodle database under the `tiny_mediacms` plugin configuration:
|
||||
|
||||
- `tiny_mediacms/autoconvertenabled` - Enable/disable auto-convert
|
||||
- `tiny_mediacms/autoconvert_baseurl` - MediaCMS base URL (e.g., https://deic.mediacms.io)
|
||||
- `tiny_mediacms/autoconvert_showtitle` - Show title option
|
||||
- `tiny_mediacms/autoconvert_linktitle` - Link title option
|
||||
- `tiny_mediacms/autoconvert_showrelated` - Show related option
|
||||
- `tiny_mediacms/autoconvert_showuseravatar` - Show user avatar option
|
||||
|
||||
### Base URL Configuration
|
||||
|
||||
The **MediaCMS base URL** setting controls which MediaCMS instances are recognized for auto-conversion:
|
||||
|
||||
- **Empty (default)**: Any MediaCMS URL will be auto-converted (e.g., URLs from any `*/view?m=*` pattern)
|
||||
- **Specific URL**: Only URLs from the specified domain will be auto-converted
|
||||
|
||||
Example configurations:
|
||||
- `https://deic.mediacms.io` - Only convert URLs from deic.mediacms.io
|
||||
- `https://media.myuniversity.edu` - Only convert URLs from your institution's MediaCMS
|
||||
|
||||
## Technical Details
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
amd/src/
|
||||
├── autoconvert.js # Main auto-convert module
|
||||
├── plugin.js # Plugin initialization (imports autoconvert)
|
||||
└── options.js # Configuration options definition
|
||||
|
||||
classes/
|
||||
└── plugininfo.php # Passes PHP settings to JavaScript
|
||||
|
||||
settings.php # Admin settings page definition
|
||||
|
||||
lang/en/
|
||||
└── tiny_mediacms.php # Language strings for settings
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Paste Detection**: The `autoconvert.js` module listens for `paste` events on the TinyMCE editor
|
||||
2. **URL Validation**: When text is pasted, it checks if it matches the MediaCMS URL pattern
|
||||
3. **HTML Generation**: If valid, it generates the responsive iframe HTML with configured options
|
||||
4. **Content Insertion**: The original URL is replaced with the embedded video
|
||||
|
||||
### JavaScript Configuration
|
||||
|
||||
The settings are passed from PHP to JavaScript via the `plugininfo.php` class:
|
||||
|
||||
```php
|
||||
protected static function get_autoconvert_configuration(): array {
|
||||
$baseurl = get_config('tiny_mediacms', 'autoconvert_baseurl');
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
'autoConvertEnabled' => (bool) get_config('tiny_mediacms', 'autoconvertenabled'),
|
||||
'autoConvertBaseUrl' => !empty($baseurl) ? $baseurl : '',
|
||||
'autoConvertOptions' => [
|
||||
'showTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_showtitle'),
|
||||
'linkTitle' => (bool) get_config('tiny_mediacms', 'autoconvert_linktitle'),
|
||||
'showRelated' => (bool) get_config('tiny_mediacms', 'autoconvert_showrelated'),
|
||||
'showUserAvatar' => (bool) get_config('tiny_mediacms', 'autoconvert_showuseravatar'),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Default Values (in options.js)
|
||||
|
||||
If PHP settings are not configured, the JavaScript uses these defaults:
|
||||
|
||||
```javascript
|
||||
registerOption(dataName, {
|
||||
processor: 'object',
|
||||
"default": {
|
||||
autoConvertEnabled: true,
|
||||
autoConvertBaseUrl: '', // Empty = allow all MediaCMS domains
|
||||
autoConvertOptions: {
|
||||
showTitle: true,
|
||||
linkTitle: true,
|
||||
showRelated: true,
|
||||
showUserAvatar: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Disabling Auto-Convert
|
||||
|
||||
To disable the feature entirely:
|
||||
1. Go to the plugin settings (see "Accessing Settings" above)
|
||||
2. Uncheck **Enable auto-convert**
|
||||
3. Save changes
|
||||
|
||||
### Programmatic Configuration
|
||||
|
||||
You can also set these values directly in the database using Moodle's `set_config()` function:
|
||||
|
||||
```php
|
||||
// Disable auto-convert
|
||||
set_config('autoconvertenabled', 0, 'tiny_mediacms');
|
||||
|
||||
// Set the MediaCMS base URL (restrict to specific domain)
|
||||
set_config('autoconvert_baseurl', 'https://deic.mediacms.io', 'tiny_mediacms');
|
||||
|
||||
// Customize embed options
|
||||
set_config('autoconvert_showtitle', 1, 'tiny_mediacms');
|
||||
set_config('autoconvert_linktitle', 0, 'tiny_mediacms');
|
||||
set_config('autoconvert_showrelated', 0, 'tiny_mediacms');
|
||||
set_config('autoconvert_showuseravatar', 1, 'tiny_mediacms');
|
||||
```
|
||||
|
||||
### CLI Configuration
|
||||
|
||||
Using Moodle CLI:
|
||||
|
||||
```bash
|
||||
# Enable auto-convert
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvertenabled --set=1
|
||||
|
||||
# Set the MediaCMS base URL
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_baseurl --set=https://deic.mediacms.io
|
||||
|
||||
# Disable showing related videos
|
||||
php admin/cli/cfg.php --component=tiny_mediacms --name=autoconvert_showrelated --set=0
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Auto-convert not working
|
||||
|
||||
1. **Check if enabled**: Verify the setting is enabled in plugin settings
|
||||
2. **Clear caches**: Purge all caches (Site administration → Development → Purge all caches)
|
||||
3. **Check URL format**: Ensure the URL matches the pattern `https://[domain]/view?m=[VIDEO_ID]`
|
||||
4. **Browser console**: Check for JavaScript errors in the browser developer console
|
||||
|
||||
### Rebuilding JavaScript
|
||||
|
||||
If you modify the source files, rebuild using:
|
||||
|
||||
```bash
|
||||
cd /path/to/moodle
|
||||
npx grunt amd --root=public/lib/editor/tiny/plugins/mediacms
|
||||
```
|
||||
|
||||
Note: Requires Node.js 22.x or compatible version as specified in Moodle's requirements.
|
||||
|
||||
## Version History
|
||||
|
||||
- **1.0.0** - Initial implementation of auto-convert feature
|
||||
@@ -0,0 +1,77 @@
|
||||
================================================================================
|
||||
MediaCMS Moodle Plugin Suite v1.0.0
|
||||
Installation Guide
|
||||
================================================================================
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Moodle 4.5 or later
|
||||
- MediaCMS instance
|
||||
- MediaCMS set as External Tool
|
||||
|
||||
|
||||
The suite consists of two plugins that are distributed as separate ZIP files:
|
||||
|
||||
- filter_mediacms-v1.0.0.zip (Text filter - install FIRST)
|
||||
- tiny_mediacms-v1.0.0.zip (TinyMCE editor button - requires the filter)
|
||||
|
||||
tiny_mediacms declares a dependency on filter_mediacms, so Moodle will not
|
||||
let you install the editor plugin without the filter being present.
|
||||
|
||||
|
||||
Installation - Option A: Upload through Moodle (recommended)
|
||||
-------------------------------------------------------------
|
||||
|
||||
1. Log in as Administrator
|
||||
2. Go to: Site Administration → Plugins → Install plugins
|
||||
3. Upload filter_mediacms-v1.0.0.zip and complete the installation
|
||||
4. Upload tiny_mediacms-v1.0.0.zip and complete the installation
|
||||
5. Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
|
||||
Installation - Option B: Extract manually
|
||||
-----------------------------------------
|
||||
|
||||
1. Extract the zip files to the Moodle root public directory:
|
||||
|
||||
cd /var/www/moodle/public
|
||||
unzip filter_mediacms-v1.0.0.zip -d filter/
|
||||
unzip tiny_mediacms-v1.0.0.zip -d lib/editor/tiny/plugins/
|
||||
|
||||
This will place files in:
|
||||
- filter/mediacms/
|
||||
- lib/editor/tiny/plugins/mediacms/
|
||||
|
||||
2. Set permissions
|
||||
chown -R www-data:www-data filter/mediacms
|
||||
chown -R www-data:www-data lib/editor/tiny/plugins/mediacms
|
||||
|
||||
3. Install through Moodle
|
||||
- Log in as Administrator
|
||||
- Go to: Site Administration → Notifications
|
||||
- Click "Upgrade Moodle database now"
|
||||
- Both plugins will be installed automatically
|
||||
- Set the MediaCMS tool under the LTI Tool
|
||||
|
||||
After installation (both options)
|
||||
---------------------------------
|
||||
|
||||
Make sure the Filter is enabled
|
||||
- As Administrator, visit Plugins, 'Manage Filters', find MediaCMS filter and enable it.
|
||||
Then place it at the top of the filter. This is important, otherwise embeds won't load.
|
||||
|
||||
|
||||
What to expect
|
||||
-------
|
||||
|
||||
1. Create a test course
|
||||
2. Add a page or label
|
||||
3. Click MediaCMS button in TinyMCE editor
|
||||
4. Try inserting from video library or pasting a URL
|
||||
|
||||
SUPPORT
|
||||
-------
|
||||
Issues: https://github.com/mediacms-io/mediacms/issues
|
||||
Docs: https://docs.mediacms.io
|
||||
|
||||
================================================================================
|
||||
@@ -0,0 +1,20 @@
|
||||
# TinyMCE MediaCMS Plugin for Moodle
|
||||
|
||||
A TinyMCE editor plugin for Moodle that provides media embedding capabilities with MediaCMS/LTI integration.
|
||||
|
||||
## Build Information
|
||||
|
||||
### Preparation
|
||||
1. Get and extract Moodle 5.1
|
||||
2. cp -r lms-plugins/mediacms-moodle/tiny/mediacms/ moodle/public/lib/editor/tiny/plugins/
|
||||
3. nvm use 22 && cd moodle/public && npm install
|
||||
|
||||
### Actual build
|
||||
4. cd lib/editor/tiny/plugins/mediacms && npx grunt amd
|
||||
|
||||
### Test the output
|
||||
5. To test the output:
|
||||
cp * ../../../../../../../lms-plugins/mediacms-moodle/tiny/mediacms/ -r
|
||||
|
||||
6. Then copy to Moodle server and purge caches
|
||||
|
||||
Vendored
Executable
+15
@@ -0,0 +1,15 @@
|
||||
define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0;
|
||||
/**
|
||||
* Tiny MediaCMS Auto-convert module.
|
||||
*
|
||||
* This module automatically converts pasted MediaCMS URLs into embedded videos.
|
||||
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
|
||||
* it will be automatically converted to an iframe embed.
|
||||
*
|
||||
* @module tiny_mediacms/autoconvert
|
||||
* @copyright 2024
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='<iframe src="'.concat(embedUrl.toString(),'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>';return html};_exports.setupAutoConvert=editor=>{const config=(0,_options.getData)(editor)||{};!1!==config.autoConvertEnabled&&(editor.on("paste",(e=>{handlePasteEvent(editor,e,config)})),editor.on("input",(e=>{handleInputEvent(editor,e,config)})))};const handlePasteEvent=(editor,e,config)=>{const clipboardData=e.clipboardData||window.clipboardData;if(!clipboardData)return;const text=clipboardData.getData("text/plain")||clipboardData.getData("text");if(!text)return;const parsed=parseMediaCMSUrl(text);if(!parsed)return;if(!isDomainAllowed(parsed,config))return;e.preventDefault(),e.stopPropagation();const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{editor.insertContent(embedHtml),editor.selection.collapse(!1)}),0)},handleInputEvent=(editor,e,config)=>{if("insertFromPaste"!==e.inputType&&"insertText"!==e.inputType)return;const node=editor.selection.getNode();if(!node||"P"!==node.nodeName)return;const text=node.textContent||"",parsed=parseMediaCMSUrl(text);if(!parsed||!isDomainAllowed(parsed,config))return;const trimmedHtml=node.innerHTML.trim();if(trimmedHtml!==text.trim()&&!trimmedHtml.startsWith(text.trim()))return;const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{const currentText=node.textContent||"",currentParsed=parseMediaCMSUrl(currentText);currentParsed&¤tParsed.originalUrl===parsed.originalUrl&&(editor.selection.select(node),editor.insertContent(embedHtml))}),100)};_exports.isMediaCMSUrl=text=>null!==parseMediaCMSUrl(text);_exports.convertToEmbed=function(url){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const parsed=parseMediaCMSUrl(url);return parsed?generateEmbedHtml(parsed,options):null}}));
|
||||
|
||||
//# sourceMappingURL=autoconvert.min.js.map
|
||||
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+10
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_mediacms/plugin",component:"tiny_mediacms",iframeButtonName:"tiny_mediacms_iframe",iframeMenuItemName:"tiny_mediacms_iframe",iframeIcon:"tiny_mediacms_iframe"},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=common.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_mediacms/plugin',\n component: 'tiny_mediacms',\n iframeButtonName: 'tiny_mediacms_iframe',\n iframeMenuItemName: 'tiny_mediacms_iframe',\n iframeIcon: 'tiny_mediacms_iframe',\n};\n"],"names":["pluginName","component","iframeButtonName","iframeMenuItemName","iframeIcon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,iBAAkB,uBAClBC,mBAAoB,uBACpBC,WAAY"}
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.iframeButtonName),menu:(menu=instanceConfig.menu,menu.insert.items="".concat(_common.iframeMenuItemName," ").concat(menu.insert.items),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&§ion.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
|
||||
|
||||
//# sourceMappingURL=configuration.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n iframeButtonName,\n iframeMenuItemName,\n} from './common';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Add the Iframe Embed to the insert menu.\n menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;\n\n return menu;\n};\n\nconst configureToolbar = (toolbar) => {\n // The toolbar contains an array of named sections.\n // The Moodle integration ensures that there is a section called 'content'.\n\n return toolbar.map((section) => {\n if (section.name === 'content') {\n // Insert the iframe button at the start of it.\n section.items.unshift(iframeButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","contextmenu","iframeButtonName","menu","insert","items","iframeMenuItemName","toolbar","map","section","name","unshift"],"mappings":"wNAoD0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,0BAC5DC,MAzBeA,KAyBKH,eAAeG,KAvBvCA,KAAKC,OAAOC,gBAAWC,uCAAsBH,KAAKC,OAAOC,OAElDF,MAsBHI,SAnBkBA,QAmBQP,eAAeO,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQJ,MAAMM,QAAQT,0BAGnBO,aAVWF,IAAAA,QAPHJ"}
|
||||
Vendored
Executable
+3
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/embedmodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EmbedModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=EmbedModal,_defineProperty(EmbedModal,"TYPE","".concat(_common.component,"/modal")),_defineProperty(EmbedModal,"TEMPLATE","".concat(_common.component,"/embed_media_modal")),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=embedmodal.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"embedmodal.min.js","sources":["../src/embedmodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class EmbedModal extends Modal {\n static TYPE = `${component}/modal`;\n static TEMPLATE = `${component}/embed_media_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,6CADAV,gCAEIU"}
|
||||
Vendored
Executable
+3
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/iframemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class IframeModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=IframeModal,_defineProperty(IframeModal,"TYPE","".concat(_common.component,"/iframemodal")),_defineProperty(IframeModal,"TEMPLATE","".concat(_common.component,"/iframe_embed_modal")),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=iframemodal.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"iframemodal.min.js","sources":["../src/iframemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Iframe Embed Modal for Tiny Media2.\n *\n * @module tiny_mediacms/iframemodal\n * @copyright 2024\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class IframeModal extends Modal {\n static TYPE = `${component}/iframemodal`;\n static TEMPLATE = `${component}/iframe_embed_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["IframeModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"kaA0BqBA,oBAAoBC,eAIrCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,kEAlBHN,6BACAU,mDADAV,iCAEIU"}
|
||||
Vendored
Executable
+3
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+10
@@ -0,0 +1,10 @@
|
||||
define("tiny_mediacms/imagehelpers",["exports","core/templates"],(function(_exports,_templates){var obj;
|
||||
/**
|
||||
* Tiny media plugin image helpers.
|
||||
*
|
||||
* @module tiny_mediacms/imagehelpers
|
||||
* @copyright 2024 Meirza <meirza.arson@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showElements=_exports.isPercentageValue=_exports.hideElements=_exports.footerImageInsert=_exports.footerImageDetails=_exports.bodyImageInsert=_exports.bodyImageDetails=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.bodyImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert",{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert_footer",{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.bodyImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details",{...templateContext}).then((_ref3=>{let{html:html,js:js}=_ref3;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details_footer",{...templateContext}).then((_ref4=>{let{html:html,js:js}=_ref4;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.showElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.remove("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.remove("d-none")}};_exports.hideElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.add("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.add("d-none")}};_exports.isPercentageValue=value=>value.match(/\d+%/)}));
|
||||
|
||||
//# sourceMappingURL=imagehelpers.min.js.map
|
||||
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
File diff suppressed because one or more lines are too long
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/imagemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class ImageModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=ImageModal,_defineProperty(ImageModal,"TYPE","".concat(_common.component,"/imagemodal")),_defineProperty(ImageModal,"TEMPLATE","".concat(_common.component,"/insert_image_modal")),ImageModal.registerModalType(),_exports.default}));
|
||||
|
||||
//# sourceMappingURL=imagemodal.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"imagemodal.min.js","sources":["../src/imagemodal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Image Modal for Tiny.\n *\n * @module tiny_mediacms/imagemodal\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class ImageModal extends Modal {\n static TYPE = `${component}/imagemodal`;\n static TEMPLATE = `${component}/insert_image_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n\nImageModal.registerModalType();\n"],"names":["ImageModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component","registerModalType"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,kDADAV,gCAEIU,0CAoBzBV,WAAWW"}
|
||||
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/manager",["exports","core/templates","core/str","core/modal","core/modal_events","./options","core/config"],(function(_exports,_templates,_str,_modal,ModalEvents,_options,_config){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),ModalEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(ModalEvents),_config=_interopRequireDefault(_config);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"area",null),this.editor=editor;const data=(0,_options.getData)(editor);this.area=data.params.area,this.area.itemid=data.fpoptions.image.itemid}async displayDialogue(){const modal=await _modal.default.create({large:!0,title:(0,_str.getString)("mediamanagerproperties","tiny_mediacms"),body:_templates.default.render("tiny_mediacms/mm2_iframe",{src:this.getIframeURL()}),removeOnClose:!0,show:!0});return modal.getRoot().on(ModalEvents.bodyRendered,(()=>{this.selectFirstElement()})),document.querySelector(".modal-lg").style.cssText="max-width: 850px",modal}selectFirstElement(){const iframe=document.getElementById("mm2-iframe");iframe.addEventListener("load",(function(){let intervalId=setInterval((function(){const iDocument=iframe.contentWindow.document;if(iDocument.querySelector(".filemanager")){const firstFocusableElement=iDocument.querySelector(".fp-navbar a:not([disabled])");firstFocusableElement&&firstFocusableElement.focus(),clearInterval(intervalId)}}),200)}))}getIframeURL(){const url=new URL("".concat(_config.default.wwwroot,"/lib/editor/tiny/plugins/mediacms/manage.php"));url.searchParams.append("elementid",this.editor.getElement().id);for(const key in this.area)url.searchParams.append(key,this.area[key]);return url.toString()}},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=manager.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"manager.min.js","sources":["../src/manager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager plugin class for Moodle.\n *\n * @module tiny_mediacms/manager\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport {getData} from './options';\nimport Config from 'core/config';\n\nexport default class MediaManager {\n\n editor = null;\n area = null;\n\n constructor(editor) {\n this.editor = editor;\n const data = getData(editor);\n this.area = data.params.area;\n this.area.itemid = data.fpoptions.image.itemid;\n }\n\n async displayDialogue() {\n const modal = await Modal.create({\n large: true,\n title: getString('mediamanagerproperties', 'tiny_mediacms'),\n body: Templates.render('tiny_mediacms/mm2_iframe', {\n src: this.getIframeURL()\n }),\n removeOnClose: true,\n show: true,\n });\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n this.selectFirstElement();\n });\n\n document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;\n return modal;\n }\n\n // It will select the first element in the file manager.\n selectFirstElement() {\n const iframe = document.getElementById('mm2-iframe');\n iframe.addEventListener('load', function() {\n let intervalId = setInterval(function() {\n const iDocument = iframe.contentWindow.document;\n if (iDocument.querySelector('.filemanager')) {\n const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');\n if (firstFocusableElement) {\n firstFocusableElement.focus();\n }\n clearInterval(intervalId);\n }\n }, 200);\n });\n }\n\n getIframeURL() {\n const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);\n url.searchParams.append('elementid', this.editor.getElement().id);\n for (const key in this.area) {\n url.searchParams.append(key, this.area[key]);\n }\n return url.toString();\n }\n}\n"],"names":["constructor","editor","data","area","params","itemid","fpoptions","image","modal","Modal","create","large","title","body","Templates","render","src","this","getIframeURL","removeOnClose","show","getRoot","on","ModalEvents","bodyRendered","selectFirstElement","document","querySelector","style","cssText","iframe","getElementById","addEventListener","intervalId","setInterval","iDocument","contentWindow","firstFocusableElement","focus","clearInterval","url","URL","Config","wwwroot","searchParams","append","getElement","id","key","toString"],"mappings":"mmDAmCIA,YAAYC,sCAHH,kCACF,WAGEA,OAASA,aACRC,MAAO,oBAAQD,aAChBE,KAAOD,KAAKE,OAAOD,UACnBA,KAAKE,OAASH,KAAKI,UAAUC,MAAMF,qCAIlCG,YAAcC,eAAMC,OAAO,CAC7BC,OAAO,EACPC,OAAO,kBAAU,yBAA0B,iBAC3CC,KAAMC,mBAAUC,OAAO,2BAA4B,CAC/CC,IAAKC,KAAKC,iBAEdC,eAAe,EACfC,MAAM,WAEVZ,MAAMa,UAAUC,GAAGC,YAAYC,cAAc,UACpCC,wBAGTC,SAASC,cAAc,aAAaC,MAAMC,2BACnCrB,MAIXiB,2BACUK,OAASJ,SAASK,eAAe,cACvCD,OAAOE,iBAAiB,QAAQ,eACxBC,WAAaC,aAAY,iBACnBC,UAAYL,OAAOM,cAAcV,YACnCS,UAAUR,cAAc,gBAAiB,OACnCU,sBAAwBF,UAAUR,cAAc,gCAClDU,uBACAA,sBAAsBC,QAE1BC,cAAcN,eAEnB,QAIXf,qBACUsB,IAAM,IAAIC,cAAOC,gBAAOC,yDAC9BH,IAAII,aAAaC,OAAO,YAAa5B,KAAKhB,OAAO6C,aAAaC,QACzD,MAAMC,OAAO/B,KAAKd,KACnBqC,IAAII,aAAaC,OAAOG,IAAK/B,KAAKd,KAAK6C,aAEpCR,IAAIS"}
|
||||
Vendored
Executable
+11
@@ -0,0 +1,11 @@
|
||||
define("tiny_mediacms/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=_exports.getLti=_exports.getImagePermissions=_exports.getEmbedPermissions=_exports.getData=void 0;
|
||||
/**
|
||||
* Options helper for Tiny Media plugin.
|
||||
*
|
||||
* @module tiny_mediacms/options
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions"),ltiName=(0,_options.getPluginOptionName)(_common.pluginName,"lti");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object",default:{mediacmsApiUrl:"",mediacmsBaseUrl:"",mediacmsPageSize:12,autoConvertEnabled:!0,autoConvertBaseUrl:"",autoConvertOptions:{showTitle:!0,linkTitle:!0,showUserAvatar:!0}}}),registerOption(ltiName,{processor:"object",default:{toolId:0,courseId:0,contentItemUrl:""}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.getImagePermissions=editor=>getPermissions(editor).image;_exports.getEmbedPermissions=editor=>getPermissions(editor).embed;_exports.getData=editor=>editor.options.get(dataName);_exports.getLti=editor=>editor.options.get(ltiName)}));
|
||||
|
||||
//# sourceMappingURL=options.min.js.map
|
||||
Vendored
Executable
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Media plugin.\n *\n * @module tiny_mediacms/options\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\nconst ltiName = getPluginOptionName(pluginName, 'lti');\n\n/**\n * Register the options for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n image: {\n filepicker: false,\n }\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n \"default\": {\n // MediaCMS video library configuration\n mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'\n mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'\n mediacmsPageSize: 12,\n // Auto-conversion settings\n autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs\n autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)\n autoConvertOptions: {\n // Default embed options for auto-converted videos\n showTitle: true,\n linkTitle: true,\n showUserAvatar: true,\n },\n },\n });\n\n registerOption(ltiName, {\n processor: 'object',\n \"default\": {\n // LTI configuration for MediaCMS iframe library\n toolId: 0, // LTI external tool ID\n courseId: 0, // Current course ID\n contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getImagePermissions = (editor) => getPermissions(editor).image;\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getEmbedPermissions = (editor) => getPermissions(editor).embed;\n\n/**\n * Get the data configuration for the Media Manager.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Get the LTI configuration for the MediaCMS iframe library.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLti = (editor) => editor.options.get(ltiName);\n"],"names":["dataName","pluginName","permissionsName","ltiName","editor","registerOption","options","register","processor","image","filepicker","mediacmsApiUrl","mediacmsBaseUrl","mediacmsPageSize","autoConvertEnabled","autoConvertBaseUrl","autoConvertOptions","showTitle","linkTitle","showUserAvatar","toolId","courseId","contentItemUrl","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,eAClDE,SAAU,gCAAoBF,mBAAY,yBAOvBG,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeH,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeL,SAAU,CACrBQ,UAAW,iBACA,CAEPG,eAAgB,GAChBC,gBAAiB,GACjBC,iBAAkB,GAElBC,oBAAoB,EACpBC,mBAAoB,GACpBC,mBAAoB,CAEhBC,WAAW,EACXC,WAAW,EACXC,gBAAgB,MAK5Bd,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPY,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBnB,QAAWA,OAAOE,QAAQkB,IAAItB,qFAQzBE,QAAWmB,eAAenB,QAAQK,mCAQlCL,QAAWmB,eAAenB,QAAQqB,uBAQ9CrB,QAAWA,OAAOE,QAAQkB,IAAIxB,0BAQ/BI,QAAWA,OAAOE,QAAQkB,IAAIrB"}
|
||||
Vendored
Executable
+10
@@ -0,0 +1,10 @@
|
||||
define("tiny_mediacms/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options","./autoconvert"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options,_autoconvert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
|
||||
/**
|
||||
* Tiny Media plugin for Moodle.
|
||||
*
|
||||
* @module tiny_mediacms/plugin
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);const isMediaCMSUrl=url=>{if(!url)return!1;try{const urlObj=new URL(url);return("/embed"===urlObj.pathname||"/view"===urlObj.pathname)&&urlObj.searchParams.has("m")}catch(e){return!1}},convertUrlsToIframes=html=>{const tempDiv=document.createElement("div");tempDiv.innerHTML=html;const nodesToReplace=[],walk=el=>{for(const child of Array.from(el.childNodes))if(child.nodeType===Node.TEXT_NODE){const url=child.textContent.trim();isMediaCMSUrl(url)&&nodesToReplace.push({node:child,url:url})}else child.nodeType===Node.ELEMENT_NODE&&"a"!==child.tagName.toLowerCase()&&walk(child)};return walk(tempDiv),nodesToReplace.forEach((_ref=>{let{node:node,url:url}=_ref;const wrapper=document.createElement("div");wrapper.innerHTML=(url=>{let embedUrl=url,width=560,height=315;try{const urlObj=new URL(url);"/view"===urlObj.pathname&&(urlObj.pathname="/embed");const w=parseInt(urlObj.searchParams.get("width")),h=parseInt(urlObj.searchParams.get("height"));w>0&&(width=w),h>0&&(height=h),embedUrl=urlObj.toString()}catch(e){}const style="width:100%;max-width:".concat(width,"px;height:auto;")+"aspect-ratio:".concat(width," / ").concat(height,";display:block;margin:0 auto;border:0;");return'<iframe src="'.concat(embedUrl,'" width="').concat(width,'" height="').concat(height,'" ')+'style="'.concat(style,'" frameborder="0" allowfullscreen></iframe>')})(url);const iframe=wrapper.firstChild;iframe&&node.parentNode.replaceChild(iframe,node)})),tempDiv.innerHTML};var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(Options.register(editor),setupCommands(editor),(0,_autoconvert.setupAutoConvert)(editor),editor.on("BeforeSetContent",(e=>{e.content&&"string"==typeof e.content&&(e.content=convertUrlsToIframes(e.content))})),editor.on("GetContent",(e=>{if("html"===e.format){const tempDiv=document.createElement("div");tempDiv.innerHTML=e.content,tempDiv.querySelectorAll(".tiny-mediacms-edit-btn").forEach((btn=>btn.remove())),tempDiv.querySelectorAll("iframe").forEach((iframe=>{const src=iframe.getAttribute("src");if(isMediaCMSUrl(src)){const wrapper=iframe.closest(".tiny-mediacms-iframe-wrapper")||iframe.closest(".tiny-iframe-responsive"),p=document.createElement("p");p.appendChild(document.createTextNode(src)),wrapper?(wrapper.parentNode.insertBefore(p,wrapper),wrapper.remove()):(iframe.parentNode.insertBefore(p,iframe),iframe.remove())}})),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper, .tiny-iframe-responsive").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),e.content=tempDiv.innerHTML}})),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=plugin.min.js.map
|
||||
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+3
@@ -0,0 +1,3 @@
|
||||
define("tiny_mediacms/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_imagecms_urlentrysubmit",imageBrowser:".openimagecmsbrowser",addUrl:".tiny_imagecms_addurl",deleteImage:".tiny_imagecms_deleteicon"},elements:{form:"form.tiny_imagecms_form",alignSettings:".tiny_imagecms_button",alt:".tiny_imagecms_altentry",altWarning:".tiny_imagecms_altwarning",height:".tiny_imagecms_heightentry",width:".tiny_imagecms_widthentry",url:".tiny_imagecms_urlentry",urlWarning:".tiny_imagecms_urlwarning",size:".tiny_imagecms_size",presentation:".tiny_imagecms_presentation",constrain:".tiny_imagecms_constrain",customStyle:".tiny_imagecms_customstyle",preview:".tiny_imagecms_preview",previewBox:".tiny_imagecms_preview_box",loaderIcon:".tiny_imagecms_loader",loaderIconContainer:".tiny_imagecms_loader_container",insertImage:".tiny_imagecms_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_imagecms_dropzone_container",fileInput:"#tiny_imagecms_fileinput",fileNameLabel:".tiny_imagecms_filename",sizeOriginal:".tiny_imagecms_sizeoriginal",sizeCustom:".tiny_imagecms_sizecustom",properties:".tiny_imagecms_properties"},styles:{responsive:"img-fluid"}},EMBED:{actions:{submit:".tiny_mediacms_submit",mediaBrowser:".openmediacmsbrowser"},elements:{form:"form.tiny_mediacms_form",source:".tiny_mediacms_source",track:".tiny_mediacms_track",mediaSource:".tiny_mediacms_media_source",linkSource:".tiny_mediacms_link_source",linkSize:".tiny_mediacms_link_size",posterSource:".tiny_mediacms_poster_source",posterSize:".tiny_mediacms_poster_size",displayOptions:".tiny_mediacms_display_options",name:".tiny_mediacms_name_entry",title:".tiny_mediacms_title_entry",url:".tiny_mediacms_url_entry",width:".tiny_mediacms_width_entry",height:".tiny_mediacms_height_entry",trackSource:".tiny_mediacms_track_source",trackKind:".tiny_mediacms_track_kind_entry",trackLabel:".tiny_mediacms_track_label_entry",trackLang:".tiny_mediacms_track_lang_entry",trackDefault:".tiny_mediacms_track_default",mediaControl:".tiny_mediacms_controls",mediaAutoplay:".tiny_mediacms_autoplay",mediaMute:".tiny_mediacms_mute",mediaLoop:".tiny_mediacms_loop",advancedSettings:".tiny_mediacms_advancedsettings",linkTab:'li[data-medium-type="link"]',videoTab:'li[data-medium-type="video"]',audioTab:'li[data-medium-type="audio"]',linkPane:'.tab-pane[data-medium-type="link"]',videoPane:'.tab-pane[data-medium-type="video"]',audioPane:'.tab-pane[data-medium-type="audio"]',trackSubtitlesTab:'li[data-track-kind="subtitles"]',trackCaptionsTab:'li[data-track-kind="captions"]',trackDescriptionsTab:'li[data-track-kind="descriptions"]',trackChaptersTab:'li[data-track-kind="chapters"]',trackMetadataTab:'li[data-track-kind="metadata"]',trackSubtitlesPane:'.tab-pane[data-track-kind="subtitles"]',trackCaptionsPane:'.tab-pane[data-track-kind="captions"]',trackDescriptionsPane:'.tab-pane[data-track-kind="descriptions"]',trackChaptersPane:'.tab-pane[data-track-kind="chapters"]',trackMetadataPane:'.tab-pane[data-track-kind="metadata"]'},mediaTypes:{link:"LINK",video:"VIDEO",audio:"AUDIO"},trackKinds:{subtitles:"SUBTITLES",captions:"CAPTIONS",descriptions:"DESCRIPTIONS",chapters:"CHAPTERS",metadata:"METADATA"}},IFRAME:{actions:{remove:'[data-action="remove"]'},elements:{form:"form.tiny_iframecms_form",url:".tiny_iframecms_url",urlWarning:".tiny_iframecms_url_warning",showTitle:".tiny_iframecms_showtitle",linkTitle:".tiny_iframecms_linktitle",showUserAvatar:".tiny_iframecms_showuseravatar",textLinkOnly:".tiny_iframecms_textlinkonly",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",width:".tiny_iframecms_width",height:".tiny_iframecms_height",preview:".tiny_iframecms_preview",previewContainer:".tiny_iframecms_preview_container",tabs:".tiny_iframecms_tabs",tabUrlBtn:".tiny_iframecms_tab_url_btn",tabIframeLibraryBtn:".tiny_iframecms_tab_iframe_library_btn",tabUploadMediaBtn:".tiny_iframecms_upload_media_btn",paneUrl:".tiny_iframecms_pane_url",paneIframeLibrary:".tiny_iframecms_pane_iframe_library",iframeLibraryContainer:".tiny_iframecms_iframe_library_container",iframeLibraryPlaceholder:".tiny_iframecms_iframe_library_placeholder",iframeLibraryLoading:".tiny_iframecms_iframe_library_loading",iframeLibraryFrame:".tiny_iframecms_iframe_library_frame"}}},_exports.default}));
|
||||
|
||||
//# sourceMappingURL=selectors.min.js.map
|
||||
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+10
@@ -0,0 +1,10 @@
|
||||
define("tiny_mediacms/usedfiles",["exports","core/templates","core/config"],(function(_exports,Templates,_config){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Templates=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
|
||||
/**
|
||||
* Tiny Media Manager usedfiles.
|
||||
*
|
||||
* @module tiny_mediacms/usedfiles
|
||||
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(files,userContext,itemId,elementId){this.files=files,this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"']"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?<filename>.+?)[\\?\"']","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_mediacms/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}}));
|
||||
|
||||
//# sourceMappingURL=usedfiles.min.js.map
|
||||
Vendored
Executable
+1
File diff suppressed because one or more lines are too long
Vendored
Executable
+263
@@ -0,0 +1,263 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny MediaCMS Auto-convert module.
|
||||
*
|
||||
* This module automatically converts pasted MediaCMS URLs into embedded videos.
|
||||
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
|
||||
* it will be automatically converted to an iframe embed.
|
||||
*
|
||||
* @module tiny_mediacms/autoconvert
|
||||
* @copyright 2024
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {getData} from './options';
|
||||
|
||||
/**
|
||||
* Regular expression patterns for MediaCMS URLs.
|
||||
* Matches URLs like:
|
||||
* - https://deic.mediacms.io/view?m=JpBd1Zvdl
|
||||
* - https://example.mediacms.io/view?m=VIDEO_ID
|
||||
* - Custom domains configured in the plugin
|
||||
*/
|
||||
const MEDIACMS_VIEW_URL_PATTERN = /^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/;
|
||||
|
||||
/**
|
||||
* Check if a string is a valid MediaCMS view URL.
|
||||
*
|
||||
* @param {string} text - The text to check
|
||||
* @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL
|
||||
*/
|
||||
const parseMediaCMSUrl = (text) => {
|
||||
if (!text || typeof text !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = text.trim();
|
||||
|
||||
// Check for MediaCMS view URL pattern
|
||||
const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN);
|
||||
if (match) {
|
||||
return {
|
||||
baseUrl: match[1],
|
||||
videoId: match[2],
|
||||
originalUrl: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the pasted URL's domain is allowed based on configuration.
|
||||
*
|
||||
* @param {Object} parsed - Parsed URL info
|
||||
* @param {Object} config - Plugin configuration
|
||||
* @returns {boolean} - True if the domain is allowed
|
||||
*/
|
||||
const isDomainAllowed = (parsed, config) => {
|
||||
// If no specific base URL is configured, allow all MediaCMS domains
|
||||
const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl;
|
||||
if (!configuredBaseUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the URL's base matches the configured base URL
|
||||
try {
|
||||
const configuredUrl = new URL(configuredBaseUrl);
|
||||
const pastedUrl = new URL(parsed.baseUrl);
|
||||
return configuredUrl.host === pastedUrl.host;
|
||||
} catch (e) {
|
||||
// If URL parsing fails, allow the conversion
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the iframe embed HTML for a MediaCMS video.
|
||||
*
|
||||
* @param {Object} parsed - Parsed URL info
|
||||
* @param {Object} options - Embed options
|
||||
* @returns {string} - The iframe HTML
|
||||
*/
|
||||
const generateEmbedHtml = (parsed, options = {}) => {
|
||||
// Build the embed URL with default options
|
||||
const embedUrl = new URL(`${parsed.baseUrl}/embed`);
|
||||
embedUrl.searchParams.set('m', parsed.videoId);
|
||||
|
||||
// Apply default options (all enabled by default for best user experience)
|
||||
embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0');
|
||||
embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0');
|
||||
embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0');
|
||||
|
||||
// Generate responsive iframe HTML matching the template output format.
|
||||
// Uses aspect-ratio CSS for responsive sizing (16:9 default).
|
||||
// The wrapper will be added by editor for UI (edit button), then stripped on save.
|
||||
const html = `<iframe src="${embedUrl.toString()}" ` +
|
||||
`style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" ` +
|
||||
`allowfullscreen="allowfullscreen"></iframe>`;
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up auto-conversion for the editor.
|
||||
* This registers event handlers to detect pasted MediaCMS URLs.
|
||||
*
|
||||
* @param {TinyMCE} editor - The TinyMCE editor instance
|
||||
*/
|
||||
export const setupAutoConvert = (editor) => {
|
||||
const config = getData(editor) || {};
|
||||
|
||||
// Check if auto-convert is enabled (default: true)
|
||||
if (config.autoConvertEnabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle paste events
|
||||
editor.on('paste', (e) => {
|
||||
handlePasteEvent(editor, e, config);
|
||||
});
|
||||
|
||||
// Also handle input events for drag-and-drop text or keyboard paste
|
||||
editor.on('input', (e) => {
|
||||
handleInputEvent(editor, e, config);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle paste events to detect and convert MediaCMS URLs.
|
||||
*
|
||||
* @param {TinyMCE} editor - The TinyMCE editor instance
|
||||
* @param {Event} e - The paste event
|
||||
* @param {Object} config - Plugin configuration
|
||||
*/
|
||||
const handlePasteEvent = (editor, e, config) => {
|
||||
// Get pasted text from clipboard
|
||||
const clipboardData = e.clipboardData || window.clipboardData;
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to get plain text first
|
||||
const text = clipboardData.getData('text/plain') || clipboardData.getData('text');
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a MediaCMS URL
|
||||
const parsed = parseMediaCMSUrl(text);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if domain is allowed
|
||||
if (!isDomainAllowed(parsed, config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default paste behavior
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Generate and insert the embed HTML
|
||||
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
|
||||
|
||||
// Use a slight delay to ensure the editor is ready
|
||||
setTimeout(() => {
|
||||
editor.insertContent(embedHtml);
|
||||
// Move cursor after the inserted content
|
||||
editor.selection.collapse(false);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle input events to catch URLs that might have been pasted without triggering paste event.
|
||||
* This is a fallback for certain browsers/scenarios.
|
||||
*
|
||||
* @param {TinyMCE} editor - The TinyMCE editor instance
|
||||
* @param {Event} e - The input event
|
||||
* @param {Object} config - Plugin configuration
|
||||
*/
|
||||
const handleInputEvent = (editor, e, config) => {
|
||||
// Only process inputType 'insertFromPaste' if paste event didn't catch it
|
||||
if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertText') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current node and check if it contains just a URL
|
||||
const node = editor.selection.getNode();
|
||||
if (!node || node.nodeName !== 'P') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the paragraph contains only a MediaCMS URL
|
||||
const text = node.textContent || '';
|
||||
const parsed = parseMediaCMSUrl(text);
|
||||
|
||||
if (!parsed || !isDomainAllowed(parsed, config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't convert if there's other content in the paragraph
|
||||
const trimmedHtml = node.innerHTML.trim();
|
||||
if (trimmedHtml !== text.trim() && !trimmedHtml.startsWith(text.trim())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the embed HTML
|
||||
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
|
||||
|
||||
// Replace the paragraph content with the embed
|
||||
// Use a slight delay to let the input event complete
|
||||
setTimeout(() => {
|
||||
// Re-check that the node still contains the URL (user might have typed more)
|
||||
const currentText = node.textContent || '';
|
||||
const currentParsed = parseMediaCMSUrl(currentText);
|
||||
|
||||
if (currentParsed && currentParsed.originalUrl === parsed.originalUrl) {
|
||||
// Select and replace the entire node
|
||||
editor.selection.select(node);
|
||||
editor.insertContent(embedHtml);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a text is a MediaCMS URL (public helper).
|
||||
*
|
||||
* @param {string} text - The text to check
|
||||
* @returns {boolean} - True if it's a MediaCMS URL
|
||||
*/
|
||||
export const isMediaCMSUrl = (text) => {
|
||||
return parseMediaCMSUrl(text) !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a MediaCMS URL to embed HTML (public helper).
|
||||
*
|
||||
* @param {string} url - The MediaCMS URL
|
||||
* @param {Object} options - Embed options
|
||||
* @returns {string|null} - The embed HTML or null if not a valid URL
|
||||
*/
|
||||
export const convertToEmbed = (url, options = {}) => {
|
||||
const parsed = parseMediaCMSUrl(url);
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return generateEmbedHtml(parsed, options);
|
||||
};
|
||||
Vendored
Executable
+308
@@ -0,0 +1,308 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny Media commands.
|
||||
*
|
||||
* @module tiny_mediacms/commands
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {getStrings} from 'core/str';
|
||||
import {
|
||||
component,
|
||||
iframeButtonName,
|
||||
iframeMenuItemName,
|
||||
iframeIcon,
|
||||
} from './common';
|
||||
import IframeEmbed from './iframeembed';
|
||||
import {getButtonImage} from 'editor_tiny/utils';
|
||||
|
||||
const isIframe = (node) => node.nodeName.toLowerCase() === 'iframe' ||
|
||||
(node.classList && node.classList.contains('tiny-iframe-responsive')) ||
|
||||
(node.classList && node.classList.contains('tiny-mediacms-iframe-wrapper')) ||
|
||||
(node.nodeName.toLowerCase() === 'a' && node.getAttribute('data-mediacms-textlink') === 'true');
|
||||
|
||||
/**
|
||||
* Wrap iframes with overlay containers that allow hover detection.
|
||||
* Since iframes capture mouse events, we add an invisible overlay on top
|
||||
* that shows the edit button on hover.
|
||||
*
|
||||
* @param {TinyMCE} editor - The editor instance
|
||||
* @param {Function} handleIframeAction - The action to perform when clicking the button
|
||||
*/
|
||||
const setupIframeOverlays = (editor, handleIframeAction) => {
|
||||
/**
|
||||
* Process all iframes in the editor and add overlay wrappers.
|
||||
*/
|
||||
const fixWrapperWidths = () => {
|
||||
const editorBody = editor.getBody();
|
||||
if (!editorBody) {
|
||||
return;
|
||||
}
|
||||
editorBody.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach((wrapper) => {
|
||||
const iframe = wrapper.querySelector('iframe');
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
const iframeStyle = iframe.getAttribute('style') || '';
|
||||
const match = iframeStyle.match(/max-width:\s*(\d+(?:\.\d+)?)px/);
|
||||
if (match) {
|
||||
wrapper.style.maxWidth = match[1] + 'px';
|
||||
wrapper.style.width = '100%';
|
||||
wrapper.style.margin = '0 auto';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const processIframes = () => {
|
||||
const editorBody = editor.getBody();
|
||||
if (!editorBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframes = editorBody.querySelectorAll('iframe');
|
||||
iframes.forEach((iframe) => {
|
||||
// If already wrapped, ensure contenteditable and EDIT button are present
|
||||
if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) {
|
||||
const existingWrapper = iframe.parentElement;
|
||||
existingWrapper.setAttribute('contenteditable', 'false');
|
||||
if (!existingWrapper.querySelector('.tiny-mediacms-edit-btn')) {
|
||||
const editBtn = editor.getDoc().createElement('button');
|
||||
editBtn.className = 'tiny-mediacms-edit-btn';
|
||||
editBtn.setAttribute('type', 'button');
|
||||
editBtn.setAttribute('title', 'Edit media embed options');
|
||||
editBtn.textContent = 'EDIT';
|
||||
existingWrapper.appendChild(editBtn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip TinyMCE internal iframes
|
||||
if (iframe.hasAttribute('data-mce-object') || iframe.hasAttribute('data-mce-placeholder')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapper div
|
||||
const wrapper = editor.getDoc().createElement('div');
|
||||
wrapper.className = 'tiny-mediacms-iframe-wrapper';
|
||||
wrapper.setAttribute('contenteditable', 'false');
|
||||
|
||||
// Create edit button (positioned inside wrapper, over the iframe)
|
||||
const editBtn = editor.getDoc().createElement('button');
|
||||
editBtn.className = 'tiny-mediacms-edit-btn';
|
||||
editBtn.setAttribute('type', 'button');
|
||||
editBtn.setAttribute('title', 'Edit media embed options');
|
||||
// Use text "EDIT" instead of icon
|
||||
editBtn.textContent = 'EDIT';
|
||||
|
||||
// Wrap the iframe: insert wrapper, move iframe into it, add button
|
||||
iframe.parentNode.insertBefore(wrapper, iframe);
|
||||
wrapper.appendChild(iframe);
|
||||
wrapper.appendChild(editBtn);
|
||||
});
|
||||
|
||||
fixWrapperWidths();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add CSS styles for hover effects to the editor's document.
|
||||
*/
|
||||
const addStyles = () => {
|
||||
const editorDoc = editor.getDoc();
|
||||
if (!editorDoc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if styles already added
|
||||
if (editorDoc.getElementById('tiny-mediacms-overlay-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = editorDoc.createElement('style');
|
||||
style.id = 'tiny-mediacms-overlay-styles';
|
||||
style.textContent = `
|
||||
.tiny-mediacms-iframe-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
vertical-align: top;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.tiny-mediacms-iframe-wrapper iframe {
|
||||
display: block;
|
||||
}
|
||||
.tiny-mediacms-edit-btn {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
padding: 8px 20px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
transition: background 0.15s, box-shadow 0.15s;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tiny-mediacms-edit-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.4);
|
||||
}
|
||||
`;
|
||||
editorDoc.head.appendChild(style);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle click on the edit button.
|
||||
*
|
||||
* @param {Event} e - The click event
|
||||
*/
|
||||
const handleOverlayClick = (e) => {
|
||||
const target = e.target;
|
||||
|
||||
// Check if clicked on edit button or its child (svg/path)
|
||||
const editBtn = target.closest('.tiny-mediacms-edit-btn');
|
||||
if (!editBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Find the associated wrapper and iframe
|
||||
const wrapper = editBtn.closest('.tiny-mediacms-iframe-wrapper');
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iframe = wrapper.querySelector('iframe');
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the wrapper so TinyMCE knows which element is selected
|
||||
editor.selection.select(wrapper);
|
||||
|
||||
// Open the edit dialog
|
||||
handleIframeAction();
|
||||
};
|
||||
|
||||
// Setup on editor init
|
||||
editor.on('init', () => {
|
||||
addStyles();
|
||||
processIframes();
|
||||
|
||||
// Handle clicks on the overlay
|
||||
editor.getBody().addEventListener('click', handleOverlayClick);
|
||||
});
|
||||
|
||||
// Re-process when content changes
|
||||
editor.on('SetContent', () => {
|
||||
processIframes();
|
||||
});
|
||||
|
||||
// Re-process when content is pasted
|
||||
editor.on('PastePostProcess', () => {
|
||||
setTimeout(processIframes, 100);
|
||||
});
|
||||
|
||||
// Re-process after undo/redo
|
||||
editor.on('Undo Redo', () => {
|
||||
processIframes();
|
||||
});
|
||||
|
||||
// Re-process on any content change (covers modal updates)
|
||||
editor.on('Change', () => {
|
||||
setTimeout(processIframes, 50);
|
||||
});
|
||||
|
||||
// Re-process when node changes (selection changes)
|
||||
editor.on('NodeChange', () => {
|
||||
processIframes();
|
||||
});
|
||||
};
|
||||
|
||||
const registerIframeCommand = (editor, iframeButtonText, iframeButtonImage) => {
|
||||
const handleIframeAction = () => {
|
||||
const iframeEmbed = new IframeEmbed(editor);
|
||||
iframeEmbed.displayDialogue();
|
||||
};
|
||||
|
||||
// Register the iframe icon
|
||||
editor.ui.registry.addIcon(iframeIcon, iframeButtonImage.html);
|
||||
|
||||
// Register the Menu Button as a toggle.
|
||||
// This means that when highlighted over an existing iframe element it will show as toggled on.
|
||||
editor.ui.registry.addToggleButton(iframeButtonName, {
|
||||
icon: iframeIcon,
|
||||
tooltip: iframeButtonText,
|
||||
onAction: handleIframeAction,
|
||||
onSetup: api => {
|
||||
const selector = [
|
||||
'iframe:not([data-mce-object]):not([data-mce-placeholder])',
|
||||
'.tiny-iframe-responsive',
|
||||
'.tiny-mediacms-iframe-wrapper',
|
||||
'a[data-mediacms-textlink="true"]'
|
||||
].join(',');
|
||||
return editor.selection.selectorChangedWithUnbind(
|
||||
selector,
|
||||
api.setActive
|
||||
).unbind;
|
||||
}
|
||||
});
|
||||
|
||||
editor.ui.registry.addMenuItem(iframeMenuItemName, {
|
||||
icon: iframeIcon,
|
||||
text: iframeButtonText,
|
||||
onAction: handleIframeAction,
|
||||
});
|
||||
|
||||
editor.ui.registry.addContextMenu(iframeButtonName, {
|
||||
update: isIframe,
|
||||
});
|
||||
|
||||
// Setup iframe overlays with edit button on hover
|
||||
setupIframeOverlays(editor, handleIframeAction);
|
||||
};
|
||||
|
||||
export const getSetup = async() => {
|
||||
const [
|
||||
iframeButtonText,
|
||||
] = await getStrings([
|
||||
'iframebuttontitle',
|
||||
].map((key) => ({key, component})));
|
||||
|
||||
const [
|
||||
iframeButtonImage,
|
||||
] = await Promise.all([
|
||||
getButtonImage('icon', component),
|
||||
]);
|
||||
|
||||
// Note: The function returned here must be synchronous and cannot use promises.
|
||||
// All promises must be resolved prior to returning the function.
|
||||
return (editor) => {
|
||||
registerIframeCommand(editor, iframeButtonText, iframeButtonImage);
|
||||
};
|
||||
};
|
||||
Vendored
Executable
+30
@@ -0,0 +1,30 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny Media common values.
|
||||
*
|
||||
* @module tiny_mediacms/common
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
export default {
|
||||
pluginName: 'tiny_mediacms/plugin',
|
||||
component: 'tiny_mediacms',
|
||||
iframeButtonName: 'tiny_mediacms_iframe',
|
||||
iframeMenuItemName: 'tiny_mediacms_iframe',
|
||||
iframeIcon: 'tiny_mediacms_iframe',
|
||||
};
|
||||
Vendored
Executable
+60
@@ -0,0 +1,60 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny Media configuration.
|
||||
*
|
||||
* @module tiny_mediacms/configuration
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import {
|
||||
iframeButtonName,
|
||||
iframeMenuItemName,
|
||||
} from './common';
|
||||
import {
|
||||
addContextmenuItem,
|
||||
} from 'editor_tiny/utils';
|
||||
|
||||
const configureMenu = (menu) => {
|
||||
// Add the Iframe Embed to the insert menu.
|
||||
menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;
|
||||
|
||||
return menu;
|
||||
};
|
||||
|
||||
const configureToolbar = (toolbar) => {
|
||||
// The toolbar contains an array of named sections.
|
||||
// The Moodle integration ensures that there is a section called 'content'.
|
||||
|
||||
return toolbar.map((section) => {
|
||||
if (section.name === 'content') {
|
||||
// Insert the iframe button at the start of it.
|
||||
section.items.unshift(iframeButtonName);
|
||||
}
|
||||
|
||||
return section;
|
||||
});
|
||||
};
|
||||
|
||||
export const configure = (instanceConfig) => {
|
||||
// Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.
|
||||
return {
|
||||
contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),
|
||||
menu: configureMenu(instanceConfig.menu),
|
||||
toolbar: configureToolbar(instanceConfig.toolbar),
|
||||
};
|
||||
};
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny Media plugin Embed class for Moodle.
|
||||
*
|
||||
* @module tiny_mediacms/embed
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Templates from 'core/templates';
|
||||
import {
|
||||
getString,
|
||||
getStrings,
|
||||
} from 'core/str';
|
||||
import * as ModalEvents from 'core/modal_events';
|
||||
import {displayFilepicker} from 'editor_tiny/utils';
|
||||
import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';
|
||||
import {component} from "./common";
|
||||
import EmbedModal from './embedmodal';
|
||||
import Selectors from './selectors';
|
||||
import {getEmbedPermissions} from './options';
|
||||
import {getFilePicker} from 'editor_tiny/options';
|
||||
|
||||
export default class MediaEmbed {
|
||||
editor = null;
|
||||
canShowFilePicker = false;
|
||||
canShowFilePickerPoster = false;
|
||||
canShowFilePickerTrack = false;
|
||||
|
||||
/**
|
||||
* @property {Object} The names of the alignment options.
|
||||
*/
|
||||
helpStrings = null;
|
||||
|
||||
/**
|
||||
* @property {boolean} Indicate that the user is updating the media or not.
|
||||
*/
|
||||
isUpdating = false;
|
||||
|
||||
/**
|
||||
* @property {Object} The currently selected media.
|
||||
*/
|
||||
selectedMedia = null;
|
||||
|
||||
constructor(editor) {
|
||||
const permissions = getEmbedPermissions(editor);
|
||||
|
||||
// Indicates whether the file picker can be shown.
|
||||
this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'media') !== 'undefined');
|
||||
this.canShowFilePickerPoster = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');
|
||||
this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');
|
||||
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
async getHelpStrings() {
|
||||
if (!this.helpStrings) {
|
||||
const [addSource, tracks, subtitles, captions, descriptions, chapters, metadata] = await getStrings([
|
||||
'addsource_help',
|
||||
'tracks_help',
|
||||
'subtitles_help',
|
||||
'captions_help',
|
||||
'descriptions_help',
|
||||
'chapters_help',
|
||||
'metadata_help',
|
||||
].map((key) => ({
|
||||
key,
|
||||
component,
|
||||
})));
|
||||
|
||||
this.helpStrings = {addSource, tracks, subtitles, captions, descriptions, chapters, metadata};
|
||||
}
|
||||
|
||||
return this.helpStrings;
|
||||
}
|
||||
|
||||
async getTemplateContext(data) {
|
||||
const languages = this.prepareMoodleLang();
|
||||
|
||||
const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {
|
||||
data[`${key.toLowerCase()}helpicon`] = {text};
|
||||
});
|
||||
|
||||
return Object.assign({}, {
|
||||
elementid: this.editor.getElement().id,
|
||||
showfilepicker: this.canShowFilePicker,
|
||||
showfilepickerposter: this.canShowFilePickerPoster,
|
||||
showfilepickertrack: this.canShowFilePickerTrack,
|
||||
langsinstalled: languages.installed,
|
||||
langsavailable: languages.available,
|
||||
link: true,
|
||||
video: false,
|
||||
audio: false,
|
||||
isupdating: this.isUpdating,
|
||||
}, data, helpIcons);
|
||||
}
|
||||
|
||||
async displayDialogue() {
|
||||
this.selectedMedia = this.getSelectedMedia();
|
||||
const data = Object.assign({}, this.getCurrentEmbedData());
|
||||
this.isUpdating = Object.keys(data).length !== 0;
|
||||
|
||||
this.currentModal = await EmbedModal.create({
|
||||
title: getString('createmedia', 'tiny_mediacms'),
|
||||
templateContext: await this.getTemplateContext(data),
|
||||
});
|
||||
|
||||
await this.registerEventListeners(this.currentModal);
|
||||
}
|
||||
|
||||
getCurrentEmbedData() {
|
||||
const properties = this.getMediumProperties();
|
||||
if (!properties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const processedProperties = {};
|
||||
processedProperties[properties.type.toLowerCase()] = properties;
|
||||
processedProperties.link = false;
|
||||
|
||||
return processedProperties;
|
||||
}
|
||||
|
||||
getSelectedMedia() {
|
||||
const mediaElm = this.editor.selection.getNode();
|
||||
|
||||
if (!mediaElm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {
|
||||
return mediaElm;
|
||||
}
|
||||
|
||||
if (mediaElm.querySelector('video')) {
|
||||
return mediaElm.querySelector('video');
|
||||
}
|
||||
|
||||
if (mediaElm.querySelector('audio')) {
|
||||
return mediaElm.querySelector('audio');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getMediumProperties() {
|
||||
const boolAttr = (elem, attr) => {
|
||||
// As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
|
||||
// So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
|
||||
return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
|
||||
};
|
||||
|
||||
const tracks = {
|
||||
subtitles: [],
|
||||
captions: [],
|
||||
descriptions: [],
|
||||
chapters: [],
|
||||
metadata: []
|
||||
};
|
||||
const sources = [];
|
||||
|
||||
const medium = this.selectedMedia;
|
||||
if (!medium) {
|
||||
return null;
|
||||
}
|
||||
medium.querySelectorAll('track').forEach((track) => {
|
||||
tracks[track.getAttribute('kind')].push({
|
||||
src: track.getAttribute('src'),
|
||||
srclang: track.getAttribute('srclang'),
|
||||
label: track.getAttribute('label'),
|
||||
defaultTrack: boolAttr(track, 'default')
|
||||
});
|
||||
});
|
||||
|
||||
medium.querySelectorAll('source').forEach((source) => {
|
||||
sources.push(source.src);
|
||||
});
|
||||
|
||||
return {
|
||||
type: medium.nodeName.toLowerCase() === 'video' ? Selectors.EMBED.mediaTypes.video : Selectors.EMBED.mediaTypes.audio,
|
||||
sources,
|
||||
poster: medium.getAttribute('poster'),
|
||||
title: medium.getAttribute('title'),
|
||||
width: medium.getAttribute('width'),
|
||||
height: medium.getAttribute('height'),
|
||||
autoplay: boolAttr(medium, 'autoplay'),
|
||||
loop: boolAttr(medium, 'loop'),
|
||||
muted: boolAttr(medium, 'muted'),
|
||||
controls: boolAttr(medium, 'controls'),
|
||||
tracks,
|
||||
};
|
||||
}
|
||||
|
||||
prepareMoodleLang() {
|
||||
const moodleLangs = getMoodleLang(this.editor);
|
||||
const currentLanguage = getCurrentLanguage(this.editor);
|
||||
|
||||
const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({
|
||||
lang,
|
||||
code,
|
||||
"default": lang === currentLanguage,
|
||||
}));
|
||||
|
||||
const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({
|
||||
lang,
|
||||
code,
|
||||
"default": lang === currentLanguage,
|
||||
}));
|
||||
|
||||
return {
|
||||
installed,
|
||||
available,
|
||||
};
|
||||
}
|
||||
|
||||
getMoodleLangObj(subtitleLang) {
|
||||
const {available} = getMoodleLang(this.editor);
|
||||
|
||||
if (available[subtitleLang]) {
|
||||
return {
|
||||
lang: subtitleLang,
|
||||
code: available[subtitleLang],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
filePickerCallback(params, element, fpType) {
|
||||
if (params.url !== '') {
|
||||
const tabPane = element.closest('.tab-pane');
|
||||
element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;
|
||||
|
||||
if (tabPane.id === this.editor.getElement().id + '_' + Selectors.EMBED.mediaTypes.link.toLowerCase()) {
|
||||
tabPane.querySelector(Selectors.EMBED.elements.name).value = params.file;
|
||||
}
|
||||
|
||||
if (fpType === 'subtitle') {
|
||||
// If the file is subtitle file. We need to match the language and label for that file.
|
||||
const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];
|
||||
const langObj = this.getMoodleLangObj(subtitleLang);
|
||||
if (langObj) {
|
||||
const track = element.closest(Selectors.EMBED.elements.track);
|
||||
track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();
|
||||
track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMediaSourceComponent(element, callback) {
|
||||
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
|
||||
const clone = sourceElement.cloneNode(true);
|
||||
|
||||
sourceElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
|
||||
sourceElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
|
||||
|
||||
sourceElement.parentNode.insertBefore(clone, sourceElement.nextSibling);
|
||||
|
||||
if (callback) {
|
||||
callback(clone);
|
||||
}
|
||||
}
|
||||
|
||||
removeMediaSourceComponent(element) {
|
||||
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
|
||||
sourceElement.remove();
|
||||
}
|
||||
|
||||
addTrackComponent(element, callback) {
|
||||
const trackElement = element.closest(Selectors.EMBED.elements.track);
|
||||
const clone = trackElement.cloneNode(true);
|
||||
|
||||
trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
|
||||
trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
|
||||
|
||||
trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);
|
||||
|
||||
if (callback) {
|
||||
callback(clone);
|
||||
}
|
||||
}
|
||||
|
||||
removeTrackComponent(element) {
|
||||
const sourceElement = element.closest(Selectors.EMBED.elements.track);
|
||||
sourceElement.remove();
|
||||
}
|
||||
|
||||
getMediumTypeFromTabPane(tabPane) {
|
||||
return tabPane.getAttribute('data-medium-type');
|
||||
}
|
||||
|
||||
getTrackTypeFromTabPane(tabPane) {
|
||||
return tabPane.getAttribute('data-track-kind');
|
||||
}
|
||||
|
||||
getMediaHTML(form) {
|
||||
const mediumType = this.getMediumTypeFromTabPane(form.querySelector('.root.tab-content > .tab-pane.active'));
|
||||
const tabContent = form.querySelector(Selectors.EMBED.elements[mediumType.toLowerCase() + 'Pane']);
|
||||
|
||||
return this['getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);
|
||||
}
|
||||
|
||||
getMediaHTMLLink(tab) {
|
||||
const context = {
|
||||
url: tab.querySelector(Selectors.EMBED.elements.url).value,
|
||||
name: tab.querySelector(Selectors.EMBED.elements.name).value || false
|
||||
};
|
||||
|
||||
return context.url ? Templates.renderForPromise('tiny_mediacms/embed_media_link', context) : '';
|
||||
}
|
||||
|
||||
getMediaHTMLVideo(tab) {
|
||||
const context = this.getContextForMediaHTML(tab);
|
||||
context.width = tab.querySelector(Selectors.EMBED.elements.width).value || false;
|
||||
context.height = tab.querySelector(Selectors.EMBED.elements.height).value || false;
|
||||
context.poster = tab.querySelector(
|
||||
`${Selectors.EMBED.elements.posterSource} ${Selectors.EMBED.elements.url}`
|
||||
).value || false;
|
||||
|
||||
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_video', context) : '';
|
||||
}
|
||||
|
||||
getMediaHTMLAudio(tab) {
|
||||
const context = this.getContextForMediaHTML(tab);
|
||||
|
||||
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_audio', context) : '';
|
||||
}
|
||||
|
||||
getContextForMediaHTML(tab) {
|
||||
const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({
|
||||
track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,
|
||||
kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),
|
||||
label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||
|
||||
track.querySelector(Selectors.EMBED.elements.trackLang).value,
|
||||
srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
|
||||
defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null
|
||||
})).filter((track) => !!track.track);
|
||||
|
||||
const sources = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.mediaSource + ' '
|
||||
+ Selectors.EMBED.elements.url))
|
||||
.filter((source) => !!source.value)
|
||||
.map((source) => source.value);
|
||||
|
||||
return {
|
||||
sources,
|
||||
description: tab.querySelector(Selectors.EMBED.elements.mediaSource + ' '
|
||||
+ Selectors.EMBED.elements.url).value || false,
|
||||
tracks,
|
||||
showControls: tab.querySelector(Selectors.EMBED.elements.mediaControl).checked,
|
||||
autoplay: tab.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
|
||||
muted: tab.querySelector(Selectors.EMBED.elements.mediaMute).checked,
|
||||
loop: tab.querySelector(Selectors.EMBED.elements.mediaLoop).checked,
|
||||
title: tab.querySelector(Selectors.EMBED.elements.title).value || false
|
||||
};
|
||||
}
|
||||
|
||||
getFilepickerTypeFromElement(element) {
|
||||
if (element.closest(Selectors.EMBED.elements.posterSource)) {
|
||||
return 'image';
|
||||
}
|
||||
if (element.closest(Selectors.EMBED.elements.trackSource)) {
|
||||
return 'subtitle';
|
||||
}
|
||||
|
||||
return 'media';
|
||||
}
|
||||
|
||||
async clickHandler(e) {
|
||||
const element = e.target;
|
||||
|
||||
const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);
|
||||
if (mediaBrowser) {
|
||||
e.preventDefault();
|
||||
const fpType = this.getFilepickerTypeFromElement(element);
|
||||
const params = await displayFilepicker(this.editor, fpType);
|
||||
this.filePickerCallback(params, element, fpType);
|
||||
}
|
||||
|
||||
const addComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .addcomponent');
|
||||
if (addComponentSourceAction) {
|
||||
e.preventDefault();
|
||||
this.addMediaSourceComponent(element);
|
||||
}
|
||||
|
||||
const removeComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .removecomponent');
|
||||
if (removeComponentSourceAction) {
|
||||
e.preventDefault();
|
||||
this.removeMediaSourceComponent(element);
|
||||
}
|
||||
|
||||
const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');
|
||||
if (addComponentTrackAction) {
|
||||
e.preventDefault();
|
||||
this.addTrackComponent(element);
|
||||
}
|
||||
|
||||
const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');
|
||||
if (removeComponentTrackAction) {
|
||||
e.preventDefault();
|
||||
this.removeTrackComponent(element);
|
||||
}
|
||||
|
||||
// Only allow one track per tab to be selected as "default".
|
||||
const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);
|
||||
if (trackDefaultAction && trackDefaultAction.checked) {
|
||||
const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));
|
||||
|
||||
element.parentElement
|
||||
.closest('.root.tab-content')
|
||||
.querySelectorAll(Selectors.EMBED.elements.trackDefault)
|
||||
.forEach((select) => {
|
||||
if (select !== element && getKind(element) === getKind(select)) {
|
||||
select.checked = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleDialogueSubmission(event, modal) {
|
||||
const {html} = await this.getMediaHTML(modal.getRoot()[0]);
|
||||
if (html) {
|
||||
if (this.isUpdating) {
|
||||
this.selectedMedia.outerHTML = html;
|
||||
this.isUpdating = false;
|
||||
} else {
|
||||
this.editor.insertContent(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async registerEventListeners(modal) {
|
||||
await modal.getBody();
|
||||
const $root = modal.getRoot();
|
||||
const root = $root[0];
|
||||
if (this.canShowFilePicker || this.canShowFilePickerPoster || this.canShowFilePickerTrack) {
|
||||
root.addEventListener('click', this.clickHandler.bind(this));
|
||||
}
|
||||
|
||||
$root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));
|
||||
$root.on(ModalEvents.hidden, () => {
|
||||
this.currentModal.destroy();
|
||||
});
|
||||
$root.on(ModalEvents.shown, () => {
|
||||
root.querySelectorAll(Selectors.EMBED.elements.trackLang).forEach((dropdown) => {
|
||||
const defaultVal = dropdown.getAttribute('data-value');
|
||||
if (defaultVal) {
|
||||
dropdown.value = defaultVal;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+47
@@ -0,0 +1,47 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Embedded Media Management Modal for Tiny.
|
||||
*
|
||||
* @module tiny_mediacms/embedmodal
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Modal from 'core/modal';
|
||||
import {component} from './common';
|
||||
|
||||
export default class EmbedModal extends Modal {
|
||||
static TYPE = `${component}/modal`;
|
||||
static TEMPLATE = `${component}/embed_media_modal`;
|
||||
|
||||
registerEventListeners() {
|
||||
// Call the parent registration.
|
||||
super.registerEventListeners();
|
||||
|
||||
// Register to close on save/cancel.
|
||||
this.registerCloseOnSave();
|
||||
this.registerCloseOnCancel();
|
||||
}
|
||||
|
||||
configure(modalConfig) {
|
||||
modalConfig.large = true;
|
||||
modalConfig.removeOnClose = true;
|
||||
modalConfig.show = true;
|
||||
|
||||
super.configure(modalConfig);
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+1072
File diff suppressed because it is too large
Load Diff
Vendored
Executable
+47
@@ -0,0 +1,47 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Iframe Embed Modal for Tiny Media2.
|
||||
*
|
||||
* @module tiny_mediacms/iframemodal
|
||||
* @copyright 2024
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Modal from 'core/modal';
|
||||
import {component} from './common';
|
||||
|
||||
export default class IframeModal extends Modal {
|
||||
static TYPE = `${component}/iframemodal`;
|
||||
static TEMPLATE = `${component}/iframe_embed_modal`;
|
||||
|
||||
registerEventListeners() {
|
||||
// Call the parent registration.
|
||||
super.registerEventListeners();
|
||||
|
||||
// Register to close on save/cancel.
|
||||
this.registerCloseOnSave();
|
||||
this.registerCloseOnCancel();
|
||||
}
|
||||
|
||||
configure(modalConfig) {
|
||||
modalConfig.large = true;
|
||||
modalConfig.removeOnClose = true;
|
||||
modalConfig.show = true;
|
||||
|
||||
super.configure(modalConfig);
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Tiny Media plugin Image class for Moodle.
|
||||
*
|
||||
* @module tiny_mediacms/image
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import Selectors from './selectors';
|
||||
import ImageModal from './imagemodal';
|
||||
import {getImagePermissions} from './options';
|
||||
import {getFilePicker} from 'editor_tiny/options';
|
||||
import {ImageInsert} from 'tiny_mediacms/imageinsert';
|
||||
import {ImageDetails} from 'tiny_mediacms/imagedetails';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {getString} from 'core/str';
|
||||
import {
|
||||
bodyImageInsert,
|
||||
footerImageInsert,
|
||||
bodyImageDetails,
|
||||
footerImageDetails,
|
||||
showElements,
|
||||
hideElements,
|
||||
isPercentageValue,
|
||||
} from 'tiny_mediacms/imagehelpers';
|
||||
|
||||
prefetchStrings('tiny_mediacms', [
|
||||
'imageurlrequired',
|
||||
'sizecustom_help',
|
||||
]);
|
||||
|
||||
export default class MediaImage {
|
||||
canShowFilePicker = false;
|
||||
editor = null;
|
||||
currentModal = null;
|
||||
/**
|
||||
* @type {HTMLElement|null} The root element.
|
||||
*/
|
||||
root = null;
|
||||
|
||||
constructor(editor) {
|
||||
const permissions = getImagePermissions(editor);
|
||||
const options = getFilePicker(editor, 'image');
|
||||
// Indicates whether the file picker can be shown.
|
||||
this.canShowFilePicker = permissions.filepicker
|
||||
&& (typeof options !== 'undefined')
|
||||
&& Object.keys(options.repositories).length > 0;
|
||||
// Indicates whether the drop zone area can be shown.
|
||||
this.canShowDropZone = (typeof options !== 'undefined') &&
|
||||
Object.values(options.repositories).some(repository => repository.type === 'upload');
|
||||
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
async displayDialogue() {
|
||||
const currentImageData = await this.getCurrentImageData();
|
||||
this.currentModal = await ImageModal.create();
|
||||
this.root = this.currentModal.getRoot()[0];
|
||||
if (currentImageData && currentImageData.src) {
|
||||
this.loadPreviewImage(currentImageData.src);
|
||||
} else {
|
||||
this.loadInsertImage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an insert image view asynchronously.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadInsertImage = async function() {
|
||||
const templateContext = {
|
||||
elementid: this.editor.id,
|
||||
showfilepicker: this.canShowFilePicker,
|
||||
showdropzone: this.canShowDropZone,
|
||||
};
|
||||
|
||||
Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])
|
||||
.then(() => {
|
||||
const imageinsert = new ImageInsert(
|
||||
this.root,
|
||||
this.editor,
|
||||
this.currentModal,
|
||||
this.canShowFilePicker,
|
||||
this.canShowDropZone,
|
||||
);
|
||||
imageinsert.init();
|
||||
return;
|
||||
})
|
||||
.catch(error => {
|
||||
window.console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
async getTemplateContext(data) {
|
||||
return {
|
||||
elementid: this.editor.id,
|
||||
showfilepicker: this.canShowFilePicker,
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
async getCurrentImageData() {
|
||||
const selectedImageProperties = this.getSelectedImageProperties();
|
||||
if (!selectedImageProperties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const properties = {...selectedImageProperties};
|
||||
|
||||
if (properties.src) {
|
||||
properties.haspreview = true;
|
||||
}
|
||||
|
||||
if (!properties.alt) {
|
||||
properties.presentation = true;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously loads and previews an image from the provided URL.
|
||||
*
|
||||
* @param {string} url - The URL of the image to load and preview.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadPreviewImage = async function(url) {
|
||||
this.startImageLoading();
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
image.addEventListener('error', async() => {
|
||||
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
|
||||
urlWarningLabelEle.innerHTML = await getString('imageurlrequired', 'tiny_mediacms');
|
||||
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
|
||||
this.stopImageLoading();
|
||||
});
|
||||
|
||||
image.addEventListener('load', async() => {
|
||||
const currentImageData = await this.getCurrentImageData();
|
||||
let templateContext = await this.getTemplateContext(currentImageData);
|
||||
templateContext.sizecustomhelpicon = {text: await getString('sizecustom_help', 'tiny_mediacms')};
|
||||
|
||||
Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
|
||||
.then(() => {
|
||||
this.stopImageLoading();
|
||||
return;
|
||||
})
|
||||
.then(() => {
|
||||
const imagedetails = new ImageDetails(
|
||||
this.root,
|
||||
this.editor,
|
||||
this.currentModal,
|
||||
this.canShowFilePicker,
|
||||
this.canShowDropZone,
|
||||
url,
|
||||
image,
|
||||
);
|
||||
imagedetails.init();
|
||||
return;
|
||||
})
|
||||
.catch(error => {
|
||||
window.console.log(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getSelectedImageProperties() {
|
||||
const image = this.getSelectedImage();
|
||||
if (!image) {
|
||||
this.selectedImage = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties = {
|
||||
src: null,
|
||||
alt: null,
|
||||
width: null,
|
||||
height: null,
|
||||
presentation: false,
|
||||
customStyle: '', // Custom CSS styles applied to the image.
|
||||
};
|
||||
|
||||
const getImageHeight = (image) => {
|
||||
if (!isPercentageValue(String(image.height))) {
|
||||
return parseInt(image.height, 10);
|
||||
}
|
||||
|
||||
return image.height;
|
||||
};
|
||||
|
||||
const getImageWidth = (image) => {
|
||||
if (!isPercentageValue(String(image.width))) {
|
||||
return parseInt(image.width, 10);
|
||||
}
|
||||
|
||||
return image.width;
|
||||
};
|
||||
|
||||
// Get the current selection.
|
||||
this.selectedImage = image;
|
||||
|
||||
properties.customStyle = image.style.cssText;
|
||||
|
||||
const width = getImageWidth(image);
|
||||
if (width !== 0) {
|
||||
properties.width = width;
|
||||
}
|
||||
|
||||
const height = getImageHeight(image);
|
||||
if (height !== 0) {
|
||||
properties.height = height;
|
||||
}
|
||||
|
||||
properties.src = image.getAttribute('src');
|
||||
properties.alt = image.getAttribute('alt') || '';
|
||||
properties.presentation = (image.getAttribute('role') === 'presentation');
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
getSelectedImage() {
|
||||
const imgElm = this.editor.selection.getNode();
|
||||
const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');
|
||||
if (figureElm) {
|
||||
return this.editor.dom.select('img', figureElm)[0];
|
||||
}
|
||||
|
||||
if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {
|
||||
return null;
|
||||
}
|
||||
return imgElm;
|
||||
}
|
||||
|
||||
isPlaceholderImage(imgElm) {
|
||||
if (imgElm.nodeName.toUpperCase() !== 'IMG') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the upload loader and disables UI elements while loading a file.
|
||||
*/
|
||||
startImageLoading() {
|
||||
showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
|
||||
hideElements(Selectors.IMAGE.elements.insertImage, this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the upload loader and disables UI elements while loading a file.
|
||||
*/
|
||||
stopImageLoading() {
|
||||
hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
|
||||
showElements(Selectors.IMAGE.elements.insertImage, this.root);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user