From 3b2327a57126e9c67a9bfa9167a449e426131711 Mon Sep 17 00:00:00 2001 From: smiley Date: Tue, 5 Mar 2024 23:07:26 +0100 Subject: [PATCH] :sparkles: init from chillerlan/php-oauth-providers https://github.com/chillerlan/php-oauth-providers/commit/af69c0da19a48d44dcc23040b0dfccdd9ab3e35d --- .gitignore | 2 +- config/.env_example | 229 +++++++++++++ examples/OAuthExampleSessionStorage.php | 85 +++++ examples/OAuthProviderFactory.php | 115 +++++++ .../GitHub/gist-spotify-top-tracks.php | 97 ++++++ examples/Providers/GitHub/github-common.php | 16 + examples/Providers/LastFM/cache/.gitkeep | 0 examples/Providers/LastFM/lastfm-common.php | 17 + .../Providers/LastFM/topalbum-patchwork.html | 66 ++++ .../Providers/LastFM/topalbum-patchwork.php | 154 +++++++++ examples/Providers/LastFM/urlcache/.gitkeep | 0 .../Providers/Spotify/MixesDBTrackSearch.php | 126 +++++++ examples/Providers/Spotify/SpotifyClient.php | 319 ++++++++++++++++++ .../Providers/Spotify/SpotifyNewReleases.php | 148 ++++++++ examples/Providers/Spotify/mixesdb-scrape.php | 116 +++++++ .../Spotify/mixesdb-track-search.php | 36 ++ examples/Providers/Spotify/new-releases.php | 44 +++ examples/Providers/Spotify/playlist-diff.php | 40 +++ examples/Providers/Spotify/spotify-common.php | 21 ++ examples/create-docblocks.php | 84 +++++ examples/get-token/Amazon.php | 22 ++ examples/get-token/BattleNet.php | 22 ++ examples/get-token/BigCartel.php | 28 ++ examples/get-token/Bitbucket.php | 22 ++ examples/get-token/Deezer.php | 22 ++ examples/get-token/DeviantArt.php | 22 ++ examples/get-token/Discogs.php | 22 ++ examples/get-token/Discord.php | 22 ++ examples/get-token/Flickr.php | 30 ++ examples/get-token/Foursquare.php | 22 ++ examples/get-token/GitHub.php | 22 ++ examples/get-token/GitLab.php | 22 ++ examples/get-token/Google.php | 23 ++ examples/get-token/Imgur.php | 59 ++++ examples/get-token/LastFM.php | 51 +++ examples/get-token/MailChimp.php | 57 ++++ examples/get-token/Mastodon.php | 24 ++ examples/get-token/MicrosoftGraph.php | 22 ++ examples/get-token/Mixcloud.php | 22 ++ examples/get-token/MusicBrainz.php | 54 +++ examples/get-token/NPROne.php | 22 ++ examples/get-token/OpenCaching.php | 24 ++ examples/get-token/OpenStreetmap.php | 22 ++ examples/get-token/OpenStreetmap2.php | 22 ++ examples/get-token/Patreon.php | 22 ++ examples/get-token/PayPal.php | 23 ++ examples/get-token/Slack.php | 22 ++ examples/get-token/SoundCloud.php | 22 ++ examples/get-token/Spotify.php | 22 ++ examples/get-token/SteamOpenID.php | 52 +++ examples/get-token/Stripe.php | 22 ++ examples/get-token/Tumblr.php | 22 ++ examples/get-token/Twitch.php | 22 ++ examples/get-token/Twitter.php | 30 ++ examples/get-token/Vimeo.php | 28 ++ examples/get-token/Wordpress.php | 22 ++ examples/get-token/_flow-oauth1.php | 43 +++ examples/get-token/_flow-oauth2-no-state.php | 44 +++ examples/get-token/_flow-oauth2.php | 44 +++ examples/provider-example-common.php | 45 +++ phpunit.xml.dist | 11 +- src/Providers/Amazon.php | 63 ++++ src/Providers/AzureActiveDirectory.php | 30 ++ src/Providers/BattleNet.php | 103 ++++++ src/Providers/BigCartel.php | 106 ++++++ src/Providers/Bitbucket.php | 49 +++ src/Providers/Deezer.php | 146 ++++++++ src/Providers/DeviantArt.php | 104 ++++++ src/Providers/Discogs.php | 54 +++ src/Providers/Discord.php | 113 +++++++ src/Providers/Flickr.php | 82 +++++ src/Providers/Foursquare.php | 73 ++++ src/Providers/GitHub.php | 86 +++++ src/Providers/GitLab.php | 49 +++ src/Providers/Google.php | 62 ++++ src/Providers/GuildWars2.php | 121 +++++++ src/Providers/Imgur.php | 55 +++ src/Providers/Instagram.php | 66 ++++ src/Providers/LastFM.php | 219 ++++++++++++ src/Providers/MailChimp.php | 104 ++++++ src/Providers/Mastodon.php | 111 ++++++ src/Providers/MicrosoftGraph.php | 58 ++++ src/Providers/Mixcloud.php | 53 +++ src/Providers/MusicBrainz.php | 121 +++++++ src/Providers/NPROne.php | 157 +++++++++ src/Providers/OpenCaching.php | 51 +++ src/Providers/OpenStreetmap.php | 53 +++ src/Providers/OpenStreetmap2.php | 71 ++++ src/Providers/Patreon.php | 88 +++++ src/Providers/PayPal.php | 131 +++++++ src/Providers/PayPalSandbox.php | 22 ++ src/Providers/Slack.php | 114 +++++++ src/Providers/SoundCloud.php | 60 ++++ src/Providers/Spotify.php | 105 ++++++ src/Providers/SteamOpenID.php | 124 +++++++ src/Providers/Stripe.php | 92 +++++ src/Providers/Tumblr.php | 85 +++++ src/Providers/Tumblr2.php | 62 ++++ src/Providers/Twitch.php | 155 +++++++++ src/Providers/Twitter.php | 59 ++++ src/Providers/TwitterCC.php | 50 +++ src/Providers/Vimeo.php | 105 ++++++ src/Providers/WordPress.php | 57 ++++ src/Providers/YouTube.php | 21 ++ .../Providers/ChillerlanHttpClientFactory.php | 30 ++ tests/Providers/GuzzleHttpClientFactory.php | 31 ++ tests/Providers/Live/AmazonAPITest.php | 32 ++ tests/Providers/Live/BattleNetAPITest.php | 29 ++ tests/Providers/Live/BigCartelAPITest.php | 37 ++ tests/Providers/Live/BitbucketAPITest.php | 29 ++ tests/Providers/Live/DeezerAPITest.php | 33 ++ tests/Providers/Live/DeviantArtAPITest.php | 29 ++ tests/Providers/Live/DiscogsAPITest.php | 31 ++ tests/Providers/Live/DiscordAPITest.php | 43 +++ tests/Providers/Live/FlickrAPITest.php | 44 +++ tests/Providers/Live/FoursquareAPITest.php | 33 ++ tests/Providers/Live/GitHubAPITest.php | 29 ++ tests/Providers/Live/GitLabAPITest.php | 29 ++ tests/Providers/Live/GoogleAPITest.php | 33 ++ tests/Providers/Live/GuildWars2APITest.php | 45 +++ tests/Providers/Live/ImgurAPITest.php | 37 ++ tests/Providers/Live/LastFMAPITest.php | 41 +++ tests/Providers/Live/MailChimpAPITest.php | 40 +++ tests/Providers/Live/MastodonAPITest.php | 43 +++ .../Providers/Live/MicrosoftGraphAPITest.php | 29 ++ tests/Providers/Live/MixcloudAPITest.php | 29 ++ tests/Providers/Live/MusicBrainzAPITest.php | 39 +++ tests/Providers/Live/NPROneAPITest.php | 29 ++ tests/Providers/Live/OpenCachingAPITest.php | 29 ++ .../Providers/Live/OpenStreetmap2APITest.php | 29 ++ tests/Providers/Live/OpenStreetmapAPITest.php | 32 ++ tests/Providers/Live/Patreon1APITest.php | 39 +++ tests/Providers/Live/Patreon2APITest.php | 38 +++ tests/Providers/Live/PayPalAPITest.php | 41 +++ tests/Providers/Live/SlackAPITest.php | 29 ++ tests/Providers/Live/SoundcloudAPITest.php | 33 ++ tests/Providers/Live/SpotifyAPITest.php | 36 ++ tests/Providers/Live/SteamOpenIDAPITest.php | 34 ++ tests/Providers/Live/StripeAPITest.php | 33 ++ tests/Providers/Live/Tumblr2APITest.php | 29 ++ tests/Providers/Live/TumblrAPITest.php | 37 ++ tests/Providers/Live/TwitchAPITest.php | 29 ++ tests/Providers/Live/TwitterAPITest.php | 43 +++ tests/Providers/Live/TwitterCCAPITest.php | 28 ++ tests/Providers/Live/VimeoAPITest.php | 29 ++ tests/Providers/Live/WordpressAPITest.php | 29 ++ tests/Providers/OAuth1APITestAbstract.php | 24 ++ tests/Providers/OAuth2APITestAbstract.php | 49 +++ tests/Providers/OAuthAPITestAbstract.php | 104 ++++++ tests/Providers/OAuthTestHttpClient.php | 68 ++++ .../OAuthTestHttpClientFactoryInterface.php | 22 ++ tests/Providers/Unit/AmazonTest.php | 23 ++ tests/Providers/Unit/BattleNetTest.php | 39 +++ tests/Providers/Unit/BigCartelTest.php | 31 ++ tests/Providers/Unit/BitbucketTest.php | 23 ++ tests/Providers/Unit/DeezerTest.php | 74 ++++ tests/Providers/Unit/DeviantArtTest.php | 30 ++ tests/Providers/Unit/DiscogsTest.php | 23 ++ tests/Providers/Unit/DiscordTest.php | 23 ++ tests/Providers/Unit/FlickrTest.php | 30 ++ tests/Providers/Unit/FoursquareTest.php | 23 ++ tests/Providers/Unit/GitHubTest.php | 23 ++ tests/Providers/Unit/GitLabTest.php | 23 ++ tests/Providers/Unit/GoogleTest.php | 23 ++ tests/Providers/Unit/GuildWars2Test.php | 70 ++++ tests/Providers/Unit/ImgurTest.php | 23 ++ tests/Providers/Unit/LastFMTest.php | 120 +++++++ tests/Providers/Unit/MailChimpTest.php | 69 ++++ tests/Providers/Unit/MastodonTest.php | 37 ++ tests/Providers/Unit/MicrosoftGraphTest.php | 23 ++ tests/Providers/Unit/MixcloudTest.php | 23 ++ tests/Providers/Unit/MusicBrainzTest.php | 23 ++ tests/Providers/Unit/NPROneTest.php | 64 ++++ tests/Providers/Unit/OpenCachingTest.php | 23 ++ tests/Providers/Unit/OpenStreetmap2Test.php | 23 ++ tests/Providers/Unit/OpenStreetmapTest.php | 23 ++ tests/Providers/Unit/Patreon1Test.php | 23 ++ tests/Providers/Unit/Patreon2Test.php | 23 ++ tests/Providers/Unit/PayPalTest.php | 23 ++ tests/Providers/Unit/SlackTest.php | 23 ++ tests/Providers/Unit/SoundCloudTest.php | 23 ++ tests/Providers/Unit/SpotifyTest.php | 23 ++ tests/Providers/Unit/SteamOpenIDTest.php | 117 +++++++ tests/Providers/Unit/StripeTest.php | 23 ++ tests/Providers/Unit/Tumblr2Test.php | 23 ++ tests/Providers/Unit/TumblrTest.php | 23 ++ tests/Providers/Unit/TwitchTest.php | 27 ++ tests/Providers/Unit/TwitterCCTest.php | 46 +++ tests/Providers/Unit/TwitterTest.php | 23 ++ tests/Providers/Unit/VimeoTest.php | 30 ++ tests/Providers/Unit/WordPressTest.php | 23 ++ tests/Providers/Unit/YouTubeTest.php | 23 ++ 192 files changed, 9672 insertions(+), 2 deletions(-) create mode 100644 config/.env_example create mode 100644 examples/OAuthExampleSessionStorage.php create mode 100644 examples/OAuthProviderFactory.php create mode 100644 examples/Providers/GitHub/gist-spotify-top-tracks.php create mode 100644 examples/Providers/GitHub/github-common.php create mode 100644 examples/Providers/LastFM/cache/.gitkeep create mode 100644 examples/Providers/LastFM/lastfm-common.php create mode 100644 examples/Providers/LastFM/topalbum-patchwork.html create mode 100644 examples/Providers/LastFM/topalbum-patchwork.php create mode 100644 examples/Providers/LastFM/urlcache/.gitkeep create mode 100644 examples/Providers/Spotify/MixesDBTrackSearch.php create mode 100644 examples/Providers/Spotify/SpotifyClient.php create mode 100644 examples/Providers/Spotify/SpotifyNewReleases.php create mode 100644 examples/Providers/Spotify/mixesdb-scrape.php create mode 100644 examples/Providers/Spotify/mixesdb-track-search.php create mode 100644 examples/Providers/Spotify/new-releases.php create mode 100644 examples/Providers/Spotify/playlist-diff.php create mode 100644 examples/Providers/Spotify/spotify-common.php create mode 100644 examples/create-docblocks.php create mode 100644 examples/get-token/Amazon.php create mode 100644 examples/get-token/BattleNet.php create mode 100644 examples/get-token/BigCartel.php create mode 100644 examples/get-token/Bitbucket.php create mode 100644 examples/get-token/Deezer.php create mode 100644 examples/get-token/DeviantArt.php create mode 100644 examples/get-token/Discogs.php create mode 100644 examples/get-token/Discord.php create mode 100644 examples/get-token/Flickr.php create mode 100644 examples/get-token/Foursquare.php create mode 100644 examples/get-token/GitHub.php create mode 100644 examples/get-token/GitLab.php create mode 100644 examples/get-token/Google.php create mode 100644 examples/get-token/Imgur.php create mode 100644 examples/get-token/LastFM.php create mode 100644 examples/get-token/MailChimp.php create mode 100644 examples/get-token/Mastodon.php create mode 100644 examples/get-token/MicrosoftGraph.php create mode 100644 examples/get-token/Mixcloud.php create mode 100644 examples/get-token/MusicBrainz.php create mode 100644 examples/get-token/NPROne.php create mode 100644 examples/get-token/OpenCaching.php create mode 100644 examples/get-token/OpenStreetmap.php create mode 100644 examples/get-token/OpenStreetmap2.php create mode 100644 examples/get-token/Patreon.php create mode 100644 examples/get-token/PayPal.php create mode 100644 examples/get-token/Slack.php create mode 100644 examples/get-token/SoundCloud.php create mode 100644 examples/get-token/Spotify.php create mode 100644 examples/get-token/SteamOpenID.php create mode 100644 examples/get-token/Stripe.php create mode 100644 examples/get-token/Tumblr.php create mode 100644 examples/get-token/Twitch.php create mode 100644 examples/get-token/Twitter.php create mode 100644 examples/get-token/Vimeo.php create mode 100644 examples/get-token/Wordpress.php create mode 100644 examples/get-token/_flow-oauth1.php create mode 100644 examples/get-token/_flow-oauth2-no-state.php create mode 100644 examples/get-token/_flow-oauth2.php create mode 100644 examples/provider-example-common.php create mode 100644 src/Providers/Amazon.php create mode 100644 src/Providers/AzureActiveDirectory.php create mode 100644 src/Providers/BattleNet.php create mode 100644 src/Providers/BigCartel.php create mode 100644 src/Providers/Bitbucket.php create mode 100644 src/Providers/Deezer.php create mode 100644 src/Providers/DeviantArt.php create mode 100644 src/Providers/Discogs.php create mode 100644 src/Providers/Discord.php create mode 100644 src/Providers/Flickr.php create mode 100644 src/Providers/Foursquare.php create mode 100644 src/Providers/GitHub.php create mode 100644 src/Providers/GitLab.php create mode 100644 src/Providers/Google.php create mode 100644 src/Providers/GuildWars2.php create mode 100644 src/Providers/Imgur.php create mode 100644 src/Providers/Instagram.php create mode 100644 src/Providers/LastFM.php create mode 100644 src/Providers/MailChimp.php create mode 100644 src/Providers/Mastodon.php create mode 100644 src/Providers/MicrosoftGraph.php create mode 100644 src/Providers/Mixcloud.php create mode 100644 src/Providers/MusicBrainz.php create mode 100644 src/Providers/NPROne.php create mode 100644 src/Providers/OpenCaching.php create mode 100644 src/Providers/OpenStreetmap.php create mode 100644 src/Providers/OpenStreetmap2.php create mode 100644 src/Providers/Patreon.php create mode 100644 src/Providers/PayPal.php create mode 100644 src/Providers/PayPalSandbox.php create mode 100644 src/Providers/Slack.php create mode 100644 src/Providers/SoundCloud.php create mode 100644 src/Providers/Spotify.php create mode 100644 src/Providers/SteamOpenID.php create mode 100644 src/Providers/Stripe.php create mode 100644 src/Providers/Tumblr.php create mode 100644 src/Providers/Tumblr2.php create mode 100644 src/Providers/Twitch.php create mode 100644 src/Providers/Twitter.php create mode 100644 src/Providers/TwitterCC.php create mode 100644 src/Providers/Vimeo.php create mode 100644 src/Providers/WordPress.php create mode 100644 src/Providers/YouTube.php create mode 100644 tests/Providers/ChillerlanHttpClientFactory.php create mode 100644 tests/Providers/GuzzleHttpClientFactory.php create mode 100644 tests/Providers/Live/AmazonAPITest.php create mode 100644 tests/Providers/Live/BattleNetAPITest.php create mode 100644 tests/Providers/Live/BigCartelAPITest.php create mode 100644 tests/Providers/Live/BitbucketAPITest.php create mode 100644 tests/Providers/Live/DeezerAPITest.php create mode 100644 tests/Providers/Live/DeviantArtAPITest.php create mode 100644 tests/Providers/Live/DiscogsAPITest.php create mode 100644 tests/Providers/Live/DiscordAPITest.php create mode 100644 tests/Providers/Live/FlickrAPITest.php create mode 100644 tests/Providers/Live/FoursquareAPITest.php create mode 100644 tests/Providers/Live/GitHubAPITest.php create mode 100644 tests/Providers/Live/GitLabAPITest.php create mode 100644 tests/Providers/Live/GoogleAPITest.php create mode 100644 tests/Providers/Live/GuildWars2APITest.php create mode 100644 tests/Providers/Live/ImgurAPITest.php create mode 100644 tests/Providers/Live/LastFMAPITest.php create mode 100644 tests/Providers/Live/MailChimpAPITest.php create mode 100644 tests/Providers/Live/MastodonAPITest.php create mode 100644 tests/Providers/Live/MicrosoftGraphAPITest.php create mode 100644 tests/Providers/Live/MixcloudAPITest.php create mode 100644 tests/Providers/Live/MusicBrainzAPITest.php create mode 100644 tests/Providers/Live/NPROneAPITest.php create mode 100644 tests/Providers/Live/OpenCachingAPITest.php create mode 100644 tests/Providers/Live/OpenStreetmap2APITest.php create mode 100644 tests/Providers/Live/OpenStreetmapAPITest.php create mode 100644 tests/Providers/Live/Patreon1APITest.php create mode 100644 tests/Providers/Live/Patreon2APITest.php create mode 100644 tests/Providers/Live/PayPalAPITest.php create mode 100644 tests/Providers/Live/SlackAPITest.php create mode 100644 tests/Providers/Live/SoundcloudAPITest.php create mode 100644 tests/Providers/Live/SpotifyAPITest.php create mode 100644 tests/Providers/Live/SteamOpenIDAPITest.php create mode 100644 tests/Providers/Live/StripeAPITest.php create mode 100644 tests/Providers/Live/Tumblr2APITest.php create mode 100644 tests/Providers/Live/TumblrAPITest.php create mode 100644 tests/Providers/Live/TwitchAPITest.php create mode 100644 tests/Providers/Live/TwitterAPITest.php create mode 100644 tests/Providers/Live/TwitterCCAPITest.php create mode 100644 tests/Providers/Live/VimeoAPITest.php create mode 100644 tests/Providers/Live/WordpressAPITest.php create mode 100644 tests/Providers/OAuth1APITestAbstract.php create mode 100644 tests/Providers/OAuth2APITestAbstract.php create mode 100644 tests/Providers/OAuthAPITestAbstract.php create mode 100644 tests/Providers/OAuthTestHttpClient.php create mode 100644 tests/Providers/OAuthTestHttpClientFactoryInterface.php create mode 100644 tests/Providers/Unit/AmazonTest.php create mode 100644 tests/Providers/Unit/BattleNetTest.php create mode 100644 tests/Providers/Unit/BigCartelTest.php create mode 100644 tests/Providers/Unit/BitbucketTest.php create mode 100644 tests/Providers/Unit/DeezerTest.php create mode 100644 tests/Providers/Unit/DeviantArtTest.php create mode 100644 tests/Providers/Unit/DiscogsTest.php create mode 100644 tests/Providers/Unit/DiscordTest.php create mode 100644 tests/Providers/Unit/FlickrTest.php create mode 100644 tests/Providers/Unit/FoursquareTest.php create mode 100644 tests/Providers/Unit/GitHubTest.php create mode 100644 tests/Providers/Unit/GitLabTest.php create mode 100644 tests/Providers/Unit/GoogleTest.php create mode 100644 tests/Providers/Unit/GuildWars2Test.php create mode 100644 tests/Providers/Unit/ImgurTest.php create mode 100644 tests/Providers/Unit/LastFMTest.php create mode 100644 tests/Providers/Unit/MailChimpTest.php create mode 100644 tests/Providers/Unit/MastodonTest.php create mode 100644 tests/Providers/Unit/MicrosoftGraphTest.php create mode 100644 tests/Providers/Unit/MixcloudTest.php create mode 100644 tests/Providers/Unit/MusicBrainzTest.php create mode 100644 tests/Providers/Unit/NPROneTest.php create mode 100644 tests/Providers/Unit/OpenCachingTest.php create mode 100644 tests/Providers/Unit/OpenStreetmap2Test.php create mode 100644 tests/Providers/Unit/OpenStreetmapTest.php create mode 100644 tests/Providers/Unit/Patreon1Test.php create mode 100644 tests/Providers/Unit/Patreon2Test.php create mode 100644 tests/Providers/Unit/PayPalTest.php create mode 100644 tests/Providers/Unit/SlackTest.php create mode 100644 tests/Providers/Unit/SoundCloudTest.php create mode 100644 tests/Providers/Unit/SpotifyTest.php create mode 100644 tests/Providers/Unit/SteamOpenIDTest.php create mode 100644 tests/Providers/Unit/StripeTest.php create mode 100644 tests/Providers/Unit/Tumblr2Test.php create mode 100644 tests/Providers/Unit/TumblrTest.php create mode 100644 tests/Providers/Unit/TwitchTest.php create mode 100644 tests/Providers/Unit/TwitterCCTest.php create mode 100644 tests/Providers/Unit/TwitterTest.php create mode 100644 tests/Providers/Unit/VimeoTest.php create mode 100644 tests/Providers/Unit/WordPressTest.php create mode 100644 tests/Providers/Unit/YouTubeTest.php diff --git a/.gitignore b/.gitignore index 7399d902..a783a3ff 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ .docs/* vendor/* config/.env -config/*.token* +config/**/*.token* config/cacert.pem composer.lock phpcs.xml diff --git a/config/.env_example b/config/.env_example new file mode 100644 index 00000000..141209db --- /dev/null +++ b/config/.env_example @@ -0,0 +1,229 @@ + +# https://account.arena.net/applications +GW2_TOKEN= +#GW2_TOKEN_NAME= + +# https://sellercentral.amazon.com/gp/homepage.html +AMAZON_KEY= +AMAZON_SECRET= +AMAZON_CALLBACK_URL= + +# https://develop.battle.net/access/clients +BATTLENET_KEY= +BATTLENET_SECRET= +BATTLENET_CALLBACK_URL= +#BATTLENET_TESTUSER= + +# https://bigcartel.wufoo.com/confirm/big-cartel-api-application/ +BIGCARTEL_KEY= +BIGCARTEL_SECRET= +BIGCARTEL_CALLBACK_URL= + +# bitbucket account settings -> OAuth -> OAuth consumers -> create +BITBUCKET_KEY= +BITBUCKET_SECRET= +BITBUCKET_CALLBACK_URL= +#BITBUCKET_TESTUSER= + +# http://developers.deezer.com/myapps/ +DEEZER_KEY= +DEEZER_SECRET= +DEEZER_CALLBACK_URL= +#DEEZER_TESTUSER= + +# https://www.deviantart.com/developers/apps +DEVIANTART_KEY= +DEVIANTART_SECRET= +DEVIANTART_CALLBACK_URL= +DEVIANTART_TESTUSER= + +# https://www.discogs.com/settings/developers +DISCOGS_KEY= +DISCOGS_SECRET= +DISCOGS_CALLBACK_URL= +#DISCOGS_TESTUSER= + +# https://discordapp.com/developers/applications/me +DISCORD_KEY= +DISCORD_SECRET= +DISCORD_CALLBACK_URL= +#DISCORD_TESTUSER= + +# https://www.flickr.com/services/apps/create/ +FLICKR_KEY= +FLICKR_SECRET= +FLICKR_CALLBACK_URL= + +# https://foursquare.com/developers/ +FOURSQUARE_KEY= +FOURSQUARE_SECRET= +FOURSQUARE_CALLBACK_URL= +#FOURSQUARE_TESTUSER= + +# https://github.com/settings/applications/ +GITHUB_KEY= +GITHUB_SECRET= +GITHUB_CALLBACK_URL= +#GITHUB_TESTUSER= + +# https://gitlab.com/profile/applications +GITLAB_KEY= +GITLAB_SECRET= +GITLAB_CALLBACK_URL= +#GITLAB_TESTUSER= + +# https://developer.gitter.im/apps +GITTER_KEY= +GITTER_SECRET= +GITTER_CALLBACK_URL= +#GITTER_TESTUSER= + +# https://console.developers.google.com/apis/credentials +GOOGLE_KEY= +GOOGLE_SECRET= +GOOGLE_CALLBACK_URL= +GOOGLE_TESTUSER= + +# https://api.imgur.com/oauth2/addclient +IMGUR_KEY= +IMGUR_SECRET= +IMGUR_CALLBACK_URL= + +# https://www.instagram.com/developer/clients/manage/ +INSTAGRAM_KEY= +INSTAGRAM_SECRET= +INSTAGRAM_CALLBACK_URL= + +# http://www.last.fm/api/account/create +LASTFM_KEY= +LASTFM_SECRET= +LASTFM_CALLBACK_URL= + +# https://admin.mailchimp.com/account/oauth2/ +MAILCHIMP_KEY= +MAILCHIMP_SECRET= +MAILCHIMP_CALLBACK_URL= +#MAILCHIMP_TESTUSER= + +# https://{MASTODON INSTANCE}/settings/applications +MASTODON_KEY= +MASTODON_SECRET= +MASTODON_CALLBACK_URL= +MASTODON_INSTANCE=https://mastodon.social +#MASTODON_TESTUSER= + +# https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps +MICROSOFT_AAD_KEY= +MICROSOFT_AAD_SECRET= +MICROSOFT_AAD_CALLBACK_URL= +MICROSOFT_AAD_TESTUSER= + +# https://www.mixcloud.com/developers/create/ +MIXCLOUD_KEY= +MIXCLOUD_SECRET= +MIXCLOUD_CALLBACK_URL= +#MIXCLOUD_TESTUSER= + +# https://musicbrainz.org/account/applications +MUSICBRAINZ_KEY= +MUSICBRAINZ_SECRET= +MUSICBRAINZ_CALLBACK_URL= + +# https://dev.npr.org/console +NPRONE_KEY= +NPRONE_SECRET= +NPRONE_CALLBACK_URL= +#NPRONE_TESTUSER= + +# https://www.opencaching.de/okapi/signup.html +OKAPI_KEY= +OKAPI_SECRET= +OKAPI_CALLBACK_URL= +OKAPI_TESTUSER= + +# https://www.openstreetmap.org/user//oauth_clients +OPENSTREETMAP_KEY= +OPENSTREETMAP_SECRET= +OPENSTREETMAP_CALLBACK_URL= +#OPENSTREETMAP_TESTUSER= + +# https://www.openstreetmap.org/oauth2/applications +OPENSTREETMAP2_KEY= +OPENSTREETMAP2_SECRET= +OPENSTREETMAP2_CALLBACK_URL= +#OPENSTREETMAP2_TESTUSER= + +# https://www.patreon.com/portal/registration/register-clients +PATREON_KEY= +PATREON_SECRET= +PATREON_CALLBACK_URL= +#PATREON_TESTUSER= + +# https://developer.paypal.com/developer/applications/ +PAYPAL_KEY= +PAYPAL_SECRET= +PAYPAL_CALLBACK_URL= +#PAYPAL_TESTUSER= + +PAYPAL_SANDBOX_KEY= +PAYPAL_SANDBOX_SECRET= +PAYPAL_SANDBOX_CALLBACK_URL= +#PAYPAL_SANDBOX_TESTUSER= + +# https://api.slack.com/apps/ +SLACK_KEY= +SLACK_SECRET= +SLACK_CALLBACK_URL= +#SLACK_TESTUSER= + +# https://soundcloud.com/you/apps/ +SOUNDCLOUD_KEY= +SOUNDCLOUD_SECRET= +SOUNDCLOUD_CALLBACK_URL= +#SOUNDCLOUD_TESTUSER= + +# https://developer.spotify.com/my-applications/ +SPOTIFY_KEY= +SPOTIFY_SECRET= +SPOTIFY_CALLBACK_URL= +#SPOTIFY_TESTUSER= + +# http://steamcommunity.com/dev/apikey +STEAMOPENID_KEY= +STEAMOPENID_SECRET= +STEAMOPENID_CALLBACK_URL= + +# https://dashboard.stripe.com/account/apikeys +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_CALLBACK_URL= +#STRIPE_TESTUSER= + +# https://www.tumblr.com/oauth/apps +TUMBLR_KEY= +TUMBLR_SECRET= +TUMBLR_CALLBACK_URL= +#TUMBLR_TESTUSER= + +# https://www.twitch.tv/kraken/oauth2/clients/new +TWITCH_KEY= +TWITCH_SECRET= +TWITCH_CALLBACK_URL= +TWITCH_TESTUSER= + +# https://developer.twitter.com/apps +TWITTER_KEY= +TWITTER_SECRET= +TWITTER_CALLBACK_URL= + +# https://developer.vimeo.com/apps/ +VIMEO_KEY= +VIMEO_SECRET= +VIMEO_CALLBACK_URL= +#VIMEO_TESTUSER= + +# https://developer.wordpress.com/apps/new/ +WORDPRESS_KEY= +WORDPRESS_SECRET= +WORDPRESS_CALLBACK_URL= +#WORDPRESS_TESTUSER= diff --git a/examples/OAuthExampleSessionStorage.php b/examples/OAuthExampleSessionStorage.php new file mode 100644 index 00000000..a5f2ca7b --- /dev/null +++ b/examples/OAuthExampleSessionStorage.php @@ -0,0 +1,85 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\OAuthOptions; +use chillerlan\OAuth\Storage\{OAuthStorageException, SessionStorage}; +use chillerlan\Settings\SettingsContainerInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +class OAuthExampleSessionStorage extends SessionStorage{ + + protected string|null $storagepath; + + /** + * OAuthExampleSessionStorage constructor. + * + * @throws \chillerlan\OAuth\Storage\OAuthStorageException + */ + public function __construct( + OAuthOptions|SettingsContainerInterface $options = new OAuthOptions, + LoggerInterface $logger = new NullLogger, + string|null $storagepath = null, + ){ + parent::__construct($options, $logger); + + if($storagepath !== null){ + $storagepath = trim($storagepath); + + if(!is_dir($storagepath) || !is_writable($storagepath)){ + throw new OAuthStorageException('invalid storage path'); + } + + $storagepath = realpath($storagepath); + } + + $this->storagepath = $storagepath; + } + + /** + * @inheritDoc + */ + public function storeAccessToken(AccessToken $token, string $service = null):static{ + parent::storeAccessToken($token, $service); + + if($this->storagepath !== null){ + $tokenfile = sprintf('%s/%s.token.json', $this->storagepath, $this->getServiceName($service)); + + if(file_put_contents($tokenfile, $token->toJSON()) === false){ + throw new OAuthStorageException('unable to access file storage'); + } + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getAccessToken(string $service = null):AccessToken{ + $service = $this->getServiceName($service); + + if($this->hasAccessToken($service)){ + return (new AccessToken)->fromJSON($_SESSION[$this->tokenVar][$service]); + } + + if($this->storagepath !== null){ + $tokenfile = sprintf('%s/%s.token.json', $this->storagepath, $service); + + if(file_exists($tokenfile)){ + return (new AccessToken)->fromJSON(file_get_contents($tokenfile)); + } + } + + throw new OAuthStorageException(sprintf('token for service "%s" not found', $service)); + } + +} diff --git a/examples/OAuthProviderFactory.php b/examples/OAuthProviderFactory.php new file mode 100644 index 00000000..563add16 --- /dev/null +++ b/examples/OAuthProviderFactory.php @@ -0,0 +1,115 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +require_once __DIR__.'/OAuthExampleSessionStorage.php'; + +use chillerlan\DotEnv\DotEnv; +use chillerlan\OAuth\Core\OAuth1Interface; +use chillerlan\OAuth\Core\OAuth2Interface; +use chillerlan\OAuth\Core\OAuthInterface; +use chillerlan\OAuth\OAuthOptions; +use chillerlan\OAuth\Storage\MemoryStorage; +use chillerlan\Settings\SettingsContainerInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\NullHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use Psr\Log\LoggerInterface; + +/** + * + */ +class OAuthProviderFactory{ + + protected DotEnv $dotEnv; + protected LoggerInterface $logger; + protected OAuthOptions|SettingsContainerInterface $options; + + public function __construct( + protected ClientInterface $http, + protected RequestFactoryInterface $requestFactory, + protected StreamFactoryInterface $streamFactory, + protected UriFactoryInterface $uriFactory, + protected string $cfgDir = __DIR__.'/../config', + string $envFile = '.env', + string $logLevel = null, + ){ + ini_set('date.timezone', 'Europe/Amsterdam'); + + $this->dotEnv = (new DotEnv($this->cfgDir, $envFile, false))->load(); + $this->logger = $this->initLogger($logLevel); + } + + protected function initLogger(string|null $logLevel):LoggerInterface{ + $logger = new Logger('log', [new NullHandler]); + + if($logLevel !== null){ + $formatter = new LineFormatter(null, 'Y-m-d H:i:s', true, true); + $formatter->setJsonPrettyPrint(true); + + $logHandler = (new StreamHandler('php://stdout', $logLevel))->setFormatter($formatter); + + $logger->pushHandler($logHandler); + } + + return $logger; + } + + public function getProvider(string $providerFQN, string $envVar, bool $sessionStorage = true):OAuthInterface|OAuth1Interface|OAuth2Interface{ + $options = new OAuthOptions; + + $options->key = ($this->getEnvVar($envVar.'_KEY') ?? ''); + $options->secret = ($this->getEnvVar($envVar.'_SECRET') ?? ''); + $options->callbackURL = ($this->getEnvVar($envVar.'_CALLBACK_URL') ?? ''); + $options->tokenAutoRefresh = true; + $options->sessionStart = true; + + $storage = new MemoryStorage; + + if($sessionStorage === true){ + $storage = new OAuthExampleSessionStorage(options: $options, storagepath: $this->cfgDir); + } + + return new $providerFQN( + $options, + $this->http, + $this->requestFactory, + $this->streamFactory, + $this->uriFactory, + $storage, + $this->logger, + ); + } + + public function getEnvVar(string $var):mixed{ + return $this->dotEnv->get($var); + } + + public function getLogger():LoggerInterface{ + return $this->logger; + } + + public function getRequestFactory():RequestFactoryInterface{ + return $this->requestFactory; + } + + public function getStreamFactory():StreamFactoryInterface{ + return $this->streamFactory; + } + + public function getUriFactory():UriFactoryInterface{ + return $this->uriFactory; + } + +} diff --git a/examples/Providers/GitHub/gist-spotify-top-tracks.php b/examples/Providers/GitHub/gist-spotify-top-tracks.php new file mode 100644 index 00000000..ff9567c2 --- /dev/null +++ b/examples/Providers/GitHub/gist-spotify-top-tracks.php @@ -0,0 +1,97 @@ + + * @copyright 2022 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * invoke the spotify client first + * + * @var \chillerlan\OAuth\Providers\Spotify $spotify + */ +require_once __DIR__.'/../Spotify/spotify-common.php'; + +/** + * @var \OAuthProviderFactory $factory + * @var \chillerlan\OAuth\Providers\GitHub $github + */ + +require_once __DIR__.'/github-common.php'; + +$logger = $factory->getLogger(); + +$gistID = null; // set to null to create a new gist +$gistname = '🎵 My Spotify Top Tracks'; +$description = 'auto generated spotify track list'; +$public = false; + +// fetch top tracks +$tracks = $spotify->request(path: '/v1/me/top/tracks', params: ['time_range' => 'short_term']); +#$tracks = $spotify->request(path: '/v1/me/player/recently-played'); + +if($tracks->getStatusCode() !== 200){ + throw new RuntimeException('could not fetch spotify top tracks'); +} + +$json = MessageUtil::decodeJSON($tracks); +// the JSON body for the gist +$body = [ + 'description' => $description, + 'public' => $public, + 'files' => [ + $gistname => ['filename' => $gistname, 'content' => ''], + $gistname.'.md' => ['filename' => $gistname.'.md', 'content' => ''], + ], +]; + +// create the file content +foreach($json->items as $track){ + $t = ($track->track ?? $track); // recent tracks or top tracks object + + // plain text + $body['files'][$gistname]['content'] .= sprintf( + "%s - %s\n", + ($t->artists[0]->name ?? ''), + ($t->name ?? ''), + ); + + // markdown + $body['files'][$gistname.'.md']['content'] .= sprintf( + "1. [%s](%s) - [%s](%s)\n", // the "1." will create an ordered list starting at 1 + ($t->artists[0]->name ?? ''), + ($t->artists[0]->external_urls->spotify ?? ''), + ($t->name ?? ''), + ($t->external_urls->spotify ?? '') + ); +} + +// create/update the gist +$path = '/gists'; +$method = 'POST'; + +if($gistID !== null){ + $path .= '/'.$gistID; + $method = 'PATCH'; +} + +$response = $github->request(path: $path, method: $method, body: $body, headers: ['content-type' => 'application/json']); + +if($response->getStatusCode() === 201){ + $json = MessageUtil::decodeJSON($response); + + $logger->info(sprintf('created gist https://gist.github.com/%s', $json->id)); +} +elseif($response->getStatusCode() === 200){ + $logger->info(sprintf('updated gist https://gist.github.com/%s', $gistID)); +} +else{ + throw new RuntimeException(sprintf("error while creating/updating gist: \n\n%s", MessageUtil::toString($response))); +} + +exit; diff --git a/examples/Providers/GitHub/github-common.php b/examples/Providers/GitHub/github-common.php new file mode 100644 index 00000000..7bcd7178 --- /dev/null +++ b/examples/Providers/GitHub/github-common.php @@ -0,0 +1,16 @@ + + * @copyright 2022 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\GitHub; + +$ENVVAR = 'GITHUB'; + +require_once __DIR__.'/../../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$github = $factory->getProvider(GitHub::class, $ENVVAR); diff --git a/examples/Providers/LastFM/cache/.gitkeep b/examples/Providers/LastFM/cache/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/Providers/LastFM/lastfm-common.php b/examples/Providers/LastFM/lastfm-common.php new file mode 100644 index 00000000..56ee7e12 --- /dev/null +++ b/examples/Providers/LastFM/lastfm-common.php @@ -0,0 +1,17 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\LastFM; + +$ENVVAR = 'LASTFM'; + +require_once __DIR__.'/../../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$lfm = $factory->getProvider(LastFM::class, $ENVVAR); diff --git a/examples/Providers/LastFM/topalbum-patchwork.html b/examples/Providers/LastFM/topalbum-patchwork.html new file mode 100644 index 00000000..9336be17 --- /dev/null +++ b/examples/Providers/LastFM/topalbum-patchwork.html @@ -0,0 +1,66 @@ + + + + + + Last.fm top albums chart + + + + +
+ + + + + + + + + + + + + + + + + +
+
+ + + + + + + diff --git a/examples/Providers/LastFM/topalbum-patchwork.php b/examples/Providers/LastFM/topalbum-patchwork.php new file mode 100644 index 00000000..d0e5b36f --- /dev/null +++ b/examples/Providers/LastFM/topalbum-patchwork.php @@ -0,0 +1,154 @@ + + * @copyright 2019 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * @var \chillerlan\OAuth\Providers\LastFM $lfm + */ + +require_once __DIR__.'/lastfm-common.php'; + +$urlcache = './urlcache'; // downloaded album covers +$imgcache = './cache'; // generated patchworks + +try{ + $request = json_decode(file_get_contents('php://input')); + + if(!$request || !isset($request->username)){ + header('HTTP/1.1 400 Bad Request'); + sendResponse(['error' => 'invalid request']); + } + + $user = trim($request->username); + $rows = max(0, min(intval($request->height), 10)); + $cols = max(0, min(intval($request->width), 10)); + $imageSize = max(30, min(intval($request->imagesize), 150)); + $period = trim($request->period); + $limit = ($rows * $cols + 10); + + // doesn't necessarily need session auth, api key alone is sufficient + $response = $lfm->request('user.getTopAlbums', ['user' => $user, 'period' => $period, 'limit' => $limit]); + + if($response->getStatusCode() !== 200){ + header('HTTP/1.1 '.$response->getStatusCode().' '.$response->getReasonPhrase()); + sendResponse(['error' => 'last.fm error']); + } + + $json = MessageUtil::decodeJSON($response); + + if(!$json || !isset($json->topalbums->album)){ + header('HTTP/1.1 500 Internal Server Error'); + sendResponse(['error' => '...']); + } + + // a not-too-unique hash + $hash = sha1(json_encode([$rows, $cols, $imageSize, + array_column($json->topalbums->album, 'artist'), + array_column($json->topalbums->album, 'name'), + array_column($json->topalbums->album, 'mbid'), + ])); + + $imagefile = $imgcache.'/'.$hash.'.jpg'; + + if(file_exists($imagefile)){ + header('HTTP/1.1 200 OK'); + sendResponse(['image' => '', 'cached' => true]); + } + + $res = []; + + foreach(array_column($json->topalbums->album, 'image') as $img){ + + if(empty($img)){ + continue; + } + + try{ + $path = getImage($img[(count($img) - 1)]->{'#text'}, $urlcache); + $ext = pathinfo($path, PATHINFO_EXTENSION); + + $res[] = match($ext){ + 'jpg' => imagecreatefromjpeg($path), + 'png' => imagecreatefrompng($path), + 'gif' => imagecreatefromgif($path), + }; + } + catch(Throwable){ + continue; + } + } + + $patchwork = imagecreatetruecolor(($cols * $imageSize), ($rows * $imageSize)); + $bg = imagecolorallocate($patchwork, 0, 0, 0); + imagefill($patchwork, 0, 0, $bg); + + for($y = 0; $y < $rows; $y++){ + for($x = 0; $x < $cols; $x++){ + + if(empty($res)){ + break; + } + + $img = array_shift($res); + imagecopyresampled($patchwork, $img, ($x * $imageSize), ($y * $imageSize), 0, 0, $imageSize, $imageSize, imagesx($img), imagesy($img)); + imagedestroy($img); + } + } + + // save the image into a file + imagejpeg($patchwork, $imagefile, 85); + imagedestroy($patchwork); + + if(file_exists($imagefile)){ + header('HTTP/1.1 200 OK'); + sendResponse(['image' => '', 'cached' => false]); + } + +} +// Pokémon exception handler +catch(Exception $e){ + header('HTTP/1.1 500 Internal Server Error'); + sendResponse(['error' => $e->getMessage()]); +} + +exit; + +function getImage(string $url, string $urlcache):string{ + $path = parse_url($url, PHP_URL_PATH); + + if(file_exists($urlcache.$path)){ + return $urlcache.$path; + } + + $dir = $urlcache.dirname($path); + $imagedata = file_get_contents($url); + + if(!file_exists($dir)){ + mkdir($dir, 0777, true); + } + + file_put_contents($urlcache.$path, $imagedata); + + return $urlcache.$path; +} + +function sendResponse(array $response):void{ + header('Content-type: application/json;charset=utf-8;'); + + echo json_encode($response, JSON_PRETTY_PRINT); + exit; +} + diff --git a/examples/Providers/LastFM/urlcache/.gitkeep b/examples/Providers/LastFM/urlcache/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/Providers/Spotify/MixesDBTrackSearch.php b/examples/Providers/Spotify/MixesDBTrackSearch.php new file mode 100644 index 00000000..3c8d0a54 --- /dev/null +++ b/examples/Providers/Spotify/MixesDBTrackSearch.php @@ -0,0 +1,126 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * + */ +class MixesDBTrackSearch extends SpotifyClient{ + + /** + * search tracks on spotify from the given mixesdb track lists + */ + public function getTracks( + string $clubnightsJSON, + int $since, + int $until, + array $find = [], + int $limit = 5, + bool $playlistPerSet = false, + ):void{ + $clubnights = json_decode(file_get_contents($clubnightsJSON), true); + $tracks = []; + + foreach($clubnights as $date => $sets){ + $date = strtotime($date); + // skip by date + if($date < $since || $date > $until){ + continue; + } + + foreach($sets as $name => $set){ + // skip by inclusion list + if($this->setContains($name, $find)){ + continue; + } + + $this->logger->info($name); + $setTracks = []; + + foreach($set as $track){ + $track = $this->cleanTrack($track); + + if(empty($track)){ + continue; + } + + $this->logger->info(sprintf('search: %s', $track)); + + $response = $this->request('/v1/search', [ + 'q' => $this->getSearchTerm($track), + 'type' => 'track', + 'limit' => $limit, + 'market' => $this->market, + ]); + + usleep(self::sleepTimer); + + if($response->getStatusCode() !== 200){ + continue; + } + + $data = MessageUtil::decodeJSON($response); + + foreach($data->tracks->items as $i => $item){ + $setTracks[$item->id] = $item->id; + + $this->logger->info(sprintf('found: [%s][%s] %s - %s', ++$i, $item->id, implode(', ', array_column($item->artists, 'name')), $item->name)); + } + + } + + if($playlistPerSet){ + $playlistID = $this->createPlaylist($name, ''); + $this->addTracks($playlistID, $setTracks); + } + + $tracks = array_merge($tracks, $setTracks); + } + + } + + if(!$playlistPerSet){ + $playlistID = $this->createPlaylist('mixesdb search result', implode(', ', $find)); + $this->addTracks($playlistID, $tracks); + } + + } + + /** + * check a string for the occurence of any in the given array of needles + */ + protected function setContains(string $haystack, array $needles):bool{ + $haystack = mb_strtolower($haystack); + + return !empty($needles) && str_replace(array_map('mb_strtolower', $needles), '', $haystack) === $haystack; + } + + /** + * clean any unwanted symbols/strings from the track name + */ + protected function cleanTrack(string $track):string{ + // strip time codes [01:23] and record IDs [EYE Q - 001] from name + return trim(preg_replace(['/^\[[\d:?]+\] /', '/ \[[^]]+\]/'], '', $track), ' -?'); + } + + /** + * prepare the spotify search term + */ + protected function getSearchTerm(string $track):string{ + $at = explode(' - ', $track, 2); // artist - track + + return match (count($at)){ + 1 => sprintf('artist:%1$s track:%1$s', $at[0]), + 2 => sprintf('artist:%s track:%s', $at[0], $at[1]), + }; + } + +} diff --git a/examples/Providers/Spotify/SpotifyClient.php b/examples/Providers/Spotify/SpotifyClient.php new file mode 100644 index 00000000..c57db033 --- /dev/null +++ b/examples/Providers/Spotify/SpotifyClient.php @@ -0,0 +1,319 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\OAuthOptions; +use chillerlan\OAuth\Providers\Spotify; +use chillerlan\OAuth\Storage\MemoryStorage; +use chillerlan\OAuth\Storage\OAuthStorageInterface; +use chillerlan\Settings\SettingsContainerInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +/** + * + */ +class SpotifyClient extends Spotify{ + + protected const sleepTimer = 250000; // sleep between requests (µs) + + protected object $me; + protected string $id; + protected string $market; + protected array $artists = []; + protected array $albums = []; + + public function __construct( + OAuthOptions|SettingsContainerInterface $options, + ClientInterface $http, + RequestFactoryInterface $requestFactory, + StreamFactoryInterface $streamFactory, + UriFactoryInterface $uriFactory, + OAuthStorageInterface $storage = new MemoryStorage, + LoggerInterface $logger = new NullLogger + ){ + parent::__construct($options, $http, $requestFactory, $streamFactory, $uriFactory, $storage, $logger); + + // set the servicename to the original provider's name so that we use the same tokens + $this->serviceName = 'Spotify'; + $this->getMe(); + } + + /** + * @param string[] $vars + */ + protected function saveToFile(array $vars, string $dir):void{ + + foreach($vars as $var){ + file_put_contents( + sprintf('%s/%s.json', rtrim($dir, '\\/'), $var), + json_encode($this->{$var}, (JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)) + ); + } + + } + + /** + * @param string[] $vars + */ + protected function loadFromFile(array $vars, string $dir):bool{ + + foreach($vars as $var){ + $file = sprintf('%s/%s.json', rtrim($dir, '\\/'), $var); + + if(!file_exists($file)){ + return false; + } + + + $data = json_decode(file_get_contents($file)); + + foreach($data as $k => $v){ + $this->{$var}[$k] = $v; + } + + } + + return true; + } + + /** + * fetch the currently authenticated user + */ + protected function getMe():void{ + $me = $this->me(); + + if($me->getStatusCode() !== 200){ + throw new RuntimeException('could not fetch data from /me endpoint'); + } + + $json = MessageUtil::decodeJSON($me); + + if($json === false || !isset($json->country, $json->id)){ + throw new RuntimeException('invalid response from /me endpoint'); + } + + $this->me = $json; + $this->id = $this->me->id; + $this->market = $this->me->country; + } + + /** + * fetch the artists the user is following + */ + public function getFollowedArtists():array{ + $this->artists = []; + + $params = [ + 'type' => 'artist', + 'limit' => 50, // API max = 50 artists + 'after' => null, + ]; + + do{ + $meFollowing = $this->request('/v1/me/following', $params); + $data = MessageUtil::decodeJSON($meFollowing); + + if($meFollowing->getStatusCode() === 200){ + + foreach($data->artists->items as $artist){ + $this->artists[$artist->id] = $artist; + + $this->logger->info('artist: '.$artist->name); + } + + $params['after'] = ($data->artists->cursors->after ?? ''); + + $this->logger->info(sprintf('next cursor: %s', $params['after'])); + } + // not dealing with this + else{ + + if(isset($data->error)){ + $this->logger->error($data->error->message.' ('.$data->error->status.')'); + } + + break; + } + + usleep(self::sleepTimer); + } + while($params['after'] !== ''); + + $this->logger->info(sprintf('fetched %s artists', count($this->artists))); + + return $this->artists; + } + + /** + * fetch the releases for the followed artists + */ + public function getArtistReleases():array{ + $this->albums = []; + + foreach($this->artists as $artistID => $artist){ + // WTB bulk endpoint /artists/albums?ids=artist_id1,artist_id2,... + $artistAlbums = $this->request(sprintf('/v1/artists/%s/albums', $artistID), ['market' => $this->market]); + + if($artistAlbums->getStatusCode() !== 200){ + $this->logger->warning(sprintf('could not fetch albums for artist "%s"', $artist->name)); + + continue; + } + + $data = MessageUtil::decodeJSON($artistAlbums); + + if(!isset($data->items)){ + $this->logger->warning(sprintf('albums response empty for artist "%s"', $artist->name)); + + continue; + } + + foreach($data->items as $album){ + $this->albums[$artistID][$album->id] = $album; + + $this->logger->info(sprintf('album: %s - %s', $artist->name, $album->name)); + + } + + usleep(self::sleepTimer); + } + + return $this->albums; + } + + /** + * get the tracks from the given playlist + */ + public function getPlaylist(string $playlistID):array{ + + $params = [ + 'fields' => 'total,limit,offset,items(track(id,name,album(id,name),artists(id,name)))', + 'market' => $this->market, + 'offset' => 0, + 'limit' => 100, + ]; + + $playlist = []; + $retry = 0; + + do{ + $response = $this->request(sprintf('/v1/playlists/%s/tracks', $playlistID), $params); + + if($retry > 3){ + throw new RuntimeException('error while retrieving playlist'); + } + + if($response->getStatusCode() !== 200){ + $this->logger->warning(sprintf('playlist endpoint http/%s', $response->getStatusCode())); + + $retry++; + + continue; + } + + $json = MessageUtil::decodeJSON($response); + + if(!isset($json->items)){ + $this->logger->warning('empty playlist response'); + + $retry++; + + continue; + } + + foreach($json->items as $item){ + $playlist[$item->track->id] = $item->track; + } + + $params['offset'] += 100; + $retry = 0; + + } + while($params['offset'] <= $json->total); + + return $playlist; + } + + /** + * create a new playlist + */ + public function createPlaylist(string $name, string $description):string{ + + $createPlaylist = $this->request( + path : sprintf('/v1/users/%s/playlists', $this->id), + method : 'POST', + body : [ + 'name' => $name, + 'description' => $description, + // we'll never create public playlists - that's up to the user to decide + 'public' => false, + 'collaborative' => false, + ], + headers: ['Content-Type' => 'application/json'], + ); + + if($createPlaylist->getStatusCode() !== 201){ + throw new RuntimeException('could not create a new playlist'); + } + + $playlist = MessageUtil::decodeJSON($createPlaylist); + + if(!isset($playlist->id)){ + throw new RuntimeException('invalid create playlist response'); + } + + $this->logger->info(sprintf('created playlist: "%s" ("%s")', $name, $description)); + $this->logger->info(sprintf('spotify:user:%s:playlist:%s', $this->id, $playlist->id)); + $this->logger->info(sprintf('https://open.spotify.com/playlist/%s', $playlist->id)); + + return $playlist->id; + } + + /** + * add the tracks to the given playlist + */ + public function addTracks(string $playlistID, array $trackIDs):static{ + + $uris = array_chunk( + array_map(fn(string $t):string => 'spotify:track:'.$t , array_values($trackIDs)), // why not just ids??? + 100 // API max = 100 track URIs + ); + + foreach($uris as $i => $chunk){ + + $playlistAddTracks = $this->request( + path : sprintf('/v1/playlists/%s/tracks', $playlistID), + method : 'POST', + body : ['uris' => $chunk], + headers: ['Content-Type' => 'application/json'], + ); + + usleep(self::sleepTimer); + + if($playlistAddTracks->getStatusCode() === 201){ + $json = MessageUtil::decodeJSON($playlistAddTracks); + + $this->logger->info(sprintf('added tracks %s/%s [%s]', ++$i, count($uris), $json->snapshot_id)); + + continue; + } + + $this->logger->warning(sprintf('error adding tracks: http/%s', $playlistAddTracks->getStatusCode())); // idc + } + + return $this; + } + +} diff --git a/examples/Providers/Spotify/SpotifyNewReleases.php b/examples/Providers/Spotify/SpotifyNewReleases.php new file mode 100644 index 00000000..93989274 --- /dev/null +++ b/examples/Providers/Spotify/SpotifyNewReleases.php @@ -0,0 +1,148 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * + */ +class SpotifyNewReleases extends SpotifyClient{ + + protected array $newAlbums = []; + + /** + * the script runner + */ + public function getNewReleases( + int $since, + int $until, + int $minTracks, + bool $skipVariousArtist, + bool $skipAppearsOn, + bool $fromCache, + string $cacheDir = __DIR__, + ):void{ + $loaded = $fromCache && $this->loadFromFile(['artists', 'albums'], $cacheDir); + + if(!$loaded){ + $this->getFollowedArtists(); + $this->getArtistReleases(); + $this->saveToFile(['artists', 'albums'], $cacheDir); + } + + $this->filterReleases($since, $until, $minTracks, $skipVariousArtist, $skipAppearsOn); + $this->getNewAlbumTracks($since, $until); + } + + /** + * filters the releases for the followed artists and dumps the release info to the console + */ + public function filterReleases(int $since, int $until, int $minTracks, bool $skipVariousArtist, bool $skipAppearsOn):void{ + $this->newAlbums = []; + $releaseinfo = []; + + foreach($this->albums as $albums){ + + foreach($albums as $album){ + + // skip if the release has fewer than the minimum tracks + if($album->total_tracks < $minTracks){ + continue; + } + + // skip the "Various Artists" samplers + if( + $skipVariousArtist + && !empty($album->artists) + && strtolower($album->artists[0]->name) === 'various artists' + ){ + continue; + } + + // skip "appears on" releases + if($skipAppearsOn && $album->album_group === 'appears_on'){ + continue; + } + + $releaseDate = match($album->release_date_precision){ + 'month' => $album->release_date.'-01', + 'year' => $album->release_date.'-01-01', + default => $album->release_date, + }; + + $rdate = strtotime($releaseDate); + + // skip if the release is outside the date range + if($rdate < $since || $rdate > $until){ + continue; + } + + $this->newAlbums[$album->id] = $album->id; + $releaseinfo[$releaseDate][] = $album; + } + + } + + // sort the $releaseinfo array by release date (descending) + krsort($releaseinfo); + + // dump the new release info to console + foreach($releaseinfo as $date => $releases){ + [$year, $month, $day] = explode('-', $date); + + $this->logger->info(''); + $this->logger->info(date('--- l, jS F Y\: ---', mktime(0, 0, 0, (int)$month, (int)$day, (int)$year))); + $this->logger->info(''); + + foreach($releases as $k => $release){ + $this->logger->info('['.(++$k).'] '.implode(', ', array_column($release->artists, 'name')).' - '.$release->name); + } + + $this->logger->info(''); + } + + } + + /** + * fetches the tracks for the filtered releases and puts the first of each album into a playlist + */ + protected function getNewAlbumTracks(int $since, int $until):void{ + $newtracks = []; + + // fetch the album tracks (why aren't the tracks in the albums response???) + foreach(array_chunk(array_values($this->newAlbums), 20, true) as $chunk){ // API max = 20 albums + $albums = $this->request('/v1/albums', ['ids' => implode(',', $chunk), 'market' => $this->market]); + $data = MessageUtil::decodeJSON($albums); + + if(!isset($data->albums)){ + $this->logger->warning('invalid albums response'); + + continue; + } + + foreach($data->albums as $album){ + $tracks = array_column($album->tracks->items, 'id'); + $id = array_shift($tracks); + + $newtracks[$id] = $id; + } + + usleep(self::sleepTimer); + } + + $playlistID = $this->createPlaylist( + sprintf('new releases %s - %s', date('d.m.Y', $since), date('d.m.Y', $until)), + sprintf('new releases by the artists i\'m following, %s - %s', date('d.m.Y', $since), date('d.m.Y', $until)) + ); + + $this->addTracks($playlistID, $newtracks); + } + +} diff --git a/examples/Providers/Spotify/mixesdb-scrape.php b/examples/Providers/Spotify/mixesdb-scrape.php new file mode 100644 index 00000000..4f9c9ec0 --- /dev/null +++ b/examples/Providers/Spotify/mixesdb-scrape.php @@ -0,0 +1,116 @@ + + * @copyright 2023 smiley + * @license MIT + * + * @noinspection PhpComposerExtensionStubsInspection + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * @var \OAuthProviderFactory $factory + * @var \chillerlan\OAuth\Providers\Spotify $spotify + * @var \Psr\Log\LoggerInterface $logger + * @var string $file + */ +require_once __DIR__.'/spotify-common.php'; + +$logger = $factory->getLogger(); +$requestFactory = $factory->getRequestFactory(); + +$file ??= __DIR__.'/mixesdb-data.json'; +$baseURL = 'https://www.mixesdb.com'; +$catPath = '/db/index.php?title=Category:Clubnight'; +$tracklist = []; + +// suppress html parse errors +libxml_use_internal_errors(true); + +do{ + $logger->info($catPath); + + // fetch the category page + $catRequest = $requestFactory->createRequest('GET', $baseURL.$catPath); + $catResponse = $spotify->sendRequest($catRequest); + + if($catResponse->getStatusCode() !== 200){ + break; + } + + $catDOM = new DOMDocument('1.0', 'UTF-8'); + $catDOM->loadHTML(MessageUtil::getContents($catResponse)); + + // get the pages from the category list + foreach($catDOM->getElementById('catMixesList')->childNodes as $node){ + + if($node->nodeType !== XML_ELEMENT_NODE){ + continue; + } + + $page = $node->childNodes[0]->attributes->getNamedItem('href')->nodeValue; + + // get the date string + preg_match('#\d{4}-\d{2}-\d{2}#', $page, $match); + + if(!isset($match[0])){ + continue; + } + + // fetch the page + $pageRequest = $requestFactory->createRequest('GET', $baseURL.$page); + $pageResponse = $spotify->sendRequest($pageRequest); + + if($pageResponse->getStatusCode() !== 200){ + continue; + } + + $pageDOM = new DOMDocument('1.0', 'UTF-8'); + $pageDOM->loadHTML(MessageUtil::getContents($pageResponse)); + + $name = $pageDOM->getElementById('firstHeading')->nodeValue; + + if(!empty($name)){ + $logger->info($name); + + // get the tracklist + foreach($pageDOM->getElementsByTagName('ol') as $li){ + foreach($li->childNodes as $e){ + $tracklist[$match[0]][$name][] = trim($e->nodeValue); + } + } + + } + else{ + $logger->warning(sprintf('name empty for page: "%s"', $page)); + } + + // try not to hammer + usleep(500000); + } + + // get the next page from the category navigation + $catPath = null; + + foreach($catDOM->getElementById('catcount')->getElementsByTagName('a') as $node){ + if($node->textContent === 'next 200'){ + $catPath = $node->attributes->getNamedItem('href')->nodeValue; + + break; + } + } + +} +while(!empty($catPath)); + +file_put_contents($file, json_encode($tracklist, (JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE))); diff --git a/examples/Providers/Spotify/mixesdb-track-search.php b/examples/Providers/Spotify/mixesdb-track-search.php new file mode 100644 index 00000000..53f9bbb5 --- /dev/null +++ b/examples/Providers/Spotify/mixesdb-track-search.php @@ -0,0 +1,36 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +/** + * @var \OAuthProviderFactory $factory + * @var \chillerlan\OAuth\Providers\Spotify $spotify + * @var string $ENVVAR + */ +require_once __DIR__.'/spotify-common.php'; +require_once __DIR__.'/MixesDBTrackSearch.php'; + +$file = __DIR__.'/clubnights.json'; +$since = strtotime('1990-05-05'); // first clubnight: 1990-05-05 +$until = strtotime('2000-01-01'); // last clubnight: 2014-06-07 (studio), 2014-06-14 (live) +$find = ['Dag', 'Fenslau', 'Pascal' /* F.E.O.S. */, 'Talla', 'Taucher', 'Tom Wax', 'Ulli Brenner', 'Väth']; +$limit = 5; +$playlistPerSet = false; + +if(!file_exists($file)){ + include __DIR__.'/mixesdb-scrape.php'; +} + +$spotify = $factory->getProvider(MixesDBTrackSearch::class, $ENVVAR); +$spotify->getTracks($file, $since, $until, $find, $limit, $playlistPerSet); + +exit; diff --git a/examples/Providers/Spotify/new-releases.php b/examples/Providers/Spotify/new-releases.php new file mode 100644 index 00000000..1f096c05 --- /dev/null +++ b/examples/Providers/Spotify/new-releases.php @@ -0,0 +1,44 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +/** + * @var \OAuthProviderFactory $factory + * @var \chillerlan\OAuth\Providers\Spotify $spotify + * @var string $ENVVAR + */ + +require_once __DIR__.'/spotify-common.php'; +require_once __DIR__.'/SpotifyNewReleases.php'; + +$since = strtotime('last Saturday'); // (time() - 7 * 86400); // last week +$until = time(); // adjust to your likes +$minTracks = 1; // minimum number of tracks per album (1 = single releases) +$skipAppearsOn = true; +$skipVariousArtist = true; +$fromCache = false; + +$spotify = $factory->getProvider(SpotifyNewReleases::class, $ENVVAR); +$spotify->getNewReleases($since, $until, $minTracks, $skipVariousArtist, $skipAppearsOn, $fromCache); + +/* +// crawl for yearly album releases in the given range +foreach(range(1970, 1979) as $year){ + $since = \mktime(0, 0, 0, 1, 1, $year); + $until = \mktime(23, 59, 59, 12, 31, $year); + + $client->getNewReleases($since, $until, 5, false, true, true); +} +*/ + +exit; diff --git a/examples/Providers/Spotify/playlist-diff.php b/examples/Providers/Spotify/playlist-diff.php new file mode 100644 index 00000000..4d0a19aa --- /dev/null +++ b/examples/Providers/Spotify/playlist-diff.php @@ -0,0 +1,40 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +require_once __DIR__.'/spotify-common.php'; + +class PlaylistDiff extends SpotifyClient{ + + public function diff(string $playlistID1, string $playlistID2, bool $createAsPlaylist = false):array{ + $p1 = array_keys($this->getPlaylist($playlistID1)); + $p2 = array_keys($this->getPlaylist($playlistID2)); + $diff = array_diff($p1, $p2); + + if($createAsPlaylist){ + $playlistID = $this->createPlaylist( + 'playlist diff', + sprintf('diff between playlists "spotify:playlist:%s" and "spotify:playlist:%s"', $playlistID1, $playlistID2) + ); + $this->addTracks($playlistID, $diff); + } + + return $diff; + } + +}; + +/** + * @var \OAuthProviderFactory $factory + * @var \chillerlan\OAuth\Providers\Spotify $spotify + * @var string $ENVVAR + */ + +$spotify = $factory->getProvider(PlaylistDiff::class, $ENVVAR); +$spotify->diff('37i9dQZF1DX4UtSsGT1Sbe', '37i9dQZF1DXb57FjYWz00c', true); diff --git a/examples/Providers/Spotify/spotify-common.php b/examples/Providers/Spotify/spotify-common.php new file mode 100644 index 00000000..dd300481 --- /dev/null +++ b/examples/Providers/Spotify/spotify-common.php @@ -0,0 +1,21 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthExamples\Providers\Spotify; + +use chillerlan\OAuth\Providers\Spotify; + +$ENVVAR = 'SPOTIFY'; + +require_once __DIR__.'/../../provider-example-common.php'; +require_once __DIR__.'/SpotifyClient.php'; + +/** @var \OAuthProviderFactory $factory */ +$spotify = $factory->getProvider(Spotify::class, $ENVVAR); diff --git a/examples/create-docblocks.php b/examples/create-docblocks.php new file mode 100644 index 00000000..ce67b316 --- /dev/null +++ b/examples/create-docblocks.php @@ -0,0 +1,84 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Core\{ClientCredentials, OAuth1Interface, OAuth2Interface, OAuthInterface}; + +/** + * @var \Psr\Http\Client\ClientInterface $http + * @var \chillerlan\Settings\SettingsContainerInterface $options + * @var \Psr\Log\LoggerInterface $logger + */ + +require_once __DIR__.'/provider-example-common.php'; + +$table = [ + '| Provider | API keys | revoke access | OAuth | `ClientCredentials` |', + '|----------|----------|---------------|-------|---------------------|', +]; + +foreach(getProviders(__DIR__.'/../src') as $p){ + /** @var \OAuthProviderFactory $factory */ + $provider = $factory->getProvider($p['fqcn'], '', false); + + $oauth = match(true){ + $provider instanceof OAuth2Interface => '2', + $provider instanceof OAuth1Interface => '1', + default => '-', + }; + + $table[] = '| ['.$p['name'].']('.$provider->apiDocs.')'. + ' | [link]('.$provider->applicationURL.')'. + ' | '.((!$provider->userRevokeURL) ? '' : '[link]('.$provider->userRevokeURL.')'). + ' | '.$oauth. + ' | '.(($provider instanceof ClientCredentials) ? '✓' : ''). + ' |' ; + + printf("%s\n", $p['fqcn']); +} + +$file = __DIR__.'/../README.md'; +$readme = file_get_contents($file); +$start = (strpos($readme, '') + 8); +$end = strpos($readme, ''); + +file_put_contents($file, str_replace(substr($readme, $start, ($end - $start)), "\n".implode("\n", $table)."\n", $readme)); + +exit; + +// @todo +function getProviders(string $providerDir):array{ + $providerDir = realpath($providerDir); + $providers = []; + + /** @var \SplFileInfo $e */ + foreach(new IteratorIterator(new DirectoryIterator($providerDir)) as $e){ + + if($e->getExtension() !== 'php'){ + continue; + } + + $class = 'chillerlan\\OAuth\\Providers\\'.substr($e->getFilename(), 0, -4); + + try{ + $r = new ReflectionClass($class); + + if(!$r->implementsInterface(OAuthInterface::class) || $r->isAbstract()){ + continue; + } + + $providers[hash('crc32b', $r->getShortName())] = ['name' => $r->getShortName(), 'fqcn' => $class]; + } + catch(Throwable $e){ + continue; + } + + } + + return $providers; +} diff --git a/examples/get-token/Amazon.php b/examples/get-token/Amazon.php new file mode 100644 index 00000000..4c15b4e8 --- /dev/null +++ b/examples/get-token/Amazon.php @@ -0,0 +1,22 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Amazon; + +$ENVVAR ??= 'AMAZON'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Amazon::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/BattleNet.php b/examples/get-token/BattleNet.php new file mode 100644 index 00000000..81dbf459 --- /dev/null +++ b/examples/get-token/BattleNet.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\BattleNet; + +$ENVVAR ??= 'BATTLENET'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(BattleNet::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/BigCartel.php b/examples/get-token/BigCartel.php new file mode 100644 index 00000000..32a60b5c --- /dev/null +++ b/examples/get-token/BigCartel.php @@ -0,0 +1,28 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\BigCartel; + +$ENVVAR ??= 'BIGCARTEL'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(BigCartel::class, $ENVVAR); + +/* + * The BigCartel AccessToken instance holds additional values: + * + * $account_id = $token->extraParams['account_id']; + */ + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Bitbucket.php b/examples/get-token/Bitbucket.php new file mode 100644 index 00000000..ff4ebacb --- /dev/null +++ b/examples/get-token/Bitbucket.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Bitbucket; + +$ENVVAR ??= 'BITBUCKET'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Bitbucket::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Deezer.php b/examples/get-token/Deezer.php new file mode 100644 index 00000000..31a3b352 --- /dev/null +++ b/examples/get-token/Deezer.php @@ -0,0 +1,22 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Deezer; + +$ENVVAR ??= 'DEEZER'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Deezer::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/DeviantArt.php b/examples/get-token/DeviantArt.php new file mode 100644 index 00000000..41d1ec26 --- /dev/null +++ b/examples/get-token/DeviantArt.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\DeviantArt; + +$ENVVAR ??= 'DEVIANTART'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(DeviantArt::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Discogs.php b/examples/get-token/Discogs.php new file mode 100644 index 00000000..3b432dae --- /dev/null +++ b/examples/get-token/Discogs.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Discogs; + +$ENVVAR ??= 'DISCOGS'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Discogs::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/Discord.php b/examples/get-token/Discord.php new file mode 100644 index 00000000..3af43464 --- /dev/null +++ b/examples/get-token/Discord.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Discord; + +$ENVVAR ??= 'DISCORD'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Discord::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Flickr.php b/examples/get-token/Flickr.php new file mode 100644 index 00000000..cb2e3ea8 --- /dev/null +++ b/examples/get-token/Flickr.php @@ -0,0 +1,30 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Flickr; + +$ENVVAR ??= 'FLICKR'; +$PARAMS ??= ['perms' => Flickr::PERM_DELETE]; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Flickr::class, $ENVVAR); + +/* + * The Flickr AccessToken instance holds additional values: + * + * $user_name = $token->extraParams['username']; + * $user_id = $token->extraParams['user_nsid']; + */ + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/Foursquare.php b/examples/get-token/Foursquare.php new file mode 100644 index 00000000..6bdafe31 --- /dev/null +++ b/examples/get-token/Foursquare.php @@ -0,0 +1,22 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Foursquare; + +$ENVVAR ??= 'FOURSQUARE'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Foursquare::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2-no-state.php'; + +exit; diff --git a/examples/get-token/GitHub.php b/examples/get-token/GitHub.php new file mode 100644 index 00000000..7c6d56d5 --- /dev/null +++ b/examples/get-token/GitHub.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\GitHub; + +$ENVVAR ??= 'GITHUB'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(GitHub::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/GitLab.php b/examples/get-token/GitLab.php new file mode 100644 index 00000000..3a4b0aa6 --- /dev/null +++ b/examples/get-token/GitLab.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\GitLab; + +$ENVVAR ??= 'GITLAB'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(GitLab::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Google.php b/examples/get-token/Google.php new file mode 100644 index 00000000..a7b10305 --- /dev/null +++ b/examples/get-token/Google.php @@ -0,0 +1,23 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Google; + +$ENVVAR ??= 'GOOGLE'; +$PARAMS ??= ['access_type' => 'online']; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Google::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Imgur.php b/examples/get-token/Imgur.php new file mode 100644 index 00000000..bde97f2a --- /dev/null +++ b/examples/get-token/Imgur.php @@ -0,0 +1,59 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Imgur; + +$ENVVAR ??= 'IMGUR'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** + * @var \OAuthProviderFactory $factory + * @var array|null $PARAMS + * @var array|null $SCOPES + */ + +$provider = $factory->getProvider(Imgur::class, $ENVVAR); +$storage = $provider->getStorage(); +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS, $SCOPES)); +} +// step 3: receive the access token +elseif(isset($_GET['code']) && isset($_GET['state'])){ + $token = $provider->getAccessToken($_GET['code'], $_GET['state']); + + $username = $token->extraParams['account_username']; + $id = $token->extraParams['account_id']; + + // set the expiry to a sane period + $token->expires = (time() + 2592000); // 30 days + // save the token [...] + $storage->storeAccessToken($token); + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} + +exit; diff --git a/examples/get-token/LastFM.php b/examples/get-token/LastFM.php new file mode 100644 index 00000000..8a82211a --- /dev/null +++ b/examples/get-token/LastFM.php @@ -0,0 +1,51 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\LastFM; + +$ENVVAR ??= 'LASTFM'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** + * @var \OAuthProviderFactory $factory + * @var array|null $PARAMS + */ + +$provider = $factory->getProvider(LastFM::class, $ENVVAR); +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS)); +} +// step 3: receive the access token +elseif(isset($_GET['token'])){ + $token = $provider->getAccessToken($_GET['token']); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} + +exit; diff --git a/examples/get-token/MailChimp.php b/examples/get-token/MailChimp.php new file mode 100644 index 00000000..f399dbfa --- /dev/null +++ b/examples/get-token/MailChimp.php @@ -0,0 +1,57 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\MailChimp; + +$ENVVAR ??= 'MAILCHIMP'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** + * @var \OAuthProviderFactory $factory + * @var array|null $PARAMS + * @var array|null $SCOPES + */ + +$provider = $factory->getProvider(MailChimp::class, $ENVVAR); +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS, $SCOPES)); +} +// step 3: receive the access token +elseif(isset($_GET['code']) && isset($_GET['state'])){ + $token = $provider->getAccessToken($_GET['code'], $_GET['state']); + + // MailChimp needs another call to the auth metadata endpoint + // to receive the datacenter prefix/API URL, which will then + // be stored in AccessToken::$extraParams + $token = $provider->getTokenMetadata($token); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} + +exit; diff --git a/examples/get-token/Mastodon.php b/examples/get-token/Mastodon.php new file mode 100644 index 00000000..f8c0ed72 --- /dev/null +++ b/examples/get-token/Mastodon.php @@ -0,0 +1,24 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Mastodon; + +$ENVVAR ??= 'MASTODON'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Mastodon::class, $ENVVAR); +// set the mastodon instance we're about to request data from +$provider->setInstance($factory->getEnvVar($ENVVAR.'_INSTANCE')); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/MicrosoftGraph.php b/examples/get-token/MicrosoftGraph.php new file mode 100644 index 00000000..591a6b11 --- /dev/null +++ b/examples/get-token/MicrosoftGraph.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\MicrosoftGraph; + +$ENVVAR ??= 'MICROSOFT_AAD'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(MicrosoftGraph::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Mixcloud.php b/examples/get-token/Mixcloud.php new file mode 100644 index 00000000..847e7f34 --- /dev/null +++ b/examples/get-token/Mixcloud.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Mixcloud; + +$ENVVAR ??= 'MIXCLOUD'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Mixcloud::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2-no-state.php'; + +exit; diff --git a/examples/get-token/MusicBrainz.php b/examples/get-token/MusicBrainz.php new file mode 100644 index 00000000..e05bbdea --- /dev/null +++ b/examples/get-token/MusicBrainz.php @@ -0,0 +1,54 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\MusicBrainz; + +$ENVVAR ??= 'MUSICBRAINZ'; +$PARAMS ??= [ + 'access_type' => 'offline', + 'approval_prompt' => 'force', + 'state' => sha1(random_bytes(256)), +]; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(MusicBrainz::class, $ENVVAR); +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS)); +} +// step 3: receive the access token +elseif(isset($_GET['code']) && isset($_GET['state'])){ + $token = $provider->getAccessToken($_GET['code'], $_GET['state']); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + $response = $provider->request(sprintf('/artist/%s', '573510d6-bb5d-4d07-b0aa-ea6afe39e28d'), ['inc' => 'url-rels work-rels']); + + echo '
'.print_r(MessageUtil::decodeJSON($response), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} + +exit; diff --git a/examples/get-token/NPROne.php b/examples/get-token/NPROne.php new file mode 100644 index 00000000..232f41e3 --- /dev/null +++ b/examples/get-token/NPROne.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\NPROne; + +$ENVVAR ??= 'NPRONE'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(NPROne::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/OpenCaching.php b/examples/get-token/OpenCaching.php new file mode 100644 index 00000000..265b4a11 --- /dev/null +++ b/examples/get-token/OpenCaching.php @@ -0,0 +1,24 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\OpenCaching; + +$ENVVAR ??= 'OKAPI'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$PARAMS ??= ['oauth_callback' => $factory->getEnvVar($ENVVAR.'_CALLBACK_URL')]; + +$provider = $factory->getProvider(OpenCaching::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/OpenStreetmap.php b/examples/get-token/OpenStreetmap.php new file mode 100644 index 00000000..702e1372 --- /dev/null +++ b/examples/get-token/OpenStreetmap.php @@ -0,0 +1,22 @@ + + * @copyright 2019 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\OpenStreetmap; + +$ENVVAR ??= 'OPENSTREETMAP'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(OpenStreetmap::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/OpenStreetmap2.php b/examples/get-token/OpenStreetmap2.php new file mode 100644 index 00000000..0471dca8 --- /dev/null +++ b/examples/get-token/OpenStreetmap2.php @@ -0,0 +1,22 @@ + + * @copyright 2024 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\OpenStreetmap2; + +$ENVVAR ??= 'OPENSTREETMAP2'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(OpenStreetmap2::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Patreon.php b/examples/get-token/Patreon.php new file mode 100644 index 00000000..17aba124 --- /dev/null +++ b/examples/get-token/Patreon.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Patreon; + +$ENVVAR ??= 'PATREON'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Patreon::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/PayPal.php b/examples/get-token/PayPal.php new file mode 100644 index 00000000..a112141f --- /dev/null +++ b/examples/get-token/PayPal.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\PayPal; + +$ENVVAR ??= 'PAYPAL'; // PAYPAL_SANDBOX +$PARAMS ??= ['flowEntry' => 'static', 'fullPage' => 'true']; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(PayPal::class, $ENVVAR); // PayPalSandbox + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Slack.php b/examples/get-token/Slack.php new file mode 100644 index 00000000..b42792a1 --- /dev/null +++ b/examples/get-token/Slack.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Slack; + +$ENVVAR ??= 'SLACK'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Slack::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/SoundCloud.php b/examples/get-token/SoundCloud.php new file mode 100644 index 00000000..21d999b9 --- /dev/null +++ b/examples/get-token/SoundCloud.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\SoundCloud; + +$ENVVAR ??= 'SOUNDCLOUD'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(SoundCloud::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2-no-state.php'; + +exit; diff --git a/examples/get-token/Spotify.php b/examples/get-token/Spotify.php new file mode 100644 index 00000000..8daa7ad1 --- /dev/null +++ b/examples/get-token/Spotify.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Spotify; + +$ENVVAR ??= 'SPOTIFY'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Spotify::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/SteamOpenID.php b/examples/get-token/SteamOpenID.php new file mode 100644 index 00000000..68bac659 --- /dev/null +++ b/examples/get-token/SteamOpenID.php @@ -0,0 +1,52 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\SteamOpenID; + +$ENVVAR ??= 'STEAMOPENID'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(SteamOpenID::class, $ENVVAR); +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL()); +} +// step 3: receive the access token +elseif(isset($_GET['openid_sig']) && isset($_GET['openid_signed'])){ + $token = $provider->getAccessToken($_GET); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +//step 3.1: oh noes! +elseif(isset($_GET['openid_error'])){ // openid.error -> https://stackoverflow.com/questions/68651/ + exit('oh noes: '.$_GET['openid_error']); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + $token = $provider->getStorage()->getAccessToken($name); // the user's steamid is stored as access token + $response = $provider->request('/ISteamUser/GetPlayerSummaries/v2', ['steamids' => $token->accessToken]); + + echo '
'.print_r(MessageUtil::decodeJSON($response), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} diff --git a/examples/get-token/Stripe.php b/examples/get-token/Stripe.php new file mode 100644 index 00000000..5610de67 --- /dev/null +++ b/examples/get-token/Stripe.php @@ -0,0 +1,22 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Stripe; + +$ENVVAR ??= 'STRIPE'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Stripe::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Tumblr.php b/examples/get-token/Tumblr.php new file mode 100644 index 00000000..aa5d24f1 --- /dev/null +++ b/examples/get-token/Tumblr.php @@ -0,0 +1,22 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Tumblr; + +$ENVVAR ??= 'TUMBLR'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Tumblr::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/Twitch.php b/examples/get-token/Twitch.php new file mode 100644 index 00000000..5cfff734 --- /dev/null +++ b/examples/get-token/Twitch.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Twitch; + +$ENVVAR ??= 'TWITCH'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Twitch::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Twitter.php b/examples/get-token/Twitter.php new file mode 100644 index 00000000..cd072dc7 --- /dev/null +++ b/examples/get-token/Twitter.php @@ -0,0 +1,30 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Twitter; + +$ENVVAR ??= 'TWITTER'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Twitter::class, $ENVVAR); + +/* + * The Twitter AccessToken instance holds additional values: + * + * $screen_name = $token->extraParams['screen_name']; + * $user_id = $token->extraParams['user_id']; + */ + +require_once __DIR__.'/_flow-oauth1.php'; + +exit; diff --git a/examples/get-token/Vimeo.php b/examples/get-token/Vimeo.php new file mode 100644 index 00000000..937b6e53 --- /dev/null +++ b/examples/get-token/Vimeo.php @@ -0,0 +1,28 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\Vimeo; + +$ENVVAR ??= 'VIMEO'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(Vimeo::class, $ENVVAR); + +/* + * The Vimeo AccessToken instance holds additional values: + * + * $app = $token->extraParams['app']; + */ + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/Wordpress.php b/examples/get-token/Wordpress.php new file mode 100644 index 00000000..978b7356 --- /dev/null +++ b/examples/get-token/Wordpress.php @@ -0,0 +1,22 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +use chillerlan\OAuth\Providers\WordPress; + +$ENVVAR ??= 'WORDPRESS'; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthProviderFactory $factory */ +$provider = $factory->getProvider(WordPress::class, $ENVVAR); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/examples/get-token/_flow-oauth1.php b/examples/get-token/_flow-oauth1.php new file mode 100644 index 00000000..f17efc7f --- /dev/null +++ b/examples/get-token/_flow-oauth1.php @@ -0,0 +1,43 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * @var \chillerlan\OAuth\Core\OAuth1Interface $provider + * @var array|null $PARAMS + */ + +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS)); +} +// step 3: receive the access token +elseif(isset($_GET['oauth_token']) && isset($_GET['oauth_verifier'])){ + $token = $provider->getAccessToken($_GET['oauth_token'], $_GET['oauth_verifier']); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} diff --git a/examples/get-token/_flow-oauth2-no-state.php b/examples/get-token/_flow-oauth2-no-state.php new file mode 100644 index 00000000..4d5777b8 --- /dev/null +++ b/examples/get-token/_flow-oauth2-no-state.php @@ -0,0 +1,44 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * @var \chillerlan\OAuth\Core\OAuth2Interface $provider + * @var array|null $PARAMS + * @var array|null $SCOPES + */ + +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS, $SCOPES)); +} +// step 3: receive the access token +elseif(isset($_GET['code'])){ + $token = $provider->getAccessToken($_GET['code']); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} diff --git a/examples/get-token/_flow-oauth2.php b/examples/get-token/_flow-oauth2.php new file mode 100644 index 00000000..60f88ccf --- /dev/null +++ b/examples/get-token/_flow-oauth2.php @@ -0,0 +1,44 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +use chillerlan\HTTP\Utils\MessageUtil; + +/** + * @var \chillerlan\OAuth\Core\OAuth2Interface $provider + * @var array|null $PARAMS + * @var array|null $SCOPES + */ + +$name = $provider->serviceName; + +// step 2: redirect to the provider's login screen +if(isset($_GET['login']) && $_GET['login'] === $name){ + header('Location: '.$provider->getAuthURL($PARAMS, $SCOPES)); +} +// step 3: receive the access token +elseif(isset($_GET['code']) && isset($_GET['state'])){ + $token = $provider->getAccessToken($_GET['code'], $_GET['state']); + + // save the token [...] + + // access granted, redirect + header('Location: ?granted='.$name); +} +// step 4: verify the token and use the API +elseif(isset($_GET['granted']) && $_GET['granted'] === $name){ + echo '
'.print_r(MessageUtil::decodeJSON($provider->me()), true).'
'. + ''; +} +// step 1 (optional): display a login link +else{ + echo 'connect with '.$name.'!'; +} diff --git a/examples/provider-example-common.php b/examples/provider-example-common.php new file mode 100644 index 00000000..63b86b17 --- /dev/null +++ b/examples/provider-example-common.php @@ -0,0 +1,45 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\HttpFactory; + +/** + * allow to use a different autoloader to make it easier to use the examples (@todo: WIP) + * + * @var string $AUTOLOADER - path to an alternate autoloader + */ +require_once ($AUTOLOADER ?? __DIR__.'/../vendor/autoload.php'); +require_once __DIR__.'/OAuthProviderFactory.php'; + +/** + * these vars are supposed to be set before this file is included to ease testing + * + * @var string $ENVFILE - the name of the .env file in case it differs from the default + * @var string $ENVVAR - name prefix for the environment variable + * @var string $CFGDIR - the directory where configuration is stored (.env, cacert, tokens) + * @var string $LOGLEVEL - log level for the test logger, use 'none' to suppress logging + * @var array|null $PARAMS - additional params to pass to getAuthURL() + * @var array|null $SCOPES - a set of scopes for the current provider (OAuth2 only) + */ +$ENVFILE ??= '.env'; +$ENVVAR ??= ''; +$CFGDIR ??= __DIR__.'/../config'; +$LOGLEVEL ??= 'info'; +$PARAMS ??= null; +$SCOPES ??= null; + +$httpFactory = new HttpFactory; +$http = new Client([ + 'verify' => $CFGDIR.'/cacert.pem', + 'headers' => [ + 'User-Agent' => 'chillerlanPhpOAuth/5.0.0 +https://github.com/chillerlan/php-oauth-core', + ], +]); + +$factory = new OAuthProviderFactory($http, $httpFactory, $httpFactory, $httpFactory, $CFGDIR, $ENVFILE, $LOGLEVEL); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d8b5d854..66477b24 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,8 +23,17 @@ - + + + + + + + + diff --git a/src/Providers/Amazon.php b/src/Providers/Amazon.php new file mode 100644 index 00000000..69251ec4 --- /dev/null +++ b/src/Providers/Amazon.php @@ -0,0 +1,63 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * Amazon Login/OAuth + * + * @see https://login.amazon.com/ + * @see https://developer.amazon.com/docs/login-with-amazon/documentation-overview.html + * @see https://images-na.ssl-images-amazon.com/images/G/01/lwa/dev/docs/website-developer-guide._TTH_.pdf + * @see https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/offamazonpayments/LoginAndPayWithAmazonIntegrationGuide._V335378063_.pdf + */ +class Amazon extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_PROFILE = 'profile'; + public const SCOPE_PROFILE_USER_ID = 'profile:user_id'; + public const SCOPE_POSTAL_CODE = 'postal_code'; + + protected array $defaultScopes = [ + self::SCOPE_PROFILE, + self::SCOPE_PROFILE_USER_ID, + ]; + + protected string $authURL = 'https://www.amazon.com/ap/oa'; + protected string $accessTokenURL = 'https://www.amazon.com/ap/oatoken'; + protected string $apiURL = 'https://api.amazon.com'; + protected string|null $apiDocs = 'https://login.amazon.com/'; + protected string|null $applicationURL = 'https://sellercentral.amazon.com/hz/home'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/user/profile'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/AzureActiveDirectory.php b/src/Providers/AzureActiveDirectory.php new file mode 100644 index 00000000..04f90554 --- /dev/null +++ b/src/Providers/AzureActiveDirectory.php @@ -0,0 +1,30 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider}; + +/** + * @see https://docs.microsoft.com/azure/active-directory/develop/v2-app-types + */ +abstract class AzureActiveDirectory extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_OPENID = 'openid'; + public const SCOPE_OPENID_EMAIL = 'email'; + public const SCOPE_OPENID_PROFILE = 'profile'; + public const SCOPE_OFFLINE_ACCESS = 'offline_access'; + + protected string $authURL = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize'; + protected string $accessTokenURL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; + protected string|null $userRevokeURL = 'https://account.live.com/consent/Manage'; + protected string|null $applicationURL = 'https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps'; + +} diff --git a/src/Providers/BattleNet.php b/src/Providers/BattleNet.php new file mode 100644 index 00000000..a4d1961b --- /dev/null +++ b/src/Providers/BattleNet.php @@ -0,0 +1,103 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ClientCredentials, CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use Throwable; +use function in_array, sprintf, strtolower; + +/** + * Battle.net OAuth + * + * @see https://develop.battle.net/documentation + */ +class BattleNet extends OAuth2Provider implements ClientCredentials, CSRFToken{ + + public const SCOPE_OPENID = 'openid'; + public const SCOPE_PROFILE_D3 = 'd3.profile'; + public const SCOPE_PROFILE_SC2 = 'sc2.profile'; + public const SCOPE_PROFILE_WOW = 'wow.profile'; + + protected array $defaultScopes = [ + self::SCOPE_OPENID, + self::SCOPE_PROFILE_D3, + self::SCOPE_PROFILE_SC2, + self::SCOPE_PROFILE_WOW, + ]; + + protected string|null $apiDocs = 'https://develop.battle.net/documentation'; + protected string|null $applicationURL = 'https://develop.battle.net/access/clients'; + protected string|null $userRevokeURL = 'https://account.blizzard.com/connections'; + + // the URL for the "OAuth" endpoints + // @see https://develop.battle.net/documentation/battle-net/oauth-apis + protected string $battleNetOauth = 'https://oauth.battle.net'; + protected string $region = 'eu'; + // these URLs will be set dynamically, depending on the chose datacenter + protected string $apiURL = 'https://eu.api.blizzard.com'; + protected string $authURL = 'https://oauth.battle.net/authorize'; + protected string $accessTokenURL = 'https://oauth.battle.net/token'; + + /** + * Set the datacenter URLs for the given region + * + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function setRegion(string $region):static{ + $region = strtolower($region); + + if(!in_array($region, ['cn', 'eu', 'kr', 'tw', 'us'], true)){ + throw new ProviderException('invalid region: '.$region); + } + + $this->region = $region; + $this->apiURL = sprintf('https://%s.api.blizzard.com', $this->region); + $this->battleNetOauth = 'https://oauth.battle.net'; + + if($region === 'cn'){ + $this->apiURL = 'https://gateway.battlenet.com.cn'; + $this->battleNetOauth = 'https://oauth.battlenet.com.cn'; + } + + $this->authURL = $this->battleNetOauth.'/authorize'; + $this->accessTokenURL = $this->battleNetOauth.'/token'; + + return $this; + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $request = $this->requestFactory->createRequest('GET', $this->battleNetOauth.'/oauth/userinfo'); + $response = $this->sendRequest($this->getRequestAuthorization($request, $this->storage->getAccessToken())); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + try{ + $json = MessageUtil::decodeJSON($response); + } + catch(Throwable $e){ + } + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/BigCartel.php b/src/Providers/BigCartel.php new file mode 100644 index 00000000..4e04002e --- /dev/null +++ b/src/Providers/BigCartel.php @@ -0,0 +1,106 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate}; +use Psr\Http\Message\ResponseInterface; +use function sodium_bin2base64, sprintf; +use const SODIUM_BASE64_VARIANT_ORIGINAL; + +/** + * BigCartel OAuth + * + * @see https://developers.bigcartel.com/api/v1 + * @see https://bigcartel.wufoo.com/confirm/big-cartel-api-application/ + */ +class BigCartel extends OAuth2Provider implements CSRFToken, TokenInvalidate{ + + protected string $authURL = 'https://my.bigcartel.com/oauth/authorize'; + protected string $accessTokenURL = 'https://api.bigcartel.com/oauth/token'; + protected string $revokeURL = 'https://api.bigcartel.com/oauth/deauthorize/%s'; // sprintf() user id! + protected string $apiURL = 'https://api.bigcartel.com/v1'; + protected string|null $userRevokeURL = 'https://my.bigcartel.com/account'; + protected string|null $apiDocs = 'https://developers.bigcartel.com/api/v1'; + protected string|null $applicationURL = 'https://bigcartel.wufoo.com/forms/big-cartel-api-application/'; + protected array $apiHeaders = ['Accept' => 'application/vnd.api+json']; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/accounts'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error)){ + throw new ProviderException($json->error); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token === null && !$this->storage->hasAccessToken()){ + throw new ProviderException('no token given'); + } + + $token ??= $this->storage->getAccessToken(); + + $auth = sodium_bin2base64(sprintf('%s:%s', $this->options->key, $this->options->secret), SODIUM_BASE64_VARIANT_ORIGINAL); + + $request = $this->requestFactory + ->createRequest('POST', sprintf($this->revokeURL, $this->getAccountID($token))) + ->withHeader('Authorization', sprintf('Basic %s', $auth)) + ; + + // bypass the request authoritation + $response = $this->http->sendRequest($request); + + if($response->getStatusCode() === 204){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + + /** + * Try to get the user ID from either the token or the me() endpoint + * + * @throws \chillerlan\OAuth\Core\ProviderException + */ + protected function getAccountID(AccessToken $token):string{ + + if(isset($token->extraParams['account_id'])){ + return (string)$token->extraParams['account_id']; + } + + $json = MessageUtil::decodeJSON($this->me()); + + if(isset($json->data[0]->id)){ + return (string)$json->data[0]->id; + } + + throw new ProviderException('cannot determine account id'); + } + +} diff --git a/src/Providers/Bitbucket.php b/src/Providers/Bitbucket.php new file mode 100644 index 00000000..9d0e6c09 --- /dev/null +++ b/src/Providers/Bitbucket.php @@ -0,0 +1,49 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developer.atlassian.com/cloud/bitbucket/oauth-2/ + */ +class Bitbucket extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenRefresh{ + + protected string $authURL = 'https://bitbucket.org/site/oauth2/authorize'; + protected string $accessTokenURL = 'https://bitbucket.org/site/oauth2/access_token'; + protected string $apiURL = 'https://api.bitbucket.org/2.0'; + protected string|null $apiDocs = 'https://developer.atlassian.com/bitbucket/api/2/reference/'; + protected string|null $applicationURL = 'https://developer.atlassian.com/apps/'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/user'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error->message)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Deezer.php b/src/Providers/Deezer.php new file mode 100644 index 00000000..51792f19 --- /dev/null +++ b/src/Providers/Deezer.php @@ -0,0 +1,146 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\{ResponseInterface, UriInterface}; +use function array_merge, implode, sprintf; +use const PHP_QUERY_RFC1738; + +/** + * @see https://developers.deezer.com/api/oauth + * + * sure, you *can* use different parameter names than the standard ones... and what about JSON? + * https://xkcd.com/927/ + */ +class Deezer extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_BASIC = 'basic_access'; + public const SCOPE_EMAIL = 'email'; + public const SCOPE_OFFLINE_ACCESS = 'offline_access'; + public const SCOPE_MANAGE_LIBRARY = 'manage_library'; + public const SCOPE_MANAGE_COMMUNITY = 'manage_community'; + public const SCOPE_DELETE_LIBRARY = 'delete_library'; + public const SCOPE_LISTENING_HISTORY = 'listening_history'; + + protected array $defaultScopes = [ + self::SCOPE_BASIC, + self::SCOPE_EMAIL, + self::SCOPE_OFFLINE_ACCESS, + self::SCOPE_MANAGE_LIBRARY, + self::SCOPE_LISTENING_HISTORY, + ]; + + protected string $authURL = 'https://connect.deezer.com/oauth/auth.php'; + protected string $accessTokenURL = 'https://connect.deezer.com/oauth/access_token.php'; + protected string $apiURL = 'https://api.deezer.com'; + protected string|null $userRevokeURL = 'https://www.deezer.com/account/apps'; + protected string|null $apiDocs = 'https://developers.deezer.com/api'; + protected string|null $applicationURL = 'http://developers.deezer.com/myapps'; + protected int $authMethod = self::AUTH_METHOD_QUERY; + + /** + * @inheritDoc + */ + public function getAuthURL(array|null $params = null, array|null $scopes = null):UriInterface{ + $params ??= []; + + if(isset($params['client_secret'])){ + unset($params['client_secret']); + } + + $params = array_merge($params, [ + 'app_id' => $this->options->key, + 'redirect_uri' => $this->options->callbackURL, + 'perms' => implode($this->scopesDelimiter, ($scopes ?? [])), + ]); + + $params = $this->setState($params); + + return $this->uriFactory->createUri(QueryUtil::merge($this->authURL, $params)); + } + + /** + * @inheritDoc + */ + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + $this->checkState($state); + + $body = [ + 'app_id' => $this->options->key, + 'secret' => $this->options->secret, + 'code' => $code, + 'output' => 'json', + ]; + + $request = $this->requestFactory + ->createRequest('POST', $this->accessTokenURL) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('Accept-Encoding', 'identity') + ->withBody($this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738))); + + $token = $this->parseTokenResponse($this->http->sendRequest($request)); + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @inheritDoc + */ + protected function parseTokenResponse(ResponseInterface $response):AccessToken{ + $data = QueryUtil::parse(MessageUtil::decompress($response)); + + if(isset($data['error_reason'])){ + throw new ProviderException('error retrieving access token: "'.$data['error_reason'].'"'); + } + + if(!isset($data['access_token'])){ + throw new ProviderException('token missing'); + } + + $token = $this->createAccessToken(); + + $token->accessToken = $data['access_token']; + $token->expires = (int)($data['expires'] ?? $data['expires_in'] ?? AccessToken::EOL_NEVER_EXPIRES); + $token->refreshToken = ($data['refresh_token'] ?? null); + + unset($data['expires'], $data['expires_in'], $data['refresh_token'], $data['access_token']); + + $token->extraParams = $data; + + return $token; + } + + /** + * deezer keeps testing my sanity - HTTP/200 on invalid token... sure + * + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/user/me'); + $status = $response->getStatusCode(); + $json = MessageUtil::decodeJSON($response); + + if($status === 200 && !isset($json->error)){ + return $response; + } + + if(isset($json->error)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/DeviantArt.php b/src/Providers/DeviantArt.php new file mode 100644 index 00000000..12024bac --- /dev/null +++ b/src/Providers/DeviantArt.php @@ -0,0 +1,104 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ + AccessToken, ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate, TokenRefresh +}; +use Psr\Http\Message\ResponseInterface; +use Throwable; +use function sprintf; + +/** + * @see https://www.deviantart.com/developers/ + */ +class DeviantArt extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenInvalidate, TokenRefresh{ + + public const SCOPE_BASIC = 'basic'; + public const SCOPE_BROWSE = 'browse'; + public const SCOPE_COLLECTION = 'collection'; + public const SCOPE_COMMENT_POST = 'comment.post'; + public const SCOPE_FEED = 'feed'; + public const SCOPE_GALLERY = 'gallery'; + public const SCOPE_MESSAGE = 'message'; + public const SCOPE_NOTE = 'note'; + public const SCOPE_STASH = 'stash'; + public const SCOPE_USER = 'user'; + public const SCOPE_USER_MANAGE = 'user.manage'; + + protected array $defaultScopes = [ + self::SCOPE_BASIC, + self::SCOPE_BROWSE, + ]; + + protected string $authURL = 'https://www.deviantart.com/oauth2/authorize'; + protected string $accessTokenURL = 'https://www.deviantart.com/oauth2/token'; + protected string $revokeURL = 'https://www.deviantart.com/oauth2/revoke'; + protected string $apiURL = 'https://www.deviantart.com/api/v1/oauth2'; + protected string|null $userRevokeURL = 'https://www.deviantart.com/settings/applications'; + protected string|null $apiDocs = 'https://www.deviantart.com/developers/'; + protected string|null $applicationURL = 'https://www.deviantart.com/developers/apps'; + protected array $apiHeaders = ['dA-minor-version' => '20210526']; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/user/whoami'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token !== null){ + // to revoke a token different from the one of the currently authenticated user, + // we're going to clone the provider and feed the other token for the invalidate request + $provider = clone $this; + $provider->storeAccessToken($token); + $response = $provider->request(path: $this->revokeURL, method: 'POST'); + } + else{ + $response = $this->request(path: $this->revokeURL, method: 'POST'); + } + + try{ + $json = MessageUtil::decodeJSON($response); + } + catch(Throwable $e){ + return false; + } + + if($response->getStatusCode() === 200 && isset($json->success) && $json->success === true){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/Discogs.php b/src/Providers/Discogs.php new file mode 100644 index 00000000..25de594a --- /dev/null +++ b/src/Providers/Discogs.php @@ -0,0 +1,54 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\OAuth1Provider; +use chillerlan\OAuth\Core\ProviderException; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://www.discogs.com/developers/ + * @see https://www.discogs.com/developers/#page:authentication,header:authentication-oauth-flow + */ +class Discogs extends OAuth1Provider{ + + protected string $requestTokenURL = 'https://api.discogs.com/oauth/request_token'; + protected string $authURL = 'https://www.discogs.com/oauth/authorize'; + protected string $accessTokenURL = 'https://api.discogs.com/oauth/access_token'; + protected string $apiURL = 'https://api.discogs.com'; + protected string|null $userRevokeURL = 'https://www.discogs.com/settings/applications'; + protected string|null $apiDocs = 'https://www.discogs.com/developers/'; + protected string|null $applicationURL = 'https://www.discogs.com/settings/developers'; + protected array $apiHeaders = ['Accept' => 'application/vnd.discogs.v2.discogs+json']; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/oauth/identity'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Discord.php b/src/Providers/Discord.php new file mode 100644 index 00000000..4626eefa --- /dev/null +++ b/src/Providers/Discord.php @@ -0,0 +1,113 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ + AccessToken, ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate, TokenRefresh +}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://discord.com/developers/docs/topics/oauth2 + */ +class Discord extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenRefresh, TokenInvalidate{ + + public const SCOPE_APPLICATIONS_COMMANDS = 'applications.commands'; + public const SCOPE_APPLICATIONS_COMMANDS_UPDATE = 'applications.commands.update'; + public const SCOPE_APPLICATIONS_COMMANDS_PERMISSIONS_UPDATE = 'applications.commands.permissions.update'; + public const SCOPE_APPLICATIONS_ENTITLEMENTS = 'applications.entitlements'; + public const SCOPE_BOT = 'bot'; + public const SCOPE_CONNECTIONS = 'connections'; + public const SCOPE_EMAIL = 'email'; + public const SCOPE_GDM_JOIN = 'gdm.join'; + public const SCOPE_GUILDS = 'guilds'; + public const SCOPE_GUILDS_JOIN = 'guilds.join'; + public const SCOPE_GUILDS_MEMBERS_READ = 'guilds.members.read'; + public const SCOPE_IDENTIFY = 'identify'; + public const SCOPE_MESSAGES_READ = 'messages.read'; + public const SCOPE_RELATIONSHIPS_READ = 'relationships.read'; + public const SCOPE_ROLE_CONNECTIONS_WRITE = 'role_connections.write'; + public const SCOPE_RPC = 'rpc'; + public const SCOPE_RPC_ACTIVITIES_WRITE = 'rpc.activities.write'; + public const SCOPE_RPC_NOTIFICATIONS_READ = 'rpc.notifications.read'; + public const SCOPE_WEBHOOK_INCOMING = 'webhook.incoming'; + + protected array $defaultScopes = [ + self::SCOPE_CONNECTIONS, + self::SCOPE_EMAIL, + self::SCOPE_IDENTIFY, + self::SCOPE_GUILDS, + self::SCOPE_GUILDS_JOIN, + self::SCOPE_GDM_JOIN, + self::SCOPE_MESSAGES_READ, + ]; + + protected string $authURL = 'https://discordapp.com/api/oauth2/authorize'; + protected string $accessTokenURL = 'https://discordapp.com/api/oauth2/token'; + protected string $revokeURL = 'https://discordapp.com/api/oauth2/token/revoke'; + protected string $apiURL = 'https://discordapp.com/api'; + protected string|null $apiDocs = 'https://discordapp.com/developers/'; + protected string|null $applicationURL = 'https://discordapp.com/developers/applications/'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/users/@me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken $token = null):bool{ + + if($token === null && !$this->storage->hasAccessToken()){ + throw new ProviderException('no token given'); + } + + $token ??= $this->storage->getAccessToken(); + + $response = $this->request( + path : $this->revokeURL, + method : 'POST', + body : [ + 'client_id' => $this->options->key, + 'client_secret' => $this->options->secret, + 'token' => $token->accessToken, + ], + headers: ['Content-Type' => 'application/x-www-form-urlencoded'] + ); + + if($response->getStatusCode() === 200){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/Flickr.php b/src/Providers/Flickr.php new file mode 100644 index 00000000..9429c1ef --- /dev/null +++ b/src/Providers/Flickr.php @@ -0,0 +1,82 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{OAuth1Provider, ProviderException}; +use Psr\Http\Message\{ResponseInterface, StreamInterface}; +use function array_merge, sprintf; + +/** + * @see https://www.flickr.com/services/api/auth.oauth.html + * @see https://www.flickr.com/services/api/ + */ +class Flickr extends OAuth1Provider{ + + public const PERM_READ = 'read'; + public const PERM_WRITE = 'write'; + public const PERM_DELETE = 'delete'; + + protected string $requestTokenURL = 'https://www.flickr.com/services/oauth/request_token'; + protected string $authURL = 'https://www.flickr.com/services/oauth/authorize'; + protected string $accessTokenURL = 'https://www.flickr.com/services/oauth/access_token'; + protected string $apiURL = 'https://api.flickr.com/services/rest'; + protected string|null $userRevokeURL = 'https://www.flickr.com/services/auth/list.gne'; + protected string|null $apiDocs = 'https://www.flickr.com/services/api/'; + protected string|null $applicationURL = 'https://www.flickr.com/services/apps/create/'; + + /** + * @inheritDoc + */ + public function request( + string $path, + array|null $params = null, + string|null $method = null, + StreamInterface|array|string|null $body = null, + array|null $headers = null, + string|null $protocolVersion = null + ):ResponseInterface{ + + $params = array_merge(($params ?? []), [ + 'method' => $path, + 'format' => 'json', + 'nojsoncallback' => true, + ]); + + $request = $this->getRequestAuthorization( + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable */ + $this->requestFactory->createRequest(($method ?? 'POST'), QueryUtil::merge($this->apiURL, $params)), + $this->storage->getAccessToken($this->serviceName) + ); + + return $this->http->sendRequest($request); + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('flickr.test.login'); + $status = $response->getStatusCode(); + $json = MessageUtil::decodeJSON($response); + + if($status === 200 && isset($json->user)){ + return $response; + } + + if(isset($json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Foursquare.php b/src/Providers/Foursquare.php new file mode 100644 index 00000000..40733a78 --- /dev/null +++ b/src/Providers/Foursquare.php @@ -0,0 +1,73 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{OAuth2Provider, ProviderException}; +use Psr\Http\Message\{ResponseInterface, StreamInterface}; +use function array_merge, explode, sprintf; + +/** + * @see https://developer.foursquare.com/docs/ + * @see https://developer.foursquare.com/overview/auth + */ +class Foursquare extends OAuth2Provider{ + + protected const API_VERSIONDATE = '20190225'; + + protected string $authURL = 'https://foursquare.com/oauth2/authenticate'; + protected string $accessTokenURL = 'https://foursquare.com/oauth2/access_token'; + protected string $apiURL = 'https://api.foursquare.com'; + protected string|null $userRevokeURL = 'https://foursquare.com/settings/connections'; + protected string|null $apiDocs = 'https://developer.foursquare.com/docs'; + protected string|null $applicationURL = 'https://foursquare.com/developers/apps'; + protected string $authMethodQuery = 'oauth_token'; + protected int $authMethod = self::AUTH_METHOD_QUERY; + + /** + * @inheritDoc + */ + public function request( + string $path, + array|null $params = null, + string|null $method = null, + StreamInterface|array|string|null $body = null, + array|null $headers = null, + string|null $protocolVersion = null + ):ResponseInterface{ + $queryparams = QueryUtil::parse($this->uriFactory->createUri($this->apiURL.$path)->getPath()); + $queryparams['v'] = $this::API_VERSIONDATE; + $queryparams['m'] = 'foursquare'; + + return parent::request(explode('?', $path)[0], array_merge(($params ?? []), $queryparams), $method, $body, $headers); + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v2/users/self'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->meta, $json->meta->errorDetail)){ + throw new ProviderException($json->meta->errorDetail); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/GitHub.php b/src/Providers/GitHub.php new file mode 100644 index 00000000..b2a2e824 --- /dev/null +++ b/src/Providers/GitHub.php @@ -0,0 +1,86 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/ + * @see https://developer.github.com/v3/ + * @see https://docs.github.com/en/developers/apps/building-github-apps/refreshing-user-to-server-access-tokens + */ +class GitHub extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_USER = 'user'; + public const SCOPE_USER_EMAIL = 'user:email'; + public const SCOPE_USER_FOLLOW = 'user:follow'; + public const SCOPE_PUBLIC_REPO = 'public_repo'; + public const SCOPE_REPO = 'repo'; + public const SCOPE_REPO_DEPLOYMENT = 'repo_deployment'; + public const SCOPE_REPO_STATUS = 'repo:status'; + public const SCOPE_REPO_INVITE = 'repo:invite'; + public const SCOPE_REPO_DELETE = 'delete_repo'; + public const SCOPE_NOTIFICATIONS = 'notifications'; + public const SCOPE_GIST = 'gist'; + public const SCOPE_REPO_HOOK_READ = 'read:repo_hook'; + public const SCOPE_REPO_HOOK_WRITE = 'write:repo_hook'; + public const SCOPE_REPO_HOOK_ADMIN = 'admin:repo_hook'; + public const SCOPE_ORG_HOOK_ADMIN = 'admin:org_hook'; + public const SCOPE_ORG_READ = 'read:org'; + public const SCOPE_ORG_WRITE = 'write:org'; + public const SCOPE_ORG_ADMIN = 'admin:org'; + public const SCOPE_PUBLIC_KEY_READ = 'read:public_key'; + public const SCOPE_PUBLIC_KEY_WRITE = 'write:public_key'; + public const SCOPE_PUBLIC_KEY_ADMIN = 'admin:public_key'; + public const SCOPE_GPG_KEY_READ = 'read:gpg_key'; + public const SCOPE_GPG_KEY_WRITE = 'write:gpg_key'; + public const SCOPE_GPG_KEY_ADMIN = 'admin:gpg_key'; + + protected array $defaultScopes = [ + self::SCOPE_USER, + self::SCOPE_USER_EMAIL, + self::SCOPE_PUBLIC_REPO, + self::SCOPE_GIST, + ]; + + protected string $authURL = 'https://github.com/login/oauth/authorize'; + protected string $accessTokenURL = 'https://github.com/login/oauth/access_token'; + protected string $apiURL = 'https://api.github.com'; + protected string|null $userRevokeURL = 'https://github.com/settings/applications'; + protected string|null $apiDocs = 'https://developer.github.com/'; + protected string|null $applicationURL = 'https://github.com/settings/developers'; + protected array $authHeaders = ['Accept' => 'application/json']; + protected array $apiHeaders = ['Accept' => 'application/vnd.github.beta+json']; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/user'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/GitLab.php b/src/Providers/GitLab.php new file mode 100644 index 00000000..c6867652 --- /dev/null +++ b/src/Providers/GitLab.php @@ -0,0 +1,49 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://docs.gitlab.com/ee/api/oauth2.html + */ +class GitLab extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenRefresh{ + + protected string $authURL = 'https://gitlab.com/oauth/authorize'; + protected string $accessTokenURL = 'https://gitlab.com/oauth/token'; + protected string $apiURL = 'https://gitlab.com/api'; + protected string|null $apiDocs = 'https://docs.gitlab.com/ee/api/README.html'; + protected string|null $applicationURL = 'https://gitlab.com/profile/applications'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v4/user'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Google.php b/src/Providers/Google.php new file mode 100644 index 00000000..702c41e6 --- /dev/null +++ b/src/Providers/Google.php @@ -0,0 +1,62 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developers.google.com/identity/protocols/OAuth2WebServer + * @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount + * @see https://developers.google.com/oauthplayground/ + */ +class Google extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_EMAIL = 'email'; + public const SCOPE_PROFILE = 'profile'; + public const SCOPE_USERINFO_EMAIL = 'https://www.googleapis.com/auth/userinfo.email'; + public const SCOPE_USERINFO_PROFILE = 'https://www.googleapis.com/auth/userinfo.profile'; + + protected array $defaultScopes = [ + self::SCOPE_EMAIL, + self::SCOPE_PROFILE, + ]; + + protected string $authURL = 'https://accounts.google.com/o/oauth2/auth'; + protected string $accessTokenURL = 'https://accounts.google.com/o/oauth2/token'; + protected string $apiURL = 'https://www.googleapis.com'; + protected string|null $userRevokeURL = 'https://myaccount.google.com/permissions'; + protected string|null $apiDocs = 'https://developers.google.com/oauthplayground/'; + protected string|null $applicationURL = 'https://console.developers.google.com/apis/credentials'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/userinfo/v2/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error->message)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/GuildWars2.php b/src/Providers/GuildWars2.php new file mode 100644 index 00000000..1d6624ac --- /dev/null +++ b/src/Providers/GuildWars2.php @@ -0,0 +1,121 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{AccessToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\{ResponseInterface, UriInterface}; +use function implode, preg_match, sprintf, str_starts_with, substr; + +/** + * GW2 does not support authentication (anymore) but the API still works like a regular OAUth API, so... + * + * @see https://api.guildwars2.com/v2 + * @see https://wiki.guildwars2.com/wiki/API:Main + */ +class GuildWars2 extends OAuth2Provider{ + + public const SCOPE_ACCOUNT = 'account'; + public const SCOPE_INVENTORIES = 'inventories'; + public const SCOPE_CHARACTERS = 'characters'; + public const SCOPE_TRADINGPOST = 'tradingpost'; + public const SCOPE_WALLET = 'wallet'; + public const SCOPE_UNLOCKS = 'unlocks'; + public const SCOPE_PVP = 'pvp'; + public const SCOPE_BUILDS = 'builds'; + public const SCOPE_PROGRESSION = 'progression'; + public const SCOPE_GUILDS = 'guilds'; + + protected const AUTH_ERRMSG = 'GuildWars2 does not support authentication anymore.'; + + protected string $authURL = 'https://account.arena.net/applications/create'; + protected string $apiURL = 'https://api.guildwars2.com'; + protected string|null $userRevokeURL = 'https://account.arena.net/applications'; + protected string|null $apiDocs = 'https://wiki.guildwars2.com/wiki/API:Main'; + protected string|null $applicationURL = 'https://account.arena.net/applications'; + + /** + * @param string $access_token + * + * @return \chillerlan\OAuth\Core\AccessToken + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function storeGW2Token(string $access_token):AccessToken{ + + if(!preg_match('/^[a-f\d\-]{72}$/i', $access_token)){ + throw new ProviderException('invalid token'); + } + + // to verify the token we need to send a request without authentication + $request = $this->requestFactory + ->createRequest('GET', QueryUtil::merge($this->apiURL.'/v2/tokeninfo', ['access_token' => $access_token])) + ; + + $tokeninfo = MessageUtil::decodeJSON($this->http->sendRequest($request)); + + if(isset($tokeninfo->id) && str_starts_with($access_token, $tokeninfo->id)){ + $token = $this->createAccessToken(); + + $token->accessToken = $access_token; + $token->accessTokenSecret = substr($access_token, 36, 36); // the actual token + $token->expires = AccessToken::EOL_NEVER_EXPIRES; + $token->extraParams = [ + 'token_type' => 'Bearer', + 'id' => $tokeninfo->id, + 'name' => $tokeninfo->name, + 'scope' => implode($this->scopesDelimiter, $tokeninfo->permissions), + ]; + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + throw new ProviderException('unverified token'); // @codeCoverageIgnore + } + + /** + * @inheritdoc + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function getAuthURL(array|null $params = null, array|null $scopes = null):UriInterface{ + throw new ProviderException($this::AUTH_ERRMSG); + } + + /** + * @inheritdoc + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + throw new ProviderException($this::AUTH_ERRMSG); + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v2/tokeninfo'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->text)){ + throw new ProviderException($json->text); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Imgur.php b/src/Providers/Imgur.php new file mode 100644 index 00000000..b72498e2 --- /dev/null +++ b/src/Providers/Imgur.php @@ -0,0 +1,55 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * Note: imgur sends an "expires_in" of 315360000 (10 years!) for access tokens, + * but states in the docs that tokens expire after one month. + * Either manually saving the expiry with the token to trigger auto refresh + * or manually refreshing via the refreshAccessToken() method is required. + * + * @see https://apidocs.imgur.com/ + */ +class Imgur extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + protected string $authURL = 'https://api.imgur.com/oauth2/authorize'; + protected string $accessTokenURL = 'https://api.imgur.com/oauth2/token'; + protected string $apiURL = 'https://api.imgur.com'; + protected string|null $userRevokeURL = 'https://imgur.com/account/settings/apps'; + protected string|null $apiDocs = 'https://apidocs.imgur.com'; + protected string|null $applicationURL = 'https://api.imgur.com/oauth2/addclient'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/3/account/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->data, $json->data->error)){ + throw new ProviderException($json->data->error); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Instagram.php b/src/Providers/Instagram.php new file mode 100644 index 00000000..6502ca5d --- /dev/null +++ b/src/Providers/Instagram.php @@ -0,0 +1,66 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @todo: instagram API has changed entirely and i won't bother fixing it because reasons + * + * @see https://developers.facebook.com/docs/instagram + * @see https://developers.facebook.com/docs/instagram-basic-display-api/reference/oauth-authorize + */ +class Instagram extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_BASIC = 'basic'; + public const SCOPE_COMMENTS = 'comments'; + public const SCOPE_RELATIONSHIPS = 'relationships'; + public const SCOPE_LIKES = 'likes'; + public const SCOPE_PUBLIC_CONTENT = 'public_content'; + public const SCOPE_FOLLOWER_LIST = 'follower_list'; + + protected array $defaultScopes = [ + self::SCOPE_BASIC, + self::SCOPE_PUBLIC_CONTENT, + ]; + + protected string $authURL = 'https://api.instagram.com/oauth/authorize'; + protected string $accessTokenURL = 'https://api.instagram.com/oauth/access_token'; + protected string $apiURL = 'https://api.instagram.com'; + protected string|null $userRevokeURL = 'https://www.instagram.com/accounts/manage_access/'; + protected string|null $apiDocs = 'https://www.instagram.com/developer/'; + protected string|null $applicationURL = 'https://www.instagram.com/developer/clients/manage/'; +# protected int $authMethod = self::AUTH_METHOD_QUERY; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/me/'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/LastFM.php b/src/Providers/LastFM.php new file mode 100644 index 00000000..a8294abf --- /dev/null +++ b/src/Providers/LastFM.php @@ -0,0 +1,219 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{AccessToken, OAuthProvider, ProviderException}; +use Psr\Http\Message\{RequestInterface, ResponseInterface, StreamInterface, UriInterface}; +use Throwable; +use function array_merge, in_array, is_array, ksort, md5, sprintf, trigger_error; +use const PHP_QUERY_RFC1738; + +/** + * @see https://www.last.fm/api/authentication + */ +class LastFM extends OAuthProvider{ + + public const PERIOD_OVERALL = 'overall'; + public const PERIOD_7DAY = '7day'; + public const PERIOD_1MONTH = '1month'; + public const PERIOD_3MONTH = '3month'; + public const PERIOD_6MONTH = '6month'; + public const PERIOD_12MONTH = '12month'; + public const PERIODS = [ + self::PERIOD_OVERALL, + self::PERIOD_7DAY, + self::PERIOD_1MONTH, + self::PERIOD_3MONTH, + self::PERIOD_6MONTH, + self::PERIOD_12MONTH, + ]; + + protected string $authURL = 'https://www.last.fm/api/auth'; + protected string $apiURL = 'https://ws.audioscrobbler.com/2.0'; + protected string|null $userRevokeURL = 'https://www.last.fm/settings/applications'; + protected string|null $apiDocs = 'https://www.last.fm/api/'; + protected string|null $applicationURL = 'https://www.last.fm/api/account/create'; + + /** + * @inheritdoc + */ + public function getAuthURL(array|null $params = null):UriInterface{ + + $params = array_merge(($params ?? []), [ + 'api_key' => $this->options->key, + ]); + + return $this->uriFactory->createUri(QueryUtil::merge($this->authURL, $params)); + } + + /** + * + */ + protected function getSignature(array $params):string{ + ksort($params); + + $signature = ''; + + foreach($params as $k => $v){ + + if(in_array($k, ['format', 'callback'])){ + continue; + } + + $signature .= $k.$v; + } + + return md5($signature.$this->options->secret); + } + + /** + * + */ + public function getAccessToken(string $session_token):AccessToken{ + + $params = [ + 'method' => 'auth.getSession', + 'format' => 'json', + 'api_key' => $this->options->key, + 'token' => $session_token, + ]; + + $params['api_sig'] = $this->getSignature($params); + + $request = $this->requestFactory->createRequest('GET', QueryUtil::merge($this->apiURL, $params)); + + return $this->parseTokenResponse($this->http->sendRequest($request)); + } + + /** + * @throws \chillerlan\OAuth\Core\ProviderException + */ + protected function parseTokenResponse(ResponseInterface $response):AccessToken{ + + try{ + $data = MessageUtil::decodeJSON($response, true); + + if(!$data || !is_array($data)){ + trigger_error(''); + } + } + catch(Throwable $e){ + throw new ProviderException('unable to parse token response'); + } + + if(isset($data['error'])){ + throw new ProviderException('error retrieving access token: '.$data['message']); + } + elseif(!isset($data['session']['key'])){ + throw new ProviderException('token missing'); + } + + $token = $this->createAccessToken(); + + $token->accessToken = $data['session']['key']; + $token->expires = AccessToken::EOL_NEVER_EXPIRES; + + unset($data['session']['key']); + + $token->extraParams = $data; + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @inheritDoc + */ + public function request( + string $path, + array|null $params = null, + string|null $method = null, + StreamInterface|array|string|null $body = null, + array|null $headers = null, + string|null $protocolVersion = null + ):ResponseInterface{ + + if($body !== null && !is_array($body)){ + throw new ProviderException('$body must be an array'); + } + + $method ??= 'GET'; + $params ??= []; + $body ??= []; + + $params = array_merge($params, $body, [ + 'method' => $path, + 'format' => 'json', + 'api_key' => $this->options->key, + 'sk' => $this->storage->getAccessToken($this->serviceName)->accessToken, + ]); + + $params['api_sig'] = $this->getSignature($params); + + if($method === 'POST'){ + $body = $params; + $params = []; + } + + /** @phan-suppress-next-line PhanTypeMismatchArgumentNullable */ + $request = $this->requestFactory->createRequest($method, QueryUtil::merge($this->apiURL, $params)); + + foreach(array_merge($this->apiHeaders, ($headers ?? [])) as $header => $value){ + $request = $request->withAddedHeader($header, $value); + } + + if($method === 'POST'){ + $request = $request->withHeader('Content-Type', 'application/x-www-form-urlencoded'); + $body = $this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738)); + $request = $request->withBody($body); + } + + return $this->http->sendRequest($request); + } + + /** + * @inheritDoc + * @codeCoverageIgnore + */ + public function getRequestAuthorization(RequestInterface $request, AccessToken $token):RequestInterface{ + return $request; + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('user.getInfo'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @todo + * + * @param array $tracks + */ +# public function scrobble(array $tracks){} + +} diff --git a/src/Providers/MailChimp.php b/src/Providers/MailChimp.php new file mode 100644 index 00000000..9f2498f0 --- /dev/null +++ b/src/Providers/MailChimp.php @@ -0,0 +1,104 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException}; +use chillerlan\OAuth\OAuthException; +use Psr\Http\Message\{ResponseInterface, StreamInterface}; +use function array_merge, sprintf; + +/** + * @see http://developer.mailchimp.com/ + * @see http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-oauth2/ + */ +class MailChimp extends OAuth2Provider implements CSRFToken{ + + protected const API_BASE = 'https://%s.api.mailchimp.com'; + protected const METADATA_ENDPOINT = 'https://login.mailchimp.com/oauth2/metadata'; + + protected string $authURL = 'https://login.mailchimp.com/oauth2/authorize'; + protected string $accessTokenURL = 'https://login.mailchimp.com/oauth2/token'; + protected string|null $apiDocs = 'https://developer.mailchimp.com/'; + protected string|null $applicationURL = 'https://admin.mailchimp.com/account/oauth2/'; + protected string $authMethodHeader = 'OAuth'; + + /** + * @throws \chillerlan\OAuth\OAuthException + */ + public function getTokenMetadata(AccessToken|null $token = null):AccessToken{ + + $token ??= $this->storage->getAccessToken($this->serviceName); + + if(!$token instanceof AccessToken){ + throw new OAuthException('invalid token'); // @codeCoverageIgnore + } + + $request = $this->requestFactory + ->createRequest('GET', $this::METADATA_ENDPOINT) + ->withHeader('Authorization', 'OAuth '.$token->accessToken) + ; + + $response = $this->http->sendRequest($request); + + if($response->getStatusCode() !== 200){ + throw new OAuthException('metadata response error'); // @codeCoverageIgnore + } + + $token->extraParams = array_merge($token->extraParams, MessageUtil::decodeJSON($response, true)); + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * prepare the API URL from the token metadata + * + * @inheritdoc + */ + public function request( + string $path, + array|null $params = null, + string|null $method = null, + StreamInterface|array|string|null $body = null, + array|null $headers = null, + string|null $protocolVersion = null, + ):ResponseInterface{ + $token = $this->storage->getAccessToken($this->serviceName); + + $this->apiURL = sprintf($this::API_BASE, $token->extraParams['dc']); + + return parent::request($path, $params, $method, $body, $headers, $protocolVersion); + } + + /** + * @see https://mailchimp.com/developer/marketing/api/root/list-api-root-resources/ + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/3.0/'); // trailing slash! + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->detail)){ + throw new ProviderException($json->detail); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Mastodon.php b/src/Providers/Mastodon.php new file mode 100644 index 00000000..2660bd08 --- /dev/null +++ b/src/Providers/Mastodon.php @@ -0,0 +1,111 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use chillerlan\OAuth\OAuthException; +use Psr\Http\Message\ResponseInterface; +use function array_merge, sprintf; +use const PHP_QUERY_RFC1738; + +/** + * @see https://docs.joinmastodon.org/client/intro/ + * @see https://docs.joinmastodon.org/methods/apps/oauth/ + */ +class Mastodon extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_READ = 'read'; + public const SCOPE_WRITE = 'write'; + public const SCOPE_FOLLOW = 'follow'; + public const SCOPE_PUSH = 'push'; + + protected string|null $apiDocs = 'https://docs.joinmastodon.org/api/'; + + protected array $defaultScopes = [ + self::SCOPE_READ, + self::SCOPE_FOLLOW, + ]; + + protected string $instance = ''; + + /** + * set the internal URLs for the given Mastodon instance + * + * @throws \chillerlan\OAuth\OAuthException + */ + public function setInstance(string $instance):static{ + $instance = $this->uriFactory->createUri($instance)->withPath('')->withQuery('')->withFragment(''); + + if($instance->getHost() === ''){ + throw new OAuthException('invalid instance URL'); + } + + // @todo: check if host exists/responds + $this->instance = (string)$instance; + $this->apiURL = (string)$instance->withPath('/api'); + $this->authURL = (string)$instance->withPath('/oauth/authorize'); + $this->accessTokenURL = (string)$instance->withPath('/oauth/token'); + $this->userRevokeURL = (string)$instance->withPath('/oauth/authorized_applications'); + $this->applicationURL = (string)$instance->withPath('/settings/applications'); + + return $this; + } + + /** + * @inheritDoc + */ + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + + $body = [ + 'client_id' => $this->options->key, + 'client_secret' => $this->options->secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->options->callbackURL, + ]; + + $request = $this->requestFactory + ->createRequest('POST', $this->accessTokenURL) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('Accept-Encoding', 'identity') + ->withBody($this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738))); + + $token = $this->parseTokenResponse($this->http->sendRequest($request)); + // store the instance the token belongs to + $token->extraParams = array_merge($token->extraParams, ['instance' => $this->instance]); + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v1/accounts/verify_credentials'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error)){ + throw new ProviderException($json->error); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/MicrosoftGraph.php b/src/Providers/MicrosoftGraph.php new file mode 100644 index 00000000..2fc193d0 --- /dev/null +++ b/src/Providers/MicrosoftGraph.php @@ -0,0 +1,58 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\ProviderException; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://docs.microsoft.com/graph/permissions-reference + */ +class MicrosoftGraph extends AzureActiveDirectory{ + + public const SCOPE_USER_READ = 'User.Read'; + public const SCOPE_USER_READBASIC_ALL = 'User.ReadBasic.All'; + + protected array $defaultScopes = [ + self::SCOPE_OPENID, + self::SCOPE_OPENID_EMAIL, + self::SCOPE_OPENID_PROFILE, + self::SCOPE_OFFLINE_ACCESS, + self::SCOPE_USER_READ, + self::SCOPE_USER_READBASIC_ALL, + ]; + + protected string $apiURL = 'https://graph.microsoft.com'; + protected string|null $apiDocs = 'https://docs.microsoft.com/graph/overview'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v1.0/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error->message)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Mixcloud.php b/src/Providers/Mixcloud.php new file mode 100644 index 00000000..d64ff3a5 --- /dev/null +++ b/src/Providers/Mixcloud.php @@ -0,0 +1,53 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * note: a missing slash at the end of the path will end up in an HTTP/301 + * + * @see https://www.mixcloud.com/developers/ + */ +class Mixcloud extends OAuth2Provider{ + + protected string $authURL = 'https://www.mixcloud.com/oauth/authorize'; + protected string $accessTokenURL = 'https://www.mixcloud.com/oauth/access_token'; + protected string $apiURL = 'https://api.mixcloud.com'; + protected string|null $userRevokeURL = 'https://www.mixcloud.com/settings/applications/'; + protected string|null $apiDocs = 'https://www.mixcloud.com/developers/'; + protected string|null $applicationURL = 'https://www.mixcloud.com/developers/create/'; + protected int $authMethod = self::AUTH_METHOD_QUERY; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/me/'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error->message)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/MusicBrainz.php b/src/Providers/MusicBrainz.php new file mode 100644 index 00000000..126b1a06 --- /dev/null +++ b/src/Providers/MusicBrainz.php @@ -0,0 +1,121 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use chillerlan\HTTP\Utils\QueryUtil; +use Psr\Http\Message\{ResponseInterface, StreamInterface}; +use function date, explode, in_array, sprintf, strtoupper; +use const PHP_QUERY_RFC1738; + +/** + * @see https://musicbrainz.org/doc/Development + * @see https://musicbrainz.org/doc/Development/OAuth2 + */ +class MusicBrainz extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_PROFILE = 'profile'; + public const SCOPE_EMAIL = 'email'; + public const SCOPE_TAG = 'tag'; + public const SCOPE_RATING = 'rating'; + public const SCOPE_COLLECTION = 'collection'; + public const SCOPE_SUBMIT_ISRC = 'submit_isrc'; + public const SCOPE_SUBMIT_BARCODE = 'submit_barcode'; + + protected array $defaultScopes = [ + self::SCOPE_PROFILE, + self::SCOPE_EMAIL, + self::SCOPE_TAG, + self::SCOPE_RATING, + self::SCOPE_COLLECTION, + ]; + + protected string $authURL = 'https://musicbrainz.org/oauth2/authorize'; + protected string $accessTokenURL = 'https://musicbrainz.org/oauth2/token'; + protected string $apiURL = 'https://musicbrainz.org/ws/2'; + protected string|null $userRevokeURL = 'https://musicbrainz.org/account/applications'; + protected string|null $apiDocs = 'https://musicbrainz.org/doc/Development'; + protected string|null $applicationURL = 'https://musicbrainz.org/account/applications'; + + /** + * @inheritdoc + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function refreshAccessToken(AccessToken|null $token = null):AccessToken{ + + if($token === null){ + $token = $this->storage->getAccessToken($this->serviceName); + } + + $refreshToken = $token->refreshToken; + + if(empty($refreshToken)){ + throw new ProviderException( + sprintf('no refresh token available, token expired [%s]', date('Y-m-d h:i:s A', $token->expires)) + ); + } + + $body = [ + 'client_id' => $this->options->key, + 'client_secret' => $this->options->secret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ]; + + $request = $this->requestFactory + ->createRequest('POST', ($this->refreshTokenURL ?? $this->accessTokenURL)) // refreshTokenURL is used in tests + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('Accept-Encoding', 'identity') + ->withBody($this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738))) + ; + + $newToken = $this->parseTokenResponse($this->http->sendRequest($request)); + + if(empty($newToken->refreshToken)){ + $newToken->refreshToken = $refreshToken; + } + + $this->storage->storeAccessToken($newToken, $this->serviceName); + + return $newToken; + } + + /** + * @inheritDoc + */ + public function request( + string $path, + array|null $params = null, + string|null $method = null, + StreamInterface|array|string|null $body = null, + array|null $headers = null, + string|null $protocolVersion = null, + ):ResponseInterface{ + $params = ($params ?? []); + $method = strtoupper(($method ?? 'GET')); + $token = $this->storage->getAccessToken($this->serviceName); + + if($token->isExpired()){ + $this->refreshAccessToken($token); + } + + if(!isset($params['fmt'])){ + $params['fmt'] = 'json'; + } + + if(in_array($method, ['POST', 'PUT', 'DELETE']) && !isset($params['client'])){ + $params['client'] = $this->options->user_agent; // @codeCoverageIgnore + } + + return parent::request(explode('?', $path)[0], $params, $method, $body, $headers); + } + +} diff --git a/src/Providers/NPROne.php b/src/Providers/NPROne.php new file mode 100644 index 00000000..ccb387aa --- /dev/null +++ b/src/Providers/NPROne.php @@ -0,0 +1,157 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate, TokenRefresh}; +use Psr\Http\Message\{RequestInterface, ResponseInterface}; +use function in_array, ltrim, rtrim, sprintf, strtolower, str_contains; + +/** + * @see https://dev.npr.org + * @see https://github.com/npr/npr-one-backend-proxy-php + */ +class NPROne extends OAuth2Provider implements CSRFToken, TokenRefresh, TokenInvalidate{ + + public const SCOPE_IDENTITY_READONLY = 'identity.readonly'; + public const SCOPE_IDENTITY_WRITE = 'identity.write'; + public const SCOPE_LISTENING_READONLY = 'listening.readonly'; + public const SCOPE_LISTENING_WRITE = 'listening.write'; + public const SCOPE_LOCALACTIVATION = 'localactivation'; + + protected array $defaultScopes = [ + self::SCOPE_IDENTITY_READONLY, + self::SCOPE_LISTENING_READONLY, + ]; + + protected string $apiURL = 'https://listening.api.npr.org'; + protected string $authURL = 'https://authorization.api.npr.org/v2/authorize'; + protected string $accessTokenURL = 'https://authorization.api.npr.org/v2/token'; + protected string $revokeURL = 'https://authorization.api.npr.org/v2/token/revoke'; + protected string|null $apiDocs = 'https://dev.npr.org/api/'; + protected string|null $applicationURL = 'https://dev.npr.org/console'; + + /** + * Sets the API to work with ("listening" is set as default) + * + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function setAPI(string $api):static{ + $api = strtolower($api); + + if(!in_array($api, ['identity', 'listening', 'station'])){ + throw new ProviderException(sprintf('invalid API: "%s"', $api)); + } + + $this->apiURL = sprintf('https://%s.api.npr.org', $api); + + return $this; + } + + /** + * @inheritDoc + */ + protected function getRequestTarget(string $uri):string{ + $parsedURL = $this->uriFactory->createUri($uri); + + // for some reason we were given a host name + if($parsedURL->getHost() !== ''){ + + // back out if it doesn't match + if(!str_contains($parsedURL->getHost(), '.api.npr.org')){ + throw new ProviderException('given host does not match provider host'); // @codeCoverageIgnore + } + + // we explicitly ignore any existing parameters here + return (string)$parsedURL->withQuery('')->withFragment(''); + } + + $parsedPath = $parsedURL->getPath(); + $apiURL = rtrim($this->apiURL, '/'); + + if($parsedPath === ''){ + return $apiURL; + } + + return sprintf('%s/%s', $apiURL, ltrim($parsedPath, '/')); + } + + /** + * @inheritDoc + */ + public function sendRequest(RequestInterface $request):ResponseInterface{ + + // get authorization only if we request the provider API + if(str_contains((string)$request->getUri(), '.api.npr.org')){ + $token = $this->storage->getAccessToken($this->serviceName); + + // attempt to refresh an expired token + if($this->options->tokenAutoRefresh && ($token->isExpired() || $token->expires === $token::EOL_UNKNOWN)){ + $token = $this->refreshAccessToken($token); // @codeCoverageIgnore + } + + $request = $this->getRequestAuthorization($request, $token); + } + + return $this->http->sendRequest($request); + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('https://identity.api.npr.org/v2/user'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->errors)){ + throw new ProviderException($json->errors[0]->text); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token === null && !$this->storage->hasAccessToken()){ + throw new ProviderException('no token given'); + } + + $token ??= $this->storage->getAccessToken(); + + $response = $this->request( + path: $this->revokeURL, + method: 'POST', + body: [ + 'token' => $token->accessToken, + 'token_type_hint' => 'access_token', + ], + headers: ['Content-Type' => 'application/x-www-form-urlencoded'] + ); + + if($response->getStatusCode() === 200){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/OpenCaching.php b/src/Providers/OpenCaching.php new file mode 100644 index 00000000..5f4ece61 --- /dev/null +++ b/src/Providers/OpenCaching.php @@ -0,0 +1,51 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{OAuth1Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://www.opencaching.de/okapi/ + */ +class OpenCaching extends OAuth1Provider{ + + protected string $requestTokenURL = 'https://www.opencaching.de/okapi/services/oauth/request_token'; + protected string $authURL = 'https://www.opencaching.de/okapi/services/oauth/authorize'; + protected string $accessTokenURL = 'https://www.opencaching.de/okapi/services/oauth/access_token'; + protected string $apiURL = 'https://www.opencaching.de/okapi/services'; + protected string|null $userRevokeURL = 'https://www.opencaching.de/okapi/apps/'; + protected string|null $apiDocs = 'https://www.opencaching.de/okapi/'; + protected string|null $applicationURL = 'https://www.opencaching.de/okapi/signup.html'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/users/user', ['fields' => 'uuid|username|profile_url|internal_id|date_registered|caches_found|caches_notfound|caches_hidden|rcmds_given|rcmds_left|rcmd_founds_needed|home_location']); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/OpenStreetmap.php b/src/Providers/OpenStreetmap.php new file mode 100644 index 00000000..b9393950 --- /dev/null +++ b/src/Providers/OpenStreetmap.php @@ -0,0 +1,53 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{OAuth1Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf, strip_tags; + +/** + * @see https://wiki.openstreetmap.org/wiki/API + * @see https://wiki.openstreetmap.org/wiki/OAuth + * + * @deprecated https://github.com/openstreetmap/operations/issues/867 + */ +class OpenStreetmap extends OAuth1Provider{ + + protected string $requestTokenURL = 'https://www.openstreetmap.org/oauth/request_token'; + protected string $authURL = 'https://www.openstreetmap.org/oauth/authorize'; + protected string $accessTokenURL = 'https://www.openstreetmap.org/oauth/access_token'; + protected string $apiURL = 'https://api.openstreetmap.org'; + protected string|null $apiDocs = 'https://wiki.openstreetmap.org/wiki/API'; + protected string|null $applicationURL = 'https://www.openstreetmap.org/user/{USERNAME}/oauth_clients'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/api/0.6/user/details.json'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $body = MessageUtil::getContents($response); + + if(!empty($body)){ + throw new ProviderException(strip_tags($body)); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/OpenStreetmap2.php b/src/Providers/OpenStreetmap2.php new file mode 100644 index 00000000..fb003a74 --- /dev/null +++ b/src/Providers/OpenStreetmap2.php @@ -0,0 +1,71 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf, strip_tags; + +/** + * @see https://wiki.openstreetmap.org/wiki/API + * @see https://wiki.openstreetmap.org/wiki/OAuth + * @see https://www.openstreetmap.org/.well-known/oauth-authorization-server + * + * @see https://github.com/chillerlan/php-oauth-providers/issues/2 + */ +class OpenStreetmap2 extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_READ_PREFS = 'read_prefs'; + public const SCOPE_WRITE_PREFS = 'write_prefs'; + public const SCOPE_WRITE_DIARY = 'write_diary'; + public const SCOPE_WRITE_API = 'write_api'; + public const SCOPE_READ_GPX = 'read_gpx'; + public const SCOPE_WRITE_GPX = 'write_gpx'; + public const SCOPE_WRITE_NOTES = 'write_notes'; +# public const SCOPE_READ_EMAIL = 'read_email'; +# public const SCOPE_SKIP_AUTH = 'skip_authorization'; + public const SCOPE_WRITE_REDACTIONS = 'write_redactions'; + public const SCOPE_OPENID = 'openid'; + + protected array $defaultScopes = [ + self::SCOPE_READ_GPX, + self::SCOPE_READ_PREFS, + ]; + + protected string $authURL = 'https://www.openstreetmap.org/oauth2/authorize'; + protected string $accessTokenURL = 'https://www.openstreetmap.org/oauth2/token'; +# protected string $revokeURL = 'https://www.openstreetmap.org/oauth2/revoke'; // not implemented yet? + protected string $apiURL = 'https://api.openstreetmap.org'; + protected string|null $apiDocs = 'https://wiki.openstreetmap.org/wiki/API'; + protected string|null $applicationURL = 'https://www.openstreetmap.org/oauth2/applications'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/api/0.6/user/details.json'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $body = MessageUtil::getContents($response); + + if(!empty($body)){ + throw new ProviderException(strip_tags($body)); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Patreon.php b/src/Providers/Patreon.php new file mode 100644 index 00000000..91fd597f --- /dev/null +++ b/src/Providers/Patreon.php @@ -0,0 +1,88 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function explode, in_array, sprintf; + +/** + * @see https://docs.patreon.com/ + * @see https://docs.patreon.com/#oauth + * @see https://docs.patreon.com/#apiv2-oauth + */ +class Patreon extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_V1_USERS = 'users'; + public const SCOPE_V1_PLEDGES_TO_ME = 'pledges-to-me'; + public const SCOPE_V1_MY_CAMPAIGN = 'my-campaign'; + + // wow, consistency... + public const SCOPE_V2_IDENTITY = 'identity'; + public const SCOPE_V2_IDENTITY_EMAIL = 'identity[email]'; + public const SCOPE_V2_IDENTITY_MEMBERSHIPS = 'identity.memberships'; + public const SCOPE_V2_CAMPAIGNS = 'campaigns'; + public const SCOPE_V2_CAMPAIGNS_WEBHOOK = 'w:campaigns.webhook'; + public const SCOPE_V2_CAMPAIGNS_MEMBERS = 'campaigns.members'; + public const SCOPE_V2_CAMPAIGNS_MEMBERS_EMAIL = 'campaigns.members[email]'; + public const SCOPE_V2_CAMPAIGNS_MEMBERS_ADDRESS = 'campaigns.members.address'; + + protected array $defaultScopes = [ + self::SCOPE_V2_IDENTITY, + self::SCOPE_V2_IDENTITY_EMAIL, + self::SCOPE_V2_IDENTITY_MEMBERSHIPS, + self::SCOPE_V2_CAMPAIGNS, + self::SCOPE_V2_CAMPAIGNS_MEMBERS, + ]; + + protected string $authURL = 'https://www.patreon.com/oauth2/authorize'; + protected string $accessTokenURL = 'https://www.patreon.com/api/oauth2/token'; + protected string $apiURL = 'https://www.patreon.com/api/oauth2'; + protected string|null $apiDocs = 'https://docs.patreon.com/'; + protected string|null $applicationURL = 'https://www.patreon.com/portal/registration/register-clients'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $token = $this->storage->getAccessToken($this->serviceName); + $scopes = explode(' ', $token->extraParams['scope']); + + if(in_array(self::SCOPE_V2_IDENTITY, $scopes)){ + $endpoint = '/v2/identity'; + $params = ['fields[user]' => 'about,created,email,first_name,full_name,image_url,last_name,social_connections,thumb_url,url,vanity']; + } + elseif(in_array(self::SCOPE_V1_USERS, $scopes)){ + $endpoint = '/api/current_user'; + $params = []; + } + else{ + throw new ProviderException('invalid scopes for the identity endpoint'); + } + + $response = $this->request($endpoint, $params); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->errors[0]->code_name)){ + throw new ProviderException($json->errors[0]->code_name); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/PayPal.php b/src/Providers/PayPal.php new file mode 100644 index 00000000..c34e793e --- /dev/null +++ b/src/Providers/PayPal.php @@ -0,0 +1,131 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{AccessToken, ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function array_column, base64_encode, explode, implode, is_array, json_decode, sprintf; +use const PHP_QUERY_RFC1738; + +/** + * @see https://developer.paypal.com/docs/connect-with-paypal/integrate/ + */ +class PayPal extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenRefresh{ + + public const SCOPE_BASIC_AUTH = 'openid'; + public const SCOPE_FULL_NAME = 'profile'; + public const SCOPE_EMAIL = 'email'; + public const SCOPE_ADDRESS = 'address'; + public const SCOPE_ACCOUNT = 'https://uri.paypal.com/services/paypalattributes'; + + protected array $defaultScopes = [ + self::SCOPE_BASIC_AUTH, + self::SCOPE_EMAIL, + ]; + + protected string $accessTokenURL = 'https://api.paypal.com/v1/oauth2/token'; + protected string $authURL = 'https://www.paypal.com/connect'; + protected string $apiURL = 'https://api.paypal.com'; + protected string|null $applicationURL = 'https://developer.paypal.com/developer/applications/'; + protected string|null $apiDocs = 'https://developer.paypal.com/docs/connect-with-paypal/reference/'; + + /** + * @inheritDoc + */ + protected function parseTokenResponse(ResponseInterface $response):AccessToken{ + $data = json_decode(MessageUtil::decompress($response), true); + + if(!is_array($data)){ + throw new ProviderException('unable to parse token response'); + } + + if(isset($data['error'])){ + throw new ProviderException(sprintf('error retrieving access token: "%s"', $data['error'])); + } + + // @codeCoverageIgnoreStart + if(isset($data['name'], $data['message'])){ + $msg = sprintf('error retrieving access token: "%s" [%s]', $data['message'], $data['name']); + + if(isset($data['links']) && is_array($data['links'])){ + $msg .= "\n".implode("\n", array_column($data['links'], 'href')); + } + + throw new ProviderException($msg); + } + // @codeCoverageIgnoreEnd + + if(!isset($data['access_token'])){ + throw new ProviderException('token missing'); + } + + $token = $this->createAccessToken(); + + $token->accessToken = $data['access_token']; + $token->expires = ($data['expires_in'] ?? AccessToken::EOL_NEVER_EXPIRES); + $token->refreshToken = ($data['refresh_token'] ?? null); + $token->scopes = explode($this->scopesDelimiter, ($data['scope'] ?? '')); + + unset($data['expires_in'], $data['refresh_token'], $data['access_token'], $data['scope']); + + $token->extraParams = $data; + + return $token; + } + + /** + * @inheritDoc + */ + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + $this->checkState($state); // we're an instance of CSRFToken + + $body = [ + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->options->callbackURL, + ]; + + $request = $this->requestFactory + ->createRequest('POST', $this->accessTokenURL) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withHeader('Accept-Encoding', 'identity') + ->withHeader('Authorization', 'Basic '.base64_encode($this->options->key.':'.$this->options->secret)) + ->withBody($this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738))); + + $token = $this->parseTokenResponse($this->http->sendRequest($request)); + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v1/identity/oauth2/userinfo', ['schema' => 'paypalv1.1']); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/PayPalSandbox.php b/src/Providers/PayPalSandbox.php new file mode 100644 index 00000000..1cae61c1 --- /dev/null +++ b/src/Providers/PayPalSandbox.php @@ -0,0 +1,22 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +/** + * + */ +class PayPalSandbox extends PayPal{ + + protected string $authURL = 'https://www.sandbox.paypal.com/connect'; + protected string $accessTokenURL = 'https://api.sandbox.paypal.com/v1/oauth2/token'; + protected string $apiURL = 'https://api.sandbox.paypal.com'; + +} diff --git a/src/Providers/Slack.php b/src/Providers/Slack.php new file mode 100644 index 00000000..4c693373 --- /dev/null +++ b/src/Providers/Slack.php @@ -0,0 +1,114 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://api.slack.com/docs/oauth + * @see https://api.slack.com/docs/sign-in-with-slack + * @see https://api.slack.com/docs/token-types + */ +class Slack extends OAuth2Provider implements CSRFToken{ + + // bot token + public const SCOPE_BOT = 'bot'; + + // user token + public const SCOPE_ADMIN = 'admin'; + public const SCOPE_CHAT_WRITE_BOT = 'chat:write:bot'; + public const SCOPE_CLIENT = 'client'; + public const SCOPE_DND_READ = 'dnd:read'; + public const SCOPE_DND_WRITE = 'dnd:write'; + public const SCOPE_FILES_READ = 'files:read'; + public const SCOPE_FILES_WRITE_USER = 'files:write:user'; + public const SCOPE_IDENTIFY = 'identify'; + public const SCOPE_IDENTITY_AVATAR = 'identity.avatar'; + public const SCOPE_IDENTITY_BASIC = 'identity.basic'; + public const SCOPE_IDENTITY_EMAIL = 'identity.email'; + public const SCOPE_IDENTITY_TEAM = 'identity.team'; + public const SCOPE_INCOMING_WEBHOOK = 'incoming-webhook'; + public const SCOPE_POST = 'post'; + public const SCOPE_READ = 'read'; + public const SCOPE_REMINDERS_READ = 'reminders:read'; + public const SCOPE_REMINDERS_WRITE = 'reminders:write'; + public const SCOPE_SEARCH_READ = 'search:read'; + public const SCOPE_STARS_READ = 'stars:read'; + public const SCOPE_STARS_WRITE = 'stars:write'; + + // user & workspace tokens + public const SCOPE_CHANNELS_HISTORY = 'channels:history'; + public const SCOPE_CHANNELS_READ = 'channels:read'; + public const SCOPE_CHANNELS_WRITE = 'channels:write'; + public const SCOPE_CHAT_WRITE_USER = 'chat:write:user'; + public const SCOPE_COMMANDS = 'commands'; + public const SCOPE_EMOJI_READ = 'emoji:read'; + public const SCOPE_GROUPS_HISTORY = 'groups:history'; + public const SCOPE_GROUPS_READ = 'groups:read'; + public const SCOPE_GROUPS_WRITE = 'groups:write'; + public const SCOPE_IM_HISTORY = 'im:history'; + public const SCOPE_IM_READ = 'im:read'; + public const SCOPE_IM_WRITE = 'im:write'; + public const SCOPE_LINKS_READ = 'links:read'; + public const SCOPE_LINKS_WRITE = 'links:write'; + public const SCOPE_MPIM_HISTORY = 'mpim:history'; + public const SCOPE_MPIM_READ = 'mpim:read'; + public const SCOPE_MPIM_WRITE = 'mpim:write'; + public const SCOPE_PINS_READ = 'pins:read'; + public const SCOPE_PINS_WRITE = 'pins:write'; + public const SCOPE_REACTIONS_READ = 'reactions:read'; + public const SCOPE_REACTIONS_WRITE = 'reactions:write'; + public const SCOPE_TEAM_READ = 'team:read'; + public const SCOPE_USERGROUPS_READ = 'usergroups:read'; + public const SCOPE_USERGROUPS_WRITE = 'usergroups:write'; + public const SCOPE_USERS_PROFILE_READ = 'users.profile:read'; + public const SCOPE_USERS_PROFILE_WRITE = 'users.profile:write'; + public const SCOPE_USERS_READ = 'users:read'; + public const SCOPE_USERS_READ_EMAIL = 'users:read.email'; + public const SCOPE_USERS_WRITE = 'users:write'; + + protected array $defaultScopes = [ + self::SCOPE_IDENTITY_AVATAR, + self::SCOPE_IDENTITY_BASIC, + self::SCOPE_IDENTITY_EMAIL, + self::SCOPE_IDENTITY_TEAM, + ]; + + protected string $authURL = 'https://slack.com/oauth/authorize'; + protected string $accessTokenURL = 'https://slack.com/api/oauth.access'; + protected string $apiURL = 'https://slack.com/api'; + protected string|null $userRevokeURL = 'https://slack.com/apps/manage'; + protected string|null $apiDocs = 'https://api.slack.com'; + protected string|null $applicationURL = 'https://api.slack.com/apps'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/users.identity'); + $status = $response->getStatusCode(); + $json = MessageUtil::decodeJSON($response); + + if($status === 200 && isset($json->ok) && $json->ok === true){ + return $response; + } + + if(isset($json->error)){ + throw new ProviderException($json->error); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/SoundCloud.php b/src/Providers/SoundCloud.php new file mode 100644 index 00000000..35a0d243 --- /dev/null +++ b/src/Providers/SoundCloud.php @@ -0,0 +1,60 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ClientCredentials, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developers.soundcloud.com/ + * @see https://developers.soundcloud.com/docs/api/guide#authentication + * @see https://developers.soundcloud.com/blog/security-updates-api + */ +class SoundCloud extends OAuth2Provider implements ClientCredentials, TokenRefresh{ + + public const SCOPE_NONEXPIRING = 'non-expiring'; +# public const SCOPE_EMAIL = 'email'; // ??? + + protected array $defaultScopes = [ + self::SCOPE_NONEXPIRING, + ]; + + protected string $authURL = 'https://api.soundcloud.com/connect'; + protected string $accessTokenURL = 'https://api.soundcloud.com/oauth2/token'; + protected string $apiURL = 'https://api.soundcloud.com'; + protected string|null $userRevokeURL = 'https://soundcloud.com/settings/connections'; + protected string|null $apiDocs = 'https://developers.soundcloud.com/'; + protected string|null $applicationURL = 'https://soundcloud.com/you/apps'; + protected string $authMethodHeader = 'OAuth'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->status)){ + throw new ProviderException($json->status); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Spotify.php b/src/Providers/Spotify.php new file mode 100644 index 00000000..c7467429 --- /dev/null +++ b/src/Providers/Spotify.php @@ -0,0 +1,105 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developer.spotify.com/documentation/web-api + * @see https://developer.spotify.com/documentation/web-api/tutorials/code-flow + * @see https://developer.spotify.com/documentation/web-api/tutorials/client-credentials-flow + */ +class Spotify extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenRefresh{ + + /** + * @see https://developer.spotify.com/documentation/web-api/concepts/scopes + */ + // images + public const SCOPE_UGC_IMAGE_UPLOAD = 'ugc-image-upload'; + // spotify connect + public const SCOPE_USER_READ_PLAYBACK_STATE = 'user-read-playback-state'; + public const SCOPE_USER_MODIFY_PLAYBACK_STATE = 'user-modify-playback-state'; + public const SCOPE_USER_READ_CURRENTLY_PLAYING = 'user-read-currently-playing'; + // playback +# public const SCOPE_APP_REMOTE_CONTROL = 'app-remote-control'; // currently only on ios and android + public const SCOPE_STREAMING = 'streaming'; // web playback SDK + // playlists + public const SCOPE_PLAYLIST_READ_PRIVATE = 'playlist-read-private'; + public const SCOPE_PLAYLIST_READ_COLLABORATIVE = 'playlist-read-collaborative'; + public const SCOPE_PLAYLIST_MODIFY_PRIVATE = 'playlist-modify-private'; + public const SCOPE_PLAYLIST_MODIFY_PUBLIC = 'playlist-modify-public'; + // follow + public const SCOPE_USER_FOLLOW_MODIFY = 'user-follow-modify'; + public const SCOPE_USER_FOLLOW_READ = 'user-follow-read'; + // listening history + public const SCOPE_USER_READ_PLAYBACK_POSITION = 'user-read-playback-position'; + public const SCOPE_USER_TOP_READ = 'user-top-read'; + public const SCOPE_USER_READ_RECENTLY_PLAYED = 'user-read-recently-played'; + // library + public const SCOPE_USER_LIBRARY_MODIFY = 'user-library-modify'; + public const SCOPE_USER_LIBRARY_READ = 'user-library-read'; + // users + public const SCOPE_USER_READ_EMAIL = 'user-read-email'; + public const SCOPE_USER_READ_PRIVATE = 'user-read-private'; + // open access + public const SCOPE_USER_SOA_LINK = 'user-soa-link'; + public const SCOPE_USER_SOA_UNLINK = 'user-soa-unlink'; + public const SCOPE_USER_MANAGE_ENTITLEMENTS = 'user-manage-entitlements'; + public const SCOPE_USER_MANAGE_PARTNER = 'user-manage-partner'; + public const SCOPE_USER_CREATE_PARTNER = 'user-create-partner'; + + protected array $defaultScopes = [ + self::SCOPE_PLAYLIST_READ_COLLABORATIVE, + self::SCOPE_PLAYLIST_MODIFY_PUBLIC, + self::SCOPE_USER_FOLLOW_MODIFY, + self::SCOPE_USER_FOLLOW_READ, + self::SCOPE_USER_LIBRARY_READ, + self::SCOPE_USER_LIBRARY_MODIFY, + self::SCOPE_USER_TOP_READ, + self::SCOPE_USER_READ_EMAIL, + self::SCOPE_STREAMING, + self::SCOPE_USER_READ_PLAYBACK_STATE, + self::SCOPE_USER_MODIFY_PLAYBACK_STATE, + self::SCOPE_USER_READ_CURRENTLY_PLAYING, + self::SCOPE_USER_READ_RECENTLY_PLAYED, + ]; + + protected string $authURL = 'https://accounts.spotify.com/authorize'; + protected string $accessTokenURL = 'https://accounts.spotify.com/api/token'; + protected string $apiURL = 'https://api.spotify.com'; + protected string|null $userRevokeURL = 'https://www.spotify.com/account/apps/'; + protected string|null $apiDocs = 'https://developer.spotify.com/documentation/web-api/'; + protected string|null $applicationURL = 'https://developer.spotify.com/dashboard'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v1/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error->message)){ + throw new ProviderException($json->error->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/SteamOpenID.php b/src/Providers/SteamOpenID.php new file mode 100644 index 00000000..f7ac43a0 --- /dev/null +++ b/src/Providers/SteamOpenID.php @@ -0,0 +1,124 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\QueryUtil; +use chillerlan\OAuth\Core\{AccessToken, OAuthProvider, ProviderException}; +use Psr\Http\Message\{RequestInterface, ResponseInterface, UriInterface}; +use function explode, intval, preg_replace; + +/** + * @see https://steamcommunity.com/dev + * @see https://partner.steamgames.com/doc/webapi_overview + * @see https://steamwebapi.azurewebsites.net/ + */ +class SteamOpenID extends OAuthProvider{ + + protected string $authURL = 'https://steamcommunity.com/openid/login'; + protected string $accessTokenURL = 'https://steamcommunity.com/openid/login'; + protected string $apiURL = 'https://api.steampowered.com'; + protected string|null $applicationURL = 'https://steamcommunity.com/dev/apikey'; + protected string|null $apiDocs = 'https://developer.valvesoftware.com/wiki/Steam_Web_API'; + + /** + * @inheritDoc + */ + public function getAuthURL(array|null $params = null):UriInterface{ + + // we ignore user supplied params here + $params = [ + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.mode' => 'checkid_setup', + 'openid.return_to' => $this->options->callbackURL, + 'openid.realm' => $this->options->key, + 'openid.identity' => 'http://specs.openid.net/auth/2.0/identifier_select', + 'openid.claimed_id' => 'http://specs.openid.net/auth/2.0/identifier_select', + ]; + + return $this->uriFactory->createUri(QueryUtil::merge($this->authURL, $params)); + } + + /** + * + */ + public function getAccessToken(array $received):AccessToken{ + + $body = [ + 'openid.mode' => 'check_authentication', + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.sig' => $received['openid_sig'], + ]; + + foreach(explode(',', $received['openid_signed']) as $item){ + $body['openid.'.$item] = $received['openid_'.$item]; + } + + $request = $this->requestFactory + ->createRequest('POST', $this->accessTokenURL) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->streamFactory->createStream(QueryUtil::build($body))); + + $token = $this->parseTokenResponse($this->http->sendRequest($request)); + $id = preg_replace('/[^\d]/', '', $received['openid_claimed_id']); + + // as this method is intended for one-time authentication only we'll not receive a token. + // instead we're gonna save the verified steam user id as token as it is required + // for several "authenticated" endpoints. + $token->accessToken = $id; + $token->extraParams = [ + 'claimed_id' => $received['openid_claimed_id'], + 'id_int' => intval($id), + ]; + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @throws \chillerlan\OAuth\Core\ProviderException + */ + protected function parseTokenResponse(ResponseInterface $response):AccessToken{ + $data = explode("\x0a", (string)$response->getBody()); + + if(!isset($data[1]) || !str_starts_with($data[1], 'is_valid')){ + throw new ProviderException('unable to parse token response'); + } + + if($data[1] !== 'is_valid:true'){ + throw new ProviderException('invalid id'); + } + + // the response is only validation, so we'll just return an empty token and add the id in the next step + $token = $this->createAccessToken(); + + $token->accessToken = 'SteamID'; + $token->expires = AccessToken::EOL_NEVER_EXPIRES; + + return $token; + } + + /** + * + */ + public function getRequestAuthorization(RequestInterface $request, AccessToken $token):RequestInterface{ + $uri = (string)$request->getUri(); + $params = ['key' => $this->options->secret]; + + // the steamid parameter does not necessarily specify the current user, so add it only when it's not already set + if(!str_contains($uri, 'steamid=')){ + $params['steamid']= $token->accessToken; + } + + return $request->withUri($this->uriFactory->createUri(QueryUtil::merge($uri, $params))); + } + +} diff --git a/src/Providers/Stripe.php b/src/Providers/Stripe.php new file mode 100644 index 00000000..4f4bef1f --- /dev/null +++ b/src/Providers/Stripe.php @@ -0,0 +1,92 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate, TokenRefresh}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://stripe.com/docs/api + * @see https://stripe.com/docs/connect/authentication + * @see https://stripe.com/docs/connect/oauth-reference + * @see https://stripe.com/docs/connect/standard-accounts + * @see https://gist.github.com/amfeng/3507366 + */ +class Stripe extends OAuth2Provider implements CSRFToken, TokenRefresh, TokenInvalidate{ + + public const SCOPE_READ_WRITE = 'read_write'; + public const SCOPE_READ_ONLY = 'read_only'; + + protected array $defaultScopes = [ + self::SCOPE_READ_ONLY, + ]; + + protected string $authURL = 'https://connect.stripe.com/oauth/authorize'; + protected string $accessTokenURL = 'https://connect.stripe.com/oauth/token'; + protected string $revokeURL = 'https://connect.stripe.com/oauth/deauthorize'; + protected string $apiURL = 'https://api.stripe.com/v1'; + protected string|null $userRevokeURL = 'https://dashboard.stripe.com/account/applications'; + protected string|null $apiDocs = 'https://stripe.com/docs/api'; + protected string|null $applicationURL = 'https://dashboard.stripe.com/apikeys'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/accounts'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->error_description)){ + throw new ProviderException($json->error_description); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token === null && !$this->storage->hasAccessToken()){ + throw new ProviderException('no token given'); + } + + $token ??= $this->storage->getAccessToken(); + + $response = $this->request( + path : $this->revokeURL, + method : 'POST', + body : [ + 'client_id' => $this->options->key, + 'stripe_user_id' => ($token->extraParams['stripe_user_id'] ?? ''), + ], + headers: ['Content-Type' => 'application/x-www-form-urlencoded'] + ); + + if($response->getStatusCode() === 200){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/Tumblr.php b/src/Providers/Tumblr.php new file mode 100644 index 00000000..522359d9 --- /dev/null +++ b/src/Providers/Tumblr.php @@ -0,0 +1,85 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, OAuth1Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * Tumblr OAuth1 + * + * @see https://www.tumblr.com/docs/en/api/v2#oauth1-authorization + */ +class Tumblr extends OAuth1Provider{ + + protected string $requestTokenURL = 'https://www.tumblr.com/oauth/request_token'; + protected string $authURL = 'https://www.tumblr.com/oauth/authorize'; + protected string $accessTokenURL = 'https://www.tumblr.com/oauth/access_token'; + protected string $apiURL = 'https://api.tumblr.com'; + protected string|null $userRevokeURL = 'https://www.tumblr.com/settings/apps'; + protected string|null $apiDocs = 'https://www.tumblr.com/docs/en/api/v2'; + protected string|null $applicationURL = 'https://www.tumblr.com/oauth/apps'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v2/user/info'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->meta, $json->meta->msg)){ + throw new ProviderException($json->meta->msg); + } + + throw new ProviderException(sprintf('user info error HTTP/%s', $status)); + } + + /** + * Exchange the current token for an OAuth2 token - this will invalidate the OAuth1 token. + * + * @see https://www.tumblr.com/docs/en/api/v2#v2oauth2exchange---oauth1-to-oauth2-token-exchange + * + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function exchangeForOAuth2Token():AccessToken{ + $response = $this->request(path: '/v2/oauth2/exchange', method: 'POST'); + $status = $response->getStatusCode(); + $json = MessageUtil::decodeJSON($response); + + if($status === 200){ + $token = $this->createAccessToken(); + + $token->accessToken = $json->access_token; + $token->refreshToken = $json->refresh_token; + $token->expires = $json->expires_in; + $token->extraParams = ['scope' => $json->scope, 'token_type' => $json->token_type]; + + $this->storage->storeAccessToken($token); + + return $token; + } + + if(isset($json->meta, $json->meta->msg)){ + throw new ProviderException($json->meta->msg); + } + + throw new ProviderException(sprintf('token exchange error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Tumblr2.php b/src/Providers/Tumblr2.php new file mode 100644 index 00000000..6a93b347 --- /dev/null +++ b/src/Providers/Tumblr2.php @@ -0,0 +1,62 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException, TokenRefresh}; +use chillerlan\HTTP\Utils\MessageUtil; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * Tumblr OAuth2 + * + * @see https://www.tumblr.com/docs/en/api/v2#oauth2-authorization + */ +class Tumblr2 extends OAuth2Provider implements CSRFToken, TokenRefresh{ + + public const SCOPE_BASIC = 'basic'; + public const SCOPE_WRITE = 'write'; + public const SCOPE_OFFLINE_ACCESS = 'offline_access'; + + protected array $defaultScopes = [ + self::SCOPE_BASIC, + self::SCOPE_WRITE, + self::SCOPE_OFFLINE_ACCESS, + ]; + + protected string $authURL = 'https://www.tumblr.com/v2/oauth2/authorize'; + protected string $accessTokenURL = 'https://www.tumblr.com/v2/oauth2/token'; + protected string $apiURL = 'https://api.tumblr.com'; + protected string|null $userRevokeURL = 'https://www.tumblr.com/settings/apps'; + protected string|null $apiDocs = 'https://www.tumblr.com/docs/en/api/v2'; + protected string|null $applicationURL = 'https://www.tumblr.com/oauth/apps'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v2/user/info'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->meta, $json->meta->msg)){ + throw new ProviderException($json->meta->msg); + } + + throw new ProviderException(sprintf('user info error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/Twitch.php b/src/Providers/Twitch.php new file mode 100644 index 00000000..7de85d91 --- /dev/null +++ b/src/Providers/Twitch.php @@ -0,0 +1,155 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\{MessageUtil, QueryUtil}; +use chillerlan\OAuth\Core\{ + AccessToken, ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate, TokenRefresh +}; +use Psr\Http\Message\{RequestInterface, ResponseInterface}; +use function implode, sprintf; +use const PHP_QUERY_RFC1738; + +/** + * @see https://dev.twitch.tv/docs/api/reference/ + * @see https://dev.twitch.tv/docs/authentication/ + */ +class Twitch extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenInvalidate, TokenRefresh{ + + public const SCOPE_ANALYTICS_READ_EXTENSIONS = 'analytics:read:extensions'; + public const SCOPE_ANALYTICS_READ_GAMES = 'analytics:read:games'; + public const SCOPE_BITS_READ = 'bits:read'; + public const SCOPE_CHANNEL_EDIT_COMMERCIAL = 'channel:edit:commercial'; + public const SCOPE_CHANNEL_MANAGE_BROADCAST = 'channel:manage:broadcast'; + public const SCOPE_CHANNEL_MANAGE_EXTENSIONS = 'channel:manage:extensions'; + public const SCOPE_CHANNEL_MANAGE_REDEMPTIONS = 'channel:manage:redemptions'; + public const SCOPE_CHANNEL_MANAGE_VIDEOS = 'channel:manage:videos'; + public const SCOPE_CHANNEL_READ_EDITORS = 'channel:read:editors'; + public const SCOPE_CHANNEL_READ_HYPE_TRAIN = 'channel:read:hype_train'; + public const SCOPE_CHANNEL_READ_REDEMPTIONS = 'channel:read:redemptions'; + public const SCOPE_CHANNEL_READ_STREAM_KEY = 'channel:read:stream_key'; + public const SCOPE_CHANNEL_READ_SUBSCRIPTIONS = 'channel:read:subscriptions'; + public const SCOPE_CLIPS_EDIT = 'clips:edit'; + public const SCOPE_MODERATION_READ = 'moderation:read'; + public const SCOPE_USER_EDIT = 'user:edit'; + public const SCOPE_USER_EDIT_FOLLOWS = 'user:edit:follows'; + public const SCOPE_USER_READ_BLOCKED_USERS = 'user:read:blocked_users'; + public const SCOPE_USER_MANAGE_BLOCKED_USERS = 'user:manage:blocked_users'; + public const SCOPE_USER_READ_BROADCAST = 'user:read:broadcast'; + public const SCOPE_USER_READ_EMAIL = 'user:read:email'; + public const SCOPE_USER_READ_SUBSCRIPTIONS = 'user:read:subscriptions'; + + protected array $defaultScopes = [ + self::SCOPE_USER_READ_EMAIL, + ]; + + protected string $authURL = 'https://id.twitch.tv/oauth2/authorize'; + protected string $accessTokenURL = 'https://id.twitch.tv/oauth2/token'; + protected string $revokeURL = 'https://id.twitch.tv/oauth2/revoke'; + protected string $apiURL = 'https://api.twitch.tv'; + protected string|null $userRevokeURL = 'https://www.twitch.tv/settings/connections'; + protected string|null $apiDocs = 'https://dev.twitch.tv/docs/api/reference/'; + protected string|null $applicationURL = 'https://dev.twitch.tv/console/apps/create'; + protected array $authHeaders = ['Accept' => 'application/vnd.twitchtv.v5+json']; + protected array $apiHeaders = ['Accept' => 'application/vnd.twitchtv.v5+json']; + + /** + * @see https://dev.twitch.tv/docs/authentication#oauth-client-credentials-flow-app-access-tokens + */ + public function getClientCredentialsToken(array|null $scopes = null):AccessToken{ + + $params = [ + 'client_id' => $this->options->key, + 'client_secret' => $this->options->secret, + 'grant_type' => 'client_credentials', + ]; + + if($scopes !== null){ + $params['scope'] = implode($this->scopesDelimiter, $scopes); + } + + $request = $this->requestFactory + ->createRequest('POST', ($this->clientCredentialsTokenURL ?? $this->accessTokenURL)) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ->withBody($this->streamFactory->createStream(QueryUtil::build($params, PHP_QUERY_RFC1738))) + ; + + foreach($this->authHeaders as $header => $value){ + $request = $request->withAddedHeader($header, $value); + } + + $token = $this->parseTokenResponse($this->http->sendRequest($request)); + + $this->storage->storeAccessToken($token, $this->serviceName); + + return $token; + } + + /** + * @inheritDoc + */ + public function getRequestAuthorization(RequestInterface $request, AccessToken $token):RequestInterface{ + return $request + ->withHeader('Authorization', $this->authMethodHeader.' '.$token->accessToken) + ->withHeader('Client-ID', $this->options->key); + } + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/helix/users'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token === null && !$this->storage->hasAccessToken()){ + throw new ProviderException('no token given'); + } + + $token ??= $this->storage->getAccessToken(); + + $response = $this->request( + path : $this->revokeURL, + method : 'POST', + body : [ + 'client_id' => $this->options->key, + 'token' => $token->accessToken, + ], + headers: ['Content-Type' => 'application/x-www-form-urlencoded'] + ); + + if($response->getStatusCode() === 200){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/Twitter.php b/src/Providers/Twitter.php new file mode 100644 index 00000000..885cf0c2 --- /dev/null +++ b/src/Providers/Twitter.php @@ -0,0 +1,59 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{OAuth1Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @todo: twitter is dead. fuck elon musk. + * + * @see https://developer.twitter.com/en/docs/basics/authentication/overview/oauth + */ +class Twitter extends OAuth1Provider{ + + // choose your fighter + /** @see https://developer.twitter.com/en/docs/basics/authentication/api-reference/authorize */ + protected string $authURL = 'https://api.twitter.com/oauth/authorize'; + /** @see https://developer.twitter.com/en/docs/basics/authentication/api-reference/authenticate */ +# protected string $authURL = 'https://api.twitter.com/oauth/authenticate'; + + protected string $requestTokenURL = 'https://api.twitter.com/oauth/request_token'; + protected string $accessTokenURL = 'https://api.twitter.com/oauth/access_token'; + protected string $apiURL = 'https://api.twitter.com'; + protected string|null $userRevokeURL = 'https://twitter.com/settings/applications'; + protected string|null $apiDocs = 'https://developer.twitter.com/docs'; + protected string|null $applicationURL = 'https://developer.twitter.com/apps'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/1.1/account/verify_credentials.json'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->errors, $json->errors[0]->message)){ + throw new ProviderException($json->errors[0]->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/TwitterCC.php b/src/Providers/TwitterCC.php new file mode 100644 index 00000000..d61cac7a --- /dev/null +++ b/src/Providers/TwitterCC.php @@ -0,0 +1,50 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\OAuth\Core\{ClientCredentials, OAuth2Provider, ProviderException, AccessToken}; +use Psr\Http\Message\UriInterface; + +/** + * @todo: twitter is dead. fuck elon musk. + * + * @see https://dev.twitter.com/overview/api + * @see https://developer.twitter.com/en/docs/basics/authentication/overview/application-only + * + * @todo: https://developer.twitter.com/en/docs/basics/authentication/api-reference/invalidate_token + */ +class TwitterCC extends OAuth2Provider implements ClientCredentials{ + + protected const AUTH_ERRMSG = 'TwitterCC only supports Client Credentials Grant, use the Twitter OAuth1 class for authentication instead.'; + + protected string $apiURL = 'https://api.twitter.com'; + protected string|null $clientCredentialsTokenURL = 'https://api.twitter.com/oauth2/token'; + protected string|null $userRevokeURL = 'https://twitter.com/settings/applications'; + protected string|null $apiDocs = 'https://developer.twitter.com/en/docs/basics/authentication/overview/application-only'; + protected string|null $applicationURL = 'https://developer.twitter.com/apps'; + + /** + * @inheritdoc + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function getAuthURL(array|null $params = null, array|null $scopes = null):UriInterface{ + throw new ProviderException($this::AUTH_ERRMSG); + } + + /** + * @inheritdoc + * @throws \chillerlan\OAuth\Core\ProviderException + */ + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + throw new ProviderException($this::AUTH_ERRMSG); + } + +} diff --git a/src/Providers/Vimeo.php b/src/Providers/Vimeo.php new file mode 100644 index 00000000..2ab8f6f8 --- /dev/null +++ b/src/Providers/Vimeo.php @@ -0,0 +1,105 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, ClientCredentials, CSRFToken, OAuth2Provider, ProviderException, TokenInvalidate}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developer.vimeo.com/ + * @see https://developer.vimeo.com/api/authentication + */ +class Vimeo extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenInvalidate{ + + /** + * @see https://developer.vimeo.com/api/authentication#understanding-the-auth-process + */ + public const SCOPE_PUBLIC = 'public'; + public const SCOPE_PRIVATE = 'private'; + public const SCOPE_PURCHASED = 'purchased'; + public const SCOPE_CREATE = 'create'; + public const SCOPE_EDIT = 'edit'; + public const SCOPE_DELETE = 'delete'; + public const SCOPE_INTERACT = 'interact'; + public const SCOPE_STATS = 'stats'; + public const SCOPE_UPLOAD = 'upload'; + public const SCOPE_PROMO_CODES = 'promo_codes'; + public const SCOPE_VIDEO_FILES = 'video_files'; + + // @see https://developer.vimeo.com/api/changelog + protected const API_VERSION = '3.4'; + + protected array $defaultScopes = [ + self::SCOPE_PUBLIC, + self::SCOPE_PRIVATE, + self::SCOPE_INTERACT, + self::SCOPE_STATS, + ]; + + protected string $authURL = 'https://api.vimeo.com/oauth/authorize'; + protected string $accessTokenURL = 'https://api.vimeo.com/oauth/access_token'; + protected string $revokeURL = 'https://api.vimeo.com/tokens'; + protected string $apiURL = 'https://api.vimeo.com'; + protected string|null $userRevokeURL = 'https://vimeo.com/settings/apps'; + protected string|null $clientCredentialsTokenURL = 'https://api.vimeo.com/oauth/authorize/client'; + protected string|null $apiDocs = 'https://developer.vimeo.com'; + protected string|null $applicationURL = 'https://developer.vimeo.com/apps'; + protected array $authHeaders = ['Accept' => 'application/vnd.vimeo.*+json;version='.self::API_VERSION]; + protected array $apiHeaders = ['Accept' => 'application/vnd.vimeo.*+json;version='.self::API_VERSION]; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->developer_message)){ + throw new ProviderException($json->developer_message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + + /** + * @inheritDoc + */ + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + if($token !== null){ + // to revoke a token different from the one of the currently authenticated user, + // we're going to clone the provider and feed the other token for the invalidate request + $provider = clone $this; + $provider->storeAccessToken($token); + $response = $provider->request(path: $this->revokeURL, method: 'DELETE'); + } + else{ + $response = $this->request(path: $this->revokeURL, method: 'DELETE'); + } + + if($response->getStatusCode() === 204){ + $this->storage->clearAccessToken(); + + return true; + } + + return false; + } + +} diff --git a/src/Providers/WordPress.php b/src/Providers/WordPress.php new file mode 100644 index 00000000..13d7563e --- /dev/null +++ b/src/Providers/WordPress.php @@ -0,0 +1,57 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, ProviderException}; +use Psr\Http\Message\ResponseInterface; +use function sprintf; + +/** + * @see https://developer.wordpress.com/docs/oauth2/ + */ +class WordPress extends OAuth2Provider implements CSRFToken{ + + public const SCOPE_AUTH = 'auth'; + public const SCOPE_GLOBAL = 'global'; + + protected array $defaultScopes = [ + self::SCOPE_GLOBAL, + ]; + + protected string $authURL = 'https://public-api.wordpress.com/oauth2/authorize'; + protected string $accessTokenURL = 'https://public-api.wordpress.com/oauth2/token'; + protected string $apiURL = 'https://public-api.wordpress.com/rest'; + protected string|null $userRevokeURL = 'https://wordpress.com/me/security/connected-applications'; + protected string|null $apiDocs = 'https://developer.wordpress.com/docs/api/'; + protected string|null $applicationURL = 'https://developer.wordpress.com/apps/'; + + /** + * @inheritDoc + */ + public function me():ResponseInterface{ + $response = $this->request('/v1/me'); + $status = $response->getStatusCode(); + + if($status === 200){ + return $response; + } + + $json = MessageUtil::decodeJSON($response); + + if(isset($json->error, $json->message)){ + throw new ProviderException($json->message); + } + + throw new ProviderException(sprintf('user info error error HTTP/%s', $status)); + } + +} diff --git a/src/Providers/YouTube.php b/src/Providers/YouTube.php new file mode 100644 index 00000000..35135ec9 --- /dev/null +++ b/src/Providers/YouTube.php @@ -0,0 +1,21 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuth\Providers; + +/** + * + */ +class YouTube extends Google{ + + public const SCOPE_YOUTUBE = 'https://www.googleapis.com/auth/youtube'; + public const SCOPE_YOUTUBE_GDATA = 'https://gdata.youtube.com'; + +} diff --git a/tests/Providers/ChillerlanHttpClientFactory.php b/tests/Providers/ChillerlanHttpClientFactory.php new file mode 100644 index 00000000..dce86643 --- /dev/null +++ b/tests/Providers/ChillerlanHttpClientFactory.php @@ -0,0 +1,30 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use chillerlan\HTTP\HTTPOptions; +use chillerlan\HTTP\Psr18\CurlClient; +use Psr\Http\Client\ClientInterface; + +final class ChillerlanHttpClientFactory implements OAuthTestHttpClientFactoryInterface{ + + /** + * @inheritDoc + */ + public static function getClient(string $cfgdir):ClientInterface{ + $options = new HTTPOptions; + $options->ca_info = $cfgdir.'/cacert.pem'; + $options->user_agent = 'chillerlanPhpOAuth/5.0.0 +https://github.com/chillerlan/php-oauth-core'; + + return new CurlClient($options); + } + +} diff --git a/tests/Providers/GuzzleHttpClientFactory.php b/tests/Providers/GuzzleHttpClientFactory.php new file mode 100644 index 00000000..56cd8244 --- /dev/null +++ b/tests/Providers/GuzzleHttpClientFactory.php @@ -0,0 +1,31 @@ += 7.3 (and Guzzle PSR-7 >= 2.0 for the PSR-17 factories) + * + * @created 01.04.2021 + * @author smiley + * @copyright 2021 smiley + * @license MIT + * + * @noinspection ALL + */ + +namespace chillerlan\OAuthTest\Providers; + +use GuzzleHttp\Client; +use Psr\Http\Client\ClientInterface; + +final class GuzzleHttpClientFactory implements OAuthTestHttpClientFactoryInterface{ + + public static function getClient(string $cfgdir):ClientInterface{ + return new Client([ + 'verify' => $cfgdir.'/cacert.pem', + 'headers' => [ + 'User-Agent' => 'chillerlanPhpOAuth/5.0.0 +https://github.com/chillerlan/php-oauth-core', + ], + ]); + } + +} diff --git a/tests/Providers/Live/AmazonAPITest.php b/tests/Providers/Live/AmazonAPITest.php new file mode 100644 index 00000000..645c66b1 --- /dev/null +++ b/tests/Providers/Live/AmazonAPITest.php @@ -0,0 +1,32 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Amazon; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; +use function preg_match; + +/** + * Amazon API usage tests/examples + * + * @property \chillerlan\OAuth\Providers\Amazon $provider + */ +class AmazonAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Amazon::class; + protected string $ENV = 'AMAZON'; + + public function testMe():void{ + $this::assertMatchesRegularExpression('/[a-z\d.]+/i', MessageUtil::decodeJSON($this->provider->me())->user_id); + } + +} diff --git a/tests/Providers/Live/BattleNetAPITest.php b/tests/Providers/Live/BattleNetAPITest.php new file mode 100644 index 00000000..cee86d7b --- /dev/null +++ b/tests/Providers/Live/BattleNetAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\BattleNet; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\BattleNet $provider + */ +class BattleNetAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = BattleNet::class; + protected string $ENV = 'BATTLENET'; + + public function testMe():void{ + $this::assertSame($this->testuser, explode('#', MessageUtil::decodeJSON($this->provider->me())->battletag)[0]); + } + +} diff --git a/tests/Providers/Live/BigCartelAPITest.php b/tests/Providers/Live/BigCartelAPITest.php new file mode 100644 index 00000000..1636fa75 --- /dev/null +++ b/tests/Providers/Live/BigCartelAPITest.php @@ -0,0 +1,37 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\BigCartel; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\BigCartel $provider + */ +class BigCartelAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = BigCartel::class; + protected string $ENV = 'BIGCARTEL'; + + protected int $account_id; + + protected function setUp():void{ + parent::setUp(); + + $this->account_id = (int)$this->storage->getAccessToken($this->provider->serviceName)->extraParams['account_id']; + } + + public function testMe():void{ + $this::assertSame($this->account_id, (int)MessageUtil::decodeJSON($this->provider->me())->data[0]->id); + } + +} diff --git a/tests/Providers/Live/BitbucketAPITest.php b/tests/Providers/Live/BitbucketAPITest.php new file mode 100644 index 00000000..2a4e6db6 --- /dev/null +++ b/tests/Providers/Live/BitbucketAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Bitbucket; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Bitbucket $provider + */ +class BitbucketAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Bitbucket::class; + protected string $ENV = 'BITBUCKET'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/DeezerAPITest.php b/tests/Providers/Live/DeezerAPITest.php new file mode 100644 index 00000000..d69d508f --- /dev/null +++ b/tests/Providers/Live/DeezerAPITest.php @@ -0,0 +1,33 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Deezer; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Spotify API usage tests/examples + * + * @link https://developer.spotify.com/web-api/endpoint-reference/ + * + * @property \chillerlan\OAuth\Providers\Deezer $provider + */ +class DeezerAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Deezer::class; + protected string $ENV = 'DEEZER'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->name); + } + +} diff --git a/tests/Providers/Live/DeviantArtAPITest.php b/tests/Providers/Live/DeviantArtAPITest.php new file mode 100644 index 00000000..4c10dc02 --- /dev/null +++ b/tests/Providers/Live/DeviantArtAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\DeviantArt; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\DeviantArt $provider + */ +class DeviantArtAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = DeviantArt::class; + protected string $ENV = 'DEVIANTART'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/DiscogsAPITest.php b/tests/Providers/Live/DiscogsAPITest.php new file mode 100644 index 00000000..958994c7 --- /dev/null +++ b/tests/Providers/Live/DiscogsAPITest.php @@ -0,0 +1,31 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Discogs; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; + +/** + * Discogs API test + * + * @property \chillerlan\OAuth\Providers\Discogs $provider + */ +class DiscogsAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = Discogs::class; + protected string $ENV = 'DISCOGS'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/DiscordAPITest.php b/tests/Providers/Live/DiscordAPITest.php new file mode 100644 index 00000000..627d8d7c --- /dev/null +++ b/tests/Providers/Live/DiscordAPITest.php @@ -0,0 +1,43 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\Providers\Discord; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Discord $provider + */ +class DiscordAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Discord::class; + protected string $ENV = 'DISCORD'; + + public function testRequestCredentialsToken():void{ + $token = $this->provider->getClientCredentialsToken([Discord::SCOPE_CONNECTIONS, Discord::SCOPE_IDENTIFY]); + + $this::assertInstanceOf(AccessToken::class, $token); + $this::assertIsString($token->accessToken); + + if($token->expires !== AccessToken::EOL_NEVER_EXPIRES){ + $this::assertGreaterThan(time(), $token->expires); + } + + $this->logger->debug('APITestSupportsOAuth2ClientCredentials', $token->toArray()); + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/FlickrAPITest.php b/tests/Providers/Live/FlickrAPITest.php new file mode 100644 index 00000000..308142b2 --- /dev/null +++ b/tests/Providers/Live/FlickrAPITest.php @@ -0,0 +1,44 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Flickr; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Flickr $provider + */ +class FlickrAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = Flickr::class; + protected string $ENV = 'FLICKR'; + + protected string $test_name; + protected string $test_id; + + protected function setUp():void{ + parent::setUp(); + + $tokenParams = $this->storage->getAccessToken($this->provider->serviceName)->extraParams; + + $this->test_name = $tokenParams['username']; + $this->test_id = $tokenParams['user_nsid']; + } + + public function testMe():void{ + $j = MessageUtil::decodeJSON($this->provider->me()); + + $this::assertSame($this->test_name, $j->user->username->_content); + $this::assertSame($this->test_id, $j->user->id); + } + +} diff --git a/tests/Providers/Live/FoursquareAPITest.php b/tests/Providers/Live/FoursquareAPITest.php new file mode 100644 index 00000000..1aa703bb --- /dev/null +++ b/tests/Providers/Live/FoursquareAPITest.php @@ -0,0 +1,33 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Foursquare; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Foursquare API usage tests/examples + * + * @link https://developer.foursquare.com/docs + * + * @property \chillerlan\OAuth\Providers\Foursquare $provider + */ +class FoursquareAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Foursquare::class; + protected string $ENV = 'FOURSQUARE'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->response->user->id); + } + +} diff --git a/tests/Providers/Live/GitHubAPITest.php b/tests/Providers/Live/GitHubAPITest.php new file mode 100644 index 00000000..1c169104 --- /dev/null +++ b/tests/Providers/Live/GitHubAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\GitHub; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GitHub $provider + */ +class GitHubAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = GitHub::class; + protected string $ENV = 'GITHUB'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->login); + } + +} diff --git a/tests/Providers/Live/GitLabAPITest.php b/tests/Providers/Live/GitLabAPITest.php new file mode 100644 index 00000000..cf76d3a8 --- /dev/null +++ b/tests/Providers/Live/GitLabAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\GitLab; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GitLab $provider + */ +class GitLabAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = GitLab::class; + protected string $ENV = 'GITLAB'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/GoogleAPITest.php b/tests/Providers/Live/GoogleAPITest.php new file mode 100644 index 00000000..6195f453 --- /dev/null +++ b/tests/Providers/Live/GoogleAPITest.php @@ -0,0 +1,33 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Google; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Google API usage tests/examples + * + * @link https://developers.google.com/oauthplayground/ + * + * @property \chillerlan\OAuth\Providers\Google $provider + */ +class GoogleAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Google::class; + protected string $ENV = 'GOOGLE'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->email); + } + +} diff --git a/tests/Providers/Live/GuildWars2APITest.php b/tests/Providers/Live/GuildWars2APITest.php new file mode 100644 index 00000000..b7eb7cfb --- /dev/null +++ b/tests/Providers/Live/GuildWars2APITest.php @@ -0,0 +1,45 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\Providers\GuildWars2; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GuildWars2 $provider + */ +class GuildWars2APITest extends OAuth2APITestAbstract{ + + protected string $FQN = GuildWars2::class; + protected string $ENV = ''; + + protected AccessToken $token; + protected string $tokenname; + + protected function setUp():void{ + parent::setUp(); + + $tokenfile = $this->CFG.'/GuildWars2.token.json'; + + $this->token = !file_exists($tokenfile) + ? $this->provider->storeGW2Token($this->dotEnv->GW2_TOKEN) + : (new AccessToken)->fromJSON(file_get_contents($tokenfile)); + + $this->tokenname = $this->dotEnv->GW2_TOKEN_NAME; + } + + public function testMe():void{ + $this::assertSame($this->tokenname, MessageUtil::decodeJSON($this->provider->me())->name); + } + +} diff --git a/tests/Providers/Live/ImgurAPITest.php b/tests/Providers/Live/ImgurAPITest.php new file mode 100644 index 00000000..d8abdbc0 --- /dev/null +++ b/tests/Providers/Live/ImgurAPITest.php @@ -0,0 +1,37 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Imgur; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Imgur $provider + */ +class ImgurAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Imgur::class; + protected string $ENV = 'IMGUR'; + + protected function setUp():void{ + parent::setUp(); + + $token = $this->storage->getAccessToken($this->provider->serviceName); + + $this->testuser = $token->extraParams['account_id']; + } + + public function testMe():void{ + $this::assertSame((int)$this->testuser, MessageUtil::decodeJSON($this->provider->me())->data->id); + } + +} diff --git a/tests/Providers/Live/LastFMAPITest.php b/tests/Providers/Live/LastFMAPITest.php new file mode 100644 index 00000000..517ccd96 --- /dev/null +++ b/tests/Providers/Live/LastFMAPITest.php @@ -0,0 +1,41 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\LastFM; +use chillerlan\OAuthTest\Providers\OAuthAPITestAbstract; + +/** + * last.fm API test & examples + * + * @link https://www.last.fm/api/intro + * + * @property \chillerlan\OAuth\Providers\LastFM $provider + */ +class LastFMAPITest extends OAuthAPITestAbstract{ + + protected string $FQN = LastFM::class; + protected string $ENV = 'LASTFM'; + + protected function setUp():void{ + parent::setUp(); + + // username is stored in the session token + $token = $this->storage->getAccessToken($this->provider->serviceName); + $this->testuser = $token->extraParams['session']['name']; + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->user->name); + } + +} diff --git a/tests/Providers/Live/MailChimpAPITest.php b/tests/Providers/Live/MailChimpAPITest.php new file mode 100644 index 00000000..9666b8c3 --- /dev/null +++ b/tests/Providers/Live/MailChimpAPITest.php @@ -0,0 +1,40 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\MailChimp; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * MailChimp API usage tests/examples + * + * @link http://developer.mailchimp.com/documentation/mailchimp/reference/overview/ + * + * @property \chillerlan\OAuth\Providers\MailChimp $provider + */ +class MailChimpAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = MailChimp::class; + protected string $ENV = 'MAILCHIMP'; + + public function testGetTokenMetadata():void{ + $token = $this->storage->getAccessToken($this->provider->serviceName); + $token = $this->provider->getTokenMetadata($token); + + $this::assertSame($this->testuser, $token->extraParams['accountname']); + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->account_name); + } + +} diff --git a/tests/Providers/Live/MastodonAPITest.php b/tests/Providers/Live/MastodonAPITest.php new file mode 100644 index 00000000..10739f15 --- /dev/null +++ b/tests/Providers/Live/MastodonAPITest.php @@ -0,0 +1,43 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Mastodon; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Spotify API usage tests/examples + * + * @link https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md + * + * @property \chillerlan\OAuth\Providers\Mastodon $provider + */ +class MastodonAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Mastodon::class; + protected string $ENV = 'MASTODON'; + + protected string $testInstance; + + protected function setUp():void{ + parent::setUp(); + + $this->testInstance = ($this->dotEnv->get($this->ENV.'_INSTANCE') ?? ''); + + $this->provider->setInstance($this->testInstance); + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->acct); + } + +} diff --git a/tests/Providers/Live/MicrosoftGraphAPITest.php b/tests/Providers/Live/MicrosoftGraphAPITest.php new file mode 100644 index 00000000..b3f87a7b --- /dev/null +++ b/tests/Providers/Live/MicrosoftGraphAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\MicrosoftGraph; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\MicrosoftGraph $provider + */ +class MicrosoftGraphAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = MicrosoftGraph::class; + protected string $ENV = 'MICROSOFT_AAD'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->userPrincipalName); + } + +} diff --git a/tests/Providers/Live/MixcloudAPITest.php b/tests/Providers/Live/MixcloudAPITest.php new file mode 100644 index 00000000..46d7937e --- /dev/null +++ b/tests/Providers/Live/MixcloudAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Mixcloud; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Mixcloud $provider + */ +class MixcloudAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Mixcloud::class; + protected string $ENV = 'MIXCLOUD'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/MusicBrainzAPITest.php b/tests/Providers/Live/MusicBrainzAPITest.php new file mode 100644 index 00000000..2b1f1c47 --- /dev/null +++ b/tests/Providers/Live/MusicBrainzAPITest.php @@ -0,0 +1,39 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\MusicBrainz; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +class MusicBrainzAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = MusicBrainz::class; + protected string $ENV = 'MUSICBRAINZ'; + + public function testArtistId():void{ + $r = $this->provider->request('/artist/573510d6-bb5d-4d07-b0aa-ea6afe39e28d', ['inc' => 'url-rels work-rels']); + $j = MessageUtil::decodeJSON($r); + + $this::assertSame('Helium', $j->name); + $this::assertSame('573510d6-bb5d-4d07-b0aa-ea6afe39e28d', $j->id); + } + + public function testArtistIdXML():void{ + $r = $this->provider->request('/artist/573510d6-bb5d-4d07-b0aa-ea6afe39e28d', ['inc' => 'url-rels work-rels', 'fmt' => 'xml']); + $x = MessageUtil::decodeXML($r); + + $this::assertSame('Helium', (string)$x->artist[0]->name); + $this::assertSame('573510d6-bb5d-4d07-b0aa-ea6afe39e28d', (string)$x->artist[0]->attributes()['id']); + } + + +} diff --git a/tests/Providers/Live/NPROneAPITest.php b/tests/Providers/Live/NPROneAPITest.php new file mode 100644 index 00000000..94f09c17 --- /dev/null +++ b/tests/Providers/Live/NPROneAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\NPROne; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\NPROne $provider + */ +class NPROneAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = NPROne::class; + protected string $ENV = 'NPRONE'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->attributes->email); + } + +} diff --git a/tests/Providers/Live/OpenCachingAPITest.php b/tests/Providers/Live/OpenCachingAPITest.php new file mode 100644 index 00000000..73eae691 --- /dev/null +++ b/tests/Providers/Live/OpenCachingAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\OpenCaching; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenCaching $provider + */ +class OpenCachingAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = OpenCaching::class; + protected string $ENV = 'OKAPI'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/Live/OpenStreetmap2APITest.php b/tests/Providers/Live/OpenStreetmap2APITest.php new file mode 100644 index 00000000..492cad62 --- /dev/null +++ b/tests/Providers/Live/OpenStreetmap2APITest.php @@ -0,0 +1,29 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\OpenStreetmap2; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenStreetmap2 $provider + */ +class OpenStreetmap2APITest extends OAuth2APITestAbstract{ + + protected string $FQN = OpenStreetmap2::class; + protected string $ENV = 'OPENSTREETMAP2'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->user->display_name); + } + +} diff --git a/tests/Providers/Live/OpenStreetmapAPITest.php b/tests/Providers/Live/OpenStreetmapAPITest.php new file mode 100644 index 00000000..550d0bfe --- /dev/null +++ b/tests/Providers/Live/OpenStreetmapAPITest.php @@ -0,0 +1,32 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\OpenStreetmap; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenStreetmap $provider + */ +class OpenStreetmapAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = OpenStreetmap::class; + protected string $ENV = 'OPENSTREETMAP'; + + public function testMe():void{ + // json + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->user->display_name); + // xml + $this::assertSame($this->testuser, MessageUtil::decodeXML($this->provider->me(false))->user->attributes()->display_name->__toString()); + } + +} diff --git a/tests/Providers/Live/Patreon1APITest.php b/tests/Providers/Live/Patreon1APITest.php new file mode 100644 index 00000000..c87fefdf --- /dev/null +++ b/tests/Providers/Live/Patreon1APITest.php @@ -0,0 +1,39 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\Providers\Patreon; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; +use function file_get_contents; +use function var_dump; + +/** + * @property \chillerlan\OAuth\Providers\Patreon $provider + */ +class Patreon1APITest extends OAuth2APITestAbstract{ + + protected string $FQN = Patreon::class; + protected string $ENV = 'PATREON1'; + + protected function setUp():void{ + parent::setUp(); + $tokenfile = file_get_contents($this->CFG.'\\'.$this->provider->serviceName.'1.token.json'); + + $this->storage->storeAccessToken((new AccessToken)->fromJSON($tokenfile), $this->provider->serviceName); + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->data->attributes->email); + } + +} diff --git a/tests/Providers/Live/Patreon2APITest.php b/tests/Providers/Live/Patreon2APITest.php new file mode 100644 index 00000000..ea611b8b --- /dev/null +++ b/tests/Providers/Live/Patreon2APITest.php @@ -0,0 +1,38 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\Providers\Patreon; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; +use function file_get_contents; + +/** + * @property \chillerlan\OAuth\Providers\Patreon $provider + */ +class Patreon2APITest extends OAuth2APITestAbstract{ + + protected string $FQN = Patreon::class; + protected string $ENV = 'PATREON2'; + + protected function setUp():void{ + parent::setUp(); + $tokenfile = file_get_contents($this->CFG.'\\'.$this->provider->serviceName.'2.token.json'); + + $this->storage->storeAccessToken((new AccessToken)->fromJSON($tokenfile), $this->provider->serviceName); + } + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->data->attributes->email); + } + +} diff --git a/tests/Providers/Live/PayPalAPITest.php b/tests/Providers/Live/PayPalAPITest.php new file mode 100644 index 00000000..7ddc29fe --- /dev/null +++ b/tests/Providers/Live/PayPalAPITest.php @@ -0,0 +1,41 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\PayPal; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\PayPal $provider + */ +class PayPalAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = PayPal::class; + protected string $ENV = 'PAYPAL'; // PAYPAL_SANDBOX + + public function testMe():void{ + $json = MessageUtil::decodeJSON($this->provider->me()); + + if(!isset($json->emails) || !is_array($json->emails) || empty($json->emails)){ + $this->markTestSkipped('no email found'); + } + + foreach($json->emails as $email){ + if($email->primary){ + $this::assertSame($this->testuser, $email->value); + return; + } + } + + } + +} diff --git a/tests/Providers/Live/SlackAPITest.php b/tests/Providers/Live/SlackAPITest.php new file mode 100644 index 00000000..f11caaa1 --- /dev/null +++ b/tests/Providers/Live/SlackAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Slack; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Slack $provider + */ +class SlackAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Slack::class; + protected string $ENV = 'SLACK'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->user->email); + } + +} diff --git a/tests/Providers/Live/SoundcloudAPITest.php b/tests/Providers/Live/SoundcloudAPITest.php new file mode 100644 index 00000000..4ebf29e2 --- /dev/null +++ b/tests/Providers/Live/SoundcloudAPITest.php @@ -0,0 +1,33 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\SoundCloud; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\SoundCloud $provider + */ +class SoundcloudAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = SoundCloud::class; + protected string $ENV = 'SOUNDCLOUD'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + + public function testRequestCredentialsToken():void{ + $this::markTestSkipped('may fail because SoundCloud deleted older applications'); + } + +} diff --git a/tests/Providers/Live/SpotifyAPITest.php b/tests/Providers/Live/SpotifyAPITest.php new file mode 100644 index 00000000..20b14299 --- /dev/null +++ b/tests/Providers/Live/SpotifyAPITest.php @@ -0,0 +1,36 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Spotify; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Spotify API usage tests/examples + * + * Please note that Spotify ids may change and so these test may fail at times + * + * @link https://developer.spotify.com/web-api/endpoint-reference/ + * + * @property \chillerlan\OAuth\Providers\Spotify $provider + */ +class SpotifyAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Spotify::class; + protected string $ENV = 'SPOTIFY'; + + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->id); + } + +} diff --git a/tests/Providers/Live/SteamOpenIDAPITest.php b/tests/Providers/Live/SteamOpenIDAPITest.php new file mode 100644 index 00000000..4674484e --- /dev/null +++ b/tests/Providers/Live/SteamOpenIDAPITest.php @@ -0,0 +1,34 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\OAuth\Providers\SteamOpenID; +use chillerlan\OAuthTest\Providers\OAuthAPITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\SteamOpenID $provider + */ +class SteamOpenIDAPITest extends OAuthAPITestAbstract{ + + protected string $FQN = SteamOpenID::class; + protected string $ENV = 'STEAMOPENID'; + + protected int $id; + + protected function setUp():void{ + parent::setUp(); + + $token = $this->storage->getAccessToken($this->provider->serviceName); + + $this->id = $token->extraParams['id_int']; // SteamID64 + } + +} diff --git a/tests/Providers/Live/StripeAPITest.php b/tests/Providers/Live/StripeAPITest.php new file mode 100644 index 00000000..1b7390b8 --- /dev/null +++ b/tests/Providers/Live/StripeAPITest.php @@ -0,0 +1,33 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Stripe; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * Stripe API usage tests/examples + * + * @link https://stripe.com/docs/api + * + * @property \chillerlan\OAuth\Providers\Stripe $provider + */ +class StripeAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Stripe::class; + protected string $ENV = 'STRIPE'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->data[0]->email); + } + +} diff --git a/tests/Providers/Live/Tumblr2APITest.php b/tests/Providers/Live/Tumblr2APITest.php new file mode 100644 index 00000000..8fce848d --- /dev/null +++ b/tests/Providers/Live/Tumblr2APITest.php @@ -0,0 +1,29 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Tumblr2; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Tumblr2 $provider + */ +class Tumblr2APITest extends OAuth2APITestAbstract{ + + protected string $FQN = Tumblr2::class; + protected string $ENV = 'TUMBLR'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->response->user->name); + } + +} diff --git a/tests/Providers/Live/TumblrAPITest.php b/tests/Providers/Live/TumblrAPITest.php new file mode 100644 index 00000000..4ad43c09 --- /dev/null +++ b/tests/Providers/Live/TumblrAPITest.php @@ -0,0 +1,37 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Tumblr; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; +use function var_dump; + +/** + * @property \chillerlan\OAuth\Providers\Tumblr $provider + */ +class TumblrAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = Tumblr::class; + protected string $ENV = 'TUMBLR'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->response->user->name); + } + + public function testTokenExchange():void{ + // only outcomment if wou want to deliberately invaildate your current token + $this::markTestSkipped('N/A - will invalidate the current token'); + + $this::assertSame('bearer', $this->provider->exchangeForOAuth2Token()->extraParams['token_type']); + } + +} diff --git a/tests/Providers/Live/TwitchAPITest.php b/tests/Providers/Live/TwitchAPITest.php new file mode 100644 index 00000000..72544f19 --- /dev/null +++ b/tests/Providers/Live/TwitchAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Twitch; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Twitch $provider + */ +class TwitchAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Twitch::class; + protected string $ENV = 'TWITCH'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->data[0]->display_name); + } + +} diff --git a/tests/Providers/Live/TwitterAPITest.php b/tests/Providers/Live/TwitterAPITest.php new file mode 100644 index 00000000..94b0cc17 --- /dev/null +++ b/tests/Providers/Live/TwitterAPITest.php @@ -0,0 +1,43 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Twitter; +use chillerlan\OAuthTest\Providers\OAuth1APITestAbstract; + +/** + * Twitter API tests & examples + * + * @link https://developer.twitter.com/en/docs/api-reference-index + * + * @property \chillerlan\OAuth\Providers\Twitter $provider + */ +class TwitterAPITest extends OAuth1APITestAbstract{ + + protected string $FQN = Twitter::class; + protected string $ENV = 'TWITTER'; + + protected string $screen_name; + protected int $user_id; + + protected function setUp():void{ + parent::setUp(); + + $token = $this->storage->getAccessToken($this->provider->serviceName); + $this->screen_name = $token->extraParams['screen_name']; + } + + public function testMe():void{ + $this::assertSame($this->screen_name, MessageUtil::decodeJSON($this->provider->me())->screen_name); + } + +} diff --git a/tests/Providers/Live/TwitterCCAPITest.php b/tests/Providers/Live/TwitterCCAPITest.php new file mode 100644 index 00000000..98704c34 --- /dev/null +++ b/tests/Providers/Live/TwitterCCAPITest.php @@ -0,0 +1,28 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\OAuth\Providers\TwitterCC; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\TwitterCC $provider + */ +class TwitterCCAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = TwitterCC::class; + protected string $ENV = 'TWITTER'; + + public function testMeErrorException():void{ + $this::markTestSkipped('not implemented'); + } + +} diff --git a/tests/Providers/Live/VimeoAPITest.php b/tests/Providers/Live/VimeoAPITest.php new file mode 100644 index 00000000..606b8be1 --- /dev/null +++ b/tests/Providers/Live/VimeoAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\Vimeo; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Vimeo $provider + */ +class VimeoAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = Vimeo::class; + protected string $ENV = 'VIMEO'; + + public function testMe():void{ + $this::assertSame('https://vimeo.com/'.$this->testuser, MessageUtil::decodeJSON($this->provider->me())->link); + } + +} diff --git a/tests/Providers/Live/WordpressAPITest.php b/tests/Providers/Live/WordpressAPITest.php new file mode 100644 index 00000000..ed2d89f3 --- /dev/null +++ b/tests/Providers/Live/WordpressAPITest.php @@ -0,0 +1,29 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Live; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Providers\WordPress; +use chillerlan\OAuthTest\Providers\OAuth2APITestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\WordPress $provider + */ +class WordpressAPITest extends OAuth2APITestAbstract{ + + protected string $FQN = WordPress::class; + protected string $ENV = 'WORDPRESS'; + + public function testMe():void{ + $this::assertSame($this->testuser, MessageUtil::decodeJSON($this->provider->me())->username); + } + +} diff --git a/tests/Providers/OAuth1APITestAbstract.php b/tests/Providers/OAuth1APITestAbstract.php new file mode 100644 index 00000000..77823397 --- /dev/null +++ b/tests/Providers/OAuth1APITestAbstract.php @@ -0,0 +1,24 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use chillerlan\OAuth\Core\OAuth1Interface; + +/** + * @property \chillerlan\OAuth\Core\OAuth1Interface $provider + */ +abstract class OAuth1APITestAbstract extends OAuthAPITestAbstract{ + + public function testOAuth1Instance():void{ + $this::assertInstanceOf(OAuth1Interface::class, $this->provider); + } + +} diff --git a/tests/Providers/OAuth2APITestAbstract.php b/tests/Providers/OAuth2APITestAbstract.php new file mode 100644 index 00000000..6fd17f63 --- /dev/null +++ b/tests/Providers/OAuth2APITestAbstract.php @@ -0,0 +1,49 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use chillerlan\OAuth\Core\{AccessToken, ClientCredentials, OAuth2Interface}; +use chillerlan\OAuth\Storage\MemoryStorage; + +use function time; + +/** + * @property \chillerlan\OAuth\Core\OAuth2Interface $provider + */ +abstract class OAuth2APITestAbstract extends OAuthAPITestAbstract{ + + protected array $clientCredentialsScopes = []; + + public function testOAuth2Instance():void{ + $this::assertInstanceOf(OAuth2Interface::class, $this->provider); + } + + public function testRequestCredentialsToken():void{ + + if(!$this->provider instanceof ClientCredentials){ + $this->markTestSkipped('ClientCredentials N/A'); + } + + $this->provider->setStorage(new MemoryStorage); + + $token = $this->provider->getClientCredentialsToken($this->clientCredentialsScopes); + + $this::assertInstanceOf(AccessToken::class, $token); + $this::assertIsString($token->accessToken); + + if($token->expires !== AccessToken::EOL_NEVER_EXPIRES){ + $this::assertGreaterThan(time(), $token->expires); + } + + $this->logger->debug('OAuth2ClientCredentials', $token->toArray()); + } + +} diff --git a/tests/Providers/OAuthAPITestAbstract.php b/tests/Providers/OAuthAPITestAbstract.php new file mode 100644 index 00000000..8ce86167 --- /dev/null +++ b/tests/Providers/OAuthAPITestAbstract.php @@ -0,0 +1,104 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use chillerlan\DotEnv\DotEnv; +use chillerlan\OAuth\Core\{AccessToken, ProviderException}; +use chillerlan\OAuth\OAuthOptions; +use chillerlan\OAuth\Storage\{MemoryStorage, OAuthStorageInterface}; +use chillerlan\OAuthTest\OAuthTestMemoryStorage; +use chillerlan\Settings\SettingsContainerInterface; +use Exception; +use Psr\Http\Client\ClientInterface; +use Psr\Log\LoggerInterface; +use function constant, defined, realpath; + +/** + * @property \chillerlan\OAuth\Core\OAuthInterface $provider + */ +abstract class OAuthAPITestAbstract extends OAuthProviderTestAbstract{ + + protected DotEnv $dotEnv; + protected string $ENV; + protected string $CFG; + + /** a test username for live API tests, defined in .env as {ENV-PREFIX}_TESTUSER*/ + protected string $testuser; + + /** + * @throws \Exception + */ + protected function setUp():void{ + + foreach(['TEST_CFGDIR', 'TEST_ENVFILE'] as $constant){ + if(!defined($constant)){ + throw new Exception($constant.' not set -> see phpunit.xml'); + } + } + + // set the config dir and .env config before initializing the provider + $this->CFG = realpath(__DIR__.'/../../'.constant('TEST_CFGDIR')); + $this->dotEnv = (new DotEnv($this->CFG, constant('TEST_ENVFILE'), false))->load(); + $this->testuser = (string)$this->dotEnv->get($this->ENV.'_TESTUSER'); + + // init provider etc. + parent::setUp(); + + // is_ci is now set + if($this->is_ci){ + $this->markTestSkipped('not on CI (set TEST_IS_CI in phpunit.xml to "false" if you want to run live API tests)'); + } + + } + + protected function initOptions():SettingsContainerInterface{ + return new OAuthOptions([ + 'key' => ($this->dotEnv->get($this->ENV.'_KEY') ?? ''), + 'secret' => ($this->dotEnv->get($this->ENV.'_SECRET') ?? ''), + 'tokenAutoRefresh' => true, + ]); + } + + protected function initStorage(SettingsContainerInterface $options):OAuthStorageInterface{ + return new OAuthTestMemoryStorage($options, $this->CFG); + } + + protected function initHttp(SettingsContainerInterface $options, LoggerInterface $logger, array $responses):ClientInterface{ + return new OAuthTestHttpClient($this->CFG, $logger); + } + + public function testTokenInvalidate():void{ + $this::markTestSkipped('TokenInvalidate N/A on Live Test'); + } + + public function testMe():void{ + $this::markTestSkipped('not implemented'); + } + + public function testMeErrorException():void{ + $this::expectException(ProviderException::class); + $token = $this->storage->getAccessToken($this->provider->serviceName); + // avoid refresh + $token->expires = AccessToken::EOL_NEVER_EXPIRES; + // invalidate token + $token->accessToken = 'nope'; + $token->accessTokenSecret = 'what'; + + // using a temp storage here so that the local tokens won't be overwritten + $tempStorage = (new MemoryStorage)->storeAccessToken($token, $this->provider->serviceName); + + $this->provider + ->setStorage($tempStorage) + ->me() + ; + } + +} diff --git a/tests/Providers/OAuthTestHttpClient.php b/tests/Providers/OAuthTestHttpClient.php new file mode 100644 index 00000000..9644f1c9 --- /dev/null +++ b/tests/Providers/OAuthTestHttpClient.php @@ -0,0 +1,68 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use chillerlan\HTTP\Utils\MessageUtil; +use Psr\Http\Client\{ClientExceptionInterface, ClientInterface}; +use Psr\Http\Message\{RequestInterface, ResponseInterface}; +use Psr\Log\{LoggerAwareInterface, LoggerAwareTrait, LoggerInterface, NullLogger}; +use Exception, Throwable; + +use function constant, defined, get_class, usleep; + +final class OAuthTestHttpClient implements ClientInterface, LoggerAwareInterface{ + use LoggerAwareTrait; + + protected ClientInterface $http; + + public function __construct( + string $cfgdir, + LoggerInterface $logger = null + ){ + + if(!defined('TEST_CLIENT_FACTORY')){ + throw new Exception('TEST_CLIENT_FACTORY in phpunit.xml not set'); + } + + $clientFactory = constant('TEST_CLIENT_FACTORY'); + + $this->http = $clientFactory::getClient($cfgdir); + $this->logger = ($logger ?? new NullLogger); + } + + /** + * @inheritDoc + */ + public function sendRequest(RequestInterface $request):ResponseInterface{ + $this->logger->debug("\n----HTTP-REQUEST----\n".MessageUtil::toString($request)); + usleep(250000); + + try{ + $response = $this->http->sendRequest($request); + } + catch(Throwable $e){ + $this->logger->debug("\n----HTTP-ERROR------\n"); + $this->logger->error($e->getMessage()); + $this->logger->error($e->getTraceAsString()); + + if(!$e instanceof ClientExceptionInterface){ + throw new Exception('unexpected exception, does not implement "ClientExceptionInterface": '.get_class($e)); + } + + throw $e; + } + + $this->logger->debug("\n----HTTP-RESPONSE---\n".MessageUtil::toString($response)); + + return $response; + } + +} diff --git a/tests/Providers/OAuthTestHttpClientFactoryInterface.php b/tests/Providers/OAuthTestHttpClientFactoryInterface.php new file mode 100644 index 00000000..11a692b0 --- /dev/null +++ b/tests/Providers/OAuthTestHttpClientFactoryInterface.php @@ -0,0 +1,22 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers; + +use Psr\Http\Client\ClientInterface; + +interface OAuthTestHttpClientFactoryInterface{ + + /** + * Returns a fully prepared http client instance + */ + public static function getClient(string $cfgdir):ClientInterface; + +} diff --git a/tests/Providers/Unit/AmazonTest.php b/tests/Providers/Unit/AmazonTest.php new file mode 100644 index 00000000..5375509a --- /dev/null +++ b/tests/Providers/Unit/AmazonTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Amazon; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Amazon $provider + */ +class AmazonTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Amazon::class; + +} diff --git a/tests/Providers/Unit/BattleNetTest.php b/tests/Providers/Unit/BattleNetTest.php new file mode 100644 index 00000000..f91c8197 --- /dev/null +++ b/tests/Providers/Unit/BattleNetTest.php @@ -0,0 +1,39 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Core\ProviderException; +use chillerlan\OAuth\Providers\BattleNet; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\BattleNet $provider + */ +class BattleNetTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = BattleNet::class; + + public function testSetRegion():void{ + $this->provider->setRegion('cn'); + $this::assertSame('https://gateway.battlenet.com.cn', $this->provider->apiURL); + + $this->provider->setRegion('us'); + $this::assertSame('https://us.api.blizzard.com', $this->provider->apiURL); + } + + public function testSetRegionException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('invalid region: foo'); + + $this->provider->setRegion('foo'); + } + +} diff --git a/tests/Providers/Unit/BigCartelTest.php b/tests/Providers/Unit/BigCartelTest.php new file mode 100644 index 00000000..3744d10c --- /dev/null +++ b/tests/Providers/Unit/BigCartelTest.php @@ -0,0 +1,31 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\BigCartel; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\BigCartel $provider + */ +class BigCartelTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = BigCartel::class; + + protected function setUp():void{ + // modify test response data before loading into the test http client + $this->testResponses['/oauth2/revoke_token'] = ''; + $this->testResponses['/oauth2/api/accounts'] = '{"data":[{"id":"12345"}]}'; + + parent::setUp(); + } + +} diff --git a/tests/Providers/Unit/BitbucketTest.php b/tests/Providers/Unit/BitbucketTest.php new file mode 100644 index 00000000..75e9cf44 --- /dev/null +++ b/tests/Providers/Unit/BitbucketTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Bitbucket; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Bitbucket $provider + */ +class BitbucketTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Bitbucket::class; + +} diff --git a/tests/Providers/Unit/DeezerTest.php b/tests/Providers/Unit/DeezerTest.php new file mode 100644 index 00000000..e87b8f17 --- /dev/null +++ b/tests/Providers/Unit/DeezerTest.php @@ -0,0 +1,74 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Core\{AccessToken, ProviderException}; +use chillerlan\OAuth\Providers\Deezer; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; +use function time; + +/** + * @property \chillerlan\OAuth\Providers\Deezer $provider + */ +class DeezerTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Deezer::class; + + protected array $testResponses = [ + '/oauth2/access_token' => 'access_token=test_access_token&expires_in=3600&state=test_state&scope=some_scope%20other_scope', + '/oauth2/api/request' => '{"data":"such data! much wow!"}', + ]; + + public function testGetAuthURL():void{ + $this::assertStringContainsString( + 'https://connect.deezer.com/oauth/auth.php?app_id='.$this->options->key + .'&foo=bar&perms=basic_access%20email&redirect_uri=https%3A%2F%2Flocalhost%2Fcallback&state=', + (string)$this->provider->getAuthURL( + ['foo' => 'bar', 'client_secret' => 'not-so-secret'], + [Deezer::SCOPE_BASIC, Deezer::SCOPE_EMAIL] + ) + ); + } + + public function testParseTokenResponse():void{ + $token = $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [ + $this->responseFactory->createResponse()->withBody($this->streamFactory->createStream('access_token=whatever')) + ]); + + $this::assertInstanceOf(AccessToken::class, $token); + $this::assertSame('whatever', $token->accessToken); + } + + public function testParseTokenResponseErrorException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('error retrieving access token:'); + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [ + $this->responseFactory->createResponse()->withBody($this->streamFactory->createStream('error_reason=whatever')) + ]); + } + + public function testParseTokenResponseNoDataException():void{ + $this::markTestSkipped('N/A'); + } + + public function testGetAccessToken():void{ + $token = $this->provider->getAccessToken('foo', 'test_state'); + + $this::assertSame('test_access_token', $token->accessToken); + $this::assertGreaterThan(time(), $token->expires); + } + +} diff --git a/tests/Providers/Unit/DeviantArtTest.php b/tests/Providers/Unit/DeviantArtTest.php new file mode 100644 index 00000000..95665934 --- /dev/null +++ b/tests/Providers/Unit/DeviantArtTest.php @@ -0,0 +1,30 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\DeviantArt; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\DeviantArt $provider + */ +class DeviantArtTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = DeviantArt::class; + + protected function setUp():void{ + // modify test response data before loading into the test http client + $this->testResponses['/oauth2/revoke_token'] = '{"success": true}'; + + parent::setUp(); + } + +} diff --git a/tests/Providers/Unit/DiscogsTest.php b/tests/Providers/Unit/DiscogsTest.php new file mode 100644 index 00000000..47adf1df --- /dev/null +++ b/tests/Providers/Unit/DiscogsTest.php @@ -0,0 +1,23 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Discogs; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Discogs $provider + */ +class DiscogsTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = Discogs::class; + +} diff --git a/tests/Providers/Unit/DiscordTest.php b/tests/Providers/Unit/DiscordTest.php new file mode 100644 index 00000000..f99b3c66 --- /dev/null +++ b/tests/Providers/Unit/DiscordTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Discord; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Discord $provider + */ +class DiscordTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Discord::class; + +} diff --git a/tests/Providers/Unit/FlickrTest.php b/tests/Providers/Unit/FlickrTest.php new file mode 100644 index 00000000..c263d443 --- /dev/null +++ b/tests/Providers/Unit/FlickrTest.php @@ -0,0 +1,30 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Flickr; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Flickr $provider + */ +class FlickrTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = Flickr::class; + + protected array $testResponses = [ + '/oauth1/request_token' => 'oauth_token=test_request_token&oauth_token_secret=test_request_token_secret&oauth_callback_confirmed=true', + '/oauth1/access_token' => 'oauth_token=test_access_token&oauth_token_secret=test_access_token_secret&oauth_callback_confirmed=true', + // the Flickr client does not add a path, so "/request" is missing + '/oauth1/api' => '{"data":"such data! much wow!"}', + ]; + +} diff --git a/tests/Providers/Unit/FoursquareTest.php b/tests/Providers/Unit/FoursquareTest.php new file mode 100644 index 00000000..c3422062 --- /dev/null +++ b/tests/Providers/Unit/FoursquareTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Foursquare; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Foursquare $provider + */ +class FoursquareTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Foursquare::class; + +} diff --git a/tests/Providers/Unit/GitHubTest.php b/tests/Providers/Unit/GitHubTest.php new file mode 100644 index 00000000..1e26032d --- /dev/null +++ b/tests/Providers/Unit/GitHubTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\GitHub; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GitHub $provider + */ +class GitHubTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = GitHub::class; + +} diff --git a/tests/Providers/Unit/GitLabTest.php b/tests/Providers/Unit/GitLabTest.php new file mode 100644 index 00000000..d1673ec3 --- /dev/null +++ b/tests/Providers/Unit/GitLabTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\GitLab; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GitLab $provider + */ +class GitLabTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = GitLab::class; + +} diff --git a/tests/Providers/Unit/GoogleTest.php b/tests/Providers/Unit/GoogleTest.php new file mode 100644 index 00000000..259cfaab --- /dev/null +++ b/tests/Providers/Unit/GoogleTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Google; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Google $provider + */ +class GoogleTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Google::class; + +} diff --git a/tests/Providers/Unit/GuildWars2Test.php b/tests/Providers/Unit/GuildWars2Test.php new file mode 100644 index 00000000..86d382aa --- /dev/null +++ b/tests/Providers/Unit/GuildWars2Test.php @@ -0,0 +1,70 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Core\ProviderException; +use chillerlan\OAuth\Providers\GuildWars2; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\GuildWars2 $provider + */ +class GuildWars2Test extends OAuth2ProviderTestAbstract{ + + protected string $FQN = GuildWars2::class; + + protected array $testResponses = [ + '/gw2/auth/v2/tokeninfo' => '{"id":"00000000-1111-2222-3333-444444444444","name":"GW2Token","permissions":["foo","bar"]}', + '/oauth2/api/request' => '{"data":"such data! much wow!"}', + ]; + + public function testStoreGW2Token():void{ + $this->reflection->getProperty('apiURL')->setValue($this->provider, 'https://localhost/gw2/auth'); + + $id = '00000000-1111-2222-3333-444444444444'; + $secret = '55555555-6666-7777-8888-999999999999'; + + $token = $this->provider->storeGW2Token($id.$secret); + + $this::assertSame($id.$secret, $token->accessToken); + $this::assertSame($secret, $token->accessTokenSecret); + } + + public function testStoreGW2InvalidToken():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('invalid token'); + + $this->provider->storeGW2Token('foo'); + } + + public function testGetAuthURL():void{ + $this->markTestSkipped('N/A'); + } + + public function testGetAccessToken():void{ + $this->markTestSkipped('N/A'); + } + + public function testRequestGetAuthURLNotSupportedException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('GuildWars2 does not support authentication anymore.'); + + $this->provider->getAuthURL(); + } + + public function testRequestGetAccessTokenNotSupportedException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('GuildWars2 does not support authentication anymore.'); + + $this->provider->getAccessToken('foo'); + } + +} diff --git a/tests/Providers/Unit/ImgurTest.php b/tests/Providers/Unit/ImgurTest.php new file mode 100644 index 00000000..e64a0ef5 --- /dev/null +++ b/tests/Providers/Unit/ImgurTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Imgur; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Imgur $provider + */ +class ImgurTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Imgur::class; + +} diff --git a/tests/Providers/Unit/LastFMTest.php b/tests/Providers/Unit/LastFMTest.php new file mode 100644 index 00000000..c04139d7 --- /dev/null +++ b/tests/Providers/Unit/LastFMTest.php @@ -0,0 +1,120 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, ProviderException}; +use chillerlan\OAuth\Providers\LastFM; +use chillerlan\OAuthTest\Providers\OAuthProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\LastFM $provider + */ +class LastFMTest extends OAuthProviderTestAbstract{ + + protected string $FQN = LastFM::class; + + protected array $testResponses = [ + '/lastfm/auth' => '{"session":{"key":"session_key"}}', + '/lastfm/api/request' => '{"data":"such data! much wow!"}', + ]; + + public function setUp():void{ + parent::setUp(); + + $this->provider->storeAccessToken(new AccessToken(['accessToken' => 'foo'])); + $this->reflection->getProperty('apiURL')->setValue($this->provider, '/lastfm/api/request'); + } + + public function testGetAuthURL():void{ + $url = $this->provider->getAuthURL(['foo' => 'bar']); + + $this::assertSame('https://www.last.fm/api/auth?api_key='.$this->options->key.'&foo=bar', (string)$url); + } + + public function testGetSignature():void{ + $signature = $this->reflection + ->getMethod('getSignature') + ->invokeArgs($this->provider, [['foo' => 'bar', 'format' => 'whatever', 'callback' => 'nope']]); + + $this::assertSame('cb143650fa678449f5492a2aa6fab216', $signature); + } + + public function testParseTokenResponse():void{ + $r = $this->responseFactory + ->createResponse() + ->withBody($this->streamFactory->createStream('{"session":{"key":"whatever"}}')) + ; + + $token = $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$r]); + + $this::assertSame('whatever', $token->accessToken); + } + + public function testParseTokenResponseNoData():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('unable to parse token response'); + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$this->responseFactory->createResponse()]); + } + + public function testParseTokenResponseError():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('error retrieving access token:'); + + $r = $this->responseFactory + ->createResponse() + ->withBody($this->streamFactory->createStream('{"error":42,"message":"whatever"}')) + ; + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$r]); + } + + public function testParseTokenResponseNoToken():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('token missing'); + + $r = $this->responseFactory->createResponse()->withBody($this->streamFactory->createStream('{"session":[]}')); + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$r]); + } + + public function testGetAccessToken():void{ + $this->reflection->getProperty('apiURL')->setValue($this->provider, '/lastfm/auth'); + + $token = $this->provider->getAccessToken('session_token'); + + $this::assertSame('session_key', $token->accessToken); + } + + // coverage + public function testRequest():void{ + $r = $this->provider->request(''); + + $this::assertSame('such data! much wow!', MessageUtil::decodeJSON($r)->data); + } + + // coverage + public function testRequestPost():void{ + $r = $this->provider->request('', [], 'POST', ['foo' => 'bar'], ['Content-Type' => 'whatever']); + + $this::assertSame('such data! much wow!', MessageUtil::decodeJSON($r)->data); + } + +} diff --git a/tests/Providers/Unit/MailChimpTest.php b/tests/Providers/Unit/MailChimpTest.php new file mode 100644 index 00000000..c84d7a69 --- /dev/null +++ b/tests/Providers/Unit/MailChimpTest.php @@ -0,0 +1,69 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\OAuthException; +use chillerlan\OAuth\Providers\MailChimp; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\MailChimp $provider + */ +class MailChimpTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = MailChimp::class; + + protected array $testResponses = [ + '/oauth2/access_token' => + '{"access_token":"test_access_token","expires_in":3600,"state":"test_state","scope":"some_scope other_scope"}', + '/oauth2/metadata' => + '{"metadata":"whatever"}', + '/3.0/' => + '{"data":"such data! much wow! (/3.0/)"}', + ]; + + protected AccessToken $token; + + public function setUp():void{ + parent::setUp(); + + $this->token = new AccessToken([ + 'accessToken' => 'test_access_token_secret', + 'expires' => 1, + 'extraParams' => ['dc' => 'bar'], + ]); + } + + public function testRequest():void{ + $this->storage->storeAccessToken($this->token, $this->provider->serviceName); + $this::assertSame('such data! much wow! (/3.0/)', MessageUtil::decodeJSON($this->provider->request('/3.0/'))->data); + } + + public function testRequestInvalidAuthTypeException():void{ + $this->expectException(OAuthException::class); + $this->expectExceptionMessage('invalid auth type'); + + $this->reflection->getProperty('authMethod')->setValue($this->provider, -1); + + $this->storage->storeAccessToken($this->token, $this->provider->serviceName); + + $this->provider->request(''); + } + + public function testGetTokenMetadata():void{ + $token = $this->provider->getTokenMetadata($this->token); + + $this::assertSame('whatever', $token->extraParams['metadata']); + } + +} diff --git a/tests/Providers/Unit/MastodonTest.php b/tests/Providers/Unit/MastodonTest.php new file mode 100644 index 00000000..d454b3b6 --- /dev/null +++ b/tests/Providers/Unit/MastodonTest.php @@ -0,0 +1,37 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\OAuthException; +use chillerlan\OAuth\Providers\Mastodon; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Mastodon $provider + */ +class MastodonTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Mastodon::class; + + public function testGetAuthURL():void{ + $this->provider->setInstance('https://localhost'); + + parent::testGetAuthURL(); + } + + public function testSetInvalidInstance():void{ + $this->expectException(OAuthException::class); + $this->expectExceptionMessage('invalid instance URL'); + + $this->provider->setInstance('whatever'); + } + +} diff --git a/tests/Providers/Unit/MicrosoftGraphTest.php b/tests/Providers/Unit/MicrosoftGraphTest.php new file mode 100644 index 00000000..079a9c36 --- /dev/null +++ b/tests/Providers/Unit/MicrosoftGraphTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\MicrosoftGraph; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\MicrosoftGraph $provider + */ +class MicrosoftGraphTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = MicrosoftGraph::class; + +} diff --git a/tests/Providers/Unit/MixcloudTest.php b/tests/Providers/Unit/MixcloudTest.php new file mode 100644 index 00000000..d4f74cf8 --- /dev/null +++ b/tests/Providers/Unit/MixcloudTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Mixcloud; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Mixcloud $provider + */ +class MixcloudTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Mixcloud::class; + +} diff --git a/tests/Providers/Unit/MusicBrainzTest.php b/tests/Providers/Unit/MusicBrainzTest.php new file mode 100644 index 00000000..25579b00 --- /dev/null +++ b/tests/Providers/Unit/MusicBrainzTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\MusicBrainz; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\MusicBrainz $provider + */ +class MusicBrainzTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = MusicBrainz::class; + +} diff --git a/tests/Providers/Unit/NPROneTest.php b/tests/Providers/Unit/NPROneTest.php new file mode 100644 index 00000000..1c730f9b --- /dev/null +++ b/tests/Providers/Unit/NPROneTest.php @@ -0,0 +1,64 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Core\AccessToken; +use chillerlan\OAuth\OAuthException; +use chillerlan\OAuth\Providers\NPROne; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; +use PHPUnit\Framework\Attributes\DataProvider; + +/** + * @property \chillerlan\OAuth\Providers\NPROne $provider + */ +class NPROneTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = NPROne::class; + + protected function setUp():void{ + // modify test response data before loading into the test http client + // using the api url exit of NPROne::getRequestTarget() because reasons + $this->testResponses['/oauth2/api/revoke_token'] = '{"message":"token revoked"}'; + + parent::setUp(); + + $this->reflection->getProperty('revokeURL')->setValue($this->provider, '/revoke_token'); + + } + + public function testRequestInvalidAuthTypeException():void{ + $this->expectException(OAuthException::class); + $this->expectExceptionMessage('invalid auth type'); + + $this->reflection->getProperty('authMethod')->setValue($this->provider, -1); + + $token = new AccessToken(['accessToken' => 'test_access_token_secret', 'expires' => 1]); + $this->storage->storeAccessToken($token, $this->provider->serviceName); + + $this->provider->request('https://foo.api.npr.org/'); + } + + #[DataProvider('requestTargetProvider')] + public function testGetRequestTarget(string $path, string $expected):void{ + $this::markTestSkipped('N/A'); + } + + public function testSetAPI():void{ + $this->provider = $this->initProvider(); + + $this::assertSame('https://listening.api.npr.org', $this->reflection->getProperty('apiURL')->getValue($this->provider)); + + $this->provider->setAPI('station'); + + $this::assertSame('https://station.api.npr.org', $this->reflection->getProperty('apiURL')->getValue($this->provider)); + } + +} diff --git a/tests/Providers/Unit/OpenCachingTest.php b/tests/Providers/Unit/OpenCachingTest.php new file mode 100644 index 00000000..f2a5488b --- /dev/null +++ b/tests/Providers/Unit/OpenCachingTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\OpenCaching; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenCaching $provider + */ +class OpenCachingTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = OpenCaching::class; + +} diff --git a/tests/Providers/Unit/OpenStreetmap2Test.php b/tests/Providers/Unit/OpenStreetmap2Test.php new file mode 100644 index 00000000..27a7bf4e --- /dev/null +++ b/tests/Providers/Unit/OpenStreetmap2Test.php @@ -0,0 +1,23 @@ + + * @copyright 2024 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\OpenStreetmap2; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenStreetmap $provider + */ +class OpenStreetmap2Test extends OAuth2ProviderTestAbstract{ + + protected string $FQN = OpenStreetmap2::class; + +} diff --git a/tests/Providers/Unit/OpenStreetmapTest.php b/tests/Providers/Unit/OpenStreetmapTest.php new file mode 100644 index 00000000..0d6e3048 --- /dev/null +++ b/tests/Providers/Unit/OpenStreetmapTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\OpenStreetmap; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\OpenStreetmap $provider + */ +class OpenStreetmapTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = OpenStreetmap::class; + +} diff --git a/tests/Providers/Unit/Patreon1Test.php b/tests/Providers/Unit/Patreon1Test.php new file mode 100644 index 00000000..5a7556f5 --- /dev/null +++ b/tests/Providers/Unit/Patreon1Test.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Patreon; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Patreon $provider + */ +class Patreon1Test extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Patreon::class; + +} diff --git a/tests/Providers/Unit/Patreon2Test.php b/tests/Providers/Unit/Patreon2Test.php new file mode 100644 index 00000000..16a57675 --- /dev/null +++ b/tests/Providers/Unit/Patreon2Test.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Patreon; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Patreon $provider + */ +class Patreon2Test extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Patreon::class; + +} diff --git a/tests/Providers/Unit/PayPalTest.php b/tests/Providers/Unit/PayPalTest.php new file mode 100644 index 00000000..3eea168a --- /dev/null +++ b/tests/Providers/Unit/PayPalTest.php @@ -0,0 +1,23 @@ + + * @copyright 2019 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\PayPal; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\PayPal $provider + */ +class PayPalTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = PayPal::class; + +} diff --git a/tests/Providers/Unit/SlackTest.php b/tests/Providers/Unit/SlackTest.php new file mode 100644 index 00000000..3f845f92 --- /dev/null +++ b/tests/Providers/Unit/SlackTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Slack; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Slack $provider + */ +class SlackTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Slack::class; + +} diff --git a/tests/Providers/Unit/SoundCloudTest.php b/tests/Providers/Unit/SoundCloudTest.php new file mode 100644 index 00000000..69ff9b52 --- /dev/null +++ b/tests/Providers/Unit/SoundCloudTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\SoundCloud; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\SoundCloud $provider + */ +class SoundCloudTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = SoundCloud::class; + +} diff --git a/tests/Providers/Unit/SpotifyTest.php b/tests/Providers/Unit/SpotifyTest.php new file mode 100644 index 00000000..157735a6 --- /dev/null +++ b/tests/Providers/Unit/SpotifyTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Spotify; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Spotify $provider + */ +class SpotifyTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Spotify::class; + +} diff --git a/tests/Providers/Unit/SteamOpenIDTest.php b/tests/Providers/Unit/SteamOpenIDTest.php new file mode 100644 index 00000000..09913de3 --- /dev/null +++ b/tests/Providers/Unit/SteamOpenIDTest.php @@ -0,0 +1,117 @@ + + * @copyright 2021 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\HTTP\Utils\MessageUtil; +use chillerlan\OAuth\Core\{AccessToken, ProviderException}; +use chillerlan\OAuth\Providers\SteamOpenID; +use chillerlan\OAuthTest\Providers\OAuthProviderTestAbstract; +use function urlencode; + +/** + * @property \chillerlan\OAuth\Providers\SteamOpenID $provider + */ +class SteamOpenIDTest extends OAuthProviderTestAbstract{ + + protected const ID_VALID = "ns:http://specs.openid.net/auth/2.0\x0ais_valid:true\x0a"; + protected const ID_INVALID = "ns:http://specs.openid.net/auth/2.0\x0ais_valid:false\x0a"; + + protected string $FQN = SteamOpenID::class; + + protected array $testResponses = [ + '/steam/id' => self::ID_VALID, + '/steam/api/request' => '{"data":"such data! much wow!"}', + ]; + + protected function setUp():void{ + parent::setUp(); + + $this->provider->storeAccessToken(new AccessToken(['accessToken' => 'foo'])); + $this->reflection->getProperty('accessTokenURL')->setValue($this->provider, 'https://localhost/steam/id'); + $this->reflection->getProperty('apiURL')->setValue($this->provider, 'https://localhost/steam/api/request'); + } + + public function testGetAuthURL():void{ + $url = $this->provider->getAuthURL(['foo' => 'bar']); + + $expected = 'https://steamcommunity.com/openid/login' + .'?openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select' + .'&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select' + .'&openid.mode=checkid_setup' + .'&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0' + .'&openid.realm='.$this->options->key + .'&openid.return_to='.urlencode($this->options->callbackURL); + + $this::assertSame($expected, (string)$url); + } + + public function testGetAccessToken():void{ + + $received = [ + 'openid_ns' => 'http://specs.openid.net/auth/2.0', + 'openid_mode' => 'id_res', + 'openid_op_endpoint' => 'https://steamcommunity.com/openid/login', + 'openid_claimed_id' => 'https://steamcommunity.com/openid/id/69420', + 'openid_identity' => 'https://steamcommunity.com/openid/id/69420', + 'openid_return_to' => 'https://smiley.codes/oauth/', + 'openid_response_nonce' => '2021-03-16T06:40:46ZtLLZ4JqhLZ2IULBg8x2P8YitHQY=', + 'openid_assoc_handle' => '1234567890', + 'openid_signed' => 'signed,op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle', + 'openid_sig' => '7WEtj64YlaJLNqL6M0gZvVmOLFg=', + ]; + + $this::assertSame('69420', $this->provider->getAccessToken($received)->accessToken); + } + + public function testParseTokenResponse():void{ + $r = $this->responseFactory + ->createResponse() + ->withBody($this->streamFactory->createStream(self::ID_VALID)) + ; + + $token = $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$r]); + + $this::assertSame('SteamID', $token->accessToken); + } + + public function testParseTokenResponseNoData():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('unable to parse token response'); + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$this->responseFactory->createResponse()]); + } + + public function testParseTokenResponseInvalidID():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('invalid id'); + + $r = $this->responseFactory + ->createResponse() + ->withBody($this->streamFactory->createStream(self::ID_INVALID)) + ; + + $this->reflection + ->getMethod('parseTokenResponse') + ->invokeArgs($this->provider, [$r]); + } + + // coverage + public function testRequest():void{ + $r = $this->provider->request(''); + + $this::assertSame('such data! much wow!', MessageUtil::decodeJSON($r)->data); + } + +} diff --git a/tests/Providers/Unit/StripeTest.php b/tests/Providers/Unit/StripeTest.php new file mode 100644 index 00000000..84b9cd33 --- /dev/null +++ b/tests/Providers/Unit/StripeTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Stripe; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Stripe $provider + */ +class StripeTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Stripe::class; + +} diff --git a/tests/Providers/Unit/Tumblr2Test.php b/tests/Providers/Unit/Tumblr2Test.php new file mode 100644 index 00000000..7252eecf --- /dev/null +++ b/tests/Providers/Unit/Tumblr2Test.php @@ -0,0 +1,23 @@ + + * @copyright 2023 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Tumblr2; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Tumblr2 $provider + */ +class Tumblr2Test extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Tumblr2::class; + +} diff --git a/tests/Providers/Unit/TumblrTest.php b/tests/Providers/Unit/TumblrTest.php new file mode 100644 index 00000000..1940639f --- /dev/null +++ b/tests/Providers/Unit/TumblrTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Tumblr; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Tumblr $provider + */ +class TumblrTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = Tumblr::class; + +} diff --git a/tests/Providers/Unit/TwitchTest.php b/tests/Providers/Unit/TwitchTest.php new file mode 100644 index 00000000..1eac6d19 --- /dev/null +++ b/tests/Providers/Unit/TwitchTest.php @@ -0,0 +1,27 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Twitch; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Twitch $provider + */ +class TwitchTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Twitch::class; + + public function testRequestInvalidAuthTypeException():void{ + $this::markTestSkipped('N/A'); + } + +} diff --git a/tests/Providers/Unit/TwitterCCTest.php b/tests/Providers/Unit/TwitterCCTest.php new file mode 100644 index 00000000..96020b20 --- /dev/null +++ b/tests/Providers/Unit/TwitterCCTest.php @@ -0,0 +1,46 @@ + + * @copyright 2017 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Core\ProviderException; +use chillerlan\OAuth\Providers\TwitterCC; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\TwitterCC $provider + */ +class TwitterCCTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = TwitterCC::class; + + public function testGetAuthURL():void{ + $this->markTestSkipped('N/A'); + } + + public function testGetAccessToken():void{ + $this->markTestSkipped('N/A'); + } + + public function testRequestGetAuthURLNotSupportedException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('TwitterCC only supports Client Credentials Grant'); + + $this->provider->getAuthURL(); + } + + public function testRequestGetAccessTokenNotSupportedException():void{ + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('TwitterCC only supports Client Credentials Grant'); + + $this->provider->getAccessToken('foo'); + } + +} diff --git a/tests/Providers/Unit/TwitterTest.php b/tests/Providers/Unit/TwitterTest.php new file mode 100644 index 00000000..db25cacf --- /dev/null +++ b/tests/Providers/Unit/TwitterTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Twitter; +use chillerlan\OAuthTest\Providers\OAuth1ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Twitter $provider + */ +class TwitterTest extends OAuth1ProviderTestAbstract{ + + protected string $FQN = Twitter::class; + +} diff --git a/tests/Providers/Unit/VimeoTest.php b/tests/Providers/Unit/VimeoTest.php new file mode 100644 index 00000000..b4c21525 --- /dev/null +++ b/tests/Providers/Unit/VimeoTest.php @@ -0,0 +1,30 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\Vimeo; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\Vimeo $provider + */ +class VimeoTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = Vimeo::class; + + protected function setUp():void{ + // modify test response data before loading into the test http client + $this->testResponses['/oauth2/revoke_token'] = ''; + + parent::setUp(); + } + +} diff --git a/tests/Providers/Unit/WordPressTest.php b/tests/Providers/Unit/WordPressTest.php new file mode 100644 index 00000000..cf5b473b --- /dev/null +++ b/tests/Providers/Unit/WordPressTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 Smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\WordPress; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\WordPress $provider + */ +class WordPressTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = WordPress::class; + +} diff --git a/tests/Providers/Unit/YouTubeTest.php b/tests/Providers/Unit/YouTubeTest.php new file mode 100644 index 00000000..762e544a --- /dev/null +++ b/tests/Providers/Unit/YouTubeTest.php @@ -0,0 +1,23 @@ + + * @copyright 2018 smiley + * @license MIT + */ + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\OAuth\Providers\YouTube; +use chillerlan\OAuthTest\Providers\OAuth2ProviderTestAbstract; + +/** + * @property \chillerlan\OAuth\Providers\YouTube $provider + */ +class YouTubeTest extends OAuth2ProviderTestAbstract{ + + protected string $FQN = YouTube::class; + +}