-
Notifications
You must be signed in to change notification settings - Fork 83
Description
Summary
When duplicating a course that uses Content Drip with after_finishing_prerequisites mode, the prerequisite lesson/quiz/assignment IDs in _content_drip_settings are copied verbatim to the duplicated course items. The duplicated items end up referencing prerequisites from the original course instead of their corresponding items in the duplicated course. This causes three problems:
- Students can be blocked from progression — the runtime prerequisite check evaluates completion against original-course items that the student is not enrolled in.
- Stale prerequisites are invisible in the editor — the prerequisite selector only shows items belonging to the current course, so the copied original-course IDs don't appear and can't be removed or corrected through the UI.
- UI correction can be unreliable or impossible — since stale IDs are hidden from the editor, instructors often cannot confirm or fully fix the bad linkage without manually inspecting/editing
_content_drip_settingsinwp_postmeta.
Steps to Reproduce
- Create a course with at least two lessons under a topic.
- Enable Content Drip on the course with drip type set to "After Finishing Prerequisites".
- Edit Lesson B and set Lesson A as its prerequisite. Save.
- Duplicate the course (from My Courses or WP Admin).
- Open the duplicated course and edit the duplicated Lesson B.
- Check the Content Drip prerequisite selector.
- Inspect duplicated Lesson B post meta (
_content_drip_settings['prerequisites']) and verify the stored IDs still reference original-course items.
Expected Behavior
The duplicated Lesson B should reference the duplicated Lesson A (the new post ID) as its prerequisite. The prerequisite should appear selected in the editor and function correctly for enrolled students.
Actual Behavior
- The duplicated Lesson B's
_content_drip_settings['prerequisites']contains the original Lesson A's post ID. - The prerequisite selector in the duplicated Lesson B's edit UI shows no selection (or does not display the prerequisite at all), because the original Lesson A doesn't belong to the duplicated course's topic tree.
- Students enrolled in the duplicated course who reach Lesson B are shown a prerequisite lock referencing the original Lesson A, which they cannot complete because they are not enrolled in the original course.
Root Cause
Course_Duplicator::duplicate_post_meta() (tutor-pro/classes/Course_Duplicator.php, lines 295–325) iterates all post meta from the original item and writes it to the duplicated item. It has explicit special-case handling for a few meta keys:
_tutor_course_price_type→ forced tofree_tutor_course_product_id→ skipped entirely_tutor_course_id_for_assignments→ remapped to the new course ID
However, _content_drip_settings passes through the generic copy path with no transformation. The prerequisites array inside this serialized meta value contains post IDs of items from the original course, and these IDs are written as-is to the duplicated item.
The duplicator does not maintain an old-to-new post ID map during the recursive duplication of course → topics → lessons/quizzes/assignments. It only tracks duplicated_post_ids as a guard against reprocessing, not as a mapping table. This means even if a remap branch were added to duplicate_post_meta(), it would not have the mapping data available at that point.
Why it's invisible in the editor
The Content Drip prerequisite selector (tutor-pro/addons/content-drip/views/content-drip-lesson.php, lines 137–160) generates its option list from get_topics($course_id) → get_course_contents_by_topic($topic->ID), which only returns items belonging to the current course. The selected attribute is applied via in_array($topic_item->ID, $prerequisites) inside that loop — there is no rendering path for prerequisite IDs that fall outside the generated option set. So the stale original-course IDs are stored but never displayed.
Why students get blocked
The runtime prerequisite check (tutor-pro/addons/content-drip/classes/ContentDrip.php, lines 349–389) reads the stored prerequisite IDs, calls get_post($id) on each, and checks completion/submission/attempt status based on post type. It does not verify that the prerequisite item belongs to the same course as the content being accessed. If the stored ID points to a valid post in the original course, the check proceeds against that post and can block progression for learners in the duplicated course.
Impact
- Severity: High — any course using Content Drip prerequisites that gets duplicated will produce a broken duplicate where students cannot progress past prerequisite-gated content.
- Affected versions: Any version with the current
Course_Duplicator::duplicate_post_meta()implementation and Content Drip addon. - Affected post types: Lessons, quizzes, and assignments — all three use the same
_content_drip_settingsmeta and the same save/read/runtime paths.
Suggested Fix
The existing _tutor_course_id_for_assignments remap in duplicate_post_meta() (line 320–322) works because it replaces a single course ID with $absolute_course_id, which is already available as a parameter. _content_drip_settings['prerequisites'] is different — it contains an array of child item post IDs (lessons, quizzes, assignments) that were each individually duplicated during the recursive pass. Remapping these requires knowing which new post ID corresponds to each old one, and the duplicator doesn't currently track that. duplicated_post_ids (line 71–75) is used only as a processed-origin guard set, not as an old→new mapping.
The fix requires two changes in Course_Duplicator:
-
Track old→new ID mappings during recursion. Each time
duplicate_post()creates a new child item, record$old_id → $new_idin a map on the class instance (alongside the existingduplicated_post_idsset). -
Add a remap branch in
duplicate_post_meta()for_content_drip_settings, following the same pattern as the existing_tutor_course_id_for_assignmentsbranch. After unserializing the meta value, check if$value['prerequisites']exists and is a non-empty array. If so, iterate each stored ID and replace it with the corresponding new ID from the map built in step 1. IDs with no mapping should be dropped to avoid stale references.
Prior art: import tooling already solves this
The course import flow in Tutor Pro already implements old→new ID remapping for the same class of problem:
ContentMapHandler(tutor-pro/tools/handlers/ContentMapHandler.php, lines 22–80) stores old→new ID maps viaset_content_map()/update_content_map()/get_content_map(), keyed by content type (course, topic, lesson, assignment, quiz).- Each importer records old→new mappings after insertion —
Importer.php(lines 436–442) for courses/topics,LessonImporter.php(lines 102–107),AssignmentImporter.php(lines 105–110),QuizImporter.php(lines 410–416). Helper::prepare_meta()(tutor-pro/tools/Helper.php, lines 287–320) then rewrites specific meta keys using those maps — notably_tutor_course_id_for_assignmentsat lines 317–320.
The duplicator's version would be simpler since it only needs a single instance-level $old_to_new array rather than a persistent option-based map, but the pattern is the same: record mappings during creation, apply them during meta copy.
Related observation (separate scope): import path
The same issue may also affect the course import flow. _content_drip_settings is included in generic postmeta insertion during import (tutor-pro/tools/Helper.php, lines 294–302; LessonImporter.php, lines 110–124; AssignmentImporter.php, lines 113–127; QuizImporter.php, lines 401–405) without a dedicated remap branch for prerequisites, even though the import infrastructure has the old→new mapping data available via ContentMapHandler. This may be worth addressing in the same fix.
Environment
- Tutor LMS Pro (Content Drip addon + Course Duplicator)
- WordPress (any recent version)
- Reproducible on any environment where Content Drip prerequisites are configured before course duplication