gocodebox/lifterlms-rest

View on GitHub
includes/class-llms-rest-webhooks.php

Summary

Maintainability
C
1 day
Test Coverage
A
91%
<?php
/**
 * CRUD Webhooks
 *
 * @package LifterLMS_REST/Classes
 *
 * @since 1.0.0-beta.1
 * @version 1.0.0-beta.27
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_REST_Webhooks class
 *
 * @since 1.0.0-beta.1
 * @since 1.0.0-beta.3 Fix formatting error on the default webhook name string.
 * @since 1.0.0-beta.6 "access plan" not "access_plan" for human reading.
 * @since 1.0.0-beta.11 `'save_post_*'` hooks number of arguments reduced to two.
 */
class LLMS_REST_Webhooks extends LLMS_REST_Database_Resource {

    use LLMS_REST_Trait_Singleton;

    /**
     * Resource Name/ID key.
     *
     * @var string
     */
    protected $id = 'webhook';

    /**
     * Resource Model classname.
     *
     * @var string
     */
    protected $model = 'LLMS_REST_Webhook';

    /**
     * Default column values (for creating).
     *
     * @var array
     */
    protected $default_column_values = array();

    /**
     * Array of read only column names.
     *
     * @var array
     */
    protected $read_only_columns = array(
        'id',
    );

    /**
     * Array of required columns (for creating).
     *
     * @var array
     */
    protected $required_columns = array(
        'topic',
        'delivery_url',
    );

    /**
     * Create a new API Key
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column.
     *
     * @param array $data Associative array of data to set to a key's properties.
     * @return WP_Error|LLMS_REST_Webhook
     */
    public function create( $data ) {

        $data = $this->create_prepare( $data );
        if ( is_wp_error( $data ) ) {
            return $data;
        }

        // Can't set this property during creation.
        unset( $data['failure_count'] );

        return $this->save( new $this->model(), $data );

    }

    /**
     * Retrieve the base admin url for managing API keys.
     *
     * @since 1.0.0-beta.1
     *
     * @return string
     */
    public function get_admin_url() {
        return add_query_arg(
            array(
                'page'    => 'llms-settings',
                'tab'     => 'rest-api',
                'section' => 'webhooks',
            ),
            admin_url( 'admin.php' )
        );
    }

    /**
     * Get default column values.
     *
     * Overrides parent to dynamically set the class variable since several defaults are generated through functions.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.3 Fix formatting error.
     * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column.
     * @since 1.0.0-beta.27 Don't use deprecated `strftime` function.
     *
     * @return array
     */
    public function get_default_column_values() {

        $this->default_column_values = array(
            'secret'        => wp_generate_password( 50, true, true ),
            'status'        => 'disabled',
            'failure_count' => 0,
            'user_id'       => get_current_user_id(),
            'name'          => sprintf(
                // Translators: %1$s = created date.
                __( 'Webhook created on %1$s', 'lifterlms' ),
                wp_date(
                    sprintf(
                        // Translators: %1$s Date format as specified in the WordPress General Settings, %1$s Time format as specified in the WordPress General Settings.
                        _x( '%1$s @ %2$s', 'Webhook created on date parsed by wp_date', 'lifterlms' ),
                        get_option( 'date_format' ),
                        get_option( 'time_format' )
                    )
                )
            ),
        );

        return parent::get_default_column_values();

    }

    /**
     * Retrieve the translated resource name.
     *
     * @since 1.0.0-beta.1
     *
     * @return string
     */
    protected function get_i18n_name() {
        return __( 'Webhook', 'lifterlms' );
    }

    /**
     * Retrieves a list of webhook statuses.
     *
     * @since 1.0.0-beta.1
     *
     * @return array
     */
    public function get_statuses() {

        /**
         * Filter the available webhook statuses.
         *
         * @since 1.0.0-beta.1
         *
         * @param array $statuses Array of statuses.
         */
        return apply_filters(
            'llms_rest_webhook_statuses',
            array(
                'active'   => __( 'Active', 'lifterlms' ),
                'paused'   => __( 'Paused', 'lifterlms' ),
                'disabled' => __( 'Disabled', 'lifterlms' ),
            )
        );

    }

