Skip to content

Commit

Permalink
Merge pull request u01jmg3#291 from noec764/Feature_Modification_KeyV…
Browse files Browse the repository at this point in the history
…alueFromString

Modify key value from string
  • Loading branch information
u01jmg3 authored Jan 12, 2022
2 parents 4e15a99 + 92d433e commit d1fdfa2
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 74 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ need to be evaluated before non-fitting events can be dropped.
| `isValidIanaTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a valid IANA time zone |
| `isValidWindowsTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is a recognised Windows (non-CLDR) time zone |
| `isValidTimeZoneId` | `$timeZone` | `protected` | Checks if a time zone is valid (IANA, CLDR, or Windows) |
| `keyValueFromString` | `$text` | `protected` | Gets the key value pair from an iCal string |
| `keyValueFromString` | `$text` | `public` | Gets the key value pair from an iCal string |
| `parseLine` | `$line` | `protected` | Parses a line from an iCal file into an array of tokens |
| `mb_chr` | `$code` | `protected` | Provides a polyfill for PHP 7.2's `mb_chr()`, which is a multibyte safe version of `chr()` |
| `mb_str_replace` | `$search`, `$replace`, `$subject`, `$count = 0` | `protected` | Replaces all occurrences of a search string with a given replacement string |
| `escapeParamText` | `$candidateText` | `protected` | Places double-quotes around texts that have characters not permitted in parameter-texts, but are permitted in quoted-texts. |
Expand Down
160 changes: 87 additions & 73 deletions src/ICal/ICal.php
Original file line number Diff line number Diff line change
Expand Up @@ -982,95 +982,109 @@ protected function addCalendarComponentWithKeyAndValue($component, $keyword, $va
* @param string $text
* @return array|boolean
*/
protected function keyValueFromString($text)
public function keyValueFromString($text)
{
$text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');

$colon = strpos($text, ':');
$quote = strpos($text, '"');
if ($colon === false) {
$matches = array();
} elseif ($quote === false || $colon < $quote) {
list($before, $after) = explode(':', $text, 2);
$matches = array($text, $before, $after);
} else {
list($before, $text) = explode('"', $text, 2);
$text = '"' . $text;
$matches = str_getcsv($text, ':');
$combinedValue = '';

foreach (array_keys($matches) as $key) {
if ($key === 0) {
if (!empty($before)) {
$matches[$key] = $before . '"' . $matches[$key] . '"';
}
$splitLine = $this->parseLine($text);
$object = array();
$paramObj = array();
$valueObj = '';
$i = 0;

while ($i < count($splitLine)) {
// The first token corresponds to the property name
if ($i == 0) {
$object[0] = $splitLine[$i];
$i++;
continue;
}

// After each semicolon define the property parameters
if ($splitLine[$i] == ';') {
$i++;
$paramName = $splitLine[$i];
$i += 2;
$paramValue = array();
$multiValue = false;
// A parameter can have multiple values separated by a comma
while ($i + 1 < count($splitLine) && $splitLine[$i + 1] == ',') {
$paramValue[] = $splitLine[$i];
$i += 2;
$multiValue = true;
}

if ($multiValue) {
$paramValue[] = $splitLine[$i];
} else {
if ($key > 1) {
$combinedValue .= ':';
}
$paramValue = $splitLine[$i];
}

// Create object with paramName => paramValue
$paramObj[$paramName] = $paramValue;
}

$combinedValue .= $matches[$key];
// After a colon all tokens are concatenated (non-standard behaviour because the property can have multiple values
// according to RFC5545)
if ($splitLine[$i] == ':') {
$i++;
while ($i < count($splitLine)) {
$valueObj .= $splitLine[$i];
$i++;
}
}

$matches = array_slice($matches, 0, 2);
$matches[1] = $combinedValue;
array_unshift($matches, $before . $text);
$i++;
}

if ($matches === []) {
return false;
// Object construction
if (count($paramObj) > 0) {
$object[1][0] = $valueObj;
$object[1][1] = $paramObj;
} else {
$object[1] = $valueObj;
}

if (preg_match('/^([A-Z-]+)([;][\w\W]*)?$/', $matches[1])) {
$matches = array_splice($matches, 1, 2); // Remove first match and re-align ordering

// Process properties
if (preg_match('/([A-Z-]+)[;]([\w\W]*)/', $matches[0], $properties)) {
// Remove first match
array_shift($properties);
// Fix to ignore everything in keyword after a ; (e.g. Language, TZID, etc.)
$matches[0] = $properties[0];
array_shift($properties); // Repeat removing first match

$formatted = array();
foreach ($properties as $property) {
// Match semicolon separator outside of quoted substrings
preg_match_all('~[^' . PHP_EOL . '";]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '";]*)*~', $property, $attributes);
// Remove multi-dimensional array and use the first key
$attributes = (count($attributes) === 0) ? array($property) : reset($attributes);

if (is_array($attributes)) {
foreach ($attributes as $attribute) {
// Match equals sign separator outside of quoted substrings
preg_match_all(
'~[^' . PHP_EOL . '"=]+(?:"[^"\\\]*(?:\\\.[^"\\\]*)*"[^' . PHP_EOL . '"=]*)*~',
$attribute,
$values
);
// Remove multi-dimensional array and use the first key
$value = (count($values) === 0) ? null : reset($values);
return $object ?: false;
}

if (is_array($value) && isset($value[1])) {
// Remove double quotes from beginning and end only
$formatted[$value[0]] = trim($value[1], '"');
}
}
}
/**
* Parses a line from an iCal file into an array of tokens
*
* @param string $line
* @return array
*/
protected function parseLine($line)
{
$words = array();
$word = '';
// The use of str_split is not a problem here even if the character set is in utf8
// Indeed we only compare the characters , ; : = " which are on a single byte
$arrayOfChar = str_split($line);
$inDoubleQuotes = false;

foreach ($arrayOfChar as $char) {
// Don't stop the word on ; , : = if it is enclosed in double quotes
if ($char === '"') {
if ($word !== '') {
$words[] = $word;
}

// Assign the keyword property information
$properties[0] = $formatted;
$word = '';
$inDoubleQuotes = !$inDoubleQuotes;
} elseif (!in_array($char, array(';', ':', ',', '=')) || $inDoubleQuotes) {
$word .= $char;
} else {
if ($word !== '') {
$words[] = $word;
}

// Add match to beginning of array
array_unshift($properties, $matches[1]);
$matches[1] = $properties;
$words[] = $char;
$word = '';
}

return $matches;
} else {
return false; // Ignore this match
}

$words[] = $word;

return $words;
}

/**
Expand Down
83 changes: 83 additions & 0 deletions tests/KeyValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

use ICal\ICal;
use PHPUnit\Framework\TestCase;

class KeyValueTest extends TestCase
{
public function testBoundaryCharactersInsideQuotes()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:[email protected]',
1 => array(
'PARTSTAT' => 'TENTATIVE',
'CN' => 'ju: @ag.com = Ju ; ',
),
),
);

$this->assertLines(
'ATTENDEE;PARTSTAT=TENTATIVE;CN="ju: @ag.com = Ju ; ":mailto:[email protected]',
$checks
);
}

public function testUtf8Characters()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:juëǯ@ag.com',
1 => array(
'PARTSTAT' => 'TENTATIVE',
'CN' => 'juëǯĻ',
),
),
);

$this->assertLines(
'ATTENDEE;PARTSTAT=TENTATIVE;CN=juëǯĻ:mailto:juëǯ@ag.com',
$checks
);

$checks = array(
0 => 'SUMMARY',
1 => ' I love emojis 😀😁😁 ë, ǯ, Ļ',
);

$this->assertLines(
'SUMMARY: I love emojis 😀😁😁 ë, ǯ, Ļ',
$checks
);
}

public function testParametersOfKeysWithMultipleValues()
{
$checks = array(
0 => 'ATTENDEE',
1 => array(
0 => 'mailto:[email protected]',
1 => array(
'DELEGATED-TO' => array(
0 => 'mailto:[email protected]',
1 => 'mailto:[email protected]',
),
),
),
);

$this->assertLines(
'ATTENDEE;DELEGATED-TO="mailto:[email protected]","mailto:[email protected]":mailto:[email protected]',
$checks
);
}

private function assertLines($lines, array $checks)
{
$ical = new ICal();

self::assertEquals($ical->keyValueFromString($lines), $checks);
}
}

0 comments on commit d1fdfa2

Please sign in to comment.