├── 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']); 132 | $this->setApiSecret($config['apiSecret']); 133 | $this->setApiCallback($config['apiCallback']); 134 | } elseif (is_string($config)) { 135 | // if you only want to access public data 136 | $this->setApiKey($config); 137 | } else { 138 | throw new InstagramException('Error: __construct() - Configuration data is missing.'); 139 | } 140 | } 141 | 142 | /** 143 | * Generates the OAuth login URL. 144 | * 145 | * @param string[] $scopes Requesting additional permissions 146 | * 147 | * @return string Instagram OAuth login URL 148 | * 149 | * @throws \MetzWeb\Instagram\InstagramException 150 | */ 151 | public function getLoginUrl($scopes = array('basic')) 152 | { 153 | if (is_array($scopes) && count(array_intersect($scopes, $this->_scopes)) === count($scopes)) { 154 | return self::API_OAUTH_URL . '?client_id=' . $this->getApiKey() . '&redirect_uri=' . urlencode($this->getApiCallback()) . '&scope=' . implode('+', 155 | $scopes) . '&response_type=code'; 156 | } 157 | 158 | throw new InstagramException("Error: getLoginUrl() - The parameter isn't an array or invalid scope permissions used."); 159 | } 160 | 161 | /** 162 | * Search for a user. 163 | * 164 | * @param string $name Instagram username 165 | * @param int $limit Limit of returned results 166 | * 167 | * @return mixed 168 | */ 169 | public function searchUser($name, $limit = 0) 170 | { 171 | $params = array(); 172 | 173 | $params['q'] = $name; 174 | if ($limit > 0) { 175 | $params['count'] = $limit; 176 | } 177 | 178 | return $this->_makeCall('users/search', $params); 179 | } 180 | 181 | /** 182 | * Get user info. 183 | * 184 | * @param int $id Instagram user ID 185 | * 186 | * @return mixed 187 | */ 188 | public function getUser($id = 0) 189 | { 190 | if ($id === 0) { 191 | $id = 'self'; 192 | } 193 | 194 | return $this->_makeCall('users/' . $id); 195 | } 196 | 197 | /** 198 | * Get user recent media. 199 | * 200 | * @param int|string $id Instagram user ID 201 | * @param int $limit Limit of returned results 202 | * @param int $min_id Return media later than this min_id 203 | * @param int $max_id Return media earlier than this max_id 204 | * 205 | * @return mixed 206 | */ 207 | public function getUserMedia($id = 'self', $limit = 0, $min_id = null, $max_id = null) 208 | { 209 | $params = array(); 210 | 211 | if ($limit > 0) { 212 | $params['count'] = $limit; 213 | } 214 | if (isset($min_id)) { 215 | $params['min_id'] = $min_id; 216 | } 217 | if (isset($max_id)) { 218 | $params['max_id'] = $max_id; 219 | } 220 | 221 | return $this->_makeCall('users/' . $id . '/media/recent', $params); 222 | } 223 | 224 | /** 225 | * Get the liked photos of a user. 226 | * 227 | * @param int $limit Limit of returned results 228 | * @param int $max_like_id Return media liked before this id 229 | * 230 | * @return mixed 231 | */ 232 | public function getUserLikes($limit = 0, $max_like_id = null) 233 | { 234 | $params = array(); 235 | 236 | if ($limit > 0) { 237 | $params['count'] = $limit; 238 | } 239 | if (isset($max_id)) { 240 | $params['max_like_id'] = $max_like_id; 241 | } 242 | 243 | return $this->_makeCall('users/self/media/liked', $params); 244 | } 245 | 246 | /** 247 | * DEPRECATED 248 | * Get the list of users this user follows 249 | * 250 | * @param int|string $id Instagram user ID. 251 | * @param int $limit Limit of returned results 252 | * 253 | * @return void 254 | */ 255 | public function getUserFollows($id = 'self', $limit = 0) 256 | { 257 | return $this->getFollows($id, $limit); 258 | } 259 | 260 | /** 261 | * Get the list of users the authenticated user follows. 262 | * 263 | * @return mixed 264 | */ 265 | public function getFollows($id = 'self', $limit = 0) 266 | { 267 | $params = array(); 268 | 269 | if ($limit > 0) { 270 | $params['count'] = $limit; 271 | } 272 | 273 | return $this->_makeCall('users/' . $id . '/follows', $params); 274 | } 275 | 276 | /** 277 | * DEPRECATED 278 | * Get the list of users this user is followed by. 279 | * 280 | * @param int|string $id Instagram user ID 281 | * @param int $limit Limit of returned results 282 | * 283 | * @return void 284 | */ 285 | public function getUserFollower($id = 'self', $limit = 0) 286 | { 287 | return $this->getFollower($id, $limit); 288 | } 289 | 290 | /** 291 | * Get the list of users this user is followed by. 292 | * 293 | * @return mixed 294 | */ 295 | public function getFollower($id = 'self', $limit = 0) 296 | { 297 | $params = array(); 298 | 299 | if ($limit > 0) { 300 | $params['count'] = $limit; 301 | } 302 | 303 | return $this->_makeCall('users/' . $id . '/followed-by', $params); 304 | } 305 | 306 | /** 307 | * Get information about a relationship to another user. 308 | * 309 | * @param int $id Instagram user ID 310 | * 311 | * @return mixed 312 | */ 313 | public function getUserRelationship($id) 314 | { 315 | return $this->_makeCall('users/' . $id . '/relationship'); 316 | } 317 | 318 | /** 319 | * Get the value of X-RateLimit-Remaining header field. 320 | * 321 | * @return int X-RateLimit-Remaining API calls left within 1 hour 322 | */ 323 | public function getRateLimit() 324 | { 325 | return $this->_xRateLimitRemaining; 326 | } 327 | 328 | /** 329 | * Modify the relationship between the current user and the target user. 330 | * 331 | * @param string $action Action command (follow/unfollow/approve/ignore) 332 | * @param int $user Target user ID 333 | * 334 | * @return mixed 335 | * 336 | * @throws \MetzWeb\Instagram\InstagramException 337 | */ 338 | public function modifyRelationship($action, $user) 339 | { 340 | if (in_array($action, $this->_actions) && isset($user)) { 341 | return $this->_makeCall('users/' . $user . '/relationship', array('action' => $action), 'POST'); 342 | } 343 | 344 | throw new InstagramException('Error: modifyRelationship() | This method requires an action command and the target user id.'); 345 | } 346 | 347 | /** 348 | * Search media by its location. 349 | * 350 | * @param float $lat Latitude of the center search coordinate 351 | * @param float $lng Longitude of the center search coordinate 352 | * @param int $distance Distance in metres (default is 1km (distance=1000), max. is 5km) 353 | * 354 | * @return mixed 355 | */ 356 | public function searchMedia($lat, $lng, $distance = 1000) 357 | { 358 | return $this->_makeCall('media/search', array( 359 | 'lat' => $lat, 360 | 'lng' => $lng, 361 | 'distance' => $distance 362 | )); 363 | } 364 | 365 | /** 366 | * Get media by its id. 367 | * 368 | * @param int $id Instagram media ID 369 | * 370 | * @return mixed 371 | */ 372 | public function getMedia($id) 373 | { 374 | return $this->_makeCall('media/' . $id); 375 | } 376 | 377 | /** 378 | * Search for tags by name. 379 | * 380 | * @param string $name Valid tag name 381 | * 382 | * @return mixed 383 | */ 384 | public function searchTags($name) 385 | { 386 | return $this->_makeCall('tags/search', array('q' => $name)); 387 | } 388 | 389 | /** 390 | * Get info about a tag 391 | * 392 | * @param string $name Valid tag name 393 | * 394 | * @return mixed 395 | */ 396 | public function getTag($name) 397 | { 398 | return $this->_makeCall('tags/' . $name); 399 | } 400 | 401 | /** 402 | * Get a recently tagged media. 403 | * 404 | * @param string $name Valid tag name 405 | * @param int $limit Limit of returned results 406 | * @param int $min_tag_id Return media before this min_tag_id 407 | * @param int $max_tag_id Return media after this max_tag_id 408 | * 409 | * @return mixed 410 | */ 411 | public function getTagMedia($name, $limit = 0, $min_tag_id = null, $max_tag_id = null) 412 | { 413 | $params = array(); 414 | 415 | if ($limit > 0) { 416 | $params['count'] = $limit; 417 | } 418 | if (isset($min_tag_id)) { 419 | $params['min_tag_id'] = $min_tag_id; 420 | } 421 | if (isset($max_tag_id)) { 422 | $params['max_tag_id'] = $max_tag_id; 423 | } 424 | 425 | return $this->_makeCall('tags/' . $name . '/media/recent', $params); 426 | } 427 | 428 | /** 429 | * Get a list of users who have liked this media. 430 | * 431 | * @param int $id Instagram media ID 432 | * 433 | * @return mixed 434 | */ 435 | public function getMediaLikes($id) 436 | { 437 | return $this->_makeCall('media/' . $id . '/likes'); 438 | } 439 | 440 | /** 441 | * Get a list of comments for this media. 442 | * 443 | * @param int $id Instagram media ID 444 | * 445 | * @return mixed 446 | */ 447 | public function getMediaComments($id) 448 | { 449 | return $this->_makeCall('media/' . $id . '/comments'); 450 | } 451 | 452 | /** 453 | * Add a comment on a media. 454 | * 455 | * @param int $id Instagram media ID 456 | * @param string $text Comment content 457 | * 458 | * @return mixed 459 | */ 460 | public function addMediaComment($id, $text) 461 | { 462 | return $this->_makeCall('media/' . $id . '/comments', array('text' => $text), 'POST'); 463 | } 464 | 465 | /** 466 | * Remove user comment on a media. 467 | * 468 | * @param int $id Instagram media ID 469 | * @param string $commentID User comment ID 470 | * 471 | * @return mixed 472 | */ 473 | public function deleteMediaComment($id, $commentID) 474 | { 475 | return $this->_makeCall('media/' . $id . '/comments/' . $commentID, null, 'DELETE'); 476 | } 477 | 478 | /** 479 | * Set user like on a media. 480 | * 481 | * @param int $id Instagram media ID 482 | * 483 | * @return mixed 484 | */ 485 | public function likeMedia($id) 486 | { 487 | return $this->_makeCall('media/' . $id . '/likes', null, 'POST'); 488 | } 489 | 490 | /** 491 | * Remove user like on a media. 492 | * 493 | * @param int $id Instagram media ID 494 | * 495 | * @return mixed 496 | */ 497 | public function deleteLikedMedia($id) 498 | { 499 | return $this->_makeCall('media/' . $id . '/likes', null, 'DELETE'); 500 | } 501 | 502 | /** 503 | * Get information about a location. 504 | * 505 | * @param int $id Instagram location ID 506 | * 507 | * @return mixed 508 | */ 509 | public function getLocation($id) 510 | { 511 | return $this->_makeCall('locations/' . $id); 512 | } 513 | 514 | /** 515 | * Get recent media from a given location. 516 | * 517 | * @param int $id Instagram location ID 518 | * @param int $min_id Return media before this min_id 519 | * @param int $max_id Return media after this max_id 520 | * 521 | * @return mixed 522 | */ 523 | public function getLocationMedia($id, $min_id = null, $max_id = null) 524 | { 525 | $params = array(); 526 | 527 | if (isset($min_id)) { 528 | $params['min_id'] = $min_id; 529 | } 530 | if (isset($max_id)) { 531 | $params['max_id'] = $max_id; 532 | } 533 | 534 | return $this->_makeCall('locations/' . $id . '/media/recent', $params); 535 | } 536 | 537 | /** 538 | * Get recent media from a given location. 539 | * 540 | * @param float $lat Latitude of the center search coordinate 541 | * @param float $lng Longitude of the center search coordinate 542 | * @param int $distance Distance in meter (max. distance: 5km = 5000) 543 | * @param int $facebook_places_id Returns a location mapped off of a 544 | * Facebook places id. If used, a Foursquare 545 | * id and lat, lng are not required. 546 | * @param int $foursquare_id Returns a location mapped off of a foursquare v2 547 | * api location id. If used, you are not 548 | * required to use lat and lng. 549 | * 550 | * @return mixed 551 | */ 552 | public function searchLocation($lat, $lng, $distance = 1000, $facebook_places_id = null, $foursquare_id = null) 553 | { 554 | $params['lat'] = $lat; 555 | $params['lng'] = $lng; 556 | $params['distance'] = $distance; 557 | if (isset($facebook_places_id)) { 558 | $params['facebook_places_id'] = $facebook_places_id; 559 | } 560 | if (isset($foursquare_id)) { 561 | $params['foursquare_id'] = $foursquare_id; 562 | } 563 | 564 | return $this->_makeCall('locations/search', $params); 565 | } 566 | 567 | /** 568 | * Pagination feature. 569 | * 570 | * @param object $obj Instagram object returned by a method 571 | * @param int $limit Limit of returned results 572 | * 573 | * @return mixed 574 | * 575 | * @throws \MetzWeb\Instagram\InstagramException 576 | */ 577 | public function pagination($obj, $limit = 0) 578 | { 579 | if (is_object($obj) && !is_null($obj->pagination)) { 580 | if (!isset($obj->pagination->next_url)) { 581 | return; 582 | } 583 | 584 | $apiCall = explode('?', $obj->pagination->next_url); 585 | 586 | if (count($apiCall) < 2) { 587 | return; 588 | } 589 | 590 | $function = str_replace(self::API_URL, '', $apiCall[0]); 591 | $count = ($limit) ? $limit : count($obj->data); 592 | 593 | if (isset($obj->pagination->next_max_tag_id)) { 594 | return $this->_makeCall($function, array('max_tag_id' => $obj->pagination->next_max_tag_id, 'count' => $count)); 595 | } 596 | 597 | return $this->_makeCall($function, array('next_max_id' => $obj->pagination->next_max_id, 'count' => $count)); 598 | } 599 | throw new InstagramException("Error: pagination() | This method doesn't support pagination."); 600 | } 601 | 602 | /** 603 | * Get the OAuth data of a user by the returned callback code. 604 | * 605 | * @param string $code OAuth2 code variable (after a successful login) 606 | * @param bool $token If it's true, only the access token will be returned 607 | * 608 | * @return mixed 609 | */ 610 | public function getOAuthToken($code, $token = false) 611 | { 612 | $apiData = array( 613 | 'grant_type' => 'authorization_code', 614 | 'client_id' => $this->getApiKey(), 615 | 'client_secret' => $this->getApiSecret(), 616 | 'redirect_uri' => $this->getApiCallback(), 617 | 'code' => $code 618 | ); 619 | 620 | $result = $this->_makeOAuthCall($apiData); 621 | 622 | return !$token ? $result : $result->access_token; 623 | } 624 | 625 | /** 626 | * The call operator. 627 | * 628 | * @param string $function API resource path 629 | * @param array $params Additional request parameters 630 | * @param string $method Request type GET|POST 631 | * 632 | * @return mixed 633 | * 634 | * @throws \MetzWeb\Instagram\InstagramException 635 | */ 636 | protected function _makeCall($function, $params = null, $method = 'GET') 637 | { 638 | if (!isset($this->_accesstoken)) { 639 | throw new InstagramException("Error: _makeCall() | $function - This method requires an authenticated users access token."); 640 | } 641 | 642 | $authMethod = '?access_token=' . $this->getAccessToken(); 643 | 644 | $paramString = null; 645 | 646 | if (isset($params) && is_array($params)) { 647 | $paramString = '&' . http_build_query($params); 648 | } 649 | 650 | $apiCall = self::API_URL . $function . $authMethod . (('GET' === $method) ? $paramString : null); 651 | 652 | // we want JSON 653 | $headerData = array('Accept: application/json'); 654 | 655 | if ($this->_signedheader) { 656 | $apiCall .= (strstr($apiCall, '?') ? '&' : '?') . 'sig=' . $this->_signHeader($function, $authMethod, $params); 657 | } 658 | 659 | $ch = curl_init(); 660 | 661 | if ($this->_proxyServer) { 662 | curl_setopt($ch, CURLOPT_PROXY, $this->_proxyServer); 663 | curl_setopt($ch, CURLOPT_PROXYPORT, $this->_proxyPort); 664 | if ($this->_proxyUser) { 665 | $proxyauth = $this->_proxyUser . ':' . $this->_proxyPwd; 666 | curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyauth); 667 | } 668 | } 669 | 670 | curl_setopt($ch, CURLOPT_URL, $apiCall); 671 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData); 672 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); 673 | curl_setopt($ch, CURLOPT_TIMEOUT, 90); 674 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 675 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 676 | curl_setopt($ch, CURLOPT_HEADER, true); 677 | 678 | switch ($method) { 679 | case 'POST': 680 | curl_setopt($ch, CURLOPT_POST, count($params)); 681 | curl_setopt($ch, CURLOPT_POSTFIELDS, ltrim($paramString, '&')); 682 | break; 683 | case 'DELETE': 684 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 685 | break; 686 | } 687 | 688 | $jsonData = curl_exec($ch); 689 | 690 | if (!$jsonData) { 691 | throw new InstagramException('Error: _makeCall() - cURL error: ' . curl_error($ch)); 692 | } 693 | 694 | // split header from JSON data 695 | // and assign each to a variable 696 | list($headerContent, $jsonData) = explode("\r\n\r\n", $jsonData, 2); 697 | 698 | // convert header content into an array 699 | $headers = $this->processHeaders($headerContent); 700 | 701 | // get the 'X-Ratelimit-Remaining' header value 702 | if (isset($headers['X-Ratelimit-Remaining'])) { 703 | $this->_xRateLimitRemaining = trim($headers['X-Ratelimit-Remaining']); 704 | } 705 | 706 | curl_close($ch); 707 | 708 | return json_decode($jsonData); 709 | } 710 | 711 | /** 712 | * The OAuth call operator. 713 | * 714 | * @param array $apiData The post API data 715 | * 716 | * @return mixed 717 | * 718 | * @throws \MetzWeb\Instagram\InstagramException 719 | */ 720 | private function _makeOAuthCall($apiData) 721 | { 722 | $apiHost = self::API_OAUTH_TOKEN_URL; 723 | 724 | $ch = curl_init(); 725 | curl_setopt($ch, CURLOPT_URL, $apiHost); 726 | 727 | if ($this->_proxyServer) { 728 | curl_setopt($ch, CURLOPT_PROXY, $this->_proxyServer); 729 | curl_setopt($ch, CURLOPT_PROXYPORT, $this->_proxyPort); 730 | if ($this->_proxyUser) { 731 | $proxyAuth = $this->_proxyUser . ':' . $this->_proxyPwd; 732 | curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyAuth); 733 | } 734 | } 735 | 736 | curl_setopt($ch, CURLOPT_POST, count($apiData)); 737 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($apiData)); 738 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: application/json')); 739 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 740 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 741 | curl_setopt($ch, CURLOPT_TIMEOUT, 90); 742 | $jsonData = curl_exec($ch); 743 | 744 | if (!$jsonData) { 745 | throw new InstagramException('Error: _makeOAuthCall() - cURL error: ' . curl_error($ch)); 746 | } 747 | 748 | curl_close($ch); 749 | 750 | return json_decode($jsonData); 751 | } 752 | 753 | /** 754 | * Sign header by using endpoint, parameters and the API secret. 755 | * 756 | * @param string 757 | * @param string 758 | * @param array 759 | * 760 | * @return string The signature 761 | */ 762 | private function _signHeader($endpoint, $authMethod, $params) 763 | { 764 | if (!is_array($params)) { 765 | $params = array(); 766 | } 767 | if ($authMethod) { 768 | list($key, $value) = explode('=', substr($authMethod, 1), 2); 769 | $params[$key] = $value; 770 | } 771 | $baseString = '/' . $endpoint; 772 | ksort($params); 773 | foreach ($params as $key => $value) { 774 | $baseString .= '|' . $key . '=' . $value; 775 | } 776 | $signature = hash_hmac('sha256', $baseString, $this->_apisecret, false); 777 | 778 | return $signature; 779 | } 780 | 781 | /** 782 | * Read and process response header content. 783 | * 784 | * @param array 785 | * 786 | * @return array 787 | */ 788 | private function processHeaders($headerContent) 789 | { 790 | $headers = array(); 791 | 792 | foreach (explode("\r\n", $headerContent) as $i => $line) { 793 | if ($i === 0) { 794 | $headers['http_code'] = $line; 795 | continue; 796 | } 797 | 798 | list($key, $value) = explode(':', $line); 799 | $headers[$key] = $value; 800 | } 801 | 802 | return $headers; 803 | } 804 | 805 | /** 806 | * Access Token Setter. 807 | * 808 | * @param object|string $data 809 | * 810 | * @return void 811 | */ 812 | public function setAccessToken($data) 813 | { 814 | $token = is_object($data) ? $data->access_token : $data; 815 | 816 | $this->_accesstoken = $token; 817 | } 818 | 819 | /** 820 | * Access Token Getter. 821 | * 822 | * @return string 823 | */ 824 | public function getAccessToken() 825 | { 826 | return $this->_accesstoken; 827 | } 828 | 829 | /** 830 | * API-key Setter 831 | * 832 | * @param string $apiKey 833 | * 834 | * @return void 835 | */ 836 | public function setApiKey($apiKey) 837 | { 838 | $this->_apikey = $apiKey; 839 | } 840 | 841 | /** 842 | * API Key Getter 843 | * 844 | * @return string 845 | */ 846 | public function getApiKey() 847 | { 848 | return $this->_apikey; 849 | } 850 | 851 | /** 852 | * API Secret Setter 853 | * 854 | * @param string $apiSecret 855 | * 856 | * @return void 857 | */ 858 | public function setApiSecret($apiSecret) 859 | { 860 | $this->_apisecret = $apiSecret; 861 | } 862 | 863 | /** 864 | * API Secret Getter. 865 | * 866 | * @return string 867 | */ 868 | public function getApiSecret() 869 | { 870 | return $this->_apisecret; 871 | } 872 | 873 | /** 874 | * API Callback URL Setter. 875 | * 876 | * @param string $apiCallback 877 | * 878 | * @return void 879 | */ 880 | public function setApiCallback($apiCallback) 881 | { 882 | $this->_callbackurl = $apiCallback; 883 | } 884 | 885 | /** 886 | * API Callback URL Getter. 887 | * 888 | * @return string 889 | */ 890 | public function getApiCallback() 891 | { 892 | return $this->_callbackurl; 893 | } 894 | 895 | /** 896 | * Enforce Signed Header. 897 | * 898 | * @param bool $signedHeader 899 | * 900 | * @return void 901 | */ 902 | public function setSignedHeader($signedHeader) 903 | { 904 | $this->_signedheader = $signedHeader; 905 | } 906 | 907 | /** 908 | * Get the proxy server. 909 | * 910 | * @return string 911 | */ 912 | public function getProxyServer() { 913 | return $this->_proxyServer; 914 | } 915 | 916 | /** 917 | * Set the proxy server. 918 | * 919 | * @param string $server 920 | * 921 | * @return void 922 | */ 923 | public function setProxyServer($server) { 924 | $this->_proxyServer = $server; 925 | } 926 | 927 | /** 928 | * Get the proxy server username. 929 | * 930 | * @return string 931 | */ 932 | public function getProxyUser(){ 933 | return $this->_proxyUser; 934 | } 935 | 936 | /** 937 | * Get the proxy server password. 938 | * 939 | * @return string 940 | */ 941 | public function getProxyPwd(){ 942 | return $this->_proxyPwd; 943 | } 944 | 945 | /** 946 | * Set the proxy username. 947 | * 948 | * @param string $user 949 | * 950 | * @return void 951 | */ 952 | public function setProxyUser($user){ 953 | $this->_proxyUser = $user; 954 | } 955 | 956 | /** 957 | * Set the proxy password. 958 | * 959 | * @param string $pwd 960 | * 961 | * @return void 962 | */ 963 | public function setProxyPwd($pwd){ 964 | $this->_proxyPwd = $pwd; 965 | } 966 | 967 | /** 968 | * Get proxy port. 969 | * 970 | * @return int 971 | */ 972 | public function getProxyPort() { 973 | return $this->_proxyPort; 974 | } 975 | 976 | /** 977 | * Set the proxy port. 978 | * 979 | * @param int $port 980 | * 981 | * @return void 982 | */ 983 | public function setProxyPort($port){ 984 | $this->_proxyPort = $port; 985 | } 986 | } 987 | 988 | -------------------------------------------------------------------------------- /src/InstagramException.php: -------------------------------------------------------------------------------- 1 |