    /**
     * Retrieves a list of webhook topics.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.6 Fix translated access plans typo.
     * @since 1.0.0-beta.18 Remove access_plan.restored topic - access plan post type doesn't support trashing.
     *
     * @return array
     */
    public function get_topics() {

        /**
         * Filter the available webhook topics.
         *
         * @since 1.0.0-beta.1
         *
         * @param array $topics Array of topics.
         */
        return apply_filters(
            'llms_rest_webhook_topics',
            array(
                'course.created'      => __( 'Course created', 'lifterlms' ),
                'course.updated'      => __( 'Course updated', 'lifterlms' ),
                'course.deleted'      => __( 'Course deleted', 'lifterlms' ),
                'course.restored'     => __( 'Course restored', 'lifterlms' ),
                'section.created'     => __( 'Section created', 'lifterlms' ),
                'section.updated'     => __( 'Section updated', 'lifterlms' ),
                'section.deleted'     => __( 'Section deleted', 'lifterlms' ),
                'lesson.created'      => __( 'Lesson created', 'lifterlms' ),
                'lesson.updated'      => __( 'Lesson updated', 'lifterlms' ),
                'lesson.deleted'      => __( 'Lesson deleted', 'lifterlms' ),
                'lesson.restored'     => __( 'Lesson restored', 'lifterlms' ),
                'membership.created'  => __( 'Membership created', 'lifterlms' ),
                'membership.updated'  => __( 'Membership updated', 'lifterlms' ),
                'membership.deleted'  => __( 'Membership deleted', 'lifterlms' ),
                'membership.restored' => __( 'Membership restored', 'lifterlms' ),
                'access_plan.created' => __( 'Access Plan created', 'lifterlms' ),
                'access_plan.updated' => __( 'Access Plan updated', 'lifterlms' ),
                'access_plan.deleted' => __( 'Access Plan deleted', 'lifterlms' ),
                'order.created'       => __( 'Order created', 'lifterlms' ),
                'order.updated'       => __( 'Order updated', 'lifterlms' ),
                'order.deleted'       => __( 'Order deleted', 'lifterlms' ),
                'order.restored'      => __( 'Order restored', 'lifterlms' ),
                'transaction.created' => __( 'Transaction created', 'lifterlms' ),
                'transaction.updated' => __( 'Transaction updated', 'lifterlms' ),
                'transaction.deleted' => __( 'Transaction deleted', 'lifterlms' ),
                'student.created'     => __( 'Student created', 'lifterlms' ),
                'student.updated'     => __( 'Student updated', 'lifterlms' ),
                'student.deleted'     => __( 'Student deleted', 'lifterlms' ),
                'enrollment.created'  => __( 'Enrollment created', 'lifterlms' ),
                'enrollment.updated'  => __( 'Enrollment updated', 'lifterlms' ),
                'enrollment.deleted'  => __( 'Enrollment deleted', 'lifterlms' ),
                'progress.updated'    => __( 'Progress updated', 'lifterlms' ),
                'progress.deleted'    => __( 'Progress deleted', 'lifterlms' ),
                'instructor.created'  => __( 'Instructor created', 'lifterlms' ),
                'instructor.updated'  => __( 'Instructor updated', 'lifterlms' ),
                'instructor.deleted'  => __( 'Instructor deleted', 'lifterlms' ),
                'action'              => __( 'Action', 'lifterlms' ),
            )
        );

    }

    /**
     * Retrieve a list of hooks for each topic.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.11 `'save_post_*'` hooks number of arguments reduced to two.
     * @since 1.0.0-beta.23 Replaced deprecated `llms_user_removed_from_membership_level` action hook with `llms_user_removed_from_membership`.
     *
     * @return array
     */
    public function get_hooks() {

        $hooks = array(

            // Courses.
            'course.created'      => array(
                'save_post_course' => 2,
            ),
            'course.updated'      => array(
                'edit_post_course' => 2,
            ),
            'course.deleted'      => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),
            'course.restored'     => array(
                'untrashed_post' => 1,
            ),

            // Sections.
            'section.created'     => array(
                'save_post_section' => 2,
            ),
            'section.updated'     => array(
                'edit_post_section' => 2,
            ),
            'section.deleted'     => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),

