Skip to content

ICS export option#732

Open
Akhill2020 wants to merge 1 commit into
mainfrom
Export-ICS-feed
Open

ICS export option#732
Akhill2020 wants to merge 1 commit into
mainfrom
Export-ICS-feed

Conversation

@Akhill2020

@Akhill2020 Akhill2020 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Description: I have added ICS Export button similar to print calendar button user can check this from appearance and button will display below the calendar in grid/list view by pressing button user can download the ICS file.

Clickup: https://app.clickup.com/t/1867958/86d3e1phg
After:

Summary by CodeRabbit

  • New Features

    • Added an optional Export ICS button to frontend calendars.
    • Visitors can download calendar events as .ics files for use in calendar applications.
    • Added support for exporting event details, including dates, times, descriptions, locations, and links.
    • Added administrative settings to enable ICS export per calendar.
    • Export controls are hidden when printing.
  • Documentation

    • Updated the changelog for version 4.1.0.

@Akhill2020
Akhill2020 requested a review from rosinghal July 16, 2026 09:13
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a configurable frontend ICS export option that generates .ics downloads from calendar events, integrates shared button rendering into calendar views, adds button styling, and documents the 4.1.0 feature.

Changes

ICS Export

Layer / File(s) Summary
Export configuration and plugin wiring
includes/admin/metaboxes/settings.php, includes/main.php
Adds the _display_ics_export setting, persists its value, and registers the export handler during plugin loading.
ICS request and event generation
includes/ics-export.php
Validates export requests, builds deduplicated VCALENDAR/VEVENT content, escapes and folds RFC 5545 lines, and returns downloadable ICS responses.
Frontend export controls
includes/calendars/views/default-calendar-*.php, assets/css/default-calendar-grid.css, readme.txt
Uses shared button rendering in calendar views, styles the ICS button, hides it during printing, and records the 4.1.0 changelog entry.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Visitor
  participant CalendarView
  participant Ics_Export
  participant Calendar
  Visitor->>CalendarView: Select ICS export
  CalendarView->>Ics_Export: Request export URL
  Ics_Export->>Calendar: Validate setting and load events
  Calendar-->>Ics_Export: Calendar event groups
  Ics_Export-->>Visitor: Download .ics file
Loading

Suggested reviewers: rosinghal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding a configurable ICS export feature for calendars.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Export-ICS-feed

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.5)

PHP Warning: require(/vendor/composer/../guzzlehttp/promises/src/functions_include.php): Failed to open stream: No such file or directory in /vendor/composer/autoload_real.php on line 39
Warning: require(/vendor/composer/../guzzlehttp/promises/src/functions_include.php): Failed to open stream: No such file or directory in /vendor/composer/autoload_real.php on line 39
PHP Fatal error: Uncaught Error: Failed opening required '/vendor/composer/../guzzlehttp/promises/src/functions_include.php' (include_path='.:/usr/share/php') in /vendor/composer/autoload_real.php:39
Stack trace:
#0 /vendor/composer/autoload_real.php(43): {closure}()
#1 /vendor/autoload.php(25): ComposerAutoloaderInit7fcbe9274d3bbe8ac8bbda80dbee18b8::getLoader()
#2 phar:///usr/bin/phpstan/bin/phpstan(46): require_once('...')
#3 phar:///usr/bin/phpstan/bin/phpstan(107): _PHPStan_02959ca10{closure}()
#4 /usr/bin/phpstan(7): require('...')
#5 {main}
thrown in /vendor/composer/autoload_real.php on line 39
Fatal error: Uncaught Error: Failed opening required '/vendor/composer/../guzzlehttp/promises/src/functions_include.php' (include_path='.:/usr/share/php') in /vendor/composer/autoload_real.php:39
Stack trace:
#0 /vendor/composer/autoload_real.php(43): {closure}()
#1 /vendor/autoload.php(25): ComposerAutoloaderInit7fcbe9274d3bbe8ac8bbda80dbee18b8::getLoader()
#2 phar:///usr/bin/phpstan/bin/phpstan(46): require_once('...')
#3 phar:///usr/bin/phpstan/bin/phpstan(107): _PHPStan_02959ca10{closure}()
#4 /usr/bin/phpstan(7): require('...')
#5 {main}
thrown in /vendor/composer/autoload_real.php on line 39


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@includes/ics-export.php`:
- Around line 275-279: Update the line-folding loop to use mb_strcut() instead
of substr() when extracting each 75-byte segment, preserving the existing
CRLF-and-space folding format and remaining-line handling while ensuring
multibyte characters are never split.
- Around line 195-200: Update the event end-date calculation in the
`$event->end_dt` branch to normalize `end_dt` with `startOfDay()->addDay()`
before formatting it as `Ymd`. Remove the brittle `addSeconds(59)` logic while
preserving the existing fallback calculation for events without an end date.
- Around line 51-54: Update the calendar validation around
get_post($calendar_id) to allow exports only when the calendar is published or
the current user is authorized to access its private or draft status. Preserve
the existing invalid-post and wrong-post-type rejection, and terminate
unauthorized requests with the established 404 response without exposing
calendar events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 186f69c9-9df8-46e3-82e8-c50d967d3f61

📥 Commits

Reviewing files that changed from the base of the PR and between 6756744 and 2d65ff9.

📒 Files selected for processing (7)
  • assets/css/default-calendar-grid.css
  • includes/admin/metaboxes/settings.php
  • includes/calendars/views/default-calendar-grid.php
  • includes/calendars/views/default-calendar-list.php
  • includes/ics-export.php
  • includes/main.php
  • readme.txt

Comment thread includes/ics-export.php
Comment on lines +51 to +54
$post = get_post($calendar_id);
if (!$post || 'calendar' !== $post->post_type) {
wp_die(esc_html__('Calendar not found.', 'google-calendar-events'), 404);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Add permission check to prevent unauthorized exports of private calendars.

get_post() retrieves the calendar post without verifying if it is published or if the current user has access to it. This allows unauthorized users to export events from draft or private calendars by simply guessing the calendar ID.

🔒 Proposed fix to add permission checks
 		$post = get_post($calendar_id);
 		if (!$post || 'calendar' !== $post->post_type) {
 			wp_die(esc_html__('Calendar not found.', 'google-calendar-events'), 404);
 		}
+
+		if ($post->post_status !== 'publish' && !current_user_can('read_post', $calendar_id)) {
+			wp_die(esc_html__('You do not have permission to view this calendar.', 'google-calendar-events'), 403);
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$post = get_post($calendar_id);
if (!$post || 'calendar' !== $post->post_type) {
wp_die(esc_html__('Calendar not found.', 'google-calendar-events'), 404);
}
$post = get_post($calendar_id);
if (!$post || 'calendar' !== $post->post_type) {
wp_die(esc_html__('Calendar not found.', 'google-calendar-events'), 404);
}
if ($post->post_status !== 'publish' && !current_user_can('read_post', $calendar_id)) {
wp_die(esc_html__('You do not have permission to view this calendar.', 'google-calendar-events'), 403);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/ics-export.php` around lines 51 - 54, Update the calendar validation
around get_post($calendar_id) to allow exports only when the calendar is
published or the current user is authorized to access its private or draft
status. Preserve the existing invalid-post and wrong-post-type rejection, and
terminate unauthorized requests with the established 404 response without
exposing calendar events.

