Git Product home page Git Product logo

lifterlms-rest's Introduction

LifterLMS REST API

Test PHPUnit PHP Tests Coverage PHPCS Coding Standards Code Climate maintainability Code Climate test coverage

PHP Supported Version

Contributions Welcome Slack community


A REST API feature plugin for LifterLMS.


This specification (and repository) is in beta. It is not yet a fully-functional API. API changes will be continue to be made without deprecation until 1.0.0 is released as a "stable" API.

Contributing Contributions Welcome

We are looking for both API specification designers and developers interested in contributing. Read our contribution guidelines here.

Specification & Documentation

The LifterLMS REST API follows the OpenAPI Specification (Version 3.0.0).

REST API documentation is available at gocodebox.github.io/lifterlms-rest/.

The full OpenAPI spec can be downloaded in json or yaml formats.

Building & Developing REST API Doc spec

This repo uses ReDoc.

To build the docs locally for development:

  • npm start: Starts the development server.
  • npm run build: Bundles the spec and prepares web_deploy folder with static assets.
  • npm test: Validates the spec.
  • npm run gh-pages: Deploys docs to GitHub Pages. You don't need to run it manually if you have Travis CI configured.

Tests and Coding Standards

The LifterLMS REST API adheres to the documentation and coding standards defined for the LifterLMS Core codebase.

  • composer run check-cs: Check coding and documentation standards, showing warnings and errors.
  • composer run check-cs-errors: Check coding and documentation standards, showing errors only.

To run the phpunit test suite:

  • composer run tests-install: Install the test suite.
  • composer run tests-run: Run the test suite.

Building and Publishing Releases

  • llms-dev log:write: Write changelog.
  • llms-dev ver:update: Update version numbers.
  • npm run build: Build a release: spec, doc code snippets, and included language files.
  • llms-dev archive: Build distributable zip file.
  • llms-dev publish:gh: Publish release.
  • Open a Pull Request in the LifterLMS Core to upgrade the library.

These steps require write access to the repository as well as access to the internal development CLI llms-dev. Developers and maintainers are provided with required permissions as needed.

lifterlms-rest's People

Contributors

brianhogg avatar dependabot[bot] avatar eri-trabiccolo avatar github-actions[bot] avatar ideadude avatar pondermatic avatar thomasplevy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

lifterlms-rest's Issues

How many nesting levels for our api fields?

Schermata 2019-07-02 alle 17 24 48

I think we have to "define" how much nested our fields can be, for two reasons:

  1. rules :D which means "practical in the future"
  2. "practical now"
    There's a core helper that, given a response data (array) and a context (and implicitly a schema), will filter the "response" by context:
    https://github.com/WordPress/WordPress/blob/5.2.2/wp-includes/rest-api/endpoints/class-wp-rest-controller.php#L233
    As you can see it will only work with a maximum of one nesting level (it will work with the 'content' => ['raw','rendered'] structure type).

I happen to use it, of course we can override it, and we'll fix the point 2), but are we sure it'll be a good choice for the future?

Of course I see that helper is kinda limited and we can possibly do it better.

Password protected posts

Add logic to handle access to password protected llms posts.

We can look at how the WordPress core does in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

Resources to Document

Phase 1 (Completed)

Resources to be made available in the first phase of the API. Target public release August 2019.

  • Student accounts /students
  • Student enrollments /students/{id}/enrollments
  • Student Progress /students/{id}/progress
  • Instructors /instructors
  • Instructor Content /instructors/{id}/content
  • Courses /courses
  • Course Enrollments /courses/{id}/enrollments
  • Course Instructors /courses/{id}/instructors /instructors?post={id}
  • Course Contents (syllabus/outline) /courses/{id}/contents
  • Sections /sections
  • Lessons /lessons
  • Quizzes /quizzes
  • Quiz Questions /questions
  • Enrollments /enrollments
  • Memberships /memberships
  • Membership Enrollments /memberships/{id}/enrollments /enrollments?post={id}
  • Access Plans /access-plans
  • Webhooks

Ecommerce

  • Orders /orders
  • Transactions /orders/{id}/transactions
  • Refunds /orders/{id}/refunds
  • Coupons /coupons

Engagements

  • Engagements
  • Achievements
  • Certificates
  • engagement emails

Grades & Quizzes

  • Course Grades /courses/{id}/grades
  • Quiz Attempts /quizzes/{id}/attempts

