├── src ├── Generator.php ├── Generators │ ├── WebOffice.php │ ├── WebOutlook.php │ ├── Google.php │ ├── BaseOutlook.php │ ├── Yahoo.php │ └── Ics.php ├── Exceptions │ └── InvalidLink.php └── Link.php ├── psalm-baseline.xml ├── psalm.xml ├── LICENSE.md ├── .php-cs-fixer.php ├── composer.json ├── CHANGELOG.md └── README.md /src/Generator.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Generators/WebOffice.php: -------------------------------------------------------------------------------- 1 | format(self::DATETIME_FORMAT)}`) must be greater than FROM time (`{$from->format(self::DATETIME_FORMAT)}`)"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->ignoreDotFiles(true) 10 | ->ignoreVCS(true); 11 | 12 | return (new PhpCsFixer\Config()) 13 | ->setRules([ 14 | '@PSR12' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 17 | 'no_unused_imports' => true, 18 | 'not_operator_with_successor_space' => true, 19 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 20 | 'phpdoc_scalar' => true, 21 | 'unary_operator_spaces' => true, 22 | 'binary_operator_spaces' => true, 23 | 'blank_line_before_statement' => [ 24 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 25 | ], 26 | 'phpdoc_single_line_var_spacing' => true, 27 | 'phpdoc_var_without_name' => true, 28 | 'class_attributes_separation' => [ 29 | 'elements' => [ 30 | 'method' => 'one', 31 | ], 32 | ], 33 | 'method_argument_space' => [ 34 | 'on_multiline' => 'ensure_fully_multiline', 35 | 'keep_multiple_spaces_after_comma' => true, 36 | ], 37 | 'single_trait_insert_per_statement' => true, 38 | ]) 39 | ->setFinder($finder); 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/calendar-links", 3 | "description": "Generate add to calendar links for Google, iCal and other calendar systems", 4 | "license": "MIT", 5 | "keywords": [ 6 | "spatie", 7 | "calendar-links" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Sebastian De Deyne", 12 | "email": "sebastian@spatie.be", 13 | "homepage": "https://spatie.be", 14 | "role": "Developer" 15 | } 16 | ], 17 | "homepage": "https://github.com/spatie/calendar-links", 18 | "require": { 19 | "php": "^8.1" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^3.49", 23 | "phpunit/phpunit": "^10.5", 24 | "spatie/phpunit-snapshot-assertions": "^5.1", 25 | "vimeo/psalm": "^5.22" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Spatie\\CalendarLinks\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Spatie\\CalendarLinks\\Tests\\": "tests" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true 39 | }, 40 | "scripts": { 41 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes", 42 | "psalm": "vendor/bin/psalm", 43 | "psalm:ci": "vendor/bin/psalm --shepherd", 44 | "test": "vendor/bin/phpunit", 45 | "test:update-snapshots": "vendor/bin/phpunit -d --update-snapshots" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Generators/Google.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Google implements Generator 15 | { 16 | /** @see https://www.php.net/manual/en/function.date.php */ 17 | private const DATE_FORMAT = 'Ymd'; 18 | 19 | /** @see https://www.php.net/manual/en/function.date.php */ 20 | private const DATETIME_FORMAT = 'Ymd\THis'; 21 | 22 | /** @psalm-var GoogleUrlParameters */ 23 | protected array $urlParameters = []; 24 | 25 | /** @psalm-param GoogleUrlParameters $urlParameters */ 26 | public function __construct(array $urlParameters = []) 27 | { 28 | $this->urlParameters = $urlParameters; 29 | } 30 | 31 | /** @var non-empty-string */ 32 | protected const BASE_URL = 'https://calendar.google.com/calendar/render?action=TEMPLATE'; 33 | 34 | /** @inheritDoc */ 35 | public function generate(Link $link): string 36 | { 37 | $url = self::BASE_URL; 38 | 39 | $dateTimeFormat = $link->allDay ? self::DATE_FORMAT : self::DATETIME_FORMAT; 40 | $url .= '&dates='.$link->from->format($dateTimeFormat).'/'.$link->to->format($dateTimeFormat); 41 | $url .= '&ctz=' . $link->from->getTimezone()->getName(); 42 | $url .= '&text='.urlencode($link->title); 43 | 44 | if ($link->description) { 45 | $url .= '&details='.urlencode($link->description); 46 | } 47 | 48 | if ($link->address) { 49 | $url .= '&location='.urlencode($link->address); 50 | } 51 | 52 | foreach ($this->urlParameters as $key => $value) { 53 | $url .= '&'.urlencode($key).(in_array($value, [null, ''], true) ? '' : '='.urlencode((string) $value)); 54 | } 55 | 56 | return $url; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Generators/BaseOutlook.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class BaseOutlook implements Generator 16 | { 17 | /** @see https://www.php.net/manual/en/function.date.php */ 18 | private const DATE_FORMAT = 'Y-m-d'; 19 | 20 | /** @see https://www.php.net/manual/en/function.date.php */ 21 | private const DATETIME_FORMAT = 'Y-m-d\TH:i:s\Z'; 22 | 23 | /** @psalm-var OutlookUrlParameters */ 24 | protected array $urlParameters = []; 25 | 26 | /** 27 | * Get base URL for links. 28 | * @return non-empty-string 29 | */ 30 | abstract protected function baseUrl(): string; 31 | 32 | /** @psalm-param OutlookUrlParameters $urlParameters */ 33 | public function __construct(array $urlParameters = []) 34 | { 35 | $this->urlParameters = $urlParameters; 36 | } 37 | 38 | /** @inheritDoc */ 39 | public function generate(Link $link): string 40 | { 41 | $url = $this->baseUrl(); 42 | 43 | if ($link->allDay) { 44 | $url .= '&startdt='.$link->from->format(self::DATE_FORMAT); 45 | $url .= '&enddt='.$link->to->format(self::DATE_FORMAT); 46 | $url .= '&allday=true'; 47 | } else { 48 | $url .= '&startdt='.(clone $link->from)->setTimezone(new DateTimeZone('UTC'))->format(self::DATETIME_FORMAT); 49 | $url .= '&enddt='.(clone $link->to)->setTimezone(new DateTimeZone('UTC'))->format(self::DATETIME_FORMAT); 50 | } 51 | 52 | $url .= '&subject='.$this->sanitizeString($link->title); 53 | 54 | if ($link->description) { 55 | $url .= '&body='.$this->sanitizeString($link->description); 56 | } 57 | 58 | if ($link->address) { 59 | $url .= '&location='.$this->sanitizeString($link->address); 60 | } 61 | 62 | foreach ($this->urlParameters as $key => $value) { 63 | $url .= '&'.urlencode($key).(in_array($value, [null, ''], true) ? '' : '='.$this->sanitizeString((string) $value)); 64 | } 65 | 66 | return $url; 67 | } 68 | 69 | private function sanitizeString(string $input): string 70 | { 71 | return rawurlencode($input); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Generators/Yahoo.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Yahoo implements Generator 16 | { 17 | /** @see https://www.php.net/manual/en/function.date.php */ 18 | private const DATE_FORMAT = 'Ymd'; 19 | 20 | /** @see https://www.php.net/manual/en/function.date.php */ 21 | private const DATETIME_FORMAT = 'Ymd\THis\Z'; 22 | 23 | /** @var non-empty-string */ 24 | protected const BASE_URL = 'https://calendar.yahoo.com/?v=60&view=d&type=20'; 25 | 26 | /** @inheritDoc */ 27 | /** @psalm-var YahooUrlParameters */ 28 | protected array $urlParameters = []; 29 | 30 | /** @psalm-param YahooUrlParameters $urlParameters */ 31 | public function __construct(array $urlParameters = []) 32 | { 33 | $this->urlParameters = $urlParameters; 34 | } 35 | 36 | /** {@inheritDoc} */ 37 | public function generate(Link $link): string 38 | { 39 | $url = self::BASE_URL; 40 | 41 | $dateTimeFormat = $link->allDay ? self::DATE_FORMAT : self::DATETIME_FORMAT; 42 | 43 | if ($link->allDay) { 44 | $url .= '&ST='.$link->from->format($dateTimeFormat); 45 | $url .= '&DUR=allday'; 46 | $url .= '&ET='.$link->to->format($dateTimeFormat); 47 | } else { 48 | $utcStartDateTime = $link->from->setTimezone(new DateTimeZone('UTC')); 49 | $utcEndDateTime = $link->to->setTimezone(new DateTimeZone('UTC')); 50 | $url .= '&ST='.$utcStartDateTime->format($dateTimeFormat); 51 | $url .= '&ET='.$utcEndDateTime->format($dateTimeFormat); 52 | } 53 | 54 | $url .= '&TITLE='.$this->sanitizeText($link->title); 55 | 56 | if ($link->description) { 57 | $url .= '&DESC='.$this->sanitizeText($link->description); 58 | } 59 | 60 | if ($link->address) { 61 | $url .= '&in_loc='.$this->sanitizeText($link->address); 62 | } 63 | 64 | foreach ($this->urlParameters as $key => $value) { 65 | $url .= '&'.urlencode($key).(in_array($value, [null, ''], true) ? '' : '='.$this->sanitizeText((string) $value)); 66 | } 67 | 68 | return $url; 69 | } 70 | 71 | /** 72 | * Prepare text to use used in URL and parsed by the service. 73 | * @param string $text 74 | * @return string 75 | */ 76 | private function sanitizeText(string $text): string 77 | { 78 | return rawurlencode($text); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `calendar-links` will be documented in this file 4 | 5 | ## 1.8.2 - 2022-12-11 6 | ### Changed 7 | - ICS: Use `DESCRIPTION` instead of `X-ALT-DESC` (as it has better support) by @cdubz in #158 8 | - Chore: fix tests, fix and improve CI 9 | 10 | ## 1.8.1 - 2022-12-01 11 | ### Changed 12 | - Remove PHP 7.4 support 13 | - Update dependencies 14 | 15 | ## 1.8.0 - 2022-08-20 16 | ### Changed 17 | - ICS: Add `PRODID` and `DTSTAMP` required parameters to make ICS valid by @makbeta 18 | - ICS: Fix HTML description for Outlook 2016 by @karthikbodu 19 | - Outlook: extract common logic for `WebOffice` and `WebOutlook` into a parent class by @lptn 20 | 21 | ### Fixed 22 | - Simplify format of test snapshots: do not use base64 by @lptn 23 | - Fix typo in README by @fabpot 24 | 25 | ## 1.7.2 - 2022-06-09 26 | ### Fixed 27 | - Outlook: Fixed #148 Support HTML-formatted description by @dravenk in #150 28 | 29 | ## 1.7.1 - 2022-02-14 30 | ### Changed 31 | - Outlook: Fixed location field characters (by @dravenk in #144) 32 | - Add missing dependency of php-cs-fixer and update it 33 | 34 | ## 1.7.0 - 2022-02-13 35 | ### Changed 36 | - New: Add support for outlook.office.com $link->webOffice(); (@dravenk and @gulios) 37 | - Google: Add timezone name if it is specified in both `from` and `to` dates and is the same for both (@bradyemerson) 38 | 39 | ## 1.6.0 - 2021-04-22 40 | ### Changed 41 | - Drop support for PHP 7.2 and PHP 7.3 42 | 43 | ## 1.5.0 - 2021-04-22 44 | ### Changed 45 | - ICS: support URLs as option (@gulios) 46 | - ICS: support all day events spanning multiple days (@mrshowerman) 47 | 48 | ## 1.4.4 - 2021-04-13 49 | ### Fixed 50 | - Yahoo link doesn’t work (yahoo changed param names) (@mukeshsah08). 51 | - Exception message on invalid dates range (idea by @jason-nabooki) 52 | 53 | ## 1.4.3 - 2021-03-05 54 | ### Changed 55 | - Google: use UTC timezone to bypass problems with some timezone names unsupported by Google calendar (⚠️ backwards-incompatible if you extended Google Generator) 56 | 57 | ### Fixed 58 | - Spaces replaced by "+" on Outlook.com #109 59 | 60 | ## 1.4.2 - 2020-09-01 61 | ### Changed 62 | - Simplify extending of ICS Generator 63 | 64 | ## 1.4.1 - 2020-08-27 65 | ### Changed 66 | - Simplify extending of WebOutlook (e.g. for Office365) 67 | - Yahoo: use `allday` parameter only for a single-day events 68 | - Improve exception hierarchy: `InvalidLink` now extends `\InvalidArgumentException` 69 | 70 | ### Added 71 | - Add more tests, reorganize existing 72 | 73 | ## 1.4.0 - 2020-05-02 74 | ### Added 75 | - Allow specifying custom `UID` ICS links (https://github.com/spatie/calendar-links/pull/85) 76 | - Support PHP 8.0 77 | - Support immutable dates (`\DateTimeImmutable::class`) 78 | 79 | ### Changed 80 | - Require PHP 7.2+ 81 | 82 | ## 1.3.0 - 2020-04-29 83 | - Support custom generators (`$link->formatWith(new Your\Generator()`) 84 | - Fix iCal links that contains special chars (use base64 for encoding) 85 | - Fix Outlook links: use new base URI and datetime formats 86 | - Fix Yahoo links: events had invalid end datetime (due to a bug on Yahoo side) 87 | 88 | ## 1.2.4 - 2019-07-17 89 | - Fix Google links for all-day events (use next day as end-date for single-day events) 90 | - Fix Outlook links for all-day events (omit `enddt` for single-day events) 91 | - Add a new `Link::createAllDay` static constructor to simplify creating of all-day events 92 | 93 | ## 1.2.3 - 2019-02-14 94 | - Fix iCal all day links (use DURATION according RFC 5545) 95 | 96 | ## 1.2.2 - 2019-01-15 97 | - Fix Yahoo links for multiple days events 98 | 99 | ## 1.2.1 - 2019-01-13 100 | - Fix iCal: Use CRLF instead of LF (according RFC 5545) 101 | - Fix iCal: Specify UID property (according RFC 5545) 102 | - Fix iCal: Escape `;` character (according RFC 5545) 103 | - Fix iCal: Remove empty new line from .ics files 104 | 105 | ## 1.2.0 - 2019-01-10 106 | - Support timezones 107 | - Add outlook.com link generator 108 | 109 | ## 1.1.1 - 2018-10-08 110 | - Fix Yahoo links 111 | 112 | ## 1.1.0 - 2018-08-13 113 | - Add all day support 114 | 115 | ## 1.0.3 - 2018-07-23 116 | - Fix newlines in description 117 | 118 | ## 1.0.2 - 2018-05-15 119 | - Fix for iCal links in Safari 120 | 121 | ## 1.0.1 - 2018-04-30 122 | - Use `\n` instead of `%0A` when generating an ics file 123 | 124 | ## 1.0.0 - 2017-09-29 125 | - initial release 126 | -------------------------------------------------------------------------------- /src/Link.php: -------------------------------------------------------------------------------- 1 | title = $title; 38 | $this->allDay = $allDay; 39 | 40 | // Ensures timezones match. 41 | if ($from->getTimezone()->getName() !== $to->getTimezone()->getName()) { 42 | $to = (clone $to)->setTimezone($from->getTimezone()); 43 | } 44 | 45 | $this->from = \DateTimeImmutable::createFromInterface($from); 46 | $this->to = \DateTimeImmutable::createFromInterface($to); 47 | 48 | // Ensures from date is earlier than to date. 49 | if ($this->from > $this->to) { 50 | throw InvalidLink::negativeDateRange($this->from, $this->to); 51 | } 52 | } 53 | 54 | /** 55 | * @throws \Spatie\CalendarLinks\Exceptions\InvalidLink When date range is invalid. 56 | */ 57 | public static function create(string $title, \DateTimeInterface $from, \DateTimeInterface $to): static 58 | { 59 | return new static($title, $from, $to); 60 | } 61 | 62 | /** 63 | * @param positive-int $numberOfDays 64 | * @throws \Spatie\CalendarLinks\Exceptions\InvalidLink When date range is invalid. 65 | */ 66 | public static function createAllDay(string $title, \DateTimeInterface $from, int $numberOfDays = 1): static 67 | { 68 | $to = (clone $from)->modify("+$numberOfDays days"); 69 | assert($to instanceof \DateTimeInterface); 70 | 71 | return new static($title, $from, $to, true); 72 | } 73 | 74 | /** Set description of the Event. */ 75 | public function description(string $description): static 76 | { 77 | $this->description = $description; 78 | 79 | return $this; 80 | } 81 | 82 | /** Set the address of the Event. */ 83 | public function address(string $address): static 84 | { 85 | $this->address = $address; 86 | 87 | return $this; 88 | } 89 | 90 | public function formatWith(Generator $generator): string 91 | { 92 | return $generator->generate($this); 93 | } 94 | 95 | /** @psalm-param GoogleUrlParameters $urlParameters */ 96 | public function google(array $urlParameters = []): string 97 | { 98 | return $this->formatWith(new Google($urlParameters)); 99 | } 100 | 101 | /** 102 | * @psalm-param IcsOptions $options ICS specific properties and components 103 | * @psalm-param IcsPresentationOptions $presentationOptions 104 | * @return string 105 | */ 106 | public function ics(array $options = [], array $presentationOptions = []): string 107 | { 108 | return $this->formatWith(new Ics($options, $presentationOptions)); 109 | } 110 | 111 | /** @psalm-param YahooUrlParameters $urlParameters */ 112 | public function yahoo(array $urlParameters = []): string 113 | { 114 | return $this->formatWith(new Yahoo($urlParameters)); 115 | } 116 | 117 | /** @psalm-param OutlookUrlParameters $urlParameters */ 118 | public function webOutlook(array $urlParameters = []): string 119 | { 120 | return $this->formatWith(new WebOutlook($urlParameters)); 121 | } 122 | 123 | /** @psalm-param OutlookUrlParameters $urlParameters */ 124 | public function webOffice(array $urlParameters = []): string 125 | { 126 | return $this->formatWith(new WebOffice($urlParameters)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate add to calendar links for Google, iCal and other calendar systems 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/calendar-links.svg?style=flat-square)](https://packagist.org/packages/spatie/calendar-links) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/calendar-links.svg?style=flat-square)](https://packagist.org/packages/spatie/calendar-links) 5 | [![Run Tests](https://github.com/spatie/calendar-links/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spatie/calendar-links/actions/workflows/run-tests.yml) 6 | [![Quality Score](https://img.shields.io/scrutinizer/g/spatie/calendar-links.svg?style=flat-square)](https://scrutinizer-ci.com/g/spatie/calendar-links) 7 | [![Type coverage](https://shepherd.dev/github/spatie/calendar-links/coverage.svg)](https://shepherd.dev/github/spatie/calendar-links) 8 | [![Psalm level](https://shepherd.dev/github/spatie/calendar-links/level.svg)](https://shepherd.dev/github/spatie/calendar-links) 9 | 10 | 11 | Using this package, you can generate links to add events to calendar systems. Here's a quick example: 12 | 13 | ```php 14 | use Spatie\CalendarLinks\Link; 15 | 16 | Link::create( 17 | 'Birthday', 18 | DateTime::createFromFormat('Y-m-d H:i', '2018-02-01 09:00'), 19 | DateTime::createFromFormat('Y-m-d H:i', '2018-02-01 18:00') 20 | )->google(); 21 | ``` 22 | 23 | This will output: `https://calendar.google.com/calendar/render?action=TEMPLATE&text=Birthday&dates=20180201T090000/20180201T180000&sprop=&sprop=name:` 24 | 25 | If you follow that link (and are authenticated with Google), you’ll see a screen to add the event to your calendar. 26 | 27 | The package can also generate ics files that you can open in several email and calendar programs, including Microsoft Outlook, Google Calendar, and Apple Calendar. 28 | 29 | ## Support us 30 | 31 | [](https://spatie.be/github-ad-click/calendar-links) 32 | 33 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 34 | 35 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 36 | 37 | ## Installation 38 | 39 | You can install the package via composer: 40 | 41 | ```sh 42 | composer require spatie/calendar-links 43 | ``` 44 | 45 | ## Usage 46 | 47 | ```php 48 | description('Cookies & cocktails!') 56 | ->address('Kruikstraat 22, 2018 Antwerpen'); 57 | 58 | // Generate a link to create an event on Google calendar 59 | echo $link->google(); 60 | 61 | // Generate a link to create an event on Yahoo calendar 62 | echo $link->yahoo(); 63 | 64 | // Generate a link to create an event on outlook.live.com calendar 65 | echo $link->webOutlook(); 66 | 67 | // Generate a link to create an event on outlook.office.com calendar 68 | echo $link->webOffice(); 69 | 70 | // Generate a data URI for an ics file (for iCal & Outlook) 71 | echo $link->ics(); 72 | echo $link->ics(['UID' => 'custom-id']); // Custom UID (to update existing events) 73 | echo $link->ics(['URL' => 'https://my-page.com']); // Custom URL 74 | echo $link->ics(['REMINDER' => []]); // Add the default reminder (for iCal & Outlook) 75 | echo $link->ics(['REMINDER' => ['DESCRIPTION' => 'Remind me', 'TIME' => new \DateTime('tomorrow 12:30 UTC')]]); // Add a custom reminder 76 | echo $link->ics([], ['format' => 'file']); // use file output; e.g. to attach ics as a file to an email. 77 | 78 | // Generate a data URI using arbitrary generator: 79 | echo $link->formatWith(new \Your\Generator()); 80 | ``` 81 | 82 | ## Package principles 83 | 84 | 1. it should produce a small output (to keep page-size small) 85 | 2. it should be fast (no any external heavy dependencies) 86 | 3. all `Link` class features should be supported by at least 2 generators (different services have different features) 87 | 88 | ## Changelog 89 | 90 | Please see [CHANGELOG](CHANGELOG.md) for more information. 91 | 92 | ## Testing 93 | 94 | ```sh 95 | composer test 96 | ``` 97 | 98 | ## Contributing 99 | 100 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 101 | 102 | ## Security 103 | 104 | If you've found a bug regarding security, please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 105 | 106 | ## Postcardware 107 | 108 | You're free to use this package (it's [MIT-licensed](LICENSE.md)), but if it makes it to your production environment, we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 109 | 110 | Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. 111 | 112 | We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards). 113 | ## Credits 114 | 115 | - [Sebastian De Deyne](https://github.com/sebastiandedeyne) 116 | - [All Contributors](../../contributors) 117 | 118 | ## License 119 | 120 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 121 | -------------------------------------------------------------------------------- /src/Generators/Ics.php: -------------------------------------------------------------------------------- 1 | options = $options; 38 | $this->presentationOptions = $presentationOptions; 39 | } 40 | 41 | /** @inheritDoc */ 42 | public function generate(Link $link): string 43 | { 44 | $url = [ 45 | 'BEGIN:VCALENDAR', 46 | 'VERSION:2.0', // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.4 47 | 'PRODID:'.($this->options['PRODID'] ?? 'Spatie calendar-links'), // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.7.3 48 | 'BEGIN:VEVENT', 49 | 'UID:'.($this->options['UID'] ?? $this->generateEventUid($link)), 50 | 'SUMMARY:'.$this->escapeString($link->title), 51 | ]; 52 | 53 | $dateTimeFormat = $link->allDay ? $this->dateFormat : $this->dateTimeFormat; 54 | 55 | if ($link->allDay) { 56 | $url[] = 'DTSTAMP:'.$link->from->format($dateTimeFormat); 57 | $url[] = 'DTSTART:'.$link->from->format($dateTimeFormat); 58 | $url[] = 'DURATION:P'.(max(1, $link->from->diff($link->to)->days)).'D'; 59 | } else { 60 | $url[] = 'DTSTAMP:'.gmdate($dateTimeFormat, $link->from->getTimestamp()); 61 | $url[] = 'DTSTART:'.gmdate($dateTimeFormat, $link->from->getTimestamp()); 62 | $url[] = 'DTEND:'.gmdate($dateTimeFormat, $link->to->getTimestamp()); 63 | } 64 | 65 | if ($link->description) { 66 | $url[] = 'DESCRIPTION:'.$this->escapeString(strip_tags($link->description)); 67 | } 68 | if ($link->address) { 69 | $url[] = 'LOCATION:'.$this->escapeString($link->address); 70 | } 71 | 72 | if (isset($this->options['URL'])) { 73 | $url[] = 'URL;VALUE=URI:'.$this->options['URL']; 74 | } 75 | 76 | if (is_array($this->options['REMINDER'] ?? null)) { 77 | $url = [...$url, ...$this->generateAlertComponent($link)]; 78 | } 79 | 80 | $url[] = 'END:VEVENT'; 81 | $url[] = 'END:VCALENDAR'; 82 | 83 | $format = $this->presentationOptions['format'] ?? self::FORMAT_HTML; 84 | 85 | return match ($format) { 86 | 'file' => $this->buildFile($url), 87 | default => $this->buildLink($url), 88 | }; 89 | } 90 | 91 | /** 92 | * @param non-empty-list $propertiesAndComponents 93 | * @return non-empty-string 94 | */ 95 | protected function buildLink(array $propertiesAndComponents): string 96 | { 97 | return 'data:text/calendar;charset=utf8;base64,'.base64_encode(implode("\r\n", $propertiesAndComponents)); 98 | } 99 | 100 | /** 101 | * @param non-empty-list $propertiesAndComponents 102 | * @return non-empty-string 103 | */ 104 | protected function buildFile(array $propertiesAndComponents): string 105 | { 106 | return implode("\r\n", $propertiesAndComponents); 107 | } 108 | 109 | /** @see https://tools.ietf.org/html/rfc5545.html#section-3.3.11 */ 110 | protected function escapeString(string $field): string 111 | { 112 | return addcslashes($field, "\r\n,;"); 113 | } 114 | 115 | /** @see https://tools.ietf.org/html/rfc5545#section-3.8.4.7 */ 116 | protected function generateEventUid(Link $link): string 117 | { 118 | return md5(sprintf( 119 | '%s%s%s%s', 120 | $link->from->format(\DateTimeInterface::ATOM), 121 | $link->to->format(\DateTimeInterface::ATOM), 122 | $link->title, 123 | $link->address 124 | )); 125 | } 126 | 127 | /** 128 | * @param \Spatie\CalendarLinks\Link $link 129 | * @return list 130 | */ 131 | private function generateAlertComponent(Link $link): array 132 | { 133 | $description = $this->options['REMINDER']['DESCRIPTION'] ?? null; 134 | if (! is_string($description)) { 135 | $description = 'Reminder: '.$this->escapeString($link->title); 136 | } 137 | 138 | $trigger = 'TRIGGER:-PT15M'; 139 | if (($reminderTime = $this->options['REMINDER']['TIME'] ?? null) instanceof \DateTimeInterface) { 140 | $trigger = 'TRIGGER;VALUE=DATE-TIME:'.gmdate($this->dateTimeFormat, $reminderTime->getTimestamp()); 141 | } 142 | 143 | $alarmComponent = []; 144 | $alarmComponent[] = 'BEGIN:VALARM'; 145 | $alarmComponent[] = 'ACTION:DISPLAY'; 146 | $alarmComponent[] = 'DESCRIPTION:'.$description; 147 | $alarmComponent[] = $trigger; 148 | $alarmComponent[] = 'END:VALARM'; 149 | 150 | return $alarmComponent; 151 | } 152 | } 153 | --------------------------------------------------------------------------------