Compare commits
7 Commits
1b6df4a9e4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f1ac693fe8 | |||
| c091ed1371 | |||
| 164df2f770 | |||
| 232802b0ce | |||
| 4dae370deb | |||
| a5c44e106e | |||
| 70a9f30336 |
88
.github/spec.prompt.md
vendored
88
.github/spec.prompt.md
vendored
@@ -1,88 +0,0 @@
|
||||
# Page Link Feature Specification
|
||||
|
||||
## Goals
|
||||
- Allow linking a Dokuwiki page to a media folder via a stable UUID.
|
||||
- Enable the `blobs` alias so plugin syntaxes can use `blobs/*` in place of a concrete path when the page is linked.
|
||||
- Keep behavior stable and maintainable; avoid heavy scanning on each page view.
|
||||
|
||||
## Non-Goals
|
||||
- Automatic creation of `.pagelink` files (user must create manually).
|
||||
- Automatic selection of which folder to link (user decides by placing `.pagelink`).
|
||||
- Cross-page bulk management UI.
|
||||
|
||||
## Core Concept
|
||||
- Each page can have **one** UUID stored in page metadata.
|
||||
- A folder is considered linked to the page if it contains a `.pagelink` file whose content matches the page’s UUID.
|
||||
|
||||
## Data Model
|
||||
- Page metadata key: `pagelink` with value `<UUID>`.
|
||||
- `.pagelink` file content: a UUID string.
|
||||
- Cache mapping file in plugin cache folder: maps UUID → folder path.
|
||||
- Cache file format: JSON (human-readable, easy to debug).
|
||||
- Cache file name: `pagelink_cache.json`.
|
||||
|
||||
## UUID
|
||||
- UUID is created only via a toolbar button.
|
||||
- UUID is created once and stored in metadata.
|
||||
- UUID is v4 lowercase.
|
||||
- UUID is not shown directly to the user (except for copy-to-clipboard).
|
||||
|
||||
## UI/UX Behavior
|
||||
- Above page content (next to pageid), show link status.
|
||||
- If UUID exists and folder is linked: show the linked folder name.
|
||||
- If UUID exists but no folder is linked: show a “not linked yet” status.
|
||||
- Clicking the status copies the UUID to clipboard for easy `.pagelink` creation.
|
||||
- Status text: "Not linked: Copy ID"
|
||||
- Copy action does not show anything. No native toasts available
|
||||
- If UUID does not exist: no status is shown.
|
||||
|
||||
## Linking Rules
|
||||
- Folder can be moved or renamed; link remains as long as `.pagelink` file stays in that folder.
|
||||
- Deleting the `.pagelink` file unlinks the folder.
|
||||
- If multiple folders contain the same UUID:
|
||||
- Page shows a linked status with the first found folder.
|
||||
- Triangle warning icon appears next to folder name.
|
||||
|
||||
## Search Scope
|
||||
- Searches for `.pagelink` files are limited to paths under root aliases defined in `paths` in [admin/main.php](../admin/main.php).
|
||||
- New setting `pagelink_search_depth` limits maximum depth under each root path.
|
||||
- Example: depth `2` means only folders at most 2 levels deep are searched.
|
||||
- Integer value, default `3`.
|
||||
- No negative values allowed.
|
||||
|
||||
## Cache Strategy
|
||||
- Mapping of UUID → folder path is cached in a mapping file for performance.
|
||||
- On page load:
|
||||
1. If page has UUID, check cache for linked folder.
|
||||
2. If cache miss, scan folders (within scope) for `.pagelink`.
|
||||
3. Update cache with any matches.
|
||||
- Cache invalidation strategy:
|
||||
- No automatic cache rebuilds.
|
||||
- When encountering stale cache entry (moved/deleted folder) while looking for a pages linked folder, remove from cache and rescan.
|
||||
|
||||
- Cache writes are atomic (write temp + rename) and use `LOCK_EX`.
|
||||
|
||||
## Performance Constraints
|
||||
- Avoid full-tree scans on every page view.
|
||||
|
||||
## Security & Safety
|
||||
- Never scan outside configured root paths.
|
||||
- ignore symlinks.
|
||||
- Handle unreadable folders/files gracefully (no fatal errors).
|
||||
|
||||
## Acceptance Criteria
|
||||
- Clicking toolbar button sets metadata `pagelink` to a valid UUID.
|
||||
- If a matching `.pagelink` exists in a scoped folder, `blobs/*` resolves to that folder.
|
||||
- If `.pagelink` is deleted, the link is removed on next lookup and UI reflects “not linked yet”.
|
||||
- Cache prevents repeated scans on consecutive page views.
|
||||
- Search depth obeys `pagelink_search_depth`.
|
||||
|
||||
## Edge Cases
|
||||
- Page has UUID but `.pagelink` file is empty or invalid UUID.
|
||||
- Multiple `.pagelink` files with same UUID in different folders.
|
||||
- UUID in metadata is malformed.
|
||||
- Folder contains `.pagelink` but no read permissions.
|
||||
- Page is deleted/renamed after UUID creation.
|
||||
|
||||
## Documentation Updates
|
||||
- Update README to describe the feature, settings, and manual `.pagelink` workflow.
|
||||
141
README.md
141
README.md
@@ -23,9 +23,12 @@ luxtools provides DokuWiki syntax that:
|
||||
|
||||
- Lists a directory's direct children (files + folders) or files matching a glob pattern
|
||||
- Renders an image thumbnail gallery (with lightbox)
|
||||
- Groups multiple `{{image>...}}` blocks in compact grid/flex layouts
|
||||
- Provides "open this folder/path" links for local workflows
|
||||
- Embeds file-backed scratchpads with a minimal inline editor (no wiki revisions)
|
||||
- Links a page to a media folder via a UUID (.pagelink), enabling a `blobs/` alias
|
||||
- Adds a Page ID download link in the page info area to fetch a `.pagelink` file
|
||||
- Renders a basic calendar widget with clickable day links to chronological pages
|
||||
|
||||
It also ships a small file-serving endpoint (`lib/plugins/luxtools/file.php`) used
|
||||
to deliver files and generate cached thumbnails.
|
||||
@@ -47,6 +50,13 @@ If you install this plugin manually, make sure it is installed in:
|
||||
|
||||
If the folder is called differently, DokuWiki will not load it.
|
||||
|
||||
This plugin uses Composer dependencies shipped inside `vendor/`.
|
||||
If dependencies are missing in your local checkout, run:
|
||||
|
||||
```bash
|
||||
php composer.phar install
|
||||
```
|
||||
|
||||
|
||||
## Project structure (developer notes)
|
||||
|
||||
@@ -97,15 +107,20 @@ Key settings:
|
||||
|
||||
- **paths**
|
||||
Allowed base filesystem roots (one per line). Each root can be followed by:
|
||||
- `A> /Alias/` (optional) alias used in wiki syntax and open links
|
||||
- `A> Alias` (optional) alias used in wiki syntax and open links
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
/srv/share/Datascape/
|
||||
A> /Scape/
|
||||
A> Scape
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Wiki syntax accepts aliases in path form (for example `Scape/sub/folder`).
|
||||
- Open links sent to the local client service are emitted as `Alias>relative/path`
|
||||
(for example `Scape>sub/folder`) so each client can resolve its own local root.
|
||||
|
||||
luxtools links use the plugin endpoint:
|
||||
|
||||
`lib/plugins/luxtools/file.php?root=...&file=...`
|
||||
@@ -142,6 +157,20 @@ Key settings:
|
||||
URL of a local client service used by `{{open>...}}` and directory links.
|
||||
See luxtools-client.
|
||||
|
||||
- **image_base_path**
|
||||
Base filesystem path used for chronological photo integration.
|
||||
On canonical day pages (`chronological:YYYY:MM:DD`), files that start with
|
||||
`YYYY-MM-DD` are listed automatically.
|
||||
If a yearly subfolder exists (for example `.../2026/`), it is preferred.
|
||||
|
||||
- **calendar_ics_files**
|
||||
Local calendar `.ics` files (one absolute file path per line).
|
||||
Events are parsed by `sabre/vobject` and shown on matching chronological day pages.
|
||||
Recurrence and exclusions from the ICS are respected. For timed entries, the
|
||||
page stores the original timestamp and renders the visible time in the
|
||||
browser's local timezone.
|
||||
Multi-day events appear on each overlapping day.
|
||||
|
||||
- **pagelink_search_depth**
|
||||
Maximum directory depth for `.pagelink` discovery under each configured root.
|
||||
`0` means only the root directory itself is checked.
|
||||
@@ -211,18 +240,25 @@ Supported input examples include:
|
||||
- `2026-01-30 13:45`
|
||||
- `2026-01-30T13:45:00`
|
||||
|
||||
### 0.2) Editor toolbar: Page Link
|
||||
### 0.2) Page Link: link a page to a folder
|
||||
|
||||
The **Page Link** toolbar button creates a page-scoped UUID and stores it in
|
||||
page metadata. This UUID is used to link the page to a folder that contains
|
||||
a `.pagelink` file with the same UUID.
|
||||
Page linking uses a page-scoped UUID stored in page metadata. This UUID is used
|
||||
to link the page to a folder that contains a `.pagelink` file with the same UUID.
|
||||
|
||||
Workflow:
|
||||
The Page Link workflow is driven by the **Page ID link** in the page info area
|
||||
(page footer, `.docInfo`):
|
||||
|
||||
1. Click **Page Link** in the editor toolbar to create the UUID.
|
||||
2. View the page and copy the UUID from the “Not linked: Copy ID” status.
|
||||
3. Create a `.pagelink` file in the target folder (within your configured
|
||||
`paths` roots) and paste the UUID into that file.
|
||||
1. **Link Page** (page has no UUID yet)
|
||||
Creates the UUID and downloads a `.pagelink` file.
|
||||
2. **Download Link File** (page has UUID, but no linked folder found)
|
||||
Downloads the `.pagelink` file.
|
||||
3. **Unlink Page** (page is linked)
|
||||
Prompts for confirmation, removes the `.pagelink` file from the linked folder
|
||||
(if found), removes the UUID from the page, and refreshes the page.
|
||||
|
||||
After downloading the `.pagelink` file, place it into the folder you want to
|
||||
link (within your configured `paths` roots). Once DokuWiki can discover it,
|
||||
the page becomes “linked”.
|
||||
|
||||
Once linked, you can use `blobs/` as an alias in luxtools syntax on that page,
|
||||
for example:
|
||||
@@ -232,6 +268,38 @@ for example:
|
||||
{{directory>blobs/&recursive=1}}
|
||||
```
|
||||
|
||||
### 0.3) Calendar widget
|
||||
|
||||
Render a basic monthly calendar that links each day to canonical chronological pages:
|
||||
|
||||
```
|
||||
{{calendar>}}
|
||||
{{calendar>2024-10}}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `{{calendar>}}` renders the current month.
|
||||
- `{{calendar>YYYY-MM}}` renders a specific month.
|
||||
- Day links target `chronological:YYYY:MM:DD`.
|
||||
- Header month/year links target `chronological:YYYY:MM` and `chronological:YYYY`.
|
||||
- Prev/next month buttons update the widget in-place without a full page reload.
|
||||
- Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node.
|
||||
|
||||
### 0.4) Virtual chronological day pages
|
||||
|
||||
When a canonical day page (for example `chronological:2026:02:13`) does not yet
|
||||
exist, luxtools renders a virtual page in normal show mode instead of the
|
||||
default "page does not exist" output.
|
||||
|
||||
The virtual page includes:
|
||||
|
||||
- a German-formatted heading (for example `Freitag, 13. Februar 2026`)
|
||||
- matching local calendar events from configured `.ics` files (when available)
|
||||
- matching day photos (via existing `{{images>...}}` rendering) when available
|
||||
|
||||
The page is only created once you edit and save actual content.
|
||||
|
||||
### 1) List files by glob pattern
|
||||
|
||||
The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table:
|
||||
@@ -297,7 +365,52 @@ The image links to the full-size version when clicked.
|
||||
|
||||
Remote images (HTTP/HTTPS URLs) are linked directly without proxying or thumbnailing.
|
||||
|
||||
### 5) Open a local path/folder (best-effort)
|
||||
### 5) Group multiple image boxes compactly
|
||||
|
||||
Use `<grouping> ... </grouping>` to arrange multiple `{{image>...}}` entries in less vertical space.
|
||||
|
||||
```text
|
||||
<grouping>
|
||||
{{image>/Scape/photos/1.jpg|One|300}}
|
||||
{{image>/Scape/photos/2.jpg|Two|300}}
|
||||
{{image>/Scape/photos/3.jpg|Three|300}}
|
||||
{{image>/Scape/photos/4.jpg|Four|300}}
|
||||
</grouping>
|
||||
|
||||
<grouping layout="flex" gap="0" justify="start" align="start">
|
||||
{{image>/Scape/photos/1.jpg|One|220}}
|
||||
{{image>/Scape/photos/2.jpg|Two|220}}
|
||||
{{image>/Scape/photos/3.jpg|Three|220}}
|
||||
</grouping>
|
||||
|
||||
<grouping layout="grid" cols="3" gap="0.4rem">
|
||||
{{image>/Scape/photos/1.jpg|One|260}}
|
||||
{{image>/Scape/photos/2.jpg|Two|260}}
|
||||
{{image>/Scape/photos/3.jpg|Three|260}}
|
||||
</grouping>
|
||||
|
||||
<grouping layout="flex" gap="0.5rem" justify="space-between" align="center">
|
||||
{{image>/Scape/photos/1.jpg|One|220}}
|
||||
{{image>/Scape/photos/2.jpg|Two|220}}
|
||||
{{image>/Scape/photos/3.jpg|Three|220}}
|
||||
</grouping>
|
||||
```
|
||||
|
||||
Supported attributes on the opening tag:
|
||||
|
||||
- `layout`: `flex` (default) or `grid`
|
||||
- `cols`: integer >= 1 (default `2`, used by `grid`)
|
||||
- `gap`: CSS length token such as `0`, `0.6rem`, `8px` (default `0`)
|
||||
- `justify`: `start`, `center`, `end`, `space-between`, `space-around`, `space-evenly` (default `start`)
|
||||
- `align`: `start`, `center`, `end`, `stretch`, `baseline` (default `start`)
|
||||
|
||||
Notes:
|
||||
- The wrapper only controls layout. It adds no own border/background/frame.
|
||||
- Invalid values silently fall back to defaults.
|
||||
- Unknown attributes render a small warning string, e.g. `[grouping: unknown option(s): gpa]`.
|
||||
- Existing standalone `{{image>...}}` behavior is unchanged outside `<grouping>`.
|
||||
|
||||
### 6) Open a local path/folder (best-effort)
|
||||
|
||||
```
|
||||
{{open>/Scape/projects|Open projects folder}}
|
||||
@@ -309,7 +422,7 @@ Behaviour:
|
||||
- Prefer calling the configured local client service (open_service_url).
|
||||
- Fall back to opening a file:// URL in a new tab (often blocked by browsers).
|
||||
|
||||
### 6) Scratchpads (shared, file-backed, no page revisions)
|
||||
### 7) Scratchpads (shared, file-backed, no page revisions)
|
||||
|
||||
```
|
||||
{{scratchpad>start}}
|
||||
@@ -317,7 +430,7 @@ Behaviour:
|
||||
|
||||
Scratchpads render the referenced file as wikitext and (when you have edit rights on the host page) provide an inline editor that saves directly to the backing file.
|
||||
|
||||
### 7) Link Favicons (automatic)
|
||||
### 8) Link Favicons (automatic)
|
||||
|
||||
External links automatically display the favicon of the linked website. This feature:
|
||||
|
||||
|
||||
138
_test/ChronoIDTest.php
Normal file
138
_test/ChronoIDTest.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools\test;
|
||||
|
||||
use dokuwiki\plugin\luxtools\ChronoID;
|
||||
use DokuWikiTest;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* Chronological ID helper tests.
|
||||
*
|
||||
* @group plugin_luxtools
|
||||
* @group plugins
|
||||
*/
|
||||
class ChronoIDTest extends DokuWikiTest
|
||||
{
|
||||
protected function assertBool(bool $expected, bool $actual, string $message): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new \Exception($message);
|
||||
}
|
||||
}
|
||||
|
||||
protected function assertStringOrNull(?string $expected, ?string $actual, string $message): void
|
||||
{
|
||||
if ($expected !== $actual) {
|
||||
throw new \Exception($message . ' expected=' . var_export($expected, true) . ' actual=' . var_export($actual, true));
|
||||
}
|
||||
}
|
||||
|
||||
public function testIsIsoDateValidCases(): void
|
||||
{
|
||||
$valid = [
|
||||
'2024-10-24',
|
||||
'2024-02-29',
|
||||
];
|
||||
|
||||
foreach ($valid as $date) {
|
||||
$this->assertBool(true, ChronoID::isIsoDate($date), 'Expected valid ISO date: ' . $date);
|
||||
}
|
||||
}
|
||||
|
||||
public function testIsIsoDateInvalidCases(): void
|
||||
{
|
||||
$invalid = [
|
||||
'2023-02-29',
|
||||
'2024-13-01',
|
||||
'2024-00-10',
|
||||
'24-10-2024',
|
||||
'2024/10/24',
|
||||
'2024-10-24 12:00:00',
|
||||
'2024-10-24T12:00:00',
|
||||
'0000-01-01',
|
||||
];
|
||||
|
||||
foreach ($invalid as $date) {
|
||||
$this->assertBool(false, ChronoID::isIsoDate($date), 'Expected invalid ISO date: ' . $date);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDateToDayId(): void
|
||||
{
|
||||
$this->assertStringOrNull('chronological:2024:10:24', ChronoID::dateToDayId('2024-10-24'), 'dateToDayId failed');
|
||||
$this->assertStringOrNull('journal:chrono:2024:10:24', ChronoID::dateToDayId('2024-10-24', 'journal:chrono'), 'dateToDayId with custom namespace failed');
|
||||
|
||||
$this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24T12:00:00'), 'datetime should be rejected');
|
||||
$this->assertStringOrNull(null, ChronoID::dateToDayId('2024-13-01'), 'invalid month should be rejected');
|
||||
$this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24', ''), 'empty namespace should be rejected');
|
||||
$this->assertStringOrNull(null, ChronoID::dateToDayId('2024-10-24', 'bad namespace!'), 'invalid namespace should be rejected');
|
||||
}
|
||||
|
||||
public function testCanonicalIdChecks(): void
|
||||
{
|
||||
$this->assertBool(true, ChronoID::isDayId('chronological:2024:10:24'), 'valid day ID should be accepted');
|
||||
$this->assertBool(true, ChronoID::isMonthId('chronological:2024:10'), 'valid month ID should be accepted');
|
||||
$this->assertBool(true, ChronoID::isYearId('chronological:2024'), 'valid year ID should be accepted');
|
||||
|
||||
$this->assertBool(false, ChronoID::isDayId('2024:10:24'), 'missing namespace should be rejected as day ID');
|
||||
$this->assertBool(false, ChronoID::isDayId('chronological:2024-10-24'), 'hyphen date in ID should be rejected as day ID');
|
||||
$this->assertBool(false, ChronoID::isDayId('chronological:2023:02:29'), 'invalid Gregorian day should be rejected');
|
||||
|
||||
$this->assertBool(false, ChronoID::isMonthId('chronological:2024:13'), 'invalid month 13 should be rejected');
|
||||
$this->assertBool(false, ChronoID::isMonthId('chronological:2024:00'), 'invalid month 00 should be rejected');
|
||||
$this->assertBool(false, ChronoID::isMonthId('chronological:2024-10'), 'invalid month format should be rejected');
|
||||
|
||||
$this->assertBool(false, ChronoID::isYearId('chronological:0000'), 'year 0000 should be rejected');
|
||||
$this->assertBool(false, ChronoID::isYearId('chronological:24'), 'short year should be rejected');
|
||||
$this->assertBool(false, ChronoID::isYearId('chronological:2024:10'), 'month ID should not pass as year ID');
|
||||
}
|
||||
|
||||
public function testConversions(): void
|
||||
{
|
||||
$this->assertStringOrNull('chronological:2024:10', ChronoID::dayIdToMonthId('chronological:2024:10:24'), 'dayIdToMonthId failed');
|
||||
$this->assertStringOrNull('chronological:2024', ChronoID::monthIdToYearId('chronological:2024:10'), 'monthIdToYearId failed');
|
||||
|
||||
$this->assertStringOrNull(null, ChronoID::dayIdToMonthId('chronological:2024:13:24'), 'invalid day ID should map to null month ID');
|
||||
$this->assertStringOrNull(null, ChronoID::dayIdToMonthId('chronological:2024:10'), 'month ID should not map via dayIdToMonthId');
|
||||
$this->assertStringOrNull(null, ChronoID::monthIdToYearId('chronological:2024:13'), 'invalid month ID should map to null year ID');
|
||||
$this->assertStringOrNull(null, ChronoID::monthIdToYearId('chronological:2024:10:24'), 'day ID should not map via monthIdToYearId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration-style smoke test for canonical ID matrix acceptance/rejection.
|
||||
*/
|
||||
public function testCanonicalPageIdSmokeMatrix(): void
|
||||
{
|
||||
$accepted = [
|
||||
['day', 'chronological:2024:10:24'],
|
||||
['month', 'chronological:2024:10'],
|
||||
['year', 'chronological:2024'],
|
||||
];
|
||||
|
||||
foreach ($accepted as [$kind, $id]) {
|
||||
if ($kind === 'day') {
|
||||
$this->assertBool(true, ChronoID::isDayId($id), 'Expected accepted day ID: ' . $id);
|
||||
} elseif ($kind === 'month') {
|
||||
$this->assertBool(true, ChronoID::isMonthId($id), 'Expected accepted month ID: ' . $id);
|
||||
} else {
|
||||
$this->assertBool(true, ChronoID::isYearId($id), 'Expected accepted year ID: ' . $id);
|
||||
}
|
||||
}
|
||||
|
||||
$rejected = [
|
||||
'2024:10:24',
|
||||
'chronological:2024-10-24',
|
||||
'chronological:2024:13:01',
|
||||
'chronological:2024:00',
|
||||
'chronological:0000',
|
||||
];
|
||||
|
||||
foreach ($rejected as $id) {
|
||||
$this->assertBool(false, ChronoID::isDayId($id), 'Unexpected day ID acceptance: ' . $id);
|
||||
$this->assertBool(false, ChronoID::isMonthId($id), 'Unexpected month ID acceptance: ' . $id);
|
||||
$this->assertBool(false, ChronoID::isYearId($id), 'Unexpected year ID acceptance: ' . $id);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
_test/ChronologicalDateAutoLinkerTest.php
Normal file
42
_test/ChronologicalDateAutoLinkerTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools\test;
|
||||
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker;
|
||||
use DokuWikiTest;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* Tests for extracted chronological auto-linker.
|
||||
*
|
||||
* @group plugin_luxtools
|
||||
* @group plugins
|
||||
*/
|
||||
class ChronologicalDateAutoLinkerTest extends DokuWikiTest
|
||||
{
|
||||
public function testLinksPlainTextDate(): void
|
||||
{
|
||||
$html = '<p>Meeting on 2024-10-24</p>';
|
||||
$out = ChronologicalDateAutoLinker::linkHtml($html);
|
||||
|
||||
$decoded = urldecode($out);
|
||||
if (strpos($decoded, 'chronological:2024:10:24') === false) {
|
||||
throw new \Exception('Expected canonical link target not found');
|
||||
}
|
||||
}
|
||||
|
||||
public function testSkipsCodeContent(): void
|
||||
{
|
||||
$html = '<p>Outside 2024-10-25</p><code>Inside 2024-10-24</code>';
|
||||
$out = ChronologicalDateAutoLinker::linkHtml($html);
|
||||
|
||||
$decoded = urldecode($out);
|
||||
if (strpos($decoded, 'chronological:2024:10:25') === false) {
|
||||
throw new \Exception('Expected outside date link not found');
|
||||
}
|
||||
if (strpos($decoded, 'chronological:2024:10:24') !== false) {
|
||||
throw new \Exception('Date inside code block should not be auto-linked');
|
||||
}
|
||||
}
|
||||
}
|
||||
37
_test/ChronologicalDayTemplateTest.php
Normal file
37
_test/ChronologicalDayTemplateTest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools\test;
|
||||
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDayTemplate;
|
||||
use DokuWikiTest;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* Tests for German day template generation.
|
||||
*
|
||||
* @group plugin_luxtools
|
||||
* @group plugins
|
||||
*/
|
||||
class ChronologicalDayTemplateTest extends DokuWikiTest
|
||||
{
|
||||
public function testBuildForDayIdGermanHeading(): void
|
||||
{
|
||||
$tpl = ChronologicalDayTemplate::buildForDayId('chronological:2026:02:13');
|
||||
if (!is_string($tpl) || $tpl === '') {
|
||||
throw new \Exception('Expected non-empty day template');
|
||||
}
|
||||
|
||||
if (strpos($tpl, '====== Freitag, 13. Februar 2026 ======') === false) {
|
||||
throw new \Exception('Expected German formatted heading not found');
|
||||
}
|
||||
}
|
||||
|
||||
public function testBuildForDayIdRejectsInvalid(): void
|
||||
{
|
||||
$tpl = ChronologicalDayTemplate::buildForDayId('chronological:2026:02');
|
||||
if ($tpl !== null) {
|
||||
throw new \Exception('Expected null for non-day ID');
|
||||
}
|
||||
}
|
||||
}
|
||||
222
_test/ChronologicalIcsEventsTest.php
Normal file
222
_test/ChronologicalIcsEventsTest.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools\test;
|
||||
|
||||
use dokuwiki\plugin\luxtools\ChronologicalIcsEvents;
|
||||
use DokuWikiTest;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* Tests for local ICS event parsing.
|
||||
*
|
||||
* @group plugin_luxtools
|
||||
* @group plugins
|
||||
*/
|
||||
class ChronologicalIcsEventsTest extends DokuWikiTest
|
||||
{
|
||||
public function testEventsForDateParsesAllDayAndTimedEntries(): void
|
||||
{
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('case_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics = $dir . '/calendar.ics';
|
||||
|
||||
$content = "BEGIN:VCALENDAR\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "DTSTART;VALUE=DATE:20260216\n"
|
||||
. "SUMMARY:Ganztag Event\n"
|
||||
. "END:VEVENT\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "DTSTART:20260216T134500\n"
|
||||
. "SUMMARY:Termin A\n"
|
||||
. "END:VEVENT\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "DTSTART:20260217T090000\n"
|
||||
. "SUMMARY:Anderer Tag\n"
|
||||
. "END:VEVENT\n"
|
||||
. "END:VCALENDAR\n";
|
||||
@file_put_contents($ics, $content);
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16');
|
||||
if (count($events) !== 2) {
|
||||
throw new \Exception('Expected 2 events for 2026-02-16, got ' . count($events));
|
||||
}
|
||||
|
||||
$summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events);
|
||||
if (!in_array('Ganztag Event', $summaries, true)) {
|
||||
throw new \Exception('Missing all-day event summary');
|
||||
}
|
||||
if (!in_array('Termin A', $summaries, true)) {
|
||||
throw new \Exception('Missing timed event summary');
|
||||
}
|
||||
|
||||
$timed = null;
|
||||
foreach ($events as $event) {
|
||||
if ((string)($event['summary'] ?? '') === 'Termin A') {
|
||||
$timed = $event;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($timed)) {
|
||||
throw new \Exception('Timed event payload missing');
|
||||
}
|
||||
if (trim((string)($timed['startIso'] ?? '')) === '') {
|
||||
throw new \Exception('Timed event should expose startIso for client-side timezone conversion');
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventsForDateReadsMultipleConfiguredFiles(): void
|
||||
{
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('multi_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics1 = $dir . '/one.ics';
|
||||
$ics2 = $dir . '/two.ics';
|
||||
|
||||
@file_put_contents($ics1, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T100000\nSUMMARY:Eins\nEND:VEVENT\nEND:VCALENDAR\n");
|
||||
@file_put_contents($ics2, "BEGIN:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20260218T110000\nSUMMARY:Zwei\nEND:VEVENT\nEND:VCALENDAR\n");
|
||||
|
||||
$config = $ics1 . "\n" . $ics2;
|
||||
$events = ChronologicalIcsEvents::eventsForDate($config, '2026-02-18');
|
||||
|
||||
if (count($events) !== 2) {
|
||||
throw new \Exception('Expected 2 events from two files, got ' . count($events));
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventsForDateSupportsWeeklyRecurrence(): void
|
||||
{
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('rrule_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics = $dir . '/recurring.ics';
|
||||
|
||||
$content = "BEGIN:VCALENDAR\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "UID:weekly-1\n"
|
||||
. "DTSTART:20260205T090000\n"
|
||||
. "RRULE:FREQ=WEEKLY;INTERVAL=1\n"
|
||||
. "SUMMARY:Wiederkehrender Termin\n"
|
||||
. "END:VEVENT\n"
|
||||
. "END:VCALENDAR\n";
|
||||
@file_put_contents($ics, $content);
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12');
|
||||
if (count($events) < 1) {
|
||||
throw new \Exception('Expected recurring event on 2026-02-12, got none');
|
||||
}
|
||||
|
||||
$summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events);
|
||||
if (!in_array('Wiederkehrender Termin', $summaries, true)) {
|
||||
throw new \Exception('Recurring summary not found on matching date');
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventsForDateRespectsExdateForRecurringEvent(): void
|
||||
{
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('exdate_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics = $dir . '/recurring-exdate.ics';
|
||||
|
||||
$content = "BEGIN:VCALENDAR\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "UID:weekly-2\n"
|
||||
. "DTSTART:20260205T090000\n"
|
||||
. "RRULE:FREQ=WEEKLY;COUNT=4\n"
|
||||
. "EXDATE:20260212T090000\n"
|
||||
. "SUMMARY:Termin mit Ausnahme\n"
|
||||
. "END:VEVENT\n"
|
||||
. "END:VCALENDAR\n";
|
||||
@file_put_contents($ics, $content);
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-12');
|
||||
$summaries = array_map(static fn(array $e): string => (string)$e['summary'], $events);
|
||||
if (in_array('Termin mit Ausnahme', $summaries, true)) {
|
||||
throw new \Exception('Recurring event with EXDATE should not appear on excluded day');
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventsForDateKeepsUtcDateAndTimeAsIs(): void
|
||||
{
|
||||
$previousTimezone = date_default_timezone_get();
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
|
||||
try {
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('tz_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics = $dir . '/timezone.ics';
|
||||
|
||||
$content = "BEGIN:VCALENDAR\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "UID:utc-shift\n"
|
||||
. "DTSTART:20260216T233000Z\n"
|
||||
. "SUMMARY:UTC Spaet\n"
|
||||
. "END:VEVENT\n"
|
||||
. "END:VCALENDAR\n";
|
||||
@file_put_contents($ics, $content);
|
||||
|
||||
$eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16');
|
||||
$eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17');
|
||||
|
||||
$summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16);
|
||||
$summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17);
|
||||
|
||||
if (!in_array('UTC Spaet', $summaries16, true)) {
|
||||
throw new \Exception('UTC event should stay on its own UTC date');
|
||||
}
|
||||
if (in_array('UTC Spaet', $summaries17, true)) {
|
||||
throw new \Exception('UTC event should not be shifted to next day by server timezone');
|
||||
}
|
||||
|
||||
$utcEvent = null;
|
||||
foreach ($eventsOn16 as $entry) {
|
||||
if ((string)($entry['summary'] ?? '') === 'UTC Spaet') {
|
||||
$utcEvent = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_array($utcEvent)) {
|
||||
throw new \Exception('UTC event payload missing after day match');
|
||||
}
|
||||
if ((string)($utcEvent['time'] ?? '') !== '23:30') {
|
||||
throw new \Exception('UTC event time should remain unchanged (expected 23:30)');
|
||||
}
|
||||
} finally {
|
||||
date_default_timezone_set($previousTimezone);
|
||||
}
|
||||
}
|
||||
|
||||
public function testEventsForDateShowsMultiDayAllDayEventOnOverlappingDays(): void
|
||||
{
|
||||
$dir = TMP_DIR . '/chrono_ics/' . uniqid('multiday_', true);
|
||||
@mkdir($dir, 0777, true);
|
||||
$ics = $dir . '/multiday.ics';
|
||||
|
||||
$content = "BEGIN:VCALENDAR\n"
|
||||
. "BEGIN:VEVENT\n"
|
||||
. "UID:multi-day-1\n"
|
||||
. "DTSTART;VALUE=DATE:20260216\n"
|
||||
. "DTEND;VALUE=DATE:20260218\n"
|
||||
. "SUMMARY:Mehrtagesereignis\n"
|
||||
. "END:VEVENT\n"
|
||||
. "END:VCALENDAR\n";
|
||||
@file_put_contents($ics, $content);
|
||||
|
||||
$eventsOn16 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-16');
|
||||
$eventsOn17 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-17');
|
||||
$eventsOn18 = ChronologicalIcsEvents::eventsForDate($ics, '2026-02-18');
|
||||
|
||||
$summaries16 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn16);
|
||||
$summaries17 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn17);
|
||||
$summaries18 = array_map(static fn(array $e): string => (string)$e['summary'], $eventsOn18);
|
||||
|
||||
if (!in_array('Mehrtagesereignis', $summaries16, true)) {
|
||||
throw new \Exception('Multi-day all-day event should appear on start day');
|
||||
}
|
||||
if (!in_array('Mehrtagesereignis', $summaries17, true)) {
|
||||
throw new \Exception('Multi-day all-day event should appear on overlapping day');
|
||||
}
|
||||
if (in_array('Mehrtagesereignis', $summaries18, true)) {
|
||||
throw new \Exception('Multi-day all-day event should respect exclusive DTEND day');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,9 +115,20 @@ EOT
|
||||
public function testMapToAliasPath()
|
||||
{
|
||||
$mapped = $this->path->mapToAliasPath('/linux/another/path/some/folder');
|
||||
$this->assertEquals('alias/some/folder', $mapped);
|
||||
$this->assertEquals('alias>some/folder', $mapped);
|
||||
|
||||
$mappedRoot = $this->path->mapToAliasPath('/linux/another/path/');
|
||||
$this->assertEquals('alias>', $mappedRoot);
|
||||
|
||||
$unmapped = $this->path->mapToAliasPath('/linux/file/path/example.txt');
|
||||
$this->assertEquals('/linux/file/path/example.txt', $unmapped);
|
||||
}
|
||||
|
||||
public function testMapToAliasPathLegacyAliasStyle()
|
||||
{
|
||||
$path = new Path("/srv/share/Datascape/\nA> /Scape/\n");
|
||||
|
||||
$mapped = $path->mapToAliasPath('/srv/share/Datascape/projects/demo');
|
||||
$this->assertEquals('Scape>projects/demo', $mapped);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +239,101 @@ class plugin_luxtools_test extends DokuWikiTest
|
||||
$this->assertStringContainsString('height="150"', $xhtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouping wrapper should use default flex mode with zero gap.
|
||||
*/
|
||||
public function test_grouping_default_flex()
|
||||
{
|
||||
$imagePath = TMP_DIR . '/filelistdata/exampleimage.png';
|
||||
$syntax = '<grouping>'
|
||||
. '{{image>' . $imagePath . '|One|120}}'
|
||||
. '{{image>' . $imagePath . '|Two|120}}'
|
||||
. '</grouping>';
|
||||
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
$doc = new Document();
|
||||
$doc->html($xhtml);
|
||||
|
||||
$structure = [
|
||||
'div.luxtools-grouping.luxtools-grouping--flex' => 1,
|
||||
'div.luxtools-grouping .luxtools-imagebox' => 2,
|
||||
];
|
||||
$this->structureCheck($doc, $structure);
|
||||
|
||||
$style = (string)$doc->find('div.luxtools-grouping')->first()->attr('style');
|
||||
$this->assertStringContainsString('--luxtools-grouping-cols: 2', $style);
|
||||
$this->assertStringContainsString('--luxtools-grouping-gap: 0', $style);
|
||||
$this->assertStringContainsString('--luxtools-grouping-justify: start', $style);
|
||||
$this->assertStringContainsString('--luxtools-grouping-align: start', $style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouping wrapper should accept custom flex layout and gap.
|
||||
*/
|
||||
public function test_grouping_custom_flex()
|
||||
{
|
||||
$imagePath = TMP_DIR . '/filelistdata/exampleimage.png';
|
||||
$syntax = '<grouping layout="flex" gap="8px">'
|
||||
. '{{image>' . $imagePath . '|One|120}}'
|
||||
. '{{image>' . $imagePath . '|Two|120}}'
|
||||
. '</grouping>';
|
||||
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
$doc = new Document();
|
||||
$doc->html($xhtml);
|
||||
|
||||
$structure = [
|
||||
'div.luxtools-grouping.luxtools-grouping--flex' => 1,
|
||||
'div.luxtools-grouping .luxtools-imagebox' => 2,
|
||||
];
|
||||
$this->structureCheck($doc, $structure);
|
||||
|
||||
$style = (string)$doc->find('div.luxtools-grouping')->first()->attr('style');
|
||||
$this->assertStringContainsString('--luxtools-grouping-gap: 8px', $style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Grouping wrapper should accept justify and align controls.
|
||||
*/
|
||||
public function test_grouping_justify_and_align()
|
||||
{
|
||||
$imagePath = TMP_DIR . '/filelistdata/exampleimage.png';
|
||||
$syntax = '<grouping layout="flex" justify="space-between" align="center">'
|
||||
. '{{image>' . $imagePath . '|One|120}}'
|
||||
. '{{image>' . $imagePath . '|Two|120}}'
|
||||
. '</grouping>';
|
||||
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
$doc = new Document();
|
||||
$doc->html($xhtml);
|
||||
|
||||
$style = (string)$doc->find('div.luxtools-grouping')->first()->attr('style');
|
||||
$this->assertStringContainsString('--luxtools-grouping-justify: space-between', $style);
|
||||
$this->assertStringContainsString('--luxtools-grouping-align: center', $style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown grouping attributes should render a warning string.
|
||||
*/
|
||||
public function test_grouping_unknown_option_warning()
|
||||
{
|
||||
$imagePath = TMP_DIR . '/filelistdata/exampleimage.png';
|
||||
$syntax = '<grouping gpa="0.5rem">'
|
||||
. '{{image>' . $imagePath . '|One|120}}'
|
||||
. '</grouping>';
|
||||
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
$this->assertStringContainsString('[grouping: unknown option(s): gpa]', $xhtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the built-in file endpoint includes the host page id so file.php can
|
||||
* enforce per-page ACL.
|
||||
@@ -309,4 +404,121 @@ class plugin_luxtools_test extends DokuWikiTest
|
||||
// Directory row should trigger the same behaviour as {{open>...}} for that folder
|
||||
$this->assertStringContainsString('data-path="/Scape/exampledir"', $xhtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict ISO dates in plain text should be auto-linked to canonical day IDs.
|
||||
*/
|
||||
public function test_auto_link_iso_date_plain_text()
|
||||
{
|
||||
$instructions = p_get_instructions('Meeting with John on 2024-10-24.');
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
if (strpos($xhtml, '>2024-10-24</a>') === false) {
|
||||
throw new \Exception('Auto-link text for 2024-10-24 not found');
|
||||
}
|
||||
|
||||
if (strpos(urldecode($xhtml), 'chronological:2024:10:24') === false) {
|
||||
throw new \Exception('Auto-link target chronological:2024:10:24 not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-linking must not run inside code blocks.
|
||||
*/
|
||||
public function test_auto_link_skips_code_blocks()
|
||||
{
|
||||
$syntax = 'Outside date 2024-10-25.' . "\n\n" . '<code>Inside code 2024-10-24</code>';
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
if (strpos($xhtml, '>2024-10-25</a>') === false) {
|
||||
throw new \Exception('Outside date 2024-10-25 was not auto-linked');
|
||||
}
|
||||
|
||||
if (strpos(urldecode($xhtml), 'chronological:2024:10:25') === false) {
|
||||
throw new \Exception('Outside auto-link target chronological:2024:10:25 not found');
|
||||
}
|
||||
|
||||
if (strpos(urldecode($xhtml), 'chronological:2024:10:24') !== false) {
|
||||
throw new \Exception('Date inside code block was incorrectly auto-linked');
|
||||
}
|
||||
|
||||
if (strpos($xhtml, 'Inside code 2024-10-24') === false) {
|
||||
throw new \Exception('Code block content was unexpectedly altered');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar widget should render links to canonical day IDs.
|
||||
*/
|
||||
public function test_calendar_widget_links_canonical_day_ids()
|
||||
{
|
||||
$instructions = p_get_instructions('{{calendar>2024-10}}');
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
if (strpos($xhtml, 'luxtools-calendar') === false) {
|
||||
throw new \Exception('Calendar container not rendered');
|
||||
}
|
||||
|
||||
$decoded = urldecode($xhtml);
|
||||
if (strpos($decoded, 'chronological:2024:10:01') === false) {
|
||||
throw new \Exception('Expected canonical day link for 2024-10-01 not found');
|
||||
}
|
||||
if (strpos($decoded, 'chronological:2024:10:31') === false) {
|
||||
throw new \Exception('Expected canonical day link for 2024-10-31 not found');
|
||||
}
|
||||
|
||||
if (strpos($decoded, 'chronological:2024:10') === false) {
|
||||
throw new \Exception('Expected month link chronological:2024:10 not found in header');
|
||||
}
|
||||
if (strpos($decoded, 'chronological:2024') === false) {
|
||||
throw new \Exception('Expected year link chronological:2024 not found in header');
|
||||
}
|
||||
if (strpos($decoded, 'chronological:2024:09') === false) {
|
||||
throw new \Exception('Expected previous month canonical ID chronological:2024:09 not found');
|
||||
}
|
||||
if (strpos($decoded, 'chronological:2024:11') === false) {
|
||||
throw new \Exception('Expected next month canonical ID chronological:2024:11 not found');
|
||||
}
|
||||
|
||||
if (strpos($xhtml, 'luxtools-calendar-nav') === false) {
|
||||
throw new \Exception('Calendar navigation container not rendered');
|
||||
}
|
||||
|
||||
if (strpos($xhtml, 'luxtools-calendar-nav-button') === false) {
|
||||
throw new \Exception('Calendar navigation buttons not rendered');
|
||||
}
|
||||
|
||||
if (strpos($xhtml, 'data-luxtools-calendar="1"') === false) {
|
||||
throw new \Exception('Calendar JS state attribute not rendered');
|
||||
}
|
||||
if (strpos($xhtml, 'data-luxtools-ajax-url=') === false) {
|
||||
throw new \Exception('Calendar AJAX endpoint metadata not rendered');
|
||||
}
|
||||
|
||||
if (strpos($xhtml, 'luxtools-calendar-day') === false || strpos($xhtml, '<td class="luxtools-calendar-day') === false) {
|
||||
throw new \Exception('Calendar day cells not rendered as expected');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty calendar target should default to current month rendering.
|
||||
*/
|
||||
public function test_calendar_widget_defaults_to_current_month()
|
||||
{
|
||||
$instructions = p_get_instructions('{{calendar>}}');
|
||||
$xhtml = p_render('xhtml', $instructions, $info);
|
||||
|
||||
if (strpos($xhtml, 'luxtools-calendar-table') === false) {
|
||||
throw new \Exception('Calendar table not rendered for default month');
|
||||
}
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$parts = explode('-', $today);
|
||||
$expected = 'chronological:' . $parts[0] . ':' . $parts[1] . ':' . $parts[2];
|
||||
|
||||
if (strpos(urldecode($xhtml), $expected) === false) {
|
||||
throw new \Exception('Expected canonical link for current date not found: ' . $expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
467
action.php
467
action.php
@@ -3,6 +3,11 @@
|
||||
use dokuwiki\Extension\ActionPlugin;
|
||||
use dokuwiki\Extension\Event;
|
||||
use dokuwiki\Extension\EventHandler;
|
||||
use dokuwiki\plugin\luxtools\ChronoID;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalDayTemplate;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalIcsEvents;
|
||||
require_once(__DIR__ . '/autoload.php');
|
||||
|
||||
/**
|
||||
@@ -10,8 +15,11 @@ require_once(__DIR__ . '/autoload.php');
|
||||
*/
|
||||
class action_plugin_luxtools extends ActionPlugin
|
||||
{
|
||||
/** @var bool Guard to prevent postprocess appenders during internal renders */
|
||||
protected static $internalRenderInProgress = false;
|
||||
|
||||
/** @inheritdoc */
|
||||
public function register(Doku_Event_Handler $controller)
|
||||
public function register(EventHandler $controller)
|
||||
{
|
||||
$controller->register_hook(
|
||||
"TPL_METAHEADER_OUTPUT",
|
||||
@@ -19,12 +27,48 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$this,
|
||||
"addScripts",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"RENDERER_CONTENT_POSTPROCESS",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"autoLinkChronologicalDates",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"RENDERER_CONTENT_POSTPROCESS",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"appendChronologicalDayEvents",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"RENDERER_CONTENT_POSTPROCESS",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"appendChronologicalDayPhotos",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"COMMON_PAGETPL_LOAD",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"prefillChronologicalDayTemplate",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"TPL_ACT_RENDER",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"renderVirtualChronologicalDayPage",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"CSS_STYLES_INCLUDED",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"addTemporaryInputStyles",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"AJAX_CALL_UNKNOWN",
|
||||
"BEFORE",
|
||||
$this,
|
||||
"handleCalendarWidgetAjax",
|
||||
);
|
||||
$controller->register_hook(
|
||||
"TOOLBAR_DEFINE",
|
||||
"AFTER",
|
||||
@@ -52,6 +96,7 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
"date-fix.js",
|
||||
"page-link.js",
|
||||
"linkfavicon.js",
|
||||
"calendar-widget.js",
|
||||
"main.js",
|
||||
];
|
||||
|
||||
@@ -63,6 +108,46 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve server-rendered calendar widget HTML for month navigation.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function handleCalendarWidgetAjax(Event $event, $param)
|
||||
{
|
||||
if ($event->data !== 'luxtools_calendar_month') return;
|
||||
|
||||
$event->preventDefault();
|
||||
$event->stopPropagation();
|
||||
|
||||
global $INPUT;
|
||||
|
||||
$year = (int)$INPUT->int('year');
|
||||
$month = (int)$INPUT->int('month');
|
||||
$baseNs = trim((string)$INPUT->str('base'));
|
||||
if ($baseNs === '') {
|
||||
$baseNs = 'chronological';
|
||||
}
|
||||
|
||||
if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) {
|
||||
http_status(400);
|
||||
echo 'Invalid month';
|
||||
return;
|
||||
}
|
||||
|
||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs);
|
||||
if ($html === '') {
|
||||
http_status(500);
|
||||
echo 'Calendar rendering failed';
|
||||
return;
|
||||
}
|
||||
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include temporary global input styling via css.php so @ini_* placeholders resolve.
|
||||
*
|
||||
@@ -84,6 +169,378 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$event->data['files'][DOKU_PLUGIN . $plugin . '/temp-input-colors.css'] = DOKU_BASE . 'lib/plugins/' . $plugin . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link strict ISO dates (YYYY-MM-DD) in rendered XHTML text nodes.
|
||||
*
|
||||
* Excludes content inside tags where links should not be altered.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function autoLinkChronologicalDates(Event $event, $param)
|
||||
{
|
||||
if (!is_array($event->data)) return;
|
||||
|
||||
$mode = (string)($event->data[0] ?? '');
|
||||
if ($mode !== 'xhtml') return;
|
||||
|
||||
$doc = $event->data[1] ?? null;
|
||||
if (!is_string($doc) || $doc === '') return;
|
||||
if (!preg_match('/\d{4}-\d{2}-\d{2}/', $doc)) return;
|
||||
|
||||
$event->data[1] = ChronologicalDateAutoLinker::linkHtml($doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill new chronological day pages with a German date headline.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function prefillChronologicalDayTemplate(Event $event, $param)
|
||||
{
|
||||
if (!is_array($event->data)) return;
|
||||
|
||||
$id = (string)($event->data['id'] ?? '');
|
||||
if ($id === '') return;
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$id = (string)cleanID($id);
|
||||
}
|
||||
if ($id === '') return;
|
||||
if (!ChronoID::isDayId($id)) return;
|
||||
|
||||
$template = ChronologicalDayTemplate::buildForDayId($id);
|
||||
if ($template === null || $template === '') return;
|
||||
|
||||
$event->data['tpl'] = $template;
|
||||
$event->data['tplfile'] = '';
|
||||
$event->data['doreplace'] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append matching date-prefixed photos to chronological day page output.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function appendChronologicalDayPhotos(Event $event, $param)
|
||||
{
|
||||
if (self::$internalRenderInProgress) return;
|
||||
if (!is_array($event->data)) return;
|
||||
|
||||
$mode = (string)($event->data[0] ?? '');
|
||||
if ($mode !== 'xhtml') return;
|
||||
|
||||
global $ACT;
|
||||
if (!is_string($ACT) || $ACT !== 'show') return;
|
||||
|
||||
$doc = $event->data[1] ?? null;
|
||||
if (!is_string($doc)) return;
|
||||
if (str_contains($doc, 'luxtools-chronological-photos')) return;
|
||||
|
||||
global $ID;
|
||||
$id = is_string($ID) ? $ID : '';
|
||||
if ($id === '') return;
|
||||
if (function_exists('cleanID')) {
|
||||
$id = (string)cleanID($id);
|
||||
}
|
||||
if ($id === '') return;
|
||||
|
||||
$parts = ChronoID::parseDayId($id);
|
||||
if ($parts === null) return;
|
||||
|
||||
if (!function_exists('page_exists') || !page_exists($id)) return;
|
||||
|
||||
$basePath = trim((string)$this->getConf('image_base_path'));
|
||||
if ($basePath === '') return;
|
||||
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
||||
if (!$this->hasAnyChronologicalPhotos($dateIso)) return;
|
||||
|
||||
$photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
|
||||
if ($photosHtml === '') return;
|
||||
|
||||
$event->data[1] = $doc . $photosHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append local calendar events to existing chronological day pages.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function appendChronologicalDayEvents(Event $event, $param)
|
||||
{
|
||||
static $appendInProgress = false;
|
||||
if ($appendInProgress) return;
|
||||
if (self::$internalRenderInProgress) return;
|
||||
|
||||
if (!is_array($event->data)) return;
|
||||
|
||||
$mode = (string)($event->data[0] ?? '');
|
||||
if ($mode !== 'xhtml') return;
|
||||
|
||||
global $ACT;
|
||||
if (!is_string($ACT) || $ACT !== 'show') return;
|
||||
|
||||
$doc = $event->data[1] ?? null;
|
||||
if (!is_string($doc)) return;
|
||||
if (str_contains($doc, 'luxtools-chronological-events')) return;
|
||||
|
||||
global $ID;
|
||||
$id = is_string($ID) ? $ID : '';
|
||||
if ($id === '') return;
|
||||
if (function_exists('cleanID')) {
|
||||
$id = (string)cleanID($id);
|
||||
}
|
||||
if ($id === '') return;
|
||||
|
||||
$parts = ChronoID::parseDayId($id);
|
||||
if ($parts === null) return;
|
||||
if (!function_exists('page_exists') || !page_exists($id)) return;
|
||||
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
||||
$appendInProgress = true;
|
||||
try {
|
||||
$eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
|
||||
} finally {
|
||||
$appendInProgress = false;
|
||||
}
|
||||
if ($eventsHtml === '') return;
|
||||
|
||||
$event->data[1] = $doc . $eventsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render chronological day photos using existing {{images>...}} syntax.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function renderChronologicalPhotosMacro(string $dateIso): string
|
||||
{
|
||||
$syntax = $this->buildChronologicalImagesSyntax($dateIso);
|
||||
if ($syntax === '') return '';
|
||||
|
||||
if (self::$internalRenderInProgress) return '';
|
||||
self::$internalRenderInProgress = true;
|
||||
|
||||
try {
|
||||
$info = ['cache' => false];
|
||||
$instructions = p_get_instructions($syntax);
|
||||
$galleryHtml = (string)p_render('xhtml', $instructions, $info);
|
||||
} finally {
|
||||
self::$internalRenderInProgress = false;
|
||||
}
|
||||
|
||||
if ($galleryHtml === '') return '';
|
||||
|
||||
$title = (string)$this->getLang('chronological_photos_title');
|
||||
if ($title === '') $title = 'Photos';
|
||||
|
||||
return '<div class="luxtools-plugin luxtools-chronological-photos">'
|
||||
. '<h2>' . hsc($title) . '</h2>'
|
||||
. $galleryHtml
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build {{images>...}} syntax for a given day.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function buildChronologicalImagesSyntax(string $dateIso): string
|
||||
{
|
||||
$basePath = trim((string)$this->getConf('image_base_path'));
|
||||
if ($basePath === '') return '';
|
||||
|
||||
$base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
|
||||
if (!is_dir($base) || !is_readable($base)) return '';
|
||||
|
||||
$yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
|
||||
$targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
|
||||
|
||||
return '{{images>' . $targetDir . $dateIso . '*&recursive=0}}';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a virtual day page for missing chronological day IDs.
|
||||
*
|
||||
* Shows a German date heading and existing day photos (if any) without creating the page.
|
||||
*
|
||||
* @param Event $event
|
||||
* @param mixed $param
|
||||
* @return void
|
||||
*/
|
||||
public function renderVirtualChronologicalDayPage(Event $event, $param)
|
||||
{
|
||||
if (!is_string($event->data) || $event->data !== 'show') return;
|
||||
|
||||
global $ID;
|
||||
$id = is_string($ID) ? $ID : '';
|
||||
if ($id === '') return;
|
||||
if (function_exists('cleanID')) {
|
||||
$id = (string)cleanID($id);
|
||||
}
|
||||
if ($id === '') return;
|
||||
|
||||
if (!ChronoID::isDayId($id)) return;
|
||||
if (function_exists('page_exists') && page_exists($id)) return;
|
||||
|
||||
$wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? '';
|
||||
if ($wikiText === '') return;
|
||||
|
||||
$parts = ChronoID::parseDayId($id);
|
||||
$extraHtml = '';
|
||||
if ($parts !== null) {
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']);
|
||||
|
||||
$eventsHtml = $this->renderChronologicalEventsHtml($dateIso);
|
||||
if ($eventsHtml !== '') {
|
||||
$extraHtml .= $eventsHtml;
|
||||
}
|
||||
|
||||
if ($this->hasAnyChronologicalPhotos($dateIso)) {
|
||||
$photosHtml = $this->renderChronologicalPhotosMacro($dateIso);
|
||||
if ($photosHtml !== '') {
|
||||
$extraHtml .= $photosHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$editUrl = function_exists('wl') ? (string)wl($id, ['do' => 'edit']) : '';
|
||||
$createLinkHtml = '';
|
||||
if ($editUrl !== '') {
|
||||
$label = (string)$this->getLang('btn_create');
|
||||
if ($label === '') $label = 'Create this page';
|
||||
$createLinkHtml = '<p><a href="' . hsc($editUrl) . '">✎ ' . hsc($label) . '</a></p>';
|
||||
}
|
||||
|
||||
$info = ['cache' => false];
|
||||
$instructions = p_get_instructions($wikiText);
|
||||
$html = (string)p_render('xhtml', $instructions, $info);
|
||||
|
||||
echo $html . $createLinkHtml . $extraHtml;
|
||||
$event->preventDefault();
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is at least one date-prefixed image for the given day.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasAnyChronologicalPhotos(string $dateIso): bool
|
||||
{
|
||||
if (!ChronoID::isIsoDate($dateIso)) return false;
|
||||
|
||||
$basePath = trim((string)$this->getConf('image_base_path'));
|
||||
if ($basePath === '') return false;
|
||||
|
||||
$base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath);
|
||||
if (!is_dir($base) || !is_readable($base)) return false;
|
||||
|
||||
$yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/';
|
||||
$targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base;
|
||||
|
||||
$pattern = rtrim($targetDir, '/') . '/' . $dateIso . '*';
|
||||
$matches = glob($pattern) ?: [];
|
||||
foreach ($matches as $match) {
|
||||
if (!is_file($match)) continue;
|
||||
$ext = strtolower(pathinfo($match, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render local calendar events section for a given date.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function renderChronologicalEventsHtml(string $dateIso): string
|
||||
{
|
||||
$icsConfig = (string)$this->getConf('calendar_ics_files');
|
||||
if (trim($icsConfig) === '') return '';
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
|
||||
if ($events === []) return '';
|
||||
|
||||
$title = (string)$this->getLang('chronological_events_title');
|
||||
if ($title === '') $title = 'Events';
|
||||
|
||||
$items = '';
|
||||
foreach ($events as $entry) {
|
||||
$summary = trim((string)($entry['summary'] ?? ''));
|
||||
if ($summary === '') $summary = '(ohne Titel)';
|
||||
|
||||
$time = trim((string)($entry['time'] ?? ''));
|
||||
$startIso = trim((string)($entry['startIso'] ?? ''));
|
||||
$isAllDay = (bool)($entry['allDay'] ?? false);
|
||||
|
||||
if ($isAllDay || $time === '') {
|
||||
$items .= '<li>' . hsc($summary) . '</li>';
|
||||
} else {
|
||||
$timeHtml = '<span class="luxtools-event-time"';
|
||||
if ($startIso !== '') {
|
||||
$timeHtml .= ' data-luxtools-start="' . hsc($startIso) . '"';
|
||||
}
|
||||
$timeHtml .= '>' . hsc($time) . '</span>';
|
||||
$items .= '<li>' . $timeHtml . ' - ' . hsc($summary) . '</li>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($items === '') return '';
|
||||
$html = '<ul>' . $items . '</ul>';
|
||||
|
||||
return '<div class="luxtools-plugin luxtools-chronological-events">'
|
||||
. '<h2>' . hsc($title) . '</h2>'
|
||||
. $html
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build wiki bullet list for local calendar events.
|
||||
*
|
||||
* @param string $dateIso
|
||||
* @return string
|
||||
*/
|
||||
protected function buildChronologicalEventsWiki(string $dateIso): string
|
||||
{
|
||||
$icsConfig = (string)$this->getConf('calendar_ics_files');
|
||||
if (trim($icsConfig) === '') return '';
|
||||
|
||||
$events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso);
|
||||
if ($events === []) return '';
|
||||
|
||||
$lines = [];
|
||||
foreach ($events as $event) {
|
||||
$summary = trim((string)($event['summary'] ?? ''));
|
||||
if ($summary === '') $summary = '(ohne Titel)';
|
||||
$summary = str_replace(["\n", "\r"], ' ', $summary);
|
||||
|
||||
$time = trim((string)($event['time'] ?? ''));
|
||||
if ((bool)($event['allDay'] ?? false) || $time === '') {
|
||||
$lines[] = ' * ' . $summary;
|
||||
} else {
|
||||
$lines[] = ' * ' . $time . ' - ' . $summary;
|
||||
}
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom toolbar button for code blocks.
|
||||
*
|
||||
@@ -120,13 +577,5 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
"icon" => "../../plugins/luxtools/images/date-fix-all.svg",
|
||||
"block" => false,
|
||||
];
|
||||
|
||||
// Page Link: create a page-scoped UUID for .pagelink linking
|
||||
$event->data[] = [
|
||||
"type" => "LuxtoolsPageLink",
|
||||
"title" => $this->getLang("toolbar_pagelink_title"),
|
||||
"icon" => "../../plugins/luxtools/images/pagelink.svg",
|
||||
"block" => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
'thumb_placeholder',
|
||||
'gallery_thumb_scale',
|
||||
'open_service_url',
|
||||
'image_base_path',
|
||||
'calendar_ics_files',
|
||||
'pagelink_search_depth',
|
||||
];
|
||||
|
||||
@@ -86,6 +88,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
$newConf['thumb_placeholder'] = $INPUT->str('thumb_placeholder');
|
||||
$newConf['gallery_thumb_scale'] = $INPUT->str('gallery_thumb_scale');
|
||||
$newConf['open_service_url'] = $INPUT->str('open_service_url');
|
||||
$newConf['image_base_path'] = $INPUT->str('image_base_path');
|
||||
|
||||
$icsFiles = $INPUT->str('calendar_ics_files');
|
||||
$icsFiles = str_replace(["\r\n", "\r"], "\n", $icsFiles);
|
||||
$newConf['calendar_ics_files'] = $icsFiles;
|
||||
|
||||
$depth = (int)$INPUT->int('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
@@ -228,6 +235,17 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
echo '<input type="text" class="edit" name="open_service_url" value="' . hsc((string)$this->getConf('open_service_url')) . '" />';
|
||||
echo '</label><br />';
|
||||
|
||||
// image_base_path
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('image_base_path')) . '</span> ';
|
||||
echo '<input type="text" class="edit" name="image_base_path" value="' . hsc((string)$this->getConf('image_base_path')) . '" />';
|
||||
echo '</label><br />';
|
||||
|
||||
// calendar_ics_files
|
||||
$icsFiles = $this->normalizeMultilineDisplay((string)$this->getConf('calendar_ics_files'), 'calendar_ics_files');
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('calendar_ics_files')) . '</span><br />';
|
||||
echo '<textarea name="calendar_ics_files" rows="4" cols="80" class="edit">' . hsc($icsFiles) . '</textarea>';
|
||||
echo '</label><br />';
|
||||
|
||||
// pagelink_search_depth
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('pagelink_search_depth')) . '</span> ';
|
||||
echo '<input type="number" class="edit" min="0" name="pagelink_search_depth" value="' . hsc((string)$this->getConf('pagelink_search_depth')) . '" />';
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
* This file registers a minimal autoloader for the plugin namespace.
|
||||
*/
|
||||
|
||||
$composerAutoload = __DIR__ . '/vendor/autoload.php';
|
||||
if (is_file($composerAutoload)) {
|
||||
require_once $composerAutoload;
|
||||
}
|
||||
|
||||
spl_autoload_register(static function ($class) {
|
||||
$prefix = 'dokuwiki\\plugin\\luxtools\\';
|
||||
$prefixLen = strlen($prefix);
|
||||
|
||||
5
composer.json
Normal file
5
composer.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"require": {
|
||||
"sabre/vobject": "^4.5"
|
||||
}
|
||||
}
|
||||
252
composer.lock
generated
Normal file
252
composer.lock
generated
Normal file
@@ -0,0 +1,252 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "440454aa6bd2975652e94f60998e9adc",
|
||||
"packages": [
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
"version": "3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/uri.git",
|
||||
"reference": "38eeab6ed9eec435a2188db489d4649c56272c51"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/uri/zipball/38eeab6ed9eec435a2188db489d4649c56272c51",
|
||||
"reference": "38eeab6ed9eec435a2188db489d4649c56272c51",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Uri\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Functions for making sense out of URIs.",
|
||||
"homepage": "http://sabre.io/uri/",
|
||||
"keywords": [
|
||||
"rfc3986",
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/uri/issues",
|
||||
"source": "https://github.com/fruux/sabre-uri"
|
||||
},
|
||||
"time": "2024-09-04T15:30:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
|
||||
"phpunit/php-invoker": "^2.0 || ^3.1",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest": {
|
||||
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||
},
|
||||
"bin": [
|
||||
"bin/vobject",
|
||||
"bin/generate_vcards"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabre\\VObject\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Dominik Tobschall",
|
||||
"email": "dominik@fruux.com",
|
||||
"homepage": "http://tobschall.de/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Ivan Enderlin",
|
||||
"email": "ivan.enderlin@hoa-project.net",
|
||||
"homepage": "http://mnt.io/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||
"homepage": "http://sabre.io/vobject/",
|
||||
"keywords": [
|
||||
"availability",
|
||||
"freebusy",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"jCal",
|
||||
"jCard",
|
||||
"recurrence",
|
||||
"rfc2425",
|
||||
"rfc2426",
|
||||
"rfc2739",
|
||||
"rfc4770",
|
||||
"rfc5545",
|
||||
"rfc5546",
|
||||
"rfc6321",
|
||||
"rfc6350",
|
||||
"rfc6351",
|
||||
"rfc6474",
|
||||
"rfc6638",
|
||||
"rfc6715",
|
||||
"rfc6868",
|
||||
"vCalendar",
|
||||
"vCard",
|
||||
"vcf",
|
||||
"xCal",
|
||||
"xCard"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||
"source": "https://github.com/fruux/sabre-vobject"
|
||||
},
|
||||
"time": "2026-01-12T10:45:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabre/xml",
|
||||
"version": "4.0.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/xml.git",
|
||||
"reference": "a89257fd188ce30e456b841b6915f27905dfdbe3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/xml/zipball/a89257fd188ce30e456b841b6915f27905dfdbe3",
|
||||
"reference": "a89257fd188ce30e456b841b6915f27905dfdbe3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"lib-libxml": ">=2.6.20",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"sabre/uri": ">=2.0,<4.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Deserializer/functions.php",
|
||||
"lib/Serializer/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Xml\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab",
|
||||
"email": "markus.staab@redaxo.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "sabre/xml is an XML library that you may not hate.",
|
||||
"homepage": "https://sabre.io/xml/",
|
||||
"keywords": [
|
||||
"XMLReader",
|
||||
"XMLWriter",
|
||||
"dom",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/xml/issues",
|
||||
"source": "https://github.com/fruux/sabre-xml"
|
||||
},
|
||||
"time": "2024-09-06T08:00:55+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
@@ -34,6 +34,12 @@ $conf['gallery_thumb_scale'] = 1;
|
||||
// Local client service used by {{open>...}}.
|
||||
$conf['open_service_url'] = 'http://127.0.0.1:8765';
|
||||
|
||||
// Base filesystem path for chronological photo integration.
|
||||
$conf['image_base_path'] = '';
|
||||
|
||||
// Local calendar ICS files (one absolute file path per line).
|
||||
$conf['calendar_ics_files'] = '';
|
||||
|
||||
// Maximum depth when searching for .pagelink files under allowed roots.
|
||||
$conf['pagelink_search_depth'] = 3;
|
||||
|
||||
|
||||
129
js/calendar-widget.js
Normal file
129
js/calendar-widget.js
Normal file
@@ -0,0 +1,129 @@
|
||||
/* global window, document, fetch, URLSearchParams */
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var Luxtools = window.Luxtools || (window.Luxtools = {});
|
||||
|
||||
function findCalendarRoot(target) {
|
||||
var el = target;
|
||||
while (el && el !== document) {
|
||||
if (el.classList && el.classList.contains('luxtools-calendar') && el.getAttribute('data-luxtools-calendar') === '1') {
|
||||
return el;
|
||||
}
|
||||
el = el.parentNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNextMonth(year, month, direction) {
|
||||
var cursor = new Date(year, month - 1, 1);
|
||||
cursor.setMonth(cursor.getMonth() + direction);
|
||||
return {
|
||||
year: cursor.getFullYear(),
|
||||
month: cursor.getMonth() + 1
|
||||
};
|
||||
}
|
||||
|
||||
function parseCalendarFromHtml(html) {
|
||||
if (!html) return null;
|
||||
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
|
||||
return wrapper.querySelector('div.luxtools-calendar[data-luxtools-calendar="1"]');
|
||||
}
|
||||
|
||||
function setCalendarBusy(calendar, busy) {
|
||||
if (!calendar) return;
|
||||
|
||||
if (busy) {
|
||||
calendar.setAttribute('data-luxtools-loading', '1');
|
||||
} else {
|
||||
calendar.removeAttribute('data-luxtools-loading');
|
||||
}
|
||||
|
||||
var buttons = calendar.querySelectorAll('button.luxtools-calendar-nav-button');
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
buttons[i].disabled = !!busy;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchCalendarMonth(calendar, year, month) {
|
||||
var ajaxUrl = calendar.getAttribute('data-luxtools-ajax-url') || '';
|
||||
if (!ajaxUrl) return Promise.reject(new Error('Missing calendar ajax url'));
|
||||
|
||||
var baseNs = calendar.getAttribute('data-base-ns') || 'chronological';
|
||||
var params = new URLSearchParams({
|
||||
call: 'luxtools_calendar_month',
|
||||
year: String(year),
|
||||
month: String(month),
|
||||
base: baseNs
|
||||
});
|
||||
|
||||
var url = ajaxUrl + (ajaxUrl.indexOf('?') >= 0 ? '&' : '?') + params.toString();
|
||||
|
||||
return fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Calendar request failed: ' + response.status);
|
||||
}
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
function navigateCalendarMonth(calendar, direction) {
|
||||
var year = parseInt(calendar.getAttribute('data-current-year') || '', 10);
|
||||
var month = parseInt(calendar.getAttribute('data-current-month') || '', 10);
|
||||
if (!year || !month) return;
|
||||
|
||||
var next = getNextMonth(year, month, direction);
|
||||
setCalendarBusy(calendar, true);
|
||||
|
||||
fetchCalendarMonth(calendar, next.year, next.month)
|
||||
.then(function (html) {
|
||||
var replacement = parseCalendarFromHtml(html);
|
||||
if (!replacement) {
|
||||
throw new Error('Calendar markup missing in response');
|
||||
}
|
||||
|
||||
calendar.replaceWith(replacement);
|
||||
})
|
||||
.catch(function () {
|
||||
var fallbackLink = calendar.querySelector('a.luxtools-calendar-month-link');
|
||||
if (fallbackLink && fallbackLink.href) {
|
||||
window.location.href = fallbackLink.href;
|
||||
}
|
||||
})
|
||||
.finally(function () {
|
||||
setCalendarBusy(calendar, false);
|
||||
});
|
||||
}
|
||||
|
||||
function onCalendarClick(event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.classList || !target.classList.contains('luxtools-calendar-nav-button')) return;
|
||||
|
||||
var calendar = findCalendarRoot(target);
|
||||
if (!calendar) return;
|
||||
|
||||
var direction = parseInt(target.getAttribute('data-luxtools-dir') || '0', 10);
|
||||
if (direction !== -1 && direction !== 1) return;
|
||||
|
||||
event.preventDefault();
|
||||
navigateCalendarMonth(calendar, direction);
|
||||
}
|
||||
|
||||
function initCalendarWidgets() {
|
||||
document.addEventListener('click', onCalendarClick, false);
|
||||
}
|
||||
|
||||
Luxtools.CalendarWidget = {
|
||||
init: initCalendarWidgets
|
||||
};
|
||||
})();
|
||||
38
js/main.js
38
js/main.js
@@ -8,6 +8,7 @@
|
||||
var OpenService = Luxtools.OpenService;
|
||||
var GalleryThumbnails = Luxtools.GalleryThumbnails;
|
||||
var Scratchpads = Luxtools.Scratchpads;
|
||||
var CalendarWidget = Luxtools.CalendarWidget;
|
||||
|
||||
// ============================================================
|
||||
// Click Handlers
|
||||
@@ -79,12 +80,49 @@
|
||||
});
|
||||
}
|
||||
|
||||
function initChronologicalEventTimes() {
|
||||
var nodes = document.querySelectorAll('.luxtools-event-time[data-luxtools-start]');
|
||||
if (!nodes || nodes.length === 0) return;
|
||||
|
||||
var formatter;
|
||||
try {
|
||||
formatter = new Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
formatter = null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var node = nodes[i];
|
||||
var raw = node.getAttribute('data-luxtools-start') || '';
|
||||
if (!raw) continue;
|
||||
|
||||
var date = new Date(raw);
|
||||
if (isNaN(date.getTime())) continue;
|
||||
|
||||
var label;
|
||||
if (formatter) {
|
||||
label = formatter.format(date);
|
||||
} else {
|
||||
var hh = String(date.getHours()).padStart(2, '0');
|
||||
var mm = String(date.getMinutes()).padStart(2, '0');
|
||||
label = hh + ':' + mm;
|
||||
}
|
||||
|
||||
node.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Initialize
|
||||
// ============================================================
|
||||
document.addEventListener('click', onClick, false);
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (GalleryThumbnails && GalleryThumbnails.init) GalleryThumbnails.init();
|
||||
initChronologicalEventTimes();
|
||||
if (CalendarWidget && CalendarWidget.init) CalendarWidget.init();
|
||||
}, false);
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (Scratchpads && Scratchpads.init) Scratchpads.init();
|
||||
|
||||
173
js/page-link.js
173
js/page-link.js
@@ -43,7 +43,7 @@
|
||||
|
||||
function requestPageLink(cmd, params) {
|
||||
var pageId = getPageId();
|
||||
if (!pageId) return false;
|
||||
if (!pageId) return Promise.reject(new Error('missing page id'));
|
||||
|
||||
var endpoint = getBaseUrl() + 'lib/plugins/luxtools/pagelink.php';
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
window.fetch(endpoint, {
|
||||
return window.fetch(endpoint, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
@@ -68,13 +68,7 @@
|
||||
}
|
||||
return body;
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn('PageLink request failed:', err);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function ensurePageLink() {
|
||||
@@ -85,85 +79,102 @@
|
||||
return requestPageLink('unlink', { sectok: getSectok() });
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (!text) return;
|
||||
if (window.navigator && window.navigator.clipboard && window.navigator.clipboard.writeText) {
|
||||
window.navigator.clipboard.writeText(text).catch(function () {});
|
||||
return;
|
||||
}
|
||||
|
||||
function triggerDownload(pageId) {
|
||||
try {
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.setAttribute('readonly', 'readonly');
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (e) {}
|
||||
document.body.removeChild(textarea);
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
function attachCopyTargets() {
|
||||
var targets = document.querySelectorAll('[data-luxtools-pagelink-copy="1"]');
|
||||
if (!targets || !targets.length) return;
|
||||
|
||||
targets.forEach(function (el) {
|
||||
if (!el || !el.getAttribute) return;
|
||||
el.setAttribute('role', 'button');
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
copyToClipboard(String(el.getAttribute('data-uuid') || '').trim());
|
||||
});
|
||||
el.addEventListener('keydown', function (e) {
|
||||
if (!e || (e.key !== 'Enter' && e.key !== ' ')) return;
|
||||
e.preventDefault();
|
||||
copyToClipboard(String(el.getAttribute('data-uuid') || '').trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addBtnActionLuxtoolsPageLink = function ($btn, props, edid) {
|
||||
$btn.on('click', function () {
|
||||
var pageId = getPageId();
|
||||
if (!pageId) return false;
|
||||
|
||||
var endpoint = getBaseUrl() + 'lib/plugins/luxtools/pagelink.php';
|
||||
var query = endpoint + '?cmd=info&id=' + encodeURIComponent(pageId);
|
||||
var href = endpoint + '?cmd=download&id=' + encodeURIComponent(pageId);
|
||||
|
||||
window.fetch(query, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
}).then(function (res) {
|
||||
return res.json().catch(function () { return null; }).then(function (body) {
|
||||
if (!res.ok || !body || body.ok !== true) {
|
||||
throw new Error('request failed');
|
||||
}
|
||||
return body;
|
||||
});
|
||||
}).then(function (info) {
|
||||
if (info && info.uuid) {
|
||||
if (window.confirm('Unlink page?')) {
|
||||
unlinkPageLink();
|
||||
}
|
||||
return;
|
||||
}
|
||||
var a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.download = '.pagelink';
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
ensurePageLink();
|
||||
}).catch(function (err) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn('PageLink info failed:', err);
|
||||
function fetchPageLinkInfo(pageId) {
|
||||
if (!pageId) return Promise.reject(new Error('missing page id'));
|
||||
|
||||
var endpoint = getBaseUrl() + 'lib/plugins/luxtools/pagelink.php';
|
||||
var query = endpoint + '?cmd=info&id=' + encodeURIComponent(pageId);
|
||||
|
||||
return window.fetch(query, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin'
|
||||
}).then(function (res) {
|
||||
return res.json().catch(function () { return null; }).then(function (body) {
|
||||
if (!res.ok || !body || body.ok !== true) {
|
||||
throw new Error('request failed');
|
||||
}
|
||||
ensurePageLink();
|
||||
return body;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
return 'luxtools-pagelink';
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', attachCopyTargets, false);
|
||||
function attachDocInfoLink() {
|
||||
var container = document.querySelector('.docInfo');
|
||||
if (!container || !container.getAttribute) return;
|
||||
if (container.getAttribute('data-luxtools-pagelink-docinfo') === '1') return;
|
||||
container.setAttribute('data-luxtools-pagelink-docinfo', '1');
|
||||
|
||||
var pageId = getPageId();
|
||||
if (!pageId) return;
|
||||
|
||||
fetchPageLinkInfo(pageId).then(function (info) {
|
||||
var link = document.createElement('a');
|
||||
link.href = '#';
|
||||
|
||||
if (!info || !info.uuid) {
|
||||
link.textContent = 'Link Page';
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
ensurePageLink().then(function (res) {
|
||||
if (!res || !res.uuid) throw new Error('no uuid');
|
||||
triggerDownload(pageId);
|
||||
}).catch(function (err) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn('PageLink ensure failed:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (info.linked) {
|
||||
link.textContent = 'Unlink Page';
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (!window.confirm('Unlink page?')) return;
|
||||
unlinkPageLink().then(function () {
|
||||
window.setTimeout(function () {
|
||||
try { window.location.reload(); } catch (e2) {}
|
||||
}, 400);
|
||||
}).catch(function (err) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn('PageLink unlink failed:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
link.textContent = 'Download Link File';
|
||||
link.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
triggerDownload(pageId);
|
||||
});
|
||||
}
|
||||
|
||||
var first = container.firstChild;
|
||||
container.insertBefore(link, first);
|
||||
if (first) {
|
||||
container.insertBefore(document.createTextNode(' · '), first);
|
||||
}
|
||||
}).catch(function () {
|
||||
// ignore failures
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
attachDocInfoLink();
|
||||
}, false);
|
||||
})();
|
||||
|
||||
@@ -62,6 +62,10 @@ $lang["gallery_thumb_scale"] =
|
||||
"Skalierungsfaktor für Galerie-Thumbnails. 2 erzeugt schärfere Thumbnails auf HiDPI-Displays (Anzeige bleibt 150×150).";
|
||||
$lang["open_service_url"] =
|
||||
"URL des lokalen Client-Dienstes für {{open>...}} (z.B. http://127.0.0.1:8765).";
|
||||
$lang["image_base_path"] =
|
||||
"Basis-Dateisystempfad für die chronologische Foto-Integration.";
|
||||
$lang["calendar_ics_files"] =
|
||||
"Lokale Kalender-.ics-Dateien (ein absoluter Dateipfad pro Zeile).";
|
||||
$lang["pagelink_search_depth"] =
|
||||
"Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root).";
|
||||
|
||||
@@ -77,6 +81,7 @@ $lang["toolbar_code_title"] = "Code-Block";
|
||||
$lang["toolbar_code_sample"] = "Ihr Code hier";
|
||||
$lang["toolbar_datefix_title"] = "Datums-Fix";
|
||||
$lang["toolbar_datefix_all_title"] = "Datums-Fix (Alle)";
|
||||
$lang["toolbar_pagelink_title"] = "Seiten-Link";
|
||||
$lang["pagelink_unlinked"] = "Seite nicht verknüpft (ID kopieren)";
|
||||
$lang["pagelink_unlinked"] = "Seite nicht verknüpft";
|
||||
$lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft";
|
||||
$lang["chronological_photos_title"] = "Fotos";
|
||||
$lang["chronological_events_title"] = "Termine";
|
||||
|
||||
@@ -62,6 +62,10 @@ $lang["gallery_thumb_scale"] =
|
||||
"Gallery thumbnail scale factor. Use 2 for sharper thumbnails on HiDPI screens (still displayed as 150×150).";
|
||||
$lang["open_service_url"] =
|
||||
"Local client service URL for the {{open>...}} button (e.g. http://127.0.0.1:8765).";
|
||||
$lang["image_base_path"] =
|
||||
"Base filesystem path for chronological photo integration.";
|
||||
$lang["calendar_ics_files"] =
|
||||
"Local calendar .ics files (one absolute file path per line).";
|
||||
$lang["pagelink_search_depth"] =
|
||||
"Maximum directory depth for .pagelink search (0 = only root).";
|
||||
|
||||
@@ -77,6 +81,8 @@ $lang["toolbar_code_title"] = "Code Block";
|
||||
$lang["toolbar_code_sample"] = "your code here";
|
||||
$lang["toolbar_datefix_title"] = "Date Fix";
|
||||
$lang["toolbar_datefix_all_title"] = "Date Fix (All)";
|
||||
$lang["toolbar_pagelink_title"] = "Page Link";
|
||||
$lang["pagelink_unlinked"] = "Page not linked (Copy ID)";
|
||||
$lang["pagelink_unlinked"] = "Page not linked";
|
||||
$lang["pagelink_multi_warning"] = "Multiple folders linked";
|
||||
$lang["calendar_err_badmonth"] = "Invalid calendar month. Use YYYY-MM.";
|
||||
$lang["chronological_photos_title"] = "Photos";
|
||||
$lang["chronological_events_title"] = "Events";
|
||||
|
||||
37
pagelink.php
37
pagelink.php
@@ -46,12 +46,19 @@ if ($cmd === '' || $pageId === '') {
|
||||
luxtools_pagelink_json(400, ['ok' => false, 'error' => 'missing parameters']);
|
||||
}
|
||||
|
||||
if (!function_exists('auth_quickaclcheck') || !defined('AUTH_EDIT')) {
|
||||
if (!function_exists('auth_quickaclcheck')) {
|
||||
luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']);
|
||||
}
|
||||
|
||||
if (auth_quickaclcheck($pageId) < AUTH_EDIT) {
|
||||
luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']);
|
||||
$acl = auth_quickaclcheck($pageId);
|
||||
if ($cmd === 'info' || $cmd === 'download') {
|
||||
if (!defined('AUTH_READ') || $acl < AUTH_READ) {
|
||||
luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']);
|
||||
}
|
||||
} else {
|
||||
if (!defined('AUTH_EDIT') || $acl < AUTH_EDIT) {
|
||||
luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cmd === 'info') {
|
||||
@@ -83,6 +90,30 @@ if ($cmd === 'info') {
|
||||
]);
|
||||
}
|
||||
|
||||
if ($cmd === 'download') {
|
||||
$depth = (int)$syntax->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$syntax->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
if ($uuid === null || $uuid === '') {
|
||||
http_status(404);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
echo 'not linked';
|
||||
exit;
|
||||
}
|
||||
|
||||
http_status(200);
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename=".pagelink"; filename*=UTF-8\'\'%2Epagelink');
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
echo $uuid;
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($cmd === 'ensure') {
|
||||
if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
luxtools_pagelink_json(405, ['ok' => false, 'error' => 'method not allowed']);
|
||||
|
||||
241
src/ChronoID.php
Normal file
241
src/ChronoID.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
/**
|
||||
* Helper for canonical chronological page IDs.
|
||||
*
|
||||
* Canonical structure:
|
||||
* - Day: baseNs:YYYY:MM:DD
|
||||
* - Month: baseNs:YYYY:MM
|
||||
* - Year: baseNs:YYYY
|
||||
*
|
||||
* Date input format:
|
||||
* - Strict ISO date only: YYYY-MM-DD
|
||||
*/
|
||||
class ChronoID
|
||||
{
|
||||
/**
|
||||
* Check if a string is a strict ISO date and a valid Gregorian date.
|
||||
*
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
public static function isIsoDate(string $value): bool
|
||||
{
|
||||
return self::parseIsoDate($value) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert YYYY-MM-DD to canonical day page ID.
|
||||
*
|
||||
* @param string $value Date in strict YYYY-MM-DD format
|
||||
* @param string $baseNs Base namespace, default chronological
|
||||
* @return string|null Canonical day ID or null on invalid input
|
||||
*/
|
||||
public static function dateToDayId(string $value, string $baseNs = 'chronological'): ?string
|
||||
{
|
||||
$parts = self::parseIsoDate($value);
|
||||
if ($parts === null) return null;
|
||||
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
[$year, $month, $day] = $parts;
|
||||
return sprintf('%s:%04d:%02d:%02d', $ns, $year, $month, $day);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a page ID is a canonical day ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return bool
|
||||
*/
|
||||
public static function isDayId(string $id, string $baseNs = 'chronological'): bool
|
||||
{
|
||||
return self::parseDayId($id, $baseNs) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a page ID is a canonical month ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return bool
|
||||
*/
|
||||
public static function isMonthId(string $id, string $baseNs = 'chronological'): bool
|
||||
{
|
||||
return self::parseMonthId($id, $baseNs) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a page ID is a canonical year ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return bool
|
||||
*/
|
||||
public static function isYearId(string $id, string $baseNs = 'chronological'): bool
|
||||
{
|
||||
return self::parseYearId($id, $baseNs) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert canonical day ID to canonical month ID.
|
||||
*
|
||||
* @param string $dayId
|
||||
* @param string $baseNs
|
||||
* @return string|null
|
||||
*/
|
||||
public static function dayIdToMonthId(string $dayId, string $baseNs = 'chronological'): ?string
|
||||
{
|
||||
$parts = self::parseDayId($dayId, $baseNs);
|
||||
if ($parts === null) return null;
|
||||
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
return sprintf('%s:%04d:%02d', $ns, $parts['year'], $parts['month']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert canonical month ID to canonical year ID.
|
||||
*
|
||||
* @param string $monthId
|
||||
* @param string $baseNs
|
||||
* @return string|null
|
||||
*/
|
||||
public static function monthIdToYearId(string $monthId, string $baseNs = 'chronological'): ?string
|
||||
{
|
||||
$parts = self::parseMonthId($monthId, $baseNs);
|
||||
if ($parts === null) return null;
|
||||
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
return sprintf('%s:%04d', $ns, $parts['year']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse canonical day ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return array{year:int,month:int,day:int}|null
|
||||
*/
|
||||
public static function parseDayId(string $id, string $baseNs = 'chronological'): ?array
|
||||
{
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
$id = trim($id);
|
||||
$pattern = '/^' . preg_quote($ns, '/') . ':(\d{4}):(\d{2}):(\d{2})$/';
|
||||
if (!preg_match($pattern, $id, $matches)) return null;
|
||||
|
||||
$year = (int)$matches[1];
|
||||
$month = (int)$matches[2];
|
||||
$day = (int)$matches[3];
|
||||
if ($year < 1) return null;
|
||||
if (!checkdate($month, $day, $year)) return null;
|
||||
|
||||
return [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse canonical month ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return array{year:int,month:int}|null
|
||||
*/
|
||||
public static function parseMonthId(string $id, string $baseNs = 'chronological'): ?array
|
||||
{
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
$id = trim($id);
|
||||
$pattern = '/^' . preg_quote($ns, '/') . ':(\d{4}):(\d{2})$/';
|
||||
if (!preg_match($pattern, $id, $matches)) return null;
|
||||
|
||||
$year = (int)$matches[1];
|
||||
$month = (int)$matches[2];
|
||||
if ($year < 1) return null;
|
||||
if ($month < 1 || $month > 12) return null;
|
||||
|
||||
return [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse canonical year ID.
|
||||
*
|
||||
* @param string $id
|
||||
* @param string $baseNs
|
||||
* @return array{year:int}|null
|
||||
*/
|
||||
public static function parseYearId(string $id, string $baseNs = 'chronological'): ?array
|
||||
{
|
||||
$ns = self::normalizeBaseNs($baseNs);
|
||||
if ($ns === null) return null;
|
||||
|
||||
$id = trim($id);
|
||||
$pattern = '/^' . preg_quote($ns, '/') . ':(\d{4})$/';
|
||||
if (!preg_match($pattern, $id, $matches)) return null;
|
||||
|
||||
$year = (int)$matches[1];
|
||||
if ($year < 1) return null;
|
||||
|
||||
return ['year' => $year];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse strict ISO date YYYY-MM-DD.
|
||||
*
|
||||
* @param string $value
|
||||
* @return int[]|null [year, month, day] or null
|
||||
*/
|
||||
protected static function parseIsoDate(string $value): ?array
|
||||
{
|
||||
$value = trim($value);
|
||||
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$year = (int)$matches[1];
|
||||
$month = (int)$matches[2];
|
||||
$day = (int)$matches[3];
|
||||
|
||||
if ($year < 1) return null;
|
||||
if (!checkdate($month, $day, $year)) return null;
|
||||
|
||||
return [$year, $month, $day];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize and validate base namespace.
|
||||
*
|
||||
* Allows one or more namespace segments with characters [a-z0-9_-].
|
||||
*
|
||||
* @param string $baseNs
|
||||
* @return string|null
|
||||
*/
|
||||
protected static function normalizeBaseNs(string $baseNs): ?string
|
||||
{
|
||||
$baseNs = strtolower(trim($baseNs));
|
||||
$baseNs = trim($baseNs, ':');
|
||||
if ($baseNs === '') return null;
|
||||
|
||||
if (!preg_match('/^[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)*$/', $baseNs)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $baseNs;
|
||||
}
|
||||
}
|
||||
143
src/ChronologicalCalendarWidget.php
Normal file
143
src/ChronologicalCalendarWidget.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
/**
|
||||
* Render the chronological calendar widget markup.
|
||||
*/
|
||||
class ChronologicalCalendarWidget
|
||||
{
|
||||
/**
|
||||
* Render full calendar widget HTML for one month.
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @param string $baseNs
|
||||
* @return string
|
||||
*/
|
||||
public static function render(int $year, int $month, string $baseNs = 'chronological'): string
|
||||
{
|
||||
if (!self::isValidMonth($year, $month)) return '';
|
||||
|
||||
$firstDayTs = mktime(0, 0, 0, $month, 1, $year);
|
||||
$daysInMonth = (int)date('t', $firstDayTs);
|
||||
$firstWeekday = (int)date('N', $firstDayTs); // 1..7 (Mon..Sun)
|
||||
|
||||
$monthCursor = \DateTimeImmutable::createFromFormat('!Y-n-j', sprintf('%04d-%d-1', $year, $month));
|
||||
if (!($monthCursor instanceof \DateTimeImmutable)) return '';
|
||||
|
||||
$prevMonth = $monthCursor->sub(new \DateInterval('P1M'));
|
||||
$nextMonth = $monthCursor->add(new \DateInterval('P1M'));
|
||||
|
||||
$monthStartDate = sprintf('%04d-%02d-01', $year, $month);
|
||||
$monthDayId = ChronoID::dateToDayId($monthStartDate, $baseNs);
|
||||
$monthId = $monthDayId !== null ? ChronoID::dayIdToMonthId($monthDayId, $baseNs) : null;
|
||||
$yearId = $monthId !== null ? ChronoID::monthIdToYearId($monthId, $baseNs) : null;
|
||||
|
||||
$prevStartDate = $prevMonth->format('Y-m-d');
|
||||
$prevDayId = ChronoID::dateToDayId($prevStartDate, $baseNs);
|
||||
$prevMonthId = $prevDayId !== null ? ChronoID::dayIdToMonthId($prevDayId, $baseNs) : null;
|
||||
|
||||
$nextStartDate = $nextMonth->format('Y-m-d');
|
||||
$nextDayId = ChronoID::dateToDayId($nextStartDate, $baseNs);
|
||||
$nextMonthId = $nextDayId !== null ? ChronoID::dayIdToMonthId($nextDayId, $baseNs) : null;
|
||||
|
||||
$leadingEmpty = $firstWeekday - 1;
|
||||
$totalCells = (int)ceil(($leadingEmpty + $daysInMonth) / 7) * 7;
|
||||
|
||||
$todayY = (int)date('Y');
|
||||
$todayM = (int)date('m');
|
||||
$todayD = (int)date('d');
|
||||
|
||||
$dayUrlTemplate = function_exists('wl') ? (string)wl('__LUXTOOLS_ID_RAW__') : '';
|
||||
$monthUrlTemplate = $dayUrlTemplate;
|
||||
$yearUrlTemplate = $dayUrlTemplate;
|
||||
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
|
||||
|
||||
$html = '<div class="luxtools-plugin luxtools-calendar" data-luxtools-calendar="1"'
|
||||
. ' data-base-ns="' . hsc($baseNs) . '"'
|
||||
. ' data-current-year="' . hsc((string)$year) . '"'
|
||||
. ' data-current-month="' . hsc(sprintf('%02d', $month)) . '"'
|
||||
. ' data-day-url-template="' . hsc($dayUrlTemplate) . '"'
|
||||
. ' data-month-url-template="' . hsc($monthUrlTemplate) . '"'
|
||||
. ' data-year-url-template="' . hsc($yearUrlTemplate) . '"'
|
||||
. ' data-luxtools-ajax-url="' . hsc($ajaxUrl) . '"'
|
||||
. ' data-prev-month-id="' . hsc((string)$prevMonthId) . '"'
|
||||
. ' data-next-month-id="' . hsc((string)$nextMonthId) . '"'
|
||||
. '>';
|
||||
|
||||
$html .= '<div class="luxtools-calendar-nav">';
|
||||
$html .= '<div class="luxtools-calendar-nav-prev">';
|
||||
$html .= '<button type="button" class="luxtools-calendar-nav-button" data-luxtools-dir="-1" aria-label="Previous month">◀</button>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="luxtools-calendar-title">';
|
||||
$monthLabel = date('F', $firstDayTs);
|
||||
if ($monthId !== null && function_exists('wl')) {
|
||||
$html .= '<a class="luxtools-calendar-month-link" href="' . hsc((string)wl($monthId)) . '">' . hsc($monthLabel) . '</a>';
|
||||
} else {
|
||||
$html .= hsc($monthLabel);
|
||||
}
|
||||
$html .= ' ';
|
||||
if ($yearId !== null && function_exists('wl')) {
|
||||
$html .= '<a class="luxtools-calendar-year-link" href="' . hsc((string)wl($yearId)) . '">' . hsc((string)$year) . '</a>';
|
||||
} else {
|
||||
$html .= hsc((string)$year);
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="luxtools-calendar-nav-next">';
|
||||
$html .= '<button type="button" class="luxtools-calendar-nav-button" data-luxtools-dir="1" aria-label="Next month">▶</button>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<table class="luxtools-calendar-table">';
|
||||
$html .= '<thead><tr>';
|
||||
foreach (['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] as $weekday) {
|
||||
$html .= '<th scope="col">' . hsc($weekday) . '</th>';
|
||||
}
|
||||
$html .= '</tr></thead><tbody>';
|
||||
|
||||
for ($cell = 0; $cell < $totalCells; $cell++) {
|
||||
if ($cell % 7 === 0) $html .= '<tr>';
|
||||
|
||||
$dayNumber = $cell - $leadingEmpty + 1;
|
||||
if ($dayNumber < 1 || $dayNumber > $daysInMonth) {
|
||||
$html .= '<td class="luxtools-calendar-day luxtools-calendar-day-empty"></td>';
|
||||
} else {
|
||||
$date = sprintf('%04d-%02d-%02d', $year, $month, $dayNumber);
|
||||
$dayId = ChronoID::dateToDayId($date, $baseNs);
|
||||
|
||||
$classes = 'luxtools-calendar-day';
|
||||
if ($year === $todayY && $month === $todayM && $dayNumber === $todayD) {
|
||||
$classes .= ' luxtools-calendar-day-today';
|
||||
}
|
||||
|
||||
$html .= '<td class="' . hsc($classes) . '">';
|
||||
if ($dayId !== null && function_exists('html_wikilink')) {
|
||||
$html .= (string)html_wikilink($dayId, (string)$dayNumber);
|
||||
} else {
|
||||
$html .= hsc((string)$dayNumber);
|
||||
}
|
||||
$html .= '</td>';
|
||||
}
|
||||
|
||||
if ($cell % 7 === 6) $html .= '</tr>';
|
||||
}
|
||||
|
||||
$html .= '</tbody></table></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return bool
|
||||
*/
|
||||
public static function isValidMonth(int $year, int $month): bool
|
||||
{
|
||||
if ($year < 1) return false;
|
||||
if ($month < 1 || $month > 12) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
134
src/ChronologicalDateAutoLinker.php
Normal file
134
src/ChronologicalDateAutoLinker.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
/**
|
||||
* Auto-links strict ISO dates in rendered XHTML fragments.
|
||||
*/
|
||||
class ChronologicalDateAutoLinker
|
||||
{
|
||||
/** @var string[] Tags where auto-linking must be disabled */
|
||||
protected static $blockedTags = [
|
||||
'a',
|
||||
'code',
|
||||
'pre',
|
||||
'script',
|
||||
'style',
|
||||
'textarea',
|
||||
];
|
||||
|
||||
/**
|
||||
* Link valid ISO dates in HTML text nodes while skipping blocked tags.
|
||||
*
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
public static function linkHtml(string $html): string
|
||||
{
|
||||
$parts = preg_split('/(<[^>]+>)/u', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
if (!is_array($parts)) return $html;
|
||||
|
||||
$blocked = [];
|
||||
foreach (self::$blockedTags as $tag) {
|
||||
$blocked[$tag] = 0;
|
||||
}
|
||||
|
||||
$out = '';
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '') {
|
||||
$out .= $part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($part, '<')) {
|
||||
self::updateBlockedTagCounters($part, $blocked);
|
||||
$out .= $part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (self::isBlockedContext($blocked)) {
|
||||
$out .= $part;
|
||||
continue;
|
||||
}
|
||||
|
||||
$out .= self::linkText($part);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link strict ISO dates in plain text.
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected static function linkText(string $text): string
|
||||
{
|
||||
$replaced = preg_replace_callback(
|
||||
'/(?<!\d)(\d{4}-\d{2}-\d{2})(?!\d)/',
|
||||
static function (array $matches): string {
|
||||
$date = (string)($matches[1] ?? '');
|
||||
if ($date === '') return $matches[0];
|
||||
|
||||
$id = ChronoID::dateToDayId($date);
|
||||
if ($id === null) return $matches[0];
|
||||
|
||||
if (function_exists('html_wikilink')) {
|
||||
return (string)html_wikilink($id, $date);
|
||||
}
|
||||
|
||||
if (function_exists('wl')) {
|
||||
return '<a href="' . hsc((string)wl($id)) . '">' . hsc($date) . '</a>';
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
},
|
||||
$text
|
||||
);
|
||||
|
||||
return is_string($replaced) ? $replaced : $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blocked-tag counters while traversing HTML tokens.
|
||||
*
|
||||
* @param string $token
|
||||
* @param array<string,int> $blocked
|
||||
* @return void
|
||||
*/
|
||||
protected static function updateBlockedTagCounters(string $token, array &$blocked): void
|
||||
{
|
||||
if (!preg_match('/^<\s*(\/?)\s*([a-zA-Z0-9:_-]+)/', $token, $matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$isClosing = $matches[1] === '/';
|
||||
$tag = strtolower((string)$matches[2]);
|
||||
if (!array_key_exists($tag, $blocked)) return;
|
||||
|
||||
if ($isClosing) {
|
||||
if ($blocked[$tag] > 0) $blocked[$tag]--;
|
||||
return;
|
||||
}
|
||||
|
||||
$selfClosing = preg_match('/\/\s*>$/', $token) === 1;
|
||||
if (!$selfClosing) {
|
||||
$blocked[$tag]++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if traversal is currently inside a blocked context.
|
||||
*
|
||||
* @param array<string,int> $blocked
|
||||
* @return bool
|
||||
*/
|
||||
protected static function isBlockedContext(array $blocked): bool
|
||||
{
|
||||
foreach ($blocked as $count) {
|
||||
if ($count > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
71
src/ChronologicalDayTemplate.php
Normal file
71
src/ChronologicalDayTemplate.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
/**
|
||||
* Builds page template content for chronological day pages.
|
||||
*/
|
||||
class ChronologicalDayTemplate
|
||||
{
|
||||
/**
|
||||
* Build a German date heading template for a canonical day ID.
|
||||
*
|
||||
* Example output:
|
||||
* ====== Freitag, 13. Februar 2026 ======
|
||||
*
|
||||
* @param string $dayId
|
||||
* @param string $baseNs
|
||||
* @return string|null
|
||||
*/
|
||||
public static function buildForDayId(string $dayId, string $baseNs = 'chronological'): ?string
|
||||
{
|
||||
$parts = ChronoID::parseDayId($dayId, $baseNs);
|
||||
if ($parts === null) return null;
|
||||
|
||||
$formatted = self::formatGermanDate($parts['year'], $parts['month'], $parts['day']);
|
||||
return '====== ' . $formatted . " ======\n\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date with German day/month names and style:
|
||||
* Freitag, 13. Februar 2026
|
||||
*
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @param int $day
|
||||
* @return string
|
||||
*/
|
||||
protected static function formatGermanDate(int $year, int $month, int $day): string
|
||||
{
|
||||
$weekdays = [
|
||||
1 => 'Montag',
|
||||
2 => 'Dienstag',
|
||||
3 => 'Mittwoch',
|
||||
4 => 'Donnerstag',
|
||||
5 => 'Freitag',
|
||||
6 => 'Samstag',
|
||||
7 => 'Sonntag',
|
||||
];
|
||||
|
||||
$months = [
|
||||
1 => 'Januar',
|
||||
2 => 'Februar',
|
||||
3 => 'März',
|
||||
4 => 'April',
|
||||
5 => 'Mai',
|
||||
6 => 'Juni',
|
||||
7 => 'Juli',
|
||||
8 => 'August',
|
||||
9 => 'September',
|
||||
10 => 'Oktober',
|
||||
11 => 'November',
|
||||
12 => 'Dezember',
|
||||
];
|
||||
|
||||
$weekdayIndex = (int)date('N', mktime(0, 0, 0, $month, $day, $year));
|
||||
$weekday = $weekdays[$weekdayIndex] ?? '';
|
||||
$monthName = $months[$month] ?? '';
|
||||
|
||||
return sprintf('%s, %d. %s %d', $weekday, $day, $monthName, $year);
|
||||
}
|
||||
}
|
||||
283
src/ChronologicalIcsEvents.php
Normal file
283
src/ChronologicalIcsEvents.php
Normal file
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\Reader;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Read local ICS files using sabre/vobject and expose events for one day.
|
||||
*/
|
||||
class ChronologicalIcsEvents
|
||||
{
|
||||
/** @var array<string,array<int,array{summary:string,time:string,startIso:string,allDay:bool}>> In-request cache */
|
||||
protected static $runtimeCache = [];
|
||||
|
||||
/**
|
||||
* Return events for one day (YYYY-MM-DD) from configured local ICS files.
|
||||
*
|
||||
* @param string $icsConfig Multiline list of local ICS file paths
|
||||
* @param string $dateIso
|
||||
* @return array<int,array{summary:string,time:string,startIso:string,allDay:bool}>
|
||||
*/
|
||||
public static function eventsForDate(string $icsConfig, string $dateIso): array
|
||||
{
|
||||
if (!ChronoID::isIsoDate($dateIso)) return [];
|
||||
|
||||
$files = self::parseConfiguredFiles($icsConfig);
|
||||
if ($files === []) return [];
|
||||
|
||||
$signature = self::buildSignature($files);
|
||||
if ($signature === '') return [];
|
||||
|
||||
$cacheKey = $signature . '|' . $dateIso;
|
||||
|
||||
if (isset(self::$runtimeCache[$cacheKey])) {
|
||||
return self::$runtimeCache[$cacheKey];
|
||||
}
|
||||
|
||||
$utc = new DateTimeZone('UTC');
|
||||
$rangeStart = new DateTimeImmutable($dateIso . ' 00:00:00', $utc);
|
||||
$rangeStart = $rangeStart->sub(new DateInterval('P1D'));
|
||||
$rangeEnd = $rangeStart->add(new DateInterval('P3D'));
|
||||
|
||||
$events = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
foreach (self::readEventsFromFile($file, $dateIso, $rangeStart, $rangeEnd) as $entry) {
|
||||
$dedupeKey = implode('|', [
|
||||
(string)($entry['summary'] ?? ''),
|
||||
(string)($entry['time'] ?? ''),
|
||||
((bool)($entry['allDay'] ?? false)) ? '1' : '0',
|
||||
]);
|
||||
if (isset($seen[$dedupeKey])) continue;
|
||||
$seen[$dedupeKey] = true;
|
||||
$events[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
usort($events, static function (array $a, array $b): int {
|
||||
$aAllDay = (bool)($a['allDay'] ?? false);
|
||||
$bAllDay = (bool)($b['allDay'] ?? false);
|
||||
if ($aAllDay !== $bAllDay) {
|
||||
return $aAllDay ? -1 : 1;
|
||||
}
|
||||
|
||||
$timeCmp = strcmp((string)($a['time'] ?? ''), (string)($b['time'] ?? ''));
|
||||
if ($timeCmp !== 0) return $timeCmp;
|
||||
|
||||
return strcmp((string)($a['summary'] ?? ''), (string)($b['summary'] ?? ''));
|
||||
});
|
||||
|
||||
self::$runtimeCache[$cacheKey] = $events;
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $icsConfig
|
||||
* @return string[]
|
||||
*/
|
||||
protected static function parseConfiguredFiles(string $icsConfig): array
|
||||
{
|
||||
$files = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $icsConfig) ?: [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string)$line);
|
||||
if ($line === '') continue;
|
||||
if (str_starts_with($line, '#')) continue;
|
||||
|
||||
$path = Path::cleanPath($line, false);
|
||||
if (!is_file($path) || !is_readable($path)) continue;
|
||||
$files[] = $path;
|
||||
}
|
||||
|
||||
$files = array_values(array_unique($files));
|
||||
sort($files, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build signature from file path + mtime + size.
|
||||
*
|
||||
* @param string[] $files
|
||||
* @return string
|
||||
*/
|
||||
protected static function buildSignature(array $files): string
|
||||
{
|
||||
if ($files === []) return '';
|
||||
$parts = [];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$mtime = @filemtime($file) ?: 0;
|
||||
$size = @filesize($file) ?: 0;
|
||||
$parts[] = $file . '|' . $mtime . '|' . $size;
|
||||
}
|
||||
|
||||
return sha1(implode("\n", $parts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one ICS file and return normalized events for the target day.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $dateIso
|
||||
* @param DateTimeImmutable $rangeStart
|
||||
* @param DateTimeImmutable $rangeEnd
|
||||
* @return array<int,array{summary:string,time:string,startIso:string,allDay:bool}>
|
||||
*/
|
||||
protected static function readEventsFromFile(
|
||||
string $file,
|
||||
string $dateIso,
|
||||
DateTimeImmutable $rangeStart,
|
||||
DateTimeImmutable $rangeEnd
|
||||
): array
|
||||
{
|
||||
$raw = @file_get_contents($file);
|
||||
if (!is_string($raw) || trim($raw) === '') return [];
|
||||
|
||||
try {
|
||||
$component = Reader::read($raw, Reader::OPTION_FORGIVING);
|
||||
if (!($component instanceof VCalendar)) return [];
|
||||
|
||||
$expanded = $component->expand($rangeStart, $rangeEnd);
|
||||
if (!($expanded instanceof VCalendar)) return [];
|
||||
|
||||
return self::collectEventsFromCalendar($expanded, $dateIso);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param VCalendar $calendar
|
||||
* @param string $dateIso
|
||||
* @return array<int,array{summary:string,time:string,startIso:string,allDay:bool}>
|
||||
*/
|
||||
protected static function collectEventsFromCalendar(
|
||||
VCalendar $calendar,
|
||||
string $dateIso
|
||||
): array {
|
||||
$result = [];
|
||||
$seen = [];
|
||||
|
||||
foreach ($calendar->select('VEVENT') as $vevent) {
|
||||
if (!($vevent instanceof VEvent)) continue;
|
||||
|
||||
$normalized = self::normalizeEventForDay($vevent, $dateIso);
|
||||
if ($normalized === null) continue;
|
||||
|
||||
$dedupeKey = implode('|', [
|
||||
(string)($normalized['uid'] ?? ''),
|
||||
(string)($normalized['rid'] ?? ''),
|
||||
(string)($normalized['start'] ?? ''),
|
||||
(string)($normalized['summary'] ?? ''),
|
||||
(string)($normalized['time'] ?? ''),
|
||||
((bool)($normalized['allDay'] ?? false)) ? '1' : '0',
|
||||
]);
|
||||
if (isset($seen[$dedupeKey])) continue;
|
||||
$seen[$dedupeKey] = true;
|
||||
|
||||
$result[] = [
|
||||
'summary' => (string)$normalized['summary'],
|
||||
'time' => (string)$normalized['time'],
|
||||
'startIso' => (string)$normalized['start'],
|
||||
'allDay' => (bool)$normalized['allDay'],
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert VEVENT to output item when it intersects the target day.
|
||||
*
|
||||
* @param VEvent $vevent
|
||||
* @param string $dateIso
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
protected static function normalizeEventForDay(
|
||||
VEvent $vevent,
|
||||
string $dateIso
|
||||
): ?array
|
||||
{
|
||||
if (!isset($vevent->DTSTART)) return null;
|
||||
if (!ChronoID::isIsoDate($dateIso)) return null;
|
||||
|
||||
$isAllDay = strtoupper((string)($vevent->DTSTART['VALUE'] ?? '')) === 'DATE';
|
||||
|
||||
$start = self::toImmutableDateTime($vevent->DTSTART->getDateTime());
|
||||
if ($start === null) return null;
|
||||
|
||||
$end = null;
|
||||
if (isset($vevent->DTEND)) {
|
||||
$end = self::toImmutableDateTime($vevent->DTEND->getDateTime());
|
||||
} elseif (isset($vevent->DURATION)) {
|
||||
try {
|
||||
$duration = $vevent->DURATION->getDateInterval();
|
||||
if ($duration instanceof DateInterval) {
|
||||
$end = $start->add($duration);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$end = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($end === null) {
|
||||
$end = $isAllDay ? $start->add(new DateInterval('P1D')) : $start;
|
||||
}
|
||||
|
||||
if ($end <= $start) {
|
||||
$end = $isAllDay ? $start->add(new DateInterval('P1D')) : $start;
|
||||
}
|
||||
|
||||
$eventTimezone = $start->getTimezone();
|
||||
$dayStart = new DateTimeImmutable($dateIso . ' 00:00:00', $eventTimezone);
|
||||
$dayEnd = $dayStart->add(new DateInterval('P1D'));
|
||||
|
||||
$intersects = ($start < $dayEnd) && ($end > $dayStart);
|
||||
if (!$intersects && !$isAllDay && $start >= $dayStart && $start < $dayEnd && $end == $start) {
|
||||
$intersects = true;
|
||||
}
|
||||
if (!$intersects) return null;
|
||||
|
||||
$summary = trim((string)($vevent->SUMMARY ?? ''));
|
||||
if ($summary === '') $summary = '(ohne Titel)';
|
||||
|
||||
$uid = trim((string)($vevent->UID ?? ''));
|
||||
$rid = '';
|
||||
if (isset($vevent->{'RECURRENCE-ID'})) {
|
||||
$rid = trim((string)$vevent->{'RECURRENCE-ID'});
|
||||
}
|
||||
|
||||
return [
|
||||
'uid' => $uid,
|
||||
'rid' => $rid,
|
||||
'start' => $start->format(DateTimeInterface::ATOM),
|
||||
'summary' => $summary,
|
||||
'time' => $isAllDay ? '' : $start->format('H:i'),
|
||||
'allDay' => $isAllDay,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTimeInterface $dateTime
|
||||
* @return DateTimeImmutable|null
|
||||
*/
|
||||
protected static function toImmutableDateTime(DateTimeInterface $dateTime): ?DateTimeImmutable
|
||||
{
|
||||
if ($dateTime instanceof DateTimeImmutable) return $dateTime;
|
||||
|
||||
$immutable = DateTimeImmutable::createFromFormat('U', (string)$dateTime->getTimestamp());
|
||||
if (!($immutable instanceof DateTimeImmutable)) return null;
|
||||
|
||||
return $immutable->setTimezone($dateTime->getTimezone());
|
||||
}
|
||||
}
|
||||
159
src/PageLinkTrait.php
Normal file
159
src/PageLinkTrait.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace dokuwiki\plugin\luxtools;
|
||||
|
||||
/**
|
||||
* Trait for pagelink-related functionality shared across syntax handlers.
|
||||
*
|
||||
* Provides methods for:
|
||||
* - Detecting blobs alias paths
|
||||
* - Resolving the blobs root folder from page metadata
|
||||
* - Rendering "page not linked" messages
|
||||
* - Building path configs with blobs alias support
|
||||
*
|
||||
* Requirements for using classes:
|
||||
* - Must have getConf() method (from SyntaxPlugin)
|
||||
* - Must have getLang() method (from SyntaxPlugin)
|
||||
*/
|
||||
trait PageLinkTrait
|
||||
{
|
||||
/**
|
||||
* Check if the given path uses the blobs alias.
|
||||
*
|
||||
* @param string $path
|
||||
* @return bool
|
||||
*/
|
||||
protected function isBlobsPath(string $path): bool
|
||||
{
|
||||
$trimmed = ltrim($path, '/');
|
||||
return preg_match('/^blobs(\/|$)/', $trimmed) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Page not linked" message with copy ID affordance.
|
||||
*
|
||||
* @param \Doku_Renderer $renderer
|
||||
*/
|
||||
protected function renderPageNotLinked(\Doku_Renderer $renderer): void
|
||||
{
|
||||
$text = (string)$this->getLang('pagelink_unlinked');
|
||||
|
||||
if ($renderer instanceof \Doku_Renderer_xhtml) {
|
||||
$renderer->doc .= '<span class="luxtools-pagelink-status">' . hsc($text) . '</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
$renderer->cdata('[n/a: ' . $text . ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current page UUID (if any).
|
||||
*
|
||||
* @return string The UUID or empty string
|
||||
*/
|
||||
protected function getPageUuidSafe(): string
|
||||
{
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
return $uuid ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current page's pagelink folder for the blobs alias.
|
||||
*
|
||||
* Results are cached per page ID within a single request.
|
||||
*
|
||||
* @return string The linked folder path or empty string if not linked
|
||||
*/
|
||||
protected function resolveBlobsRoot(): string
|
||||
{
|
||||
static $cached = [];
|
||||
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (isset($cached[$pageId])) {
|
||||
return (string)$cached[$pageId];
|
||||
}
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
if ($uuid === null) {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$linkInfo = $pageLink->resolveUuid($uuid);
|
||||
$folder = $linkInfo['folder'] ?? '';
|
||||
if (!is_string($folder) || $folder === '') {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$cached[$pageId] = $folder;
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a path config string with the blobs alias appended (if available).
|
||||
*
|
||||
* @param string|null $blobsRoot The blobs root folder (or null to auto-resolve)
|
||||
* @return string The path config string
|
||||
*/
|
||||
protected function buildPathConfigWithBlobs(?string $blobsRoot = null): string
|
||||
{
|
||||
$pathConfig = (string)$this->getConf('paths');
|
||||
|
||||
if ($blobsRoot === null) {
|
||||
$blobsRoot = $this->resolveBlobsRoot();
|
||||
}
|
||||
|
||||
if ($blobsRoot !== '') {
|
||||
$pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs";
|
||||
}
|
||||
|
||||
return $pathConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Path helper with blobs alias support.
|
||||
*
|
||||
* @param string|null $blobsRoot The blobs root folder (or null to auto-resolve)
|
||||
* @return Path
|
||||
*/
|
||||
protected function createPathHelperWithBlobs(?string $blobsRoot = null): Path
|
||||
{
|
||||
return new Path($this->buildPathConfigWithBlobs($blobsRoot));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Path helper using only the base paths config (no blobs alias).
|
||||
*
|
||||
* @return Path
|
||||
*/
|
||||
protected function createPathHelper(): Path
|
||||
{
|
||||
return new Path((string)$this->getConf('paths'));
|
||||
}
|
||||
}
|
||||
37
src/Path.php
37
src/Path.php
@@ -99,10 +99,10 @@ class Path
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a real filesystem path back to a configured alias, if available.
|
||||
* Map a real filesystem path back to an open-service alias, if available.
|
||||
*
|
||||
* Example: root "/share/Datascape/" with alias "/Scape/" maps
|
||||
* "/share/Datascape/some/folder" -> "/Scape/some/folder".
|
||||
* "/share/Datascape/some/folder" -> "Scape>some/folder".
|
||||
*
|
||||
* If no alias matches, the input path is returned unchanged (except for
|
||||
* normalization of slashes and dot-segments).
|
||||
@@ -117,12 +117,17 @@ class Path
|
||||
// normalize input for matching, but do not force a trailing slash
|
||||
$normalized = static::cleanPath($path, false);
|
||||
|
||||
// collect root->alias mappings (avoid alias keys that reference the same config)
|
||||
// collect root->alias mappings for open-service links
|
||||
// (avoid alias keys that reference the same config)
|
||||
$mappings = [];
|
||||
foreach ($this->paths as $key => $info) {
|
||||
if (!isset($info['root']) || $key !== $info['root']) continue;
|
||||
if (empty($info['alias'])) continue;
|
||||
$mappings[$info['root']] = $info['alias'];
|
||||
|
||||
$alias = $this->normalizeOpenAlias((string)$info['alias']);
|
||||
if ($alias === '') continue;
|
||||
|
||||
$mappings[$info['root']] = $alias;
|
||||
}
|
||||
|
||||
if ($mappings === []) return $normalized;
|
||||
@@ -131,16 +136,34 @@ class Path
|
||||
uksort($mappings, static fn($a, $b) => strlen($b) - strlen($a));
|
||||
|
||||
foreach ($mappings as $root => $alias) {
|
||||
$rootNoTrailingSlash = rtrim($root, '/');
|
||||
if (!str_starts_with($normalized, $root) && $normalized !== $rootNoTrailingSlash) continue;
|
||||
|
||||
$suffix = '';
|
||||
if (str_starts_with($normalized, $root)) {
|
||||
$suffix = substr($normalized, strlen($root));
|
||||
$alias = static::cleanPath($alias, true);
|
||||
return rtrim($alias, '/') . '/' . $suffix;
|
||||
$suffix = (string)substr($normalized, strlen($root));
|
||||
}
|
||||
$suffix = ltrim($suffix, '/');
|
||||
|
||||
return $alias . '>' . $suffix;
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy path-like aliases (e.g. /Scape/) to open aliases (Scape).
|
||||
*
|
||||
* @param string $alias
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeOpenAlias($alias)
|
||||
{
|
||||
$alias = trim($alias);
|
||||
$alias = trim($alias, '/\\');
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean a path for better comparison
|
||||
*
|
||||
|
||||
146
style.css
146
style.css
@@ -49,11 +49,9 @@ div.luxtools-plugin .luxtools-empty {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
/* Page link copy message (unlinked blobs alias) */
|
||||
a.luxtools-pagelink-copy,
|
||||
a.luxtools-pagelink-copy:visited {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
/* Page link status (unlinked blobs alias) */
|
||||
span.luxtools-pagelink-status {
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.3;
|
||||
margin: 0.25em 0;
|
||||
@@ -62,8 +60,6 @@ a.luxtools-pagelink-copy:visited {
|
||||
border-radius: 0.2em;
|
||||
background-color: @ini_background_alt;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Image gallery spacing. */
|
||||
@@ -426,6 +422,31 @@ html.luxtools-noscroll body {
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Grouping wrapper (compact image layout container)
|
||||
* ======================================================================== */
|
||||
|
||||
.luxtools-grouping {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--luxtools-grouping-cols, 2), minmax(0, 1fr));
|
||||
gap: var(--luxtools-grouping-gap, 0);
|
||||
justify-content: var(--luxtools-grouping-justify, start);
|
||||
align-items: var(--luxtools-grouping-align, start);
|
||||
}
|
||||
|
||||
.luxtools-grouping.luxtools-grouping--flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--luxtools-grouping-gap, 0);
|
||||
}
|
||||
|
||||
/* Let the grouping layout fully control item placement. */
|
||||
.luxtools-grouping .luxtools-imagebox {
|
||||
float: none;
|
||||
clear: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Imagebox (Wikipedia-style image with caption)
|
||||
* ======================================================================== */
|
||||
@@ -487,3 +508,114 @@ html.luxtools-noscroll body {
|
||||
padding: 3px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* Calendar widget
|
||||
* ======================================================================== */
|
||||
|
||||
div.luxtools-calendar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
font-size: 88%;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25em;
|
||||
font-size: 95%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav-prev {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav-next {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav-prev a,
|
||||
div.luxtools-calendar .luxtools-calendar-nav-next a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav-button {
|
||||
border: 1px solid @ini_border;
|
||||
background-color: @ini_background_alt;
|
||||
color: @ini_text;
|
||||
font: inherit;
|
||||
line-height: 1;
|
||||
padding: 0.2em 0.45em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.luxtools-calendar .luxtools-calendar-nav-button:hover,
|
||||
div.luxtools-calendar .luxtools-calendar-nav-button:focus {
|
||||
background-color: @ini_highlight;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
div.luxtools-calendar table.luxtools-calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
div.luxtools-calendar table.luxtools-calendar-table th,
|
||||
div.luxtools-calendar table.luxtools-calendar-table td {
|
||||
border: 1px solid @ini_border;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.luxtools-calendar table.luxtools-calendar-table th {
|
||||
background-color: @ini_background_alt;
|
||||
font-size: 85%;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day-empty {
|
||||
background-color: @ini_background_alt;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day-today {
|
||||
background-color: @ini_highlight;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 1.9em;
|
||||
background: transparent;
|
||||
text-decoration: none;
|
||||
border-bottom: 0;
|
||||
box-shadow: none;
|
||||
padding: 0.1em 0;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day a:hover,
|
||||
div.luxtools-calendar td.luxtools-calendar-day a:focus,
|
||||
div.luxtools-calendar td.luxtools-calendar-day a:active,
|
||||
div.luxtools-calendar td.luxtools-calendar-day a:visited {
|
||||
text-decoration: none;
|
||||
border-bottom: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day:hover {
|
||||
background-color: @ini_background_alt;
|
||||
}
|
||||
|
||||
div.luxtools-calendar td.luxtools-calendar-day.luxtools-calendar-day-today:hover {
|
||||
background-color: @ini_highlight;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use dokuwiki\Extension\SyntaxPlugin;
|
||||
use dokuwiki\plugin\luxtools\Crawler;
|
||||
use dokuwiki\plugin\luxtools\Output;
|
||||
use dokuwiki\plugin\luxtools\Path;
|
||||
use dokuwiki\plugin\luxtools\PageLink;
|
||||
use dokuwiki\plugin\luxtools\PageLinkTrait;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
@@ -16,6 +16,7 @@ require_once(__DIR__ . '/../autoload.php');
|
||||
if (!class_exists('syntax_plugin_luxtools_abstract', false)) {
|
||||
abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
|
||||
{
|
||||
use PageLinkTrait;
|
||||
/**
|
||||
* Returns the syntax keyword (e.g., 'files', 'directory', 'images').
|
||||
* Used for pattern matching and plugin registration.
|
||||
@@ -209,16 +210,12 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
|
||||
protected function getPathInfoSafe(string $basePath, \Doku_Renderer $renderer)
|
||||
{
|
||||
try {
|
||||
$pathConfig = (string)$this->getConf('paths');
|
||||
$blobsRoot = $this->resolveBlobsRoot();
|
||||
if ($blobsRoot === '' && $this->isBlobsPath($basePath)) {
|
||||
$this->renderPageNotLinked($renderer);
|
||||
return false;
|
||||
}
|
||||
if ($blobsRoot !== '') {
|
||||
$pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs";
|
||||
}
|
||||
$pathHelper = new Path($pathConfig);
|
||||
$pathHelper = $this->createPathHelperWithBlobs($blobsRoot);
|
||||
return $pathHelper->getPathInfo($basePath);
|
||||
} catch (\Exception $e) {
|
||||
$this->renderError($renderer, 'error_outsidejail');
|
||||
@@ -226,99 +223,6 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given path uses the blobs alias.
|
||||
*/
|
||||
protected function isBlobsPath(string $path): bool
|
||||
{
|
||||
$trimmed = ltrim($path, '/');
|
||||
return preg_match('/^blobs(\/|$)/', $trimmed) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Page not linked" message with copy ID affordance.
|
||||
*/
|
||||
protected function renderPageNotLinked(\Doku_Renderer $renderer): void
|
||||
{
|
||||
$uuid = $this->getPageUuidSafe();
|
||||
$text = (string)$this->getLang('pagelink_unlinked');
|
||||
|
||||
if ($renderer instanceof \Doku_Renderer_xhtml) {
|
||||
$renderer->doc .= '<a href="#" class="luxtools-pagelink-copy" data-luxtools-pagelink-copy="1"'
|
||||
. ' data-uuid="' . hsc($uuid) . '"'
|
||||
. '>' . hsc($text) . '</a>';
|
||||
return;
|
||||
}
|
||||
|
||||
$renderer->cdata('[n/a: ' . $text . ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current page UUID (if any).
|
||||
*/
|
||||
protected function getPageUuidSafe(): string
|
||||
{
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
return $uuid ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current page's pagelink folder for the blobs alias.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function resolveBlobsRoot(): string
|
||||
{
|
||||
static $cached = [];
|
||||
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (isset($cached[$pageId])) {
|
||||
return (string)$cached[$pageId];
|
||||
}
|
||||
|
||||
$pathConfig = (string)$this->getConf('paths');
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink($pathConfig, $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
if ($uuid === null) {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$linkInfo = $pageLink->resolveUuid($uuid);
|
||||
$folder = $linkInfo['folder'] ?? '';
|
||||
if (!is_string($folder) || $folder === '') {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$cached[$pageId] = $folder;
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure a Crawler instance.
|
||||
*
|
||||
|
||||
147
syntax/calendar.php
Normal file
147
syntax/calendar.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
use dokuwiki\Extension\SyntaxPlugin;
|
||||
use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* luxtools Plugin: Calendar widget syntax.
|
||||
*
|
||||
* Syntax:
|
||||
* - {{calendar>}} current month
|
||||
* - {{calendar>YYYY-MM}} specific month
|
||||
* - {{calendar>YYYY-MM&base=chronological}} custom base namespace (optional)
|
||||
*/
|
||||
class syntax_plugin_luxtools_calendar extends SyntaxPlugin
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function getType()
|
||||
{
|
||||
return 'substition';
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getPType()
|
||||
{
|
||||
return 'block';
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getSort()
|
||||
{
|
||||
return 224;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function connectTo($mode)
|
||||
{
|
||||
$this->Lexer->addSpecialPattern('\{\{calendar>.*?\}\}', $mode, 'plugin_luxtools_calendar');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function handle($match, $state, $pos, Doku_Handler $handler)
|
||||
{
|
||||
$match = substr($match, strlen('{{calendar>'), -2);
|
||||
[$target, $flags] = array_pad(explode('&', $match, 2), 2, '');
|
||||
|
||||
$target = trim((string)$target);
|
||||
$params = $this->parseFlags($flags);
|
||||
$baseNs = $params['base'] ?? 'chronological';
|
||||
|
||||
$resolved = $this->resolveTargetMonth($target);
|
||||
if ($resolved === null) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => 'calendar_err_badmonth',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'year' => $resolved['year'],
|
||||
'month' => $resolved['month'],
|
||||
'base' => $baseNs,
|
||||
];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function render($format, Doku_Renderer $renderer, $data)
|
||||
{
|
||||
if ($data === false || !is_array($data)) return false;
|
||||
if ($format !== 'xhtml') return false;
|
||||
if (!($renderer instanceof Doku_Renderer_xhtml)) return false;
|
||||
|
||||
if (!($data['ok'] ?? false)) {
|
||||
$message = (string)$this->getLang((string)($data['error'] ?? 'calendar_err_badmonth'));
|
||||
if ($message === '') $message = 'Invalid calendar month. Use YYYY-MM.';
|
||||
$renderer->doc .= '<div class="luxtools-plugin luxtools-calendar"><div class="luxtools-empty">' . hsc($message) . '</div></div>';
|
||||
return true;
|
||||
}
|
||||
|
||||
$year = (int)$data['year'];
|
||||
$month = (int)$data['month'];
|
||||
$baseNs = (string)$data['base'];
|
||||
|
||||
$renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $flags
|
||||
* @return array<string,string>
|
||||
*/
|
||||
protected function parseFlags(string $flags): array
|
||||
{
|
||||
$params = [];
|
||||
foreach (explode('&', $flags) as $flag) {
|
||||
if (trim($flag) === '') continue;
|
||||
[$name, $value] = array_pad(explode('=', $flag, 2), 2, '');
|
||||
$name = strtolower(trim($name));
|
||||
$value = trim($value);
|
||||
if ($name === '') continue;
|
||||
$params[$name] = $value;
|
||||
}
|
||||
|
||||
if (!isset($params['base']) || trim($params['base']) === '') {
|
||||
$params['base'] = 'chronological';
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve target string to year/month.
|
||||
*
|
||||
* Accepted formats:
|
||||
* - '' (current month)
|
||||
* - YYYY-MM
|
||||
*
|
||||
* @param string $target
|
||||
* @return array{year:int,month:int}|null
|
||||
*/
|
||||
protected function resolveTargetMonth(string $target): ?array
|
||||
{
|
||||
if ($target === '') {
|
||||
return [
|
||||
'year' => (int)date('Y'),
|
||||
'month' => (int)date('m'),
|
||||
];
|
||||
}
|
||||
|
||||
if (!preg_match('/^(\d{4})-(\d{2})$/', $target, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$year = (int)$matches[1];
|
||||
$month = (int)$matches[2];
|
||||
if ($year < 1) return null;
|
||||
if ($month < 1 || $month > 12) return null;
|
||||
|
||||
return [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
];
|
||||
}
|
||||
}
|
||||
282
syntax/grouping.php
Normal file
282
syntax/grouping.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
use dokuwiki\Extension\SyntaxPlugin;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
|
||||
/**
|
||||
* luxtools Plugin: Grouping wrapper syntax.
|
||||
*
|
||||
* Wraps multiple blocks (typically {{image>...}}) and applies compact layout
|
||||
* without adding visual box styling of its own.
|
||||
*
|
||||
* Syntax:
|
||||
* <grouping layout="flex" gap="0" justify="start" align="start">
|
||||
* {{image>...}}
|
||||
* {{image>...}}
|
||||
* </grouping>
|
||||
*/
|
||||
class syntax_plugin_luxtools_grouping extends SyntaxPlugin
|
||||
{
|
||||
/** @inheritdoc */
|
||||
public function getType()
|
||||
{
|
||||
return 'container';
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getPType()
|
||||
{
|
||||
return 'block';
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getSort()
|
||||
{
|
||||
// Slightly after image syntax
|
||||
return 316;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function getAllowedTypes()
|
||||
{
|
||||
return ['container', 'substition', 'protected', 'disabled', 'formatting'];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function connectTo($mode)
|
||||
{
|
||||
$this->Lexer->addEntryPattern('<grouping(?:\s+[^>]*)?>(?=.*</grouping>)', $mode, 'plugin_luxtools_grouping');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function postConnect()
|
||||
{
|
||||
$this->Lexer->addExitPattern('</grouping>', 'plugin_luxtools_grouping');
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function handle($match, $state, $pos, Doku_Handler $handler)
|
||||
{
|
||||
if ($state === DOKU_LEXER_ENTER) {
|
||||
$parsed = $this->parseOpeningTag($match);
|
||||
return [
|
||||
'state' => $state,
|
||||
'params' => $parsed['params'],
|
||||
'unknown' => $parsed['unknown'],
|
||||
];
|
||||
}
|
||||
|
||||
if ($state === DOKU_LEXER_UNMATCHED) {
|
||||
return [
|
||||
'state' => $state,
|
||||
'text' => $match,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
];
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function render($format, Doku_Renderer $renderer, $data)
|
||||
{
|
||||
if (!is_array($data) || !isset($data['state'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$state = (int)$data['state'];
|
||||
|
||||
if ($format !== 'xhtml') {
|
||||
if ($state === DOKU_LEXER_UNMATCHED && isset($data['text'])) {
|
||||
$renderer->cdata((string)$data['text']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!($renderer instanceof Doku_Renderer_xhtml)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($state === DOKU_LEXER_ENTER) {
|
||||
$params = isset($data['params']) && is_array($data['params']) ? $data['params'] : $this->getDefaultParams();
|
||||
$unknown = isset($data['unknown']) && is_array($data['unknown']) ? $data['unknown'] : [];
|
||||
|
||||
$layout = ($params['layout'] === 'flex') ? 'flex' : 'grid';
|
||||
$cols = (int)$params['cols'];
|
||||
if ($cols < 1) {
|
||||
$cols = 2;
|
||||
}
|
||||
|
||||
$gap = (string)$params['gap'];
|
||||
if (!$this->isValidCssLength($gap)) {
|
||||
$gap = '0';
|
||||
}
|
||||
|
||||
$justify = (string)$params['justify'];
|
||||
if (!$this->isValidJustify($justify)) {
|
||||
$justify = 'start';
|
||||
}
|
||||
|
||||
$align = (string)$params['align'];
|
||||
if (!$this->isValidAlign($align)) {
|
||||
$align = 'start';
|
||||
}
|
||||
|
||||
$renderer->doc .= '<div class="luxtools-grouping luxtools-grouping--' . hsc($layout) . '"'
|
||||
. ' style="--luxtools-grouping-cols: ' . $cols
|
||||
. '; --luxtools-grouping-gap: ' . hsc($gap)
|
||||
. '; --luxtools-grouping-justify: ' . hsc($justify)
|
||||
. '; --luxtools-grouping-align: ' . hsc($align)
|
||||
. ';">';
|
||||
|
||||
if ($unknown !== []) {
|
||||
$renderer->doc .= '<span class="luxtools-grouping-warning">'
|
||||
. hsc('[grouping: unknown option(s): ' . implode(', ', $unknown) . ']')
|
||||
. '</span>';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($state === DOKU_LEXER_UNMATCHED) {
|
||||
if (isset($data['text'])) {
|
||||
$renderer->cdata((string)$data['text']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($state === DOKU_LEXER_EXIT) {
|
||||
$renderer->doc .= '</div>';
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse opening <grouping ...> tag attributes.
|
||||
*
|
||||
* Supports attribute style only, e.g.
|
||||
* layout="grid" cols="3" gap="8px" justify="center" align="stretch".
|
||||
* Unknown or invalid values are ignored and defaults are used.
|
||||
*
|
||||
* @param string $match
|
||||
* @return array{params:array{layout:string,cols:int,gap:string,justify:string,align:string},unknown:array<int,string>}
|
||||
*/
|
||||
protected function parseOpeningTag(string $match): array
|
||||
{
|
||||
$params = $this->getDefaultParams();
|
||||
$unknown = [];
|
||||
|
||||
if (!preg_match('/^<grouping\b(.*?)>$/is', $match, $tagMatch)) {
|
||||
return ['params' => $params, 'unknown' => $unknown];
|
||||
}
|
||||
|
||||
$attrPart = (string)$tagMatch[1];
|
||||
if ($attrPart === '') {
|
||||
return ['params' => $params, 'unknown' => $unknown];
|
||||
}
|
||||
|
||||
if (preg_match_all('/([a-zA-Z_:][a-zA-Z0-9:._-]*)\s*=\s*(["\'])(.*?)\2/s', $attrPart, $attrMatches, PREG_SET_ORDER)) {
|
||||
foreach ($attrMatches as $item) {
|
||||
$name = strtolower(trim((string)$item[1]));
|
||||
$value = trim((string)$item[3]);
|
||||
|
||||
if ($name === 'layout') {
|
||||
$value = strtolower($value);
|
||||
if (in_array($value, ['grid', 'flex'], true)) {
|
||||
$params['layout'] = $value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($name === 'cols') {
|
||||
if (preg_match('/^\d+$/', $value)) {
|
||||
$cols = (int)$value;
|
||||
if ($cols > 0) {
|
||||
$params['cols'] = min($cols, 12);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($name === 'gap') {
|
||||
if ($this->isValidCssLength($value)) {
|
||||
$params['gap'] = $value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($name === 'justify') {
|
||||
$value = strtolower($value);
|
||||
if ($this->isValidJustify($value)) {
|
||||
$params['justify'] = $value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($name === 'align') {
|
||||
$value = strtolower($value);
|
||||
if ($this->isValidAlign($value)) {
|
||||
$params['align'] = $value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$unknown[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
if ($unknown !== []) {
|
||||
$unknown = array_values(array_unique($unknown));
|
||||
}
|
||||
|
||||
return ['params' => $params, 'unknown' => $unknown];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{layout:string,cols:int,gap:string,justify:string,align:string}
|
||||
*/
|
||||
protected function getDefaultParams(): array
|
||||
{
|
||||
return [
|
||||
'layout' => 'flex',
|
||||
'cols' => 2,
|
||||
'gap' => '0',
|
||||
'justify' => 'start',
|
||||
'align' => 'start',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a simple CSS length token.
|
||||
*
|
||||
* Allows "0" and common explicit units used in docs/examples.
|
||||
*/
|
||||
protected function isValidCssLength(string $value): bool
|
||||
{
|
||||
$value = trim($value);
|
||||
if ($value === '0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool)preg_match('/^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|%|vw|vh)$/', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate justify-content compatible values.
|
||||
*/
|
||||
protected function isValidJustify(string $value): bool
|
||||
{
|
||||
return in_array($value, ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate align-items compatible values.
|
||||
*/
|
||||
protected function isValidAlign(string $value): bool
|
||||
{
|
||||
return in_array($value, ['start', 'center', 'end', 'stretch', 'baseline'], true);
|
||||
}
|
||||
}
|
||||
107
syntax/image.php
107
syntax/image.php
@@ -2,7 +2,7 @@
|
||||
|
||||
use dokuwiki\Extension\SyntaxPlugin;
|
||||
use dokuwiki\plugin\luxtools\Path;
|
||||
use dokuwiki\plugin\luxtools\PageLink;
|
||||
use dokuwiki\plugin\luxtools\PageLinkTrait;
|
||||
use dokuwiki\plugin\luxtools\ThumbnailHelper;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
@@ -16,6 +16,7 @@ require_once(__DIR__ . '/../autoload.php');
|
||||
*/
|
||||
class syntax_plugin_luxtools_image extends SyntaxPlugin
|
||||
{
|
||||
use PageLinkTrait;
|
||||
/** @inheritdoc */
|
||||
public function getType()
|
||||
{
|
||||
@@ -139,12 +140,7 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
|
||||
return true;
|
||||
}
|
||||
|
||||
$pathConfig = (string)$this->getConf('paths');
|
||||
if ($blobsRoot !== '') {
|
||||
$pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs";
|
||||
}
|
||||
|
||||
$pathHelper = new Path($pathConfig);
|
||||
$pathHelper = $this->createPathHelperWithBlobs($blobsRoot);
|
||||
// Use addTrailingSlash=false since this is a file path, not a directory
|
||||
$pathInfo = $pathHelper->getPathInfo($data['path'], false);
|
||||
} catch (\Exception $e) {
|
||||
@@ -226,96 +222,6 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
|
||||
return DOKU_BASE . 'lib/plugins/luxtools/file.php?' . http_build_query($params, '', '&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given path uses the blobs alias.
|
||||
*/
|
||||
protected function isBlobsPath(string $path): bool
|
||||
{
|
||||
$trimmed = ltrim($path, '/');
|
||||
return preg_match('/^blobs(\/|$)/', $trimmed) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Page not linked" message with copy ID affordance.
|
||||
*/
|
||||
protected function renderPageNotLinked(\Doku_Renderer $renderer): void
|
||||
{
|
||||
$uuid = $this->getPageUuidSafe();
|
||||
$text = (string)$this->getLang('pagelink_unlinked');
|
||||
|
||||
if ($renderer instanceof \Doku_Renderer_xhtml) {
|
||||
$renderer->doc .= '<a href="#" class="luxtools-pagelink-copy" data-luxtools-pagelink-copy="1"'
|
||||
. ' data-uuid="' . hsc($uuid) . '"'
|
||||
. '>' . hsc($text) . '</a>';
|
||||
return;
|
||||
}
|
||||
|
||||
$renderer->cdata('[n/a: ' . $text . ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current page UUID (if any).
|
||||
*/
|
||||
protected function getPageUuidSafe(): string
|
||||
{
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
return $uuid ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current page's pagelink folder for the blobs alias.
|
||||
*/
|
||||
protected function resolveBlobsRoot(): string
|
||||
{
|
||||
static $cached = [];
|
||||
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (isset($cached[$pageId])) {
|
||||
return (string)$cached[$pageId];
|
||||
}
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
if ($uuid === null) {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$linkInfo = $pageLink->resolveUuid($uuid);
|
||||
$folder = $linkInfo['folder'] ?? '';
|
||||
if (!is_string($folder) || $folder === '') {
|
||||
$cached[$pageId] = '';
|
||||
return '';
|
||||
}
|
||||
|
||||
$cached[$pageId] = $folder;
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the imagebox HTML.
|
||||
*
|
||||
@@ -346,8 +252,9 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
|
||||
$outerStyle = ' style="width: ' . ($width + 10) . 'px;"';
|
||||
}
|
||||
|
||||
// Use thumbnail metadata from helper
|
||||
$dataThumbAttr = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"';
|
||||
// Use thumbnail metadata from helper.
|
||||
// JS loader expects data-src (same convention as gallery thumbnails).
|
||||
$dataThumbAttr = $thumb['isFinal'] ? '' : ' data-src="' . hsc($thumb['thumbUrl']) . '"';
|
||||
|
||||
// Build image attributes
|
||||
$imgAttrs = 'class="media luxtools-thumb" loading="lazy" decoding="async"';
|
||||
@@ -366,7 +273,7 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin
|
||||
// Image with link to full size
|
||||
$renderer->doc .= '<a href="' . hsc($fullUrl) . '" class="media" target="_blank">';
|
||||
$renderer->doc .= '<img src="' . hsc($thumb['url']) . '" ' . $imgAttrs . $dataThumbAttr . ' />';
|
||||
$renderer->doc .= '</a>';;
|
||||
$renderer->doc .= '</a>';
|
||||
|
||||
// Caption
|
||||
if ($caption !== '') {
|
||||
|
||||
102
syntax/open.php
102
syntax/open.php
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
use dokuwiki\Extension\SyntaxPlugin;
|
||||
use dokuwiki\plugin\luxtools\PageLink;
|
||||
use dokuwiki\plugin\luxtools\PageLinkTrait;
|
||||
use dokuwiki\plugin\luxtools\Path;
|
||||
|
||||
require_once(__DIR__ . '/../autoload.php');
|
||||
@@ -14,6 +14,7 @@ require_once(__DIR__ . '/../autoload.php');
|
||||
*/
|
||||
class syntax_plugin_luxtools_open extends SyntaxPlugin
|
||||
{
|
||||
use PageLinkTrait;
|
||||
/** @inheritdoc */
|
||||
public function getType()
|
||||
{
|
||||
@@ -86,9 +87,7 @@ class syntax_plugin_luxtools_open extends SyntaxPlugin
|
||||
}
|
||||
|
||||
try {
|
||||
$pathConfig = (string)$this->getConf('paths');
|
||||
$pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs";
|
||||
$pathHelper = new Path($pathConfig);
|
||||
$pathHelper = $this->createPathHelperWithBlobs($blobsRoot);
|
||||
$resolvedPath = $path;
|
||||
$isBlobsRoot = (rtrim($resolvedPath, '/') === 'blobs');
|
||||
if ($isBlobsRoot) {
|
||||
@@ -105,8 +104,22 @@ class syntax_plugin_luxtools_open extends SyntaxPlugin
|
||||
// Map local paths back to their configured aliases before opening.
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $path)) {
|
||||
try {
|
||||
$pathHelper = new Path((string)$this->getConf('paths'));
|
||||
$path = $pathHelper->mapToAliasPath($path);
|
||||
$pathHelper = $this->createPathHelper();
|
||||
|
||||
// If the input itself uses a configured path alias (legacy syntax
|
||||
// like "alias/sub/path"), resolve it first so the emitted open
|
||||
// path uses the new client-side alias format "ALIAS>relative".
|
||||
$resolvedPath = $path;
|
||||
try {
|
||||
$pathInfo = $pathHelper->getPathInfo($path, false);
|
||||
if (isset($pathInfo['path']) && is_string($pathInfo['path']) && $pathInfo['path'] !== '') {
|
||||
$resolvedPath = $pathInfo['path'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// keep original path as-is when it is not in configured roots
|
||||
}
|
||||
|
||||
$path = $pathHelper->mapToAliasPath($resolvedPath);
|
||||
} catch (\Exception $e) {
|
||||
// ignore mapping failures
|
||||
}
|
||||
@@ -145,81 +158,4 @@ class syntax_plugin_luxtools_open extends SyntaxPlugin
|
||||
$renderer->doc .= $renderer->_formatLink($link);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given path uses the blobs alias.
|
||||
*/
|
||||
protected function isBlobsPath(string $path): bool
|
||||
{
|
||||
$trimmed = ltrim($path, '/');
|
||||
return preg_match('/^blobs(\/|$)/', $trimmed) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current page's pagelink folder for the blobs alias.
|
||||
*/
|
||||
protected function resolveBlobsRoot(): string
|
||||
{
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
if ($uuid === null) return '';
|
||||
|
||||
$linkInfo = $pageLink->resolveUuid($uuid);
|
||||
$folder = $linkInfo['folder'] ?? '';
|
||||
if (!is_string($folder) || $folder === '') return '';
|
||||
|
||||
return $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the "Page not linked" message with copy ID affordance.
|
||||
*/
|
||||
protected function renderPageNotLinked(\Doku_Renderer $renderer): void
|
||||
{
|
||||
$uuid = $this->getPageUuidSafe();
|
||||
$text = (string)$this->getLang('pagelink_unlinked');
|
||||
|
||||
if ($renderer instanceof \Doku_Renderer_xhtml) {
|
||||
$renderer->doc .= '<a href="#" class="luxtools-pagelink-copy" data-luxtools-pagelink-copy="1"'
|
||||
. ' data-uuid="' . hsc($uuid) . '"'
|
||||
. '>' . hsc($text) . '</a>';
|
||||
return;
|
||||
}
|
||||
|
||||
$renderer->cdata('[n/a: ' . $text . ']');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current page UUID (if any).
|
||||
*/
|
||||
protected function getPageUuidSafe(): string
|
||||
{
|
||||
global $ID;
|
||||
$pageId = is_string($ID) ? $ID : '';
|
||||
if ($pageId === '') return '';
|
||||
|
||||
if (function_exists('cleanID')) {
|
||||
$pageId = (string)cleanID($pageId);
|
||||
}
|
||||
if ($pageId === '') return '';
|
||||
|
||||
$depth = (int)$this->getConf('pagelink_search_depth');
|
||||
if ($depth < 0) $depth = 0;
|
||||
|
||||
$pageLink = new PageLink((string)$this->getConf('paths'), $depth);
|
||||
$uuid = $pageLink->getPageUuid($pageId);
|
||||
return $uuid ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
22
vendor/autoload.php
vendored
Normal file
22
vendor/autoload.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit440454aa6bd2975652e94f60998e9adc::getLoader();
|
||||
119
vendor/bin/generate_vcards
vendored
Executable file
119
vendor/bin/generate_vcards
vendored
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../sabre/vobject/bin/generate_vcards)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/sabre/vobject/bin/generate_vcards';
|
||||
119
vendor/bin/vobject
vendored
Executable file
119
vendor/bin/vobject
vendored
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Proxy PHP file generated by Composer
|
||||
*
|
||||
* This file includes the referenced bin path (../sabre/vobject/bin/vobject)
|
||||
* using a stream wrapper to prevent the shebang from being output on PHP<8
|
||||
*
|
||||
* @generated
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
if (!class_exists('Composer\BinProxyWrapper')) {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class BinProxyWrapper
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
public function stream_read($count)
|
||||
{
|
||||
$data = fread($this->handle, $count);
|
||||
|
||||
if ($this->position === 0) {
|
||||
$data = preg_replace('{^#!.*\r?\n}', '', $data);
|
||||
}
|
||||
|
||||
$this->position += strlen($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function stream_cast($castAs)
|
||||
{
|
||||
return $this->handle;
|
||||
}
|
||||
|
||||
public function stream_close()
|
||||
{
|
||||
fclose($this->handle);
|
||||
}
|
||||
|
||||
public function stream_lock($operation)
|
||||
{
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
public function stream_eof()
|
||||
{
|
||||
return feof($this->handle);
|
||||
}
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/sabre/vobject/bin/vobject');
|
||||
}
|
||||
}
|
||||
|
||||
return include __DIR__ . '/..'.'/sabre/vobject/bin/vobject';
|
||||
579
vendor/composer/ClassLoader.php
vendored
Normal file
579
vendor/composer/ClassLoader.php
vendored
Normal file
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
396
vendor/composer/InstalledVersions.php
vendored
Normal file
396
vendor/composer/InstalledVersions.php
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
21
vendor/composer/LICENSE
vendored
Normal file
21
vendor/composer/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
10
vendor/composer/autoload_classmap.php
vendored
Normal file
10
vendor/composer/autoload_classmap.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
);
|
||||
12
vendor/composer/autoload_files.php
vendored
Normal file
12
vendor/composer/autoload_files.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
|
||||
'3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
|
||||
'93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
|
||||
);
|
||||
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
12
vendor/composer/autoload_psr4.php
vendored
Normal file
12
vendor/composer/autoload_psr4.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
|
||||
'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
|
||||
'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
|
||||
);
|
||||
50
vendor/composer/autoload_real.php
vendored
Normal file
50
vendor/composer/autoload_real.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit440454aa6bd2975652e94f60998e9adc
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit440454aa6bd2975652e94f60998e9adc', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit440454aa6bd2975652e94f60998e9adc', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit440454aa6bd2975652e94f60998e9adc::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$files;
|
||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}, null, null);
|
||||
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||
$requireFile($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
52
vendor/composer/autoload_static.php
vendored
Normal file
52
vendor/composer/autoload_static.php
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit440454aa6bd2975652e94f60998e9adc
|
||||
{
|
||||
public static $files = array (
|
||||
'383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php',
|
||||
'3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php',
|
||||
'93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Sabre\\Xml\\' => 10,
|
||||
'Sabre\\VObject\\' => 14,
|
||||
'Sabre\\Uri\\' => 10,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Sabre\\Xml\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabre/xml/lib',
|
||||
),
|
||||
'Sabre\\VObject\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabre/vobject/lib',
|
||||
),
|
||||
'Sabre\\Uri\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabre/uri/lib',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit440454aa6bd2975652e94f60998e9adc::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
248
vendor/composer/installed.json
vendored
Normal file
248
vendor/composer/installed.json
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
"version": "3.0.2",
|
||||
"version_normalized": "3.0.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/uri.git",
|
||||
"reference": "38eeab6ed9eec435a2188db489d4649c56272c51"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/uri/zipball/38eeab6ed9eec435a2188db489d4649c56272c51",
|
||||
"reference": "38eeab6ed9eec435a2188db489d4649c56272c51",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"time": "2024-09-04T15:30:08+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Uri\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Functions for making sense out of URIs.",
|
||||
"homepage": "http://sabre.io/uri/",
|
||||
"keywords": [
|
||||
"rfc3986",
|
||||
"uri",
|
||||
"url"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/uri/issues",
|
||||
"source": "https://github.com/fruux/sabre-uri"
|
||||
},
|
||||
"install-path": "../sabre/uri"
|
||||
},
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"version": "4.5.8",
|
||||
"version_normalized": "4.5.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/vobject.git",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/vobject/zipball/d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"reference": "d554eb24d64232922e1eab5896cc2f84b3b9ffb1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabre/xml": "^2.1 || ^3.0 || ^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0",
|
||||
"phpunit/php-invoker": "^2.0 || ^3.1",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6"
|
||||
},
|
||||
"suggest": {
|
||||
"hoa/bench": "If you would like to run the benchmark scripts"
|
||||
},
|
||||
"time": "2026-01-12T10:45:19+00:00",
|
||||
"bin": [
|
||||
"bin/vobject",
|
||||
"bin/generate_vcards"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0.x-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabre\\VObject\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Dominik Tobschall",
|
||||
"email": "dominik@fruux.com",
|
||||
"homepage": "http://tobschall.de/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Ivan Enderlin",
|
||||
"email": "ivan.enderlin@hoa-project.net",
|
||||
"homepage": "http://mnt.io/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||
"homepage": "http://sabre.io/vobject/",
|
||||
"keywords": [
|
||||
"availability",
|
||||
"freebusy",
|
||||
"iCalendar",
|
||||
"ical",
|
||||
"ics",
|
||||
"jCal",
|
||||
"jCard",
|
||||
"recurrence",
|
||||
"rfc2425",
|
||||
"rfc2426",
|
||||
"rfc2739",
|
||||
"rfc4770",
|
||||
"rfc5545",
|
||||
"rfc5546",
|
||||
"rfc6321",
|
||||
"rfc6350",
|
||||
"rfc6351",
|
||||
"rfc6474",
|
||||
"rfc6638",
|
||||
"rfc6715",
|
||||
"rfc6868",
|
||||
"vCalendar",
|
||||
"vCard",
|
||||
"vcf",
|
||||
"xCal",
|
||||
"xCard"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/vobject/issues",
|
||||
"source": "https://github.com/fruux/sabre-vobject"
|
||||
},
|
||||
"install-path": "../sabre/vobject"
|
||||
},
|
||||
{
|
||||
"name": "sabre/xml",
|
||||
"version": "4.0.6",
|
||||
"version_normalized": "4.0.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sabre-io/xml.git",
|
||||
"reference": "a89257fd188ce30e456b841b6915f27905dfdbe3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sabre-io/xml/zipball/a89257fd188ce30e456b841b6915f27905dfdbe3",
|
||||
"reference": "a89257fd188ce30e456b841b6915f27905dfdbe3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-xmlreader": "*",
|
||||
"ext-xmlwriter": "*",
|
||||
"lib-libxml": ">=2.6.20",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"sabre/uri": ">=2.0,<4.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"time": "2024-09-06T08:00:55+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Deserializer/functions.php",
|
||||
"lib/Serializer/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Sabre\\Xml\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab",
|
||||
"email": "markus.staab@redaxo.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "sabre/xml is an XML library that you may not hate.",
|
||||
"homepage": "https://sabre.io/xml/",
|
||||
"keywords": [
|
||||
"XMLReader",
|
||||
"XMLWriter",
|
||||
"dom",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"issues": "https://github.com/sabre-io/xml/issues",
|
||||
"source": "https://github.com/fruux/sabre-xml"
|
||||
},
|
||||
"install-path": "../sabre/xml"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
50
vendor/composer/installed.php
vendored
Normal file
50
vendor/composer/installed.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b70aae837708d2d4458a68e4dbc5801ca173048d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => 'b70aae837708d2d4458a68e4dbc5801ca173048d',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabre/uri' => array(
|
||||
'pretty_version' => '3.0.2',
|
||||
'version' => '3.0.2.0',
|
||||
'reference' => '38eeab6ed9eec435a2188db489d4649c56272c51',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabre/uri',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabre/vobject' => array(
|
||||
'pretty_version' => '4.5.8',
|
||||
'version' => '4.5.8.0',
|
||||
'reference' => 'd554eb24d64232922e1eab5896cc2f84b3b9ffb1',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabre/vobject',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabre/xml' => array(
|
||||
'pretty_version' => '4.0.6',
|
||||
'version' => '4.0.6.0',
|
||||
'reference' => 'a89257fd188ce30e456b841b6915f27905dfdbe3',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabre/xml',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
25
vendor/composer/platform_check.php
vendored
Normal file
25
vendor/composer/platform_check.php
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70400)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
);
|
||||
}
|
||||
27
vendor/sabre/uri/LICENSE
vendored
Normal file
27
vendor/sabre/uri/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (C) 2014-2019 fruux GmbH (https://fruux.com/)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name Sabre nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
68
vendor/sabre/uri/composer.json
vendored
Normal file
68
vendor/sabre/uri/composer.json
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "sabre/uri",
|
||||
"description": "Functions for making sense out of URIs.",
|
||||
"keywords": [
|
||||
"URI",
|
||||
"URL",
|
||||
"rfc3986"
|
||||
],
|
||||
"homepage": "http://sabre.io/uri/",
|
||||
"license": "BSD-3-Clause",
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Evert Pot",
|
||||
"email": "me@evertpot.com",
|
||||
"homepage": "http://evertpot.com/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/group/sabredav-discuss",
|
||||
"source": "https://github.com/fruux/sabre-uri"
|
||||
},
|
||||
"autoload": {
|
||||
"files" : [
|
||||
"lib/functions.php"
|
||||
],
|
||||
"psr-4" : {
|
||||
"Sabre\\Uri\\" : "lib/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Sabre\\Uri\\": "tests/Uri"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.64",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-strict-rules": "^1.6",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit" : "^9.6"
|
||||
},
|
||||
"scripts": {
|
||||
"phpstan": [
|
||||
"phpstan analyse lib tests"
|
||||
],
|
||||
"cs-fixer": [
|
||||
"php-cs-fixer fix"
|
||||
],
|
||||
"phpunit": [
|
||||
"phpunit --configuration tests/phpunit.xml"
|
||||
],
|
||||
"test": [
|
||||
"composer phpstan",
|
||||
"composer cs-fixer",
|
||||
"composer phpunit"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"phpstan/extension-installer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
19
vendor/sabre/uri/lib/InvalidUriException.php
vendored
Normal file
19
vendor/sabre/uri/lib/InvalidUriException.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\Uri;
|
||||
|
||||
/**
|
||||
* Invalid Uri.
|
||||
*
|
||||
* This is thrown when an attempt was made to use Sabre\Uri parse a uri that
|
||||
* it could not.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (https://evertpot.com/)
|
||||
* @license http://sabre.io/license/
|
||||
*/
|
||||
class InvalidUriException extends \Exception
|
||||
{
|
||||
}
|
||||
20
vendor/sabre/uri/lib/Version.php
vendored
Normal file
20
vendor/sabre/uri/lib/Version.php
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\Uri;
|
||||
|
||||
/**
|
||||
* This class contains the version number for this package.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/
|
||||
*/
|
||||
class Version
|
||||
{
|
||||
/**
|
||||
* Full version number.
|
||||
*/
|
||||
public const VERSION = '3.0.2';
|
||||
}
|
||||
425
vendor/sabre/uri/lib/functions.php
vendored
Normal file
425
vendor/sabre/uri/lib/functions.php
vendored
Normal file
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Sabre\Uri;
|
||||
|
||||
/**
|
||||
* This file contains all the uri handling functions.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolves relative urls, like a browser would.
|
||||
*
|
||||
* This function takes a basePath, which itself _may_ also be relative, and
|
||||
* then applies the relative path on top of it.
|
||||
*
|
||||
* @throws InvalidUriException
|
||||
*/
|
||||
function resolve(string $basePath, string $newPath): string
|
||||
{
|
||||
$delta = parse($newPath);
|
||||
|
||||
// If the new path defines a scheme, it's absolute and we can just return
|
||||
// that.
|
||||
if (null !== $delta['scheme']) {
|
||||
return build($delta);
|
||||
}
|
||||
|
||||
$base = parse($basePath);
|
||||
$pick = function ($part) use ($base, $delta) {
|
||||
if (null !== $delta[$part]) {
|
||||
return $delta[$part];
|
||||
} elseif (null !== $base[$part]) {
|
||||
return $base[$part];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
$newParts = [];
|
||||
|
||||
$newParts['scheme'] = $pick('scheme');
|
||||
$newParts['host'] = $pick('host');
|
||||
$newParts['port'] = $pick('port');
|
||||
|
||||
if (is_string($delta['path']) and strlen($delta['path']) > 0) {
|
||||
// If the path starts with a slash
|
||||
if ('/' === $delta['path'][0]) {
|
||||
$path = $delta['path'];
|
||||
} else {
|
||||
// Removing last component from base path.
|
||||
$path = (string) $base['path'];
|
||||
$length = strrpos($path, '/');
|
||||
if (false !== $length) {
|
||||
$path = substr($path, 0, $length);
|
||||
}
|
||||
$path .= '/'.$delta['path'];
|
||||
}
|
||||
} else {
|
||||
$path = $base['path'] ?? '/';
|
||||
if ('' === $path) {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
// Removing .. and .
|
||||
$pathParts = explode('/', $path);
|
||||
$newPathParts = [];
|
||||
foreach ($pathParts as $pathPart) {
|
||||
switch ($pathPart) {
|
||||
// case '' :
|
||||
case '.':
|
||||
break;
|
||||
case '..':
|
||||
array_pop($newPathParts);
|
||||
break;
|
||||
default:
|
||||
$newPathParts[] = $pathPart;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$path = implode('/', $newPathParts);
|
||||
|
||||
// If the source url ended with a /, we want to preserve that.
|
||||
$newParts['path'] = 0 === strpos($path, '/') ? $path : '/'.$path;
|
||||
// From PHP 8, no "?" query at all causes 'query' to be null.
|
||||
// An empty query "http://example.com/foo?" causes 'query' to be the empty string
|
||||
if (null !== $delta['query'] && '' !== $delta['query']) {
|
||||
$newParts['query'] = $delta['query'];
|
||||
} elseif (isset($base['query']) && null === $delta['host'] && null === $delta['path']) {
|
||||
// Keep the old query if host and path didn't change
|
||||
$newParts['query'] = $base['query'];
|
||||
}
|
||||
// From PHP 8, no "#" fragment at all causes 'fragment' to be null.
|
||||
// An empty fragment "http://example.com/foo#" causes 'fragment' to be the empty string
|
||||
if (null !== $delta['fragment'] && '' !== $delta['fragment']) {
|
||||
$newParts['fragment'] = $delta['fragment'];
|
||||
}
|
||||
|
||||
return build($newParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a URI or partial URI as its argument, and normalizes it.
|
||||
*
|
||||
* After normalizing a URI, you can safely compare it to other URIs.
|
||||
* This function will for instance convert a %7E into a tilde, according to
|
||||
* rfc3986.
|
||||
*
|
||||
* It will also change a %3a into a %3A.
|
||||
*
|
||||
* @throws InvalidUriException
|
||||
*/
|
||||
function normalize(string $uri): string
|
||||
{
|
||||
$parts = parse($uri);
|
||||
|
||||
if (null !== $parts['path']) {
|
||||
$pathParts = explode('/', ltrim($parts['path'], '/'));
|
||||
$newPathParts = [];
|
||||
foreach ($pathParts as $pathPart) {
|
||||
switch ($pathPart) {
|
||||
case '.':
|
||||
// skip
|
||||
break;
|
||||
case '..':
|
||||
// One level up in the hierarchy
|
||||
array_pop($newPathParts);
|
||||
break;
|
||||
default:
|
||||
// Ensuring that everything is correctly percent-encoded.
|
||||
$newPathParts[] = rawurlencode(rawurldecode($pathPart));
|
||||
break;
|
||||
}
|
||||
}
|
||||
$parts['path'] = '/'.implode('/', $newPathParts);
|
||||
}
|
||||
|
||||
if (null !== $parts['scheme']) {
|
||||
$parts['scheme'] = strtolower($parts['scheme']);
|
||||
$defaultPorts = [
|
||||
'http' => '80',
|
||||
'https' => '443',
|
||||
];
|
||||
|
||||
if (null !== $parts['port'] && isset($defaultPorts[$parts['scheme']]) && $defaultPorts[$parts['scheme']] == $parts['port']) {
|
||||
// Removing default ports.
|
||||
unset($parts['port']);
|
||||
}
|
||||
// A few HTTP specific rules.
|
||||
switch ($parts['scheme']) {
|
||||
case 'http':
|
||||
case 'https':
|
||||
if (null === $parts['path']) {
|
||||
// An empty path is equivalent to / in http.
|
||||
$parts['path'] = '/';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $parts['host']) {
|
||||
$parts['host'] = strtolower($parts['host']);
|
||||
}
|
||||
|
||||
return build($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a URI and returns its individual components.
|
||||
*
|
||||
* This method largely behaves the same as PHP's parse_url, except that it will
|
||||
* return an array with all the array keys, including the ones that are not
|
||||
* set by parse_url, which makes it a bit easier to work with.
|
||||
*
|
||||
* Unlike PHP's parse_url, it will also convert any non-ascii characters to
|
||||
* percent-encoded strings. PHP's parse_url corrupts these characters on OS X.
|
||||
*
|
||||
* In the return array, key "port" is an int value. Other keys have a string value.
|
||||
* "Unused" keys have value null.
|
||||
*
|
||||
* @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null}
|
||||
*
|
||||
* @throws InvalidUriException
|
||||
*/
|
||||
function parse(string $uri): array
|
||||
{
|
||||
// Normally a URI must be ASCII. However, often it's not and
|
||||
// parse_url might corrupt these strings.
|
||||
//
|
||||
// For that reason we take any non-ascii characters from the uri and
|
||||
// uriencode them first.
|
||||
$uri = preg_replace_callback(
|
||||
'/[^[:ascii:]]/u',
|
||||
function ($matches) {
|
||||
return rawurlencode($matches[0]);
|
||||
},
|
||||
$uri
|
||||
);
|
||||
|
||||
if (null === $uri) {
|
||||
throw new InvalidUriException('Invalid, or could not parse URI');
|
||||
}
|
||||
|
||||
$result = parse_url($uri);
|
||||
if (false === $result) {
|
||||
$result = _parse_fallback($uri);
|
||||
} else {
|
||||
// Add empty host and leading slash to Windows file paths
|
||||
// file:///C:/path or file:///C:\path
|
||||
// Note: the regex fragment [a-zA-Z]:[\/\\\\].* end up being
|
||||
// [a-zA-Z]:[\/\\].*
|
||||
// The 4 backslash in a row are the way to get 2 backslash into the actual string
|
||||
// that is used as the regex. The 2 backslash are then the way to get 1 backslash
|
||||
// character into the character set "a forward slash or a backslash"
|
||||
if (isset($result['scheme']) && 'file' === $result['scheme'] && isset($result['path'])
|
||||
&& 1 === preg_match('/^(?<windows_path> [a-zA-Z]:([\/\\\\].*)?)$/x', $result['path'])) {
|
||||
$result['path'] = '/'.$result['path'];
|
||||
$result['host'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* phpstan is not able to process all the things that happen while this function
|
||||
* constructs the result array. It only understands the $result is
|
||||
* non-empty-array<string, mixed>
|
||||
*
|
||||
* But the detail of the returned array is correctly specified in the PHPdoc
|
||||
* above the function call.
|
||||
*
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
return
|
||||
$result + [
|
||||
'scheme' => null,
|
||||
'host' => null,
|
||||
'path' => null,
|
||||
'port' => null,
|
||||
'user' => null,
|
||||
'query' => null,
|
||||
'fragment' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This function takes the components returned from PHP's parse_url, and uses
|
||||
* it to generate a new uri.
|
||||
*
|
||||
* @param array<string, int|string|null> $parts
|
||||
*/
|
||||
function build(array $parts): string
|
||||
{
|
||||
$uri = '';
|
||||
|
||||
$authority = '';
|
||||
if (isset($parts['host'])) {
|
||||
$authority = $parts['host'];
|
||||
if (isset($parts['user'])) {
|
||||
$authority = $parts['user'].'@'.$authority;
|
||||
}
|
||||
if (isset($parts['port'])) {
|
||||
$authority = $authority.':'.$parts['port'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parts['scheme'])) {
|
||||
// If there's a scheme, there's also a host.
|
||||
$uri = $parts['scheme'].':';
|
||||
}
|
||||
if ('' !== $authority || (isset($parts['scheme']) && 'file' === $parts['scheme'])) {
|
||||
// No scheme, but there is a host.
|
||||
$uri .= '//'.$authority;
|
||||
}
|
||||
|
||||
if (isset($parts['path'])) {
|
||||
$uri .= $parts['path'];
|
||||
}
|
||||
if (isset($parts['query'])) {
|
||||
$uri .= '?'.$parts['query'];
|
||||
}
|
||||
if (isset($parts['fragment'])) {
|
||||
$uri .= '#'.$parts['fragment'];
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'dirname' and 'basename' for a path.
|
||||
*
|
||||
* The reason there is a custom function for this purpose, is because
|
||||
* basename() is locale aware (behaviour changes if C locale or a UTF-8 locale
|
||||
* is used) and we need a method that just operates on UTF-8 characters.
|
||||
*
|
||||
* In addition basename and dirname are platform aware, and will treat
|
||||
* backslash (\) as a directory separator on Windows.
|
||||
*
|
||||
* This method returns the 2 components as an array.
|
||||
*
|
||||
* If there is no dirname, it will return an empty string. Any / appearing at
|
||||
* the end of the string is stripped off.
|
||||
*
|
||||
* @return list<mixed>
|
||||
*/
|
||||
function split(string $path): array
|
||||
{
|
||||
$matches = [];
|
||||
if (1 === preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u', $path, $matches)) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
|
||||
return [null, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is another implementation of parse_url, except this one is
|
||||
* fully written in PHP.
|
||||
*
|
||||
* The reason is that the PHP bug team is not willing to admit that there are
|
||||
* bugs in the parse_url implementation.
|
||||
*
|
||||
* This function is only called if the main parse method fails. It's pretty
|
||||
* crude and probably slow, so the original parse_url is usually preferred.
|
||||
*
|
||||
* @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null}
|
||||
*
|
||||
* @throws InvalidUriException
|
||||
*/
|
||||
function _parse_fallback(string $uri): array
|
||||
{
|
||||
// Normally a URI must be ASCII, however. However, often it's not and
|
||||
// parse_url might corrupt these strings.
|
||||
//
|
||||
// For that reason we take any non-ascii characters from the uri and
|
||||
// uriencode them first.
|
||||
$uri = preg_replace_callback(
|
||||
'/[^[:ascii:]]/u',
|
||||
function ($matches) {
|
||||
return rawurlencode($matches[0]);
|
||||
},
|
||||
$uri
|
||||
);
|
||||
|
||||
if (null === $uri) {
|
||||
throw new InvalidUriException('Invalid, or could not parse URI');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'scheme' => null,
|
||||
'host' => null,
|
||||
'port' => null,
|
||||
'user' => null,
|
||||
'path' => null,
|
||||
'fragment' => null,
|
||||
'query' => null,
|
||||
];
|
||||
|
||||
if (1 === preg_match('% ^([A-Za-z][A-Za-z0-9+-\.]+): %x', $uri, $matches)) {
|
||||
$result['scheme'] = $matches[1];
|
||||
// Take what's left.
|
||||
$uri = substr($uri, strlen($result['scheme']) + 1);
|
||||
if (false === $uri) {
|
||||
// There was nothing left.
|
||||
$uri = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Taking off a fragment part
|
||||
if (false !== strpos($uri, '#')) {
|
||||
list($uri, $result['fragment']) = explode('#', $uri, 2);
|
||||
}
|
||||
// Taking off the query part
|
||||
if (false !== strpos($uri, '?')) {
|
||||
list($uri, $result['query']) = explode('?', $uri, 2);
|
||||
}
|
||||
|
||||
if ('///' === substr($uri, 0, 3)) {
|
||||
// The triple slash uris are a bit unusual, but we have special handling
|
||||
// for them.
|
||||
$path = substr($uri, 2);
|
||||
if (false === $path) {
|
||||
throw new \RuntimeException('The string cannot be false');
|
||||
}
|
||||
$result['path'] = $path;
|
||||
$result['host'] = '';
|
||||
} elseif ('//' === substr($uri, 0, 2)) {
|
||||
// Uris that have an authority part.
|
||||
$regex = '%^
|
||||
//
|
||||
(?: (?<user> [^:@]+) (: (?<pass> [^@]+)) @)?
|
||||
(?<host> ( [^:/]* | \[ [^\]]+ \] ))
|
||||
(?: : (?<port> [0-9]+))?
|
||||
(?<path> / .*)?
|
||||
$%x';
|
||||
if (1 !== preg_match($regex, $uri, $matches)) {
|
||||
throw new InvalidUriException('Invalid, or could not parse URI');
|
||||
}
|
||||
if (isset($matches['host']) && '' !== $matches['host']) {
|
||||
$result['host'] = $matches['host'];
|
||||
}
|
||||
if (isset($matches['port'])) {
|
||||
$port = (int) $matches['port'];
|
||||
if ($port > 0) {
|
||||
$result['port'] = $port;
|
||||
}
|
||||
}
|
||||
if (isset($matches['path'])) {
|
||||
$result['path'] = $matches['path'];
|
||||
}
|
||||
if (isset($matches['user']) && '' !== $matches['user']) {
|
||||
$result['user'] = $matches['user'];
|
||||
}
|
||||
if (isset($matches['pass']) && '' !== $matches['pass']) {
|
||||
$result['pass'] = $matches['pass'];
|
||||
}
|
||||
} else {
|
||||
$result['path'] = $uri;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
27
vendor/sabre/vobject/LICENSE
vendored
Normal file
27
vendor/sabre/vobject/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/)
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name Sabre nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
55
vendor/sabre/vobject/README.md
vendored
Normal file
55
vendor/sabre/vobject/README.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
sabre/vobject
|
||||
=============
|
||||
|
||||
The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545)
|
||||
and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP.
|
||||
|
||||
The goal of the VObject library is to create a very complete library, with an easy-to-use API.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Make sure you have [Composer][1] installed, and then run:
|
||||
|
||||
composer require sabre/vobject "^4.0"
|
||||
|
||||
This package requires PHP 5.5. If you need the PHP 5.3/5.4 version of this package instead, use:
|
||||
|
||||
|
||||
composer require sabre/vobject "^3.4"
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
* [Working with vCards](http://sabre.io/vobject/vcard/)
|
||||
* [Working with iCalendar](http://sabre.io/vobject/icalendar/)
|
||||
|
||||
|
||||
|
||||
Build status
|
||||
------------
|
||||
|
||||
| branch | status |
|
||||
| ------ | ------ |
|
||||
| master | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
| 3.5 | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
| 3.4 | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
| 3.1 | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
| 2.1 | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
| 2.0 | [](https://travis-ci.org/sabre-io/vobject) |
|
||||
|
||||
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions.
|
||||
|
||||
Made at fruux
|
||||
-------------
|
||||
|
||||
This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
|
||||
|
||||
[1]: https://getcomposer.org/
|
||||
12
vendor/sabre/vobject/bin/bench.php
vendored
Executable file
12
vendor/sabre/vobject/bin/bench.php
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$data = stream_get_contents(STDIN);
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
$lol = Sabre\VObject\Reader::read($data);
|
||||
|
||||
echo 'time: '.(microtime(true) - $start)."\n";
|
||||
53
vendor/sabre/vobject/bin/bench_freebusygenerator.php
vendored
Normal file
53
vendor/sabre/vobject/bin/bench_freebusygenerator.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
if ($argc < 2) {
|
||||
echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " freebusy benchmark\n";
|
||||
echo "\n";
|
||||
echo "This script can be used to measure the speed of generating a\n";
|
||||
echo "free-busy report based on a calendar.\n";
|
||||
echo "\n";
|
||||
echo "The process will be repeated 100 times to get accurate stats\n";
|
||||
echo "\n";
|
||||
echo 'Usage: '.$argv[0]." inputfile.ics\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
list(, $inputFile) = $argv;
|
||||
|
||||
$bench = new Hoa\Bench\Bench();
|
||||
$bench->parse->start();
|
||||
|
||||
$vcal = Sabre\VObject\Reader::read(fopen($inputFile, 'r'));
|
||||
|
||||
$bench->parse->stop();
|
||||
|
||||
$repeat = 100;
|
||||
$start = new \DateTime('2000-01-01');
|
||||
$end = new \DateTime('2020-01-01');
|
||||
$timeZone = new \DateTimeZone('America/Toronto');
|
||||
|
||||
$bench->fb->start();
|
||||
|
||||
for ($i = 0; $i < $repeat; ++$i) {
|
||||
$fb = new Sabre\VObject\FreeBusyGenerator($start, $end, $vcal, $timeZone);
|
||||
$results = $fb->getResult();
|
||||
}
|
||||
$bench->fb->stop();
|
||||
|
||||
echo $bench,"\n";
|
||||
|
||||
function formatMemory($input)
|
||||
{
|
||||
if (strlen($input) > 6) {
|
||||
return round($input / (1024 * 1024)).'M';
|
||||
} elseif (strlen($input) > 3) {
|
||||
return round($input / 1024).'K';
|
||||
}
|
||||
}
|
||||
|
||||
unset($input, $splitter);
|
||||
|
||||
echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n";
|
||||
echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n";
|
||||
64
vendor/sabre/vobject/bin/bench_manipulatevcard.php
vendored
Normal file
64
vendor/sabre/vobject/bin/bench_manipulatevcard.php
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
if ($argc < 2) {
|
||||
echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " manipulation benchmark\n";
|
||||
echo "\n";
|
||||
echo "This script can be used to measure the speed of opening a large amount of\n";
|
||||
echo "vcards, making a few alterations and serializing them again.\n";
|
||||
echo 'system.';
|
||||
echo "\n";
|
||||
echo 'Usage: '.$argv[0]." inputfile.vcf\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
list(, $inputFile) = $argv;
|
||||
|
||||
$input = file_get_contents($inputFile);
|
||||
|
||||
$splitter = new Sabre\VObject\Splitter\VCard($input);
|
||||
|
||||
$bench = new Hoa\Bench\Bench();
|
||||
|
||||
while (true) {
|
||||
$bench->parse->start();
|
||||
$vcard = $splitter->getNext();
|
||||
$bench->parse->pause();
|
||||
|
||||
if (!$vcard) {
|
||||
break;
|
||||
}
|
||||
|
||||
$bench->manipulate->start();
|
||||
$vcard->{'X-FOO'} = 'Random new value!';
|
||||
$emails = [];
|
||||
if (isset($vcard->EMAIL)) {
|
||||
foreach ($vcard->EMAIL as $email) {
|
||||
$emails[] = (string) $email;
|
||||
}
|
||||
}
|
||||
$bench->manipulate->pause();
|
||||
|
||||
$bench->serialize->start();
|
||||
$vcard2 = $vcard->serialize();
|
||||
$bench->serialize->pause();
|
||||
|
||||
$vcard->destroy();
|
||||
}
|
||||
|
||||
echo $bench,"\n";
|
||||
|
||||
function formatMemory($input)
|
||||
{
|
||||
if (strlen($input) > 6) {
|
||||
return round($input / (1024 * 1024)).'M';
|
||||
} elseif (strlen($input) > 3) {
|
||||
return round($input / 1024).'K';
|
||||
}
|
||||
}
|
||||
|
||||
unset($input, $splitter);
|
||||
|
||||
echo 'peak memory usage: '.formatMemory(memory_get_peak_usage()), "\n";
|
||||
echo 'current memory usage: '.formatMemory(memory_get_usage()), "\n";
|
||||
48
vendor/sabre/vobject/bin/fetch_windows_zones.php
vendored
Executable file
48
vendor/sabre/vobject/bin/fetch_windows_zones.php
vendored
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$windowsZonesUrl = 'https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml';
|
||||
$outputFile = __DIR__.'/../lib/timezonedata/windowszones.php';
|
||||
|
||||
echo 'Fetching timezone map from: '.$windowsZonesUrl, "\n";
|
||||
|
||||
$data = file_get_contents($windowsZonesUrl);
|
||||
$xml = simplexml_load_string($data);
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($xml->xpath('//mapZone') as $mapZone) {
|
||||
$from = (string) $mapZone['other'];
|
||||
$to = (string) $mapZone['type'];
|
||||
|
||||
list($to) = explode(' ', $to, 2);
|
||||
|
||||
if (!isset($map[$from])) {
|
||||
$map[$from] = $to;
|
||||
}
|
||||
}
|
||||
|
||||
ksort($map);
|
||||
echo "Writing to: $outputFile\n";
|
||||
|
||||
$f = fopen($outputFile, 'w');
|
||||
fwrite($f, "<?php\n\n");
|
||||
fwrite($f, "/**\n");
|
||||
fwrite($f, " * Automatically generated timezone file\n");
|
||||
fwrite($f, " *\n");
|
||||
fwrite($f, ' * Last update: '.date(DATE_W3C)."\n");
|
||||
fwrite($f, ' * Source: '.$windowsZonesUrl."\n");
|
||||
fwrite($f, " *\n");
|
||||
fwrite($f, " * @copyright Copyright (C) fruux GmbH (https://fruux.com/).\n");
|
||||
fwrite($f, " * @license http://sabre.io/license/ Modified BSD License\n");
|
||||
fwrite($f, " */\n");
|
||||
fwrite($f, "\n");
|
||||
fwrite($f, 'return ');
|
||||
fwrite($f, var_export($map, true).';');
|
||||
fclose($f);
|
||||
|
||||
echo "Formatting\n";
|
||||
|
||||
exec(__DIR__.'/../vendor/bin/php-cs-fixer fix '.escapeshellarg($outputFile));
|
||||
|
||||
echo "Done\n";
|
||||
241
vendor/sabre/vobject/bin/generate_vcards
vendored
Executable file
241
vendor/sabre/vobject/bin/generate_vcards
vendored
Executable file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
// This sucks.. we have to try to find the composer autoloader. But chances
|
||||
// are, we can't find it this way. So we'll do our bestest
|
||||
$paths = [
|
||||
__DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly
|
||||
__DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency.
|
||||
];
|
||||
|
||||
foreach($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('Sabre\\VObject\\Version')) {
|
||||
fwrite(STDERR, "Composer autoloader could not be properly loaded.\n");
|
||||
die(1);
|
||||
}
|
||||
|
||||
if ($argc < 2) {
|
||||
|
||||
$version = Version::VERSION;
|
||||
|
||||
$help = <<<HI
|
||||
sabre/vobject $version
|
||||
Usage:
|
||||
generate_vcards [count]
|
||||
|
||||
Options:
|
||||
count The number of random vcards to generate
|
||||
|
||||
Examples:
|
||||
generate_vcards 1000 > testdata.vcf
|
||||
|
||||
HI;
|
||||
|
||||
fwrite(STDERR, $help);
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$count = (int)$argv[1];
|
||||
if ($count < 1) {
|
||||
fwrite(STDERR, "Count must be at least 1\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "sabre/vobject " . Version::VERSION . "\n");
|
||||
fwrite(STDERR, "Generating " . $count . " vcards in vCard 4.0 format\n");
|
||||
|
||||
/**
|
||||
* The following list is just some random data we compiled from various
|
||||
* sources online.
|
||||
*
|
||||
* Very little thought went into compiling this list, and certainly nothing
|
||||
* political or ethical.
|
||||
*
|
||||
* We would _love_ more additions to this to add more variation to this list.
|
||||
*
|
||||
* Send us PR's and don't be shy adding your own first and last name for fun.
|
||||
*/
|
||||
|
||||
$sets = array(
|
||||
"nl" => array(
|
||||
"country" => "Netherlands",
|
||||
"boys" => array(
|
||||
"Anno",
|
||||
"Bram",
|
||||
"Daan",
|
||||
"Evert",
|
||||
"Finn",
|
||||
"Jayden",
|
||||
"Jens",
|
||||
"Jesse",
|
||||
"Levi",
|
||||
"Lucas",
|
||||
"Luuk",
|
||||
"Milan",
|
||||
"René",
|
||||
"Sem",
|
||||
"Sibrand",
|
||||
"Willem",
|
||||
),
|
||||
"girls" => array(
|
||||
"Celia",
|
||||
"Emma",
|
||||
"Fenna",
|
||||
"Geke",
|
||||
"Inge",
|
||||
"Julia",
|
||||
"Lisa",
|
||||
"Lotte",
|
||||
"Mila",
|
||||
"Sara",
|
||||
"Sophie",
|
||||
"Tess",
|
||||
"Zoë",
|
||||
),
|
||||
"last" => array(
|
||||
"Bakker",
|
||||
"Bos",
|
||||
"De Boer",
|
||||
"De Groot",
|
||||
"De Jong",
|
||||
"De Vries",
|
||||
"Jansen",
|
||||
"Janssen",
|
||||
"Meyer",
|
||||
"Mulder",
|
||||
"Peters",
|
||||
"Smit",
|
||||
"Van Dijk",
|
||||
"Van den Berg",
|
||||
"Visser",
|
||||
"Vos",
|
||||
),
|
||||
),
|
||||
"us" => array(
|
||||
"country" => "United States",
|
||||
"boys" => array(
|
||||
"Aiden",
|
||||
"Alexander",
|
||||
"Charles",
|
||||
"David",
|
||||
"Ethan",
|
||||
"Jacob",
|
||||
"James",
|
||||
"Jayden",
|
||||
"John",
|
||||
"Joseph",
|
||||
"Liam",
|
||||
"Mason",
|
||||
"Michael",
|
||||
"Noah",
|
||||
"Richard",
|
||||
"Robert",
|
||||
"Thomas",
|
||||
"William",
|
||||
),
|
||||
"girls" => array(
|
||||
"Ava",
|
||||
"Barbara",
|
||||
"Chloe",
|
||||
"Dorothy",
|
||||
"Elizabeth",
|
||||
"Emily",
|
||||
"Emma",
|
||||
"Isabella",
|
||||
"Jennifer",
|
||||
"Lily",
|
||||
"Linda",
|
||||
"Margaret",
|
||||
"Maria",
|
||||
"Mary",
|
||||
"Mia",
|
||||
"Olivia",
|
||||
"Patricia",
|
||||
"Roxy",
|
||||
"Sophia",
|
||||
"Susan",
|
||||
"Zoe",
|
||||
),
|
||||
"last" => array(
|
||||
"Smith",
|
||||
"Johnson",
|
||||
"Williams",
|
||||
"Jones",
|
||||
"Brown",
|
||||
"Davis",
|
||||
"Miller",
|
||||
"Wilson",
|
||||
"Moore",
|
||||
"Taylor",
|
||||
"Anderson",
|
||||
"Thomas",
|
||||
"Jackson",
|
||||
"White",
|
||||
"Harris",
|
||||
"Martin",
|
||||
"Thompson",
|
||||
"Garcia",
|
||||
"Martinez",
|
||||
"Robinson",
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$current = 0;
|
||||
|
||||
$r = function($arr) {
|
||||
|
||||
return $arr[mt_rand(0,count($arr)-1)];
|
||||
|
||||
};
|
||||
|
||||
$bdayStart = strtotime('-85 years');
|
||||
$bdayEnd = strtotime('-20 years');
|
||||
|
||||
while($current < $count) {
|
||||
|
||||
$current++;
|
||||
fwrite(STDERR, "\033[100D$current/$count");
|
||||
|
||||
$country = array_rand($sets);
|
||||
$gender = mt_rand(0,1)?'girls':'boys';
|
||||
|
||||
$vcard = new Component\VCard(array(
|
||||
'VERSION' => '4.0',
|
||||
'FN' => $r($sets[$country][$gender]) . ' ' . $r($sets[$country]['last']),
|
||||
'UID' => UUIDUtil::getUUID(),
|
||||
));
|
||||
|
||||
$bdayRatio = mt_rand(0,9);
|
||||
|
||||
if($bdayRatio < 2) {
|
||||
// 20% has a birthday property with a full date
|
||||
$dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
|
||||
$vcard->add('BDAY', $dt->format('Ymd'));
|
||||
|
||||
} elseif ($bdayRatio < 3) {
|
||||
// 10% we only know the month and date of
|
||||
$dt = new \DateTime('@' . mt_rand($bdayStart, $bdayEnd));
|
||||
$vcard->add('BDAY', '--' . $dt->format('md'));
|
||||
}
|
||||
if ($result = $vcard->validate()) {
|
||||
ob_start();
|
||||
echo "\nWe produced an invalid vcard somehow!\n";
|
||||
foreach($result as $message) {
|
||||
echo " " . $message['message'] . "\n";
|
||||
}
|
||||
fwrite(STDERR, ob_get_clean());
|
||||
}
|
||||
echo $vcard->serialize();
|
||||
|
||||
}
|
||||
|
||||
fwrite(STDERR,"\nDone.\n");
|
||||
87
vendor/sabre/vobject/bin/generateicalendardata.php
vendored
Executable file
87
vendor/sabre/vobject/bin/generateicalendardata.php
vendored
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Sabre\VObject;
|
||||
|
||||
if ($argc < 2) {
|
||||
$cmd = $argv[0];
|
||||
fwrite(STDERR, <<<HI
|
||||
Fruux test data generator
|
||||
|
||||
This script generates a lot of test data. This is used for profiling and stuff.
|
||||
Currently it just generates events in a single calendar.
|
||||
|
||||
The iCalendar output goes to stdout. Other messages to stderr.
|
||||
|
||||
{$cmd} [events]
|
||||
|
||||
|
||||
HI
|
||||
);
|
||||
exit();
|
||||
}
|
||||
|
||||
$events = 100;
|
||||
|
||||
if (isset($argv[1])) {
|
||||
$events = (int) $argv[1];
|
||||
}
|
||||
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
fwrite(STDERR, 'Generating '.$events." events\n");
|
||||
|
||||
$currentDate = new DateTime('-'.round($events / 2).' days');
|
||||
|
||||
$calendar = new VObject\Component\VCalendar();
|
||||
|
||||
$ii = 0;
|
||||
|
||||
while ($ii < $events) {
|
||||
++$ii;
|
||||
|
||||
$event = $calendar->add('VEVENT');
|
||||
$event->DTSTART = 'bla';
|
||||
$event->SUMMARY = 'Event #'.$ii;
|
||||
$event->UID = md5(microtime(true));
|
||||
|
||||
$doctorRandom = mt_rand(1, 1000);
|
||||
|
||||
switch ($doctorRandom) {
|
||||
// All-day event
|
||||
case 1:
|
||||
$event->DTEND = 'bla';
|
||||
$dtStart = clone $currentDate;
|
||||
$dtEnd = clone $currentDate;
|
||||
$dtEnd->modify('+'.mt_rand(1, 3).' days');
|
||||
$event->DTSTART->setDateTime($dtStart);
|
||||
$event->DTSTART['VALUE'] = 'DATE';
|
||||
$event->DTEND->setDateTime($dtEnd);
|
||||
break;
|
||||
case 2:
|
||||
$event->RRULE = 'FREQ=DAILY;COUNT='.mt_rand(1, 10);
|
||||
// no break intentional
|
||||
default:
|
||||
$dtStart = clone $currentDate;
|
||||
$dtStart->setTime(mt_rand(1, 23), mt_rand(0, 59), mt_rand(0, 59));
|
||||
$event->DTSTART->setDateTime($dtStart);
|
||||
$event->DURATION = 'PT'.mt_rand(1, 3).'H';
|
||||
break;
|
||||
}
|
||||
|
||||
$currentDate->modify('+ '.mt_rand(0, 3).' days');
|
||||
}
|
||||
fwrite(STDERR, "Validating\n");
|
||||
|
||||
$result = $calendar->validate();
|
||||
if ($result) {
|
||||
fwrite(STDERR, "Errors!\n");
|
||||
fwrite(STDERR, print_r($result, true));
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
fwrite(STDERR, "Serializing this beast\n");
|
||||
|
||||
echo $calendar->serialize();
|
||||
|
||||
fwrite(STDERR, "done.\n");
|
||||
160
vendor/sabre/vobject/bin/mergeduplicates.php
vendored
Executable file
160
vendor/sabre/vobject/bin/mergeduplicates.php
vendored
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
// This sucks.. we have to try to find the composer autoloader. But chances
|
||||
// are, we can't find it this way. So we'll do our bestest
|
||||
$paths = [
|
||||
__DIR__.'/../vendor/autoload.php', // In case vobject is cloned directly
|
||||
__DIR__.'/../../../autoload.php', // In case vobject is a composer dependency.
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('Sabre\\VObject\\Version')) {
|
||||
fwrite(STDERR, "Composer autoloader could not be loaded.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo 'sabre/vobject ', Version::VERSION, " duplicate contact merge tool\n";
|
||||
|
||||
if ($argc < 3) {
|
||||
echo "\n";
|
||||
echo 'Usage: ', $argv[0], " input.vcf output.vcf [debug.log]\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$input = fopen($argv[1], 'r');
|
||||
$output = fopen($argv[2], 'w');
|
||||
$debug = isset($argv[3]) ? fopen($argv[3], 'w') : null;
|
||||
|
||||
$splitter = new Splitter\VCard($input);
|
||||
|
||||
// The following properties are ignored. If they appear in some vcards
|
||||
// but not in others, we don't consider them for the sake of finding
|
||||
// differences.
|
||||
$ignoredProperties = [
|
||||
'PRODID',
|
||||
'VERSION',
|
||||
'REV',
|
||||
'UID',
|
||||
'X-ABLABEL',
|
||||
];
|
||||
|
||||
$collectedNames = [];
|
||||
|
||||
$stats = [
|
||||
'Total vcards' => 0,
|
||||
'No FN property' => 0,
|
||||
'Ignored duplicates' => 0,
|
||||
'Merged values' => 0,
|
||||
'Error' => 0,
|
||||
'Unique cards' => 0,
|
||||
'Total written' => 0,
|
||||
];
|
||||
|
||||
function writeStats()
|
||||
{
|
||||
global $stats;
|
||||
foreach ($stats as $name => $value) {
|
||||
echo str_pad($name, 23, ' ', STR_PAD_RIGHT), str_pad($value, 6, ' ', STR_PAD_LEFT), "\n";
|
||||
}
|
||||
// Moving cursor back a few lines.
|
||||
echo "\033[".count($stats).'A';
|
||||
}
|
||||
|
||||
function write($vcard)
|
||||
{
|
||||
global $stats, $output;
|
||||
|
||||
++$stats['Total written'];
|
||||
fwrite($output, $vcard->serialize()."\n");
|
||||
}
|
||||
|
||||
while ($vcard = $splitter->getNext()) {
|
||||
++$stats['Total vcards'];
|
||||
writeStats();
|
||||
|
||||
$fn = isset($vcard->FN) ? (string) $vcard->FN : null;
|
||||
|
||||
if (empty($fn)) {
|
||||
// Immediately write this vcard, we don't compare it.
|
||||
++$stats['No FN property'];
|
||||
++$stats['Unique cards'];
|
||||
write($vcard);
|
||||
$vcard->destroy();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($collectedNames[$fn])) {
|
||||
$collectedNames[$fn] = $vcard;
|
||||
++$stats['Unique cards'];
|
||||
continue;
|
||||
} else {
|
||||
// Starting comparison for all properties. We only check if properties
|
||||
// in the current vcard exactly appear in the earlier vcard as well.
|
||||
foreach ($vcard->children() as $newProp) {
|
||||
if (in_array($newProp->name, $ignoredProperties)) {
|
||||
// We don't care about properties such as UID and REV.
|
||||
continue;
|
||||
}
|
||||
$ok = false;
|
||||
foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
|
||||
if ($compareProp->serialize() === $newProp->serialize()) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
if ('EMAIL' === $newProp->name || 'TEL' === $newProp->name) {
|
||||
// We're going to make another attempt to find this
|
||||
// property, this time just by value. If we find it, we
|
||||
// consider it a success.
|
||||
foreach ($collectedNames[$fn]->select($newProp->name) as $compareProp) {
|
||||
if ($compareProp->getValue() === $newProp->getValue()) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
// Merging the new value in the old vcard.
|
||||
$collectedNames[$fn]->add(clone $newProp);
|
||||
$ok = true;
|
||||
++$stats['Merged values'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
// echo $newProp->serialize() . " does not appear in earlier vcard!\n";
|
||||
++$stats['Error'];
|
||||
if ($debug) {
|
||||
fwrite($debug, "Missing '".$newProp->name."' property in duplicate. Earlier vcard:\n".$collectedNames[$fn]->serialize()."\n\nLater:\n".$vcard->serialize()."\n\n");
|
||||
}
|
||||
|
||||
$vcard->destroy();
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vcard->destroy();
|
||||
++$stats['Ignored duplicates'];
|
||||
}
|
||||
|
||||
foreach ($collectedNames as $vcard) {
|
||||
// Overwriting any old PRODID
|
||||
$vcard->PRODID = '-//Sabre//Sabre VObject '.Version::VERSION.'//EN';
|
||||
write($vcard);
|
||||
writeStats();
|
||||
}
|
||||
|
||||
echo str_repeat("\n", count($stats)), "\nDone.\n";
|
||||
32
vendor/sabre/vobject/bin/rrulebench.php
vendored
Normal file
32
vendor/sabre/vobject/bin/rrulebench.php
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
include __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
if ($argc < 4) {
|
||||
echo 'sabre/vobject ', Sabre\VObject\Version::VERSION, " RRULE benchmark\n";
|
||||
echo "\n";
|
||||
echo "This script can be used to measure the speed of the 'recurrence expansion'\n";
|
||||
echo 'system.';
|
||||
echo "\n";
|
||||
echo 'Usage: '.$argv[0]." inputfile.ics startdate enddate\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
list(, $inputFile, $startDate, $endDate) = $argv;
|
||||
|
||||
$bench = new Hoa\Bench\Bench();
|
||||
$bench->parse->start();
|
||||
|
||||
echo "Parsing.\n";
|
||||
$vobj = Sabre\VObject\Reader::read(fopen($inputFile, 'r'));
|
||||
|
||||
$bench->parse->stop();
|
||||
|
||||
echo "Expanding.\n";
|
||||
$bench->expand->start();
|
||||
|
||||
$vobj->expand(new DateTime($startDate), new DateTime($endDate));
|
||||
|
||||
$bench->expand->stop();
|
||||
|
||||
echo $bench,"\n";
|
||||
27
vendor/sabre/vobject/bin/vobject
vendored
Executable file
27
vendor/sabre/vobject/bin/vobject
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
// This sucks.. we have to try to find the composer autoloader. But chances
|
||||
// are, we can't find it this way. So we'll do our bestest
|
||||
$paths = [
|
||||
__DIR__ . '/../vendor/autoload.php', // In case vobject is cloned directly
|
||||
__DIR__ . '/../../../autoload.php', // In case vobject is a composer dependency.
|
||||
];
|
||||
|
||||
foreach($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
include $path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!class_exists('Sabre\\VObject\\Version')) {
|
||||
fwrite(STDERR, "Composer autoloader could not be loaded.\n");
|
||||
die(1);
|
||||
}
|
||||
|
||||
$cli = new Cli();
|
||||
exit($cli->main($argv));
|
||||
|
||||
107
vendor/sabre/vobject/composer.json
vendored
Normal file
107
vendor/sabre/vobject/composer.json
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"name": "sabre/vobject",
|
||||
"description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
|
||||
"keywords" : [
|
||||
"iCalendar",
|
||||
"iCal",
|
||||
"vCalendar",
|
||||
"vCard",
|
||||
"jCard",
|
||||
"jCal",
|
||||
"ics",
|
||||
"vcf",
|
||||
"xCard",
|
||||
"xCal",
|
||||
"freebusy",
|
||||
"recurrence",
|
||||
"availability",
|
||||
"rfc2425",
|
||||
"rfc2426",
|
||||
"rfc2739",
|
||||
"rfc4770",
|
||||
"rfc5545",
|
||||
"rfc5546",
|
||||
"rfc6321",
|
||||
"rfc6350",
|
||||
"rfc6351",
|
||||
"rfc6474",
|
||||
"rfc6638",
|
||||
"rfc6715",
|
||||
"rfc6868"
|
||||
],
|
||||
"homepage" : "http://sabre.io/vobject/",
|
||||
"license" : "BSD-3-Clause",
|
||||
"require" : {
|
||||
"php" : "^7.1 || ^8.0",
|
||||
"ext-mbstring" : "*",
|
||||
"sabre/xml" : "^2.1 || ^3.0 || ^4.0"
|
||||
},
|
||||
"require-dev" : {
|
||||
"friendsofphp/php-cs-fixer": "~2.17.1",
|
||||
"phpunit/phpunit" : "^7.5 || ^8.5 || ^9.6",
|
||||
"phpunit/php-invoker" : "^2.0 || ^3.1",
|
||||
"phpstan/phpstan": "^0.12 || ^1.12 || ^2.0"
|
||||
},
|
||||
"suggest" : {
|
||||
"hoa/bench" : "If you would like to run the benchmark scripts"
|
||||
},
|
||||
"authors" : [
|
||||
{
|
||||
"name" : "Evert Pot",
|
||||
"email" : "me@evertpot.com",
|
||||
"homepage" : "http://evertpot.com/",
|
||||
"role" : "Developer"
|
||||
},
|
||||
{
|
||||
"name" : "Dominik Tobschall",
|
||||
"email" : "dominik@fruux.com",
|
||||
"homepage" : "http://tobschall.de/",
|
||||
"role" : "Developer"
|
||||
},
|
||||
{
|
||||
"name" : "Ivan Enderlin",
|
||||
"email" : "ivan.enderlin@hoa-project.net",
|
||||
"homepage" : "http://mnt.io/",
|
||||
"role" : "Developer"
|
||||
}
|
||||
],
|
||||
"support" : {
|
||||
"forum" : "https://groups.google.com/group/sabredav-discuss",
|
||||
"source" : "https://github.com/fruux/sabre-vobject"
|
||||
},
|
||||
"autoload" : {
|
||||
"psr-4" : {
|
||||
"Sabre\\VObject\\" : "lib/"
|
||||
}
|
||||
},
|
||||
"autoload-dev" : {
|
||||
"psr-4" : {
|
||||
"Sabre\\VObject\\" : "tests/VObject"
|
||||
}
|
||||
},
|
||||
"bin" : [
|
||||
"bin/vobject",
|
||||
"bin/generate_vcards"
|
||||
],
|
||||
"extra" : {
|
||||
"branch-alias" : {
|
||||
"dev-master" : "4.0.x-dev"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpstan": [
|
||||
"phpstan analyse lib tests"
|
||||
],
|
||||
"cs-fixer": [
|
||||
"php-cs-fixer fix"
|
||||
],
|
||||
"phpunit": [
|
||||
"phpunit --configuration tests/phpunit.xml"
|
||||
],
|
||||
"test": [
|
||||
"composer phpstan",
|
||||
"composer cs-fixer",
|
||||
"composer phpunit"
|
||||
]
|
||||
}
|
||||
}
|
||||
172
vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php
vendored
Normal file
172
vendor/sabre/vobject/lib/BirthdayCalendarGenerator.php
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
/**
|
||||
* This class generates birthday calendars.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Dominik Tobschall (http://tobschall.de/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class BirthdayCalendarGenerator
|
||||
{
|
||||
/**
|
||||
* Input objects.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $objects = [];
|
||||
|
||||
/**
|
||||
* Default year.
|
||||
* Used for dates without a year.
|
||||
*/
|
||||
const DEFAULT_YEAR = 2000;
|
||||
|
||||
/**
|
||||
* Output format for the SUMMARY.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $format = '%1$s\'s Birthday';
|
||||
|
||||
/**
|
||||
* Creates the generator.
|
||||
*
|
||||
* Check the setTimeRange and setObjects methods for details about the
|
||||
* arguments.
|
||||
*
|
||||
* @param mixed $objects
|
||||
*/
|
||||
public function __construct($objects = null)
|
||||
{
|
||||
if ($objects) {
|
||||
$this->setObjects($objects);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input objects.
|
||||
*
|
||||
* You must either supply a vCard as a string or as a Component/VCard object.
|
||||
* It's also possible to supply an array of strings or objects.
|
||||
*
|
||||
* @param mixed $objects
|
||||
*/
|
||||
public function setObjects($objects)
|
||||
{
|
||||
if (!is_array($objects)) {
|
||||
$objects = [$objects];
|
||||
}
|
||||
|
||||
$this->objects = [];
|
||||
foreach ($objects as $object) {
|
||||
if (is_string($object)) {
|
||||
$vObj = Reader::read($object);
|
||||
if (!$vObj instanceof Component\VCard) {
|
||||
throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects');
|
||||
}
|
||||
|
||||
$this->objects[] = $vObj;
|
||||
} elseif ($object instanceof Component\VCard) {
|
||||
$this->objects[] = $object;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the output format for the SUMMARY.
|
||||
*
|
||||
* @param string $format
|
||||
*/
|
||||
public function setFormat($format)
|
||||
{
|
||||
$this->format = $format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input data and returns a VCALENDAR.
|
||||
*
|
||||
* @return Component/VCalendar
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
$calendar = new VCalendar();
|
||||
|
||||
foreach ($this->objects as $object) {
|
||||
// Skip if there is no BDAY property.
|
||||
if (!$object->select('BDAY')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We've seen clients (ez-vcard) putting "BDAY:" properties
|
||||
// without a value into vCards. If we come across those, we'll
|
||||
// skip them.
|
||||
if (empty($object->BDAY->getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're always converting to vCard 4.0 so we can rely on the
|
||||
// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
|
||||
$object = $object->convert(Document::VCARD40);
|
||||
|
||||
// Skip if the card has no FN property.
|
||||
if (!isset($object->FN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if the BDAY property is not of the right type.
|
||||
if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if we can't parse the BDAY value.
|
||||
try {
|
||||
$dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue());
|
||||
} catch (InvalidDataException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set a year if it's not set.
|
||||
$unknownYear = false;
|
||||
|
||||
if (!$dateParts['year']) {
|
||||
$object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date'];
|
||||
|
||||
$unknownYear = true;
|
||||
}
|
||||
|
||||
// Create event.
|
||||
$event = $calendar->add('VEVENT', [
|
||||
'SUMMARY' => sprintf($this->format, $object->FN->getValue()),
|
||||
'DTSTART' => new \DateTime($object->BDAY->getValue()),
|
||||
'RRULE' => 'FREQ=YEARLY',
|
||||
'TRANSP' => 'TRANSPARENT',
|
||||
]);
|
||||
|
||||
// add VALUE=date
|
||||
$event->DTSTART['VALUE'] = 'DATE';
|
||||
|
||||
// Add X-SABRE-BDAY property.
|
||||
if ($unknownYear) {
|
||||
$event->add('X-SABRE-BDAY', 'BDAY', [
|
||||
'X-SABRE-VCARD-UID' => $object->UID->getValue(),
|
||||
'X-SABRE-VCARD-FN' => $object->FN->getValue(),
|
||||
'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR,
|
||||
]);
|
||||
} else {
|
||||
$event->add('X-SABRE-BDAY', 'BDAY', [
|
||||
'X-SABRE-VCARD-UID' => $object->UID->getValue(),
|
||||
'X-SABRE-VCARD-FN' => $object->FN->getValue(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $calendar;
|
||||
}
|
||||
}
|
||||
705
vendor/sabre/vobject/lib/Cli.php
vendored
Normal file
705
vendor/sabre/vobject/lib/Cli.php
vendored
Normal file
@@ -0,0 +1,705 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* This is the CLI interface for sabre-vobject.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Cli
|
||||
{
|
||||
/**
|
||||
* No output.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $quiet = false;
|
||||
|
||||
/**
|
||||
* Help display.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $showHelp = false;
|
||||
|
||||
/**
|
||||
* Whether to spit out 'mimedir' or 'json' format.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* JSON pretty print.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $pretty;
|
||||
|
||||
/**
|
||||
* Source file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $inputPath;
|
||||
|
||||
/**
|
||||
* Destination file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $outputPath;
|
||||
|
||||
/**
|
||||
* output stream.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $stdout;
|
||||
|
||||
/**
|
||||
* stdin.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $stdin;
|
||||
|
||||
/**
|
||||
* stderr.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $stderr;
|
||||
|
||||
/**
|
||||
* Input format (one of json or mimedir).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $inputFormat;
|
||||
|
||||
/**
|
||||
* Makes the parser less strict.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $forgiving = false;
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function main(array $argv)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
// We cannot easily test this, so we'll skip it. Pretty basic anyway.
|
||||
|
||||
if (!$this->stderr) {
|
||||
$this->stderr = fopen('php://stderr', 'w');
|
||||
}
|
||||
if (!$this->stdout) {
|
||||
$this->stdout = fopen('php://stdout', 'w');
|
||||
}
|
||||
if (!$this->stdin) {
|
||||
$this->stdin = fopen('php://stdin', 'r');
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
try {
|
||||
list($options, $positional) = $this->parseArguments($argv);
|
||||
|
||||
if (isset($options['q'])) {
|
||||
$this->quiet = true;
|
||||
}
|
||||
$this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION));
|
||||
|
||||
foreach ($options as $name => $value) {
|
||||
switch ($name) {
|
||||
case 'q':
|
||||
// Already handled earlier.
|
||||
break;
|
||||
case 'h':
|
||||
case 'help':
|
||||
$this->showHelp();
|
||||
|
||||
return 0;
|
||||
break;
|
||||
case 'format':
|
||||
switch ($value) {
|
||||
// jcard/jcal documents
|
||||
case 'jcard':
|
||||
case 'jcal':
|
||||
// specific document versions
|
||||
case 'vcard21':
|
||||
case 'vcard30':
|
||||
case 'vcard40':
|
||||
case 'icalendar20':
|
||||
// specific formats
|
||||
case 'json':
|
||||
case 'mimedir':
|
||||
// icalendar/vcad
|
||||
case 'icalendar':
|
||||
case 'vcard':
|
||||
$this->format = $value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('Unknown format: '.$value);
|
||||
}
|
||||
break;
|
||||
case 'pretty':
|
||||
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
|
||||
$this->pretty = true;
|
||||
}
|
||||
break;
|
||||
case 'forgiving':
|
||||
$this->forgiving = true;
|
||||
break;
|
||||
case 'inputformat':
|
||||
switch ($value) {
|
||||
// json formats
|
||||
case 'jcard':
|
||||
case 'jcal':
|
||||
case 'json':
|
||||
$this->inputFormat = 'json';
|
||||
break;
|
||||
|
||||
// mimedir formats
|
||||
case 'mimedir':
|
||||
case 'icalendar':
|
||||
case 'vcard':
|
||||
case 'vcard21':
|
||||
case 'vcard30':
|
||||
case 'vcard40':
|
||||
case 'icalendar20':
|
||||
$this->inputFormat = 'mimedir';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException('Unknown format: '.$value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Unknown option: '.$name);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === count($positional)) {
|
||||
$this->showHelp();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (1 === count($positional)) {
|
||||
throw new InvalidArgumentException('Inputfile is a required argument');
|
||||
}
|
||||
|
||||
if (count($positional) > 3) {
|
||||
throw new InvalidArgumentException('Too many arguments');
|
||||
}
|
||||
|
||||
if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) {
|
||||
throw new InvalidArgumentException('Unknown command: '.$positional[0]);
|
||||
}
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->showHelp();
|
||||
$this->log('Error: '.$e->getMessage(), 'red');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$command = $positional[0];
|
||||
|
||||
$this->inputPath = $positional[1];
|
||||
$this->outputPath = isset($positional[2]) ? $positional[2] : '-';
|
||||
|
||||
if ('-' !== $this->outputPath) {
|
||||
$this->stdout = fopen($this->outputPath, 'w');
|
||||
}
|
||||
|
||||
if (!$this->inputFormat) {
|
||||
if ('.json' === substr($this->inputPath, -5)) {
|
||||
$this->inputFormat = 'json';
|
||||
} else {
|
||||
$this->inputFormat = 'mimedir';
|
||||
}
|
||||
}
|
||||
if (!$this->format) {
|
||||
if ('.json' === substr($this->outputPath, -5)) {
|
||||
$this->format = 'json';
|
||||
} else {
|
||||
$this->format = 'mimedir';
|
||||
}
|
||||
}
|
||||
|
||||
$realCode = 0;
|
||||
|
||||
try {
|
||||
while ($input = $this->readInput()) {
|
||||
$returnCode = $this->$command($input);
|
||||
if (0 !== $returnCode) {
|
||||
$realCode = $returnCode;
|
||||
}
|
||||
}
|
||||
} catch (EofException $e) {
|
||||
// end of file
|
||||
} catch (\Exception $e) {
|
||||
$this->log('Error: '.$e->getMessage(), 'red');
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
return $realCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the help message.
|
||||
*/
|
||||
protected function showHelp()
|
||||
{
|
||||
$this->log('Usage:', 'yellow');
|
||||
$this->log(' vobject [options] command [arguments]');
|
||||
$this->log('');
|
||||
$this->log('Options:', 'yellow');
|
||||
$this->log($this->colorize('green', ' -q ')."Don't output anything.");
|
||||
$this->log($this->colorize('green', ' -help -h ').'Display this help message.');
|
||||
$this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,');
|
||||
$this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.');
|
||||
$this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.');
|
||||
$this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it');
|
||||
$this->log(' must be specified here.');
|
||||
// Only PHP 5.4 and up
|
||||
if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
|
||||
$this->log($this->colorize('green', ' --pretty ').'json pretty-print.');
|
||||
}
|
||||
$this->log('');
|
||||
$this->log('Commands:', 'yellow');
|
||||
$this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.');
|
||||
$this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.');
|
||||
$this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.');
|
||||
$this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.');
|
||||
$this->log(
|
||||
<<<HELP
|
||||
|
||||
If source_file is set as '-', STDIN will be used.
|
||||
If output_file is omitted, STDOUT will be used.
|
||||
All other output is sent to STDERR.
|
||||
|
||||
HELP
|
||||
);
|
||||
|
||||
$this->log('Examples:', 'yellow');
|
||||
$this->log(' vobject convert contact.vcf contact.json');
|
||||
$this->log(' vobject convert --format=vcard40 old.vcf new.vcf');
|
||||
$this->log(' vobject convert --inputformat=json --format=mimedir - -');
|
||||
$this->log(' vobject color calendar.ics');
|
||||
$this->log('');
|
||||
$this->log('https://github.com/fruux/sabre-vobject', 'purple');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a VObject file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function validate(Component $vObj)
|
||||
{
|
||||
$returnCode = 0;
|
||||
|
||||
switch ($vObj->name) {
|
||||
case 'VCALENDAR':
|
||||
$this->log('iCalendar: '.(string) $vObj->VERSION);
|
||||
break;
|
||||
case 'VCARD':
|
||||
$this->log('vCard: '.(string) $vObj->VERSION);
|
||||
break;
|
||||
}
|
||||
|
||||
$warnings = $vObj->validate();
|
||||
if (!count($warnings)) {
|
||||
$this->log(' No warnings!');
|
||||
} else {
|
||||
$levels = [
|
||||
1 => 'REPAIRED',
|
||||
2 => 'WARNING',
|
||||
3 => 'ERROR',
|
||||
];
|
||||
$returnCode = 2;
|
||||
foreach ($warnings as $warn) {
|
||||
$extra = '';
|
||||
if ($warn['node'] instanceof Property) {
|
||||
$extra = ' (property: "'.$warn['node']->name.'")';
|
||||
}
|
||||
$this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
|
||||
}
|
||||
}
|
||||
|
||||
return $returnCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repairs a VObject file.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function repair(Component $vObj)
|
||||
{
|
||||
$returnCode = 0;
|
||||
|
||||
switch ($vObj->name) {
|
||||
case 'VCALENDAR':
|
||||
$this->log('iCalendar: '.(string) $vObj->VERSION);
|
||||
break;
|
||||
case 'VCARD':
|
||||
$this->log('vCard: '.(string) $vObj->VERSION);
|
||||
break;
|
||||
}
|
||||
|
||||
$warnings = $vObj->validate(Node::REPAIR);
|
||||
if (!count($warnings)) {
|
||||
$this->log(' No warnings!');
|
||||
} else {
|
||||
$levels = [
|
||||
1 => 'REPAIRED',
|
||||
2 => 'WARNING',
|
||||
3 => 'ERROR',
|
||||
];
|
||||
$returnCode = 2;
|
||||
foreach ($warnings as $warn) {
|
||||
$extra = '';
|
||||
if ($warn['node'] instanceof Property) {
|
||||
$extra = ' (property: "'.$warn['node']->name.'")';
|
||||
}
|
||||
$this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra);
|
||||
}
|
||||
}
|
||||
fwrite($this->stdout, $vObj->serialize());
|
||||
|
||||
return $returnCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a vObject file to a new format.
|
||||
*
|
||||
* @param Component $vObj
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function convert($vObj)
|
||||
{
|
||||
$json = false;
|
||||
$convertVersion = null;
|
||||
$forceInput = null;
|
||||
|
||||
switch ($this->format) {
|
||||
case 'json':
|
||||
$json = true;
|
||||
if ('VCARD' === $vObj->name) {
|
||||
$convertVersion = Document::VCARD40;
|
||||
}
|
||||
break;
|
||||
case 'jcard':
|
||||
$json = true;
|
||||
$forceInput = 'VCARD';
|
||||
$convertVersion = Document::VCARD40;
|
||||
break;
|
||||
case 'jcal':
|
||||
$json = true;
|
||||
$forceInput = 'VCALENDAR';
|
||||
break;
|
||||
case 'mimedir':
|
||||
case 'icalendar':
|
||||
case 'icalendar20':
|
||||
case 'vcard':
|
||||
break;
|
||||
case 'vcard21':
|
||||
$convertVersion = Document::VCARD21;
|
||||
break;
|
||||
case 'vcard30':
|
||||
$convertVersion = Document::VCARD30;
|
||||
break;
|
||||
case 'vcard40':
|
||||
$convertVersion = Document::VCARD40;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($forceInput && $vObj->name !== $forceInput) {
|
||||
throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format);
|
||||
}
|
||||
if ($convertVersion) {
|
||||
$vObj = $vObj->convert($convertVersion);
|
||||
}
|
||||
if ($json) {
|
||||
$jsonOptions = 0;
|
||||
if ($this->pretty) {
|
||||
$jsonOptions = JSON_PRETTY_PRINT;
|
||||
}
|
||||
fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions));
|
||||
} else {
|
||||
fwrite($this->stdout, $vObj->serialize());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorizes a file.
|
||||
*
|
||||
* @param Component $vObj
|
||||
*/
|
||||
protected function color($vObj)
|
||||
{
|
||||
$this->serializeComponent($vObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ansi color string for a color name.
|
||||
*
|
||||
* @param string $color
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function colorize($color, $str, $resetTo = 'default')
|
||||
{
|
||||
$colors = [
|
||||
'cyan' => '1;36',
|
||||
'red' => '1;31',
|
||||
'yellow' => '1;33',
|
||||
'blue' => '0;34',
|
||||
'green' => '0;32',
|
||||
'default' => '0',
|
||||
'purple' => '0;35',
|
||||
];
|
||||
|
||||
return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m';
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out a string in specific color.
|
||||
*
|
||||
* @param string $color
|
||||
* @param string $str
|
||||
*/
|
||||
protected function cWrite($color, $str)
|
||||
{
|
||||
fwrite($this->stdout, $this->colorize($color, $str));
|
||||
}
|
||||
|
||||
protected function serializeComponent(Component $vObj)
|
||||
{
|
||||
$this->cWrite('cyan', 'BEGIN');
|
||||
$this->cWrite('red', ':');
|
||||
$this->cWrite('yellow', $vObj->name."\n");
|
||||
|
||||
/**
|
||||
* Gives a component a 'score' for sorting purposes.
|
||||
*
|
||||
* This is solely used by the childrenSort method.
|
||||
*
|
||||
* A higher score means the item will be lower in the list.
|
||||
* To avoid score collisions, each "score category" has a reasonable
|
||||
* space to accommodate elements. The $key is added to the $score to
|
||||
* preserve the original relative order of elements.
|
||||
*
|
||||
* @param int $key
|
||||
* @param array $array
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
$sortScore = function ($key, $array) {
|
||||
if ($array[$key] instanceof Component) {
|
||||
// We want to encode VTIMEZONE first, this is a personal
|
||||
// preference.
|
||||
if ('VTIMEZONE' === $array[$key]->name) {
|
||||
$score = 300000000;
|
||||
|
||||
return $score + $key;
|
||||
} else {
|
||||
$score = 400000000;
|
||||
|
||||
return $score + $key;
|
||||
}
|
||||
} else {
|
||||
// Properties get encoded first
|
||||
// VCARD version 4.0 wants the VERSION property to appear first
|
||||
if ($array[$key] instanceof Property) {
|
||||
if ('VERSION' === $array[$key]->name) {
|
||||
$score = 100000000;
|
||||
|
||||
return $score + $key;
|
||||
} else {
|
||||
// All other properties
|
||||
$score = 200000000;
|
||||
|
||||
return $score + $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$children = $vObj->children();
|
||||
$tmp = $children;
|
||||
uksort(
|
||||
$children,
|
||||
function ($a, $b) use ($sortScore, $tmp) {
|
||||
$sA = $sortScore($a, $tmp);
|
||||
$sB = $sortScore($b, $tmp);
|
||||
|
||||
return $sA - $sB;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($children as $child) {
|
||||
if ($child instanceof Component) {
|
||||
$this->serializeComponent($child);
|
||||
} else {
|
||||
$this->serializeProperty($child);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cWrite('cyan', 'END');
|
||||
$this->cWrite('red', ':');
|
||||
$this->cWrite('yellow', $vObj->name."\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Colorizes a property.
|
||||
*/
|
||||
protected function serializeProperty(Property $property)
|
||||
{
|
||||
if ($property->group) {
|
||||
$this->cWrite('default', $property->group);
|
||||
$this->cWrite('red', '.');
|
||||
}
|
||||
|
||||
$this->cWrite('yellow', $property->name);
|
||||
|
||||
foreach ($property->parameters as $param) {
|
||||
$this->cWrite('red', ';');
|
||||
$this->cWrite('blue', $param->serialize());
|
||||
}
|
||||
$this->cWrite('red', ':');
|
||||
|
||||
if ($property instanceof Property\Binary) {
|
||||
$this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)');
|
||||
} else {
|
||||
$parts = $property->getParts();
|
||||
$first1 = true;
|
||||
// Looping through property values
|
||||
foreach ($parts as $part) {
|
||||
if ($first1) {
|
||||
$first1 = false;
|
||||
} else {
|
||||
$this->cWrite('red', $property->delimiter);
|
||||
}
|
||||
$first2 = true;
|
||||
// Looping through property sub-values
|
||||
foreach ((array) $part as $subPart) {
|
||||
if ($first2) {
|
||||
$first2 = false;
|
||||
} else {
|
||||
// The sub-value delimiter is always comma
|
||||
$this->cWrite('red', ',');
|
||||
}
|
||||
|
||||
$subPart = strtr(
|
||||
$subPart,
|
||||
[
|
||||
'\\' => $this->colorize('purple', '\\\\', 'green'),
|
||||
';' => $this->colorize('purple', '\;', 'green'),
|
||||
',' => $this->colorize('purple', '\,', 'green'),
|
||||
"\n" => $this->colorize('purple', "\\n\n\t", 'green'),
|
||||
"\r" => '',
|
||||
]
|
||||
);
|
||||
|
||||
$this->cWrite('green', $subPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->cWrite('default', "\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the list of arguments.
|
||||
*/
|
||||
protected function parseArguments(array $argv)
|
||||
{
|
||||
$positional = [];
|
||||
$options = [];
|
||||
|
||||
for ($ii = 0; $ii < count($argv); ++$ii) {
|
||||
// Skipping the first argument.
|
||||
if (0 === $ii) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$v = $argv[$ii];
|
||||
|
||||
if ('--' === substr($v, 0, 2)) {
|
||||
// This is a long-form option.
|
||||
$optionName = substr($v, 2);
|
||||
$optionValue = true;
|
||||
if (strpos($optionName, '=')) {
|
||||
list($optionName, $optionValue) = explode('=', $optionName);
|
||||
}
|
||||
$options[$optionName] = $optionValue;
|
||||
} elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) {
|
||||
// This is a short-form option.
|
||||
foreach (str_split(substr($v, 1)) as $option) {
|
||||
$options[$option] = true;
|
||||
}
|
||||
} else {
|
||||
$positional[] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
return [$options, $positional];
|
||||
}
|
||||
|
||||
protected $parser;
|
||||
|
||||
/**
|
||||
* Reads the input file.
|
||||
*
|
||||
* @return Component
|
||||
*/
|
||||
protected function readInput()
|
||||
{
|
||||
if (!$this->parser) {
|
||||
if ('-' !== $this->inputPath) {
|
||||
$this->stdin = fopen($this->inputPath, 'r');
|
||||
}
|
||||
|
||||
if ('mimedir' === $this->inputFormat) {
|
||||
$this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
|
||||
} else {
|
||||
$this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->parser->parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to STDERR.
|
||||
*
|
||||
* @param string $msg
|
||||
*/
|
||||
protected function log($msg, $color = 'default')
|
||||
{
|
||||
if (!$this->quiet) {
|
||||
if ('default' !== $color) {
|
||||
$msg = $this->colorize($color, $msg);
|
||||
}
|
||||
fwrite($this->stderr, $msg."\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
672
vendor/sabre/vobject/lib/Component.php
vendored
Normal file
672
vendor/sabre/vobject/lib/Component.php
vendored
Normal file
@@ -0,0 +1,672 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
* Component.
|
||||
*
|
||||
* A component represents a group of properties, such as VCALENDAR, VEVENT, or
|
||||
* VCARD.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Component extends Node
|
||||
{
|
||||
/**
|
||||
* Component name.
|
||||
*
|
||||
* This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* A list of properties and/or sub-components.
|
||||
*
|
||||
* @var array<string, Component|Property>
|
||||
*/
|
||||
protected $children = [];
|
||||
|
||||
/**
|
||||
* Creates a new component.
|
||||
*
|
||||
* You can specify the children either in key=>value syntax, in which case
|
||||
* properties will automatically be created, or you can just pass a list of
|
||||
* Component and Property object.
|
||||
*
|
||||
* By default, a set of sensible values will be added to the component. For
|
||||
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
|
||||
* ensure that this does not happen, set $defaults to false.
|
||||
*
|
||||
* @param string|null $name such as VCALENDAR, VEVENT
|
||||
* @param bool $defaults
|
||||
*/
|
||||
public function __construct(Document $root, $name, array $children = [], $defaults = true)
|
||||
{
|
||||
$this->name = isset($name) ? strtoupper($name) : '';
|
||||
$this->root = $root;
|
||||
|
||||
if ($defaults) {
|
||||
// This is a terribly convoluted way to do this, but this ensures
|
||||
// that the order of properties as they are specified in both
|
||||
// defaults and the childrens list, are inserted in the object in a
|
||||
// natural way.
|
||||
$list = $this->getDefaults();
|
||||
$nodes = [];
|
||||
foreach ($children as $key => $value) {
|
||||
if ($value instanceof Node) {
|
||||
if (isset($list[$value->name])) {
|
||||
unset($list[$value->name]);
|
||||
}
|
||||
$nodes[] = $value;
|
||||
} else {
|
||||
$list[$key] = $value;
|
||||
}
|
||||
}
|
||||
foreach ($list as $key => $value) {
|
||||
$this->add($key, $value);
|
||||
}
|
||||
foreach ($nodes as $node) {
|
||||
$this->add($node);
|
||||
}
|
||||
} else {
|
||||
foreach ($children as $k => $child) {
|
||||
if ($child instanceof Node) {
|
||||
// Component or Property
|
||||
$this->add($child);
|
||||
} else {
|
||||
// Property key=>value
|
||||
$this->add($k, $child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new property or component, and returns the new item.
|
||||
*
|
||||
* This method has 3 possible signatures:
|
||||
*
|
||||
* add(Component $comp) // Adds a new component
|
||||
* add(Property $prop) // Adds a new property
|
||||
* add($name, $value, array $parameters = []) // Adds a new property
|
||||
* add($name, array $children = []) // Adds a new component
|
||||
* by name.
|
||||
*
|
||||
* @return Node
|
||||
*/
|
||||
public function add()
|
||||
{
|
||||
$arguments = func_get_args();
|
||||
|
||||
if ($arguments[0] instanceof Node) {
|
||||
if (isset($arguments[1])) {
|
||||
throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
|
||||
}
|
||||
$arguments[0]->parent = $this;
|
||||
$newNode = $arguments[0];
|
||||
} elseif (is_string($arguments[0])) {
|
||||
$newNode = call_user_func_array([$this->root, 'create'], $arguments);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
|
||||
}
|
||||
|
||||
$name = $newNode->name;
|
||||
if (isset($this->children[$name])) {
|
||||
$this->children[$name][] = $newNode;
|
||||
} else {
|
||||
$this->children[$name] = [$newNode];
|
||||
}
|
||||
|
||||
return $newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method removes a component or property from this component.
|
||||
*
|
||||
* You can either specify the item by name (like DTSTART), in which case
|
||||
* all properties/components with that name will be removed, or you can
|
||||
* pass an instance of a property or component, in which case only that
|
||||
* exact item will be removed.
|
||||
*
|
||||
* @param string|Property|Component $item
|
||||
*/
|
||||
public function remove($item)
|
||||
{
|
||||
if (is_string($item)) {
|
||||
// If there's no dot in the name, it's an exact property name and
|
||||
// we can just wipe out all those properties.
|
||||
//
|
||||
if (false === strpos($item, '.')) {
|
||||
unset($this->children[strtoupper($item)]);
|
||||
|
||||
return;
|
||||
}
|
||||
// If there was a dot, we need to ask select() to help us out and
|
||||
// then we just call remove recursively.
|
||||
foreach ($this->select($item) as $child) {
|
||||
$this->remove($child);
|
||||
}
|
||||
} else {
|
||||
foreach ($this->select($item->name) as $k => $child) {
|
||||
if ($child === $item) {
|
||||
unset($this->children[$item->name][$k]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a flat list of all the properties and components in this
|
||||
* component.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->children as $childGroup) {
|
||||
$result = array_merge($result, $childGroup);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method only returns a list of sub-components. Properties are
|
||||
* ignored.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getComponents()
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($child instanceof self) {
|
||||
$result[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with elements that match the specified name.
|
||||
*
|
||||
* This function is also aware of MIME-Directory groups (as they appear in
|
||||
* vcards). This means that if a property is grouped as "HOME.EMAIL", it
|
||||
* will also be returned when searching for just "EMAIL". If you want to
|
||||
* search for a property in a specific group, you can select on the entire
|
||||
* string ("HOME.EMAIL"). If you want to search on a specific property that
|
||||
* has not been assigned a group, specify ".EMAIL".
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function select($name)
|
||||
{
|
||||
$group = null;
|
||||
$name = strtoupper($name);
|
||||
if (false !== strpos($name, '.')) {
|
||||
list($group, $name) = explode('.', $name, 2);
|
||||
}
|
||||
if ('' === $name) {
|
||||
$name = null;
|
||||
}
|
||||
|
||||
if (!is_null($name)) {
|
||||
$result = isset($this->children[$name]) ? $this->children[$name] : [];
|
||||
|
||||
if (is_null($group)) {
|
||||
return $result;
|
||||
} else {
|
||||
// If we have a group filter as well, we need to narrow it down
|
||||
// more.
|
||||
return array_filter(
|
||||
$result,
|
||||
function ($child) use ($group) {
|
||||
return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If we got to this point, it means there was no 'name' specified for
|
||||
// searching, implying that this is a group-only search.
|
||||
$result = [];
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) {
|
||||
$result[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the object back into a serialized blob.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
$str = 'BEGIN:'.$this->name."\r\n";
|
||||
|
||||
/**
|
||||
* Gives a component a 'score' for sorting purposes.
|
||||
*
|
||||
* This is solely used by the childrenSort method.
|
||||
*
|
||||
* A higher score means the item will be lower in the list.
|
||||
* To avoid score collisions, each "score category" has a reasonable
|
||||
* space to accommodate elements. The $key is added to the $score to
|
||||
* preserve the original relative order of elements.
|
||||
*
|
||||
* @param int $key
|
||||
* @param array $array
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
$sortScore = function ($key, $array) {
|
||||
if ($array[$key] instanceof Component) {
|
||||
// We want to encode VTIMEZONE first, this is a personal
|
||||
// preference.
|
||||
if ('VTIMEZONE' === $array[$key]->name) {
|
||||
$score = 300000000;
|
||||
|
||||
return $score + $key;
|
||||
} else {
|
||||
$score = 400000000;
|
||||
|
||||
return $score + $key;
|
||||
}
|
||||
} else {
|
||||
// Properties get encoded first
|
||||
// VCARD version 4.0 wants the VERSION property to appear first
|
||||
if ($array[$key] instanceof Property) {
|
||||
if ('VERSION' === $array[$key]->name) {
|
||||
$score = 100000000;
|
||||
|
||||
return $score + $key;
|
||||
} else {
|
||||
// All other properties
|
||||
$score = 200000000;
|
||||
|
||||
return $score + $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$children = $this->children();
|
||||
$tmp = $children;
|
||||
uksort(
|
||||
$children,
|
||||
function ($a, $b) use ($sortScore, $tmp) {
|
||||
$sA = $sortScore($a, $tmp);
|
||||
$sB = $sortScore($b, $tmp);
|
||||
|
||||
return $sA - $sB;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ($children as $child) {
|
||||
$str .= $child->serialize();
|
||||
}
|
||||
$str .= 'END:'.$this->name."\r\n";
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns an array, with the representation as it should be
|
||||
* encoded in JSON. This is used to create jCard or jCal documents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$components = [];
|
||||
$properties = [];
|
||||
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($child instanceof self) {
|
||||
$components[] = $child->jsonSerialize();
|
||||
} else {
|
||||
$properties[] = $child->jsonSerialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
strtolower($this->name),
|
||||
$properties,
|
||||
$components,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the data into XML. This is used to create xCard or
|
||||
* xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
public function xmlSerialize(Xml\Writer $writer): void
|
||||
{
|
||||
$components = [];
|
||||
$properties = [];
|
||||
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($child instanceof self) {
|
||||
$components[] = $child;
|
||||
} else {
|
||||
$properties[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$writer->startElement(strtolower($this->name));
|
||||
|
||||
if (!empty($properties)) {
|
||||
$writer->startElement('properties');
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$property->xmlSerialize($writer);
|
||||
}
|
||||
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
if (!empty($components)) {
|
||||
$writer->startElement('components');
|
||||
|
||||
foreach ($components as $component) {
|
||||
$component->xmlSerialize($writer);
|
||||
}
|
||||
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Magic property accessors {{{ */
|
||||
|
||||
/**
|
||||
* Using 'get' you will either get a property or component.
|
||||
*
|
||||
* If there were no child-elements found with the specified name,
|
||||
* null is returned.
|
||||
*
|
||||
* To use this, this may look something like this:
|
||||
*
|
||||
* $event = $calendar->VEVENT;
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Property|null
|
||||
*/
|
||||
public function __get($name)
|
||||
{
|
||||
if ('children' === $name) {
|
||||
throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
|
||||
}
|
||||
|
||||
$matches = $this->select($name);
|
||||
if (0 === count($matches)) {
|
||||
return;
|
||||
} else {
|
||||
$firstMatch = current($matches);
|
||||
/* @var $firstMatch Property */
|
||||
$firstMatch->setIterator(new ElementList(array_values($matches)));
|
||||
|
||||
return $firstMatch;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks if a sub-element with the specified name exists.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($name)
|
||||
{
|
||||
$matches = $this->select($name);
|
||||
|
||||
return count($matches) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the setter method you can add properties or subcomponents.
|
||||
*
|
||||
* You can either pass a Component, Property
|
||||
* object, or a string to automatically create a Property.
|
||||
*
|
||||
* If the item already exists, it will be removed. If you want to add
|
||||
* a new item with the same name, always use the add() method.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$name = strtoupper($name);
|
||||
$this->remove($name);
|
||||
if ($value instanceof self || $value instanceof Property) {
|
||||
$this->add($value);
|
||||
} else {
|
||||
$this->add($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all properties and components within this component with the
|
||||
* specified name.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function __unset($name)
|
||||
{
|
||||
$this->remove($name);
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/**
|
||||
* This method is automatically called when the object is cloned.
|
||||
* Specifically, this will ensure all child elements are also cloned.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->children as $childName => $childGroup) {
|
||||
foreach ($childGroup as $key => $child) {
|
||||
$clonedChild = clone $child;
|
||||
$clonedChild->parent = $this;
|
||||
$clonedChild->root = $this->root;
|
||||
$this->children[$childName][$key] = $clonedChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* It is also possible to specify defaults and severity levels for
|
||||
* violating the rule.
|
||||
*
|
||||
* See the VEVENT implementation for getValidationRules for a more complex
|
||||
* example.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
|
||||
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on).
|
||||
* 2 - A warning.
|
||||
* 3 - An error.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$rules = $this->getValidationRules();
|
||||
$defaults = $this->getDefaults();
|
||||
|
||||
$propertyCounters = [];
|
||||
|
||||
$messages = [];
|
||||
|
||||
foreach ($this->children() as $child) {
|
||||
$name = strtoupper($child->name);
|
||||
if (!isset($propertyCounters[$name])) {
|
||||
$propertyCounters[$name] = 1;
|
||||
} else {
|
||||
++$propertyCounters[$name];
|
||||
}
|
||||
$messages = array_merge($messages, $child->validate($options));
|
||||
}
|
||||
|
||||
foreach ($rules as $propName => $rule) {
|
||||
switch ($rule) {
|
||||
case '0':
|
||||
if (isset($propertyCounters[$propName])) {
|
||||
$messages[] = [
|
||||
'level' => 3,
|
||||
'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
break;
|
||||
case '1':
|
||||
if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) {
|
||||
$repaired = false;
|
||||
if ($options & self::REPAIR && isset($defaults[$propName])) {
|
||||
$this->add($propName, $defaults[$propName]);
|
||||
$repaired = true;
|
||||
}
|
||||
$messages[] = [
|
||||
'level' => $repaired ? 1 : 3,
|
||||
'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
break;
|
||||
case '+':
|
||||
if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
|
||||
$messages[] = [
|
||||
'level' => 3,
|
||||
'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
break;
|
||||
case '*':
|
||||
break;
|
||||
case '?':
|
||||
if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
|
||||
$level = 3;
|
||||
|
||||
// We try to repair the same property appearing multiple times with the exact same value
|
||||
// by removing the duplicates and keeping only one property
|
||||
if ($options & self::REPAIR) {
|
||||
$properties = array_unique($this->select($propName), SORT_REGULAR);
|
||||
|
||||
if (1 === count($properties)) {
|
||||
$this->remove($propName);
|
||||
$this->add($properties[0]);
|
||||
|
||||
$level = 1;
|
||||
}
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'level' => $level,
|
||||
'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method on a document if you're done using it.
|
||||
*
|
||||
* It's intended to remove all circular references, so PHP can easily clean
|
||||
* it up.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
parent::destroy();
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
$child->destroy();
|
||||
}
|
||||
}
|
||||
$this->children = [];
|
||||
}
|
||||
}
|
||||
123
vendor/sabre/vobject/lib/Component/Available.php
vendored
Normal file
123
vendor/sabre/vobject/lib/Component/Available.php
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* The Available sub-component.
|
||||
*
|
||||
* This component adds functionality to a component, specific for AVAILABLE
|
||||
* components.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Ivan Enderlin
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Available extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns the 'effective start' and 'effective end' of this VAVAILABILITY
|
||||
* component.
|
||||
*
|
||||
* We use the DTSTART and DTEND or DURATION to determine this.
|
||||
*
|
||||
* The returned value is an array containing DateTimeImmutable instances.
|
||||
* If either the start or end is 'unbounded' its value will be null
|
||||
* instead.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEffectiveStartEnd()
|
||||
{
|
||||
$effectiveStart = $this->DTSTART->getDateTime();
|
||||
if (isset($this->DTEND)) {
|
||||
$effectiveEnd = $this->DTEND->getDateTime();
|
||||
} else {
|
||||
$effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
|
||||
}
|
||||
|
||||
return [$effectiveStart, $effectiveEnd];
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTART' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
|
||||
'DTEND' => '?',
|
||||
'DURATION' => '?',
|
||||
|
||||
'CREATED' => '?',
|
||||
'DESCRIPTION' => '?',
|
||||
'LAST-MODIFIED' => '?',
|
||||
'RECURRENCE-ID' => '?',
|
||||
'RRULE' => '?',
|
||||
'SUMMARY' => '?',
|
||||
|
||||
'CATEGORIES' => '*',
|
||||
'COMMENT' => '*',
|
||||
'CONTACT' => '*',
|
||||
'EXDATE' => '*',
|
||||
'RDATE' => '*',
|
||||
|
||||
'AVAILABLE' => '*',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
|
||||
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on).
|
||||
* 2 - A warning.
|
||||
* 3 - An error.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$result = parent::validate($options);
|
||||
|
||||
if (isset($this->DTEND) && isset($this->DURATION)) {
|
||||
$result[] = [
|
||||
'level' => 3,
|
||||
'message' => 'DTEND and DURATION cannot both be present',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
138
vendor/sabre/vobject/lib/Component/VAlarm.php
vendored
Normal file
138
vendor/sabre/vobject/lib/Component/VAlarm.php
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
|
||||
/**
|
||||
* VAlarm component.
|
||||
*
|
||||
* This component contains some additional functionality specific for VALARMs.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VAlarm extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns a DateTime object when this alarm is going to trigger.
|
||||
*
|
||||
* This ignores repeated alarm, only the first trigger is returned.
|
||||
*
|
||||
* @return DateTimeImmutable
|
||||
*/
|
||||
public function getEffectiveTriggerTime()
|
||||
{
|
||||
$trigger = $this->TRIGGER;
|
||||
if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) {
|
||||
$triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
|
||||
$related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START';
|
||||
|
||||
$parentComponent = $this->parent;
|
||||
if ('START' === $related) {
|
||||
if ('VTODO' === $parentComponent->name) {
|
||||
$propName = 'DUE';
|
||||
} else {
|
||||
$propName = 'DTSTART';
|
||||
}
|
||||
|
||||
$effectiveTrigger = $parentComponent->$propName->getDateTime();
|
||||
$effectiveTrigger = $effectiveTrigger->add($triggerDuration);
|
||||
} else {
|
||||
if ('VTODO' === $parentComponent->name) {
|
||||
$endProp = 'DUE';
|
||||
} elseif ('VEVENT' === $parentComponent->name) {
|
||||
$endProp = 'DTEND';
|
||||
} else {
|
||||
throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
|
||||
}
|
||||
|
||||
if (isset($parentComponent->$endProp)) {
|
||||
$effectiveTrigger = $parentComponent->$endProp->getDateTime();
|
||||
$effectiveTrigger = $effectiveTrigger->add($triggerDuration);
|
||||
} elseif (isset($parentComponent->DURATION)) {
|
||||
$effectiveTrigger = $parentComponent->DTSTART->getDateTime();
|
||||
$duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
|
||||
$effectiveTrigger = $effectiveTrigger->add($duration);
|
||||
$effectiveTrigger = $effectiveTrigger->add($triggerDuration);
|
||||
} else {
|
||||
$effectiveTrigger = $parentComponent->DTSTART->getDateTime();
|
||||
$effectiveTrigger = $effectiveTrigger->add($triggerDuration);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$effectiveTrigger = $trigger->getDateTime();
|
||||
}
|
||||
|
||||
return $effectiveTrigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true or false depending on if the event falls in the specified
|
||||
* time-range. This is used for filtering purposes.
|
||||
*
|
||||
* The rules used to determine if an event falls within the specified
|
||||
* time-range is based on the CalDAV specification.
|
||||
*
|
||||
* @param DateTime $start
|
||||
* @param DateTime $end
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
|
||||
{
|
||||
$effectiveTrigger = $this->getEffectiveTriggerTime();
|
||||
|
||||
if (isset($this->DURATION)) {
|
||||
$duration = VObject\DateTimeParser::parseDuration($this->DURATION);
|
||||
$repeat = (string) $this->REPEAT;
|
||||
if (!$repeat) {
|
||||
$repeat = 1;
|
||||
}
|
||||
|
||||
$period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat);
|
||||
|
||||
foreach ($period as $occurrence) {
|
||||
if ($start <= $occurrence && $end > $occurrence) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} else {
|
||||
return $start <= $effectiveTrigger && $end > $effectiveTrigger;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'ACTION' => 1,
|
||||
'TRIGGER' => 1,
|
||||
|
||||
'DURATION' => '?',
|
||||
'REPEAT' => '?',
|
||||
|
||||
'ATTACH' => '?',
|
||||
];
|
||||
}
|
||||
}
|
||||
149
vendor/sabre/vobject/lib/Component/VAvailability.php
vendored
Normal file
149
vendor/sabre/vobject/lib/Component/VAvailability.php
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* The VAvailability component.
|
||||
*
|
||||
* This component adds functionality to a component, specific for VAVAILABILITY
|
||||
* components.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Ivan Enderlin
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VAvailability extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns true or false depending on if the event falls in the specified
|
||||
* time-range. This is used for filtering purposes.
|
||||
*
|
||||
* The rules used to determine if an event falls within the specified
|
||||
* time-range is based on:
|
||||
*
|
||||
* https://tools.ietf.org/html/draft-daboo-calendar-availability-05#section-3.1
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
|
||||
{
|
||||
list($effectiveStart, $effectiveEnd) = $this->getEffectiveStartEnd();
|
||||
|
||||
return
|
||||
(is_null($effectiveStart) || $start < $effectiveEnd) &&
|
||||
(is_null($effectiveEnd) || $end > $effectiveStart)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'effective start' and 'effective end' of this VAVAILABILITY
|
||||
* component.
|
||||
*
|
||||
* We use the DTSTART and DTEND or DURATION to determine this.
|
||||
*
|
||||
* The returned value is an array containing DateTimeImmutable instances.
|
||||
* If either the start or end is 'unbounded' its value will be null
|
||||
* instead.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getEffectiveStartEnd()
|
||||
{
|
||||
$effectiveStart = null;
|
||||
$effectiveEnd = null;
|
||||
|
||||
if (isset($this->DTSTART)) {
|
||||
$effectiveStart = $this->DTSTART->getDateTime();
|
||||
}
|
||||
if (isset($this->DTEND)) {
|
||||
$effectiveEnd = $this->DTEND->getDateTime();
|
||||
} elseif ($effectiveStart && isset($this->DURATION)) {
|
||||
$effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
|
||||
}
|
||||
|
||||
return [$effectiveStart, $effectiveEnd];
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
|
||||
'BUSYTYPE' => '?',
|
||||
'CLASS' => '?',
|
||||
'CREATED' => '?',
|
||||
'DESCRIPTION' => '?',
|
||||
'DTSTART' => '?',
|
||||
'LAST-MODIFIED' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'PRIORITY' => '?',
|
||||
'SEQUENCE' => '?',
|
||||
'SUMMARY' => '?',
|
||||
'URL' => '?',
|
||||
'DTEND' => '?',
|
||||
'DURATION' => '?',
|
||||
|
||||
'CATEGORIES' => '*',
|
||||
'COMMENT' => '*',
|
||||
'CONTACT' => '*',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
|
||||
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on).
|
||||
* 2 - A warning.
|
||||
* 3 - An error.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$result = parent::validate($options);
|
||||
|
||||
if (isset($this->DTEND) && isset($this->DURATION)) {
|
||||
$result[] = [
|
||||
'level' => 3,
|
||||
'message' => 'DTEND and DURATION cannot both be present',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
528
vendor/sabre/vobject/lib/Component/VCalendar.php
vendored
Normal file
528
vendor/sabre/vobject/lib/Component/VCalendar.php
vendored
Normal file
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject;
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
use Sabre\VObject\Property;
|
||||
use Sabre\VObject\Recur\EventIterator;
|
||||
use Sabre\VObject\Recur\NoInstancesException;
|
||||
|
||||
/**
|
||||
* The VCalendar component.
|
||||
*
|
||||
* This component adds functionality to a component, specific for a VCALENDAR.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VCalendar extends VObject\Document
|
||||
{
|
||||
/**
|
||||
* The default name for this component.
|
||||
*
|
||||
* This should be 'VCALENDAR' or 'VCARD'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $defaultName = 'VCALENDAR';
|
||||
|
||||
/**
|
||||
* This is a list of components, and which classes they should map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $componentMap = [
|
||||
'VCALENDAR' => self::class,
|
||||
'VALARM' => VAlarm::class,
|
||||
'VEVENT' => VEvent::class,
|
||||
'VFREEBUSY' => VFreeBusy::class,
|
||||
'VAVAILABILITY' => VAvailability::class,
|
||||
'AVAILABLE' => Available::class,
|
||||
'VJOURNAL' => VJournal::class,
|
||||
'VTIMEZONE' => VTimeZone::class,
|
||||
'VTODO' => VTodo::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of value-types, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $valueMap = [
|
||||
'BINARY' => VObject\Property\Binary::class,
|
||||
'BOOLEAN' => VObject\Property\Boolean::class,
|
||||
'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'DATE' => VObject\Property\ICalendar\Date::class,
|
||||
'DATE-TIME' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DURATION' => VObject\Property\ICalendar\Duration::class,
|
||||
'FLOAT' => VObject\Property\FloatValue::class,
|
||||
'INTEGER' => VObject\Property\IntegerValue::class,
|
||||
'PERIOD' => VObject\Property\ICalendar\Period::class,
|
||||
'RECUR' => VObject\Property\ICalendar\Recur::class,
|
||||
'TEXT' => VObject\Property\Text::class,
|
||||
'TIME' => VObject\Property\Time::class,
|
||||
'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
|
||||
'URI' => VObject\Property\Uri::class,
|
||||
'UTC-OFFSET' => VObject\Property\UtcOffset::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of properties, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $propertyMap = [
|
||||
// Calendar properties
|
||||
'CALSCALE' => VObject\Property\FlatText::class,
|
||||
'METHOD' => VObject\Property\FlatText::class,
|
||||
'PRODID' => VObject\Property\FlatText::class,
|
||||
'VERSION' => VObject\Property\FlatText::class,
|
||||
|
||||
// Component properties
|
||||
'ATTACH' => VObject\Property\Uri::class,
|
||||
'CATEGORIES' => VObject\Property\Text::class,
|
||||
'CLASS' => VObject\Property\FlatText::class,
|
||||
'COMMENT' => VObject\Property\FlatText::class,
|
||||
'DESCRIPTION' => VObject\Property\FlatText::class,
|
||||
'GEO' => VObject\Property\FloatValue::class,
|
||||
'LOCATION' => VObject\Property\FlatText::class,
|
||||
'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class,
|
||||
'PRIORITY' => VObject\Property\IntegerValue::class,
|
||||
'RESOURCES' => VObject\Property\Text::class,
|
||||
'STATUS' => VObject\Property\FlatText::class,
|
||||
'SUMMARY' => VObject\Property\FlatText::class,
|
||||
|
||||
// Date and Time Component Properties
|
||||
'COMPLETED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTEND' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DUE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTSTART' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DURATION' => VObject\Property\ICalendar\Duration::class,
|
||||
'FREEBUSY' => VObject\Property\ICalendar\Period::class,
|
||||
'TRANSP' => VObject\Property\FlatText::class,
|
||||
|
||||
// Time Zone Component Properties
|
||||
'TZID' => VObject\Property\FlatText::class,
|
||||
'TZNAME' => VObject\Property\FlatText::class,
|
||||
'TZOFFSETFROM' => VObject\Property\UtcOffset::class,
|
||||
'TZOFFSETTO' => VObject\Property\UtcOffset::class,
|
||||
'TZURL' => VObject\Property\Uri::class,
|
||||
|
||||
// Relationship Component Properties
|
||||
'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'CONTACT' => VObject\Property\FlatText::class,
|
||||
'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class,
|
||||
'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RELATED-TO' => VObject\Property\FlatText::class,
|
||||
'URL' => VObject\Property\Uri::class,
|
||||
'UID' => VObject\Property\FlatText::class,
|
||||
|
||||
// Recurrence Component Properties
|
||||
'EXDATE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RDATE' => VObject\Property\ICalendar\DateTime::class,
|
||||
'RRULE' => VObject\Property\ICalendar\Recur::class,
|
||||
'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545
|
||||
|
||||
// Alarm Component Properties
|
||||
'ACTION' => VObject\Property\FlatText::class,
|
||||
'REPEAT' => VObject\Property\IntegerValue::class,
|
||||
'TRIGGER' => VObject\Property\ICalendar\Duration::class,
|
||||
|
||||
// Change Management Component Properties
|
||||
'CREATED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'DTSTAMP' => VObject\Property\ICalendar\DateTime::class,
|
||||
'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'SEQUENCE' => VObject\Property\IntegerValue::class,
|
||||
|
||||
// Request Status
|
||||
'REQUEST-STATUS' => VObject\Property\Text::class,
|
||||
|
||||
// Additions from draft-daboo-valarm-extensions-04
|
||||
'ALARM-AGENT' => VObject\Property\Text::class,
|
||||
'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class,
|
||||
'PROXIMITY' => VObject\Property\Text::class,
|
||||
'DEFAULT-ALARM' => VObject\Property\Boolean::class,
|
||||
|
||||
// Additions from draft-daboo-calendar-availability-05
|
||||
'BUSYTYPE' => VObject\Property\Text::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the current document type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDocumentType()
|
||||
{
|
||||
return self::ICALENDAR20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all 'base components'. For instance, if an Event has
|
||||
* a recurrence rule, and one instance is overridden, the overridden event
|
||||
* will have the same UID, but will be excluded from this list.
|
||||
*
|
||||
* VTIMEZONE components will always be excluded.
|
||||
*
|
||||
* @param string $componentName filter by component name
|
||||
*
|
||||
* @return VObject\Component[]
|
||||
*/
|
||||
public function getBaseComponents($componentName = null)
|
||||
{
|
||||
$isBaseComponent = function ($component) {
|
||||
if (!$component instanceof VObject\Component) {
|
||||
return false;
|
||||
}
|
||||
if ('VTIMEZONE' === $component->name) {
|
||||
return false;
|
||||
}
|
||||
if (isset($component->{'RECURRENCE-ID'})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if ($componentName) {
|
||||
// Early exit
|
||||
return array_filter(
|
||||
$this->select($componentName),
|
||||
$isBaseComponent
|
||||
);
|
||||
}
|
||||
|
||||
$components = [];
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if (!$child instanceof Component) {
|
||||
// If one child is not a component, they all are so we skip
|
||||
// the entire group.
|
||||
continue 2;
|
||||
}
|
||||
if ($isBaseComponent($child)) {
|
||||
$components[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first component that is not a VTIMEZONE, and does not have
|
||||
* an RECURRENCE-ID.
|
||||
*
|
||||
* If there is no such component, null will be returned.
|
||||
*
|
||||
* @param string $componentName filter by component name
|
||||
*
|
||||
* @return VObject\Component|null
|
||||
*/
|
||||
public function getBaseComponent($componentName = null)
|
||||
{
|
||||
$isBaseComponent = function ($component) {
|
||||
if (!$component instanceof VObject\Component) {
|
||||
return false;
|
||||
}
|
||||
if ('VTIMEZONE' === $component->name) {
|
||||
return false;
|
||||
}
|
||||
if (isset($component->{'RECURRENCE-ID'})) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if ($componentName) {
|
||||
foreach ($this->select($componentName) as $child) {
|
||||
if ($isBaseComponent($child)) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Searching all components
|
||||
foreach ($this->children as $childGroup) {
|
||||
foreach ($childGroup as $child) {
|
||||
if ($isBaseComponent($child)) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand all events in this VCalendar object and return a new VCalendar
|
||||
* with the expanded events.
|
||||
*
|
||||
* If this calendar object, has events with recurrence rules, this method
|
||||
* can be used to expand the event into multiple sub-events.
|
||||
*
|
||||
* Each event will be stripped from its recurrence information, and only
|
||||
* the instances of the event in the specified timerange will be left
|
||||
* alone.
|
||||
*
|
||||
* In addition, this method will cause timezone information to be stripped,
|
||||
* and normalized to UTC.
|
||||
*
|
||||
* @param DateTimeZone $timeZone reference timezone for floating dates and
|
||||
* times
|
||||
*
|
||||
* @return VCalendar
|
||||
*/
|
||||
public function expand(DateTimeInterface $start, DateTimeInterface $end, ?DateTimeZone $timeZone = null)
|
||||
{
|
||||
$newChildren = [];
|
||||
$recurringEvents = [];
|
||||
|
||||
if (!$timeZone) {
|
||||
$timeZone = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
$stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) {
|
||||
foreach ($component->children() as $componentChild) {
|
||||
if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) {
|
||||
$dt = $componentChild->getDateTimes($timeZone);
|
||||
// We only need to update the first timezone, because
|
||||
// setDateTimes will match all other timezones to the
|
||||
// first.
|
||||
$dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC'));
|
||||
$componentChild->setDateTimes($dt);
|
||||
} elseif ($componentChild instanceof Component) {
|
||||
$stripTimezones($componentChild);
|
||||
}
|
||||
}
|
||||
|
||||
return $component;
|
||||
};
|
||||
|
||||
foreach ($this->children() as $child) {
|
||||
if ($child instanceof Property && 'PRODID' !== $child->name) {
|
||||
// We explicitly want to ignore PRODID, because we want to
|
||||
// overwrite it with our own.
|
||||
$newChildren[] = clone $child;
|
||||
} elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) {
|
||||
// We're also stripping all VTIMEZONE objects because we're
|
||||
// converting everything to UTC.
|
||||
if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) {
|
||||
// Handle these a bit later.
|
||||
$uid = (string) $child->UID;
|
||||
if (!$uid) {
|
||||
throw new InvalidDataException('Every VEVENT object must have a UID property');
|
||||
}
|
||||
if (isset($recurringEvents[$uid])) {
|
||||
$recurringEvents[$uid][] = clone $child;
|
||||
} else {
|
||||
$recurringEvents[$uid] = [clone $child];
|
||||
}
|
||||
} elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) {
|
||||
$newChildren[] = $stripTimezones(clone $child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($recurringEvents as $events) {
|
||||
try {
|
||||
$it = new EventIterator($events, null, $timeZone);
|
||||
} catch (NoInstancesException $e) {
|
||||
// This event is recurring, but it doesn't have a single
|
||||
// instance. We are skipping this event from the output
|
||||
// entirely.
|
||||
continue;
|
||||
}
|
||||
$it->fastForward($start);
|
||||
|
||||
while ($it->valid() && $it->getDTStart() < $end) {
|
||||
if ($it->getDTEnd() > $start) {
|
||||
$newChildren[] = $stripTimezones($it->getEventObject());
|
||||
}
|
||||
$it->next();
|
||||
}
|
||||
}
|
||||
|
||||
return new self($newChildren);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [
|
||||
'VERSION' => '2.0',
|
||||
'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
|
||||
'CALSCALE' => 'GREGORIAN',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'PRODID' => 1,
|
||||
'VERSION' => 1,
|
||||
|
||||
'CALSCALE' => '?',
|
||||
'METHOD' => '?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
* Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
|
||||
* Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on).
|
||||
* 2 - A warning.
|
||||
* 3 - An error.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$warnings = parent::validate($options);
|
||||
|
||||
if ($ver = $this->VERSION) {
|
||||
if ('2.0' !== (string) $ver) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$uidList = [];
|
||||
$componentsFound = 0;
|
||||
$componentTypes = [];
|
||||
|
||||
foreach ($this->children() as $child) {
|
||||
if ($child instanceof Component) {
|
||||
++$componentsFound;
|
||||
|
||||
if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) {
|
||||
continue;
|
||||
}
|
||||
$componentTypes[] = $child->name;
|
||||
|
||||
$uid = (string) $child->UID;
|
||||
$isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1;
|
||||
if (isset($uidList[$uid])) {
|
||||
++$uidList[$uid]['count'];
|
||||
if ($isMaster && $uidList[$uid]['hasMaster']) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'More than one master object was found for the object with UID '.$uid,
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
$uidList[$uid]['hasMaster'] += $isMaster;
|
||||
} else {
|
||||
$uidList[$uid] = [
|
||||
'count' => 1,
|
||||
'hasMaster' => $isMaster,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === $componentsFound) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'An iCalendar object must have at least 1 component.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
if ($options & self::PROFILE_CALDAV) {
|
||||
if (count($uidList) > 1) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
if (0 === count($componentTypes)) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
if (count(array_unique($componentTypes)) > 1) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($this->METHOD)) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all components with a specific UID value.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getByUID($uid)
|
||||
{
|
||||
return array_filter($this->getComponents(), function ($item) use ($uid) {
|
||||
if (!$itemUid = $item->select('UID')) {
|
||||
return false;
|
||||
}
|
||||
$itemUid = current($itemUid)->getValue();
|
||||
|
||||
return $uid === $itemUid;
|
||||
});
|
||||
}
|
||||
}
|
||||
541
vendor/sabre/vobject/lib/Component/VCard.php
vendored
Normal file
541
vendor/sabre/vobject/lib/Component/VCard.php
vendored
Normal file
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use Sabre\VObject;
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
* The VCard component.
|
||||
*
|
||||
* This component represents the BEGIN:VCARD and END:VCARD found in every
|
||||
* vcard.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VCard extends VObject\Document
|
||||
{
|
||||
/**
|
||||
* The default name for this component.
|
||||
*
|
||||
* This should be 'VCALENDAR' or 'VCARD'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $defaultName = 'VCARD';
|
||||
|
||||
/**
|
||||
* Caching the version number.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $version = null;
|
||||
|
||||
/**
|
||||
* This is a list of components, and which classes they should map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $componentMap = [
|
||||
'VCARD' => VCard::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of value-types, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $valueMap = [
|
||||
'BINARY' => VObject\Property\Binary::class,
|
||||
'BOOLEAN' => VObject\Property\Boolean::class,
|
||||
'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only
|
||||
'DATE' => VObject\Property\VCard\Date::class,
|
||||
'DATE-TIME' => VObject\Property\VCard\DateTime::class,
|
||||
'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only
|
||||
'FLOAT' => VObject\Property\FloatValue::class,
|
||||
'INTEGER' => VObject\Property\IntegerValue::class,
|
||||
'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class,
|
||||
'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only
|
||||
'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class,
|
||||
'TEXT' => VObject\Property\Text::class,
|
||||
'TIME' => VObject\Property\Time::class,
|
||||
'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
|
||||
'URI' => VObject\Property\Uri::class,
|
||||
'URL' => VObject\Property\Uri::class, // vCard 2.1 only
|
||||
'UTC-OFFSET' => VObject\Property\UtcOffset::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of properties, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $propertyMap = [
|
||||
// vCard 2.1 properties and up
|
||||
'N' => VObject\Property\Text::class,
|
||||
'FN' => VObject\Property\FlatText::class,
|
||||
'PHOTO' => VObject\Property\Binary::class,
|
||||
'BDAY' => VObject\Property\VCard\DateAndOrTime::class,
|
||||
'ADR' => VObject\Property\Text::class,
|
||||
'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0
|
||||
'TEL' => VObject\Property\FlatText::class,
|
||||
'EMAIL' => VObject\Property\FlatText::class,
|
||||
'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0
|
||||
'GEO' => VObject\Property\FlatText::class,
|
||||
'TITLE' => VObject\Property\FlatText::class,
|
||||
'ROLE' => VObject\Property\FlatText::class,
|
||||
'LOGO' => VObject\Property\Binary::class,
|
||||
// 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
|
||||
// not supported at the moment
|
||||
'ORG' => VObject\Property\Text::class,
|
||||
'NOTE' => VObject\Property\FlatText::class,
|
||||
'REV' => VObject\Property\VCard\TimeStamp::class,
|
||||
'SOUND' => VObject\Property\FlatText::class,
|
||||
'URL' => VObject\Property\Uri::class,
|
||||
'UID' => VObject\Property\FlatText::class,
|
||||
'VERSION' => VObject\Property\FlatText::class,
|
||||
'KEY' => VObject\Property\FlatText::class,
|
||||
'TZ' => VObject\Property\Text::class,
|
||||
|
||||
// vCard 3.0 properties
|
||||
'CATEGORIES' => VObject\Property\Text::class,
|
||||
'SORT-STRING' => VObject\Property\FlatText::class,
|
||||
'PRODID' => VObject\Property\FlatText::class,
|
||||
'NICKNAME' => VObject\Property\Text::class,
|
||||
'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0
|
||||
|
||||
// rfc2739 properties
|
||||
'FBURL' => VObject\Property\Uri::class,
|
||||
'CAPURI' => VObject\Property\Uri::class,
|
||||
'CALURI' => VObject\Property\Uri::class,
|
||||
'CALADRURI' => VObject\Property\Uri::class,
|
||||
|
||||
// rfc4770 properties
|
||||
'IMPP' => VObject\Property\Uri::class,
|
||||
|
||||
// vCard 4.0 properties
|
||||
'SOURCE' => VObject\Property\Uri::class,
|
||||
'XML' => VObject\Property\FlatText::class,
|
||||
'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class,
|
||||
'CLIENTPIDMAP' => VObject\Property\Text::class,
|
||||
'LANG' => VObject\Property\VCard\LanguageTag::class,
|
||||
'GENDER' => VObject\Property\Text::class,
|
||||
'KIND' => VObject\Property\FlatText::class,
|
||||
'MEMBER' => VObject\Property\Uri::class,
|
||||
'RELATED' => VObject\Property\Uri::class,
|
||||
|
||||
// rfc6474 properties
|
||||
'BIRTHPLACE' => VObject\Property\FlatText::class,
|
||||
'DEATHPLACE' => VObject\Property\FlatText::class,
|
||||
'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class,
|
||||
|
||||
// rfc6715 properties
|
||||
'EXPERTISE' => VObject\Property\FlatText::class,
|
||||
'HOBBY' => VObject\Property\FlatText::class,
|
||||
'INTEREST' => VObject\Property\FlatText::class,
|
||||
'ORG-DIRECTORY' => VObject\Property\FlatText::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the current document type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDocumentType()
|
||||
{
|
||||
if (!$this->version) {
|
||||
$version = (string) $this->VERSION;
|
||||
|
||||
switch ($version) {
|
||||
case '2.1':
|
||||
$this->version = self::VCARD21;
|
||||
break;
|
||||
case '3.0':
|
||||
$this->version = self::VCARD30;
|
||||
break;
|
||||
case '4.0':
|
||||
$this->version = self::VCARD40;
|
||||
break;
|
||||
default:
|
||||
// We don't want to cache the version if it's unknown,
|
||||
// because we might get a version property in a bit.
|
||||
return self::UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the document to a different vcard version.
|
||||
*
|
||||
* Use one of the VCARD constants for the target. This method will return
|
||||
* a copy of the vcard in the new version.
|
||||
*
|
||||
* At the moment the only supported conversion is from 3.0 to 4.0.
|
||||
*
|
||||
* If input and output version are identical, a clone is returned.
|
||||
*
|
||||
* @param int $target
|
||||
*
|
||||
* @return VCard
|
||||
*/
|
||||
public function convert($target)
|
||||
{
|
||||
$converter = new VObject\VCardConverter();
|
||||
|
||||
return $converter->convert($this, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* VCards with version 2.1, 3.0 and 4.0 are found.
|
||||
*
|
||||
* If the VCARD doesn't know its version, 2.1 is assumed.
|
||||
*/
|
||||
const DEFAULT_VERSION = self::VCARD21;
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on)
|
||||
* 2 - An inconsequential issue
|
||||
* 3 - A severe issue.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
$versionMap = [
|
||||
self::VCARD21 => '2.1',
|
||||
self::VCARD30 => '3.0',
|
||||
self::VCARD40 => '4.0',
|
||||
];
|
||||
|
||||
$version = $this->select('VERSION');
|
||||
if (1 === count($version)) {
|
||||
$version = (string) $this->VERSION;
|
||||
if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
|
||||
'node' => $this,
|
||||
];
|
||||
if ($options & self::REPAIR) {
|
||||
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
|
||||
}
|
||||
}
|
||||
if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
}
|
||||
$uid = $this->select('UID');
|
||||
if (0 === count($uid)) {
|
||||
if ($options & self::PROFILE_CARDDAV) {
|
||||
// Required for CardDAV
|
||||
$warningLevel = 3;
|
||||
$message = 'vCards on CardDAV servers MUST have a UID property.';
|
||||
} else {
|
||||
// Not required for regular vcards
|
||||
$warningLevel = 2;
|
||||
$message = 'Adding a UID to a vCard property is recommended.';
|
||||
}
|
||||
if ($options & self::REPAIR) {
|
||||
$this->UID = VObject\UUIDUtil::getUUID();
|
||||
$warningLevel = 1;
|
||||
}
|
||||
$warnings[] = [
|
||||
'level' => $warningLevel,
|
||||
'message' => $message,
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
$fn = $this->select('FN');
|
||||
if (1 !== count($fn)) {
|
||||
$repaired = false;
|
||||
if (($options & self::REPAIR) && 0 === count($fn)) {
|
||||
// We're going to try to see if we can use the contents of the
|
||||
// N property.
|
||||
if (isset($this->N)) {
|
||||
$value = explode(';', (string) $this->N);
|
||||
if (isset($value[1]) && $value[1]) {
|
||||
$this->FN = $value[1].' '.$value[0];
|
||||
} else {
|
||||
$this->FN = $value[0];
|
||||
}
|
||||
$repaired = true;
|
||||
|
||||
// Otherwise, the ORG property may work
|
||||
} elseif (isset($this->ORG)) {
|
||||
$this->FN = (string) $this->ORG;
|
||||
$repaired = true;
|
||||
|
||||
// Otherwise, the NICKNAME property may work
|
||||
} elseif (isset($this->NICKNAME)) {
|
||||
$this->FN = (string) $this->NICKNAME;
|
||||
$repaired = true;
|
||||
|
||||
// Otherwise, the EMAIL property may work
|
||||
} elseif (isset($this->EMAIL)) {
|
||||
$this->FN = (string) $this->EMAIL;
|
||||
$repaired = true;
|
||||
}
|
||||
}
|
||||
$warnings[] = [
|
||||
'level' => $repaired ? 1 : 3,
|
||||
'message' => 'The FN property must appear in the VCARD component exactly 1 time',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
parent::validate($options),
|
||||
$warnings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'ADR' => '*',
|
||||
'ANNIVERSARY' => '?',
|
||||
'BDAY' => '?',
|
||||
'CALADRURI' => '*',
|
||||
'CALURI' => '*',
|
||||
'CATEGORIES' => '*',
|
||||
'CLIENTPIDMAP' => '*',
|
||||
'EMAIL' => '*',
|
||||
'FBURL' => '*',
|
||||
'IMPP' => '*',
|
||||
'GENDER' => '?',
|
||||
'GEO' => '*',
|
||||
'KEY' => '*',
|
||||
'KIND' => '?',
|
||||
'LANG' => '*',
|
||||
'LOGO' => '*',
|
||||
'MEMBER' => '*',
|
||||
'N' => '?',
|
||||
'NICKNAME' => '*',
|
||||
'NOTE' => '*',
|
||||
'ORG' => '*',
|
||||
'PHOTO' => '*',
|
||||
'PRODID' => '?',
|
||||
'RELATED' => '*',
|
||||
'REV' => '?',
|
||||
'ROLE' => '*',
|
||||
'SOUND' => '*',
|
||||
'SOURCE' => '*',
|
||||
'TEL' => '*',
|
||||
'TITLE' => '*',
|
||||
'TZ' => '*',
|
||||
'URL' => '*',
|
||||
'VERSION' => '1',
|
||||
'XML' => '*',
|
||||
|
||||
// FN is commented out, because it's already handled by the
|
||||
// validate function, which may also try to repair it.
|
||||
// 'FN' => '+',
|
||||
'UID' => '?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preferred field.
|
||||
*
|
||||
* VCards can indicate whether a field such as ADR, TEL or EMAIL is
|
||||
* preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
|
||||
* being a number between 1 and 100).
|
||||
*
|
||||
* If neither of those parameters are specified, the first is returned, if
|
||||
* a field with that name does not exist, null is returned.
|
||||
*
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return VObject\Property|null
|
||||
*/
|
||||
public function preferred($propertyName)
|
||||
{
|
||||
$preferred = null;
|
||||
$lastPref = 101;
|
||||
foreach ($this->select($propertyName) as $field) {
|
||||
$pref = 101;
|
||||
if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
|
||||
$pref = 1;
|
||||
} elseif (isset($field['PREF'])) {
|
||||
$pref = $field['PREF']->getValue();
|
||||
}
|
||||
|
||||
if ($pref < $lastPref || is_null($preferred)) {
|
||||
$preferred = $field;
|
||||
$lastPref = $pref;
|
||||
}
|
||||
}
|
||||
|
||||
return $preferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
|
||||
*
|
||||
* This function will return null if the property does not exist. If there are
|
||||
* multiple properties with the same TYPE value, only one will be returned.
|
||||
*
|
||||
* @param string $propertyName
|
||||
* @param string $type
|
||||
*
|
||||
* @return VObject\Property|null
|
||||
*/
|
||||
public function getByType($propertyName, $type)
|
||||
{
|
||||
foreach ($this->select($propertyName) as $field) {
|
||||
if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [
|
||||
'VERSION' => '4.0',
|
||||
'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
|
||||
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns an array, with the representation as it should be
|
||||
* encoded in json. This is used to create jCard or jCal documents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
// A vcard does not have sub-components, so we're overriding this
|
||||
// method to remove that array element.
|
||||
$properties = [];
|
||||
|
||||
foreach ($this->children() as $child) {
|
||||
$properties[] = $child->jsonSerialize();
|
||||
}
|
||||
|
||||
return [
|
||||
strtolower($this->name),
|
||||
$properties,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the data into XML. This is used to create xCard or
|
||||
* xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
public function xmlSerialize(Xml\Writer $writer): void
|
||||
{
|
||||
$propertiesByGroup = [];
|
||||
|
||||
foreach ($this->children() as $property) {
|
||||
$group = $property->group;
|
||||
|
||||
if (!isset($propertiesByGroup[$group])) {
|
||||
$propertiesByGroup[$group] = [];
|
||||
}
|
||||
|
||||
$propertiesByGroup[$group][] = $property;
|
||||
}
|
||||
|
||||
$writer->startElement(strtolower($this->name));
|
||||
|
||||
foreach ($propertiesByGroup as $group => $properties) {
|
||||
if (!empty($group)) {
|
||||
$writer->startElement('group');
|
||||
$writer->writeAttribute('name', strtolower($group));
|
||||
}
|
||||
|
||||
foreach ($properties as $property) {
|
||||
switch ($property->name) {
|
||||
case 'VERSION':
|
||||
break;
|
||||
|
||||
case 'XML':
|
||||
$value = $property->getParts();
|
||||
$fragment = new Xml\Element\XmlFragment($value[0]);
|
||||
$writer->write($fragment);
|
||||
break;
|
||||
|
||||
default:
|
||||
$property->xmlSerialize($writer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($group)) {
|
||||
$writer->endElement();
|
||||
}
|
||||
}
|
||||
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default class for a property name.
|
||||
*
|
||||
* @param string $propertyName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getClassNameForPropertyName($propertyName)
|
||||
{
|
||||
$className = parent::getClassNameForPropertyName($propertyName);
|
||||
|
||||
// In vCard 4, BINARY no longer exists, and we need URI instead.
|
||||
if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) {
|
||||
return VObject\Property\Uri::class;
|
||||
}
|
||||
|
||||
return $className;
|
||||
}
|
||||
}
|
||||
140
vendor/sabre/vobject/lib/Component/VEvent.php
vendored
Normal file
140
vendor/sabre/vobject/lib/Component/VEvent.php
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
use Sabre\VObject\Recur\EventIterator;
|
||||
use Sabre\VObject\Recur\NoInstancesException;
|
||||
|
||||
/**
|
||||
* VEvent component.
|
||||
*
|
||||
* This component contains some additional functionality specific for VEVENT's.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VEvent extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns true or false depending on if the event falls in the specified
|
||||
* time-range. This is used for filtering purposes.
|
||||
*
|
||||
* The rules used to determine if an event falls within the specified
|
||||
* time-range is based on the CalDAV specification.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
|
||||
{
|
||||
if ($this->RRULE || $this->RDATE) {
|
||||
try {
|
||||
$it = new EventIterator($this, null, $start->getTimezone());
|
||||
} catch (NoInstancesException $e) {
|
||||
// If we've caught this exception, there are no instances
|
||||
// for the event that fall into the specified time-range.
|
||||
return false;
|
||||
}
|
||||
|
||||
$it->fastForward($start);
|
||||
|
||||
// We fast-forwarded to a spot where the end-time of the
|
||||
// recurrence instance exceeded the start of the requested
|
||||
// time-range.
|
||||
//
|
||||
// If the starttime of the recurrence did not exceed the
|
||||
// end of the time range as well, we have a match.
|
||||
return $it->getDTStart() < $end && $it->getDTEnd() > $start;
|
||||
}
|
||||
|
||||
$effectiveStart = $this->DTSTART->getDateTime($start->getTimezone());
|
||||
if (isset($this->DTEND)) {
|
||||
// The DTEND property is considered non inclusive. So for a 3 day
|
||||
// event in july, dtstart and dtend would have to be July 1st and
|
||||
// July 4th respectively.
|
||||
//
|
||||
// See:
|
||||
// http://tools.ietf.org/html/rfc5545#page-54
|
||||
$effectiveEnd = $this->DTEND->getDateTime($end->getTimezone());
|
||||
} elseif (isset($this->DURATION)) {
|
||||
$effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION));
|
||||
} elseif (!$this->DTSTART->hasTime()) {
|
||||
$effectiveEnd = $effectiveStart->modify('+1 day');
|
||||
} else {
|
||||
$effectiveEnd = $effectiveStart;
|
||||
}
|
||||
|
||||
return
|
||||
($start < $effectiveEnd) && ($end > $effectiveStart)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [
|
||||
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
|
||||
'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
$hasMethod = isset($this->parent->METHOD);
|
||||
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
'DTSTART' => $hasMethod ? '?' : '1',
|
||||
'CLASS' => '?',
|
||||
'CREATED' => '?',
|
||||
'DESCRIPTION' => '?',
|
||||
'GEO' => '?',
|
||||
'LAST-MODIFIED' => '?',
|
||||
'LOCATION' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'PRIORITY' => '?',
|
||||
'SEQUENCE' => '?',
|
||||
'STATUS' => '?',
|
||||
'SUMMARY' => '?',
|
||||
'TRANSP' => '?',
|
||||
'URL' => '?',
|
||||
'RECURRENCE-ID' => '?',
|
||||
'RRULE' => '?',
|
||||
'DTEND' => '?',
|
||||
'DURATION' => '?',
|
||||
|
||||
'ATTACH' => '*',
|
||||
'ATTENDEE' => '*',
|
||||
'CATEGORIES' => '*',
|
||||
'COMMENT' => '*',
|
||||
'CONTACT' => '*',
|
||||
'EXDATE' => '*',
|
||||
'REQUEST-STATUS' => '*',
|
||||
'RELATED-TO' => '*',
|
||||
'RESOURCES' => '*',
|
||||
'RDATE' => '*',
|
||||
];
|
||||
}
|
||||
}
|
||||
93
vendor/sabre/vobject/lib/Component/VFreeBusy.php
vendored
Normal file
93
vendor/sabre/vobject/lib/Component/VFreeBusy.php
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* The VFreeBusy component.
|
||||
*
|
||||
* This component adds functionality to a component, specific for VFREEBUSY
|
||||
* components.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VFreeBusy extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Checks based on the contained FREEBUSY information, if a timeslot is
|
||||
* available.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFree(DateTimeInterface $start, DatetimeInterface $end)
|
||||
{
|
||||
foreach ($this->select('FREEBUSY') as $freebusy) {
|
||||
// We are only interested in FBTYPE=BUSY (the default),
|
||||
// FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
|
||||
if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The freebusy component can hold more than 1 value, separated by
|
||||
// commas.
|
||||
$periods = explode(',', (string) $freebusy);
|
||||
|
||||
foreach ($periods as $period) {
|
||||
// Every period is formatted as [start]/[end]. The start is an
|
||||
// absolute UTC time, the end may be an absolute UTC time, or
|
||||
// duration (relative) value.
|
||||
list($busyStart, $busyEnd) = explode('/', $period);
|
||||
|
||||
$busyStart = VObject\DateTimeParser::parse($busyStart);
|
||||
$busyEnd = VObject\DateTimeParser::parse($busyEnd);
|
||||
if ($busyEnd instanceof \DateInterval) {
|
||||
$busyEnd = $busyStart->add($busyEnd);
|
||||
}
|
||||
|
||||
if ($start < $busyEnd && $end > $busyStart) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
|
||||
'CONTACT' => '?',
|
||||
'DTSTART' => '?',
|
||||
'DTEND' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'URL' => '?',
|
||||
|
||||
'ATTENDEE' => '*',
|
||||
'COMMENT' => '*',
|
||||
'FREEBUSY' => '*',
|
||||
'REQUEST-STATUS' => '*',
|
||||
];
|
||||
}
|
||||
}
|
||||
101
vendor/sabre/vobject/lib/Component/VJournal.php
vendored
Normal file
101
vendor/sabre/vobject/lib/Component/VJournal.php
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* VJournal component.
|
||||
*
|
||||
* This component contains some additional functionality specific for VJOURNALs.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VJournal extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns true or false depending on if the event falls in the specified
|
||||
* time-range. This is used for filtering purposes.
|
||||
*
|
||||
* The rules used to determine if an event falls within the specified
|
||||
* time-range is based on the CalDAV specification.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
|
||||
{
|
||||
$dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null;
|
||||
if ($dtstart) {
|
||||
$effectiveEnd = $dtstart;
|
||||
if (!$this->DTSTART->hasTime()) {
|
||||
$effectiveEnd = $effectiveEnd->modify('+1 day');
|
||||
}
|
||||
|
||||
return $start <= $effectiveEnd && $end > $dtstart;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
|
||||
'CLASS' => '?',
|
||||
'CREATED' => '?',
|
||||
'DTSTART' => '?',
|
||||
'LAST-MODIFIED' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'RECURRENCE-ID' => '?',
|
||||
'SEQUENCE' => '?',
|
||||
'STATUS' => '?',
|
||||
'SUMMARY' => '?',
|
||||
'URL' => '?',
|
||||
|
||||
'RRULE' => '?',
|
||||
|
||||
'ATTACH' => '*',
|
||||
'ATTENDEE' => '*',
|
||||
'CATEGORIES' => '*',
|
||||
'COMMENT' => '*',
|
||||
'CONTACT' => '*',
|
||||
'DESCRIPTION' => '*',
|
||||
'EXDATE' => '*',
|
||||
'RELATED-TO' => '*',
|
||||
'RDATE' => '*',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [
|
||||
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
|
||||
'DTSTAMP' => gmdate('Ymd\\THis\\Z'),
|
||||
];
|
||||
}
|
||||
}
|
||||
63
vendor/sabre/vobject/lib/Component/VTimeZone.php
vendored
Normal file
63
vendor/sabre/vobject/lib/Component/VTimeZone.php
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* The VTimeZone component.
|
||||
*
|
||||
* This component adds functionality to a component, specific for VTIMEZONE
|
||||
* components.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VTimeZone extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns the PHP DateTimeZone for this VTIMEZONE component.
|
||||
*
|
||||
* If we can't accurately determine the timezone, this method will return
|
||||
* UTC.
|
||||
*
|
||||
* @return \DateTimeZone
|
||||
*/
|
||||
public function getTimeZone()
|
||||
{
|
||||
return VObject\TimeZoneUtil::getTimeZone((string) $this->TZID, $this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'TZID' => 1,
|
||||
|
||||
'LAST-MODIFIED' => '?',
|
||||
'TZURL' => '?',
|
||||
|
||||
// At least 1 STANDARD or DAYLIGHT must appear.
|
||||
//
|
||||
// The validator is not specific yet to pick this up, so these
|
||||
// rules are too loose.
|
||||
'STANDARD' => '*',
|
||||
'DAYLIGHT' => '*',
|
||||
];
|
||||
}
|
||||
}
|
||||
181
vendor/sabre/vobject/lib/Component/VTodo.php
vendored
Normal file
181
vendor/sabre/vobject/lib/Component/VTodo.php
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Component;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Sabre\VObject;
|
||||
|
||||
/**
|
||||
* VTodo component.
|
||||
*
|
||||
* This component contains some additional functionality specific for VTODOs.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class VTodo extends VObject\Component
|
||||
{
|
||||
/**
|
||||
* Returns true or false depending on if the event falls in the specified
|
||||
* time-range. This is used for filtering purposes.
|
||||
*
|
||||
* The rules used to determine if an event falls within the specified
|
||||
* time-range is based on the CalDAV specification.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end)
|
||||
{
|
||||
$dtstart = isset($this->DTSTART) ? $this->DTSTART->getDateTime() : null;
|
||||
$duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null;
|
||||
$due = isset($this->DUE) ? $this->DUE->getDateTime() : null;
|
||||
$completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null;
|
||||
$created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null;
|
||||
|
||||
if ($dtstart) {
|
||||
if ($duration) {
|
||||
$effectiveEnd = $dtstart->add($duration);
|
||||
|
||||
return $start <= $effectiveEnd && $end > $dtstart;
|
||||
} elseif ($due) {
|
||||
return
|
||||
($start < $due || $start <= $dtstart) &&
|
||||
($end > $dtstart || $end >= $due);
|
||||
} else {
|
||||
return $start <= $dtstart && $end > $dtstart;
|
||||
}
|
||||
}
|
||||
if ($due) {
|
||||
return $start < $due && $end >= $due;
|
||||
}
|
||||
if ($completed && $created) {
|
||||
return
|
||||
($start <= $created || $start <= $completed) &&
|
||||
($end >= $created || $end >= $completed);
|
||||
}
|
||||
if ($completed) {
|
||||
return $start <= $completed && $end >= $completed;
|
||||
}
|
||||
if ($created) {
|
||||
return $end > $created;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple list of validation rules.
|
||||
*
|
||||
* This is simply a list of properties, and how many times they either
|
||||
* must or must not appear.
|
||||
*
|
||||
* Possible values per property:
|
||||
* * 0 - Must not appear.
|
||||
* * 1 - Must appear exactly once.
|
||||
* * + - Must appear at least once.
|
||||
* * * - Can appear any number of times.
|
||||
* * ? - May appear, but not more than once.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function getValidationRules()
|
||||
{
|
||||
return [
|
||||
'UID' => 1,
|
||||
'DTSTAMP' => 1,
|
||||
|
||||
'CLASS' => '?',
|
||||
'COMPLETED' => '?',
|
||||
'CREATED' => '?',
|
||||
'DESCRIPTION' => '?',
|
||||
'DTSTART' => '?',
|
||||
'GEO' => '?',
|
||||
'LAST-MODIFIED' => '?',
|
||||
'LOCATION' => '?',
|
||||
'ORGANIZER' => '?',
|
||||
'PERCENT' => '?',
|
||||
'PRIORITY' => '?',
|
||||
'RECURRENCE-ID' => '?',
|
||||
'SEQUENCE' => '?',
|
||||
'STATUS' => '?',
|
||||
'SUMMARY' => '?',
|
||||
'URL' => '?',
|
||||
|
||||
'RRULE' => '?',
|
||||
'DUE' => '?',
|
||||
'DURATION' => '?',
|
||||
|
||||
'ATTACH' => '*',
|
||||
'ATTENDEE' => '*',
|
||||
'CATEGORIES' => '*',
|
||||
'COMMENT' => '*',
|
||||
'CONTACT' => '*',
|
||||
'EXDATE' => '*',
|
||||
'REQUEST-STATUS' => '*',
|
||||
'RELATED-TO' => '*',
|
||||
'RESOURCES' => '*',
|
||||
'RDATE' => '*',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on)
|
||||
* 2 - An inconsequential issue
|
||||
* 3 - A severe issue.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$result = parent::validate($options);
|
||||
if (isset($this->DUE) && isset($this->DTSTART)) {
|
||||
$due = $this->DUE;
|
||||
$dtStart = $this->DTSTART;
|
||||
|
||||
if ($due->getValueType() !== $dtStart->getValueType()) {
|
||||
$result[] = [
|
||||
'level' => 3,
|
||||
'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART',
|
||||
'node' => $due,
|
||||
];
|
||||
} elseif ($due->getDateTime() < $dtStart->getDateTime()) {
|
||||
$result[] = [
|
||||
'level' => 3,
|
||||
'message' => 'DUE must occur after DTSTART',
|
||||
'node' => $due,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should return a list of default property values.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaults()
|
||||
{
|
||||
return [
|
||||
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
|
||||
'DTSTAMP' => date('Ymd\\THis\\Z'),
|
||||
];
|
||||
}
|
||||
}
|
||||
560
vendor/sabre/vobject/lib/DateTimeParser.php
vendored
Normal file
560
vendor/sabre/vobject/lib/DateTimeParser.php
vendored
Normal file
@@ -0,0 +1,560 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* DateTimeParser.
|
||||
*
|
||||
* This class is responsible for parsing the several different date and time
|
||||
* formats iCalendar and vCards have.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class DateTimeParser
|
||||
{
|
||||
/**
|
||||
* Parses an iCalendar (rfc5545) formatted datetime and returns a
|
||||
* DateTimeImmutable object.
|
||||
*
|
||||
* Specifying a reference timezone is optional. It will only be used
|
||||
* if the non-UTC format is used. The argument is used as a reference, the
|
||||
* returned DateTimeImmutable object will still be in the UTC timezone.
|
||||
*
|
||||
* @param string $dt
|
||||
* @param DateTimeZone $tz
|
||||
*
|
||||
* @return DateTimeImmutable
|
||||
*/
|
||||
public static function parseDateTime($dt, ?DateTimeZone $tz = null)
|
||||
{
|
||||
// Format is YYYYMMDD + "T" + hhmmss
|
||||
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches);
|
||||
|
||||
if (!$result) {
|
||||
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
|
||||
}
|
||||
|
||||
if ('Z' === $matches[7] || is_null($tz)) {
|
||||
$tz = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz);
|
||||
} catch (\Exception $e) {
|
||||
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.
|
||||
*
|
||||
* @param string $date
|
||||
* @param DateTimeZone $tz
|
||||
*
|
||||
* @return DateTimeImmutable
|
||||
*/
|
||||
public static function parseDate($date, ?DateTimeZone $tz = null)
|
||||
{
|
||||
// Format is YYYYMMDD
|
||||
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches);
|
||||
|
||||
if (!$result) {
|
||||
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
|
||||
}
|
||||
|
||||
if (is_null($tz)) {
|
||||
$tz = new DateTimeZone('UTC');
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz);
|
||||
} catch (\Exception $e) {
|
||||
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an iCalendar (RFC5545) formatted duration value.
|
||||
*
|
||||
* This method will either return a DateTimeInterval object, or a string
|
||||
* suitable for strtotime or DateTime::modify.
|
||||
*
|
||||
* @param string $duration
|
||||
* @param bool $asString
|
||||
*
|
||||
* @return DateInterval|string
|
||||
*/
|
||||
public static function parseDuration($duration, $asString = false)
|
||||
{
|
||||
$result = preg_match('/^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/', $duration, $matches);
|
||||
if (!$result) {
|
||||
throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration);
|
||||
}
|
||||
|
||||
if (!$asString) {
|
||||
$invert = false;
|
||||
|
||||
if (isset($matches['plusminus']) && '-' === $matches['plusminus']) {
|
||||
$invert = true;
|
||||
}
|
||||
|
||||
$parts = [
|
||||
'week',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
|
||||
}
|
||||
|
||||
// We need to re-construct the $duration string, because weeks and
|
||||
// days are not supported by DateInterval in the same string.
|
||||
$duration = 'P';
|
||||
$days = $matches['day'];
|
||||
|
||||
if ($matches['week']) {
|
||||
$days += $matches['week'] * 7;
|
||||
}
|
||||
|
||||
if ($days) {
|
||||
$duration .= $days.'D';
|
||||
}
|
||||
|
||||
if ($matches['minute'] || $matches['second'] || $matches['hour']) {
|
||||
$duration .= 'T';
|
||||
|
||||
if ($matches['hour']) {
|
||||
$duration .= $matches['hour'].'H';
|
||||
}
|
||||
|
||||
if ($matches['minute']) {
|
||||
$duration .= $matches['minute'].'M';
|
||||
}
|
||||
|
||||
if ($matches['second']) {
|
||||
$duration .= $matches['second'].'S';
|
||||
}
|
||||
}
|
||||
|
||||
if ('P' === $duration) {
|
||||
$duration = 'PT0S';
|
||||
}
|
||||
|
||||
$iv = new DateInterval($duration);
|
||||
|
||||
if ($invert) {
|
||||
$iv->invert = true;
|
||||
}
|
||||
|
||||
return $iv;
|
||||
}
|
||||
|
||||
$parts = [
|
||||
'week',
|
||||
'day',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
];
|
||||
|
||||
$newDur = '';
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (isset($matches[$part]) && $matches[$part]) {
|
||||
$newDur .= ' '.$matches[$part].' '.$part.'s';
|
||||
}
|
||||
}
|
||||
|
||||
$newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur);
|
||||
|
||||
if ('+' === $newDur) {
|
||||
$newDur = '+0 seconds';
|
||||
}
|
||||
|
||||
return $newDur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses either a Date or DateTime, or Duration value.
|
||||
*
|
||||
* @param string $date
|
||||
* @param DateTimeZone|string $referenceTz
|
||||
*
|
||||
* @return DateTimeImmutable|DateInterval
|
||||
*/
|
||||
public static function parse($date, $referenceTz = null)
|
||||
{
|
||||
if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) {
|
||||
return self::parseDuration($date);
|
||||
} elseif (8 === strlen($date)) {
|
||||
return self::parseDate($date, $referenceTz);
|
||||
} else {
|
||||
return self::parseDateTime($date, $referenceTz);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method parses a vCard date and or time value.
|
||||
*
|
||||
* This can be used for the DATE, DATE-TIME, TIMESTAMP and
|
||||
* DATE-AND-OR-TIME value.
|
||||
*
|
||||
* This method returns an array, not a DateTime value.
|
||||
*
|
||||
* The elements in the array are in the following order:
|
||||
* year, month, date, hour, minute, second, timezone
|
||||
*
|
||||
* Almost any part of the string may be omitted. It's for example legal to
|
||||
* just specify seconds, leave out the year, etc.
|
||||
*
|
||||
* Timezone is either returned as 'Z' or as '+0800'
|
||||
*
|
||||
* For any non-specified values null is returned.
|
||||
*
|
||||
* List of date formats that are supported:
|
||||
* YYYY
|
||||
* YYYY-MM
|
||||
* YYYYMMDD
|
||||
* --MMDD
|
||||
* ---DD
|
||||
*
|
||||
* YYYY-MM-DD
|
||||
* --MM-DD
|
||||
* ---DD
|
||||
*
|
||||
* List of supported time formats:
|
||||
*
|
||||
* HH
|
||||
* HHMM
|
||||
* HHMMSS
|
||||
* -MMSS
|
||||
* --SS
|
||||
*
|
||||
* HH
|
||||
* HH:MM
|
||||
* HH:MM:SS
|
||||
* -MM:SS
|
||||
* --SS
|
||||
*
|
||||
* A full basic-format date-time string looks like :
|
||||
* 20130603T133901
|
||||
*
|
||||
* A full extended-format date-time string looks like :
|
||||
* 2013-06-03T13:39:01
|
||||
*
|
||||
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
||||
* UTC, or a string like -0500 or +1100.
|
||||
*
|
||||
* @param string $date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parseVCardDateTime($date)
|
||||
{
|
||||
$regex = '/^
|
||||
(?: # date part
|
||||
(?:
|
||||
(?: (?<year> [0-9]{4}) (?: -)?| --)
|
||||
(?<month> [0-9]{2})?
|
||||
|---)
|
||||
(?<date> [0-9]{2})?
|
||||
)?
|
||||
(?:T # time part
|
||||
(?<hour> [0-9]{2} | -)
|
||||
(?<minute> [0-9]{2} | -)?
|
||||
(?<second> [0-9]{2})?
|
||||
|
||||
(?: \.[0-9]{3})? # milliseconds
|
||||
(?P<timezone> # timezone offset
|
||||
|
||||
Z | (?: \+|-)(?: [0-9]{4})
|
||||
|
||||
)?
|
||||
|
||||
)?
|
||||
$/x';
|
||||
|
||||
if (!preg_match($regex, $date, $matches)) {
|
||||
// Attempting to parse the extended format.
|
||||
$regex = '/^
|
||||
(?: # date part
|
||||
(?: (?<year> [0-9]{4}) - | -- )
|
||||
(?<month> [0-9]{2}) -
|
||||
(?<date> [0-9]{2})
|
||||
)?
|
||||
(?:T # time part
|
||||
|
||||
(?: (?<hour> [0-9]{2}) : | -)
|
||||
(?: (?<minute> [0-9]{2}) : | -)?
|
||||
(?<second> [0-9]{2})?
|
||||
|
||||
(?: \.[0-9]{3})? # milliseconds
|
||||
(?P<timezone> # timezone offset
|
||||
|
||||
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
|
||||
|
||||
)?
|
||||
|
||||
)?
|
||||
$/x';
|
||||
|
||||
if (!preg_match($regex, $date, $matches)) {
|
||||
throw new InvalidDataException('Invalid vCard date-time string: '.$date);
|
||||
}
|
||||
}
|
||||
$parts = [
|
||||
'year',
|
||||
'month',
|
||||
'date',
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
'timezone',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($parts as $part) {
|
||||
if (empty($matches[$part])) {
|
||||
$result[$part] = null;
|
||||
} elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
|
||||
$result[$part] = null;
|
||||
} else {
|
||||
$result[$part] = $matches[$part];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method parses a vCard TIME value.
|
||||
*
|
||||
* This method returns an array, not a DateTime value.
|
||||
*
|
||||
* The elements in the array are in the following order:
|
||||
* hour, minute, second, timezone
|
||||
*
|
||||
* Almost any part of the string may be omitted. It's for example legal to
|
||||
* just specify seconds, leave out the hour etc.
|
||||
*
|
||||
* Timezone is either returned as 'Z' or as '+08:00'
|
||||
*
|
||||
* For any non-specified values null is returned.
|
||||
*
|
||||
* List of supported time formats:
|
||||
*
|
||||
* HH
|
||||
* HHMM
|
||||
* HHMMSS
|
||||
* -MMSS
|
||||
* --SS
|
||||
*
|
||||
* HH
|
||||
* HH:MM
|
||||
* HH:MM:SS
|
||||
* -MM:SS
|
||||
* --SS
|
||||
*
|
||||
* A full basic-format time string looks like :
|
||||
* 133901
|
||||
*
|
||||
* A full extended-format time string looks like :
|
||||
* 13:39:01
|
||||
*
|
||||
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
||||
* UTC, or a string like -0500 or +11:00.
|
||||
*
|
||||
* @param string $date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parseVCardTime($date)
|
||||
{
|
||||
$regex = '/^
|
||||
(?<hour> [0-9]{2} | -)
|
||||
(?<minute> [0-9]{2} | -)?
|
||||
(?<second> [0-9]{2})?
|
||||
|
||||
(?: \.[0-9]{3})? # milliseconds
|
||||
(?P<timezone> # timezone offset
|
||||
|
||||
Z | (?: \+|-)(?: [0-9]{4})
|
||||
|
||||
)?
|
||||
$/x';
|
||||
|
||||
if (!preg_match($regex, $date, $matches)) {
|
||||
// Attempting to parse the extended format.
|
||||
$regex = '/^
|
||||
(?: (?<hour> [0-9]{2}) : | -)
|
||||
(?: (?<minute> [0-9]{2}) : | -)?
|
||||
(?<second> [0-9]{2})?
|
||||
|
||||
(?: \.[0-9]{3})? # milliseconds
|
||||
(?P<timezone> # timezone offset
|
||||
|
||||
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
|
||||
|
||||
)?
|
||||
$/x';
|
||||
|
||||
if (!preg_match($regex, $date, $matches)) {
|
||||
throw new InvalidDataException('Invalid vCard time string: '.$date);
|
||||
}
|
||||
}
|
||||
$parts = [
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
'timezone',
|
||||
];
|
||||
|
||||
$result = [];
|
||||
foreach ($parts as $part) {
|
||||
if (empty($matches[$part])) {
|
||||
$result[$part] = null;
|
||||
} elseif ('-' === $matches[$part]) {
|
||||
$result[$part] = null;
|
||||
} else {
|
||||
$result[$part] = $matches[$part];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method parses a vCard date and or time value.
|
||||
*
|
||||
* This can be used for the DATE, DATE-TIME and
|
||||
* DATE-AND-OR-TIME value.
|
||||
*
|
||||
* This method returns an array, not a DateTime value.
|
||||
* The elements in the array are in the following order:
|
||||
* year, month, date, hour, minute, second, timezone
|
||||
* Almost any part of the string may be omitted. It's for example legal to
|
||||
* just specify seconds, leave out the year, etc.
|
||||
*
|
||||
* Timezone is either returned as 'Z' or as '+0800'
|
||||
*
|
||||
* For any non-specified values null is returned.
|
||||
*
|
||||
* List of date formats that are supported:
|
||||
* 20150128
|
||||
* 2015-01
|
||||
* --01
|
||||
* --0128
|
||||
* ---28
|
||||
*
|
||||
* List of supported time formats:
|
||||
* 13
|
||||
* 1353
|
||||
* 135301
|
||||
* -53
|
||||
* -5301
|
||||
* --01 (unreachable, see the tests)
|
||||
* --01Z
|
||||
* --01+1234
|
||||
*
|
||||
* List of supported date-time formats:
|
||||
* 20150128T13
|
||||
* --0128T13
|
||||
* ---28T13
|
||||
* ---28T1353
|
||||
* ---28T135301
|
||||
* ---28T13Z
|
||||
* ---28T13+1234
|
||||
*
|
||||
* See the regular expressions for all the possible patterns.
|
||||
*
|
||||
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
||||
* UTC, or a string like -0500 or +1100.
|
||||
*
|
||||
* @param string $date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parseVCardDateAndOrTime($date)
|
||||
{
|
||||
// \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
|
||||
$valueDate = '/^(?J)(?:'.
|
||||
'(?<year>\d{4})(?<month>\d\d)(?<date>\d\d)'.
|
||||
'|(?<year>\d{4})-(?<month>\d\d)'.
|
||||
'|--(?<month>\d\d)(?<date>\d\d)?'.
|
||||
'|---(?<date>\d\d)'.
|
||||
')$/';
|
||||
|
||||
// (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
|
||||
$valueTime = '/^(?J)(?:'.
|
||||
'((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
|
||||
'|-(?<minute>\d\d)(?<second>\d\d)?'.
|
||||
'|--(?<second>\d\d))'.
|
||||
'(?<timezone>(Z|[+\-]\d\d(\d\d)?))?'.
|
||||
')$/';
|
||||
|
||||
// (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
|
||||
$valueDateTime = '/^(?:'.
|
||||
'((?<year0>\d{4})(?<month0>\d\d)(?<date0>\d\d)'.
|
||||
'|--(?<month1>\d\d)(?<date1>\d\d)'.
|
||||
'|---(?<date2>\d\d))'.
|
||||
'T'.
|
||||
'(?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
|
||||
'(?<timezone>(Z|[+\-]\d\d(\d\d?)))?'.
|
||||
')$/';
|
||||
|
||||
// date-and-or-time is date | date-time | time
|
||||
// in this strict order.
|
||||
|
||||
if (0 === preg_match($valueDate, $date, $matches)
|
||||
&& 0 === preg_match($valueDateTime, $date, $matches)
|
||||
&& 0 === preg_match($valueTime, $date, $matches)) {
|
||||
throw new InvalidDataException('Invalid vCard date-time string: '.$date);
|
||||
}
|
||||
|
||||
$parts = [
|
||||
'year' => null,
|
||||
'month' => null,
|
||||
'date' => null,
|
||||
'hour' => null,
|
||||
'minute' => null,
|
||||
'second' => null,
|
||||
'timezone' => null,
|
||||
];
|
||||
|
||||
// The $valueDateTime expression has a bug with (?J) so we simulate it.
|
||||
$parts['date0'] = &$parts['date'];
|
||||
$parts['date1'] = &$parts['date'];
|
||||
$parts['date2'] = &$parts['date'];
|
||||
$parts['month0'] = &$parts['month'];
|
||||
$parts['month1'] = &$parts['month'];
|
||||
$parts['year0'] = &$parts['year'];
|
||||
|
||||
foreach ($parts as $part => &$value) {
|
||||
if (!empty($matches[$part])) {
|
||||
$value = $matches[$part];
|
||||
}
|
||||
}
|
||||
|
||||
unset($parts['date0']);
|
||||
unset($parts['date1']);
|
||||
unset($parts['date2']);
|
||||
unset($parts['month0']);
|
||||
unset($parts['month1']);
|
||||
unset($parts['year0']);
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
||||
269
vendor/sabre/vobject/lib/Document.php
vendored
Normal file
269
vendor/sabre/vobject/lib/Document.php
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* Document.
|
||||
*
|
||||
* A document is just like a component, except that it's also the top level
|
||||
* element.
|
||||
*
|
||||
* Both a VCALENDAR and a VCARD are considered documents.
|
||||
*
|
||||
* This class also provides a registry for document types.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class Document extends Component
|
||||
{
|
||||
/**
|
||||
* Unknown document type.
|
||||
*/
|
||||
const UNKNOWN = 1;
|
||||
|
||||
/**
|
||||
* vCalendar 1.0.
|
||||
*/
|
||||
const VCALENDAR10 = 2;
|
||||
|
||||
/**
|
||||
* iCalendar 2.0.
|
||||
*/
|
||||
const ICALENDAR20 = 3;
|
||||
|
||||
/**
|
||||
* vCard 2.1.
|
||||
*/
|
||||
const VCARD21 = 4;
|
||||
|
||||
/**
|
||||
* vCard 3.0.
|
||||
*/
|
||||
const VCARD30 = 5;
|
||||
|
||||
/**
|
||||
* vCard 4.0.
|
||||
*/
|
||||
const VCARD40 = 6;
|
||||
|
||||
/**
|
||||
* The default name for this component.
|
||||
*
|
||||
* This should be 'VCALENDAR' or 'VCARD'.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $defaultName;
|
||||
|
||||
/**
|
||||
* List of properties, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $propertyMap = [];
|
||||
|
||||
/**
|
||||
* List of components, along with which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $componentMap = [];
|
||||
|
||||
/**
|
||||
* List of value-types, and which classes they map to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $valueMap = [];
|
||||
|
||||
/**
|
||||
* Creates a new document.
|
||||
*
|
||||
* We're changing the default behavior slightly here. First, we don't want
|
||||
* to have to specify a name (we already know it), and we want to allow
|
||||
* children to be specified in the first argument.
|
||||
*
|
||||
* But, the default behavior also works.
|
||||
*
|
||||
* So the two sigs:
|
||||
*
|
||||
* new Document(array $children = [], $defaults = true);
|
||||
* new Document(string $name, array $children = [], $defaults = true)
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$args = func_get_args();
|
||||
$name = static::$defaultName;
|
||||
if (0 === count($args) || is_array($args[0])) {
|
||||
$children = isset($args[0]) ? $args[0] : [];
|
||||
$defaults = isset($args[1]) ? $args[1] : true;
|
||||
} else {
|
||||
$name = $args[0];
|
||||
$children = isset($args[1]) ? $args[1] : [];
|
||||
$defaults = isset($args[2]) ? $args[2] : true;
|
||||
}
|
||||
parent::__construct($this, $name, $children, $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current document type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getDocumentType()
|
||||
{
|
||||
return self::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new component or property.
|
||||
*
|
||||
* If it's a known component, we will automatically call createComponent.
|
||||
* otherwise, we'll assume it's a property and call createProperty instead.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $arg1,... Unlimited number of args
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function create($name)
|
||||
{
|
||||
if (isset(static::$componentMap[strtoupper($name)])) {
|
||||
return call_user_func_array([$this, 'createComponent'], func_get_args());
|
||||
} else {
|
||||
return call_user_func_array([$this, 'createProperty'], func_get_args());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new component.
|
||||
*
|
||||
* This method automatically searches for the correct component class, based
|
||||
* on its name.
|
||||
*
|
||||
* You can specify the children either in key=>value syntax, in which case
|
||||
* properties will automatically be created, or you can just pass a list of
|
||||
* Component and Property object.
|
||||
*
|
||||
* By default, a set of sensible values will be added to the component. For
|
||||
* an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
|
||||
* ensure that this does not happen, set $defaults to false.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $children
|
||||
* @param bool $defaults
|
||||
*
|
||||
* @return Component
|
||||
*/
|
||||
public function createComponent($name, ?array $children = null, $defaults = true)
|
||||
{
|
||||
$name = strtoupper($name);
|
||||
$class = Component::class;
|
||||
|
||||
if (isset(static::$componentMap[$name])) {
|
||||
$class = static::$componentMap[$name];
|
||||
}
|
||||
if (is_null($children)) {
|
||||
$children = [];
|
||||
}
|
||||
|
||||
return new $class($this, $name, $children, $defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method for creating new properties.
|
||||
*
|
||||
* This method automatically searches for the correct property class, based
|
||||
* on its name.
|
||||
*
|
||||
* You can specify the parameters either in key=>value syntax, in which case
|
||||
* parameters will automatically be created, or you can just pass a list of
|
||||
* Parameter objects.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @param array $parameters
|
||||
* @param string $valueType Force a specific valuetype, such as URI or TEXT
|
||||
*/
|
||||
public function createProperty($name, $value = null, ?array $parameters = null, $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property
|
||||
{
|
||||
// If there's a . in the name, it means it's prefixed by a groupname.
|
||||
if (false !== ($i = strpos($name, '.'))) {
|
||||
$group = substr($name, 0, $i);
|
||||
$name = strtoupper(substr($name, $i + 1));
|
||||
} else {
|
||||
$name = strtoupper($name);
|
||||
$group = null;
|
||||
}
|
||||
|
||||
$class = null;
|
||||
|
||||
// If a VALUE parameter is supplied, we have to use that
|
||||
// According to https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
|
||||
// If the property's value is the default value type, then this
|
||||
// parameter need not be specified. However, if the property's
|
||||
// default value type is overridden by some other allowable value
|
||||
// type, then this parameter MUST be specified.
|
||||
if (!$valueType) {
|
||||
$valueType = $parameters['VALUE'] ?? null;
|
||||
}
|
||||
|
||||
if ($valueType) {
|
||||
// The valueType argument comes first to figure out the correct
|
||||
// class.
|
||||
$class = $this->getClassNameForPropertyValue($valueType);
|
||||
}
|
||||
|
||||
// If the value parameter is not set or set to something we do not recognize
|
||||
// we do not attempt to interpret or parse the datass value as specified in
|
||||
// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.20
|
||||
// So when we so far did not get a class-name, we use the default for the property
|
||||
if (is_null($class)) {
|
||||
$class = $this->getClassNameForPropertyName($name);
|
||||
}
|
||||
|
||||
if (is_null($parameters)) {
|
||||
$parameters = [];
|
||||
}
|
||||
|
||||
return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a full class-name for a value parameter.
|
||||
*
|
||||
* For instance, DTSTART may have VALUE=DATE. In that case we will look in
|
||||
* our valueMap table and return the appropriate class name.
|
||||
*
|
||||
* This method returns null if we don't have a specialized class.
|
||||
*
|
||||
* @param string $valueParam
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getClassNameForPropertyValue($valueParam)
|
||||
{
|
||||
$valueParam = strtoupper($valueParam);
|
||||
if (isset(static::$valueMap[$valueParam])) {
|
||||
return static::$valueMap[$valueParam];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default class for a property name.
|
||||
*
|
||||
* @param string $propertyName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getClassNameForPropertyName($propertyName)
|
||||
{
|
||||
if (isset(static::$propertyMap[$propertyName])) {
|
||||
return static::$propertyMap[$propertyName];
|
||||
} else {
|
||||
return Property\Unknown::class;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
vendor/sabre/vobject/lib/ElementList.php
vendored
Normal file
52
vendor/sabre/vobject/lib/ElementList.php
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use ArrayIterator;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* VObject ElementList.
|
||||
*
|
||||
* This class represents a list of elements. Lists are the result of queries,
|
||||
* such as doing $vcalendar->vevent where there's multiple VEVENT objects.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ElementList extends ArrayIterator
|
||||
{
|
||||
/* {{{ ArrayAccess Interface */
|
||||
|
||||
/**
|
||||
* Sets an item through ArrayAccess.
|
||||
*
|
||||
* @param int $offset
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
throw new LogicException('You can not add new objects to an ElementList');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an item through ArrayAccess.
|
||||
*
|
||||
* This method just forwards the request to the inner iterator
|
||||
*
|
||||
* @param int $offset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
throw new LogicException('You can not remove objects from an ElementList');
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
}
|
||||
15
vendor/sabre/vobject/lib/EofException.php
vendored
Normal file
15
vendor/sabre/vobject/lib/EofException.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* Exception thrown by parser when the end of the stream has been reached,
|
||||
* before this was expected.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class EofException extends ParseException
|
||||
{
|
||||
}
|
||||
185
vendor/sabre/vobject/lib/FreeBusyData.php
vendored
Normal file
185
vendor/sabre/vobject/lib/FreeBusyData.php
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* FreeBusyData is a helper class that manages freebusy information.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class FreeBusyData
|
||||
{
|
||||
/**
|
||||
* Start timestamp.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $start;
|
||||
|
||||
/**
|
||||
* End timestamp.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $end;
|
||||
|
||||
/**
|
||||
* A list of free-busy times.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
public function __construct($start, $end)
|
||||
{
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->data = [];
|
||||
|
||||
$this->data[] = [
|
||||
'start' => $this->start,
|
||||
'end' => $this->end,
|
||||
'type' => 'FREE',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds free or busytime to the data.
|
||||
*
|
||||
* @param int $start
|
||||
* @param int $end
|
||||
* @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
|
||||
*/
|
||||
public function add($start, $end, $type)
|
||||
{
|
||||
if ($start > $this->end || $end < $this->start) {
|
||||
// This new data is outside our timerange.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($start < $this->start) {
|
||||
// The item starts before our requested time range
|
||||
$start = $this->start;
|
||||
}
|
||||
if ($end > $this->end) {
|
||||
// The item ends after our requested time range
|
||||
$end = $this->end;
|
||||
}
|
||||
|
||||
// Finding out where we need to insert the new item.
|
||||
$currentIndex = 0;
|
||||
while ($start > $this->data[$currentIndex]['end']) {
|
||||
++$currentIndex;
|
||||
}
|
||||
|
||||
// The standard insertion point will be one _after_ the first
|
||||
// overlapping item.
|
||||
$insertStartIndex = $currentIndex + 1;
|
||||
|
||||
$newItem = [
|
||||
'start' => $start,
|
||||
'end' => $end,
|
||||
'type' => $type,
|
||||
];
|
||||
|
||||
$precedingItem = $this->data[$insertStartIndex - 1];
|
||||
if ($this->data[$insertStartIndex - 1]['start'] === $start) {
|
||||
// The old item starts at the exact same point as the new item.
|
||||
--$insertStartIndex;
|
||||
}
|
||||
|
||||
// Now we know where to insert the item, we need to know where it
|
||||
// starts overlapping with items on the tail end. We need to start
|
||||
// looking one item before the insertStartIndex, because it's possible
|
||||
// that the new item 'sits inside' the previous old item.
|
||||
if ($insertStartIndex > 0) {
|
||||
$currentIndex = $insertStartIndex - 1;
|
||||
} else {
|
||||
$currentIndex = 0;
|
||||
}
|
||||
|
||||
while ($end > $this->data[$currentIndex]['end']) {
|
||||
++$currentIndex;
|
||||
}
|
||||
|
||||
// What we are about to insert into the array
|
||||
$newItems = [
|
||||
$newItem,
|
||||
];
|
||||
|
||||
// This is the amount of items that are completely overwritten by the
|
||||
// new item.
|
||||
$itemsToDelete = $currentIndex - $insertStartIndex;
|
||||
if ($this->data[$currentIndex]['end'] <= $end) {
|
||||
++$itemsToDelete;
|
||||
}
|
||||
|
||||
// If itemsToDelete was -1, it means that the newly inserted item is
|
||||
// actually sitting inside an existing one. This means we need to split
|
||||
// the item at the current position in two and insert the new item in
|
||||
// between.
|
||||
if (-1 === $itemsToDelete) {
|
||||
$itemsToDelete = 0;
|
||||
if ($newItem['end'] < $precedingItem['end']) {
|
||||
$newItems[] = [
|
||||
'start' => $newItem['end'] + 1,
|
||||
'end' => $precedingItem['end'],
|
||||
'type' => $precedingItem['type'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
array_splice(
|
||||
$this->data,
|
||||
$insertStartIndex,
|
||||
$itemsToDelete,
|
||||
$newItems
|
||||
);
|
||||
|
||||
$doMerge = false;
|
||||
$mergeOffset = $insertStartIndex;
|
||||
$mergeItem = $newItem;
|
||||
$mergeDelete = 1;
|
||||
|
||||
if (isset($this->data[$insertStartIndex - 1])) {
|
||||
// Updating the start time of the previous item.
|
||||
$this->data[$insertStartIndex - 1]['end'] = $start;
|
||||
|
||||
// If the previous and the current are of the same type, we can
|
||||
// merge them into one item.
|
||||
if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) {
|
||||
$doMerge = true;
|
||||
--$mergeOffset;
|
||||
++$mergeDelete;
|
||||
$mergeItem['start'] = $this->data[$insertStartIndex - 1]['start'];
|
||||
}
|
||||
}
|
||||
if (isset($this->data[$insertStartIndex + 1])) {
|
||||
// Updating the start time of the next item.
|
||||
$this->data[$insertStartIndex + 1]['start'] = $end;
|
||||
|
||||
// If the next and the current are of the same type, we can
|
||||
// merge them into one item.
|
||||
if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) {
|
||||
$doMerge = true;
|
||||
++$mergeDelete;
|
||||
$mergeItem['end'] = $this->data[$insertStartIndex + 1]['end'];
|
||||
}
|
||||
}
|
||||
if ($doMerge) {
|
||||
array_splice(
|
||||
$this->data,
|
||||
$mergeOffset,
|
||||
$mergeDelete,
|
||||
[$mergeItem]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
549
vendor/sabre/vobject/lib/FreeBusyGenerator.php
vendored
Normal file
549
vendor/sabre/vobject/lib/FreeBusyGenerator.php
vendored
Normal file
@@ -0,0 +1,549 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Recur\EventIterator;
|
||||
use Sabre\VObject\Recur\NoInstancesException;
|
||||
|
||||
/**
|
||||
* This class helps with generating FREEBUSY reports based on existing sets of
|
||||
* objects.
|
||||
*
|
||||
* It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
|
||||
* generates a single VFREEBUSY object.
|
||||
*
|
||||
* VFREEBUSY components are described in RFC5545, The rules for what should
|
||||
* go in a single freebusy report is taken from RFC4791, section 7.10.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class FreeBusyGenerator
|
||||
{
|
||||
/**
|
||||
* Input objects.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $objects = [];
|
||||
|
||||
/**
|
||||
* Start of range.
|
||||
*
|
||||
* @var DateTimeInterface|null
|
||||
*/
|
||||
protected $start;
|
||||
|
||||
/**
|
||||
* End of range.
|
||||
*
|
||||
* @var DateTimeInterface|null
|
||||
*/
|
||||
protected $end;
|
||||
|
||||
/**
|
||||
* VCALENDAR object.
|
||||
*
|
||||
* @var Document
|
||||
*/
|
||||
protected $baseObject;
|
||||
|
||||
/**
|
||||
* Reference timezone.
|
||||
*
|
||||
* When we are calculating busy times, and we come across so-called
|
||||
* floating times (times without a timezone), we use the reference timezone
|
||||
* instead.
|
||||
*
|
||||
* This is also used for all-day events.
|
||||
*
|
||||
* This defaults to UTC.
|
||||
*
|
||||
* @var DateTimeZone
|
||||
*/
|
||||
protected $timeZone;
|
||||
|
||||
/**
|
||||
* A VAVAILABILITY document.
|
||||
*
|
||||
* If this is set, its information will be included when calculating
|
||||
* freebusy time.
|
||||
*
|
||||
* @var Document
|
||||
*/
|
||||
protected $vavailability;
|
||||
|
||||
/**
|
||||
* Creates the generator.
|
||||
*
|
||||
* Check the setTimeRange and setObjects methods for details about the
|
||||
* arguments.
|
||||
*
|
||||
* @param DateTimeInterface $start
|
||||
* @param DateTimeInterface $end
|
||||
* @param mixed $objects
|
||||
* @param DateTimeZone $timeZone
|
||||
*/
|
||||
public function __construct(?DateTimeInterface $start = null, ?DateTimeInterface $end = null, $objects = null, ?DateTimeZone $timeZone = null)
|
||||
{
|
||||
$this->setTimeRange($start, $end);
|
||||
|
||||
if ($objects) {
|
||||
$this->setObjects($objects);
|
||||
}
|
||||
if (is_null($timeZone)) {
|
||||
$timeZone = new DateTimeZone('UTC');
|
||||
}
|
||||
$this->setTimeZone($timeZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the VCALENDAR object.
|
||||
*
|
||||
* If this is set, it will not be generated for you. You are responsible
|
||||
* for setting things like the METHOD, CALSCALE, VERSION, etc..
|
||||
*
|
||||
* The VFREEBUSY object will be automatically added though.
|
||||
*/
|
||||
public function setBaseObject(Document $vcalendar)
|
||||
{
|
||||
$this->baseObject = $vcalendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a VAVAILABILITY document.
|
||||
*/
|
||||
public function setVAvailability(Document $vcalendar)
|
||||
{
|
||||
$this->vavailability = $vcalendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input objects.
|
||||
*
|
||||
* You must either specify a vcalendar object as a string, or as the parse
|
||||
* Component.
|
||||
* It's also possible to specify multiple objects as an array.
|
||||
*
|
||||
* @param mixed $objects
|
||||
*/
|
||||
public function setObjects($objects)
|
||||
{
|
||||
if (!is_array($objects)) {
|
||||
$objects = [$objects];
|
||||
}
|
||||
|
||||
$this->objects = [];
|
||||
foreach ($objects as $object) {
|
||||
if (is_string($object) || is_resource($object)) {
|
||||
$this->objects[] = Reader::read($object);
|
||||
} elseif ($object instanceof Component) {
|
||||
$this->objects[] = $object;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time range.
|
||||
*
|
||||
* Any freebusy object falling outside of this time range will be ignored.
|
||||
*
|
||||
* @param DateTimeInterface $start
|
||||
* @param DateTimeInterface $end
|
||||
*/
|
||||
public function setTimeRange(?DateTimeInterface $start = null, ?DateTimeInterface $end = null)
|
||||
{
|
||||
if (!$start) {
|
||||
$start = new DateTimeImmutable(Settings::$minDate);
|
||||
}
|
||||
if (!$end) {
|
||||
$end = new DateTimeImmutable(Settings::$maxDate);
|
||||
}
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the reference timezone for floating times.
|
||||
*/
|
||||
public function setTimeZone(DateTimeZone $timeZone)
|
||||
{
|
||||
$this->timeZone = $timeZone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the input data and returns a correct VFREEBUSY object, wrapped in
|
||||
* a VCALENDAR.
|
||||
*
|
||||
* @return Component
|
||||
*/
|
||||
public function getResult()
|
||||
{
|
||||
$fbData = new FreeBusyData(
|
||||
$this->start->getTimeStamp(),
|
||||
$this->end->getTimeStamp()
|
||||
);
|
||||
if ($this->vavailability) {
|
||||
$this->calculateAvailability($fbData, $this->vavailability);
|
||||
}
|
||||
|
||||
$this->calculateBusy($fbData, $this->objects);
|
||||
|
||||
return $this->generateFreeBusyCalendar($fbData);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes a VAVAILABILITY component and figures out all the
|
||||
* available times.
|
||||
*/
|
||||
protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability)
|
||||
{
|
||||
$vavailComps = iterator_to_array($vavailability->VAVAILABILITY);
|
||||
usort(
|
||||
$vavailComps,
|
||||
function ($a, $b) {
|
||||
// We need to order the components by priority. Priority 1
|
||||
// comes first, up until priority 9. Priority 0 comes after
|
||||
// priority 9. No priority implies priority 0.
|
||||
//
|
||||
// Yes, I'm serious.
|
||||
$priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0;
|
||||
$priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0;
|
||||
|
||||
if (0 === $priorityA) {
|
||||
$priorityA = 10;
|
||||
}
|
||||
if (0 === $priorityB) {
|
||||
$priorityB = 10;
|
||||
}
|
||||
|
||||
return $priorityA - $priorityB;
|
||||
}
|
||||
);
|
||||
|
||||
// Now we go over all the VAVAILABILITY components and figure if
|
||||
// there's any we don't need to consider.
|
||||
//
|
||||
// This is can be because of one of two reasons: either the
|
||||
// VAVAILABILITY component falls outside the time we are interested in,
|
||||
// or a different VAVAILABILITY component with a higher priority has
|
||||
// already completely covered the time-range.
|
||||
$old = $vavailComps;
|
||||
$new = [];
|
||||
|
||||
foreach ($old as $vavail) {
|
||||
list($compStart, $compEnd) = $vavail->getEffectiveStartEnd();
|
||||
|
||||
// We don't care about datetimes that are earlier or later than the
|
||||
// start and end of the freebusy report, so this gets normalized
|
||||
// first.
|
||||
if (is_null($compStart) || $compStart < $this->start) {
|
||||
$compStart = $this->start;
|
||||
}
|
||||
if (is_null($compEnd) || $compEnd > $this->end) {
|
||||
$compEnd = $this->end;
|
||||
}
|
||||
|
||||
// If the item fell out of the timerange, we can just skip it.
|
||||
if ($compStart > $this->end || $compEnd < $this->start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Going through our existing list of components to see if there's
|
||||
// a higher priority component that already fully covers this one.
|
||||
foreach ($new as $higherVavail) {
|
||||
list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd();
|
||||
if (
|
||||
(is_null($higherStart) || $higherStart < $compStart) &&
|
||||
(is_null($higherEnd) || $higherEnd > $compEnd)
|
||||
) {
|
||||
// Component is fully covered by a higher priority
|
||||
// component. We can skip this component.
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
// We're keeping it!
|
||||
$new[] = $vavail;
|
||||
}
|
||||
|
||||
// Lastly, we need to traverse the remaining components and fill in the
|
||||
// freebusydata slots.
|
||||
//
|
||||
// We traverse the components in reverse, because we want the higher
|
||||
// priority components to override the lower ones.
|
||||
foreach (array_reverse($new) as $vavail) {
|
||||
$busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE';
|
||||
list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd();
|
||||
|
||||
// Making the component size no larger than the requested free-busy
|
||||
// report range.
|
||||
if (!$vavailStart || $vavailStart < $this->start) {
|
||||
$vavailStart = $this->start;
|
||||
}
|
||||
if (!$vavailEnd || $vavailEnd > $this->end) {
|
||||
$vavailEnd = $this->end;
|
||||
}
|
||||
|
||||
// Marking the entire time range of the VAVAILABILITY component as
|
||||
// busy.
|
||||
$fbData->add(
|
||||
$vavailStart->getTimeStamp(),
|
||||
$vavailEnd->getTimeStamp(),
|
||||
$busyType
|
||||
);
|
||||
|
||||
// Looping over the AVAILABLE components.
|
||||
if (isset($vavail->AVAILABLE)) {
|
||||
foreach ($vavail->AVAILABLE as $available) {
|
||||
list($availStart, $availEnd) = $available->getEffectiveStartEnd();
|
||||
$fbData->add(
|
||||
$availStart->getTimeStamp(),
|
||||
$availEnd->getTimeStamp(),
|
||||
'FREE'
|
||||
);
|
||||
|
||||
if ($available->RRULE) {
|
||||
// Our favourite thing: recurrence!!
|
||||
|
||||
$rruleIterator = new Recur\RRuleIterator(
|
||||
$available->RRULE->getValue(),
|
||||
$availStart
|
||||
);
|
||||
$rruleIterator->fastForward($vavailStart);
|
||||
|
||||
$startEndDiff = $availStart->diff($availEnd);
|
||||
|
||||
while ($rruleIterator->valid()) {
|
||||
$recurStart = $rruleIterator->current();
|
||||
$recurEnd = $recurStart->add($startEndDiff);
|
||||
|
||||
if ($recurStart > $vavailEnd) {
|
||||
// We're beyond the legal timerange.
|
||||
break;
|
||||
}
|
||||
|
||||
if ($recurEnd > $vavailEnd) {
|
||||
// Truncating the end if it exceeds the
|
||||
// VAVAILABILITY end.
|
||||
$recurEnd = $vavailEnd;
|
||||
}
|
||||
|
||||
$fbData->add(
|
||||
$recurStart->getTimeStamp(),
|
||||
$recurEnd->getTimeStamp(),
|
||||
'FREE'
|
||||
);
|
||||
|
||||
$rruleIterator->next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes an array of iCalendar objects and applies its busy
|
||||
* times on fbData.
|
||||
*
|
||||
* @param VCalendar[] $objects
|
||||
*/
|
||||
protected function calculateBusy(FreeBusyData $fbData, array $objects)
|
||||
{
|
||||
foreach ($objects as $key => $object) {
|
||||
foreach ($object->getBaseComponents() as $component) {
|
||||
switch ($component->name) {
|
||||
case 'VEVENT':
|
||||
$FBTYPE = 'BUSY';
|
||||
if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) {
|
||||
break;
|
||||
}
|
||||
if (isset($component->STATUS)) {
|
||||
$status = strtoupper($component->STATUS);
|
||||
if ('CANCELLED' === $status) {
|
||||
break;
|
||||
}
|
||||
if ('TENTATIVE' === $status) {
|
||||
$FBTYPE = 'BUSY-TENTATIVE';
|
||||
}
|
||||
}
|
||||
|
||||
$times = [];
|
||||
|
||||
if ($component->RRULE) {
|
||||
try {
|
||||
$iterator = new EventIterator($object, (string) $component->UID, $this->timeZone);
|
||||
} catch (NoInstancesException $e) {
|
||||
// This event is recurring, but it doesn't have a single
|
||||
// instance. We are skipping this event from the output
|
||||
// entirely.
|
||||
unset($this->objects[$key]);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->start) {
|
||||
$iterator->fastForward($this->start);
|
||||
}
|
||||
|
||||
$maxRecurrences = Settings::$maxRecurrences;
|
||||
|
||||
while ($iterator->valid() && --$maxRecurrences) {
|
||||
$startTime = $iterator->getDTStart();
|
||||
if ($this->end && $startTime > $this->end) {
|
||||
break;
|
||||
}
|
||||
$times[] = [
|
||||
$iterator->getDTStart(),
|
||||
$iterator->getDTEnd(),
|
||||
];
|
||||
|
||||
$iterator->next();
|
||||
}
|
||||
} else {
|
||||
$startTime = $component->DTSTART->getDateTime($this->timeZone);
|
||||
if ($this->end && $startTime > $this->end) {
|
||||
break;
|
||||
}
|
||||
$endTime = null;
|
||||
if (isset($component->DTEND)) {
|
||||
$endTime = $component->DTEND->getDateTime($this->timeZone);
|
||||
} elseif (isset($component->DURATION)) {
|
||||
$duration = DateTimeParser::parseDuration((string) $component->DURATION);
|
||||
$endTime = clone $startTime;
|
||||
$endTime = $endTime->add($duration);
|
||||
} elseif (!$component->DTSTART->hasTime()) {
|
||||
$endTime = clone $startTime;
|
||||
$endTime = $endTime->modify('+1 day');
|
||||
} else {
|
||||
// The event had no duration (0 seconds)
|
||||
break;
|
||||
}
|
||||
|
||||
$times[] = [$startTime, $endTime];
|
||||
}
|
||||
|
||||
foreach ($times as $time) {
|
||||
if ($this->end && $time[0] > $this->end) {
|
||||
break;
|
||||
}
|
||||
if ($this->start && $time[1] < $this->start) {
|
||||
break;
|
||||
}
|
||||
|
||||
$fbData->add(
|
||||
$time[0]->getTimeStamp(),
|
||||
$time[1]->getTimeStamp(),
|
||||
$FBTYPE
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'VFREEBUSY':
|
||||
foreach ($component->FREEBUSY as $freebusy) {
|
||||
$fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY';
|
||||
|
||||
// Skipping intervals marked as 'free'
|
||||
if ('FREE' === $fbType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values = explode(',', $freebusy);
|
||||
foreach ($values as $value) {
|
||||
list($startTime, $endTime) = explode('/', $value);
|
||||
$startTime = DateTimeParser::parseDateTime($startTime);
|
||||
|
||||
if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) {
|
||||
$duration = DateTimeParser::parseDuration($endTime);
|
||||
$endTime = clone $startTime;
|
||||
$endTime = $endTime->add($duration);
|
||||
} else {
|
||||
$endTime = DateTimeParser::parseDateTime($endTime);
|
||||
}
|
||||
|
||||
if ($this->start && $this->start > $endTime) {
|
||||
continue;
|
||||
}
|
||||
if ($this->end && $this->end < $startTime) {
|
||||
continue;
|
||||
}
|
||||
$fbData->add(
|
||||
$startTime->getTimeStamp(),
|
||||
$endTime->getTimeStamp(),
|
||||
$fbType
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method takes a FreeBusyData object and generates the VCALENDAR
|
||||
* object associated with it.
|
||||
*
|
||||
* @return VCalendar
|
||||
*/
|
||||
protected function generateFreeBusyCalendar(FreeBusyData $fbData)
|
||||
{
|
||||
if ($this->baseObject) {
|
||||
$calendar = $this->baseObject;
|
||||
} else {
|
||||
$calendar = new VCalendar();
|
||||
}
|
||||
|
||||
$vfreebusy = $calendar->createComponent('VFREEBUSY');
|
||||
$calendar->add($vfreebusy);
|
||||
|
||||
if ($this->start) {
|
||||
$dtstart = $calendar->createProperty('DTSTART');
|
||||
$dtstart->setDateTime($this->start);
|
||||
$vfreebusy->add($dtstart);
|
||||
}
|
||||
if ($this->end) {
|
||||
$dtend = $calendar->createProperty('DTEND');
|
||||
$dtend->setDateTime($this->end);
|
||||
$vfreebusy->add($dtend);
|
||||
}
|
||||
|
||||
$tz = new \DateTimeZone('UTC');
|
||||
$dtstamp = $calendar->createProperty('DTSTAMP');
|
||||
$dtstamp->setDateTime(new DateTimeImmutable('now', $tz));
|
||||
$vfreebusy->add($dtstamp);
|
||||
|
||||
foreach ($fbData->getData() as $busyTime) {
|
||||
$busyType = strtoupper($busyTime['type']);
|
||||
|
||||
// Ignoring all the FREE parts, because those are already assumed.
|
||||
if ('FREE' === $busyType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz);
|
||||
$busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz);
|
||||
|
||||
$prop = $calendar->createProperty(
|
||||
'FREEBUSY',
|
||||
$busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z')
|
||||
);
|
||||
|
||||
// Only setting FBTYPE if it's not BUSY, because BUSY is the
|
||||
// default anyway.
|
||||
if ('BUSY' !== $busyType) {
|
||||
$prop['FBTYPE'] = $busyType;
|
||||
}
|
||||
$vfreebusy->add($prop);
|
||||
}
|
||||
|
||||
return $calendar;
|
||||
}
|
||||
}
|
||||
1003
vendor/sabre/vobject/lib/ITip/Broker.php
vendored
Normal file
1003
vendor/sabre/vobject/lib/ITip/Broker.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
16
vendor/sabre/vobject/lib/ITip/ITipException.php
vendored
Normal file
16
vendor/sabre/vobject/lib/ITip/ITipException.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\ITip;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* This message is emitted in case of serious problems with iTip messages.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ITipException extends Exception
|
||||
{
|
||||
}
|
||||
136
vendor/sabre/vobject/lib/ITip/Message.php
vendored
Normal file
136
vendor/sabre/vobject/lib/ITip/Message.php
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\ITip;
|
||||
|
||||
/**
|
||||
* This class represents an iTip message.
|
||||
*
|
||||
* A message holds all the information relevant to the message, including the
|
||||
* object itself.
|
||||
*
|
||||
* It should for the most part be treated as immutable.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Message
|
||||
{
|
||||
/**
|
||||
* The object's UID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $uid;
|
||||
|
||||
/**
|
||||
* The component type, such as VEVENT.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $component;
|
||||
|
||||
/**
|
||||
* Contains the ITip method, which is something like REQUEST, REPLY or
|
||||
* CANCEL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $method;
|
||||
|
||||
/**
|
||||
* The current sequence number for the event.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $sequence;
|
||||
|
||||
/**
|
||||
* The senders' email address.
|
||||
*
|
||||
* Note that this does not imply that this has to be used in a From: field
|
||||
* if the message is sent by email. It may also be populated in Reply-To:
|
||||
* or not at all.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $sender;
|
||||
|
||||
/**
|
||||
* The name of the sender. This is often populated from a CN parameter from
|
||||
* either the ORGANIZER or ATTENDEE, depending on the message.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $senderName;
|
||||
|
||||
/**
|
||||
* The recipient's email address.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $recipient;
|
||||
|
||||
/**
|
||||
* The name of the recipient. This is usually populated with the CN
|
||||
* parameter from the ATTENDEE or ORGANIZER property, if it's available.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $recipientName;
|
||||
|
||||
/**
|
||||
* After the message has been delivered, this should contain a string such
|
||||
* as : 1.1;Sent or 1.2;Delivered.
|
||||
*
|
||||
* In case of a failure, this will hold the error status code.
|
||||
*
|
||||
* See:
|
||||
* http://tools.ietf.org/html/rfc6638#section-7.3
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $scheduleStatus;
|
||||
|
||||
/**
|
||||
* The iCalendar / iTip body.
|
||||
*
|
||||
* @var \Sabre\VObject\Component\VCalendar
|
||||
*/
|
||||
public $message;
|
||||
|
||||
/**
|
||||
* This will be set to true, if the iTip broker considers the change
|
||||
* 'significant'.
|
||||
*
|
||||
* In practice, this means that we'll only mark it true, if for instance
|
||||
* DTSTART changed. This allows systems to only send iTip messages when
|
||||
* significant changes happened. This is especially useful for iMip, as
|
||||
* normally a ton of messages may be generated for normal calendar use.
|
||||
*
|
||||
* To see the list of properties that are considered 'significant', check
|
||||
* out Sabre\VObject\ITip\Broker::$significantChangeProperties.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $significantChange = true;
|
||||
|
||||
/**
|
||||
* Returns the schedule status as a string.
|
||||
*
|
||||
* For example:
|
||||
* 1.2
|
||||
*
|
||||
* @return mixed bool|string
|
||||
*/
|
||||
public function getScheduleStatus()
|
||||
{
|
||||
if (!$this->scheduleStatus) {
|
||||
return false;
|
||||
} else {
|
||||
list($scheduleStatus) = explode(';', $this->scheduleStatus);
|
||||
|
||||
return $scheduleStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php
vendored
Normal file
18
vendor/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\ITip;
|
||||
|
||||
/**
|
||||
* SameOrganizerForAllComponentsException.
|
||||
*
|
||||
* This exception is emitted when an event is encountered with more than one
|
||||
* component (e.g.: exceptions), but the organizer is not identical in every
|
||||
* component.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class SameOrganizerForAllComponentsException extends ITipException
|
||||
{
|
||||
}
|
||||
15
vendor/sabre/vobject/lib/InvalidDataException.php
vendored
Normal file
15
vendor/sabre/vobject/lib/InvalidDataException.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* This exception is thrown whenever an invalid value is found anywhere in a
|
||||
* iCalendar or vCard object.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class InvalidDataException extends \Exception
|
||||
{
|
||||
}
|
||||
256
vendor/sabre/vobject/lib/Node.php
vendored
Normal file
256
vendor/sabre/vobject/lib/Node.php
vendored
Normal file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
* A node is the root class for every element in an iCalendar of vCard object.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable, \JsonSerializable, Xml\XmlSerializable
|
||||
{
|
||||
/**
|
||||
* The following constants are used by the validate() method.
|
||||
*
|
||||
* If REPAIR is set, the validator will attempt to repair any broken data
|
||||
* (if possible).
|
||||
*/
|
||||
const REPAIR = 1;
|
||||
|
||||
/**
|
||||
* If this option is set, the validator will operate on the vcards on the
|
||||
* assumption that the vcards need to be valid for CardDAV.
|
||||
*
|
||||
* This means for example that the UID is required, whereas it is not for
|
||||
* regular vcards.
|
||||
*/
|
||||
const PROFILE_CARDDAV = 2;
|
||||
|
||||
/**
|
||||
* If this option is set, the validator will operate on iCalendar objects
|
||||
* on the assumption that the vcards need to be valid for CalDAV.
|
||||
*
|
||||
* This means for example that calendars can only contain objects with
|
||||
* identical component types and UIDs.
|
||||
*/
|
||||
const PROFILE_CALDAV = 4;
|
||||
|
||||
/**
|
||||
* Reference to the parent object, if this is not the top object.
|
||||
*
|
||||
* @var Node
|
||||
*/
|
||||
public $parent;
|
||||
|
||||
/**
|
||||
* Iterator override.
|
||||
*
|
||||
* @var ElementList
|
||||
*/
|
||||
protected $iterator = null;
|
||||
|
||||
/**
|
||||
* The root document.
|
||||
*
|
||||
* @var Component
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* Serializes the node into a mimedir format.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function serialize();
|
||||
|
||||
/**
|
||||
* This method returns an array, with the representation as it should be
|
||||
* encoded in JSON. This is used to create jCard or jCal documents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
abstract public function jsonSerialize();
|
||||
|
||||
/**
|
||||
* This method serializes the data into XML. This is used to create xCard or
|
||||
* xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
abstract public function xmlSerialize(Xml\Writer $writer): void;
|
||||
|
||||
/**
|
||||
* Call this method on a document if you're done using it.
|
||||
*
|
||||
* It's intended to remove all circular references, so PHP can easily clean
|
||||
* it up.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
$this->parent = null;
|
||||
$this->root = null;
|
||||
}
|
||||
|
||||
/* {{{ IteratorAggregator interface */
|
||||
|
||||
/**
|
||||
* Returns the iterator for this object.
|
||||
*
|
||||
* @return ElementList
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
if (!is_null($this->iterator)) {
|
||||
return $this->iterator;
|
||||
}
|
||||
|
||||
return new ElementList([$this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the overridden iterator.
|
||||
*
|
||||
* Note that this is not actually part of the iterator interface
|
||||
*/
|
||||
public function setIterator(ElementList $iterator)
|
||||
{
|
||||
$this->iterator = $iterator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* Node::REPAIR - May attempt to automatically repair the problem.
|
||||
*
|
||||
* This method returns an array with detected problems.
|
||||
* Every element has the following properties:
|
||||
*
|
||||
* * level - problem level.
|
||||
* * message - A human-readable string describing the issue.
|
||||
* * node - A reference to the problematic node.
|
||||
*
|
||||
* The level means:
|
||||
* 1 - The issue was repaired (only happens if REPAIR was turned on)
|
||||
* 2 - An inconsequential issue
|
||||
* 3 - A severe issue.
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* {{{ Countable interface */
|
||||
|
||||
/**
|
||||
* Returns the number of elements.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function count()
|
||||
{
|
||||
$it = $this->getIterator();
|
||||
|
||||
return $it->count();
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/* {{{ ArrayAccess Interface */
|
||||
|
||||
/**
|
||||
* Checks if an item exists through ArrayAccess.
|
||||
*
|
||||
* This method just forwards the request to the inner iterator
|
||||
*
|
||||
* @param int $offset
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($offset)
|
||||
{
|
||||
$iterator = $this->getIterator();
|
||||
|
||||
return $iterator->offsetExists($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an item through ArrayAccess.
|
||||
*
|
||||
* This method just forwards the request to the inner iterator
|
||||
*
|
||||
* @param int $offset
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
$iterator = $this->getIterator();
|
||||
|
||||
return $iterator->offsetGet($offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an item through ArrayAccess.
|
||||
*
|
||||
* This method just forwards the request to the inner iterator
|
||||
*
|
||||
* @param int $offset
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($offset, $value)
|
||||
{
|
||||
$iterator = $this->getIterator();
|
||||
$iterator->offsetSet($offset, $value);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
//
|
||||
// This method always throws an exception, so we ignore the closing
|
||||
// brace
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/**
|
||||
* Sets an item through ArrayAccess.
|
||||
*
|
||||
* This method just forwards the request to the inner iterator
|
||||
*
|
||||
* @param int $offset
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($offset)
|
||||
{
|
||||
$iterator = $this->getIterator();
|
||||
$iterator->offsetUnset($offset);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
//
|
||||
// This method always throws an exception, so we ignore the closing
|
||||
// brace
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/* }}} */
|
||||
}
|
||||
75
vendor/sabre/vobject/lib/PHPUnitAssertions.php
vendored
Normal file
75
vendor/sabre/vobject/lib/PHPUnitAssertions.php
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* PHPUnit Assertions.
|
||||
*
|
||||
* This trait can be added to your unittest to make it easier to test iCalendar
|
||||
* and/or vCards.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
trait PHPUnitAssertions
|
||||
{
|
||||
/**
|
||||
* This method tests whether two vcards or icalendar objects are
|
||||
* semantically identical.
|
||||
*
|
||||
* It supports objects being supplied as strings, streams or
|
||||
* Sabre\VObject\Component instances.
|
||||
*
|
||||
* PRODID is removed from both objects as this is often changes and would
|
||||
* just get in the way.
|
||||
*
|
||||
* CALSCALE will automatically get removed if it's set to GREGORIAN.
|
||||
*
|
||||
* Any property that has the value **ANY** will be treated as a wildcard.
|
||||
*
|
||||
* @param resource|string|Component $expected
|
||||
* @param resource|string|Component $actual
|
||||
* @param string $message
|
||||
*/
|
||||
public function assertVObjectEqualsVObject($expected, $actual, $message = '')
|
||||
{
|
||||
$getObj = function ($input) {
|
||||
if (is_resource($input)) {
|
||||
$input = stream_get_contents($input);
|
||||
}
|
||||
if (is_string($input)) {
|
||||
$input = Reader::read($input);
|
||||
}
|
||||
if (!$input instanceof Component) {
|
||||
$this->fail('Input must be a string, stream or VObject component');
|
||||
}
|
||||
unset($input->PRODID);
|
||||
if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) {
|
||||
unset($input->CALSCALE);
|
||||
}
|
||||
|
||||
return $input;
|
||||
};
|
||||
|
||||
$expected = $getObj($expected)->serialize();
|
||||
$actual = $getObj($actual)->serialize();
|
||||
|
||||
// Finding wildcards in expected.
|
||||
preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
$actual = preg_replace(
|
||||
'|^'.preg_quote($match[1], '|').':(.*)\r$|m',
|
||||
$match[1].':**ANY**'."\r",
|
||||
$actual
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
$expected,
|
||||
$actual,
|
||||
$message
|
||||
);
|
||||
}
|
||||
}
|
||||
368
vendor/sabre/vobject/lib/Parameter.php
vendored
Normal file
368
vendor/sabre/vobject/lib/Parameter.php
vendored
Normal file
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use ArrayIterator;
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
* VObject Parameter.
|
||||
*
|
||||
* This class represents a parameter. A parameter is always tied to a property.
|
||||
* In the case of:
|
||||
* DTSTART;VALUE=DATE:20101108
|
||||
* VALUE=DATE would be the parameter name and value.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Parameter extends Node
|
||||
{
|
||||
/**
|
||||
* Parameter name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* vCard 2.1 allows parameters to be encoded without a name.
|
||||
*
|
||||
* We can deduce the parameter name based on its value.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $noName = false;
|
||||
|
||||
/**
|
||||
* Parameter value.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* Sets up the object.
|
||||
*
|
||||
* It's recommended to use the create:: factory method instead.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
*/
|
||||
public function __construct(Document $root, $name, $value = null)
|
||||
{
|
||||
$this->root = $root;
|
||||
if (is_null($name)) {
|
||||
$this->noName = true;
|
||||
$this->name = static::guessParameterNameByValue($value);
|
||||
} else {
|
||||
$this->name = strtoupper($name);
|
||||
}
|
||||
|
||||
// If guessParameterNameByValue() returns an empty string
|
||||
// above, we're actually dealing with a parameter that has no value.
|
||||
// In that case we have to move the value to the name.
|
||||
if ('' === $this->name) {
|
||||
$this->noName = false;
|
||||
$this->name = strtoupper($value);
|
||||
} else {
|
||||
$this->setValue($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to guess property name by value, can be used for vCard 2.1 nameless parameters.
|
||||
*
|
||||
* Figuring out what the name should have been. Note that a ton of
|
||||
* these are rather silly in 2014 and would probably rarely be
|
||||
* used, but we like to be complete.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function guessParameterNameByValue($value)
|
||||
{
|
||||
switch (strtoupper($value)) {
|
||||
// Encodings
|
||||
case '7-BIT':
|
||||
case 'QUOTED-PRINTABLE':
|
||||
case 'BASE64':
|
||||
$name = 'ENCODING';
|
||||
break;
|
||||
|
||||
// Common types
|
||||
case 'WORK':
|
||||
case 'HOME':
|
||||
case 'PREF':
|
||||
// Delivery Label Type
|
||||
case 'DOM':
|
||||
case 'INTL':
|
||||
case 'POSTAL':
|
||||
case 'PARCEL':
|
||||
// Telephone types
|
||||
case 'VOICE':
|
||||
case 'FAX':
|
||||
case 'MSG':
|
||||
case 'CELL':
|
||||
case 'PAGER':
|
||||
case 'BBS':
|
||||
case 'MODEM':
|
||||
case 'CAR':
|
||||
case 'ISDN':
|
||||
case 'VIDEO':
|
||||
// EMAIL types (lol)
|
||||
case 'AOL':
|
||||
case 'APPLELINK':
|
||||
case 'ATTMAIL':
|
||||
case 'CIS':
|
||||
case 'EWORLD':
|
||||
case 'INTERNET':
|
||||
case 'IBMMAIL':
|
||||
case 'MCIMAIL':
|
||||
case 'POWERSHARE':
|
||||
case 'PRODIGY':
|
||||
case 'TLX':
|
||||
case 'X400':
|
||||
// Photo / Logo format types
|
||||
case 'GIF':
|
||||
case 'CGM':
|
||||
case 'WMF':
|
||||
case 'BMP':
|
||||
case 'DIB':
|
||||
case 'PICT':
|
||||
case 'TIFF':
|
||||
case 'PDF':
|
||||
case 'PS':
|
||||
case 'JPEG':
|
||||
case 'MPEG':
|
||||
case 'MPEG2':
|
||||
case 'AVI':
|
||||
case 'QTIME':
|
||||
// Sound Digital Audio Type
|
||||
case 'WAVE':
|
||||
case 'PCM':
|
||||
case 'AIFF':
|
||||
// Key types
|
||||
case 'X509':
|
||||
case 'PGP':
|
||||
$name = 'TYPE';
|
||||
break;
|
||||
|
||||
// Value types
|
||||
case 'INLINE':
|
||||
case 'URL':
|
||||
case 'CONTENT-ID':
|
||||
case 'CID':
|
||||
$name = 'VALUE';
|
||||
break;
|
||||
|
||||
default:
|
||||
$name = '';
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current value.
|
||||
*
|
||||
* This may be either a single, or multiple strings in an array.
|
||||
*
|
||||
* @param string|array $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value.
|
||||
*
|
||||
* This method will always return a string, or null. If there were multiple
|
||||
* values, it will automatically concatenate them (separated by comma).
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
if (is_array($this->value)) {
|
||||
return implode(',', $this->value);
|
||||
} else {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets multiple values for this parameter.
|
||||
*/
|
||||
public function setParts(array $value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values for this parameter.
|
||||
*
|
||||
* If there were no values, an empty array will be returned.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParts()
|
||||
{
|
||||
if (is_array($this->value)) {
|
||||
return $this->value;
|
||||
} elseif (is_null($this->value)) {
|
||||
return [];
|
||||
} else {
|
||||
return [$this->value];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value to this parameter.
|
||||
*
|
||||
* If the argument is specified as an array, all items will be added to the
|
||||
* parameter value list.
|
||||
*
|
||||
* @param string|array $part
|
||||
*/
|
||||
public function addValue($part)
|
||||
{
|
||||
if (is_null($this->value)) {
|
||||
$this->value = $part;
|
||||
} else {
|
||||
$this->value = array_merge((array) $this->value, (array) $part);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this parameter contains the specified value.
|
||||
*
|
||||
* This is a case-insensitive match. It makes sense to call this for for
|
||||
* instance the TYPE parameter, to see if it contains a keyword such as
|
||||
* 'WORK' or 'FAX'.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has($value)
|
||||
{
|
||||
return in_array(
|
||||
strtolower($value),
|
||||
array_map('strtolower', (array) $this->value)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the object back into a serialized blob.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
$value = $this->getParts();
|
||||
|
||||
if (0 === count($value)) {
|
||||
return $this->name.'=';
|
||||
}
|
||||
|
||||
if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) {
|
||||
return implode(';', $value);
|
||||
}
|
||||
|
||||
return $this->name.'='.array_reduce(
|
||||
$value,
|
||||
function ($out, $item) {
|
||||
if (!is_null($out)) {
|
||||
$out .= ',';
|
||||
}
|
||||
|
||||
// If there's no special characters in the string, we'll use the simple
|
||||
// format.
|
||||
//
|
||||
// The list of special characters is defined as:
|
||||
//
|
||||
// Any character except CONTROL, DQUOTE, ";", ":", ","
|
||||
//
|
||||
// by the iCalendar spec:
|
||||
// https://tools.ietf.org/html/rfc5545#section-3.1
|
||||
//
|
||||
// And we add ^ to that because of:
|
||||
// https://tools.ietf.org/html/rfc6868
|
||||
//
|
||||
// But we've found that iCal (7.0, shipped with OSX 10.9)
|
||||
// severely trips on + characters not being quoted, so we
|
||||
// added + as well.
|
||||
if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
|
||||
return $out.$item;
|
||||
} else {
|
||||
// Enclosing in double-quotes, and using RFC6868 for encoding any
|
||||
// special characters
|
||||
$out .= '"'.strtr(
|
||||
$item,
|
||||
[
|
||||
'^' => '^^',
|
||||
"\n" => '^n',
|
||||
'"' => '^\'',
|
||||
]
|
||||
).'"';
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns an array, with the representation as it should be
|
||||
* encoded in JSON. This is used to create jCard or jCal documents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the data into XML. This is used to create xCard or
|
||||
* xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
public function xmlSerialize(Xml\Writer $writer): void
|
||||
{
|
||||
foreach (explode(',', $this->value) as $value) {
|
||||
$writer->writeElement('text', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this object is being cast to a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the iterator for this object.
|
||||
*
|
||||
* @return ElementList
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function getIterator()
|
||||
{
|
||||
if (!is_null($this->iterator)) {
|
||||
return $this->iterator;
|
||||
}
|
||||
|
||||
return $this->iterator = new ArrayIterator((array) $this->value);
|
||||
}
|
||||
}
|
||||
14
vendor/sabre/vobject/lib/ParseException.php
vendored
Normal file
14
vendor/sabre/vobject/lib/ParseException.php
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
/**
|
||||
* Exception thrown by Reader if an invalid object was attempted to be parsed.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class ParseException extends \Exception
|
||||
{
|
||||
}
|
||||
190
vendor/sabre/vobject/lib/Parser/Json.php
vendored
Normal file
190
vendor/sabre/vobject/lib/Parser/Json.php
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Parser;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VCard;
|
||||
use Sabre\VObject\Document;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\ParseException;
|
||||
use Sabre\VObject\Property\FlatText;
|
||||
use Sabre\VObject\Property\Text;
|
||||
|
||||
/**
|
||||
* Json Parser.
|
||||
*
|
||||
* This parser parses both the jCal and jCard formats.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Json extends Parser
|
||||
{
|
||||
/**
|
||||
* The input data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* Root component.
|
||||
*
|
||||
* @var Document
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* This method starts the parsing process.
|
||||
*
|
||||
* If the input was not supplied during construction, it's possible to pass
|
||||
* it here instead.
|
||||
*
|
||||
* If either input or options are not supplied, the defaults will be used.
|
||||
*
|
||||
* @param resource|string|array|null $input
|
||||
* @param int $options
|
||||
*
|
||||
* @return \Sabre\VObject\Document
|
||||
*/
|
||||
public function parse($input = null, $options = 0)
|
||||
{
|
||||
if (!is_null($input)) {
|
||||
$this->setInput($input);
|
||||
}
|
||||
if (is_null($this->input)) {
|
||||
throw new EofException('End of input stream, or no input supplied');
|
||||
}
|
||||
|
||||
if (0 !== $options) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
switch ($this->input[0]) {
|
||||
case 'vcalendar':
|
||||
$this->root = new VCalendar([], false);
|
||||
break;
|
||||
case 'vcard':
|
||||
$this->root = new VCard([], false);
|
||||
break;
|
||||
default:
|
||||
throw new ParseException('The root component must either be a vcalendar, or a vcard');
|
||||
}
|
||||
foreach ($this->input[1] as $prop) {
|
||||
$this->root->add($this->parseProperty($prop));
|
||||
}
|
||||
if (isset($this->input[2])) {
|
||||
foreach ($this->input[2] as $comp) {
|
||||
$this->root->add($this->parseComponent($comp));
|
||||
}
|
||||
}
|
||||
|
||||
// Resetting the input so we can throw an feof exception the next time.
|
||||
$this->input = null;
|
||||
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a component.
|
||||
*
|
||||
* @return \Sabre\VObject\Component
|
||||
*/
|
||||
public function parseComponent(array $jComp)
|
||||
{
|
||||
// We can remove $self from PHP 5.4 onward.
|
||||
$self = $this;
|
||||
|
||||
$properties = array_map(
|
||||
function ($jProp) use ($self) {
|
||||
return $self->parseProperty($jProp);
|
||||
},
|
||||
$jComp[1]
|
||||
);
|
||||
|
||||
if (isset($jComp[2])) {
|
||||
$components = array_map(
|
||||
function ($jComp) use ($self) {
|
||||
return $self->parseComponent($jComp);
|
||||
},
|
||||
$jComp[2]
|
||||
);
|
||||
} else {
|
||||
$components = [];
|
||||
}
|
||||
|
||||
return $this->root->createComponent(
|
||||
$jComp[0],
|
||||
array_merge($properties, $components),
|
||||
$defaults = false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses properties.
|
||||
*
|
||||
* @return \Sabre\VObject\Property
|
||||
*/
|
||||
public function parseProperty(array $jProp)
|
||||
{
|
||||
list(
|
||||
$propertyName,
|
||||
$parameters,
|
||||
$valueType
|
||||
) = $jProp;
|
||||
|
||||
$propertyName = strtoupper($propertyName);
|
||||
|
||||
// This is the default class we would be using if we didn't know the
|
||||
// value type. We're using this value later in this function.
|
||||
$defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName);
|
||||
|
||||
$parameters = (array) $parameters;
|
||||
|
||||
$value = array_slice($jProp, 3);
|
||||
|
||||
$valueType = strtoupper($valueType);
|
||||
|
||||
if (isset($parameters['group'])) {
|
||||
$propertyName = $parameters['group'].'.'.$propertyName;
|
||||
unset($parameters['group']);
|
||||
}
|
||||
|
||||
$prop = $this->root->createProperty($propertyName, null, $parameters, $valueType);
|
||||
$prop->setJsonValue($value);
|
||||
|
||||
// We have to do something awkward here. FlatText as well as Text
|
||||
// represents TEXT values. We have to normalize these here. In the
|
||||
// future we can get rid of FlatText once we're allowed to break BC
|
||||
// again.
|
||||
if (FlatText::class === $defaultPropertyClass) {
|
||||
$defaultPropertyClass = Text::class;
|
||||
}
|
||||
|
||||
// If the value type we received (e.g.: TEXT) was not the default value
|
||||
// type for the given property (e.g.: BDAY), we need to add a VALUE=
|
||||
// parameter.
|
||||
if ($defaultPropertyClass !== get_class($prop)) {
|
||||
$prop['VALUE'] = $valueType;
|
||||
}
|
||||
|
||||
return $prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input data.
|
||||
*
|
||||
* @param resource|string|array $input
|
||||
*/
|
||||
public function setInput($input)
|
||||
{
|
||||
if (is_resource($input)) {
|
||||
$input = stream_get_contents($input);
|
||||
}
|
||||
if (is_string($input)) {
|
||||
$input = json_decode($input);
|
||||
}
|
||||
$this->input = $input;
|
||||
}
|
||||
}
|
||||
710
vendor/sabre/vobject/lib/Parser/MimeDir.php
vendored
Normal file
710
vendor/sabre/vobject/lib/Parser/MimeDir.php
vendored
Normal file
@@ -0,0 +1,710 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Parser;
|
||||
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VCard;
|
||||
use Sabre\VObject\Document;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\Node;
|
||||
use Sabre\VObject\ParseException;
|
||||
|
||||
/**
|
||||
* MimeDir parser.
|
||||
*
|
||||
* This class parses iCalendar 2.0 and vCard 2.1, 3.0 and 4.0 files. This
|
||||
* parser will return one of the following two objects from the parse method:
|
||||
*
|
||||
* Sabre\VObject\Component\VCalendar
|
||||
* Sabre\VObject\Component\VCard
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class MimeDir extends Parser
|
||||
{
|
||||
public const TOKEN_PROPNAME = 1;
|
||||
public const TOKEN_PROPVALUE = 2;
|
||||
public const TOKEN_PARAMNAME = 3;
|
||||
public const TOKEN_PARAMVALUE = 4;
|
||||
|
||||
/**
|
||||
* The input stream.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* Root component.
|
||||
*
|
||||
* @var Component
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* By default all input will be assumed to be UTF-8.
|
||||
*
|
||||
* However, both iCalendar and vCard might be encoded using different
|
||||
* character sets. The character set is usually set in the mime-type.
|
||||
*
|
||||
* If this is the case, use setEncoding to specify that a different
|
||||
* encoding will be used. If this is set, the parser will automatically
|
||||
* convert all incoming data to UTF-8.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $charset = 'UTF-8';
|
||||
|
||||
/**
|
||||
* The list of character sets we support when decoding.
|
||||
*
|
||||
* This would be a const expression but for now we need to support PHP 5.5
|
||||
*/
|
||||
protected static $SUPPORTED_CHARSETS = [
|
||||
'UTF-8',
|
||||
'ISO-8859-1',
|
||||
'Windows-1252',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parses an iCalendar or vCard file.
|
||||
*
|
||||
* Pass a stream or a string. If null is parsed, the existing buffer is
|
||||
* used.
|
||||
*
|
||||
* @param string|resource|null $input
|
||||
* @param int $options
|
||||
*
|
||||
* @return \Sabre\VObject\Document
|
||||
*/
|
||||
public function parse($input = null, $options = 0)
|
||||
{
|
||||
$this->root = null;
|
||||
|
||||
if (!is_null($input)) {
|
||||
$this->setInput($input);
|
||||
}
|
||||
|
||||
if (!\is_resource($this->input)) {
|
||||
// Null was passed as input, but there was no existing input buffer
|
||||
// There is nothing to parse.
|
||||
throw new ParseException('No input provided to parse');
|
||||
}
|
||||
|
||||
if (0 !== $options) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
$this->parseDocument();
|
||||
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* By default all input will be assumed to be UTF-8.
|
||||
*
|
||||
* However, both iCalendar and vCard might be encoded using different
|
||||
* character sets. The character set is usually set in the mime-type.
|
||||
*
|
||||
* If this is the case, use setEncoding to specify that a different
|
||||
* encoding will be used. If this is set, the parser will automatically
|
||||
* convert all incoming data to UTF-8.
|
||||
*
|
||||
* @param string $charset
|
||||
*/
|
||||
public function setCharset($charset)
|
||||
{
|
||||
if (!in_array($charset, self::$SUPPORTED_CHARSETS)) {
|
||||
throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')');
|
||||
}
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input buffer. Must be a string or stream.
|
||||
*
|
||||
* @param resource|string $input
|
||||
*/
|
||||
public function setInput($input)
|
||||
{
|
||||
// Resetting the parser
|
||||
$this->lineIndex = 0;
|
||||
$this->startLine = 0;
|
||||
|
||||
if (is_string($input)) {
|
||||
// Converting to a stream.
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, $input);
|
||||
rewind($stream);
|
||||
$this->input = $stream;
|
||||
} elseif (is_resource($input)) {
|
||||
$this->input = $input;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('This parser can only read from strings or streams.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an entire document.
|
||||
*/
|
||||
protected function parseDocument()
|
||||
{
|
||||
$line = $this->readLine();
|
||||
|
||||
// BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF).
|
||||
// It's 0xEF 0xBB 0xBF in UTF-8 hex.
|
||||
if (3 <= strlen($line)
|
||||
&& 0xef === ord($line[0])
|
||||
&& 0xbb === ord($line[1])
|
||||
&& 0xbf === ord($line[2])) {
|
||||
$line = substr($line, 3);
|
||||
}
|
||||
|
||||
switch (strtoupper($line)) {
|
||||
case 'BEGIN:VCALENDAR':
|
||||
$class = VCalendar::$componentMap['VCALENDAR'];
|
||||
break;
|
||||
case 'BEGIN:VCARD':
|
||||
$class = VCard::$componentMap['VCARD'];
|
||||
break;
|
||||
default:
|
||||
throw new ParseException('This parser only supports VCARD and VCALENDAR files');
|
||||
}
|
||||
|
||||
$this->root = new $class([], false);
|
||||
|
||||
while (true) {
|
||||
// Reading until we hit END:
|
||||
try {
|
||||
$line = $this->readLine();
|
||||
} catch (EofException $oEx) {
|
||||
$line = 'END:'.$this->root->name;
|
||||
}
|
||||
if ('END:' === strtoupper(substr($line, 0, 4))) {
|
||||
break;
|
||||
}
|
||||
$result = $this->parseLine($line);
|
||||
if ($result) {
|
||||
$this->root->add($result);
|
||||
}
|
||||
}
|
||||
|
||||
$name = strtoupper(substr($line, 4));
|
||||
if ($name !== $this->root->name) {
|
||||
throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a line, and if it hits a component, it will also attempt to parse
|
||||
* the entire component.
|
||||
*
|
||||
* @param string $line Unfolded line
|
||||
*
|
||||
* @return Node
|
||||
*/
|
||||
protected function parseLine($line)
|
||||
{
|
||||
// Start of a new component
|
||||
if ('BEGIN:' === strtoupper(substr($line, 0, 6))) {
|
||||
if (substr($line, 6) === $this->root->name) {
|
||||
throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name);
|
||||
}
|
||||
$component = $this->root->createComponent(substr($line, 6), [], false);
|
||||
|
||||
while (true) {
|
||||
// Reading until we hit END:
|
||||
$line = $this->readLine();
|
||||
if ('END:' === strtoupper(substr($line, 0, 4))) {
|
||||
break;
|
||||
}
|
||||
$result = $this->parseLine($line);
|
||||
if ($result) {
|
||||
$component->add($result);
|
||||
}
|
||||
}
|
||||
|
||||
$name = strtoupper(substr($line, 4));
|
||||
if ($name !== $component->name) {
|
||||
throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"');
|
||||
}
|
||||
|
||||
return $component;
|
||||
} else {
|
||||
// Property reader
|
||||
$property = $this->readProperty($line);
|
||||
if (!$property) {
|
||||
// Ignored line
|
||||
return false;
|
||||
}
|
||||
|
||||
return $property;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to look ahead 1 line every time to see if we need to 'unfold'
|
||||
* the next line.
|
||||
*
|
||||
* If that was not the case, we store it here.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $lineBuffer;
|
||||
|
||||
/**
|
||||
* The real current line number.
|
||||
*/
|
||||
protected $lineIndex = 0;
|
||||
|
||||
/**
|
||||
* In the case of unfolded lines, this property holds the line number for
|
||||
* the start of the line.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $startLine = 0;
|
||||
|
||||
/**
|
||||
* Contains a 'raw' representation of the current line.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rawLine;
|
||||
|
||||
/**
|
||||
* Reads a single line from the buffer.
|
||||
*
|
||||
* This method strips any newlines and also takes care of unfolding.
|
||||
*
|
||||
* @throws \Sabre\VObject\EofException
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function readLine()
|
||||
{
|
||||
if (!\is_null($this->lineBuffer)) {
|
||||
$rawLine = $this->lineBuffer;
|
||||
$this->lineBuffer = null;
|
||||
} else {
|
||||
do {
|
||||
$eof = \feof($this->input);
|
||||
|
||||
$rawLine = \fgets($this->input);
|
||||
|
||||
if ($eof || (\feof($this->input) && false === $rawLine)) {
|
||||
throw new EofException('End of document reached prematurely');
|
||||
}
|
||||
if (false === $rawLine) {
|
||||
throw new ParseException('Error reading from input stream');
|
||||
}
|
||||
$rawLine = \rtrim($rawLine, "\r\n");
|
||||
} while ('' === $rawLine); // Skipping empty lines
|
||||
++$this->lineIndex;
|
||||
}
|
||||
$line = $rawLine;
|
||||
|
||||
$this->startLine = $this->lineIndex;
|
||||
|
||||
// Looking ahead for folded lines.
|
||||
while (true) {
|
||||
$nextLine = \rtrim(\fgets($this->input), "\r\n");
|
||||
++$this->lineIndex;
|
||||
if (!$nextLine) {
|
||||
break;
|
||||
}
|
||||
if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) {
|
||||
$curLine = \substr($nextLine, 1);
|
||||
$line .= $curLine;
|
||||
$rawLine .= "\n ".$curLine;
|
||||
} else {
|
||||
$this->lineBuffer = $nextLine;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->rawLine = $rawLine;
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a property or component from a line.
|
||||
*/
|
||||
protected function readProperty($line)
|
||||
{
|
||||
if ($this->options & self::OPTION_FORGIVING) {
|
||||
$propNameToken = 'A-Z0-9\-\._\\/';
|
||||
} else {
|
||||
$propNameToken = 'A-Z0-9\-\.';
|
||||
}
|
||||
|
||||
$paramNameToken = 'A-Z0-9\-';
|
||||
$safeChar = '^";:,';
|
||||
$qSafeChar = '^"';
|
||||
|
||||
$regex = "/
|
||||
^(?P<name> [$propNameToken]+ ) (?=[;:]) # property name
|
||||
|
|
||||
(?<=:)(?P<propValue> .+)$ # property value
|
||||
|
|
||||
;(?P<paramName> [$paramNameToken]+) (?=[=;:]) # parameter name
|
||||
|
|
||||
(=|,)(?P<paramValue> # parameter value
|
||||
(?: [$safeChar]*) |
|
||||
\"(?: [$qSafeChar]+)\"
|
||||
) (?=[;:,])
|
||||
/xi";
|
||||
|
||||
//echo $regex, "\n"; exit();
|
||||
preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
|
||||
|
||||
$property = [
|
||||
'name' => null,
|
||||
'parameters' => [],
|
||||
'value' => null,
|
||||
];
|
||||
|
||||
/*
|
||||
* Keep track on the last token we parsed in order to do
|
||||
* better error checking
|
||||
*/
|
||||
$lastToken = null;
|
||||
|
||||
$lastParam = null;
|
||||
|
||||
/*
|
||||
* Looping through all the tokens.
|
||||
*
|
||||
* Note that we are looping through them in reverse order, because if a
|
||||
* sub-pattern matched, the subsequent named patterns will not show up
|
||||
* in the result.
|
||||
*/
|
||||
foreach ($matches as $match) {
|
||||
if (isset($match['paramValue'])) {
|
||||
if ($match['paramValue'] && '"' === $match['paramValue'][0]) {
|
||||
$value = substr($match['paramValue'], 1, -1);
|
||||
} else {
|
||||
$value = $match['paramValue'];
|
||||
}
|
||||
|
||||
$value = $this->unescapeParam($value);
|
||||
|
||||
if (is_null($lastParam)) {
|
||||
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
|
||||
// When the property can't be matched and the configuration
|
||||
// option is set to ignore invalid lines, we ignore this line
|
||||
// This can happen when servers provide faulty data as iCloud
|
||||
// frequently does with X-APPLE-STRUCTURED-LOCATION
|
||||
$lastToken = self::TOKEN_PARAMVALUE;
|
||||
continue;
|
||||
}
|
||||
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
|
||||
}
|
||||
|
||||
if ('=' == $match[0][0] && self::TOKEN_PARAMNAME != $lastToken) {
|
||||
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.': Missing parameter name for parameter value "'.$match['paramValue'].'"');
|
||||
}
|
||||
|
||||
if (is_null($property['parameters'][$lastParam])) {
|
||||
$property['parameters'][$lastParam] = $value;
|
||||
} elseif (is_array($property['parameters'][$lastParam])) {
|
||||
$property['parameters'][$lastParam][] = $value;
|
||||
} elseif ($property['parameters'][$lastParam] === $value) {
|
||||
// When the current value of the parameter is the same as the
|
||||
// new one, then we can leave the current parameter as it is.
|
||||
} else {
|
||||
$property['parameters'][$lastParam] = [
|
||||
$property['parameters'][$lastParam],
|
||||
$value,
|
||||
];
|
||||
}
|
||||
$lastToken = self::TOKEN_PARAMVALUE;
|
||||
continue;
|
||||
}
|
||||
if (isset($match['paramName'])) {
|
||||
$lastParam = strtoupper($match['paramName']);
|
||||
if (!isset($property['parameters'][$lastParam])) {
|
||||
$property['parameters'][$lastParam] = null;
|
||||
}
|
||||
$lastToken = self::TOKEN_PARAMNAME;
|
||||
continue;
|
||||
}
|
||||
if (isset($match['propValue'])) {
|
||||
$property['value'] = $match['propValue'];
|
||||
$lastToken = self::TOKEN_PROPVALUE;
|
||||
continue;
|
||||
}
|
||||
if (isset($match['name']) && 0 < strlen($match['name'])) {
|
||||
$property['name'] = strtoupper($match['name']);
|
||||
$lastToken = self::TOKEN_PROPNAME;
|
||||
continue;
|
||||
}
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
throw new \LogicException('This code should not be reachable');
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if (is_null($property['value'])) {
|
||||
$property['value'] = '';
|
||||
}
|
||||
if (!isset($property['name']) || 0 == strlen($property['name'])) {
|
||||
if ($this->options & self::OPTION_IGNORE_INVALID_LINES) {
|
||||
return false;
|
||||
}
|
||||
throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions');
|
||||
}
|
||||
|
||||
// vCard 2.1 states that parameters may appear without a name, and only
|
||||
// a value. We can deduce the value based on its name.
|
||||
//
|
||||
// Our parser will get those as parameters without a value instead, so
|
||||
// we're filtering these parameters out first.
|
||||
$namedParameters = [];
|
||||
$namelessParameters = [];
|
||||
|
||||
foreach ($property['parameters'] as $name => $value) {
|
||||
if (!is_null($value)) {
|
||||
$namedParameters[$name] = $value;
|
||||
} else {
|
||||
$namelessParameters[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line);
|
||||
|
||||
foreach ($namelessParameters as $namelessParameter) {
|
||||
$propObj->add(null, $namelessParameter);
|
||||
}
|
||||
|
||||
if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) {
|
||||
$propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue());
|
||||
} else {
|
||||
$charset = $this->charset;
|
||||
if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) {
|
||||
// vCard 2.1 allows the character set to be specified per property.
|
||||
$charset = (string) $propObj['CHARSET'];
|
||||
}
|
||||
switch (strtolower($charset)) {
|
||||
case 'utf-8':
|
||||
break;
|
||||
case 'windows-1252':
|
||||
case 'iso-8859-1':
|
||||
$property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset);
|
||||
break;
|
||||
default:
|
||||
throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']);
|
||||
}
|
||||
$propObj->setRawMimeDirValue($property['value']);
|
||||
}
|
||||
|
||||
return $propObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes a property value.
|
||||
*
|
||||
* vCard 2.1 says:
|
||||
* * Semi-colons must be escaped in some property values, specifically
|
||||
* ADR, ORG and N.
|
||||
* * Semi-colons must be escaped in parameter values, because semi-colons
|
||||
* are also use to separate values.
|
||||
* * No mention of escaping backslashes with another backslash.
|
||||
* * newlines are not escaped either, instead QUOTED-PRINTABLE is used to
|
||||
* span values over more than 1 line.
|
||||
*
|
||||
* vCard 3.0 says:
|
||||
* * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be
|
||||
* escaped, all time time.
|
||||
* * Comma's are used for delimiters in multiple values
|
||||
* * (rfc2426) Adds to to this that the semi-colon MUST also be escaped,
|
||||
* as in some properties semi-colon is used for separators.
|
||||
* * Properties using semi-colons: N, ADR, GEO, ORG
|
||||
* * Both ADR and N's individual parts may be broken up further with a
|
||||
* comma.
|
||||
* * Properties using commas: NICKNAME, CATEGORIES
|
||||
*
|
||||
* vCard 4.0 (rfc6350) says:
|
||||
* * Commas must be escaped.
|
||||
* * Semi-colons may be escaped, an unescaped semi-colon _may_ be a
|
||||
* delimiter, depending on the property.
|
||||
* * Backslashes must be escaped
|
||||
* * Newlines must be escaped as either \N or \n.
|
||||
* * Some compound properties may contain multiple parts themselves, so a
|
||||
* comma within a semi-colon delimited property may also be unescaped
|
||||
* to denote multiple parts _within_ the compound property.
|
||||
* * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP.
|
||||
* * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID.
|
||||
*
|
||||
* Even though the spec says that commas must always be escaped, the
|
||||
* example for GEO in Section 6.5.2 seems to violate this.
|
||||
*
|
||||
* iCalendar 2.0 (rfc5545) says:
|
||||
* * Commas or semi-colons may be used as delimiters, depending on the
|
||||
* property.
|
||||
* * Commas, semi-colons, backslashes, newline (\N or \n) are always
|
||||
* escaped, unless they are delimiters.
|
||||
* * Colons shall not be escaped.
|
||||
* * Commas can be considered the 'default delimiter' and is described as
|
||||
* the delimiter in cases where the order of the multiple values is
|
||||
* insignificant.
|
||||
* * Semi-colons are described as the delimiter for 'structured values'.
|
||||
* They are specifically used in Semi-colons are used as a delimiter in
|
||||
* REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however.
|
||||
*
|
||||
* Now for the parameters
|
||||
*
|
||||
* If delimiter is not set (empty string) this method will just return a string.
|
||||
* If it's a comma or a semi-colon the string will be split on those
|
||||
* characters, and always return an array.
|
||||
*
|
||||
* @param string $input
|
||||
* @param string $delimiter
|
||||
*
|
||||
* @return string|string[]
|
||||
*/
|
||||
public static function unescapeValue($input, $delimiter = ';')
|
||||
{
|
||||
$regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )';
|
||||
if ($delimiter) {
|
||||
$regex .= ' | ('.$delimiter.')';
|
||||
}
|
||||
$regex .= ') #x';
|
||||
|
||||
$matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
$resultArray = [];
|
||||
$result = '';
|
||||
|
||||
foreach ($matches as $match) {
|
||||
switch ($match) {
|
||||
case '\\\\':
|
||||
$result .= '\\';
|
||||
break;
|
||||
case '\N':
|
||||
case '\n':
|
||||
$result .= "\n";
|
||||
break;
|
||||
case '\;':
|
||||
$result .= ';';
|
||||
break;
|
||||
case '\,':
|
||||
$result .= ',';
|
||||
break;
|
||||
case $delimiter:
|
||||
$resultArray[] = $result;
|
||||
$result = '';
|
||||
break;
|
||||
default:
|
||||
$result .= $match;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$resultArray[] = $result;
|
||||
|
||||
return $delimiter ? $resultArray : $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes a parameter value.
|
||||
*
|
||||
* vCard 2.1:
|
||||
* * Does not mention a mechanism for this. In addition, double quotes
|
||||
* are never used to wrap values.
|
||||
* * This means that parameters can simply not contain colons or
|
||||
* semi-colons.
|
||||
*
|
||||
* vCard 3.0 (rfc2425, rfc2426):
|
||||
* * Parameters _may_ be surrounded by double quotes.
|
||||
* * If this is not the case, semi-colon, colon and comma may simply not
|
||||
* occur (the comma used for multiple parameter values though).
|
||||
* * If it is surrounded by double-quotes, it may simply not contain
|
||||
* double-quotes.
|
||||
* * This means that a parameter can in no case encode double-quotes, or
|
||||
* newlines.
|
||||
*
|
||||
* vCard 4.0 (rfc6350)
|
||||
* * Behavior seems to be identical to vCard 3.0
|
||||
*
|
||||
* iCalendar 2.0 (rfc5545)
|
||||
* * Behavior seems to be identical to vCard 3.0
|
||||
*
|
||||
* Parameter escaping mechanism (rfc6868) :
|
||||
* * This rfc describes a new way to escape parameter values.
|
||||
* * New-line is encoded as ^n
|
||||
* * ^ is encoded as ^^.
|
||||
* * " is encoded as ^'
|
||||
*
|
||||
* @param string $input
|
||||
*/
|
||||
private function unescapeParam($input)
|
||||
{
|
||||
return
|
||||
preg_replace_callback(
|
||||
'#(\^(\^|n|\'))#',
|
||||
function ($matches) {
|
||||
switch ($matches[2]) {
|
||||
case 'n':
|
||||
return "\n";
|
||||
case '^':
|
||||
return '^';
|
||||
case '\'':
|
||||
return '"';
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
},
|
||||
$input
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the full quoted printable value.
|
||||
*
|
||||
* We need a special method for this, because newlines have both a meaning
|
||||
* in vCards, and in QuotedPrintable.
|
||||
*
|
||||
* This method does not do any decoding.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function extractQuotedPrintableValue()
|
||||
{
|
||||
// We need to parse the raw line again to get the start of the value.
|
||||
//
|
||||
// We are basically looking for the first colon (:), but we need to
|
||||
// skip over the parameters first, as they may contain one.
|
||||
$regex = '/^
|
||||
(?: [^:])+ # Anything but a colon
|
||||
(?: "[^"]")* # A parameter in double quotes
|
||||
: # start of the value we really care about
|
||||
(.*)$
|
||||
/xs';
|
||||
|
||||
preg_match($regex, $this->rawLine, $matches);
|
||||
|
||||
$value = $matches[1];
|
||||
// Removing the first whitespace character from every line. Kind of
|
||||
// like unfolding, but we keep the newline.
|
||||
$value = str_replace("\n ", "\n", $value);
|
||||
|
||||
// Microsoft products don't always correctly fold lines, they may be
|
||||
// missing a whitespace. So if 'forgiving' is turned on, we will take
|
||||
// those as well.
|
||||
if ($this->options & self::OPTION_FORGIVING) {
|
||||
while ('=' === substr($value, -1) && $this->lineBuffer) {
|
||||
// Reading the line
|
||||
$this->readLine();
|
||||
// Grabbing the raw form
|
||||
$value .= "\n".$this->rawLine;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
75
vendor/sabre/vobject/lib/Parser/Parser.php
vendored
Normal file
75
vendor/sabre/vobject/lib/Parser/Parser.php
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Parser;
|
||||
|
||||
/**
|
||||
* Abstract parser.
|
||||
*
|
||||
* This class serves as a base-class for the different parsers.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class Parser
|
||||
{
|
||||
/**
|
||||
* Turning on this option makes the parser more forgiving.
|
||||
*
|
||||
* In the case of the MimeDir parser, this means that the parser will
|
||||
* accept slashes and underscores in property names, and it will also
|
||||
* attempt to fix Microsoft vCard 2.1's broken line folding.
|
||||
*/
|
||||
const OPTION_FORGIVING = 1;
|
||||
|
||||
/**
|
||||
* If this option is turned on, any lines we cannot parse will be ignored
|
||||
* by the reader.
|
||||
*/
|
||||
const OPTION_IGNORE_INVALID_LINES = 2;
|
||||
|
||||
/**
|
||||
* Bitmask of parser options.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Creates the parser.
|
||||
*
|
||||
* Optionally, it's possible to parse the input stream here.
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param int $options any parser options (OPTION constants)
|
||||
*/
|
||||
public function __construct($input = null, $options = 0)
|
||||
{
|
||||
if (!is_null($input)) {
|
||||
$this->setInput($input);
|
||||
}
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method starts the parsing process.
|
||||
*
|
||||
* If the input was not supplied during construction, it's possible to pass
|
||||
* it here instead.
|
||||
*
|
||||
* If either input or options are not supplied, the defaults will be used.
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract public function parse($input = null, $options = 0);
|
||||
|
||||
/**
|
||||
* Sets the input data.
|
||||
*
|
||||
* @param mixed $input
|
||||
*/
|
||||
abstract public function setInput($input);
|
||||
}
|
||||
377
vendor/sabre/vobject/lib/Parser/XML.php
vendored
Normal file
377
vendor/sabre/vobject/lib/Parser/XML.php
vendored
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Parser;
|
||||
|
||||
use Sabre\VObject\Component;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VCard;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\ParseException;
|
||||
use Sabre\Xml as SabreXml;
|
||||
|
||||
/**
|
||||
* XML Parser.
|
||||
*
|
||||
* This parser parses both the xCal and xCard formats.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Ivan Enderlin
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class XML extends Parser
|
||||
{
|
||||
const XCAL_NAMESPACE = 'urn:ietf:params:xml:ns:icalendar-2.0';
|
||||
const XCARD_NAMESPACE = 'urn:ietf:params:xml:ns:vcard-4.0';
|
||||
|
||||
/**
|
||||
* The input data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* A pointer/reference to the input.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $pointer;
|
||||
|
||||
/**
|
||||
* Document, root component.
|
||||
*
|
||||
* @var \Sabre\VObject\Document
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* Creates the parser.
|
||||
*
|
||||
* Optionally, it's possible to parse the input stream here.
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param int $options any parser options (OPTION constants)
|
||||
*/
|
||||
public function __construct($input = null, $options = 0)
|
||||
{
|
||||
if (0 === $options) {
|
||||
$options = parent::OPTION_FORGIVING;
|
||||
}
|
||||
|
||||
parent::__construct($input, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse xCal or xCard.
|
||||
*
|
||||
* @param resource|string $input
|
||||
* @param int $options
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @return \Sabre\VObject\Document
|
||||
*/
|
||||
public function parse($input = null, $options = 0)
|
||||
{
|
||||
if (!is_null($input)) {
|
||||
$this->setInput($input);
|
||||
}
|
||||
|
||||
if (0 !== $options) {
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
if (is_null($this->input)) {
|
||||
throw new EofException('End of input stream, or no input supplied');
|
||||
}
|
||||
|
||||
switch ($this->input['name']) {
|
||||
case '{'.self::XCAL_NAMESPACE.'}icalendar':
|
||||
$this->root = new VCalendar([], false);
|
||||
$this->pointer = &$this->input['value'][0];
|
||||
$this->parseVCalendarComponents($this->root);
|
||||
break;
|
||||
|
||||
case '{'.self::XCARD_NAMESPACE.'}vcards':
|
||||
foreach ($this->input['value'] as &$vCard) {
|
||||
$this->root = new VCard(['version' => '4.0'], false);
|
||||
$this->pointer = &$vCard;
|
||||
$this->parseVCardComponents($this->root);
|
||||
|
||||
// We just parse the first <vcard /> element.
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ParseException('Unsupported XML standard');
|
||||
}
|
||||
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a xCalendar component.
|
||||
*/
|
||||
protected function parseVCalendarComponents(Component $parentComponent)
|
||||
{
|
||||
foreach ($this->pointer['value'] ?: [] as $children) {
|
||||
switch (static::getTagName($children['name'])) {
|
||||
case 'properties':
|
||||
$this->pointer = &$children['value'];
|
||||
$this->parseProperties($parentComponent);
|
||||
break;
|
||||
|
||||
case 'components':
|
||||
$this->pointer = &$children;
|
||||
$this->parseComponent($parentComponent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a xCard component.
|
||||
*/
|
||||
protected function parseVCardComponents(Component $parentComponent)
|
||||
{
|
||||
$this->pointer = &$this->pointer['value'];
|
||||
$this->parseProperties($parentComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse xCalendar and xCard properties.
|
||||
*
|
||||
* @param string $propertyNamePrefix
|
||||
*/
|
||||
protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '')
|
||||
{
|
||||
foreach ($this->pointer ?: [] as $xmlProperty) {
|
||||
list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
|
||||
|
||||
$propertyName = $tagName;
|
||||
$propertyValue = [];
|
||||
$propertyParameters = [];
|
||||
$propertyType = 'text';
|
||||
|
||||
// A property which is not part of the standard.
|
||||
if (self::XCAL_NAMESPACE !== $namespace
|
||||
&& self::XCARD_NAMESPACE !== $namespace) {
|
||||
$propertyName = 'xml';
|
||||
$value = '<'.$tagName.' xmlns="'.$namespace.'"';
|
||||
|
||||
foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) {
|
||||
$value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"';
|
||||
}
|
||||
|
||||
$value .= '>'.$xmlProperty['value'].'</'.$tagName.'>';
|
||||
|
||||
$propertyValue = [$value];
|
||||
|
||||
$this->createProperty(
|
||||
$parentComponent,
|
||||
$propertyName,
|
||||
$propertyParameters,
|
||||
$propertyType,
|
||||
$propertyValue
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// xCard group.
|
||||
if ('group' === $propertyName) {
|
||||
if (!isset($xmlProperty['attributes']['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->pointer = &$xmlProperty['value'];
|
||||
$this->parseProperties(
|
||||
$parentComponent,
|
||||
strtoupper($xmlProperty['attributes']['name']).'.'
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect parameters.
|
||||
foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) {
|
||||
if (!is_array($xmlPropertyChild)
|
||||
|| 'parameters' !== static::getTagName($xmlPropertyChild['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xmlParameters = $xmlPropertyChild['value'];
|
||||
|
||||
foreach ($xmlParameters as $xmlParameter) {
|
||||
$propertyParameterValues = [];
|
||||
|
||||
foreach ($xmlParameter['value'] as $xmlParameterValues) {
|
||||
$propertyParameterValues[] = $xmlParameterValues['value'];
|
||||
}
|
||||
|
||||
$propertyParameters[static::getTagName($xmlParameter['name'])]
|
||||
= implode(',', $propertyParameterValues);
|
||||
}
|
||||
|
||||
array_splice($xmlProperty['value'], $i, 1);
|
||||
}
|
||||
|
||||
$propertyNameExtended = ($this->root instanceof VCalendar
|
||||
? 'xcal'
|
||||
: 'xcard').':'.$propertyName;
|
||||
|
||||
switch ($propertyNameExtended) {
|
||||
case 'xcal:geo':
|
||||
$propertyType = 'float';
|
||||
$propertyValue['latitude'] = 0;
|
||||
$propertyValue['longitude'] = 0;
|
||||
|
||||
foreach ($xmlProperty['value'] as $xmlRequestChild) {
|
||||
$propertyValue[static::getTagName($xmlRequestChild['name'])]
|
||||
= $xmlRequestChild['value'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xcal:request-status':
|
||||
$propertyType = 'text';
|
||||
|
||||
foreach ($xmlProperty['value'] as $xmlRequestChild) {
|
||||
$propertyValue[static::getTagName($xmlRequestChild['name'])]
|
||||
= $xmlRequestChild['value'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xcal:freebusy':
|
||||
$propertyType = 'freebusy';
|
||||
// We don't break because we only want to set
|
||||
// another property type.
|
||||
|
||||
// no break
|
||||
case 'xcal:categories':
|
||||
case 'xcal:resources':
|
||||
case 'xcal:exdate':
|
||||
foreach ($xmlProperty['value'] as $specialChild) {
|
||||
$propertyValue[static::getTagName($specialChild['name'])]
|
||||
= $specialChild['value'];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xcal:rdate':
|
||||
$propertyType = 'date-time';
|
||||
|
||||
foreach ($xmlProperty['value'] as $specialChild) {
|
||||
$tagName = static::getTagName($specialChild['name']);
|
||||
|
||||
if ('period' === $tagName) {
|
||||
$propertyParameters['value'] = 'PERIOD';
|
||||
$propertyValue[] = implode('/', $specialChild['value']);
|
||||
} else {
|
||||
$propertyValue[] = $specialChild['value'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$propertyType = static::getTagName($xmlProperty['value'][0]['name']);
|
||||
|
||||
foreach ($xmlProperty['value'] as $value) {
|
||||
$propertyValue[] = $value['value'];
|
||||
}
|
||||
|
||||
if ('date' === $propertyType) {
|
||||
$propertyParameters['value'] = 'DATE';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$this->createProperty(
|
||||
$parentComponent,
|
||||
$propertyNamePrefix.$propertyName,
|
||||
$propertyParameters,
|
||||
$propertyType,
|
||||
$propertyValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a component.
|
||||
*/
|
||||
protected function parseComponent(Component $parentComponent)
|
||||
{
|
||||
$components = $this->pointer['value'] ?: [];
|
||||
|
||||
foreach ($components as $component) {
|
||||
$componentName = static::getTagName($component['name']);
|
||||
$currentComponent = $this->root->createComponent(
|
||||
$componentName,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
$this->pointer = &$component;
|
||||
$this->parseVCalendarComponents($currentComponent);
|
||||
|
||||
$parentComponent->add($currentComponent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a property.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $parameters
|
||||
* @param string $type
|
||||
* @param mixed $value
|
||||
*/
|
||||
protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value)
|
||||
{
|
||||
$property = $this->root->createProperty(
|
||||
$name,
|
||||
null,
|
||||
$parameters,
|
||||
$type
|
||||
);
|
||||
$parentComponent->add($property);
|
||||
$property->setXmlValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the input data.
|
||||
*
|
||||
* @param resource|string $input
|
||||
*/
|
||||
public function setInput($input)
|
||||
{
|
||||
if (is_resource($input)) {
|
||||
$input = stream_get_contents($input);
|
||||
}
|
||||
|
||||
if (is_string($input)) {
|
||||
$reader = new SabreXml\Reader();
|
||||
$reader->elementMap['{'.self::XCAL_NAMESPACE.'}period']
|
||||
= XML\Element\KeyValue::class;
|
||||
$reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur']
|
||||
= XML\Element\KeyValue::class;
|
||||
$reader->xml($input);
|
||||
$input = $reader->parse();
|
||||
}
|
||||
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag name from a Clark notation.
|
||||
*
|
||||
* @param string $clarkedTagName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getTagName($clarkedTagName)
|
||||
{
|
||||
list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName);
|
||||
|
||||
return $tagName;
|
||||
}
|
||||
}
|
||||
63
vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php
vendored
Normal file
63
vendor/sabre/vobject/lib/Parser/XML/Element/KeyValue.php
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Parser\XML\Element;
|
||||
|
||||
use Sabre\Xml as SabreXml;
|
||||
|
||||
/**
|
||||
* Our own sabre/xml key-value element.
|
||||
*
|
||||
* It just removes the clark notation.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Ivan Enderlin
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class KeyValue extends SabreXml\Element\KeyValue
|
||||
{
|
||||
/**
|
||||
* The deserialize method is called during xml parsing.
|
||||
*
|
||||
* This method is called statically, this is because in theory this method
|
||||
* may be used as a type of constructor, or factory method.
|
||||
*
|
||||
* Often you want to return an instance of the current class, but you are
|
||||
* free to return other data as well.
|
||||
*
|
||||
* Important note 2: You are responsible for advancing the reader to the
|
||||
* next element. Not doing anything will result in a never-ending loop.
|
||||
*
|
||||
* If you just want to skip parsing for this element altogether, you can
|
||||
* just call $reader->next();
|
||||
*
|
||||
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
|
||||
* the next element.
|
||||
*
|
||||
* @param XML\Reader $reader
|
||||
*/
|
||||
public static function xmlDeserialize(SabreXml\Reader $reader): array
|
||||
{
|
||||
// If there's no children, we don't do anything.
|
||||
if ($reader->isEmptyElement) {
|
||||
$reader->next();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$values = [];
|
||||
$reader->read();
|
||||
|
||||
do {
|
||||
if (SabreXml\Reader::ELEMENT === $reader->nodeType) {
|
||||
$name = $reader->localName;
|
||||
$values[$name] = $reader->parseCurrentElement()['value'];
|
||||
} else {
|
||||
$reader->read();
|
||||
}
|
||||
} while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType);
|
||||
|
||||
$reader->read();
|
||||
|
||||
return $values;
|
||||
}
|
||||
}
|
||||
646
vendor/sabre/vobject/lib/Property.php
vendored
Normal file
646
vendor/sabre/vobject/lib/Property.php
vendored
Normal file
@@ -0,0 +1,646 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject;
|
||||
|
||||
use Sabre\Xml;
|
||||
|
||||
/**
|
||||
* Property.
|
||||
*
|
||||
* A property is always in a KEY:VALUE structure, and may optionally contain
|
||||
* parameters.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
abstract class Property extends Node
|
||||
{
|
||||
/**
|
||||
* Property name.
|
||||
*
|
||||
* This will contain a string such as DTSTART, SUMMARY, FN.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* Property group.
|
||||
*
|
||||
* This is only used in vcards
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $group;
|
||||
|
||||
/**
|
||||
* List of parameters.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $parameters = [];
|
||||
|
||||
/**
|
||||
* Current value.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* In case this is a multi-value property. This string will be used as a
|
||||
* delimiter.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $delimiter = ';';
|
||||
|
||||
/**
|
||||
* The line number in the original iCalendar / vCard file
|
||||
* that corresponds with the current node
|
||||
* if the node was read from a file.
|
||||
*/
|
||||
public $lineIndex;
|
||||
|
||||
/**
|
||||
* The line string from the original iCalendar / vCard file
|
||||
* that corresponds with the current node
|
||||
* if the node was read from a file.
|
||||
*/
|
||||
public $lineString;
|
||||
|
||||
/**
|
||||
* Creates the generic property.
|
||||
*
|
||||
* Parameters must be specified in key=>value syntax.
|
||||
*
|
||||
* @param Component $root The root document
|
||||
* @param string $name
|
||||
* @param string|array|null $value
|
||||
* @param array $parameters List of parameters
|
||||
* @param string $group The vcard property group
|
||||
*/
|
||||
public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null, ?int $lineIndex = null, ?string $lineString = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->group = $group;
|
||||
|
||||
$this->root = $root;
|
||||
|
||||
foreach ($parameters as $k => $v) {
|
||||
$this->add($k, $v);
|
||||
}
|
||||
|
||||
if (!is_null($value)) {
|
||||
$this->setValue($value);
|
||||
}
|
||||
|
||||
if (!is_null($lineIndex)) {
|
||||
$this->lineIndex = $lineIndex;
|
||||
}
|
||||
|
||||
if (!is_null($lineString)) {
|
||||
$this->lineString = $lineString;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current value.
|
||||
*
|
||||
* This may be either a single, or multiple strings in an array.
|
||||
*
|
||||
* @param string|array $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value.
|
||||
*
|
||||
* This method will always return a singular value. If this was a
|
||||
* multi-value object, some decision will be made first on how to represent
|
||||
* it as a string.
|
||||
*
|
||||
* To get the correct multi-value version, use getParts.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
if (is_array($this->value)) {
|
||||
if (0 == count($this->value)) {
|
||||
return;
|
||||
} elseif (1 === count($this->value)) {
|
||||
return $this->value[0];
|
||||
} else {
|
||||
return $this->getRawMimeDirValue();
|
||||
}
|
||||
} else {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a multi-valued property.
|
||||
*/
|
||||
public function setParts(array $parts)
|
||||
{
|
||||
$this->value = $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a multi-valued property.
|
||||
*
|
||||
* This method always returns an array, if there was only a single value,
|
||||
* it will still be wrapped in an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParts()
|
||||
{
|
||||
if (is_null($this->value)) {
|
||||
return [];
|
||||
} elseif (is_array($this->value)) {
|
||||
return $this->value;
|
||||
} else {
|
||||
return [$this->value];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new parameter.
|
||||
*
|
||||
* If a parameter with same name already existed, the values will be
|
||||
* combined.
|
||||
* If nameless parameter is added, we try to guess its name.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|array|null $value
|
||||
*/
|
||||
public function add($name, $value = null)
|
||||
{
|
||||
$noName = false;
|
||||
if (null === $name) {
|
||||
$name = Parameter::guessParameterNameByValue($value);
|
||||
$noName = true;
|
||||
}
|
||||
|
||||
if (isset($this->parameters[strtoupper($name)])) {
|
||||
$this->parameters[strtoupper($name)]->addValue($value);
|
||||
} else {
|
||||
$param = new Parameter($this->root, $name, $value);
|
||||
$param->noName = $noName;
|
||||
$this->parameters[$param->name] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable list of children.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function parameters()
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of value.
|
||||
*
|
||||
* This corresponds to the VALUE= parameter. Every property also has a
|
||||
* 'default' valueType.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getValueType();
|
||||
|
||||
/**
|
||||
* Sets a raw value coming from a mimedir (iCalendar/vCard) file.
|
||||
*
|
||||
* This has been 'unfolded', so only 1 line will be passed. Unescaping is
|
||||
* not yet done, but parameters are not included.
|
||||
*
|
||||
* @param string $val
|
||||
*/
|
||||
abstract public function setRawMimeDirValue($val);
|
||||
|
||||
/**
|
||||
* Returns a raw mime-dir representation of the value.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getRawMimeDirValue();
|
||||
|
||||
/**
|
||||
* Turns the object back into a serialized blob.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize()
|
||||
{
|
||||
$str = $this->name;
|
||||
if ($this->group) {
|
||||
$str = $this->group.'.'.$this->name;
|
||||
}
|
||||
|
||||
foreach ($this->parameters() as $param) {
|
||||
$str .= ';'.$param->serialize();
|
||||
}
|
||||
|
||||
$str .= ':'.$this->getRawMimeDirValue();
|
||||
|
||||
$str = \preg_replace(
|
||||
'/(
|
||||
(?:^.)? # 1 additional byte in first line because of missing single space (see next line)
|
||||
.{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
|
||||
(?![\x80-\xbf]) # prevent splitting multibyte characters
|
||||
)/x',
|
||||
"$1\r\n ",
|
||||
$str
|
||||
);
|
||||
|
||||
// remove single space after last CRLF
|
||||
return \substr($str, 0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value, in the format it should be encoded for JSON.
|
||||
*
|
||||
* This method must always return an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getJsonValue()
|
||||
{
|
||||
return $this->getParts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JSON value, as it would appear in a jCard or jCal object.
|
||||
*
|
||||
* The value must always be an array.
|
||||
*/
|
||||
public function setJsonValue(array $value)
|
||||
{
|
||||
if (1 === count($value)) {
|
||||
$this->setValue(reset($value));
|
||||
} else {
|
||||
$this->setValue($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns an array, with the representation as it should be
|
||||
* encoded in JSON. This is used to create jCard or jCal documents.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($this->parameters as $parameter) {
|
||||
if ('VALUE' === $parameter->name) {
|
||||
continue;
|
||||
}
|
||||
$parameters[strtolower($parameter->name)] = $parameter->jsonSerialize();
|
||||
}
|
||||
// In jCard, we need to encode the property-group as a separate 'group'
|
||||
// parameter.
|
||||
if ($this->group) {
|
||||
$parameters['group'] = $this->group;
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
[
|
||||
strtolower($this->name),
|
||||
(object) $parameters,
|
||||
strtolower($this->getValueType()),
|
||||
],
|
||||
$this->getJsonValue()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate data from a XML subtree, as it would appear in a xCard or xCal
|
||||
* object.
|
||||
*/
|
||||
public function setXmlValue(array $value)
|
||||
{
|
||||
$this->setJsonValue($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes the data into XML. This is used to create xCard or
|
||||
* xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
public function xmlSerialize(Xml\Writer $writer): void
|
||||
{
|
||||
$parameters = [];
|
||||
|
||||
foreach ($this->parameters as $parameter) {
|
||||
if ('VALUE' === $parameter->name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameters[] = $parameter;
|
||||
}
|
||||
|
||||
$writer->startElement(strtolower($this->name));
|
||||
|
||||
if (!empty($parameters)) {
|
||||
$writer->startElement('parameters');
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$writer->startElement(strtolower($parameter->name));
|
||||
$writer->write($parameter);
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
$this->xmlSerializeValue($writer);
|
||||
$writer->endElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method serializes only the value of a property. This is used to
|
||||
* create xCard or xCal documents.
|
||||
*
|
||||
* @param Xml\Writer $writer XML writer
|
||||
*/
|
||||
protected function xmlSerializeValue(Xml\Writer $writer)
|
||||
{
|
||||
$valueType = strtolower($this->getValueType());
|
||||
|
||||
foreach ($this->getJsonValue() as $values) {
|
||||
foreach ((array) $values as $value) {
|
||||
$writer->writeElement($valueType, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this object is being cast to a string.
|
||||
*
|
||||
* If the property only had a single value, you will get just that. In the
|
||||
* case the property had multiple values, the contents will be escaped and
|
||||
* combined with ,.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return (string) $this->getValue();
|
||||
}
|
||||
|
||||
/* ArrayAccess interface {{{ */
|
||||
|
||||
/**
|
||||
* Checks if an array element exists.
|
||||
*
|
||||
* @param mixed $name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetExists($name)
|
||||
{
|
||||
if (is_int($name)) {
|
||||
return parent::offsetExists($name);
|
||||
}
|
||||
|
||||
$name = strtoupper($name);
|
||||
|
||||
foreach ($this->parameters as $parameter) {
|
||||
if ($parameter->name == $name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parameter.
|
||||
*
|
||||
* If the parameter does not exist, null is returned.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Node
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetGet($name)
|
||||
{
|
||||
if (is_int($name)) {
|
||||
return parent::offsetGet($name);
|
||||
}
|
||||
$name = strtoupper($name);
|
||||
|
||||
if (!isset($this->parameters[$name])) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->parameters[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new parameter.
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetSet($name, $value)
|
||||
{
|
||||
if (is_int($name)) {
|
||||
parent::offsetSet($name, $value);
|
||||
// @codeCoverageIgnoreStart
|
||||
// This will never be reached, because an exception is always
|
||||
// thrown.
|
||||
return;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
$param = new Parameter($this->root, $name, $value);
|
||||
$this->parameters[$param->name] = $param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes one or more parameters with the specified name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function offsetUnset($name)
|
||||
{
|
||||
if (is_int($name)) {
|
||||
parent::offsetUnset($name);
|
||||
// @codeCoverageIgnoreStart
|
||||
// This will never be reached, because an exception is always
|
||||
// thrown.
|
||||
return;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
unset($this->parameters[strtoupper($name)]);
|
||||
}
|
||||
|
||||
/* }}} */
|
||||
|
||||
/**
|
||||
* This method is automatically called when the object is cloned.
|
||||
* Specifically, this will ensure all child elements are also cloned.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->parameters as $key => $child) {
|
||||
$this->parameters[$key] = clone $child;
|
||||
$this->parameters[$key]->parent = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the node for correctness.
|
||||
*
|
||||
* The following options are supported:
|
||||
* - Node::REPAIR - If something is broken, and automatic repair may
|
||||
* be attempted.
|
||||
*
|
||||
* An array is returned with warnings.
|
||||
*
|
||||
* Every item in the array has the following properties:
|
||||
* * level - (number between 1 and 3 with severity information)
|
||||
* * message - (human readable message)
|
||||
* * node - (reference to the offending node)
|
||||
*
|
||||
* @param int $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function validate($options = 0)
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
// Checking if our value is UTF-8
|
||||
if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
|
||||
$oldValue = $this->getRawMimeDirValue();
|
||||
$level = 3;
|
||||
if ($options & self::REPAIR) {
|
||||
$newValue = StringUtil::convertToUTF8($oldValue);
|
||||
if (true || StringUtil::isUTF8($newValue)) {
|
||||
$this->setRawMimeDirValue($newValue);
|
||||
$level = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) {
|
||||
$message = 'Property contained a control character (0x'.bin2hex($matches[1]).')';
|
||||
} else {
|
||||
$message = 'Property is not valid UTF-8! '.$oldValue;
|
||||
}
|
||||
|
||||
$warnings[] = [
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
|
||||
// Checking if the propertyname does not contain any invalid bytes.
|
||||
if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
|
||||
$warnings[] = [
|
||||
'level' => $options & self::REPAIR ? 1 : 3,
|
||||
'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed',
|
||||
'node' => $this,
|
||||
];
|
||||
if ($options & self::REPAIR) {
|
||||
// Uppercasing and converting underscores to dashes.
|
||||
$this->name = strtoupper(
|
||||
str_replace('_', '-', $this->name)
|
||||
);
|
||||
// Removing every other invalid character
|
||||
$this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
|
||||
}
|
||||
}
|
||||
|
||||
if ($encoding = $this->offsetGet('ENCODING')) {
|
||||
if (Document::VCARD40 === $this->root->getDocumentType()) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'ENCODING parameter is not valid in vCard 4.',
|
||||
'node' => $this,
|
||||
];
|
||||
} else {
|
||||
$encoding = (string) $encoding;
|
||||
|
||||
$allowedEncoding = [];
|
||||
|
||||
switch ($this->root->getDocumentType()) {
|
||||
case Document::ICALENDAR20:
|
||||
$allowedEncoding = ['8BIT', 'BASE64'];
|
||||
break;
|
||||
case Document::VCARD21:
|
||||
$allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT'];
|
||||
break;
|
||||
case Document::VCARD30:
|
||||
$allowedEncoding = ['B'];
|
||||
//Repair vCard30 that use BASE64 encoding
|
||||
if ($options & self::REPAIR) {
|
||||
if ('BASE64' === strtoupper($encoding)) {
|
||||
$encoding = 'B';
|
||||
$this['ENCODING'] = $encoding;
|
||||
$warnings[] = [
|
||||
'level' => 1,
|
||||
'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) {
|
||||
$warnings[] = [
|
||||
'level' => 3,
|
||||
'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.',
|
||||
'node' => $this,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validating inner parameters
|
||||
foreach ($this->parameters as $param) {
|
||||
$warnings = array_merge($warnings, $param->validate($options));
|
||||
}
|
||||
|
||||
return $warnings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method on a document if you're done using it.
|
||||
*
|
||||
* It's intended to remove all circular references, so PHP can easily clean
|
||||
* it up.
|
||||
*/
|
||||
public function destroy()
|
||||
{
|
||||
parent::destroy();
|
||||
foreach ($this->parameters as $param) {
|
||||
$param->destroy();
|
||||
}
|
||||
$this->parameters = [];
|
||||
}
|
||||
}
|
||||
109
vendor/sabre/vobject/lib/Property/Binary.php
vendored
Normal file
109
vendor/sabre/vobject/lib/Property/Binary.php
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Sabre\VObject\Property;
|
||||
|
||||
use Sabre\VObject\Property;
|
||||
|
||||
/**
|
||||
* BINARY property.
|
||||
*
|
||||
* This object represents BINARY values.
|
||||
*
|
||||
* Binary values are most commonly used by the iCalendar ATTACH property, and
|
||||
* the vCard PHOTO property.
|
||||
*
|
||||
* This property will transparently encode and decode to base64.
|
||||
*
|
||||
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
||||
* @author Evert Pot (http://evertpot.com/)
|
||||
* @license http://sabre.io/license/ Modified BSD License
|
||||
*/
|
||||
class Binary extends Property
|
||||
{
|
||||
/**
|
||||
* In case this is a multi-value property. This string will be used as a
|
||||
* delimiter.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $delimiter = '';
|
||||
|
||||
/**
|
||||
* Updates the current value.
|
||||
*
|
||||
* This may be either a single, or multiple strings in an array.
|
||||
*
|
||||
* @param string|array $value
|
||||
*/
|
||||
public function setValue($value)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
if (1 === count($value)) {
|
||||
$this->value = $value[0];
|
||||
} else {
|
||||
throw new \InvalidArgumentException('The argument must either be a string or an array with only one child');
|
||||
}
|
||||
} else {
|
||||
$this->value = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a raw value coming from a mimedir (iCalendar/vCard) file.
|
||||
*
|
||||
* This has been 'unfolded', so only 1 line will be passed. Unescaping is
|
||||
* not yet done, but parameters are not included.
|
||||
*
|
||||
* @param string $val
|
||||
*/
|
||||
public function setRawMimeDirValue($val)
|
||||
{
|
||||
$this->value = base64_decode($val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a raw mime-dir representation of the value.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRawMimeDirValue()
|
||||
{
|
||||
return base64_encode($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of value.
|
||||
*
|
||||
* This corresponds to the VALUE= parameter. Every property also has a
|
||||
* 'default' valueType.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValueType()
|
||||
{
|
||||
return 'BINARY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value, in the format it should be encoded for json.
|
||||
*
|
||||
* This method must always return an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getJsonValue()
|
||||
{
|
||||
return [base64_encode($this->getValue())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the json value, as it would appear in a jCard or jCal object.
|
||||
*
|
||||
* The value must always be an array.
|
||||
*/
|
||||
public function setJsonValue(array $value)
|
||||
{
|
||||
$value = array_map('base64_decode', $value);
|
||||
parent::setJsonValue($value);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user