PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / trunk
Tutor LMS – eLearning and online course solution vtrunk
4.0.0 3.9.15 3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / SampleCourse.php
tutor / classes Last commit date
Addons.php 7 hours ago Admin.php 7 hours ago Ajax.php 7 hours ago Announcements.php 7 hours ago Assets.php 7 hours ago Backend_Page_Trait.php 1 year ago BaseController.php 1 year ago Config.php 7 hours ago Container.php 11 months ago Course.php 7 hours ago Course_Embed.php 3 years ago Course_Filter.php 7 hours ago Course_List.php 7 hours ago Course_Settings_Tabs.php 7 hours ago Course_Widget.php 1 year ago Custom_Validation.php 7 hours ago Dashboard.php 7 hours ago Earnings.php 9 months ago FormHandler.php 7 hours ago Frontend.php 7 hours ago Gutenberg.php 1 year ago Icon.php 7 hours ago Input.php 7 hours ago Instructor.php 7 hours ago Instructors_List.php 7 hours ago Lesson.php 7 hours ago Options_V2.php 7 hours ago Permalink.php 7 hours ago Post_types.php 1 day ago Private_Course_Access.php 7 hours ago Q_And_A.php 7 hours ago Question_Answers_List.php 11 months ago Quiz.php 7 hours ago QuizBuilder.php 7 hours ago Quiz_Attempts_List.php 7 hours ago RestAPI.php 2 years ago Reviews.php 7 hours ago Rewrite_Rules.php 2 years ago SampleCourse.php 7 hours ago Shortcode.php 7 hours ago Singleton.php 1 year ago Student.php 7 hours ago Students_List.php 1 year ago Taxonomies.php 1 year ago Template.php 7 hours ago Theme_Compatibility.php 3 years ago Tools.php 1 year ago Tools_V2.php 4 weeks ago Tutor.php 7 hours ago TutorEDD.php 7 hours ago Tutor_Base.php 2 years ago Tutor_Setup.php 7 hours ago Upgrader.php 7 hours ago User.php 7 hours ago UserPreference.php 7 hours ago Utils.php 7 hours ago Video_Stream.php 3 years ago WhatsNew.php 10 months ago Withdraw.php 7 hours ago Withdraw_Requests_List.php 11 months ago WooCommerce.php 7 hours ago
SampleCourse.php
771 lines
1 <?php
2 /**
3 * Sample Course Importer
4 *
5 * @package Tutor\Setup
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com.
8 * @since 4.0.0
9 */
10
11 namespace TUTOR;
12
13 use Tutor\Models\CourseModel;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Class SampleCourse
19 *
20 * Handles immediate, synchronous import of sample courses from a remote JSON file URL.
21 */
22 class SampleCourse {
23
24 /**
25 * Max number of course import.
26 *
27 * @var int
28 */
29 private $max_course_import = 4;
30
31 /**
32 * Map to store relations of old course/item IDs to new database IDs.
33 *
34 * @var array
35 */
36 private $courses_map = array();
37
38 /**
39 * Imports sample courses from a JSON file URL.
40 *
41 * Runs synchronously in a single request and is optimized for small-scale imports.
42 *
43 * @param string $json_file_url The URL of the sample courses JSON file.
44 *
45 * @return array|\WP_Error Array of newly created course IDs on success, or WP_Error on failure.
46 */
47 public function import( $json_file_url ) {
48 if ( empty( $json_file_url ) ) {
49 return new \WP_Error( 'invalid_url', __( 'Invalid JSON file URL provided.', 'tutor' ) );
50 }
51
52 // Fetch raw content from S3 or external URL.
53 $response = wp_safe_remote_get( $json_file_url, array( 'timeout' => 300 ) );
54 if ( is_wp_error( $response ) ) {
55 return $response;
56 }
57
58 $body = wp_remote_retrieve_body( $response );
59 if ( empty( $body ) ) {
60 return new \WP_Error( 'empty_body', __( 'The JSON file is empty.', 'tutor' ) );
61 }
62
63 $contents = json_decode( $body, true );
64 if ( ! is_array( $contents ) || ! isset( $contents['data'] ) ) {
65 return new \WP_Error( 'invalid_json', __( 'Invalid sample course JSON structure.', 'tutor' ) );
66 }
67
68 // Determine if media files should be downloaded (defaults to true for sample courses)..
69 $keep_media_files = isset( $contents['keep_media_files'] ) ? (bool) $contents['keep_media_files'] : true;
70
71 $imported_course_ids = array();
72 $this->courses_map = array();
73
74 // Raise memory limits and execution time for the single request process..
75 if ( function_exists( 'wp_raise_memory_limit' ) ) {
76 wp_raise_memory_limit( 'admin' );
77 }
78
79 @set_time_limit( 600 );
80
81 // Iterate through the sections of the JSON file..
82 foreach ( $contents['data'] as $section ) {
83 $content_type = $section['content_type'] ?? '';
84 $data_list = $section['data'] ?? array();
85
86 // Only import course data lists..
87 if ( 'courses' !== $content_type && tutor()->course_post_type !== $content_type ) {
88 continue;
89 }
90
91 if ( ! is_array( $data_list ) ) {
92 continue;
93 }
94
95 $data_list = array_slice( $data_list, 0, $this->max_course_import );
96 foreach ( $data_list as $course ) {
97 $import_id = $course['ID'] ?? null;
98 $child_contents = $course['contents'] ?? null;
99 $taxonomies = $course['taxonomies'] ?? null;
100 $meta = $course['meta'] ?? null;
101 $thumbnail_url = $course['thumbnail_url'] ?? null;
102 $attachment_links = $course['attachment_links'] ?? null;
103 $attachment_ids = array();
104
105 // Clean and prepare post data..
106 $cleaned_course = $this->unset_post_data( $course );
107 $prepared_course = $this->prepare_post_data( $cleaned_course );
108
109 if ( is_wp_error( $prepared_course ) ) {
110 continue;
111 }
112
113 // Insert course post..
114 $course_id = wp_insert_post( $prepared_course, true );
115 if ( is_wp_error( $course_id ) ) {
116 continue;
117 }
118
119 $imported_course_ids[] = $course_id;
120
121 // Map the old course ID to the newly created course ID..
122 if ( $import_id ) {
123 $this->courses_map[ $import_id ] = array( 'course_id' => $course_id );
124 }
125
126 // Set course categories and tags..
127 if ( $taxonomies ) {
128 $this->set_course_taxonomies( $course_id, $taxonomies );
129 }
130
131 // Fetch and download attachments if requested..
132 if ( $keep_media_files && $attachment_links ) {
133 $attachment_ids = $this->get_attachment_ids_from_urls( $attachment_links );
134 }
135
136 // Import course metadata..
137 if ( $meta ) {
138 $this->import_post_meta( $course_id, $meta, $keep_media_files );
139 }
140
141 // Save attachment meta if attachments were processed..
142 if ( ! empty( $attachment_ids ) ) {
143 update_post_meta( $course_id, '_tutor_attachments', $attachment_ids );
144 }
145
146 // Save course thumbnail..
147 if ( $keep_media_files && $thumbnail_url ) {
148 $this->save_post_thumbnail( $thumbnail_url, $course_id );
149 }
150
151 // Recursively import course topics, lessons, quizzes, and assignments..
152 if ( is_array( $child_contents ) && count( $child_contents ) ) {
153 $this->import_course_topics( $child_contents, $course_id, $keep_media_files );
154 }
155 }
156 }
157
158 return $imported_course_ids;
159 }
160
161 /**
162 * Recursively imports course topics and their child contents.
163 *
164 * @param array $topics List of topic content arrays.
165 * @param int $course_id The parent course post ID.
166 * @param bool $keep_media_files Whether to keep media files.
167 *
168 * @return void
169 */
170 private function import_course_topics( $topics, $course_id, $keep_media_files = false ) {
171 foreach ( $topics as $topic ) {
172 $post_type = $topic['post_type'] ?? '';
173
174 if ( 'topics' !== $post_type && tutor()->topics_post_type !== $post_type ) {
175 continue;
176 }
177
178 $children = $topic['children'] ?? array();
179
180 // Clean and prepare topic post data..
181 $cleaned_topic = $this->unset_post_data( $topic );
182 $prepared_topic = $this->prepare_post_data( $cleaned_topic );
183 $prepared_topic['post_parent'] = $course_id;
184
185 $topic_id = wp_insert_post( $prepared_topic, true );
186 if ( is_wp_error( $topic_id ) ) {
187 continue;
188 }
189
190 // Save topic metadata..
191 $meta = $topic['meta'] ?? null;
192 if ( $meta ) {
193 $this->import_post_meta( $topic_id, $meta, $keep_media_files );
194 }
195
196 // Process children of this topic (lessons, quizzes, assignments)..
197 if ( is_array( $children ) && count( $children ) ) {
198 foreach ( $children as $child ) {
199 $child_post_type = $child['post_type'] ?? '';
200
201 if ( tutor()->lesson_post_type === $child_post_type ) {
202 $this->import_lesson( $child, $topic_id, $keep_media_files );
203 } elseif ( tutor()->quiz_post_type === $child_post_type ) {
204 $this->import_quiz( $child, $topic_id, $keep_media_files );
205 } elseif ( tutor()->assignment_post_type === $child_post_type ) {
206 $this->import_assignment( $child, $topic_id, $keep_media_files );
207 }
208 }
209 }
210 }
211 }
212
213 /**
214 * Imports a lesson under a topic.
215 *
216 * @param array $lesson_data Lesson data array.
217 * @param int $parent_topic_id Parent topic ID.
218 * @param bool $keep_media_files Whether to keep media files.
219 *
220 * @return int|\WP_Error The created lesson ID or WP_Error.
221 */
222 private function import_lesson( $lesson_data, $parent_topic_id, $keep_media_files = false ) {
223 $meta = $lesson_data['meta'] ?? null;
224 $thumbnail_url = $lesson_data['thumbnail_url'] ?? null;
225 $attachment_links = $lesson_data['attachment_links'] ?? null;
226 $attachment_ids = array();
227
228 // Clean and prepare post data..
229 $lesson_data = $this->unset_post_data( $lesson_data );
230 $lesson_data = $this->prepare_post_data( $lesson_data );
231 $lesson_data['post_parent'] = $parent_topic_id;
232
233 $lesson_id = wp_insert_post( $lesson_data, true );
234 if ( is_wp_error( $lesson_id ) ) {
235 return $lesson_id;
236 }
237
238 // Download media attachments.
239 if ( $keep_media_files && is_array( $attachment_links ) ) {
240 $attachment_ids = $this->get_attachment_ids_from_urls( $attachment_links );
241 }
242
243 // Import lesson metadata.
244 if ( $meta ) {
245 $this->import_post_meta( $lesson_id, $meta, $keep_media_files );
246 }
247
248 if ( ! empty( $attachment_ids ) ) {
249 update_post_meta( $lesson_id, '_tutor_attachments', $attachment_ids );
250 }
251
252 if ( $keep_media_files && $thumbnail_url ) {
253 $this->save_post_thumbnail( $thumbnail_url, $lesson_id );
254 }
255
256 return $lesson_id;
257 }
258
259 /**
260 * Imports an assignment under a topic.
261 *
262 * @param array $assignment_data Assignment data array.
263 * @param int $parent_topic_id Parent topic ID.
264 * @param bool $keep_media_files Whether to keep media files.
265 *
266 * @return int|\WP_Error The created assignment ID or WP_Error.
267 */
268 private function import_assignment( $assignment_data, $parent_topic_id, $keep_media_files = false ) {
269 $meta = $assignment_data['meta'] ?? null;
270 $attachment_links = $assignment_data['attachment_links'] ?? null;
271 $attachment_ids = array();
272
273 // Clean and prepare post data.
274 $assignment_data = $this->unset_post_data( $assignment_data );
275 $assignment_data = $this->prepare_post_data( $assignment_data );
276 $assignment_data['post_parent'] = $parent_topic_id;
277
278 $assignment_id = wp_insert_post( $assignment_data, true );
279 if ( is_wp_error( $assignment_id ) ) {
280 return $assignment_id;
281 }
282
283 // Download media attachments.
284 if ( $keep_media_files && is_array( $attachment_links ) ) {
285 $attachment_ids = $this->get_attachment_ids_from_urls( $attachment_links );
286 }
287
288 // Import assignment metadata.
289 if ( $meta ) {
290 $this->import_post_meta( $assignment_id, $meta, $keep_media_files );
291 }
292
293 if ( ! empty( $attachment_ids ) ) {
294 update_post_meta( $assignment_id, '_tutor_assignment_attachments', $attachment_ids );
295 }
296
297 // Update parent course ID for assignments.
298 $topic_id = wp_get_post_parent_id( $assignment_id );
299 $course_id = wp_get_post_parent_id( $topic_id );
300 update_post_meta( $assignment_id, '_tutor_course_id_for_assignments', $course_id );
301
302 return $assignment_id;
303 }
304
305 /**
306 * Imports a quiz under a topic.
307 *
308 * @param array $quiz_data Quiz data array.
309 * @param int $parent_topic_id Parent topic ID.
310 * @param bool $keep_media_files Whether to keep media files.
311 *
312 * @return int|\WP_Error The created quiz ID or WP_Error.
313 */
314 private function import_quiz( $quiz_data, $parent_topic_id, $keep_media_files = false ) {
315 $meta = $quiz_data['meta'] ?? null;
316 $question_answer = $quiz_data['question_answer'] ?? null;
317
318 // Clean and prepare post data.
319 $quiz_data = $this->unset_post_data( $quiz_data );
320 $quiz_data = $this->prepare_post_data( $quiz_data );
321 $quiz_data['post_parent'] = $parent_topic_id;
322
323 $quiz_id = wp_insert_post( $quiz_data, true );
324 if ( is_wp_error( $quiz_id ) ) {
325 return $quiz_id;
326 }
327
328 // Import quiz metadata.
329 if ( $meta ) {
330 $this->import_post_meta( $quiz_id, $meta, $keep_media_files );
331 }
332
333 // Parse and save quiz questions and answers.
334 if ( is_array( $question_answer ) && count( $question_answer ) ) {
335 $quiz_question_answer = $this->flatten_quiz_question_answer( array( $quiz_id => $question_answer ) );
336 $this->save_quiz_questions_answers( $quiz_question_answer, $quiz_id );
337 }
338
339 return $quiz_id;
340 }
341
342 /**
343 * Flattens nested quiz questions and answers into a structured list.
344 *
345 * @param array $data Data containing quiz question/answers.
346 *
347 * @return array Structured array of questions and answers.
348 */
349 private function flatten_quiz_question_answer( $data ) {
350 $flatten_content = array(
351 'question' => array(),
352 'answers' => array(),
353 );
354
355 if ( is_array( $data ) ) {
356 foreach ( $data as $quiz_id => $question_answer ) {
357 if ( ! is_array( $question_answer ) ) {
358 continue;
359 }
360 foreach ( $question_answer as $qa ) {
361 $question = $qa['question'] ?? null;
362 $answers = $qa['answers'] ?? null;
363
364 if ( $question ) {
365 $question['quiz_id'] = $quiz_id;
366 $flatten_content['question'][] = $question;
367 }
368
369 if ( is_array( $answers ) ) {
370 foreach ( $answers as $answer ) {
371 $flatten_content['answers'][] = $answer;
372 }
373 }
374 }
375 }
376 }
377
378 return $flatten_content;
379 }
380
381 /**
382 * Saves quiz questions and answers directly to database tables.
383 *
384 * Uses direct `$wpdb->insert` to preserve mappings and retrieve correct autoincremented keys.
385 *
386 * @param array $quiz_questions_answers Flattened list of quiz questions and answers.
387 * @param int $quiz_id The target quiz post ID.
388 *
389 * @return void
390 */
391 private function save_quiz_questions_answers( $quiz_questions_answers, $quiz_id ) {
392 global $wpdb;
393
394 $table_question = "{$wpdb->prefix}tutor_quiz_questions";
395 $table_answer = "{$wpdb->prefix}tutor_quiz_question_answers";
396
397 $questions = $quiz_questions_answers['question'] ?? array();
398 $answers = $quiz_questions_answers['answers'] ?? array();
399 $question_ids_map = array();
400
401 // 1. Insert Questions.
402 foreach ( $questions as $q ) {
403 $old_question_id = $q['question_id'] ?? 0;
404
405 // Handle question type normalization.
406 if ( isset( $q['question_type'] ) ) {
407 if ( 'image_matching' === $q['question_type'] ) {
408 $q['question_type'] = 'matching';
409 }
410 if ( 'single_choice' === $q['question_type'] ) {
411 $q['question_type'] = 'multiple_choice';
412 }
413 }
414
415 $q['question_title'] = isset( $q['question_title'] ) ? wp_unslash( $q['question_title'] ) : '';
416 if ( isset( $q['question_description'] ) ) {
417 $q['question_description'] = wp_unslash( $q['question_description'] );
418 }
419 if ( isset( $q['answer_explanation'] ) ) {
420 $q['answer_explanation'] = wp_unslash( $q['answer_explanation'] );
421 }
422
423 // Clean settings.
424 $settings = $q['question_settings'] ?? array();
425 if ( is_string( $settings ) ) {
426 $settings = maybe_unserialize( $settings );
427 }
428 if ( is_array( $settings ) ) {
429 if ( isset( $settings['question_type'] ) ) {
430 if ( 'single_choice' === $settings['question_type'] ) {
431 $settings['question_type'] = 'multiple_choice';
432 }
433 if ( 'image_matching' === $settings['question_type'] ) {
434 $settings['is_image_matching'] = true;
435 }
436 }
437 }
438 $q['question_settings'] = maybe_serialize( $settings );
439
440 unset( $q['question_id'] );
441 $q['quiz_id'] = $quiz_id;
442
443 $inserted = $wpdb->insert( $table_question, $q );
444 if ( false !== $inserted ) {
445 $new_question_id = $wpdb->insert_id;
446 if ( $old_question_id ) {
447 $question_ids_map[ $old_question_id ] = $new_question_id;
448 }
449 }
450 }
451
452 // 2. Insert Answers.
453 foreach ( $answers as $a ) {
454 $old_belongs_to = $a['belongs_question_id'] ?? 0;
455
456 // Assign the newly generated parent question ID.
457 if ( isset( $question_ids_map[ $old_belongs_to ] ) ) {
458 $a['belongs_question_id'] = $question_ids_map[ $old_belongs_to ];
459 } else {
460 continue;
461 }
462
463 if ( isset( $a['belongs_question_type'] ) ) {
464 if ( 'single_choice' === $a['belongs_question_type'] ) {
465 $a['belongs_question_type'] = 'multiple_choice';
466 }
467 if ( 'image_matching' === $a['belongs_question_type'] ) {
468 $a['belongs_question_type'] = 'matching';
469 }
470 }
471
472 if ( isset( $a['answer_title'] ) ) {
473 $a['answer_title'] = wp_unslash( $a['answer_title'] );
474 }
475
476 // Process remote answer image if applicable.
477 if ( ! empty( $a['image_url'] ) ) {
478 $upload_data = $this->upload_file_by_url( $a['image_url'] );
479 if ( ! is_wp_error( $upload_data ) ) {
480 $a['image_id'] = $upload_data['id'];
481 }
482 }
483
484 unset( $a['image_url'] );
485 unset( $a['answer_id'] );
486
487 $wpdb->insert( $table_answer, $a );
488 }
489 }
490
491 /**
492 * Sanitizes and prepares post data fields.
493 *
494 * @param array $post Raw post data array.
495 *
496 * @return array Sanitize post array.
497 */
498 private function prepare_post_data( $post ) {
499 $post = sanitize_post( $post, 'db' );
500 $post['post_author'] = get_current_user_id() ? get_current_user_id() : 1;
501 return $post;
502 }
503
504 /**
505 * Removes fields that are not part of standard post tables.
506 *
507 * @param array $post_data Post data containing export tags.
508 *
509 * @return array Stripped post data.
510 */
511 private function unset_post_data( $post_data ) {
512 $keys = array( 'ID', 'filter', 'meta', 'thumbnail_url', 'child_posts', 'attachment_links', 'question', 'answers', 'courses', 'contents' );
513 $updated_data = $post_data;
514
515 foreach ( $keys as $key ) {
516 if ( isset( $updated_data[ $key ] ) ) {
517 unset( $updated_data[ $key ] );
518 }
519 }
520
521 return $updated_data;
522 }
523
524 /**
525 * Downloads and saves categories and tags for the course.
526 *
527 * @param int $course_id Newly created course ID.
528 * @param array $taxonomies Categories and tags arrays.
529 *
530 * @return void
531 */
532 private function set_course_taxonomies( $course_id, $taxonomies ) {
533 $categories = $taxonomies['categories'] ?? array();
534 $tags = $taxonomies['tags'] ?? array();
535 $cat_taxonomy = class_exists( 'Tutor\Models\CourseModel' ) ? CourseModel::COURSE_CATEGORY : 'course-category';
536 $tag_taxonomy = class_exists( 'Tutor\Models\CourseModel' ) ? CourseModel::COURSE_TAG : 'course-tag';
537
538 // Set categories.
539 if ( ! empty( $categories ) && is_array( $categories ) ) {
540 $term_ids = array();
541 foreach ( $categories as $category ) {
542 $term = term_exists( $category['name'], $cat_taxonomy );
543 if ( ! $term ) {
544 $parent_id = 0;
545 if ( ! empty( $category['parent'] ) ) {
546 $parent_name = '';
547 foreach ( $categories as $c ) {
548 if ( (int) $c['term_id'] === (int) $category['parent'] ) {
549 $parent_name = $c['name'];
550 break;
551 }
552 }
553 if ( $parent_name ) {
554 $parent_term = get_term_by( 'name', $parent_name, $cat_taxonomy );
555 if ( $parent_term ) {
556 $parent_id = $parent_term->term_id;
557 }
558 }
559 }
560
561 $inserted = wp_insert_term(
562 $category['name'],
563 $cat_taxonomy,
564 array(
565 'parent' => $parent_id,
566 'description' => $category['description'] ?? '',
567 'slug' => $category['slug'] ?? '',
568 )
569 );
570 if ( ! is_wp_error( $inserted ) ) {
571 $term_ids[] = (int) $inserted['term_id'];
572 }
573 } else {
574 $term_ids[] = (int) ( is_array( $term ) ? $term['term_id'] : $term );
575 }
576 }
577 wp_set_object_terms( $course_id, $term_ids, $cat_taxonomy );
578 }
579
580 // Set tags.
581 if ( ! empty( $tags ) && is_array( $tags ) ) {
582 $term_ids = array();
583 foreach ( $tags as $tag ) {
584 $term = term_exists( $tag['name'], $tag_taxonomy );
585 if ( ! $term ) {
586 $inserted = wp_insert_term(
587 $tag['name'],
588 $tag_taxonomy,
589 array(
590 'description' => $tag['description'] ?? '',
591 'slug' => $tag['slug'] ?? '',
592 )
593 );
594 if ( ! is_wp_error( $inserted ) ) {
595 $term_ids[] = (int) $inserted['term_id'];
596 }
597 } else {
598 $term_ids[] = (int) ( is_array( $term ) ? $term['term_id'] : $term );
599 }
600 }
601 wp_set_object_terms( $course_id, $term_ids, $tag_taxonomy );
602 }
603 }
604
605 /**
606 * Imports metadata for any custom post.
607 *
608 * @param int $post_id Post ID.
609 * @param array $post_meta Associative array of metadata keys and values.
610 * @param bool $keep_media_files Whether to keep media files.
611 *
612 * @return void
613 */
614 private function import_post_meta( $post_id, $post_meta, $keep_media_files = false ) {
615 if ( ! is_array( $post_meta ) || ! count( $post_meta ) ) {
616 return;
617 }
618
619 $normalized_meta = array();
620 foreach ( $post_meta as $key => $values ) {
621 $normalized_meta[ $key ] = is_array( $values ) ? ( isset( $values[0] ) ? $values[0] : null ) : $values;
622 }
623
624 unset( $normalized_meta['_thumbnail_id'] );
625 unset( $normalized_meta['_tutor_attachments'] );
626 unset( $normalized_meta['_tutor_assignment_attachments'] );
627 unset( $normalized_meta['_tutorstarter_schema'] );
628
629 foreach ( $normalized_meta as $key => $value ) {
630 if ( '_video' === $key ) {
631 if ( is_array( $value ) && isset( $value['source'] ) && 'html5' === $value['source'] && $keep_media_files ) {
632 $video_url = $value['source_html5'] ?? '';
633 $upload_data = $this->upload_file_by_url( $video_url );
634 if ( ! is_wp_error( $upload_data ) ) {
635 $value['source_video_id'] = $upload_data['id'];
636 $value['source_html5'] = $upload_data['url'];
637 }
638 }
639 }
640
641 if ( '_tutor_course_id_for_assignments' === $key ) {
642 if ( isset( $this->courses_map[ $value ] ) ) {
643 $value = $this->courses_map[ $value ]['course_id'];
644 }
645 }
646
647 update_post_meta( $post_id, $key, $value );
648 }
649 }
650
651 /**
652 * Helper function to download and register files from remote URL.
653 *
654 * @param string $file_url Remote URL of the file.
655 *
656 * @return array|\WP_Error Meta of the newly created attachment, or WP_Error on failure.
657 */
658 private function upload_file_by_url( $file_url ) {
659 if ( empty( $file_url ) ) {
660 return new \WP_Error( 'invalid_file_url', 'Invalid file URL provided.' );
661 }
662
663 $upload_dir_info = wp_upload_dir();
664 $upload_dir = $upload_dir_info['basedir'];
665
666 $parse_url = parse_url( $file_url );
667 $base_url = ( $parse_url['scheme'] ?? 'http' ) . '://' . ( $parse_url['host'] ?? '' );
668
669 if ( isset( $parse_url['port'] ) ) {
670 $base_url .= ':' . $parse_url['port'];
671 }
672
673 if ( isset( $parse_url['path'] ) ) {
674 $base_url .= strstr( $parse_url['path'], 'wp-content', true );
675 }
676
677 $file_name = basename( $file_url );
678 $source_dir_url = str_replace( $file_name, '', $file_url );
679 $source_dir_part = str_replace( $base_url . 'wp-content/uploads/', '', $source_dir_url );
680
681 $file_path = trailingslashit( $upload_dir ) . trailingslashit( $source_dir_part ) . $file_name;
682 $target_dir = trailingslashit( $upload_dir ) . trailingslashit( $source_dir_part );
683
684 try {
685 if ( ! file_exists( $file_path ) ) {
686 if ( ! file_exists( $target_dir ) ) {
687 wp_mkdir_p( $target_dir );
688 }
689
690 $response = wp_safe_remote_get( $file_url, array( 'timeout' => 300 ) );
691 $file_data = wp_remote_retrieve_body( $response );
692
693 if ( ! empty( $file_data ) ) {
694 file_put_contents( $file_path, $file_data );
695 } else {
696 return new \WP_Error( 'download_failed', 'Failed to download content ' . $file_url );
697 }
698 }
699 } catch ( \Throwable $th ) {
700 return new \WP_Error( 'download_failed', 'Failed to download content ' . $file_url . ': ' . $th->getMessage() );
701 }
702
703 $file_type = wp_check_filetype( $file_name );
704
705 $final_file_url = str_replace( $source_dir_url, site_url( '/wp-content/uploads/' . $source_dir_part ), $file_url );
706
707 $attachment_args = array(
708 'guid' => $final_file_url,
709 'post_mime_type' => $file_type['type'],
710 'post_title' => preg_replace( '/\.[^.]+$/', '', $file_name ),
711 'post_status' => 'inherit',
712 );
713
714 $attach_id = wp_insert_attachment( $attachment_args, $file_path, 0, true );
715
716 if ( is_wp_error( $attach_id ) ) {
717 return $attach_id;
718 }
719
720 if ( wp_attachment_is_image( $attach_id ) ) {
721 require_once ABSPATH . 'wp-admin/includes/image.php';
722 $attach_data = wp_generate_attachment_metadata( $attach_id, $file_path );
723 wp_update_attachment_metadata( $attach_id, $attach_data );
724 }
725
726 return array(
727 'url' => $final_file_url,
728 'id' => $attach_id,
729 'name' => $file_name,
730 'upload_path' => trailingslashit( $source_dir_part ) . $file_name,
731 'type' => $file_type,
732 );
733 }
734
735 /**
736 * Sets the thumbnail of a post.
737 *
738 * @param string $thumbnail_url Thumbnail remote URL.
739 * @param int $post_id Post ID.
740 *
741 * @return bool
742 */
743 private function save_post_thumbnail( $thumbnail_url, $post_id ) {
744 $upload_data = $this->upload_file_by_url( $thumbnail_url );
745 if ( ! is_wp_error( $upload_data ) ) {
746 return (bool) set_post_thumbnail( $post_id, $upload_data['id'] );
747 }
748 return false;
749 }
750
751 /**
752 * Helper function to retrieve multiple attachment IDs from URLs.
753 *
754 * @param array $attachment_urls List of remote attachment URLs.
755 *
756 * @return array List of local attachment IDs.
757 */
758 private function get_attachment_ids_from_urls( array $attachment_urls ) {
759 $attachment_ids = array();
760 foreach ( $attachment_urls as $url ) {
761 if ( $url ) {
762 $upload_data = $this->upload_file_by_url( $url );
763 if ( ! is_wp_error( $upload_data ) ) {
764 $attachment_ids[] = $upload_data['id'];
765 }
766 }
767 }
768 return $attachment_ids;
769 }
770 }
771