From e37b681629cf382416a9279f73c8c8ad834428b7 Mon Sep 17 00:00:00 2001 From: Sebastian Molenda Date: Sun, 9 Feb 2025 18:54:42 +0100 Subject: [PATCH 1/4] Add write protection with examples and tests --- examples/cli/partial_updates.php | 91 ++++++++++++++++++ examples/cli/pn.jpg | Bin 0 -> 1751 bytes examples/cli/using_etag.php | 66 +++++++++++++ src/PubNub/Endpoints/Endpoint.php | 6 +- .../Objects/Channel/SetChannelMetadata.php | 3 + .../Endpoints/Objects/MatchesETagTrait.php | 20 ++++ .../Objects/ObjectsCollectionEndpoint.php | 3 +- .../Objects/UUID/SetUUIDMetadata.php | 3 + .../Channel/PNGetAllChannelMetadataResult.php | 55 +++++++---- .../Channel/PNGetChannelMetadataResult.php | 53 +++++----- .../Channel/PNSetChannelMetadataResult.php | 61 ++++++++---- .../UUID/PNGetAllUUIDMetadataResult.php | 47 +++++---- .../Objects/UUID/PNGetUUIDMetadataResult.php | 67 ++++++++----- .../Objects/UUID/PNSetUUIDMetadataResult.php | 72 ++++++++------ .../SetChannelMetadataEndpointTest.php | 59 +++++++++++- .../uuid/SetUUIDMetadataEndpointTest.php | 61 +++++++++++- 16 files changed, 525 insertions(+), 142 deletions(-) create mode 100644 examples/cli/partial_updates.php create mode 100644 examples/cli/pn.jpg create mode 100644 examples/cli/using_etag.php create mode 100644 src/PubNub/Endpoints/Objects/MatchesETagTrait.php diff --git a/examples/cli/partial_updates.php b/examples/cli/partial_updates.php new file mode 100644 index 00000000..27b31698 --- /dev/null +++ b/examples/cli/partial_updates.php @@ -0,0 +1,91 @@ +setPublishKey("demo"); +$pnconf->setSubscribeKey("demo"); +$pnconf->setUuid("example"); + +$pubnub = new PubNub($pnconf); + +$channel = "demo_example"; + +print("We're setting the channel's $channel additional info.\n"); +print("\tTo exit type '/exit'\n"); +print("\tTo show the current object type '/show'\n"); +print("\tTo show this help type '/help'\n"); + +print("Enter the channel name: "); +$name = trim(fgets(STDIN)); + +print("Enter the channel description: "); +$description = trim(fgets(STDIN)); + +// Set channel metadata +$pubnub->setChannelMetadata() + ->channel($channel) + ->meta([ + "name" => $name, + "description" => $description, + ]) + ->sync(); + +print("The channel has been created with name and description.\n"); + +while (true) { + // Fetch current channel metadata + $response = $pubnub->getChannelMetadata() + ->channel($channel) + ->sync(); + + print("Enter the field name: "); + $fieldName = trim(fgets(STDIN)); + + if ($fieldName === '/exit') { + exit(); + } elseif ($fieldName === '/show') { + print_r($response); + continue; + } elseif ($fieldName === '/help') { + print("\tTo exit type '/exit'\n"); + print("\tTo show the current object type '/show'\n"); + print("\tTo show this help type '/help'\n"); + continue; + } + + print("Enter the field value: "); + $fieldValue = trim(fgets(STDIN)); + + // Prepare custom fields + $custom = (array)$response->getCustom(); + + if (isset($custom[$fieldName])) { + print("Field $fieldName already has a value. Overwrite? (y/n): "); + $confirmation = trim(fgets(STDIN)); + + if (strtolower($confirmation) !== 'y') { + print("Object will not be updated.\n"); + continue; + } + } + + // Update custom field + $custom[$fieldName] = $fieldValue; + + // Writing the updated object back to the server + $pubnub->setChannelMetadata() + ->channel($channel) + ->meta([ + "name" => $response->getName(), + "description" => $response->getDescription(), + "custom" => $custom, + ]) + ->sync(); + print("Object has been updated.\n"); +} diff --git a/examples/cli/pn.jpg b/examples/cli/pn.jpg new file mode 100644 index 0000000000000000000000000000000000000000..225b5f18fbc55df0c13b1e305077c6449f3503d3 GIT binary patch literal 1751 zcmb_ado+}37=OR{X3Pv3rX(?q86%}qj7v>sV@w8nlv|EM#IQJnaT%AAGn6nXiD`@4 zi7_}x;vkc1x5%YsZKudJ;;`Fs=(5tqW%e67YxkV(k3DCf?|k3;JHOxWeV*rizE{zw z7zLW&a-28-f*|0CJV3Do;BAUA6v>Ir_IGu6;W#HR0TdSd=CJ5s`uzcLzJl z;UnG@>=1y!MgRk2zzE>RMzLI7oxxXQt`~75@zg|_>#MeZ7gU3SW4Va5fV_U(s2BkN zkS~JO5(H679ExDAK;AMwsl+izg9x9a#33tKw(MEKjmw@x?rg-Oq>q`l2-dQYscDQiwM!>nuJi~%XLbTJY;I`BZPB7MXsI(OS~ID}hLFI}vdPTPl;untE0mEQH?R}~diB{Mg%{q{$IT&LHqNn&zO z{6l?|3#DOZgol0HHaSuETzkB4pBLe9{uXU$lvsBCQu@{Bx6f`D8{DaRFW@JJS6xQe ztvh$Rn`+9(NQf>65=E)Cvn}St#bc+uU7B@1S3I=3+}>&t{7V!_>hLm}YIKn4wvl&A ztS)4BD1fMC?4x8;uXu8QXZS~boOFy;O)Hx#j`yEUaM8ADDfxkYqh6zG>R9%Kp&8qH zoEymVNg>(JbT|+0`fOd6r9b8wCtOO7`BW~XXUMx!#U8EcjKpgTt*1SQ{8Mg3)b=Fk zl?dhr#gm1Z=V}~B;dC6D6gw&2t(~r`?&Gz|8m8q~`DK!zsPPXSpKnf=Ob2`lV-D2M zRP!d^(B2s{9`%i07mS|2^)|%H%(mh8c)tj*aOxYwclb8%Fx6!H<5SY6W*6_if~V*d z((P;w=Dd6SUlV-JW4)dYT^PEG(@_A|`xSIkz!O`d`zgcSS~W;n(s(_wTCcPIrx@ve<+i3+S3>4IL zPV`nmlPSGv$D5b9+MxD<4>u*xt~E$G4i#ClB<~SUURJg>a{jD-qnnpHV|`zg?lWPK z&bs~9MY`BEfAWrllw92tF;=;Fv^S|LxS+mXOt;LdFDaB4HDj-%?%f@p;>SrY`_qE1 s7H<6hmc_9U682FPmD5^(KZ}<~Ug+WsS3lNFGd`l3M#b47AEBcEPgVOtd;kCd literal 0 HcmV?d00001 diff --git a/examples/cli/using_etag.php b/examples/cli/using_etag.php new file mode 100644 index 00000000..540b7037 --- /dev/null +++ b/examples/cli/using_etag.php @@ -0,0 +1,66 @@ +setPublishKey('demo'); +$config->setSubscribeKey('demo'); +$config->setUuid("example"); + +$config_2 = clone $config; +$config_2->setUuid("example_2"); + +$pubnub = new PubNub($config); +$pubnub_2 = new PubNub($config_2); + +$sample_user = [ + "uuid" => "SampleUser", + "name" => "John Doe", + "email" => "jd@example.com", + "custom" => ["age" => 42, "address" => "123 Main St."] +]; + +// One client creates a metadata for the user "SampleUser" and successfully writes it to the server. +$set_result = $pubnub->setUUIDMetadata()->uuid("SampleUser")->meta($sample_user)->sync(); + +// We store the eTag for the user for further updates. +$original_e_tag = $set_result->getETag(); + +print("We receive the eTag for the user: $original_e_tag" . PHP_EOL); + +// Another client sets the user meta with the same UUID but different data. +$overwrite_result = $pubnub_2->setUUIDMetadata()->uuid("SampleUser")->meta(["name" => "Jane Doe"])->sync(); +$new_e_tag = $overwrite_result->getETag(); + +// We can verify that there is a new eTag for the user. +print( + "After overwrite there's a new eTag: $original_e_tag === $new_e_tag? " + . ($original_e_tag === $new_e_tag ? "true" : "false") . PHP_EOL +); + +// We modify the user and try to update it. +$updated_user = array_merge($sample_user, ["custom" => ["age" => 43, "address" => "321 Other St."]]); + +try { + $update_result = $pubnub->setUUIDMetadata() + ->uuid("SampleUser") + ->meta($updated_user) + ->ifMatchesETag($original_e_tag) + ->sync(); + print_r($update_result); +} catch (PubNubServerException $e) { + if ($e->getStatusCode() === 412) { + print( + "Update failed because eTag mismatch: " . $e->getServerErrorMessage() + . "\nHTTP Status Code: " . $e->getStatusCode() . PHP_EOL + ); + } else { + print( "Unexpected error: " . $e->getMessage() . PHP_EOL); + } +} catch (Exception $e) { + echo "Unexpected error: " . $e->getMessage() . PHP_EOL; +} diff --git a/src/PubNub/Endpoints/Endpoint.php b/src/PubNub/Endpoints/Endpoint.php index a4b79ace..d73907a6 100755 --- a/src/PubNub/Endpoints/Endpoint.php +++ b/src/PubNub/Endpoints/Endpoint.php @@ -2,6 +2,7 @@ namespace PubNub\Endpoints; +use Exception; use PubNub\Enums\PNOperationType; use PubNub\Enums\PNHttpMethod; use PubNub\Enums\PNStatusCategory; @@ -32,6 +33,9 @@ abstract class Endpoint protected int $endpointOperationType; protected string $endpointName; + /** @var string[] */ + protected array $customHeaders = []; + protected const RESPONSE_IS_JSON = true; /** @var PubNub */ @@ -178,7 +182,7 @@ protected function defaultHeaders() protected function customHeaders() { - return []; + return $this->customHeaders; } /** diff --git a/src/PubNub/Endpoints/Objects/Channel/SetChannelMetadata.php b/src/PubNub/Endpoints/Objects/Channel/SetChannelMetadata.php index c275ce55..179d8339 100644 --- a/src/PubNub/Endpoints/Objects/Channel/SetChannelMetadata.php +++ b/src/PubNub/Endpoints/Objects/Channel/SetChannelMetadata.php @@ -3,6 +3,7 @@ namespace PubNub\Endpoints\Objects\Channel; use PubNub\Endpoints\Endpoint; +use PubNub\Endpoints\Objects\MatchesETagTrait; use PubNub\Enums\PNHttpMethod; use PubNub\Enums\PNOperationType; use PubNub\Exceptions\PubNubValidationException; @@ -11,6 +12,8 @@ class SetChannelMetadata extends Endpoint { + use MatchesETagTrait; + protected const PATH = "/v2/objects/%s/channels/%s"; /** @var string */ diff --git a/src/PubNub/Endpoints/Objects/MatchesETagTrait.php b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php new file mode 100644 index 00000000..d571d4af --- /dev/null +++ b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php @@ -0,0 +1,20 @@ +eTag = $eTag; + $this->customHeaders['If-Match'] = $eTag; + return $this; + } +} diff --git a/src/PubNub/Endpoints/Objects/ObjectsCollectionEndpoint.php b/src/PubNub/Endpoints/Objects/ObjectsCollectionEndpoint.php index aa5265be..cec40cb9 100644 --- a/src/PubNub/Endpoints/Objects/ObjectsCollectionEndpoint.php +++ b/src/PubNub/Endpoints/Objects/ObjectsCollectionEndpoint.php @@ -61,5 +61,4 @@ public function sort($sort) return $this; } - -} \ No newline at end of file +} diff --git a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php index 986b90dc..c623d20d 100644 --- a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php +++ b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php @@ -3,6 +3,7 @@ namespace PubNub\Endpoints\Objects\UUID; use PubNub\Endpoints\Endpoint; +use PubNub\Endpoints\Objects\MatchesETagTrait; use PubNub\Enums\PNHttpMethod; use PubNub\Enums\PNOperationType; use PubNub\Exceptions\PubNubValidationException; @@ -11,6 +12,8 @@ class SetUUIDMetadata extends Endpoint { + use MatchesETagTrait; + protected const PATH = "/v2/objects/%s/uuids/%s"; /** @var string */ diff --git a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php index 18830d04..ebcb3980 100755 --- a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php @@ -2,7 +2,6 @@ namespace PubNub\Models\Consumer\Objects\Channel; - class PNGetAllChannelMetadataResult { /** @var integer */ @@ -17,19 +16,24 @@ class PNGetAllChannelMetadataResult /** @var array */ protected $data; + /** @var ?string */ + protected $eTag; + /** * PNGetAllChannelMetadataResult constructor. * @param integer $totalCount * @param string $prev * @param string $next * @param array $data + * @param $string $eTag */ - function __construct($totalCount, $prev, $next, $data) + public function __construct($totalCount, $prev, $next, $data, ?string $eTag = null) { $this->totalCount = $totalCount; $this->prev = $prev; $this->next = $next; $this->data = $data; + $this->eTag = $eTag; } /** @@ -64,15 +68,28 @@ public function getData() return $this->data; } + /** + * @return ?string + */ + public function getETag(): ?string + { + return $this->eTag; + } + public function __toString() { - if (!empty($data)) - { - $data_string = json_encode($data); + if (!empty($data)) { + $data_string = json_encode($data); } - return sprintf("totalCount: %s, prev: %s, next: %s, data: %s", - $this->totalCount, $this->prev, $this->next, $data_string); + return sprintf( + "totalCount: %s, prev: %s, next: %s, data: %s, eTag: %s", + $this->totalCount, + $this->prev, + $this->next, + $data_string, + $this->eTag + ); } /** @@ -85,32 +102,32 @@ public static function fromPayload(array $payload) $prev = null; $next = null; $data = null; + $eTag = null; - if (array_key_exists("totalCount", $payload)) - { + if (array_key_exists("totalCount", $payload)) { $totalCount = $payload["totalCount"]; } - if (array_key_exists("prev", $payload)) - { + if (array_key_exists("prev", $payload)) { $prev = $payload["prev"]; } - if (array_key_exists("next", $payload)) - { + if (array_key_exists("next", $payload)) { $next = $payload["next"]; } - if (array_key_exists("data", $payload)) - { + if (array_key_exists("data", $payload)) { $data = []; - foreach($payload["data"] as $value) - { + foreach ($payload["data"] as $value) { array_push($data, PNGetChannelMetadataResult::fromPayload([ "data" => $value ])); } } - return new PNGetAllChannelMetadataResult($totalCount, $prev, $next, $data); + if (array_key_exists("eTag", $payload)) { + $eTag = $payload["eTag"]; + } + + return new PNGetAllChannelMetadataResult($totalCount, $prev, $next, $data, $eTag); } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/Objects/Channel/PNGetChannelMetadataResult.php b/src/PubNub/Models/Consumer/Objects/Channel/PNGetChannelMetadataResult.php index cce3cb28..00c9a92d 100755 --- a/src/PubNub/Models/Consumer/Objects/Channel/PNGetChannelMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/Channel/PNGetChannelMetadataResult.php @@ -31,7 +31,7 @@ class PNGetChannelMetadataResult * @param string $updated * @param string $eTag */ - function __construct($id, $name, $description, $custom = null, $updated = null, $eTag = null) + public function __construct($id, $name, $description, $custom = null, $updated = null, $eTag = null) { $this->id = $id; $this->name = $name; @@ -92,17 +92,24 @@ public function getETag() public function __toString() { $custom_string = ""; - - foreach($this->custom as $key => $value) { + + foreach ($this->custom as $key => $value) { if (strlen($custom_string) > 0) { $custom_string .= ", "; } $custom_string .= "$key: $value"; } - - return sprintf("Channel metadata set: id: %s, name: %s, description: %s, custom: %s, updated: %s, eTag: %s", - $this->id, $this->name, $this->description, "[" . $custom_string . "]", $this->updated, $this->eTag); + + return sprintf( + "Channel metadata set: id: %s, name: %s, description: %s, custom: %s, updated: %s, eTag: %s", + $this->id, + $this->name, + $this->description, + "[" . $custom_string . "]", + $this->updated, + $this->eTag + ); } /** @@ -111,7 +118,7 @@ public function __toString() */ public static function fromPayload(array $payload) { - $meta = $payload["data"]; + $data = $payload["data"]; $id = null; $name = null; $description = null; @@ -119,36 +126,30 @@ public static function fromPayload(array $payload) $updated = null; $eTag = null; - if (array_key_exists("id", $meta)) - { - $id = $meta["id"]; + if (array_key_exists("id", $data)) { + $id = $data["id"]; } - if (array_key_exists("name", $meta)) - { - $name = $meta["name"]; + if (array_key_exists("name", $data)) { + $name = $data["name"]; } - if (array_key_exists("description", $meta)) - { - $description = $meta["description"]; + if (array_key_exists("description", $data)) { + $description = $data["description"]; } - if (array_key_exists("custom", $meta)) - { - $custom = (object)$meta["custom"]; + if (array_key_exists("custom", $data)) { + $custom = (object)$data["custom"]; } - if (array_key_exists("updated", $meta)) - { - $updated = (object)$meta["updated"]; + if (array_key_exists("updated", $data)) { + $updated = (object)$data["updated"]; } - if (array_key_exists("eTag", $meta)) - { - $eTag = (object)$meta["eTag"]; + if (array_key_exists("eTag", $data)) { + $eTag = (object)$data["eTag"]; } return new PNGetChannelMetadataResult($id, $name, $description, (object) $custom, $updated, $eTag); } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/Objects/Channel/PNSetChannelMetadataResult.php b/src/PubNub/Models/Consumer/Objects/Channel/PNSetChannelMetadataResult.php index 0626c68f..e0a6e154 100755 --- a/src/PubNub/Models/Consumer/Objects/Channel/PNSetChannelMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/Channel/PNSetChannelMetadataResult.php @@ -16,19 +16,24 @@ class PNSetChannelMetadataResult /** @var array */ protected $custom; + /** @var ?string */ + protected ?string $eTag; + /** * PNSetChannelMetadataResult constructor. * @param string $id * @param string $name * @param string $description * @param array $custom + * @param ?string $eTag */ - function __construct($id, $name, $description, $custom = null) + public function __construct($id, $name, $description, $custom = null, ?string $eTag = null) { $this->id = $id; $this->name = $name; $this->description = $description; $this->custom = $custom; + $this->eTag = $eTag; } /** @@ -63,20 +68,34 @@ public function getCustom() return $this->custom; } + /** + * @return ?string + */ + public function getETag(): ?string + { + return $this->eTag; + } + public function __toString() { $custom_string = ""; - - foreach($this->custom as $key => $value) { + + foreach ($this->custom as $key => $value) { if (strlen($custom_string) > 0) { $custom_string .= ", "; } $custom_string .= "$key: $value"; } - - return sprintf("Channel metadata set: id: %s, name: %s, description: %s, custom: %s", - $this->id, $this->name, $this->description, "[" . $custom_string . "]"); + + return sprintf( + "Channel metadata set: id: %s, name: %s, description: %s, custom: %s, eTag: %s", + $this->id, + $this->name, + $this->description, + "[" . $custom_string . "]", + $this->eTag + ); } /** @@ -85,32 +104,32 @@ public function __toString() */ public static function fromPayload(array $payload) { - $meta = $payload["data"]; + $data = $payload["data"]; $id = null; $name = null; $description = null; $custom = null; + $eTag = null; - if (array_key_exists("id", $meta)) - { - $id = $meta["id"]; + if (array_key_exists("id", $data)) { + $id = $data["id"]; } - if (array_key_exists("name", $meta)) - { - $name = $meta["name"]; + if (array_key_exists("name", $data)) { + $name = $data["name"]; } - if (array_key_exists("description", $meta)) - { - $description = $meta["description"]; + if (array_key_exists("description", $data)) { + $description = $data["description"]; } - if (array_key_exists("custom", $meta)) - { - $custom = (object)$meta["custom"]; + if (array_key_exists("custom", $data)) { + $custom = (object)$data["custom"]; } - return new PNSetChannelMetadataResult($id, $name, $description, (object) $custom); + if (array_key_exists("eTag", $data)) { + $eTag = $data["eTag"]; + } + return new PNSetChannelMetadataResult($id, $name, $description, (object)$custom, $eTag); } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php b/src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php index 73f37714..f3eb2e3f 100755 --- a/src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php @@ -2,7 +2,6 @@ namespace PubNub\Models\Consumer\Objects\UUID; - class PNGetAllUUIDMetadataResult { /** @var integer */ @@ -17,19 +16,24 @@ class PNGetAllUUIDMetadataResult /** @var array */ protected $data; + /** @var ?string */ + protected $eTag; + /** * PNGetAllUUIDMetadataResult constructor. * @param integer $totalCount * @param string $prev * @param string $next * @param array $data + * @param ?string $eTag */ - function __construct($totalCount, $prev, $next, $data) + public function __construct($totalCount, $prev, $next, $data, ?string $eTag = null) { $this->totalCount = $totalCount; $this->prev = $prev; $this->next = $next; $this->data = $data; + $this->eTag = $eTag; } /** @@ -66,13 +70,18 @@ public function getData() public function __toString() { - if (!empty($data)) - { - $data_string = json_encode($data); + if (!empty($data)) { + $data_string = json_encode($data); } - return sprintf("totalCount: %s, prev: %s, next: %s, data: %s", - $this->totalCount, $this->prev, $this->next, $data_string); + return sprintf( + "totalCount: %s, prev: %s, next: %s, data: %s, eTag: %s", + $this->totalCount, + $this->prev, + $this->next, + $data_string, + $this->eTag, + ); } /** @@ -85,32 +94,32 @@ public static function fromPayload(array $payload) $prev = null; $next = null; $data = null; + $eTag = null; - if (array_key_exists("totalCount", $payload)) - { + if (array_key_exists("totalCount", $payload)) { $totalCount = $payload["totalCount"]; } - if (array_key_exists("prev", $payload)) - { + if (array_key_exists("prev", $payload)) { $prev = $payload["prev"]; } - if (array_key_exists("next", $payload)) - { + if (array_key_exists("next", $payload)) { $next = $payload["next"]; } - if (array_key_exists("data", $payload)) - { + if (array_key_exists("data", $payload)) { $data = []; - foreach($payload["data"] as $value) - { + foreach ($payload["data"] as $value) { array_push($data, PNGetUUIDMetadataResult::fromPayload([ "data" => $value ])); } } - return new PNGetAllUUIDMetadataResult($totalCount, $prev, $next, $data); + if (array_key_exists("eTag", $payload)) { + $eTag = $payload["eTag"]; + } + + return new PNGetAllUUIDMetadataResult($totalCount, $prev, $next, $data, $eTag); } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php index e97ce0de..3c185de6 100755 --- a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php @@ -39,8 +39,16 @@ class PNGetUUIDMetadataResult * @param string $updated * @param string $eTag */ - function __construct($id, $name, $externalId, $profileUrl, $email, $custom = null, $updated = null, $eTag = null) - { + public function __construct( + $id, + $name, + $externalId, + $profileUrl, + $email, + $custom = null, + $updated = null, + $eTag = null + ) { $this->id = $id; $this->name = $name; $this->externalId = $externalId; @@ -118,8 +126,8 @@ public function getETag() public function __toString() { $custom_string = ""; - - foreach($this->custom as $key => $value) { + + foreach ($this->custom as $key => $value) { if (strlen($custom_string) > 0) { $custom_string .= ", "; } @@ -127,8 +135,18 @@ public function __toString() $custom_string .= "$key: $value"; } - return sprintf("UUID metadata set: id: %s, name: %s, externalId: %s, profileUrl: %s, email: %s, custom: %s, updated: %s, eTag: %s", - $this->id, $this->name, $this->externalId, $this->profileUrl, $this->email, "[" . $custom_string . "]", $this->updated, $this->eTag); + return sprintf( + "UUID metadata set: id: %s, name: %s, externalId: %s, profileUrl: %s, email: %s, custom: %s, updated: %s," + . " eTag: %s", + $this->id, + $this->name, + $this->externalId, + $this->profileUrl, + $this->email, + "[" . $custom_string . "]", + $this->updated, + $this->eTag + ); } /** @@ -147,46 +165,47 @@ public static function fromPayload(array $payload) $updated = null; $eTag = null; - if (array_key_exists("id", $meta)) - { + if (array_key_exists("id", $meta)) { $id = $meta["id"]; } - if (array_key_exists("name", $meta)) - { + if (array_key_exists("name", $meta)) { $name = $meta["name"]; } - if (array_key_exists("externalId", $meta)) - { + if (array_key_exists("externalId", $meta)) { $externalId = $meta["externalId"]; } - if (array_key_exists("profileUrl", $meta)) - { + if (array_key_exists("profileUrl", $meta)) { $profileUrl = $meta["profileUrl"]; } - if (array_key_exists("email", $meta)) - { + if (array_key_exists("email", $meta)) { $email = $meta["email"]; } - if (array_key_exists("custom", $meta)) - { + if (array_key_exists("custom", $meta)) { $custom = (object)$meta["custom"]; } - if (array_key_exists("updated", $meta)) - { + if (array_key_exists("updated", $meta)) { $updated = (object)$meta["updated"]; } - if (array_key_exists("eTag", $meta)) - { + if (array_key_exists("eTag", $meta)) { $eTag = (object)$meta["eTag"]; } - return new PNGetUUIDMetadataResult($id, $name, $externalId, $profileUrl, $email, (object) $custom, $updated, $eTag); + return new PNGetUUIDMetadataResult( + $id, + $name, + $externalId, + $profileUrl, + $email, + (object) $custom, + $updated, + $eTag + ); } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/Objects/UUID/PNSetUUIDMetadataResult.php b/src/PubNub/Models/Consumer/Objects/UUID/PNSetUUIDMetadataResult.php index fde08bbf..04ddff69 100755 --- a/src/PubNub/Models/Consumer/Objects/UUID/PNSetUUIDMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/UUID/PNSetUUIDMetadataResult.php @@ -22,6 +22,8 @@ class PNSetUUIDMetadataResult /** @var array */ protected $custom; + protected ?string $eTag; + /** * PNSetUUIDMetadataResult constructor. * @param string $id @@ -30,8 +32,9 @@ class PNSetUUIDMetadataResult * @param array $profileUrl * @param array $email * @param array $custom + * @param ?string $eTag */ - function __construct($id, $name, $externalId, $profileUrl, $email, $custom = null) + public function __construct($id, $name, $externalId, $profileUrl, $email, $custom = null, $eTag = null) { $this->id = $id; $this->name = $name; @@ -39,6 +42,7 @@ function __construct($id, $name, $externalId, $profileUrl, $email, $custom = nul $this->profileUrl = $profileUrl; $this->email = $email; $this->custom = $custom; + $this->eTag = $eTag; } /** @@ -81,6 +85,14 @@ public function getEmail() return $this->email; } + /** + * @return string + */ + public function getETag(): ?string + { + return $this->eTag; + } + /** * @return object */ @@ -92,17 +104,24 @@ public function getCustom() public function __toString() { $custom_string = ""; - - foreach($this->custom as $key => $value) { + + foreach ($this->custom as $key => $value) { if (strlen($custom_string) > 0) { $custom_string .= ", "; } $custom_string .= "$key: $value"; } - - return sprintf("UUID metadata set: id: %s, name: %s, externalId: %s, profileUrl: %s, email: %s, custom: %s", - $this->id, $this->name, $this->externalId, $this->profileUrl, $this->email, "[" . $custom_string . "]"); + + return sprintf( + "UUID metadata set: id: %s, name: %s, externalId: %s, profileUrl: %s, email: %s, custom: %s", + $this->id, + $this->name, + $this->externalId, + $this->profileUrl, + $this->email, + "[" . $custom_string . "]" + ); } /** @@ -111,44 +130,43 @@ public function __toString() */ public static function fromPayload(array $payload) { - $meta = $payload["data"]; + $data = $payload["data"]; $id = null; $name = null; $externalId = null; $profileUrl = null; $email = null; $custom = null; + $eTag = null; + + if (array_key_exists("id", $data)) { + $id = $data["id"]; + } - if (array_key_exists("id", $meta)) - { - $id = $meta["id"]; + if (array_key_exists("name", $data)) { + $name = $data["name"]; } - if (array_key_exists("name", $meta)) - { - $name = $meta["name"]; + if (array_key_exists("externalId", $data)) { + $externalId = $data["externalId"]; } - if (array_key_exists("externalId", $meta)) - { - $externalId = $meta["externalId"]; + if (array_key_exists("profileUrl", $data)) { + $profileUrl = $data["profileUrl"]; } - if (array_key_exists("profileUrl", $meta)) - { - $profileUrl = $meta["profileUrl"]; + if (array_key_exists("email", $data)) { + $email = $data["email"]; } - if (array_key_exists("email", $meta)) - { - $email = $meta["email"]; + if (array_key_exists("custom", $data)) { + $custom = (object)$data["custom"]; } - if (array_key_exists("custom", $meta)) - { - $custom = (object)$meta["custom"]; + if (array_key_exists("eTag", $data)) { + $eTag = $data["eTag"]; } - return new PNSetUUIDMetadataResult($id, $name, $externalId, $profileUrl, $email, (object) $custom); + return new PNSetUUIDMetadataResult($id, $name, $externalId, $profileUrl, $email, (object) $custom, $eTag); } -} \ No newline at end of file +} diff --git a/tests/integrational/objects/channel/SetChannelMetadataEndpointTest.php b/tests/integrational/objects/channel/SetChannelMetadataEndpointTest.php index a3f6d029..abd15966 100644 --- a/tests/integrational/objects/channel/SetChannelMetadataEndpointTest.php +++ b/tests/integrational/objects/channel/SetChannelMetadataEndpointTest.php @@ -3,7 +3,7 @@ namespace Tests\Integrational\Objects\Channel; use PubNubTestCase; - +use PubNub\Exceptions\PubNubServerException; class SetChannelMetadataEndpointTest extends PubNubTestCase { @@ -21,7 +21,7 @@ public function testAddMetadataToChannel() ] ]) ->sync(); - + $this->assertNotEmpty($response); $this->assertEquals("ch", $response->getId()); $this->assertEquals("ch_name", $response->getName()); @@ -33,4 +33,59 @@ public function testAddMetadataToChannel() $this->assertEquals("aa", $custom->a); $this->assertEquals("bb", $custom->b); } + + public function testIfMatchesEtagWriteProtection(): void + { + $response = $this->pubnub->setChannelMetadata() + ->channel("ch") + ->meta([ + "id" => "ch", + "name" => "ch_name", + "description" => "ch_description", + "custom" => [ + "a" => "aa", + "b" => "bb" + ] + ]) + ->sync(); + + $this->assertNotEmpty($response); + $this->assertEquals("ch", $response->getId()); + $this->assertNotEmpty($response->getETag()); + $eTag = $response->getETag(); + + $overwrite = $this->pubnub->setChannelMetadata() + ->channel("ch") + ->meta(["id" => "ch", + "name" => "edited_ch_name", + "description" => "edited_ch_description", + "custom" => [ + "c" => "cc", + "d" => "cc" + ]]) + ->sync(); + + $this->assertNotEmpty($overwrite); + $this->assertNotEmpty($overwrite->getETag()); + $this->assertNotEquals($eTag, $overwrite->getETag()); + + try { + $response = $this->pubnub->setChannelMetadata() + ->channel("ch") + ->meta([ + "id" => "ch", + "name" => "Channel", + "description" => "Some testing is happening", + "custom" => [ + "a" => "aa", + "b" => "bb" + ] + ]) + ->ifMatchesETag($eTag) + ->sync(); + } catch (PubNubServerException $exception) { + $this->assertEquals("412", $exception->getStatusCode()); + $this->assertNotEmpty($exception->getServerErrorMessage()); + } + } } diff --git a/tests/integrational/objects/uuid/SetUUIDMetadataEndpointTest.php b/tests/integrational/objects/uuid/SetUUIDMetadataEndpointTest.php index b6502d84..afcf6e8f 100644 --- a/tests/integrational/objects/uuid/SetUUIDMetadataEndpointTest.php +++ b/tests/integrational/objects/uuid/SetUUIDMetadataEndpointTest.php @@ -2,9 +2,9 @@ namespace Tests\Integrational\Objects\UUID; +use PubNub\Exceptions\PubNubServerException; use PubNubTestCase; - class SetUUIDMetadataEndpointTest extends PubNubTestCase { public function testAddMetadataToUUID() @@ -37,4 +37,63 @@ public function testAddMetadataToUUID() $this->assertEquals("aa", $custom->a); $this->assertEquals("bb", $custom->b); } + + + public function testIfMatchesEtagWriteProtection(): void + { + $response = $this->pubnub->setUUIDMetadata() + ->uuid("uuid") + ->meta([ + "id" => "uuid", + "name" => "Some Name", + "description" => "Some description", + "custom" => [ + "a" => "aa", + "b" => "bb" + ] + ]) + ->sync(); + + $this->assertNotEmpty($response); + $this->assertEquals("uuid", $response->getId()); + $this->assertNotEmpty($response->getETag()); + $eTag = $response->getETag(); + + $overwrite = $this->pubnub->setUUIDMetadata() + ->uuid("uuid") + ->meta([ + "id" => "uuid", + "name" => "Edited Some Name", + "description" => "Edited Some description", + "custom" => [ + "c" => "cc", + "d" => "dd" + ] + ]) + ->sync(); + + $this->assertNotEmpty($overwrite); + $this->assertNotEmpty($overwrite->getETag()); + $this->assertNotEquals($eTag, $overwrite->getETag()); + + try { + $response = $this->pubnub->setUUIDMetadata() + ->uuid("uuid") + ->meta([ + "id" => "uuid", + "name" => "Some Name Fixed", + "description" => "Some description Fixed", + "custom" => [ + "a" => "aaa", + "b" => "bbb" + ] + ]) + + ->ifMatchesETag($eTag) + ->sync(); + } catch (PubNubServerException $exception) { + $this->assertEquals("412", $exception->getStatusCode()); + $this->assertNotEmpty($exception->getServerErrorMessage()); + } + } } From c888b5671d152872fe7b3503fec70efdfdbdaba9 Mon Sep 17 00:00:00 2001 From: Sebastian Molenda Date: Mon, 17 Feb 2025 15:52:15 +0100 Subject: [PATCH 2/4] post review --- .../Objects/Channel/PNGetAllChannelMetadataResult.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php index ebcb3980..64c5677d 100755 --- a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php @@ -25,7 +25,7 @@ class PNGetAllChannelMetadataResult * @param string $prev * @param string $next * @param array $data - * @param $string $eTag + * @param ?$string $eTag */ public function __construct($totalCount, $prev, $next, $data, ?string $eTag = null) { @@ -78,8 +78,9 @@ public function getETag(): ?string public function __toString() { - if (!empty($data)) { - $data_string = json_encode($data); + $data_string = null; + if (!empty($this->data)) { + $data_string = json_encode($this->data); } return sprintf( From 127ff2adb87eca73e55ade22b88e66c9e6e7c9fa Mon Sep 17 00:00:00 2001 From: Sebastian Molenda Date: Mon, 17 Feb 2025 15:58:03 +0100 Subject: [PATCH 3/4] Linter --- phpstan-baseline.neon | 15 --------------- .../Channel/PNGetAllChannelMetadataResult.php | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5baf1f0b..73dd8381 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3326,21 +3326,6 @@ parameters: count: 1 path: src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php - - - message: "#^Undefined variable\\: \\$data$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php - - - - message: "#^Undefined variable\\: \\$data_string$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php - - - - message: "#^Variable \\$data in empty\\(\\) is never defined\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php - - message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\Channel\\\\PNGetChannelMetadataResult\\:\\:__construct\\(\\) has parameter \\$custom with no value type specified in iterable type array\\.$#" count: 1 diff --git a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php index 64c5677d..79fc920a 100755 --- a/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/Channel/PNGetAllChannelMetadataResult.php @@ -25,7 +25,7 @@ class PNGetAllChannelMetadataResult * @param string $prev * @param string $next * @param array $data - * @param ?$string $eTag + * @param ?string $eTag */ public function __construct($totalCount, $prev, $next, $data, ?string $eTag = null) { From 9ede105770e2ee7048478f552f3109bf15983258 Mon Sep 17 00:00:00 2001 From: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:29:46 +0000 Subject: [PATCH 4/4] PubNub SDK 7.4.0 release. --- .pubnub.yml | 11 ++++++++--- CHANGELOG.md | 6 ++++++ README.md | 2 +- composer.json | 2 +- src/PubNub/PubNub.php | 2 +- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.pubnub.yml b/.pubnub.yml index ad00128d..2dc369e0 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,8 +1,13 @@ name: php -version: 7.3.0 +version: 7.4.0 schema: 1 scm: github.com/pubnub/php changelog: + - date: 2025-02-18 + version: 7.4.0 + changes: + - type: feature + text: "Write protection with If-Match eTag header for setting channel and uuid metadata." - date: 2025-02-05 version: 7.3.0 changes: @@ -447,8 +452,8 @@ sdks: - x86-64 - distribution-type: library distribution-repository: GitHub release - package-name: php-7.3.0.zip - location: https://github.com/pubnub/php/releases/tag/7.3.0 + package-name: php-7.4.0.zip + location: https://github.com/pubnub/php/releases/tag/7.4.0 requires: - name: rmccue/requests min-version: 1.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 469cad3d..999f04d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 7.4.0 +February 18 2025 + +#### Added +- Write protection with If-Match eTag header for setting channel and uuid metadata. + ## 7.3.0 February 05 2025 diff --git a/README.md b/README.md index 284a471c..380f88b7 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ You will need the publish and subscribe keys to authenticate your app. Get your { "require": { - "pubnub/pubnub": "7.3.0" + "pubnub/pubnub": "7.4.0" } } ``` diff --git a/composer.json b/composer.json index d7903910..53d1b7ec 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "keywords": ["api", "real-time", "realtime", "real time", "ajax", "push"], "homepage": "http://www.pubnub.com/", "license": "proprietary", - "version": "7.3.0", + "version": "7.4.0", "authors": [ { "name": "PubNub", diff --git a/src/PubNub/PubNub.php b/src/PubNub/PubNub.php index 93ce6e07..e05d02f1 100644 --- a/src/PubNub/PubNub.php +++ b/src/PubNub/PubNub.php @@ -62,7 +62,7 @@ class PubNub implements LoggerAwareInterface { - protected const SDK_VERSION = "7.3.0"; + protected const SDK_VERSION = "7.4.0"; protected const SDK_NAME = "PubNub-PHP"; public static $MAX_SEQUENCE = 65535;