Comment thread includes/ics-export.php
Comment on lines +195 to +200
if ($event->end_dt) {
// Stored all-day end is last inclusive moment; ICS DTEND is exclusive next date.
$end_date = $event->end_dt->copy()->addSeconds(59)->format('Ymd');
} else {
$end_date = $start_dt->copy()->startOfDay()->addDay()->format('Ymd');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use startOfDay()->addDay() to safely calculate the exclusive end date.

Adding 59 seconds to the inclusive end time is brittle. If the time stored in end_dt is not exactly at the end of the minute (e.g., 23:59:00 instead of 23:59:59), adding 59 seconds will not cross over to the next calendar day (23:59:59 is still the same day). This truncates the all-day event in the ICS output by one day.

🐛 Proposed fix for robust date math
 			if ($event->end_dt) {
 				// Stored all-day end is last inclusive moment; ICS DTEND is exclusive next date.
-				$end_date = $event->end_dt->copy()->addSeconds(59)->format('Ymd');
+				$end_date = $event->end_dt->copy()->startOfDay()->addDay()->format('Ymd');
 			} else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ($event->end_dt) {
// Stored all-day end is last inclusive moment; ICS DTEND is exclusive next date.
$end_date = $event->end_dt->copy()->addSeconds(59)->format('Ymd');
} else {
$end_date = $start_dt->copy()->startOfDay()->addDay()->format('Ymd');
}
if ($event->end_dt) {
// Stored all-day end is last inclusive moment; ICS DTEND is exclusive next date.
$end_date = $event->end_dt->copy()->startOfDay()->addDay()->format('Ymd');
} else {
$end_date = $start_dt->copy()->startOfDay()->addDay()->format('Ymd');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/ics-export.php` around lines 195 - 200, Update the event end-date
calculation in the `$event->end_dt` branch to normalize `end_dt` with
`startOfDay()->addDay()` before formatting it as `Ymd`. Remove the brittle
`addSeconds(59)` logic while preserving the existing fallback calculation for
events without an end date.

Comment thread includes/ics-export.php
Comment on lines +275 to +279
$folded = '';
while (strlen($line) > 75) {
$folded .= substr($line, 0, 75) . "\r\n ";
$line = substr($line, 75);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use mb_strcut() to fold lines without breaking multibyte characters.

substr() splits the string rigidly by bytes. If a multibyte UTF-8 character (e.g., an emoji or foreign character) crosses the 75-byte boundary, it gets sliced in half. This produces invalid UTF-8 byte sequences that will break parsing in some calendar clients. mb_strcut() correctly slices strings up to a specific byte length without severing multibyte characters.

🐛 Proposed fix for multibyte line folding
 		$folded = '';
 		while (strlen($line) > 75) {
-			$folded .= substr($line, 0, 75) . "\r\n ";
-			$line = substr($line, 75);
+			if (function_exists('mb_strcut')) {
+				$chunk = mb_strcut($line, 0, 75, 'UTF-8');
+			} else {
+				$chunk = substr($line, 0, 75);
+			}
+			$folded .= $chunk . "\r\n ";
+			$line = substr($line, strlen($chunk));
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$folded = '';
while (strlen($line) > 75) {
$folded .= substr($line, 0, 75) . "\r\n ";
$line = substr($line, 75);
}
$folded = '';
while (strlen($line) > 75) {
if (function_exists('mb_strcut')) {
$chunk = mb_strcut($line, 0, 75, 'UTF-8');
} else {
$chunk = substr($line, 0, 75);
}
$folded .= $chunk . "\r\n ";
$line = substr($line, strlen($chunk));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@includes/ics-export.php` around lines 275 - 279, Update the line-folding loop
to use mb_strcut() instead of substr() when extracting each 75-byte segment,
preserving the existing CRLF-and-space folding format and remaining-line
handling while ensuring multibyte characters are never split.

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.

1 participant