Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new calendar endpoints #831

Open
wants to merge 65 commits into
base: main
Choose a base branch
from

Conversation

carstingaxion
Copy link
Collaborator

@carstingaxion carstingaxion commented Aug 29, 2024

Original PR description

As a proof-of-concept PR, this code needs for sure some orchestration!

The used anonymous functions are my preferred way of experimenting, but not meant to stay.

I can see 3 different ways to go on:

  1. a custom class-add-to-calendar.php
  2. Or even a further abstraction into another new class-rewrite-api.php that handles all the rewrites stuff and is only used by the current class-event.php
  3. Or a new class-endpoints.php, that could also be the future home for Add subscribeable Ical feeds #603
  4. Or maybe there is even better places within the existing code base?

Following path number 2 could be interesting for the future, as it could be reused, if GatherPress would want to create /tickets or /qr endpoints on events, for example. Long time ago created a similar abstraction to route AJAX requests, because it was (maybe still is) usually around 10 times faster than the REST API.

———

I’ve also tried using add_rewrite_endpoint() in the first run, but this would have added the new endpoints to all EP_PERMALINK rewrites, which would have meant: all non-hierarchical post types. In that case, I would have had to set custom 404s on all non-event post types, which ended up being very tedious and a lot of additional code. That’s why I went with add_rewrite_rule() which allows a restriction on one post type.

Description of the Change

This PR add several new URL endpoints to events, one per each add-to-calendar link. The new endpoints are:

  • example.org/event/my-sample-event/ical
  • example.org/event/my-sample-event/iCal (Alias for /ical)
  • example.org/event/my-sample-event/outlook (Alias for /ical)
  • example.org/event/my-sample-event/googlecalendar
  • example.org/event/my-sample-event/yahoocalendar

With the original intention to help with #704, this PR also does some more subtle changes to the generated ics file, like changing its default line breaks.

Because this renders into a sites frontend output, this PR additionally introduces a way to overwrite the generated iCal via a theme template. This opens another way of extension and can be used to test this PR.

With the original intention to help with #704, this PR also does some more subtle changes [...]

Can be stated as totally outdated!

Unfortunately, this PR does NOT help with #704,
but instead has grown from 193 to 2607 lines of code and now closes #603 instead.

An extensive description can be found in a new /docs/developer/custom-url-endpoints article, that is also part of this PR.

custom-url-endpoints__office365-calendar

This PR introduces the following of new endpoints:

  • example.org/event/my-sample-event/ical

    provides a download-able .ics file in ical format.

  • example.org/event/my-sample-event/outlook

    provides the same download-able file as an alias.

  • example.org/event/my-sample-event/google-calendar

    redirects to create a new event in Google Calendar.

  • example.org/event/my-sample-event/yahoo-calendar

    redirects to create a new event in Yahoo Calendar.

  • example.org/event/feed/ical

    provides a subscribe-able event feed in ical format with all events of the site.

  • example.org/venue/my-sample-venue/feed/ical

    provides a subscribe-able event feed in ical format with all events at that venue.

  • example.org/topic/my-sample-topic/feed/ical

    provides a subscribe-able event feed in ical format with all events grouped into that topic.

How to test the Change

  • Test the Add to Google calendar
    1. (You need an google account & use their calendar)
    2. Go to the frontend of any event and hit the the Add to calendar > Google button
    3. Confirm that the drafted event in the Google calendar matches the GatherPress event
  • Test the Add to Yahoo calendar
    1. (You need an yahoo account & use their calendar)
    2. Go to the frontend of any event and hit the the Add to calendar > Yahoo button
    3. Confirm that the drafted event in the Yahoo calendar matches the GatherPress event
  • create a ical-download.php file and put it at the root of your active theme to overwrite the plugin template
    1. Create that file
    2. Put some echo ‚hello themed iCal world‘; into it
    3. Go to the frontend of any event and hit the the Add to calendar > iCal button (to make use of your template)
  • https://icalendar.org/validator.html

Changelog Entry

Added - Human readable, shareable and subscribe-able „Add to calendar“ links for each event.

Credits

Props @carstingaxion

Checklist:

Related PRs that can be reviewed & merged after this:

To get the new endpoints into the block-editor, one of the following would be required:

