gocodebox/lifterlms

View on GitHub
includes/controllers/class.llms.controller.certificates.php

Summary

Maintainability
B
6 hrs
Test Coverage
D
69%
<?php
/**
 * LLMS_Controller_Certificates class
 *
 * @package LifterLMS/Controllers/Classes
 *
 * @since 3.18.0
 * @version 5.9.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Handles awarded user certificates.
 *
 * @since 3.18.0
 * @since 3.35.0 Sanitize `$_POST` data.
 * @since 3.37.4 Modify `llms_certificate` post type registration to allow certificate templates to be exported.
 *               When exporting a certificate template, use the `post_author` for the certificate's WP User ID.
 * @since 4.3.1 Properly use an `error` notice to display a WP_Error when trying to download a certificate.
 * @since 6.0.0 Extended from the LLMS_Abstract_Controller_User_Engagements class.
 */
class LLMS_Controller_Certificates extends LLMS_Abstract_Controller_User_Engagements {

    /**
     * Type of user engagement.
     *
     * @since 6.0.0
     *
     * @var string
     */
    protected $engagement_type = 'certificate';

    /**
     * Constructor.
     *
     * @since 3.18.0
     * @since 3.37.4 Add filter hook for `lifterlms_register_post_type_llms_certificate`.
     * @since 5.5.0 Drop usage of deprecated `lifterlms_register_post_type_llms_certificate` in favor of `lifterlms_register_post_type_certificate`.
     *
     * @return void
     */
    public function __construct() {

        parent::__construct();

        add_filter( 'lifterlms_register_post_type_certificate', array( $this, 'maybe_allow_public_query' ) );

        add_action( 'init', array( $this, 'maybe_handle_reporting_actions' ) );
        add_action( 'wp', array( $this, 'maybe_authenticate_export_generation' ) );
    }

    /**
     * Returns a translated text of the given type.
     *
     * @since 6.0.0
     *
     * @param int   $text_type One of the LLMS_Abstract_Controller_User_Engagements::TEXT_ constants.
     * @param array $variables Optional variables that are used in sprintf().
     * @return string
     */
    protected function get_text( $text_type, $variables = array() ) {

        switch ( $text_type ) {
            case self::TEXT_SYNC_AWARDED_ENGAGEMENT_INSUFFICIENT_PERMISSIONS:
                return sprintf(
                    /* translators: %1$d: awarded certificate ID */
                    __( 'Sorry, you are not allowed to edit the awarded certificate #%1$d.', 'lifterlms' ),
                    ( $variables['engagement_id'] ?? 0 )
                );
            case self::TEXT_SYNC_AWARDED_ENGAGEMENT_INVALID_TEMPLATE:
                return sprintf(
                    /* translators: %1$d: awarded certificate ID */
                    __( 'Sorry, the awarded certificate #%1$d does not have a valid certificate template.', 'lifterlms' ),
                    ( $variables['engagement_id'] ?? 0 )
                );
            case self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INSUFFICIENT_PERMISSIONS:
                return __( 'Sorry, you are not allowed to edit awarded certificates.', 'lifterlms' );
            case self::TEXT_SYNC_AWARDED_ENGAGEMENTS_INVALID_NONCE:
                return __( 'Sorry, you are not allowed to sync awarded certificates.', 'lifterlms' );
            case self::TEXT_SYNC_MISSING_AWARDED_ENGAGEMENT_ID:
                return __( 'Sorry, you need to provide a valid awarded certificate ID.', 'lifterlms' );
            case self::TEXT_SYNC_MISSING_ENGAGEMENT_TEMPLATE_ID:
                return __( 'Sorry, you need to provide a valid certificate template ID.', 'lifterlms' );
            default:
                return parent::get_text( $text_type );
        }
    }

