1<?php 2require_once 'Soundcloud/Exception.php'; 3require_once 'Soundcloud/Version.php'; 4 5/** 6 * SoundCloud API wrapper with support for authentication using OAuth 2 7 * 8 * @category Services 9 * @package Services_Soundcloud 10 * @author Anton Lindqvist <anton@qvister.se> 11 * @copyright 2010 Anton Lindqvist <anton@qvister.se> 12 * @license http://www.opensource.org/licenses/mit-license.php MIT 13 * @link http://github.com/mptre/php-soundcloud 14 */ 15class Services_Soundcloud 16{ 17 18 /** 19 * Custom cURL option 20 * 21 * @var integer 22 * 23 * @access public 24 */ 25 const CURLOPT_OAUTH_TOKEN = 173; 26 27 /** 28 * Access token returned by the service provider after a successful authentication 29 * 30 * @var string 31 * 32 * @access private 33 */ 34 private $_accessToken; 35 36 /** 37 * Version of the API to use 38 * 39 * @var integer 40 * 41 * @access private 42 * @static 43 */ 44 private static $_apiVersion = 1; 45 46 /** 47 * Supported audio MIME types 48 * 49 * @var array 50 * 51 * @access private 52 * @static 53 */ 54 private static $_audioMimeTypes = array( 55 'aac' => 'video/mp4', 56 'aiff' => 'audio/x-aiff', 57 'flac' => 'audio/flac', 58 'mp3' => 'audio/mpeg', 59 'ogg' => 'audio/ogg', 60 'wav' => 'audio/x-wav' 61 ); 62 63 /** 64 * OAuth client id 65 * 66 * @var string 67 * 68 * @access private 69 */ 70 private $_clientId; 71 72 /** 73 * OAuth client secret 74 * 75 * @var string 76 * 77 * @access private 78 */ 79 private $_clientSecret; 80 81 /** 82 * Default cURL options 83 * 84 * @var array 85 * 86 * @access private 87 * @static 88 */ 89 private static $_curlDefaultOptions = array( 90 CURLOPT_HEADER => true, 91 CURLOPT_RETURNTRANSFER => true, 92 CURLOPT_USERAGENT => '' 93 ); 94 95 /** 96 * cURL options 97 * 98 * @var array 99 * 100 * @access private 101 */ 102 private $_curlOptions; 103 104 /** 105 * Development mode 106 * 107 * @var boolean 108 * 109 * @access private 110 */ 111 private $_development; 112 113 /** 114 * Available API domains 115 * 116 * @var array 117 * 118 * @access private 119 * @static 120 */ 121 private static $_domains = array( 122 'development' => 'sandbox-soundcloud.com', 123 'production' => 'soundcloud.com' 124 ); 125 126 /** 127 * HTTP response body from the last request 128 * 129 * @var string 130 * 131 * @access private 132 */ 133 private $_lastHttpResponseBody; 134 135 /** 136 * HTTP response code from the last request 137 * 138 * @var integer 139 * 140 * @access private 141 */ 142 private $_lastHttpResponseCode; 143 144 /** 145 * HTTP response headers from last request 146 * 147 * @var array 148 * 149 * @access private 150 */ 151 private $_lastHttpResponseHeaders; 152 153 /** 154 * OAuth paths 155 * 156 * @var array 157 * 158 * @access private 159 * @static 160 */ 161 private static $_paths = array( 162 'authorize' => 'connect', 163 'access_token' => 'oauth2/token', 164 ); 165 166 /** 167 * OAuth redirect URI 168 * 169 * @var string 170 * 171 * @access private 172 */ 173 private $_redirectUri; 174 175 /** 176 * API response format MIME type 177 * 178 * @var string 179 * 180 * @access private 181 */ 182 private $_requestFormat; 183 184 /** 185 * Available response formats 186 * 187 * @var array 188 * 189 * @access private 190 * @static 191 */ 192 private static $_responseFormats = array( 193 '*' => '*/*', 194 'json' => 'application/json', 195 'xml' => 'application/xml' 196 ); 197 198 /** 199 * HTTP user agent 200 * 201 * @var string 202 * 203 * @access private 204 * @static 205 */ 206 private static $_userAgent = 'PHP-SoundCloud'; 207 208 /** 209 * Class constructor 210 * 211 * @param string $clientId OAuth client id 212 * @param string $clientSecret OAuth client secret 213 * @param string $redirectUri OAuth redirect URI 214 * @param boolean $development Sandbox mode 215 * 216 * @return void 217 * @throws Services_Soundcloud_Missing_Client_Id_Exception 218 * 219 * @access public 220 */ 221 function __construct($clientId, $clientSecret, $redirectUri = null, $development = false) 222 { 223 if (empty($clientId)) { 224 throw new Services_Soundcloud_Missing_Client_Id_Exception(); 225 } 226 227 $this->_clientId = $clientId; 228 $this->_clientSecret = $clientSecret; 229 $this->_redirectUri = $redirectUri; 230 $this->_development = $development; 231 $this->_responseFormat = self::$_responseFormats['json']; 232 $this->_curlOptions = self::$_curlDefaultOptions; 233 $this->_curlOptions[CURLOPT_USERAGENT] .= $this->_getUserAgent(); 234 } 235 236 /** 237 * Get authorization URL 238 * 239 * @param array $params Optional query string parameters 240 * 241 * @return string 242 * 243 * @access public 244 * @see Soundcloud::_buildUrl() 245 */ 246 function getAuthorizeUrl($params = array()) 247 { 248 $defaultParams = array( 249 'client_id' => $this->_clientId, 250 'redirect_uri' => $this->_redirectUri, 251 'response_type' => 'code' 252 ); 253 $params = array_merge($defaultParams, $params); 254 255 return $this->_buildUrl(self::$_paths['authorize'], $params, false); 256 } 257 258 /** 259 * Get access token URL 260 * 261 * @param array $params Optional query string parameters 262 * 263 * @return string 264 * 265 * @access public 266 * @see Soundcloud::_buildUrl() 267 */ 268 function getAccessTokenUrl($params = array()) 269 { 270 return $this->_buildUrl(self::$_paths['access_token'], $params, false); 271 } 272 273 /** 274 * Retrieve access token through credentials flow 275 * 276 * @param string $username Username 277 * @param string $password Password 278 * 279 * @return mixed 280 * 281 * @access public 282 */ 283 function credentialsFlow($username, $password) 284 { 285 $postData = array( 286 'client_id' => $this->_clientId, 287 'client_secret' => $this->_clientSecret, 288 'username' => $username, 289 'password' => $password, 290 'grant_type' => 'password' 291 ); 292 293 $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData); 294 $response = json_decode( 295 $this->_request($this->getAccessTokenUrl(), $options), 296 true 297 ); 298 299 if (array_key_exists('access_token', $response)) { 300 $this->_accessToken = $response['access_token']; 301 302 return $response; 303 } else { 304 return false; 305 } 306 } 307 308 /** 309 * Retrieve access token 310 * 311 * @param string $code Optional OAuth code returned from the service provider 312 * @param array $postData Optional post data 313 * @param array $curlOptions Optional cURL options 314 * 315 * @return mixed 316 * 317 * @access public 318 * @see Soundcloud::_getAccessToken() 319 */ 320 function accessToken($code = null, $postData = array(), $curlOptions = array()) 321 { 322 $defaultPostData = array( 323 'code' => $code, 324 'client_id' => $this->_clientId, 325 'client_secret' => $this->_clientSecret, 326 'redirect_uri' => $this->_redirectUri, 327 'grant_type' => 'authorization_code' 328 ); 329 $postData = array_filter(array_merge($defaultPostData, $postData)); 330 331 return $this->_getAccessToken($postData, $curlOptions); 332 } 333 334 /** 335 * Refresh access token 336 * 337 * @param string $refreshToken The token to refresh 338 * @param array $postData Optional post data 339 * @param array $curlOptions Optional cURL options 340 * 341 * @return mixed 342 * @see Soundcloud::_getAccessToken() 343 * 344 * @access public 345 */ 346 function accessTokenRefresh($refreshToken, $postData = array(), $curlOptions = array()) 347 { 348 $defaultPostData = array( 349 'refresh_token' => $refreshToken, 350 'client_id' => $this->_clientId, 351 'client_secret' => $this->_clientSecret, 352 'redirect_uri' => $this->_redirectUri, 353 'grant_type' => 'refresh_token' 354 ); 355 $postData = array_merge($defaultPostData, $postData); 356 357 return $this->_getAccessToken($postData, $curlOptions); 358 } 359 360 /** 361 * Get access token 362 * 363 * @return mixed 364 * 365 * @access public 366 */ 367 function getAccessToken() 368 { 369 return $this->_accessToken; 370 } 371 372 /** 373 * Get API version 374 * 375 * @return integer 376 * 377 * @access public 378 */ 379 function getApiVersion() 380 { 381 return self::$_apiVersion; 382 } 383 384 /** 385 * Get the corresponding MIME type for a given file extension 386 * 387 * @param string $extension Given extension 388 * 389 * @return string 390 * @throws Services_Soundcloud_Unsupported_Audio_Format_Exception 391 * 392 * @access public 393 */ 394 function getAudioMimeType($extension) 395 { 396 if (array_key_exists($extension, self::$_audioMimeTypes)) { 397 return self::$_audioMimeTypes[$extension]; 398 } else { 399 throw new Services_Soundcloud_Unsupported_Audio_Format_Exception(); 400 } 401 } 402 403 /** 404 * Get cURL options 405 * 406 * @param string $key Optional options key 407 * 408 * @return mixed 409 * 410 * @access public 411 */ 412 function getCurlOptions($key = null) 413 { 414 if ($key) { 415 return (array_key_exists($key, $this->_curlOptions)) 416 ? $this->_curlOptions[$key] 417 : false; 418 } else { 419 return $this->_curlOptions; 420 } 421 } 422 423 /** 424 * Get development mode 425 * 426 * @return boolean 427 * 428 * @access public 429 */ 430 function getDevelopment() 431 { 432 return $this->_development; 433 } 434 435 /** 436 * Get HTTP response header 437 * 438 * @param string $header Name of the header 439 * 440 * @return mixed 441 * 442 * @access public 443 */ 444 function getHttpHeader($header) 445 { 446 if (is_array($this->_lastHttpResponseHeaders) 447 && array_key_exists($header, $this->_lastHttpResponseHeaders) 448 ) { 449 return $this->_lastHttpResponseHeaders[$header]; 450 } else { 451 return false; 452 } 453 } 454 455 /** 456 * Get redirect URI 457 * 458 * @return string 459 * 460 * @access public 461 */ 462 function getRedirectUri() 463 { 464 return $this->_redirectUri; 465 } 466 467 /** 468 * Get response format 469 * 470 * @return string 471 * 472 * @access public 473 */ 474 function getResponseFormat() 475 { 476 return $this->_responseFormat; 477 } 478 479 /** 480 * Set access token 481 * 482 * @param string $accessToken Access token 483 * 484 * @return object 485 * 486 * @access public 487 */ 488 function setAccessToken($accessToken) 489 { 490 $this->_accessToken = $accessToken; 491 492 return $this; 493 } 494 495 /** 496 * Set cURL options 497 * 498 * The method accepts arguments in two ways. 499 * 500 * You could pass two arguments when adding a single option. 501 * <code> 502 * $soundcloud->setCurlOptions(CURLOPT_SSL_VERIFYHOST, 0); 503 * </code> 504 * 505 * You could also pass an associative array when adding multiple options. 506 * <code> 507 * $soundcloud->setCurlOptions(array( 508 * CURLOPT_SSL_VERIFYHOST => 0, 509 * CURLOPT_SSL_VERIFYPEER => 0 510 * )); 511 * </code> 512 * 513 * @return object 514 * 515 * @access public 516 */ 517 function setCurlOptions() 518 { 519 $args = func_get_args(); 520 $options = (is_array($args[0])) 521 ? $args[0] 522 : array($args[0] => $args[1]); 523 524 foreach ($options as $key => $val) { 525 $this->_curlOptions[$key] = $val; 526 } 527 528 return $this; 529 } 530 531 /** 532 * Set redirect URI 533 * 534 * @param string $redirectUri Redirect URI 535 * 536 * @return object 537 * 538 * @access public 539 */ 540 function setRedirectUri($redirectUri) 541 { 542 $this->_redirectUri = $redirectUri; 543 544 return $this; 545 } 546 547 /** 548 * Set response format 549 * 550 * @param string $format Response format, could either be XML or JSON 551 * 552 * @return object 553 * @throws Services_Soundcloud_Unsupported_Response_Format_Exception 554 * 555 * @access public 556 */ 557 function setResponseFormat($format) 558 { 559 if (array_key_exists($format, self::$_responseFormats)) { 560 $this->_responseFormat = self::$_responseFormats[$format]; 561 } else { 562 throw new Services_Soundcloud_Unsupported_Response_Format_Exception(); 563 } 564 565 return $this; 566 } 567 568 /** 569 * Set development mode 570 * 571 * @param boolean $development Development mode 572 * 573 * @return object 574 * 575 * @access public 576 */ 577 function setDevelopment($development) 578 { 579 $this->_development = $development; 580 581 return $this; 582 } 583 584 /** 585 * Send a GET HTTP request 586 * 587 * @param string $path Request path 588 * @param array $params Optional query string parameters 589 * @param array $curlOptions Optional cURL options 590 * 591 * @return mixed 592 * 593 * @access public 594 * @see Soundcloud::_request() 595 */ 596 function get($path, $params = array(), $curlOptions = array()) 597 { 598 $url = $this->_buildUrl($path, $params); 599 600 return $this->_request($url, $curlOptions); 601 } 602 603 /** 604 * Send a POST HTTP request 605 * 606 * @param string $path Request path 607 * @param array $postData Optional post data 608 * @param array $curlOptions Optional cURL options 609 * 610 * @return mixed 611 * 612 * @access public 613 * @see Soundcloud::_request() 614 */ 615 function post($path, $postData = array(), $curlOptions = array()) 616 { 617 $url = $this->_buildUrl($path); 618 $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData); 619 $options += $curlOptions; 620 621 return $this->_request($url, $options); 622 } 623 624 /** 625 * Send a PUT HTTP request 626 * 627 * @param string $path Request path 628 * @param array $postData Optional post data 629 * @param array $curlOptions Optional cURL options 630 * 631 * @return mixed 632 * 633 * @access public 634 * @see Soundcloud::_request() 635 */ 636 function put($path, $postData, $curlOptions = array()) 637 { 638 $url = $this->_buildUrl($path); 639 $options = array( 640 CURLOPT_CUSTOMREQUEST => 'PUT', 641 CURLOPT_POSTFIELDS => $postData 642 ); 643 $options += $curlOptions; 644 645 return $this->_request($url, $options); 646 } 647 648 /** 649 * Send a DELETE HTTP request 650 * 651 * @param string $path Request path 652 * @param array $params Optional query string parameters 653 * @param array $curlOptions Optional cURL options 654 * 655 * @return mixed 656 * 657 * @access public 658 * @see Soundcloud::_request() 659 */ 660 function delete($path, $params = array(), $curlOptions = array()) 661 { 662 $url = $this->_buildUrl($path, $params); 663 $options = array(CURLOPT_CUSTOMREQUEST => 'DELETE'); 664 $options += $curlOptions; 665 666 return $this->_request($url, $options); 667 } 668 669 /** 670 * Download track 671 * 672 * @param integer $trackId Track id to download 673 * @param array $params Optional query string parameters 674 * @param array $curlOptions Optional cURL options 675 * 676 * @return mixed 677 * 678 * @access public 679 * @see Soundcloud::_request() 680 */ 681 function download($trackId, $params = array(), $curlOptions = array()) 682 { 683 $lastResponseFormat = array_pop(explode('/', $this->getResponseFormat())); 684 $defaultParams = array('oauth_token' => $this->getAccessToken()); 685 $defaultCurlOptions = array( 686 CURLOPT_FOLLOWLOCATION => true, 687 self::CURLOPT_OAUTH_TOKEN => false 688 ); 689 $url = $this->_buildUrl( 690 'tracks/' . $trackId . '/download', 691 array_merge($defaultParams, $params) 692 ); 693 $options = $defaultCurlOptions + $curlOptions; 694 695 $this->setResponseFormat('*'); 696 697 $response = $this->_request($url, $options); 698 699 // rollback to the previously defined response format. 700 $this->setResponseFormat($lastResponseFormat); 701 702 return $response; 703 } 704 705 /** 706 * Update a existing playlist 707 * 708 * @param integer $playlistId The playlist id 709 * @param array $trackIds Tracks to add to the playlist 710 * @param array $optionalPostData Optional playlist fields to update 711 * 712 * @return mixed 713 * 714 * @access public 715 * @see Soundcloud::_request() 716 */ 717 public function updatePlaylist($playlistId, $trackIds, $optionalPostData = null) 718 { 719 $url = $this->_buildUrl('playlists/' . $playlistId); 720 $postData = array(); 721 722 foreach ($trackIds as $trackId) { 723 array_push($postData, 'playlist[tracks][][id]=' . $trackId); 724 } 725 726 if (is_array($optionalPostData)) { 727 foreach ($optionalPostData as $key => $val) { 728 array_push($postData, 'playlist[' . $key . ']=' . $val); 729 } 730 } 731 732 $postData = implode('&', $postData); 733 $curlOptions = array( 734 CURLOPT_CUSTOMREQUEST => 'PUT', 735 CURLOPT_HTTPHEADER => array('Content-Length' => strlen($postData)), 736 CURLOPT_POSTFIELDS => $postData 737 ); 738 739 return $this->_request($url, $curlOptions); 740 } 741 742 /** 743 * Construct default HTTP request headers 744 * 745 * @param boolean $includeAccessToken Include access token 746 * 747 * @return array $headers 748 * 749 * @access protected 750 */ 751 protected function _buildDefaultHeaders($includeAccessToken = true) 752 { 753 $headers = array(); 754 755 if ($this->_responseFormat) { 756 array_push($headers, 'Accept: ' . $this->_responseFormat); 757 } 758 759 if ($includeAccessToken && $this->_accessToken) { 760 array_push($headers, 'Authorization: OAuth ' . $this->_accessToken); 761 } 762 763 return $headers; 764 } 765 766 /** 767 * Construct a URL 768 * 769 * @param string $path Relative or absolute URI 770 * @param array $params Optional query string parameters 771 * @param boolean $includeVersion Include API version 772 * 773 * @return string $url 774 * 775 * @access protected 776 */ 777 protected function _buildUrl($path, $params = array(), $includeVersion = true) 778 { 779 if (!$this->_accessToken) { 780 $params['consumer_key'] = $this->_clientId; 781 } 782 783 if (preg_match('/^https?\:\/\//', $path)) { 784 $url = $path; 785 } else { 786 $url = 'https://'; 787 $url .= (!preg_match('/connect/', $path)) ? 'api.' : ''; 788 $url .= ($this->_development) 789 ? self::$_domains['development'] 790 : self::$_domains['production']; 791 $url .= '/'; 792 $url .= ($includeVersion) ? 'v' . self::$_apiVersion . '/' : ''; 793 $url .= $path; 794 } 795 796 $url .= (count($params)) ? '?' . http_build_query($params) : ''; 797 798 return $url; 799 } 800 801 /** 802 * Retrieve access token 803 * 804 * @param array $postData Post data 805 * @param array $curlOptions Optional cURL options 806 * 807 * @return mixed 808 * 809 * @access protected 810 */ 811 protected function _getAccessToken($postData, $curlOptions = array()) 812 { 813 $options = array(CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData); 814 $options += $curlOptions; 815 $response = json_decode( 816 $this->_request($this->getAccessTokenUrl(), $options), 817 true 818 ); 819 820 if (array_key_exists('access_token', $response)) { 821 $this->_accessToken = $response['access_token']; 822 823 return $response; 824 } else { 825 return false; 826 } 827 } 828 829 /** 830 * Get HTTP user agent 831 * 832 * @return string 833 * 834 * @access protected 835 */ 836 protected function _getUserAgent() 837 { 838 return self::$_userAgent . '/' . new Services_Soundcloud_Version; 839 } 840 841 /** 842 * Parse HTTP headers 843 * 844 * @param string $headers HTTP headers 845 * 846 * @return array $parsedHeaders 847 * 848 * @access protected 849 */ 850 protected function _parseHttpHeaders($headers) 851 { 852 $headers = explode("\n", trim($headers)); 853 $parsedHeaders = array(); 854 855 foreach ($headers as $header) { 856 if (!preg_match('/\:\s/', $header)) { 857 continue; 858 } 859 860 list($key, $val) = explode(': ', $header, 2); 861 $key = str_replace('-', '_', strtolower($key)); 862 $val = trim($val); 863 864 $parsedHeaders[$key] = $val; 865 } 866 867 return $parsedHeaders; 868 } 869 870 /** 871 * Validate HTTP response code 872 * 873 * @param integer $code HTTP code 874 * 875 * @return boolean 876 * 877 * @access protected 878 */ 879 protected function _validResponseCode($code) 880 { 881 return (bool)preg_match('/^20[0-9]{1}$/', $code); 882 } 883 884 /** 885 * Performs the actual HTTP request using cURL 886 * 887 * @param string $url Absolute URL to request 888 * @param array $curlOptions Optional cURL options 889 * 890 * @return mixed 891 * @throws Services_Soundcloud_Invalid_Http_Response_Code_Exception 892 * 893 * @access protected 894 */ 895 protected function _request($url, $curlOptions = array()) 896 { 897 $ch = curl_init($url); 898 $options = $this->_curlOptions; 899 $options += $curlOptions; 900 901 if (array_key_exists(self::CURLOPT_OAUTH_TOKEN, $options)) { 902 $includeAccessToken = $options[self::CURLOPT_OAUTH_TOKEN]; 903 unset($options[self::CURLOPT_OAUTH_TOKEN]); 904 } else { 905 $includeAccessToken = true; 906 } 907 908 if (array_key_exists(CURLOPT_HTTPHEADER, $options)) { 909 $options[CURLOPT_HTTPHEADER] = array_merge( 910 $this->_buildDefaultHeaders(), 911 $curlOptions[CURLOPT_HTTPHEADER] 912 ); 913 } else { 914 $options[CURLOPT_HTTPHEADER] = $this->_buildDefaultHeaders( 915 $includeAccessToken 916 ); 917 } 918 919 curl_setopt_array($ch, $options); 920 921 $data = curl_exec($ch); 922 $info = curl_getinfo($ch); 923 924 curl_close($ch); 925 926 if (array_key_exists(CURLOPT_HEADER, $options) && $options[CURLOPT_HEADER]) { 927 $this->_lastHttpResponseHeaders = $this->_parseHttpHeaders( 928 substr($data, 0, $info['header_size']) 929 ); 930 $this->_lastHttpResponseBody = substr($data, $info['header_size']); 931 } else { 932 $this->_lastHttpResponseHeaders = array(); 933 $this->_lastHttpResponseBody = $data; 934 } 935 936 $this->_lastHttpResponseCode = $info['http_code']; 937 938 if ($this->_validResponseCode($this->_lastHttpResponseCode)) { 939 return $this->_lastHttpResponseBody; 940 } else { 941 throw new Services_Soundcloud_Invalid_Http_Response_Code_Exception( 942 null, 943 0, 944 $this->_lastHttpResponseBody, 945 $this->_lastHttpResponseCode 946 ); 947 } 948 } 949 950} 951