Skip to content

Commit 4975d85

Browse files
committed
fix(vue/documents): force cid/sid/gid on POST /api/documents
The shared axios request interceptor in assets/vue/config/api.js reads getRawCourseContext() from window.location.search and injects cid/sid/gid into config.params. When the SPA navigates inside a course without preserving ?cid=… in the URL, that read returns nulls, no params are injected, and the POST goes out as plain /api/documents (no query). On the server side, CidReqListener sees an empty $request->get('cid'), treats the request as out-of-course, and calls cleanSessionHandler() which removes session.course. CreateDocumentFileAction then runs buildResourceLinkListFromContext() which finds no course in session and no cid in the query, so the resource_link entry is built without cid. The new document is created but is orphaned — no resource_link binding it to the course — so it disappears from the documents list immediately after creation. The security check still passes because admins inherit ROLE_CURRENT_COURSE_TEACHER via Symfony's role_hierarchy (security.yaml), masking the bug for everyone with ROLE_ADMIN. Affected: every /api/documents POST that goes through the legacy makeService("documents").createWithFormData path — i.e. folder creation and any flow using store.dispatch("documents/createWithFormData", …). The chunked uploader at DocumentsUpload.vue:609 was already constructing its own URL with explicit cid/sid/gid and is unaffected. Surgical fix in services/documents.js: - Override createWithFormData to construct its own POST request. - Read cid/sid/gid from the Pinia cidReq store first (authoritative, maintained by router guards, survives URL changes) and fall back to getCourseContext() (URL-based) only when the store is not yet active (early bootstrap / tests). - Append them as query parameters on /api/documents. Confirmed: the same code path exists on v2.0.2 and on chamilo/chamilo-lms upstream master, so this is a community-wide bug. Forwarding it upstream as a separate PR is on the to-do.
1 parent 147e70e commit 4975d85

1 file changed

Lines changed: 47 additions & 3 deletions

File tree

assets/vue/services/documents.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import makeService, { asResponse, toServiceError } from "./api"
22
import baseService from "./baseService"
33
import prettyBytes from "pretty-bytes"
4+
import api from "../config/api"
5+
import { useCidReqStore } from "../store/cidReq"
6+
import { getCourseContext } from "../utils/courseContext"
47

58
const oldService = makeService("documents")
69

@@ -220,11 +223,52 @@ export default {
220223

221224
/**
222225
* Override createWithFormData only for documents to avoid breaking other modules.
223-
* This keeps api.js untouched and prevents sending searchFieldValues as "[object Object]".
226+
* Two reasons:
227+
* 1. Flattens searchFieldValues so FormData does not serialize them as "[object Object]".
228+
* 2. Forces the current course/session/group context (cid/sid/gid) onto the POST URL.
229+
* The shared axios interceptor in config/api.js reads getRawCourseContext() from
230+
* window.location.search, which is empty when the SPA navigates without preserving
231+
* ?cid=. Without cid in the request, CidReqListener wipes the session course;
232+
* CreateDocumentFileAction then builds a resource_link with no cid, and the new
233+
* document hangs orphaned (not visible in the course documents list). Reading the
234+
* Pinia cidReq store directly here is the canonical source maintained by the
235+
* router guards and survives URL changes.
224236
*/
225-
createWithFormData(payload) {
237+
async createWithFormData(payload) {
226238
const prepared = flattenSearchFieldValues(payload)
227-
return oldService.createWithFormData(prepared)
239+
const formData = buildFormData(prepared)
240+
241+
// Course context: Pinia store is authoritative; getCourseContext() (URL-based)
242+
// is the fallback for early init before the store is hydrated.
243+
let cid = 0
244+
let sid = 0
245+
let gid = 0
246+
try {
247+
const store = useCidReqStore()
248+
cid = Number(store.course?.id ?? 0) || 0
249+
sid = Number(store.session?.id ?? 0) || 0
250+
gid = Number(store.group?.id ?? 0) || 0
251+
} catch {
252+
// Pinia not active (test or pre-mount) — fall through to URL parse.
253+
}
254+
if (!cid) {
255+
const fromUrl = getCourseContext()
256+
cid = fromUrl.cid
257+
sid = fromUrl.sid
258+
gid = fromUrl.gid
259+
}
260+
261+
const params = {}
262+
if (cid > 0) params.cid = cid
263+
if (sid > 0) params.sid = sid
264+
if (gid > 0) params.gid = gid
265+
266+
try {
267+
const response = await api.post("/api/documents", formData, { params })
268+
return asResponse(response)
269+
} catch (error) {
270+
throw toServiceError(error)
271+
}
228272
},
229273

230274
/**

0 commit comments

Comments
 (0)