From ce85e197fa577b2097c8062fcce0d8dd8969c103 Mon Sep 17 00:00:00 2001
From: Alex Kazhukhouski
Date: Mon, 10 Mar 2025 16:22:24 +0100
Subject: [PATCH 1/3] POC Previews Plugin
---
plugins/hwp-previews/README.md | 100 +++
plugins/hwp-previews/composer.json | 16 +
plugins/hwp-previews/composer.lock | 84 ++
plugins/hwp-previews/hwp-previews.php | 38 +
plugins/hwp-previews/src/Plugin.php | 289 +++++++
.../src/Post/Data/Post_Data_Model.php | 29 +
.../Post_Parent_Manager_Interface.php | 13 +
.../src/Post/Parent/Post_Parent_Manager.php | 35 +
.../Contracts/Post_Slug_Manager_Interface.php | 15 +
.../Post_Slug_Repository_Interface.php | 11 +
.../src/Post/Slug/Post_Slug_Manager.php | 72 ++
.../src/Post/Slug/Post_Slug_Repository.php | 23 +
.../Post_Statuses_Config_Interface.php | 13 +
.../src/Post/Status/Post_Statuses_Config.php | 34 +
.../Contracts/Post_Types_Config_Interface.php | 19 +
.../src/Post/Type/Post_Types_Config.php | 56 ++
.../Preview_Parameter_Names_Model.php | 47 ++
.../Preview_Query_Argument_Interface.php | 12 +
.../Preview_Template_Resolver_Interface.php | 13 +
.../Template/Preview_Template_Resolver.php | 43 +
.../Preview_Parameter_Builder_Interface.php | 13 +
.../Preview_URL_Generator_Interface.php | 13 +
.../Preview/URL/Preview_Parameter_Builder.php | 72 ++
.../src/Preview/URL/Preview_URL_Generator.php | 48 ++
.../Settings/Headless_Preview_Settings.php | 762 ++++++++++++++++++
plugins/hwp-previews/src/Shared/Model.php | 24 +
.../Auth/Contracts/Token_Auth_Interface.php | 11 +
.../src/Token/Auth/Token_Auth.php | 29 +
.../Contracts/Token_Extractor_Interface.php | 11 +
.../src/Token/Extractor/Token_Extractor.php | 23 +
.../Contracts/Token_Generator_Interface.php | 11 +
.../src/Token/Generator/Token_Generator.php | 24 +
.../Contracts/Token_Manager_Interface.php | 29 +
.../src/Token/Manager/JWT_Token_Manager.php | 96 +++
.../Token_REST_Controller_Interface.php | 11 +
.../src/Token/REST/Token_REST_Controller.php | 68 ++
.../Contracts/Token_Verifier_Interface.php | 13 +
.../src/Token/Verifier/Token_Verifier.php | 33 +
.../hwp-previews/templates/hwp-preview.php | 41 +
plugins/hwp-previews/vendor/autoload.php | 25 +
.../vendor/composer/ClassLoader.php | 579 +++++++++++++
.../vendor/composer/InstalledVersions.php | 362 +++++++++
plugins/hwp-previews/vendor/composer/LICENSE | 21 +
.../vendor/composer/autoload_classmap.php | 10 +
.../vendor/composer/autoload_namespaces.php | 9 +
.../vendor/composer/autoload_psr4.php | 11 +
.../vendor/composer/autoload_real.php | 38 +
.../vendor/composer/autoload_static.php | 44 +
.../vendor/composer/installed.json | 72 ++
.../vendor/composer/installed.php | 32 +
.../vendor/composer/platform_check.php | 26 +
.../vendor/firebase/php-jwt/CHANGELOG.md | 198 +++++
.../vendor/firebase/php-jwt/LICENSE | 30 +
.../vendor/firebase/php-jwt/README.md | 425 ++++++++++
.../vendor/firebase/php-jwt/composer.json | 42 +
.../php-jwt/src/BeforeValidException.php | 18 +
.../firebase/php-jwt/src/CachedKeySet.php | 274 +++++++
.../firebase/php-jwt/src/ExpiredException.php | 18 +
.../vendor/firebase/php-jwt/src/JWK.php | 355 ++++++++
.../vendor/firebase/php-jwt/src/JWT.php | 667 +++++++++++++++
.../src/JWTExceptionWithPayloadInterface.php | 20 +
.../vendor/firebase/php-jwt/src/Key.php | 55 ++
.../php-jwt/src/SignatureInvalidException.php | 7 +
63 files changed, 5632 insertions(+)
create mode 100644 plugins/hwp-previews/README.md
create mode 100644 plugins/hwp-previews/composer.json
create mode 100644 plugins/hwp-previews/composer.lock
create mode 100644 plugins/hwp-previews/hwp-previews.php
create mode 100644 plugins/hwp-previews/src/Plugin.php
create mode 100644 plugins/hwp-previews/src/Post/Data/Post_Data_Model.php
create mode 100644 plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php
create mode 100644 plugins/hwp-previews/src/Post/Parent/Post_Parent_Manager.php
create mode 100644 plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Manager_Interface.php
create mode 100644 plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Repository_Interface.php
create mode 100644 plugins/hwp-previews/src/Post/Slug/Post_Slug_Manager.php
create mode 100644 plugins/hwp-previews/src/Post/Slug/Post_Slug_Repository.php
create mode 100644 plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php
create mode 100644 plugins/hwp-previews/src/Post/Status/Post_Statuses_Config.php
create mode 100644 plugins/hwp-previews/src/Post/Type/Contracts/Post_Types_Config_Interface.php
create mode 100644 plugins/hwp-previews/src/Post/Type/Post_Types_Config.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Names_Model.php
create mode 100644 plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Query_Argument_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Template_Resolver_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/Template/Preview_Template_Resolver.php
create mode 100644 plugins/hwp-previews/src/Preview/URL/Contracts/Preview_Parameter_Builder_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/URL/Contracts/Preview_URL_Generator_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/URL/Preview_Parameter_Builder.php
create mode 100644 plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
create mode 100644 plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
create mode 100644 plugins/hwp-previews/src/Shared/Model.php
create mode 100644 plugins/hwp-previews/src/Token/Auth/Contracts/Token_Auth_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/Auth/Token_Auth.php
create mode 100644 plugins/hwp-previews/src/Token/Extractor/Contracts/Token_Extractor_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/Extractor/Token_Extractor.php
create mode 100644 plugins/hwp-previews/src/Token/Generator/Contracts/Token_Generator_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/Generator/Token_Generator.php
create mode 100644 plugins/hwp-previews/src/Token/Manager/Contracts/Token_Manager_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/Manager/JWT_Token_Manager.php
create mode 100644 plugins/hwp-previews/src/Token/REST/Contracts/Token_REST_Controller_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/REST/Token_REST_Controller.php
create mode 100644 plugins/hwp-previews/src/Token/Verifier/Contracts/Token_Verifier_Interface.php
create mode 100644 plugins/hwp-previews/src/Token/Verifier/Token_Verifier.php
create mode 100644 plugins/hwp-previews/templates/hwp-preview.php
create mode 100644 plugins/hwp-previews/vendor/autoload.php
create mode 100644 plugins/hwp-previews/vendor/composer/ClassLoader.php
create mode 100644 plugins/hwp-previews/vendor/composer/InstalledVersions.php
create mode 100644 plugins/hwp-previews/vendor/composer/LICENSE
create mode 100644 plugins/hwp-previews/vendor/composer/autoload_classmap.php
create mode 100644 plugins/hwp-previews/vendor/composer/autoload_namespaces.php
create mode 100644 plugins/hwp-previews/vendor/composer/autoload_psr4.php
create mode 100644 plugins/hwp-previews/vendor/composer/autoload_real.php
create mode 100644 plugins/hwp-previews/vendor/composer/autoload_static.php
create mode 100644 plugins/hwp-previews/vendor/composer/installed.json
create mode 100644 plugins/hwp-previews/vendor/composer/installed.php
create mode 100644 plugins/hwp-previews/vendor/composer/platform_check.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/CHANGELOG.md
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/LICENSE
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/README.md
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/composer.json
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/BeforeValidException.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/CachedKeySet.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/ExpiredException.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/JWK.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/JWT.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/Key.php
create mode 100644 plugins/hwp-previews/vendor/firebase/php-jwt/src/SignatureInvalidException.php
diff --git a/plugins/hwp-previews/README.md b/plugins/hwp-previews/README.md
new file mode 100644
index 0000000..57eaf86
--- /dev/null
+++ b/plugins/hwp-previews/README.md
@@ -0,0 +1,100 @@
+# Headless WordPress Preview Plugin
+
+## Overview
+
+The Headless WordPress Preview Plugin (HWP Previews) enables seamless preview functionality for headless WordPress implementations. It solves the common challenge of previewing unpublished content when using WordPress as a headless CMS with a separate frontend.
+**THE PLUGIN AIMS TO BE VERY MUCH CONFIGURABLE APPROACH FOR THE PREVIEW FUNCTIONALITY, SO IT CAN BE USED WITH ANY FE Framework.**
+
+## Features
+
+- **Preview in iFrame**: Display previews within WordPress admin in an iFrame, with JWT token authentication that bypasses typical authentication requirements. This allows you to see your frontend preview directly within WordPress.
+- **Filtered Preview URLs**: Alternatively, you can choose to only filter the preview URLs in the default WordPress places and view previews in new tabs without the iFrame approach.
+- **Unique Post Slug Management**: Forces unique slugs for specifically configured post types and post statuses, ensuring consistency across all content states (published, draft, pending, etc.).
+- **Post Status Parent Support**: Enables configured post statuses of specified post types to be available as parent options in the WordPress admin, allowing for proper hierarchy construction with unpublished content.
+- **Draft Route Support**: Provides special handling for Next.js or Nuxt draft mode by allowing configuration of a draft API path that gets concatenated to the preview URL.
+- **Token-based Authentication**: Secures preview links with JWT tokens to ensure only authorized users can access unpublished content.
+- **REST API Token Verification**: Validates token authenticity for preview REST requests.
+- **Configurable Parameters**: Customize URL parameters used for previews.
+
+## Requirements
+
+- WordPress 5.7+
+- PHP 7.4+
+
+## Configuration
+
+### Settings
+
+- **Preview URL**: Set the base URL of your headless frontend
+- **Token Secret**: Set a secure string to use for JWT token generation
+- **Post Types**: Select which post types should have preview functionality
+- **Post Statuses**: Select which post statuses should be previewable
+- **Enable Unique Post Slug** (`ENABLE_UNIQUE_POST_SLUG`): Forces unique slug generation for the configured post types and statuses (default: enabled)
+- **Enable Post Statuses as Parent** (`ENABLE_POST_STATUSES_AS_PARENT`): Allows selecting unpublished content as parent posts in WordPress admin for the configured post types (default: enabled)
+- **Generate Preview Links** (`GENERATE_PREVIEW_LINKS`): Replaces WordPress preview links with headless preview URLs (default: disabled)
+- **Token Auth Enabled** (`TOKEN_AUTH_ENABLED`): Secures previews with JWT token-based authentication (default: enabled)
+- **Preview in iFrame** (`PREVIEW_IN_IFRAME`): Displays previews in an iFrame within WordPress admin (default: enabled)
+- **REST Token Verification** (`REST_TOKEN_VERIFICATION`): Enables token verification for REST API endpoints (default: enabled)
+- **Draft Route** (`DRAFT_ROUTE`): Specify a custom route for draft content that gets concatenated to the preview URL. Useful for Next.js or Nuxt draft mode APIs (e.g., `/api/preview` or `/api/draft`)
+- **Generate Preview Token** (`GENERATE_PREVIEW_TOKEN`): Controls whether JWT tokens are generated for preview URLs (default: enabled)
+- **Preview Parameter Names** (`PREVIEW_PARAMETER_NAMES`): Customize the URL parameter names if needed
+
+## How It Works
+
+### iFrame Preview Mode
+1. When enabled, this feature intercepts the standard WordPress preview functionality
+2. Instead of the default WordPress preview, the plugin renders an iFrame containing your frontend
+3. The iFrame URL includes a JWT token that authenticates the user, bypassing normal frontend authentication
+4. This allows viewing the frontend preview directly within WordPress admin
+
+### Alternative: URL Filtering Approach
+1. If you prefer not to use the iFrame approach, you can enable just the URL filtering functionality
+2. This replaces the standard WordPress preview links with links to your headless frontend
+3. Clicking preview will open your frontend in a new tab instead of within an iFrame
+
+### Unique Slug Management
+1. When enabled, the plugin ensures that slugs are unique across all configured post types and statuses
+2. This prevents conflicts that can occur when drafts and published content share the same slug
+3. Only applies to the post types and statuses you specify in settings
+
+### Post Status Parent Support
+1. By default, WordPress only allows published content to be selected as parent content
+2. This feature enables drafts and other unpublished content to appear in parent selection dropdowns
+3. Only applies to the post types and statuses configured in settings
+
+### Draft Route Support
+1. For frameworks like Next.js or Nuxt that have special draft mode APIs
+2. The plugin can append a configured draft route to the preview URL
+3. This activates the draft mode in your frontend framework
+
+### Token Authentication
+1. The plugin generates JWT tokens for secure preview access
+2. These tokens can be verified by your frontend to ensure only authorized users can see unpublished content
+3. Includes a verification endpoint at `/wp-json/hwp-previews/v1/verify-preview-token?token=` - something that can help FE to validate preview requester
+
+## Customizing Templates
+
+You can customize the preview templates by filtering `hwp_previews_template_dir_path`:
+
+```php
+add_filter('hwp_previews_template_dir_path', function($path) {
+ return get_stylesheet_directory() . '/templates';
+});
+```
+
+## Filters
+
+- `hwp_preview_args`: Allows modifying preview URL parameters
+- `hwp_previews_template_dir_path`: Change the template directory for preview templates - completely
+- `hwp_previews_header_name`
+- `hwp_previews_header_args`
+
+We can provide more filters if needed.
+
+## Actions:
+- `hwp_previews_before_get_header`
+- `hwp_previews_after_get_header`
+- `hwp_previews_before_get_footer`
+- `hwp_previews_after_get_footer`
+
+We can provide more actions if needed.
diff --git a/plugins/hwp-previews/composer.json b/plugins/hwp-previews/composer.json
new file mode 100644
index 0000000..1d40461
--- /dev/null
+++ b/plugins/hwp-previews/composer.json
@@ -0,0 +1,16 @@
+{
+ "name": "wpengine/hwp-previews",
+ "description": "Plugin for headless post previews.",
+ "type": "wordpress-plugin",
+ "prefer-stable": true,
+ "minimum-stability": "dev",
+ "autoload": {
+ "psr-4": {
+ "HWP\\Previews\\": "src/"
+ }
+ },
+ "require": {
+ "php": ">=7.4",
+ "firebase/php-jwt": "^6.11"
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/composer.lock b/plugins/hwp-previews/composer.lock
new file mode 100644
index 0000000..de1fc99
--- /dev/null
+++ b/plugins/hwp-previews/composer.lock
@@ -0,0 +1,84 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "72736c17dd331a5ebe8e0bc4565a896f",
+ "packages": [
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712",
+ "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.0"
+ },
+ "time": "2025-01-23T05:11:06+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "dev",
+ "stability-flags": {},
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": ">=7.4"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.6.0"
+}
diff --git a/plugins/hwp-previews/hwp-previews.php b/plugins/hwp-previews/hwp-previews.php
new file mode 100644
index 0000000..b635d68
--- /dev/null
+++ b/plugins/hwp-previews/hwp-previews.php
@@ -0,0 +1,38 @@
+init_dependencies();
+ $this->register_hooks();
+ }
+
+ /**
+ * Get the singleton instance of the Plugin.
+ *
+ * @return Plugin
+ */
+ public static function get_instance(): Plugin {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Initialize all dependencies.
+ *
+ * @return void
+ */
+ private function init_dependencies(): void {
+ $this->settings = new Headless_Preview_Settings();
+
+ $post_types = $this->settings->get_setting( Headless_Preview_Settings::POST_TYPES, [] );
+ $post_statuses = $this->settings->get_setting( Headless_Preview_Settings::POST_STATUSES, [] );
+
+ if ( ! $post_types || ! $post_statuses ) {
+ return; // TODO: actually do something here.
+ }
+
+ // Configs.
+ $this->types_config = new Post_Types_Config( $post_types );
+ $this->statuses_config = new Post_Statuses_Config( $post_statuses );
+
+ // Dependencies.
+ $token_secret = $this->settings->get_setting( Headless_Preview_Settings::TOKEN_SECRET, '' ); // is required.
+ $parameter_names = $this->settings->get_setting( Headless_Preview_Settings::PREVIEW_PARAMETER_NAMES, [
+ 'preview' => 'preview',
+ 'token' => 'token',
+ 'post_slug' => 'slug',
+ 'post_id' => 'p',
+ 'post_type' => 'type',
+ 'post_uri' => 'uri',
+ 'graphql_single' => 'gql'
+ ] );
+
+ $this->slug_repository = new Post_Slug_Repository();
+ $this->template_resolver = new Preview_Template_Resolver( $this->types_config, $this->statuses_config );
+ $this->url_generator = new Preview_URL_Generator( $this->types_config, $this->statuses_config );
+ $this->parameter_builder = new Preview_Parameter_Builder(
+ new Preview_Parameter_Names_Model( $parameter_names )
+ );
+
+ // Token related.
+ $this->jwt_token = new JWT_Token_Manager( $token_secret );
+ $this->token_extractor = new Token_Extractor();
+ $this->token_generator = new Token_Generator( $this->jwt_token );
+ $this->token_verifier = new Token_Verifier( $this->jwt_token );
+ $this->token_auth = new Token_Auth( $this->token_verifier );
+ $this->token_rest_controller = new Token_Rest_Controller( $this->token_verifier, $this->token_auth::PREVIEW_NONCE_ACTION );
+ }
+
+ private function register_hooks(): void {
+ // Todo: better way of doing settings, these a for the demo only.
+
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::ENABLE_UNIQUE_POST_SLUG, true ) ) {
+ $this->enable_unique_post_slug();
+ }
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::ENABLE_POST_STATUSES_AS_PARENT, true ) ) {
+ $this->enable_post_statuses_as_parent();
+ }
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::GENERATE_PREVIEW_LINKS, false ) ) {
+ $this->enable_generate_preview_url();
+ }
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::TOKEN_AUTH_ENABLED, true ) ) {
+ $this->enable_user_auth_for_preview();
+ }
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::PREVIEW_IN_IFRAME, true ) ) {
+ $this->enable_preview_in_iframe_functionality();
+ }
+
+ if ( $this->settings->get_setting( Headless_Preview_Settings::REST_TOKEN_VERIFICATION, true ) ) {
+ $this->enable_rest_route_token_verification();
+ }
+ }
+
+ private function enable_unique_post_slug(): void {
+ $post_slug_manager = new Post_Slug_Manager( $this->types_config, $this->statuses_config, $this->slug_repository );
+
+ add_filter( 'wp_insert_post_data', static function ( $data, $postarr ) use ( $post_slug_manager ) {
+ global $wp_rewrite;
+
+ $post_slug = $post_slug_manager->force_unique_post_slug(
+ new WP_Post( new Post_Data_Model( $data, (int) ( $postarr['ID'] ?? 0 ) ) ),
+ $wp_rewrite
+ );
+
+ if ( $post_slug ) {
+ $data['post_name'] = $post_slug;
+ }
+
+ return $data;
+ }, 10, 2 );
+ }
+
+ private function enable_post_statuses_as_parent(): void {
+ $post_parent_manager = new Post_Parent_Manager( $this->types_config, $this->statuses_config );
+
+ $post_parent_manager_callback = static function ( $args ) use ( $post_parent_manager ) {
+ $post_type = ! empty( $args['post_type'] ) ? get_post_type_object( (string) $args['post_type'] ) : null;
+ if ( $post_type ) {
+ $args['post_status'] = $post_parent_manager->get_post_statuses_as_parent( $post_type );
+ }
+
+ return $args;
+ };
+
+ add_filter( 'page_attributes_dropdown_pages_args', $post_parent_manager_callback );
+ add_filter( 'quick_edit_dropdown_pages_args', $post_parent_manager_callback );
+
+ // And for Gutenberg.
+ foreach ( $this->types_config->get_post_types() as $post_type ) {
+ $post_type_object = get_post_type_object( $post_type );
+ if ( ! $post_type_object || ! $this->types_config->supports_gutenberg( $post_type_object ) ) {
+ continue;
+ }
+ add_filter( 'rest_' . $post_type . '_query', $post_parent_manager_callback );
+ }
+ }
+
+ private function enable_generate_preview_url(): void {
+ add_filter( 'preview_post_link', function ( $link, $post ) {
+ return $this->generate_preview_url( $post ) ?: $link;
+ }, 10, 2 );
+
+ /**
+ * Hack Function that changes the preview link for draft articles,
+ * this must be removed when wordpress do the properly fix https://github.com/WordPress/gutenberg/issues/13998
+ */
+ foreach ( $this->types_config->get_post_types() as $post_type ) {
+ add_filter( 'rest_prepare_' . $post_type, function ( $response, $post ) {
+ $preview_url = $this->generate_preview_url( $post );
+ if ( $preview_url ) {
+ $response->data['link'] = $preview_url;
+ }
+
+ return $response;
+ }, 10, 2 );
+ }
+ }
+
+ private function enable_user_auth_for_preview(): void {
+ add_filter( 'determine_current_user', function ( $user_id ) {
+ if ( $user_id ) {
+ return $user_id;
+ }
+
+ $token = $this->token_extractor->get_token();
+ if ( ! $token ) {
+ return $user_id;
+ }
+
+ return $this->token_auth->determine_preview_user( $token ) ?: $user_id;
+ } );
+ }
+
+ private function enable_preview_in_iframe_functionality(): void {
+ add_filter( 'template_include', function ( $template ) {
+ if ( ! is_preview() ) {
+ return $template;
+ }
+
+ $post = get_post();
+ if ( ! $post instanceof WP_Post ) {
+ return $template;
+ }
+
+ $template_dir_path = (string) apply_filters(
+ 'hwp_previews_template_dir_path',
+ plugin_dir_path( dirname( __FILE__ ) ) . 'templates'
+ );
+
+ $preview_template = $this->template_resolver->resolve_template_path( $post, $template_dir_path, true );
+
+ if ( ! $preview_template ) { // TODO: Do something about it.
+ return $template;
+ }
+
+ set_query_var( $this->template_resolver::HWP_PREVIEWS_IFRAME_PREVIEW_URL, $this->generate_preview_url( $post ) );
+
+ return $preview_template;
+ }, 999 );
+ }
+
+ private function generate_preview_url( WP_Post $post ): string {
+ $preview_url = $this->settings->get_setting( Headless_Preview_Settings::PREVIEW_URL, '' );
+ if ( ! $preview_url ) {
+ return ''; // todo: maybe do something?
+ }
+
+ $token_auth = $this->settings->get_setting( Headless_Preview_Settings::TOKEN_AUTH_ENABLED, true );
+ $draft_route = $this->settings->get_setting( Headless_Preview_Settings::DRAFT_ROUTE, '' );
+
+ $token = '';
+ if ( $this->settings->get_setting( Headless_Preview_Settings::GENERATE_PREVIEW_TOKEN, true ) ) {
+ $token = $this->token_generator->generate_token( $token_auth ? [ 'data' => [ 'user' => [ 'id' => get_current_user_id() ] ] ] : [], 'preview_url_nonce', 300 );
+ }
+
+ $args = (array) apply_filters( 'hwp_preview_args', $this->parameter_builder->build_preview_args( $post, $token ), $post );
+
+ return $this->url_generator->generate_url( $post, $preview_url, $args, $draft_route );
+ }
+
+ public function enable_rest_route_token_verification(): void {
+ add_action( 'rest_api_init', function () {
+ $this->token_rest_controller->register_routes( 'hwp-previews/v1' );
+ } );
+ }
+
+}
diff --git a/plugins/hwp-previews/src/Post/Data/Post_Data_Model.php b/plugins/hwp-previews/src/Post/Data/Post_Data_Model.php
new file mode 100644
index 0000000..3d7e1ac
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Data/Post_Data_Model.php
@@ -0,0 +1,29 @@
+ $data
+ * @param int $post_id
+ */
+ public function __construct( array $data, int $post_id = 0 ) {
+ $this->ID = (int) ( $data['ID'] ?? $post_id );
+ $this->post_status = (string) ( $data['post_status'] ?? '' );
+ $this->post_type = (string) ( $data['post_type'] ?? '' );
+ $this->post_name = (string) ( $data['post_name'] ?? '' );
+ $this->post_title = (string) ( $data['post_title'] ?? '' );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php b/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php
new file mode 100644
index 0000000..9002b60
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Parent/Contracts/Post_Parent_Manager_Interface.php
@@ -0,0 +1,13 @@
+post_types = $post_types;
+ $this->post_statuses = $post_statuses;
+ }
+
+ public function get_post_statuses_as_parent( WP_Post_Type $post_type ): array {
+ if (
+ ! $this->post_types->is_post_type_applicable( $post_type->name ) ||
+ ! $this->post_types->is_hierarchical( $post_type )
+ ) {
+ return [];
+ }
+
+ return array_intersect( self::POST_STATUSES, $this->post_statuses->get_post_statuses() );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Manager_Interface.php b/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Manager_Interface.php
new file mode 100644
index 0000000..fffa427
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Slug/Contracts/Post_Slug_Manager_Interface.php
@@ -0,0 +1,15 @@
+types = $types;
+ $this->statuses = $statuses;
+ $this->slug_repository = $slug_repository;
+ }
+
+ public function force_unique_post_slug( WP_Post $post, wp_rewrite $wp_rewrite ): string {
+ if (
+ ! $post->ID ||
+ ! $this->types->is_post_type_applicable( $post->post_type ) ||
+ ! $this->statuses->is_post_status_applicable( $post->post_status )
+ ) {
+ return '';
+ }
+
+ $slug = $post->post_name ?: sanitize_title( $post->post_title, "$post->post_status-$post->ID" );
+ $feeds = is_array( $wp_rewrite->feeds ) ? $wp_rewrite->feeds : [];
+
+ return $this->generate_unique_slug( $slug, $post->post_type, $post->ID, array_merge( $feeds, [ 'embed' ] ) );
+ }
+
+ /**
+ * From WordPress core: wp-includes/post.php
+ *
+ * @param string $slug
+ * @param string $post_type
+ * @param int $post_id
+ * @param array $reserved_slugs
+ *
+ * @return string
+ */
+ public function generate_unique_slug( string $slug, string $post_type, int $post_id, array $reserved_slugs ): string {
+ $slug = $slug ?: 'undefined';
+
+ if ( ! $this->slug_repository->is_slug_taken( $slug, $post_type, $post_id ) && ! in_array( $slug, $reserved_slugs, true ) ) {
+ return $slug;
+ }
+
+ $suffix = 2;
+ do {
+ $new_slug = _truncate_post_slug( $slug, 200 - ( strlen( (string) $suffix ) + 1 ) ) . "-$suffix";
+ $suffix ++;
+ } while ( $this->slug_repository->is_slug_taken( $new_slug, $post_type, $post_id ) );
+
+ return $new_slug;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Post/Slug/Post_Slug_Repository.php b/plugins/hwp-previews/src/Post/Slug/Post_Slug_Repository.php
new file mode 100644
index 0000000..901f695
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Slug/Post_Slug_Repository.php
@@ -0,0 +1,23 @@
+wpdb = $wpdb;
+ }
+
+ public function is_slug_taken( string $slug, string $post_type, int $post_id ): bool {
+ $query = "SELECT post_name FROM {$this->wpdb->posts} WHERE post_name = %s AND post_type = %s AND ID != %d LIMIT 1";
+
+ return (bool) $this->wpdb->get_var( $this->wpdb->prepare( $query, $slug, $post_type, $post_id ) );
+ }
+}
diff --git a/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php b/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php
new file mode 100644
index 0000000..47e60ce
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Status/Contracts/Post_Statuses_Config_Interface.php
@@ -0,0 +1,13 @@
+post_statuses = $post_statuses;
+ }
+
+ public function get_post_statuses(): array {
+ return $this->post_statuses;
+ }
+
+ /**
+ * TODO: add post status verification to support custom post types in future. Or anything else.
+ *
+ * @param string $post_status
+ *
+ * @return bool
+ */
+ public function is_post_status_applicable( string $post_status ): bool {
+ return in_array( $post_status, $this->post_statuses, true );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Post/Type/Contracts/Post_Types_Config_Interface.php b/plugins/hwp-previews/src/Post/Type/Contracts/Post_Types_Config_Interface.php
new file mode 100644
index 0000000..3f6a8c9
--- /dev/null
+++ b/plugins/hwp-previews/src/Post/Type/Contracts/Post_Types_Config_Interface.php
@@ -0,0 +1,19 @@
+post_types = $post_types;
+ }
+
+ public function get_post_types(): array {
+ return $this->post_types;
+ }
+
+ public function is_post_type_applicable( string $post_type ): bool {
+ return in_array( $post_type, $this->post_types, true ) && post_type_exists( $post_type );
+ }
+
+ public function is_hierarchical( WP_Post_Type $post_type ): bool {
+ return $post_type->hierarchical;
+ }
+
+ public function supports_gutenberg( WP_Post_Type $post_type ): bool {
+ if (
+ empty( $post_type->show_in_rest ) ||
+ empty( $post_type->supports ) ||
+ ! is_array( $post_type->supports ) ||
+ ! in_array( 'editor', $post_type->supports )
+ ) {
+ return false;
+ }
+
+ if ( ! is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
+ return true;
+ }
+
+ $classic_editor_settings = (array) get_option( 'classic-editor-settings', [] );
+
+ return ! (
+ ! empty( $classic_editor_settings['post_types'] ) &&
+ is_array( $classic_editor_settings['post_types'] ) &&
+ in_array( $post_type->name, $classic_editor_settings['post_types'] )
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Names_Model.php b/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Names_Model.php
new file mode 100644
index 0000000..1ff0905
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Names_Model.php
@@ -0,0 +1,47 @@
+preview = (string) $names['preview'];
+ }
+
+ $this->token = (string) ( $names['token'] ?? '' ); // can be `token` by default.
+ $this->post_slug = (string) ( $names['post_slug'] ?? '' ); // can be `slug` by default.
+ $this->post_id = (string) ( $names['post_id'] ?? '' ); // can be `p` by default.
+ $this->post_type = (string) ( $names['post_type'] ?? '' ); // can be `type` by default.
+ $this->post_uri = (string) ( $names['post_uri'] ?? '' ); // can be `uri` by default.
+ $this->graphql_single = (string) ( $names['graphql_single'] ?? '' ); // can be `gql` by default.
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Query_Argument_Interface.php b/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Query_Argument_Interface.php
new file mode 100644
index 0000000..5b3fbc4
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Template/Contracts/Preview_Query_Argument_Interface.php
@@ -0,0 +1,12 @@
+types = $types;
+ $this->statuses = $statuses;
+ }
+
+ public function resolve_template_path( WP_Post $post, string $template_dir_path, bool $per_post_type = false ): string {
+ if (
+ ! $template_dir_path ||
+ ! $this->types->is_post_type_applicable( $post->post_type ) ||
+ ! $this->statuses->is_post_status_applicable( $post->post_status ) ||
+ ! is_preview()
+ ) {
+ return '';
+ }
+
+ $template = $template_dir_path . '/hwp-preview.php';
+ $template_type = $template_dir_path . '/hwp-preview-' . $post->post_type . '.php';
+
+ if ( $per_post_type && file_exists( $template_type ) ) {
+ return $template_type;
+ }
+
+ return file_exists( $template ) ? $template : '';
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_Parameter_Builder_Interface.php b/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_Parameter_Builder_Interface.php
new file mode 100644
index 0000000..a2d0f74
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_Parameter_Builder_Interface.php
@@ -0,0 +1,13 @@
+parameter_names = $parameter_names;
+ }
+
+ /**
+ * @param WP_Post $post
+ * @param string $token
+ *
+ * @return array
+ */
+ public function build_preview_args( WP_Post $post, string $token ): array {
+ // Add default preview param.
+ $args = [
+ $this->parameter_names->preview => 'true',
+ ];
+
+ // Add post slug param.
+ if ( $this->parameter_names->post_slug ) {
+ $args[ $this->parameter_names->post_slug ] = $post->post_name;
+ }
+
+ // Add post id param.
+ if ( $this->parameter_names->post_id ) {
+ $args[ $this->parameter_names->post_id ] = $post->ID;
+ }
+
+ // Add post type param.
+ if ( $this->parameter_names->post_type ) {
+ $args[ $this->parameter_names->post_type ] = $post->post_type;
+ }
+
+ // Add graphql single name param.
+ if ( $this->parameter_names->graphql_single ) {
+ $post_type_object = get_post_type_object( $post->post_type );
+
+ if ( ! empty( $post_type_object->graphql_single_name ) ) {
+ $args[ $this->parameter_names->graphql_single ] = ucfirst( $post_type_object->graphql_single_name );
+ }
+ }
+
+ // Add page uri param.
+ if ( $this->parameter_names->post_uri ) {
+ $page_uri = (string) get_page_uri( $post->ID );
+
+ if ($page_uri) {
+ $args[ $this->parameter_names->post_uri ] = $page_uri;
+ }
+ }
+
+ // Add token param.
+ if ( $this->parameter_names->token && $token ) {
+ $args[ $this->parameter_names->token ] = $token;
+ }
+
+ return $args;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php b/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
new file mode 100644
index 0000000..3fb5b28
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
@@ -0,0 +1,48 @@
+types = $types;
+ $this->statuses = $statuses;
+ }
+
+ public function generate_url(
+ WP_Post $post,
+ string $frontend_url,
+ array $args,
+ string $draft_route = ''
+ ): string {
+ if (
+ ! $frontend_url ||
+ ! $this->types->is_post_type_applicable( $post->post_type ) ||
+ ! $this->statuses->is_post_status_applicable( $post->post_status )
+ ) {
+ return '';
+ }
+
+ // Format frontend URL to the draft route handler.
+ if ( $draft_route ) {
+ $frontend_url = trailingslashit( $frontend_url ) . $draft_route;
+ }
+
+ if ( $args ) {
+ return add_query_arg( $args, $frontend_url );
+ }
+
+ return $frontend_url;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php b/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
new file mode 100644
index 0000000..8e02ad9
--- /dev/null
+++ b/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
@@ -0,0 +1,762 @@
+ false,
+ self::PREVIEW_URL => 'http://localhost:3000',
+ self::ENABLE_UNIQUE_POST_SLUG => true,
+ self::GENERATE_PREVIEW_TOKEN => true,
+ self::ENABLE_POST_STATUSES_AS_PARENT => true,
+ self::TOKEN_AUTH_ENABLED => true,
+ self::PREVIEW_IN_IFRAME => true,
+ self::TOKEN_SECRET => '',
+ self::PREVIEW_PARAMETER_NAMES => [
+ 'preview' => 'preview',
+ 'token' => 'token',
+ 'post_slug' => 'slug',
+ 'post_id' => 'p',
+ 'post_type' => 'type',
+ 'post_uri' => 'uri',
+ 'graphql_single' => 'gql'
+ ],
+ self::POST_TYPES => [ 'post', 'page' ],
+ self::POST_STATUSES => [ 'future', 'draft', 'pending', 'private', 'publish' ],
+ self::DRAFT_ROUTE => '',
+ self::REST_TOKEN_VERIFICATION => true
+ ];
+
+ /**
+ * Settings
+ */
+ private $settings;
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ add_action( 'admin_menu', [ $this, 'add_settings_page' ] );
+ add_action( 'admin_init', [ $this, 'register_settings' ] );
+
+ $this->settings = get_option( self::OPTION_NAME, $this->defaults );
+ }
+
+ /**
+ * Add settings page to WP Admin menu
+ */
+ public function add_settings_page() {
+ add_options_page(
+ 'Headless Preview Settings',
+ 'Headless Preview',
+ 'manage_options',
+ 'headless-preview',
+ [ $this, 'render_settings_page' ]
+ );
+ }
+
+ /**
+ * Register the settings
+ */
+ public function register_settings() {
+ register_setting(
+ 'headless-preview',
+ self::OPTION_NAME,
+ [ $this, 'sanitize_settings' ]
+ );
+
+ // General Settings Section
+ add_settings_section(
+ 'general_settings',
+ 'General Settings',
+ [ $this, 'render_general_section' ],
+ 'headless-preview'
+ );
+
+ // Preview Settings Section
+ add_settings_section(
+ 'preview_settings',
+ 'Preview Settings',
+ [ $this, 'render_preview_section' ],
+ 'headless-preview'
+ );
+
+ // Authentication Settings Section
+ add_settings_section(
+ 'auth_settings',
+ 'Authentication Settings',
+ [ $this, 'render_auth_section' ],
+ 'headless-preview'
+ );
+
+ // URL Parameters Settings Section
+ add_settings_section(
+ 'url_params_settings',
+ 'URL Parameters',
+ [ $this, 'render_url_params_section' ],
+ 'headless-preview'
+ );
+
+ // Post Types and Statuses Section
+ add_settings_section(
+ 'post_settings',
+ 'Post Types and Statuses',
+ [ $this, 'render_post_section' ],
+ 'headless-preview'
+ );
+
+ // REST API Settings Section
+ add_settings_section(
+ 'rest_settings',
+ 'REST API Settings',
+ [ $this, 'render_rest_section' ],
+ 'headless-preview'
+ );
+
+ // General Settings Fields
+ add_settings_field(
+ 'preview_url',
+ 'Headless Frontend URL',
+ [ $this, 'render_preview_url_field' ],
+ 'headless-preview',
+ 'general_settings'
+ );
+
+ add_settings_field(
+ 'draft_route',
+ 'Draft Route',
+ [ $this, 'render_draft_route_field' ],
+ 'headless-preview',
+ 'general_settings'
+ );
+
+ // Preview Settings Fields
+ add_settings_field(
+ 'generate_preview_links',
+ 'Generate Preview Links',
+ [ $this, 'render_generate_preview_links_field' ],
+ 'headless-preview',
+ 'preview_settings'
+ );
+
+ add_settings_field(
+ 'preview_in_iframe',
+ 'Preview in iFrame',
+ [ $this, 'render_preview_in_iframe_field' ],
+ 'headless-preview',
+ 'preview_settings'
+ );
+
+ // Authentication Settings Fields
+ add_settings_field(
+ 'generate_preview_token',
+ 'Generate Preview Token',
+ [ $this, 'render_generate_preview_token_field' ],
+ 'headless-preview',
+ 'auth_settings'
+ );
+
+ add_settings_field(
+ 'token_auth_enabled',
+ 'Enable Token Authentication',
+ [ $this, 'render_token_auth_enabled_field' ],
+ 'headless-preview',
+ 'auth_settings'
+ );
+
+ add_settings_field(
+ 'token_secret',
+ 'Token Secret',
+ [ $this, 'render_token_secret_field' ],
+ 'headless-preview',
+ 'auth_settings'
+ );
+
+ // URL Parameters Fields
+ add_settings_field(
+ 'preview_parameter_names',
+ 'Parameter Names',
+ [ $this, 'render_preview_parameter_names_field' ],
+ 'headless-preview',
+ 'url_params_settings'
+ );
+
+ // Post Types and Statuses Fields
+ add_settings_field(
+ 'post_types',
+ 'Post Types',
+ [ $this, 'render_post_types_field' ],
+ 'headless-preview',
+ 'post_settings'
+ );
+
+ add_settings_field(
+ 'post_statuses',
+ 'Post Statuses',
+ [ $this, 'render_post_statuses_field' ],
+ 'headless-preview',
+ 'post_settings'
+ );
+
+ add_settings_field(
+ 'enable_unique_post_slug',
+ 'Enable Unique Post Slug',
+ [ $this, 'render_enable_unique_post_slug_field' ],
+ 'headless-preview',
+ 'post_settings'
+ );
+
+ add_settings_field(
+ 'enable_post_statuses_as_parent',
+ 'Enable Post Statuses as Parent',
+ [ $this, 'render_enable_post_statuses_as_parent_field' ],
+ 'headless-preview',
+ 'post_settings'
+ );
+
+ // REST API Fields
+ add_settings_field(
+ 'rest_token_verification',
+ 'Enable REST Token Verification',
+ [ $this, 'render_rest_token_verification_field' ],
+ 'headless-preview',
+ 'rest_settings'
+ );
+ }
+
+ /**
+ * Render the General section description
+ */
+ public function render_general_section() {
+ echo '
Basic settings for your headless frontend configuration.
';
+ }
+
+ /**
+ * Render the Preview section description
+ */
+ public function render_preview_section() {
+ echo '
Configure how content previews are generated and displayed.
';
+ }
+
+ /**
+ * Render the Authentication section description
+ */
+ public function render_auth_section() {
+ echo '
Settings related to token generation and authentication for previews.
';
+ }
+
+ /**
+ * Render the URL Parameters section description
+ */
+ public function render_url_params_section() {
+ echo '
Customize the parameters used in preview URLs. Leave a field empty to omit that parameter.
';
+ }
+
+ /**
+ * Render the Post Types and Statuses section description
+ */
+ public function render_post_section() {
+ echo '
Configure which post types and statuses should use the headless preview functionality.
';
+ }
+
+ /**
+ * Render the REST API section description
+ */
+ public function render_rest_section() {
+ echo '
Settings for REST API integration for token verification.
';
+ }
+
+ /**
+ * Render the Generate Preview Links field
+ */
+ public function render_generate_preview_links_field() {
+ $value = isset( $this->settings['generate_preview_links'] ) ? $this->settings['generate_preview_links'] : false;
+ ?>
+
+
+ When enabled, WordPress's default preview links will be replaced with links to your headless frontend.
+ Should be disabled by default for compatibility with standard WordPress workflows.
+
+ The base URL of your headless frontend (e.g., Next.js or Nuxt application).
+ For local development, use something like 'http://localhost:3000'.
+ For production, use your actual domain like 'https://example.com'.
+
+ When enabled, a JWT token will be included in preview URLs for security purposes.
+ If AUTH via TOKEN is enabled - it might be used for auth.
+ Also it might be used for the FE validation of the preview requester.
+ This helps secure your preview content from unauthorized access.
+
+ When enabled, authentication is bypassed for iframe previews.
+ This setting only works when Preview in iFrame is enabled.
+ Useful for streamlining the preview experience within the WordPress admin.
+
+ When enabled, previews will be displayed in an iframe within the WordPress admin interface,
+ rather than opening in a new tab. This disables preview link generation as it uses a custom template.
+ Provides a more seamless editing experience within WordPress.
+
+ A secret key used to sign JWT tokens. Should be kept secure and not shared.
+ Changing this will invalidate all existing preview tokens.
+ For maximum security, use a long, random string of characters.
+
+ Customize the parameter names used in preview URLs. Leave a field empty to omit that parameter from the URL.
+ Changes will affect how your frontend receives and processes preview requests.
+
+ Optional path to append to the frontend URL for draft previews (e.g., 'api/draft' for Next.js).
+ This route is appended to your frontend URL for handling draft content.
+ For Next.js Draft Mode, use something like 'api/draft' or 'api/preview'.
+ For Nuxt, it depends on your preview setup. Leave empty if not using a special draft route.
+
+ Select which post types should have the headless preview functionality enabled.
+ For custom post types, make sure they support the 'custom-fields' feature to store preview data.
+
+ Select which post statuses should be supported for previews.
+ This determines which types of content can be viewed in preview mode.
+ For example, 'draft' allows previewing unpublished content, while 'private' allows previewing
+ content that's only visible to logged-in users on the WordPress side.
+
+ When enabled, a custom REST API endpoint will be created at:
+ /wp-json/hwp-previews/v1/verify-preview-token?token=<string>
+ This can be used by your frontend to verify that preview tokens are valid before showing preview content.
+ This helps implement secure previewing in your headless frontend application.
+
+ When enabled, the plugin ensures that post slugs remain unique across all post types and statuses in the
+ preview context.
+ This is important for proper URL generation and navigation in your headless frontend, especially when
+ dealing with
+ draft content that might have duplicate slugs. Enabling this setting helps prevent URL conflicts and ensures
+ each piece of content has a unique identifier in your frontend application.
+
+ When enabled, posts with non-published statuses (like drafts or pending) can be used as parent posts in
+ hierarchical structures.
+ This is particularly useful for previewing complex content structures where parent-child relationships
+ exist, and you need to preview
+ changes to the hierarchy before publishing. This setting allows you to create and preview nested page
+ structures where parent pages
+ may still be in draft or other non-published states.
+
+ About Headless Preview: These settings configure how WordPress previews content
+ on your headless frontend. If you're using WordPress as a headless CMS with a separate frontend
+ (like Next.js, Nuxt, etc.), these options allow you to customize how preview links are generated
+ and authenticated.
+
+
+
+
+
+
Usage Information
+
After saving these settings, you may need to:
+
+
Configure your frontend application to handle preview URLs with the parameters defined above
+
+
Set up authentication handling in your frontend to validate preview tokens
+
If using iFrame previews, ensure your frontend allows being displayed in an iframe (check
+ X-Frame-Options headers)
+
+
+
+
+ settings[ $key ] ) ? $this->settings[ $key ] : $default;
+ }
+}
diff --git a/plugins/hwp-previews/src/Shared/Model.php b/plugins/hwp-previews/src/Shared/Model.php
new file mode 100644
index 0000000..dcf2354
--- /dev/null
+++ b/plugins/hwp-previews/src/Shared/Model.php
@@ -0,0 +1,24 @@
+verifier = $verifier;
+ }
+
+ public function determine_preview_user( string $token ): int {
+ $token = $this->verifier->verify_token( $token, self::PREVIEW_NONCE_ACTION );
+ if ( ! $token || empty( $token->data->user->id ) ) {
+ return 0;
+ }
+
+ return $token->data->user->id;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Token/Extractor/Contracts/Token_Extractor_Interface.php b/plugins/hwp-previews/src/Token/Extractor/Contracts/Token_Extractor_Interface.php
new file mode 100644
index 0000000..b8d0db3
--- /dev/null
+++ b/plugins/hwp-previews/src/Token/Extractor/Contracts/Token_Extractor_Interface.php
@@ -0,0 +1,11 @@
+token_manager = $token_manager;
+ }
+
+ public function generate_token( array $data, string $nonce, int $exp = 360 ): string {
+ $data['data']['nonce'] = wp_create_nonce( $nonce );
+
+ return $this->token_manager->generate_token( $data, $exp );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Token/Manager/Contracts/Token_Manager_Interface.php b/plugins/hwp-previews/src/Token/Manager/Contracts/Token_Manager_Interface.php
new file mode 100644
index 0000000..f5104c6
--- /dev/null
+++ b/plugins/hwp-previews/src/Token/Manager/Contracts/Token_Manager_Interface.php
@@ -0,0 +1,29 @@
+secret = $secret;
+ $this->alg = $alg;
+ }
+
+ public function generate_token( array $data, int $expiration = 360 ): string {
+ $issued_at = time();
+ $token_data = array_merge( $data, [
+ 'iat' => $issued_at,
+ 'exp' => $issued_at + $expiration,
+ 'iss' => get_site_url(),
+ ] );
+
+ try {
+ return JWT::encode( $token_data, $this->secret, $this->alg );
+ } catch ( Exception $e ) {
+ // TODO: Exception handling.
+ return '';
+ }
+ }
+
+ /**
+ * Todo: proper exception handling.
+ *
+ * @param string $token
+ *
+ * @return stdClass|null
+ */
+ public function verify_token( string $token, int $leeway = 60 ): ?stdClass {
+ if ( empty( $token ) ) {
+ return null;
+ }
+
+ JWT::$leeway = $leeway;
+
+ try {
+ $decoded = JWT::decode( $token, new Key( $this->secret, $this->alg ) );
+
+ // Check if the token has expired.
+ if ( isset( $decoded->exp ) && time() > $decoded->exp ) {
+ return null;
+ }
+
+ // Check if the token was issued by this server.
+ if ( isset( $token->iss ) && ! in_array( $token->iss, (array) get_bloginfo('url')) ) {
+ // See https://github.com/wp-graphql/wp-graphql-jwt-authentication/issues/111
+ add_filter( 'graphql_response_status_code', fn() => 401 );
+
+ return null;
+ }
+
+ // TODO: Any more checks here?
+
+ return $decoded;
+ } catch ( Exception $e ) {
+ return null;
+ }
+ }
+
+ public function refresh_token( string $token, int $expiration = 360 ): ?string {
+ $token_data = $this->verify_token( $token );
+
+ if ( null === $token_data ) {
+ return null;
+ }
+
+ // Convert stdClass to array
+ $data = json_decode( json_encode( $token_data ), true );
+
+ // Remove timing claims that will be re-added
+ unset( $data['iat'] );
+ unset( $data['exp'] );
+ unset( $data['iss'] );
+
+ $refreshed_token = $this->generate_token( $data, $expiration );
+
+ return empty( $refreshed_token ) ? null : $refreshed_token;
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Token/REST/Contracts/Token_REST_Controller_Interface.php b/plugins/hwp-previews/src/Token/REST/Contracts/Token_REST_Controller_Interface.php
new file mode 100644
index 0000000..99ca7ae
--- /dev/null
+++ b/plugins/hwp-previews/src/Token/REST/Contracts/Token_REST_Controller_Interface.php
@@ -0,0 +1,11 @@
+verifier = $verifier;
+ $this->nonce_action = $nonce_action;
+ }
+
+ public function register_routes( string $namespace ): void {
+ $result = register_rest_route( $namespace, '/verify-preview-token', [
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => [ $this, 'route_callback' ],
+ 'permission_callback' => '__return_true', // todo: is that correct?
+ 'args' => [
+ self::TOKEN_PARAMETER => [
+ 'required' => true,
+ 'validate_callback' => function ( $param ) {
+ return is_string( $param ) && ! empty( $param );
+ }
+ ]
+ ]
+ ] );
+
+ if ( ! $result ) {
+ // todo: Do something with the error?
+ }
+ }
+
+ public function route_callback( WP_REST_Request $request ): WP_REST_Response {
+ if ( ! $request->has_valid_params() ) {
+ return new WP_REST_Response( [ 'error' => 'Body does not have valid parameters' ], 400 );
+ }
+
+ $token = (string) json_decode( (string) $request->get_param( self::TOKEN_PARAMETER ) );
+
+ if ( ! $token ) {
+ return new WP_REST_Response( [ 'error' => 'A token is required' ], 400 );
+ }
+
+ $success = $this->verifier->verify_token( $token, $this->nonce_action );
+
+// if ( ! $success ) {
+// return new WP_REST_Response( [ 'valid' => false, 'error' => 'The token is not valid.' ], 500 );
+// }
+
+ return new WP_REST_Response( [ 'valid' => true, 'data' => [ 'success' => true ] ], 200 );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Token/Verifier/Contracts/Token_Verifier_Interface.php b/plugins/hwp-previews/src/Token/Verifier/Contracts/Token_Verifier_Interface.php
new file mode 100644
index 0000000..cb7a494
--- /dev/null
+++ b/plugins/hwp-previews/src/Token/Verifier/Contracts/Token_Verifier_Interface.php
@@ -0,0 +1,13 @@
+token_manager = $token_manager;
+ }
+
+ public function verify_token( string $token, string $nonce_action ): ?stdClass {
+ $token = $this->token_manager->verify_token( $token );
+
+ if (
+ ! $token ||
+ empty( $token->data->nonce ) ||
+ wp_verify_nonce( $token->data->nonce, $nonce_action ) !== 1
+ ) {
+ return null;
+ }
+
+ return $token;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/templates/hwp-preview.php b/plugins/hwp-previews/templates/hwp-preview.php
new file mode 100644
index 0000000..9adefb9
--- /dev/null
+++ b/plugins/hwp-previews/templates/hwp-preview.php
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier
+ * @author Jordi Boggiano
+ * @see https://www.php-fig.org/psr/psr-0/
+ * @see https://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+ /** @var \Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
+ private $vendorDir;
+
+ // PSR-4
+ /**
+ * @var array>
+ */
+ private $prefixLengthsPsr4 = array();
+ /**
+ * @var array>
+ */
+ private $prefixDirsPsr4 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ /**
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array>>
+ */
+ private $prefixesPsr0 = array();
+ /**
+ * @var list
+ */
+ private $fallbackDirsPsr0 = array();
+
+ /** @var bool */
+ private $useIncludePath = false;
+
+ /**
+ * @var array
+ */
+ private $classMap = array();
+
+ /** @var bool */
+ private $classMapAuthoritative = false;
+
+ /**
+ * @var array
+ */
+ private $missingClasses = array();
+
+ /** @var string|null */
+ private $apcuPrefix;
+
+ /**
+ * @var array
+ */
+ private static $registeredLoaders = array();
+
+ /**
+ * @param string|null $vendorDir
+ */
+ public function __construct($vendorDir = null)
+ {
+ $this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
+ }
+
+ return array();
+ }
+
+ /**
+ * @return array>
+ */
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ /**
+ * @return list
+ */
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ /**
+ * @return array Array of classname => path
+ */
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ *
+ * @return void
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @return void
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ $paths = (array) $paths;
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param list|string $paths The PSR-0 base directories
+ *
+ * @return void
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param list|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return void
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ *
+ * @return void
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Turns off searching the prefix and fallback directories for classes
+ * that have not been registered with the class map.
+ *
+ * @param bool $classMapAuthoritative
+ *
+ * @return void
+ */
+ public function setClassMapAuthoritative($classMapAuthoritative)
+ {
+ $this->classMapAuthoritative = $classMapAuthoritative;
+ }
+
+ /**
+ * Should class lookup fail if not found in the current class map?
+ *
+ * @return bool
+ */
+ public function isClassMapAuthoritative()
+ {
+ return $this->classMapAuthoritative;
+ }
+
+ /**
+ * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+ *
+ * @param string|null $apcuPrefix
+ *
+ * @return void
+ */
+ public function setApcuPrefix($apcuPrefix)
+ {
+ $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+ }
+
+ /**
+ * The APCu prefix in use, or null if APCu caching is not enabled.
+ *
+ * @return string|null
+ */
+ public function getApcuPrefix()
+ {
+ return $this->apcuPrefix;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ *
+ * @return void
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+
+ if (null === $this->vendorDir) {
+ return;
+ }
+
+ if ($prepend) {
+ self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
+ } else {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ self::$registeredLoaders[$this->vendorDir] = $this;
+ }
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ *
+ * @return void
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+
+ if (null !== $this->vendorDir) {
+ unset(self::$registeredLoaders[$this->vendorDir]);
+ }
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return true|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ $includeFile = self::$includeFile;
+ $includeFile($file);
+
+ return true;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+ if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+ return false;
+ }
+ if (null !== $this->apcuPrefix) {
+ $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+ if ($hit) {
+ return $file;
+ }
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if (false === $file && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if (null !== $this->apcuPrefix) {
+ apcu_add($this->apcuPrefix.$class, $file);
+ }
+
+ if (false === $file) {
+ // Remember that this class does not exist.
+ $this->missingClasses[$class] = true;
+ }
+
+ return $file;
+ }
+
+ /**
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
+ *
+ * @return array
+ */
+ public static function getRegisteredLoaders()
+ {
+ return self::$registeredLoaders;
+ }
+
+ /**
+ * @param string $class
+ * @param string $ext
+ * @return string|false
+ */
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ $subPath = $class;
+ while (false !== $lastPos = strrpos($subPath, '\\')) {
+ $subPath = substr($subPath, 0, $lastPos);
+ $search = $subPath . '\\';
+ if (isset($this->prefixDirsPsr4[$search])) {
+ $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+ foreach ($this->prefixDirsPsr4[$search] as $dir) {
+ if (file_exists($file = $dir . $pathEnd)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = \Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
+}
diff --git a/plugins/hwp-previews/vendor/composer/InstalledVersions.php b/plugins/hwp-previews/vendor/composer/InstalledVersions.php
new file mode 100644
index 0000000..07b32ed
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/InstalledVersions.php
@@ -0,0 +1,362 @@
+
+ * Jordi Boggiano
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+/**
+ * This class is copied in every Composer installed project and available to all
+ *
+ * See also https://getcomposer.org/doc/07-runtime.md#installed-versions
+ *
+ * To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
+ */
+class InstalledVersions
+{
+ /**
+ * @var mixed[]|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null
+ */
+ private static $installed;
+
+ /**
+ * @var bool|null
+ */
+ private static $canGetVendors;
+
+ /**
+ * @var array[]
+ * @psalm-var array}>
+ */
+ private static $installedByVendor = array();
+
+ /**
+ * Returns a list of all package names which are present, either by being installed, replaced or provided
+ *
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackages()
+ {
+ $packages = array();
+ foreach (self::getInstalled() as $installed) {
+ $packages[] = array_keys($installed['versions']);
+ }
+
+ if (1 === \count($packages)) {
+ return $packages[0];
+ }
+
+ return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
+ }
+
+ /**
+ * Returns a list of all package names with a specific type e.g. 'library'
+ *
+ * @param string $type
+ * @return string[]
+ * @psalm-return list
+ */
+ public static function getInstalledPackagesByType($type)
+ {
+ $packagesByType = array();
+
+ foreach (self::getInstalled() as $installed) {
+ foreach ($installed['versions'] as $name => $package) {
+ if (isset($package['type']) && $package['type'] === $type) {
+ $packagesByType[] = $name;
+ }
+ }
+ }
+
+ return $packagesByType;
+ }
+
+ /**
+ * Checks whether the given package is installed
+ *
+ * This also returns true if the package name is provided or replaced by another package
+ *
+ * @param string $packageName
+ * @param bool $includeDevRequirements
+ * @return bool
+ */
+ public static function isInstalled($packageName, $includeDevRequirements = true)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (isset($installed['versions'][$packageName])) {
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks whether the given package satisfies a version constraint
+ *
+ * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
+ *
+ * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
+ *
+ * @param VersionParser $parser Install composer/semver to have access to this class and functionality
+ * @param string $packageName
+ * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
+ * @return bool
+ */
+ public static function satisfies(VersionParser $parser, $packageName, $constraint)
+ {
+ $constraint = $parser->parseConstraints((string) $constraint);
+ $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
+
+ return $provided->matches($constraint);
+ }
+
+ /**
+ * Returns a version constraint representing all the range(s) which are installed for a given package
+ *
+ * It is easier to use this via isInstalled() with the $constraint argument if you need to check
+ * whether a given version of a package is installed, and not just whether it exists
+ *
+ * @param string $packageName
+ * @return string Version constraint usable with composer/semver
+ */
+ public static function getVersionRanges($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ $ranges = array();
+ if (isset($installed['versions'][$packageName]['pretty_version'])) {
+ $ranges[] = $installed['versions'][$packageName]['pretty_version'];
+ }
+ if (array_key_exists('aliases', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
+ }
+ if (array_key_exists('replaced', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
+ }
+ if (array_key_exists('provided', $installed['versions'][$packageName])) {
+ $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
+ }
+
+ return implode(' || ', $ranges);
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
+ */
+ public static function getPrettyVersion($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['pretty_version'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['pretty_version'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
+ */
+ public static function getReference($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ if (!isset($installed['versions'][$packageName]['reference'])) {
+ return null;
+ }
+
+ return $installed['versions'][$packageName]['reference'];
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @param string $packageName
+ * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
+ */
+ public static function getInstallPath($packageName)
+ {
+ foreach (self::getInstalled() as $installed) {
+ if (!isset($installed['versions'][$packageName])) {
+ continue;
+ }
+
+ return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
+ }
+
+ throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
+ }
+
+ /**
+ * @return array
+ * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
+ */
+ public static function getRootPackage()
+ {
+ $installed = self::getInstalled();
+
+ return $installed[0]['root'];
+ }
+
+ /**
+ * Returns the raw installed.php data for custom implementations
+ *
+ * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
+ * @return array[]
+ * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}
+ */
+ public static function getRawData()
+ {
+ @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ self::$installed = include __DIR__ . '/installed.php';
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ return self::$installed;
+ }
+
+ /**
+ * Returns the raw data of all installed.php which are currently loaded for custom implementations
+ *
+ * @return array[]
+ * @psalm-return list}>
+ */
+ public static function getAllRawData()
+ {
+ return self::getInstalled();
+ }
+
+ /**
+ * Lets you reload the static array from another file
+ *
+ * This is only useful for complex integrations in which a project needs to use
+ * this class but then also needs to execute another project's autoloader in process,
+ * and wants to ensure both projects have access to their version of installed.php.
+ *
+ * A typical case would be PHPUnit, where it would need to make sure it reads all
+ * the data it needs from this class, then call reload() with
+ * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
+ * the project in which it runs can then also use this class safely, without
+ * interference between PHPUnit's dependencies and the project's dependencies.
+ *
+ * @param array[] $data A vendor/composer/installed.php data set
+ * @return void
+ *
+ * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data
+ */
+ public static function reload($data)
+ {
+ self::$installed = $data;
+ self::$installedByVendor = array();
+ }
+
+ /**
+ * @return array[]
+ * @psalm-return list}>
+ */
+ private static function getInstalled()
+ {
+ if (null === self::$canGetVendors) {
+ self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
+ }
+
+ $installed = array();
+ $copiedLocalDir = false;
+
+ if (self::$canGetVendors) {
+ foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+ if (isset(self::$installedByVendor[$vendorDir])) {
+ $installed[] = self::$installedByVendor[$vendorDir];
+ } elseif (is_file($vendorDir.'/composer/installed.php')) {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require $vendorDir.'/composer/installed.php';
+ self::$installedByVendor[$vendorDir] = $required;
+ $installed[] = $required;
+ if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
+ self::$installed = $required;
+ $copiedLocalDir = true;
+ }
+ }
+ }
+ }
+
+ if (null === self::$installed) {
+ // only require the installed.php file if this file is loaded from its dumped location,
+ // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
+ if (substr(__DIR__, -8, 1) !== 'C') {
+ /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */
+ $required = require __DIR__ . '/installed.php';
+ self::$installed = $required;
+ } else {
+ self::$installed = array();
+ }
+ }
+
+ if (self::$installed !== array() && !$copiedLocalDir) {
+ $installed[] = self::$installed;
+ }
+
+ return $installed;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/composer/LICENSE b/plugins/hwp-previews/vendor/composer/LICENSE
new file mode 100644
index 0000000..f27399a
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/LICENSE
@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/plugins/hwp-previews/vendor/composer/autoload_classmap.php b/plugins/hwp-previews/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..0fb0a2c
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/autoload_classmap.php
@@ -0,0 +1,10 @@
+ $vendorDir . '/composer/InstalledVersions.php',
+);
diff --git a/plugins/hwp-previews/vendor/composer/autoload_namespaces.php b/plugins/hwp-previews/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..15a2ff3
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,9 @@
+ array($baseDir . '/src'),
+ 'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
+);
diff --git a/plugins/hwp-previews/vendor/composer/autoload_real.php b/plugins/hwp-previews/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..3d6ac63
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/autoload_real.php
@@ -0,0 +1,38 @@
+register(true);
+
+ return $loader;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/composer/autoload_static.php b/plugins/hwp-previews/vendor/composer/autoload_static.php
new file mode 100644
index 0000000..c4f7253
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/autoload_static.php
@@ -0,0 +1,44 @@
+
+ array (
+ 'HWP\\Previews\\' => 13,
+ ),
+ 'F' =>
+ array (
+ 'Firebase\\JWT\\' => 13,
+ ),
+ );
+
+ public static $prefixDirsPsr4 = array (
+ 'HWP\\Previews\\' =>
+ array (
+ 0 => __DIR__ . '/../..' . '/src',
+ ),
+ 'Firebase\\JWT\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
+ ),
+ );
+
+ public static $classMap = array (
+ 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ );
+
+ public static function getInitializer(ClassLoader $loader)
+ {
+ return \Closure::bind(function () use ($loader) {
+ $loader->prefixLengthsPsr4 = ComposerStaticInit72736c17dd331a5ebe8e0bc4565a896f::$prefixLengthsPsr4;
+ $loader->prefixDirsPsr4 = ComposerStaticInit72736c17dd331a5ebe8e0bc4565a896f::$prefixDirsPsr4;
+ $loader->classMap = ComposerStaticInit72736c17dd331a5ebe8e0bc4565a896f::$classMap;
+
+ }, null, ClassLoader::class);
+ }
+}
diff --git a/plugins/hwp-previews/vendor/composer/installed.json b/plugins/hwp-previews/vendor/composer/installed.json
new file mode 100644
index 0000000..aedce0f
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/installed.json
@@ -0,0 +1,72 @@
+{
+ "packages": [
+ {
+ "name": "firebase/php-jwt",
+ "version": "v6.11.0",
+ "version_normalized": "6.11.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/firebase/php-jwt.git",
+ "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712",
+ "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ },
+ "suggest": {
+ "ext-sodium": "Support EdDSA (Ed25519) signatures",
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
+ },
+ "time": "2025-01-23T05:11:06+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "jwt",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/firebase/php-jwt/issues",
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.0"
+ },
+ "install-path": "../firebase/php-jwt"
+ }
+ ],
+ "dev": true,
+ "dev-package-names": []
+}
diff --git a/plugins/hwp-previews/vendor/composer/installed.php b/plugins/hwp-previews/vendor/composer/installed.php
new file mode 100644
index 0000000..3fc591f
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/installed.php
@@ -0,0 +1,32 @@
+ array(
+ 'name' => 'wpengine/hwp-previews',
+ 'pretty_version' => '1.0.0+no-version-set',
+ 'version' => '1.0.0.0',
+ 'reference' => null,
+ 'type' => 'wordpress-plugin',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev' => true,
+ ),
+ 'versions' => array(
+ 'firebase/php-jwt' => array(
+ 'pretty_version' => 'v6.11.0',
+ 'version' => '6.11.0.0',
+ 'reference' => '8f718f4dfc9c5d5f0c994cdfd103921b43592712',
+ 'type' => 'library',
+ 'install_path' => __DIR__ . '/../firebase/php-jwt',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ 'wpengine/hwp-previews' => array(
+ 'pretty_version' => '1.0.0+no-version-set',
+ 'version' => '1.0.0.0',
+ 'reference' => null,
+ 'type' => 'wordpress-plugin',
+ 'install_path' => __DIR__ . '/../../',
+ 'aliases' => array(),
+ 'dev_requirement' => false,
+ ),
+ ),
+);
diff --git a/plugins/hwp-previews/vendor/composer/platform_check.php b/plugins/hwp-previews/vendor/composer/platform_check.php
new file mode 100644
index 0000000..adfb472
--- /dev/null
+++ b/plugins/hwp-previews/vendor/composer/platform_check.php
@@ -0,0 +1,26 @@
+= 80000)) {
+ $issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
+}
+
+if ($issues) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
+ } elseif (!headers_sent()) {
+ echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
+ }
+ }
+ trigger_error(
+ 'Composer detected issues in your platform: ' . implode(' ', $issues),
+ E_USER_ERROR
+ );
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/CHANGELOG.md b/plugins/hwp-previews/vendor/firebase/php-jwt/CHANGELOG.md
new file mode 100644
index 0000000..01fcc07
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/CHANGELOG.md
@@ -0,0 +1,198 @@
+# Changelog
+
+## [6.11.0](https://github.com/firebase/php-jwt/compare/v6.10.2...v6.11.0) (2025-01-23)
+
+
+### Features
+
+* support octet typed JWK ([#587](https://github.com/firebase/php-jwt/issues/587)) ([7cb8a26](https://github.com/firebase/php-jwt/commit/7cb8a265fa81edf2fa6ef8098f5bc5ae573c33ad))
+
+
+### Bug Fixes
+
+* refactor constructor Key to use PHP 8.0 syntax ([#577](https://github.com/firebase/php-jwt/issues/577)) ([29fa2ce](https://github.com/firebase/php-jwt/commit/29fa2ce9e0582cd397711eec1e80c05ce20fabca))
+
+## [6.10.2](https://github.com/firebase/php-jwt/compare/v6.10.1...v6.10.2) (2024-11-24)
+
+
+### Bug Fixes
+
+* Mitigate PHP8.4 deprecation warnings ([#570](https://github.com/firebase/php-jwt/issues/570)) ([76808fa](https://github.com/firebase/php-jwt/commit/76808fa227f3811aa5cdb3bf81233714b799a5b5))
+* support php 8.4 ([#583](https://github.com/firebase/php-jwt/issues/583)) ([e3d68b0](https://github.com/firebase/php-jwt/commit/e3d68b044421339443c74199edd020e03fb1887e))
+
+## [6.10.1](https://github.com/firebase/php-jwt/compare/v6.10.0...v6.10.1) (2024-05-18)
+
+
+### Bug Fixes
+
+* ensure ratelimit expiry is set every time ([#556](https://github.com/firebase/php-jwt/issues/556)) ([09cb208](https://github.com/firebase/php-jwt/commit/09cb2081c2c3bc0f61e2f2a5fbea5741f7498648))
+* ratelimit cache expiration ([#550](https://github.com/firebase/php-jwt/issues/550)) ([dda7250](https://github.com/firebase/php-jwt/commit/dda725033585ece30ff8cae8937320d7e9f18bae))
+
+## [6.10.0](https://github.com/firebase/php-jwt/compare/v6.9.0...v6.10.0) (2023-11-28)
+
+
+### Features
+
+* allow typ header override ([#546](https://github.com/firebase/php-jwt/issues/546)) ([79cb30b](https://github.com/firebase/php-jwt/commit/79cb30b729a22931b2fbd6b53f20629a83031ba9))
+
+## [6.9.0](https://github.com/firebase/php-jwt/compare/v6.8.1...v6.9.0) (2023-10-04)
+
+
+### Features
+
+* add payload to jwt exception ([#521](https://github.com/firebase/php-jwt/issues/521)) ([175edf9](https://github.com/firebase/php-jwt/commit/175edf958bb61922ec135b2333acf5622f2238a2))
+
+## [6.8.1](https://github.com/firebase/php-jwt/compare/v6.8.0...v6.8.1) (2023-07-14)
+
+
+### Bug Fixes
+
+* accept float claims but round down to ignore them ([#492](https://github.com/firebase/php-jwt/issues/492)) ([3936842](https://github.com/firebase/php-jwt/commit/39368423beeaacb3002afa7dcb75baebf204fe7e))
+* different BeforeValidException messages for nbf and iat ([#526](https://github.com/firebase/php-jwt/issues/526)) ([0a53cf2](https://github.com/firebase/php-jwt/commit/0a53cf2986e45c2bcbf1a269f313ebf56a154ee4))
+
+## [6.8.0](https://github.com/firebase/php-jwt/compare/v6.7.0...v6.8.0) (2023-06-14)
+
+
+### Features
+
+* add support for P-384 curve ([#515](https://github.com/firebase/php-jwt/issues/515)) ([5de4323](https://github.com/firebase/php-jwt/commit/5de4323f4baf4d70bca8663bd87682a69c656c3d))
+
+
+### Bug Fixes
+
+* handle invalid http responses ([#508](https://github.com/firebase/php-jwt/issues/508)) ([91c39c7](https://github.com/firebase/php-jwt/commit/91c39c72b22fc3e1191e574089552c1f2041c718))
+
+## [6.7.0](https://github.com/firebase/php-jwt/compare/v6.6.0...v6.7.0) (2023-06-14)
+
+
+### Features
+
+* add ed25519 support to JWK (public keys) ([#452](https://github.com/firebase/php-jwt/issues/452)) ([e53979a](https://github.com/firebase/php-jwt/commit/e53979abae927de916a75b9d239cfda8ce32be2a))
+
+## [6.6.0](https://github.com/firebase/php-jwt/compare/v6.5.0...v6.6.0) (2023-06-13)
+
+
+### Features
+
+* allow get headers when decoding token ([#442](https://github.com/firebase/php-jwt/issues/442)) ([fb85f47](https://github.com/firebase/php-jwt/commit/fb85f47cfaeffdd94faf8defdf07164abcdad6c3))
+
+
+### Bug Fixes
+
+* only check iat if nbf is not used ([#493](https://github.com/firebase/php-jwt/issues/493)) ([398ccd2](https://github.com/firebase/php-jwt/commit/398ccd25ea12fa84b9e4f1085d5ff448c21ec797))
+
+## [6.5.0](https://github.com/firebase/php-jwt/compare/v6.4.0...v6.5.0) (2023-05-12)
+
+
+### Bug Fixes
+
+* allow KID of '0' ([#505](https://github.com/firebase/php-jwt/issues/505)) ([9dc46a9](https://github.com/firebase/php-jwt/commit/9dc46a9c3e5801294249cfd2554c5363c9f9326a))
+
+
+### Miscellaneous Chores
+
+* drop support for PHP 7.3 ([#495](https://github.com/firebase/php-jwt/issues/495))
+
+## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08)
+
+
+### Features
+
+* add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95))
+* improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c))
+
+## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01)
+
+
+### Bug Fixes
+
+* check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd))
+
+## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01)
+
+
+### Bug Fixes
+
+* casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22))
+* string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2))
+
+## 6.3.0 / 2022-07-15
+
+ - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399))
+ - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435))
+
+## 6.2.0 / 2022-05-14
+
+ - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397))
+ - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)).
+
+## 6.1.0 / 2022-03-23
+
+ - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0
+ - Add parameter typing and return types where possible
+
+## 6.0.0 / 2022-01-24
+
+ - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information.
+ - New Key object to prevent key/algorithm type confusion (#365)
+ - Add JWK support (#273)
+ - Add ES256 support (#256)
+ - Add ES384 support (#324)
+ - Add Ed25519 support (#343)
+
+## 5.0.0 / 2017-06-26
+- Support RS384 and RS512.
+ See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)!
+- Add an example for RS256 openssl.
+ See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)!
+- Detect invalid Base64 encoding in signature.
+ See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)!
+- Update `JWT::verify` to handle OpenSSL errors.
+ See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)!
+- Add `array` type hinting to `decode` method
+ See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)!
+- Add all JSON error types.
+ See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)!
+- Bugfix 'kid' not in given key list.
+ See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)!
+- Miscellaneous cleanup, documentation and test fixes.
+ See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115),
+ [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and
+ [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman),
+ [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)!
+
+## 4.0.0 / 2016-07-17
+- Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)!
+- Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)!
+- Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)!
+- Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)!
+
+## 3.0.0 / 2015-07-22
+- Minimum PHP version updated from `5.2.0` to `5.3.0`.
+- Add `\Firebase\JWT` namespace. See
+[#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to
+[@Dashron](https://github.com/Dashron)!
+- Require a non-empty key to decode and verify a JWT. See
+[#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to
+[@sjones608](https://github.com/sjones608)!
+- Cleaner documentation blocks in the code. See
+[#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to
+[@johanderuijter](https://github.com/johanderuijter)!
+
+## 2.2.0 / 2015-06-22
+- Add support for adding custom, optional JWT headers to `JWT::encode()`. See
+[#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to
+[@mcocaro](https://github.com/mcocaro)!
+
+## 2.1.0 / 2015-05-20
+- Add support for adding a leeway to `JWT:decode()` that accounts for clock skew
+between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)!
+- Add support for passing an object implementing the `ArrayAccess` interface for
+`$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)!
+
+## 2.0.0 / 2015-04-01
+- **Note**: It is strongly recommended that you update to > v2.0.0 to address
+ known security vulnerabilities in prior versions when both symmetric and
+ asymmetric keys are used together.
+- Update signature for `JWT::decode(...)` to require an array of supported
+ algorithms to use when verifying token signatures.
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/LICENSE b/plugins/hwp-previews/vendor/firebase/php-jwt/LICENSE
new file mode 100644
index 0000000..11c0146
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2011, Neuman Vong
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * Neither the name of the copyright holder nor the names of other
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/README.md b/plugins/hwp-previews/vendor/firebase/php-jwt/README.md
new file mode 100644
index 0000000..0425269
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/README.md
@@ -0,0 +1,425 @@
+
+[](https://packagist.org/packages/firebase/php-jwt)
+[](https://packagist.org/packages/firebase/php-jwt)
+[](https://packagist.org/packages/firebase/php-jwt)
+
+PHP-JWT
+=======
+A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to [RFC 7519](https://tools.ietf.org/html/rfc7519).
+
+Installation
+------------
+
+Use composer to manage your dependencies and download PHP-JWT:
+
+```bash
+composer require firebase/php-jwt
+```
+
+Optionally, install the `paragonie/sodium_compat` package from composer if your
+php env does not have libsodium installed:
+
+```bash
+composer require paragonie/sodium_compat
+```
+
+Example
+-------
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+$key = 'example_key';
+$payload = [
+ 'iss' => 'http://example.org',
+ 'aud' => 'http://example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+/**
+ * IMPORTANT:
+ * You must specify supported algorithms for your application. See
+ * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
+ * for a list of spec-compliant algorithms.
+ */
+$jwt = JWT::encode($payload, $key, 'HS256');
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
+print_r($decoded);
+
+// Pass a stdClass in as the third parameter to get the decoded header values
+$headers = new stdClass();
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'), $headers);
+print_r($headers);
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+
+/**
+ * You can add a leeway to account for when there is a clock skew times between
+ * the signing and verifying servers. It is recommended that this leeway should
+ * not be bigger than a few minutes.
+ *
+ * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef
+ */
+JWT::$leeway = 60; // $leeway in seconds
+$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
+```
+Example encode/decode headers
+-------
+Decoding the JWT headers without verifying the JWT first is NOT recommended, and is not supported by
+this library. This is because without verifying the JWT, the header values could have been tampered with.
+Any value pulled from an unverified header should be treated as if it could be any string sent in from an
+attacker. If this is something you still want to do in your application for whatever reason, it's possible to
+decode the header values manually simply by calling `json_decode` and `base64_decode` on the JWT
+header part:
+```php
+use Firebase\JWT\JWT;
+
+$key = 'example_key';
+$payload = [
+ 'iss' => 'http://example.org',
+ 'aud' => 'http://example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+$headers = [
+ 'x-forwarded-for' => 'www.google.com'
+];
+
+// Encode headers in the JWT string
+$jwt = JWT::encode($payload, $key, 'HS256', null, $headers);
+
+// Decode headers from the JWT string WITHOUT validation
+// **IMPORTANT**: This operation is vulnerable to attacks, as the JWT has not yet been verified.
+// These headers could be any value sent by an attacker.
+list($headersB64, $payloadB64, $sig) = explode('.', $jwt);
+$decoded = json_decode(base64_decode($headersB64), true);
+
+print_r($decoded);
+```
+Example with RS256 (openssl)
+----------------------------
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+$privateKey = << 'example.org',
+ 'aud' => 'example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'RS256');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
+
+/*
+ NOTE: This will now be an object instead of an associative array. To get
+ an associative array, you will need to cast it as such:
+*/
+
+$decoded_array = (array) $decoded;
+echo "Decode:\n" . print_r($decoded_array, true) . "\n";
+```
+
+Example with a passphrase
+-------------------------
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Your passphrase
+$passphrase = '[YOUR_PASSPHRASE]';
+
+// Your private key file with passphrase
+// Can be generated with "ssh-keygen -t rsa -m pem"
+$privateKeyFile = '/path/to/key-with-passphrase.pem';
+
+// Create a private key of type "resource"
+$privateKey = openssl_pkey_get_private(
+ file_get_contents($privateKeyFile),
+ $passphrase
+);
+
+$payload = [
+ 'iss' => 'example.org',
+ 'aud' => 'example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'RS256');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+// Get public key from the private key, or pull from from a file.
+$publicKey = openssl_pkey_get_details($privateKey)['key'];
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
+echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
+```
+
+Example with EdDSA (libsodium and Ed25519 signature)
+----------------------------
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Public and private keys are expected to be Base64 encoded. The last
+// non-empty line is used so that keys can be generated with
+// sodium_crypto_sign_keypair(). The secret keys generated by other tools may
+// need to be adjusted to match the input expected by libsodium.
+
+$keyPair = sodium_crypto_sign_keypair();
+
+$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
+
+$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
+
+$payload = [
+ 'iss' => 'example.org',
+ 'aud' => 'example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
+echo "Encode:\n" . print_r($jwt, true) . "\n";
+
+$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA'));
+echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
+````
+
+Example with multiple keys
+--------------------------
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\Key;
+
+// Example RSA keys from previous example
+// $privateKey1 = '...';
+// $publicKey1 = '...';
+
+// Example EdDSA keys from previous example
+// $privateKey2 = '...';
+// $publicKey2 = '...';
+
+$payload = [
+ 'iss' => 'example.org',
+ 'aud' => 'example.com',
+ 'iat' => 1356999524,
+ 'nbf' => 1357000000
+];
+
+$jwt1 = JWT::encode($payload, $privateKey1, 'RS256', 'kid1');
+$jwt2 = JWT::encode($payload, $privateKey2, 'EdDSA', 'kid2');
+echo "Encode 1:\n" . print_r($jwt1, true) . "\n";
+echo "Encode 2:\n" . print_r($jwt2, true) . "\n";
+
+$keys = [
+ 'kid1' => new Key($publicKey1, 'RS256'),
+ 'kid2' => new Key($publicKey2, 'EdDSA'),
+];
+
+$decoded1 = JWT::decode($jwt1, $keys);
+$decoded2 = JWT::decode($jwt2, $keys);
+
+echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
+echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
+```
+
+Using JWKs
+----------
+
+```php
+use Firebase\JWT\JWK;
+use Firebase\JWT\JWT;
+
+// Set of keys. The "keys" key is required. For example, the JSON response to
+// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk
+$jwks = ['keys' => []];
+
+// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key
+// objects. Pass this as the second parameter to JWT::decode.
+JWT::decode($payload, JWK::parseKeySet($jwks));
+```
+
+Using Cached Key Sets
+---------------------
+
+The `CachedKeySet` class can be used to fetch and cache JWKS (JSON Web Key Sets) from a public URI.
+This has the following advantages:
+
+1. The results are cached for performance.
+2. If an unrecognized key is requested, the cache is refreshed, to accomodate for key rotation.
+3. If rate limiting is enabled, the JWKS URI will not make more than 10 requests a second.
+
+```php
+use Firebase\JWT\CachedKeySet;
+use Firebase\JWT\JWT;
+
+// The URI for the JWKS you wish to cache the results from
+$jwksUri = 'https://www.gstatic.com/iap/verify/public_key-jwk';
+
+// Create an HTTP client (can be any PSR-7 compatible HTTP client)
+$httpClient = new GuzzleHttp\Client();
+
+// Create an HTTP request factory (can be any PSR-17 compatible HTTP request factory)
+$httpFactory = new GuzzleHttp\Psr\HttpFactory();
+
+// Create a cache item pool (can be any PSR-6 compatible cache item pool)
+$cacheItemPool = Phpfastcache\CacheManager::getInstance('files');
+
+$keySet = new CachedKeySet(
+ $jwksUri,
+ $httpClient,
+ $httpFactory,
+ $cacheItemPool,
+ null, // $expiresAfter int seconds to set the JWKS to expire
+ true // $rateLimit true to enable rate limit of 10 RPS on lookup of invalid keys
+);
+
+$jwt = 'eyJhbGci...'; // Some JWT signed by a key from the $jwkUri above
+$decoded = JWT::decode($jwt, $keySet);
+```
+
+Miscellaneous
+-------------
+
+#### Exception Handling
+
+When a call to `JWT::decode` is invalid, it will throw one of the following exceptions:
+
+```php
+use Firebase\JWT\JWT;
+use Firebase\JWT\SignatureInvalidException;
+use Firebase\JWT\BeforeValidException;
+use Firebase\JWT\ExpiredException;
+use DomainException;
+use InvalidArgumentException;
+use UnexpectedValueException;
+
+try {
+ $decoded = JWT::decode($payload, $keys);
+} catch (InvalidArgumentException $e) {
+ // provided key/key-array is empty or malformed.
+} catch (DomainException $e) {
+ // provided algorithm is unsupported OR
+ // provided key is invalid OR
+ // unknown error thrown in openSSL or libsodium OR
+ // libsodium is required but not available.
+} catch (SignatureInvalidException $e) {
+ // provided JWT signature verification failed.
+} catch (BeforeValidException $e) {
+ // provided JWT is trying to be used before "nbf" claim OR
+ // provided JWT is trying to be used before "iat" claim.
+} catch (ExpiredException $e) {
+ // provided JWT is trying to be used after "exp" claim.
+} catch (UnexpectedValueException $e) {
+ // provided JWT is malformed OR
+ // provided JWT is missing an algorithm / using an unsupported algorithm OR
+ // provided JWT algorithm does not match provided key OR
+ // provided key ID in key/key-array is empty or invalid.
+}
+```
+
+All exceptions in the `Firebase\JWT` namespace extend `UnexpectedValueException`, and can be simplified
+like this:
+
+```php
+use Firebase\JWT\JWT;
+use UnexpectedValueException;
+try {
+ $decoded = JWT::decode($payload, $keys);
+} catch (LogicException $e) {
+ // errors having to do with environmental setup or malformed JWT Keys
+} catch (UnexpectedValueException $e) {
+ // errors having to do with JWT signature and claims
+}
+```
+
+#### Casting to array
+
+The return value of `JWT::decode` is the generic PHP object `stdClass`. If you'd like to handle with arrays
+instead, you can do the following:
+
+```php
+// return type is stdClass
+$decoded = JWT::decode($payload, $keys);
+
+// cast to array
+$decoded = json_decode(json_encode($decoded), true);
+```
+
+Tests
+-----
+Run the tests using phpunit:
+
+```bash
+$ pear install PHPUnit
+$ phpunit --configuration phpunit.xml.dist
+PHPUnit 3.7.10 by Sebastian Bergmann.
+.....
+Time: 0 seconds, Memory: 2.50Mb
+OK (5 tests, 5 assertions)
+```
+
+New Lines in private keys
+-----
+
+If your private key contains `\n` characters, be sure to wrap it in double quotes `""`
+and not single quotes `''` in order to properly interpret the escaped characters.
+
+License
+-------
+[3-Clause BSD](http://opensource.org/licenses/BSD-3-Clause).
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/composer.json b/plugins/hwp-previews/vendor/firebase/php-jwt/composer.json
new file mode 100644
index 0000000..816cfd0
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "firebase/php-jwt",
+ "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
+ "homepage": "https://github.com/firebase/php-jwt",
+ "keywords": [
+ "php",
+ "jwt"
+ ],
+ "authors": [
+ {
+ "name": "Neuman Vong",
+ "email": "neuman+pear@twilio.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Anant Narayanan",
+ "email": "anant@php.net",
+ "role": "Developer"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "require": {
+ "php": "^8.0"
+ },
+ "suggest": {
+ "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
+ "ext-sodium": "Support EdDSA (Ed25519) signatures"
+ },
+ "autoload": {
+ "psr-4": {
+ "Firebase\\JWT\\": "src"
+ }
+ },
+ "require-dev": {
+ "guzzlehttp/guzzle": "^7.4",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psr/cache": "^2.0||^3.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0"
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/BeforeValidException.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/BeforeValidException.php
new file mode 100644
index 0000000..595164b
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/BeforeValidException.php
@@ -0,0 +1,18 @@
+payload = $payload;
+ }
+
+ public function getPayload(): object
+ {
+ return $this->payload;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/CachedKeySet.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/CachedKeySet.php
new file mode 100644
index 0000000..8e8e8d6
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/CachedKeySet.php
@@ -0,0 +1,274 @@
+
+ */
+class CachedKeySet implements ArrayAccess
+{
+ /**
+ * @var string
+ */
+ private $jwksUri;
+ /**
+ * @var ClientInterface
+ */
+ private $httpClient;
+ /**
+ * @var RequestFactoryInterface
+ */
+ private $httpFactory;
+ /**
+ * @var CacheItemPoolInterface
+ */
+ private $cache;
+ /**
+ * @var ?int
+ */
+ private $expiresAfter;
+ /**
+ * @var ?CacheItemInterface
+ */
+ private $cacheItem;
+ /**
+ * @var array>
+ */
+ private $keySet;
+ /**
+ * @var string
+ */
+ private $cacheKey;
+ /**
+ * @var string
+ */
+ private $cacheKeyPrefix = 'jwks';
+ /**
+ * @var int
+ */
+ private $maxKeyLength = 64;
+ /**
+ * @var bool
+ */
+ private $rateLimit;
+ /**
+ * @var string
+ */
+ private $rateLimitCacheKey;
+ /**
+ * @var int
+ */
+ private $maxCallsPerMinute = 10;
+ /**
+ * @var string|null
+ */
+ private $defaultAlg;
+
+ public function __construct(
+ string $jwksUri,
+ ClientInterface $httpClient,
+ RequestFactoryInterface $httpFactory,
+ CacheItemPoolInterface $cache,
+ ?int $expiresAfter = null,
+ bool $rateLimit = false,
+ ?string $defaultAlg = null
+ ) {
+ $this->jwksUri = $jwksUri;
+ $this->httpClient = $httpClient;
+ $this->httpFactory = $httpFactory;
+ $this->cache = $cache;
+ $this->expiresAfter = $expiresAfter;
+ $this->rateLimit = $rateLimit;
+ $this->defaultAlg = $defaultAlg;
+ $this->setCacheKeys();
+ }
+
+ /**
+ * @param string $keyId
+ * @return Key
+ */
+ public function offsetGet($keyId): Key
+ {
+ if (!$this->keyIdExists($keyId)) {
+ throw new OutOfBoundsException('Key ID not found');
+ }
+ return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg);
+ }
+
+ /**
+ * @param string $keyId
+ * @return bool
+ */
+ public function offsetExists($keyId): bool
+ {
+ return $this->keyIdExists($keyId);
+ }
+
+ /**
+ * @param string $offset
+ * @param Key $value
+ */
+ public function offsetSet($offset, $value): void
+ {
+ throw new LogicException('Method not implemented');
+ }
+
+ /**
+ * @param string $offset
+ */
+ public function offsetUnset($offset): void
+ {
+ throw new LogicException('Method not implemented');
+ }
+
+ /**
+ * @return array
+ */
+ private function formatJwksForCache(string $jwks): array
+ {
+ $jwks = json_decode($jwks, true);
+
+ if (!isset($jwks['keys'])) {
+ throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+ }
+
+ if (empty($jwks['keys'])) {
+ throw new InvalidArgumentException('JWK Set did not contain any keys');
+ }
+
+ $keys = [];
+ foreach ($jwks['keys'] as $k => $v) {
+ $kid = isset($v['kid']) ? $v['kid'] : $k;
+ $keys[(string) $kid] = $v;
+ }
+
+ return $keys;
+ }
+
+ private function keyIdExists(string $keyId): bool
+ {
+ if (null === $this->keySet) {
+ $item = $this->getCacheItem();
+ // Try to load keys from cache
+ if ($item->isHit()) {
+ // item found! retrieve it
+ $this->keySet = $item->get();
+ // If the cached item is a string, the JWKS response was cached (previous behavior).
+ // Parse this into expected format array instead.
+ if (\is_string($this->keySet)) {
+ $this->keySet = $this->formatJwksForCache($this->keySet);
+ }
+ }
+ }
+
+ if (!isset($this->keySet[$keyId])) {
+ if ($this->rateLimitExceeded()) {
+ return false;
+ }
+ $request = $this->httpFactory->createRequest('GET', $this->jwksUri);
+ $jwksResponse = $this->httpClient->sendRequest($request);
+ if ($jwksResponse->getStatusCode() !== 200) {
+ throw new UnexpectedValueException(
+ \sprintf('HTTP Error: %d %s for URI "%s"',
+ $jwksResponse->getStatusCode(),
+ $jwksResponse->getReasonPhrase(),
+ $this->jwksUri,
+ ),
+ $jwksResponse->getStatusCode()
+ );
+ }
+ $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody());
+
+ if (!isset($this->keySet[$keyId])) {
+ return false;
+ }
+
+ $item = $this->getCacheItem();
+ $item->set($this->keySet);
+ if ($this->expiresAfter) {
+ $item->expiresAfter($this->expiresAfter);
+ }
+ $this->cache->save($item);
+ }
+
+ return true;
+ }
+
+ private function rateLimitExceeded(): bool
+ {
+ if (!$this->rateLimit) {
+ return false;
+ }
+
+ $cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
+
+ $cacheItemData = [];
+ if ($cacheItem->isHit() && \is_array($data = $cacheItem->get())) {
+ $cacheItemData = $data;
+ }
+
+ $callsPerMinute = $cacheItemData['callsPerMinute'] ?? 0;
+ $expiry = $cacheItemData['expiry'] ?? new \DateTime('+60 seconds', new \DateTimeZone('UTC'));
+
+ if (++$callsPerMinute > $this->maxCallsPerMinute) {
+ return true;
+ }
+
+ $cacheItem->set(['expiry' => $expiry, 'callsPerMinute' => $callsPerMinute]);
+ $cacheItem->expiresAt($expiry);
+ $this->cache->save($cacheItem);
+ return false;
+ }
+
+ private function getCacheItem(): CacheItemInterface
+ {
+ if (\is_null($this->cacheItem)) {
+ $this->cacheItem = $this->cache->getItem($this->cacheKey);
+ }
+
+ return $this->cacheItem;
+ }
+
+ private function setCacheKeys(): void
+ {
+ if (empty($this->jwksUri)) {
+ throw new RuntimeException('JWKS URI is empty');
+ }
+
+ // ensure we do not have illegal characters
+ $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
+
+ // add prefix
+ $key = $this->cacheKeyPrefix . $key;
+
+ // Hash keys if they exceed $maxKeyLength of 64
+ if (\strlen($key) > $this->maxKeyLength) {
+ $key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
+ }
+
+ $this->cacheKey = $key;
+
+ if ($this->rateLimit) {
+ // add prefix
+ $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
+
+ // Hash keys if they exceed $maxKeyLength of 64
+ if (\strlen($rateLimitKey) > $this->maxKeyLength) {
+ $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
+ }
+
+ $this->rateLimitCacheKey = $rateLimitKey;
+ }
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/ExpiredException.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/ExpiredException.php
new file mode 100644
index 0000000..12fef09
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/ExpiredException.php
@@ -0,0 +1,18 @@
+payload = $payload;
+ }
+
+ public function getPayload(): object
+ {
+ return $this->payload;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWK.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWK.php
new file mode 100644
index 0000000..405dcc4
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWK.php
@@ -0,0 +1,355 @@
+
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+class JWK
+{
+ private const OID = '1.2.840.10045.2.1';
+ private const ASN1_OBJECT_IDENTIFIER = 0x06;
+ private const ASN1_SEQUENCE = 0x10; // also defined in JWT
+ private const ASN1_BIT_STRING = 0x03;
+ private const EC_CURVES = [
+ 'P-256' => '1.2.840.10045.3.1.7', // Len: 64
+ 'secp256k1' => '1.3.132.0.10', // Len: 64
+ 'P-384' => '1.3.132.0.34', // Len: 96
+ // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
+ ];
+
+ // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
+ // This library supports the following subtypes:
+ private const OKP_SUBTYPES = [
+ 'Ed25519' => true, // RFC 8037
+ ];
+
+ /**
+ * Parse a set of JWK keys
+ *
+ * @param array $jwks The JSON Web Key Set as an associative array
+ * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
+ * JSON Web Key Set
+ *
+ * @return array An associative array of key IDs (kid) to Key objects
+ *
+ * @throws InvalidArgumentException Provided JWK Set is empty
+ * @throws UnexpectedValueException Provided JWK Set was invalid
+ * @throws DomainException OpenSSL failure
+ *
+ * @uses parseKey
+ */
+ public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array
+ {
+ $keys = [];
+
+ if (!isset($jwks['keys'])) {
+ throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
+ }
+
+ if (empty($jwks['keys'])) {
+ throw new InvalidArgumentException('JWK Set did not contain any keys');
+ }
+
+ foreach ($jwks['keys'] as $k => $v) {
+ $kid = isset($v['kid']) ? $v['kid'] : $k;
+ if ($key = self::parseKey($v, $defaultAlg)) {
+ $keys[(string) $kid] = $key;
+ }
+ }
+
+ if (0 === \count($keys)) {
+ throw new UnexpectedValueException('No supported algorithms found in JWK Set');
+ }
+
+ return $keys;
+ }
+
+ /**
+ * Parse a JWK key
+ *
+ * @param array $jwk An individual JWK
+ * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
+ * JSON Web Key Set
+ *
+ * @return Key The key object for the JWK
+ *
+ * @throws InvalidArgumentException Provided JWK is empty
+ * @throws UnexpectedValueException Provided JWK was invalid
+ * @throws DomainException OpenSSL failure
+ *
+ * @uses createPemFromModulusAndExponent
+ */
+ public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key
+ {
+ if (empty($jwk)) {
+ throw new InvalidArgumentException('JWK must not be empty');
+ }
+
+ if (!isset($jwk['kty'])) {
+ throw new UnexpectedValueException('JWK must contain a "kty" parameter');
+ }
+
+ if (!isset($jwk['alg'])) {
+ if (\is_null($defaultAlg)) {
+ // The "alg" parameter is optional in a KTY, but an algorithm is required
+ // for parsing in this library. Use the $defaultAlg parameter when parsing the
+ // key set in order to prevent this error.
+ // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
+ throw new UnexpectedValueException('JWK must contain an "alg" parameter');
+ }
+ $jwk['alg'] = $defaultAlg;
+ }
+
+ switch ($jwk['kty']) {
+ case 'RSA':
+ if (!empty($jwk['d'])) {
+ throw new UnexpectedValueException('RSA private keys are not supported');
+ }
+ if (!isset($jwk['n']) || !isset($jwk['e'])) {
+ throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
+ }
+
+ $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
+ $publicKey = \openssl_pkey_get_public($pem);
+ if (false === $publicKey) {
+ throw new DomainException(
+ 'OpenSSL error: ' . \openssl_error_string()
+ );
+ }
+ return new Key($publicKey, $jwk['alg']);
+ case 'EC':
+ if (isset($jwk['d'])) {
+ // The key is actually a private key
+ throw new UnexpectedValueException('Key data must be for a public key');
+ }
+
+ if (empty($jwk['crv'])) {
+ throw new UnexpectedValueException('crv not set');
+ }
+
+ if (!isset(self::EC_CURVES[$jwk['crv']])) {
+ throw new DomainException('Unrecognised or unsupported EC curve');
+ }
+
+ if (empty($jwk['x']) || empty($jwk['y'])) {
+ throw new UnexpectedValueException('x and y not set');
+ }
+
+ $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
+ return new Key($publicKey, $jwk['alg']);
+ case 'OKP':
+ if (isset($jwk['d'])) {
+ // The key is actually a private key
+ throw new UnexpectedValueException('Key data must be for a public key');
+ }
+
+ if (!isset($jwk['crv'])) {
+ throw new UnexpectedValueException('crv not set');
+ }
+
+ if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
+ throw new DomainException('Unrecognised or unsupported OKP key subtype');
+ }
+
+ if (empty($jwk['x'])) {
+ throw new UnexpectedValueException('x not set');
+ }
+
+ // This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
+ $publicKey = JWT::convertBase64urlToBase64($jwk['x']);
+ return new Key($publicKey, $jwk['alg']);
+ case 'oct':
+ if (!isset($jwk['k'])) {
+ throw new UnexpectedValueException('k not set');
+ }
+
+ return new Key(JWT::urlsafeB64Decode($jwk['k']), $jwk['alg']);
+ default:
+ break;
+ }
+
+ return null;
+ }
+
+ /**
+ * Converts the EC JWK values to pem format.
+ *
+ * @param string $crv The EC curve (only P-256 & P-384 is supported)
+ * @param string $x The EC x-coordinate
+ * @param string $y The EC y-coordinate
+ *
+ * @return string
+ */
+ private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
+ {
+ $pem =
+ self::encodeDER(
+ self::ASN1_SEQUENCE,
+ self::encodeDER(
+ self::ASN1_SEQUENCE,
+ self::encodeDER(
+ self::ASN1_OBJECT_IDENTIFIER,
+ self::encodeOID(self::OID)
+ )
+ . self::encodeDER(
+ self::ASN1_OBJECT_IDENTIFIER,
+ self::encodeOID(self::EC_CURVES[$crv])
+ )
+ ) .
+ self::encodeDER(
+ self::ASN1_BIT_STRING,
+ \chr(0x00) . \chr(0x04)
+ . JWT::urlsafeB64Decode($x)
+ . JWT::urlsafeB64Decode($y)
+ )
+ );
+
+ return \sprintf(
+ "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
+ wordwrap(base64_encode($pem), 64, "\n", true)
+ );
+ }
+
+ /**
+ * Create a public key represented in PEM format from RSA modulus and exponent information
+ *
+ * @param string $n The RSA modulus encoded in Base64
+ * @param string $e The RSA exponent encoded in Base64
+ *
+ * @return string The RSA public key represented in PEM format
+ *
+ * @uses encodeLength
+ */
+ private static function createPemFromModulusAndExponent(
+ string $n,
+ string $e
+ ): string {
+ $mod = JWT::urlsafeB64Decode($n);
+ $exp = JWT::urlsafeB64Decode($e);
+
+ $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
+ $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
+
+ $rsaPublicKey = \pack(
+ 'Ca*a*a*',
+ 48,
+ self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
+ $modulus,
+ $publicExponent
+ );
+
+ // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
+ $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
+ $rsaPublicKey = \chr(0) . $rsaPublicKey;
+ $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
+
+ $rsaPublicKey = \pack(
+ 'Ca*a*',
+ 48,
+ self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
+ $rsaOID . $rsaPublicKey
+ );
+
+ return "-----BEGIN PUBLIC KEY-----\r\n" .
+ \chunk_split(\base64_encode($rsaPublicKey), 64) .
+ '-----END PUBLIC KEY-----';
+ }
+
+ /**
+ * DER-encode the length
+ *
+ * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
+ * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
+ *
+ * @param int $length
+ * @return string
+ */
+ private static function encodeLength(int $length): string
+ {
+ if ($length <= 0x7F) {
+ return \chr($length);
+ }
+
+ $temp = \ltrim(\pack('N', $length), \chr(0));
+
+ return \pack('Ca*', 0x80 | \strlen($temp), $temp);
+ }
+
+ /**
+ * Encodes a value into a DER object.
+ * Also defined in Firebase\JWT\JWT
+ *
+ * @param int $type DER tag
+ * @param string $value the value to encode
+ * @return string the encoded object
+ */
+ private static function encodeDER(int $type, string $value): string
+ {
+ $tag_header = 0;
+ if ($type === self::ASN1_SEQUENCE) {
+ $tag_header |= 0x20;
+ }
+
+ // Type
+ $der = \chr($tag_header | $type);
+
+ // Length
+ $der .= \chr(\strlen($value));
+
+ return $der . $value;
+ }
+
+ /**
+ * Encodes a string into a DER-encoded OID.
+ *
+ * @param string $oid the OID string
+ * @return string the binary DER-encoded OID
+ */
+ private static function encodeOID(string $oid): string
+ {
+ $octets = explode('.', $oid);
+
+ // Get the first octet
+ $first = (int) array_shift($octets);
+ $second = (int) array_shift($octets);
+ $oid = \chr($first * 40 + $second);
+
+ // Iterate over subsequent octets
+ foreach ($octets as $octet) {
+ if ($octet == 0) {
+ $oid .= \chr(0x00);
+ continue;
+ }
+ $bin = '';
+
+ while ($octet) {
+ $bin .= \chr(0x80 | ($octet & 0x7f));
+ $octet >>= 7;
+ }
+ $bin[0] = $bin[0] & \chr(0x7f);
+
+ // Convert to big endian if necessary
+ if (pack('V', 65534) == pack('L', 65534)) {
+ $oid .= strrev($bin);
+ } else {
+ $oid .= $bin;
+ }
+ }
+
+ return $oid;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWT.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWT.php
new file mode 100644
index 0000000..dd9292a
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWT.php
@@ -0,0 +1,667 @@
+
+ * @author Anant Narayanan
+ * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
+ * @link https://github.com/firebase/php-jwt
+ */
+class JWT
+{
+ private const ASN1_INTEGER = 0x02;
+ private const ASN1_SEQUENCE = 0x10;
+ private const ASN1_BIT_STRING = 0x03;
+
+ /**
+ * When checking nbf, iat or expiration times,
+ * we want to provide some extra leeway time to
+ * account for clock skew.
+ *
+ * @var int
+ */
+ public static $leeway = 0;
+
+ /**
+ * Allow the current timestamp to be specified.
+ * Useful for fixing a value within unit testing.
+ * Will default to PHP time() value if null.
+ *
+ * @var ?int
+ */
+ public static $timestamp = null;
+
+ /**
+ * @var array
+ */
+ public static $supported_algs = [
+ 'ES384' => ['openssl', 'SHA384'],
+ 'ES256' => ['openssl', 'SHA256'],
+ 'ES256K' => ['openssl', 'SHA256'],
+ 'HS256' => ['hash_hmac', 'SHA256'],
+ 'HS384' => ['hash_hmac', 'SHA384'],
+ 'HS512' => ['hash_hmac', 'SHA512'],
+ 'RS256' => ['openssl', 'SHA256'],
+ 'RS384' => ['openssl', 'SHA384'],
+ 'RS512' => ['openssl', 'SHA512'],
+ 'EdDSA' => ['sodium_crypto', 'EdDSA'],
+ ];
+
+ /**
+ * Decodes a JWT string into a PHP object.
+ *
+ * @param string $jwt The JWT
+ * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs
+ * (kid) to Key objects.
+ * If the algorithm used is asymmetric, this is
+ * the public key.
+ * Each Key object contains an algorithm and
+ * matching key.
+ * Supported algorithms are 'ES384','ES256',
+ * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384'
+ * and 'RS512'.
+ * @param stdClass $headers Optional. Populates stdClass with headers.
+ *
+ * @return stdClass The JWT's payload as a PHP object
+ *
+ * @throws InvalidArgumentException Provided key/key-array was empty or malformed
+ * @throws DomainException Provided JWT is malformed
+ * @throws UnexpectedValueException Provided JWT was invalid
+ * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
+ * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
+ * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
+ * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
+ *
+ * @uses jsonDecode
+ * @uses urlsafeB64Decode
+ */
+ public static function decode(
+ string $jwt,
+ $keyOrKeyArray,
+ ?stdClass &$headers = null
+ ): stdClass {
+ // Validate JWT
+ $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
+
+ if (empty($keyOrKeyArray)) {
+ throw new InvalidArgumentException('Key may not be empty');
+ }
+ $tks = \explode('.', $jwt);
+ if (\count($tks) !== 3) {
+ throw new UnexpectedValueException('Wrong number of segments');
+ }
+ list($headb64, $bodyb64, $cryptob64) = $tks;
+ $headerRaw = static::urlsafeB64Decode($headb64);
+ if (null === ($header = static::jsonDecode($headerRaw))) {
+ throw new UnexpectedValueException('Invalid header encoding');
+ }
+ if ($headers !== null) {
+ $headers = $header;
+ }
+ $payloadRaw = static::urlsafeB64Decode($bodyb64);
+ if (null === ($payload = static::jsonDecode($payloadRaw))) {
+ throw new UnexpectedValueException('Invalid claims encoding');
+ }
+ if (\is_array($payload)) {
+ // prevent PHP Fatal Error in edge-cases when payload is empty array
+ $payload = (object) $payload;
+ }
+ if (!$payload instanceof stdClass) {
+ throw new UnexpectedValueException('Payload must be a JSON object');
+ }
+ $sig = static::urlsafeB64Decode($cryptob64);
+ if (empty($header->alg)) {
+ throw new UnexpectedValueException('Empty algorithm');
+ }
+ if (empty(static::$supported_algs[$header->alg])) {
+ throw new UnexpectedValueException('Algorithm not supported');
+ }
+
+ $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
+
+ // Check the algorithm
+ if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
+ // See issue #351
+ throw new UnexpectedValueException('Incorrect key for this algorithm');
+ }
+ if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) {
+ // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures
+ $sig = self::signatureToDER($sig);
+ }
+ if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
+ throw new SignatureInvalidException('Signature verification failed');
+ }
+
+ // Check the nbf if it is defined. This is the time that the
+ // token can actually be used. If it's not yet that time, abort.
+ if (isset($payload->nbf) && floor($payload->nbf) > ($timestamp + static::$leeway)) {
+ $ex = new BeforeValidException(
+ 'Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf)
+ );
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+
+ // Check that this token has been created before 'now'. This prevents
+ // using tokens that have been created for later use (and haven't
+ // correctly used the nbf claim).
+ if (!isset($payload->nbf) && isset($payload->iat) && floor($payload->iat) > ($timestamp + static::$leeway)) {
+ $ex = new BeforeValidException(
+ 'Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat)
+ );
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+
+ // Check if this token has expired.
+ if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
+ $ex = new ExpiredException('Expired token');
+ $ex->setPayload($payload);
+ throw $ex;
+ }
+
+ return $payload;
+ }
+
+ /**
+ * Converts and signs a PHP array into a JWT string.
+ *
+ * @param array $payload PHP array
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
+ * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256',
+ * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
+ * @param string $keyId
+ * @param array $head An array with header elements to attach
+ *
+ * @return string A signed JWT
+ *
+ * @uses jsonEncode
+ * @uses urlsafeB64Encode
+ */
+ public static function encode(
+ array $payload,
+ $key,
+ string $alg,
+ ?string $keyId = null,
+ ?array $head = null
+ ): string {
+ $header = ['typ' => 'JWT'];
+ if (isset($head)) {
+ $header = \array_merge($header, $head);
+ }
+ $header['alg'] = $alg;
+ if ($keyId !== null) {
+ $header['kid'] = $keyId;
+ }
+ $segments = [];
+ $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
+ $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
+ $signing_input = \implode('.', $segments);
+
+ $signature = static::sign($signing_input, $key, $alg);
+ $segments[] = static::urlsafeB64Encode($signature);
+
+ return \implode('.', $segments);
+ }
+
+ /**
+ * Sign a string with a given key and algorithm.
+ *
+ * @param string $msg The message to sign
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
+ * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
+ * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
+ *
+ * @return string An encrypted message
+ *
+ * @throws DomainException Unsupported algorithm or bad key was specified
+ */
+ public static function sign(
+ string $msg,
+ $key,
+ string $alg
+ ): string {
+ if (empty(static::$supported_algs[$alg])) {
+ throw new DomainException('Algorithm not supported');
+ }
+ list($function, $algorithm) = static::$supported_algs[$alg];
+ switch ($function) {
+ case 'hash_hmac':
+ if (!\is_string($key)) {
+ throw new InvalidArgumentException('key must be a string when using hmac');
+ }
+ return \hash_hmac($algorithm, $msg, $key, true);
+ case 'openssl':
+ $signature = '';
+ if (!\is_resource($key) && !openssl_pkey_get_private($key)) {
+ throw new DomainException('OpenSSL unable to validate key');
+ }
+ $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
+ if (!$success) {
+ throw new DomainException('OpenSSL unable to sign data');
+ }
+ if ($alg === 'ES256' || $alg === 'ES256K') {
+ $signature = self::signatureFromDER($signature, 256);
+ } elseif ($alg === 'ES384') {
+ $signature = self::signatureFromDER($signature, 384);
+ }
+ return $signature;
+ case 'sodium_crypto':
+ if (!\function_exists('sodium_crypto_sign_detached')) {
+ throw new DomainException('libsodium is not available');
+ }
+ if (!\is_string($key)) {
+ throw new InvalidArgumentException('key must be a string when using EdDSA');
+ }
+ try {
+ // The last non-empty line is used as the key.
+ $lines = array_filter(explode("\n", $key));
+ $key = base64_decode((string) end($lines));
+ if (\strlen($key) === 0) {
+ throw new DomainException('Key cannot be empty string');
+ }
+ return sodium_crypto_sign_detached($msg, $key);
+ } catch (Exception $e) {
+ throw new DomainException($e->getMessage(), 0, $e);
+ }
+ }
+
+ throw new DomainException('Algorithm not supported');
+ }
+
+ /**
+ * Verify a signature with the message, key and method. Not all methods
+ * are symmetric, so we must have a separate verify and sign method.
+ *
+ * @param string $msg The original message (header and body)
+ * @param string $signature The original signature
+ * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
+ * @param string $alg The algorithm
+ *
+ * @return bool
+ *
+ * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
+ */
+ private static function verify(
+ string $msg,
+ string $signature,
+ $keyMaterial,
+ string $alg
+ ): bool {
+ if (empty(static::$supported_algs[$alg])) {
+ throw new DomainException('Algorithm not supported');
+ }
+
+ list($function, $algorithm) = static::$supported_algs[$alg];
+ switch ($function) {
+ case 'openssl':
+ $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
+ if ($success === 1) {
+ return true;
+ }
+ if ($success === 0) {
+ return false;
+ }
+ // returns 1 on success, 0 on failure, -1 on error.
+ throw new DomainException(
+ 'OpenSSL error: ' . \openssl_error_string()
+ );
+ case 'sodium_crypto':
+ if (!\function_exists('sodium_crypto_sign_verify_detached')) {
+ throw new DomainException('libsodium is not available');
+ }
+ if (!\is_string($keyMaterial)) {
+ throw new InvalidArgumentException('key must be a string when using EdDSA');
+ }
+ try {
+ // The last non-empty line is used as the key.
+ $lines = array_filter(explode("\n", $keyMaterial));
+ $key = base64_decode((string) end($lines));
+ if (\strlen($key) === 0) {
+ throw new DomainException('Key cannot be empty string');
+ }
+ if (\strlen($signature) === 0) {
+ throw new DomainException('Signature cannot be empty string');
+ }
+ return sodium_crypto_sign_verify_detached($signature, $msg, $key);
+ } catch (Exception $e) {
+ throw new DomainException($e->getMessage(), 0, $e);
+ }
+ case 'hash_hmac':
+ default:
+ if (!\is_string($keyMaterial)) {
+ throw new InvalidArgumentException('key must be a string when using hmac');
+ }
+ $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
+ return self::constantTimeEquals($hash, $signature);
+ }
+ }
+
+ /**
+ * Decode a JSON string into a PHP object.
+ *
+ * @param string $input JSON string
+ *
+ * @return mixed The decoded JSON string
+ *
+ * @throws DomainException Provided string was invalid JSON
+ */
+ public static function jsonDecode(string $input)
+ {
+ $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
+
+ if ($errno = \json_last_error()) {
+ self::handleJsonError($errno);
+ } elseif ($obj === null && $input !== 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $obj;
+ }
+
+ /**
+ * Encode a PHP array into a JSON string.
+ *
+ * @param array $input A PHP array
+ *
+ * @return string JSON representation of the PHP array
+ *
+ * @throws DomainException Provided object could not be encoded to valid JSON
+ */
+ public static function jsonEncode(array $input): string
+ {
+ $json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
+ if ($errno = \json_last_error()) {
+ self::handleJsonError($errno);
+ } elseif ($json === 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ if ($json === false) {
+ throw new DomainException('Provided object could not be encoded to valid JSON');
+ }
+ return $json;
+ }
+
+ /**
+ * Decode a string with URL-safe Base64.
+ *
+ * @param string $input A Base64 encoded string
+ *
+ * @return string A decoded string
+ *
+ * @throws InvalidArgumentException invalid base64 characters
+ */
+ public static function urlsafeB64Decode(string $input): string
+ {
+ return \base64_decode(self::convertBase64UrlToBase64($input));
+ }
+
+ /**
+ * Convert a string in the base64url (URL-safe Base64) encoding to standard base64.
+ *
+ * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding)
+ *
+ * @return string A Base64 encoded string with standard characters (+/) and padding (=), when
+ * needed.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc4648
+ */
+ public static function convertBase64UrlToBase64(string $input): string
+ {
+ $remainder = \strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= \str_repeat('=', $padlen);
+ }
+ return \strtr($input, '-_', '+/');
+ }
+
+ /**
+ * Encode a string with URL-safe Base64.
+ *
+ * @param string $input The string you want encoded
+ *
+ * @return string The base64 encode of what you passed in
+ */
+ public static function urlsafeB64Encode(string $input): string
+ {
+ return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
+ }
+
+
+ /**
+ * Determine if an algorithm has been provided for each Key
+ *
+ * @param Key|ArrayAccess|array $keyOrKeyArray
+ * @param string|null $kid
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return Key
+ */
+ private static function getKey(
+ $keyOrKeyArray,
+ ?string $kid
+ ): Key {
+ if ($keyOrKeyArray instanceof Key) {
+ return $keyOrKeyArray;
+ }
+
+ if (empty($kid) && $kid !== '0') {
+ throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
+ }
+
+ if ($keyOrKeyArray instanceof CachedKeySet) {
+ // Skip "isset" check, as this will automatically refresh if not set
+ return $keyOrKeyArray[$kid];
+ }
+
+ if (!isset($keyOrKeyArray[$kid])) {
+ throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
+ }
+
+ return $keyOrKeyArray[$kid];
+ }
+
+ /**
+ * @param string $left The string of known length to compare against
+ * @param string $right The user-supplied string
+ * @return bool
+ */
+ public static function constantTimeEquals(string $left, string $right): bool
+ {
+ if (\function_exists('hash_equals')) {
+ return \hash_equals($left, $right);
+ }
+ $len = \min(self::safeStrlen($left), self::safeStrlen($right));
+
+ $status = 0;
+ for ($i = 0; $i < $len; $i++) {
+ $status |= (\ord($left[$i]) ^ \ord($right[$i]));
+ }
+ $status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
+
+ return ($status === 0);
+ }
+
+ /**
+ * Helper method to create a JSON error.
+ *
+ * @param int $errno An error number from json_last_error()
+ *
+ * @throws DomainException
+ *
+ * @return void
+ */
+ private static function handleJsonError(int $errno): void
+ {
+ $messages = [
+ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+ JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
+ JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+ JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
+ JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
+ ];
+ throw new DomainException(
+ isset($messages[$errno])
+ ? $messages[$errno]
+ : 'Unknown JSON error: ' . $errno
+ );
+ }
+
+ /**
+ * Get the number of bytes in cryptographic strings.
+ *
+ * @param string $str
+ *
+ * @return int
+ */
+ private static function safeStrlen(string $str): int
+ {
+ if (\function_exists('mb_strlen')) {
+ return \mb_strlen($str, '8bit');
+ }
+ return \strlen($str);
+ }
+
+ /**
+ * Convert an ECDSA signature to an ASN.1 DER sequence
+ *
+ * @param string $sig The ECDSA signature to convert
+ * @return string The encoded DER object
+ */
+ private static function signatureToDER(string $sig): string
+ {
+ // Separate the signature into r-value and s-value
+ $length = max(1, (int) (\strlen($sig) / 2));
+ list($r, $s) = \str_split($sig, $length);
+
+ // Trim leading zeros
+ $r = \ltrim($r, "\x00");
+ $s = \ltrim($s, "\x00");
+
+ // Convert r-value and s-value from unsigned big-endian integers to
+ // signed two's complement
+ if (\ord($r[0]) > 0x7f) {
+ $r = "\x00" . $r;
+ }
+ if (\ord($s[0]) > 0x7f) {
+ $s = "\x00" . $s;
+ }
+
+ return self::encodeDER(
+ self::ASN1_SEQUENCE,
+ self::encodeDER(self::ASN1_INTEGER, $r) .
+ self::encodeDER(self::ASN1_INTEGER, $s)
+ );
+ }
+
+ /**
+ * Encodes a value into a DER object.
+ *
+ * @param int $type DER tag
+ * @param string $value the value to encode
+ *
+ * @return string the encoded object
+ */
+ private static function encodeDER(int $type, string $value): string
+ {
+ $tag_header = 0;
+ if ($type === self::ASN1_SEQUENCE) {
+ $tag_header |= 0x20;
+ }
+
+ // Type
+ $der = \chr($tag_header | $type);
+
+ // Length
+ $der .= \chr(\strlen($value));
+
+ return $der . $value;
+ }
+
+ /**
+ * Encodes signature from a DER object.
+ *
+ * @param string $der binary signature in DER format
+ * @param int $keySize the number of bits in the key
+ *
+ * @return string the signature
+ */
+ private static function signatureFromDER(string $der, int $keySize): string
+ {
+ // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
+ list($offset, $_) = self::readDER($der);
+ list($offset, $r) = self::readDER($der, $offset);
+ list($offset, $s) = self::readDER($der, $offset);
+
+ // Convert r-value and s-value from signed two's compliment to unsigned
+ // big-endian integers
+ $r = \ltrim($r, "\x00");
+ $s = \ltrim($s, "\x00");
+
+ // Pad out r and s so that they are $keySize bits long
+ $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
+ $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
+
+ return $r . $s;
+ }
+
+ /**
+ * Reads binary DER-encoded data and decodes into a single object
+ *
+ * @param string $der the binary data in DER format
+ * @param int $offset the offset of the data stream containing the object
+ * to decode
+ *
+ * @return array{int, string|null} the new offset and the decoded object
+ */
+ private static function readDER(string $der, int $offset = 0): array
+ {
+ $pos = $offset;
+ $size = \strlen($der);
+ $constructed = (\ord($der[$pos]) >> 5) & 0x01;
+ $type = \ord($der[$pos++]) & 0x1f;
+
+ // Length
+ $len = \ord($der[$pos++]);
+ if ($len & 0x80) {
+ $n = $len & 0x1f;
+ $len = 0;
+ while ($n-- && $pos < $size) {
+ $len = ($len << 8) | \ord($der[$pos++]);
+ }
+ }
+
+ // Value
+ if ($type === self::ASN1_BIT_STRING) {
+ $pos++; // Skip the first contents octet (padding indicator)
+ $data = \substr($der, $pos, $len - 1);
+ $pos += $len - 1;
+ } elseif (!$constructed) {
+ $data = \substr($der, $pos, $len);
+ $pos += $len;
+ } else {
+ $data = null;
+ }
+
+ return [$pos, $data];
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
new file mode 100644
index 0000000..7933ed6
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php
@@ -0,0 +1,20 @@
+algorithm;
+ }
+
+ /**
+ * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
+ */
+ public function getKeyMaterial()
+ {
+ return $this->keyMaterial;
+ }
+}
diff --git a/plugins/hwp-previews/vendor/firebase/php-jwt/src/SignatureInvalidException.php b/plugins/hwp-previews/vendor/firebase/php-jwt/src/SignatureInvalidException.php
new file mode 100644
index 0000000..d35dee9
--- /dev/null
+++ b/plugins/hwp-previews/vendor/firebase/php-jwt/src/SignatureInvalidException.php
@@ -0,0 +1,7 @@
+
Date: Mon, 10 Mar 2025 18:03:49 +0100
Subject: [PATCH 2/3] settings logic and description changes
---
plugins/hwp-previews/src/Plugin.php | 34 +++++++++++++-----
.../Settings/Headless_Preview_Settings.php | 36 ++-----------------
.../src/Token/REST/Token_REST_Controller.php | 8 ++---
.../hwp-previews/templates/hwp-preview.php | 2 ++
4 files changed, 34 insertions(+), 46 deletions(-)
diff --git a/plugins/hwp-previews/src/Plugin.php b/plugins/hwp-previews/src/Plugin.php
index 28a9bfe..b589b06 100644
--- a/plugins/hwp-previews/src/Plugin.php
+++ b/plugins/hwp-previews/src/Plugin.php
@@ -136,7 +136,8 @@ private function register_hooks(): void {
$this->enable_post_statuses_as_parent();
}
- if ( $this->settings->get_setting( Headless_Preview_Settings::GENERATE_PREVIEW_LINKS, false ) ) {
+ // The preview url generation works only when the iframe preview disabled.
+ if ( ! $this->settings->get_setting( Headless_Preview_Settings::PREVIEW_IN_IFRAME, true ) ) {
$this->enable_generate_preview_url();
}
@@ -267,17 +268,32 @@ private function generate_preview_url( WP_Post $post ): string {
return ''; // todo: maybe do something?
}
- $token_auth = $this->settings->get_setting( Headless_Preview_Settings::TOKEN_AUTH_ENABLED, true );
- $draft_route = $this->settings->get_setting( Headless_Preview_Settings::DRAFT_ROUTE, '' );
+ $args = (array) apply_filters(
+ 'hwp_preview_args',
+ $this->parameter_builder->build_preview_args( $post, $this->generate_token() ),
+ $post
+ );
- $token = '';
- if ( $this->settings->get_setting( Headless_Preview_Settings::GENERATE_PREVIEW_TOKEN, true ) ) {
- $token = $this->token_generator->generate_token( $token_auth ? [ 'data' => [ 'user' => [ 'id' => get_current_user_id() ] ] ] : [], 'preview_url_nonce', 300 );
- }
+ return $this->url_generator->generate_url(
+ $post,
+ $preview_url,
+ $args,
+ $this->settings->get_setting( Headless_Preview_Settings::DRAFT_ROUTE, '' )
+ );
+ }
- $args = (array) apply_filters( 'hwp_preview_args', $this->parameter_builder->build_preview_args( $post, $token ), $post );
+ private function generate_token(): string {
+ if (! $this->settings->get_setting( Headless_Preview_Settings::GENERATE_PREVIEW_TOKEN, true )) {
+ return '';
+ }
- return $this->url_generator->generate_url( $post, $preview_url, $args, $draft_route );
+ return $this->token_generator->generate_token(
+ $this->settings->get_setting( Headless_Preview_Settings::TOKEN_AUTH_ENABLED, true ) ?
+ [ 'data' => [ 'user' => [ 'id' => get_current_user_id() ] ] ] :
+ [],
+ 'preview_url_nonce',
+ 300
+ );
}
public function enable_rest_route_token_verification(): void {
diff --git a/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php b/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
index 8e02ad9..ba0f4d1 100644
--- a/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
+++ b/plugins/hwp-previews/src/Settings/Headless_Preview_Settings.php
@@ -12,7 +12,6 @@ class Headless_Preview_Settings {
*/
const OPTION_NAME = 'headless_preview_settings';
- public const GENERATE_PREVIEW_LINKS = 'generate_preview_links';
public const ENABLE_UNIQUE_POST_SLUG = 'enable_unique_post_slug';
public const ENABLE_POST_STATUSES_AS_PARENT = 'enable_post_statuses_as_parent';
public const PREVIEW_URL = 'preview_url';
@@ -30,7 +29,6 @@ class Headless_Preview_Settings {
* Default settings
*/
private $defaults = [
- self::GENERATE_PREVIEW_LINKS => false,
self::PREVIEW_URL => 'http://localhost:3000',
self::ENABLE_UNIQUE_POST_SLUG => true,
self::GENERATE_PREVIEW_TOKEN => true,
@@ -107,10 +105,9 @@ public function register_settings() {
'headless-preview'
);
- // Authentication Settings Section
add_settings_section(
'auth_settings',
- 'Authentication Settings',
+ 'Security Settings',
[ $this, 'render_auth_section' ],
'headless-preview'
);
@@ -156,15 +153,6 @@ public function register_settings() {
'general_settings'
);
- // Preview Settings Fields
- add_settings_field(
- 'generate_preview_links',
- 'Generate Preview Links',
- [ $this, 'render_generate_preview_links_field' ],
- 'headless-preview',
- 'preview_settings'
- );
-
add_settings_field(
'preview_in_iframe',
'Preview in iFrame',
@@ -292,24 +280,6 @@ public function render_rest_section() {
echo '
Settings for REST API integration for token verification.
';
}
- /**
- * Render the Generate Preview Links field
- */
- public function render_generate_preview_links_field() {
- $value = isset( $this->settings['generate_preview_links'] ) ? $this->settings['generate_preview_links'] : false;
- ?>
-
-
- When enabled, WordPress's default preview links will be replaced with links to your headless frontend.
- Should be disabled by default for compatibility with standard WordPress workflows.
-
- When enabled the Preview URL filtering for the WP builtin buttons won't work.
[preview_parameter_names][token]"
value=""/>
-
Authentication token parameter
+
Security token parameter
Post Slug
@@ -657,7 +628,6 @@ public function sanitize_settings( $input ) {
// Boolean fields
$boolean_fields = [
- 'generate_preview_links',
'generate_preview_token',
'token_auth_enabled',
'preview_in_iframe',
diff --git a/plugins/hwp-previews/src/Token/REST/Token_REST_Controller.php b/plugins/hwp-previews/src/Token/REST/Token_REST_Controller.php
index da1f67e..2ba110f 100644
--- a/plugins/hwp-previews/src/Token/REST/Token_REST_Controller.php
+++ b/plugins/hwp-previews/src/Token/REST/Token_REST_Controller.php
@@ -50,7 +50,7 @@ public function route_callback( WP_REST_Request $request ): WP_REST_Response {
return new WP_REST_Response( [ 'error' => 'Body does not have valid parameters' ], 400 );
}
- $token = (string) json_decode( (string) $request->get_param( self::TOKEN_PARAMETER ) );
+ $token = (string) $request->get_param( self::TOKEN_PARAMETER );
if ( ! $token ) {
return new WP_REST_Response( [ 'error' => 'A token is required' ], 400 );
@@ -58,9 +58,9 @@ public function route_callback( WP_REST_Request $request ): WP_REST_Response {
$success = $this->verifier->verify_token( $token, $this->nonce_action );
-// if ( ! $success ) {
-// return new WP_REST_Response( [ 'valid' => false, 'error' => 'The token is not valid.' ], 500 );
-// }
+ if ( ! $success ) {
+ return new WP_REST_Response( [ 'valid' => false, 'error' => 'The token is not valid.' ], 500 );
+ }
return new WP_REST_Response( [ 'valid' => true, 'data' => [ 'success' => true ] ], 200 );
}
diff --git a/plugins/hwp-previews/templates/hwp-preview.php b/plugins/hwp-previews/templates/hwp-preview.php
index 9adefb9..2d54228 100644
--- a/plugins/hwp-previews/templates/hwp-preview.php
+++ b/plugins/hwp-previews/templates/hwp-preview.php
@@ -11,6 +11,8 @@
$preview_url = (string) get_query_var( Preview_Template_Resolver::HWP_PREVIEWS_IFRAME_PREVIEW_URL );
+var_dump($preview_url);
+
do_action( 'hwp_previews_before_get_header' );
get_header( $header_name, apply_filters( 'hwp_previews_header_args', [] ) );
From b51db9f9a1fb0e4ed76ba86e4a02728adca3f509 Mon Sep 17 00:00:00 2001
From: Alex Kazhukhouski
Date: Mon, 24 Mar 2025 14:52:37 +0100
Subject: [PATCH 3/3] Adds reflection class and preview URL parameter
mechanisms
---
.../src/Preview/Link/Preview_Link_Service.php | 57 +++++++++++++++
.../Parameters/Abstract_Preview_Parameter.php | 28 ++++++++
.../Parameters/Callback_Preview_Parameter.php | 31 ++++++++
.../Preview_Parameter_Builder_Interface.php | 13 ++++
.../Contracts/Preview_Parameter_Interface.php | 17 +++++
.../Parameters/Preview_Parameter_Builder.php | 71 +++++++++++++++++++
.../Parameters/Preview_Parameter_Factory.php | 39 ++++++++++
.../Parameters/Preview_Parameter_Registry.php | 66 +++++++++++++++++
.../Parameters/WP_Post_Property_Parameter.php | 30 ++++++++
.../Preview_URL_Generator_Interface.php | 2 +-
.../src/Preview/URL/Preview_URL_Generator.php | 3 +
.../Reflection/Class_Property_Extractor.php | 64 +++++++++++++++++
.../Class_Property_Extractor_Interface.php | 11 +++
.../Contracts/Doc_Block_Parser_Interface.php | 11 +++
.../Contracts/Property_Info_Interface.php | 15 ++++
.../Property_Type_Resolver_Interface.php | 11 +++
.../src/Reflection/Doc_Block_Parser.php | 36 ++++++++++
.../src/Reflection/Property_Info.php | 32 +++++++++
.../src/Reflection/Property_Type_Resolver.php | 43 +++++++++++
19 files changed, 579 insertions(+), 1 deletion(-)
create mode 100644 plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Abstract_Preview_Parameter.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Callback_Preview_Parameter.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Contracts/Preview_Parameter_Builder_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Contracts/Preview_Parameter_Interface.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Builder.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Factory.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Registry.php
create mode 100644 plugins/hwp-previews/src/Preview/Parameters/WP_Post_Property_Parameter.php
create mode 100644 plugins/hwp-previews/src/Reflection/Class_Property_Extractor.php
create mode 100644 plugins/hwp-previews/src/Reflection/Contracts/Class_Property_Extractor_Interface.php
create mode 100644 plugins/hwp-previews/src/Reflection/Contracts/Doc_Block_Parser_Interface.php
create mode 100644 plugins/hwp-previews/src/Reflection/Contracts/Property_Info_Interface.php
create mode 100644 plugins/hwp-previews/src/Reflection/Contracts/Property_Type_Resolver_Interface.php
create mode 100644 plugins/hwp-previews/src/Reflection/Doc_Block_Parser.php
create mode 100644 plugins/hwp-previews/src/Reflection/Property_Info.php
create mode 100644 plugins/hwp-previews/src/Reflection/Property_Type_Resolver.php
diff --git a/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php b/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php
new file mode 100644
index 0000000..9aa5f64
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Link/Preview_Link_Service.php
@@ -0,0 +1,57 @@
+types = $types;
+ $this->statuses = $statuses;
+ $this->registry = $registry;
+ }
+
+ public function generate_preview_post_link( string $preview_url, WP_Post $post, string $route = '' ): string {
+ if (
+ ! $preview_url ||
+ ! $this->types->is_post_type_applicable( $post->post_type ) ||
+ ! $this->statuses->is_post_status_applicable( $post->post_status )
+ ) {
+ return '';
+ }
+
+ $parameters = [];
+
+ foreach ( $this->registry->get_all() as $parameter ) {
+ $value = $parameter->get_value( $post );
+ if ( $value ) {
+ $parameters[ $parameter->get_name() ] = urlencode( $value );
+ }
+ }
+
+ if ( empty( $parameters ) ) {
+ return $preview_url;
+ }
+
+ if ( $route ) {
+ $preview_url = trailingslashit( $preview_url ) . $route;
+ }
+
+ return add_query_arg( $parameters, $preview_url );
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/Abstract_Preview_Parameter.php b/plugins/hwp-previews/src/Preview/Parameters/Abstract_Preview_Parameter.php
new file mode 100644
index 0000000..fa318cb
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/Abstract_Preview_Parameter.php
@@ -0,0 +1,28 @@
+name = $name;
+ $this->description = $description;
+ }
+
+ public function get_name(): string {
+ return $this->name;
+ }
+
+ public function get_description(): string {
+ return $this->description;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/Callback_Preview_Parameter.php b/plugins/hwp-previews/src/Preview/Parameters/Callback_Preview_Parameter.php
new file mode 100644
index 0000000..7f220a0
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/Callback_Preview_Parameter.php
@@ -0,0 +1,31 @@
+callback = $callback;
+ }
+
+ public function get_value( WP_Post $post ): string {
+ $value = call_user_func( $this->callback, $post );
+
+ if ( ! is_string( $value ) ) {
+ throw new InvalidArgumentException( 'Callback must return a string.' );
+ }
+
+ return $value;
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/Contracts/Preview_Parameter_Builder_Interface.php b/plugins/hwp-previews/src/Preview/Parameters/Contracts/Preview_Parameter_Builder_Interface.php
new file mode 100644
index 0000000..8e45f41
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/Contracts/Preview_Parameter_Builder_Interface.php
@@ -0,0 +1,13 @@
+parameter_names = $parameter_names;
+ }
+
+ /**
+ * @param WP_Post $post
+ * @param string $token
+ *
+ * @return array
+ */
+ public function build_preview_args( WP_Post $post, string $page_uri, string $token ): array {
+ // Add default preview param.
+ $args = [
+ $this->parameter_names->preview => 'true',
+ ];
+
+ // Add post slug param.
+ if ( $this->parameter_names->post_slug ) {
+ $args[ $this->parameter_names->post_slug ] = $post->post_name;
+ }
+
+ // Add post id param.
+ if ( $this->parameter_names->post_id ) {
+ $args[ $this->parameter_names->post_id ] = $post->ID;
+ }
+
+ // Add post type param.
+ if ( $this->parameter_names->post_type ) {
+ $args[ $this->parameter_names->post_type ] = $post->post_type;
+ }
+
+ // Add graphql single name param.
+ if ( $this->parameter_names->graphql_single ) {
+ $post_type_object = get_post_type_object( $post->post_type );
+
+ if ( ! empty( $post_type_object->graphql_single_name ) ) {
+ $args[ $this->parameter_names->graphql_single ] = ucfirst( $post_type_object->graphql_single_name );
+ }
+ }
+
+ // Add page uri param.
+ if ( $this->parameter_names->post_uri ) {
+ $page_uri = (string) get_page_uri( $post->ID );
+
+ if ($page_uri) {
+ $args[ $this->parameter_names->post_uri ] = $page_uri;
+ }
+ }
+
+ // Add token param.
+ if ( $this->parameter_names->token && $token ) {
+ $args[ $this->parameter_names->token ] = $token;
+ }
+
+ return $args;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Factory.php b/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Factory.php
new file mode 100644
index 0000000..49a4faf
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/Preview_Parameter_Factory.php
@@ -0,0 +1,39 @@
+parameters[ $parameter->get_name() ] = $parameter;
+
+ return $this;
+ }
+
+ /**
+ * Unregister a parameter.
+ *
+ * @param string $name The parameter name.
+ *
+ * @return self
+ */
+ public function unregister( string $name ): self {
+ if ( isset( $this->parameters[ $name ] ) ) {
+ unset( $this->parameters[ $name ] );
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get all registered parameters.
+ *
+ * @return Preview_Parameter_Interface[]
+ */
+ public function get_all(): array {
+ return $this->parameters;
+ }
+
+ /**
+ * Get a specific parameter by name.
+ *
+ * @param string $name The parameter name.
+ *
+ * @return Preview_Parameter_Interface|null
+ */
+ public function get( string $name ): ?Preview_Parameter_Interface {
+ return $this->parameters[ $name ] ?? null;
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/Parameters/WP_Post_Property_Parameter.php b/plugins/hwp-previews/src/Preview/Parameters/WP_Post_Property_Parameter.php
new file mode 100644
index 0000000..bb82c8a
--- /dev/null
+++ b/plugins/hwp-previews/src/Preview/Parameters/WP_Post_Property_Parameter.php
@@ -0,0 +1,30 @@
+property = $property;
+ }
+
+ public function get_value( WP_Post $post ): string {
+ if ( empty( $post->{$this->property} ) ) {
+ return '';
+ }
+
+ if ( is_array( $post->{$this->property} ) || is_object( $post->{$this->property} ) ) {
+ return (string) wp_json_encode( $post->{$this->property} );
+ }
+
+ return (string) $post->{$this->property};
+ }
+
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_URL_Generator_Interface.php b/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_URL_Generator_Interface.php
index 2ab2558..5f90455 100644
--- a/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_URL_Generator_Interface.php
+++ b/plugins/hwp-previews/src/Preview/URL/Contracts/Preview_URL_Generator_Interface.php
@@ -8,6 +8,6 @@
interface Preview_URL_Generator_Interface {
- public function generate_url( WP_Post $post, string $frontend_url, array $args, string $draft_route = '' ): string;
+ public function generate_url( WP_Post $post, string $frontend_url, string $page_uri, array $args, string $draft_route = '' ): string;
}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php b/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
index 3fb5b28..d382aa0 100644
--- a/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
+++ b/plugins/hwp-previews/src/Preview/URL/Preview_URL_Generator.php
@@ -22,6 +22,7 @@ public function __construct( Post_Types_Config_Interface $types, Post_Statuses_C
public function generate_url(
WP_Post $post,
string $frontend_url,
+ string $page_uri,
array $args,
string $draft_route = ''
): string {
@@ -36,6 +37,8 @@ public function generate_url(
// Format frontend URL to the draft route handler.
if ( $draft_route ) {
$frontend_url = trailingslashit( $frontend_url ) . $draft_route;
+ } else {
+ $frontend_url = trailingslashit( $frontend_url ) . $page_uri;
}
if ( $args ) {
diff --git a/plugins/hwp-previews/src/Reflection/Class_Property_Extractor.php b/plugins/hwp-previews/src/Reflection/Class_Property_Extractor.php
new file mode 100644
index 0000000..d3c8e38
--- /dev/null
+++ b/plugins/hwp-previews/src/Reflection/Class_Property_Extractor.php
@@ -0,0 +1,64 @@
+type_resolver = $type_resolver;
+ $this->doc_block_parser = $doc_block_parser;
+ }
+
+ /**
+ * @param class-string $class_name
+ *
+ * @return Property_Info[]
+ */
+ public function extract_public_properties( string $class_name ): array {
+ try {
+ $reflection = new ReflectionClass( $class_name );
+ } catch ( Exception $e ) {
+ error_log( $e->getMessage() );
+
+ return [];
+ }
+
+ $properties = $reflection->getProperties( ReflectionProperty::IS_PUBLIC );
+ $public_properties = [];
+
+ foreach ( $properties as $property ) {
+ if ( $property->isStatic() ) {
+ continue;
+ }
+
+ $type = $this->type_resolver->resolve_type( $property );
+ $description = $this->doc_block_parser->parse_description( $property->getDocComment() );
+
+ $property_info = new Property_Info(
+ $property->getName(),
+ $type,
+ $description
+ );
+
+ $public_properties[ $property->getName() ] = $property_info;
+ }
+
+ return $public_properties;
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Reflection/Contracts/Class_Property_Extractor_Interface.php b/plugins/hwp-previews/src/Reflection/Contracts/Class_Property_Extractor_Interface.php
new file mode 100644
index 0000000..84a624b
--- /dev/null
+++ b/plugins/hwp-previews/src/Reflection/Contracts/Class_Property_Extractor_Interface.php
@@ -0,0 +1,11 @@
+name = $name;
+ $this->type = $type;
+ $this->description = $description;
+ }
+
+ public function get_name(): string {
+ return $this->name;
+ }
+
+ public function get_type(): string {
+ return $this->type;
+ }
+
+ public function get_description(): string {
+ return $this->description;
+ }
+}
\ No newline at end of file
diff --git a/plugins/hwp-previews/src/Reflection/Property_Type_Resolver.php b/plugins/hwp-previews/src/Reflection/Property_Type_Resolver.php
new file mode 100644
index 0000000..46907b3
--- /dev/null
+++ b/plugins/hwp-previews/src/Reflection/Property_Type_Resolver.php
@@ -0,0 +1,43 @@
+resolve_native_type( $property );
+ if ( $nativeType ) {
+ return $nativeType;
+ }
+
+ return $this->resolve_doc_block_type( $property );
+ }
+
+ public function resolve_native_type( ReflectionProperty $property ): string {
+ if ( $property->hasType() ) {
+ $type = $property->getType();
+
+ return $type->getName();
+ }
+
+ return '';
+ }
+
+ private function resolve_doc_block_type( ReflectionProperty $property ): string {
+ $docComment = $property->getDocComment();
+ if ( ! $docComment ) {
+ return '';
+ }
+
+ if ( preg_match( '/@var\s+([^\s]+)/', $docComment, $matches ) ) {
+ return $matches[1];
+ }
+
+ return '';
+ }
+}
\ No newline at end of file