File size: 4,076 Bytes
d9f0d1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<?php
/**

 * Courses endpoints.

 *

 * GET /bemalearn/v1/courses       public list

 * GET /bemalearn/v1/courses/{id}  public detail

 *

 * The authority on what these should return is docs/API-CONTRACT.md.

 */

if (!defined('ABSPATH')) {
    exit;
}

class BL_Courses_Controller {

    const PREVIEW_TTL = 300;

    public function register_routes() {
        register_rest_route('bemalearn/v1', '/courses', [
            'methods'             => 'GET',
            'callback'            => [$this, 'get_courses'],
            'permission_callback' => '__return_true',
        ]);

        register_rest_route('bemalearn/v1', '/courses/(?P<id>\d+)', [
            'methods'             => 'GET',
            'callback'            => [$this, 'get_course'],
            'permission_callback' => '__return_true',
            'args'                => [
                'id' => [
                    'required'          => true,
                    'validate_callback' => function ($v) {
                        return is_numeric($v) && (int) $v > 0;
                    },
                ],
            ],
        ]);
    }

    public function get_courses($request) {
        global $wpdb;

        $table = $wpdb->prefix . 'bl_courses';
        $users = $wpdb->users;

        $rows = $wpdb->get_results(
            "SELECT c.id, c.title, c.price_minor, c.currency,
                    c.enrolment_count, c.average_rating,
                    c.is_published, c.published_at,
                    u.display_name AS instructor_name
               FROM {$table} c
               LEFT JOIN {$users} u ON u.ID = c.instructor_id
              WHERE c.is_published = 1
              ORDER BY c.published_at DESC, c.id DESC"
        );

        $courses = [];
        foreach ((array) $rows as $row) {
            $courses[] = $this->shape($row);
        }

        return new WP_REST_Response([
            'courses'                 => $courses,
            'previewExpiresInSeconds' => self::PREVIEW_TTL,
        ], 200);
    }

    public function get_course($request) {
        global $wpdb;

        $id    = (int) $request->get_param('id');
        $table = $wpdb->prefix . 'bl_courses';
        $users = $wpdb->users;

        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT c.*, u.display_name AS instructor_name

               FROM {$table} c

               LEFT JOIN {$users} u ON u.ID = c.instructor_id

              WHERE c.id = %d",
            $id
        ));

        // An unpublished course is treated as absent. A 403 here would confirm
        // that the course exists, which is itself a disclosure.
        if (!$row || !(int) $row->is_published) {
            return new WP_Error('not_found', 'Course not found.', ['status' => 404]);
        }

        $shaped = $this->shape($row);
        $shaped['description'] = $row->description;
        $shaped['lessonCount'] = (int) $row->lesson_count;

        return new WP_REST_Response($shaped, 200);
    }

    /**

     * Turn a database row into the contract shape.

     *

     * enrolmentCount and averageRating are nullable in the contract: null means

     * "not yet counted", which is not the same as a measured zero. They are

     * cast only when they carry a value.

     */
    private function shape($row) {
        return [
            'id'             => (int) $row->id,
            'title'          => $row->title,
            'instructorName' => $row->instructor_name,
            'priceMinor'     => (int) $row->price_minor,
            'currency'       => $row->currency,
            'enrolmentCount' => $row->enrolment_count === null ? null : (int) $row->enrolment_count,
            'averageRating'  => $row->average_rating === null ? null : (float) $row->average_rating,
            'publishedAt'    => $row->published_at
                ? gmdate('c', strtotime($row->published_at))
                : null,
            'isPublished'    => (bool) $row->is_published,
        ];
    }
}