gocodebox/lifterlms-rest

View on GitHub
includes/server/class-llms-rest-students-progress-controller.php

Summary

Maintainability
B
6 hrs
Test Coverage
A
97%
<?php
/**
 * REST Controller for Student Progress.
 *
 * @package LifterLMS_REST/Classes
 *
 * @since 1.0.0-beta.1
 * @version 1.0.0-beta.27
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_REST_Student_Progress_Controller class.
 *
 * @since 1.0.0-beta.1
 * @since 1.0.0-beta.13 Fixed student/lesson post meta key to delete when deleting a student progress.
 * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`.
 */
class LLMS_REST_Students_Progress_Controller extends LLMS_REST_Controller {

    /**
     * Base Resource
     *
     * @var string
     */
    protected $rest_base = 'students/(?P<id>[\d]+)/progress/(?P<post_id>[\d]+)';

    /**
     * Schema properties available for ordering the collection.
     *
     * @var string[]
     */
    protected $orderby_properties = array(
        'date_created',
        'date_updated',
        'progress',
    );

    /**
     * Determine if the current user can view the requested item.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Request object.
     * @return bool
     */
    protected function check_read_item_permissions( $request ) {

        // Can read your own progress.
        if ( get_current_user_id() === $request['id'] ) {
            return true;
        }

        // Must be able to edit post and student to view other's progress.
        if ( current_user_can( 'edit_post', $request['post_id'] ) && current_user_can( 'edit_students', $request['id'] ) ) {
            return true;
        }

        return false;

    }

    /**
     * Determine if current user has permission to delete a user.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Request object.
     * @return true|WP_Error
     */
    public function delete_item_permissions_check( $request ) {

        if ( ! current_user_can( 'edit_post', $request['post_id'] ) || ! current_user_can( 'delete_students', $request['id'] ) ) {
            return llms_rest_authorization_required_error();
        }

        return true;

    }

    /**
     * Delete the object.
     *
     * Note: we do not return 404s when the resource to delete cannot be found. We assume it's already been deleted and respond with 204.
     * Errors returned by this method should be any error other than a 404!
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.13 Fixed student/lesson post meta key to delete, `_is_complete` in place of `_status`.
     *
     * @param obj             $object Instance of the object from $this->get_object().
     * @param WP_REST_Request $request Request object.
     * @return true|WP_Error true when the object is removed, WP_Error on failure.
     */
    protected function delete_object( $object, $request ) {

        $post = llms_get_post( $request['post_id'] );
        $ids  = 'lesson' === $post->get( 'type' ) ? array( $post->get( 'id' ) ) : $post->get_lessons( 'ids' );

        if ( $ids ) {
            foreach ( $ids as $id ) {
                llms_bulk_delete_user_postmeta(
                    $request['id'],
                    $id,
                    array(
                        '_is_complete'        => null,
                        '_completion_trigger' => null,
                    )
                );

            }
        }

        if ( 'lesson' !== $post->get( 'type' ) ) {
            llms_mark_incomplete( $request['id'], $post->get( 'id' ), $post->get( 'type' ) );
        }

        return true;

    }

    /**
     * Retrieve a updated/created dates for a given post.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Student                         $student Student Object.
     * @param LLMS_Course|LLMS_Section|LLMS_Lesson $post Course, Section, or Lesson post object.
     * @param string                               $order Sort order, ASC or DESC.
     * @return string|null
     */
    protected function get_date( $student, $post, $order ) {

        $lessons = 'lesson' === $post->get( 'type' ) ? array( $post->get( 'id' ) ) : $post->get_lessons( 'ids' );

        if ( $lessons ) {

            $lessons = implode( ', ', $lessons );

            global $wpdb;
            // @todo: rewrite query so we don't have to ignore CS rules.
            //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $date = $wpdb->get_var(
                $wpdb->prepare(
                    "
                SELECT updated_date
                  FROM {$wpdb->prefix}lifterlms_user_postmeta
                 WHERE user_id = %d
                   AND post_id IN ( {$lessons} )
                   AND meta_key = '_is_complete'
                 ORDER BY updated_date {$order}
                 LIMIT 1;
                ",
                    $student->get( 'id' )
                )
            );// no-cache ok.
            //phpcs:enable

            if ( $date ) {
                return mysql_to_rfc3339( $date );
            }
        }

