├── LICENSE ├── README.md ├── composer.json └── src ├── .htaccess ├── achievements.php ├── fetch_conversation.php ├── friends.php ├── games.php ├── includes ├── bootloader.php ├── classes │ ├── api.class.php │ ├── base.class.php │ └── cache.class.php ├── cookies │ └── index.php ├── kernel.php └── logs │ ├── debug.log │ ├── error.log │ └── stack_trace.log ├── index.php ├── profile.php ├── search.php └── send_message.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 XboxLeaders 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #[XboxLiveAPI](http://xboxleaders.com) 2 | ###Simple & Powerful RESTful API For Xbox LIVE 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/xboxleaders/xboxliveapi/v/stable.png)](https://packagist.org/packages/xboxleaders/xboxliveapi) [![Total Downloads](https://poser.pugx.org/xboxleaders/xboxliveapi/downloads.png)](https://packagist.org/packages/xboxleaders/xboxliveapi) [![Latest Unstable Version](https://poser.pugx.org/xboxleaders/xboxliveapi/v/unstable.png)](https://packagist.org/packages/xboxleaders/xboxliveapi) [![License](https://poser.pugx.org/xboxleaders/xboxliveapi/license.png)](https://packagist.org/packages/xboxleaders/xboxliveapi) 5 | 6 | XboxLiveAPI is a simple and powerful RESTful API created to obtain data from Xbox LIVE, created and 7 | maintained by [Jason Clemons](http://about.me/jasonclemons) and [Alan Wynn](http://twitter.com/djekl). 8 | Stay up to date [@xboxleaders](http://twitter.com/xboxleaders). 9 | 10 | Get started at http://xboxleaders.com/get-started/! 11 | 12 | Please consider [donating to the project](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6ZHLXELDHACX6) to keep the servers alive! 13 | 14 | ##License 15 | - XboxLiveAPI source files are licensed under the MIT License: 16 | - http://opensource.org/licenses/mit-license.html 17 | - The XboxLiveAPI documentation is licensed under the CC BY 3.0 License: 18 | - http://creativecommons.org/licenses/by/3.0/ 19 | - Attribution is not required, but much appreciated: 20 | - `Data Provided By XboxLeaders - http://xboxleaders.com` 21 | - Full details: http://xboxleaders.com/license/ 22 | 23 | ## Requirements 24 | 1. [PHP](http://php.net/downloads.php) 5.4 or higher required 25 | - [cURL](http://php.net/curl) 26 | - [JSON](http://pecl.php.net/package/json) 27 | 2. [Apache](http://httpd.apache.org) 2.2 or higher required 28 | - mod_header 29 | - mod_expires 30 | - mod_rewrite 31 | 3. Caching Engines (Highly Recommended!) 32 | - [APC](http://pecl.php.net/package/apc) 33 | - [XCache](http://xcache.lighttpd.net) 34 | - [Memcached](http://memcached.org) 35 | 36 | ## Composer 37 | 1. Get [Composer](http://getcomposer.org) 38 | 2. Require xboxleaders/xboxliveapi `php composer.phar require xboxleaders/xboxliveapi` 39 | 3. Installation dependencies not available 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xboxleaders/xboxliveapi", 3 | "description": "Simple & Powerful RESTful API for Xbox LIVE", 4 | "keywords": ["xbox", "xboxlive", "api", "rest"], 5 | "homepage": "http://xboxleaders.github.io/", 6 | "authors": [ 7 | { 8 | "name": "Jason Clemons", 9 | "email": "jason@xboxleaders.com", 10 | "role": "Creator/Developer", 11 | "homepage": "http://about.me/jasonclemons" 12 | }, 13 | { 14 | "name": "Alan Wynn", 15 | "email": "hello@alanwynn.me", 16 | "role": "Contributor/Developer", 17 | "homepage": "https://alanwynn.me" 18 | } 19 | ], 20 | "license": [ 21 | "MIT" 22 | ], 23 | "require": { 24 | "php": ">=5.4.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | # Rewrite the new URL before anything else 2 | Options +FollowSymLinks 3 | RewriteEngine On 4 | RewriteRule ^1\.0/(.*)$ /api/$1 [R=301,NE,L,QSA] 5 | RewriteRule ^2\.0/(.*)$ /api/$1 [R=301,NE,L,QSA] 6 | 7 | # Set the proper headers and stuff 8 | Header unset Pragma 9 | FileETag None 10 | Header unset ETag 11 | ExpiresActive On 12 | ExpiresDefault A0 13 | 14 | 15 | ExpiresDefault A0 16 | Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0, proxy-revalidate, no-transform" 17 | Header set Pragma "no-cache" 18 | 19 | 20 | # Route to the proper handler 21 | RewriteRule ^achievements\.(json|xml)$ achievements.php?format=$1 [L,QSA] 22 | RewriteRule ^games\.(json|xml)$ games.php?format=$1 [L,QSA] 23 | RewriteRule ^profile\.(json|xml)$ profile.php?format=$1 [L,QSA] 24 | RewriteRule ^friends\.(json|xml)$ friends.php?format=$1 [L,QSA] 25 | RewriteRule ^search\.(json|xml)$ search.php?format=$1 [L,QSA] 26 | 27 | RewriteCond %{REQUEST_METHOD} !^(GET|HEAD|OPTIONS|POST|PUT) 28 | RewriteRule .* - [F] -------------------------------------------------------------------------------- /src/achievements.php: -------------------------------------------------------------------------------- 1 | output_headers(); 20 | $gamertag = (isset($_GET['gamertag']) && !empty($_GET['gamertag'])) ? trim($_GET['gamertag']) : null; 21 | $gameid = (isset($_GET['gameid'])) ? (int)$_GET['gameid'] : null; 22 | $region = (isset($_GET['region']) && !empty($_GET['region'])) ? $_GET['region'] : 'en-US'; 23 | 24 | if (!$api->logged_in) { 25 | echo $api->output_error(500); 26 | } else { 27 | if (empty($gamertag)) { 28 | echo $api->output_error(301); 29 | } elseif (empty($gameid)) { 30 | echo $api->output_error(302); 31 | } elseif ($api->check_culture($region) == false) { 32 | echo $api->output_error(305); 33 | } else { 34 | $data = $api->fetch_achievements($gamertag, $gameid, $region); 35 | if ($data) { 36 | echo $api->output_payload($data); 37 | } else { 38 | echo $api->output_error($api->error); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/fetch_conversation.php: -------------------------------------------------------------------------------- 1 | output_headers(); 20 | $gamertag = (isset($_GET['gamertag']) && !empty($_GET['gamertag'])) ? trim($_GET['gamertag']) : null; 21 | $region = (isset($_GET['region']) && !empty($_GET['region'])) ? $_GET['region'] : 'en-US'; 22 | $sender = (isset($_GET['sender']) && !empty($_GET['sender'])) ? trim($_GET['sender']) : null; 23 | 24 | if (!$api->logged_in) { 25 | echo $api->output_error(500); 26 | } else { 27 | if (empty($gamertag)) { 28 | echo $api->output_error(301); 29 | } elseif (empty($sender)) { 30 | echo $api->output_error(309); 31 | } elseif ($api->check_culture($region) == false) { 32 | echo $api->output_error(305); 33 | } else { 34 | $data = $api->fetchConversationWith($gamertag, $region, $sender); 35 | if ($data) { 36 | echo $api->output_payload($data); 37 | } else { 38 | echo $api->output_error($api->error); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/friends.php: -------------------------------------------------------------------------------- 1 | output_headers(); 20 | $gamertag = (isset($_GET['gamertag']) && !empty($_GET['gamertag'])) ? trim($_GET['gamertag']) : null; 21 | $region = (isset($_GET['region']) && !empty($_GET['region'])) ? $_GET['region'] : 'en-US'; 22 | 23 | if (!$api->logged_in) { 24 | echo $api->output_error(500); 25 | } else { 26 | if (empty($gamertag)) { 27 | echo $api->output_error(301); 28 | } elseif ($api->check_culture($region) == false) { 29 | echo $api->output_error(305); 30 | } else { 31 | $data = $api->fetch_friends($gamertag, $region); 32 | if ($data) { 33 | echo $api->output_payload($data); 34 | } else { 35 | echo $api->output_error($api->error); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/games.php: -------------------------------------------------------------------------------- 1 | output_headers(); 20 | $gamertag = (isset($_GET['gamertag']) && !empty($_GET['gamertag'])) ? trim($_GET['gamertag']) : null; 21 | $region = (isset($_GET['region']) && !empty($_GET['region'])) ? $_GET['region'] : 'en-US'; 22 | 23 | if (!$api->logged_in) { 24 | echo $api->output_error(500); 25 | } else { 26 | if (empty($gamertag)) { 27 | echo $api->output_error(301); 28 | } elseif ($api->check_culture($region) == false) { 29 | echo $api->output_error(305); 30 | } else { 31 | $data = $api->fetch_games($gamertag, $region); 32 | if ($data) { 33 | echo $api->output_payload($data); 34 | } else { 35 | echo $api->output_error($api->error); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/includes/bootloader.php: -------------------------------------------------------------------------------- 1 | 'john@example.com', 32 | 'password' => 'Password123', 33 | 'gamertag' => 'Major Nelson' 34 | ) 35 | ); 36 | 37 | /*! 38 | * Pick a random email to login 39 | */ 40 | $account = $accounts[0]; 41 | if (count($accounts) > 1) { 42 | $id = rand(0, (count($accounts) - 1)); 43 | $account = $accounts[$id]; 44 | } 45 | 46 | /*! 47 | * Define the account credentials 48 | */ 49 | define('XBOX_EMAIL', $account['email']); 50 | define('XBOX_PASSWORD', $account['password']); 51 | define('XBOX_GAMERTAG', $account['gamertag']); 52 | 53 | /*! 54 | * Define some log file locations. 55 | */ 56 | define('COOKIE_FILE', 'includes/cookies/' . XBOX_EMAIL . '.jar'); 57 | define('DEBUG_FILE', 'includes/logs/debug.log'); 58 | define('STACK_TRACE_FILE', 'includes/logs/stack_trace.log'); 59 | 60 | /*! 61 | * Initiate the caching engine. 62 | */ 63 | $cache = new Cache(CACHE_ENGINE); 64 | -------------------------------------------------------------------------------- /src/includes/classes/api.class.php: -------------------------------------------------------------------------------- 1 | version . ':sendMessage.' . $gamertag; 39 | 40 | $data = $this->fetch_url($url); 41 | $post_data = '__RequestVerificationToken=' . urlencode(trim($this->find($data, ''))) . '&recipients=' . urlencode($recipients) . '&message=' . urlencode($message); 42 | 43 | $headers = array('X-Requested-With: XMLHttpRequest', 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8'); 44 | $data = $this->fetch_url('https://account.xbox.com/' . $region . '/Messages/Send', $url, 10, $post_data, $headers); 45 | 46 | return json_decode($data, true); 47 | 48 | } 49 | 50 | /** 51 | * Fetch conversation with 52 | * 53 | * @access public 54 | * @var string $gamertag 55 | * @var string $region 56 | * @var string $sender 57 | * @return array 58 | */ 59 | public function fetchConversationWith($gamertag, $region, $sender) 60 | { 61 | $gamertag = trim($gamertag); 62 | $url = 'https://account.xbox.com/' . $region . '/Messages/UserConversation?senderGamerTag=' . $sender; 63 | $key = $this->version . ':getMessages.' . $gamertag; 64 | 65 | $data = $this->fetch_url($url); 66 | $doc = new DOMDocument(); 67 | if(!empty($sender) && !empty($gamertag)) { 68 | $doc->loadHtml($data); 69 | $xpath = new DOMXPath($doc); 70 | $postThumbLinks = $xpath->query("//div[@class='messageContent']"); 71 | 72 | $i = 0; 73 | $array = array(); 74 | $last_sender = ""; 75 | foreach($postThumbLinks as $link) { 76 | 77 | $body = $this->find($link->ownerDocument->saveHTML($link),'
','
'); 78 | $time = $this->find($link->ownerDocument->saveHTML($link),'
','
'); 79 | $sender = $this->find($link->ownerDocument->saveHTML($link),'
','
'); 80 | 81 | $array[$i]['message'] = $body; 82 | $array[$i]['time'] = $time; 83 | if($sender){ 84 | $array[$i]['sender'] = $sender; 85 | $last_sender = $sender; 86 | }else{ 87 | $array[$i]['sender'] = $last_sender; 88 | } 89 | 90 | $i++; 91 | } 92 | } else { 93 | return false; 94 | } 95 | 96 | return $array; 97 | } 98 | 99 | /** 100 | * Fetch profile information 101 | * 102 | * @access public 103 | * @var string $gamertag 104 | * @var string $region 105 | * @return array 106 | */ 107 | public function fetch_profile($gamertag, $region) 108 | { 109 | $gamertag = trim($gamertag); 110 | $url = 'http://live.xbox.com/' . $region . '/Profile?gamertag=' . urlencode($gamertag); 111 | $key = $this->version . ':profile.' . $gamertag; 112 | 113 | $data = $this->__cache->fetch($key); 114 | if (!$data) { 115 | $data = $this->fetch_url($url); 116 | $freshness = 'new'; 117 | $this->__cache->store($key, $data, 3600); 118 | } else { 119 | $freshness = 'from cache'; 120 | } 121 | 122 | if (stripos($data, '
')) { 123 | $user = array(); 124 | $user['gamertag'] = trim($gamertag); 125 | $user['tier'] = (strpos($data, '
') !== false) ? 'gold' : 'silver'; 126 | $user['badges']['xboxlaunchteam'] = false; 127 | $user['badges']['nxelaunchteam'] = false; 128 | $user['badges']['kinectlaunchteam'] = false; 129 | $user['avatar']['full'] = 'http://avatar.xboxlive.com/avatar/' . $gamertag . '/avatar-body.png'; 130 | $user['avatar']['small'] = 'http://avatar.xboxlive.com/avatar/' . $gamertag . '/avatarpic-s.png'; 131 | $user['avatar']['large'] = 'http://avatar.xboxlive.com/avatar/' . $gamertag . '/avatarpic-l.png'; 132 | $user['avatar']['tile'] = trim(str_replace('https://avatar-ssl', 'http://avatar', $this->find($data, '')));
133 |             $user['gamerscore']                 = (int) trim($this->find($data, '<div class=', '
')); 134 | $user['reputation'] = 0; 135 | $user['presence'] = trim(str_replace("\r\n", ' - ', $this->find($data, '
', '
'))); 136 | $user['online'] = (bool)(strpos($user['presence'], 'Online') !== false && strpos($user['presence'], 'Online Status Unavailable') === false); 137 | $user['gamertag'] = str_replace(array(''s Profile', '\'s Profile'), '', trim($this->find($data, '

', '

'))); 138 | $user['motto'] = $this->clean(trim(strip_tags($this->find($data, '
', '
')))); 139 | $user['name'] = trim(strip_tags($this->find($data, '
'))); 140 | $user['location'] = trim(strip_tags(str_replace('', '', trim($this->find($data, '
', '
'))))); 141 | $user['biography'] = trim(strip_tags(str_replace('', '', trim($this->find($data, '
', '
'))))); 142 | 143 | $recentactivity = $this->fetch_games($gamertag, $region); 144 | if (is_array($recentactivity)) { 145 | $user['recentactivity'] = array_slice($recentactivity['games'], 0, 5, true); 146 | } else { 147 | $user['recentactivity'] = null; 148 | } 149 | 150 | if (strpos($data, '
') !== false) { 151 | if (strpos($data, 'xbox360Badge') !== false) { 152 | $user['badges']['xboxlaunchteam'] = true; 153 | } 154 | if (strpos($data, 'nxeBadge') !== false) { 155 | $user['badges']['nxelaunchteam'] = true; 156 | } 157 | if (strpos($data, 'kinectBadge') !== false) { 158 | $user['badges']['kinectlaunchteam'] = true; 159 | } 160 | } 161 | 162 | $starvalue = array( 163 | 'Empty' => 0, 164 | 'Quarter' => 1, 165 | 'Half' => 2, 166 | 'ThreeQuarter' => 3, 167 | 'Full' => 4, 168 | ); 169 | 170 | preg_match_all('~
~si', $data, $reputation); 171 | foreach ($reputation[1] as $value) { 172 | $user['reputation'] = $user['reputation'] + $starvalue[$value]; 173 | } 174 | 175 | $user['freshness'] = $freshness; 176 | 177 | return $user; 178 | } elseif (stripos($data, 'errorCode: \'404\'')) { 179 | $this->error = 501; 180 | 181 | return false; 182 | } else { 183 | $this->error = 500; 184 | $this->__cache->remove($key); 185 | $this->force_new_login(); 186 | 187 | return false; 188 | } 189 | } 190 | 191 | /** 192 | * Fetch achievement information for a specific game 193 | * 194 | * @access public 195 | * @var string $gamertag 196 | * @var int $gameid 197 | * @var string $region 198 | * @return array 199 | */ 200 | public function fetch_achievements($gamertag, $gameid, $region) 201 | { 202 | $gamertag = trim($gamertag); 203 | $url = 'https://live.xbox.com/' . $region . '/Activity/Details?titleId=' . urlencode($gameid) . '&compareTo=' . urlencode($gamertag); 204 | $key = $this->version . ':achievements.' . $gamertag . '.' . $gameid; 205 | 206 | $data = $this->__cache->fetch($key); 207 | if (!$data) { 208 | $data = $this->fetch_url($url); 209 | $freshness = 'new'; 210 | $this->__cache->store($key, $data, 3600); 211 | } else { 212 | $freshness = 'from cache'; 213 | } 214 | 215 | $json = $this->find($data, 'broker.publish(routes.activity.details.load, ', ');'); 216 | $json = json_decode($json, true); 217 | 218 | if (!empty($json)) { 219 | $achievements = array(); 220 | $achievements['gamertag'] = $g = $json['Players'][0]['Gamertag']; 221 | $achievements['game'] = $this->clean($json['Game']['Name']); 222 | $achievements['id'] = $json['Game']['Id']; 223 | $achievements['hid'] = dechex($json['Game']['Id']); 224 | $achievements['gamerscore']['current'] = $json['Game']['Progress'][$g]['Score']; 225 | $achievements['gamerscore']['total'] = $json['Game']['PossibleScore']; 226 | $achievements['achievement']['current'] = $json['Game']['Progress'][$g]['Achievements']; 227 | $achievements['achievement']['total'] = $json['Game']['PossibleAchievements']; 228 | $achievements['progress'] = $json['Players'][0]['PercentComplete']; 229 | $achievements['lastplayed'] = (int)substr($json['Game']['Progress'][$g]['LastPlayed'], 6, 10); 230 | 231 | $i = 0; 232 | foreach ($json['Achievements'] as $achievement) { 233 | $achievements['achievements'][$i]['id'] = $achievement['Id']; 234 | $achievements['achievements'][$i]['title'] = ''; 235 | $achievements['achievements'][$i]['artwork']['locked'] = 'https://live.xbox.com/Content/Images/HiddenAchievement.png'; 236 | $achievements['achievements'][$i]['artwork']['unlocked'] = 'https://live.xbox.com/Content/Images/HiddenAchievement.png'; 237 | $achievements['achievements'][$i]['title'] = 'Secret Achievement'; 238 | $achievements['achievements'][$i]['description'] = 'This is a secret achievement. Unlock it to find out more.'; 239 | $achievements['achievements'][$i]['gamerscore'] = 0; 240 | $achievements['achievements'][$i]['secret'] = (bool)$achievement['IsHidden']; 241 | $achievements['achievements'][$i]['unlocked'] = false; 242 | $achievements['achievements'][$i]['unlockdate'] = null; 243 | $achievements['achievements'][$i]['unlockedoffline'] = null; 244 | 245 | if (!$achievement['IsHidden']) { 246 | // find colored achievement tile 247 | // get image name. removes url and extension 248 | $ach_info = pathinfo($achievement['TileUrl']); 249 | // decode image name 250 | $ach_str1 = base64_decode($ach_info['filename']); 251 | // remove everything before /ach 252 | $ach_str2 = strstr($ach_str1, '/ach'); 253 | // subtract last 12 spaces and/or whitespace from string 254 | $ach_str3 = substr($ach_str2, 0, -11); 255 | // create color tile source 256 | $ach_color = 'https://image-ssl.xboxlive.com/global/t.' . dechex($json['Game']['Id']) . $ach_str3; 257 | 258 | $achievements['achievements'][$i]['artwork']['locked'] = $achievement['TileUrl']; 259 | $achievements['achievements'][$i]['artwork']['unlocked'] = $ach_color; 260 | } 261 | 262 | if (!empty($achievement['Name'])) { 263 | $achievements['achievements'][$i]['title'] = $this->clean($achievement['Name']); 264 | } 265 | 266 | if (!empty($achievement['Description'])) { 267 | $achievements['achievements'][$i]['description'] = $this->clean($achievement['Description']); 268 | } 269 | 270 | if (!empty($achievement['Score'])) { 271 | $achievements['achievements'][$i]['gamerscore'] = $achievement['Score']; 272 | } 273 | 274 | if (!empty($achievement['EarnDates'][$g]['EarnedOn'])) { 275 | $achievements['achievements'][$i]['unlocked'] = true; 276 | $achievements['achievements'][$i]['unlockdate'] = (int)substr($achievement['EarnDates'][$g]['EarnedOn'], 6, 10); 277 | $achievements['achievements'][$i]['unlockedoffline'] = (bool)$achievement['EarnDates'][$g]['IsOffline']; 278 | } 279 | 280 | $i++; 281 | } 282 | 283 | $achievements['freshness'] = $freshness; 284 | return $achievements; 285 | } elseif (stripos($data, 'errorCode: \'404\'')) { 286 | $this->error = 502; 287 | return false; 288 | } else { 289 | $this->error = 500; 290 | $this->__cache->remove($key); 291 | $this->force_new_login(); 292 | return false; 293 | } 294 | } 295 | 296 | /** 297 | * Fetch information about played games 298 | * 299 | * @access public 300 | * @var string $gamertag 301 | * @var string $region 302 | * @return array 303 | */ 304 | public function fetch_games($gamertag, $region) 305 | { 306 | $gamertag = trim($gamertag); 307 | $url = 'https://live.xbox.com/' . $region . '/Activity'; 308 | $key = $this->version . ':games.' . $gamertag; 309 | $data = $this->__cache->fetch($key); 310 | $freshness = 'from cache'; 311 | 312 | if (!$data) { 313 | $data = $this->fetch_url($url); 314 | $post_data = '__RequestVerificationToken=' . urlencode(trim($this->find($data, ''))) . '&compareTo=' . urlencode($gamertag); 315 | $headers = array('X-Requested-With: XMLHttpRequest', 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8'); 316 | $data = $this->fetch_url('https://live.xbox.com/' . $region . '/Activity/Summary?compareTo=' . urlencode($gamertag) . '&lc=1033', $url, 10, $post_data, $headers); 317 | $freshness = 'new'; 318 | $this->__cache->store($key, $data, 3600); 319 | } 320 | 321 | $json = json_decode($data, true); 322 | 323 | if ($json['Success'] == 'true' && $json['Data']['Players'][0]['Gamertag'] != $this->account_gamertag) { 324 | $json = $json['Data']; 325 | $games = array(); 326 | $games['gamertag'] = $g = $json['Players'][0]['Gamertag']; 327 | $games['gamerscore']['current'] = $json['Players'][0]['Gamerscore']; 328 | $games['gamerscore']['total'] = 0; 329 | $games['achievements']['current'] = 0; 330 | $games['achievements']['total'] = 0; 331 | $games['totalgames'] = $json['Players'][0]['GameCount']; 332 | $games['progress'] = $json['Players'][0]['PercentComplete']; 333 | 334 | $i = 0; 335 | foreach ($json['Games'] as $game) { 336 | if ($game['Progress'][$g]['LastPlayed'] !== 'null') { 337 | $games['games'][$i]['id'] = $game['Id']; 338 | $games['games'][$i]['hid'] = dechex($game['Id']); 339 | $games['games'][$i]['isapp'] = (bool)($game['PossibleScore'] == 0); 340 | $games['games'][$i]['title'] = $this->clean($game['Name']); 341 | $games['games'][$i]['artwork']['small'] = 'http://download.xbox.com/content/images/66acd000-77fe-1000-9115-d802' . dechex($game['Id']) . '/1033/boxartsm.jpg'; 342 | $games['games'][$i]['artwork']['large'] = 'http://download.xbox.com/content/images/66acd000-77fe-1000-9115-d802' . dechex($game['Id']) . '/1033/boxartlg.jpg'; 343 | $games['games'][$i]['gamerscore']['current'] = $game['Progress'][$g]['Score']; 344 | $games['games'][$i]['gamerscore']['total'] = $game['PossibleScore']; 345 | $games['games'][$i]['achievements']['current'] = $game['Progress'][$g]['Achievements']; 346 | $games['games'][$i]['achievements']['total'] = $game['PossibleAchievements']; 347 | $games['games'][$i]['progress'] = 0; 348 | $games['games'][$i]['lastplayed'] = (int)substr($game['Progress'][$g]['LastPlayed'], 6, 10); 349 | $games['gamerscore']['total'] = $games['gamerscore']['total'] + $games['games'][$i]['gamerscore']['total']; 350 | $games['achievements']['current'] = $games['achievements']['current'] + $games['games'][$i]['achievements']['current']; 351 | $games['achievements']['total'] = $games['achievements']['total'] + $games['games'][$i]['achievements']['total']; 352 | 353 | if ($game['Progress'][$g]['Achievements'] !== 0) { 354 | $games['games'][$i]['progress'] = round((($game['Progress'][$g]['Achievements'] / $game['PossibleAchievements']) * 100), 1); 355 | } 356 | 357 | $i++; 358 | } 359 | } 360 | 361 | $games['freshness'] = $freshness; 362 | return $games; 363 | } elseif ($json['Data']['Players'][0]['Gamertag'] == $this->account_gamertag) { 364 | $this->error = 501; 365 | return false; 366 | } else { 367 | $this->error = 500; 368 | $this->__cache->remove($key); 369 | $this->force_new_login(); 370 | return false; 371 | } 372 | } 373 | 374 | /** 375 | * Fetch data about a players' friends list 376 | * 377 | * @access public 378 | * @var string $gamertag 379 | * @var string $region 380 | * @return array 381 | */ 382 | public function fetch_friends($gamertag, $region) 383 | { 384 | $gamertag = trim($gamertag); 385 | $url = 'https://live.xbox.com/' . $region . '/Friends'; 386 | $key = $this->version . ':friends.' . $gamertag; 387 | $data = $this->__cache->fetch($key); 388 | $freshness = 'from cache'; 389 | 390 | if (!$data) { 391 | $data = $this->fetch_url($url); 392 | $post_data = '__RequestVerificationToken=' . urlencode(trim($this->find($data, ''))); 393 | $headers = array('X-Requested-With: XMLHttpRequest', 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8'); 394 | $data = $this->fetch_url('https://live.xbox.com/' . $region . '/Friends/List?Gamertag=' . urlencode($gamertag), $url, 10, $post_data, $headers); 395 | $freshness = 'new'; 396 | $this->__cache->store($key, $data, 3600); 397 | } 398 | 399 | $json = json_decode($data, true); 400 | 401 | if (!empty($json['Data']) && $json['Data']['Friends'] != null) { 402 | $friends = array(); 403 | $friends['total'] = 0; 404 | $friends['totalonline'] = 0; 405 | $friends['totaloffline'] = 0; 406 | 407 | $i = 0; 408 | foreach ($json['Data']['Friends'] as $friend) { 409 | $friends['friends'][$i]['gamertag'] = $friend['GamerTag']; 410 | $friends['friends'][$i]['gamerpic']['small'] = $friend['GamerTileUrl']; 411 | $friends['friends'][$i]['gamerpic']['large'] = $friend['LargeGamerTileUrl']; 412 | $friends['friends'][$i]['gamerscore'] = $friend['GamerScore']; 413 | $friends['friends'][$i]['online'] = (bool)$friend['IsOnline']; 414 | $friends['friends'][$i]['status'] = $friend['Presence']; 415 | $friends['friends'][$i]['lastseen'] = (int)substr($friend['LastSeen'], 6, 10); 416 | 417 | $friends['total']++; 418 | if ($friend['IsOnline']) { 419 | $friends['totalonline']++; 420 | } else { 421 | $friends['totaloffline']++; 422 | } 423 | 424 | $i++; 425 | } 426 | 427 | $friends['freshness'] = $freshness; 428 | return $friends; 429 | } else { 430 | $this->error = 503; 431 | $this->__cache->remove($key); 432 | $this->force_new_login(); 433 | return false; 434 | } 435 | } 436 | 437 | /** 438 | * Fetch data for a search query 439 | * 440 | * @access public 441 | * @var string $query 442 | * @var string $region 443 | * @return array 444 | */ 445 | public function fetch_search($query, $region) 446 | { 447 | $query = trim($query); 448 | $url = 'http://marketplace.xbox.com/' . $region . '/SiteSearch/xbox/?query=' . urlencode($query); 449 | $key = $this->version . ':friends.' . $query; 450 | $data = $this->__cache->fetch($key); 451 | $freshness = 'from cache'; 452 | 453 | if (!$data) { 454 | $data = $this->fetch_url($url); 455 | $freshness = 'new'; 456 | $this->__cache->store($key, $data, 3600); 457 | } 458 | 459 | $json = json_decode($data, true); 460 | if ($json['totalEntries'] >= 1) { 461 | $search = array(); 462 | $search['totalresults'] = $json['totalEntries']; 463 | $search['resultslink'] = 'http://marketplace.xbox.com' . $json['allResultsUrl']; 464 | 465 | $i = 0; 466 | foreach ($json['entries'] as $entry) { 467 | $search['results'][$i]['title'] = $entry['title']; 468 | $search['results'][$i]['parent'] = $entry['parentTitle']; 469 | $search['results'][$i]['link'] = 'http://marketplace.xbox.com' . $entry['detailsUrl']; 470 | $search['results'][$i]['image'] = $entry['image']; 471 | $search['results'][$i]['downloadtype']['class'] = $entry['downloadTypeClass']; 472 | $search['results'][$i]['downloadtype']['title'] = $entry['downloadTypeText']; 473 | $search['results'][$i]['prices']['silver'] = $entry['prices']['silver']; 474 | $search['results'][$i]['prices']['gold'] = $entry['prices']['gold']; 475 | $i++; 476 | } 477 | 478 | $search['freshness'] = $freshness; 479 | return $search; 480 | } else { 481 | $this->error = 504; 482 | $this->__cache->remove($key); 483 | return false; 484 | } 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /src/includes/classes/base.class.php: -------------------------------------------------------------------------------- 1 | 'The key supplied was not valid.', 43 | 301 => 'The gamertag supplied was not valid.', 44 | 302 => 'The gameid supplied was not valid.', 45 | 303 => 'No version specified.', 46 | 304 => 'No method specified.', 47 | 305 => 'The region supplied is not supported by Xbox LIVE.', 48 | 306 => 'The query supplied was not valid.', 49 | 307 => 'No recipient specified', 50 | 308 => 'No message specified', 51 | 309 => 'No sender specified', 52 | 500 => 'There was an internal server error.', 53 | 501 => 'The gamertag supplied does not exist on Xbox Live.', 54 | 502 => 'The gamertag supplied has never played this game or does not exist on Xbox Live.', 55 | 503 => 'No friends found, or friend\'s list is set to private.', 56 | 504 => 'The search query returned empty.', 57 | 600 => 'Could not login to Windows Live. Please email support@xboxleaders.com.', 58 | 601 => 'The Windows Live email address supplied was invalid.', 59 | 602 => 'The Windows Live password supplied was invalid.', 60 | 603 => 'Could not write to the cookie file.', 61 | 604 => 'Could not write to the debug file.', 62 | 605 => 'Could not write to the stack trace file.', 63 | 606 => 'Windows Live caused an infinite redirect loop.', 64 | 607 => 'The server timed out when trying to fetch some data.', 65 | 608 => 'Could not write to the access file.', 66 | 700 => 'The Xbox LIVE Service is down.' 67 | ); 68 | 69 | /*! 70 | * Construct the cache, runtime, and random ip address 71 | */ 72 | public function __construct($cache) 73 | { 74 | if ($cache) { 75 | $this->__cache =& $cache; 76 | } 77 | 78 | $this->runtime = microtime(true); 79 | $this->ip = rand(60, 80) . '.' . rand(60, 140) . '.' . rand(80, 120) . '.' . rand(120, 200); 80 | } 81 | 82 | /*! 83 | * Initiate the scraper 84 | */ 85 | public function init($email, $password) 86 | { 87 | // Email and password. Make sure it's URL-friendly. 88 | $this->email = urlencode($email); 89 | $this->password = urlencode($password); 90 | 91 | // Check the login session to make sure it's valid. 92 | if ($this->check_login()) { 93 | $this->logged_in = true; 94 | } else { 95 | $this->perform_login(); 96 | } 97 | } 98 | 99 | /*! 100 | * Output the necessary headers 101 | */ 102 | public function output_headers() 103 | { 104 | header('Content-Type: application/' . str_replace('jsonp', 'javascript', $this->format) . '; charset=utf-8'); 105 | header('Access-Control-Allow-Origin: *'); 106 | header('Access-Control-Max-Age: 3628800'); 107 | } 108 | 109 | /*! 110 | * Output the entire payload to the browser 111 | */ 112 | public function output_payload($data) 113 | { 114 | $payload = array( 115 | 'status' => 'success', 116 | 'version' => $this->version, 117 | 'data' => $data, 118 | 'runtime' => round(microtime(true) - $this->runtime, 3) 119 | ); 120 | 121 | // Output the data in the desired format. 122 | switch ($this->format) { 123 | case 'xml': 124 | return output_pretty_xml($payload); 125 | case 'json': 126 | return output_pretty_json($payload); 127 | case 'jsonp': 128 | return output_pretty_jsonp($payload, $this->callback); 129 | case 'php': 130 | return output_pretty_php($payload); 131 | } 132 | 133 | return false; 134 | } 135 | 136 | /*! 137 | * Output an error if one is found 138 | */ 139 | public function output_error($code) 140 | { 141 | // Output the proper response code. 142 | if (array_key_exists((int) $code, $this->errors)) { 143 | http_response_code((int) $code); 144 | } 145 | 146 | $payload = array( 147 | 'status' => 'error', 148 | 'version' => $this->version, 149 | 'data' => array( 150 | 'code' => $code, 151 | 'message' => $this->errors[$code] 152 | ), 153 | 'runtime' => round(microtime(true) - $this->runtime, 3) 154 | ); 155 | 156 | // Output the data in the desired format. 157 | switch ($this->format) { 158 | case 'xml': 159 | return output_pretty_xml($payload); 160 | case 'json': 161 | return output_pretty_json($payload); 162 | case 'jsonp': 163 | return output_pretty_jsonp($payload, $this->callback); 164 | case 'php': 165 | return output_pretty_php($payload); 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /*! 172 | * Check culture code against Xbox's list of supported regions 173 | */ 174 | public function check_culture($code) 175 | { 176 | $valid_codes = array( 177 | 'es-AR', 'en-AU', 'de-AT', 'nl-BE', 178 | 'fr-BE', 'pt-BR', 'en-CA', 'fr-CA', 179 | 'es-CL', 'es-CO', 'cs-CZ', 'da-DA', 180 | 'fi-FI', 'fr-FR', 'de-DE', 'el-GR', 181 | 'zh-HK', 'en-HK', 'hu-HU', 'en-ID', 182 | 'en-IE', 'he-IL', 'it-IT', 'ja-JP', 183 | 'ko-KR', 'es-MX', 'nl-NL', 'en-NZ', 184 | 'nb-NO', 'nn-NO', 'pl-PL', 'ru-RU', 185 | 'en-SA', 'en-SG', 'sk-SK', 'en-ZA', 186 | 'es-ES', 'sv-SE', 'de-CH', 'fr-CH', 187 | 'zh-TW', 'tr-TR', 'en-UA', 'en-GB', 188 | 'en-US' 189 | ); 190 | 191 | return in_array($code, $valid_codes, true); 192 | } 193 | 194 | /*! 195 | * Perform the actual login to Xbox LIVE 196 | */ 197 | protected function perform_login() 198 | { 199 | if (empty($this->email)) { 200 | $this->error = 601; 201 | return false; 202 | } elseif (empty($this->password)) { 203 | $this->error = 602; 204 | return false; 205 | } else { 206 | // Make some cookies. We all like cookies. Microsoft just likes them more. 207 | $this->add_cookie('.xbox.com', 'MC0', time(), '/', time() + (60*60*24*365)); 208 | $this->add_cookie('.xbox.com', 's_vi', '[CS]v1|26AD59C185011B4D-40000113004213F1[CE]', '/', time() + (60*60*24*365)); 209 | $this->add_cookie('.xbox.com', 's_nr', '1297891791797', '/', time() + (60*60*24*365)); 210 | $this->add_cookie('.xbox.com', 's_sess', '%20s_cc%3Dtrue%3B%20s_ria%3Dflash%252011%257Csilverlight%2520not%2520detected%3B%20s_sq%3D%3B', '/', time() + (60*60*24*365)); 211 | $this->add_cookie('.xbox.com', 's_pers', '%20s_vnum%3D1352674046430%2526vn%253D4%7C1352674046430%3B%20s_lastvisit%3D1324587801077%7C1419195801077%3B%20s_invisit%3Dtrue%7C1324589873289%3B', '/', time() + (60*60*24*365)); 212 | $this->add_cookie('.xbox.com', 'UtcOffsetMinutes', '60', '/', time() + (60*60*24*365)); 213 | $this->add_cookie('.xbox.com', 'xbox_info', 't=6', '/', time() + (60*60*24*365)); 214 | $this->add_cookie('.xbox.com', 'PersistentId', '0a652e56e40f42caac3ac84fad02ed01', '/', time() + (60*60*24*365)); 215 | 216 | // Send the data to Xbox and retrieve the response. 217 | $url = 'https://login.live.com/login.srf?wa=wsignin1.0&rpsnv=12&ct=' . time() . '&rver=6.2.6289.0&wp=MBI_SSL&wreply=https:%2F%2Faccount.xbox.com:443%2Fpassport%2FsetCookies.ashx%3Frru%3Dhttps%253a%252f%252faccount.xbox.com%252fen-US%252f&lc=1033&id=292543&cbcxt=0'; 218 | $result = $this->fetch_url($url, 'http://www.xbox.com/en-US/'); 219 | 220 | $this->stack_trace[] = array( 221 | 'url' => $url, 222 | 'postdata' => '', 223 | 'result' => $result 224 | ); 225 | 226 | // More cookies... 227 | $this->add_cookie('.login.live.com', 'WLOpt', 'act=[1]', time() + (60*60*24*365)); 228 | $this->add_cookie('.login.live.com', 'CkTst', 'G' . time(), '/', time() + (60*60*24*365)); 229 | 230 | // Find the right parameters. We need this data for the next request. 231 | $url = $this->find($result, 'urlPost:\'', '\','); 232 | $PPFT = $this->find($result, 'name="PPFT" id="i0327" value="', '"'); 233 | $PPSX = $this->find($result, 'srf_sRBlob=\'', '\';'); 234 | 235 | // Send the data back to Xbox and retrieve the response. 236 | $post_data = 'login=' . $this->email . '&passwd=' . $this->password . '&KMSI=1&mest=0&type=11&LoginOptions=3&PPSX=' . $PPSX . '&PPFT=' . $PPFT . '&idsbho=1&PwdPad=&sso=&i1=1&i2=1&i3=12035&i12=1&i13=1&i14=323&i15=3762&i18=__Login_Strings%7C1%2C__Login_Core%7C1%2C'; 237 | $result = $this->fetch_url($url, 'https://login.live.com/login.srf', null, $post_data); 238 | 239 | $this->stack_trace[] = array( 240 | 'url' => $url, 241 | 'post_data' => $post_data, 242 | 'result' => $result 243 | ); 244 | 245 | // MOAR COOKIESSSSSSSS!!! 246 | $this->add_cookie('.live.com', 'wlidperf', 'throughput=3&latency=961&FR=L&ST=1297790188859', '/', time() + (60*60*24*365)); 247 | $this->add_cookie('login.live.com', 'CkTst', time(), '/ppsecure/', time() + (60*60*24*365)); 248 | 249 | $url = $this->find($result, 'id="fmHF" action="', '"'); 250 | $NAP = $this->find($result, 'id="NAP" value="', '"'); 251 | $ANON = $this->find($result, 'id="ANON" value="', '"'); 252 | $t = $this->find($result, 'id="t" value="', '"'); 253 | 254 | $this->add_cookie('.xbox.com', 'ANON', $ANON, '/', time() + (60*60*24*365)); 255 | $this->add_cookie('.xbox.com', 'NAP', $NAP, '/', time() + (60*60*24*365)); 256 | 257 | // OK, last time we send data to Xbox and retrieve the response. 258 | $post_data = 'NAPExp=' . date('c', mktime(date('H'), date('i'), date('s'), date('n') + 1)) . '&NAP=' . $NAP . '&ANONExp=' . date('c', mktime(date('H'), date('i'), date('s'), date('n') + 1)) . '&ANON=' . $ANON . '&t=' . $t; 259 | $result = $this->fetch_url($url, '', 16, $post_data); 260 | 261 | $this->stack_trace[] = array( 262 | 'url' => $url, 263 | 'post_data' => $post_data, 264 | 'result' => $result 265 | ); 266 | 267 | $result = $this->fetch_url('https://account.xbox.com/en-US/social'); 268 | 269 | // Make sure we got signed in... 270 | if (stripos($result, '

Activity feed

') !== false) { 271 | $this->logged_in = true; 272 | return true; 273 | } else { 274 | $this->error = 600; 275 | $this->save_stack_trace(); 276 | return false; 277 | } 278 | 279 | } 280 | } 281 | 282 | /*! 283 | * Check the current session to see if it's logged in 284 | */ 285 | protected function check_login() 286 | { 287 | if (file_exists($this->cookie_file)) { 288 | if (time() - filemtime($this->cookie_file) >= 3600 || filesize($this->cookie_file) == 0) { 289 | $this->empty_cookie_file(); 290 | $this->logged_in = false; 291 | return false; 292 | } else { 293 | $this->logged_in = true; 294 | return true; 295 | } 296 | } else { 297 | $this->empty_cookie_file(); 298 | $this->logged_in = false; 299 | return false; 300 | } 301 | } 302 | 303 | /*! 304 | * Force a new login session 305 | */ 306 | protected function force_new_login() 307 | { 308 | $this->empty_cookie_file(); 309 | $this->logged_in = false; 310 | $this->perform_login(); 311 | 312 | if ($this->logged_in) { 313 | return true; 314 | } 315 | 316 | return false; 317 | } 318 | 319 | /*! 320 | * Perform the actual HTTP request 321 | */ 322 | protected function fetch_url($url, $referer = '', $timeout = null, $post_data = null, $headers = null) 323 | { 324 | // If we've been redirected too many times, end it. 325 | if ($this->redirects > 4) { 326 | $this->error = 606; 327 | return false; 328 | } 329 | 330 | // Set up our fake IP's. 331 | $ip = array( 332 | 'REMOTE_ADDR: ' . $this->ip, 333 | 'HTTP_X_FORWARDED_FOR: ' . $this->ip 334 | ); 335 | 336 | // If we have headers to send, send them. 337 | if ($headers) { 338 | $headers = array_merge($ip, $headers); 339 | } else { 340 | $headers = $ip; 341 | } 342 | 343 | $ch = curl_init(); 344 | curl_setopt($ch, CURLOPT_URL, $url); 345 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 346 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 347 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6'); 348 | curl_setopt($ch, CURLOPT_TIMEOUT, ($timeout) ? $timeout : $this->timeout); 349 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 350 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 351 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 352 | curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie_file); 353 | curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie_file); 354 | curl_setopt($ch, CURLOPT_AUTOREFERER, true); 355 | 356 | if (!empty($referer)) { 357 | curl_setopt($ch, CURLOPT_REFERER, $referer); 358 | } 359 | 360 | // Are we POSTing? 361 | if (!empty($post_data)) { 362 | curl_setopt($ch, CURLOPT_POST, true); 363 | curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); 364 | } 365 | 366 | $result = curl_exec($ch); 367 | 368 | // Checking for redirects 369 | if (stripos($result, 'Object moved') !== false) { 370 | $follow = urldecode(trim($this->find($result, ''))); 371 | 372 | if (substr($follow, 0, 1) == '/') { 373 | $this->redirects++; 374 | $result = $this->fetch_url('http://live.xbox.com' . $follow, $url); 375 | } else { 376 | $this->redirects++; 377 | $result = $this->fetch_url($follow, $url); 378 | } 379 | } else if (stripos($result, '