From 94ca5bb19a6cce0dee8af1d99a85c35eb60b154e Mon Sep 17 00:00:00 2001 From: Sandritsch91 Date: Thu, 26 Oct 2023 10:23:45 +0200 Subject: [PATCH] Improvements, Bugfixes, Tests (#2) * - Improvements - Bugfixes - Tests * removed debug code * Added php 8.2 to tests --- .github/workflows/build.yml | 2 +- .gitignore | 2 + composer.json | 3 +- phpunit.xml.dist | 15 + src/ActiveRecord.php | 8 +- src/Client.php | 73 +++++ src/Command.php | 89 ++++-- src/Connection.php | 1 + src/Exception.php | 1 - src/QueryBuilder.php | 125 +++++++-- src/conditions/HashConditionBuilder.php | 4 +- src/models/Attachment.php | 48 ++-- src/models/CalendarEvent.php | 32 ++- src/models/Contact.php | 4 +- src/models/Message.php | 2 +- src/models/Task.php | 194 +++++++++++++ .../transformers/ExchangeTransformer.php | 264 ++++++++++++++++++ tests/AttachmentTest.php | 107 +++++++ tests/AttendeeTest.php | 49 ++++ tests/CalendarEventTest.php | 105 ++++++- tests/ContactTest.php | 81 ++++++ tests/EwsToRuleTest.php | 221 +++++++++++++++ tests/FolderTest.php | 84 ++++++ tests/MessageTest.php | 80 ++++++ tests/RuleToEwsTest.php | 148 ++++++++++ tests/TaskTest.php | 232 +++++++++++++++ 26 files changed, 1877 insertions(+), 97 deletions(-) create mode 100644 src/models/Task.php create mode 100644 src/recurrence/transformers/ExchangeTransformer.php create mode 100644 tests/AttachmentTest.php create mode 100644 tests/AttendeeTest.php create mode 100644 tests/ContactTest.php create mode 100644 tests/EwsToRuleTest.php create mode 100644 tests/FolderTest.php create mode 100644 tests/RuleToEwsTest.php create mode 100644 tests/TaskTest.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1df9ab6..bf65411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - php: [ '7.3', '7.4', '8.0', '8.1' ] + php: [ '7.3', '7.4', '8.0', '8.1', '8.2'] steps: - name: Checkout diff --git a/.gitignore b/.gitignore index 2411a82..1926d06 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ composer.lock # phpunit itself is not needed phpunit.phar +.phpunit.result.cache +tests/log diff --git a/composer.json b/composer.json index 2906a86..d052f3b 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,8 @@ "ext-soap": "*", "yiisoft/yii2": "^2.0.20", "php-ews/php-ews": "^1.0.0", - "simialbi/yii2-simialbi-base": ">=0.13.1 <1.0 | ^1.0.0" + "simialbi/yii2-simialbi-base": ">=0.13.1 <1.0 | ^1.0.0", + "simshaun/recurr": "^5.0.0" }, "require-dev": { "yiisoft/yii2-coding-standards": "~2.0", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5311924..7ac2524 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,4 +11,19 @@ ./tests + + + + + + + ./src/conditions + ./src/models + ./src/recurrence/transformers + + /path/to/files + ./tests/TestCase.php + + + diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php index b1f0f54..3dd03c0 100644 --- a/src/ActiveRecord.php +++ b/src/ActiveRecord.php @@ -57,7 +57,7 @@ public static function primaryKey(): array /** * {@inheritDoc} - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException */ public static function find(): ActiveQuery { @@ -67,7 +67,7 @@ public static function find(): ActiveQuery /** * {@inheritDoc} * @return \simialbi\yii2\ews\Connection - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException */ public static function getDb(): Connection { @@ -77,7 +77,7 @@ public static function getDb(): Connection /** * {@inheritDoc} * @return boolean - * @throws \yii\base\NotSupportedException|Exception|InvalidConfigException + * @throws \yii\base\NotSupportedException|Exception|InvalidConfigException|\ReflectionException */ public static function updateAll($attributes, $condition = null): bool { @@ -207,7 +207,7 @@ public function insert($runValidation = true, $attributes = null, array $params * * @param string|null $value The date time string * @return string|null The formatted date time string - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException */ public function typeCastDateTime(?string $value): ?string { diff --git a/src/Client.php b/src/Client.php index 24921ae..4b2268f 100644 --- a/src/Client.php +++ b/src/Client.php @@ -9,6 +9,7 @@ use jamesiarmes\PhpEws\ClassMap; use jamesiarmes\PhpNtlm\SoapClient; use Yii; +use yii\base\InvalidConfigException; class Client extends \jamesiarmes\PhpEws\Client { @@ -17,6 +18,22 @@ class Client extends \jamesiarmes\PhpEws\Client */ protected string $location; + /** + * {@inheritDoc} + * @param string $timezone The default timezone to set (Exchange format). Defaults to `W. Europe standard time`. + * @throws InvalidConfigException + */ + public function __construct($server = null, $username = null, $password = null, $version = self::VERSION_2013, ?string $timezone = null) + { + parent::__construct($server, $username, $password, $version); + + if (null === $timezone) { + $this->autoSetTimezone(); + } else { + $this->setTimezone($timezone); + } + } + /** * Sets the location property * @@ -50,4 +67,60 @@ protected function initializeSoapClient(): SoapClient return $this->soap; } + + /** + * Try to set exchange timezone from app configuration + * + * @return void + * @throws InvalidConfigException + */ + private function autoSetTimezone(): void + { + $list = timezone_abbreviations_list(); + $offset = 0; + $timezoneName = Yii::$app->formatter->asDate('now', 'VV'); + + foreach ($list as $items) { + foreach ($items as $item) { + if ($item['dst']) { + continue 2; + } + if ($item['timezone_id'] === $timezoneName) { + $offset = $item['offset']; + break 2; + } + } + } + + $this->setTimezone(match ($offset) { + -39600 => 'UTC-11', + -36000 => 'Hawaiian Standard Time', + -28800 => 'Pacific Standard Time', + -25200 => 'Mountain Standard Time', + -21600 => 'Central America Standard Time', + -18000 => 'Eastern Standard Time', + -16200 => 'Venezuela Standard Time', + -14400 => 'SA Western Standard Time', + -10800 => 'Pacific SA Standard Time', + -7200 => 'UTC-02', + -3600 => 'Cape Verde Standard Time', + default => 'UTC', + 3600 => 'W. Europe Standard Time', + 7200 => 'FLE Standard Time', + 10800 => 'Arab Standard Time', + 12600 => 'Iran Standard Time', + 14400 => 'Caucasus Standard Time', + 19800 => 'India Standard Time', + 20700 => 'Nepal Standard Time', + 21600 => 'Bangladesh Standard Time', + 23400 => 'Myanmar Standard Time', + 25200 => 'SE Asia Standard Time', + 28800 => 'Singapore Standard Time', + 32400 => 'Tokyo Standard Time', + 36000 => 'AUS Eastern Standard Time', + 39600 => 'Central Pacific Standard Time', + 43200 => 'UTC+12', + 46800 => 'Samoa Standard Time' + }); + } } diff --git a/src/Command.php b/src/Command.php index cac81d8..c016e71 100644 --- a/src/Command.php +++ b/src/Command.php @@ -230,9 +230,13 @@ public function execute(): \jamesiarmes\PhpEws\Type\ItemIdType|bool switch ($method) { case 'CreateItem': case 'UpdateItem': - if (isset($message->Items->Message)) { - $return = $message->Items->Message[0]->ItemId; - } else { + foreach (['Message', 'CalendarItem', 'Task', 'Item'] as $itemType) { + if (isset($message->Items->{$itemType}[0])) { + $return = $message->Items->{$itemType}[0]->ItemId; + break; + } + } + if (!$return) { foreach ($message->Items as $item) { /** @var \jamesiarmes\PhpEws\Type\ItemType[] $item */ $return = $item[0]->ItemId; @@ -299,35 +303,13 @@ protected function queryInternal(?string $method = null): mixed $message = ArrayHelper::getValue($response, "ResponseMessages.{$method}ResponseMessage"); if (is_array($message)) { - $message = array_shift($message); - } - - if ($message->ResponseCode !== ResponseCodeType::NO_ERROR) { - throw new Exception($message->MessageText, [ - 'responseCode' => $message->ResponseCode, - 'responseClass' => $message->ResponseClass - ]); - } - - $result = match ($method) { - 'FindItem' => ArrayHelper::getValue($message, 'RootFolder.Items'), - 'FindFolder' => ArrayHelper::getValue($message, 'RootFolder.Folders'), - 'GetItem' => ArrayHelper::getValue($message, 'Items'), - default => [], - }; - - // TODO: Find better solution - if (!is_array($result)) { - $r = new \ReflectionClass($result); - foreach ($r->getProperties() as $property) { - if ($property->isPublic() && is_array($result->{$property->name}) && !empty($result->{$property->name})) { - $result = $result->{$property->name}; - break; - } - } - } - if ($result instanceof \jamesiarmes\PhpEws\ArrayType\ArrayOfRealItemsType) { $result = []; + while ($m = array_shift($message)) { + $tmp = $this->getResult($m, $method); + $result = array_merge($result, $tmp); + } + } else { + $result = $this->getResult($message, $method); } } catch (Exception $e) { throw $e; @@ -346,4 +328,49 @@ protected function queryInternal(?string $method = null): mixed return $result; } + + /** + * @param mixed $message + * @param string|null $method + * @return array|mixed + * @throws Exception + * @throws \ReflectionException + * @throws \Exception + */ + protected function getResult(mixed $message, ?string $method): mixed + { + if ($message->ResponseCode !== ResponseCodeType::NO_ERROR) { + if ($message->ResponseCode === 'ErrorItemNotFound') { + return []; + } + throw new Exception($message->MessageText, [ + 'responseCode' => $message->ResponseCode, + 'responseClass' => $message->ResponseClass + ]); + } + + $result = match ($method) { + 'FindItem' => ArrayHelper::getValue($message, 'RootFolder.Items'), + 'FindFolder' => ArrayHelper::getValue($message, 'RootFolder.Folders'), + 'GetItem' => ArrayHelper::getValue($message, 'Items'), + default => [], + }; + + // TODO: Find better solution + if (!is_array($result)) { + $r = new \ReflectionClass($result); + foreach ($r->getProperties() as $property) { + if ($property->isPublic() && is_array($result->{$property->name}) && !empty($result->{$property->name})) { + $result = $result->{$property->name}; + break; + } + } + } + + if ($result instanceof \jamesiarmes\PhpEws\ArrayType\ArrayOfRealItemsType) { + $result = []; + } + + return $result; + } } diff --git a/src/Connection.php b/src/Connection.php index 4758487..95585f6 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -103,6 +103,7 @@ class Connection extends Component /** * Get the [[Client]] instance. * @return Client + * @throws InvalidConfigException */ public function getClient(): Client { diff --git a/src/Exception.php b/src/Exception.php index 19d9af6..3fb1bc7 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -6,7 +6,6 @@ namespace simialbi\yii2\ews; - /** * Exception represents an exception that is caused by some EWS-related operations. * diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index 1b209aa..ba137b4 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -13,7 +13,9 @@ use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfFieldOrdersType; use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfItemChangeDescriptionsType; use jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfItemChangesType; +use jamesiarmes\PhpEws\Enumeration\AffectedTaskOccurrencesType; use jamesiarmes\PhpEws\Enumeration\CalendarItemCreateOrDeleteOperationType; +use jamesiarmes\PhpEws\Enumeration\CalendarItemUpdateOperationType; use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType; use jamesiarmes\PhpEws\Enumeration\DisposalType; use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType; @@ -47,11 +49,16 @@ use jamesiarmes\PhpEws\Type\SetItemFieldType; use jamesiarmes\PhpEws\Type\TargetFolderIdType; use jamesiarmes\PhpEws\Type\TaskType; +use Recurr\Exception\InvalidRRule; +use Recurr\Rule; use simialbi\yii2\ews\models\CalendarEvent; use simialbi\yii2\ews\models\Contact; use simialbi\yii2\ews\models\Folder; use simialbi\yii2\ews\models\Message; +use simialbi\yii2\ews\models\Task; +use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer; use Yii; +use yii\base\InvalidConfigException; use yii\base\NotSupportedException; use yii\db\ExpressionInterface; use yii\helpers\ArrayHelper; @@ -107,7 +114,22 @@ public function build($query, $params = []): array $this->_config = [ 'class' => FindItemType::class ]; - if (isset($query->where['id'], $query->where['changeKey'])) { + if (isset($query->where['id'])) { + if (!is_array($query->where['id'])) { + $query->where['id'] = [$query->where['id']]; + } + if (isset($query->where['changeKey']) && !is_array($query->where['changeKey'])) { + $query->where['changeKey'] = [$query->where['changeKey']]; + } + $ids = []; + + for ($i = 0; $i < count($query->where['id']); $i++) { + $ids[] = Yii::createObject([ + 'class' => ItemIdType::class, + 'Id' => $query->where['id'][$i], + 'ChangeKey' => $query->where['changeKey'][$i] ?? null + ]); + } $this->_config = [ 'class' => GetItemType::class, 'ItemShape' => Yii::createObject([ @@ -116,20 +138,13 @@ public function build($query, $params = []): array ]), 'ItemIds' => Yii::createObject([ 'class' => NonEmptyArrayOfBaseItemIdsType::class, - 'ItemId' => [ - Yii::createObject([ - 'class' => ItemIdType::class, - 'Id' => $query->where['id'], - 'ChangeKey' => $query->where['changeKey'] - ]) - ] + 'ItemId' => $ids ]) ]; } else { if (empty($params['folderId'])) { $params['folderId'] = DistinguishedFolderIdNameType::INBOX; if ($query instanceof ActiveQuery) { - /** @var ActiveQuery $query */ $this->_modelClass = $query->modelClass; switch ($this->_modelClass) { @@ -142,6 +157,9 @@ public function build($query, $params = []): array case Contact::class: $params['folderId'] = DistinguishedFolderIdNameType::CONTACTS; break; + case Task::class: + $params['folderId'] = DistinguishedFolderIdNameType::TODO_SEARCH; + break; } } } @@ -375,6 +393,9 @@ public function update($table, $columns, $condition, &$params): bool|BaseRequest if ($table::modelName() === MessageType::class) { $config['MessageDisposition'] = MessageDispositionType::SAVE_ONLY; } + if ($table::modelName() === CalendarItemType::class) { + $config['SendMeetingInvitationsOrCancellations'] = CalendarItemUpdateOperationType::SEND_ONLY_TO_CHANGED; + } return Yii::createObject($config); } @@ -382,7 +403,7 @@ public function update($table, $columns, $condition, &$params): bool|BaseRequest /** * {@inheritDoc} * - * @param string $table active record class name + * @param string|ActiveRecord $table active record class name * * @return BaseRequestType|object|false * @throws \yii\base\InvalidConfigException @@ -409,6 +430,13 @@ public function delete($table, $condition, &$params): bool|BaseRequestType ]) ]; + if ($table::modelName() === CalendarItemType::class) { + $config['SendMeetingCancellations'] = CalendarItemCreateOrDeleteOperationType::SEND_TO_ALL_AND_SAVE_COPY; + } + if ($table::modelName() === TaskType::class) { + $config['AffectedTaskOccurrences'] = AffectedTaskOccurrencesType::ALL; + } + return Yii::createObject($config); } @@ -525,10 +553,9 @@ public function buildCondition($condition, &$params): object|array */ public function buildExpression(ExpressionInterface $expression, &$params = []): object|array { + /** @var object|ExpressionInterface $expression */ $expression = parent::buildExpression($expression, $params); - /** @var object $expression */ - return $expression; } @@ -561,9 +588,12 @@ public function getUriFromProperty(string $property): ?string case 'subject': return UnindexedFieldURIType::ITEM_SUBJECT; case 'body': + case 'format': return UnindexedFieldURIType::ITEM_BODY; case 'type': return UnindexedFieldURIType::CALENDAR_ITEM_TYPE; + case 'recurrence': + return UnindexedFieldURIType::CALENDAR_RECURRENCE; case 'isRecurring': return UnindexedFieldURIType::CALENDAR_IS_RECURRING; case 'isAllDay': @@ -578,6 +608,8 @@ public function getUriFromProperty(string $property): ?string return UnindexedFieldURIType::ITEM_LAST_MODIFIED_TIME; case 'createdAt': return UnindexedFieldURIType::ITEM_DATE_TIME_CREATED; + case 'attachments': + return UnindexedFieldURIType::ITEM_ATTACHMENTS; } break; case Message::class: @@ -604,6 +636,8 @@ public function getUriFromProperty(string $property): ?string return UnindexedFieldURIType::ITEM_DATE_TIME_CREATED; case 'updatedAt': return UnindexedFieldURIType::ITEM_LAST_MODIFIED_TIME; + case 'attachments': + return UnindexedFieldURIType::ITEM_ATTACHMENTS; } break; case Folder::class: @@ -627,7 +661,7 @@ public function getUriFromProperty(string $property): ?string /** * {@inheritDoc} - * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException + * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException|InvalidRRule */ protected function prepareInsertValues($table, $columns, $params = []): array { @@ -694,36 +728,73 @@ protected function prepareUpdateSets($table, $columns, $params = []): array try { $value = $this->castDataType($mapping[$name]['dataType'], $value, false, $params); - } catch (NotSupportedException $e) { + } catch (NotSupportedException|InvalidRRule) { continue; } $val = [ 'class' => $table::modelName() ]; - if ($mapping[$name]['foreignModel']) { + if (!empty($mapping[$name]['foreignModel'])) { $tmp = explode('.', $mapping[$name]['foreignField']); $field = array_shift($tmp); - $val[$field] = Yii::createObject(ArrayHelper::merge( + $val[$field] = !is_object($value) ? Yii::createObject(ArrayHelper::merge( ['class' => $mapping[$name]['foreignModel']], [$tmp[0] => $value] - )); + )) : $value; } else { $val[$mapping[$name]['foreignField']] = $value; } - $changes[] = Yii::createObject([ + $changes[$uri] = $this->merge($changes[$uri] ?? [], $val); + } + + return array_map(function (string $uri, array $item) use ($property): SetItemFieldType { + /** @var SetItemFieldType $obj */ + $obj = Yii::createObject([ 'class' => SetItemFieldType::class, 'FieldURI' => Yii::createObject([ 'class' => PathToUnindexedFieldType::class, 'FieldURI' => $uri ]), - $property => Yii::createObject($val) + $property => Yii::createObject($item) ]); + + return $obj; + }, array_keys($changes), array_values($changes)); + } + + /** + * Merge two objects or arrays recursively + * @param array|object $a + * @param array|object $b + * @return array|object + * @throws InvalidConfigException + */ + protected function merge(array|object $a, array|object $b): array|object + { + foreach ($b as $k => $v) { + if (is_object($a)) { + if (is_array($v)) { + $a->$k = ArrayHelper::merge($a->$k, $b->$k); + } elseif (is_object($v)) { + $a->$k = $this->merge($a->$k, $v); + } elseif (!empty($b->$k)) { + $a->$k = $b->$k; + } + } else { + if (is_array($v)) { + $a[$k] = ArrayHelper::merge($a[$k], $v); + } elseif (is_object($v)) { + $a[$k] = $this->merge($a[$k] ?? Yii::createObject(['class' => $v::class]), $v); + } elseif (!empty($v)) { + $a[$k] = $v; + } + } } - return $changes; + return $a; } /** @@ -732,13 +803,15 @@ protected function prepareUpdateSets($table, $columns, $params = []): array * @param boolean $isInsert * @param array $params * @return array|bool|float|int|ActiveRecord|string - * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException + * @throws NotSupportedException|\yii\base\InvalidConfigException|\ReflectionException|InvalidRRule */ protected function castDataType(array $dataType, mixed $value, bool $isInsert = true, array $params = []): mixed { if (count($dataType) > 1) { if (false !== in_array('\\DateTime', $dataType)) { $dataType = 'DateTime'; + } elseif (false !== in_array('\\Recurr\\Rule', $dataType)) { + $dataType = 'Recurrence'; } else { $dataType = 'string'; } @@ -776,6 +849,16 @@ protected function castDataType(array $dataType, mixed $value, bool $isInsert = case 'DateTime': $value = Yii::$app->formatter->asDatetime($value, 'yyyy-MM-dd\'T\'HH:mm:ssxxx'); break; + case 'Recurrence': + if (empty($value)) { + break; + } + $t = new ExchangeTransformer(); + if (is_string($value)) { + $value = Rule::createFromString($value); + } + $value = $t->transformRecurrenceToEws($value); + break; default: if (!$isInsert) { throw new NotSupportedException(); diff --git a/src/conditions/HashConditionBuilder.php b/src/conditions/HashConditionBuilder.php index 885de56..1f6d197 100644 --- a/src/conditions/HashConditionBuilder.php +++ b/src/conditions/HashConditionBuilder.php @@ -25,10 +25,10 @@ class HashConditionBuilder extends \yii\db\conditions\HashConditionBuilder /** * {@inheritDoc} - * @return array + * @return array|object * @throws \yii\base\InvalidConfigException */ - public function build(ExpressionInterface $expression, array &$params = []): array + public function build(ExpressionInterface $expression, array &$params = []): array|object { /** @var \yii\db\conditions\HashCondition $expression */ $hash = $expression->getHash(); diff --git a/src/models/Attachment.php b/src/models/Attachment.php index 6c374d9..f8e7e10 100644 --- a/src/models/Attachment.php +++ b/src/models/Attachment.php @@ -7,7 +7,7 @@ use jamesiarmes\PhpEws\Type\AttachmentType; use jamesiarmes\PhpEws\Type\RequestAttachmentIdType; use simialbi\yii2\ews\ActiveRecord; -use yii\helpers\ArrayHelper; +use yii\base\InvalidConfigException; /** * @property string $id => \jamesiarmes\PhpEws\Type\AttachmentIdType:AttachmentId.Id @@ -16,19 +16,14 @@ * @property string $contentId => ContentId * @property string $location => ContentLocation * @property string $mime => ContentType - * @property bool $isInline => IsInline + * @property boolean $isInline => IsInline * @property string $name => Name - * @property int $size => Size - * - * @property-read $content + * @property integer $size => Size + * @property string $content => \jamesiarmes\PhpEws\Type\FileAttachmentType:Content + * @property boolean $isContactPhoto => \jamesiarmes\PhpEws\Type\FileAttachmentType:IsContactPhoto */ class Attachment extends ActiveRecord { - /** - * @var string The binary content - */ - private string $_content; - /** * {@inheritDoc} */ @@ -43,31 +38,24 @@ public static function modelName(): string public function rules(): array { return [ - [['id', 'changeKey', 'contentId', 'location', 'mime', 'name'], 'string'], - [['isInline'], 'boolean'], - ['size', 'int'], + [['id', 'rootId', 'rootChangeKey', 'contentId', 'location', 'mime', 'name', 'content'], 'string'], + [['isInline', 'isContactPhoto'], 'boolean'], + ['size', 'integer'], - ['isInline', 'default', 'value' => false] + [['isInline', 'isContactPhoto'], 'default', 'value' => false] ]; } /** * {@inheritDoc} - */ - public function fields(): array - { - return ArrayHelper::merge(parent::fields(), ['content' => 'content']); - } - - /** - * Load binary content - * @return string - * @throws \yii\base\InvalidConfigException + * @throws InvalidConfigException * @todo Find better solution */ - public function getContent(): string + public function afterFind(): void { - if (!isset($this->_content)) { + parent::afterFind(); + + if (empty($this->content)) { $request = new GetAttachmentType(); $request->AttachmentIds = new NonEmptyArrayOfRequestAttachmentIdsType(); @@ -78,9 +66,11 @@ public function getContent(): string $response = self::getDb()->getClient()->GetAttachment($request); $message = $response->ResponseMessages->GetAttachmentResponseMessage[0]; - $this->_content = $message->Attachments->FileAttachment[0]->Content; - } + $this->content = $message->Attachments->FileAttachment[0]->Content; + $this->isContactPhoto = $message->Attachments->FileAttachment[0]->IsContactPhoto; - return $this->_content; + $this->setOldAttribute('content', $this->content); + $this->setOldAttribute('isContactPhoto', $this->isContactPhoto); + } } } diff --git a/src/models/CalendarEvent.php b/src/models/CalendarEvent.php index cd92f34..137a4dd 100644 --- a/src/models/CalendarEvent.php +++ b/src/models/CalendarEvent.php @@ -10,8 +10,10 @@ use jamesiarmes\PhpEws\Enumeration\CalendarItemTypeType; use jamesiarmes\PhpEws\Enumeration\LegacyFreeBusyType; use jamesiarmes\PhpEws\Type\CalendarItemType; +use jamesiarmes\PhpEws\Type\RecurrenceType; +use Recurr\Rule; use simialbi\yii2\ews\ActiveRecord; -use Yii; +use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer; use yii\behaviors\AttributeTypecastBehavior; /** @@ -29,13 +31,15 @@ * @property string $format => \jamesiarmes\PhpEws\Type\BodyType:Body.BodyType * @property string $location => Location * @property string $type => CalendarItemType - * @property boolean $isRecurring => IsRecurring + * @property string|\Recurr\Rule $recurrence => \jamesiarmes\PhpEws\Type\RecurrenceType:Recurrence + * @property-read boolean $isRecurring => IsRecurring * @property boolean $isAllDay => IsAllDayEvent * @property boolean $isCancelled => IsCancelled * @property boolean $isOnline => IsOnlineMeeting * @property string $status => LegacyFreeBusyStatus * @property-read string|\DateTime|integer $createdAt => DateTimeCreated * @property-read string|\DateTime|integer $updatedAt => LastModifiedTime + * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment * * @property Contact $organizer => \jamesiarmes\PhpEws\Type\SingleRecipientType:Organizer * @property Attendee[] $requiredAttendees => \jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType:RequiredAttendees.Attendee @@ -85,11 +89,25 @@ public function rules(): array ] ], + ['recurrence', 'safe'], + ['status', 'default', 'value' => LegacyFreeBusyType::BUSY], ['format', 'default', 'value' => BodyTypeType::HTML] ]; } + /** + * {@inheritDoc} + */ + public function beforeSave($insert): bool + { + if ($this->isAttributeChanged('body', false)) { + $this->markAttributeDirty('format'); + } + + return parent::beforeSave($insert); + } + /** * {@inheritDoc} */ @@ -106,7 +124,15 @@ public function behaviors(): array 'start' => [$this, 'typeCastDateTime'], 'end' => [$this, 'typeCastDateTime'], 'createdAt' => [$this, 'typeCastDateTime'], - 'updatedAt' => [$this, 'typeCastDateTime'] + 'updatedAt' => [$this, 'typeCastDateTime'], + 'recurrence' => function (string|RecurrenceType|null|array $value): ?Rule { + if (empty($value)) { + return null; + } + + $transformer = new ExchangeTransformer(); + return $transformer->transformRecurrenceFromEws($value); + } ] ] ]; diff --git a/src/models/Contact.php b/src/models/Contact.php index db39a3a..3c5612b 100644 --- a/src/models/Contact.php +++ b/src/models/Contact.php @@ -19,8 +19,8 @@ * @property string $changeKey => \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.ChangeKey * @property string $parentFolderId => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.Id * @property string $parentFolderChangeKey => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.ChangeKey - * @property string $email => Mailbox.EmailAddress - * @property string $name => Mailbox.Name + * @property string $email => \jamesiarmes\PhpEws\Type\EmailAddressType:Mailbox.EmailAddress + * @property string $name => \jamesiarmes\PhpEws\Type\EmailAddressType:Mailbox.Name */ class Contact extends ActiveRecord { diff --git a/src/models/Message.php b/src/models/Message.php index 7eed0e1..c0c65c7 100644 --- a/src/models/Message.php +++ b/src/models/Message.php @@ -31,7 +31,7 @@ * @property-read string|\DateTime|integer $createdAt => DateTimeCreated * @property-read string|\DateTime|integer $updatedAt => LastModifiedTime * - * @property Attachment[] $attachments => \jamesiarmes\PhpEws\Type\ArrayOfAttachmentsType:Attachments.FileAttachment + * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment * @property Contact $from => \jamesiarmes\PhpEws\Type\SingleRecipientType:From * @property Contact $sender => \jamesiarmes\PhpEws\Type\SingleRecipientType:Sender * @property Contact[] $to => \jamesiarmes\PhpEws\Type\ArrayOfRecipientsType:ToRecipients.Mailbox diff --git a/src/models/Task.php b/src/models/Task.php new file mode 100644 index 0000000..1694cd3 --- /dev/null +++ b/src/models/Task.php @@ -0,0 +1,194 @@ + \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.Id + * @property string $changeKey => \jamesiarmes\PhpEws\Type\ItemIdType:ItemId.ChangeKey + * @property string $parentFolderId => \jamesiarmes\PhpEws\Type\FolderIdType:ParentFolderId.Id + * @property integer $actualWork => ActualWork + * @property string $assignedTime => AssignedTime + * @property string $billingInformation => BillingInformation + * @property string $body => \jamesiarmes\PhpEws\Type\BodyType:Body._ + * @property integer $changeCount => ChangeCount + * @property string|\DateTime|integer $completeDate => CompleteDate + * @property-read string $delegationState => DelegationState + * @property string|\DateTime|integer $dueDate => DueDate + * @property string $format => \jamesiarmes\PhpEws\Type\BodyType:Body.BodyType + * @property string $importance => Importance + * @property-read integer $isAssignmentEditable => IsAssignmentEditable + * @property boolean $isComplete => IsComplete + * @property-read boolean $isRecurring => IsRecurring + * @property string $mileage => Mileage + * @property-read string $owner => Owner + * @property float $percentComplete => PercentComplete + * @property string|\Recurr\Rule $recurrence => \jamesiarmes\PhpEws\Type\TaskRecurrenceType:Recurrence + * @property string $sensitivity => Sensitivity + * @property string|\DateTime|integer $startDate => StartDate + * @property string $status => Status + * @property-read string $statusDescription => StatusDescription + * @property string $subject => Subject + * @property integer $totalWork => TotalWork + * @property-read string|\DateTime|integer $createdAt => DateTimeCreated + * @property-read string|\DateTime|integer $updatedAt => LastModifiedTime + * + * @property Attachment[] $attachments => \jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType:Attachments.FileAttachment + */ +class Task extends ActiveRecord +{ + /** + * {@inheritDoc} + */ + public static function modelName(): string + { + return TaskType::class; + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + [ + [ + 'id', + 'assignedTime', + 'billingInformation', + 'body', + 'changeKey', + 'delegationState', + 'format', + 'importance', + 'mileage', + 'owner', + 'parentFolderId', + 'statusDescription', + 'sensitivity', + 'status', + 'subject' + ], + 'string' + ], + + [['actualWork', 'changeCount', 'isAssignmentEditable', 'totalWork'], 'integer'], + + [['percentComplete'], 'double'], + + [['startDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'startDate'], + [['dueDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'dueDate'], + [['completeDate'], 'date', 'format' => 'yyyy-MM-dd HH:mm xxx', 'timestampAttribute' => 'completeDate'], + + [['isComplete', 'isRecurring'], 'boolean'], + + [['subject', 'format'], 'required'], + + ['format', 'in', 'range' => [ + BodyTypeType::HTML, + BodyTypeType::TEXT + ]], + [ + 'delegationState', + 'in', + 'range' => [ + TaskDelegateStateType::ACCEPTED, + TaskDelegateStateType::DECLINED, + TaskDelegateStateType::MAX, + TaskDelegateStateType::NO_MATCH, + TaskDelegateStateType::OWN_NEW, + TaskDelegateStateType::OWNED + ] + ], + [ + 'status', + 'in', + 'range' => [ + TaskStatusType::COMPLETED, + TaskStatusType::DEFERRED, + TaskStatusType::IN_PROGRESS, + TaskStatusType::NOT_STARTED, + TaskStatusType::WAITING_ON_OTHERS + ] + ], + [ + 'importance', + 'in', + 'range' => [ + ImportanceChoicesType::HIGH, + ImportanceChoicesType::LOW, + ImportanceChoicesType::NORMAL + ] + ], + [ + 'sensitivity', + 'in', + 'range' => [ + SensitivityChoicesType::CONFIDENTIAL, + SensitivityChoicesType::NORMAL, + SensitivityChoicesType::PERSONAL, + SensitivityChoicesType::PRIVATE_ITEM + ] + ], + + ['recurrence', 'safe'], + + ['format', 'default', 'value' => BodyTypeType::HTML] + ]; + } + + /** + * {@inheritDoc} + */ + public function beforeSave($insert): bool + { + if ($this->isAttributeChanged('body')) { + $this->markAttributeDirty('format'); + } + + return parent::beforeSave($insert); + } + + /** + * {@inheritDoc} + */ + public function behaviors(): array + { + return [ + 'typecast' => [ + 'class' => AttributeTypecastBehavior::class, + 'typecastAfterSave' => false, + 'typecastAfterValidate' => false, + 'typecastBeforeSave' => false, + 'typecastAfterFind' => true, + 'attributeTypes' => [ + 'assignedTime' => [$this, 'typeCastDateTime'], + 'completeDate' => [$this, 'typeCastDateTime'], + 'dueDate' => [$this, 'typeCastDateTime'], + 'startDate' => [$this, 'typeCastDateTime'], + 'createdAt' => [$this, 'typeCastDateTime'], + 'updatedAt' => [$this, 'typeCastDateTime'], + 'recurrence' => function (string|RecurrenceType|null|array $value): ?Rule { + if (empty($value)) { + return null; + } + + $transformer = new ExchangeTransformer(); + return $transformer->transformRecurrenceFromEws($value); + } + ] + ] + ]; + } +} diff --git a/src/recurrence/transformers/ExchangeTransformer.php b/src/recurrence/transformers/ExchangeTransformer.php new file mode 100644 index 0000000..920c749 --- /dev/null +++ b/src/recurrence/transformers/ExchangeTransformer.php @@ -0,0 +1,264 @@ +getInterval(); + $count = $rule->getCount(); + $until = $rule->getUntil(); + $startDate = $rule->getStartDate() ?? 'today'; + $endDate = $rule->getEndDate(); + + switch ($rule->getFreq()) { + case Frequency::YEARLY: + $byMonthDay = $rule->getByMonthDay(); + $byMonth = $this->partAsString($rule->getByMonth()); + if (!empty($byMonthDay)) { + $byMonthDay = $this->partAsString($byMonthDay); + $recurrence->AbsoluteYearlyRecurrence = new AbsoluteYearlyRecurrencePatternType(); + $recurrence->AbsoluteYearlyRecurrence->Month = $byMonth; + $recurrence->AbsoluteYearlyRecurrence->DayOfMonth = $byMonthDay; + } else { + $byDay = $this->partAsString($rule->getByDay()); + $byDayInt = (int)preg_replace('#[^\-+\d]#', '', $byDay); + $byDayString = preg_replace('#[\-+\d]#', '', $byDay); + $recurrence->RelativeYearlyRecurrence = new RelativeYearlyRecurrencePatternType(); + $recurrence->RelativeYearlyRecurrence->Month = $byMonth; + $recurrence->RelativeYearlyRecurrence->DayOfWeekIndex = $this->weekDayIndexToString($byDayInt); + $recurrence->RelativeYearlyRecurrence->DaysOfWeek = $this->abbrToWeekDay($byDayString); + } + break; + case Frequency::MONTHLY: + $byMonthDay = $rule->getByMonthDay(); + if (!empty($byMonthDay)) { + $byMonthDay = $this->partAsString($byMonthDay); + $recurrence->AbsoluteMonthlyRecurrence = new AbsoluteMonthlyRecurrencePatternType(); + $recurrence->AbsoluteMonthlyRecurrence->DayOfMonth = $byMonthDay; + $recurrence->AbsoluteMonthlyRecurrence->Interval = $interval; + } else { + $byDay = $this->partAsString($rule->getByDay()); + $byDayInt = (int)preg_replace('#[^\-\d]#', '', $byDay); + $byDayString = preg_replace('#[\-\d]#', '', $byDay); + $recurrence->RelativeMonthlyRecurrence = new RelativeMonthlyRecurrencePatternType(); + $recurrence->RelativeMonthlyRecurrence->DayOfWeekIndex = $this->weekDayIndexToString($byDayInt); + $recurrence->RelativeMonthlyRecurrence->DaysOfWeek = $this->abbrToWeekDay($byDayString); + $recurrence->RelativeMonthlyRecurrence->Interval = $interval; + } + break; + case Frequency::WEEKLY: + $byDay = array_map(function (string $item) { + return $this->abbrToWeekDay($item); + }, (array)$rule->getByDay()); + $recurrence->WeeklyRecurrence = new WeeklyRecurrencePatternType(); + $recurrence->WeeklyRecurrence->DaysOfWeek = implode(' ', $byDay); + $recurrence->WeeklyRecurrence->Interval = $interval; + break; + case Frequency::DAILY: + $recurrence->DailyRecurrence = new DailyRecurrencePatternType(); + $recurrence->DailyRecurrence->Interval = $interval; + break; + default: + return null; + } + + if ($count) { + $recurrence->NumberedRecurrence = new NumberedRecurrenceRangeType(); + $recurrence->NumberedRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx'); + $recurrence->NumberedRecurrence->NumberOfOccurrences = $count; + } elseif ($endDate || $until) { + $recurrence->EndDateRecurrence = new EndDateRecurrenceRangeType(); + $recurrence->EndDateRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx'); + $recurrence->EndDateRecurrence->EndDate = Yii::$app->formatter->asDatetime($endDate ?? $until, 'yyyy-MM-dd\'T\'HH:mm:ssxxx'); + } else { + $recurrence->NoEndRecurrence = new NoEndRecurrenceRangeType(); + $recurrence->NoEndRecurrence->StartDate = Yii::$app->formatter->asDatetime($startDate, 'yyyy-MM-dd\'T\'HH:mm:ssxxx'); + } + + return $recurrence; + } + + /** + * Transform an EWS RecurrencyType array to a recurrence rule + * @param array $recurrence + * @return Rule + * @throws \Exception + */ + public function transformRecurrenceFromEws(array $recurrence): Rule + { + $rule = new Rule(); + + // Start and end dates + if (isset($recurrence['NoEndRecurrence'])) { + $startDate = $recurrence['NoEndRecurrence']['StartDate']; + } elseif (isset($recurrence['EndDateRecurrence'])) { + $startDate = $recurrence['EndDateRecurrence']['StartDate']; + $rule->setUntil(new \DateTime($recurrence['EndDateRecurrence']['EndDate'])); + } elseif (isset($recurrence['NumberedRecurrence'])) { + $startDate = $recurrence['NumberedRecurrence']['StartDate']; + $rule->setCount($recurrence['NumberedRecurrence']['NumberOfOccurrences']); + } else { + throw new InvalidConfigException('No end date or count given'); + } + $rule->setStartDate(new \DateTime($startDate)); + + + // Intervals + + //daily + if (isset($recurrence['DailyRecurrence'])) { + $rule->setFreq(Frequency::DAILY); + $rule->setInterval($recurrence['DailyRecurrence']['Interval']); + } + + // weekly + if (isset($recurrence['WeeklyRecurrence'])) { + $rule->setFreq(Frequency::WEEKLY); + $rule->setInterval($recurrence['WeeklyRecurrence']['Interval']); + $days = explode(' ', $recurrence['WeeklyRecurrence']['DaysOfWeek']); + $d = []; + foreach ($days as $day) { + $d[] = $this->weekDayToAbbr($day); + } + $rule->setByDay($d); + } + + // absolute monthly + if (isset($recurrence['AbsoluteMonthlyRecurrence'])) { + $rule->setFreq(Frequency::MONTHLY); + $rule->setInterval($recurrence['AbsoluteMonthlyRecurrence']['Interval']); + $day = $recurrence['AbsoluteMonthlyRecurrence']['DayOfMonth']; + $rule->setByMonthDay([$day]); + } + + // relative monthly + if (isset($recurrence['RelativeMonthlyRecurrence'])) { + $rule->setFreq(Frequency::MONTHLY); + $rule->setInterval($recurrence['RelativeMonthlyRecurrence']['Interval']); + $day = $this->weekDayToAbbr($recurrence['RelativeMonthlyRecurrence']['DaysOfWeek']); + $week = $this->stringToWeekDayIndex($recurrence['RelativeMonthlyRecurrence']['DayOfWeekIndex']); + $rule->setByDay([$week . $day]); + } + + // absolute yearly + if (isset($recurrence['AbsoluteYearlyRecurrence'])) { + $rule->setFreq(Frequency::YEARLY); + $rule->setInterval(1); + $rule->setByMonth([date_parse($recurrence['AbsoluteYearlyRecurrence']['Month'])['month']]); + $rule->setByMonthDay([$recurrence['AbsoluteYearlyRecurrence']['DayOfMonth']]); + } + + // relative yearly + if (isset($recurrence['RelativeYearlyRecurrence'])) { + $rule->setFreq(Frequency::YEARLY); + $rule->setInterval(1); + $rule->setByMonth([date_parse($recurrence['RelativeYearlyRecurrence']['Month'])['month']]); + $day = $this->weekDayToAbbr($recurrence['RelativeYearlyRecurrence']['DaysOfWeek']); + $week = $this->stringToWeekDayIndex($recurrence['RelativeYearlyRecurrence']['DayOfWeekIndex']); + $rule->setByDay([$week . $day]); + } + + return $rule; + } + + /** + * @param $string string The string to convert + * @return int + */ + private function stringToWeekDayIndex(string $string): int + { + return match ($string) { + DayOfWeekIndexType::SECOND => 2, + DayOfWeekIndexType::THIRD => 3, + DayOfWeekIndexType::FOURTH => 4, + DayOfWeekIndexType::LAST => -1, + default => 1 + }; + } + + /** + * @param int $index The index to convert + * @return string + */ + private function weekDayIndexToString(int $index): string + { + + return match ($index) { + 2 => DayOfWeekIndexType::SECOND, + 3 => DayOfWeekIndexType::THIRD, + 4 => DayOfWeekIndexType::FOURTH, + -1 => DayOfWeekIndexType::LAST, + default => DayOfWeekIndexType::FIRST + }; + } + + /** + * @param $weekday + * @return string + */ + private function weekDayToAbbr($weekday): string + { + return match ($weekday) { + DayOfWeekType::TUESDAY => 'TU', + DayOfWeekType::WEDNESDAY => 'WE', + DayOfWeekType::THURSDAY => 'TH', + DayOfWeekType::FRIDAY => 'FR', + DayOfWeekType::SATURDAY => 'SA', + DayOfWeekType::SUNDAY => 'SU', + default => 'MO' + }; + } + + /** + * @param $abbr + * @return string + */ + private function abbrToWeekDay($abbr): string + { + return match ($abbr) { + 'TU' => DayOfWeekType::TUESDAY, + 'WE' => DayOfWeekType::WEDNESDAY, + 'TH' => DayOfWeekType::THURSDAY, + 'FR' => DayOfWeekType::FRIDAY, + 'SA' => DayOfWeekType::SATURDAY, + 'SU' => DayOfWeekType::SUNDAY, + default => DayOfWeekType::MONDAY + }; + } + + private function partAsString(array|string $part): string + { + if (is_array($part) && !ArrayHelper::isAssociative($part) && isset($part[0])) { + return $part[0]; + } + + return (string)$part; + } +} diff --git a/tests/AttachmentTest.php b/tests/AttachmentTest.php new file mode 100644 index 0000000..2ba84d4 --- /dev/null +++ b/tests/AttachmentTest.php @@ -0,0 +1,107 @@ + [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType', + 'foreignField' => 'AttachmentId.Id' + ], + 'rootId' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType', + 'foreignField' => 'AttachmentId.RootItemId' + ], + 'rootChangeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\AttachmentIdType', + 'foreignField' => 'AttachmentId.RootItemChangeKey' + ], + 'contentId' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'ContentId' + ], + 'location' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'ContentLocation' + ], + 'mime' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'ContentType' + ], + 'isInline' => [ + 'readOnly' => false, + 'dataType' => ['boolean'], + 'foreignModel' => null, + 'foreignField' => 'IsInline' + ], + 'name' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Name' + ], + 'size' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'Size' + ], + 'content' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FileAttachmentType', + 'foreignField' => 'Content' + ], + 'isContactPhoto' => [ + 'readOnly' => false, + 'dataType' => ['boolean'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FileAttachmentType', + 'foreignField' => 'IsContactPhoto' + ], + ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + + foreach ($expectedSubset as $key => $value) { + $this->assertArrayHasKey($key, $attributeMapping); + $this->assertSame($value, $attributeMapping[$key]); + } + } + + public function testModel() + { + $file = b'This is a text file'; + $attachment = new Attachment([ + 'name' => 'test', + 'content' => $file, + 'mime' => 'text/plain', + 'isInline' => false, + 'size' => strlen($file) + ]); + + $this->assertTrue($attachment->validate()); + } +} diff --git a/tests/AttendeeTest.php b/tests/AttendeeTest.php new file mode 100644 index 0000000..865b4a7 --- /dev/null +++ b/tests/AttendeeTest.php @@ -0,0 +1,49 @@ + [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType', + 'foreignField' => 'Mailbox.Name' + ], + 'email' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType', + 'foreignField' => 'Mailbox.EmailAddress' + ], + ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + + foreach ($expectedSubset as $key => $value) { + $this->assertArrayHasKey($key, $attributeMapping); + $this->assertSame($value, $attributeMapping[$key]); + } + } + + public function testModel() + { + $attendee = new Attendee([ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com' + ]); + $this->assertTrue(true, $attendee->validate()); + $this->assertEquals('jamesiarmes\PhpEws\Type\AttendeeType', Attendee::modelName()); + } +} diff --git a/tests/CalendarEventTest.php b/tests/CalendarEventTest.php index 896c7bd..822cb3d 100644 --- a/tests/CalendarEventTest.php +++ b/tests/CalendarEventTest.php @@ -6,119 +6,182 @@ namespace yiiunit\extensions\ews; +use jamesiarmes\PhpEws\Enumeration\BodyTypeType; use jamesiarmes\PhpEws\Enumeration\DefaultShapeNamesType; use jamesiarmes\PhpEws\Enumeration\DisposalType; use jamesiarmes\PhpEws\Enumeration\DistinguishedFolderIdNameType; use jamesiarmes\PhpEws\Enumeration\LegacyFreeBusyType; use jamesiarmes\PhpEws\Enumeration\SortDirectionType; use jamesiarmes\PhpEws\Enumeration\UnindexedFieldURIType; +use simialbi\yii2\ews\models\Attachment; use simialbi\yii2\ews\models\Attendee; use simialbi\yii2\ews\models\CalendarEvent; use Yii; +use yii\base\InvalidConfigException; +use yii\base\NotSupportedException; class CalendarEventTest extends TestCase { + /** + * @throws \ReflectionException + */ public function testAttributeMapping() { $attributeMapping = CalendarEvent::attributeMapping(); $expectedSubset = [ 'id' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', 'foreignField' => 'ItemId.Id' ], 'changeKey' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', 'foreignField' => 'ItemId.ChangeKey' ], 'parentFolderId' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', 'foreignField' => 'ParentFolderId.Id' ], 'parentFolderChangeKey' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', 'foreignField' => 'ParentFolderId.ChangeKey' ], 'start' => [ + 'readOnly' => false, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'Start' ], 'end' => [ + 'readOnly' => false, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'End' ], 'subject' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'Subject' ], 'body' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', 'foreignField' => 'Body._' ], 'format' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', 'foreignField' => 'Body.BodyType' ], 'location' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'Location' ], 'type' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'CalendarItemType' ], 'isRecurring' => [ + 'readOnly' => true, 'dataType' => ['boolean'], 'foreignModel' => null, 'foreignField' => 'IsRecurring' ], 'isAllDay' => [ + 'readOnly' => false, 'dataType' => ['boolean'], 'foreignModel' => null, 'foreignField' => 'IsAllDayEvent' ], 'isCancelled' => [ + 'readOnly' => false, 'dataType' => ['boolean'], 'foreignModel' => null, 'foreignField' => 'IsCancelled' ], 'isOnline' => [ + 'readOnly' => false, 'dataType' => ['boolean'], 'foreignModel' => null, 'foreignField' => 'IsOnlineMeeting' ], 'status' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'LegacyFreeBusyStatus' ], 'createdAt' => [ + 'readOnly' => true, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'DateTimeCreated' ], 'updatedAt' => [ + 'readOnly' => true, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'LastModifiedTime' ], + 'recurrence' => [ + 'readOnly' => false, + 'dataType' => ['string', '\Recurr\Rule'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\RecurrenceType', + 'foreignField' => 'Recurrence' + ], + 'organizer' => [ + 'readOnly' => false, + 'dataType' => ['Contact'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType', + 'foreignField' => 'Organizer' + ], + 'requiredAttendees' => [ + 'readOnly' => false, + 'dataType' => ['Attendee[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType', + 'foreignField' => 'RequiredAttendees.Attendee' + ], + 'optionalAttendees' => [ + 'readOnly' => false, + 'dataType' => ['Attendee[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType', + 'foreignField' => 'OptionalAttendees.Attendee' + ], + 'attachments' => [ + 'readOnly' => false, + 'dataType' => ['Attachment[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType', + 'foreignField' => 'Attachments.FileAttachment' + ] ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + foreach ($expectedSubset as $key => $value) { $this->assertArrayHasKey($key, $attributeMapping); $this->assertSame($value, $attributeMapping[$key]); } } + /** + * @throws InvalidConfigException + */ public function testQueryBuilderFind() { $query = CalendarEvent::find(); @@ -168,6 +231,11 @@ public function testQueryBuilderFind() $this->assertEquals(SortDirectionType::ASCENDING, $request->SortOrder->FieldOrder[0]->Order); } + /** + * @throws NotSupportedException + * @throws InvalidConfigException + * @throws \ReflectionException + */ public function testQueryBuilderInsert() { $startDate = Yii::$app->formatter->asDate('+2 hours', 'yyyy-MM-dd HH:mm xxx'); @@ -176,14 +244,30 @@ public function testQueryBuilderInsert() $event->subject = 'Test'; $event->body = '

This is a test

'; + $event->format = BodyTypeType::HTML; $event->start = $startDate; $event->end = $endDate; $event->requiredAttendees = [ new Attendee(['name' => 'John Doe', 'email' => 'john.doe@example.com']), new Attendee(['name' => 'Jane Doe', 'email' => 'jane.doe@example.com']) ]; + $event->recurrence = 'FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=20;BYMONTH=11'; - $this->assertEquals(true, $event->validate()); + $file = b'This is a text file'; + $attachments = [ + new Attachment([ + 'name' => 'test', + 'content' => $file, + 'mime' => 'text/plain', + 'isInline' => false, + 'size' => strlen($file) + ]) + ]; + $event->attachments = $attachments; + + $event->beforeSave(true); + + $this->assertTrue($event->validate()); $values = $event->getDirtyAttributes(); $params = [ @@ -213,11 +297,27 @@ public function testQueryBuilderInsert() $this->assertEquals('

This is a test

', $calendarItem->Body->_); $this->assertEquals('Test', $calendarItem->Subject); $this->assertInstanceOf('jamesiarmes\PhpEws\ArrayType\NonEmptyArrayOfAttendeesType', $calendarItem->RequiredAttendees); + + // Attendee $attendee = $calendarItem->RequiredAttendees->Attendee[0]; $this->assertInstanceOf('jamesiarmes\PhpEws\Type\AttendeeType', $attendee); $this->assertInstanceOf('jamesiarmes\PhpEws\Type\EmailAddressType', $attendee->Mailbox); $this->assertEquals('john.doe@example.com', $attendee->Mailbox->EmailAddress); $this->assertEquals('John Doe', $attendee->Mailbox->Name); + + // Recurrence + $this->assertInstanceOf('jamesiarmes\PhpEws\Type\RecurrenceType', $calendarItem->Recurrence); + $this->assertIsObject($calendarItem->Recurrence); + $this->assertIsObject($calendarItem->Recurrence->NoEndRecurrence); + + // Attachments + $attachment = $calendarItem->Attachments->FileAttachment[0]; + $this->assertObjectHasProperty('Attachments', $calendarItem); + $this->assertInstanceOf('jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType', $calendarItem->Attachments); + $this->assertObjectHasProperty('FileAttachment', $calendarItem->Attachments); + $this->assertInstanceOf('jamesiarmes\PhpEws\Type\AttachmentType', $attachment); + $this->assertEquals('text/plain', $attachment->ContentType); + $this->assertEquals(19, $attachment->Size); } public function testQueryBuilderUpdate() @@ -228,6 +328,7 @@ public function testQueryBuilderUpdate() $event->subject = 'Test'; $event->body = '

This is a test

'; + $event->format = 'HTML'; $event->start = $startDate; $event->end = $endDate; $event->requiredAttendees = [ @@ -275,6 +376,7 @@ public function testQueryBuilderUpdate() $this->assertEquals(date('c', strtotime($endDate)), $updates->SetItemField[3]->CalendarItem->End); $this->assertEquals(UnindexedFieldURIType::CALENDAR_LEGACY_FREE_BUSY_STATUS, $updates->SetItemField[4]->FieldURI->FieldURI); $this->assertEquals(LegacyFreeBusyType::BUSY, $updates->SetItemField[4]->CalendarItem->LegacyFreeBusyStatus); + $this->assertObjectHasProperty('SendMeetingInvitationsOrCancellations', $request); } public function testQueryBuilderDelete() @@ -293,5 +395,6 @@ public function testQueryBuilderDelete() $this->assertInstanceOf('jamesiarmes\PhpEws\Type\ItemIdType', $request->ItemIds->ItemId[0]); $this->assertEquals('7007ACC7-3202-11D1-AAD2-00805FC1270E', $request->ItemIds->ItemId[0]->ChangeKey); $this->assertEquals('AAajslgkha32394isdg==', $request->ItemIds->ItemId[0]->Id); + $this->assertObjectHasProperty('SendMeetingCancellations', $request); } } diff --git a/tests/ContactTest.php b/tests/ContactTest.php new file mode 100644 index 0000000..f64af55 --- /dev/null +++ b/tests/ContactTest.php @@ -0,0 +1,81 @@ + [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.Id' + ], + 'changeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.ChangeKey' + ], + 'parentFolderId' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', + 'foreignField' => 'ParentFolderId.Id' + ], + 'parentFolderChangeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', + 'foreignField' => 'ParentFolderId.ChangeKey' + ], + 'name' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType', + 'foreignField' => 'Mailbox.Name' + ], + 'email' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\EmailAddressType', + 'foreignField' => 'Mailbox.EmailAddress' + ], + ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + + foreach ($expectedSubset as $key => $value) { + $this->assertArrayHasKey($key, $attributeMapping); + $this->assertSame($value, $attributeMapping[$key]); + } + } + + public function testModel() + { + $contact = new Contact([ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com' + ]); + $this->assertTrue($contact->validate()); + + $recipient = new SingleRecipientType(); + $recipient->Mailbox = new EmailAddressType(); + $recipient->Mailbox->EmailAddress = 'john.doe@example.com'; + $recipient->Mailbox->Name = 'John Doe'; + $contact = Contact::fromSingleRecipient($recipient); + self::assertTrue($contact->validate()); + } +} diff --git a/tests/EwsToRuleTest.php b/tests/EwsToRuleTest.php new file mode 100644 index 0000000..ec4b4b8 --- /dev/null +++ b/tests/EwsToRuleTest.php @@ -0,0 +1,221 @@ + + */ + +namespace yiiunit\extensions\ews; + +use simialbi\yii2\ews\recurrence\transformers\ExchangeTransformer; + +class EwsToRuleTest extends TestCase +{ + /** + * @throws \Exception + */ + public function testDaily() + { + $data = [ + 'AbsoluteMonthlyRecurrence' => null, + 'AbsoluteYearlyRecurrence' => null, + 'DailyRecurrence' => [ + 'Interval' => 5 + ], + 'EndDateRecurrence' => [ + 'StartDate' => '2025-11-17+01:00', + 'EndDate' => '2026-02-09+01:00' + ], + 'NoEndRecurrence' => null, + 'NumberedRecurrence' => null, + 'RelativeMonthlyRecurrence' => null, + 'RelativeYearlyRecurrence' => null, + 'WeeklyRecurrence' => null, + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null + ]; + + $this->assertEquals( + 'FREQ=DAILY;UNTIL=20260209T000000;INTERVAL=5', + $this->getRule($data)->getString() + ); + } + + /** + * @throws \Exception + */ + public function testeWeekly() + { + $data = [ + 'AbsoluteMonthlyRecurrence' => null, + 'AbsoluteYearlyRecurrence' => null, + 'DailyRecurrence' => null, + 'EndDateRecurrence' => [ + 'StartDate' => '2025-11-17+01:00', + 'EndDate' => '2026-05-04+02:00' + ], + 'NoEndRecurrence' => null, + 'NumberedRecurrence' => null, + 'RelativeMonthlyRecurrence' => null, + 'RelativeYearlyRecurrence' => null, + 'WeeklyRecurrence' => [ + 'Interval' => 2, + 'DaysOfWeek' => 'Monday Saturday', + 'FirstDayOfWeek' => 'Sunday' + ], + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null + ]; + + $this->assertEquals( + 'FREQ=WEEKLY;UNTIL=20260504T000000;INTERVAL=2;BYDAY=MO,SA', + $this->getRule($data)->getString() + ); + } + + /** + * @throws \Exception + */ + public function testAbsoluteMonthly() + { + $data = [ + 'AbsoluteMonthlyRecurrence' => [ + 'Interval' => 2, + 'DayOfMonth' => 17 + ], + 'AbsoluteYearlyRecurrence' => null, + 'DailyRecurrence' => null, + 'EndDateRecurrence' => null, + 'NoEndRecurrence' => null, + 'NumberedRecurrence' => [ + 'StartDate' => '2025-11-17+01:00', + 'NumberOfOccurrences' => 7 + ], + 'RelativeMonthlyRecurrence' => null, + 'RelativeYearlyRecurrence' => null, + 'WeeklyRecurrence' => null, + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null + ]; + + $this->assertEquals( + 'FREQ=MONTHLY;COUNT=7;INTERVAL=2;BYMONTHDAY=17', + $this->getRule($data)->getString() + ); + } + + /** + * @throws \Exception + */ + public function testRelativeMonthly() + { + $data = [ + 'AbsoluteMonthlyRecurrence' => null, + 'AbsoluteYearlyRecurrence' => null, + 'DailyRecurrence' => null, + 'EndDateRecurrence' => null, + 'NoEndRecurrence' => [ + 'StartDate' => '2025-11-17+01:00' + ], + 'NumberedRecurrence' => null, + 'RelativeMonthlyRecurrence' => [ + 'Interval' => 2, + 'DayOfWeekIndex' => 'Third', + 'DaysOfWeek' => 'Thursday' + ], + 'RelativeYearlyRecurrence' => null, + 'WeeklyRecurrence' => null, + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null + ]; + + $this->assertEquals( + 'FREQ=MONTHLY;INTERVAL=2;BYDAY=3TH', + $this->getRule($data)->getString() + ); + } + + /** + * @throws \Exception + */ + public function testAbsoluteYearly(): void + { + $data = [ + 'AbsoluteMonthlyRecurrence' => null, + 'AbsoluteYearlyRecurrence' => [ + 'DayOfMonth' => 20, + 'Month' => 'November' + ], + 'DailyRecurrence' => null, + 'EndDateRecurrence' => null, + 'NoEndRecurrence' => [ + 'StartDate' => '2023-11-20+01:00' + ], + 'NumberedRecurrence' => null, + 'RelativeMonthlyRecurrence' => null, + 'RelativeYearlyRecurrence' => null, + 'WeeklyRecurrence' => null, + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null, + ]; + + $this->assertEquals( + 'FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=20;BYMONTH=11', + $this->getRule($data)->getString() + ); + } + + /** + * @throws \Exception + */ + public function testRelativeYearly(): void + { + $data = [ + 'AbsoluteMonthlyRecurrence' => null, + 'AbsoluteYearlyRecurrence' => null, + 'DailyRecurrence' => null, + 'EndDateRecurrence' => [ + 'StartDate' => '2025-11-17+01:00', + 'EndDate' => '2032-11-30+01:00' + ], + 'NoEndRecurrence' => null, + 'NumberedRecurrence' => null, + 'RelativeMonthlyRecurrence' => null, + 'RelativeYearlyRecurrence' => [ + 'DayOfWeekIndex' => 'Third', + 'DaysOfWeek' => 'Monday', + 'Month' => 'November', + ], + 'WeeklyRecurrence' => null, + 'DailyRegeneration' => null, + 'MonthlyRegeneration' => null, + 'WeeklyRegeneration' => null, + 'YearlyRegeneration' => null, + ]; + + $this->assertEquals( + 'FREQ=YEARLY;UNTIL=20321130T000000;INTERVAL=1;BYDAY=3MO;BYMONTH=11', + $this->getRule($data)->getString() + ); + } + + /** + * @param array $data + * @return \Recurr\Rule + * @throws \Exception + */ + protected function getRule(array $data): \Recurr\Rule + { + $transformer = new ExchangeTransformer(); + return $transformer->transformRecurrenceFromEws($data); + } +} diff --git a/tests/FolderTest.php b/tests/FolderTest.php new file mode 100644 index 0000000..c339ac2 --- /dev/null +++ b/tests/FolderTest.php @@ -0,0 +1,84 @@ + [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.Id' + ], + 'changeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.ChangeKey' + ], + 'parentFolderId' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', + 'foreignField' => 'ParentFolderId.Id' + ], + 'parentFolderChangeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', + 'foreignField' => 'ParentFolderId.ChangeKey' + ], + 'name' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'DisplayName' + ], + 'unreadCount' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'UnreadCount' + ], + 'totalCount' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'TotalCount' + ], + 'childrenCount' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'ChildFolderCount' + ], + ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + + foreach ($expectedSubset as $key => $value) { + $this->assertArrayHasKey($key, $attributeMapping); + $this->assertSame($value, $attributeMapping[$key]); + } + } + + public function testModel() + { + $folder = new Folder([ + 'name' => 'Test Folder', + 'totalCount' => 3 + ]); + $this->assertTrue($folder->validate()); + } +} diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 260a182..0db4037 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -6,83 +6,163 @@ namespace yiiunit\extensions\ews; +use jamesiarmes\PhpEws\Enumeration\BodyTypeType; +use jamesiarmes\PhpEws\Enumeration\SensitivityChoicesType; use simialbi\yii2\ews\models\Message; class MessageTest extends TestCase { + /** + * @throws \ReflectionException + */ public function testAttributeMapping() { $attributeMapping = Message::attributeMapping(); $expectedSubset = [ 'id' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', 'foreignField' => 'ItemId.Id' ], 'changeKey' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', 'foreignField' => 'ItemId.ChangeKey' ], 'parentFolderId' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', 'foreignField' => 'ParentFolderId.Id' ], + 'attachments' => [ + 'readOnly' => false, + 'dataType' => ['Attachment[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType', + 'foreignField' => 'Attachments.FileAttachment' + ], 'sensitivity' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'Sensitivity' ], 'importance' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'Importance' ], 'subject' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'Subject' ], 'format' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', 'foreignField' => 'Body.BodyType' ], 'body' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', 'foreignField' => 'Body._' ], 'messageId' => [ + 'readOnly' => false, 'dataType' => ['string'], 'foreignModel' => null, 'foreignField' => 'InternetMessageId' ], 'isRead' => [ + 'readOnly' => false, 'dataType' => ['boolean'], 'foreignModel' => null, 'foreignField' => 'IsRead' ], 'sentAt' => [ + 'readOnly' => true, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'DateTimeSent' ], 'createdAt' => [ + 'readOnly' => true, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'DateTimeCreated' ], 'updatedAt' => [ + 'readOnly' => true, 'dataType' => ['string', '\DateTime', 'integer'], 'foreignModel' => null, 'foreignField' => 'LastModifiedTime' ], + 'from' => [ + 'readOnly' => false, + 'dataType' => ['Contact'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType', + 'foreignField' => 'From' + ], + 'sender' => [ + 'readOnly' => false, + 'dataType' => ['Contact'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\SingleRecipientType', + 'foreignField' => 'Sender' + ], + 'to' => [ + 'readOnly' => false, + 'dataType' => ['Contact[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType', + 'foreignField' => 'ToRecipients.Mailbox' + ], + 'cc' => [ + 'readOnly' => false, + 'dataType' => ['Contact[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType', + 'foreignField' => 'CcRecipients.Mailbox' + ], + 'bcc' => [ + 'readOnly' => false, + 'dataType' => ['Contact[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ArrayOfRecipientsType', + 'foreignField' => 'BccRecipients.Mailbox' + ], ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + foreach ($expectedSubset as $key => $value) { $this->assertArrayHasKey($key, $attributeMapping); $this->assertSame($value, $attributeMapping[$key]); } } + + public function testModel() + { + $message = new Message([ + 'subject' => 'Test subject', + 'body' => 'Test body', + 'format' => BodyTypeType::TEXT, + 'isRead' => false, + 'sensitivity' => SensitivityChoicesType::CONFIDENTIAL, + 'from' => [ + 'name' => 'Jane Doe', + 'email' => 'jane.doe@example.com' + ], + 'to' => [ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com' + ] + ]); + + $this->assertTrue($message->validate()); + } } diff --git a/tests/RuleToEwsTest.php b/tests/RuleToEwsTest.php new file mode 100644 index 0000000..eba97c0 --- /dev/null +++ b/tests/RuleToEwsTest.php @@ -0,0 +1,148 @@ +setFreq(Frequency::DAILY); + $rule->setUntil($endDate); + $rule->setInterval(5); + $rule->setStartDate($startDate); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals(5, $recurrenceType->DailyRecurrence->Interval); + $this->assertEquals($startDate->format('c'), $recurrenceType->EndDateRecurrence->StartDate); + $this->assertEquals($endDate->format('c'), $recurrenceType->EndDateRecurrence->EndDate); + } + + /** + * @throws InvalidArgument + * @throws InvalidRRule + * @throws InvalidConfigException + */ + public function testWeekly() + { + $startDate = new \DateTime(); + $startDate->setTime(0, 0); + + $rule = new Rule(); + $rule->setFreq(Frequency::WEEKLY); + $rule->setCount(3); + $rule->setInterval(2); + $rule->setByDay(['MO', 'SA']); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals(2, $recurrenceType->WeeklyRecurrence->Interval); + $this->assertEquals(3, $recurrenceType->NumberedRecurrence->NumberOfOccurrences); + $this->assertEquals($startDate->format('c'), $recurrenceType->NumberedRecurrence->StartDate); + } + + /** + * @throws InvalidConfigException + * @throws InvalidArgument + */ + public function testAbsoluteMonthly() + { + $rule = new Rule(); + $rule->setFreq(Frequency::MONTHLY); + $rule->setInterval(5); + $rule->setByMonthDay([13]); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals(5, $recurrenceType->AbsoluteMonthlyRecurrence->Interval); + $this->assertEquals(13, $recurrenceType->AbsoluteMonthlyRecurrence->DayOfMonth); + } + + /** + * @throws InvalidArgument + * @throws InvalidRRule + * @throws InvalidConfigException + */ + public function testRelativeMonth() + { + $rule = new Rule(); + $rule->setFreq(Frequency::MONTHLY); + $rule->setInterval(2); + $rule->setByDay(['-1SU']); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals(2, $recurrenceType->RelativeMonthlyRecurrence->Interval); + $this->assertEquals('Last', $recurrenceType->RelativeMonthlyRecurrence->DayOfWeekIndex); + $this->assertEquals('Sunday', $recurrenceType->RelativeMonthlyRecurrence->DaysOfWeek); + } + + /** + * @throws InvalidArgument + * @throws InvalidConfigException + */ + public function testAbsoluteYear() + { + $rule = new Rule(); + $rule->setFreq(Frequency::YEARLY); + $rule->setByMonthDay([12]); + $rule->setByMonth([11]); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals(12, $recurrenceType->AbsoluteYearlyRecurrence->DayOfMonth); + $this->assertEquals(11, $recurrenceType->AbsoluteYearlyRecurrence->Month); + } + + /** + * @return void + * @throws InvalidRRule + * @throws InvalidArgument|InvalidConfigException + */ + public function testRelativeYear() + { + $rule = new Rule(); + $rule->setFreq(Frequency::YEARLY); + $rule->setByMonth([1]); + $rule->setByDay(['+1MO']); + + + $recurrenceType = $this->getRecurrenceType($rule); + + $this->assertEquals('First', $recurrenceType->RelativeYearlyRecurrence->DayOfWeekIndex); + $this->assertEquals('Monday', $recurrenceType->RelativeYearlyRecurrence->DaysOfWeek); + $this->assertEquals(1, $recurrenceType->RelativeYearlyRecurrence->Month); + } + + /** + * @param Rule $rule + * @return RecurrenceType|null + * @throws InvalidConfigException + */ + protected function getRecurrenceType(Rule $rule): ?RecurrenceType + { + $t = new ExchangeTransformer(); + return $t->transformRecurrenceToEws($rule); + } +} diff --git a/tests/TaskTest.php b/tests/TaskTest.php new file mode 100644 index 0000000..6c94bea --- /dev/null +++ b/tests/TaskTest.php @@ -0,0 +1,232 @@ + [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.Id' + ], + 'changeKey' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\ItemIdType', + 'foreignField' => 'ItemId.ChangeKey' + ], + 'parentFolderId' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\FolderIdType', + 'foreignField' => 'ParentFolderId.Id' + ], + 'attachments' => [ + 'readOnly' => false, + 'dataType' => ['Attachment[]'], + 'foreignModel' => '\jamesiarmes\PhpEws\ArrayType\ArrayOfAttachmentsType', + 'foreignField' => 'Attachments.FileAttachment' + ], + 'sensitivity' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Sensitivity' + ], + 'importance' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Importance' + ], + 'subject' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Subject' + ], + 'format' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', + 'foreignField' => 'Body.BodyType' + ], + 'body' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\BodyType', + 'foreignField' => 'Body._' + ], + 'actualWork' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'ActualWork' + ], + 'assignedTime' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'AssignedTime' + ], + 'billingInformation' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'BillingInformation' + ], + 'changeCount' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'ChangeCount' + ], + 'completeDate' => [ + 'readOnly' => false, + 'dataType' => ['string', '\DateTime', 'integer'], + 'foreignModel' => null, + 'foreignField' => 'CompleteDate' + ], + 'delegationState' => [ + 'readOnly' => true, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'DelegationState' + ], + 'dueDate' => [ + 'readOnly' => false, + 'dataType' => ['string', '\DateTime', 'integer'], + 'foreignModel' => null, + 'foreignField' => 'DueDate' + ], + 'isAssignmentEditable' => [ + 'readOnly' => true, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'IsAssignmentEditable' + ], + 'isComplete' => [ + 'readOnly' => false, + 'dataType' => ['boolean'], + 'foreignModel' => null, + 'foreignField' => 'IsComplete' + ], + 'isRecurring' => [ + 'readOnly' => true, + 'dataType' => ['boolean'], + 'foreignModel' => null, + 'foreignField' => 'IsRecurring' + ], + 'mileage' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Mileage' + ], + 'owner' => [ + 'readOnly' => true, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Owner' + ], + 'percentComplete' => [ + 'readOnly' => false, + 'dataType' => ['float'], + 'foreignModel' => null, + 'foreignField' => 'PercentComplete' + ], + 'recurrence' => [ + 'readOnly' => false, + 'dataType' => ['string', '\Recurr\Rule'], + 'foreignModel' => '\jamesiarmes\PhpEws\Type\TaskRecurrenceType', + 'foreignField' => 'Recurrence' + ], + 'startDate' => [ + 'readOnly' => false, + 'dataType' => ['string', '\DateTime', 'integer'], + 'foreignModel' => null, + 'foreignField' => 'StartDate' + ], + 'status' => [ + 'readOnly' => false, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'Status' + ], + 'statusDescription' => [ + 'readOnly' => true, + 'dataType' => ['string'], + 'foreignModel' => null, + 'foreignField' => 'StatusDescription' + ], + 'totalWork' => [ + 'readOnly' => false, + 'dataType' => ['integer'], + 'foreignModel' => null, + 'foreignField' => 'TotalWork' + ], + 'createdAt' => [ + 'readOnly' => true, + 'dataType' => ['string', '\DateTime', 'integer'], + 'foreignModel' => null, + 'foreignField' => 'DateTimeCreated' + ], + 'updatedAt' => [ + 'readOnly' => true, + 'dataType' => ['string', '\DateTime', 'integer'], + 'foreignModel' => null, + 'foreignField' => 'LastModifiedTime' + ], + ]; + + foreach ($attributeMapping as $key => $value) { + $this->assertArrayHasKey($key, $expectedSubset); + } + + foreach ($expectedSubset as $key => $value) { + $this->assertArrayHasKey($key, $attributeMapping); + $this->assertSame($value, $attributeMapping[$key]); + } + } + + public function testModel() + { + $startDate = \Yii::$app->formatter->asDate('+2 hours', 'yyyy-MM-dd HH:mm xxx'); + $endDate = \Yii::$app->formatter->asDate('+2.5 hours', 'yyyy-MM-dd HH:mm xxx'); + + $task = new Task([ + 'body' => 'Test', + 'subject' => 'Test', + 'startDate' => $startDate, + 'dueDate' => $endDate, + 'percentComplete' => 0, + 'format' => BodyTypeType::HTML, + 'status' => TaskStatusType::NOT_STARTED + ]); + $this->assertTrue($task->validate()); + } + + public function testDelete() + { + $event = new Task(); + $params = []; + $request = $event::getDb()->getQueryBuilder()->delete(get_class($event), [ + 'id' => 'AAajslgkha32394isdg==', + 'changeKey' => '7007ACC7-3202-11D1-AAD2-00805FC1270E' + ], $params); + + $this->assertObjectHasProperty('AffectedTaskOccurrences', $request); + } +}