├── CHANGELOG.md ├── .gitattributes ├── .gitignore ├── .styleci.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Instagram.php └── InstagramException.php / CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | All notable changes to `cosenary/instagram` will be documented in this file. 4 | 5 | > Version 3.0 is in development and includes support for real-time subscriptions. 6 | 7 | **Instagram 2.2 - 04/10/2014** 8 | 9 | - `feature` Added "Enforce signed header" 10 | - `feature` Implemented PSR4 autoloading. 11 | - `update` Increased timeout from 5 to 20 seconds 12 | - `update` Class name, package renamed 13 | 14 | **Instagram 2.1 - 30/01/2014** 15 | 16 | - `update` added min and max_timestamp to `searchMedia()` 17 | - `update` public authentication for `getUserMedia()` method 18 | - `fix` support for inconsistent pagination return type (*relationship endpoint*) 19 | 20 | **Instagram 2.0 - 24/12/2013** 21 | 22 | - `release` version 2.0 23 | 24 | **Instagram 2.0 beta - 20/11/2013** 25 | 26 | - `feature` Added *Locations* endpoint 27 | - `update` Updated example project to display Instagram videos 28 | 29 | **Instagram 2.0 alpha 4 - 01/11/2013** 30 | 31 | - `feature` Comment endpoint implemented 32 | - `feature` New example with a fancy GUI 33 | - `update` Improved documentation 34 | 35 | **Instagram 2.0 alpha 3 - 04/09/2013** 36 | 37 | - `merge` Merged master branch updates 38 | - `update` Updated documentation 39 | - `bug` / `change` cURL CURLOPT_SSL_VERIFYPEER disabled (fixes #6, #7, #8, #16) 40 | - `feature` Added cURL error message 41 | - `feature` Added `limit` to `getTagMedia()` method 42 | 43 | **Instagram 2.0 alpha 2 - 14/06/2013** 44 | 45 | - `feature` Improved Pagination functionality 46 | - `change` Added `distance` parameter to `searchMedia()` method (thanks @jonathanwkelly) 47 | 48 | **Instagram 2.0 alpha 1 - 28/05/2012** 49 | 50 | - `feature` Added Pagination method 51 | - `feature` Added User Relationship endpoints 52 | - `feature` Added scope parameter table for the `getLoginUrl()` method 53 | 54 | **Instagram 1.5 - 31/01/2012** 55 | 56 | - `release` Second master version 57 | - `feature` Added Tag endpoints 58 | - `change` Edited the "Get started" example 59 | - `change` Now you can pass the `getOAuthToken()` object directly into `setAccessToken()` 60 | 61 | **Instagram 1.0 - 20/11/2011** 62 | 63 | - `release` First public release 64 | - `feature` Added sample App with documented code 65 | - `update` New detailed documentation 66 | 67 | **Instagram 0.8 - 16/11/2011** 68 | 69 | - `release` First inital released version 70 | - `feature` Initialize the class with a config array or string (see example) 71 | 72 | **Instagram 0.5 - 12/11/2011** 73 | 74 | - `release` Beta version 75 | - `update` Small documentation -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /example export-ignore 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING 2 | ============ 3 | 4 | Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests. 5 | 6 | ## Guidelines 7 | 8 | * Please follow the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md) and [PHP-FIG Naming Conventions](https://github.com/php-fig/fig-standards/blob/master/bylaws/002-psr-naming-conventions.md). 9 | * Remember that we follow [SemVer](http://semver.org). If you are changing the behaviour, or the public api, you may need to update the docs. 10 | * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash](http://git-scm.com/book/en/Git-Tools-Rewriting-History) them before submitting. 11 | * You may also need to [rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) to avoid merge conflicts. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2015, Christian Metz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | * Neither the name of the organisation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Image](example/assets/instagram.png) Instagram PHP API V2 2 | 3 | > **Note:** On the 17 Nov 2015 [Instagram](http://developers.instagram.com/post/133424514006/instagram-platform-update) made [changes to their API ](https://instagram.com/developer/changelog/). Apps created before Nov 17, 2015 wont be affected until Jun 2016. Apps created on or after Nov 17 2015 will require to use their updated API. Please note that this library doesn't yet support their new updates. For more information, please see [#182](https://github.com/cosenary/Instagram-PHP-API/issues/182). 4 | 5 | A PHP wrapper for the Instagram API. Feedback or bug reports are appreciated. 6 | 7 | [![Total Downloads](http://img.shields.io/packagist/dm/cosenary/instagram.svg?style=flat)](https://packagist.org/packages/cosenary/instagram) 8 | [![Latest Stable Version](http://img.shields.io/packagist/v/cosenary/instagram.svg?style=flat)](https://packagist.org/packages/cosenary/instagram) 9 | [![License](https://img.shields.io/packagist/l/cosenary/instagram.svg?style=flat)](https://packagist.org/packages/cosenary/instagram) 10 | 11 | > [Composer](#installation) package available. 12 | > Supports [Instagram Video](#instagram-videos) and [Signed Header](#signed-header). 13 | 14 | ## Requirements 15 | 16 | - PHP 5.3 or higher 17 | - cURL 18 | - Registered Instagram App 19 | 20 | ## Get started 21 | 22 | To use the Instagram API you have to register yourself as a developer at the [Instagram Developer Platform](http://instagr.am/developer/register/) and create an application. Take a look at the [uri guidelines](#samples-for-redirect-urls) before registering a redirect URI. You will receive your `client_id` and `client_secret`. 23 | 24 | --- 25 | 26 | Please note that Instagram mainly refers to »Clients« instead of »Apps«. So »Client ID« and »Client Secret« are the same as »App Key« and »App Secret«. 27 | 28 | --- 29 | 30 | > A good place to get started is the [example project](example/README.md). 31 | 32 | ### Installation 33 | 34 | I strongly advice using [Composer](https://getcomposer.org) to keep updates as smooth as possible. 35 | 36 | ``` 37 | $ composer require cosenary/instagram 38 | ``` 39 | 40 | ### Initialize the class 41 | 42 | ```php 43 | use MetzWeb\Instagram\Instagram; 44 | 45 | $instagram = new Instagram(array( 46 | 'apiKey' => 'YOUR_APP_KEY', 47 | 'apiSecret' => 'YOUR_APP_SECRET', 48 | 'apiCallback' => 'YOUR_APP_CALLBACK' 49 | )); 50 | 51 | echo "Login with Instagram"; 52 | ``` 53 | 54 | ### Authenticate user (OAuth2) 55 | 56 | ```php 57 | // grab OAuth callback code 58 | $code = $_GET['code']; 59 | $data = $instagram->getOAuthToken($code); 60 | 61 | echo 'Your username is: ' . $data->user->username; 62 | ``` 63 | 64 | ### Get user likes 65 | 66 | ```php 67 | // set user access token 68 | $instagram->setAccessToken($data); 69 | 70 | // get all user likes 71 | $likes = $instagram->getUserLikes(); 72 | 73 | // take a look at the API response 74 | echo '
';
 75 | print_r($likes);
 76 | echo '
';
 77 | ```
 78 | 
 79 | **All methods return the API data `json_decode()` - so you can directly access the data.**
 80 | 
 81 | ## Available methods
 82 | 
 83 | ### Setup Instagram
 84 | 
 85 | `new Instagram(/);`
 86 | 
 87 | `array` if you want to authenticate a user and access its data:
 88 | 
 89 | ```php
 90 | new Instagram(array(
 91 | 	'apiKey'      => 'YOUR_APP_KEY',
 92 | 	'apiSecret'   => 'YOUR_APP_SECRET',
 93 | 	'apiCallback' => 'YOUR_APP_CALLBACK'
 94 | ));
 95 | ```
 96 | 
 97 | `string` if you *only* want to access public data:
 98 | 
 99 | ```php
100 | new Instagram('YOUR_APP_KEY');
101 | ```
102 | 
103 | ### Get login URL
104 | 
105 | `getLoginUrl()`
106 | 
107 | ```php
108 | getLoginUrl(array(
109 | 	'basic',
110 | 	'likes'
111 | ));
112 | ```
113 | 
114 | **Optional scope parameters:**
115 | 
116 | 
117 | 	
118 | 		
119 | 		
120 | 		
121 | 	
122 | 	
123 | 		
124 | 		
125 | 		
126 | 	
127 | 	
128 | 		
129 | 		
130 | 		
131 | 	
132 | 	
133 | 		
134 | 		
135 | 		
136 | 	
137 | 	
138 | 		
139 | 		
140 | 		
141 | 	
142 | 
ScopeLegendMethods
basicto use all user related methods [default]getUser(), getUserFeed(), getUserFollower() etc.
relationshipsto follow and unfollow usersmodifyRelationship()
likesto like and unlike itemsgetMediaLikes(), likeMedia(), deleteLikedMedia()
commentsto create or delete commentsgetMediaComments(), addMediaComment(), deleteMediaComment()
143 | 144 | ### Get OAuth token 145 | 146 | `getOAuthToken($code, /)` 147 | 148 | `true` : Returns only the OAuth token 149 | `false` *[default]* : Returns OAuth token and profile data of the authenticated user 150 | 151 | ### Set / Get access token 152 | 153 | - Set the access token, for further method calls: `setAccessToken($token)` 154 | - Get the access token, if you want to store it for later usage: `getAccessToken()` 155 | 156 | ### User methods 157 | 158 | **Public methods** 159 | 160 | - `getUser($id)` 161 | - `searchUser($name, <$limit>)` 162 | - `getUserMedia($id, <$limit>)` 163 | 164 | **Authenticated methods** 165 | 166 | - `getUser()` 167 | - `getUserLikes(<$limit>)` 168 | - `getUserFeed(<$limit>)` 169 | - `getUserMedia(<$id>, <$limit>)` 170 | - if an `$id` isn't defined or equals `'self'`, it returns the media of the logged in user 171 | 172 | > [Sample responses of the User Endpoints.](http://instagram.com/developer/endpoints/users/) 173 | 174 | ### Relationship methods 175 | 176 | **Authenticated methods** 177 | 178 | - `getUserFollows($id, <$limit>)` 179 | - `getUserFollower($id, <$limit>)` 180 | - `getUserRelationship($id)` 181 | - `modifyRelationship($action, $user)` 182 | - `$action` : Action command (follow / unfollow / block / unblock / approve / deny) 183 | - `$user` : Target user id 184 | 185 | ```php 186 | // Follow the user with the ID 1574083 187 | $instagram->modifyRelationship('follow', 1574083); 188 | ``` 189 | 190 | --- 191 | 192 | Please note that the `modifyRelationship()` method requires the `relationships` [scope](#get-login-url). 193 | 194 | --- 195 | 196 | > [Sample responses of the Relationship Endpoints.](http://instagram.com/developer/endpoints/relationships/) 197 | 198 | ### Media methods 199 | 200 | **Public methods** 201 | 202 | - `getMedia($id)` 203 | - authenticated users receive the info, whether the queried media is liked 204 | - `getPopularMedia()` 205 | - `searchMedia($lat, $lng, <$distance>, <$minTimestamp>, <$maxTimestamp>)` 206 | - `$lat` and `$lng` are coordinates and have to be floats like: `48.145441892290336`,`11.568603515625` 207 | - `$distance` : Radial distance in meter (default is 1km = 1000, max. is 5km = 5000) 208 | - `$minTimestamp` : All media returned will be taken *later* than this timestamp (default: 5 days ago) 209 | - `$maxTimestamp` : All media returned will be taken *earlier* than this timestamp (default: now) 210 | 211 | > [Sample responses of the Media Endpoints.](http://instagram.com/developer/endpoints/media/) 212 | 213 | ### Comment methods 214 | 215 | **Public methods** 216 | 217 | - `getMediaComments($id)` 218 | 219 | **Authenticated methods** 220 | 221 | - `addMediaComment($id, $text)` 222 | - **restricted access:** please email `apidevelopers[at]instagram.com` for access 223 | - `deleteMediaComment($id, $commentID)` 224 | - the comment must be authored by the authenticated user 225 | 226 | --- 227 | 228 | Please note that the authenticated methods require the `comments` [scope](#get-login-url). 229 | 230 | --- 231 | 232 | > [Sample responses of the Comment Endpoints.](http://instagram.com/developer/endpoints/comments/) 233 | 234 | ### Tag methods 235 | 236 | **Public methods** 237 | 238 | - `getTag($name)` 239 | - `getTagMedia($name)` 240 | - `searchTags($name)` 241 | 242 | > [Sample responses of the Tag Endpoints.](http://instagram.com/developer/endpoints/tags/) 243 | 244 | ### Likes methods 245 | 246 | **Authenticated methods** 247 | 248 | - `getMediaLikes($id)` 249 | - `likeMedia($id)` 250 | - `deleteLikedMedia($id)` 251 | 252 | > How to like a Media: [Example usage](https://gist.github.com/3287237) 253 | > [Sample responses of the Likes Endpoints.](http://instagram.com/developer/endpoints/likes/) 254 | 255 | All `<...>` parameters are optional. If the limit is undefined, all available results will be returned. 256 | 257 | ## Instagram videos 258 | 259 | Instagram entries are marked with a `type` attribute (`image` or `video`), that allows you to identify videos. 260 | 261 | An example of how to embed Instagram videos by using [Video.js](http://www.videojs.com), can be found in the `/example` folder. 262 | 263 | --- 264 | 265 | **Please note:** Instagram currently doesn't allow to filter videos. 266 | 267 | --- 268 | 269 | ## Signed Header 270 | 271 | In order to prevent that your access tokens gets stolen, Instagram recommends to sign your requests with a hash of your API secret, the called endpoint and parameters. 272 | 273 | 1. Activate ["Enforce Signed Header"](http://instagram.com/developer/clients/manage/) in your Instagram client settings. 274 | 2. Enable the signed-header in your Instagram class: 275 | 276 | ```php 277 | $instagram->setSignedHeader(true); 278 | ``` 279 | 280 | 3. You are good to go! Now, all your requests will be secured with a signed header. 281 | 282 | Go into more detail about how it works in the [Instagram API Docs](http://instagram.com/developer/restrict-api-requests/#enforce-signed-header). 283 | 284 | ## Pagination 285 | 286 | Each endpoint has a maximum range of results, so increasing the `limit` parameter above the limit won't help (e.g. `getUserMedia()` has a limit of 90). 287 | 288 | That's the point where the "pagination" feature comes into play. 289 | Simply pass an object into the `pagination()` method and receive your next dataset: 290 | 291 | ```php 292 | $photos = $instagram->getTagMedia('kitten'); 293 | 294 | $result = $instagram->pagination($photos); 295 | ``` 296 | 297 | Iteration with `do-while` loop. 298 | 299 | ## Samples for redirect URLs 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 |
Registered Redirect URIRedirect URI sent to /authorizeValid?
http://yourcallback.com/http://yourcallback.com/yes
http://yourcallback.com/http://yourcallback.com/?this=thatyes
http://yourcallback.com/?this=thathttp://yourcallback.com/no
http://yourcallback.com/?this=thathttp://yourcallback.com/?this=that&another=trueyes
http://yourcallback.com/?this=thathttp://yourcallback.com/?another=true&this=thatno
http://yourcallback.com/callbackhttp://yourcallback.com/no
http://yourcallback.com/callbackhttp://yourcallback.com/callback/?type=mobileyes
343 | 344 | > If you need further information about an endpoint, take a look at the [Instagram API docs](http://instagram.com/developer/authentication/). 345 | 346 | ## Example App 347 | 348 | ![Image](http://cl.ly/image/221T1g3w3u2J/preview.png) 349 | 350 | This example project, located in the `example/` folder, helps you to get started. 351 | The code is well documented and takes you through all required steps of the OAuth2 process. 352 | Credit for the awesome Instagram icons goes to [Ricardo de Zoete Pro](http://dribbble.com/RZDESIGN). 353 | 354 | #### More examples and tutorials: 355 | 356 | - [User likes](https://gist.github.com/cosenary/3287237) 357 | - [Follow user](https://gist.github.com/cosenary/8322459) 358 | - [User follower](https://gist.github.com/cosenary/7267139) 359 | - [Load more button](https://gist.github.com/cosenary/2975779) 360 | - [User most recent media](https://gist.github.com/cosenary/1711218) 361 | - [Instagram login](https://gist.github.com/cosenary/8803601) 362 | - [Instagram signup (9lessons tutorial)](http://www.9lessons.info/2012/05/login-with-instagram-php.html) 363 | - [Laravel Wrapper](https://github.com/vinkla/instagram) 364 | 365 | > Let me know if you have to share a code example, too. 366 | 367 | ## Changelog 368 | 369 | Please see the [changelog file](CHANGELOG.md) for more information. 370 | 371 | ## Credits 372 | 373 | Copyright (c) 2011-2015 - Programmed by Christian Metz 374 | 375 | Released under the [BSD License](LICENSE). 376 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosenary/instagram", 3 | "type": "library", 4 | "description": "An easy-to-use PHP Class for accessing Instagram's API.", 5 | "keywords": ["instagram", "api"], 6 | "homepage": "https://github.com/cosenary/Instagram-PHP-API", 7 | "license": "BSD", 8 | "authors": [ 9 | { 10 | "name": "Christian Metz", 11 | "email": "christian-metz1@gmx.net", 12 | "homepage": "http://metzweb.net" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.3.0", 17 | "ext-curl": "*" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "MetzWeb\\Instagram\\": "src" 22 | } 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "2.4-dev" 27 | } 28 | }, 29 | "minimum-stability": "dev", 30 | "prefer-stable": true 31 | } 32 | -------------------------------------------------------------------------------- /src/Instagram.php: -------------------------------------------------------------------------------- 1 | setApiKey($config['apiKey']); 104 | $this->setApiSecret($config['apiSecret']); 105 | $this->setApiCallback($config['apiCallback']); 106 | } elseif (is_string($config)) { 107 | // if you only want to access public data 108 | $this->setApiKey($config); 109 | } else { 110 | throw new InstagramException('Error: __construct() - Configuration data is missing.'); 111 | } 112 | } 113 | 114 | /** 115 | * Generates the OAuth login URL. 116 | * 117 | * @param string[] $scopes Requesting additional permissions 118 | * 119 | * @return string Instagram OAuth login URL 120 | * 121 | * @throws \MetzWeb\Instagram\InstagramException 122 | */ 123 | public function getLoginUrl($scopes = array('basic')) 124 | { 125 | if (is_array($scopes) && count(array_intersect($scopes, $this->_scopes)) === count($scopes)) { 126 | return self::API_OAUTH_URL . '?client_id=' . $this->getApiKey() . '&redirect_uri=' . urlencode($this->getApiCallback()) . '&scope=' . implode('+', 127 | $scopes) . '&response_type=code'; 128 | } 129 | 130 | throw new InstagramException("Error: getLoginUrl() - The parameter isn't an array or invalid scope permissions used."); 131 | } 132 | 133 | /** 134 | * Search for a user. 135 | * 136 | * @param string $name Instagram username 137 | * @param int $limit Limit of returned results 138 | * 139 | * @return mixed 140 | */ 141 | public function searchUser($name, $limit = 0) 142 | { 143 | $params = array(); 144 | 145 | $params['q'] = $name; 146 | if ($limit > 0) { 147 | $params['count'] = $limit; 148 | } 149 | 150 | return $this->_makeCall('users/search', false, $params); 151 | } 152 | 153 | /** 154 | * Get user info. 155 | * 156 | * @param int $id Instagram user ID 157 | * 158 | * @return mixed 159 | */ 160 | public function getUser($id = 0) 161 | { 162 | $auth = false; 163 | 164 | if ($id === 0 && isset($this->_accesstoken)) { 165 | $id = 'self'; 166 | $auth = true; 167 | } 168 | 169 | return $this->_makeCall('users/' . $id, $auth); 170 | } 171 | 172 | /** 173 | * Get user activity feed. 174 | * 175 | * @param int $limit Limit of returned results 176 | * 177 | * @return mixed 178 | */ 179 | public function getUserFeed($limit = 0) 180 | { 181 | $params = array(); 182 | if ($limit > 0) { 183 | $params['count'] = $limit; 184 | } 185 | 186 | return $this->_makeCall('users/self/feed', true, $params); 187 | } 188 | 189 | /** 190 | * Get user recent media. 191 | * 192 | * @param int|string $id Instagram user ID 193 | * @param int $limit Limit of returned results 194 | * 195 | * @return mixed 196 | */ 197 | public function getUserMedia($id = 'self', $limit = 0) 198 | { 199 | $params = array(); 200 | 201 | if ($limit > 0) { 202 | $params['count'] = $limit; 203 | } 204 | 205 | return $this->_makeCall('users/' . $id . '/media/recent', strlen($this->getAccessToken()), $params); 206 | } 207 | 208 | /** 209 | * Get the liked photos of a user. 210 | * 211 | * @param int $limit Limit of returned results 212 | * 213 | * @return mixed 214 | */ 215 | public function getUserLikes($limit = 0) 216 | { 217 | $params = array(); 218 | 219 | if ($limit > 0) { 220 | $params['count'] = $limit; 221 | } 222 | 223 | return $this->_makeCall('users/self/media/liked', true, $params); 224 | } 225 | 226 | /** 227 | * Get the list of users this user follows 228 | * 229 | * @param int|string $id Instagram user ID. 230 | * @param int $limit Limit of returned results 231 | * 232 | * @return mixed 233 | */ 234 | public function getUserFollows($id = 'self', $limit = 0) 235 | { 236 | $params = array(); 237 | 238 | if ($limit > 0) { 239 | $params['count'] = $limit; 240 | } 241 | 242 | return $this->_makeCall('users/' . $id . '/follows', true, $params); 243 | } 244 | 245 | /** 246 | * Get the list of users this user is followed by. 247 | * 248 | * @param int|string $id Instagram user ID 249 | * @param int $limit Limit of returned results 250 | * 251 | * @return mixed 252 | */ 253 | public function getUserFollower($id = 'self', $limit = 0) 254 | { 255 | $params = array(); 256 | 257 | if ($limit > 0) { 258 | $params['count'] = $limit; 259 | } 260 | 261 | return $this->_makeCall('users/' . $id . '/followed-by', true, $params); 262 | } 263 | 264 | /** 265 | * Get information about a relationship to another user. 266 | * 267 | * @param int $id Instagram user ID 268 | * 269 | * @return mixed 270 | */ 271 | public function getUserRelationship($id) 272 | { 273 | return $this->_makeCall('users/' . $id . '/relationship', true); 274 | } 275 | 276 | /** 277 | * Get the value of X-RateLimit-Remaining header field. 278 | * 279 | * @return int X-RateLimit-Remaining API calls left within 1 hour 280 | */ 281 | public function getRateLimit() 282 | { 283 | return $this->_xRateLimitRemaining; 284 | } 285 | 286 | /** 287 | * Modify the relationship between the current user and the target user. 288 | * 289 | * @param string $action Action command (follow/unfollow/block/unblock/approve/deny) 290 | * @param int $user Target user ID 291 | * 292 | * @return mixed 293 | * 294 | * @throws \MetzWeb\Instagram\InstagramException 295 | */ 296 | public function modifyRelationship($action, $user) 297 | { 298 | if (in_array($action, $this->_actions) && isset($user)) { 299 | return $this->_makeCall('users/' . $user . '/relationship', true, array('action' => $action), 'POST'); 300 | } 301 | 302 | throw new InstagramException('Error: modifyRelationship() | This method requires an action command and the target user id.'); 303 | } 304 | 305 | /** 306 | * Search media by its location. 307 | * 308 | * @param float $lat Latitude of the center search coordinate 309 | * @param float $lng Longitude of the center search coordinate 310 | * @param int $distance Distance in metres (default is 1km (distance=1000), max. is 5km) 311 | * @param long $minTimestamp Media taken later than this timestamp (default: 5 days ago) 312 | * @param long $maxTimestamp Media taken earlier than this timestamp (default: now) 313 | * 314 | * @return mixed 315 | */ 316 | public function searchMedia($lat, $lng, $distance = 1000, $minTimestamp = null, $maxTimestamp = null) 317 | { 318 | return $this->_makeCall('media/search', false, array( 319 | 'lat' => $lat, 320 | 'lng' => $lng, 321 | 'distance' => $distance, 322 | 'min_timestamp' => $minTimestamp, 323 | 'max_timestamp' => $maxTimestamp 324 | )); 325 | } 326 | 327 | /** 328 | * Get media by its id. 329 | * 330 | * @param int $id Instagram media ID 331 | * 332 | * @return mixed 333 | */ 334 | public function getMedia($id) 335 | { 336 | return $this->_makeCall('media/' . $id, isset($this->_accesstoken)); 337 | } 338 | 339 | /** 340 | * Get the most popular media. 341 | * 342 | * @return mixed 343 | */ 344 | public function getPopularMedia() 345 | { 346 | return $this->_makeCall('media/popular'); 347 | } 348 | 349 | /** 350 | * Search for tags by name. 351 | * 352 | * @param string $name Valid tag name 353 | * 354 | * @return mixed 355 | */ 356 | public function searchTags($name) 357 | { 358 | return $this->_makeCall('tags/search', false, array('q' => $name)); 359 | } 360 | 361 | /** 362 | * Get info about a tag 363 | * 364 | * @param string $name Valid tag name 365 | * 366 | * @return mixed 367 | */ 368 | public function getTag($name) 369 | { 370 | return $this->_makeCall('tags/' . $name); 371 | } 372 | 373 | /** 374 | * Get a recently tagged media. 375 | * 376 | * @param string $name Valid tag name 377 | * @param int $limit Limit of returned results 378 | * 379 | * @return mixed 380 | */ 381 | public function getTagMedia($name, $limit = 0) 382 | { 383 | $params = array(); 384 | 385 | if ($limit > 0) { 386 | $params['count'] = $limit; 387 | } 388 | 389 | return $this->_makeCall('tags/' . $name . '/media/recent', false, $params); 390 | } 391 | 392 | /** 393 | * Get a list of users who have liked this media. 394 | * 395 | * @param int $id Instagram media ID 396 | * 397 | * @return mixed 398 | */ 399 | public function getMediaLikes($id) 400 | { 401 | return $this->_makeCall('media/' . $id . '/likes', true); 402 | } 403 | 404 | /** 405 | * Get a list of comments for this media. 406 | * 407 | * @param int $id Instagram media ID 408 | * 409 | * @return mixed 410 | */ 411 | public function getMediaComments($id) 412 | { 413 | return $this->_makeCall('media/' . $id . '/comments', false); 414 | } 415 | 416 | /** 417 | * Add a comment on a media. 418 | * 419 | * @param int $id Instagram media ID 420 | * @param string $text Comment content 421 | * 422 | * @return mixed 423 | */ 424 | public function addMediaComment($id, $text) 425 | { 426 | return $this->_makeCall('media/' . $id . '/comments', true, array('text' => $text), 'POST'); 427 | } 428 | 429 | /** 430 | * Remove user comment on a media. 431 | * 432 | * @param int $id Instagram media ID 433 | * @param string $commentID User comment ID 434 | * 435 | * @return mixed 436 | */ 437 | public function deleteMediaComment($id, $commentID) 438 | { 439 | return $this->_makeCall('media/' . $id . '/comments/' . $commentID, true, null, 'DELETE'); 440 | } 441 | 442 | /** 443 | * Set user like on a media. 444 | * 445 | * @param int $id Instagram media ID 446 | * 447 | * @return mixed 448 | */ 449 | public function likeMedia($id) 450 | { 451 | return $this->_makeCall('media/' . $id . '/likes', true, null, 'POST'); 452 | } 453 | 454 | /** 455 | * Remove user like on a media. 456 | * 457 | * @param int $id Instagram media ID 458 | * 459 | * @return mixed 460 | */ 461 | public function deleteLikedMedia($id) 462 | { 463 | return $this->_makeCall('media/' . $id . '/likes', true, null, 'DELETE'); 464 | } 465 | 466 | /** 467 | * Get information about a location. 468 | * 469 | * @param int $id Instagram location ID 470 | * 471 | * @return mixed 472 | */ 473 | public function getLocation($id) 474 | { 475 | return $this->_makeCall('locations/' . $id, false); 476 | } 477 | 478 | /** 479 | * Get recent media from a given location. 480 | * 481 | * @param int $id Instagram location ID 482 | * 483 | * @return mixed 484 | */ 485 | public function getLocationMedia($id) 486 | { 487 | return $this->_makeCall('locations/' . $id . '/media/recent', false); 488 | } 489 | 490 | /** 491 | * Get recent media from a given location. 492 | * 493 | * @param float $lat Latitude of the center search coordinate 494 | * @param float $lng Longitude of the center search coordinate 495 | * @param int $distance Distance in meter (max. distance: 5km = 5000) 496 | * 497 | * @return mixed 498 | */ 499 | public function searchLocation($lat, $lng, $distance = 1000) 500 | { 501 | return $this->_makeCall('locations/search', false, array('lat' => $lat, 'lng' => $lng, 'distance' => $distance)); 502 | } 503 | 504 | /** 505 | * Pagination feature. 506 | * 507 | * @param object $obj Instagram object returned by a method 508 | * @param int $limit Limit of returned results 509 | * 510 | * @return mixed 511 | * 512 | * @throws \MetzWeb\Instagram\InstagramException 513 | */ 514 | public function pagination($obj, $limit = 0) 515 | { 516 | if (is_object($obj) && !is_null($obj->pagination)) { 517 | if (!isset($obj->pagination->next_url)) { 518 | return; 519 | } 520 | 521 | $apiCall = explode('?', $obj->pagination->next_url); 522 | 523 | if (count($apiCall) < 2) { 524 | return; 525 | } 526 | 527 | $function = str_replace(self::API_URL, '', $apiCall[0]); 528 | 529 | $auth = (strpos($apiCall[1], 'access_token') !== false); 530 | 531 | if (isset($obj->pagination->next_max_id)) { 532 | return $this->_makeCall($function, $auth, array('max_id' => $obj->pagination->next_max_id, 'count' => $limit)); 533 | } 534 | 535 | return $this->_makeCall($function, $auth, array('cursor' => $obj->pagination->next_cursor, 'count' => $limit)); 536 | } 537 | 538 | throw new InstagramException("Error: pagination() | This method doesn't support pagination."); 539 | } 540 | 541 | /** 542 | * Get the OAuth data of a user by the returned callback code. 543 | * 544 | * @param string $code OAuth2 code variable (after a successful login) 545 | * @param bool $token If it's true, only the access token will be returned 546 | * 547 | * @return mixed 548 | */ 549 | public function getOAuthToken($code, $token = false) 550 | { 551 | $apiData = array( 552 | 'grant_type' => 'authorization_code', 553 | 'client_id' => $this->getApiKey(), 554 | 'client_secret' => $this->getApiSecret(), 555 | 'redirect_uri' => $this->getApiCallback(), 556 | 'code' => $code 557 | ); 558 | 559 | $result = $this->_makeOAuthCall($apiData); 560 | 561 | return !$token ? $result : $result->access_token; 562 | } 563 | 564 | /** 565 | * The call operator. 566 | * 567 | * @param string $function API resource path 568 | * @param bool $auth Whether the function requires an access token 569 | * @param array $params Additional request parameters 570 | * @param string $method Request type GET|POST 571 | * 572 | * @return mixed 573 | * 574 | * @throws \MetzWeb\Instagram\InstagramException 575 | */ 576 | protected function _makeCall($function, $auth = false, $params = null, $method = 'GET') 577 | { 578 | if (!$auth) { 579 | // if the call doesn't requires authentication 580 | $authMethod = '?client_id=' . $this->getApiKey(); 581 | } else { 582 | // if the call needs an authenticated user 583 | if (!isset($this->_accesstoken)) { 584 | throw new InstagramException("Error: _makeCall() | $function - This method requires an authenticated users access token."); 585 | } 586 | 587 | $authMethod = '?access_token=' . $this->getAccessToken(); 588 | } 589 | 590 | $paramString = null; 591 | 592 | if (isset($params) && is_array($params)) { 593 | $paramString = '&' . http_build_query($params); 594 | } 595 | 596 | $apiCall = self::API_URL . $function . $authMethod . (('GET' === $method) ? $paramString : null); 597 | 598 | // we want JSON 599 | $headerData = array('Accept: application/json'); 600 | 601 | if ($this->_signedheader) { 602 | $apiCall .= (strstr($apiCall, '?') ? '&' : '?') . 'sig=' . $this->_signHeader($function, $authMethod, $params); 603 | } 604 | 605 | $ch = curl_init(); 606 | curl_setopt($ch, CURLOPT_URL, $apiCall); 607 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData); 608 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); 609 | curl_setopt($ch, CURLOPT_TIMEOUT, 90); 610 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 611 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 612 | curl_setopt($ch, CURLOPT_HEADER, true); 613 | 614 | switch ($method) { 615 | case 'POST': 616 | curl_setopt($ch, CURLOPT_POST, count($params)); 617 | curl_setopt($ch, CURLOPT_POSTFIELDS, ltrim($paramString, '&')); 618 | break; 619 | case 'DELETE': 620 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 621 | break; 622 | } 623 | 624 | $jsonData = curl_exec($ch); 625 | // split header from JSON data 626 | // and assign each to a variable 627 | list($headerContent, $jsonData) = explode("\r\n\r\n", $jsonData, 2); 628 | 629 | // convert header content into an array 630 | $headers = $this->processHeaders($headerContent); 631 | 632 | // get the 'X-Ratelimit-Remaining' header value 633 | $this->_xRateLimitRemaining = $headers['X-Ratelimit-Remaining']; 634 | 635 | if (!$jsonData) { 636 | throw new InstagramException('Error: _makeCall() - cURL error: ' . curl_error($ch)); 637 | } 638 | 639 | curl_close($ch); 640 | 641 | return json_decode($jsonData); 642 | } 643 | 644 | /** 645 | * The OAuth call operator. 646 | * 647 | * @param array $apiData The post API data 648 | * 649 | * @return mixed 650 | * 651 | * @throws \MetzWeb\Instagram\InstagramException 652 | */ 653 | private function _makeOAuthCall($apiData) 654 | { 655 | $apiHost = self::API_OAUTH_TOKEN_URL; 656 | 657 | $ch = curl_init(); 658 | curl_setopt($ch, CURLOPT_URL, $apiHost); 659 | curl_setopt($ch, CURLOPT_POST, count($apiData)); 660 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($apiData)); 661 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json')); 662 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 663 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 664 | curl_setopt($ch, CURLOPT_TIMEOUT, 90); 665 | $jsonData = curl_exec($ch); 666 | 667 | if (!$jsonData) { 668 | throw new InstagramException('Error: _makeOAuthCall() - cURL error: ' . curl_error($ch)); 669 | } 670 | 671 | curl_close($ch); 672 | 673 | return json_decode($jsonData); 674 | } 675 | 676 | /** 677 | * Sign header by using endpoint, parameters and the API secret. 678 | * 679 | * @param string 680 | * @param string 681 | * @param array 682 | * 683 | * @return string The signature 684 | */ 685 | private function _signHeader($endpoint, $authMethod, $params) 686 | { 687 | if (!is_array($params)) { 688 | $params = array(); 689 | } 690 | if ($authMethod) { 691 | list($key, $value) = explode('=', substr($authMethod, 1), 2); 692 | $params[$key] = $value; 693 | } 694 | $baseString = '/' . $endpoint; 695 | ksort($params); 696 | foreach ($params as $key => $value) { 697 | $baseString .= '|' . $key . '=' . $value; 698 | } 699 | $signature = hash_hmac('sha256', $baseString, $this->_apisecret, false); 700 | 701 | return $signature; 702 | } 703 | 704 | /** 705 | * Read and process response header content. 706 | * 707 | * @param array 708 | * 709 | * @return array 710 | */ 711 | private function processHeaders($headerContent) 712 | { 713 | $headers = array(); 714 | 715 | foreach (explode("\r\n", $headerContent) as $i => $line) { 716 | if ($i === 0) { 717 | $headers['http_code'] = $line; 718 | continue; 719 | } 720 | 721 | list($key, $value) = explode(':', $line); 722 | $headers[$key] = $value; 723 | } 724 | 725 | return $headers; 726 | } 727 | 728 | /** 729 | * Access Token Setter. 730 | * 731 | * @param object|string $data 732 | * 733 | * @return void 734 | */ 735 | public function setAccessToken($data) 736 | { 737 | $token = is_object($data) ? $data->access_token : $data; 738 | 739 | $this->_accesstoken = $token; 740 | } 741 | 742 | /** 743 | * Access Token Getter. 744 | * 745 | * @return string 746 | */ 747 | public function getAccessToken() 748 | { 749 | return $this->_accesstoken; 750 | } 751 | 752 | /** 753 | * API-key Setter 754 | * 755 | * @param string $apiKey 756 | * 757 | * @return void 758 | */ 759 | public function setApiKey($apiKey) 760 | { 761 | $this->_apikey = $apiKey; 762 | } 763 | 764 | /** 765 | * API Key Getter 766 | * 767 | * @return string 768 | */ 769 | public function getApiKey() 770 | { 771 | return $this->_apikey; 772 | } 773 | 774 | /** 775 | * API Secret Setter 776 | * 777 | * @param string $apiSecret 778 | * 779 | * @return void 780 | */ 781 | public function setApiSecret($apiSecret) 782 | { 783 | $this->_apisecret = $apiSecret; 784 | } 785 | 786 | /** 787 | * API Secret Getter. 788 | * 789 | * @return string 790 | */ 791 | public function getApiSecret() 792 | { 793 | return $this->_apisecret; 794 | } 795 | 796 | /** 797 | * API Callback URL Setter. 798 | * 799 | * @param string $apiCallback 800 | * 801 | * @return void 802 | */ 803 | public function setApiCallback($apiCallback) 804 | { 805 | $this->_callbackurl = $apiCallback; 806 | } 807 | 808 | /** 809 | * API Callback URL Getter. 810 | * 811 | * @return string 812 | */ 813 | public function getApiCallback() 814 | { 815 | return $this->_callbackurl; 816 | } 817 | 818 | /** 819 | * Enforce Signed Header. 820 | * 821 | * @param bool $signedHeader 822 | * 823 | * @return void 824 | */ 825 | public function setSignedHeader($signedHeader) 826 | { 827 | $this->_signedheader = $signedHeader; 828 | } 829 | } 830 | -------------------------------------------------------------------------------- /src/InstagramException.php: -------------------------------------------------------------------------------- 1 |