    /**
     * Modify certificate post type registration data during a certificate template export.
     *
     * @since 3.37.4
     * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
     *
     * @link https://github.com/gocodebox/lifterlms/issues/776
     *
     * @param array $post_type_args Array of `llms_certificate` post type registration arguments.
     * @return array
     */
    public function maybe_allow_public_query( $post_type_args ) {

        if ( ! empty( $_REQUEST['_llms_cert_auth'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended

            $auth = llms_filter_input( INPUT_GET, '_llms_cert_auth', FILTER_SANITIZE_FULL_SPECIAL_CHARS );

            global $wpdb;
            $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_llms_auth_nonce' AND meta_value = %s", $auth ) ); // db call ok; no-cache ok.
            if ( $post_id && 'llms_certificate' === get_post_type( $post_id ) ) {
                $post_type_args['publicly_queryable'] = true;
            }
        }

        return $post_type_args;

    }

    /**
     * Allow cURL requests to view a certificate to be authenticated via a nonce.
     *
     * A cURL request is used to scrape the HTML and this will authenticate the scrape.
     *
     * @since 3.18.0
     * @since 3.24.0 Unknown.
     * @since 3.37.4 Use the `post_author` as the WP_User ID when exporting a certificate template.
     *
     * @return void
     */
    public function maybe_authenticate_export_generation() {

        if ( empty( $_REQUEST['_llms_cert_auth'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            return;
        }

        $post_id   = get_the_ID();
        $post_type = get_post_type( $post_id );
        if ( ! in_array( $post_type, array( 'llms_my_certificate', 'llms_certificate' ), true ) ) {
            return;
        }

        if ( get_post_meta( $post_id, '_llms_auth_nonce', true ) !== $_REQUEST['_llms_cert_auth'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            return;
        }

        $cert = new LLMS_User_Certificate( $post_id );
        $uid  = ( 'llms_certificate' === $post_type ) ? get_post_field( 'post_author', $post_id ) : $cert->get_user_id();
        wp_set_current_user( $uid );

    }

    /**
     * Handle certificate form actions
     *
     * Manages frontend actions to download and manage certificate sharing settings and reporting (admin)
     * actions to download and delete.
     *
     * The method name is a misnomer as this method handles actions on reporting screens as well as
     * on the site's frontend when actually viewing a certificate
     *
     * @since 3.18.0
     * @since 3.35.0 Sanitize `$_POST` data.
     * @since 4.5.0 Add handler for changing certificate sharing settings.
     * @since 5.9.0 Stop using deprecated `FILTER_SANITIZE_STRING`.
     *
     * @return void
     */
    public function maybe_handle_reporting_actions() {

        if ( ! llms_verify_nonce( '_llms_cert_actions_nonce', 'llms-cert-actions' ) ) {
            return;
        }

        $cert_id = absint( llms_filter_input( INPUT_POST, 'certificate_id', FILTER_SANITIZE_NUMBER_INT ) );
        if ( isset( $_POST['llms_generate_cert'] ) ) {
            $this->download( $cert_id );
        } elseif ( isset( $_POST['llms_delete_cert'] ) ) {
            $this->delete( $cert_id );
        } elseif ( isset( $_POST['llms_enable_cert_sharing'] ) ) {
            $this->change_sharing_settings( $cert_id, (bool) $_POST['llms_enable_cert_sharing'] );
        }

    }

    /**
     * Change shareable settings of a certificate.
     *
     * @since 4.5.0
     *
     * @param int  $cert_id    WP Post ID of the llms_my_certificate.
     * @param bool $is_allowed Allow share the certificate or not.
     * @return WP_Error|boolean Returns `true` on success and `false` on failure or an error object when the user does not have sufficient privileges.
     */
    private function change_sharing_settings( $cert_id, $is_allowed ) {

        $cert = new LLMS_User_Certificate( $cert_id );

        if ( ! $cert->can_user_manage() ) {
            return new WP_Error( 'insufficient-permissions', __( 'You are not allowed to manage this certificate.', 'lifterlms' ) );
        }

        return $cert->set( 'allow_sharing', $is_allowed ? 'yes' : 'no' );

    }

    /**
     * Download a Certificate.
     *
     * Generates an HTML export of the certificate from the "Download" button
     * on the View Certificate front end & on reporting backend for admins.
     *
     * @since 3.18.0
     * @since 4.3.1 Properly use an `error` notice to display a WP_Error.
     *
     * @return void
     */
    private function download( $cert_id ) {

        $filepath = llms()->certificates()->get_export( $cert_id );
        if ( is_wp_error( $filepath ) ) {
            // @todo Need to handle errors differently on admin panel.
            return llms_add_notice( $filepath->get_error_message(), 'error' );
        }

        header( 'Content-Description: File Transfer' );
        header( 'Content-Type: application/octet-stream' );
        header( 'Content-Disposition: attachment; filename="' . basename( $filepath ) . '"' );

        readfile( $filepath ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_readfile

        // Delete file after download.
        ignore_user_abort( true );
        wp_delete_file( $filepath );
        exit;
    }

}

return new LLMS_Controller_Certificates();