        return null;

    }

    /**
     * Get a single item.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_item( $request ) {

        $object   = $this->get_object( array( $request['id'], $request['post_id'] ) );
        $response = $this->prepare_item_for_response( $object, $request );

        return rest_ensure_response( $response );

    }

    /**
     * Check if a given request has access to read an item.
     *
     * @since 1.0.0-beta.1
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function get_item_permissions_check( $request ) {

        if ( ! $this->check_read_item_permissions( $request ) ) {
            return llms_rest_authorization_required_error();
        }

        return true;
    }

    /**
     * Get the API Key's schema, conforming to JSON Schema.
     *
     * @since 1.0.0-beta.27
     *
     * @return array
     */
    protected function get_item_schema_base() {

        return array(
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'students-progress',
            'type'       => 'object',
            'properties' => array(
                'student_id'   => array(
                    'description' => __( 'The ID of the student.', 'lifterlms' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'post_id'      => array(
                    'description' => __( 'The ID of the course/membership.', 'lifterlms' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'date_created' => array(
                    'description' => __( 'Creation date. Format: Y-m-d H:i:s', 'lifterlms' ),
                    'type'        => 'string',
                    'format'      => 'date-time',
                    'context'     => array( 'view', 'edit' ),
                    'arg_options' => array(
                        'validate_callback' => array( $this, 'validate_date_created' ),
                    ),
                ),
                'date_updated' => array(
                    'description' => __( 'Date last modified. Format: Y-m-d H:i:s', 'lifterlms' ),
                    'type'        => 'string',
                    'format'      => 'date-time',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'status'       => array(
                    'description' => __( 'The status of the enrollment.', 'lifterlms' ),
                    'enum'        => array( 'complete', 'incomplete' ),
                    'context'     => array( 'view', 'edit' ),
                    'type'        => 'string',
                    'required'    => true,
                ),
                'progress'     => array(
                    'description' => __( 'Student\'s progress as a percentage.', 'lifterlms' ),
                    'enum'        => array( 'complete', 'incomplete' ),
                    'context'     => array( 'view', 'edit' ),
                    'type'        => 'number',
                    'readonly'    => true,
                ),
            ),
        );

    }

    /**
     * Get object.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.26 Don't autoload current user if a falsy user id is supplied.
     *
     * @param int[] $ids {
     *     Numeric array of ids.
     *
     *     @type int $ids[0] Student id.
     *     @type int $ids[1] Post id.
     * }
     * @return object|WP_Error
     */
    protected function get_object( $ids ) {

        $obj = new stdClass();
        if ( ! is_array( $ids ) ) {
            return $obj;
        }

        $student_id = $ids[0];
        $post_id    = $ids[1];

        $post = llms_get_post( $post_id );

        $student = llms_get_student( $student_id, false );

        if ( ! $student ) {
            return llms_rest_not_found_error();
        }

        $obj->student_id = $student_id;
        $obj->post_id    = $post_id;

        if ( 'lesson' === $post->get( 'type' ) ) {
            $obj->progress = $student->is_complete( $post_id, 'lesson' ) ? (float) 100 : (float) 0;
        } else {
            $obj->progress = (float) $student->get_progress( $post_id, $post->get( 'type' ) );
        }

        $obj->status = $obj->progress < 100 ? 'incomplete' : 'complete';

        $obj->date_updated = $this->get_date( $student, $post, 'DESC' );
        $obj->date_created = $this->get_date( $student, $post, 'ASC' );

        return $obj;

    }

    /**
     * Retrieve an ID from the object
     *
     * @since 1.0.0-beta.1
     *
     * @param obj $object Item object.
     * @return int
     */
    protected function get_object_id( $object ) {

        return array( $object->student_id, $object->post_id );

    }


    /**
     * Prepare request arguments for a database insert/update.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_Rest_Request $request Request object.
     * @return array
     */
    protected function prepare_item_for_database( $request ) {

        $prepared       = parent::prepare_item_for_database( $request );
        $prepared['id'] = $request['id'];

        return $prepared;

    }

    /**
     * Prepare links for the request.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.14 Added `$request` parameter.
     *
     * @param obj             $object  Item object.
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    protected function prepare_links( $object, $request ) {

        $base = rest_url(
            sprintf(
                '/%1$s/%2$s',
                $this->namespace,
                str_replace(
                    array( '(?P<id>[\d]+)', '(?P<post_id>[\d]+)' ),
                    array( $object->student_id, $object->post_id ),
                    $this->rest_base
                )
            )
        );

        $post_type = get_post_type( $object->post_id );

        $links = array(
            'self'    => array(
                'href' => $base,
            ),
            'post'    => array(
                'type' => $post_type,
                'href' => rest_url( sprintf( '/%1$s/%2$ss/%3$d', $this->namespace, $post_type, $object->post_id ) ),
            ),
            'student' => array(
                'href' => rest_url( sprintf( '/%1$s/students/%2$d', $this->namespace, $object->student_id ) ),
            ),
        );

        return $links;

    }

    /**
     * Prepare an object for response.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Abstract_User_Data $object User object.
     * @param WP_REST_Request         $request Request object.
     * @return array
     */
    protected function prepare_object_for_response( $object, $request ) {

        return (array) $object;

    }

    /**
     * Register routes.
     *
     * @since 1.0.0-beta.1
     *
     * @return void
     */
    public function register_routes() {

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base,
            array(
                'args'   => array(
                    'id'      => array(
                        'description' => __( 'Unique identifier for the student. The WP User ID.', 'lifterlms' ),
                        'type'        => 'integer',
                    ),
                    'post_id' => array(
                        'description'       => __( 'Unique course, lesson, or section Identifer. The WordPress Post ID.', 'lifterlms' ),
                        'type'              => 'integer',
                        'validate_callback' => array( $this, 'validate_post_id' ),
                    ),
                ),
                array(
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => array( $this, 'get_item' ),
                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
                    'args'                => $this->get_get_item_params(),
                ),
                array(
                    'methods'             => 'POST',
                    'callback'            => array( $this, 'update_item' ),
                    'permission_callback' => array( $this, 'update_item_permissions_check' ),
                    'args'                => $this->get_endpoint_args_for_item_schema( 'POST' ),
                ),
                array(
                    'methods'             => WP_REST_Server::DELETABLE,
                    'callback'            => array( $this, 'delete_item' ),
                    'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                    'args'                => array(),
                ),
                'schema' => array( $this, 'get_public_item_schema' ),
            )
        );

    }

    /**
     * Determine if current user has permission to create/update an enrollment.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Request object.
     * @return true|WP_Error
     */
    public function update_item_permissions_check( $request ) {

        if ( ! current_user_can( 'edit_post', $request['post_id'] ) || ! current_user_can( 'edit_students', $request['id'] ) ) {
            return llms_rest_authorization_required_error();
        }

        return true;

    }

    /**
     * Update the object in the database with prepared data.
     *
     * @since 1.0.0-beta.1
     *
     * @param array           $prepared Prepared item data.
     * @param WP_REST_Request $request Request object.
     * @return obj Object Instance of object from $this->get_object().
     */
    protected function update_object( $prepared, $request ) {

        if ( in_array( get_post_type( $prepared['post_id'] ), array( 'course', 'section' ), true ) ) {
            $post    = llms_get_post( $prepared['post_id'] );
            $lessons = $post->get_lessons( 'ids' );
        } else {
            $lessons = array( $prepared['post_id'] );
        }

        foreach ( $lessons as $lesson_id ) {

            if ( 'complete' === $prepared['status'] ) {
                llms_mark_complete( $prepared['id'], $lesson_id, 'lesson', 'api_' . get_current_user_id() );
            } elseif ( 'incomplete' === $prepared['status'] ) {
                llms_mark_incomplete( $prepared['id'], $lesson_id, 'lesson', 'api_' . get_current_user_id() );
            }
        }

        return $this->get_object( array( $prepared['id'], $prepared['post_id'] ) );

    }

    /**
     * Validate the date_created
     *
     * @since 1.0.0-beta.1
     *
     * @param string          $value Date string.
     * @param WP_REST_Request $request Request object.
     * @param string          $param Parameter name ("post_id").
     * @return bool
     */
    public function validate_date_created( $value, $request, $param ) {

        $ts  = rest_parse_date( $value );
        $now = llms_current_time( 'U' );

        if ( $ts > $now ) {
            return llms_rest_bad_request_error( __( 'Created date cannot be in the future.', 'lifterlms' ) );
        }

        return true;
    }

    /**
     * Validate the path parameter "post_id".
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.25 Skip enrollment validation for `DELETE` request method.
     *
     * @param int             $value   Post ID.
     * @param WP_REST_Request $request Request object.
     * @param string          $param   Parameter name ("post_id").
     * @return bool
     */
    public function validate_post_id( $value, $request, $param ) {

        $post = get_post( $value );
        if ( ! $post ) {
            return false;
        } elseif ( ! in_array( $post->post_type, array( 'course', 'lesson', 'section' ), true ) ) {
            return false;
        } elseif ( 'DELETE' !== $request->get_method() && ! llms_is_user_enrolled( $request['id'], $value ) ) {
            return false;
        }

        return true;
    }

}