├── 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 | [](https://packagist.org/packages/spatie/calendar-links)
4 | [](https://packagist.org/packages/spatie/calendar-links)
5 | [](https://github.com/spatie/calendar-links/actions/workflows/run-tests.yml)
6 | [](https://scrutinizer-ci.com/g/spatie/calendar-links)
7 | [](https://shepherd.dev/github/spatie/calendar-links)
8 | [](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 |
--------------------------------------------------------------------------------