System & Status

  • Settings /settings
  • Integrations
  • Payment Gateways
  • System Status
  • Reports (sales, enrollments, etc...)

Other

Add-Ons

Assignments

  • Assignments /assignments
  • Assignment Tasks /assignments/{id}/tasks
  • Assignment Submissions /assignments/{id}/submissions

Social Learning

(todo)

Private Areas

  • Private posts /private-posts (CRUD)
  • Private Post Comments /private-posts/{id}/comments (possibly unnecessary due to commenting api available via the WP core).
  • Automations /private-post-automations/ (CRUD)

Advanced Quizzes

  • Additional question types

Unused variable `$schema` in LLMS_REST_Posts_Controller::register_routes() method.

$schema = $this->get_item_schema();
$get_item_args = array(
'context' => $this->get_context_param( array( 'default' => 'view' ) ),
);
if ( isset( $schema['properties']['password'] ) ) {
$get_item_args['password'] = array(
'description' => __( 'Post password. Required if the post is password protected.', 'lifterlms' ),
'type' => 'string',
);
}

This variable is defined but never used. I'm not user if this variable is intended to be used instead of the callback reference on line 116 (

'schema' => array( $this, 'get_public_item_schema' ),
) or if it was used previously and should be removed now as it's redundant.

Need an additional test case class in the framework specific to testing REST API Server methods

public function setUp() {
parent::setUp();
global $wp_rest_server;
$this->server = $wp_rest_server = new WP_REST_Server();
do_action( 'rest_api_init' );
}

Since not all tests will require tests of actual REST API methods run via the actual REST API server I think a second test case should be created which extends this test case and then adds these this setUp() and tearDown() methods.

This test case could be LLMS_REST_Server_Unit_Test_Case

Then other tests (functions, install, custom post types, etc...) can use this generic test case and the API Server classes / controllers / etc can extend the server class which spools up and down the server reference.

Courses: specs miss some properties

yes|no properties:

  • (access) time_period (string) Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned)
  • enrollment_period (string) Whether or not a course time period restriction is enabled [yes|no] (all checks should check for 'yes' as an empty string might be returned)
  • has_prerequisite (string) Determine if prerequisites are enabled [yes|no]
  • tile_featured_video (string) Displays the featured video instead of the featured image on course tiles [yes|no]

others:

  • average_grade (float) Calculated value of the overall average grade of all enrolled students in the course.
  • average_progress (float) Calculated value of the overall average progress of all enrolled students in the course.

sales page url feedback

@thomasplevy
I think we need a different course's sales_page_url.
Looking at:

so the lifterlms core the sales_page_url is not what the specs are describing right now.
The specs are calling sales_page_url what for the corse is sales_page_content_url
https://github.com/gocodebox/lifterlms/blob/3.33.2/includes/models/model.llms.course.php#L276

I think we can change the specs so that sales_page_url is a read-only param which equals to LLMS_Course->get_sales_page_url(), while sales_page_type, sales_page_page_id, sales_page_content_url (the former sales_page_url) are available in both edit and view contexts.

Though maybe we can also consider to be more restrictive and make the latter 3 available in edit context only. I mean, if you're a viewer should you really know how something has been built? I guess no, you shouldn't...

what do you think?

Originally posted by @eri-trabiccolo in #6 (comment)

Contexts

Add context parameters for all requests.

Available contexts:

view (default) return content for viewing/displaying
edit returns content with additional "raw" data used for displaying the content in the WP Editor for editing.

Post fields should make use of this by returning certain fields as an object with additional data depending on the context.

EG: GET /courses/{course_id} would return in view context:

{
   "content": {
       "rendered": "lorem ipsum dolor sit"
   },
   "title": {
       "rendered": "lorem ipsum dolor sit"
   },   
}

The same resource in edit context would return:

{
   "content": {
       "rendered": "lorem ipsum dolor sit"
       "raw": "lorem ipsum dolor sit"
   },
   "title": {
       "rendered": "lorem ipsum dolor sit",
       "raw": "lorem ipsum dolor sit"
   },   
}

Fields with raw and rendered values include:

post content, post title, post excerpt.

More to come

Authentication & API Credentials

The API will implement HTTP Basic Authentication (consumer key/secret) via request headers.

As a fallback for servers which cannot properly parse request headers the consumer key/secret may be passed as query string variables, for example POST /students?consumer_key={$key}&consumer_secret={$secret}

The API will only accept HTTPS requests.

