From 630eb77173845af3436a4e208b34edcc5ab1ca17 Mon Sep 17 00:00:00 2001 From: Markos Gogoulos Date: Thu, 7 May 2026 14:08:18 +0300 Subject: [PATCH] all --- cms/version.py | 2 +- files/urls.py | 4 +++ files/views/__init__.py | 1 + files/views/media.py | 28 +++++++++++++++++ .../filter/mediacms/classes/text_filter.php | 16 +--------- .../tiny/mediacms/amd/src/iframeembed.js | 31 +++++++++++++++++-- lti/views.py | 16 +--------- 7 files changed, 65 insertions(+), 33 deletions(-) diff --git a/cms/version.py b/cms/version.py index 27fd225b..5374caac 100644 --- a/cms/version.py +++ b/cms/version.py @@ -1 +1 @@ -VERSION = "8.0.1e" +VERSION = "8.0.1f" diff --git a/files/urls.py b/files/urls.py index f88d5dbd..ca8c82aa 100644 --- a/files/urls.py +++ b/files/urls.py @@ -67,6 +67,10 @@ urlpatterns = [ name="api_get_encoding", ), re_path(r"^api/v1/search$", views.MediaSearch.as_view()), + re_path( + rf"^api/v1/media/{friendly_token}/share$", + views.MediaShare.as_view(), + ), re_path( rf"^api/v1/media/{friendly_token}/actions$", views.MediaActions.as_view(), diff --git a/files/views/__init__.py b/files/views/__init__.py index 7454c0ad..7e2fa6c3 100644 --- a/files/views/__init__.py +++ b/files/views/__init__.py @@ -9,6 +9,7 @@ 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 MediaShare # noqa: F401 from .pages import about # noqa: F401 from .pages import add_subtitle # noqa: F401 from .pages import approval_required # noqa: F401 diff --git a/files/views/media.py b/files/views/media.py index 5eff0324..4eb5674d 100644 --- a/files/views/media.py +++ b/files/views/media.py @@ -4,6 +4,8 @@ from django.conf import settings from django.contrib.postgres.search import SearchQuery from django.db.models import Count, F, Prefetch, Q, prefetch_related_objects from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import permissions, status @@ -1233,3 +1235,29 @@ class MediaSearch(APIView): page = paginator.paginate_queryset(media, request) serializer = MediaSearchSerializer(page, many=True, context={"request": request}) return paginator.get_paginated_response(serializer.data) + + +@method_decorator(csrf_exempt, name='dispatch') +class MediaShare(APIView): + """Mark a media item as shared when the owner embeds it via the LTI plugin.""" + + permission_classes = [permissions.IsAuthenticated] + + def post(self, request, friendly_token): + media = get_object_or_404(Media, friendly_token=friendly_token) + if media.user != request.user: + return Response(status=status.HTTP_403_FORBIDDEN) + + MediaPermission.objects.get_or_create( + media=media, + user=request.user, + defaults={'owner_user': request.user, 'permission': 'owner'}, + ) + + courseid = request.data.get('courseid') + if courseid: + category = Category.objects.filter(lti_context_id=str(courseid), is_rbac_category=True).first() + if category: + EmbedMediaCourse.objects.get_or_create(media=media, category=category) + + return Response(status=status.HTTP_200_OK) diff --git a/lms-plugins/mediacms-moodle/filter/mediacms/classes/text_filter.php b/lms-plugins/mediacms-moodle/filter/mediacms/classes/text_filter.php index a6c2df86..2b73082f 100644 --- a/lms-plugins/mediacms-moodle/filter/mediacms/classes/text_filter.php +++ b/lms-plugins/mediacms-moodle/filter/mediacms/classes/text_filter.php @@ -269,21 +269,7 @@ class text_filter extends \core_filters\text_filter { $view_url = new moodle_url('/filter/mediacms/my_media.php', $view_params); - $launch_url = new moodle_url('/filter/mediacms/launch.php', $view_params); - - // Hidden iframe fires the LTI launch silently on every page load. - // When the media owner (teacher) loads the page, EmbedMediaLTIView's - // auto-share logic runs, marking the media as shared — same as for - // regular embedded iframes. Visits by non-owners are harmless. - $hidden_iframe = html_writer::tag('iframe', '', [ - 'src' => $launch_url->out(false), - 'style' => 'display:none;width:0;height:0;border:0;', - 'title' => '', - 'tabindex' => '-1', - 'aria-hidden' => 'true', - ]); - - return $hidden_iframe . html_writer::tag('a', $text_matches[1], [ + return html_writer::tag('a', $text_matches[1], [ 'href' => $view_url->out(false), 'target' => '_blank', 'rel' => 'noopener noreferrer', diff --git a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js index ef4d1700..675761fa 100755 --- a/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js +++ b/lms-plugins/mediacms-moodle/tiny/mediacms/amd/src/iframeembed.js @@ -7,7 +7,7 @@ import Selectors from './selectors'; import { getLti, getData } from './options'; const PREFS_KEY = 'tiny_mediacms_embed_prefs'; -const PREFS_FIELDS = ['showTitle', 'linkTitle', 'showUserAvatar', 'width', 'height']; +const PREFS_FIELDS = ['showTitle', 'linkTitle', 'showUserAvatar', 'width', 'height', 'textLinkOnly']; export default class IframeEmbed { editor = null; @@ -210,6 +210,32 @@ export default class IframeEmbed { return url.toString(); } + signalShare(values) { + const parsed = this.parseInput(values.url); + if (!parsed || parsed.isGeneric || !parsed.videoId) { + return; + } + + const editorData = getData(this.editor); + const baseUrl = parsed.isLtiLaunch + ? (editorData?.mediacmsBaseUrl || '') + : parsed.baseUrl; + + if (!baseUrl) { + return; + } + + const ltiConfig = getLti(this.editor); + const courseId = ltiConfig?.courseId || 0; + + fetch(`${baseUrl}/api/v1/media/${parsed.videoId}/share`, { + method: 'POST', + credentials: 'include', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({courseid: courseId}), + }).catch(() => {}); + } + savePrefs(values) { try { const prefs = {}; @@ -251,7 +277,7 @@ export default class IframeEmbed { showTitle: getDefault('showTitle'), linkTitle: getDefault('linkTitle'), showUserAvatar: getDefault('showUserAvatar'), - textLinkOnly: data.textLinkOnly || false, + textLinkOnly: getDefault('textLinkOnly', false), startAtEnabled: data.startAtEnabled || false, startAt: data.startAt || '0:00', width, @@ -531,6 +557,7 @@ export default class IframeEmbed { } this.savePrefs(values); + this.signalShare(values); const html = await this.generateIframeHtml(values); if (html) { if (this.isUpdating && this.selectedIframe) { diff --git a/lti/views.py b/lti/views.py index e28ca02b..e1e44d76 100644 --- a/lti/views.py +++ b/lti/views.py @@ -30,7 +30,7 @@ from pylti1p3.exception import LtiException from pylti1p3.message_launch import MessageLaunch from pylti1p3.oidc_login import OIDCLogin -from files.models import Category, EmbedMediaCourse, Media, MediaPermission +from files.models import Media, MediaPermission from rbac.models import RBACMembership from .adapters import DjangoRequest, DjangoSessionService, DjangoToolConfig @@ -728,20 +728,6 @@ class EmbedMediaLTIView(View): context_id = lti_session.get('context_id') platform_id = lti_session.get('platform_id') - # Auto-share: when the media owner loads their own embed via LTI, - # mark it as shared and link it to the course. This fires on the - # teacher's first page view after saving (Moodle redirects there automatically). - if media.user == request.user: - MediaPermission.objects.get_or_create( - media=media, - user=request.user, - defaults={'owner_user': request.user, 'permission': 'owner'}, - ) - if context_id: - category = Category.objects.filter(lti_context_id=context_id, is_rbac_category=True).first() - if category: - EmbedMediaCourse.objects.get_or_create(media=media, category=category) - if media.is_shared and context_id and platform_id: try: resource_link = (