LLMS_Certificates
Main LifterLMS Certificates “factory”
Description Description
Handles certificate generation and exports.
See also See also
- llms()->certificates()
Source Source
File: includes/class.llms.certificates.php
class LLMS_Certificates { use LLMS_Trait_Singleton, LLMS_Trait_Award_Default_Images; /** * The ID for the award type. * * Used by {@see LLMS_Trait_Award_Default_Images}. * * @var string */ protected $award_type = 'certificate'; /** * Array of Certificate types. * * @var array */ public $certs = array(); /** * Array of local hosts * * @var string[] */ private $export_local_hosts; /** * Array of hosts from which stylesheets won't be retrieved during the export * * @var string[] */ private $export_blocked_stylesheet_hosts; /** * Array of hosts from which images won't be retrieved during the export * * @var string[] */ private $export_blocked_image_hosts; /** * Constructor * * @since 1.0.0 * * @return void */ private function __construct() { $this->init(); } /** * Initialize class. * * @since 1.0.0 * @since 4.21.0 Define useful class properties used when exporting. * @since 6.0.0 Removed loading of class files that don't instantiate their class in favor of autoloading. * * @return void */ public function init() { $this->certs['LLMS_Certificate_User'] = isset( $this->certs['LLMS_Certificate_User'] ) ? $this->certs['LLMS_Certificate_User'] : include_once 'certificates/class.llms.certificate.user.php'; $this->export_local_hosts = array_unique( array( wp_parse_url( get_home_url(), PHP_URL_HOST ), wp_parse_url( get_site_url(), PHP_URL_HOST ), ) ); $this->export_blocked_stylesheet_hosts = array_unique( /** * Filters the blocked hosts for stylesheets in certificate exports * * @since 4.21.0 * * @param string[] Array of hosts to block. */ apply_filters( 'llms_certificate_export_blocked_stylesheet_hosts', array( 'fonts.googleapis.com', ) ) ); $this->export_blocked_image_hosts = array_unique( /** * Filters the blocked hosts for images in certificate exports * * @since 4.21.0 * * @param string[] Array of hosts to block. */ apply_filters( 'llms_certificate_export_blocked_image_hosts', array() ) ); } /** * Award a certificate to a user. * * Calls trigger method passing arguments * * @since 1.0.0 * @deprecated 6.0.0 `LLMS_Certificates::trigger_engagement()` is deprecated in favor of `LLMS_Engagement_Handler::handle_certificate()`. * * @param int $person_id WP_User ID. * @param int $certificate_id WP_Post ID of the certificate template. * @param int $related_post_id WP_Post ID of the related post, for example a lesson id. * @return void */ public function trigger_engagement( $person_id, $certificate_id, $related_post_id ) { _deprecated_function( 'LLMS_Certificates::trigger_engagement()', '6.0.0', 'LLMS_Engagement_Handler::handle_certificate()' ); LLMS_Engagement_Handler::handle_certificate( array( $person_id, $certificate_id, $related_post_id, null ) ); } /** * Generate a downloadable HTML file for a certificate * * @since 3.18.0 * @since 3.37.3 Added action `llms_certificate_generate_export`. * @since 4.3.1 Introduce `llms_certificate_error` WP_Error code. * * @param string $filepath Full path for the created file. * @param int $certificate_id WP_Post ID of the earned certificate. * @return mixed WP_Error or full path to the generated export. */ private function generate_export( $filepath, $certificate_id ) { $html = $this->get_export_html( $certificate_id ); if ( is_wp_error( $html ) ) { return $html; } /** * Run actions prior to certificate export generation. * * @param string $filepath Full path where the created file will be stored. Passed as a reference. * @param string $html Certificate HTML. Passed as a reference. * @param int $certificate_id WP_Post ID of the earned certificate. */ do_action_ref_array( 'llms_certificate_generate_export', array( &$filepath, &$html, $certificate_id ) ); $file = fopen( $filepath, 'w' ); if ( false === $file ) { return new WP_Error( 'llms_certificate_error', __( 'Unable to open export file (HTML certificate) for writing.', 'lifterlms' ) ); } if ( false === fwrite( $file, $html ) ) { return new WP_Error( 'llms_certificate_error', __( 'Unable to write to export file (HTML certificate).', 'lifterlms' ) ); } fclose( $file ); return $filepath; } /** * Retrieve an existing or generate a downloadable HTML file for a certificate * * @since 3.18.0 * @since 6.0.0 Use the certificate post title in favor of the deprecated meta value `_llms_certificate_title`. * * @param int $certificate_id WP Post ID of the earned certificate. * @param bool $use_cache If true will check for existence of a cached version of the file first. * @return mixed WP_Error or full path to the generated export. */ public function get_export( $certificate_id, $use_cache = false ) { if ( $use_cache ) { $cached = get_post_meta( $certificate_id, '_llms_export_filepath', true ); if ( $cached && file_exists( $cached ) ) { return $cached; } } $cert = new LLMS_User_Certificate( $certificate_id ); // Translators: %1$s = url-safe certificate title, %2$s = random alpha-numeric characters for filename obscurity. $filename = sanitize_title( sprintf( esc_attr_x( 'certificate-%1$s-%2$s', 'certificate download filename', 'lifterlms' ), $cert->get( 'title' ), wp_generate_password( 12, false, false ) ) ); $filename .= '.html'; $filepath = LLMS_TMP_DIR . $filename; // Generate the file. $filepath = $this->generate_export( $filepath, $certificate_id ); if ( $use_cache && ! is_wp_error( $filepath ) ) { update_post_meta( $certificate_id, '_llms_export_filepath', $filepath ); } return $filepath; } /** * Retrieves the HTML of a certificate which can be used to create an exportable download * * @since 3.18.0 * @since 3.24.3 Unknown. * @since 3.37.3 Refactored method into multiple functions. * @since 4.3.1 If `$this->scrape_certificate()` generates a `WP_Error` early return it. * @since 4.8.0 Remove redundant check for the presence of `DOMDocument`. * * @param int $certificate_id WP_Post ID of the earned certificate. * @return WP_Error|string HTML of the certificate on success, otherwise an error object. */ private function get_export_html( $certificate_id ) { // Retrieve the raw HTML of the page. $html = $this->scrape_certificate( $certificate_id ); if ( is_wp_error( $html ) ) { return $html; } // Modify the DOM. $html = $this->modify_dom( $html ); /** * Modify the HTML of a certificate export. * * @since 3.18.0 * * @param string $html HTML to be exported. * @param int $certificate_id WP_Post ID of the earned certificate. */ return apply_filters( 'llms_get_certificate_export_html', $html, $certificate_id ); } /** * Create a unique slug for earned certificates. * * When relying only on `wp_unique_post_slug()`, predictable URLs are created for earned certificates, * such as "certificate-of-completion-1", "certificate-of-completion-2", etc... this method creates * an obtuse and randomized suffix and appends it to the post slug. * * The unique suffix will be a randomized string at least 3 characters long and made up of lowercase letters and numbers. * * When ensuring uniqueness of the generated suffix, the length of the string will be increased by one for every 5 * encountered collisions. * * @since 6.0.0 * * @param string $title The title of the certificate being created. * @return string */ public function get_unique_slug( $title ) { $title = sanitize_title( $title ) . '-'; /** * Filters the minimum length of the suffix used to create a unique earned certificate slug. * * @since 6.0.0 * * @param int $min_strlen The minimum desired suffix string length. */ $min_strlen = apply_filters( 'llms_certificate_unique_slug_suffix_min_length', 3 ); $i = 0; do { $length = $min_strlen + floor( $i / 5 ); $slug = $title . strtolower( wp_generate_password( absint( $length ), false ) ); $i++; } while ( wp_unique_post_slug( $slug, 0, 'publish', 'llms_my_certificate', 0 ) !== $slug ); return $slug; } /** * Modify the HTML using DOMDocument. * * Preparations include: * * 1. Removing all `script` tags. * 2. Removes the WP Admin Bar. * 3. Converting all stylesheets into inline `style` tags. * 4. Removes all non stylesheet `link` tags. * 5. Converts `img` tags into data uris. * 6. Adds inline CSS to hide anything hidden in a print view. * * @since 3.37.3 * @since 3.38.1 Use `LLMS_Mime_Type_Extractor::from_file_path()` in place of `mime_content_type()` to avoid issues with PHP installs that do not support it. * @since 4.8.0 Use `llms_get_dom_document()` in favor of loading `DOMDocument` directly. * @since 4.21.0 Allow external assets (e.g. images/stylesheets from CDN) to be embedded/inlined. * Also, remove the WP Admin Bar earlier. * Move the links and images modification in specific methods. * * @param string $html Certificate HTML. * @return string */ private function modify_dom( $html ) { $dom = llms_get_dom_document( $html ); if ( is_wp_error( $dom ) ) { return $html; } // Don't throw or log warnings. $libxml_state = libxml_use_internal_errors( true ); // Remove all <scripts>. $scripts = $dom->getElementsByTagName( 'script' ); while ( $scripts && $scripts->length ) { $scripts->item( 0 )->parentNode->removeChild( $scripts->item( 0 ) ); } // Remove the admin bar (if found). $admin_bar = $dom->getElementById( 'wpadminbar' ); if ( $admin_bar ) { $admin_bar->parentNode->removeChild( $admin_bar ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } $this->modify_dom_links( $dom ); $this->modify_dom_images( $dom ); // Hide print stuff (this is faster than traversing the dom to remove the element). $header = $dom->getElementsByTagName( 'head' )->item( 0 ); $header->appendChild( $dom->createELement( 'style', '.no-print { display: none !important; }' ) ); $html = $dom->saveHTML(); // Handle errors. libxml_clear_errors(); // Restore. libxml_use_internal_errors( $libxml_state ); return $html; } /** * Modify head's <link>s of the DOMDocument. * * @since 4.21.0 * * @param DOMDocument $dom The DOMDocument containing the certificate. * @return void */ private function modify_dom_links( $dom ) { // Get all <links>. $links = $dom->getElementsByTagName( 'link' ); $to_replace = array(); // Inline stylesheets. foreach ( $links as $link ) { // Only proceed for stylesheets. if ( 'stylesheet' !== $link->getAttribute( 'rel' ) ) { continue; } $raw = $this->get_stylesheet_raw( $link->getAttribute( 'href' ) ); if ( empty( $raw ) ) { continue; } // Add it to be inlined late. $tag = $dom->createElement( 'style', $raw ); $to_replace[] = array( 'old' => $link, 'new' => $tag, ); } // Do replacements, ensures cascade order is retained. foreach ( $to_replace as $replacement ) { $replacement['old']->parentNode->replaceChild( $replacement['new'], $replacement['old'] ); } // Remove all remaining non stylesheet <links>. $links = $dom->getElementsByTagName( 'link' ); while ( $links && $links->length ) { $links->item( 0 )->parentNode->removeChild( $links->item( 0 ) ); } } /** * Get stylesheet raw content given its URL * * @since 4.21.0 * * @param string $stylesheet_href The stylesheet href. * @param boolean $allowed_only Optional. Get only stylesheet whose host is not in the `export_blocked_stylesheet_hosts` list. * @return string|false */ private function get_stylesheet_raw( $stylesheet_href, $allowed_only = true ) { $href_host = wp_parse_url( $stylesheet_href, PHP_URL_HOST ); // Only include stylesheets from non blocked hosts. if ( $allowed_only && in_array( $href_host, $this->export_blocked_stylesheet_hosts, true ) ) { return false; } // Get the actual CSS. if ( in_array( $href_host, $this->export_local_hosts, true ) ) { // Is local? $raw = file_get_contents( untrailingslashit( ABSPATH ) . wp_parse_url( $stylesheet_href, PHP_URL_PATH ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- getting a local file. } else { $response = wp_remote_get( $stylesheet_href ); $raw = wp_remote_retrieve_body( $response ); } return $raw; } /** * Modify images of the DOMDocument * * @since 4.21.0 * * @param DOMDocument $dom The DOMDocument containing the certificate. * @return void */ private function modify_dom_images( $dom ) { $images = $dom->getElementsByTagName( 'img' ); $to_remove = array(); // Convert images to data uris. foreach ( $images as $img ) { $img_data_type = $this->get_image_data_and_type( $img->getAttribute( 'src' ) ); if ( empty( $img_data_type['data'] ) || empty( $img_data_type['type'] ) ) { $to_remove[] = $img; // Save images to remove: removing them directly here will alter the collection iteration (skip). continue; } $data = base64_encode( $img_data_type['data'] );// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode $img->setAttribute( 'src', 'data:' . $img_data_type['type'] . ';base64,' . $data ); // Remove srcset and sizes attributes. $img->removeAttribute( 'sizes' ); $img->removeAttribute( 'srcset' ); // Remove useless loading attribute. $img->removeAttribute( 'loading' ); } foreach ( $to_remove as $img ) { $img->parentNode->removeChild( $img ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } } /** * Get image data and type given its source URL * * @since 4.21.0 * * @param string $image_src The image src. * @param boolean $allowed_only Optional. Get only images whose host is not in the `export_blocked_image_hosts` list. * @return array|false */ private function get_image_data_and_type( $image_src, $allowed_only = true ) { $src_host = wp_parse_url( $image_src, PHP_URL_HOST ); // Only include images from non blocked hosts. if ( $allowed_only && in_array( $src_host, $this->export_blocked_image_hosts, true ) ) { return false; } if ( in_array( $src_host, $this->export_local_hosts, true ) ) { // Is local? $imgpath = untrailingslashit( ABSPATH ) . wp_parse_url( $image_src, PHP_URL_PATH ); $data = file_get_contents( $imgpath ); // phpcs:ignore WordPress.WP.AlternativeFunctions -- getting a local file. $type = LLMS_Mime_Type_Extractor::from_file_path( $imgpath ); } else { $response = wp_remote_get( $image_src ); $data = wp_remote_retrieve_body( $response ); $type = wp_remote_retrieve_header( $response, 'content-type' ); } return compact( 'data', 'type' ); } /** * Scrape a LifterLMS Certificate permalink and return the generated HTML. * * @since 3.37.3 * * @param int $certificate_id WP_Post ID of the earned certificate (an "llms_my_certificate" post). * @return WP_Error|string WP_Error on failure or the full page HTML on success. */ private function scrape_certificate( $certificate_id ) { // Create a nonce for getting the export HTML. $token = wp_generate_password( 32, false ); update_post_meta( $certificate_id, '_llms_auth_nonce', $token ); /** * Modify the URL used to scrape the HTML of a certificate in preparation for a certificate export. * * @since 3.18.0 * * @param string $url Certificate permalink with a one-time use authorization token appended as a query string variable. * @param int $certificate_id WP_Post ID of the earned certificate (an "llms_my_certificate" post). */ $url = apply_filters( 'llms_get_certificate_export_html_url', add_query_arg( '_llms_cert_auth', $token, get_permalink( $certificate_id ) ), $certificate_id ); // Perform the request. $req = wp_safe_remote_get( $url, array( 'sslverify' => false, ) ); // Delete the token after the request. delete_post_meta( $certificate_id, '_llms_auth_nonce', $token ); // Error. if ( is_wp_error( $req ) ) { return $req; } return wp_remote_retrieve_body( $req ); } }
Expand full source code Collapse full source code View on GitHub
Methods Methods
- __construct — Constructor
- generate_export — Generate a downloadable HTML file for a certificate
- get_export — Retrieve an existing or generate a downloadable HTML file for a certificate
- get_export_html — Retrieves the HTML of a certificate which can be used to create an exportable download
- get_image_data_and_type — Get image data and type given its source URL
- get_stylesheet_raw — Get stylesheet raw content given its URL
- get_unique_slug — Create a unique slug for earned certificates.
- init — Initialize class.
- instance — Instance singleton
- modify_dom — Modify the HTML using DOMDocument.
- modify_dom_images — Modify images of the DOMDocument
- modify_dom_links — Modify head's s of the DOMDocument.
- scrape_certificate — Scrape a LifterLMS Certificate permalink and return the generated HTML.
- trigger_engagement — Award a certificate to a user. — deprecated
Changelog Changelog
Version | Description |
---|---|
6.0.0 | Changes:
|
5.3.0 | Replace singleton code with LLMS_Trait_Singleton . |
4.3.1 | When generating the certificate to export, if $this->scrape_certificate() generates a WP_Error early, return it to avoid fatal errors. |
4.21.0 | Added new class properties: $export_local_hosts , $export_blocked_stylesheet_hosts , and $export_blocked_image_hosts . |
3.38.1 | Use LLMS_Mime_Type_Extractor::from_file_path() when retrieving the certificate's images mime types during html export. |
3.37.3 | Refactored get_export_html() method. Added an action llms_certificate_generate_export to allow modification of certificate exports before being stored on the server. |
3.30.3 | Explicitly define class properties. |
1.0.0 | Introduced. |