API keys will be stored in a custom table, wp_lifterlms_api_keys with the following structure:

  `key_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL,
  `description` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `permissions` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL,
  `consumer_key` char(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `consumer_secret` char(43) COLLATE utf8mb4_unicode_ci NOT NULL,
  `last_access` datetime DEFAULT NULL,
  PRIMARY KEY (`key_id`),
  KEY `consumer_key` (`consumer_key`),
  KEY `consumer_secret` (`consumer_secret`)

Each key will belong to a WP_User (related via user_id)

Permissions are an enum: read, write, read_write, this context will be taken into account during requests. EG: a key with read permissions will receive a 403 Forbidden response when attempting to create a resources and a key with write permissions will receive a 403 Forbidden when attempting to read resources. A read_write key will have full permissions.

Furthermore, user permissions and capabilities will define the access. If an instructor is provided with an API key, they will not be able to preform updates on resources they don't have access to outside of their capabilities.

Requests made to the API using cookie authentication will authenticate via the current user and provide that user with resource access based off their user roles and capabilities.

An interface (within the WordPress admin panel) will be created to manage (CRUD) API keys.

Text content properties should have "raw" and "rendereded" fields

About the Course's access period object:
opens and closes message field in the specs contain shortcodes...
This makes me thing that we should split all the properties which can contain shortcodes (or blocks?!) into raw and rendered with the second only accessible in edit context, exactly as we do with the title,content,excerpt.
What do you think?

Originally posted by @eri-trabiccolo in #6 (comment)

Student Enrollments spec: a few questions

  1. Should we allow date creation customization?

Schermata 2019-08-06 alle 16 29 36

The core api do not really provide this... as far as I can see.
Should we implement the code for the rest api only?

Shouldn't only allow for enrollments status changes?!

  1. Should we allow 'custom' status on creation?
    Does it make sense create an enrollment for a student/course with a cancelled or expired status?

Deletions should respond with 204 and no response content

The spec defines a 204 status code for all deletions and an empty response body, eg: https://gocodebox.github.io/lifterlms-rest/#tag/Courses/paths/~1courses~1{id}/delete

It looks like we're currently mimicking the behavior of the WP Core's WP_REST_Posts_Controller and responding with a 200 as well as a body containing information about the deleted post.

I'm open to adjusting this to be a 200 with the existing response body. However, in researching the spec most apis (outside of WP) do not respond with a body when deleting a resource.

Additionally, I came across this SO thread: https://stackoverflow.com/questions/6439416/deleting-a-resource-using-http-delete

This essentially says that you should never return a 404 on a deletion request. And should always return a 204 (or 200). If we were going to return a body we would not be able to return the previous content if we go with this stance. EG: If the resource never exists (and we still return the 200/4 then we wouldn't be able to pull the deleted resource's information.

I'm leaning towards switching up the current courses to be a 204 with no body. Do you agree?

Is there a good reason I'm missing why we need to return the deleted post in the request body?

The best thing I can come up with is that a client wants to "check the work of the api" against the deleted content but they shouldn't ever really need to do that (I don't think). If we tell them it's deleted that's enough. And if for some reason they need the deleted posts information they should preform a GET first, do whatever they need, and then delete.

Right?

Webhook API Signature generation / verification

"borrow" from WooCommerce:

public function generate_signature( $payload ) {
	$hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );
	return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) );
}

Source: https://github.com/woocommerce/woocommerce/blob/master/includes/class-wc-webhook.php#L470-L483

This solution will not prevent against "replay" attacks, see Stripes solution: https://stripe.com/docs/webhooks/signatures#replay-attacks

Our method can be improved to prevent against replays by adding a timestamp to the generation and payload:

public function generate_signature( $payload ) {
	$hash_algo = apply_filters( 'llms_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() );
	$ts = current_time( 'timestamp' );
	$message = $ts . '.' . $payload;
	return 'ts=' . $ts . ',v1=' . hash_hmac( $hash_algo, $message, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true );
}
X-LLMS-Webhook-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

This will ditch the base64 encoding and add a timestamp which can be extracted and used to generate an expected signature.

The signature header would something like:

Password protected posts: should we add the 'protected' sub-property?

The WP rest api adds the sub property protected (bool) to the content and excerpt properties.
The reason behind this is that those properties, when a post is password protected, are returned as empty string, so, that there's no way to discriminate between an empty content and a protected content (in view context).
I think we should add this sub property to the specs as well.
Of course it should be added to all the properties that we want to be affected by the password protection (which at the moment are only content and excerpt).

What do you think?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.