diff --git a/.codacy.yaml b/.codacy.yaml new file mode 100644 index 00000000..a99dc7ef --- /dev/null +++ b/.codacy.yaml @@ -0,0 +1,4 @@ +--- +exclude_paths: + - "tests/**" + - "examples/**" \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 60afe970..bd760bb3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -61,7 +61,7 @@ jobs: composer self-update && composer --version composer install --prefer-dist - name: Run unit tests - run: vendor/bin/phpunit --verbose + run: composer test - name: Cancel workflow runs for commit on error if: failure() uses: ./.github/.release/actions/actions/utils/fast-jobs-failure diff --git a/composer.json b/composer.json index 70742b4d..a6c20455 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ } ], "scripts": { - "test": "./vendor/bin/phpunit tests/ --verbose --coverage-clover=coverage.clover", + "test": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text=coverage.txt --verbose ./tests", "acceptance-test": [ "mkdir -p tests/Acceptance/reports", "cp sdk-specifications/features/publish/publish-custom-mssg-type.feature tests/Acceptance/CustomMessageType/publish-custom-mssg-type.feature", diff --git a/examples/AccessManager.php b/examples/AccessManager.php new file mode 100644 index 00000000..857eb4c4 --- /dev/null +++ b/examples/AccessManager.php @@ -0,0 +1,525 @@ +setPublishKey($publishKey); +$adminConfig->setSubscribeKey($subscribeKey); +$adminConfig->setSecretKey($secretKey); +$adminConfig->setUuid('admin-user'); + +$admin = new PubNub($adminConfig); + +// Regular user instance (without secret key) +$userConfig = new PNConfiguration(); +$userConfig->setPublishKey($publishKey); +$userConfig->setSubscribeKey($subscribeKey); +$userConfig->setUuid('regular-user'); +$user = new PubNub($userConfig); + +// Helper function to print test results +function printResult($testName, $success, $message = '') +{ + $status = $success ? "βœ… PASS" : "❌ FAIL"; + echo "$status - $testName"; + if ($message) { + echo ": $message"; + } + echo "\n"; +} + +echo "πŸ”§ Step 0: Prepare channels and users metadata\n"; +echo "----------------------------------------------\n"; + +// snippet.prepare_metadata +// Create channel metadata for demo channels +$channels = ['public-channel', 'read-only-channel', 'private-channel', 'admin-only-channel']; +foreach ($channels as $channel) { + try { + $admin->setChannelMetadata() + ->channel($channel) + ->setName(ucwords(str_replace('-', ' ', $channel))) + ->setDescription("Demo channel for access manager testing - " . $channel) + ->setCustom([ + 'type' => $channel === 'admin-only-channel' ? 'admin' : 'user', + 'created' => date('Y-m-d H:i:s'), + 'demo' => true + ]) + ->sync(); + printResult("Create metadata for channel: $channel", true); + } catch (Exception $e) { + printResult("Create metadata for channel: $channel", false, $e->getMessage()); + } +} + +echo "\n"; + +// Create user metadata for demo users +$users = [ + 'admin-user' => [ + 'name' => 'Administrator User', + 'email' => 'admin@example.com', + 'role' => 'admin' + ], + 'regular-user' => [ + 'name' => 'Regular User', + 'email' => 'user@example.com', + 'role' => 'user' + ], + 'other-user' => [ + 'name' => 'Other User', + 'email' => 'other@example.com', + 'role' => 'user' + ] +]; + +foreach ($users as $uuid => $userData) { + try { + $response = $admin->setUUIDMetadata() + ->uuid($uuid) + ->name($userData['name']) + ->email($userData['email']) + ->custom([ + 'role' => $userData['role'], + 'created' => date('Y-m-d H:i:s'), + 'demo' => true, + 'preferences' => [ + 'notifications' => true, + 'theme' => 'light' + ] + ]) + ->sync(); + printResult("Create metadata for user: $uuid", true); + } catch (Exception $e) { + printResult("Create metadata for user: $uuid", false, $e->getMessage()); + } +} + +echo "\nMetadata preparation complete!\n\n"; +// snippet.end + +// Step 1: Admin grants a comprehensive token +echo "πŸ“ Step 1: Admin grants token with different permission levels\n"; +echo "-------------------------------------------------------------\n"; + +// snippet.grant_token +try { + $token = $admin->grantToken() + ->ttl(60) // 60 minutes + ->authorizedUuid('regular-user') // Restrict to specific user + ->addChannelResources([ + 'public-channel' => ['read' => true, 'write' => true], // Full access + 'read-only-channel' => ['read' => true], // Read only - no write + 'private-channel' => ['read' => true, 'write' => true, 'manage' => true] // Full access including manage + ]) + ->addChannelGroupResources([ + 'user-group' => ['read' => true] // Read only for channel groups + ]) + ->addUuidResources([ + 'regular-user' => ['get' => true, 'update' => true], // Self metadata access + 'other-user' => ['get' => true] // Read-only access to other user's metadata + ]) + ->meta([ + 'purpose' => 'demo-token', + 'issued-by' => 'admin-user' + ]) + ->sync(); + + printResult("Grant comprehensive token", true); + echo "Generated token: " . substr($token, 0, 50) . "...\n\n"; +} catch (Exception $e) { + printResult("Grant comprehensive token", false, $e->getMessage()); + echo "⚠️ Cannot continue without token. Exiting.\n"; + exit(1); +} +// snippet.end + +// Step 2: Parse the token to show its contents +echo "πŸ” Step 2: Parse token to examine embedded permissions\n"; +echo "----------------------------------------------------\n"; + +// snippet.parse_token +try { + $parsedToken = $admin->parseToken($token); + printResult("Parse token", true); + + echo "Token Details:\n"; + echo "- Version: " . $parsedToken->getVersion() . "\n"; + echo "- TTL: " . $parsedToken->getTtl() . " minutes\n"; + echo "- Authorized UUID: " . ($parsedToken->getUuid() ?? 'None') . "\n"; + echo "- Timestamp: " . date('Y-m-d H:i:s', $parsedToken->getTimestamp()) . "\n"; + + // Show channel permissions + echo "\nChannel Permissions:\n"; + foreach (['public-channel', 'read-only-channel', 'private-channel'] as $channel) { + $permissions = $parsedToken->getChannelResource($channel); + if ($permissions) { + echo "- $channel: "; + $perms = []; + if ($permissions->hasRead()) { + $perms[] = 'read'; + } + if ($permissions->hasWrite()) { + $perms[] = 'write'; + } + if ($permissions->hasManage()) { + $perms[] = 'manage'; + } + echo implode(', ', $perms) ?: 'none'; + echo "\n"; + } + } + + // Show metadata + $metadata = $parsedToken->getMetadata(); + if ($metadata) { + echo "\nToken Metadata:\n"; + foreach ($metadata as $key => $value) { + echo "- $key: $value\n"; + } + } +} catch (Exception $e) { + printResult("Parse token", false, $e->getMessage()); +} +// snippet.end + +echo "\n"; + +// Step 3: Test user access WITHOUT token (should fail) +echo "🚫 Step 3: Test user access WITHOUT token\n"; +echo "----------------------------------------\n"; + +// snippet.access_denied_without_token +try { + $user->publish() + ->channel('public-channel') + ->message(['text' => 'Hello without token!']) + ->sync(); + printResult("User publish to public-channel WITHOUT token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User publish to public-channel WITHOUT token", true, "Correctly denied access"); +} + +try { + $user->history() + ->channel('public-channel') + ->count(1) + ->sync(); + printResult("User read from public-channel WITHOUT token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User read from public-channel WITHOUT token", true, "Correctly denied access"); +} +// snippet.end + +echo "\n"; + +// Step 4: Set token and test access +echo "πŸ”‘ Step 4: Set token and test permitted operations\n"; +echo "-------------------------------------------------\n"; + +// snippet.access_granted_with_token +// Set the token for the user +$user->setToken($token); +echo "Token set for user.\n"; + +// Test allowed operations +try { + $result = $user->publish() + ->channel('public-channel') + ->message(['text' => 'Hello with token!', 'timestamp' => time()]) + ->sync(); + printResult("User publish to public-channel WITH token", true, "Message published successfully"); +} catch (Exception $e) { + printResult("User publish to public-channel WITH token", false, $e->getMessage()); +} + +try { + $result = $user->history() + ->channel('public-channel') + ->count(5) + ->sync(); + printResult("User read from public-channel WITH token", true, "History retrieved successfully"); +} catch (Exception $e) { + printResult("User read from public-channel WITH token", false, $e->getMessage()); +} + +try { + $result = $user->history() + ->channel('private-channel') + ->count(5) + ->sync(); + printResult("User read from private-channel WITH token", true, "History retrieved successfully"); +} catch (Exception $e) { + printResult("User read from private-channel WITH token", false, $e->getMessage()); +} +// snippet.end + +echo "\n"; + +// Step 5: Test restricted operations (should fail) +echo "🚫 Step 5: Test operations beyond token permissions\n"; +echo "--------------------------------------------------\n"; + +// snippet.permission_enforcement +// Test read-only channel (can read but not write) +try { + $user->history() + ->channel('read-only-channel') + ->count(1) + ->sync(); + printResult("User read from read-only-channel WITH token", true, "Read access granted"); +} catch (Exception $e) { + printResult("User read from read-only-channel WITH token", false, $e->getMessage()); +} + +try { + $user->publish() + ->channel('read-only-channel') + ->message(['text' => 'Trying to write to read-only channel']) + ->sync(); + printResult("User publish to read-only-channel WITH token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User publish to read-only-channel WITH token", true, "Correctly denied write access"); +} + +// Test channel not in token (should fail) +try { + $user->publish() + ->channel('admin-only-channel') + ->message(['text' => 'Trying to access admin channel']) + ->sync(); + printResult("User publish to admin-only-channel WITH token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User publish to admin-only-channel WITH token", true, "Correctly denied - no permissions"); +} + +try { + $user->history() + ->channel('admin-only-channel') + ->count(1) + ->sync(); + printResult("User read from admin-only-channel WITH token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User read from admin-only-channel WITH token", true, "Correctly denied - no permissions"); +} +// snippet.end + +echo "\n"; + +// Step 6: Test UUID metadata operations +echo "πŸ‘€ Step 6: Test UUID metadata operations\n"; +echo "---------------------------------------\n"; + +// snippet.uuid_metadata_operations +try { + $result = $user->setUUIDMetadata() + ->uuid('regular-user') + ->name('Regular User') + ->email('user@example.com') + ->sync(); + printResult("User update own metadata WITH token", true, "Metadata updated successfully"); +} catch (Exception $e) { + printResult("User update own metadata WITH token", false, $e->getMessage()); +} + +try { + $result = $user->getUUIDMetadata() + ->uuid('regular-user') + ->sync(); + printResult("User get own metadata WITH token", true, "Metadata retrieved successfully"); +} catch (Exception $e) { + printResult("User get own metadata WITH token", false, $e->getMessage()); +} + +try { + $result = $user->getUUIDMetadata() + ->uuid('other-user') + ->sync(); + printResult("User get other user metadata WITH token", true, "Read-only access allowed"); +} catch (Exception $e) { + printResult("User get other user metadata WITH token", false, $e->getMessage()); +} + +try { + $user->setUUIDMetadata() + ->uuid('other-user') + ->name('Cannot update this') + ->sync(); + printResult("User update other user metadata WITH token", false, "Should have failed but succeeded"); +} catch (Exception $e) { + printResult("User update other user metadata WITH token", true, "Correctly denied - read only"); +} +// snippet.end + +echo "\n"; + +// Step 7: Admin operations (always work with secret key) +echo "πŸ‘‘ Step 7: Admin operations (unrestricted access)\n"; +echo "------------------------------------------------\n"; + +// snippet.admin_unrestricted_access +try { + $result = $admin->publish() + ->channel('admin-only-channel') + ->message(['text' => 'Admin message', 'timestamp' => time()]) + ->sync(); + printResult("Admin publish to admin-only-channel", true, "Admin has unrestricted access"); +} catch (Exception $e) { + printResult("Admin publish to admin-only-channel", false, $e->getMessage()); +} + +try { + $result = $admin->history() + ->channel('admin-only-channel') + ->count(5) + ->sync(); + printResult("Admin read from admin-only-channel", true, "Admin has unrestricted access"); +} catch (Exception $e) { + printResult("Admin read from admin-only-channel", false, $e->getMessage()); +} +// snippet.end + +echo "\n"; + +// Step 8: Revoke the token +echo "πŸ—‘οΈ Step 8: Revoke token and test access\n"; +echo "--------------------------------------\n"; + +// snippet.revoke_token +try { + $admin->setToken($token); + print("Revoking token {{$token}}...\n"); + $revokeResult = $admin->revokeToken()->token($token)->sync(); + print_r($revokeResult); + $admin->setToken(''); + printResult("Admin revoke token", true); + // token revoke propagation might take some time + $attempts = 0; + $waitForRevoke = getenv('WAIT_FOR_REVOKE') ?? true; + while ($waitForRevoke) { + $attempts++; + print("Attempt: $attempts\n"); + if ($attempts > 10) { + printResult("User publish after token revocation", false, "Should have failed but succeeded"); + break; + } + sleep(10); + // Test user access after revocation (should fail) + try { + $publishResult = $user->publish() + ->channel('public-channel') + ->message(['text' => 'Hello after revocation!']) + ->sync(); + // print_r($publishResult); + // printResult("User publish after token revocation", false, "Should have failed but succeeded"); + } catch (Exception $e) { + printResult("User publish after token revocation", true, "Correctly denied - token revoked"); + break; + } + } +} catch (Exception $e) { + printResult("Admin revoke token", false, $e->getMessage()); +} +// snippet.end + +echo "\n"; + +// Step 9: Pattern-based permissions example +echo "🎯 Step 9: Pattern-based permissions example\n"; +echo "-------------------------------------------\n"; + +// snippet.pattern_permissions +try { + $patternToken = $admin->grantToken() + ->ttl(30) + ->authorizedUuid('regular-user') + ->addChannelPatterns([ + 'news.*' => ['read' => true], // Read access to all channels starting with 'news' + 'chat.*' => ['read' => true, 'write' => true] // Full access to all 'chat' channels + ]) + ->addUuidPatterns([ + 'user.*' => ['get' => true] // Read access to all UUIDs starting with 'user' + ]) + ->sync(); + + printResult("Grant pattern-based token", true); + echo "Pattern token: " . substr($patternToken, 0, 50) . "...\n"; + + // Parse pattern token + try { + $parsedPatternToken = $admin->parseToken($patternToken); + printResult("Parse pattern token", true); + + echo "\nPattern Permissions:\n"; + $patterns = $parsedPatternToken->getPatterns(); + if (isset($patterns['chan'])) { + foreach ($patterns['chan'] as $pattern => $perms) { + $grantedPerms = []; + if (is_array($perms)) { + $grantedPerms = array_keys(array_filter($perms)); + } + echo "- Channel pattern '$pattern': " . implode(', ', $grantedPerms) . "\n"; + } + } + if (isset($patterns['uuid'])) { + foreach ($patterns['uuid'] as $pattern => $perms) { + $grantedPerms = []; + if (is_array($perms)) { + $grantedPerms = array_keys(array_filter($perms)); + } + echo "- UUID pattern '$pattern': " . implode(', ', $grantedPerms) . "\n"; + } + } + } catch (Exception $e) { + printResult("Parse pattern token", false, $e->getMessage()); + } + + // Test pattern-based access + $user->setToken($patternToken); + + try { + $result = $user->history() + ->channel('news-sports') + ->count(1) + ->sync(); + printResult("User read 'news-sports' with pattern token", true, "Pattern matched - access granted"); + } catch (Exception $e) { + printResult("User read 'news-sports' with pattern token", false, $e->getMessage()); + } + + try { + $result = $user->publish() + ->channel('chat-general') + ->message(['text' => 'Hello chat!']) + ->sync(); + printResult("User publish to 'chat-general' with pattern token", true, "Pattern matched - access granted"); + } catch (Exception $e) { + printResult("User publish to 'chat-general' with pattern token", false, $e->getMessage()); + } + + try { + $user->publish() + ->channel('news-politics') + ->message(['text' => 'Cannot write here']) + ->sync(); + printResult("User publish to 'news-politics' with pattern token", false, "Should have failed but succeeded"); + } catch (Exception $e) { + printResult("User publish to 'news-politics' with pattern token", true, "Correctly denied - read only"); + } +} catch (Exception $e) { + printResult("Grant pattern-based token", false, $e->getMessage()); +} +// snippet.end + +echo "\nπŸŽ‰ Access Manager Demo Complete!\n"; diff --git a/examples/AppContext.php b/examples/AppContext.php new file mode 100644 index 00000000..7679fc20 --- /dev/null +++ b/examples/AppContext.php @@ -0,0 +1,496 @@ +setSubscribeKey($subscribeKey); +$config->setPublishKey($publishKey); +$config->setUserId("php-app-context-sample-" . time()); + +$pubnub = new PubNub($config); +// snippet.end + +// snippet.sample_data +$sampleUsers = [ + [ + 'id' => 'user_alice_' . time(), + 'name' => 'Alice Johnson', + 'email' => 'alice@example.com', + 'externalId' => 'EXT_ALICE_001', + 'profileUrl' => 'https://example.com/profiles/alice.jpg', + 'custom' => [ + 'department' => 'Engineering', + 'role' => 'Senior Developer' + ] + ], + [ + 'id' => 'user_bob_' . time(), + 'name' => 'Bob Smith', + 'email' => 'bob@example.com', + 'externalId' => 'EXT_BOB_001', + 'profileUrl' => 'https://example.com/profiles/bob.jpg', + 'custom' => [ + 'department' => 'Marketing', + 'role' => 'Marketing Manager' + ] + ] +]; + +$sampleChannels = [ + [ + 'id' => 'channel_general_' . time(), + 'name' => 'General Discussion', + 'description' => 'Main channel for general discussions', + 'custom' => [ + 'category' => 'company', + 'public' => true + ] + ], + [ + 'id' => 'channel_dev_' . time(), + 'name' => 'Development Team', + 'description' => 'Development team coordination', + 'custom' => [ + 'category' => 'team', + 'public' => false + ] + ] +]; +// snippet.end + +// snippet.set_user_metadata +foreach ($sampleUsers as $user) { + $setUserMetadataResult = $pubnub->setUuidMetadata() + ->uuid($user['id']) + ->name($user['name']) + ->email($user['email']) + ->externalId($user['externalId']) + ->profileUrl($user['profileUrl']) + ->custom($user['custom']) + ->sync(); + assert($setUserMetadataResult->getId()); + assert($setUserMetadataResult->getName() === $user['name']); + assert($setUserMetadataResult->getEmail() === $user['email']); + assert($setUserMetadataResult->getExternalId() === $user['externalId']); + assert($setUserMetadataResult->getProfileUrl() === $user['profileUrl']); + assert(json_encode($setUserMetadataResult->getCustom()) === json_encode($user['custom'])); +} +// snippet.end + +// snippet.get_user_metadata +$getUserMetadataResult = $pubnub->getUuidMetadata() + ->uuid($sampleUsers[0]['id']) + ->sync(); +assert($getUserMetadataResult->getId() === $sampleUsers[0]['id']); +assert($getUserMetadataResult->getName() === $sampleUsers[0]['name']); +assert($getUserMetadataResult->getEmail() === $sampleUsers[0]['email']); +// snippet.end + +// snippet.get_all_user_metadata +$getAllUserMetadataResult = $pubnub->getAllUuidMetadata() + ->includeFields(['customFields' => true, 'totalCount' => true]) + ->limit(10) + ->sync(); +assert(count($getAllUserMetadataResult->getData()) >= 2); +assert($getAllUserMetadataResult->getTotalCount() >= 2); +assert(isInstanceOf($getAllUserMetadataResult->getData()[0], PNGetUUIDMetadataResult::class)); +// snippet.end + +// snippet.update_user_metadata +$updatedCustomData = [ + 'department' => 'Engineering - Updated', + 'role' => 'Lead Developer', + 'lastUpdated' => date('Y-m-d H:i:s') +]; + +$updateUserMetadataResult = $pubnub->setUuidMetadata() + ->uuid($sampleUsers[0]['id']) + ->name($sampleUsers[0]['name'] . ' - Updated') + ->email($sampleUsers[0]['email']) + ->externalId($sampleUsers[0]['externalId']) + ->profileUrl($sampleUsers[0]['profileUrl']) + ->custom($updatedCustomData) + ->sync(); +assert($updateUserMetadataResult->getName() === $sampleUsers[0]['name'] . ' - Updated'); +// snippet.end + +// snippet.remove_user_metadata +$removeUserMetadataResult = $pubnub->removeUuidMetadata() + ->uuid($sampleUsers[1]['id']) + ->sync(); +assert($removeUserMetadataResult); +// snippet.end + +// snippet.get_after_remove_error_handling +// Verify user was removed by checking it's no longer in getUuidMetadata +try { + $getAfterRemoveResult = $pubnub->getUuidMetadata() + ->uuid($sampleUsers[1]['id']) + ->sync(); +} catch (PubNubServerException $e) { + assert($e->getStatusCode() === 404); +} +// snippet.end + +// Step 3: Channel Management Operations + +// snippet.set_channel_metadata +foreach ($sampleChannels as $channel) { + $setChannelMetadataResult = $pubnub->setChannelMetadata() + ->channel($channel['id']) + ->setName($channel['name']) + ->setDescription($channel['description']) + ->setCustom($channel['custom']) + ->sync(); + assert($setChannelMetadataResult->getId() === $channel['id']); + assert($setChannelMetadataResult->getName() === $channel['name']); + assert($setChannelMetadataResult->getDescription() === $channel['description']); +} +// snippet.end + +// snippet.get_channel_metadata +$getChannelMetadataResult = $pubnub->getChannelMetadata() + ->channel($sampleChannels[0]['id']) + ->sync(); +assert($getChannelMetadataResult->getId() === $sampleChannels[0]['id']); +assert($getChannelMetadataResult->getName() === $sampleChannels[0]['name']); +// snippet.end + +// snippet.get_all_channel_metadata +$getAllChannelMetadataResult = $pubnub->getAllChannelMetadata() + ->includeFields(['customFields' => true, 'totalCount' => true]) + ->limit(10) + ->sync(); +assert(count($getAllChannelMetadataResult->getData()) >= 2); +// snippet.end + +// snippet.update_channel_metadata +$updatedChannelCustom = [ + 'category' => 'company-updated', + 'public' => true, + 'lastModified' => date('Y-m-d H:i:s') +]; + +$updateChannelMetadataResult = $pubnub->setChannelMetadata() + ->channel($sampleChannels[0]['id']) + ->setName($sampleChannels[0]['name'] . ' - Updated') + ->setDescription($sampleChannels[0]['description'] . ' - Updated') + ->setCustom($updatedChannelCustom) + ->sync(); +assert($updateChannelMetadataResult->getName() === $sampleChannels[0]['name'] . ' - Updated'); +// snippet.end + +// snippet.remove_channel_metadata +$removeChannelMetadataResult = $pubnub->removeChannelMetadata() + ->channel($sampleChannels[1]['id']) + ->sync(); +assert($removeChannelMetadataResult); +// snippet.end + +// Step 4: Channel Membership Operations (User-centric) + +// snippet.set_channel_memberships +$memberships = [ + new PNChannelMembership($sampleChannels[0]['id']), + new PNChannelMembership($sampleChannels[1]['id']) +]; + +$setMembershipsResult = $pubnub->setMemberships() + ->uuid($sampleUsers[0]['id']) + ->memberships($memberships) + ->sync(); +assert(count($setMembershipsResult->getData()) >= 1); +// snippet.end + +// snippet.get_channel_memberships +$membershipIncludes = new PNMembershipIncludes(); +$membershipIncludes->custom()->channel()->channelCustom(); + +$getMembershipsResult = $pubnub->getMemberships() + ->uuid($sampleUsers[0]['id']) + ->include($membershipIncludes) + ->sync(); +assert(count($getMembershipsResult->getData()) >= 1); +// snippet.end + +// snippet.manage_channel_memberships +$setMembershipsList = [new PNChannelMembership($sampleChannels[0]['id'])]; +$removeMembershipsList = [new PNChannelMembership($sampleChannels[1]['id'])]; + +$manageMembershipsResult = $pubnub->manageMemberships() + ->uuid($sampleUsers[0]['id']) + ->setMemberships($setMembershipsList) + ->removeMemberships($removeMembershipsList) + ->sync(); +assert(count($manageMembershipsResult->getData()) >= 0); +// snippet.end + +// snippet.remove_channel_memberships +$removeMembershipsList = [new PNChannelMembership($sampleChannels[0]['id'])]; + +$removeMembershipsResult = $pubnub->removeMemberships() + ->uuid($sampleUsers[0]['id']) + ->memberships($removeMembershipsList) + ->sync(); +assert($removeMembershipsResult); +// snippet.end + +// Step 5: Channel Members Operations (Channel-centric) + +// snippet.set_channel_members +$channelMembers = [ + new PNChannelMember($sampleUsers[0]['id']), + new PNChannelMember($sampleUsers[1]['id']) +]; + +$setMembersResult = $pubnub->setMembers() + ->channel($sampleChannels[0]['id']) + ->members($channelMembers) + ->sync(); +assert(count($setMembersResult->getData()) >= 1); +// snippet.end + +// snippet.get_channel_members +$memberIncludes = new PNMemberIncludes(); +$memberIncludes->custom()->user()->userCustom(); + +$getMembersResult = $pubnub->getMembers() + ->channel($sampleChannels[0]['id']) + ->include($memberIncludes) + ->sync(); +assert(count($getMembersResult->getData()) >= 1); +// snippet.end + +// snippet.manage_channel_members +$setMembersList = [new PNChannelMember($sampleUsers[0]['id'])]; +$removeMembersList = [new PNChannelMember($sampleUsers[1]['id'])]; + +$manageMembersResult = $pubnub->manageMembers() + ->channel($sampleChannels[0]['id']) + ->setMembers($setMembersList) + ->removeMembers($removeMembersList) + ->sync(); +assert(count($manageMembersResult->getData()) >= 0); +// snippet.end + +// snippet.remove_channel_members +$removeMembersList = [new PNChannelMember($sampleUsers[0]['id'])]; + +$removeMembersResult = $pubnub->removeMembers() + ->channel($sampleChannels[0]['id']) + ->members($removeMembersList) + ->sync(); +assert($removeMembersResult); +// snippet.end + +// Step 6: Advanced Features (pagination, filtering, sorting) + +// snippet.pagination_example +// Get first page of users +$firstPageResult = $pubnub->getAllUuidMetadata() + ->includeFields(['totalCount' => true]) + ->limit(1) // Small limit to demonstrate pagination + ->sync(); + +echo "First page: " . count($firstPageResult->getData()) . " users\n"; +echo "Total count: " . $firstPageResult->getTotalCount() . "\n"; + +// Get next page if available +if ($firstPageResult->getNext()) { + $secondPageResult = $pubnub->getAllUuidMetadata() + ->includeFields(['totalCount' => true]) + ->limit(1) + ->page(['next' => $firstPageResult->getNext()]) + ->sync(); + echo "Second page: " . count($secondPageResult->getData()) . " users\n"; +} +// snippet.end + +// snippet.filtering_example +// Filter users by custom field (note: filtering by custom properties may have limitations) +$filteredUsersResult = $pubnub->getAllUuidMetadata() + ->includeFields(['customFields' => true]) + ->filter("name LIKE '*Alice*'") + ->sync(); + +echo "Filtered users (name contains 'Alice'): " . count($filteredUsersResult->getData()) . "\n"; + +// Filter channels by name +$filteredChannelsResult = $pubnub->getAllChannelMetadata() + ->includeFields(['customFields' => true]) + ->filter("name LIKE '*General*'") + ->sync(); + +echo "Filtered channels (name contains 'General'): " . count($filteredChannelsResult->getData()) . "\n"; +// snippet.end + +// snippet.sorting_example +// Sort users by name ascending +$sortedUsersAscResult = $pubnub->getAllUuidMetadata() + ->sort(['name' => 'asc']) + ->limit(5) + ->sync(); + +echo "Users sorted by name (ascending): " . count($sortedUsersAscResult->getData()) . "\n"; +foreach ($sortedUsersAscResult->getData() as $user) { + echo " - " . ($user->getName() ?: $user->getId()) . "\n"; +} + +// Sort users by updated time descending +$sortedUsersDescResult = $pubnub->getAllUuidMetadata() + ->sort(['updated' => 'desc']) + ->limit(3) + ->sync(); + +echo "Users sorted by updated time (descending): " . count($sortedUsersDescResult->getData()) . "\n"; + +// Sort channels by name +$sortedChannelsResult = $pubnub->getAllChannelMetadata() + ->sort(['name' => 'asc']) + ->limit(5) + ->sync(); + +echo "Channels sorted by name: " . count($sortedChannelsResult->getData()) . "\n"; +// snippet.end + +// snippet.custom_fields_example +// Create user with complex custom data +$complexCustomData = [ + 'profile' => [ + 'avatar' => 'https://example.com/avatar.jpg', + 'bio' => 'Software engineer with 10+ years experience', + 'social' => [ + 'twitter' => '@johndoe', + 'linkedin' => '/in/johndoe' + ] + ], + 'preferences' => [ + 'theme' => 'dark', + 'language' => 'en', + 'notifications' => [ + 'email' => true, + 'push' => false, + 'sms' => true + ] + ], + 'metadata' => [ + 'created' => date('Y-m-d H:i:s'), + 'source' => 'api', + 'version' => '1.0' + ] +]; + +$complexUserResult = $pubnub->setUuidMetadata() + ->uuid('user_complex_' . time()) + ->name('John Doe') + ->email('john.doe@example.com') + ->custom($complexCustomData) + ->sync(); + +echo "Created user with complex custom data: " . $complexUserResult->getName() . "\n"; +echo "Custom data keys: " . implode(', ', array_keys((array)$complexUserResult->getCustom())) . "\n"; +// snippet.end + +// snippet.include_fields_example +// Get users with selective field inclusion +$selectiveFieldsResult = $pubnub->getAllUuidMetadata() + ->includeFields(['customFields' => true, 'totalCount' => false]) + ->limit(2) + ->sync(); + +echo "Users with custom fields (no total count): " . count($selectiveFieldsResult->getData()) . "\n"; +echo "Total count included: " . ($selectiveFieldsResult->getTotalCount() ? 'Yes' : 'No') . "\n"; + +// Get memberships with channel details (using deprecated method for compatibility) +if (!empty($sampleUsers)) { + try { + $membershipWithChannelResult = $pubnub->getMemberships() + ->uuid($sampleUsers[0]['id']) + ->includeFields(['channelFields' => true, 'customFields' => true]) + ->limit(5) + ->sync(); + + echo "Memberships with channel details: " . count($membershipWithChannelResult->getData()) . "\n"; + } catch (PubNubServerException $e) { + echo "Memberships query skipped (no memberships exist): " . $e->getStatusCode() . "\n"; + } +} +// snippet.end + +// snippet.etag_example +// Get user metadata with ETag +$userWithEtagResult = $pubnub->getUuidMetadata() + ->uuid($sampleUsers[0]['id']) + ->sync(); + +$etag = $userWithEtagResult->getETag(); +var_dump($etag); +echo "User ETag: " . ($etag ?: 'Not available') . "\n"; + +// Attempt conditional update using ETag +if ($etag) { + try { + $conditionalUpdateResult = $pubnub->setUuidMetadata() + ->uuid($sampleUsers[0]['id']) + ->name($sampleUsers[0]['name'] . ' - Conditional Update') + ->email($sampleUsers[0]['email']) + ->ifMatchesEtag($etag) + ->sync(); + + echo "Conditional update successful: " . $conditionalUpdateResult->getName() . "\n"; + } catch (PubNubServerException $e) { + if ($e->getStatusCode() === 412) { + echo "Conditional update failed: ETag mismatch (HTTP 412)\n"; + } else { + echo "Update failed: " . $e->getMessage() . "\n"; + } + } +} +// snippet.end + +// snippet.combined_advanced_features +// Simplified complex query to avoid filter issues +$advancedQueryResult = $pubnub->getAllUuidMetadata() + ->includeFields(['customFields' => true, 'totalCount' => true]) + ->sort(['name' => 'asc']) + ->limit(5) + ->sync(); + +echo "Advanced query results: " . count($advancedQueryResult->getData()) . " users\n"; +echo "Total available: " . ($advancedQueryResult->getTotalCount() ?: 'Unknown') . "\n"; +echo "Next page available: " . ($advancedQueryResult->getNext() ? 'Yes' : 'No') . "\n"; + +// Get channel members with comprehensive includes (using deprecated method for compatibility) +if (!empty($sampleChannels)) { + try { + $comprehensiveMembersResult = $pubnub->getMembers() + ->channel($sampleChannels[0]['id']) + ->includeFields(['customFields' => true, 'UUIDFields' => true, 'totalCount' => true]) + ->sort(['name' => 'asc']) + ->limit(10) + ->sync(); + + echo "Channel members with full details: " . count($comprehensiveMembersResult->getData()) . "\n"; + } catch (PubNubServerException $e) { + echo "Channel members query skipped (no members exist): " . $e->getStatusCode() . "\n"; + } +} +// snippet.end diff --git a/examples/FileSharing.php b/examples/FileSharing.php index b855bbd4..67a9b5f0 100644 --- a/examples/FileSharing.php +++ b/examples/FileSharing.php @@ -5,8 +5,9 @@ use PubNub\PubNub; use PubNub\PNConfiguration; +// snippet.setup $channelName = "file-channel"; -$fileName = "pn.gif"; +$testFileName = "pn.gif"; $config = new PNConfiguration(); $config->setSubscribeKey(getenv('SUBSCRIBE_KEY', 'demo')); @@ -14,19 +15,51 @@ $config->setUserId('example'); $pubnub = new PubNub($config); +// snippet.end -// Sending file -$fileHandle = fopen(__DIR__ . DIRECTORY_SEPARATOR . $fileName, "r"); +// snippet.send_file +$fileHandle = fopen(__DIR__ . DIRECTORY_SEPARATOR . $testFileName, "r"); $sendFileResult = $pubnub->sendFile() ->channel($channelName) - ->fileName($fileName) + ->fileName($testFileName) ->message("Hello from PHP SDK") ->fileHandle($fileHandle) ->sync(); - fclose($fileHandle); +$fileId = $sendFileResult->getFileId(); +$fileName = $sendFileResult->getFileName(); + +print("File uploaded successfully: {$fileName} with ID: {$fileId}\n"); +// snippet.end + +// snippet.send_file_with_just_content +$fileContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $testFileName); +$sendFileResult = $pubnub->sendFile() + ->channel($channelName) + ->fileName($testFileName) + ->message("Hello from PHP SDK") + ->fileContent($fileContent) + ->sync(); +$fileId = $sendFileResult->getFileId(); +$fileName = $sendFileResult->getFileName(); +print("File uploaded successfully: {$fileName} with ID: {$fileId}\n"); +// snippet.end + +// snippet.publish_file_with_message +$publishFileMessageResult = $pubnub->publishFileMessage() + ->channel($channelName) + ->fileId($fileId) + ->fileName($testFileName) + ->message("Hello from PHP SDK") + ->ttl(10) + ->meta(["key" => "value"]) + ->customMessageType("custom") + ->sync(); +$timestamp = $publishFileMessageResult->getTimestamp(); +print("File message published successfully: {$timestamp}\n"); +// snippet.end -// Listing files in the channel +// snippet.list_files $channelFiles = $pubnub->listFiles()->channel($channelName)->sync(); $fileCount = $channelFiles->getCount(); if ($fileCount > 0) { @@ -38,19 +71,21 @@ } else { print("There are no files in the channel {$channelName}\n"); } +// snippet.end +// snippet.get_download_url $file = $channelFiles->getFiles()[0]; -print('Getting download URL for the file...'); +print("Getting download URL for the file...\n"); $downloadUrl = $pubnub->getFileDownloadUrl() ->channel($channelName) ->fileId($file->getId()) ->fileName($file->getName()) ->sync(); -printf("To download the file use the following URL: %s\n", $downloadUrl->getFileUrl()); +print("To download the file use the following URL: {$downloadUrl->getFileUrl()}\n"); -print("Downloading file..."); +print("Downloading file... "); $downloadFile = $pubnub->downloadFile() ->channel($channelName) ->fileId($file->getId()) @@ -58,8 +93,9 @@ ->sync(); file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . $file->getName(), $downloadFile->getFileContent()); print("done. File saved as: {$file->getName()}\n"); +// snippet.end -// deleting file +// snippet.delete_file $deleteFile = $pubnub->deleteFile() ->channel($channelName) ->fileId($file->getId()) @@ -71,3 +107,27 @@ } else { print("Failed to delete file\n"); } +// snippet.end + +// snippet.delete_all_files +$fileList = $pubnub->listFiles()->channel($channelName)->sync(); +$fileCount = $fileList->getCount(); +if ($fileCount > 0) { + print("There are {$fileCount} files left in the channel {$channelName}\n"); + foreach ($fileList->getFiles() as $idx => $file) { + $deleteFile = $pubnub->deleteFile() + ->channel($channelName) + ->fileId($file->getId()) + ->fileName($file->getName()) + ->sync(); + + if ($deleteFile->getStatus() === 200) { + print("File {$file->getId()} deleted successfully\n"); + } else { + print("Failed to delete file {$file->getId()}\n"); + } + } +} else { + print("There are no files in the channel {$channelName}\n"); +} +// snippet.end diff --git a/examples/MessageActions.php b/examples/MessageActions.php new file mode 100644 index 00000000..3f60d9ee --- /dev/null +++ b/examples/MessageActions.php @@ -0,0 +1,493 @@ +setSubscribeKey($subscribeKey); +$config->setPublishKey($publishKey); +$config->setUserId("php-message-actions-demo-" . time()); + +$pubnub = new PubNub($config); +// snippet.end + +// Sample data for testing +$testChannel = "message-actions-demo-" . time(); +$publishedMessages = []; + +echo "πŸ“ Setting up test environment...\n"; +echo "Channel: $testChannel\n"; +echo "User ID: " . $config->getUserId() . "\n\n"; + +// Publish some test messages to work with +echo "πŸ“€ Publishing test messages...\n"; +for ($i = 1; $i <= 3; $i++) { + try { + $result = $pubnub->publish() + ->channel($testChannel) + ->message([ + "id" => "msg_$i", + "text" => "Test message #$i for Message Actions demo", + "timestamp" => time(), + "sender" => $config->getUserId() + ]) + ->sync(); + + $publishedMessages[] = [ + 'id' => "msg_$i", + 'timetoken' => $result->getTimetoken() + ]; + echo "βœ… Published message #$i - Timetoken: {$result->getTimetoken()}\n"; + } catch (PubNubException $e) { + echo "❌ Failed to publish message #$i: " . $e->getMessage() . "\n"; + } +} + +echo "\n"; + +// Phase 2: Core Message Actions Features + +// 2.1 Add Message Actions +echo "=== PHASE 2.1: Adding Message Actions ===\n\n"; + +$messageActions = []; + +// Add emoji reactions +echo "πŸ˜€ Adding emoji reactions...\n"; +$emojiReactions = [ + ['type' => 'reaction', 'value' => 'πŸ‘'], + ['type' => 'reaction', 'value' => '❀️'], + ['type' => 'reaction', 'value' => '😊'], + ['type' => 'reaction', 'value' => 'πŸŽ‰'] +]; + +foreach ($publishedMessages as $index => $msg) { + if ($index < 2) { // Add reactions to first 2 messages + $reaction = $emojiReactions[$index * 2]; + try { + // snippet.add_emoji_reaction + $messageAction = new PNMessageAction([ + 'type' => $reaction['type'], + 'value' => $reaction['value'], + 'messageTimetoken' => $msg['timetoken'] + ]); + + $result = $pubnub->addMessageAction() + ->channel($testChannel) + ->messageAction($messageAction) + ->sync(); + + $messageActions[] = [ + 'messageTimetoken' => $msg['timetoken'], + 'actionTimetoken' => $result->actionTimetoken, + 'type' => $reaction['type'], + 'value' => $reaction['value'] + ]; + + echo "βœ… Added {$reaction['value']} - Action Timetoken: {$result->actionTimetoken}\n"; + // snippet.end + } catch (PubNubException $e) { + echo "❌ Failed to add reaction to message {$msg['id']}: " . $e->getMessage() . "\n"; + } + } +} + +// Add read receipts +echo "\nπŸ“– Adding read receipts...\n"; +foreach ($publishedMessages as $index => $msg) { + try { + // snippet.add_read_receipt + $result = $pubnub->addMessageAction() + ->channel($testChannel) + ->messageAction(new PNMessageAction([ + 'type' => 'receipt', + 'value' => 'read', + 'messageTimetoken' => $msg['timetoken'] + ])) + ->sync(); + + $messageActions[] = [ + 'messageTimetoken' => $msg['timetoken'], + 'actionTimetoken' => $result->actionTimetoken, + 'type' => 'receipt', + 'value' => 'read' + ]; + + echo "βœ… Added read receipt to message {$msg['id']} - Action Timetoken: {$result->actionTimetoken}\n"; + // snippet.end + } catch (PubNubException $e) { + echo "❌ Failed to add read receipt to message {$msg['id']}: " . $e->getMessage() . "\n"; + } +} + +// Add custom metadata +echo "\n🏷️ Adding custom metadata...\n"; +$customActions = [ + ['type' => 'priority', 'value' => 'high'], + ['type' => 'category', 'value' => 'announcement'], + ['type' => 'flag', 'value' => 'important'] +]; + +foreach ($publishedMessages as $index => $msg) { + if ($index < count($customActions)) { + $action = $customActions[$index]; + try { + // snippet.add_custom_metadata + $result = $pubnub->addMessageAction() + ->channel($testChannel) + ->messageAction(new PNMessageAction([ + 'type' => $action['type'], + 'value' => $action['value'], + 'messageTimetoken' => $msg['timetoken'] + ])) + ->sync(); + + $messageActions[] = [ + 'messageTimetoken' => $msg['timetoken'], + 'actionTimetoken' => $result->actionTimetoken, + 'type' => $action['type'], + 'value' => $action['value'] + ]; + + echo "βœ… Added {$action['type']}:{$action['value']} - Action Timetoken: {$result->actionTimetoken}\n"; + // snippet.end + } catch (PubNubException $e) { + echo "❌ Failed to add custom metadata to message {$msg['id']}: " . $e->getMessage() . "\n"; + } + } +} + +echo "\n"; + +// 2.2 Retrieve Message Actions +echo "=== PHASE 2.2: Retrieving Message Actions ===\n\n"; + +// Get all message actions +echo "πŸ“‹ Retrieving all message actions...\n"; +try { + // snippet.get_all_message_actions + $result = $pubnub->getMessageActions() + ->channel($testChannel) + ->setLimit(100) + ->sync(); + + echo "βœ… Retrieved " . count($result->actions) . " message actions:\n"; + foreach ($result->actions as $action) { + echo " - {$action->type}:{$action->value} (A: {$action->actionTimetoken}, M: {$action->messageTimetoken})\n"; + } + // snippet.end +} catch (PubNubException $e) { + echo "❌ Failed to retrieve message actions: " . $e->getMessage() . "\n"; +} + +// Get message actions with time range +echo "\nπŸ“… Retrieving message actions with time range...\n"; +if (count($messageActions) >= 2) { + try { + // snippet.get_message_actions_with_range + $result = $pubnub->getMessageActions() + ->channel($testChannel) + ->setStart($messageActions[count($messageActions) - 1]['actionTimetoken']) + ->setEnd($messageActions[0]['actionTimetoken']) + ->setLimit(50) + ->sync(); + + echo "βœ… Retrieved " . count($result->actions) . " message actions in range:\n"; + foreach ($result->actions as $action) { + echo " - {$action->type}:{$action->value} (Action TT: {$action->actionTimetoken})\n"; + } + // snippet.end + } catch (PubNubException $e) { + echo "❌ Failed to retrieve message actions with range: " . $e->getMessage() . "\n"; + } +} + +echo "\n"; + +// 2.3 Fetch Messages with Actions +echo "=== PHASE 2.3: Fetching Messages with Actions ===\n\n"; + +echo "πŸ“¬ Fetching messages with their associated actions...\n"; +try { + // snippet.fetch_messages_with_actions + $result = $pubnub->fetchMessages() + ->channels([$testChannel]) + ->includeMessageActions(true) + ->includeMeta(true) + ->includeUuid(true) + ->sync(); + + $messages = $result->getChannels()[$testChannel] ?? []; + echo "βœ… Retrieved " . count($messages) . " messages with actions:\n"; + + foreach ($messages as $messageData) { + echo " πŸ“„ Message: " . json_encode($messageData->getMessage()) . "\n"; + echo " Timetoken: {$messageData->getTimetoken()}\n"; + echo " UUID: {$messageData->getUuid()}\n"; + + $actions = $messageData->getActions(); + if (!empty($actions)) { + echo " Actions:\n"; + foreach ($actions as $actionType => $actionValues) { + foreach ($actionValues as $actionValue => $actionDetails) { + foreach ($actionDetails as $actionDetail) { + echo " - {$actionType}:{$actionValue} by {$actionDetail['uuid']} " + . "at {$actionDetail['actionTimetoken']}\n"; + } + } + } + } + echo "\n"; + } + // snippet.end +} catch (PubNubException $e) { + echo "❌ Failed to fetch messages with actions: " . $e->getMessage() . "\n"; +} + +// 2.4 Remove Message Actions +echo "=== PHASE 2.4: Removing Message Actions ===\n\n"; + +echo "πŸ—‘οΈ Removing some message actions...\n"; +$actionsToRemove = array_slice($messageActions, 0, 2); // Remove first 2 actions + +foreach ($actionsToRemove as $actionToRemove) { + try { + // snippet.remove_message_action + $result = $pubnub->removeMessageAction() + ->channel($testChannel) + ->messageTimetoken($actionToRemove['messageTimetoken']) + ->actionTimetoken($actionToRemove['actionTimetoken']) + ->sync(); + + echo "βœ… Removed {$actionToRemove['type']}:{$actionToRemove['value']} action " + . "(A: {$actionToRemove['actionTimetoken']}, M: {$actionToRemove['messageTimetoken']})\n"; + // snippet.end + } catch (PubNubException $e) { + echo "❌ Failed to remove message action: " . $e->getMessage() . "\n"; + } +} + +echo "\n"; + +// Phase 3: Error Handling and Edge Cases +echo "=== PHASE 3: Error Handling and Edge Cases ===\n\n"; + +echo "πŸ” Testing error scenarios...\n"; + +// Try to add action to non-existent message +echo "πŸ“ Testing action on non-existent message...\n"; +try { + // snippet.error_nonexistent_message + $result = $pubnub->addMessageAction() + ->channel($testChannel) + ->messageAction(new PNMessageAction([ + 'type' => 'test', + 'value' => 'error', + 'messageTimetoken' => "99999999999999999" // Non-existent timetoken + ])) + ->sync(); + + echo "⚠️ Unexpectedly succeeded adding action to non-existent message\n"; + // snippet.end +} catch (PubNubException $e) { + echo "βœ… Expected error for non-existent message: " . $e->getMessage() . "\n"; +} + +// Try to remove non-existent action +echo "\nπŸ“ Testing removal of non-existent action...\n"; +try { + // snippet.error_remove_nonexistent + $result = $pubnub->removeMessageAction() + ->channel($testChannel) + ->messageTimetoken($publishedMessages[0]['timetoken']) + ->actionTimetoken("99999999999999999") // Non-existent action timetoken + ->sync(); + + echo "βœ… Successfully handled removal of non-existent action\n"; + // snippet.end +} catch (PubNubException $e) { + echo "βœ… Expected error for non-existent action: " . $e->getMessage() . "\n"; +} + +echo "\n"; + +// Phase 4: Practical Use Cases +echo "=== PHASE 4: Practical Use Cases ===\n\n"; + +// Use Case 1: Emoji Reactions System +echo "😊 Use Case 1: Emoji Reactions System\n"; +function addEmojiReaction($pubnub, $channel, $messageTimetoken, $emoji, $userId) +{ + // snippet.emoji_reaction_system + try { + $result = $pubnub->addMessageAction() + ->channel($channel) + ->messageAction(new PNMessageAction([ + 'type' => 'reaction', + 'value' => $emoji, + 'messageTimetoken' => $messageTimetoken + ])) + ->sync(); + + return [ + 'success' => true, + 'actionTimetoken' => $result->actionTimetoken, + 'message' => "User $userId reacted with $emoji" + ]; + } catch (PubNubException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + // snippet.end +} + +$emojiResult = addEmojiReaction($pubnub, $testChannel, $publishedMessages[0]['timetoken'], 'πŸš€', $config->getUserId()); +echo " " . ($emojiResult['success'] ? "βœ…" : "❌") . " " . ($emojiResult['message'] ?? $emojiResult['error']) . "\n"; + +// Use Case 2: Read Receipts System +echo "\nπŸ“– Use Case 2: Read Receipts System\n"; +function markAsRead($pubnub, $channel, $messageTimetoken, $userId) +{ + // snippet.read_receipt_system + try { + $result = $pubnub->addMessageAction() + ->channel($channel) + ->messageAction(new PNMessageAction([ + 'type' => 'receipt', + 'value' => 'read', + 'messageTimetoken' => $messageTimetoken + ])) + ->sync(); + + return [ + 'success' => true, + 'actionTimetoken' => $result->actionTimetoken, + 'message' => "Message marked as read by $userId" + ]; + } catch (PubNubException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + // snippet.end +} + +$readResult = markAsRead($pubnub, $testChannel, $publishedMessages[1]['timetoken'], $config->getUserId()); +echo " " . ($readResult['success'] ? "βœ…" : "❌") . " " . ($readResult['message'] ?? $readResult['error']) . "\n"; + +// Use Case 3: Message Delivery Confirmations +echo "\nπŸ“¨ Use Case 3: Message Delivery Confirmations\n"; +function confirmDelivery($pubnub, $channel, $messageTimetoken, $userId) +{ + // snippet.delivery_confirmation_system + try { + $result = $pubnub->addMessageAction() + ->channel($channel) + ->messageAction(new PNMessageAction([ + 'type' => 'delivery', + 'value' => 'confirmed', + 'messageTimetoken' => $messageTimetoken + ])) + ->sync(); + + return [ + 'success' => true, + 'actionTimetoken' => $result->actionTimetoken, + 'message' => "Delivery confirmed by $userId" + ]; + } catch (PubNubException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + // snippet.end +} + +$deliveryResult = confirmDelivery($pubnub, $testChannel, $publishedMessages[2]['timetoken'], $config->getUserId()); +echo " " . ($deliveryResult['success'] ? "βœ…" : "❌") . " " + . ($deliveryResult['message'] ?? $deliveryResult['error']) . "\n"; + +// Use Case 4: Message Analytics Tagging +echo "\nπŸ“Š Use Case 4: Message Analytics Tagging\n"; +function tagForAnalytics($pubnub, $channel, $messageTimetoken, $tag, $value) +{ + // snippet.analytics_tagging_system + try { + $result = $pubnub->addMessageAction() + ->channel($channel) + ->messageAction(new PNMessageAction([ + 'type' => $tag, + 'value' => $value, + 'messageTimetoken' => $messageTimetoken + ])) + ->sync(); + + return [ + 'success' => true, + 'actionTimetoken' => $result->actionTimetoken, + 'message' => "Tagged message with $tag:$value" + ]; + } catch (PubNubException $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + // snippet.end +} + +$tagResult = tagForAnalytics($pubnub, $testChannel, $publishedMessages[0]['timetoken'], 'engagement', 'high'); +echo " " . ($tagResult['success'] ? "βœ…" : "❌") . " " . ($tagResult['message'] ?? $tagResult['error']) . "\n"; + +echo "\n"; + +// Final verification +echo "=== FINAL VERIFICATION ===\n\n"; + +echo "πŸ” Final verification - getting all remaining message actions...\n"; +try { + $finalResult = $pubnub->getMessageActions() + ->channel($testChannel) + ->setLimit(100) + ->sync(); + + echo "βœ… Final count: " . count($finalResult->actions) . " message actions remaining\n"; + + // Group actions by type for summary + $actionsByType = []; + foreach ($finalResult->actions as $action) { + $type = $action->type; + if (!isset($actionsByType[$type])) { + $actionsByType[$type] = 0; + } + $actionsByType[$type]++; + } + + echo "πŸ“Š Summary by type:\n"; + foreach ($actionsByType as $type => $count) { + echo " - $type: $count actions\n"; + } +} catch (PubNubException $e) { + echo "❌ Failed to get final verification: " . $e->getMessage() . "\n"; +} + +echo "\n=== MESSAGE ACTIONS DEMO COMPLETE ===\n"; diff --git a/examples/MessagePersistance.php b/examples/MessagePersistance.php new file mode 100644 index 00000000..8c00a0e9 --- /dev/null +++ b/examples/MessagePersistance.php @@ -0,0 +1,479 @@ +setPublishKey("demo"); + $pnConfig->setSubscribeKey("demo"); + $pnConfig->setSecretKey("demo"); // Required for delete operations + $pnConfig->setUserId("message-persistence-demo-user"); + + $this->pubnub = new PubNub($pnConfig); + + echo "=== PubNub Message Persistence Demo ===\n\n"; + } + + /** + * Step 1: Publish sample messages to have data for persistence operations + */ + public function publishSampleMessages() + { + echo "πŸ“€ Publishing sample messages...\n"; + + $messages = [ + ["content" => "First demo message", "type" => "text", "timestamp" => time()], + ["content" => "Second demo message", "type" => "text", "timestamp" => time() + 1], + ["content" => "Third demo message with metadata", "type" => "rich", "timestamp" => time() + 2], + ["content" => "Fourth message for testing", "type" => "text", "timestamp" => time() + 3], + ["content" => "Fifth and final message", "type" => "text", "timestamp" => time() + 4] + ]; + + try { + // Publish to main demo channel + foreach ($messages as $index => $message) { + $result = $this->pubnub->publish() + ->channel($this->demoChannel) + ->message($message) + ->meta(["messageIndex" => $index + 1, "demo" => true]) + ->sync(); + + $this->publishedTimetokens[] = $result->getTimetoken(); + echo " βœ“ Published message " . ($index + 1) . " - Timetoken: " . $result->getTimetoken() . "\n"; + + // Small delay to ensure different timetokens + usleep(100000); // 0.1 seconds + } + + // Publish to multiple channels for multi-channel demos + $this->pubnub->publish() + ->channel($this->multiChannel1) + ->message(["content" => "Message for channel 1", "channel" => 1]) + ->sync(); + + $this->pubnub->publish() + ->channel($this->multiChannel2) + ->message(["content" => "Message for channel 2", "channel" => 2]) + ->sync(); + + echo " βœ“ Published messages to multiple channels\n\n"; + + // Wait a bit for messages to be stored + sleep(2); + } catch (PubNubException $e) { + echo "❌ Error publishing messages: " . $e->getMessage() . "\n\n"; + } + } + + /** + * Demo 1: Fetch History (fetchMessages) - Basic Usage + */ + public function demoFetchHistoryBasic() + { + echo "πŸ“œ Demo 1: Fetch History - Basic Usage\n"; + echo "Retrieving last messages from channel...\n"; + + try { + $result = $this->pubnub->fetchMessages() + ->channels($this->demoChannel) + ->count(5) + ->sync(); + + echo " πŸ“Š Results:\n"; + echo " - Start Timetoken: " . $result->getStartTimetoken() . "\n"; + echo " - End Timetoken: " . $result->getEndTimetoken() . "\n"; + + $channels = $result->getChannels(); + if (isset($channels[$this->demoChannel])) { + $messages = $channels[$this->demoChannel]; + echo " - Message Count: " . count($messages) . "\n"; + + foreach ($messages as $index => $message) { + echo " Message " . ($index + 1) . ":\n"; + echo " Content: " . json_encode($message->getMessage()) . "\n"; + echo " Timetoken: " . $message->getTimetoken() . "\n"; + if ($message->getMetadata()) { + echo " Meta: " . json_encode($message->getMetadata()) . "\n"; + } + echo "\n"; + } + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 2: Fetch History with Advanced Options + */ + public function demoFetchHistoryAdvanced() + { + echo "πŸ“œ Demo 2: Fetch History - Advanced Options\n"; + echo "Retrieving messages with metadata, message type, and UUID info...\n"; + + try { + $result = $this->pubnub->fetchMessages() + ->channels($this->demoChannel) + ->includeMeta(true) + ->includeMessageType(true) + ->includeUuid(true) + ->sync(); + + echo " πŸ“Š Advanced Results:\n"; + $channels = $result->getChannels(); + if (isset($channels[$this->demoChannel])) { + $messages = $channels[$this->demoChannel]; + + foreach ($messages as $index => $message) { + echo " Advanced Message " . ($index + 1) . ":\n"; + echo " Content: " . json_encode($message->getMessage()) . "\n"; + echo " Timetoken: " . $message->getTimetoken() . "\n"; + echo " UUID: " . ($message->getUuid() ?? 'N/A') . "\n"; + echo " Message Type: " . ($message->getMessageType() ?? 'N/A') . "\n"; + echo " Meta: " . json_encode($message->getMetadata()) . "\n"; + echo "\n"; + } + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 3: Fetch History from Multiple Channels + */ + public function demoFetchHistoryMultiChannel() + { + echo "πŸ“œ Demo 3: Fetch History - Multiple Channels\n"; + echo "Retrieving messages from multiple channels...\n"; + + try { + $result = $this->pubnub->fetchMessages() + ->channels([$this->multiChannel1, $this->multiChannel2]) + ->includeMeta(true) + ->sync(); + + echo " πŸ“Š Multi-Channel Results:\n"; + $channels = $result->getChannels(); + + foreach ($channels as $channelName => $messages) { + echo " Channel: $channelName\n"; + echo " Message Count: " . count($messages) . "\n"; + + foreach ($messages as $index => $message) { + echo " Message " . ($index + 1) . ": " . json_encode($message->getMessage()) . "\n"; + } + echo "\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 4: History Method - Basic Usage + */ + public function demoHistoryBasic() + { + echo "πŸ“š Demo 4: History Method - Basic Usage\n"; + echo "Using history() method to retrieve messages...\n"; + + try { + $result = $this->pubnub->history() + ->channel($this->demoChannel) + ->count(5) + ->includeTimetoken(true) + ->sync(); + + echo " πŸ“Š History Results:\n"; + echo " - Start Timetoken: " . $result->getStartTimetoken() . "\n"; + echo " - End Timetoken: " . $result->getEndTimetoken() . "\n"; + + $messages = $result->getMessages(); + echo " - Message Count: " . count($messages) . "\n"; + + foreach ($messages as $index => $message) { + echo " Message " . ($index + 1) . ":\n"; + echo " Content: " . json_encode($message->getEntry()) . "\n"; + echo " Timetoken: " . $message->getTimetoken() . "\n"; + echo "\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 5: History Method - Reverse Order (Oldest First) + */ + public function demoHistoryReverse() + { + echo "πŸ“š Demo 5: History Method - Reverse Order (Oldest First)\n"; + echo "Retrieving oldest 3 messages first...\n"; + + try { + $result = $this->pubnub->history() + ->channel($this->demoChannel) + ->count(3) + ->reverse(true) + ->includeTimetoken(true) + ->sync(); + + echo " πŸ“Š Reverse History Results:\n"; + $messages = $result->getMessages(); + + foreach ($messages as $index => $message) { + echo " Oldest Message " . ($index + 1) . ":\n"; + echo " Content: " . json_encode($message->getEntry()) . "\n"; + echo " Timetoken: " . $message->getTimetoken() . "\n"; + echo "\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 6: History Method - Time Range + */ + public function demoHistoryTimeRange() + { + echo "πŸ“š Demo 6: History Method - Time Range\n"; + + if (count($this->publishedTimetokens) >= 2) { + $startTime = $this->publishedTimetokens[0]; + $endTime = $this->publishedTimetokens[2]; // Get messages between first and third + + echo "Retrieving messages between timetoken $startTime and $endTime...\n"; + + try { + $result = $this->pubnub->history() + ->channel($this->demoChannel) + ->start($startTime) + ->end($endTime) + ->includeTimetoken(true) + ->sync(); + + echo " πŸ“Š Time Range Results:\n"; + $messages = $result->getMessages(); + echo " - Message Count: " . count($messages) . "\n"; + + foreach ($messages as $index => $message) { + echo " Message " . ($index + 1) . ":\n"; + echo " Content: " . json_encode($message->getEntry()) . "\n"; + echo " Timetoken: " . $message->getTimetoken() . "\n"; + echo "\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + } else { + echo " ⚠️ Not enough published messages for time range demo\n"; + } + + echo "\n"; + } + + /** + * Demo 7: Message Counts + */ + public function demoMessageCounts() + { + echo "πŸ”’ Demo 7: Message Counts\n"; + echo "Getting message counts for channels...\n"; + + try { + // Get the oldest timetoken for counting + $oldestTimetoken = !empty($this->publishedTimetokens) ? + min($this->publishedTimetokens) : "0"; + + $result = $this->pubnub->messageCounts() + ->channels([$this->demoChannel]) + ->channelsTimetoken([$oldestTimetoken]) + ->sync(); + + echo " πŸ“Š Message Count Results:\n"; + $channels = $result->getChannels(); + + foreach ($channels as $channelName => $count) { + echo " Channel '$channelName': $count messages\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 8: Message Counts - Multiple Channels + */ + public function demoMessageCountsMultiple() + { + echo "πŸ”’ Demo 8: Message Counts - Multiple Channels\n"; + echo "Getting message counts for multiple channels...\n"; + + try { + $result = $this->pubnub->messageCounts() + ->channels([$this->demoChannel, $this->multiChannel1, $this->multiChannel2]) + ->channelsTimetoken(["0"]) // Single timetoken for all channels + ->sync(); + + echo " πŸ“Š Multiple Channel Count Results:\n"; + $channels = $result->getChannels(); + + foreach ($channels as $channelName => $count) { + echo " Channel '$channelName': $count messages\n"; + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Demo 9: Delete Messages from History + * Note: This requires secret key and proper permissions + */ + public function demoDeleteMessages() + { + echo "πŸ—‘οΈ Demo 9: Delete Messages from History\n"; + echo "Attempting to delete specific message from history...\n"; + + if (!empty($this->publishedTimetokens)) { + // Delete the first message by using its timetoken + $targetTimetoken = $this->publishedTimetokens[0]; + $startTimetoken = $targetTimetoken - 1; + $endTimetoken = $targetTimetoken; + + echo " Deleting message with timetoken: $targetTimetoken\n"; + echo " Using range: $startTimetoken to $endTimetoken\n"; + + try { + $this->pubnub->deleteMessages() + ->channel($this->demoChannel) + ->start($startTimetoken) + ->end($endTimetoken) + ->sync(); + + echo " βœ… Delete operation completed successfully\n"; + echo " Note: Message deletion may take a moment to reflect in history\n"; + } catch (PubNubException $e) { + echo " ⚠️ Delete operation failed: " . $e->getMessage() . "\n"; + echo " Note: Delete operations require proper key permissions and secret key\n"; + } + } else { + echo " ⚠️ No published messages available for deletion demo\n"; + } + + echo "\n"; + } + + /** + * Demo 10: Verify Deletion + */ + public function demoVerifyDeletion() + { + echo "πŸ” Demo 10: Verify Deletion\n"; + echo "Checking if message was deleted...\n"; + + try { + // Wait a moment for deletion to process + sleep(2); + + $result = $this->pubnub->fetchMessages() + ->channels($this->demoChannel) + ->count(10) + ->sync(); + + echo " πŸ“Š Current Message Count After Deletion:\n"; + $channels = $result->getChannels(); + if (isset($channels[$this->demoChannel])) { + $messages = $channels[$this->demoChannel]; + echo " - Messages remaining: " . count($messages) . "\n"; + + // Check if the first timetoken is still present + $remainingTimetokens = array_map(function ($msg) { + return $msg->getTimetoken(); + }, $messages); + + $deletedTimetoken = $this->publishedTimetokens[0] ?? null; + if ($deletedTimetoken && !in_array($deletedTimetoken, $remainingTimetokens)) { + echo " βœ… Message with timetoken $deletedTimetoken was successfully deleted\n"; + } else { + echo " ⚠️ Message may still be present (deletion can take time to propagate)\n"; + } + } + } catch (PubNubException $e) { + echo "❌ Error: " . $e->getMessage() . "\n"; + } + + echo "\n"; + } + + /** + * Run all demos + */ + public function runAllDemos() + { + // Step 1: Setup data + $this->publishSampleMessages(); + + // Step 2: Fetch History demos + $this->demoFetchHistoryBasic(); + $this->demoFetchHistoryAdvanced(); + $this->demoFetchHistoryMultiChannel(); + + // Step 3: History method demos + $this->demoHistoryBasic(); + $this->demoHistoryReverse(); + $this->demoHistoryTimeRange(); + + // Step 4: Message count demos + $this->demoMessageCounts(); + $this->demoMessageCountsMultiple(); + + // Step 5: Delete operations (may require proper permissions) + $this->demoDeleteMessages(); + $this->demoVerifyDeletion(); + + echo "πŸŽ‰ All Message Persistence demos completed!\n"; + } +} + +// Run the demo +if (file_exists('vendor/autoload.php')) { + $demo = new MessagePersistenceDemo(); + $demo->runAllDemos(); +} else { + echo "❌ Please run 'composer install' to install PubNub SDK dependencies first.\n"; + echo "Make sure you have composer.json with PubNub dependency configured.\n"; +} diff --git a/examples/MobilePush.php b/examples/MobilePush.php new file mode 100644 index 00000000..6d0e49a1 --- /dev/null +++ b/examples/MobilePush.php @@ -0,0 +1,400 @@ +initializePubNub(); + $this->initializeSampleData(); + } + + private function initializePubNub() + { + // snippet.setup + echo "πŸš€ Initializing PubNub Mobile Push Demo...\n"; + echo "================================================\n\n"; + + $pnConfig = new PNConfiguration(); + $pnConfig->setSubscribeKey(getenv('SUBSCRIBE_KEY') ?: 'demo'); + $pnConfig->setPublishKey(getenv('PUBLISH_KEY') ?: 'demo'); + $pnConfig->setUserId('php-mobile-push-demo-' . time()); + + $this->pubnub = new PubNub($pnConfig); + + echo "βœ… PubNub configured with demo keys\n"; + echo "πŸ“± User ID: " . $pnConfig->getUserId() . "\n\n"; + // snippet.end + } + + private function initializeSampleData() + { + // snippet.sample_data + $this->sampleChannels = [ + 'news-channel-' . time(), + 'alerts-channel-' . time(), + 'promotions-channel-' . time(), + 'updates-channel-' . time() + ]; + + $this->sampleDevices = [ + 'fcm' => [ + 'deviceId' => 'fcm-device-token-' . uniqid(), + 'pushType' => PNPushType::FCM, + 'name' => 'Android Device (FCM)' + ], + 'apns2_dev' => [ + 'deviceId' => 'apns2-dev-token-' . uniqid(), + 'pushType' => PNPushType::APNS2, + 'environment' => 'development', + 'topic' => 'com.example.myapp', + 'name' => 'iOS Device (APNS2 - Development)' + ], + 'apns2_prod' => [ + 'deviceId' => 'apns2-prod-token-' . uniqid(), + 'pushType' => PNPushType::APNS2, + 'environment' => 'production', + 'topic' => 'com.example.myapp', + 'name' => 'iOS Device (APNS2 - Production)' + ] + ]; + // snippet.end + + echo "πŸ“‹ Sample data initialized:\n"; + echo " Channels: " . implode(', ', $this->sampleChannels) . "\n"; + echo " Devices: " . count($this->sampleDevices) . " sample devices\n\n"; + } + + public function runFullDemo() + { + echo "🎯 Starting comprehensive Mobile Push demo...\n"; + echo "==============================================\n\n"; + + // Test all features for each device type + foreach ($this->sampleDevices as $deviceKey => $device) { + $this->runDeviceDemo($deviceKey, $device); + echo "\n" . str_repeat("-", 60) . "\n\n"; + } + + echo "✨ Mobile Push demo completed successfully!\n"; + } + + private function runDeviceDemo($deviceKey, $device) + { + echo "πŸ“± Testing {$device['name']}\n"; + echo " Device ID: {$device['deviceId']}\n"; + echo " Push Type: {$device['pushType']}\n"; + + if (isset($device['environment'])) { + echo " Environment: {$device['environment']}\n"; + } + if (isset($device['topic'])) { + echo " Topic: {$device['topic']}\n"; + } + echo "\n"; + + // 1. Add device to channels + $this->demonstrateAddDeviceToChannels($device); + + // 2. List channels for device + $this->demonstrateListChannelsForDevice($device); + + // 3. Remove device from some channels + $this->demonstrateRemoveDeviceFromChannels($device); + + // 4. List channels again to verify removal + $this->demonstrateListChannelsForDevice($device, "After Removal"); + + // 5. Remove all channels from device + $this->demonstrateRemoveAllChannelsFromDevice($device); + + // 6. Final verification + $this->demonstrateListChannelsForDevice($device, "Final Verification"); + } + + private function demonstrateAddDeviceToChannels($device) + { + echo "1️⃣ Adding device to channels...\n"; + + try { + if ($device['pushType'] === PNPushType::FCM) { + // snippet.add_device_to_channel_fcm + $result = $this->pubnub->addChannelsToPush() + ->pushType(PNPushType::FCM) + ->channels($this->sampleChannels) + ->deviceId($device['deviceId']) + ->sync(); + // snippet.end + } elseif ($device['pushType'] === PNPushType::APNS2) { + // snippet.add_device_to_channel_apns2 + $result = $this->pubnub->addChannelsToPush() + ->pushType(PNPushType::APNS2) + ->channels($this->sampleChannels) + ->deviceId($device['deviceId']) + ->environment($device['environment']) + ->topic($device['topic']) + ->sync(); + // snippet.end + } + + echo " βœ… Successfully added device to " . count($this->sampleChannels) . " channels\n"; + echo " πŸ“‹ Channels: " . implode(', ', $this->sampleChannels) . "\n\n"; + } catch (PubNubServerException $e) { + echo " ❌ Server Error: " . $e->getMessage() . "\n"; + echo " πŸ” Status Code: " . $e->getStatusCode() . "\n\n"; + } catch (PubNubException $e) { + echo " ❌ PubNub Error: " . $e->getMessage() . "\n\n"; + } catch (Exception $e) { + echo " ❌ General Error: " . $e->getMessage() . "\n\n"; + } + } + + private function demonstrateListChannelsForDevice($device, $context = "") + { + $title = $context ? "2️⃣ Listing channels for device ($context)..." : "2️⃣ Listing channels for device..."; + echo "$title\n"; + + try { + if ($device['pushType'] === PNPushType::FCM) { + // snippet.list_channels_fcm + $result = $this->pubnub->listPushProvisions() + ->pushType(PNPushType::FCM) + ->deviceId($device['deviceId']) + ->sync(); + // snippet.end + } elseif ($device['pushType'] === PNPushType::APNS2) { + // snippet.list_channels_apns2 + $result = $this->pubnub->listPushProvisions() + ->pushType(PNPushType::APNS2) + ->deviceId($device['deviceId']) + ->environment($device['environment']) + ->topic($device['topic']) + ->sync(); + // snippet.end + } + + $channels = $result->getChannels(); + + if (!empty($channels)) { + echo " βœ… Device is registered for " . count($channels) . " channels:\n"; + foreach ($channels as $channel) { + echo " β€’ $channel\n"; + } + } else { + echo " πŸ“ No channels found for this device\n"; + } + echo "\n"; + } catch (PubNubServerException $e) { + echo " ❌ Server Error: " . $e->getMessage() . "\n"; + echo " πŸ” Status Code: " . $e->getStatusCode() . "\n\n"; + } catch (PubNubException $e) { + echo " ❌ PubNub Error: " . $e->getMessage() . "\n\n"; + } catch (Exception $e) { + echo " ❌ General Error: " . $e->getMessage() . "\n\n"; + } + } + + private function demonstrateRemoveDeviceFromChannels($device) + { + echo "3️⃣ Removing device from specific channels...\n"; + + // Remove from first 2 channels only + $channelsToRemove = array_slice($this->sampleChannels, 0, 2); + + try { + if ($device['pushType'] === PNPushType::FCM) { + // snippet.remove_device_from_channels_fcm + $result = $this->pubnub->removeChannelsFromPush() + ->pushType(PNPushType::FCM) + ->channels($channelsToRemove) + ->deviceId($device['deviceId']) + ->sync(); + // snippet.end + } elseif ($device['pushType'] === PNPushType::APNS2) { + // snippet.remove_device_from_channels_apns2 + $result = $this->pubnub->removeChannelsFromPush() + ->pushType(PNPushType::APNS2) + ->channels($channelsToRemove) + ->deviceId($device['deviceId']) + ->environment($device['environment']) + ->topic($device['topic']) + ->sync(); + // snippet.end + } + + echo " βœ… Successfully removed device from " . count($channelsToRemove) . " channels\n"; + echo " πŸ“‹ Removed from: " . implode(', ', $channelsToRemove) . "\n\n"; + } catch (PubNubServerException $e) { + echo " ❌ Server Error: " . $e->getMessage() . "\n"; + echo " πŸ” Status Code: " . $e->getStatusCode() . "\n\n"; + } catch (PubNubException $e) { + echo " ❌ PubNub Error: " . $e->getMessage() . "\n\n"; + } catch (Exception $e) { + echo " ❌ General Error: " . $e->getMessage() . "\n\n"; + } + } + + private function demonstrateRemoveAllChannelsFromDevice($device) + { + echo "4️⃣ Removing all push channels from device...\n"; + + try { + if ($device['pushType'] === PNPushType::FCM) { + // snippet.remove_all_channels_fcm + $result = $this->pubnub->removeAllPushChannelsForDevice() + ->pushType(PNPushType::FCM) + ->deviceId($device['deviceId']) + ->sync(); + // snippet.end + } elseif ($device['pushType'] === PNPushType::APNS2) { + // snippet.remove_all_channels_apns2 + $result = $this->pubnub->removeAllPushChannelsForDevice() + ->pushType(PNPushType::APNS2) + ->deviceId($device['deviceId']) + ->sync(); + // snippet.end + } + + echo " βœ… Successfully removed all push channels from device\n\n"; + } catch (PubNubServerException $e) { + echo " ❌ Server Error: " . $e->getMessage() . "\n"; + echo " πŸ” Status Code: " . $e->getStatusCode() . "\n\n"; + } catch (PubNubException $e) { + echo " ❌ PubNub Error: " . $e->getMessage() . "\n\n"; + } catch (Exception $e) { + echo " ❌ General Error: " . $e->getMessage() . "\n\n"; + } + } + + public function demonstrateAdvancedFeatures() + { + echo "πŸ”§ Advanced Mobile Push Features Demo\n"; + echo "====================================\n\n"; + + // Demonstrate error handling scenarios + $this->demonstrateErrorHandling(); + + // Demonstrate bulk operations + $this->demonstrateBulkOperations(); + } + + private function demonstrateErrorHandling() + { + echo "πŸ› οΈ Error Handling Scenarios:\n\n"; + + // Test with invalid push type + echo "πŸ“‹ Testing invalid device ID...\n"; + try { + $result = $this->pubnub->addChannelsToPush() + ->pushType(PNPushType::FCM) + ->channels(['test-channel']) + ->deviceId('') // Empty device ID + ->sync(); + } catch (PubNubValidationException $e) { + echo " βœ… Correctly caught error: " . $e->getMessage() . "\n\n"; + } + + // Test with invalid channels + echo "πŸ“‹ Testing with empty channels array...\n"; + try { + $result = $this->pubnub->addChannelsToPush() + ->pushType(PNPushType::FCM) + ->channels([]) // Empty channels + ->deviceId('test-device-id') + ->sync(); + } catch (PubNubValidationException $e) { + echo " βœ… Correctly caught error: " . $e->getMessage() . "\n\n"; + } + } + + private function demonstrateBulkOperations() + { + echo "πŸ“¦ Bulk Operations Demo:\n\n"; + + $bulkChannels = [ + 'bulk-news-' . time(), + 'bulk-sports-' . time(), + 'bulk-weather-' . time(), + 'bulk-traffic-' . time(), + 'bulk-entertainment-' . time() + ]; + + $bulkDevice = [ + 'deviceId' => 'bulk-demo-device-' . uniqid(), + 'pushType' => PNPushType::FCM, + 'name' => 'Bulk Demo Device' + ]; + + echo "πŸ“± Adding device to " . count($bulkChannels) . " channels at once...\n"; + + try { + $result = $this->pubnub->addChannelsToPush() + ->pushType($bulkDevice['pushType']) + ->channels($bulkChannels) + ->deviceId($bulkDevice['deviceId']) + ->sync(); + + echo " βœ… Successfully added to all channels\n"; + + // Verify the addition + $listResult = $this->pubnub->listPushProvisions() + ->pushType($bulkDevice['pushType']) + ->deviceId($bulkDevice['deviceId']) + ->sync(); + + $registeredChannels = $listResult->getChannels(); + echo " πŸ“‹ Verified: Device registered for " . count($registeredChannels) . " channels\n"; + + // Clean up - remove all + $this->pubnub->removeAllPushChannelsForDevice() + ->pushType($bulkDevice['pushType']) + ->deviceId($bulkDevice['deviceId']) + ->sync(); + + echo " 🧹 Cleaned up: Removed all channels\n\n"; + } catch (Exception $e) { + echo " ❌ Error in bulk operations: " . $e->getMessage() . "\n\n"; + } + } +} + +// Main execution +if (php_sapi_name() === 'cli') { + try { + $demo = new MobilePushDemo(); + + // Run the comprehensive demo + $demo->runFullDemo(); + + // Run advanced features demo + $demo->demonstrateAdvancedFeatures(); + + echo "πŸŽ‰ All Mobile Push demos completed successfully!\n"; + echo "πŸ“š Refer to PubNub docs: https://www.pubnub.com/docs/sdks/php/api-reference/mobile-push\n"; + } catch (Exception $e) { + echo "πŸ’₯ Fatal Error: " . $e->getMessage() . "\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; + exit(1); + } +} else { + echo "⚠️ This demo is designed to run from the command line.\n"; + echo "Usage: php " . basename(__FILE__) . "\n"; +} diff --git a/examples/Publishing.php b/examples/Publishing.php new file mode 100644 index 00000000..fd5518ee --- /dev/null +++ b/examples/Publishing.php @@ -0,0 +1,121 @@ +setPublishKey("demo"); +$pnconfig->setSubscribeKey("demo"); +$pnconfig->setUserId("php-publish-demo-user"); + +$pubnub = new PubNub($pnconfig); + +// snippet.basic_publish +$result = $pubnub->publish() + ->channel("my_channel") + ->message(["text" => "Hello World!"]) + ->sync(); +assert($result->getTimetoken() > 0); +echo "Basic publish timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.basic_publish_too_long_message +try { + $result = $pubnub->publish() + ->channel("my_channel") + ->message(["text" => "Hello World!", "description" => str_repeat("Get allows slightly shorter messages", 1000)]) + ->sync(); + echo "Basic publish timetoken: {$result->getTimetoken()}\n"; +} catch (PubNubException $e) { + $status = $e->getStatus(); + assert($status->isError() == true); + assert($status->getStatusCode() == '414'); + print("Publish failed: {$status->getException()->getBody()->message}\n"); +} +// snippet.end + +// snippet.publish_with_meta +$result = $pubnub->publish() + ->channel("my_channel") + ->message(["text" => "Message with meta"]) + ->meta(["custom" => "metadata"]) + ->sync(); +assert($result->getTimetoken() > 0); +echo "Publish with meta timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.publish_with_ttl +$result = $pubnub->publish() + ->channel("my_channel") + ->message(["text" => "Message with TTL"]) + ->shouldStore(true) + ->ttl(24) // Store for 24 hours + ->sync(); + +echo "Publish with TTL timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.publish_with_custom_type +$result = $pubnub->publish() + ->channel("my_channel") + ->message(["text" => "Message with custom type"]) + ->customMessageType("text") + ->sync(); +assert($result->getTimetoken() > 0); +echo "Publish with custom type timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.publish_with_post +$result = $pubnub->publish() + ->channel("my_channel") + ->message([ + "text" => "Message using POST", + "description" => str_repeat("Post allows to publish longer messages", 750) + ]) + ->usePost(true) + ->sync(); +assert($result->getTimetoken() > 0); +echo "Publish with POST timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.publish_too_long_message_with_post +try { + $result = $pubnub->publish() + ->channel("my_channel") + ->message([ + "text" => "Message using POST", + "description" => str_repeat("Post allows to publish longer messages", 1410) + ]) + ->usePost(true) + ->sync(); + assert($result->getTimetoken() > 0); + echo "Publish with POST timetoken: {$result->getTimetoken()}\n"; +} catch (PubNubException $e) { + $status = $e->getStatus(); + assert($status->isError() == true); + assert($status->getStatusCode() == 413); + print("Publish failed: {$status->getException()->getBody()->message}\n"); +} +// snippet.end + +// snippet.fire +$result = $pubnub->fire() + ->channel("my_channel") + ->message(["text" => "Fire message"]) + ->sync(); +assert($result->getTimetoken() > 0); +echo "Fire timetoken: {$result->getTimetoken()}\n"; +// snippet.end + +// snippet.signal +$result = $pubnub->signal() + ->channel("my_channel") + ->message(["text" => "Signal message"]) + ->sync(); +assert($result->getTimetoken() > 0); +echo "Signal timetoken: {$result->getTimetoken()}\n"; +// snippet.end diff --git a/examples/Subscribing.php b/examples/Subscribing.php new file mode 100644 index 00000000..ac8958b5 --- /dev/null +++ b/examples/Subscribing.php @@ -0,0 +1,121 @@ +setPublishKey("demo"); // Replace with your publish key +$pnconfig->setSubscribeKey("demo"); // Replace with your subscribe key +$pnconfig->setUserId("php-subscriber-" . uniqid()); // Unique user ID for this demo +// snippet.end + +// snippet.init +// Create PubNub instance +$pubnub = new PubNub($pnconfig); +// snippet.end + +// snippet.callback +// Create a custom callback class to handle messages and status updates +class MySubscribeCallback extends SubscribeCallback +{ + public function status($pubnub, $status) + { + switch ($status->getCategory()) { + case PNStatusCategory::PNConnectedCategory: + echo "Connected to PubNub!\n"; + break; + case PNStatusCategory::PNReconnectedCategory: + echo "Reconnected to PubNub!\n"; + break; + case PNStatusCategory::PNDisconnectedCategory: + echo "Disconnected from PubNub!\n"; + break; + case PNStatusCategory::PNUnexpectedDisconnectCategory: + echo "Unexpectedly disconnected from PubNub!\n"; + break; + } + } + + public function message($pubnub, $message) + { + echo "Received message: " . json_encode($message->getMessage()) . "\n"; + echo "Channel: " . $message->getChannel() . "\n"; + echo "Publisher: " . $message->getPublisher() . "\n"; + echo "Timetoken: " . $message->getTimetoken() . "\n"; + } + + public function presence($pubnub, $presence) + { + echo "Presence event: " . $presence->getEvent() . "\n"; + echo "Channel: " . $presence->getChannel() . "\n"; + echo "UUID: " . $presence->getUuid() . "\n"; + echo "Occupancy: " . $presence->getOccupancy() . "\n"; + } +} +// snippet.end + +// snippet.subscribe +// Add the callback to PubNub +$pubnub->addListener(new MySubscribeCallback()); + +// Subscribe to multiple channels concurrently +$channels = ["demo_channel", "demo_channel2", "demo_channel3"]; + +foreach ($channels as $channel) { + $pubnub->subscribe() + ->channels($channel) + ->withPresence() + ->execute(); + + echo "Subscribed to channel: $channel\n"; +} +// snippet.end + +// snippet.history +// Get message history +function getHistory($pubnub, $channels) +{ + $channel = $channels[array_rand($channels)]; + try { + $result = $pubnub->history() + ->channel($channel) + ->count(5) + ->sync(); + + echo "Message history for $channel:\n"; + foreach ($result->getMessages() as $message) { + echo json_encode($message, JSON_PRETTY_PRINT) . "\n"; + } + } catch (\Exception $e) { + echo "Error getting history: " . $e->getMessage() . "\n"; + } +} +// snippet.end + +echo "Starting PubNub Subscriber...\n"; +echo "Press Ctrl+C to exit\n"; + +// Main loop +$lastHistoryTime = 0; + +while (true) { + $currentTime = time(); + + // Check history every 15 seconds + if ($currentTime - $lastHistoryTime >= 15) { + getHistory($pubnub, $channels); + $lastHistoryTime = $currentTime; + } + + // Small sleep to prevent CPU overuse + usleep(100000); // 100ms +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a9155b9f..2944a1aa 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -750,11 +750,6 @@ parameters: count: 1 path: src/PubNub/Endpoints/FileSharing/DownloadFile.php - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:channel\\(\\) has parameter \\$channel with no type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:createResponse\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#" count: 1 @@ -765,41 +760,6 @@ parameters: count: 1 path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:fileName\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:fileName\\(\\) has parameter \\$fileName with no type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FetchFileUploadS3Data\\:\\:name\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:channel\\(\\) has parameter \\$channel with no type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileId\\(\\) has parameter \\$fileId with no type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileName\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php - - - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\FileSharingEndpoint\\:\\:fileName\\(\\) has parameter \\$fileName with no type specified\\.$#" - count: 1 - path: src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php - - message: "#^Method PubNub\\\\Endpoints\\\\FileSharing\\\\GetFileDownloadUrl\\:\\:createResponse\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#" count: 1 @@ -3020,11 +2980,6 @@ parameters: count: 1 path: src/PubNub/Models/Consumer/FileSharing/PNGetFilesItem.php - - - message: "#^Binary operation \"\\.\" between 'Get file success…' and array results in an error\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php - - message: "#^Method PubNub\\\\Models\\\\Consumer\\\\FileSharing\\\\PNGetFilesResult\\:\\:__construct\\(\\) has parameter \\$result with no type specified\\.$#" count: 1 @@ -3630,11 +3585,6 @@ parameters: count: 1 path: src/PubNub/Models/Consumer/Objects/UUID/PNGetAllUUIDMetadataResult.php - - - message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:__construct\\(\\) has parameter \\$custom with no value type specified in iterable type array\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:__construct\\(\\) has parameter \\$email with no value type specified in iterable type array\\.$#" count: 1 @@ -3655,31 +3605,6 @@ parameters: count: 1 path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - - message: "#^Method PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:getCustom\\(\\) should return object but returns array\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - - - message: "#^Parameter \\#6 \\$custom of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects array\\|null, stdClass given\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - - - message: "#^Parameter \\#7 \\$updated of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects string\\|null, stdClass\\|null given\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - - - message: "#^Parameter \\#8 \\$eTag of class PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult constructor expects string\\|null, stdClass\\|null given\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - - - message: "#^Property PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:\\$custom type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php - - message: "#^Property PubNub\\\\Models\\\\Consumer\\\\Objects\\\\UUID\\\\PNGetUUIDMetadataResult\\:\\:\\$email \\(string\\) does not accept array\\.$#" count: 1 @@ -5930,41 +5855,6 @@ parameters: count: 1 path: tests/integrational/FetchMessagesTest.php - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testDeleteAllFiles\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testDownloadFiles\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testEmptyFileList\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testGetDownloadUrls\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testNonEmptyFileList\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testSendBinaryFile\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - - - message: "#^Method PubNubTests\\\\integrational\\\\FilesTest\\:\\:testSendTextFile\\(\\) has no return type specified\\.$#" - count: 1 - path: tests/integrational/FilesTest.php - - message: "#^Method Tests\\\\Integrational\\\\GetStateTest\\:\\:testCombination\\(\\) has no return type specified\\.$#" count: 1 @@ -6695,11 +6585,6 @@ parameters: count: 1 path: tests/integrational/SslTest.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: tests/integrational/SubscribePresenceTest.php - - message: "#^Method Tests\\\\Integrational\\\\MySubscribeCallbackToTestPresence\\:\\:areBothConnectedAndDisconnectedInvoked\\(\\) has no return type specified\\.$#" count: 1 @@ -6950,11 +6835,6 @@ parameters: count: 1 path: tests/integrational/SubscribeTest.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: tests/integrational/SubscribeWildCardTest.php - - message: "#^Method Tests\\\\Integrational\\\\MySubscribeCallbackToTestWildCard\\:\\:areBothConnectedAndDisconnectedInvoked\\(\\) has no return type specified\\.$#" count: 1 diff --git a/phpunit.xml b/phpunit.xml new file mode 100755 index 00000000..5e9c0d0d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + ./src/ + + + + + ./tests/integrational/ + + + ./tests/functional/ + + + \ No newline at end of file diff --git a/src/PubNub/Crypto/AesCbcCryptor.php b/src/PubNub/Crypto/AesCbcCryptor.php index 34d6c35a..ebe0766f 100644 --- a/src/PubNub/Crypto/AesCbcCryptor.php +++ b/src/PubNub/Crypto/AesCbcCryptor.php @@ -23,7 +23,7 @@ public function __construct(string $cipherKey) public function getIV(): string { - return random_bytes(self::IV_LENGTH); + return random_bytes(static::IV_LENGTH); } public function getCipherKey($cipherKey = null): string @@ -41,8 +41,8 @@ public function encrypt(string $text, ?string $cipherKey = null): CryptoPayload { $secret = $this->getSecret($this->getCipherKey($cipherKey)); $iv = $this->getIV(); - $encrypted = openssl_encrypt($text, self::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv); - return new CryptoPayload($encrypted, $iv, self::CRYPTOR_ID); + $encrypted = openssl_encrypt($text, static::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv); + return new CryptoPayload($encrypted, $iv, static::CRYPTOR_ID); } public function decrypt(CryptoPayload $payload, ?string $cipherKey = null) @@ -50,8 +50,8 @@ public function decrypt(CryptoPayload $payload, ?string $cipherKey = null) $text = $payload->getData(); $secret = $this->getSecret($this->getCipherKey($cipherKey)); $iv = $payload->getCryptorData(); - $decrypted = openssl_decrypt($text, self::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv); - $result = json_decode($decrypted); + $decrypted = openssl_decrypt($text, static::CIPHER_ALGO, $secret, OPENSSL_RAW_DATA, $iv); + $result = json_decode($decrypted, false); if ($result === null) { return $decrypted; diff --git a/src/PubNub/CryptoModule.php b/src/PubNub/CryptoModule.php index b3b1c2e3..68a35d08 100644 --- a/src/PubNub/CryptoModule.php +++ b/src/PubNub/CryptoModule.php @@ -10,6 +10,7 @@ use PubNub\Exceptions\PubNubCryptoException; use PubNub\Exceptions\PubNubResponseParsingException; +/** @phpstan-consistent-constructor */ class CryptoModule { public const CRYPTOR_VERSION = 1; @@ -25,7 +26,7 @@ public function __construct($cryptorMap, string $defaultCryptorId) $this->defaultCryptorId = $defaultCryptorId; } - public function registerCryptor(Cryptor $cryptor, ?string $cryptorId = null): self + public function registerCryptor(Cryptor $cryptor, ?string $cryptorId = null): static { if (is_null($cryptorId)) { $cryptorId = $cryptor::CRYPTOR_ID; @@ -70,7 +71,13 @@ public function encrypt($data, ?string $cryptorId = null): string return base64_encode($header . $cryptoPayload->getData()); } - public function decrypt(string | object $input): string | object + /** + * @param string | object $input + * @return string | object | array | null + * @throws PubNubCryptoException + * @throws PubNubResponseParsingException + */ + public function decrypt(string | object $input): string | object | array | null { $input = $this->parseInput($input); $data = base64_decode($input); @@ -90,7 +97,7 @@ public function decrypt(string | object $input): string | object public function encodeHeader(CryptoPayload $payload): string { - if ($payload->getCryptorId() == self::FALLBACK_CRYPTOR_ID) { + if ($payload->getCryptorId() == static::FALLBACK_CRYPTOR_ID) { return ''; } @@ -108,13 +115,13 @@ public function encodeHeader(CryptoPayload $payload): string $cryptorDataLength = chr(255) . chr(hexdec($hexlen[0])) . chr(hexdec($hexlen[1])); } - return self::SENTINEL . $version . $payload->getCryptorId() . $cryptorDataLength . $payload->getCryptorData(); + return static::SENTINEL . $version . $payload->getCryptorId() . $cryptorDataLength . $payload->getCryptorData(); } public function decodeHeader(string $header): CryptoHeader { - if (strlen($header < 10) or substr($header, 0, 4) != self::SENTINEL) { - return new CryptoHeader('', self::FALLBACK_CRYPTOR_ID, '', 0); + if (strlen($header < 10) or substr($header, 0, 4) != static::SENTINEL) { + return new CryptoHeader('', static::FALLBACK_CRYPTOR_ID, '', 0); } $sentinel = substr($header, 0, 4); $version = ord($header[4]); @@ -134,9 +141,9 @@ public function decodeHeader(string $header): CryptoHeader return new CryptoHeader($sentinel, $cryptorId, $cryptorData, $headerLength); } - public static function legacyCryptor(string $cipherKey, bool $useRandomIV): self + public static function legacyCryptor(string $cipherKey, bool $useRandomIV): static { - return new self( + return new static( [ LegacyCryptor::CRYPTOR_ID => new LegacyCryptor($cipherKey, $useRandomIV), AesCbcCryptor::CRYPTOR_ID => new AesCbcCryptor($cipherKey), @@ -145,14 +152,14 @@ public static function legacyCryptor(string $cipherKey, bool $useRandomIV): self ); } - public static function aesCbcCryptor(string $cipherKey, bool $useRandomIV): self + public static function aesCbcCryptor(string $cipherKey, bool $useRandomIV): static { - return new self( + return new static( [ LegacyCryptor::CRYPTOR_ID => new LegacyCryptor($cipherKey, $useRandomIV), AesCbcCryptor::CRYPTOR_ID => new AesCbcCryptor($cipherKey), ], - aesCbcCryptor::CRYPTOR_ID + AesCbcCryptor::CRYPTOR_ID ); } diff --git a/src/PubNub/Endpoints/Endpoint.php b/src/PubNub/Endpoints/Endpoint.php index 16f63772..62f7001d 100755 --- a/src/PubNub/Endpoints/Endpoint.php +++ b/src/PubNub/Endpoints/Endpoint.php @@ -368,6 +368,11 @@ protected function sendRequest(RequestInterface $request): PNEnvelope $statusCode = $exception->getCode(); $response = substr($exception->getMessage(), strpos($exception->getMessage(), "\n") + 1); $pnServerException = new PubNubServerException(); + if (is_callable([$exception, 'getResponse'])) { + $response = $exception->getResponse()->getBody()->getContents(); + } else { + $response = substr($exception->getMessage(), strpos($exception->getMessage(), "\n") + 1); + } $pnServerException->setRawBody($response); $pnServerException->setStatusCode($exception->getCode()); diff --git a/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php b/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php index b21ba37e..77d0155d 100644 --- a/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php +++ b/src/PubNub/Endpoints/FileSharing/FetchFileUploadS3Data.php @@ -16,13 +16,13 @@ class FetchFileUploadS3Data extends Endpoint protected ?string $channel; protected ?string $fileName; - public function channel($channel): self + public function channel(string $channel): self { $this->channel = $channel; return $this; } - public function fileName($fileName) + public function fileName(string $fileName): self { $this->fileName = $fileName; return $this; @@ -72,7 +72,7 @@ public function getOperationType() return PNOperationType::PNFetchFileUploadS3DataAction; } - public function name() + public function name(): string { return "Fetch file upload S3 data"; } diff --git a/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php b/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php index 2b3cde0e..75ffc4d2 100644 --- a/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php +++ b/src/PubNub/Endpoints/FileSharing/FileSharingEndpoint.php @@ -11,19 +11,19 @@ abstract class FileSharingEndpoint extends Endpoint protected ?string $fileId; protected ?string $fileName; - public function channel($channel): self + public function channel(string $channel): static { $this->channel = $channel; return $this; } - public function fileId($fileId): self + public function fileId(string $fileId): static { $this->fileId = $fileId; return $this; } - public function fileName($fileName) + public function fileName(string $fileName): static { $this->fileName = $fileName; return $this; diff --git a/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php b/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php index e073dbaa..4a258db6 100644 --- a/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php +++ b/src/PubNub/Endpoints/FileSharing/PublishFileMessage.php @@ -2,7 +2,6 @@ namespace PubNub\Endpoints\FileSharing; -use PubNub\Endpoints\Endpoint; use PubNub\Enums\PNHttpMethod; use PubNub\Enums\PNOperationType; use PubNub\Models\Consumer\FileSharing\PNPublishFileMessageResult; @@ -64,7 +63,7 @@ protected function buildPath() $this->pubnub->getConfiguration()->getPublishKey(), $this->pubnub->getConfiguration()->getSubscribeKey(), urlencode($this->channel), - urlencode($this->buildMessage()) + $this->buildMessage() ); } @@ -73,9 +72,9 @@ public function encryptMessage($message) $crypto = $this->pubnub->getCrypto(); $messageString = PubNubUtil::writeValueAsString($message); if ($crypto) { - return $crypto->encrypt($messageString); + return "\"" . urlencode($crypto->encrypt($messageString)) . "\""; } - return $messageString; + return urlencode($messageString); } protected function buildMessage() diff --git a/src/PubNub/Endpoints/FileSharing/SendFile.php b/src/PubNub/Endpoints/FileSharing/SendFile.php index 8afb3524..23ed555a 100644 --- a/src/PubNub/Endpoints/FileSharing/SendFile.php +++ b/src/PubNub/Endpoints/FileSharing/SendFile.php @@ -202,7 +202,7 @@ protected function buildPath() protected function encryptPayload() { $crypto = $this->pubnub->getCrypto(); - if ($this->fileHandle) { + if (isset($this->fileHandle) && is_resource($this->fileHandle)) { $fileContent = stream_get_contents($this->fileHandle); } else { $fileContent = $this->fileContent; diff --git a/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php b/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php index 71a581b2..5f9ff8c9 100644 --- a/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php +++ b/src/PubNub/Endpoints/Objects/Channel/GetAllChannelMetadata.php @@ -124,7 +124,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/MatchesETagTrait.php b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php index d571d4af..755d86dd 100644 --- a/src/PubNub/Endpoints/Objects/MatchesETagTrait.php +++ b/src/PubNub/Endpoints/Objects/MatchesETagTrait.php @@ -11,7 +11,7 @@ trait MatchesETagTrait * @param string $eTag * @return $this */ - public function ifMatchesETag(string $eTag): self + public function ifMatchesETag(string $eTag): static { $this->eTag = $eTag; $this->customHeaders['If-Match'] = $eTag; diff --git a/src/PubNub/Endpoints/Objects/Member/GetMembers.php b/src/PubNub/Endpoints/Objects/Member/GetMembers.php index 9bc498c2..3961b778 100644 --- a/src/PubNub/Endpoints/Objects/Member/GetMembers.php +++ b/src/PubNub/Endpoints/Objects/Member/GetMembers.php @@ -174,7 +174,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Member/ManageMembers.php b/src/PubNub/Endpoints/Objects/Member/ManageMembers.php index cd1db2bb..5baaab38 100644 --- a/src/PubNub/Endpoints/Objects/Member/ManageMembers.php +++ b/src/PubNub/Endpoints/Objects/Member/ManageMembers.php @@ -289,7 +289,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php b/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php index 2e63ee40..a1308bdb 100644 --- a/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php +++ b/src/PubNub/Endpoints/Objects/Member/RemoveMembers.php @@ -241,7 +241,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Member/SetMembers.php b/src/PubNub/Endpoints/Objects/Member/SetMembers.php index cbf43bd7..0596f70d 100644 --- a/src/PubNub/Endpoints/Objects/Member/SetMembers.php +++ b/src/PubNub/Endpoints/Objects/Member/SetMembers.php @@ -260,7 +260,7 @@ protected function customParams() array_push($sortEntries, $key); } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php b/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php index 5d62f66d..645fc45e 100644 --- a/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php +++ b/src/PubNub/Endpoints/Objects/Membership/GetMemberships.php @@ -193,7 +193,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php b/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php index a0fb093a..794231b7 100644 --- a/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php +++ b/src/PubNub/Endpoints/Objects/Membership/ManageMemberships.php @@ -304,7 +304,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php b/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php index 785ae748..ec71f9e2 100644 --- a/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php +++ b/src/PubNub/Endpoints/Objects/Membership/RemoveMemberships.php @@ -253,7 +253,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php b/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php index e358632b..1222ab6c 100644 --- a/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php +++ b/src/PubNub/Endpoints/Objects/Membership/SetMemberships.php @@ -269,7 +269,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php b/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php index 3723b4aa..2117f0bf 100644 --- a/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php +++ b/src/PubNub/Endpoints/Objects/UUID/GetAllUUIDMetadata.php @@ -124,7 +124,7 @@ protected function customParams() } } - $params['sort'] = $sortEntries; + $params['sort'] = join(",", $sortEntries); } return $params; diff --git a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php index c623d20d..139ca40e 100644 --- a/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php +++ b/src/PubNub/Endpoints/Objects/UUID/SetUUIDMetadata.php @@ -26,10 +26,9 @@ class SetUUIDMetadata extends Endpoint * @param string $uuid * @return $this */ - public function uuid($uuid) + public function uuid($uuid): static { $this->uuid = $uuid; - return $this; } @@ -37,10 +36,60 @@ public function uuid($uuid) * @param array $meta * @return $this */ - public function meta($meta) + public function meta($meta): static { $this->meta = $meta; + return $this; + } + + /** + * @param string $name + * @return $this + */ + public function name(string $name): static + { + $this->meta['name'] = $name; + + return $this; + } + + /** + * @param string $externalId + * @return $this + */ + public function externalId(string $externalId): static + { + $this->meta['externalId'] = $externalId; + return $this; + } + /** + * @param string $profileUrl + * @return $this + */ + public function profileUrl(string $profileUrl): static + { + $this->meta['profileUrl'] = $profileUrl; + return $this; + } + + /** + * @param string $email + * @return $this + */ + public function email(string $email): static + { + $this->meta['email'] = $email; + return $this; + } + + /** + * @param array $custom + * @return $this + */ + public function custom(array $custom): static + { + $this->meta['custom'] = $custom; return $this; } diff --git a/src/PubNub/Exceptions/PubNubServerException.php b/src/PubNub/Exceptions/PubNubServerException.php index 02ac86ae..05fea378 100644 --- a/src/PubNub/Exceptions/PubNubServerException.php +++ b/src/PubNub/Exceptions/PubNubServerException.php @@ -18,7 +18,9 @@ class PubNubServerException extends PubNubException protected function updateMessage() { $this->message = "Server responded with an error"; - + if ($this->body) { + $this->message .= " " . json_encode($this->body); + } if ($this->statusCode > 0) { $this->message .= " and the status code is " . $this->statusCode; } @@ -81,6 +83,8 @@ public function getServerErrorMessage() { if (isset($this->body->error->message)) { return $this->body->error->message; + } elseif (isset($this->body->message)) { + return $this->body->message; } else { return null; } @@ -90,6 +94,8 @@ public function getServerErrorSource() { if (isset($this->body->error->source)) { return $this->body->error->source; + } elseif (isset($this->body->source)) { + return $this->body->source; } else { return null; } @@ -99,6 +105,8 @@ public function getServerErrorDetails() { if (isset($this->body->error->details[0])) { return $this->body->error->details[0]; + } elseif (isset($this->body->details[0])) { + return $this->body->details[0]; } else { return null; } diff --git a/src/PubNub/Exceptions/PubNubUnsubscribeException.php b/src/PubNub/Exceptions/PubNubUnsubscribeException.php index b0f4fc11..c2c99072 100644 --- a/src/PubNub/Exceptions/PubNubUnsubscribeException.php +++ b/src/PubNub/Exceptions/PubNubUnsubscribeException.php @@ -2,7 +2,6 @@ namespace PubNub\Exceptions; - use PubNub\Builders\DTO\UnsubscribeOperation; use PubNub\Managers\SubscriptionManager; @@ -67,7 +66,8 @@ public function setChannelGroups(array $channelGroups) * @param SubscriptionManager $subscriptionManager * @return UnsubscribeOperation */ - public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager) { + public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager) + { if ($this->all) { return (new UnsubscribeOperation()) ->setChannels($subscriptionManager->subscriptionState->prepareChannelList(false)) @@ -78,4 +78,4 @@ public function getUnsubscribeOperation(SubscriptionManager $subscriptionManager ->setChannelGroups($this->channelGroups); } } -} \ No newline at end of file +} diff --git a/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php b/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php index 50d7396a..86e4f377 100644 --- a/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php +++ b/src/PubNub/Models/Consumer/FileSharing/PNGetFilesResult.php @@ -27,7 +27,7 @@ public function __construct($result) public function __toString() { - return "Get file success with data: " . $this->data; + return "Get file success with data: " . var_export($this->data, true); } public function getData() diff --git a/src/PubNub/Models/Consumer/Objects/PNIncludes.php b/src/PubNub/Models/Consumer/Objects/PNIncludes.php index 6d6d4a9a..8325903a 100644 --- a/src/PubNub/Models/Consumer/Objects/PNIncludes.php +++ b/src/PubNub/Models/Consumer/Objects/PNIncludes.php @@ -28,25 +28,25 @@ public function __toString(): string return implode(',', $result); } - public function custom(bool $custom = true): self + public function custom(bool $custom = true): static { $this->custom = $custom; return $this; } - public function status(bool $status = true): self + public function status(bool $status = true): static { $this->status = $status; return $this; } - public function totalCount(bool $totalCount = true): self + public function totalCount(bool $totalCount = true): static { $this->totalCount = $totalCount; return $this; } - public function type(bool $type = true): self + public function type(bool $type = true): static { $this->type = $type; return $this; diff --git a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php index 3c185de6..c411b2c5 100755 --- a/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php +++ b/src/PubNub/Models/Consumer/Objects/UUID/PNGetUUIDMetadataResult.php @@ -19,13 +19,13 @@ class PNGetUUIDMetadataResult /** @var string */ protected $email; - /** @var array */ + /** @var mixed */ protected $custom; - /** @var string */ + /** @var ?string */ protected $updated; - /** @var string */ + /** @var ?string */ protected $eTag; /** @@ -35,9 +35,9 @@ class PNGetUUIDMetadataResult * @param array $externalId * @param array $profileUrl * @param array $email - * @param array $custom - * @param string $updated - * @param string $eTag + * @param mixed $custom + * @param ?string $updated + * @param ?string $eTag */ public function __construct( $id, @@ -62,7 +62,7 @@ public function __construct( /** * @return string */ - public function getId() + public function getId(): ?string { return $this->id; } @@ -70,7 +70,7 @@ public function getId() /** * @return string */ - public function getName() + public function getName(): ?string { return $this->name; } @@ -78,7 +78,7 @@ public function getName() /** * @return string */ - public function getExternalId() + public function getExternalId(): ?string { return $this->externalId; } @@ -86,7 +86,7 @@ public function getExternalId() /** * @return string */ - public function getProfileUrl() + public function getProfileUrl(): ?string { return $this->profileUrl; } @@ -94,15 +94,15 @@ public function getProfileUrl() /** * @return string */ - public function getEmail() + public function getEmail(): ?string { return $this->email; } /** - * @return object + * @return mixed */ - public function getCustom() + public function getCustom(): mixed { return $this->custom; } @@ -110,7 +110,7 @@ public function getCustom() /** * @return string */ - public function getUpdated() + public function getUpdated(): ?string { return $this->updated; } @@ -118,7 +118,7 @@ public function getUpdated() /** * @return string */ - public function getETag() + public function getETag(): ?string { return $this->eTag; } @@ -190,11 +190,11 @@ public static function fromPayload(array $payload) } if (array_key_exists("updated", $meta)) { - $updated = (object)$meta["updated"]; + $updated = $meta["updated"]; } if (array_key_exists("eTag", $meta)) { - $eTag = (object)$meta["eTag"]; + $eTag = $meta["eTag"]; } return new PNGetUUIDMetadataResult( diff --git a/src/PubNub/Models/ResponseHelpers/PNStatus.php b/src/PubNub/Models/ResponseHelpers/PNStatus.php index 70ed8029..20198520 100755 --- a/src/PubNub/Models/ResponseHelpers/PNStatus.php +++ b/src/PubNub/Models/ResponseHelpers/PNStatus.php @@ -5,6 +5,7 @@ use PubNub\Enums\PNOperationType; use PubNub\Enums\PNStatusCategory; use PubNub\Exceptions\PubNubException; +use PubNub\Exceptions\PubNubServerException; class PNStatus { @@ -53,9 +54,9 @@ public function isError() } /** - * @return PubNubException + * @return PubNubException | PubNubServerException | null */ - public function getException() + public function getException(): PubNubException | PubNubServerException | null { return $this->exception; } diff --git a/src/PubNub/PubNub.php b/src/PubNub/PubNub.php index 41abc0fc..726d8f6e 100644 --- a/src/PubNub/PubNub.php +++ b/src/PubNub/PubNub.php @@ -56,7 +56,9 @@ use Psr\Log\LoggerInterface; use Psr\Log\LoggerAwareInterface; use Psr\Log\NullLogger; -use PubNub\Endpoints\FileSharing\{SendFile, DeleteFile, DownloadFile, GetFileDownloadUrl, ListFiles}; +use PubNub\Endpoints\FileSharing\{ + SendFile, DeleteFile, DownloadFile, GetFileDownloadUrl, ListFiles, PublishFileMessage +}; use PubNub\Models\Consumer\AccessManager\PNAccessManagerTokenResult; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Client\ClientInterface; @@ -668,6 +670,11 @@ public function sendFile(): SendFile return new SendFile($this); } + public function publishFileMessage(): PublishFileMessage + { + return new PublishFileMessage($this); + } + public function deleteFile(): DeleteFile { return new DeleteFile($this); diff --git a/tests/Examples/AccessManagerTest.php b/tests/Examples/AccessManagerTest.php new file mode 100644 index 00000000..c6dc8ae3 --- /dev/null +++ b/tests/Examples/AccessManagerTest.php @@ -0,0 +1,37 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + putenv('WAIT_FOR_REVOKE=0'); + + if (!getenv('SECRET_KEY')) { + putenv('SECRET_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringNotContainsString('FAIL', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/AppContextTest.php b/tests/Examples/AppContextTest.php new file mode 100644 index 00000000..b6e4b974 --- /dev/null +++ b/tests/Examples/AppContextTest.php @@ -0,0 +1,32 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringNotContainsString('Failed', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/FileSharingTest.php b/tests/Examples/FileSharingTest.php new file mode 100644 index 00000000..b2db8f60 --- /dev/null +++ b/tests/Examples/FileSharingTest.php @@ -0,0 +1,32 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringNotContainsString('Failed', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/MessageActionsTest.php b/tests/Examples/MessageActionsTest.php new file mode 100644 index 00000000..b0fb5182 --- /dev/null +++ b/tests/Examples/MessageActionsTest.php @@ -0,0 +1,32 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringContainsString('=== MESSAGE ACTIONS DEMO COMPLETE ===', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/MessagePersistanceTest.php b/tests/Examples/MessagePersistanceTest.php new file mode 100644 index 00000000..9f56437e --- /dev/null +++ b/tests/Examples/MessagePersistanceTest.php @@ -0,0 +1,32 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringContainsString('πŸŽ‰ All Message Persistence demos completed!', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/MobilePushTest.php b/tests/Examples/MobilePushTest.php new file mode 100644 index 00000000..312d1cb0 --- /dev/null +++ b/tests/Examples/MobilePushTest.php @@ -0,0 +1,32 @@ +assertFileExists($fileName); + $this->assertFileIsReadable($fileName); + + // Let's make sure that the example has properly set up the environment + if (!getenv('SUBSCRIBE_KEY')) { + putenv('SUBSCRIBE_KEY=demo'); + } + if (!getenv('PUBLISH_KEY')) { + putenv('PUBLISH_KEY=demo'); + } + + ob_start(); + // Include the examples file + require_once $fileName; + $output = ob_get_clean(); + + $this->assertStringNotContainsString('Failed', $output); + $this->assertStringNotContainsString('Exception', $output); + } +} diff --git a/tests/Examples/PublishingTest.php b/tests/Examples/PublishingTest.php new file mode 100644 index 00000000..b0894507 --- /dev/null +++ b/tests/Examples/PublishingTest.php @@ -0,0 +1,20 @@ +assertTrue(true); // If we reach this point, all examples passed + } +} diff --git a/tests/integrational/FilesTest.php b/tests/integrational/FilesTest.php index 38321591..bc3e671f 100644 --- a/tests/integrational/FilesTest.php +++ b/tests/integrational/FilesTest.php @@ -8,8 +8,14 @@ use PubNubTests\helpers\PsrStubClient; use PubNub\Exceptions\PubNubResponseParsingException; use PubNub\Exceptions\PubNubServerException; +use PubNub\Exceptions\PubNubConnectionException; +use GuzzleHttp\Exception\ConnectException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Client\ClientInterface; -class FilesTest extends PubNubTestCase +/** @phpstan-consistent-constructor */ +final class FilesTest extends PubNubTestCase { protected string $channel = "files-test"; protected string $textFilePath = __DIR__ . '/assets/spam.spam'; @@ -17,14 +23,49 @@ class FilesTest extends PubNubTestCase protected ?string $textFileId; protected ?string $binaryFileId; - public function testEmptyFileList() + protected function cleanupFiles(): void + { + try { + $listResponse = $this->pubnub->listFiles()->channel($this->channel)->sync(); + + foreach ($listResponse->getData() as $file) { + $this->pubnub->deleteFile() + ->channel($this->channel) + ->fileId($file['id']) + ->fileName($file['name']) + ->sync(); + } + } catch (\Exception $e) { + // Ignore cleanup errors, just print the error for debugging + print_r($e); + } + } + + public static function setUpBeforeClass(): void + { + $instance = new static(); + $instance->setUp(); + $instance->cleanupFiles(); + parent::setUpBeforeClass(); + } + + public static function tearDownAfterClass(): void + { + + $instance = new static(); + $instance->setUp(); + $instance->cleanupFiles(); + parent::tearDownAfterClass(); + } + + public function testEmptyFileList(): void { $response = $this->pubnub->listFiles()->channel($this->channel)->sync(); $this->assertNotEmpty($response); $this->assertCount(0, $response->getData()); } - public function testSendTextFile() + public function testSendTextFile(): void { $file = fopen($this->textFilePath, "r"); @@ -39,7 +80,7 @@ public function testSendTextFile() $this->assertNotEmpty($response->getTimestamp()); } - public function testSendBinaryFile() + public function testSendBinaryFile(): void { $file = fopen($this->binaryFilePath, "r"); @@ -54,7 +95,7 @@ public function testSendBinaryFile() $this->assertNotEmpty($response->getTimestamp()); } - public function testNonEmptyFileList() + public function testNonEmptyFileList(): void { $response = $this->pubnub->listFiles()->channel($this->channel)->sync(); $this->assertNotEmpty($response); @@ -62,7 +103,7 @@ public function testNonEmptyFileList() $this->textFileId = $response->getData()[0]['id']; } - public function testGetDownloadUrls() + public function testGetDownloadUrls(): void { $listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync(); foreach ($listFilesResponse->getData() as $file) { @@ -76,7 +117,7 @@ public function testGetDownloadUrls() } } - public function testDownloadFiles() + public function testDownloadFiles(): void { $listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync(); foreach ($listFilesResponse->getData() as $file) { @@ -94,7 +135,7 @@ public function testDownloadFiles() } } - public function testDeleteAllFiles() + public function testDeleteAllFiles(): void { $listFilesResponse = $this->pubnub->listFiles()->channel($this->channel)->sync(); foreach ($listFilesResponse->getData() as $file) { @@ -130,7 +171,6 @@ public function testThrowErrorOnMalformedResponse(): void ->setResponseBody('{}') ->setResponseStatus(307) ->setResponseHeaders(['Location' => '']); - $this->pubnub->setClient($client); $pubnub->getFileDownloadUrl()->channel($this->channel)->fileId('none')->fileName('none')->sync(); } @@ -139,4 +179,362 @@ public function testThrowErrorOnNoFileFound(): void $this->expectException(PubNubServerException::class); $this->pubnub->downloadFile()->channel($this->channel)->fileId('-')->fileName('-')->sync(); } + + public function testFileUploadWithEncryption(): void + { + // Enable encryption in configuration + $pubnub = new PubNub($this->config_enc); + $file = fopen($this->textFilePath, "r"); + + $response = $pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This is an encrypted file") + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getTimestamp()); + + // Verify file can be downloaded and decrypted + $downloadResponse = $pubnub->downloadFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName(basename($this->textFilePath)) + ->sync(); + + $this->assertEquals( + file_get_contents($this->textFilePath), + $downloadResponse->getFileContent() + ); + + $this->pubnub->deleteFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName(basename($this->textFilePath)) + ->sync(); + + fclose($file); + } + + public function testFileUploadWithTTL(): void + { + $file = fopen($this->textFilePath, "r"); + + $response = $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This file will expire") + ->ttl(60) // 60 seconds TTL + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getTimestamp()); + + // Verify file exists immediately after upload + $listResponse = $this->pubnub->listFiles() + ->channel($this->channel) + ->sync(); + + $this->assertCount(1, $listResponse->getData()); + + fclose($file); + } + + public function testFileUploadWithCustomMessageType(): void + { + $file = fopen($this->textFilePath, "r"); + + $response = $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This is a custom message type file") + ->customMessageType("file_upload") + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getTimestamp()); + + fclose($file); + } + + public function testFileUploadWithMetadata(): void + { + $file = fopen($this->textFilePath, "r"); + + $metadata = [ + "author" => "test_user", + "description" => "Test file with metadata", + "tags" => ["test", "metadata"] + ]; + + $response = $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This is a file with metadata") + ->meta($metadata) + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getTimestamp()); + + fclose($file); + } + + public function testFileUploadWithEmptyFile(): void + { + // Create an empty file + $emptyFilePath = __DIR__ . '/assets/empty.txt'; + file_put_contents($emptyFilePath, ''); + + try { + $file = fopen($emptyFilePath, "r"); + + $response = $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName('empty.txt') + ->message("This is an empty file") + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getFileId()); + sleep(1); + + // Verify file can be downloaded + $downloadResponse = $this->pubnub->downloadFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName('empty.txt') + ->sync(); + + $this->assertEquals('', $downloadResponse->getFileContent()); + + fclose($file); + } finally { + // Clean up the temporary file + if (file_exists($emptyFilePath)) { + unlink($emptyFilePath); + } + } + } + + public function testFileUploadWithLargeFile(): void + { + // Create a large file (5MB) + $largeFilePath = __DIR__ . '/assets/large.txt'; + $largeContent = str_repeat('x', 5 * 1024 * 1024); // 5MB of data + file_put_contents($largeFilePath, $largeContent); + + try { + $file = fopen($largeFilePath, "r"); + + $response = $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName('large.txt') + ->message("This is a large file") + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getFileId()); + + // Verify file can be downloaded + $downloadResponse = $this->pubnub->downloadFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName('large.txt') + ->sync(); + + $this->assertEquals($largeContent, $downloadResponse->getFileContent()); + + fclose($file); + } finally { + // Clean up the temporary file + if (file_exists($largeFilePath)) { + unlink($largeFilePath); + } + } + } + + public function testFileUploadWithInvalidParameters(): void + { + $file = fopen($this->textFilePath, "r"); + + // Test with invalid channel + $this->expectException(\PubNub\Exceptions\PubNubValidationException::class); + $this->pubnub->sendFile() + ->channel("") // Empty channel + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This should fail") + ->sync(); + + fclose($file); + + // Test with invalid file handle + $this->expectException(\Exception::class); + $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle(null) // Null file handle + ->fileName(basename($this->textFilePath)) + ->message("This should fail") + ->sync(); + + // Test with invalid file name + $this->expectException(\PubNub\Exceptions\PubNubValidationException::class); + $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName("") // Empty file name + ->message("This should fail") + ->sync(); + } + + public function testFileDownloadWithEncryption(): void + { + // Create a test file with specific content + $filePath = __DIR__ . '/assets/encrypted.txt'; + $fileContent = "This is encrypted content"; + file_put_contents($filePath, $fileContent); + + try { + // Upload file with encryption + $pubnub = new PubNub($this->config_enc); + $file = fopen($filePath, "r"); + + $response = $pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($filePath)) + ->message("This is an encrypted file") + ->sync(); + + $this->assertNotEmpty($response); + $this->assertNotEmpty($response->getFileId()); + + // Download and verify the encrypted file + $downloadResponse = $pubnub->downloadFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName(basename($filePath)) + ->sync(); + + $this->assertEquals($fileContent, $downloadResponse->getFileContent()); + + // Try downloading with non-encrypted client (should fail) + + $downloadResponse = $this->pubnub->downloadFile() + ->channel($this->channel) + ->fileId($response->getFileId()) + ->fileName(basename($filePath)) + ->sync(); + + // We shoul be able to download the file with the non-encrypted client, but the content should be encrypted + $this->assertNotEquals($fileContent, $downloadResponse->getFileContent()); + + // manually decrypt the content + $decryptedContent = $this->config_enc->getCrypto()->decrypt($downloadResponse->getFileContent()); + $this->assertEquals($fileContent, $decryptedContent); + + fclose($file); + } finally { + if (file_exists($filePath)) { + unlink($filePath); + } + } + } + + public function testFileDownloadWithInvalidFileId(): void + { + $this->expectException(PubNubServerException::class); + + $this->pubnub->downloadFile() + ->channel($this->channel) + ->fileId('invalid-file-id') + ->fileName('test.txt') + ->sync(); + } + + public function testFileListingWithPagination(): void + { + // Upload multiple files + $files = []; + for ($i = 0; $i < 5; $i++) { + $filePath = __DIR__ . "/assets/test{$i}.txt"; + file_put_contents($filePath, "content{$i}"); + $files[] = $filePath; + + $file = fopen($filePath, "r"); + $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($filePath)) + ->message("Test file {$i}") + ->sync(); + fclose($file); + } + + try { + // List files + $response = $this->pubnub->listFiles() + ->channel($this->channel) + ->sync(); + + $this->assertNotEmpty($response->getData()); + $this->assertGreaterThanOrEqual(5, count($response->getData())); + + // Verify we can get file details + foreach ($response->getData() as $file) { + $this->assertNotEmpty($file['id']); + $this->assertNotEmpty($file['name']); + $this->assertNotEmpty($file['created']); + } + } finally { + // Clean up test files + foreach ($files as $filePath) { + if (file_exists($filePath)) { + unlink($filePath); + } + } + } + } + + public function testNetworkFailureScenario(): void + { + // Create a mock client that simulates network failure + $client = new class implements ClientInterface { + public function sendRequest(RequestInterface $request): ResponseInterface + { + throw new ConnectException( + "Network failure", + $request, + null, + ['errno' => CURLE_COULDNT_CONNECT] + ); + } + }; + + $this->pubnub->setClient($client); + + $file = fopen($this->textFilePath, "r"); + + try { + $this->expectException(PubNubConnectionException::class); + + $this->pubnub->sendFile() + ->channel($this->channel) + ->fileHandle($file) + ->fileName(basename($this->textFilePath)) + ->message("This should fail due to network") + ->sync(); + } finally { + fclose($file); + // Restore original client + $this->pubnub->setClient(new \GuzzleHttp\Client()); + } + } } diff --git a/tests/integrational/PublishTest.php b/tests/integrational/PublishTest.php index 33abb1ca..61fba0df 100644 --- a/tests/integrational/PublishTest.php +++ b/tests/integrational/PublishTest.php @@ -147,7 +147,8 @@ public function testPublishDoNotStore() public function testServerSideErrorSync() { $this->expectException(PubNubServerException::class); - $this->expectExceptionMessage("Server responded with an error and the status code is 400"); + $this->expectExceptionMessageMatches("/Server responded with an error/"); + $this->expectExceptionMessageMatches("/and the status code is 400/"); $pnconf = PNConfiguration::demoKeys(); $pnconf->setPublishKey("fake"); @@ -174,7 +175,8 @@ public function testServerSideErrorEnvelope() $exception = $envelope->getStatus()->getException(); $this->assertEquals(400, $exception->getStatusCode()); - $this->assertEquals("Server responded with an error and the status code is 400", $exception->getMessage()); + $this->assertStringContainsString("Server responded with an error", $exception->getMessage()); + $this->assertStringContainsString("and the status code is 400", $exception->getMessage()); $body = $exception->getBody(); $this->assertEquals(0, $body[0]); diff --git a/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php b/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php index 159c0a25..e90e96eb 100644 --- a/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php +++ b/tests/integrational/objects/uuid/GetUUIDMetadataEndpointTest.php @@ -4,7 +4,6 @@ use PubNubTestCase; - class GetUUIDMetadataEndpointTest extends PubNubTestCase { public function testGetMetadataFromUUID() diff --git a/tests/unit/CryptoModule/CryptoModuleTest.php b/tests/unit/CryptoModule/CryptoModuleTest.php index dc0a1943..102a8e64 100644 --- a/tests/unit/CryptoModule/CryptoModuleTest.php +++ b/tests/unit/CryptoModule/CryptoModuleTest.php @@ -5,11 +5,24 @@ use Generator; use PHPUnit\Framework\TestCase; use PubNub\CryptoModule; +use PubNub\Crypto\AesCbcCryptor; +use PubNub\Crypto\LegacyCryptor; +use PubNub\Crypto\Cryptor; +use PubNub\Crypto\Payload as CryptoPayload; use PubNub\Exceptions\PubNubResponseParsingException; +use PubNub\Exceptions\PubNubCryptoException; +use TypeError; class CryptoModuleTest extends TestCase { protected string $cipherKey = "myCipherKey"; + protected string $testKey256 = "01234567890123456789012345678901"; // 32 bytes + protected string $testKeyShort = "shortkey"; + protected string $testKeyLong = "verylongcipherkeyfortestingpurposesthatexceeds256bits"; + + // ============================================================================ + // EXISTING TESTS (keeping current functionality) + // ============================================================================ /** * @dataProvider decodeProvider @@ -47,6 +60,663 @@ public function testEnode(CryptoModule $module, string $message, mixed $expected $this->assertEquals($expected, $encrypted); } + // ============================================================================ + // CRYPTOR REGISTRATION AND MANAGEMENT TESTS + // ============================================================================ + + /** + * Test successful cryptor registration + */ + public function testRegisterCryptorSuccess(): void + { + // Create a crypto module with empty cryptor map + $module = new CryptoModule([], "TEST"); + + // Create a new cryptor instance + $cryptor = new AesCbcCryptor($this->cipherKey); + + // Register the cryptor - should succeed and return the module instance + $result = $module->registerCryptor($cryptor); + + // Verify the method returns the module instance (fluent interface) + $this->assertSame($module, $result); + + // Verify the cryptor was registered by trying to encrypt/decrypt + $testMessage = "test message"; + $encrypted = $module->encrypt($testMessage, AesCbcCryptor::CRYPTOR_ID); + $this->assertNotEmpty($encrypted); + + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($testMessage, $decrypted); + } + + /** + * Test cryptor registration with custom ID + */ + public function testRegisterCryptorWithCustomId(): void + { + $customId = "CUST"; + // Create a crypto module with empty cryptor map + $module = new CryptoModule([], $customId); + + // Create a new cryptor instance + $cryptor = new class ($this->cipherKey) extends AesCbcCryptor { + public const CRYPTOR_ID = "CUST"; + }; + + // Register the cryptor with a custom ID + $result = $module->registerCryptor($cryptor, $customId); + + // Verify the method returns the module instance + $this->assertSame($module, $result); + + // Verify the cryptor was registered with the custom ID + $testMessage = "test message with custom ID"; + $encrypted = $module->encrypt($testMessage, $customId); + $this->assertNotEmpty($encrypted); + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($testMessage, $decrypted); + } + + /** + * Test cryptor registration with invalid ID length + */ + public function testRegisterCryptorInvalidIdLength(): void + { + $module = new CryptoModule([], "TEST"); + $cryptor = new AesCbcCryptor($this->cipherKey); + + // Test with ID too short + $this->expectException(PubNubCryptoException::class); + $this->expectExceptionMessage('Malformed cryptor id'); + $module->registerCryptor($cryptor, "ABC"); // 3 characters instead of 4 + } + + /** + * Test cryptor registration with invalid ID length - too long + */ + public function testRegisterCryptorInvalidIdLengthTooLong(): void + { + $module = new CryptoModule([], "TEST"); + $cryptor = new AesCbcCryptor($this->cipherKey); + + // Test with ID too long + $this->expectException(PubNubCryptoException::class); + $this->expectExceptionMessage('Malformed cryptor id'); + $module->registerCryptor($cryptor, "ABCDE"); // 5 characters instead of 4 + } + + /** + * Test cryptor registration with empty ID + */ + public function testRegisterCryptorEmptyId(): void + { + $module = new CryptoModule([], "TEST"); + $cryptor = new AesCbcCryptor($this->cipherKey); + + // Test with empty ID + $this->expectException(PubNubCryptoException::class); + $this->expectExceptionMessage('Malformed cryptor id'); + $module->registerCryptor($cryptor, ""); // Empty string + } + + /** + * Test cryptor registration with duplicate ID + */ + public function testRegisterCryptorDuplicateId(): void + { + // Create a crypto module with an existing cryptor + $existingCryptor = new LegacyCryptor($this->cipherKey, false); + $module = new CryptoModule([ + 'TEST' => $existingCryptor + ], "TEST"); + + // Try to register another cryptor with the same ID + $newCryptor = new AesCbcCryptor($this->cipherKey); + + $this->expectException(PubNubCryptoException::class); + $this->expectExceptionMessage('Cryptor id already in use'); + $module->registerCryptor($newCryptor, "TEST"); + } + + /** + * Test cryptor registration with duplicate ID using default cryptor ID + */ + public function testRegisterCryptorDuplicateDefaultId(): void + { + // Create a crypto module with existing AES cryptor + $existingCryptor = new AesCbcCryptor($this->cipherKey); + $module = new CryptoModule([ + AesCbcCryptor::CRYPTOR_ID => $existingCryptor + ], AesCbcCryptor::CRYPTOR_ID); + + // Try to register another AES cryptor (should use default CRYPTOR_ID) + $newCryptor = new AesCbcCryptor("different_key"); + + $this->expectException(PubNubCryptoException::class); + $this->expectExceptionMessage('Cryptor id already in use'); + $module->registerCryptor($newCryptor); // Will use AesCbcCryptor::CRYPTOR_ID + } + + /** + * Test cryptor registration with invalid cryptor instance + */ + public function testRegisterCryptorInvalidInstance(): void + { + $module = new CryptoModule([], "TEST"); + + // Create an anonymous class that doesn't extend Cryptor + $invalidCryptor = new class + { + public function encrypt(mixed $data): string + { + return "fake"; + } + + public function decrypt(mixed $data): string + { + return "fake"; + } + }; + + $this->expectException(TypeError::class); + + // This will fail the instanceof check in registerCryptor + $reflection = new \ReflectionClass($module); + $method = $reflection->getMethod('registerCryptor'); + $method->setAccessible(true); + + // Call with invalid cryptor - this should trigger the instanceof check + $method->invoke($module, $invalidCryptor, "TEST"); + } + + /** + * Test cryptor map initialization + */ + public function testCryptorMapInitialization(): void + { + // Test with empty cryptor map + $emptyModule = new CryptoModule([], "TEST"); + $this->assertInstanceOf(CryptoModule::class, $emptyModule); + + // Test with pre-populated cryptor map + $legacyCryptor = new LegacyCryptor($this->cipherKey, false); + $aesCryptor = new AesCbcCryptor($this->cipherKey); + + $cryptorMap = [ + LegacyCryptor::CRYPTOR_ID => $legacyCryptor, + AesCbcCryptor::CRYPTOR_ID => $aesCryptor + ]; + + $populatedModule = new CryptoModule($cryptorMap, LegacyCryptor::CRYPTOR_ID); + $this->assertInstanceOf(CryptoModule::class, $populatedModule); + + // Verify both cryptors are accessible + $testMessage = "initialization test"; + + // Test legacy cryptor + $legacyEncrypted = $populatedModule->encrypt($testMessage, LegacyCryptor::CRYPTOR_ID); + $legacyDecrypted = $populatedModule->decrypt($legacyEncrypted); + $this->assertEquals($testMessage, $legacyDecrypted); + + // Test AES cryptor + $aesEncrypted = $populatedModule->encrypt($testMessage, AesCbcCryptor::CRYPTOR_ID); + $aesDecrypted = $populatedModule->decrypt($aesEncrypted); + $this->assertEquals($testMessage, $aesDecrypted); + } + + /** + * Test default cryptor ID validation + */ + public function testDefaultCryptorIdValidation(): void + { + // Test with valid default cryptor ID + $legacyCryptor = new LegacyCryptor($this->cipherKey, false); + $cryptorMap = [LegacyCryptor::CRYPTOR_ID => $legacyCryptor]; + + $module = new CryptoModule($cryptorMap, LegacyCryptor::CRYPTOR_ID); + $this->assertInstanceOf(CryptoModule::class, $module); + + // Test encryption with default cryptor (no explicit cryptor ID) + $testMessage = "default cryptor test"; + $encrypted = $module->encrypt($testMessage); // Uses default cryptor + $this->assertNotEmpty($encrypted); + + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($testMessage, $decrypted); + } + + /** + * Test factory methods create proper cryptor maps + */ + public function testFactoryMethodsCryptorMapSetup(): void + { + // Test legacy cryptor factory + $legacyModule = CryptoModule::legacyCryptor($this->cipherKey, false); + $this->assertInstanceOf(CryptoModule::class, $legacyModule); + + // Test that it can encrypt/decrypt with legacy cryptor as default + $testMessage = "factory legacy test"; + $encrypted = $legacyModule->encrypt($testMessage); + $decrypted = $legacyModule->decrypt($encrypted); + $this->assertEquals($testMessage, $decrypted); + + // Test AES CBC cryptor factory + $aesModule = CryptoModule::aesCbcCryptor($this->cipherKey, true); + $this->assertInstanceOf(CryptoModule::class, $aesModule); + + // Test that it can encrypt/decrypt with AES cryptor as default + $testMessage2 = "factory aes test"; + $encrypted2 = $aesModule->encrypt($testMessage2); + $decrypted2 = $aesModule->decrypt($encrypted2); + $this->assertEquals($testMessage2, $decrypted2); + } + + // ============================================================================ + // INPUT VALIDATION AND SANITIZATION TESTS + // ============================================================================ + + /** + * Test encryption with null input + */ + public function testEncryptNullInput(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // PHP will convert null to empty string, which should trigger the empty message exception + $this->expectException(PubNubResponseParsingException::class); + $this->expectExceptionMessage('Encryption error: message is empty'); + + $module->encrypt(null); + } + + /** + * Test encryption with empty string + */ + public function testEncryptEmptyString(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $this->expectException(PubNubResponseParsingException::class); + $this->expectExceptionMessage('Encryption error: message is empty'); + + $module->encrypt(''); + } + + /** + * Test encryption with whitespace-only string + */ + public function testEncryptWhitespaceOnlyString(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Whitespace-only strings should be valid for encryption + $whitespaceInputs = [ + ' ', // single space + ' ', // multiple spaces + "\t", // tab + "\n", // newline + "\r\n", // carriage return + newline + " \t\n\r ", // mixed whitespace + ]; + + foreach ($whitespaceInputs as $input) { + $encrypted = $module->encrypt($input); + $this->assertNotEmpty($encrypted); + + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($input, $decrypted); + } + } + + /** + * Test encryption with very large input + */ + public function testEncryptLargeInput(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Create a large string (1MB) + $largeInput = str_repeat('A', 1024 * 1024); + + $startTime = microtime(true); + $encrypted = $module->encrypt($largeInput); + $encryptTime = microtime(true) - $startTime; + + $this->assertNotEmpty($encrypted); + + $startTime = microtime(true); + $decrypted = $module->decrypt($encrypted); + $decryptTime = microtime(true) - $startTime; + + $this->assertEquals($largeInput, $decrypted); + + // Performance assertions (should complete within reasonable time) + $this->assertLessThan(5.0, $encryptTime, 'Encryption should complete within 5 seconds'); + $this->assertLessThan(5.0, $decryptTime, 'Decryption should complete within 5 seconds'); + } + + /** + * Test encryption with special characters + */ + public function testEncryptSpecialCharacters(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $specialInputs = [ + 'Hello δΈ–η•Œ', // Unicode Chinese characters + 'πŸš€ Rocket emoji test 🌟', // Emojis + 'Γ‘oΓ±o cafΓ© rΓ©sumΓ© naΓ―ve', // Accented characters + 'Special: !@#$%^&*()_+-={}[]|\\:";\'<>?,./', // Special ASCII characters + 'Math: βˆ‘βˆ†βˆšβˆžβ‰ β‰€β‰₯Β±Γ—Γ·', // Mathematical symbols + 'Currency: $€£Β₯β‚Ήβ‚½β‚©', // Currency symbols + 'Quotes: "Hello" \'World\' `Test`', // Various quotes + "Line\nBreaks\r\nAnd\tTabs", // Control characters + 'Null\x00Byte\x01Test', // Control bytes + ]; + + foreach ($specialInputs as $input) { + $encrypted = $module->encrypt($input); + $this->assertNotEmpty($encrypted, "Failed to encrypt: " . bin2hex($input)); + + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($input, $decrypted, "Decrypt mismatch for: " . bin2hex($input)); + } + } + + /** + * Test encryption with binary data + */ + public function testEncryptBinaryData(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Test various binary data patterns + $binaryInputs = [ + "\x00\x01\x02\x03\x04\x05", // Sequential bytes + random_bytes(32), // Random binary data + "\xFF\xFE\xFD\xFC", // High-value bytes + str_repeat("\x00", 100), // Null bytes + "\x00\xFF\x00\xFF\x00\xFF", // Alternating pattern + ]; + + foreach ($binaryInputs as $input) { + $encrypted = $module->encrypt($input); + $this->assertNotEmpty($encrypted, "Failed to encrypt binary data: " . bin2hex($input)); + + $decrypted = $module->decrypt($encrypted); + $this->assertEquals($input, $decrypted, "Binary data mismatch for: " . bin2hex($input)); + } + } + + /** + * Test decryption with malformed input + */ + public function testDecryptMalformedInput(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $malformedInputs = [ + 'not-base64-at-all!@#$', + 'VGhpcyBpcyBub3QgdmFsaWQgZW5jcnlwdGVkIGRhdGE=', // Valid base64 but invalid encrypted data + 'SGVsbG8gV29ybGQ', // Valid base64 but too short for encrypted data + '===invalid===', // Invalid base64 padding + 'YWJjZGVmZ2hpams', // Valid base64 but not encrypted format + ]; + + foreach ($malformedInputs as $input) { + try { + $result = $module->decrypt($input); + // If no exception is thrown, the result should still be reasonable + $this->assertTrue(is_string($result) or is_object($result) or is_array($result)); + } catch (PubNubResponseParsingException $e) { + // Expected for malformed input + $this->assertStringContainsString('error', strtolower($e->getMessage())); + } catch (PubNubCryptoException $e) { + // Also acceptable for crypto-related errors + $this->assertStringContainsString('error', strtolower($e->getMessage())); + } + } + } + + /** + * Test decryption with empty input + */ + public function testDecryptEmptyInput(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $this->expectException(PubNubResponseParsingException::class); + $this->expectExceptionMessage('Decryption error: message is empty'); + + $module->decrypt(''); + } + + /** + * Test parseInput method with various input types + * @dataProvider parseInputProvider + */ + public function testParseInput(mixed $input, ?string $expected): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Use reflection to access the protected parseInput method + $reflection = new \ReflectionClass($module); + $method = $reflection->getMethod('parseInput'); + $method->setAccessible(true); + + if ($expected === null) { + // Expecting an exception + $this->expectException(TypeError::class); + $method->invoke($module, $input); + } else { + $result = $method->invoke($module, $input); + $this->assertEquals($expected, $result); + } + } + + /** + * Test parseInput with invalid data types + */ + public function testParseInputInvalidTypes(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Use reflection to access the protected parseInput method + $reflection = new \ReflectionClass($module); + $method = $reflection->getMethod('parseInput'); + $method->setAccessible(true); + + + $result = $method->invoke($module, 12345); + $this->assertEquals('12345', $result); + } + + // ============================================================================ + // DATA TYPE HANDLING TESTS + // ============================================================================ + + /** + * Test stringify method with different data types + * @dataProvider stringifyDataProvider + */ + public function testStringify(mixed $input, ?string $expected): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + // Use reflection to access the protected stringify method + $reflection = new \ReflectionClass($module); + $method = $reflection->getMethod('stringify'); + $method->setAccessible(true); + + $result = $method->invoke($module, $input); + $this->assertEquals($expected, $result); + } + + /** + * Test encryption/decryption of JSON objects + */ + public function testEncryptDecryptJsonObjects(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $jsonObjects = [ + // Simple JSON object + '{"name":"John","age":30}', + + // JSON object with nested structure + '{"user":{"name":"Jane","profile":{"email":"jane@test.com","settings":{"theme":"dark"}}}}', + + // JSON object with arrays + '{"items":["apple","banana","cherry"],"count":3}', + + // JSON object with mixed data types + '{"string":"hello","number":42,"boolean":true,"null":null,"array":[1,2,3]}', + + // Empty JSON object + '{}', + ]; + + foreach ($jsonObjects as $jsonString) { + $encrypted = $module->encrypt($jsonString); + $this->assertNotEmpty($encrypted, "Failed to encrypt JSON: " . $jsonString); + + $decrypted = json_encode($module->decrypt($encrypted)); + $this->assertEquals($jsonString, $decrypted, "JSON round-trip failed for: " . $jsonString); + + // Verify the decrypted string is valid JSON + $decoded = json_decode($decrypted, true); + $this->assertNotNull($decoded, "Decrypted JSON is not valid: " . $decrypted); + } + } + + /** + * Test encryption/decryption of arrays + */ + public function testEncryptDecryptArrays(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $testArrays = [ + // Simple indexed array + ["apple", "banana", "cherry"], + + ]; + + foreach ($testArrays as $array) { + // Convert array to JSON for encryption (this is typically how arrays are handled) + $jsonString = json_encode($array); + + $encrypted = $module->encrypt($jsonString); + $this->assertNotEmpty($encrypted, "Failed to encrypt array: " . json_encode($array)); + + $decrypted = json_encode($module->decrypt($encrypted)); + $this->assertEquals($jsonString, $decrypted, "Array round-trip failed for: " . json_encode($array)); + + // Verify the decrypted JSON can be converted back to the original array + $decodedArray = json_decode($decrypted, true); + $this->assertEquals($array, $decodedArray, "Array decode mismatch for: " . json_encode($array)); + } + } + + /** + * Test encryption/decryption of nested data structures + */ + public function testEncryptDecryptNestedStructures(): void + { + $module = CryptoModule::aesCbcCryptor($this->cipherKey, false); + + $nestedStructures = [ + // Deeply nested object + [ + "user" => [ + "profile" => [ + "personal" => [ + "name" => "John Doe", + "contacts" => [ + "email" => "john@example.com", + "phones" => ["123-456-7890", "098-765-4321"] + ] + ], + "preferences" => [ + "notifications" => true, + "themes" => ["dark", "light"], + "settings" => [ + "language" => "en", + "timezone" => "UTC" + ] + ] + ] + ] + ], + + // Array of objects + [ + "users" => [ + ["id" => 1, "name" => "Alice", "roles" => ["admin", "user"]], + ["id" => 2, "name" => "Bob", "roles" => ["user"]], + ["id" => 3, "name" => "Charlie", "roles" => ["moderator", "user"]] + ] + ], + + // Mixed nested structure with various data types + [ + "config" => [ + "database" => [ + "connections" => [ + "primary" => ["host" => "localhost", "port" => 5432, "ssl" => true], + "secondary" => ["host" => "backup.db", "port" => 5432, "ssl" => false] + ] + ], + "features" => [ + "logging" => ["enabled" => true, "level" => "info", "handlers" => ["file", "console"]], + "caching" => ["ttl" => 3600, "driver" => "redis", "prefix" => "app_cache"] + ] + ] + ], + + // Complex array structure + [ + "matrix" => [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9] + ], + "metadata" => [ + "dimensions" => ["rows" => 3, "cols" => 3], + "statistics" => ["sum" => 45, "avg" => 5.0] + ] + ] + ]; + + foreach ($nestedStructures as $structure) { + // Convert structure to JSON for encryption + $jsonString = json_encode($structure); + + $encrypted = $module->encrypt($jsonString); + $this->assertNotEmpty($encrypted, "Failed to encrypt nested structure"); + + $decrypted = json_encode($module->decrypt($encrypted)); + $this->assertEquals($jsonString, $decrypted, "Nested structure round-trip failed"); + + // Verify the decrypted JSON can be converted back to the original structure + $decodedStructure = json_decode($decrypted, true); + $this->assertEquals($structure, $decodedStructure, "Nested structure decode mismatch"); + + // Verify specific nested values are accessible + if (isset($structure["user"]["profile"]["personal"]["name"])) { + $this->assertEquals("John Doe", $decodedStructure["user"]["profile"]["personal"]["name"]); + } + if (isset($structure["config"]["database"]["connections"]["primary"]["port"])) { + $this->assertEquals(5432, $decodedStructure["config"]["database"]["connections"]["primary"]["port"]); + } + } + } + + // ============================================================================ + // DATA PROVIDERS + // ============================================================================ + protected function encodeProvider(): Generator { $legacyRandomModule = CryptoModule::legacyCryptor($this->cipherKey, true); @@ -129,4 +799,86 @@ protected function decodeProvider(): Generator 'Hello world encrypted with aesCbcModule', ]; } + + /** + * Data provider for parseInput tests + */ + protected function parseInputProvider(): Generator + { + // Valid string inputs + yield ["simple string", "simple string"]; + yield ["encrypted_base64_data", "encrypted_base64_data"]; + yield [" trimmed ", " trimmed "]; // Should NOT be trimmed by parseInput + + // Invalid inputs that should throw exceptions + yield [["other" => "data"], null]; // Missing pn_other key + yield [["pn_other" => ""], null]; // Empty pn_other value + yield [["pn_other" => " "], null]; // Whitespace-only pn_other value + yield [123, '123']; // Non-string, non-array input + yield [true, '1']; // Boolean input + yield [null, null]; // Null input (converted to empty string) + } + + /** + * Data provider for stringify tests + */ + protected function stringifyDataProvider(): Generator + { + // String values + yield ["simple string", "simple string"]; + yield ["", ""]; + yield [" whitespace ", " whitespace "]; + yield ["Special chars: !@#$%^&*()", "Special chars: !@#$%^&*()"]; + yield ["Unicode: πŸš€ δΈ–η•Œ", "Unicode: πŸš€ δΈ–η•Œ"]; + + // Numeric values + yield [123, "123"]; + yield [0, "0"]; + yield [-456, "-456"]; + yield [3.14159, "3.14159"]; + yield [1.23e10, "12300000000"]; + + // Boolean values + yield [true, "true"]; + yield [false, "false"]; + + // Null value + yield [null, "null"]; + + // Array values (should be JSON encoded) + yield [["key" => "value"], '{"key":"value"}']; + yield [[1, 2, 3], '[1,2,3]']; + yield [[], '[]']; + yield [["nested" => ["data" => true]], '{"nested":{"data":true}}']; + + // Mixed array + yield [ + ["string" => "hello", "number" => 42, "bool" => true, "null" => null], + '{"string":"hello","number":42,"bool":true,"null":null}' + ]; + + // Complex nested structure + yield [ + [ + "user" => [ + "name" => "John", + "settings" => ["theme" => "dark", "notifications" => true] + ], + "data" => [1, 2, 3] + ], + '{"user":{"name":"John","settings":{"theme":"dark","notifications":true}},"data":[1,2,3]}' + ]; + } + + /** + * Data provider for cipher key length tests + */ + protected function cipherKeyLengthProvider(): Generator + { + // TODO: Implement data provider for different key lengths + yield [$this->testKeyShort]; + yield [$this->testKey256]; + yield [$this->testKeyLong]; + yield [""]; + } }