includes/server/class-llms-rest-students-progress-controller.php
<?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;
}
}