            // Lessons.
            'lesson.created'      => array(
                'save_post_lesson' => 2,
            ),
            'lesson.updated'      => array(
                'edit_post_lesson' => 2,
            ),
            'lesson.deleted'      => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),
            'lesson.restored'     => array(
                'untrashed_post' => 1,
            ),

            // Memberships.
            'membership.created'  => array(
                'save_post_llms_membership' => 2,
            ),
            'membership.updated'  => array(
                'edit_post_llms_membership' => 2,
            ),
            'membership.deleted'  => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),
            'membership.restored' => array(
                'untrashed_post' => 1,
            ),

            // Access Plans.
            'access_plan.created' => array(
                'save_post_llms_access_plan' => 2,
            ),
            'access_plan.updated' => array(
                'edit_post_llms_access_plan' => 2,
            ),
            'access_plan.deleted' => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),

            // Orders.
            'order.created'       => array(
                'save_post_llms_order' => 2,
            ),
            'order.updated'       => array(
                'edit_post_llms_order' => 2,
            ),
            'order.deleted'       => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),

            // Transactions.
            'transaction.created' => array(
                'save_post_llms_transaction' => 2,
            ),
            'transaction.updated' => array(
                'edit_post_llms_transaction' => 2,
            ),
            'transaction.deleted' => array(
                'wp_trash_post' => 1,
                'delete_post'   => 1,
            ),

            // Students.
            'student.created'     => array(
                'user_register'             => 1,
                'lifterlms_user_registered' => 1,
            ),
            'student.updated'     => array(
                'profile_update'         => 1,
                'lifterlms_user_updated' => 1,
            ),
            'student.deleted'     => array(
                'delete_user' => 1,
            ),

            // Instructors.
            'instructor.created'  => array(
                'user_register' => 1,
            ),
            'instructor.updated'  => array(
                'profile_update' => 1,
            ),
            'instructor.deleted'  => array(
                'delete_user' => 1,
            ),

            'enrollment.created'  => array(
                'llms_user_course_enrollment_created'     => 2,
                'llms_user_membership_enrollment_created' => 2,
            ),
            'enrollment.updated'  => array(
                'llms_user_course_enrollment_updated'     => 2,
                'llms_user_membership_enrollment_updated' => 2,
                'llms_user_removed_from_course'           => 2,
                'llms_user_removed_from_membership'       => 2,
            ),
            'enrollment.deleted'  => array(
                'llms_user_enrollment_deleted' => 2,
            ),

            'progress.updated'    => array(
                'llms_mark_complete'   => 2,
                'llms_mark_incomplete' => 2,
            ),

        );

        return apply_filters( 'llms_rest_webhooks_get_hooks', $hooks );

    }

    /**
     * Retrieve an array of supported post types used as resources for webhooks.
     *
     * @since 1.0.0-beta.1
     *
     * @return string[]
     */
    public function get_post_type_resources() {

        /**
         * Filter the list of supported post types used as resources for webhooks.
         *
         * @param string[] $post_types Array of post type names.
         */
        return apply_filters(
            'llms_rest_get_post_type_resources',
            array(
                'course',
                'section',
                'lesson',
                'llms_membership',
                'llms_access_plan',
                'llms_order',
                'llms_transaction',
            )
        );

    }

    /**
     * Validate data supplied for creating/updating a key.
     *
     * @since 1.0.0-beta.1
     *
     * @param array $data Associative array of data to set to a key's properties.
     * @return WP_Error|true When data is invalid will return a WP_Error with information about the invalid properties,
     *                            otherwise `true` denoting data is valid.
     */
    protected function is_data_valid( $data ) {

        // Validate Status.
        if ( isset( $data['status'] ) && ! in_array( $data['status'], array_keys( $this->get_statuses() ), true ) ) {
            // Translators: %s = Invalid permission string.
            return new WP_Error( 'llms_rest_webhook_invalid_status', sprintf( __( '"%s" is not a valid status.', 'lifterlms' ), $data['status'] ) );
        }

        // Validate Topics.
        if ( isset( $data['topic'] ) && ! $this->is_topic_valid( $data['topic'] ) ) {
            // Translators: %s = Invalid permission string.
            return new WP_Error( 'llms_rest_webhook_invalid_topic', sprintf( __( '"%s" is not a valid topic.', 'lifterlms' ), $data['topic'] ) );
        }

        // Prevent empty / blank values being passed.
        foreach ( array( 'name', 'delivery_url' ) as $key ) {
            if ( isset( $data[ $key ] ) && empty( $data[ $key ] ) ) {
                // Translators: %s = field name.
                return new WP_Error( 'llms_rest_webhook_invalid_' . $key, sprintf( __( '"%s" is required.', 'lifterlms' ), $key ) );
            }
        }

        return true;

    }

    /**
     * Determine if a given topic is valid.
     *
     * @since 1.0.0-beta.1
     *
     * @param string $topic Topic.
     * @return bool
     */
    public function is_topic_valid( $topic ) {

        $split = explode( '.', $topic );

        if ( 'action' === $split[0] && ! empty( $split[1] ) ) {
            return true;
        } elseif ( in_array( $topic, array_keys( $this->get_topics() ), true ) && 'action' !== $topic ) {
            return true;
        }

        return false;

    }

    /**
     * Load webhooks
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.16
     *
     * @return int Number of hooks loaded.
     */
    public function load() {

        /**
         * Limit the number of webhooks that are loaded. By default all webhooks are loaded.
         *
         * @since 1.0.0-beta.1
         * @since 1.0.0-beta.16 When retrieving the webhooks, instantiate the webhooks query passing `no_found_rows` arg as `true`,
         *                     to improve performance (no pagination is needed).
         * @param int $limit Number of webhooks to load. Default `null` loads all webhooks.
         */
        $limit = apply_filters( 'llms_load_webhooks_limit', null );

        $hooks = new LLMS_REST_Webhooks_Query(
            array(
                'status'        => 'active',
                'per_page'      => $limit ? $limit : 999,
                'no_found_rows' => true,
            )
        );

        $loaded = 0;
        foreach ( $hooks->get_webhooks() as $hook ) {
            $hook->enqueue();
            $loaded++;
        }

        return $loaded;

    }

    /**
     * Persist data.
     *
     * This method assumes the supplied data has already been validated and sanitized.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_REST_Webhook $obj Webhook object.
     * @param array             $data Associative array of data to persist.
     * @return obj
     */
    protected function save( $obj, $data ) {

        if ( isset( $data['delivery_url'] ) && ( ! $obj->exists() || $obj->exists() && $data['delivery_url'] !== $obj->get( 'delivery_url' ) ) ) {
            $obj->set( 'delivery_url', $data['delivery_url'] );
            $ping = $obj->ping();
            if ( is_wp_error( $ping ) ) {
                return $ping;
            }
        }

        $obj->setup( $data )->save();
        return $obj;

    }

    /**
     * Prepare data for an update.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.17 Remove reference to 'pending_delivery' (unused) column.
     *
     * @param array $data Associative array of data to set to a resources properties.
     * @return LLMS_REST_Webhook|WP_Error
     */
    protected function update_prepare( $data ) {

        $url = isset( $data['delivery_url'] );

        // Merge in (some) default values.
        $defaults = $this->get_default_column_values();
        unset( $defaults['failure_count'] );
        $data = wp_parse_args( array_filter( $data ), $defaults );

        // URL was supplied but empty so add it back in to get caught by validation.
        if ( $url && ! isset( $data['delivery_url'] ) ) {
            $data['delivery_url'] = '';
        }

        // Validate via default parent methods.
        $data = parent::update_prepare( $data );

        if ( is_wp_error( $data ) ) {
            return $data;
        }

        // Add updated date.
        $data['updated'] = llms_current_time( 'mysql' );

        return $data;

    }

}