diff --git a/src/Auth/APIToken.php b/src/Auth/APIToken.php new file mode 100644 index 0000000..7ec00cc --- /dev/null +++ b/src/Auth/APIToken.php @@ -0,0 +1,25 @@ +apiToken = $apiToken; + } + + public function getHeaders(): array + { + return [ + 'Authorization' => 'Bearer ' . $this->apiToken + ]; + } +} diff --git a/src/Configurations/FirewallRuleOptions.php b/src/Configurations/FirewallRuleOptions.php new file mode 100644 index 0000000..93f0531 --- /dev/null +++ b/src/Configurations/FirewallRuleOptions.php @@ -0,0 +1,46 @@ + false, + 'action' => 'block' + ]; + + public function getArray(): array + { + return $this->configs; + } + + public function setPaused(bool $paused) + { + $this->configs['paused'] = $paused; + } + + public function setActionBlock() + { + $this->configs['action'] = 'block'; + } + + public function setActionAllow() + { + $this->configs['action'] = 'allow'; + } + + public function setActionChallenge() + { + $this->configs['action'] = 'challenge'; + } + + public function setActionJsChallenge() + { + $this->configs['action'] = 'js_challenge'; + } + + public function setActionLog() + { + $this->configs['action'] = 'log'; + } +} diff --git a/src/Configurations/PageRulesActions.php b/src/Configurations/PageRulesActions.php index 359d3c7..70b4b3a 100755 --- a/src/Configurations/PageRulesActions.php +++ b/src/Configurations/PageRulesActions.php @@ -33,6 +33,13 @@ class PageRulesActions implements Configurations ]); } + public function setOriginCacheControl(bool $active) + { + $this->addConfigurationOption('explicit_cache_control', [ + 'value' => $this->getBoolAsOnOrOff($active) + ]); + } + public function setBrowserIntegrityCheck(bool $active) { $this->addConfigurationOption('browser_check', [ diff --git a/src/Endpoints/Accounts.php b/src/Endpoints/Accounts.php new file mode 100644 index 0000000..4d4d80a --- /dev/null +++ b/src/Endpoints/Accounts.php @@ -0,0 +1,75 @@ +adapter = $adapter; + } + + public function listAccounts( + int $page = 1, + int $perPage = 20, + string $direction = '' + ): \stdClass { + $query = [ + 'page' => $page, + 'per_page' => $perPage + ]; + + if (!empty($direction)) { + $query['direction'] = $direction; + } + + $user = $this->adapter->get('accounts', $query); + $this->body = json_decode($user->getBody()); + + return (object)['result' => $this->body->result, 'result_info' => $this->body->result_info]; + } + + public function getDomains(string $accountID): array + { + $response = $this->adapter->get('accounts/' . $accountID . '/registrar/domains'); + + $this->body = json_decode($response->getBody()); + + return $this->body->result; + } + + public function getDomainDetails(string $accountID, string $domainName): \stdClass + { + $response = $this->adapter->get('accounts/' . $accountID . '/registrar/domains/' . $domainName); + + $this->body = json_decode($response->getBody()); + + return $this->body->result; + } + + public function lockDomain(string $accountID, string $domainName): \stdClass + { + $response = $this->adapter->put('accounts/' . $accountID . '/registrar/domains/' . $domainName, ['locked' => true]); + $this->body = json_decode($response->getBody()); + return $this->body; + } + + public function unlockDomain(string $accountID, string $domainName): \stdClass + { + $response = $this->adapter->put('accounts/' . $accountID . '/registrar/domains/' . $domainName, ['locked' => false]); + $this->body = json_decode($response->getBody()); + return $this->body; + } +} diff --git a/src/Endpoints/DNS.php b/src/Endpoints/DNS.php index 41a2b27..4a23fde 100644 --- a/src/Endpoints/DNS.php +++ b/src/Endpoints/DNS.php @@ -124,8 +124,8 @@ class DNS implements API public function getRecordID(string $zoneID, string $type = '', string $name = ''): string { $records = $this->listRecords($zoneID, $type, $name); - if (isset($records->result{0}->id)) { - return $records->result{0}->id; + if (isset($records->result[0]->id)) { + return $records->result[0]->id; } return false; } diff --git a/src/Endpoints/Firewall.php b/src/Endpoints/Firewall.php new file mode 100644 index 0000000..14ebb76 --- /dev/null +++ b/src/Endpoints/Firewall.php @@ -0,0 +1,120 @@ +adapter = $adapter; + } + + public function createFirewallRules( + string $zoneID, + array $rules + ): bool { + $query = $this->adapter->post('zones/' . $zoneID . '/firewall/rules', $rules); + $body = json_decode($query->getBody()); + + foreach ($body->result as $result) { + if (!isset($result->id)) { + return false; + } + } + + return true; + } + + public function createFirewallRule( + string $zoneID, + string $expression, + FirewallRuleOptions $options, + string $description = null, + int $priority = null + ): bool { + $rule = array_merge([ + 'filter' => [ + 'expression' => $expression, + 'paused' => false + ] + ], $options->getArray()); + + if ($description !== null) { + $rule['description'] = $description; + } + + if ($priority !== null) { + $rule['priority'] = $priority; + } + + return $this->createFirewallRules($zoneID, [$rule]); + } + + public function listFirewallRules( + string $zoneID, + int $page = 1, + int $perPage = 50 + ): \stdClass { + $query = [ + 'page' => $page, + 'per_page' => $perPage, + ]; + + $rules = $this->adapter->get('zones/' . $zoneID . '/firewall/rules', $query); + $body = json_decode($rules->getBody()); + + return (object)['result' => $body->result, 'result_info' => $body->result_info]; + } + + public function deleteFirewallRule( + string $zoneID, + string $ruleID + ): bool { + $rule = $this->adapter->delete('zones/' . $zoneID . '/firewall/rules/' . $ruleID); + + $body = json_decode($rule->getBody()); + + if (isset($body->result->id)) { + return true; + } + + return false; + } + + public function updateFirewallRule( + string $zoneID, + string $ruleID, + string $filterID, + string $expression, + FirewallRuleOptions $options, + string $description = null, + int $priority = null + ): \stdClass { + $rule = array_merge([ + 'id' => $ruleID, + 'filter' => [ + 'id' => $filterID, + 'expression' => $expression, + 'paused' => false + ] + ], $options->getArray()); + + if ($description !== null) { + $rule['description'] = $description; + } + + if ($priority !== null) { + $rule['priority'] = $priority; + } + + $rule = $this->adapter->put('zones/' . $zoneID . '/firewall/rules/' . $ruleID, $rule); + $body = json_decode($rule->getBody()); + + return $body->result; + } +} diff --git a/src/Endpoints/PageRules.php b/src/Endpoints/PageRules.php index c8480f4..c7e5964 100644 --- a/src/Endpoints/PageRules.php +++ b/src/Endpoints/PageRules.php @@ -109,6 +109,37 @@ class PageRules implements API return $this->body->result; } + public function editPageRule( + string $zoneID, + string $ruleID, + PageRulesTargets $target, + PageRulesActions $actions, + bool $active = null, + int $priority = null + ): bool { + $options = []; + $options['targets'] = $target->getArray(); + $options['actions'] = $actions->getArray(); + + if ($active !== null) { + $options['status'] = $active == true ? 'active' : 'disabled'; + } + + if ($priority !== null) { + $options['priority'] = $priority; + } + + $query = $this->adapter->put('zones/' . $zoneID . '/pagerules/' . $ruleID, $options); + + $this->body = json_decode($query->getBody()); + + if (isset($this->body->result->id)) { + return true; + } + + return false; + } + public function updatePageRule( string $zoneID, string $ruleID, diff --git a/src/Endpoints/ZoneSettings.php b/src/Endpoints/ZoneSettings.php index a32b84f..b7baf48 100644 --- a/src/Endpoints/ZoneSettings.php +++ b/src/Endpoints/ZoneSettings.php @@ -9,9 +9,12 @@ namespace Cloudflare\API\Endpoints; use Cloudflare\API\Adapter\Adapter; +use Cloudflare\API\Traits\BodyAccessorTrait; class ZoneSettings implements API { + use BodyAccessorTrait; + private $adapter; public function __construct(Adapter $adapter) @@ -75,6 +78,20 @@ class ZoneSettings implements API return false; } + public function getServerSideExcludeSetting($zoneID) + { + $return = $this->adapter->get( + 'zones/' . $zoneID . '/settings/server_side_exclude' + ); + $body = json_decode($return->getBody()); + + if ($body->success) { + return $body->result->value; + } + + return false; + } + public function getHotlinkProtectionSetting($zoneID) { $return = $this->adapter->get( @@ -177,4 +194,21 @@ class ZoneSettings implements API return false; } + + public function updateServerSideExcludeSetting($zoneID, $value) + { + $return = $this->adapter->patch( + 'zones/' . $zoneID . '/settings/server_side_exclude', + [ + 'value' => $value + ] + ); + $body = json_decode($return->getBody()); + + if ($body->success) { + return $body->result->value; + } + + return false; + } } diff --git a/src/Endpoints/Zones.php b/src/Endpoints/Zones.php index 03773c2..19390b1 100644 --- a/src/Endpoints/Zones.php +++ b/src/Endpoints/Zones.php @@ -58,6 +58,30 @@ class Zones implements API return false; } + public function pause(string $zoneID): bool + { + $user = $this->adapter->patch('zones/' . $zoneID, ['paused' => true]); + $this->body = json_decode($user->getBody()); + + if (isset($this->body->result->id)) { + return true; + } + + return false; + } + + public function unpause(string $zoneID): bool + { + $user = $this->adapter->patch('zones/' . $zoneID, ['paused' => false]); + $this->body = json_decode($user->getBody()); + + if (isset($this->body->result->id)) { + return true; + } + + return false; + } + public function getZoneById( string $zoneId ): \stdClass { diff --git a/tests/Auth/APITokenTest.php b/tests/Auth/APITokenTest.php new file mode 100644 index 0000000..ea7ec3c --- /dev/null +++ b/tests/Auth/APITokenTest.php @@ -0,0 +1,21 @@ +getHeaders(); + + $this->assertArrayHasKey('Authorization', $headers); + + $this->assertEquals('Bearer zKq9RDO6PbCjs6PRUXF3BoqFi3QdwY36C2VfOaRy', $headers['Authorization']); + + $this->assertCount(1, $headers); + } +} diff --git a/tests/Endpoints/AccountsTest.php b/tests/Endpoints/AccountsTest.php new file mode 100644 index 0000000..db2b1be --- /dev/null +++ b/tests/Endpoints/AccountsTest.php @@ -0,0 +1,37 @@ +getPsr7JsonResponseForFixture('Endpoints/listAccounts.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('get')->willReturn($response); + + $mock->expects($this->once()) + ->method('get') + ->with( + $this->equalTo('accounts'), + $this->equalTo([ + 'page' => 1, + 'per_page' => 20, + 'direction' => 'desc', + ]) + ); + + $accounts = new \Cloudflare\API\Endpoints\Accounts($mock); + $result = $accounts->listAccounts(1, 20, 'desc'); + + $this->assertObjectHasAttribute('result', $result); + $this->assertObjectHasAttribute('result_info', $result); + + $this->assertEquals('023e105f4ecef8ad9ca31a8372d0c353', $result->result[0]->id); + $this->assertEquals(1, $result->result_info->page); + $this->assertEquals('023e105f4ecef8ad9ca31a8372d0c353', $accounts->getBody()->result[0]->id); + } +} diff --git a/tests/Endpoints/FirewallTest.php b/tests/Endpoints/FirewallTest.php new file mode 100644 index 0000000..c2b2ed6 --- /dev/null +++ b/tests/Endpoints/FirewallTest.php @@ -0,0 +1,177 @@ +getPsr7JsonResponseForFixture('Endpoints/createFirewallRules.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('post')->willReturn($response); + + $mock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo('zones/023e105f4ecef8ad9ca31a8372d0c353/firewall/rules'), + $this->equalTo([ + [ + 'action' => 'block', + 'description' => 'Foo', + 'filter' => [ + 'expression' => 'http.cookie eq "foo"', + 'paused' => false + ], + ], + [ + 'action' => 'block', + 'description' => 'Bar', + 'filter' => [ + 'expression' => 'http.cookie eq "bar"', + 'paused' => false + ], + ] + ]) + ); + + $firewall = new Cloudflare\API\Endpoints\Firewall($mock); + $result = $firewall->createFirewallRules( + '023e105f4ecef8ad9ca31a8372d0c353', + [ + [ + 'filter' => [ + 'expression' => 'http.cookie eq "foo"', + 'paused' => false + ], + 'action' => 'block', + 'description' => 'Foo' + ], + [ + 'filter' => [ + 'expression' => 'http.cookie eq "bar"', + 'paused' => false + ], + 'action' => 'block', + 'description' => 'Bar' + ], + ] + ); + $this->assertTrue($result); + } + + public function testCreatePageRule() + { + $response = $this->getPsr7JsonResponseForFixture('Endpoints/createFirewallRule.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('post')->willReturn($response); + + $mock->expects($this->once()) + ->method('post') + ->with( + $this->equalTo('zones/023e105f4ecef8ad9ca31a8372d0c353/firewall/rules'), + $this->equalTo([ + [ + 'action' => 'block', + 'description' => 'Foobar', + 'filter' => [ + 'expression' => 'http.cookie eq "foobar"', + 'paused' => false + ], + 'paused' => false + ] + ]) + ); + + $firewall = new Cloudflare\API\Endpoints\Firewall($mock); + $options = new \Cloudflare\API\Configurations\FirewallRuleOptions(); + $options->setActionBlock(); + $result = $firewall->createFirewallRule( + '023e105f4ecef8ad9ca31a8372d0c353', + 'http.cookie eq "foobar"', + $options, + 'Foobar' + ); + $this->assertTrue($result); + } + + public function testListFirewallRules() + { + $response = $this->getPsr7JsonResponseForFixture('Endpoints/listFirewallRules.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('get')->willReturn($response); + + $mock->expects($this->once()) + ->method('get') + ->with( + $this->equalTo('zones/023e105f4ecef8ad9ca31a8372d0c353/firewall/rules'), + $this->equalTo([ + 'page' => 1, + 'per_page' => 50 + ]) + ); + + $firewall = new Cloudflare\API\Endpoints\Firewall($mock); + $result = $firewall->listFirewallRules('023e105f4ecef8ad9ca31a8372d0c353'); + + $this->assertObjectHasAttribute('result', $result); + $this->assertObjectHasAttribute('result_info', $result); + + $this->assertEquals('970b10321e3f4adda674c912b5f76591', $result->result[0]->id); + } + + public function testDeleteFirewallRule() + { + $response = $this->getPsr7JsonResponseForFixture('Endpoints/deleteFirewallRule.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('delete')->willReturn($response); + + $mock->expects($this->once()) + ->method('delete') + ->with( + $this->equalTo('zones/023e105f4ecef8ad9ca31a8372d0c353/firewall/rules/970b10321e3f4adda674c912b5f76591') + ); + + $firewall = new Cloudflare\API\Endpoints\Firewall($mock); + $firewall->deleteFirewallRule('023e105f4ecef8ad9ca31a8372d0c353', '970b10321e3f4adda674c912b5f76591'); + } + + public function testUpdateFirewallRule() + { + $response = $this->getPsr7JsonResponseForFixture('Endpoints/updateFirewallRule.json'); + + $mock = $this->getMockBuilder(\Cloudflare\API\Adapter\Adapter::class)->getMock(); + $mock->method('put')->willReturn($response); + + $mock->expects($this->once()) + ->method('put') + ->with( + $this->equalTo('zones/023e105f4ecef8ad9ca31a8372d0c353/firewall/rules/970b10321e3f4adda674c912b5f76591'), + $this->equalTo([ + 'id' => '970b10321e3f4adda674c912b5f76591', + 'action' => 'block', + 'description' => 'Foo', + 'filter' => [ + 'id' => '5def9c4297e0466cb0736b838345d910', + 'expression' => 'http.cookie eq "foo"', + 'paused' => false + ], + 'paused' => false + ]) + ); + + $firewall = new Cloudflare\API\Endpoints\Firewall($mock); + $options = new \Cloudflare\API\Configurations\FirewallRuleOptions(); + $options->setActionBlock(); + $result = $firewall->updateFirewallRule( + '023e105f4ecef8ad9ca31a8372d0c353', + '970b10321e3f4adda674c912b5f76591', + '5def9c4297e0466cb0736b838345d910', + 'http.cookie eq "foo"', + $options, + 'Foo' + ); + $this->assertEquals('970b10321e3f4adda674c912b5f76591', $result->id); + } +} diff --git a/tests/Endpoints/SSLTest.php b/tests/Endpoints/SSLTest.php index 1f0fc53..c2a1b40 100644 --- a/tests/Endpoints/SSLTest.php +++ b/tests/Endpoints/SSLTest.php @@ -38,7 +38,7 @@ class SSLTest extends TestCase $result = $sslMock->getSSLVerificationStatus('c2547eb745079dac9320b638f5e225cf483cc5cfdda41'); $this->assertObjectHasAttribute('result', $result); - $this->assertEquals('active', $result->result{0}->certificate_status); + $this->assertEquals('active', $result->result[0]->certificate_status); } public function testGetHTTPSRedirectSetting() diff --git a/tests/Endpoints/ZoneSettingsTest.php b/tests/Endpoints/ZoneSettingsTest.php new file mode 100644 index 0000000..05bc2a7 --- /dev/null +++ b/tests/Endpoints/ZoneSettingsTest.php @@ -0,0 +1,39 @@ +getPsr7JsonResponseForFixture('Endpoints/getServerSideExclude.json'); + + $mock = $this->getMockBuilder(Adapter::class)->getMock(); + $mock->method('get')->willReturn($response); + + $mock->expects($this->once())->method('get'); + + $zones = new ZoneSettings($mock); + $result = $zones->getServerSideExcludeSetting('023e105f4ecef8ad9ca31a8372d0c353'); + + $this->assertSame('on', $result); + } + + public function testUpdateServerSideExcludeSetting() + { + $response = $this->getPsr7JsonResponseForFixture('Endpoints/updateServerSideExclude.json'); + + $mock = $this->getMockBuilder(Adapter::class)->getMock(); + $mock->method('patch')->willReturn($response); + + $mock->expects($this->once())->method('patch'); + + $zones = new ZoneSettings($mock); + $result = $zones->updateServerSideExcludeSetting('023e105f4ecef8ad9ca31a8372d0c353', 'on'); + + $this->assertSame('on', $result); + } +} diff --git a/tests/Fixtures/Endpoints/createFirewallRule.json b/tests/Fixtures/Endpoints/createFirewallRule.json new file mode 100644 index 0000000..e00b22c --- /dev/null +++ b/tests/Fixtures/Endpoints/createFirewallRule.json @@ -0,0 +1,20 @@ +{ + "result": [ + { + "id": "970b10321e3f4adda674c912b5f76591", + "paused": false, + "description": "Foobar", + "action": "block", + "filter": { + "id": "70f39827184d487e97cc286b960f4cc3", + "expression": "http.cookie eq \"foobar\"", + "paused": false + }, + "created_on": "2019-07-05T15:53:15Z", + "modified_on": "2019-07-05T15:53:15Z" + } + ], + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/Fixtures/Endpoints/createFirewallRules.json b/tests/Fixtures/Endpoints/createFirewallRules.json new file mode 100644 index 0000000..0b08b18 --- /dev/null +++ b/tests/Fixtures/Endpoints/createFirewallRules.json @@ -0,0 +1,33 @@ +{ + "result": [ + { + "id": "970b10321e3f4adda674c912b5f76591", + "paused": false, + "description": "Foo", + "action": "block", + "filter": { + "id": "70f39827184d487e97cc286b960f4cc3", + "expression": "http.cookie eq \"foo\"", + "paused": false + }, + "created_on": "2019-07-05T15:53:15Z", + "modified_on": "2019-07-05T15:53:15Z" + }, + { + "id": "42c05fd0e0af4d17a361d2d1423476bc", + "paused": false, + "description": "Bar", + "action": "block", + "filter": { + "id": "246b4d9f5f51471485bdc95e1c6b53a7", + "expression": "http.cookie eq \"bar\"", + "paused": false + }, + "created_on": "2019-07-05T15:53:15Z", + "modified_on": "2019-07-05T15:53:15Z" + } + ], + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/Fixtures/Endpoints/deleteFirewallRule.json b/tests/Fixtures/Endpoints/deleteFirewallRule.json new file mode 100644 index 0000000..8a99884 --- /dev/null +++ b/tests/Fixtures/Endpoints/deleteFirewallRule.json @@ -0,0 +1,8 @@ +{ + "result": { + "id": "970b10321e3f4adda674c912b5f76591" + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/Fixtures/Endpoints/getServerSideExclude.json b/tests/Fixtures/Endpoints/getServerSideExclude.json new file mode 100644 index 0000000..8da6ab1 --- /dev/null +++ b/tests/Fixtures/Endpoints/getServerSideExclude.json @@ -0,0 +1,11 @@ +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "server_side_exclude", + "value": "on", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } +} diff --git a/tests/Fixtures/Endpoints/listAccounts.json b/tests/Fixtures/Endpoints/listAccounts.json new file mode 100644 index 0000000..0e6275a --- /dev/null +++ b/tests/Fixtures/Endpoints/listAccounts.json @@ -0,0 +1,20 @@ +{ + "success": true, + "errors": [], + "messages": [], + "result": [ + { + "id": "023e105f4ecef8ad9ca31a8372d0c353", + "name": "Example Account", + "settings": { + "enforce_twofactor": true + } + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "count": 1, + "total_count": 2000 + } +} diff --git a/tests/Fixtures/Endpoints/listFirewallRules.json b/tests/Fixtures/Endpoints/listFirewallRules.json new file mode 100644 index 0000000..a343395 --- /dev/null +++ b/tests/Fixtures/Endpoints/listFirewallRules.json @@ -0,0 +1,27 @@ +{ + "result": [ + { + "id": "970b10321e3f4adda674c912b5f76591", + "paused": false, + "description": "Foobar", + "action": "block", + "filter": { + "id": "70f39827184d487e97cc286b960f4cc3", + "expression": "http.cookie eq \"foobar\"", + "paused": false + }, + "created_on": "2019-07-05T15:53:15Z", + "modified_on": "2019-07-05T15:53:15Z" + } + ], + "success": true, + "errors": [], + "messages": [], + "result_info": { + "page": 1, + "per_page": 50, + "count": 1, + "total_count": 1, + "total_pages": 1 + } +} diff --git a/tests/Fixtures/Endpoints/updateFirewallRule.json b/tests/Fixtures/Endpoints/updateFirewallRule.json new file mode 100644 index 0000000..27359e0 --- /dev/null +++ b/tests/Fixtures/Endpoints/updateFirewallRule.json @@ -0,0 +1,19 @@ +{ + "result": { + "id": "970b10321e3f4adda674c912b5f76591", + "paused": false, + "description": "Foo", + "action": "block", + "filter": { + "id": "5def9c4297e0466cb0736b838345d910", + "expression": "http.cookie eq \"foo\"", + "paused": false + }, + "created_on": "2019-07-05T15:53:15Z", + "modified_on": "2019-07-05T18:07:46Z", + "index": 1 + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/Fixtures/Endpoints/updateServerSideExclude.json b/tests/Fixtures/Endpoints/updateServerSideExclude.json new file mode 100644 index 0000000..8da6ab1 --- /dev/null +++ b/tests/Fixtures/Endpoints/updateServerSideExclude.json @@ -0,0 +1,11 @@ +{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "server_side_exclude", + "value": "on", + "editable": true, + "modified_on": "2014-01-01T05:20:00.12345Z" + } +}