@carstingaxion carstingaxion mentioned this pull request Aug 29, 2024
61 tasks
@carstingaxion

This comment was marked as outdated.

@carstingaxion

This comment was marked as resolved.

includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/templates/frontend/ical-download.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
includes/core/classes/class-event-setup.php Outdated Show resolved Hide resolved
@carstingaxion

This comment was marked as resolved.

@mauteri
Copy link
Contributor

mauteri commented Sep 4, 2024

Thanks for your review @mauteri, but that was exactly what I wanted to pretend you from with my comment and the PR description.

I meant to start with a more strategic What-should-go-where before going into the details.

What do you think?

Sorry, I'm still a bit brain dead from being off for 2 weeks and trying to get back in the flow of things. I think the class-calendar.php (or class-add-to-calendar.php... I like the former a bit more because it is a shorter class name) is the approach I would go with. And move all calendar-related methods to that new class. Smaller, concise classes are easier to maintain and much more approachable.

Copy link

Preview changes with Playground

You can preview the least recent changes for PR#831 by following one of the links below:

⚠️ Note: The preview sites are created using WordPress Playground. You can add content, edit settings, and test the themes as you would on a real site, but please note that changes are not saved between sessions.

@carstingaxion
Copy link
Collaborator Author

Hello @mauteri ,

I've updated the PR description with more relevant info and fixed the simple errors.

The remaining CS failures and Unit test failures are kind of intentional, to remind us what needs attention.

I will now not be able to add some helping review comments, but open it up for Reviews anyway.
If someone has time and wants to take a look, it would be a pleasure.

@carstingaxion
Copy link
Collaborator Author

I'm going to start adding some review-comments for you now @mauteri. Would be cool, if you find the time (or someone else) to go trough all of this me...melange.

Copy link
Collaborator Author

@carstingaxion carstingaxion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mauteri I added 16 comments to help navigate to the most problematic parts.

Comment on lines +43 to +61
* Class constructor.
*
* Initializes the `Endpoint_Template` object by setting the slug, callback, and
* plugin template directory. The parent constructor initializes the slug and callback,
* while this constructor adds the plugin template default.
*
* @since 1.0.0
*
* @param string $slug The slug used to identify the endpoint in the URL.
* @param callable $callback The callback function to retrieve file name and path of the endpoint template.
* @param string $plugin_template_dir The directory path for the plugin templates.
*/
public function __construct( string $slug, callable $callback, string $plugin_template_dir = '' ) {
parent::__construct( $slug, $callback );
$this->plugin_template_dir = ( ! empty( $plugin_template_dir ) ) ? $plugin_template_dir : sprintf(
'%s/includes/templates/endpoints',
GATHERPRESS_CORE_PATH
);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method may need some better docs, why the 3rd param is needed or why it may be useful to extenders.

Comment on lines +121 to +147
/**
* 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;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better put in the Utility class?

Comment on lines +149 to +168
/**
* 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;
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe better put in the Utility class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class, as well as the three other classes, that extend Endpoint are probably the best start to reduce clutter.

I can feel that this can and should be optimized, but wasn't able to do so.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class, as well as the three other classes, that extend Endpoint are probably the best start to reduce clutter.

I can feel that this can and should be optimized, but wasn't able to do so.

Comment on lines +247 to +272
/**
* 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 );
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could be simplified if Event_Setup->maybe_create_flush_rewrite_rules_flag() could maybe become a public method ?! Which in itself could be simplified, like I described in #880

Comment on lines +472 to +513
/**
* 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
);
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This two methods also look a lot like as they were better placed in a more common place.

return;
}

// @todo "add_filter('feed_content_type')" here, if the subscribe-able feed need something different than text/cal.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be removed.

Comment on lines +234 to +243
// @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']
),
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should GatherPress establish a route like example.org/feed/ical as an alias for example.org/event/feed/ical, which could be nice, but also a duplicated-content issue?

Comment on lines +794 to +825
/**
* @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 );
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I brazenly stole this code, because I knew it is working for years and I also knew that I couldn't do better.
But I don't just want to pirate Stephen Harris' code, without proper attrribution - or rewriting it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add subscribeable Ical feeds
2 participants