From 76bdf3f54b460ea4fe952cc098541e998e51da7c Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 27 Sep 2024 23:58:05 +0200 Subject: [PATCH 01/52] Autoload endpoint files --- includes/core/classes/class-autoloader.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/core/classes/class-autoloader.php b/includes/core/classes/class-autoloader.php index b1edef7f6..d2515f184 100644 --- a/includes/core/classes/class-autoloader.php +++ b/includes/core/classes/class-autoloader.php @@ -88,6 +88,7 @@ static function ( string $class_string = '' ): void { switch ( $class_type ) { case 'commands': + case 'endpoints': case 'settings': case 'traits': array_pop( $structure ); From 24ee3f83d6cd544283fd936fd58c789571977a72 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:01:32 +0200 Subject: [PATCH 02/52] NEW Endpoint Class for Custom Rewrite Rules and Query Handling --- .../core/classes/endpoints/class-endpoint.php | 513 ++++++++++++++++++ 1 file changed, 513 insertions(+) create mode 100644 includes/core/classes/endpoints/class-endpoint.php diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php new file mode 100644 index 000000000..4cffa6f03 --- /dev/null +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -0,0 +1,513 @@ +is_valid_registration( $type_name, $types, $object_type ) ) { + $this->query_var = $query_var; + $this->validation_callback = $validation_callback; + $this->types = $types; + $this->reg_ex = $reg_ex; + $this->object_type = $object_type; + + $this->setup_hooks(); + } + } + + /** + * Set up hooks for various purposes. + * + * This method adds hooks for different purposes as needed. + * + * @since 1.0.0 + * + * @return void + */ + protected function setup_hooks(): void { + + global $wp_filter; + $current_filter = current_filter(); + $current_priority = $wp_filter[ $current_filter ]->current_priority(); + + add_action( $current_filter, array( $this, 'init' ), $current_priority + 1 ); + + if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { + $feed_slug = $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' )[0]; + $action = sprintf( + 'gatherpress_load_feed_template_for_%s', + $feed_slug + ); + + // Do not hook this action multiple times. + if ( 0 < did_action( $action ) ) { + return; + } + // Hook into WordPress' feed handling to load the custom feed template. + add_action( + sprintf( + 'do_feed_%s', + $feed_slug + ), + array( $this, 'load_feed_template' ) + ); + do_action( $action ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound + } + } + + /** + * Initializes the endpoint by registering rewrite rules and handling query variables. + * + * The method generates rewrite rules for the endpoint based on the post type or taxonomy rewrite base + * and matches against the provided slugs. It also filters allowed query variables to include the custom query variable for the endpoint. + * The method hooks into the `template_redirect` action to handles template loading + * or redirecting based on the endpoint type. + * + * @since 1.0.0 + * + * @return void + */ + public function init(): void { + + // Retrieve the rewrite base (slug) for the post type or taxonomy. + $rewrite_base = $this->type_object->rewrite['slug']; + $slugs = join( '|', $this->get_slugs() ); + // Build the regular expression pattern for matching the custom endpoint URL structure. + $reg_ex_pattern = sprintf( + $this->reg_ex, + $rewrite_base, + $slugs + ); + // Define the URL structure for handling matched requests via query vars. + // Example result: 'index.php?gatherpress_event=$matches[1]&gatherpress_ext_calendar=$matches[2]'. + $rewrite_url = add_query_arg( + $this->get_rewrite_atts(), + 'index.php' + ); + + // Add the rewrite rule to WordPress. + add_rewrite_rule( $reg_ex_pattern, $rewrite_url, 'top' ); + + // Trigger a flush of rewrite rules. + $this->maybe_flush_rewrite_rules( $reg_ex_pattern, $rewrite_url ); + + // Allow the custom query variable by filtering the public query vars. + add_filter( 'query_vars', array( $this, 'allow_query_vars' ) ); + + // Handle whether to include a template or redirect the request. + add_action( 'template_redirect', array( $this, 'template_redirect' ) ); + } + + /** + * Defines the rewrite replacement attributes for the custom feed endpoint. + * + * This method defines the rewrite replacement attributes + * for the custom feed endpoint to be further processed by add_rewrite_rule(). + * + * @since 1.0.0 + * + * @return array The rewrite replacement attributes for add_rewrite_rule(). + */ + public function get_rewrite_atts(): array { + return array( + $this->type_object->name => '$matches[1]', + $this->query_var => '$matches[2]', + ); + } + + /** + * Creates a flag option to indicate that rewrite rules need to be flushed. + * + * This method checks if the generated rewrite rules already exist in the DB, + * and if its rewrite URL matches the recently generated rewrite URL. + * If any of this checks fail, the option to flush the rewrite rules will be set. + * + * This method DOES NO checks if the 'gatherpress_flush_rewrite_rules_flag' option + * exists. It just adds the option and sets it to true. This flag + * is being used to determine when rewrite rules should be flushed. + * + * @since 1.0.0 + * + * @param string $reg_ex_pattern The regular expression pattern for matching the custom endpoint URL structure. + * @param string $rewrite_url The URL structure for handling matched requests via query vars. + * @return void + */ + private function maybe_flush_rewrite_rules( string $reg_ex_pattern, string $rewrite_url ): void { + $rules = get_option( 'rewrite_rules' ); + + if ( ! isset( $rules[ $reg_ex_pattern ] ) || $rules[ $reg_ex_pattern ] !== $rewrite_url ) { + // Event_Setup->maybe_create_flush_rewrite_rules_flag // @todo maybe make this a public method ?! + // @see https://github.com/GatherPress/gatherpress/blob/3d91f2bcb30b5d02ebf459cd5a42d4f43bc05ea5/includes/core/classes/class-settings.php#L760C1-L761C63 . + add_option( 'gatherpress_flush_rewrite_rules_flag', true ); + } + } + + /** + * Validates the registration of the endpoint based on timing, object type and given endpoint types. + * + * This method ensures that: + * - The action `init` has been fired, meaning the WordPress environment is fully set up. + * - The provided object type (post type or taxonomy) is registered. + * - Rewrites are enabled for the object type (e.g., post type or taxonomy) to support custom endpoints. + * + * If the validation fails, appropriate warnings are triggered using `wp_trigger_error()`. + * + * @since 1.0.0 + * + * @param string $type_name The name of the post type or taxonomy to validate. + * @param array $types Array of endpoint types to register (redirects/templates). + * @param string $object_type The type of object ('post' or 'taxonomy'). + * @return bool Returns true if registration is valid, false otherwise. + */ + private function is_valid_registration( string $type_name, array $types, string $object_type ): bool { + + if ( 0 === did_action( 'init' ) ) { + wp_trigger_error( + __CLASS__, + 'was called too early! Run on init:11 to make all the rewrite-vodoo work.', + E_USER_WARNING + ); + return false; + } + + if ( empty( $types ) ) { + wp_trigger_error( + __CLASS__, + 'can not be called without endpoint types. Add at least one of either "Endpoint_Redirect" or "Endpoint_Template" to the list of types.', + E_USER_WARNING + ); + return false; + } + + if ( ! in_array( $object_type, array( 'post_type', 'taxonomy' ), true ) ) { + wp_trigger_error( + __CLASS__, + "called on '$type_name' doesn't work, because '$object_type' is no supported object type. Use either 'post_type' or 'taxonomy'.", + E_USER_WARNING + ); + return false; + } + + // Store the validated post type or taxonomy object for later use. + switch ( $object_type ) { + case 'taxonomy': + $this->type_object = get_taxonomy( $type_name ); + break; + + case 'post_type': + $this->type_object = get_post_type_object( $type_name ); + break; + } + + if ( ! $this->type_object instanceof WP_Post_Type && ! $this->type_object instanceof WP_Taxonomy ) { + wp_trigger_error( + __CLASS__, + "was called too early! Make sure the '$type_name' $object_type is already registered.", + E_USER_WARNING + ); + return false; + } + + if ( false === $this->type_object->rewrite ) { + wp_trigger_error( + __CLASS__, + "called on '$type_name' doesn't work, because this $object_type has rewrites disabled.", + E_USER_WARNING + ); + return false; + } + return true; + } + + /** + * Filters the query variables allowed before processing. + * + * Adds the custom query variable used by the endpoint to the list of allowed + * public query variables so that it can be recognized and used by WordPress. + * + * @since 1.0.0 + * + * @param string[] $public_query_vars The array of allowed query variable names. + * @return string[] The updated array of allowed query variable names. + */ + public function allow_query_vars( array $public_query_vars ): array { + $public_query_vars[] = $this->query_var; + return $public_query_vars; + } + + /** + * Fires before determining which template to load or whether to redirect. + * + * This method is responsible for: + * - Validating the query to ensure the endpoint is correctly matched. + * - Performing redirects if the current endpoint has associated redirects. + * - Loading a custom template if the endpoint defines one. + * + * @since 1.0.0 + * + * @see https://developer.wordpress.org/reference/hooks/template_redirect/ + * + * @return void + */ + public function template_redirect(): void { + + if ( ! $this->is_valid_query() ) { + return; + } + + // Get the currently requested endpoint from the list of registered endpoint types. + $endpoint_type = current( + wp_list_filter( + $this->types, + array( + 'slug' => get_query_var( $this->query_var ), + ) + ) + ); + $endpoint_type->activate(); + } + + /** + * Load the theme-overridable feed template from the plugin. + * + * This method ensures that a feed template is loaded when a request is made to + * a custom feed endpoint. If the theme provides an override for the feed template, + * it will be used; otherwise, the default template from the plugin is loaded. The + * method ensures that WordPress does not return a 404 for custom feed URLs. + * + * A call to any post types /feed/anything endpoint is handled by WordPress + * prior 'Endpoint_Template's template_include hook would run. + * Therefore WordPress will throw an xml'ed 404 error, + * if nothing is hooked onto the 'do_feed_anything' action. + * + * That's the reason for this method, it delivers what WordPress wants + * and re-uses the parameters provided by the class. + * + * We expect that a endpoint, that contains the /feed/ string, only has one 'Redirect_Template' attached. + * This might be wrong or short sightened, please open an issue in that case: https://github.com/GatherPress/gatherpress/issues + * + * Until then, we *just* use the first of the provided endpoint-types, + * to hook into WordPress, which should be the valid template endpoint. + * + * @since 1.0.0 + * + * @return void + */ + public function load_feed_template() { + load_template( $this->types[0]->template_include( false ) ); + } + + /** + * Checks if the current query is valid for this endpoint. + * + * This method uses the validation callback provided during construction + * to ensure that the query is valid. It also checks if the custom query + * variable is populated. + * + * @since 1.0.0 + * + * @return bool True if the query is valid, false otherwise. + */ + public function is_valid_query(): bool { + return ( $this->validation_callback )() && ! empty( get_query_var( $this->query_var ) ); + } + + /** + * Retrieves the slugs of the specified endpoint types. + * + * This method filters the `types` array to get the slugs for either a specific type of endpoint + * (e.g., `Endpoint_Redirect` or `Endpoint_Template`) or returns slugs for all types if no type + * is specified. + * + * @since 1.0.0 + * + * @param string|null $entity Optional. The class name of the endpoint type to filter by (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * If null, it retrieves slugs for all types. + * @return string[] An array of slugs for the specified or all types. + */ + protected function get_slugs( ?string $entity = null ): array { + // Determine Endpoint_Types to get slug names from. + $types = ( null === $entity ) + // All? + ? $this->types + // Or a specific type? + : array_filter( + $this->types, + function ( $type ) use ( $entity ) { + return self::is_of_class( $type, $entity ); + } + ); + return wp_list_pluck( $types, 'slug' ); + } + + /** + * Checks if the given endpoint type is an instance of the specified class. + * + * This method verifies whether the provided `$type` is an instance of the `$entity` + * class. It first checks if the `$entity` exists in the defined list of valid endpoint + * classes by calling `is_in_class()`. If the entity is valid, it further checks if the + * `$type` is an instance of that class. + * + * @since 1.0.0 + * + * @param Endpoint_Type $type The endpoint type object to check. + * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * @return bool True if the `$type` is an instance of the `$entity` class, false otherwise. + */ + private static function is_of_class( Endpoint_Type $type, string $entity ): bool { + return self::is_in_class( $entity ) && $type instanceof $entity; + } + + /** + * Checks if the given entity is a valid endpoint class in the current namespace. + * + * This method verifies whether the provided `$entity` exists in the predefined list + * of valid endpoint classes within the current namespace. It helps ensure that only + * valid classes (like `Endpoint_Redirect` or `Endpoint_Template`) are used when + * checking endpoint types. + * + * @since 1.0.0 + * + * @param string $entity The class name of the entity to check (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * @return bool True if the `$entity` is a valid endpoint class, false otherwise. + */ + private static function is_in_class( string $entity ): bool { + return in_array( + $entity, + array( + __NAMESPACE__ . '\Endpoint_Redirect', + __NAMESPACE__ . '\Endpoint_Template', + ), + true + ); + } +} \ No newline at end of file From 27f9efb4c1d34d2ff69347a276a1da55f3a6ec4f Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:03:48 +0200 Subject: [PATCH 03/52] New Abstract class for defining custom endpoint types --- .../classes/endpoints/class-endpoint-type.php | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 includes/core/classes/endpoints/class-endpoint-type.php diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php new file mode 100644 index 000000000..8f0e66e7d --- /dev/null +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -0,0 +1,90 @@ +slug = $slug; + $this->callback = $callback; + } + + /** + * Activate Endpoint_Type by hooking into relevant parts. + * + * @since 1.0.0 + * + * @return void + */ + abstract public function activate(): void; +} \ No newline at end of file From caa9effe1ced3ccffe2fd509573dd77a7d7786ee Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:11:04 +0200 Subject: [PATCH 04/52] Fix for CS --- includes/core/classes/endpoints/class-endpoint-type.php | 2 +- includes/core/classes/endpoints/class-endpoint.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php index 8f0e66e7d..9a1f0db16 100644 --- a/includes/core/classes/endpoints/class-endpoint-type.php +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -87,4 +87,4 @@ public function __construct( * @return void */ abstract public function activate(): void; -} \ No newline at end of file +} diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index 4cffa6f03..b7910ea59 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -510,4 +510,4 @@ private static function is_in_class( string $entity ): bool { true ); } -} \ No newline at end of file +} From a32cdb7a820ea43de80564aaa5aa97efbf716775 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:20:35 +0200 Subject: [PATCH 05/52] NEW Class responsible for handling redirect-based endpoints --- .../endpoints/class-endpoint-redirect.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 includes/core/classes/endpoints/class-endpoint-redirect.php diff --git a/includes/core/classes/endpoints/class-endpoint-redirect.php b/includes/core/classes/endpoints/class-endpoint-redirect.php new file mode 100644 index 000000000..3c611df1c --- /dev/null +++ b/includes/core/classes/endpoints/class-endpoint-redirect.php @@ -0,0 +1,91 @@ +url = ( $this->callback )(); + if ( $this->url ) { + // Add the target host to the list of allowed redirect hosts. + add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) ); + // Perform a safe redirection to the target URL. Defaults to a 302 status code. + wp_safe_redirect( $this->url ); + exit; // Always exit after redirecting. + } + } + + /** + * Filters the list of allowed hosts to include the redirect target. + * + * This method ensures that the host of the target URL is added to the list of allowed + * redirect hosts, allowing the redirection to proceed safely. It is hooked into the + * `allowed_redirect_hosts` filter, which controls the domains that `wp_safe_redirect()` + * is allowed to redirect to. + * + * @see https://developer.wordpress.org/reference/hooks/allowed_redirect_hosts/ + * + * @since 1.0.0 + * + * @param string[] $hosts An array of allowed host names. + * @return string[] The updated array of allowed host names, including the redirect target. + */ + public function allowed_redirect_hosts( array $hosts ): array { + return array_merge( + $hosts, + array( + wp_parse_url( $this->url, PHP_URL_HOST ), + ) + ); + } +} \ No newline at end of file From f5e6bd3c4788356af87ec6deea0b971207d7cb0f Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:21:21 +0200 Subject: [PATCH 06/52] NEW Class responsible for handling template-based endpoints --- .../endpoints/class-endpoint-template.php | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 includes/core/classes/endpoints/class-endpoint-template.php diff --git a/includes/core/classes/endpoints/class-endpoint-template.php b/includes/core/classes/endpoints/class-endpoint-template.php new file mode 100644 index 000000000..cc213be25 --- /dev/null +++ b/includes/core/classes/endpoints/class-endpoint-template.php @@ -0,0 +1,168 @@ +plugin_template_dir = ( ! empty( $plugin_template_dir ) ) ? $plugin_template_dir : sprintf( + '%s/includes/templates/endpoints', + GATHERPRESS_CORE_PATH + ); + } + + + /** + * Activate Endpoint_Type by hooking into relevant parts. + * + * @since 1.0.0 + * + * @return void + */ + public function activate(): void { + // Filters the path of the current template before including it. + add_filter( 'template_include', array( $this, 'template_include' ) ); + } + + /** + * Filters the path of the current template before including it. + * + * This method checks if the theme or child theme provides a custom template for the + * current endpoint. If a theme template exists, it will use that; otherwise, it will + * fall back to the default template provided by the plugin. The template information + * is provided by the callback set during the construction of the endpoint. + * + * @since 1.0.0 + * + * @param string $template The path of the default template to include. + * @return string The path of the template to include, either from the theme or plugin. + */ + public function template_include( string $template ): string { + $presets = $this->get_template_presets(); + + $file_name = $presets['file_name']; + $dir_path = $presets['dir_path'] ?? $this->plugin_template_dir; + + // Check if the theme provides a custom template. + $theme_template = $this->get_template_from_theme( $file_name ); + if ( $theme_template ) { + return $theme_template; + } + + // Check if the plugin has a template file. + $plugin_template = $this->get_template_from_plugin( $file_name, $dir_path, ); + if ( $plugin_template ) { + return $plugin_template; + } + + // Fallback to the default template. + return $template; + } + + + /** + * Retrieve template presets by invoking the callback. + * + * @return array Template preset data including file_name and optional dir_path. + */ + protected function get_template_presets(): array { + return ( $this->callback )(); + } + + /** + * Locate a template in the theme or child theme. + * + * @todo Maybe better put in the Utility class? + * + * @param string $file_name The name of the template file. + * @return string|false The path to the theme template or false if not found. + */ + protected function get_template_from_theme( string $file_name ) { + + // locate_template() doesn't cares, + // but locate_block_template() needs this to be an array. + $templates = array( $file_name ); + + // First, search for PHP templates, which block themes can also use. + $template = locate_template( $templates ); + + // Pass the result into the block template locator and let it figure + // out whether block templates are supported and this template exists. + $template = locate_block_template( + $template, + pathinfo( $file_name, PATHINFO_FILENAME ), // Name of the file without extension. + $templates + ); + + return ( is_string( $template ) && ! empty( $template ) ) ? $template : false; + } + + /** + * Build the full path to the plugin's template file. + * + * @todo Maybe better put in the Utility class? + * + * @param string $file_name The name of the template file. + * @param string $dir_path The directory path where the template is stored. + * @return string|false The full path to the template file or false if file not exists. + */ + protected function get_template_from_plugin( string $file_name, string $dir_path ): string { + // Remove prefix to keep file-names simple, + // for templates of core GatherPress. + if ( $this->plugin_template_dir === $dir_path ) { + $file_name = Utility::unprefix_key( $file_name ); + } + + $template = trailingslashit( $dir_path ) . $file_name; + return file_exists( $template ) ? $template : false; + } +} \ No newline at end of file From f57774b22d3de90d0df7fc59b69d8e995e2cf853 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:23:59 +0200 Subject: [PATCH 07/52] The BEGINNING of a unit test --- .../class-test-endpoint-template.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php new file mode 100644 index 000000000..216cc04ba --- /dev/null +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -0,0 +1,76 @@ + 'filter', + 'name' => 'template_include', + 'priority' => 10, + 'callback' => array( $instance, 'template_include' ), + ), + ); + + $this->assert_hooks( $hooks, $instance ); + } */ + + /** + * Tests template_include method when the theme has the template. + * + * @covers ::template_include + * + * @return void + */ + public function test_template_include_with_theme_template(): void { + $slug = 'custom-endpoint'; + $callback = function () { + return array( + 'file_name' => 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + $plugin_default = '/mock/plugin/templates'; + $template_default = '/default/template.php'; + + // Create a mock for Endpoint_Template. + $endpoint = new Endpoint_Template( $slug, $callback, $plugin_default ); + + // Simulate theme template existing. + // ...???? + + $template = $endpoint->template_include( $template_default ); + + // Assert that the theme template is used. + // $this->assertSame('/path/to/theme/theme-endpoint-template.php', $template); // ..???? + $this->assertSame( '/default/template.php', $template ); + } +} \ No newline at end of file From 7688800b8f015f56a2af5100fc6625a59e61fa98 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 00:40:38 +0200 Subject: [PATCH 08/52] Fix for CS --- includes/core/classes/endpoints/class-endpoint-redirect.php | 2 +- includes/core/classes/endpoints/class-endpoint-template.php | 2 +- .../core/classes/endpoints/class-test-endpoint-template.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-redirect.php b/includes/core/classes/endpoints/class-endpoint-redirect.php index 3c611df1c..635d5225c 100644 --- a/includes/core/classes/endpoints/class-endpoint-redirect.php +++ b/includes/core/classes/endpoints/class-endpoint-redirect.php @@ -88,4 +88,4 @@ public function allowed_redirect_hosts( array $hosts ): array { ) ); } -} \ No newline at end of file +} diff --git a/includes/core/classes/endpoints/class-endpoint-template.php b/includes/core/classes/endpoints/class-endpoint-template.php index cc213be25..cc4cf603d 100644 --- a/includes/core/classes/endpoints/class-endpoint-template.php +++ b/includes/core/classes/endpoints/class-endpoint-template.php @@ -165,4 +165,4 @@ protected function get_template_from_plugin( string $file_name, string $dir_path $template = trailingslashit( $dir_path ) . $file_name; return file_exists( $template ) ? $template : false; } -} \ No newline at end of file +} diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 216cc04ba..056e69aa0 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -73,4 +73,4 @@ public function test_template_include_with_theme_template(): void { // $this->assertSame('/path/to/theme/theme-endpoint-template.php', $template); // ..???? $this->assertSame( '/default/template.php', $template ); } -} \ No newline at end of file +} From 045f49371159c25bc1fe98142da97a3e5a528c45 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 16:43:46 +0200 Subject: [PATCH 09/52] Simplify logic wether to load a feed template or anything other for the endpoint --- .../core/classes/endpoints/class-endpoint.php | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index b7910ea59..fe25861f5 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -160,28 +160,6 @@ protected function setup_hooks(): void { $current_priority = $wp_filter[ $current_filter ]->current_priority(); add_action( $current_filter, array( $this, 'init' ), $current_priority + 1 ); - - if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { - $feed_slug = $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' )[0]; - $action = sprintf( - 'gatherpress_load_feed_template_for_%s', - $feed_slug - ); - - // Do not hook this action multiple times. - if ( 0 < did_action( $action ) ) { - return; - } - // Hook into WordPress' feed handling to load the custom feed template. - add_action( - sprintf( - 'do_feed_%s', - $feed_slug - ), - array( $this, 'load_feed_template' ) - ); - do_action( $action ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound - } } /** @@ -223,8 +201,16 @@ public function init(): void { // Allow the custom query variable by filtering the public query vars. add_filter( 'query_vars', array( $this, 'allow_query_vars' ) ); - // Handle whether to include a template or redirect the request. - add_action( 'template_redirect', array( $this, 'template_redirect' ) ); + // A call to any /feed/ endpoint is handled by WordPress + // prior "template_redirect" and as such prior 'Endpoint_Template's template_include hook would run. + $feed_slug = $this->has_feed_template(); + if ( $feed_slug ) { + // Hook into WordPress' feed handling to load the custom feed template. + add_action( sprintf( 'do_feed_%s', $feed_slug ), array( $this, 'load_feed_template' ) ); + } else { + // Handle whether to include a template or redirect the request. + add_action( 'template_redirect', array( $this, 'template_redirect' ) ); + } } /** @@ -365,35 +351,19 @@ public function allow_query_vars( array $public_query_vars ): array { } /** - * Fires before determining which template to load or whether to redirect. - * - * This method is responsible for: - * - Validating the query to ensure the endpoint is correctly matched. - * - Performing redirects if the current endpoint has associated redirects. - * - Loading a custom template if the endpoint defines one. - * - * @since 1.0.0 + * Determine wether the endpoint is meant for a feed + * and if it has a proper Endpoint_Template defined. * - * @see https://developer.wordpress.org/reference/hooks/template_redirect/ - * - * @return void + * @return string The slug of the endpoint or an empty string if not a feed template. */ - public function template_redirect(): void { - - if ( ! $this->is_valid_query() ) { - return; + protected function has_feed_template(): string { + if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { + $feed_slug = current( $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' ) ); + if ( ! empty( $feed_slug ) ) { + return $feed_slug; + } } - - // Get the currently requested endpoint from the list of registered endpoint types. - $endpoint_type = current( - wp_list_filter( - $this->types, - array( - 'slug' => get_query_var( $this->query_var ), - ) - ) - ); - $endpoint_type->activate(); + return ''; } /** @@ -426,6 +396,39 @@ public function load_feed_template() { load_template( $this->types[0]->template_include( false ) ); } + + /** + * Fires before determining which template to load or whether to redirect. + * + * This method is responsible for: + * - Validating the query to ensure the endpoint is correctly matched. + * - Performing redirects if the current endpoint has associated redirects. + * - Loading a custom template if the endpoint defines one. + * + * @since 1.0.0 + * + * @see https://developer.wordpress.org/reference/hooks/template_redirect/ + * + * @return void + */ + public function template_redirect(): void { + + if ( ! $this->is_valid_query() ) { + return; + } + + // Get the currently requested endpoint from the list of registered endpoint types. + $endpoint_type = current( + wp_list_filter( + $this->types, + array( + 'slug' => get_query_var( $this->query_var ), + ) + ) + ); + $endpoint_type->activate(); + } + /** * Checks if the current query is valid for this endpoint. * From a2c277b8877344f672141cc65f22cb57503604ad Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sat, 28 Sep 2024 16:53:25 +0200 Subject: [PATCH 10/52] Fix typo --- includes/core/classes/endpoints/class-endpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index fe25861f5..3ed124236 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -351,7 +351,7 @@ public function allow_query_vars( array $public_query_vars ): array { } /** - * Determine wether the endpoint is meant for a feed + * Determine whether the endpoint is meant for a feed * and if it has a proper Endpoint_Template defined. * * @return string The slug of the endpoint or an empty string if not a feed template. From 313fafd7a8c15cd398850572312d6ec0c16907e2 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Sun, 29 Sep 2024 23:18:33 +0200 Subject: [PATCH 11/52] WIP unit tests --- .../class-test-endpoint-template.php | 71 ++++++++++++++----- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 056e69aa0..51c345824 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -2,11 +2,11 @@ /** * Class handles unit tests for GatherPress\Core\Endpoints\Endpoint_Template. * - * @package GatherPress\Core\Endpoints + * @package GatherPress\Core * @since 1.0.0 */ -namespace GatherPress\Tests\Core; +namespace GatherPress\Tests\Core\Endpoints; use GatherPress\Core\Endpoints\Endpoint_Template; use PMC\Unit_Test\Base; @@ -19,29 +19,64 @@ * @group endpoints */ class Test_Endpoint_Template extends Base { + /** + * Coverage for __construct method. + * + * @covers ::__construct + * + * @return void + */ + public function test___construct(): void { + $slug = 'custom-endpoint'; + $callback = function () { + return array( + 'file_name' => 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + $plugin_default = '/mock/plugin/templates'; + $template_default = '/default/template.php'; + + // Create a mock for Endpoint_Template. + $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); + + $this->assertInstanceOf( Endpoint_Template::class, $instance ); + } /** * Coverage for activate. * - * @covers ::__construct * @covers ::activate * * @return void - + */ public function test_activate(): void { - $cb = function(){}; - $instance = new Endpoint_Template( 'unit-test', $cb ); - $hooks = array( - array( - 'type' => 'filter', - 'name' => 'template_include', - 'priority' => 10, - 'callback' => array( $instance, 'template_include' ), - ), - ); + $slug = 'custom-endpoint'; + $callback = function () { + return array( + 'file_name' => 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + $plugin_default = '/mock/plugin/templates'; + $template_default = '/default/template.php'; - $this->assert_hooks( $hooks, $instance ); - } */ + // Create a mock for Endpoint_Template. + $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); + // var_dump($instance); + // $instance->activate(); + + // $hooks = array( + // array( + // 'type' => 'filter', + // 'name' => 'template_include', + // 'priority' => 10, + // 'callback' => array( $instance, 'template_include' ), + // ), + // ); + + // $this->assert_hooks( $hooks, $instance ); // DOES NOT WORK WITH NON-SINGLETONS, BUT WILL NOT THROW AN ERROR. + } /** * Tests template_include method when the theme has the template. @@ -62,12 +97,12 @@ public function test_template_include_with_theme_template(): void { $template_default = '/default/template.php'; // Create a mock for Endpoint_Template. - $endpoint = new Endpoint_Template( $slug, $callback, $plugin_default ); + $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); // Simulate theme template existing. // ...???? - $template = $endpoint->template_include( $template_default ); + $template = $instance->template_include( $template_default ); // Assert that the theme template is used. // $this->assertSame('/path/to/theme/theme-endpoint-template.php', $template); // ..???? From 5be1c95009a11ebe18c78e1e3f8665424ff66934 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Mon, 30 Sep 2024 22:31:23 +0200 Subject: [PATCH 12/52] Move methods into more related class --- .../classes/endpoints/class-endpoint-type.php | 42 ++++++++++++++++++ .../core/classes/endpoints/class-endpoint.php | 43 +------------------ 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php index 9a1f0db16..d21ac06fd 100644 --- a/includes/core/classes/endpoints/class-endpoint-type.php +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -87,4 +87,46 @@ public function __construct( * @return void */ abstract public function activate(): void; + + + /** + * Checks if the given endpoint type is an instance of the specified class. + * + * This method verifies whether the provided `$type` is an instance of the `$entity` + * class. It first checks if the `$entity` exists in the defined list of valid endpoint + * classes by calling `is_in_class()`. If the entity is valid, it further checks if the + * `$type` is an instance of that class. + * + * @since 1.0.0 + * + * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * @return bool True if the `$type` is an instance of the `$entity` class, false otherwise. + */ + private function is_of_class( string $entity ): bool { + return self::is_in_class( $entity ) && $this instanceof $entity; + } + + /** + * Checks if the given entity is a valid endpoint class in the current namespace. + * + * This method verifies whether the provided `$entity` exists in the predefined list + * of valid endpoint classes within the current namespace. It helps ensure that only + * valid classes (like `Endpoint_Redirect` or `Endpoint_Template`) are used when + * checking endpoint types. + * + * @since 1.0.0 + * + * @param string $entity The class name of the entity to check (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * @return bool True if the `$entity` is a valid endpoint class, false otherwise. + */ + private static function is_in_class( string $entity ): bool { + return in_array( + $entity, + array( + __NAMESPACE__ . '\Endpoint_Redirect', + __NAMESPACE__ . '\Endpoint_Template', + ), + true + ); + } } diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index 3ed124236..f3c6b38b0 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -466,51 +466,10 @@ protected function get_slugs( ?string $entity = null ): array { : array_filter( $this->types, function ( $type ) use ( $entity ) { - return self::is_of_class( $type, $entity ); + return $type->is_of_class( $entity ); } ); return wp_list_pluck( $types, 'slug' ); } - /** - * Checks if the given endpoint type is an instance of the specified class. - * - * This method verifies whether the provided `$type` is an instance of the `$entity` - * class. It first checks if the `$entity` exists in the defined list of valid endpoint - * classes by calling `is_in_class()`. If the entity is valid, it further checks if the - * `$type` is an instance of that class. - * - * @since 1.0.0 - * - * @param Endpoint_Type $type The endpoint type object to check. - * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). - * @return bool True if the `$type` is an instance of the `$entity` class, false otherwise. - */ - private static function is_of_class( Endpoint_Type $type, string $entity ): bool { - return self::is_in_class( $entity ) && $type instanceof $entity; - } - - /** - * Checks if the given entity is a valid endpoint class in the current namespace. - * - * This method verifies whether the provided `$entity` exists in the predefined list - * of valid endpoint classes within the current namespace. It helps ensure that only - * valid classes (like `Endpoint_Redirect` or `Endpoint_Template`) are used when - * checking endpoint types. - * - * @since 1.0.0 - * - * @param string $entity The class name of the entity to check (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). - * @return bool True if the `$entity` is a valid endpoint class, false otherwise. - */ - private static function is_in_class( string $entity ): bool { - return in_array( - $entity, - array( - __NAMESPACE__ . '\Endpoint_Redirect', - __NAMESPACE__ . '\Endpoint_Template', - ), - true - ); - } } From b7eafc0f6097da6352266da2c6d371ce216daa65 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Mon, 30 Sep 2024 22:32:29 +0200 Subject: [PATCH 13/52] failing test, that I can't figure out --- .../classes/endpoints/class-test-endpoint.php | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php new file mode 100644 index 000000000..22f9e23d4 --- /dev/null +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -0,0 +1,87 @@ + 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + $plugin_default = '/mock/plugin/templates'; + $template_default = '/default/template.php'; + + // Create a mock for Endpoint. + $endpoint_template = new Endpoint_Template( $slug, $callback, $plugin_default ); + + } */ + + /** + * Coverage for get_slugs method. + * + * @covers ::get_slugs + * + * @return void + */ + public function test_get_slugs(): void { + + // // Simulate 'init' hook being fired + // \do_action('init'); + + $callback = function(){}; + $instance = new Endpoint( + 'query_var', + 'post', + $callback, + array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ), + 'reg_ex', + ); + // var_dump($instance->types); + + $this->assertSame( + array( + 'endpoint_template_1', + 'endpoint_template_2', + 'endpoint_redirect_1', + ), + Utility::invoke_hidden_method( $instance, 'get_slugs' ), + 'Failed to assert that endpoint slugs match.' + ); + + // $this->mock->wp()->reset(); + + } + +} From 41f6b3ef095b0558556e61ed22f3c9261edc4704 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Mon, 30 Sep 2024 22:33:13 +0200 Subject: [PATCH 14/52] NEW tests --- .../class-test-endpoint-template.php | 28 +++++- .../endpoints/class-test-endpoint-type.php | 93 +++++++++++++++++++ 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 51c345824..37d4b95e3 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -27,20 +27,38 @@ class Test_Endpoint_Template extends Base { * @return void */ public function test___construct(): void { - $slug = 'custom-endpoint'; + $slug = 'endpoint-template'; $callback = function () { return array( 'file_name' => 'endpoint-template.php', 'dir_path' => '/path/to/theme', ); }; + // Create a mock for Endpoint. + $endpoint_template = new Endpoint_Template( $slug, $callback ); + + $this->assertIsString( Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ) ); + $this->assertNotEmpty( Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ) ); + $this->assertSame( + sprintf( + '%s/includes/templates/endpoints', + GATHERPRESS_CORE_PATH + ), + Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ), + 'Failed to assert, plugin_template_dir is set to fallback directory.' + ); + $plugin_default = '/mock/plugin/templates'; - $template_default = '/default/template.php'; + // $template_default = '/default/template.php'; - // Create a mock for Endpoint_Template. - $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); + // Create a mock for Endpoint. + $endpoint_template = new Endpoint_Template( $slug, $callback, $plugin_default ); - $this->assertInstanceOf( Endpoint_Template::class, $instance ); + $this->assertSame( + '/mock/plugin/templates', + Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ), + 'Failed to assert, plugin_template_dir is set to test directory.' + ); } /** diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php new file mode 100644 index 000000000..ccce9b9cb --- /dev/null +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php @@ -0,0 +1,93 @@ + 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + + // Create a mock for Endpoint. + $endpoint_template = new Endpoint_Template( $slug, $callback ); + + $this->assertIsString( $endpoint_template->slug ); + $this->assertIsCallable( Utility::get_hidden_property( $endpoint_template, 'callback' ) ); + } + + /** + * Coverage for is_of_class method. + * + * @covers ::is_of_class + * + * @return void + */ + public function test_is_of_class(): void { + $instance = new Endpoint_Redirect( 'slug', function(){} ); + + $this->assertTrue( + Utility::invoke_hidden_method( $instance, 'is_of_class', array( 'GatherPress\Core\Endpoints\Endpoint_Redirect' ) ), + 'Failed to validate class in namespace.' + ); + + $this->assertFalse( + Utility::invoke_hidden_method( $instance, 'is_of_class', array( 'GatherPress\Core\Endpoints\Endpoint_Template' ) ), + 'Failed to validate non-used class in namespace.' + ); + } + + /** + * Coverage for is_in_class method. + * + * @covers ::is_in_class + * + * @return void + */ + public function test_is_in_class(): void { + $instance = new Endpoint_Redirect( 'slug', function(){} ); + + $this->assertTrue( + Utility::invoke_hidden_method( $instance, 'is_in_class', array( 'GatherPress\Core\Endpoints\Endpoint_Redirect' ) ), + 'Failed to validate class in namespace.' + ); + + $this->assertTrue( + Utility::invoke_hidden_method( $instance, 'is_in_class', array( 'GatherPress\Core\Endpoints\Endpoint_Template' ) ), + 'Failed to validate class in namespace.' + ); + + $this->assertFalse( + Utility::invoke_hidden_method( $instance, 'is_in_class', array( 'WP_Post' ) ), + 'Failed to validate class is not in namespace.' + ); + } +} From 80a1c515463279013beae9417d7a822f3e8f4866 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 00:01:00 +0200 Subject: [PATCH 15/52] Maybe its pointless to hook this onto the next round? --- includes/core/classes/endpoints/class-endpoint.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index f3c6b38b0..a2e03330e 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -140,7 +140,10 @@ public function __construct( $this->reg_ex = $reg_ex; $this->object_type = $object_type; - $this->setup_hooks(); + // Maybe its pointless to hook this onto the next round? + // $this->setup_hooks(); + // Maybe just start ? + $this->init(); } } @@ -152,15 +155,14 @@ public function __construct( * @since 1.0.0 * * @return void - */ + protected function setup_hooks(): void { - global $wp_filter; $current_filter = current_filter(); $current_priority = $wp_filter[ $current_filter ]->current_priority(); add_action( $current_filter, array( $this, 'init' ), $current_priority + 1 ); - } + } */ /** * Initializes the endpoint by registering rewrite rules and handling query variables. From 35a8512070399178fdaf7e85f37b2ef778737291 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 00:01:29 +0200 Subject: [PATCH 16/52] NEW test for constructor --- .../classes/endpoints/class-test-endpoint.php | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 22f9e23d4..f10fe0de9 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -27,22 +27,33 @@ class Test_Endpoint extends Base { * @covers ::__construct * * @return void - + */ public function test___construct(): void { - $slug = 'custom-endpoint'; - $callback = function () { - return array( - 'file_name' => 'endpoint-template.php', - 'dir_path' => '/path/to/theme', - ); - }; - $plugin_default = '/mock/plugin/templates'; - $template_default = '/default/template.php'; + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; - // Create a mock for Endpoint. - $endpoint_template = new Endpoint_Template( $slug, $callback, $plugin_default ); + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); - } */ + $this->assertSame( $query_var, $instance->query_var, 'Failed to assert that query_var is persisted.' ); + $this->assertSame( get_post_type_object( $post_type ), $instance->type_object, 'Failed to assert that type_object is persisted.' ); + $this->assertSame( $callback, $instance->validation_callback, 'Failed to assert that validation_callback is persisted.' ); + $this->assertSame( $types, $instance->types, 'Failed to assert that endpoint types are persisted.' ); + $this->assertSame( $reg_ex, $instance->reg_ex, 'Failed to assert that reg_ex is persisted.' ); + $this->assertSame( 'post_type', $instance->object_type, 'Failed to assert that object_type is set by default.' ); + } /** * Coverage for get_slugs method. @@ -52,14 +63,15 @@ public function test___construct(): void { * @return void */ public function test_get_slugs(): void { + // ini_set('display_errors', '1'); + // ini_set('display_startup_errors', '1'); + // error_reporting(E_ALL); - // // Simulate 'init' hook being fired - // \do_action('init'); - $callback = function(){}; $instance = new Endpoint( 'query_var', - 'post', + // 'post', // has rewrite=false , why? + 'gatherpress_event', $callback, array( new Endpoint_Template( 'endpoint_template_1', $callback ), @@ -68,8 +80,6 @@ public function test_get_slugs(): void { ), 'reg_ex', ); - // var_dump($instance->types); - $this->assertSame( array( 'endpoint_template_1', @@ -79,9 +89,6 @@ public function test_get_slugs(): void { Utility::invoke_hidden_method( $instance, 'get_slugs' ), 'Failed to assert that endpoint slugs match.' ); - - // $this->mock->wp()->reset(); - } } From 0b2d4608e1b63c730045fb226d5ae1d4c478a40f Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 00:08:46 +0200 Subject: [PATCH 17/52] NEW test for method get_rewrite_atts() --- .../classes/endpoints/class-test-endpoint.php | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index f10fe0de9..838347ac4 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -70,7 +70,6 @@ public function test_get_slugs(): void { $callback = function(){}; $instance = new Endpoint( 'query_var', - // 'post', // has rewrite=false , why? 'gatherpress_event', $callback, array( @@ -91,4 +90,33 @@ public function test_get_slugs(): void { ); } + /** + * Coverage for get_rewrite_atts method. + * + * @covers ::get_rewrite_atts + * + * @return void + */ + public function test_get_rewrite_atts(): void { + $callback = function(){}; + $instance = new Endpoint( + 'query_var', + 'gatherpress_event', + $callback, + array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ), + 'reg_ex', + ); + $this->assertSame( + array( + 'gatherpress_event' => '$matches[1]', + 'query_var' => '$matches[2]', + ), + $instance->get_rewrite_atts(), + 'Failed to assert that rewrite attributes match.' + ); + } } From d0c96186056c296e338ff9e69de07bf4d3b56af7 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 00:19:43 +0200 Subject: [PATCH 18/52] NEW test for allow_query_vars() method --- .../classes/endpoints/class-test-endpoint.php | 97 +++++++++++++++---- 1 file changed, 76 insertions(+), 21 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 838347ac4..fe885b9e9 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -38,7 +38,6 @@ public function test___construct(): void { new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), ); $reg_ex = 'reg_ex'; - $instance = new Endpoint( $query_var, $post_type, @@ -67,18 +66,23 @@ public function test_get_slugs(): void { // ini_set('display_startup_errors', '1'); // error_reporting(E_ALL); - $callback = function(){}; - $instance = new Endpoint( - 'query_var', - 'gatherpress_event', + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, $callback, - array( - new Endpoint_Template( 'endpoint_template_1', $callback ), - new Endpoint_Template( 'endpoint_template_2', $callback ), - new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), - ), - 'reg_ex', + $types, + $reg_ex, ); + $this->assertSame( array( 'endpoint_template_1', @@ -98,18 +102,23 @@ public function test_get_slugs(): void { * @return void */ public function test_get_rewrite_atts(): void { - $callback = function(){}; - $instance = new Endpoint( - 'query_var', - 'gatherpress_event', + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, $callback, - array( - new Endpoint_Template( 'endpoint_template_1', $callback ), - new Endpoint_Template( 'endpoint_template_2', $callback ), - new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), - ), - 'reg_ex', + $types, + $reg_ex, ); + $this->assertSame( array( 'gatherpress_event' => '$matches[1]', @@ -119,4 +128,50 @@ public function test_get_rewrite_atts(): void { 'Failed to assert that rewrite attributes match.' ); } + + /** + * Coverage for maybe_flush_rewrite_rules method. + * + * @covers ::maybe_flush_rewrite_rules + * + * @return void + */ + public function test_maybe_flush_rewrite_rules(): void {} + + /** + * Coverage for allow_query_vars method. + * + * @covers ::allow_query_vars + * + * @return void + */ + public function test_allow_query_vars(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertSame( + array( + 'apples', + 'oranges', + 'query_var', + ), + $instance->allow_query_vars( array( 'apples', 'oranges' )), + 'Failed to assert that merged query variables match.' + ); + } + } From 4d2973470ac71f840c3e3b5721ca3f2a3b3910dc Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 03:43:52 +0200 Subject: [PATCH 19/52] Make method avail. to public --- includes/core/classes/endpoints/class-endpoint-type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php index d21ac06fd..78d5403ef 100644 --- a/includes/core/classes/endpoints/class-endpoint-type.php +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -102,7 +102,7 @@ abstract public function activate(): void; * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). * @return bool True if the `$type` is an instance of the `$entity` class, false otherwise. */ - private function is_of_class( string $entity ): bool { + public function is_of_class( string $entity ): bool { return self::is_in_class( $entity ) && $this instanceof $entity; } From 18c0b4db74d666cdfd26712ca622388a525a4dac Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 03:53:10 +0200 Subject: [PATCH 20/52] NEW test for has_feed_template() method --- .../classes/endpoints/class-test-endpoint.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index fe885b9e9..aedcbcf40 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -174,4 +174,72 @@ public function test_allow_query_vars(): void { ); } + /** + * Coverage for has_feed_template method. + * + * @covers ::has_feed_template + * + * @return void + */ + public function test_has_feed_template(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertEmpty( + Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + 'Failed to assert, endpoint is not for feeds.' + ); + + $types = array( + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex/feed/'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertEmpty( + Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + 'Failed to assert, endpoint is for feeds, but has no Endpoint_Template type.' + ); + + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + ); + $reg_ex = 'reg_ex/feed/'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertSame( + 'endpoint_template_1', + Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + 'Failed to assert, that feed template is found.' + ); + } + + } From 75b408c013b7da887c94d1fdb39e3701a68364b0 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 04:07:39 +0200 Subject: [PATCH 21/52] NEW test for is_valid_query() method --- .../classes/endpoints/class-test-endpoint.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index aedcbcf40..9048bc849 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -242,4 +242,72 @@ public function test_has_feed_template(): void { } + + /** + * Coverage for is_valid_query method. + * + * @covers ::is_valid_query + * + * @return void + */ + public function test_is_valid_query(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = '__return_true'; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->mock->wp( [ + 'query_vars' => [ + $query_var => 'endpoint_template_1', + ] + ] ); + + $this->assertTrue( + $instance->is_valid_query(), + 'Failed to validate the prepared query.' + ); + + $callback = '__return_false'; + $instance_2 = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertFalse( + $instance_2->is_valid_query(), + 'Failed to validate the prepared query.' + ); + $this->mock->wp()->reset(); + + + $this->mock->wp( [ + 'is_category' => true, + 'query_vars' => [ + 'cat' => 'category-slug', + ], + ] ); + + $this->assertFalse( + $instance->is_valid_query(), + 'Failed to validate the prepared query.' + ); + + $this->mock->wp()->reset(); + } + } From 6576b3b1b32e923e589a33e24cd5b9457fa28668 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 04:25:30 +0200 Subject: [PATCH 22/52] Order test methods like class under test --- .../classes/endpoints/class-test-endpoint.php | 90 ++++++++++--------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 9048bc849..1ed3b2dc1 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -54,46 +54,6 @@ public function test___construct(): void { $this->assertSame( 'post_type', $instance->object_type, 'Failed to assert that object_type is set by default.' ); } - /** - * Coverage for get_slugs method. - * - * @covers ::get_slugs - * - * @return void - */ - public function test_get_slugs(): void { - // ini_set('display_errors', '1'); - // ini_set('display_startup_errors', '1'); - // error_reporting(E_ALL); - - $query_var = 'query_var'; - $post_type = 'gatherpress_event'; - $callback = function(){}; - $types = array( - new Endpoint_Template( 'endpoint_template_1', $callback ), - new Endpoint_Template( 'endpoint_template_2', $callback ), - new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), - ); - $reg_ex = 'reg_ex'; - $instance = new Endpoint( - $query_var, - $post_type, - $callback, - $types, - $reg_ex, - ); - - $this->assertSame( - array( - 'endpoint_template_1', - 'endpoint_template_2', - 'endpoint_redirect_1', - ), - Utility::invoke_hidden_method( $instance, 'get_slugs' ), - 'Failed to assert that endpoint slugs match.' - ); - } - /** * Coverage for get_rewrite_atts method. * @@ -136,7 +96,15 @@ public function test_get_rewrite_atts(): void { * * @return void */ - public function test_maybe_flush_rewrite_rules(): void {} + public function test_maybe_flush_rewrite_rules(): void { + // ini_set('display_errors', '1'); + // ini_set('display_startup_errors', '1'); + // // error_reporting(E_ALL); + + // var_export('hallo test welt',true); + // update_option( 'rewrite_rules', 'hallo test welt option !' ); + // var_export(get_option( 'rewrite_rules' ),true); + } /** * Coverage for allow_query_vars method. @@ -241,8 +209,6 @@ public function test_has_feed_template(): void { ); } - - /** * Coverage for is_valid_query method. * @@ -309,5 +275,41 @@ public function test_is_valid_query(): void { $this->mock->wp()->reset(); } - + + /** + * Coverage for get_slugs method. + * + * @covers ::get_slugs + * + * @return void + */ + public function test_get_slugs(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function(){}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + $reg_ex = 'reg_ex'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertSame( + array( + 'endpoint_template_1', + 'endpoint_template_2', + 'endpoint_redirect_1', + ), + Utility::invoke_hidden_method( $instance, 'get_slugs' ), + 'Failed to assert that endpoint slugs match.' + ); + } + } From f2a8528c7c5e23ad55e2bbb46b02498d797e7544 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 04:43:08 +0200 Subject: [PATCH 23/52] Fix for CS --- .../classes/endpoints/class-endpoint-type.php | 2 +- .../core/classes/endpoints/class-endpoint.php | 3 +- .../class-test-endpoint-template.php | 46 +------------ .../endpoints/class-test-endpoint-type.php | 8 +-- .../classes/endpoints/class-test-endpoint.php | 64 +++++++++---------- 5 files changed, 38 insertions(+), 85 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php index 78d5403ef..4f749562c 100644 --- a/includes/core/classes/endpoints/class-endpoint-type.php +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -99,7 +99,7 @@ abstract public function activate(): void; * * @since 1.0.0 * - * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). + * @param string $entity The class name of the entity to check against (e.g., 'Endpoint_Redirect' or 'Endpoint_Template'). * @return bool True if the `$type` is an instance of the `$entity` class, false otherwise. */ public function is_of_class( string $entity ): bool { diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index a2e03330e..3dc5534fe 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -155,7 +155,7 @@ public function __construct( * @since 1.0.0 * * @return void - + protected function setup_hooks(): void { global $wp_filter; $current_filter = current_filter(); @@ -473,5 +473,4 @@ function ( $type ) use ( $entity ) { ); return wp_list_pluck( $types, 'slug' ); } - } diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 37d4b95e3..94ad85618 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -27,14 +27,13 @@ class Test_Endpoint_Template extends Base { * @return void */ public function test___construct(): void { - $slug = 'endpoint-template'; - $callback = function () { + $slug = 'endpoint-template'; + $callback = function () { return array( 'file_name' => 'endpoint-template.php', 'dir_path' => '/path/to/theme', ); }; - // Create a mock for Endpoint. $endpoint_template = new Endpoint_Template( $slug, $callback ); $this->assertIsString( Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ) ); @@ -48,10 +47,7 @@ public function test___construct(): void { 'Failed to assert, plugin_template_dir is set to fallback directory.' ); - $plugin_default = '/mock/plugin/templates'; - // $template_default = '/default/template.php'; - - // Create a mock for Endpoint. + $plugin_default = '/mock/plugin/templates'; $endpoint_template = new Endpoint_Template( $slug, $callback, $plugin_default ); $this->assertSame( @@ -61,41 +57,6 @@ public function test___construct(): void { ); } - /** - * Coverage for activate. - * - * @covers ::activate - * - * @return void - */ - public function test_activate(): void { - $slug = 'custom-endpoint'; - $callback = function () { - return array( - 'file_name' => 'endpoint-template.php', - 'dir_path' => '/path/to/theme', - ); - }; - $plugin_default = '/mock/plugin/templates'; - $template_default = '/default/template.php'; - - // Create a mock for Endpoint_Template. - $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); - // var_dump($instance); - // $instance->activate(); - - // $hooks = array( - // array( - // 'type' => 'filter', - // 'name' => 'template_include', - // 'priority' => 10, - // 'callback' => array( $instance, 'template_include' ), - // ), - // ); - - // $this->assert_hooks( $hooks, $instance ); // DOES NOT WORK WITH NON-SINGLETONS, BUT WILL NOT THROW AN ERROR. - } - /** * Tests template_include method when the theme has the template. * @@ -114,7 +75,6 @@ public function test_template_include_with_theme_template(): void { $plugin_default = '/mock/plugin/templates'; $template_default = '/default/template.php'; - // Create a mock for Endpoint_Template. $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); // Simulate theme template existing. diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php index ccce9b9cb..f69267b33 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-type.php @@ -29,8 +29,8 @@ class Test_Endpoint_Type extends Base { * @return void */ public function test___construct(): void { - $slug = 'endpoint-template'; - $callback = function () { + $slug = 'endpoint-template'; + $callback = function () { return array( 'file_name' => 'endpoint-template.php', 'dir_path' => '/path/to/theme', @@ -52,7 +52,7 @@ public function test___construct(): void { * @return void */ public function test_is_of_class(): void { - $instance = new Endpoint_Redirect( 'slug', function(){} ); + $instance = new Endpoint_Redirect( 'slug', function () {} ); $this->assertTrue( Utility::invoke_hidden_method( $instance, 'is_of_class', array( 'GatherPress\Core\Endpoints\Endpoint_Redirect' ) ), @@ -73,7 +73,7 @@ public function test_is_of_class(): void { * @return void */ public function test_is_in_class(): void { - $instance = new Endpoint_Redirect( 'slug', function(){} ); + $instance = new Endpoint_Redirect( 'slug', function () {} ); $this->assertTrue( Utility::invoke_hidden_method( $instance, 'is_in_class', array( 'GatherPress\Core\Endpoints\Endpoint_Redirect' ) ), diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 1ed3b2dc1..3e023a7cd 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -31,7 +31,7 @@ class Test_Endpoint extends Base { public function test___construct(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; - $callback = function(){}; + $callback = function () {}; $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), @@ -64,7 +64,7 @@ public function test___construct(): void { public function test_get_rewrite_atts(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; - $callback = function(){}; + $callback = function () {}; $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), @@ -96,15 +96,7 @@ public function test_get_rewrite_atts(): void { * * @return void */ - public function test_maybe_flush_rewrite_rules(): void { - // ini_set('display_errors', '1'); - // ini_set('display_startup_errors', '1'); - // // error_reporting(E_ALL); - - // var_export('hallo test welt',true); - // update_option( 'rewrite_rules', 'hallo test welt option !' ); - // var_export(get_option( 'rewrite_rules' ),true); - } + public function test_maybe_flush_rewrite_rules(): void {} /** * Coverage for allow_query_vars method. @@ -116,7 +108,7 @@ public function test_maybe_flush_rewrite_rules(): void { public function test_allow_query_vars(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; - $callback = function(){}; + $callback = function () {}; $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), @@ -137,7 +129,7 @@ public function test_allow_query_vars(): void { 'oranges', 'query_var', ), - $instance->allow_query_vars( array( 'apples', 'oranges' )), + $instance->allow_query_vars( array( 'apples', 'oranges' ) ), 'Failed to assert that merged query variables match.' ); } @@ -152,7 +144,7 @@ public function test_allow_query_vars(): void { public function test_has_feed_template(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; - $callback = function(){}; + $callback = function () {}; $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), @@ -172,11 +164,11 @@ public function test_has_feed_template(): void { 'Failed to assert, endpoint is not for feeds.' ); - $types = array( + $types = array( new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), ); - $reg_ex = 'reg_ex/feed/'; - $instance = new Endpoint( + $reg_ex = 'reg_ex/feed/'; + $instance = new Endpoint( $query_var, $post_type, $callback, @@ -189,12 +181,12 @@ public function test_has_feed_template(): void { 'Failed to assert, endpoint is for feeds, but has no Endpoint_Template type.' ); - $types = array( + $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), ); - $reg_ex = 'reg_ex/feed/'; - $instance = new Endpoint( + $reg_ex = 'reg_ex/feed/'; + $instance = new Endpoint( $query_var, $post_type, $callback, @@ -234,18 +226,20 @@ public function test_is_valid_query(): void { $reg_ex, ); - $this->mock->wp( [ - 'query_vars' => [ - $query_var => 'endpoint_template_1', - ] - ] ); + $this->mock->wp( + array( + 'query_vars' => array( + $query_var => 'endpoint_template_1', + ), + ) + ); $this->assertTrue( $instance->is_valid_query(), 'Failed to validate the prepared query.' ); - $callback = '__return_false'; + $callback = '__return_false'; $instance_2 = new Endpoint( $query_var, $post_type, @@ -260,13 +254,14 @@ public function test_is_valid_query(): void { ); $this->mock->wp()->reset(); - - $this->mock->wp( [ - 'is_category' => true, - 'query_vars' => [ - 'cat' => 'category-slug', - ], - ] ); + $this->mock->wp( + array( + 'is_category' => true, + 'query_vars' => array( + 'cat' => 'category-slug', + ), + ) + ); $this->assertFalse( $instance->is_valid_query(), @@ -286,7 +281,7 @@ public function test_is_valid_query(): void { public function test_get_slugs(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; - $callback = function(){}; + $callback = function () {}; $types = array( new Endpoint_Template( 'endpoint_template_1', $callback ), new Endpoint_Template( 'endpoint_template_2', $callback ), @@ -311,5 +306,4 @@ public function test_get_slugs(): void { 'Failed to assert that endpoint slugs match.' ); } - } From 4994b40bf8d20c3a07490d5184efe5ab5dd55178 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 05:55:48 +0200 Subject: [PATCH 24/52] Rename class under test for consistency --- .../endpoints/class-test-endpoint-template.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 94ad85618..f50b5e118 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -34,25 +34,25 @@ public function test___construct(): void { 'dir_path' => '/path/to/theme', ); }; - $endpoint_template = new Endpoint_Template( $slug, $callback ); + $instance = new Endpoint_Template( $slug, $callback ); - $this->assertIsString( Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ) ); - $this->assertNotEmpty( Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ) ); + $this->assertIsString( Utility::get_hidden_property( $instance, 'plugin_template_dir' ) ); + $this->assertNotEmpty( Utility::get_hidden_property( $instance, 'plugin_template_dir' ) ); $this->assertSame( sprintf( '%s/includes/templates/endpoints', GATHERPRESS_CORE_PATH ), - Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ), + Utility::get_hidden_property( $instance, 'plugin_template_dir' ), 'Failed to assert, plugin_template_dir is set to fallback directory.' ); $plugin_default = '/mock/plugin/templates'; - $endpoint_template = new Endpoint_Template( $slug, $callback, $plugin_default ); + $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); $this->assertSame( '/mock/plugin/templates', - Utility::get_hidden_property( $endpoint_template, 'plugin_template_dir' ), + Utility::get_hidden_property( $instance, 'plugin_template_dir' ), 'Failed to assert, plugin_template_dir is set to test directory.' ); } From 41133082e344511efd7360708ac97fa96684cf8d Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 05:56:16 +0200 Subject: [PATCH 25/52] 100% tests for Endpoint_Redirect class --- .../class-test-endpoint-redirect.php | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php new file mode 100644 index 000000000..ad86f9e92 --- /dev/null +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php @@ -0,0 +1,94 @@ +assertSame( + $slug, + $instance->slug, + 'Failed to assert, that the endpoint slug is persisted.' + ); + + $this->assertSame( + $callback, + Utility::get_hidden_property( $instance, 'callback' ), + 'Failed to assert, that the endpoint callback is persisted.' + ); + } + + /** + * Coverage for activate method. + * + * @covers ::activate + * + * @return void + */ + public function test_activate(): void { + $slug = 'endpoint-redirect'; + $callback = function () { + return 'https://example.org/'; + }; + $instance = new Endpoint_Redirect( $slug, $callback ); + + $this->assert_redirect_to( + 'https://example.org/', + array( $instance, 'activate' ) + ); + } + + /** + * Coverage for allowed_redirect_hosts method. + * + * @covers ::allowed_redirect_hosts + * + * @return void + */ + public function test_allowed_redirect_hosts(): void { + $slug = 'endpoint-redirect'; + $callback = function () { + return 'https://example.org/'; + }; + $instance = new Endpoint_Redirect( $slug, $callback ); + Utility::set_and_get_hidden_property( $instance, 'url', ($callback)() ); + + $this->assertSame( + array( + 'apples', + 'oranges', + 'example.org' + ), + $instance->allowed_redirect_hosts( array( 'apples', 'oranges' ) ), + 'Failed to assert, that the redirect url got merged into allowed_redirect_hosts.' + ); + } +} From 797af62706f5946df99c9fbc45b83d3fc686c455 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 06:18:15 +0200 Subject: [PATCH 26/52] Fix for CS --- .../endpoints/class-test-endpoint-redirect.php | 16 ++++++++-------- .../endpoints/class-test-endpoint-template.php | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php index ad86f9e92..f050c7974 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-redirect.php @@ -27,8 +27,8 @@ class Test_Endpoint_Redirect extends Base { * @return void */ public function test___construct(): void { - $slug = 'endpoint-redirect'; - $callback = function () { + $slug = 'endpoint-redirect'; + $callback = function () { return 'https://example.org/'; }; $instance = new Endpoint_Redirect( $slug, $callback ); @@ -54,8 +54,8 @@ public function test___construct(): void { * @return void */ public function test_activate(): void { - $slug = 'endpoint-redirect'; - $callback = function () { + $slug = 'endpoint-redirect'; + $callback = function () { return 'https://example.org/'; }; $instance = new Endpoint_Redirect( $slug, $callback ); @@ -74,18 +74,18 @@ public function test_activate(): void { * @return void */ public function test_allowed_redirect_hosts(): void { - $slug = 'endpoint-redirect'; - $callback = function () { + $slug = 'endpoint-redirect'; + $callback = function () { return 'https://example.org/'; }; $instance = new Endpoint_Redirect( $slug, $callback ); - Utility::set_and_get_hidden_property( $instance, 'url', ($callback)() ); + Utility::set_and_get_hidden_property( $instance, 'url', ( $callback )() ); $this->assertSame( array( 'apples', 'oranges', - 'example.org' + 'example.org', ), $instance->allowed_redirect_hosts( array( 'apples', 'oranges' ) ), 'Failed to assert, that the redirect url got merged into allowed_redirect_hosts.' diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index f50b5e118..2551fd92b 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -27,8 +27,8 @@ class Test_Endpoint_Template extends Base { * @return void */ public function test___construct(): void { - $slug = 'endpoint-template'; - $callback = function () { + $slug = 'endpoint-template'; + $callback = function () { return array( 'file_name' => 'endpoint-template.php', 'dir_path' => '/path/to/theme', @@ -47,8 +47,8 @@ public function test___construct(): void { 'Failed to assert, plugin_template_dir is set to fallback directory.' ); - $plugin_default = '/mock/plugin/templates'; - $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); + $plugin_default = '/mock/plugin/templates'; + $instance = new Endpoint_Template( $slug, $callback, $plugin_default ); $this->assertSame( '/mock/plugin/templates', From 596070c1b6e163e011841e44ecd652385f3da8fa Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 20:45:28 +0200 Subject: [PATCH 27/52] DRY out init steps into own get_regex_pattern() method --- .../core/classes/endpoints/class-endpoint.php | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index 3dc5534fe..f4a695422 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -178,21 +178,12 @@ protected function setup_hooks(): void { */ public function init(): void { - // Retrieve the rewrite base (slug) for the post type or taxonomy. - $rewrite_base = $this->type_object->rewrite['slug']; - $slugs = join( '|', $this->get_slugs() ); // Build the regular expression pattern for matching the custom endpoint URL structure. - $reg_ex_pattern = sprintf( - $this->reg_ex, - $rewrite_base, - $slugs - ); + $reg_ex_pattern = $this->get_regex_pattern(); + // Define the URL structure for handling matched requests via query vars. // Example result: 'index.php?gatherpress_event=$matches[1]&gatherpress_ext_calendar=$matches[2]'. - $rewrite_url = add_query_arg( - $this->get_rewrite_atts(), - 'index.php' - ); + $rewrite_url = add_query_arg( $this->get_rewrite_atts(), 'index.php' ); // Add the rewrite rule to WordPress. add_rewrite_rule( $reg_ex_pattern, $rewrite_url, 'top' ); @@ -215,6 +206,22 @@ public function init(): void { } } + /** + * Build the regular expression pattern for matching the custom endpoint URL structure, + * based on the rewrite base (slug) for the post type or taxonomy. + * + * @return string + */ + private function get_regex_pattern(): string { + $rewrite_base = $this->type_object->rewrite['slug']; + $slugs = join( '|', $this->get_slugs() ); + return sprintf( + $this->reg_ex, + $rewrite_base, + $slugs + ); + } + /** * Defines the rewrite replacement attributes for the custom feed endpoint. * From b44c5541135b682c7f8d92fce0da1a7855d7ea58 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 20:46:00 +0200 Subject: [PATCH 28/52] NEW test for maybe_flush_rewrite_rules() method --- .../classes/endpoints/class-test-endpoint.php | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 3e023a7cd..dc8b5d78d 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -96,7 +96,61 @@ public function test_get_rewrite_atts(): void { * * @return void */ - public function test_maybe_flush_rewrite_rules(): void {} + public function test_maybe_flush_rewrite_rules(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function () {}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + // Regular expression to match singular event endpoints. + // Example: 'event/my-sample-event/(custom-endpoint)(/)'. + $reg_ex = '%s/([^/]+)/(%s)/?$'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + delete_option('rewrite_rules'); + delete_option('gatherpress_flush_rewrite_rules_flag'); + + $this->assertEmpty( get_option('rewrite_rules'), 'Failed to assert that rewrite_rules are unset.' ); + $this->assertFalse( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is unset.' ); + + // Build the regular expression pattern for matching the custom endpoint URL structure. + $reg_ex_pattern = Utility::invoke_hidden_method( $instance, 'get_regex_pattern' ); + + // Define the URL structure for handling matched requests via query vars. + // Example result: 'index.php?gatherpress_event=$matches[1]&gatherpress_ext_calendar=$matches[2]'. + $rewrite_url = add_query_arg( $instance->get_rewrite_atts(), 'index.php' ); + + Utility::invoke_hidden_method( $instance, 'maybe_flush_rewrite_rules', array( $reg_ex_pattern, $rewrite_url ) ); + $this->assertTrue( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is set now.' ); + + // Normally done automatically via ... + flush_rewrite_rules( false ); + delete_option('gatherpress_flush_rewrite_rules_flag'); + + $this->assertContains( + $reg_ex_pattern, + array_keys( get_option('rewrite_rules') ), + 'Failed to assert that the GatherPress rewrite_rules are now part of the rewrite_rules option.' + ); + $this->assertSame( + $rewrite_url, + get_option('rewrite_rules')[ $reg_ex_pattern ], + 'Failed to assert that the GatherPress rewrite_rules have been saved correctly.' + ); + + // Run again. + Utility::invoke_hidden_method( $instance, 'maybe_flush_rewrite_rules', array( $reg_ex_pattern, $rewrite_url ) ); + $this->assertFalse( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that the GatherPress\' flag to flush the rewrite_rules is not set again after the rewrite_rules were flushed.' ); + } /** * Coverage for allow_query_vars method. From b4d9617d94ea6296e2e08d93aee135e8c15069d7 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 1 Oct 2024 23:33:26 +0200 Subject: [PATCH 29/52] NEW test for get_regex_pattern() method --- .../classes/endpoints/class-test-endpoint.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index dc8b5d78d..32ba18763 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -54,6 +54,40 @@ public function test___construct(): void { $this->assertSame( 'post_type', $instance->object_type, 'Failed to assert that object_type is set by default.' ); } + /** + * Coverage for get_regex_pattern method. + * + * @covers ::get_regex_pattern + * + * @return void + */ + public function test_get_regex_pattern(): void { + $query_var = 'query_var'; + $post_type = 'gatherpress_event'; + $callback = function () {}; + $types = array( + new Endpoint_Template( 'endpoint_template_1', $callback ), + new Endpoint_Template( 'endpoint_template_2', $callback ), + new Endpoint_Redirect( 'endpoint_redirect_1', $callback ), + ); + // Regular expression to match singular event endpoints. + // Example: 'event/my-sample-event/(custom-endpoint)(/)'. + $reg_ex = '%s/([^/]+)/(%s)/?$'; + $instance = new Endpoint( + $query_var, + $post_type, + $callback, + $types, + $reg_ex, + ); + + $this->assertSame( + 'event/([^/]+)/(endpoint_template_1|endpoint_template_2|endpoint_redirect_1)/?$', + Utility::invoke_hidden_method( $instance, 'get_regex_pattern' ), + 'Failed to assert that the generated regex pattern matches.' + ); + } + /** * Coverage for get_rewrite_atts method. * From 8814165c6aed1bcf912ee957823b32fc567e43ac Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Wed, 2 Oct 2024 00:29:05 +0200 Subject: [PATCH 30/52] Move activate() method into Endpoint_Types --- .../endpoints/class-endpoint-redirect.php | 5 +- .../endpoints/class-endpoint-template.php | 47 ++++++++++- .../classes/endpoints/class-endpoint-type.php | 5 +- .../core/classes/endpoints/class-endpoint.php | 79 +++++-------------- .../class-test-endpoint-template.php | 20 +++++ .../classes/endpoints/class-test-endpoint.php | 12 +-- 6 files changed, 98 insertions(+), 70 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-redirect.php b/includes/core/classes/endpoints/class-endpoint-redirect.php index 635d5225c..73af5c4c2 100644 --- a/includes/core/classes/endpoints/class-endpoint-redirect.php +++ b/includes/core/classes/endpoints/class-endpoint-redirect.php @@ -16,6 +16,8 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use GatherPress\Core\Endpoints\Endpoint; + /** * Handles safe URL redirection for custom endpoints in GatherPress. * @@ -52,9 +54,10 @@ class Endpoint_Redirect extends Endpoint_Type { * * @since 1.0.0 * + * @param Endpoint|null $endpoint Class for custom rewrite endpoints and their query handling in GatherPress. * @return void */ - public function activate(): void { + public function activate( ?Endpoint $endpoint = null ): void { $this->url = ( $this->callback )(); if ( $this->url ) { // Add the target host to the list of allowed redirect hosts. diff --git a/includes/core/classes/endpoints/class-endpoint-template.php b/includes/core/classes/endpoints/class-endpoint-template.php index cc4cf603d..fef606e35 100644 --- a/includes/core/classes/endpoints/class-endpoint-template.php +++ b/includes/core/classes/endpoints/class-endpoint-template.php @@ -15,6 +15,7 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use GatherPress\Core\Endpoints\Endpoint; use GatherPress\Core\Utility; /** @@ -66,11 +67,51 @@ public function __construct( string $slug, callable $callback, string $plugin_te * * @since 1.0.0 * + * @param Endpoint|null $endpoint Class for custom rewrite endpoints and their query handling in GatherPress. * @return void */ - public function activate(): void { - // Filters the path of the current template before including it. - add_filter( 'template_include', array( $this, 'template_include' ) ); + public function activate( ?Endpoint $endpoint = null ): void { + + // A call to any /feed/ endpoint is handled different by WordPress + // and as such the 'Endpoint_Template's template_include hook would fail. + $feed_slug = ( null !== $endpoint ) ? $endpoint->has_feed() : false; + if ( $feed_slug ) { + // Hook into WordPress' feed handling to load the custom feed template. + add_action( sprintf( 'do_feed_%s', $feed_slug ), array( $this, 'load_feed_template' ) ); + } else { + // Filters the path of the current template before including it. + add_filter( 'template_include', array( $this, 'template_include' ) ); + } + } + + /** + * Load the theme-overridable feed template from the plugin. + * + * This method ensures that a feed template is loaded when a request is made to + * a custom feed endpoint. If the theme provides an override for the feed template, + * it will be used; otherwise, the default template from the plugin is loaded. The + * method ensures that WordPress does not return a 404 for custom feed URLs. + * + * A call to any post types /feed/anything endpoint is handled by WordPress + * prior 'Endpoint_Template's template_include hook would run. + * Therefore WordPress will throw an xml'ed 404 error, + * if nothing is hooked onto the 'do_feed_anything' action. + * + * That's the reason for this method, it delivers what WordPress wants + * and re-uses the parameters provided by the class. + * + * We expect that a endpoint, that contains the /feed/ string, only has one 'Redirect_Template' attached. + * This might be wrong or short sightened, please open an issue in that case: https://github.com/GatherPress/gatherpress/issues + * + * Until then, we *just* use the first of the provided endpoint-types, + * to hook into WordPress, which should be the valid template endpoint. + * + * @since 1.0.0 + * + * @return void + */ + public function load_feed_template() { + load_template( $this->template_include( false ) ); } /** diff --git a/includes/core/classes/endpoints/class-endpoint-type.php b/includes/core/classes/endpoints/class-endpoint-type.php index 4f749562c..1630a4e30 100644 --- a/includes/core/classes/endpoints/class-endpoint-type.php +++ b/includes/core/classes/endpoints/class-endpoint-type.php @@ -15,6 +15,8 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use GatherPress\Core\Endpoints\Endpoint; + /** * Abstract class for defining custom endpoint behavior. * @@ -84,9 +86,10 @@ public function __construct( * * @since 1.0.0 * + * @param Endpoint|null $endpoint Class for custom rewrite endpoints and their query handling in GatherPress. * @return void */ - abstract public function activate(): void; + abstract public function activate( ?Endpoint $endpoint = null ): void; /** diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index f4a695422..5c1caf51a 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -1,6 +1,6 @@ has_feed_template(); - if ( $feed_slug ) { - // Hook into WordPress' feed handling to load the custom feed template. - add_action( sprintf( 'do_feed_%s', $feed_slug ), array( $this, 'load_feed_template' ) ); - } else { - // Handle whether to include a template or redirect the request. - add_action( 'template_redirect', array( $this, 'template_redirect' ) ); - } + // Handle whether to include a template or redirect the request. + add_action( 'template_redirect', array( $this, 'template_redirect' ) ); } /** @@ -359,53 +351,6 @@ public function allow_query_vars( array $public_query_vars ): array { return $public_query_vars; } - /** - * Determine whether the endpoint is meant for a feed - * and if it has a proper Endpoint_Template defined. - * - * @return string The slug of the endpoint or an empty string if not a feed template. - */ - protected function has_feed_template(): string { - if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { - $feed_slug = current( $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' ) ); - if ( ! empty( $feed_slug ) ) { - return $feed_slug; - } - } - return ''; - } - - /** - * Load the theme-overridable feed template from the plugin. - * - * This method ensures that a feed template is loaded when a request is made to - * a custom feed endpoint. If the theme provides an override for the feed template, - * it will be used; otherwise, the default template from the plugin is loaded. The - * method ensures that WordPress does not return a 404 for custom feed URLs. - * - * A call to any post types /feed/anything endpoint is handled by WordPress - * prior 'Endpoint_Template's template_include hook would run. - * Therefore WordPress will throw an xml'ed 404 error, - * if nothing is hooked onto the 'do_feed_anything' action. - * - * That's the reason for this method, it delivers what WordPress wants - * and re-uses the parameters provided by the class. - * - * We expect that a endpoint, that contains the /feed/ string, only has one 'Redirect_Template' attached. - * This might be wrong or short sightened, please open an issue in that case: https://github.com/GatherPress/gatherpress/issues - * - * Until then, we *just* use the first of the provided endpoint-types, - * to hook into WordPress, which should be the valid template endpoint. - * - * @since 1.0.0 - * - * @return void - */ - public function load_feed_template() { - load_template( $this->types[0]->template_include( false ) ); - } - - /** * Fires before determining which template to load or whether to redirect. * @@ -435,7 +380,23 @@ public function template_redirect(): void { ) ) ); - $endpoint_type->activate(); + $endpoint_type->activate( $this ); + } + + /** + * Determine whether the endpoint is meant for a feed + * and if it has a proper Endpoint_Template defined. + * + * @return string The slug of the endpoint or an empty string if not a feed template. + */ + protected function has_feed(): string { + if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { + $feed_slug = current( $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' ) ); + if ( ! empty( $feed_slug ) ) { + return $feed_slug; + } + } + return ''; } /** diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 2551fd92b..5ab76ed39 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -57,6 +57,26 @@ public function test___construct(): void { ); } + /** + * Coverage for activate method. + * + * @covers ::activate + * + * @return void + */ + public function test_activate(): void { + $slug = 'endpoint-template'; + $callback = function () { + return array( + 'file_name' => 'endpoint-template.php', + 'dir_path' => '/path/to/theme', + ); + }; + $instance = new Endpoint_Template( $slug, $callback ); + + + } + /** * Tests template_include method when the theme has the template. * diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 32ba18763..2a118126f 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -223,13 +223,13 @@ public function test_allow_query_vars(): void { } /** - * Coverage for has_feed_template method. + * Coverage for has_feed method. * - * @covers ::has_feed_template + * @covers ::has_feed * * @return void */ - public function test_has_feed_template(): void { + public function test_has_feed(): void { $query_var = 'query_var'; $post_type = 'gatherpress_event'; $callback = function () {}; @@ -248,7 +248,7 @@ public function test_has_feed_template(): void { ); $this->assertEmpty( - Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + Utility::invoke_hidden_method( $instance, 'has_feed' ), 'Failed to assert, endpoint is not for feeds.' ); @@ -265,7 +265,7 @@ public function test_has_feed_template(): void { ); $this->assertEmpty( - Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + Utility::invoke_hidden_method( $instance, 'has_feed' ), 'Failed to assert, endpoint is for feeds, but has no Endpoint_Template type.' ); @@ -284,7 +284,7 @@ public function test_has_feed_template(): void { $this->assertSame( 'endpoint_template_1', - Utility::invoke_hidden_method( $instance, 'has_feed_template' ), + Utility::invoke_hidden_method( $instance, 'has_feed' ), 'Failed to assert, that feed template is found.' ); } From d73cb795f7372b49da8bef815cfa8a2fad60884b Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Wed, 2 Oct 2024 00:40:15 +0200 Subject: [PATCH 31/52] Fix for CS --- .../class-test-endpoint-template.php | 2 -- .../classes/endpoints/class-test-endpoint.php | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php index 5ab76ed39..8e7c9efa9 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint-template.php @@ -73,8 +73,6 @@ public function test_activate(): void { ); }; $instance = new Endpoint_Template( $slug, $callback ); - - } /** diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 2a118126f..32dea81ce 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -72,8 +72,8 @@ public function test_get_regex_pattern(): void { ); // Regular expression to match singular event endpoints. // Example: 'event/my-sample-event/(custom-endpoint)(/)'. - $reg_ex = '%s/([^/]+)/(%s)/?$'; - $instance = new Endpoint( + $reg_ex = '%s/([^/]+)/(%s)/?$'; + $instance = new Endpoint( $query_var, $post_type, $callback, @@ -141,8 +141,8 @@ public function test_maybe_flush_rewrite_rules(): void { ); // Regular expression to match singular event endpoints. // Example: 'event/my-sample-event/(custom-endpoint)(/)'. - $reg_ex = '%s/([^/]+)/(%s)/?$'; - $instance = new Endpoint( + $reg_ex = '%s/([^/]+)/(%s)/?$'; + $instance = new Endpoint( $query_var, $post_type, $callback, @@ -150,12 +150,12 @@ public function test_maybe_flush_rewrite_rules(): void { $reg_ex, ); - delete_option('rewrite_rules'); - delete_option('gatherpress_flush_rewrite_rules_flag'); + delete_option( 'rewrite_rules' ); + delete_option( 'gatherpress_flush_rewrite_rules_flag' ); + + $this->assertEmpty( get_option( 'rewrite_rules' ), 'Failed to assert that rewrite_rules are unset.' ); + $this->assertFalse( get_option( 'gatherpress_flush_rewrite_rules_flag' ), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is unset.' ); - $this->assertEmpty( get_option('rewrite_rules'), 'Failed to assert that rewrite_rules are unset.' ); - $this->assertFalse( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is unset.' ); - // Build the regular expression pattern for matching the custom endpoint URL structure. $reg_ex_pattern = Utility::invoke_hidden_method( $instance, 'get_regex_pattern' ); @@ -164,26 +164,26 @@ public function test_maybe_flush_rewrite_rules(): void { $rewrite_url = add_query_arg( $instance->get_rewrite_atts(), 'index.php' ); Utility::invoke_hidden_method( $instance, 'maybe_flush_rewrite_rules', array( $reg_ex_pattern, $rewrite_url ) ); - $this->assertTrue( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is set now.' ); + $this->assertTrue( get_option( 'gatherpress_flush_rewrite_rules_flag' ), 'Failed to assert that GatherPress\' flag to flush the rewrite_rules is set now.' ); // Normally done automatically via ... flush_rewrite_rules( false ); - delete_option('gatherpress_flush_rewrite_rules_flag'); + delete_option( 'gatherpress_flush_rewrite_rules_flag' ); $this->assertContains( $reg_ex_pattern, - array_keys( get_option('rewrite_rules') ), + array_keys( get_option( 'rewrite_rules' ) ), 'Failed to assert that the GatherPress rewrite_rules are now part of the rewrite_rules option.' ); $this->assertSame( $rewrite_url, - get_option('rewrite_rules')[ $reg_ex_pattern ], + get_option( 'rewrite_rules' )[ $reg_ex_pattern ], 'Failed to assert that the GatherPress rewrite_rules have been saved correctly.' ); - + // Run again. Utility::invoke_hidden_method( $instance, 'maybe_flush_rewrite_rules', array( $reg_ex_pattern, $rewrite_url ) ); - $this->assertFalse( get_option('gatherpress_flush_rewrite_rules_flag'), 'Failed to assert that the GatherPress\' flag to flush the rewrite_rules is not set again after the rewrite_rules were flushed.' ); + $this->assertFalse( get_option( 'gatherpress_flush_rewrite_rules_flag' ), 'Failed to assert that the GatherPress\' flag to flush the rewrite_rules is not set again after the rewrite_rules were flushed.' ); } /** From 4e96f17b5b5cff1810a5b3f7dbb911f216376444 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 4 Oct 2024 04:24:21 +0200 Subject: [PATCH 32/52] Fix: Parameter #1 $template of method GatherPress\Core\Endpoints\Endpoint_Template::template_include() expects string, false given. --- includes/core/classes/endpoints/class-endpoint-template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/endpoints/class-endpoint-template.php b/includes/core/classes/endpoints/class-endpoint-template.php index fef606e35..0b0c79649 100644 --- a/includes/core/classes/endpoints/class-endpoint-template.php +++ b/includes/core/classes/endpoints/class-endpoint-template.php @@ -111,7 +111,7 @@ public function activate( ?Endpoint $endpoint = null ): void { * @return void */ public function load_feed_template() { - load_template( $this->template_include( false ) ); + load_template( $this->template_include( '' ) ); } /** From 73a791b198d3fe80866e3a945cf3b3c018f440e5 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 4 Oct 2024 04:24:55 +0200 Subject: [PATCH 33/52] Remove missleading prio from error message. --- includes/core/classes/endpoints/class-endpoint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index 5c1caf51a..bae8f41f3 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -280,7 +280,7 @@ private function is_valid_registration( string $type_name, array $types, string if ( 0 === did_action( 'init' ) ) { wp_trigger_error( __CLASS__, - 'was called too early! Run on init:11 to make all the rewrite-vodoo work.', + 'was called too early! Run on init to make all the rewrite-vodoo work.', E_USER_WARNING ); return false; From dc4384fcdfb8a255d891262dcbf95e33e101e2a9 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 4 Oct 2024 04:32:58 +0200 Subject: [PATCH 34/52] Fix: Call to function is_string() with string will always evaluate to true. --- .../endpoints/class-endpoint-template.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/includes/core/classes/endpoints/class-endpoint-template.php b/includes/core/classes/endpoints/class-endpoint-template.php index 0b0c79649..4861a066c 100644 --- a/includes/core/classes/endpoints/class-endpoint-template.php +++ b/includes/core/classes/endpoints/class-endpoint-template.php @@ -111,7 +111,7 @@ public function activate( ?Endpoint $endpoint = null ): void { * @return void */ public function load_feed_template() { - load_template( $this->template_include( '' ) ); + load_template( $this->template_include() ); } /** @@ -124,10 +124,11 @@ public function load_feed_template() { * * @since 1.0.0 * - * @param string $template The path of the default template to include. + * @param string $template The path of the default template to include, + * defaults to '' so that the template loader keeps looking for templates. * @return string The path of the template to include, either from the theme or plugin. */ - public function template_include( string $template ): string { + public function template_include( string $template = '' ): string { $presets = $this->get_template_presets(); $file_name = $presets['file_name']; @@ -165,9 +166,9 @@ protected function get_template_presets(): array { * @todo Maybe better put in the Utility class? * * @param string $file_name The name of the template file. - * @return string|false The path to the theme template or false if not found. + * @return string The path to the theme template or an empty string if not found. */ - protected function get_template_from_theme( string $file_name ) { + protected function get_template_from_theme( string $file_name ): string { // locate_template() doesn't cares, // but locate_block_template() needs this to be an array. @@ -184,7 +185,7 @@ protected function get_template_from_theme( string $file_name ) { $templates ); - return ( is_string( $template ) && ! empty( $template ) ) ? $template : false; + return $template; } /** @@ -194,7 +195,7 @@ protected function get_template_from_theme( string $file_name ) { * * @param string $file_name The name of the template file. * @param string $dir_path The directory path where the template is stored. - * @return string|false The full path to the template file or false if file not exists. + * @return string The full path to the template file or an empty string if file not exists. */ protected function get_template_from_plugin( string $file_name, string $dir_path ): string { // Remove prefix to keep file-names simple, @@ -204,6 +205,6 @@ protected function get_template_from_plugin( string $file_name, string $dir_path } $template = trailingslashit( $dir_path ) . $file_name; - return file_exists( $template ) ? $template : false; + return file_exists( $template ) ? $template : ''; } } From 7480c2acb5a698bee7e6e93b7e2b8325a34bab09 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 4 Oct 2024 04:38:01 +0200 Subject: [PATCH 35/52] Fix: Call to protected method has_feed() of class GatherPress\Core\Endpoints\Endpoint. --- .../core/classes/endpoints/class-endpoint.php | 2 +- phpstan.neon | 42 +++++++++++++++++++ .../classes/endpoints/class-test-endpoint.php | 6 +-- 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 phpstan.neon diff --git a/includes/core/classes/endpoints/class-endpoint.php b/includes/core/classes/endpoints/class-endpoint.php index bae8f41f3..a0744193a 100644 --- a/includes/core/classes/endpoints/class-endpoint.php +++ b/includes/core/classes/endpoints/class-endpoint.php @@ -389,7 +389,7 @@ public function template_redirect(): void { * * @return string The slug of the endpoint or an empty string if not a feed template. */ - protected function has_feed(): string { + public function has_feed(): string { if ( false !== strpos( $this->reg_ex, '/feed/' ) ) { $feed_slug = current( $this->get_slugs( __NAMESPACE__ . '\Endpoint_Template' ) ); if ( ! empty( $feed_slug ) ) { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..a77b8b0d6 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,42 @@ +includes: + - vendor/szepeviktor/phpstan-wordpress/extension.neon + # Bleeding edge offers a preview of the next major version. + # When you enable Bleeding edge in your configuration file, you will get new rules, + # behaviour, and bug fixes that will be enabled for everyone later + # when the next PHPStan’s major version is released. + - phar://phpstan.phar/conf/bleedingEdge.neon + +parameters: + bootstrapFiles: + # Constants, functions, etc. used by GatherPress + #- phpstan.stubs +# - vendor/pmc/unit-test/src/classes/autoloader.php +# - vendor/autoload.php +# - gatherpress.php + parallel: + maximumNumberOfProcesses: 1 + processTimeout: 300.0 + + # the analysis level, from 0 (loose) to 9 (strict) + # https://phpstan.org/user-guide/rule-levels + level: 5 + + paths: + - includes/ +# - test/ + +# excludePaths: +# analyse: +# - vendor/ + + ignoreErrors: + - '#^Constant GATHERPRESS_CORE_FILE not found\.$#' + - '#^Constant GATHERPRESS_CORE_PATH not found\.$#' + - '#^Constant GATHERPRESS_CORE_URL not found\.$#' + - '#^Constant GATHERPRESS_REST_NAMESPACE not found\.$#' + - '#^Constant GATHERPRESS_REQUIRES_PHP not found\.$#' + + # core/classes/class-setup.php + # + # A dev-only error, which can occur if the gatherpress is symlinked into a WP instance or called via wp-env or Playground. + - '#^Path in require_once\(\) "\./wp-admin/includes/upgrade\.php" is not a file or it does not exist\.$#' \ No newline at end of file diff --git a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php index 32dea81ce..52091fdea 100644 --- a/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php +++ b/test/unit/php/includes/core/classes/endpoints/class-test-endpoint.php @@ -248,7 +248,7 @@ public function test_has_feed(): void { ); $this->assertEmpty( - Utility::invoke_hidden_method( $instance, 'has_feed' ), + $instance->has_feed(), 'Failed to assert, endpoint is not for feeds.' ); @@ -265,7 +265,7 @@ public function test_has_feed(): void { ); $this->assertEmpty( - Utility::invoke_hidden_method( $instance, 'has_feed' ), + $instance->has_feed(), 'Failed to assert, endpoint is for feeds, but has no Endpoint_Template type.' ); @@ -284,7 +284,7 @@ public function test_has_feed(): void { $this->assertSame( 'endpoint_template_1', - Utility::invoke_hidden_method( $instance, 'has_feed' ), + $instance->has_feed(), 'Failed to assert, that feed template is found.' ); } From fe58c49fb273829bc0582d575953503c181917ed Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 00:13:39 +0200 Subject: [PATCH 36/52] New class responsible for managing calendar-related endpoints in GatherPress. --- includes/core/classes/class-calendars.php | 826 ++++++++++++++++++ includes/core/classes/class-event-setup.php | 1 + includes/core/classes/class-event.php | 150 +--- includes/core/classes/class-setup.php | 1 + .../core/classes/class-test-event.php | 55 +- 5 files changed, 883 insertions(+), 150 deletions(-) create mode 100644 includes/core/classes/class-calendars.php diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php new file mode 100644 index 000000000..53acbd809 --- /dev/null +++ b/includes/core/classes/class-calendars.php @@ -0,0 +1,826 @@ +setup_hooks(); + } + + /** + * Set up hooks for registering custom calendar endpoints. + * + * This method hooks into the `registered_post_type_{post_type}` action to ensure that + * the custom endpoints for the `gatherpress_event` post type are registered after the + * post type is initialized. + * + * @since 1.0.0 + * + * @return void + */ + protected function setup_hooks(): void { + add_action( + sprintf( + 'registered_post_type_%s', + 'gatherpress_event' + ), + array( $this, 'init_events' ), + ); + add_action( + sprintf( + 'registered_post_type_%s', + 'gatherpress_venue' + ), + array( $this, 'init_venues' ), + ); + // @todo Maybe hook this two actions dynamically based on a registered post type?! + add_action( + 'registered_taxonomy_for_object_type', + array( $this, 'init_taxonomies' ), + 10, + 2 + ); + add_action( + 'registered_taxonomy', + array( $this, 'init_taxonomies' ), + 10, + 2 + ); + add_action( 'wp_head', array( $this, 'alternate_links' ) ); + } + + /** + * Initializes the custom calendar endpoints for single events. + * + * This method sets up a `Posttype_Single_Endpoint` for the `gatherpress_event` post type + * (because this is this class' default post type), + * adding custom endpoints for external calendar services (Google Calendar, Yahoo Calendar) + * and download templates for iCal and Outlook. + * + * @since 1.0.0 + * + * @return void + */ + public function init_events(): void { + + // Important: Register the feed endpoint before the single endpoint, + // to make sure rewrite rules get saved in the correct order. + new Posttype_Feed_Endpoint( + array( + new Endpoint_Template( self::ICAL_SLUG, array( $this, 'get_ical_feed_template' ) ), + ), + self::QUERY_VAR + ); + new Posttype_Single_Endpoint( + array( + new Endpoint_Template( self::ICAL_SLUG, array( $this, 'get_ical_file_template' ) ), + new Endpoint_Template( 'outlook', array( $this, 'get_ical_file_template' ) ), + new Endpoint_Redirect( 'google-calendar', array( $this, 'get_google_calendar_link' ) ), + new Endpoint_Redirect( 'yahoo-calendar', array( $this, 'get_yahoo_calendar_link' ) ), + ), + self::QUERY_VAR + ); + } + + /** + * Initializes the custom calendar endpoints for single venues. + * + * This method sets up a `Posttype_Single_Endpoint` for the `gatherpress_venue` post type. + * + * @since 1.0.0 + * + * @return void + */ + public function init_venues(): void { + new Posttype_Single_Feed_Endpoint( + array( + new Endpoint_Template( self::ICAL_SLUG, array( $this, 'get_ical_feed_template' ) ), + ), + self::QUERY_VAR + ); + } + + /** + * Initializes the custom calendar endpoints for taxonomies that belong to events. + * + * This method sets up one `Taxonomy_Feed_Endpoint` for each taxonomy, + * that is registered for the `gatherpress_event` post type + * and publicly available. + * + * @param string $taxonomy Name of the taxonomy that got registered last. + * @param string|array $object_type This will be a string when called via 'registered_taxonomy_for_object_type', + * and could(!) be an array when called from 'registered_taxonomy'. + * + * @return void + */ + public function init_taxonomies( string $taxonomy, $object_type ): void { + + // Stop, if the currently registered taxonomy ... + if ( // ... is not registered for the events post type. + ! in_array( 'gatherpress_event', (array) $object_type, true ) || + // ... is GatherPress' shadow-taxonomy for venues. + '_gatherpress_venue' === $taxonomy || + // ... should not be public. + ! is_taxonomy_viewable( $taxonomy ) + ) { + return; + } + + new Taxonomy_Feed_Endpoint( + array( + new Endpoint_Template( self::ICAL_SLUG, array( $this, 'get_ical_feed_template' ) ), + ), + self::QUERY_VAR, + $taxonomy + ); + } + + /** + * Prints get_bloginfo( 'name' ), + /* translators: Separator between site name and feed type in feed links. */ + 'separator' => _x( '»', 'feed link separator', 'gatherpress' ), + /* translators: 1: Site name, 2: Separator (raquo), 3: Post title. */ + 'singletitle' => __( 'đź“… %1$s %2$s %3$s iCal Download', 'gatherpress' ), + /* translators: 1: Site title, 2: Separator (raquo). */ + 'feedtitle' => __( 'đź“… %1$s %2$s iCal Feed', 'gatherpress' ), + /* translators: 1: Site name, 2: Separator (raquo), 3: Post type name. */ + 'posttypetitle' => __( 'đź“… %1$s %2$s %3$s iCal Feed', 'gatherpress' ), + /* translators: 1: Site name, 2: Separator (raquo), 3: Term name, 4: Taxonomy singular name. */ + 'taxtitle' => __( 'đź“… %1$s %2$s %3$s %4$s iCal Feed', 'gatherpress' ), + ); + + $alternate_links = array(); + + // @todo "/feed/ical" could be enabled as alias of "/event/feed/ical", + // and called with "get_feed_link( self::ICAL_SLUG )". + $alternate_links[] = array( + 'url' => get_post_type_archive_feed_link( 'gatherpress_event', self::ICAL_SLUG ), + 'attr' => sprintf( + $args['feedtitle'], + $args['blogtitle'], + $args['separator'] + ), + ); + + if ( is_singular( 'gatherpress_event' ) ) { + $alternate_links[] = array( + 'url' => self::get_url( self::ICAL_SLUG ), + 'attr' => sprintf( + $args['singletitle'], + $args['blogtitle'], + $args['separator'], + the_title_attribute( array( 'echo' => false ) ) + ), + ); + + // Get all terms, associated with the current event-post. + $terms = get_terms( + array( + 'taxonomy' => get_object_taxonomies( get_queried_object_id() ), + 'object_ids' => get_queried_object_id(), + ) + ); + // Loop over terms and generate the ical feed links for the . + array_walk( + $terms, + function ( WP_Term $term ) use ( $args, &$alternate_links ) { + $tax = get_taxonomy( $term->taxonomy ); + switch ( $term->taxonomy ) { + case '_gatherpress_venue': + $gatherpress_venue = Venue::get_instance()->get_venue_post_from_term_slug( $term->slug ); + + // An Online-Event will have no Venue; prevent error on non-existent object. + // Feels weird to use a *_comments_* function here, but it delivers clean results + // in the form of "domain.tld/event/my-sample-event/feed/ical/". + $href = ( $gatherpress_venue ) ? get_post_comments_feed_link( $gatherpress_venue->ID, self::ICAL_SLUG ) : null; + break; + + default: + $href = get_term_feed_link( $term->term_id, $term->taxonomy, self::ICAL_SLUG ); + break; + } + // Can be empty for Online-Events. + if ( ! empty( $href ) ) { + $alternate_links[] = array( + 'url' => $href, + 'attr' => sprintf( + $args['taxtitle'], + $args['blogtitle'], + $args['separator'], + $term->name, + $tax->labels->singular_name + ), + ); + } + } + ); + } elseif ( is_singular( 'gatherpress_venue' ) ) { + + // Feels weird to use a *_comments_* function here, but it delivers clean results + // in the form of "domain.tld/venue/my-sample-venue/feed/ical/". + $alternate_links[] = array( + 'url' => get_post_comments_feed_link( get_queried_object_id(), self::ICAL_SLUG ), + 'attr' => sprintf( + $args['singletitle'], + $args['blogtitle'], + $args['separator'], + the_title_attribute( array( 'echo' => false ) ) + ), + ); + } elseif ( is_tax() ) { + $term = get_queried_object(); + + if ( $term && is_object_in_taxonomy( 'gatherpress_event', $term->taxonomy ) ) { + $tax = get_taxonomy( $term->taxonomy ); + + $alternate_links[] = array( + 'url' => get_term_feed_link( $term->term_id, $term->taxonomy, self::ICAL_SLUG ), + 'attr' => sprintf( + $args['taxtitle'], + $args['blogtitle'], + $args['separator'], + $term->name, + $tax->labels->singular_name + ), + ); + } + } + + // Render tags into . + array_walk( + $alternate_links, + function ( $link ) { + printf( + '' . "\n", + esc_attr( isset( $link['type'] ) ? $link['type'] : 'text/calendar' ), + esc_attr( $link['attr'] ), + esc_url( $link['url'] ) + ); + } + ); + } + + /** + * Returns the template for the current calendar download. + * + * This method provides the template file to be used for iCal and Outlook downloads. + * + * By adding a file with the same name to your themes root folder + * or your themes `/templates` folder, this template will be used + * with priority over the default template provided by GatherPress. + * + * @since 1.0.0 + * + * @return array An array containing: + * - 'file_name': the file name of the template to be loaded from the theme. Will load defaults from the plugin if theme files do not exist. + * - 'dir_path': (Optional) Absolute path to some template directory outside of the theme folder. + */ + public function get_ical_file_template(): array { + return array( + 'file_name' => Utility::prefix_key( 'ical-download.php' ), + ); + } + + /** + * Returns the template for the subscribeable calendar feed. + * + * This method provides the template file to be used for ical-feeds. + * + * By adding a file with the same name to your themes root folder + * or your themes `/templates` folder, this template will be used + * with priority over the default template provided by GatherPress. + * + * @since 1.0.0 + * + * @return array An array containing: + * - 'file_name': the file name of the template to be loaded from the theme. + * Will load defaults from the plugin if theme files do not exist. + * - 'dir_path': (Optional) Absolute path to some template directory outside of the theme folder. + */ + public function get_ical_feed_template(): array { + return array( + 'file_name' => Utility::prefix_key( 'ical-feed.php' ), + ); + } + + /** + * Get sanitized endpoint url for a given slug, post and query parameter. + * + * Inspired by get_post_embed_url() + * + * @see https://developer.wordpress.org/reference/functions/get_post_embed_url/ + * + * @param string $endpoint_slug The visible suffix to the posts permalink. + * @param WP_Post|int|null $post The post to get the endpoint for. + * @param string $query_var The internal query variable used by WordPress to route the request. + * + * @return string|false URL of the posts endpoint or false if something went wrong. + */ + public static function get_url( string $endpoint_slug, $post = null, string $query_var = self::QUERY_VAR ) { + $post = get_post( $post ); + + if ( ! $post ) { + return false; + } + + $is_feed_endpoint = strpos( 'feed/', $endpoint_slug ); + if ( false !== $is_feed_endpoint ) { + // Feels weird to use a *_comments_* function here, but it delivers clean results + // in the form of "domain.tld/event/my-sample-event/feed/ical/". + return (string) get_post_comments_feed_link( + $post->ID, + substr( $endpoint_slug, $is_feed_endpoint ) + ); + } + + $post_url = get_permalink( $post ); + $endpoint_url = trailingslashit( $post_url ) . user_trailingslashit( $endpoint_slug ); + $path_conflict = get_page_by_path( + str_replace( home_url(), '', $endpoint_url ), + OBJECT, + get_post_types( array( 'public' => true ) ) + ); + + if ( ! get_option( 'permalink_structure' ) || $path_conflict ) { + $endpoint_url = add_query_arg( array( $query_var => $endpoint_slug ), $post_url ); + } + + /** + * Filters the endpoint URL of a specific post. + * + * @since 1.0.0 + * + * @param string $endpoint_url The post embed URL. + * @param WP_Post $post The corresponding post object. + */ + $endpoint_url = sanitize_url( + apply_filters( + 'gatherpress_endpoint_url', + $endpoint_url, + $post + ) + ); + + return (string) sanitize_url( $endpoint_url ); + } + + /** + * Get the Google Calendar add event link for the event. + * + * This method generates and returns a Google Calendar link that allows users to add the event to their + * Google Calendar. The link includes event details such as the event name, date, time, location, and a + * link to the event's details page. + * + * @since 1.0.0 + * + * @return string The Google Calendar add event link for the event. + * + * @throws Exception If there is an issue while generating the Google Calendar link. + */ + public function get_google_calendar_link(): string { + $event = new Event( get_queried_object_id() ); + $date_start = $event->get_formatted_datetime( 'Ymd', 'start', false ); + $time_start = $event->get_formatted_datetime( 'His', 'start', false ); + $date_end = $event->get_formatted_datetime( 'Ymd', 'end', false ); + $time_end = $event->get_formatted_datetime( 'His', 'end', false ); + $datetime = sprintf( '%sT%sZ/%sT%sZ', $date_start, $time_start, $date_end, $time_end ); + $venue = $event->get_venue_information(); + $location = $venue['name']; + $description = $event->get_calendar_description(); + + if ( ! empty( $venue['full_address'] ) ) { + $location .= sprintf( ', %s', $venue['full_address'] ); + } + + $params = array( + 'action' => 'TEMPLATE', + 'text' => sanitize_text_field( $event->event->post_title ), + 'dates' => sanitize_text_field( $datetime ), + 'details' => sanitize_text_field( $description ), + 'location' => sanitize_text_field( $location ), + 'sprop' => 'name:', + ); + + return add_query_arg( + rawurlencode_deep( $params ), + 'https://www.google.com/calendar/event' + ); + } + + /** + * Get the "Add to Yahoo! Calendar" link for the event. + * + * This method generates and returns a URL that allows users to add the event to their Yahoo! Calendar. + * The URL includes event details such as the event title, start time, duration, description, and location. + * + * @since 1.0.0 + * + * @return string The Yahoo! Calendar link for adding the event. + * + * @throws Exception If an error occurs while generating the Yahoo! Calendar link. + */ + public function get_yahoo_calendar_link(): string { + $event = new Event( get_queried_object_id() ); + $date_start = $event->get_formatted_datetime( 'Ymd', 'start', false ); + $time_start = $event->get_formatted_datetime( 'His', 'start', false ); + $datetime_start = sprintf( '%sT%sZ', $date_start, $time_start ); + + // Figure out duration of event in hours and minutes: hhmm format. + $diff_start = $event->get_formatted_datetime( self::DATETIME_FORMAT, 'start', false ); + $diff_end = $event->get_formatted_datetime( self::DATETIME_FORMAT, 'end', false ); + $duration = ( ( strtotime( $diff_end ) - strtotime( $diff_start ) ) / 60 / 60 ); + $full = intval( $duration ); + $fraction = ( $duration - $full ); + $hours = str_pad( intval( $duration ), 2, '0', STR_PAD_LEFT ); + $minutes = str_pad( intval( $fraction * 60 ), 2, '0', STR_PAD_LEFT ); + $venue = $event->get_venue_information(); + $location = $venue['name']; + $description = $event->get_calendar_description(); + + if ( ! empty( $venue['full_address'] ) ) { + $location .= sprintf( ', %s', $venue['full_address'] ); + } + + $params = array( + 'v' => '60', + 'view' => 'd', + 'type' => '20', + 'title' => sanitize_text_field( $event->event->post_title ), + 'st' => sanitize_text_field( $datetime_start ), + 'dur' => sanitize_text_field( (string) $hours . (string) $minutes ), + 'desc' => sanitize_text_field( $description ), + 'in_loc' => sanitize_text_field( $location ), + ); + + return add_query_arg( + rawurlencode_deep( $params ), + 'https://calendar.yahoo.com/' + ); + } + + /** + * Wraps the provided calendar data in a VCALENDAR structure for iCal. + * + * This method generates the necessary headers for an iCal file (such as the `BEGIN:VCALENDAR` + * and `END:VCALENDAR` lines), wraps the provided calendar data, and returns the entire iCal + * content as a formatted string. It also includes the blog's title and the current locale + * in the `PRODID` header, ensuring that the calendar is properly identified. + * + * @since 1.0.0 + * + * @param string $calendar_data The events to be included in the iCal file. + * @return string The complete iCal data wrapped in the VCALENDAR format. + */ + public static function get_ical_wrap( string $calendar_data ): string { + + $args = array( + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + sprintf( + 'PRODID:-//%s//GatherPress//%s', + get_bloginfo( 'title' ), + // Prpeare 2-DIGIT lang code. + strtoupper( substr( get_locale(), 0, 2 ) ) + ), + $calendar_data, + 'END:VCALENDAR', + ); + + return implode( "\r\n", $args ); + } + + /** + * Get the ICS download link for the event. + * + * This method generates and returns a URL that allows users to download the event in ICS (iCalendar) format. + * The URL includes event details such as the event title, start time, end time, description, location, and more. + * + * @since 1.0.0 + * + * @return string The ICS download link for the event. + * + * @throws Exception If an error occurs while generating the ICS download link. + */ + public static function get_ical_event(): string { + + $event = new Event( get_queried_object_id() ); + $date_start = $event->get_formatted_datetime( 'Ymd', 'start', false ); + $time_start = $event->get_formatted_datetime( 'His', 'start', false ); + $date_end = $event->get_formatted_datetime( 'Ymd', 'end', false ); + $time_end = $event->get_formatted_datetime( 'His', 'end', false ); + $datetime_start = sprintf( '%sT%sZ', $date_start, $time_start ); + $datetime_end = sprintf( '%sT%sZ', $date_end, $time_end ); + $modified_date = strtotime( $event->event->post_modified ); + $datetime_stamp = sprintf( '%sT%sZ', gmdate( 'Ymd', $modified_date ), gmdate( 'His', $modified_date ) ); + $venue = $event->get_venue_information(); + $location = $venue['name']; + $description = $event->get_calendar_description(); + + if ( ! empty( $venue['full_address'] ) ) { + $location .= sprintf( ', %s', $venue['full_address'] ); + } + + $args = array( + 'BEGIN:VEVENT', + sprintf( 'URL:%s', esc_url_raw( get_permalink( $event->event->ID ) ) ), + sprintf( 'DTSTART:%s', sanitize_text_field( $datetime_start ) ), + sprintf( 'DTEND:%s', sanitize_text_field( $datetime_end ) ), + sprintf( 'DTSTAMP:%s', sanitize_text_field( $datetime_stamp ) ), + sprintf( 'SUMMARY:%s', self::eventorganiser_fold_ical_text( sanitize_text_field( $event->event->post_title ) ) ), + sprintf( 'DESCRIPTION:%s', self::eventorganiser_fold_ical_text( sanitize_text_field( $description ) ) ), + sprintf( 'LOCATION:%s', self::eventorganiser_fold_ical_text( sanitize_text_field( $location ) ) ), + 'UID:gatherpress_' . intval( $event->event->ID ), + 'END:VEVENT', + ); + + return implode( "\r\n", $args ); + } + + /** + * Generates a list of events in iCal format based on the current query. + * + * This method generates iCal data for a list of events, + * taking into account the currently queried archive or taxonomy context. + * + * It supports fetching events from: + * - The `gatherpress_event` post type archive (upcoming and past events). + * - Single `gatherpress_venue` requests (events specific to a venue). + * - and `gatherpress_topic` taxonomy (events tagged with specific topics). + * + * It builds an iCal event list, wraps each event in the appropriate iCal format, and returns + * the entire list of events as a single string. + * + * @since 1.0.0 + * + * @return string The iCal formatted list of events, ready for export or download. + */ + public static function get_ical_list(): string { + + $event_list_type = ''; // Keep empty, to get all events from upcoming & past. + $number = ( is_feed( self::ICAL_SLUG ) ) ? -1 : get_option( 'posts_per_page' ); + $topics = array(); + $venues = array(); + $output = array(); + + if ( is_singular( 'gatherpress_venue' ) ) { + $slug = '_' . get_queried_object()->post_name; + $venues = array( $slug ); + } elseif ( is_tax() ) { + $term = get_queried_object(); + + // @todo How to be prepared for foreign taxonomies that might be registered by 3rd-parties? + if ( $term && is_object_in_taxonomy( 'gatherpress_event', $term->taxonomy ) ) { + // Add the tax to the query here. + + if ( is_tax( 'gatherpress_topic' ) ) { + $topics = array( $term->slug ); + } + } + } + + $query = Event_Query::get_instance()->get_events_list( $event_list_type, $number, $topics, $venues ); + while ( $query->have_posts() ) { + $query->the_post(); + $output[] = self::get_ical_event(); + } + + // Restore original Post Data. + wp_reset_postdata(); + + return implode( "\r\n", $output ); + } + + /** + * Generates the complete iCal file content for an event. + * + * This method calls the `get_ical_event()` method to retrieve the event data in iCal format + * and then wraps the event data using `get_ical_wrap()` to include the necessary VCALENDAR + * headers and footers. It returns the fully formatted iCal file content as a string. + * + * @since 1.0.0 + * + * @return string The complete iCal file content for the event, ready for download or export. + */ + public static function get_ical_file(): string { + return self::get_ical_wrap( self::get_ical_event() ); + } + + /** + * Generates the complete iCal file content for a list of events. + * + * This method calls the `get_ical_list()` method to retrieve the data for all (queried) events in iCal format + * and then wraps the events using `get_ical_wrap()` to include the necessary VCALENDAR + * headers and footers. + * + * @since 1.0.0 + * + * @return string The complete iCal feed containing the list of events. + */ + public static function get_ical_feed(): string { + return self::get_ical_wrap( self::get_ical_list() ); + } + + /** + * Generate the .ics filename based on the queried object. + * + * @return string Name of the calendar ics file. + */ + public static function generate_ics_filename() { + $queried_object = get_queried_object(); + $filename = 'calendar'; + + if ( is_singular( 'gatherpress_event' ) ) { + $event = new Event( $queried_object->ID ); + $date = $event->get_datetime_start( 'Y-m-d' ); + $post_name = $event->event->post_name; + $filename = $date . '_' . $post_name; + } elseif ( is_singular( 'gatherpress_venue' ) ) { + $filename = $queried_object->post_name; + } elseif ( is_tax() ) { + + // @todo How to be prepared for foreign taxonomies that might be registered by 3rd-parties? + if ( is_object_in_taxonomy( 'gatherpress_event', $queried_object->taxonomy ) ) { + $filename = $queried_object->slug; + } + } + + return $filename . '.ics'; + } + + /** + * Send the necessary headers for the iCalendar file download. + * + * @param string $filename Generated name of the file. + * + * @return void + */ + public static function send_ics_headers( string $filename ) { + + $charset = strtolower( get_option( 'blog_charset' ) ); + + header( 'Content-Description: File Transfer' ); + + // Ensure proper content type for the calendar file. + header( 'Content-Type: text/calendar; charset=' . $charset ); + + // Force download in most browsers. + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + + // Avoid browser caching issues. + header( 'Cache-Control: no-store, no-cache, must-revalidate' ); + header( 'Cache-Control: post-check=0, pre-check=0', false ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + + // Prevent content sniffing which might lead to MIME type mismatch. + header( 'X-Content-Type-Options: nosniff' ); + } + + /** + * Output the event(s) as an iCalendar (.ics) file. + * + * @return void + */ + public static function send_ics_file() { + // Start output buffering to capture all output. + ob_start(); + + // Prepare the filename. + $filename = self::generate_ics_filename(); + + // Send headers for downloading the .ics file. + self::send_ics_headers( $filename ); + + // Output the generated iCalendar content. + $get_ical_method = ( is_feed() ) ? 'get_ical_feed' : 'get_ical_file'; + echo wp_kses_post( self::$get_ical_method() ); + + // Get the generated output and calculate file size. + $ics_content = ob_get_contents(); + $filesize = strlen( $ics_content ); + + // Send the file size in the header. + header( 'Content-Length: ' . $filesize ); + + // End output buffering and clean up. + ob_end_clean(); + + // Output the iCalendar content. + echo wp_kses_post( $ics_content ); + + exit(); // Terminate the script after the file has been output. + } + + /** + * @author Stephen Harris (@stephenharris) + * @source https://github.com/stephenharris/Event-Organiser/blob/develop/includes/event-organiser-utility-functions.php#L1663 + * + * Fold text as per [iCal specifications](http://www.ietf.org/rfc/rfc2445.txt) + * + * Lines of text SHOULD NOT be longer than 75 octets, excluding the line + * break. Long content lines SHOULD be split into a multiple line + * representations using a line "folding" technique. That is, a long + * line can be split between any two characters by inserting a CRLF + * immediately followed by a single linear white space character (i.e., + * SPACE, US-ASCII decimal 32 or HTAB, US-ASCII decimal 9). Any sequence + * of CRLF followed immediately by a single linear white space character + * is ignored (i.e., removed) when processing the content type. + * + * @ignore + * @since 2.7 + * @param string $text The string to be escaped. + * @return string The escaped string. + */ + private static function eventorganiser_fold_ical_text( string $text ): string { + + $text_arr = array(); + + $lines = ceil( mb_strlen( $text ) / 75 ); + + for ( $i = 0; $i < $lines; $i++ ) { + $text_arr[ $i ] = mb_substr( $text, $i * 75, 75 ); + } + + return join( "\r\n ", $text_arr ); + } +} diff --git a/includes/core/classes/class-event-setup.php b/includes/core/classes/class-event-setup.php index 0a94f3448..57220d7fa 100644 --- a/includes/core/classes/class-event-setup.php +++ b/includes/core/classes/class-event-setup.php @@ -15,6 +15,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Exception; +use GatherPress\Core\Event; use GatherPress\Core\Traits\Singleton; use WP_Post; diff --git a/includes/core/classes/class-event.php b/includes/core/classes/class-event.php index 66e491363..393e4d0a5 100644 --- a/includes/core/classes/class-event.php +++ b/includes/core/classes/class-event.php @@ -14,6 +14,7 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use GatherPress\Core\Calendars; use DateTimeZone; use Exception; use WP_Post; @@ -443,164 +444,23 @@ public function get_calendar_links(): array { return array( 'google' => array( 'name' => __( 'Google Calendar', 'gatherpress' ), - 'link' => $this->get_google_calendar_link(), + 'link' => Calendars::get_url( 'google-calendar', $this->event->ID ), ), 'ical' => array( 'name' => __( 'iCal', 'gatherpress' ), - 'download' => $this->get_ics_calendar_download(), + 'download' => Calendars::get_url( 'ical', $this->event->ID ), ), 'outlook' => array( 'name' => __( 'Outlook', 'gatherpress' ), - 'download' => $this->get_ics_calendar_download(), + 'download' => Calendars::get_url( 'outlook', $this->event->ID ), ), 'yahoo' => array( 'name' => __( 'Yahoo Calendar', 'gatherpress' ), - 'link' => $this->get_yahoo_calendar_link(), + 'link' => Calendars::get_url( 'yahoo-calendar', $this->event->ID ), ), ); } - /** - * Get the Google Calendar add event link for the event. - * - * This method generates and returns a Google Calendar link that allows users to add the event to their - * Google Calendar. The link includes event details such as the event name, date, time, location, and a - * link to the event's details page. - * - * @since 1.0.0 - * - * @return string The Google Calendar add event link for the event. - * - * @throws Exception If there is an issue while generating the Google Calendar link. - */ - protected function get_google_calendar_link(): string { - $date_start = $this->get_formatted_datetime( 'Ymd', 'start', false ); - $time_start = $this->get_formatted_datetime( 'His', 'start', false ); - $date_end = $this->get_formatted_datetime( 'Ymd', 'end', false ); - $time_end = $this->get_formatted_datetime( 'His', 'end', false ); - $datetime = sprintf( '%sT%sZ/%sT%sZ', $date_start, $time_start, $date_end, $time_end ); - $venue = $this->get_venue_information(); - $location = $venue['name']; - $description = $this->get_calendar_description(); - - if ( ! empty( $venue['full_address'] ) ) { - $location .= sprintf( ', %s', $venue['full_address'] ); - } - - $params = array( - 'action' => 'TEMPLATE', - 'text' => sanitize_text_field( $this->event->post_title ), - 'dates' => sanitize_text_field( $datetime ), - 'details' => sanitize_text_field( $description ), - 'location' => sanitize_text_field( $location ), - 'sprop' => 'name:', - ); - - return add_query_arg( - rawurlencode_deep( $params ), - 'https://www.google.com/calendar/event' - ); - } - - /** - * Get the "Add to Yahoo! Calendar" link for the event. - * - * This method generates and returns a URL that allows users to add the event to their Yahoo! Calendar. - * The URL includes event details such as the event title, start time, duration, description, and location. - * - * @since 1.0.0 - * - * @return string The Yahoo! Calendar link for adding the event. - * - * @throws Exception If an error occurs while generating the Yahoo! Calendar link. - */ - protected function get_yahoo_calendar_link(): string { - $date_start = $this->get_formatted_datetime( 'Ymd', 'start', false ); - $time_start = $this->get_formatted_datetime( 'His', 'start', false ); - $datetime_start = sprintf( '%sT%sZ', $date_start, $time_start ); - - // Figure out duration of event in hours and minutes: hhmm format. - $diff_start = $this->get_formatted_datetime( self::DATETIME_FORMAT, 'start', false ); - $diff_end = $this->get_formatted_datetime( self::DATETIME_FORMAT, 'end', false ); - $duration = ( ( strtotime( $diff_end ) - strtotime( $diff_start ) ) / 60 / 60 ); - $full = intval( $duration ); - $fraction = ( $duration - $full ); - $hours = str_pad( intval( $duration ), 2, '0', STR_PAD_LEFT ); - $minutes = str_pad( intval( $fraction * 60 ), 2, '0', STR_PAD_LEFT ); - $venue = $this->get_venue_information(); - $location = $venue['name']; - $description = $this->get_calendar_description(); - - if ( ! empty( $venue['full_address'] ) ) { - $location .= sprintf( ', %s', $venue['full_address'] ); - } - - $params = array( - 'v' => '60', - 'view' => 'd', - 'type' => '20', - 'title' => sanitize_text_field( $this->event->post_title ), - 'st' => sanitize_text_field( $datetime_start ), - 'dur' => sanitize_text_field( (string) $hours . (string) $minutes ), - 'desc' => sanitize_text_field( $description ), - 'in_loc' => sanitize_text_field( $location ), - ); - - return add_query_arg( - rawurlencode_deep( $params ), - 'https://calendar.yahoo.com/' - ); - } - - /** - * Get the ICS download link for the event. - * - * This method generates and returns a URL that allows users to download the event in ICS (iCalendar) format. - * The URL includes event details such as the event title, start time, end time, description, location, and more. - * - * @since 1.0.0 - * - * @return string The ICS download link for the event. - * - * @throws Exception If an error occurs while generating the ICS download link. - */ - protected function get_ics_calendar_download(): string { - $date_start = $this->get_formatted_datetime( 'Ymd', 'start', false ); - $time_start = $this->get_formatted_datetime( 'His', 'start', false ); - $date_end = $this->get_formatted_datetime( 'Ymd', 'end', false ); - $time_end = $this->get_formatted_datetime( 'His', 'end', false ); - $datetime_start = sprintf( '%sT%sZ', $date_start, $time_start ); - $datetime_end = sprintf( '%sT%sZ', $date_end, $time_end ); - $modified_date = strtotime( $this->event->post_modified ); - $datetime_stamp = sprintf( '%sT%sZ', gmdate( 'Ymd', $modified_date ), gmdate( 'His', $modified_date ) ); - $venue = $this->get_venue_information(); - $location = $venue['name']; - $description = $this->get_calendar_description(); - - if ( ! empty( $venue['full_address'] ) ) { - $location .= sprintf( ', %s', $venue['full_address'] ); - } - - $args = array( - 'BEGIN:VCALENDAR', - 'VERSION:2.0', - 'PRODID:-//GatherPress//RemoteApi//EN', - 'BEGIN:VEVENT', - sprintf( 'URL:%s', esc_url_raw( get_permalink( $this->event->ID ) ) ), - sprintf( 'DTSTART:%s', sanitize_text_field( $datetime_start ) ), - sprintf( 'DTEND:%s', sanitize_text_field( $datetime_end ) ), - sprintf( 'DTSTAMP:%s', sanitize_text_field( $datetime_stamp ) ), - sprintf( 'SUMMARY:%s', sanitize_text_field( $this->event->post_title ) ), - sprintf( 'DESCRIPTION:%s', sanitize_text_field( $description ) ), - sprintf( 'LOCATION:%s', sanitize_text_field( $location ) ), - 'UID:gatherpress_' . intval( $this->event->ID ), - 'END:VEVENT', - 'END:VCALENDAR', - ); - - return 'data:text/calendar;charset=utf8,' . implode( '%0A', $args ); - } - /** * Generate a calendar event description with a link to the event details. * diff --git a/includes/core/classes/class-setup.php b/includes/core/classes/class-setup.php index 9752ea043..748bf1050 100644 --- a/includes/core/classes/class-setup.php +++ b/includes/core/classes/class-setup.php @@ -58,6 +58,7 @@ protected function __construct() { protected function instantiate_classes(): void { Assets::get_instance(); Block::get_instance(); + Calendars::get_instance(); Cli::get_instance(); Event_Query::get_instance(); Event_Rest_Api::get_instance(); diff --git a/test/unit/php/includes/core/classes/class-test-event.php b/test/unit/php/includes/core/classes/class-test-event.php index 8458c2f29..505b3df15 100644 --- a/test/unit/php/includes/core/classes/class-test-event.php +++ b/test/unit/php/includes/core/classes/class-test-event.php @@ -386,9 +386,6 @@ public function test_get_venue_information(): void { * Coverage for get_calendar_links method. * * @covers ::get_calendar_links - * @covers ::get_google_calendar_link - * @covers ::get_ics_calendar_download - * @covers ::get_yahoo_calendar_link * @covers ::get_calendar_description * * @return void @@ -422,8 +419,7 @@ public function test_get_calendar_links(): void { $event->save_datetimes( $params ); - $output = $event->get_calendar_links(); - + /* $expected_google_link = 'https://www.google.com/calendar/event?action=TEMPLATE&text=Unit%20Test%20Event&dates=20200511T150000Z%2F20200511T170000Z&details=' . rawurlencode( $description ) . '&location=Unit%20Test%20Venue%2C%20123%20Main%20Street%2C%20Montclair%2C%20NJ%2007042&sprop=name%3A'; $expected_yahoo_link = 'https://calendar.yahoo.com/?v=60&view=d&type=20&title=Unit%20Test%20Event&st=20200511T150000Z&dur=0200&desc=' . rawurlencode( $description ) . '&in_loc=Unit%20Test%20Venue%2C%20123%20Main%20Street%2C%20Montclair%2C%20NJ%2007042'; @@ -445,7 +441,56 @@ public function test_get_calendar_links(): void { 'link' => $expected_yahoo_link, ), ); + */ + $output = $event->get_calendar_links(); + $expects = array( + 'google' => array( + 'name' => 'Google Calendar', + 'link' => home_url( '/?gatherpress_event=unit-test-event&gatherpress_calendars=google-calendar' ), + ), + 'ical' => array( + 'name' => 'iCal', + 'download' => home_url( '/?gatherpress_event=unit-test-event&gatherpress_calendars=ical' ), + ), + 'outlook' => array( + 'name' => 'Outlook', + 'download' => home_url( '/?gatherpress_event=unit-test-event&gatherpress_calendars=outlook' ), + ), + 'yahoo' => array( + 'name' => 'Yahoo Calendar', + 'link' => home_url( '/?gatherpress_event=unit-test-event&gatherpress_calendars=yahoo-calendar' ), + ), + ); + + $this->assertSame( $expects, $output ); + // Update permalink structure to '/%postname%/'. + update_option( 'permalink_structure', '/%postname%/' ); + + // Reload the global rewrite rules object to ensure it reflects the changes. + global $wp_rewrite; + $wp_rewrite->init(); + flush_rewrite_rules(); + + $output = $event->get_calendar_links(); + $expects = array( + 'google' => array( + 'name' => 'Google Calendar', + 'link' => home_url( '/event/unit-test-event/google-calendar' ), + ), + 'ical' => array( + 'name' => 'iCal', + 'download' => home_url( '/event/unit-test-event/ical' ), + ), + 'outlook' => array( + 'name' => 'Outlook', + 'download' => home_url( '/event/unit-test-event/outlook' ), + ), + 'yahoo' => array( + 'name' => 'Yahoo Calendar', + 'link' => home_url( '/event/unit-test-event/yahoo-calendar' ), + ), + ); $this->assertSame( $expects, $output ); Utility::set_and_get_hidden_property( $event, 'event', null ); From 3eb3d6cc5019be4520972c5c482ae2b753a6ce13 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:18:25 +0200 Subject: [PATCH 37/52] Add 'Add-to-calendar' to feature list --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 947cdfaf0..b3b83fe55 100644 --- a/readme.md +++ b/readme.md @@ -62,6 +62,8 @@ https://www.youtube.com/watch?v=BnYS36C5d38&t=2s - Multi-event management: capability to handle multiple events simultaneously. - Multisite environment: This setup allows for centralized management while providing flexibility for each site to host its own unique events with its settings (language, timezone, date time format) and set of users. - Works with blocks. +- Add events to your calendar app, using iCal and dedicated support for platforms like *Google Calendar* or *Yahoo Calendar*. +- Subscribe with your calendar app, to stay updated about all new & upcoming events, events at a specific venue or events of a specific topic. - Fully internationalized. - Freedom to add content besides the default event/venue blocks, to remove default blocks, and add synced patterns (useful for adding consistent information across all events). From 29a39b19f2ef5691555c790dc2ab1d0d39881f81 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:26:49 +0200 Subject: [PATCH 38/52] Add overrideable-templates , like described in #929 --- .../templates/endpoints/ical-download.php | 20 +++++++++++++++++++ includes/templates/endpoints/ical-feed.php | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 includes/templates/endpoints/ical-download.php create mode 100644 includes/templates/endpoints/ical-feed.php diff --git a/includes/templates/endpoints/ical-download.php b/includes/templates/endpoints/ical-download.php new file mode 100644 index 000000000..359b87dfa --- /dev/null +++ b/includes/templates/endpoints/ical-download.php @@ -0,0 +1,20 @@ + Date: Tue, 22 Oct 2024 01:38:20 +0200 Subject: [PATCH 39/52] NEW '.../event/ical' endpoint --- .../class-posttype-feed-endpoint.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 includes/core/classes/endpoints/class-posttype-feed-endpoint.php diff --git a/includes/core/classes/endpoints/class-posttype-feed-endpoint.php b/includes/core/classes/endpoints/class-posttype-feed-endpoint.php new file mode 100644 index 000000000..16419e11f --- /dev/null +++ b/includes/core/classes/endpoints/class-posttype-feed-endpoint.php @@ -0,0 +1,96 @@ +type_object->name ) && is_feed(); + } + + /** + * Defines the rewrite replacement attributes for the custom feed endpoint. + * + * This method defines the rewrite replacement attributes + * for the custom feed endpoint to be further processed by add_rewrite_rule(). + * + * @since 1.0.0 + * + * @return array The rewrite replacement attributes for add_rewrite_rule(). + */ + public function get_rewrite_atts(): array { + return array( + $this->object_type => $this->type_object->name, + 'feed' => '$matches[1]', + ); + } +} From 6633ec5851d422b4506629245a2b3fdbd91c9cc4 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:38:57 +0200 Subject: [PATCH 40/52] NEW '.../event/xyz/ical' endpoint --- .../class-posttype-single-endpoint.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 includes/core/classes/endpoints/class-posttype-single-endpoint.php diff --git a/includes/core/classes/endpoints/class-posttype-single-endpoint.php b/includes/core/classes/endpoints/class-posttype-single-endpoint.php new file mode 100644 index 000000000..4c7a00a92 --- /dev/null +++ b/includes/core/classes/endpoints/class-posttype-single-endpoint.php @@ -0,0 +1,79 @@ +type_object->name ); + } +} From 878159f6d19e1f5abcd6e717e0dbac6b996cd0e4 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:39:25 +0200 Subject: [PATCH 41/52] NEW '.../venue/abc/ical' endpoint --- .../class-posttype-single-feed-endpoint.php | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 includes/core/classes/endpoints/class-posttype-single-feed-endpoint.php diff --git a/includes/core/classes/endpoints/class-posttype-single-feed-endpoint.php b/includes/core/classes/endpoints/class-posttype-single-feed-endpoint.php new file mode 100644 index 000000000..da28777f7 --- /dev/null +++ b/includes/core/classes/endpoints/class-posttype-single-feed-endpoint.php @@ -0,0 +1,95 @@ +type_object->name ) && is_feed(); + } + + /** + * Defines the rewrite replacement attributes for the custom feed endpoint. + * + * This method defines the rewrite replacement attributes + * for the custom feed endpoint to be further processed by add_rewrite_rule(). + * + * @since 1.0.0 + * + * @return array The rewrite replacement attributes for add_rewrite_rule(). + */ + public function get_rewrite_atts(): array { + return array( + $this->object_type => $this->type_object->name, + $this->type_object->name => '$matches[1]', + 'feed' => '$matches[2]', + ); + } +} From 13aaf2287e9e5185d02588b5630919eacaefe63a Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:39:56 +0200 Subject: [PATCH 42/52] NEW '.../topic/123/ical' endpoint --- .../class-taxonomy-feed-endpoint.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php diff --git a/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php b/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php new file mode 100644 index 000000000..59b7062ab --- /dev/null +++ b/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php @@ -0,0 +1,96 @@ +type_object->name ) && is_feed(); + } + + /** + * Defines the rewrite replacement attributes for the custom feed endpoint. + * + * This method defines the rewrite replacement attributes + * for the custom feed endpoint to be further processed by add_rewrite_rule(). + * + * @since 1.0.0 + * + * @return array The rewrite replacement attributes for add_rewrite_rule(). + */ + public function get_rewrite_atts(): array { + return array( + $this->object_type => $this->type_object->name, + $this->type_object->name => '$matches[1]', + 'feed' => '$matches[2]', + ); + } +} From 34ad38e621b8ab32798ee75d0238d2a3b7349876 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 01:42:12 +0200 Subject: [PATCH 43/52] Fix for CS --- includes/templates/endpoints/ical-download.php | 3 ++- includes/templates/endpoints/ical-feed.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/templates/endpoints/ical-download.php b/includes/templates/endpoints/ical-download.php index 359b87dfa..f5122a1bc 100644 --- a/includes/templates/endpoints/ical-download.php +++ b/includes/templates/endpoints/ical-download.php @@ -5,6 +5,7 @@ * This template is used to render an ical file to the browser. * * It can be replaced by theme authors and will override this existing template. + * * @see /docs/developer/theme-customizations/README.md * * @package GatherPress\Core @@ -17,4 +18,4 @@ use GatherPress\Core\Calendars; // Call the function to output the .ics file. -Calendars::send_ics_file(); \ No newline at end of file +Calendars::send_ics_file(); diff --git a/includes/templates/endpoints/ical-feed.php b/includes/templates/endpoints/ical-feed.php index 91746d08d..7bccdfcec 100644 --- a/includes/templates/endpoints/ical-feed.php +++ b/includes/templates/endpoints/ical-feed.php @@ -5,6 +5,7 @@ * This template is used to render an ical feed to the browser. * * It can be replaced by theme authors and will override this existing template. + * * @see /docs/developer/theme-customizations/README.md * * @package GatherPress\Core @@ -17,4 +18,4 @@ use GatherPress\Core\Calendars; // Call the function to output the .ics file. -Calendars::send_ics_file(); \ No newline at end of file +Calendars::send_ics_file(); From b028af0dcd55849086da5694462d4153d5c29ca0 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:03:47 +0200 Subject: [PATCH 44/52] Fix: Function is_archive invoked with 1 parameter, 0 required. --- .../core/classes/endpoints/class-posttype-feed-endpoint.php | 2 +- .../core/classes/endpoints/class-taxonomy-feed-endpoint.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/classes/endpoints/class-posttype-feed-endpoint.php b/includes/core/classes/endpoints/class-posttype-feed-endpoint.php index 16419e11f..e1c8f5fdd 100644 --- a/includes/core/classes/endpoints/class-posttype-feed-endpoint.php +++ b/includes/core/classes/endpoints/class-posttype-feed-endpoint.php @@ -74,7 +74,7 @@ public function __construct( * @return bool True if the current request is a valid feed request for the post type archive. */ public function is_valid(): bool { - return is_archive( $this->type_object->name ) && is_feed(); + return is_post_type_archive( $this->type_object->name ) && is_feed(); } /** diff --git a/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php b/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php index 59b7062ab..b59242b47 100644 --- a/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php +++ b/includes/core/classes/endpoints/class-taxonomy-feed-endpoint.php @@ -73,7 +73,7 @@ public function __construct( * @return bool True if the current request is a valid feed request for the post type archive. */ public function is_valid(): bool { - return is_archive( $this->type_object->name ) && is_feed(); + return is_post_type_archive( $this->type_object->name ) && is_feed(); } /** From 0ffbcbad8b873a29e277390ca706074830e8c8c0 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:11:42 +0200 Subject: [PATCH 45/52] BUGFIX: Remove superflous file --- phpstan.neon | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 phpstan.neon diff --git a/phpstan.neon b/phpstan.neon deleted file mode 100644 index a77b8b0d6..000000000 --- a/phpstan.neon +++ /dev/null @@ -1,42 +0,0 @@ -includes: - - vendor/szepeviktor/phpstan-wordpress/extension.neon - # Bleeding edge offers a preview of the next major version. - # When you enable Bleeding edge in your configuration file, you will get new rules, - # behaviour, and bug fixes that will be enabled for everyone later - # when the next PHPStan’s major version is released. - - phar://phpstan.phar/conf/bleedingEdge.neon - -parameters: - bootstrapFiles: - # Constants, functions, etc. used by GatherPress - #- phpstan.stubs -# - vendor/pmc/unit-test/src/classes/autoloader.php -# - vendor/autoload.php -# - gatherpress.php - parallel: - maximumNumberOfProcesses: 1 - processTimeout: 300.0 - - # the analysis level, from 0 (loose) to 9 (strict) - # https://phpstan.org/user-guide/rule-levels - level: 5 - - paths: - - includes/ -# - test/ - -# excludePaths: -# analyse: -# - vendor/ - - ignoreErrors: - - '#^Constant GATHERPRESS_CORE_FILE not found\.$#' - - '#^Constant GATHERPRESS_CORE_PATH not found\.$#' - - '#^Constant GATHERPRESS_CORE_URL not found\.$#' - - '#^Constant GATHERPRESS_REST_NAMESPACE not found\.$#' - - '#^Constant GATHERPRESS_REQUIRES_PHP not found\.$#' - - # core/classes/class-setup.php - # - # A dev-only error, which can occur if the gatherpress is symlinked into a WP instance or called via wp-env or Playground. - - '#^Path in require_once\(\) "\./wp-admin/includes/upgrade\.php" is not a file or it does not exist\.$#' \ No newline at end of file From 6ddf1471b4629a378927fb15a6c595b708331363 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:14:51 +0200 Subject: [PATCH 46/52] Fix: Parameter #1 $object_type of function get_object_taxonomies expects array|string|WP_Post, int given. --- includes/core/classes/class-calendars.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php index 53acbd809..b2ba7468e 100644 --- a/includes/core/classes/class-calendars.php +++ b/includes/core/classes/class-calendars.php @@ -256,7 +256,7 @@ public function alternate_links(): void { // Get all terms, associated with the current event-post. $terms = get_terms( array( - 'taxonomy' => get_object_taxonomies( get_queried_object_id() ), + 'taxonomy' => get_object_taxonomies( get_queried_object() ), 'object_ids' => get_queried_object_id(), ) ); From acd837b2ba08f983fa342c87ea636e782aa52803 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:18:55 +0200 Subject: [PATCH 47/52] Fix: Offset 'type' on array{url: string|false, attr: string} in isset() does not exist. --- includes/core/classes/class-calendars.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php index b2ba7468e..702df2cb6 100644 --- a/includes/core/classes/class-calendars.php +++ b/includes/core/classes/class-calendars.php @@ -332,7 +332,7 @@ function ( WP_Term $term ) use ( $args, &$alternate_links ) { function ( $link ) { printf( '' . "\n", - esc_attr( isset( $link['type'] ) ? $link['type'] : 'text/calendar' ), + esc_attr( 'text/calendar' ), esc_attr( $link['attr'] ), esc_url( $link['url'] ) ); From a0dfc363fdb234121facbc81fe5e5206828a9303 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:20:03 +0200 Subject: [PATCH 48/52] Fix: PHPDoc tag @throws with type GatherPress\Core\Exception is not subtype of Throwable --- includes/core/classes/class-calendars.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php index 702df2cb6..f6079427d 100644 --- a/includes/core/classes/class-calendars.php +++ b/includes/core/classes/class-calendars.php @@ -19,6 +19,7 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use Exception; use GatherPress\Core\Endpoints\Posttype_Single_Endpoint; use GatherPress\Core\Endpoints\Posttype_Single_Feed_Endpoint; use GatherPress\Core\Endpoints\Posttype_Feed_Endpoint; From b70ef88f3d4551e168c1e44ff3fe871be8358d98 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:21:56 +0200 Subject: [PATCH 49/52] Fix: Access to undefined constant GatherPress\Core\Calendars::DATETIME_FORMAT. --- includes/core/classes/class-calendars.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php index f6079427d..33434678e 100644 --- a/includes/core/classes/class-calendars.php +++ b/includes/core/classes/class-calendars.php @@ -507,8 +507,8 @@ public function get_yahoo_calendar_link(): string { $datetime_start = sprintf( '%sT%sZ', $date_start, $time_start ); // Figure out duration of event in hours and minutes: hhmm format. - $diff_start = $event->get_formatted_datetime( self::DATETIME_FORMAT, 'start', false ); - $diff_end = $event->get_formatted_datetime( self::DATETIME_FORMAT, 'end', false ); + $diff_start = $event->get_formatted_datetime( $event::DATETIME_FORMAT, 'start', false ); + $diff_end = $event->get_formatted_datetime( $event::DATETIME_FORMAT, 'end', false ); $duration = ( ( strtotime( $diff_end ) - strtotime( $diff_start ) ) / 60 / 60 ); $full = intval( $duration ); $fraction = ( $duration - $full ); From 48244f620fa9d73855a3ffcf117cb8a15a50382b Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Tue, 22 Oct 2024 10:24:29 +0200 Subject: [PATCH 50/52] Fix: Parameter #1 $string of function str_pad expects string, int given --- includes/core/classes/class-calendars.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/core/classes/class-calendars.php b/includes/core/classes/class-calendars.php index 33434678e..32c5f189c 100644 --- a/includes/core/classes/class-calendars.php +++ b/includes/core/classes/class-calendars.php @@ -512,8 +512,8 @@ public function get_yahoo_calendar_link(): string { $duration = ( ( strtotime( $diff_end ) - strtotime( $diff_start ) ) / 60 / 60 ); $full = intval( $duration ); $fraction = ( $duration - $full ); - $hours = str_pad( intval( $duration ), 2, '0', STR_PAD_LEFT ); - $minutes = str_pad( intval( $fraction * 60 ), 2, '0', STR_PAD_LEFT ); + $hours = str_pad( strval( $duration ), 2, '0', STR_PAD_LEFT ); + $minutes = str_pad( strval( $fraction * 60 ), 2, '0', STR_PAD_LEFT ); $venue = $event->get_venue_information(); $location = $venue['name']; $description = $event->get_calendar_description(); From 65f00a94fda7cabd230f9b3d6175a960c859852c Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 25 Oct 2024 02:07:32 +0200 Subject: [PATCH 51/52] Ignore phpstan error (probably introduced with #928) --- phpstan.neon.dist | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 320835a33..603801133 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -23,7 +23,18 @@ parameters: - includes/ ignoreErrors: - # core/classes/class-setup.php + # includes/core/classes/class-setup.php # # A dev-only errors, which can occur if the gatherpress is symlinked into a WP instance or called via wp-env or Playground. - '#^Path in require_once\(\) "\./wp-admin/includes/upgrade\.php" is not a file or it does not exist\.$#' + + # includes/core/classes/endpoints/class-endpoint.php + # + # A known issue, but not a problem. + # This "callable-string" is only used in a debug message and is not run. + - + # I was not able to get the escaping work for the "'", so this matches all argument.type errors in this file, + # which is not nice, but the workaround. + # message: '#Parameter #1 \$function_name of function wp_trigger_error expects callable-string, \'GatherPress\\Core\\Endpoints\\Endpoint\' given.#' + identifier: argument.type + path: includes/core/classes/endpoints/class-endpoint.php \ No newline at end of file From 3d2dfe6c74f8369f703a66b910a0602cd61f99d3 Mon Sep 17 00:00:00 2001 From: Carsten Bach Date: Fri, 25 Oct 2024 02:11:52 +0200 Subject: [PATCH 52/52] Fix: MD049/emphasis-style Emphasis style should be consistent --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 35c9cf06e..f18d1cd60 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,7 @@ GatherPress, a plugin created by and for the WordPress community, is a response ## Community-built -This project is the result of a collaborative effort to build a compelling event management application using open source tools such as _WordPress_ and _BuddyPress_ and with the grit, sweat, and love of **the community, for the community**. We encourage all interested, whether a user, community member, or developer, to follow our emerging presence on our [GatherPress Blog](https://gatherpress.org/blog/), our [GitHub repositories](https://github.com/GatherPress/), our [GatherPress Documentation](https://gatherpress.org/documentation/), or new features on our [GatherPress Playground](https://wordpress.org/plugins/gatherpress/?preview=1). +This project is the result of a collaborative effort to build a compelling event management application using open source tools such as *WordPress* and *BuddyPress* and with the grit, sweat, and love of **the community, for the community**. We encourage all interested, whether a user, community member, or developer, to follow our emerging presence on our [GatherPress Blog](https://gatherpress.org/blog/), our [GitHub repositories](https://github.com/GatherPress/), our [GatherPress Documentation](https://gatherpress.org/documentation/), or new features on our [GatherPress Playground](https://wordpress.org/plugins/gatherpress/?preview=1). ## Playground Environment @@ -161,7 +161,7 @@ Topics are like post categories, but for events. ## Contribute -If you wish to share in the collaborative of work to build _GatherPress_, please drop us a line either via [WordPress Slack](https://make.wordpress.org/chat/) or on [GatherPress.org](htps://gatherpress.org/get-involved). The development location of the GatherPress project can be found at [https://github.com/gatherpress/gatherpress](https://github.com/gatherpress/gatherpress). All contributions are welcome: code, design, user interface, documentation, translation, and more. +If you wish to share in the collaborative of work to build *GatherPress*, please drop us a line either via [WordPress Slack](https://make.wordpress.org/chat/) or on [GatherPress.org](htps://gatherpress.org/get-involved). The development location of the GatherPress project can be found at [https://github.com/gatherpress/gatherpress](https://github.com/gatherpress/gatherpress). All contributions are welcome: code, design, user interface, documentation, translation, and more. ### Read Developer Documentation