├── src └── Companion │ ├── Utils │ ├── Console.php │ ├── PBKDF2.php │ └── ID.php │ ├── Exceptions │ ├── CompanionException.php │ ├── TokenExpiredException.php │ ├── CompanionServerException.php │ └── InvalidStatusCodeException.php │ ├── Models │ ├── Method.php │ ├── CompanionResponse.php │ └── CompanionRequest.php │ ├── Http │ ├── Pem.php │ ├── public-key.pem │ ├── CookiesJar.php │ ├── Cookies.php │ └── Sight.php │ ├── Api │ ├── Report.php │ ├── Token.php │ ├── Item.php │ ├── ChatRoom.php │ ├── AddressBook.php │ ├── Payments.php │ ├── Schedule.php │ ├── Market.php │ ├── Login.php │ └── Account.php │ ├── Config │ ├── SightConfig.php │ ├── SightToken.php │ ├── CompanionSight.php │ └── CompanionTokenManager.php │ └── CompanionApi.php ├── .gitignore ├── bin ├── config.php.dist ├── cli └── cli_async ├── composer.json ├── LICENSE ├── KNOWN_ERRORS.md ├── README.md └── composer.lock /src/Companion/Utils/Console.php: -------------------------------------------------------------------------------- 1 | '', 8 | 'pass' => '', 9 | ]; 10 | -------------------------------------------------------------------------------- /src/Companion/Models/Method.php: -------------------------------------------------------------------------------- 1 | setDomain('secure.square-enix.com'); 14 | $cookie->setExpires(time() + (60*60*24*6)); 15 | $cookie->setPath('/'); 16 | $cookie->setSecure(true); 17 | 18 | return parent::setCookie($cookie); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xivapi/companion-php", 3 | "type": "library", 4 | "keywords": ["ffxiv","php","companion"], 5 | "description": "A library for interacting with the FFXIV Companion App API", 6 | "homepage": "https://github.com/xivapi/companion-php", 7 | "license": "MIT", 8 | "require": { 9 | "guzzlehttp/guzzle": "~6.0", 10 | "ramsey/uuid": "^3.8", 11 | "phpseclib/phpseclib": "^2.0", 12 | "rct567/dom-query": "^0.8.0" 13 | }, 14 | "autoload": { 15 | "psr-0": { 16 | "Companion\\": "src/" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-0": { 21 | "Companion\\Tests\\": "tests/" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Companion/Api/Report.php: -------------------------------------------------------------------------------- 1 | $value ]); 27 | 28 | $jar = self::get(); 29 | $jar->setCookie($cookie); 30 | $jar->save(self::FILENAME); 31 | 32 | self::$jar = $jar; 33 | } 34 | 35 | public static function clear() 36 | { 37 | @unlink(self::FILENAME); 38 | self::$jar = null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Companion/Utils/ID.php: -------------------------------------------------------------------------------- 1 | toString()); 23 | } 24 | 25 | /** 26 | * Refresh the static UUID 27 | */ 28 | public static function refresh() 29 | { 30 | self::$id = self::uuid(); 31 | } 32 | 33 | /** 34 | * Get the current static request UUID 35 | */ 36 | public static function get(): string 37 | { 38 | if (self::$id === null) { 39 | self::refresh(); 40 | } 41 | 42 | return self::$id; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Companion/Config/SightConfig.php: -------------------------------------------------------------------------------- 1 | response = $response; 16 | $this->uri = $uri; 17 | } 18 | 19 | public function getResponse(): Response 20 | { 21 | return $this->response; 22 | } 23 | 24 | public function getStatusCode(): int 25 | { 26 | return $this->response->getStatusCode(); 27 | } 28 | 29 | public function getJson() 30 | { 31 | return $this->response->getBody() ? json_decode($this->response->getBody()) : null; 32 | } 33 | 34 | public function getBody(): string 35 | { 36 | return (string)$this->response->getBody(); 37 | } 38 | 39 | public function getHeaders(): array 40 | { 41 | return $this->response->getHeaders(); 42 | } 43 | 44 | public function getUri(): string 45 | { 46 | return $this->uri; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Companion/Api/Token.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4()->toString(); 35 | $this->name = $name; 36 | $this->created = time(); 37 | } 38 | 39 | public function toArray(): array 40 | { 41 | return json_decode(json_encode($this), true); 42 | } 43 | 44 | public static function build(\stdClass $existing) 45 | { 46 | $obj = new SightToken(); 47 | $obj->id = $existing->id ?? Uuid::uuid4()->toString(); 48 | $obj->name = $existing->name ?? null; 49 | $obj->character = $existing->character ?? null; 50 | $obj->server = $existing->server ?? null; 51 | $obj->uid = $existing->uid ?? null; 52 | $obj->userId = $existing->userId ?? null; 53 | $obj->token = $existing->token ?? null; 54 | $obj->salt = $existing->salt ?? null; 55 | $obj->region = $existing->region ?? null; 56 | $obj->created = $existing->created ?? null; 57 | $obj->updated = $existing->updated ?? null; 58 | return $obj; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Companion/Config/CompanionSight.php: -------------------------------------------------------------------------------- 1 | 30, 14 | // should the guzzle client verify the sight https cert 15 | 'CLIENT_VERIFY' => false, 16 | // should we keep looping to ensure a result from sight? 17 | 'QUERY_LOOPED' => true, 18 | // how many loops should sight perform? 19 | 'QUERY_LOOP_COUNT' => 5, 20 | // at what interval delays should sight perform, this is in micro-seconds 21 | 'QUERY_DELAY_MS' => 800 * 1000, 22 | // the estimated token expiry time, it should last 24 hours but SE are being inconsistent... 23 | 'TOKEN_EXPIRY_HRS' => 12, 24 | // a static request id 25 | 'REQUEST_ID' => null, 26 | ]; 27 | 28 | /** @var array */ 29 | private static $settings = []; 30 | /** @var bool */ 31 | private static $async = false; 32 | 33 | /** 34 | * Set an option 35 | */ 36 | public static function set($option, $value) 37 | { 38 | // multiply any query delays in milliseconds by 1000 39 | $value = $option === 'QUERY_DELAY_MS' ? ($value * 1000) : $value; 40 | 41 | self::$settings[$option] = $value; 42 | } 43 | 44 | /** 45 | * Get an option (or return default) 46 | */ 47 | public static function get($option) 48 | { 49 | return self::$settings[$option] ?? (self::$defaults[$option] ?? false); 50 | } 51 | 52 | /** 53 | * Switch to async mode 54 | */ 55 | public static function useAsync() 56 | { 57 | self::$async = true; 58 | } 59 | 60 | /** 61 | * State if in async mode or not 62 | */ 63 | public static function isAsync(): bool 64 | { 65 | return self::$async; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /bin/cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | Token()->get() ); 19 | 20 | /* 21 | echo "Getting login url ...\n\n"; 22 | $loginUrl = $api->Account()->getLoginUrl(); 23 | $tokenId = $api->Token()->get()->token; 24 | echo "Token: {$tokenId} - Login URL: \n\n{$loginUrl}\n\n"; 25 | die; 26 | */ 27 | 28 | // login with the profiles username+password, this is not stored anywhere 29 | // once this has been done once, you don't need to do it for 24 hours for the same profile 30 | echo "Logging into account: {$config->user}\n"; 31 | $api->Account()->login($config->user, $config->pass); 32 | 33 | // 34 | // Get API-ing! 35 | // 36 | 37 | // Get a list of characters 38 | echo "Getting a list of characters\n"; 39 | $cid = null; 40 | foreach ($api->Login()->getCharacters()->accounts[0]->characters as $character) { 41 | echo "- {$character->cid} :: {$character->name} ({$character->world})\n"; 42 | 43 | if ($cid === null) { 44 | $cid = $character->cid; 45 | } 46 | } 47 | 48 | // Login with a character, this should return the region 49 | echo "log-in in with a character\n"; 50 | $api->Login()->loginCharacter($cid); 51 | 52 | // Get current logged in character; 53 | $character = $api->Login()->getCharacter()->character; 54 | echo "- Logged in as: {$character->name} ({$character->world}) \n"; 55 | 56 | // we have to get the character status (something to do with World Visit when its out.) 57 | $api->Login()->getCharacterWorlds(); 58 | 59 | print_r( $api->Market()->getItemMarketListings(3) ); 60 | 61 | print_r( $api->Token()->get() ); 62 | 63 | // (optional) save our token 64 | $api->Token()->save(); 65 | -------------------------------------------------------------------------------- /src/Companion/Api/ChatRoom.php: -------------------------------------------------------------------------------- 1 | Token()->get(); 18 | 19 | // login if the token is not active 20 | if ($api->Token()->hasExpired($token->updated)) { 21 | echo "Token expired, logging in\n\n"; 22 | $api->Account()->login($config->user, $config->pass); 23 | $api->Login()->loginCharacter($config->cid); 24 | $api->Login()->getCharacterStatus(); 25 | } else { 26 | echo "Using existing token\n\n"; 27 | } 28 | 29 | // enable async 30 | $api->useAsync(); 31 | 32 | // build our requests, these uuid is just to have something random but this 33 | // could be anything like "our_item_id_123", so long as it's unique per request 34 | $promises = [ 35 | 'item_1' => $api->market()->getItemMarketListings(5), 36 | 'item_2' => $api->market()->getItemMarketListings(6), 37 | 'item_3' => $api->market()->getItemMarketListings(7), 38 | ]; 39 | 40 | // settle our promises and wait for the responses 41 | $results = $api->Sight()->settle($promises)->wait(); 42 | 43 | // handle our promise results 44 | $prices = $api->Sight()->handle($results); 45 | 46 | /** 47 | * $prices will now be an array in the same structure as our $promises 48 | * but with responses for each key. You will either get JSON decoded as a stdClass 49 | * or you will get an error response in the format: 50 | * [ 51 | * error : 1 52 | * state : rejected 53 | * reason : exception class -- error message 54 | * ] 55 | */ 56 | 57 | echo "\nResults of first request (should be empty)\n\n"; 58 | print_r($prices); 59 | echo "\nWaiting 3 seconds ....\n\n"; 60 | sleep(3); 61 | 62 | 63 | 64 | echo "\nResults of next request\n\n"; 65 | $results = $api->Sight()->settle($promises)->wait(); 66 | $prices = $api->Sight()->handle($results); 67 | 68 | print_r($prices); 69 | -------------------------------------------------------------------------------- /src/Companion/Api/AddressBook.php: -------------------------------------------------------------------------------- 1 | Method::DELETE, 23 | 'uri' => CompanionTokenManager::getToken()->region, 24 | 'endpoint' => "/address-book/blocklist", 25 | ]); 26 | 27 | return $this->json($req); 28 | } 29 | 30 | /** 31 | * todo - investigate: updatedAt seems to be a unix timestamp 32 | * @GET("address-book") 33 | */ 34 | public function getAddressBook(int $updatedAt = null) 35 | { 36 | $req = new CompanionRequest([ 37 | 'method' => Method::GET, 38 | 'uri' => CompanionTokenManager::getToken()->region, 39 | 'endpoint' => "/address-book", 40 | ]); 41 | 42 | return $this->json($req); 43 | } 44 | 45 | /** 46 | * todo - investigate: updatedAt seems to be a unix timestamp 47 | * @GET("address-book/{cid}/profile") 48 | */ 49 | public function getCharacter(string $characterId, int $updatedAt = null) 50 | { 51 | $req = new CompanionRequest([ 52 | 'method' => Method::GET, 53 | 'uri' => CompanionTokenManager::getToken()->region, 54 | 'endpoint' => "/address-book/{$characterId}/profile", 55 | ]); 56 | 57 | return $this->json($req); 58 | } 59 | 60 | /** 61 | * todo - investigate 62 | * @POST("address-book/blocklist") 63 | */ 64 | public function postBlockList(array $json = []) 65 | { 66 | $req = new CompanionRequest([ 67 | 'method' => Method::POST, 68 | 'uri' => CompanionTokenManager::getToken()->region, 69 | 'endpoint' => "/address-book/blocklist", 70 | ]); 71 | 72 | return $this->json($req); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /KNOWN_ERRORS.md: -------------------------------------------------------------------------------- 1 | # Known Error Codes 2 | 3 | These are codes which I have seen from the Companion API and observed their behaviour and what causes them. Since there is no way to just obtain a list I am instead writing them down as I come across them. 4 | 5 | ## List 6 | 7 | | Code | Information | 8 | |----------|---------------------------------------------------------------------------------------------------------------------------------------------------| 9 | | `100000` | Companion is down for maintenance, you will get 503 error code and the message "Under Maintenance" | 10 | | `111001` | Session has expired, the token will not work. Either it has been 24 hours since creation or it was not registered on the companion app correctly. | 11 | | `111099` | You tried access a world you're currently not visiting. | 12 | | `210010` | If you get this, it means "Unauthorized Access", basically like a Website 401, either login auth has updated (user agent, pem file, etc) or you've been banned. If you think you've been banned, login to Mogstation and it will say "Account Suspended", you can still login in-game.... until the next banwave. | 13 | | `311004` | Don't know what causes this, maybe not logging into game for some time? | 14 | | `311006` | You have no characters. | 15 | | `311007` | App says session expired, but it's because the cookie wasn't set properly (or at all) so you're technically not logged in | 16 | | `311009` | likely not confirmed character status, happens if you do not callthat endpoint | 17 | | `319201` | Server being access has gone down for emergency maintenance | 18 | | `340000` | Shit broke on SE's end. Seems generic. | 19 | -------------------------------------------------------------------------------- /src/Companion/Api/Payments.php: -------------------------------------------------------------------------------- 1 | json( 22 | new CompanionRequest([ 23 | 'method' => Method::POST, 24 | 'uri' => CompanionTokenManager::getToken()->region, 25 | 'endpoint' => "/points/kupo-nuts", 26 | ]) 27 | ); 28 | } 29 | 30 | /** 31 | * @PUT("points/mog-coins/android") 32 | */ 33 | public function finishPurchase(array $json) 34 | { 35 | 36 | } 37 | 38 | /** 39 | * @GET("purchase/charge") 40 | */ 41 | public function getBillingAmount() 42 | { 43 | 44 | } 45 | 46 | /** 47 | * @GET("purchase/cesa-limit") 48 | */ 49 | public function getBillingLimits(string $updatedAt = null) 50 | { 51 | 52 | } 53 | 54 | /** 55 | * @GET("purchase/user-birth") 56 | */ 57 | public function getBirthDate() 58 | { 59 | 60 | } 61 | 62 | /** 63 | * @GET("points/history") 64 | */ 65 | public function getCurrencyHistory(int $type = null) 66 | { 67 | 68 | } 69 | 70 | /** 71 | * @GET("points/status") 72 | */ 73 | public function getCurrencyStatus() 74 | { 75 | return $this->json( 76 | new CompanionRequest([ 77 | 'method' => Method::GET, 78 | 'uri' => CompanionTokenManager::getToken()->region, 79 | 'endpoint' => "/points/status", 80 | ]) 81 | ); 82 | } 83 | 84 | /** 85 | * @GET("points/products") 86 | */ 87 | public function getProducts() 88 | { 89 | 90 | } 91 | 92 | /** 93 | * @POST("purchase/user-birth") 94 | */ 95 | public function postBirthDate(array $json) 96 | { 97 | 98 | } 99 | 100 | /** 101 | * @POST("points/interrupted-process") 102 | */ 103 | public function resumeTransaction() 104 | { 105 | 106 | } 107 | 108 | /** 109 | * @POST("purchase/transaction") 110 | */ 111 | public function setTransactionLock(array $json) 112 | { 113 | 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Companion/Api/Schedule.php: -------------------------------------------------------------------------------- 1 | Token()->set($token); 31 | } 32 | } 33 | 34 | /** 35 | * Provides nicer access to static token methods 36 | */ 37 | public function Token(): Token 38 | { 39 | return $this->getClass(Token::class); 40 | } 41 | 42 | /** 43 | * Switch to async mode 44 | */ 45 | public function useAsync(): self 46 | { 47 | CompanionSight::useAsync(); 48 | return $this; 49 | } 50 | 51 | /** 52 | * -------------------------------------------------------------------------- 53 | * Sight API 54 | * -------------------------------------------------------------------------- 55 | */ 56 | 57 | public function Account(): Account 58 | { 59 | return $this->getClass(Account::class); 60 | } 61 | 62 | public function AddressBook(): AddressBook 63 | { 64 | return $this->getClass(AddressBook::class); 65 | } 66 | 67 | public function ChatRoom(): ChatRoom 68 | { 69 | return $this->getClass(ChatRoom::class); 70 | } 71 | 72 | public function Item(): Item 73 | { 74 | return $this->getClass(Item::class); 75 | } 76 | 77 | public function Login(): Login 78 | { 79 | return $this->getClass(Login::class); 80 | } 81 | 82 | public function Market(): Market 83 | { 84 | return $this->getClass(Market::class); 85 | } 86 | 87 | public function Payments(): Payments 88 | { 89 | return $this->getClass(Payments::class); 90 | } 91 | 92 | public function Report(): Report 93 | { 94 | return $this->getClass(Report::class); 95 | } 96 | 97 | public function Schedule(): Schedule 98 | { 99 | return $this->getClass(Schedule::class); 100 | } 101 | 102 | public function Sight(): Sight 103 | { 104 | return $this->getClass(Sight::class); 105 | } 106 | 107 | /** 108 | * Either returns an existing initialized class or a new one 109 | */ 110 | private function getClass(string $className) 111 | { 112 | if (isset($this->classInstances[$className])) { 113 | return $this->classInstances[$className]; 114 | } 115 | 116 | $class = new $className(); 117 | $this->classInstances[$className] = $class; 118 | 119 | return $class; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Companion/Config/CompanionTokenManager.php: -------------------------------------------------------------------------------- 1 | updated = time(); 40 | 41 | $tokens = self::loadTokens(); 42 | $tokens->{self::$token->name} = self::$token->toArray(); 43 | $tokens = json_encode($tokens, JSON_PRETTY_PRINT); 44 | 45 | file_put_contents(self::$tokenFilename, $tokens); 46 | } 47 | 48 | /** 49 | * Load our existing tokens 50 | */ 51 | public static function loadTokens(string $tokenName = null) 52 | { 53 | if (self::$tokenFilename === null) { 54 | return null; 55 | } 56 | 57 | $tokens = file_get_contents(self::$tokenFilename); 58 | $tokens = json_decode($tokens); 59 | 60 | if ($tokens && $tokenName) { 61 | return $tokens->{$tokenName} ?? null; 62 | } 63 | 64 | return $tokens; 65 | } 66 | 67 | /** 68 | * States if a token has expired or not via a provided timestamp, the reason 69 | * the library does not maintain a timestamp is because there are various stages when 70 | * a "confirmed login" is, such as getting a character, or manually logging in. It is 71 | * the developers job to maintain a logged in state based on their rules. 72 | */ 73 | public static function hasExpired($timestamp) 74 | { 75 | return $timestamp < (time() - (60 * 60 * CompanionSight::get('TOKEN_EXPIRY_HRS'))); 76 | } 77 | 78 | /** 79 | * Get the current token 80 | */ 81 | public static function getToken() 82 | { 83 | return self::$token; 84 | } 85 | 86 | /** 87 | * Set the token to use 88 | */ 89 | public static function setToken($token): void 90 | { 91 | 92 | if (is_string($token)) { 93 | // try load an existing token 94 | if ($existing = self::loadTokens($token)) { 95 | self::$token = SightToken::build($existing); 96 | return; 97 | } 98 | 99 | // create a new token 100 | self::$token = new SightToken($token); 101 | return; 102 | } 103 | 104 | // if this is already a sight token, use it. 105 | if (get_class($token) == SightToken::class) { 106 | self::$token = $token; 107 | return; 108 | } 109 | 110 | // build from existing token provided 111 | self::$token = SightToken::build($token); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Companion/Models/CompanionRequest.php: -------------------------------------------------------------------------------- 1 | true, 27 | 'tracked_redirects' => true 28 | ]; 29 | public $return202 = false; 30 | public $headers = []; 31 | public $json = []; 32 | public $form = []; 33 | public $query = []; 34 | public $cookies = []; 35 | 36 | public function __construct(array $config) 37 | { 38 | $config = (Object)$config; 39 | $this->method = $config->method; 40 | $this->uri = $config->uri; 41 | $this->version = $config->version ?? self::VERSION; 42 | $this->endpoint = $config->endpoint ?? null; 43 | $this->json = $config->json ?? []; 44 | $this->form = $config->form ?? []; 45 | $this->query = $config->query ?? []; 46 | $this->cookies = $config->cookies ?? []; 47 | $this->redirect = $config->redirect ?? $this->redirect; 48 | $this->return202 = $config->return202 ?? $this->return202; 49 | 50 | // if we're on SE secure domain, remove version 51 | if (stripos($this->uri, self::URI_SE)) { 52 | $this->version = null; 53 | } 54 | 55 | $this->headers['Accept'] = $config->accept ?? SightConfig::ACCEPT; 56 | $this->headers['Accept-Language'] = $config->acceptLanguage ?? SightConfig::ACCEPT_LANGUAGE; 57 | $this->headers['Accept-Encoding'] = $config->acceptEncoding ?? SightConfig::ACCEPT_ENCODING; 58 | $this->headers['User-Agent'] = $config->userAgent ?? SightConfig::USER_AGENT; 59 | $this->headers['request-id'] = $config->requestId ?? ID::uuid(); 60 | $this->headers['token'] = $config->token ?? CompanionTokenManager::getToken()->token; 61 | 62 | // use any hard coded request ids 63 | $this->headers['request-id'] = CompanionSight::get('REQUEST_ID') ?: $this->headers['request-id']; 64 | $this->headers['request-id'] = strtoupper($this->headers['request-id']); 65 | 66 | // only set content type when the version exists (thus hitting the API) 67 | if ($this->version) { 68 | $this->headers['Content-Type'] = $config->contentType ?? SightConfig::CONTENT_TYPE; 69 | } 70 | 71 | $this->headers = array_merge($this->headers, $config->headers ?? []); 72 | } 73 | 74 | public function setMethod(string $method): self 75 | { 76 | $this->method = $method; 77 | return $this; 78 | } 79 | 80 | public function setRequestId(string $requestId): self 81 | { 82 | $this->headers['request-id'] = $requestId; 83 | return $this; 84 | } 85 | 86 | public function setAsync(): self 87 | { 88 | $this->async = true; 89 | return $this; 90 | } 91 | 92 | public function getUri() 93 | { 94 | return $this->uri . $this->version . $this->endpoint; 95 | } 96 | 97 | public function getOptions() 98 | { 99 | $options = [ 100 | // force redirect check on as this could be false 101 | RequestOptions::ALLOW_REDIRECTS => $this->redirect 102 | ]; 103 | 104 | $map = [ 105 | RequestOptions::HEADERS => $this->headers, 106 | RequestOptions::JSON => $this->json, 107 | RequestOptions::FORM_PARAMS => $this->form, 108 | RequestOptions::QUERY => $this->query, 109 | RequestOptions::COOKIES => $this->cookies, 110 | ]; 111 | 112 | foreach ($map as $requestOption => $requestValues) { 113 | if (empty($requestValues)) { 114 | continue; 115 | } 116 | 117 | $options[$requestOption] = $requestValues; 118 | } 119 | 120 | return $options; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | # Companion PHP 4 | 5 | A library for interacting with the FFXIV Companion App API 6 | 7 | To learn more about the FFXIV Companion App, read the [Research Doc](https://github.com/viion/ffxiv-datamining/blob/master/docs/CompanionAppApi.md). 8 | 9 | ## If you cannot use PHP 10 | 11 | XIVAPI provides endpoints that can give you a companion token with your own account and even let you query the market board. It provides both automated login or providing the SE Official Login. This is useful if you want to use your own account but only know frontend dev (eg: JavaScript), or you're building an Electron app. 12 | 13 | If you'd like to try this, please message **Vekien#3458** on discord for endpoint information. 14 | 15 | ## Terminology: 16 | 17 | - **Companion**: The Official FFXIV Companioon Mobile App. 18 | - **Sight**: The API that SE uses within the app to talk to the Game Servers. 19 | 20 | ## Library Documentation 21 | 22 | - [Ban Disclaimer](https://github.com/xivapi/companion-php/wiki/Ban-Disclaimer) - Please read this. 23 | - [Getting Started](https://github.com/xivapi/companion-php/wiki/Getting-Started) 24 | - `composer require xivapi/companion-php` 25 | - [Tokens](https://github.com/xivapi/companion-php/wiki/Tokens) 26 | - Information on how tokens work, and understanding the `SightToken` object. 27 | - [Tokens: Manual Login](https://github.com/xivapi/companion-php/wiki/Tokens:-Manual) 28 | - [Tokens: Automatic Login](https://github.com/xivapi/companion-php/wiki/Tokens:-Automatic) 29 | 30 | 31 | ----- 32 | 33 | ## Character Select Process 34 | 35 | In order to use the Sight API you need a valid character with an active Subscription (it cannot be in the free trial period). A character does not need to be at any "stage" in the game, a brand new-never logged into character will have full market board access the moment you enter a name and click "create". 36 | 37 | 38 | ### Selecting a character 39 | 40 | To access the market we first need to select our character and this requires knowing the unique "Character ID". We can find this by listing out our characters: 41 | 42 | ```php 43 | foreach ($api->Login()->getCharacters()->accounts[0]->characters as $character) { 44 | echo "- {$character->cid} :: {$character->name} ({$character->world})\n"; 45 | } 46 | ``` 47 | 48 | Characters have a unique Character ID property known as: `cid`. This ID is not the one you see on Lodestone or anything you'll be familiar with, once you find the character you want take its `cid` over to the `loginCharacter` function, for example: 49 | 50 | ```php 51 | $api->login()->loginCharacter('character_id'); 52 | ``` 53 | 54 | This will confirm with Sight that you want to use this character, you can confirm yourself by performing the following optional code: 55 | 56 | ```php 57 | // Get current logged in character#echo "Confirming logged in character ...\n"; 58 | $character = $api->login()->getCharacter()->character; 59 | echo "- Logged in as: {$character->name} ({$character->world}) \n"; 60 | ``` 61 | 62 | Now that we've told it what character to use, we have to confirm its worlds status. This is a new addition in Patch 4.4 and 63 | I believe it will be used for when World Visit system is in place. For now it is a requirement and returns the current world and your home world: 64 | 65 | ```php 66 | $api->login()->getCharacterWorlds(); 67 | ``` 68 | 69 | Once you have done this, you can now access the market board as well as any other Sight endpoint! You can find all API calls below. 70 | 71 | ## Async Requests 72 | 73 | The library supports async requests for all main data actions (not Account or Login actions). This allows you to query multiple endpoints at the same time. 74 | 75 | When you enable async it will by pass the "garauntee response" feature. The Sight API doesn't provide data in the first request, instead it will take your request and queue it up, you then need to perform the same request again to get the response. Usually a Sight request will fulfill within 3 seconds, so you could 20 concurrent requests, wait 3 seconds and then perform the same 20 concurrent requests again using the same Request ID to get your response. 76 | 77 | You can view an example of Async usage in `bin/cli_async` 78 | 79 | ## API 80 | 81 | ### Account 82 | 83 | ### AddressBook 84 | 85 | ### ChatRoom 86 | 87 | ### Item 88 | 89 | ### Login 90 | 91 | ### Market 92 | 93 | #### `$api->market()->getItemMarketListings(int $itemId)` 94 | Get the current Market Prices for an item. The item id must be the in-game dat value which is an integer, not the one on Lodestone. 95 | 96 | #### `$api->market()->getTransactionHistory(int $itemId)` 97 | Get the current Market History for an item. The item id must be the in-game dat value which is an integer, not the one on Lodestone. The maximum returned history is currently 20, this cannot be changed. 98 | 99 | #### `$api->market()->getMarketListingsByCategory(int $categoryId)` 100 | Get the current stock listings for a market category 101 | 102 | ### Payments 103 | 104 | ### Reports 105 | 106 | ### Schedule 107 | 108 | ## Testing 109 | 110 | - `php bin/cli` - Have a look into thisclass for examples 111 | 112 | --- 113 | 114 | **Library by:** Josh Freeman (Vekien on Discord: (XIVAPI) https://discord.gg/MFFVHWC) 115 | 116 | License is MIT, do whatever you want with it :) 117 | -------------------------------------------------------------------------------- /src/Companion/Api/Market.php: -------------------------------------------------------------------------------- 1 | server; 39 | 40 | if ($server == null) { 41 | throw new CompanionServerException('You must provide a server with requests to this endpoint.'); 42 | } 43 | 44 | return $this->json( 45 | new CompanionRequest([ 46 | 'method' => Method::GET, 47 | 'uri' => CompanionTokenManager::getToken()->region, 48 | 'endpoint' => "/market/items/catalog/{$itemId}/hq", 49 | 'query' => [ 50 | 'worldName' => $server 51 | ] 52 | ]) 53 | ); 54 | } 55 | 56 | /** 57 | * catalogId = itemId 58 | * @GET("market/items/catalog/{itemId}") 59 | */ 60 | public function getItemMarketListings(int $itemId, string $server = null) 61 | { 62 | $server = $server ?: CompanionTokenManager::getToken()->server; 63 | 64 | if ($server == null) { 65 | throw new CompanionServerException('You must provide a server with requests to this endpoint.'); 66 | } 67 | 68 | return $this->json( 69 | new CompanionRequest([ 70 | 'method' => Method::GET, 71 | 'uri' => CompanionTokenManager::getToken()->region, 72 | 'endpoint' => "/market/items/catalog/{$itemId}", 73 | 'query' => [ 74 | 'worldName' => $server 75 | ] 76 | ]) 77 | ); 78 | } 79 | 80 | /** 81 | * @GET("market/items/category/{categoryId}") 82 | */ 83 | public function getMarketListingsByCategory(int $categoryId, string $server = null) 84 | { 85 | $server = $server ?: CompanionTokenManager::getToken()->server; 86 | 87 | if ($server == null) { 88 | throw new CompanionServerException('You must provide a server with requests to this endpoint.'); 89 | } 90 | 91 | return $this->json( 92 | new CompanionRequest([ 93 | 'method' => Method::GET, 94 | 'uri' => CompanionTokenManager::getToken()->region, 95 | 'endpoint' => "/market/items/category/{$categoryId}", 96 | 'query' => [ 97 | 'worldName' => $server 98 | ] 99 | ]) 100 | ); 101 | } 102 | 103 | /** 104 | * @GET("market/retainers/{cid}") 105 | */ 106 | public function getRetainerInfo(string $cid) 107 | { 108 | return $this->json( 109 | new CompanionRequest([ 110 | 'method' => Method::GET, 111 | 'uri' => CompanionTokenManager::getToken()->region, 112 | 'endpoint' => "/market/retainers/{$cid}", 113 | ]) 114 | ); 115 | } 116 | 117 | /** 118 | * @GET("market/items/history/catalog/{itemId}") 119 | */ 120 | public function getTransactionHistory(int $itemId, string $server = null) 121 | { 122 | $server = $server ?: CompanionTokenManager::getToken()->server; 123 | 124 | if ($server == null) { 125 | throw new CompanionServerException('You must provide a server with requests to this endpoint.'); 126 | } 127 | 128 | return $this->json( 129 | new CompanionRequest([ 130 | 'method' => Method::GET, 131 | 'uri' => CompanionTokenManager::getToken()->region, 132 | 'endpoint' => "/market/items/history/catalog/{$itemId}", 133 | 'query' => [ 134 | 'worldName' => $server 135 | ] 136 | ]) 137 | ); 138 | } 139 | 140 | /** 141 | * @POST("market/item") 142 | */ 143 | public function purchaseItem(int $pointType = null, array $json = []) 144 | { 145 | 146 | } 147 | 148 | /** 149 | * @POST("market/retainers/{cid}/rack") 150 | */ 151 | public function registerListing(string $cid, int $pointType = null, array $json = []) 152 | { 153 | 154 | } 155 | 156 | /** 157 | * @POST("market/retainers/{cid}") 158 | */ 159 | public function resumeListing(string $cid) 160 | { 161 | 162 | } 163 | 164 | /** 165 | * @POST("market/payment/transaction") 166 | */ 167 | public function setTransactionLock() 168 | { 169 | 170 | } 171 | 172 | /** 173 | * @DELETE("market/retainers/{cid}") 174 | */ 175 | public function stopListing(string $cid) 176 | { 177 | 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Companion/Http/Sight.php: -------------------------------------------------------------------------------- 1 | handleRequest($req)->getJson(); 28 | } 29 | 30 | /** 31 | * Perform a request and return the body using a CompanionRequest 32 | */ 33 | public function body(CompanionRequest $req) 34 | { 35 | return $this->handleRequest($req)->getBody(); 36 | } 37 | 38 | /** 39 | * Perform a request and return the status using a CompanionRequest 40 | */ 41 | public function statusCode(CompanionRequest $req) 42 | { 43 | return $this->handleRequest($req)->getStatusCode(); 44 | } 45 | 46 | /** 47 | * Send a request to the Companion API 48 | */ 49 | private function handleRequest(CompanionRequest $request) 50 | { 51 | $client = new Client([ 52 | 'cookies' => Cookies::get(), 53 | 'timeout' => CompanionSight::get('CLIENT_TIMEOUT'), 54 | 'verify' => CompanionSight::get('CLIENT_VERIFY'), 55 | ]); 56 | 57 | $uri = $request->getUri(); 58 | $options = $request->getOptions(); 59 | 60 | // if async, return the request 61 | if (CompanionSight::isAsync()) { 62 | return $client->{$request->method}($uri, $options); 63 | } 64 | 65 | // if we're not looping query, perform it and return response 66 | if (CompanionSight::get('QUERY_LOOPED') === false) { 67 | return new CompanionResponse( 68 | $client->{$request->method}($uri, $options), $uri 69 | ); 70 | } 71 | 72 | $loopCount = CompanionSight::get('QUERY_LOOP_COUNT'); 73 | $loopDelay = CompanionSight::get('QUERY_DELAY_MS'); 74 | 75 | // query multiple times, as SE provide a "202" Accepted which is 76 | // their way of saying "Soon(tm)", so... try again. 77 | foreach (range(0, $loopCount) as $i) { 78 | /** @var Response $response */ 79 | $response = $client->{$request->method}($uri, $options); 80 | 81 | // if the response is 202, try again 82 | if (!$request->return202 && $response->getStatusCode() == 202) { 83 | // wait half a second 84 | usleep($loopDelay); 85 | continue; 86 | } 87 | 88 | return new CompanionResponse($response, $uri); 89 | } 90 | 91 | $loopDelayText = ceil($loopDelay / 1000); 92 | $lastResponseCode = isset($response) ? $response->getStatusCode() : 'None'; 93 | throw new \Exception("No valid response from companion, Loops: {$loopCount}/{$loopDelayText}ms - Last code: {$lastResponseCode}"); 94 | } 95 | 96 | // ------------------------------------------------ 97 | // Async logic 98 | // ------------------------------------------------ 99 | 100 | /** 101 | * Settle each request 102 | */ 103 | public function settle($promises) 104 | { 105 | return Promise\settle( 106 | $this->buildPromiseRequests($promises) 107 | ); 108 | } 109 | 110 | /** 111 | * Settle each request 112 | */ 113 | public function unwrap($promises) 114 | { 115 | return Promise\unwrap( 116 | $this->buildPromiseRequests($promises) 117 | ); 118 | } 119 | 120 | /** 121 | * Fulfill promise results 122 | */ 123 | public function handle($results): \stdClass 124 | { 125 | $unwrapped = (Object)[]; 126 | foreach ($results as $key => $response) { 127 | // convert to object 128 | $response = (Object)$response; 129 | 130 | // unwrap to our key 131 | $unwrapped->{$key} = ($response->state == 'fulfilled') 132 | ? (new CompanionResponse($response->value))->getJson() 133 | : (Object)[ 134 | 'error' => true, 135 | 'state' => $response->state, 136 | 'reason' => get_class($response->reason) ." -- ". $response->reason->getMessage() 137 | ]; 138 | } 139 | 140 | return $unwrapped; 141 | } 142 | 143 | /** 144 | * Builds up promise requests using static request ids 145 | */ 146 | private function buildPromiseRequests($promises) 147 | { 148 | /** @var CompanionRequest $request */ 149 | foreach ($promises as $requestId => $request) { 150 | // force an assigned request id 151 | $request->setRequestId($requestId); 152 | 153 | // if the request is not already async converted, do it 154 | if ($request->async === false) { 155 | // modify the method to async 156 | $request->setMethod("{$request->method}Async"); 157 | 158 | // ensure request is marked as async so we don't repeat this step 159 | $request->setAsync(); 160 | } 161 | 162 | // run the async request 163 | $promises[$requestId] = $this->handleRequest($request); 164 | } 165 | 166 | return $promises; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Companion/Api/Login.php: -------------------------------------------------------------------------------- 1 | json( 35 | new CompanionRequest([ 36 | 'method' => Method::POST, 37 | 'uri' => CompanionRequest::URI, 38 | 'endpoint' => '/login/auth', 39 | 'requestId' => ID::get(), 40 | 'query' => [ 41 | 'token' => CompanionTokenManager::getToken()->token, 42 | 'uid' => CompanionTokenManager::getToken()->uid, 43 | 'request_id' => ID::get() 44 | ], 45 | ]) 46 | ); 47 | } 48 | 49 | /** 50 | * @GET("login/character") 51 | */ 52 | public function getCharacter() 53 | { 54 | // log the character into the regional data center endpoint 55 | $res = $this->json( 56 | new CompanionRequest([ 57 | 'method' => Method::GET, 58 | 'uri' => CompanionTokenManager::getToken()->region, 59 | 'endpoint' => "/login/character", 60 | ]) 61 | ); 62 | 63 | // record character in token 64 | CompanionTokenManager::getToken()->character = $res->character->cid; 65 | CompanionTokenManager::getToken()->server = $res->character->world; 66 | CompanionTokenManager::saveTokens(); 67 | 68 | return $res; 69 | } 70 | 71 | /** 72 | * @GET("login/characters") 73 | */ 74 | public function getCharacters() 75 | { 76 | return $this->json( 77 | new CompanionRequest([ 78 | 'method' => Method::GET, 79 | 'uri' => CompanionRequest::URI, 80 | 'endpoint' => '/login/characters', 81 | ]) 82 | ); 83 | } 84 | 85 | /** 86 | * This will return the data center regional domain and log-ins this specific character 87 | * 88 | * @POST("login/characters/{characterId}") 89 | */ 90 | public function loginCharacter(string $characterId = null) 91 | { 92 | // log the character into the base endpoint 93 | $res = $this->json( 94 | new CompanionRequest([ 95 | 'method' => Method::POST, 96 | 'uri' => CompanionRequest::URI, 97 | 'endpoint' => "/login/characters/{$characterId}", 98 | 'json' => [ 99 | // This is the language of your app 100 | 'appLocaleType' => SightConfig::APP_LOCALE_TYPE, 101 | ] 102 | ]) 103 | ); 104 | 105 | CompanionTokenManager::getToken()->region = substr($res->region, 0, -1); 106 | CompanionTokenManager::saveTokens(); 107 | 108 | // call get character on DC as this will log it in. 109 | $this->getCharacter(); 110 | } 111 | 112 | /** 113 | * Get character status 114 | */ 115 | public function getCharacterStatus() 116 | { 117 | return $this->json( 118 | new CompanionRequest([ 119 | 'method' => Method::GET, 120 | 'uri' => CompanionTokenManager::getToken()->region, 121 | 'endpoint' => '/character/login-status', 122 | ]) 123 | ); 124 | } 125 | 126 | /** 127 | * Get character worlds, this shows what world you're currently in 128 | * and what your home world is. 129 | */ 130 | public function getCharacterWorlds() 131 | { 132 | return $this->json( 133 | new CompanionRequest([ 134 | 'method' => Method::GET, 135 | 'uri' => CompanionTokenManager::getToken()->region, 136 | 'endpoint' => '/character/worlds', 137 | ]) 138 | ); 139 | } 140 | 141 | /** 142 | * Get the uri region for the logged in character. 143 | * Sometimes returns blank... Unsure why 144 | * 145 | * @GET("login/region") 146 | */ 147 | public function getRegion() 148 | { 149 | return $this->json( 150 | new CompanionRequest([ 151 | 'method' => Method::GET, 152 | 'uri' => CompanionRequest::URI, 153 | 'endpoint' => '/login/region', 154 | ]) 155 | ); 156 | } 157 | 158 | /** 159 | * refresh token (you get a new one) 160 | * 161 | * @POST("login/token") 162 | */ 163 | public function postToken() 164 | { 165 | return $this->json( 166 | new CompanionRequest([ 167 | 'method' => Method::POST, 168 | 'uri' => CompanionRequest::URI, 169 | 'endpoint' => '/login/token', 170 | 'json' => [ 171 | // not sure if this has to be the same UID or a new one 172 | // if it's a new one, need userId + salt 173 | 'uid' => CompanionTokenManager::getToken()->uid, 174 | 'appVersion' => SightConfig::APP_VERSION, 175 | 'platform' => SightConfig::PLATFORM_ANDROID, 176 | ] 177 | ]) 178 | ); 179 | } 180 | 181 | /** 182 | * todo - investigate 183 | * @POST("login/advertising-id") 184 | */ 185 | public function advertisingId() 186 | { 187 | return $this->json( 188 | new CompanionRequest([ 189 | 'method' => Method::POST, 190 | 'uri' => CompanionTokenManager::getToken()->region, 191 | 'endpoint' => '/login/advertising-id', 192 | 'json' => [ 193 | // This UUID always seems to be the same 194 | 'advertisingId' => SightConfig::ADVERTISING_ID, 195 | 'isTrackingEnabled' => SightConfig::ADVERTISING_ENABLED, 196 | ] 197 | ]) 198 | ); 199 | } 200 | 201 | /** 202 | * FCM Token = Firebase Cloud Messaging token - Used for App Notifications 203 | * - https://firebase.google.com/docs/cloud-messaging 204 | * 205 | * Figured out via java: ffxiv/sight/e/o.java 206 | * 207 | * No response body for this request 208 | * @POST("login/fcm-token") 209 | */ 210 | public function fcmToken() 211 | { 212 | return $this->json( 213 | new CompanionRequest([ 214 | 'method' => Method::POST, 215 | 'uri' => CompanionTokenManager::getToken()->region, 216 | 'endpoint' => '/login/fcm-token', 217 | 'json' => [ 218 | 'fcmToken' => SightConfig::FCM_TOKEN 219 | ] 220 | ]) 221 | ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Companion/Api/Account.php: -------------------------------------------------------------------------------- 1 | cleanup(); 33 | 34 | // generate a new token and build login uri 35 | $this->getLoginUrl(); 36 | 37 | // attempt to auto-login 38 | $this->autoLoginToProfileAccount($username, $password); 39 | 40 | // authenticate 41 | if ((new Login())->postAuth()->status !== 200) { 42 | throw new \Exception('Token status could not be validated'); 43 | } 44 | 45 | $this->cleanup(); 46 | } 47 | 48 | /** 49 | * Login to the character that is registered to this config profile 50 | */ 51 | public function getLoginUrl() 52 | { 53 | // Generate a new user uuid 54 | CompanionTokenManager::getToken()->userId = ID::uuid(); 55 | CompanionTokenManager::saveTokens(); 56 | 57 | // Get a token from SE 58 | $response = $this->getToken(); 59 | CompanionTokenManager::getToken()->token = $response->token; 60 | CompanionTokenManager::getToken()->salt = $response->salt; 61 | CompanionTokenManager::saveTokens(); 62 | 63 | // Get OAuth URI 64 | $this->loginUri = $this->buildLoginUri(); 65 | return $this->loginUri; 66 | } 67 | 68 | /** 69 | * Get a new valid token + salt from SE 70 | * @POST("/login/token") 71 | */ 72 | public function getToken() 73 | { 74 | $rsa = new RSA(); 75 | $rsa->loadKey(Pem::get()); 76 | $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1); 77 | $uid = base64_encode($rsa->encrypt(CompanionTokenManager::getToken()->userId)); 78 | 79 | return $this->json( 80 | new CompanionRequest([ 81 | 'method' => Method::POST, 82 | 'uri' => CompanionRequest::URI, 83 | 'endpoint' => '/login/token', 84 | 'requestId' => ID::get(), 85 | 'json' => [ 86 | 'appVersion' => SightConfig::APP_VERSION, 87 | 'platform' => SightConfig::PLATFORM_ANDROID, // < THIS IS IMPORTANT 88 | 'uid' => $uid 89 | ] 90 | ]) 91 | ); 92 | } 93 | 94 | /** 95 | * Automatically login to an account saved. 96 | * @throws \Exception 97 | */ 98 | private function autoLoginToProfileAccount(string $username, string $password) 99 | { 100 | $html = $this->body( 101 | new CompanionRequest([ 102 | 'method' => Method::GET, 103 | 'uri' => $this->loginUri, 104 | 'version' => '', 105 | 'requestId' => ID::get(), 106 | 'userAgent' => SightConfig::USER_AGENT_2, 107 | ]) 108 | ); 109 | 110 | // if this response contains "cis_sessid" then we was auto-logged in using cookies 111 | // otherwise it's the login form and we need to login to get the cis_sessid 112 | if (stripos($html, 'cis_sessid') !== true) { 113 | preg_match('/(.*)action="(?P[^"]+)">/', $html, $matches); 114 | $action = trim($matches['action']); 115 | preg_match('/(.*)name="_STORED_" value="(?P[^"]+)">/', $html, $matches); 116 | $stored = trim($matches['stored']); 117 | 118 | // build payload to submit form 119 | $formData = [ 120 | '_STORED_' => $stored, 121 | 'sqexid' => $username, 122 | 'password' => $password, 123 | ]; 124 | 125 | $html = $this->body( 126 | new CompanionRequest([ 127 | 'method' => Method::POST, 128 | 'uri' => CompanionRequest::URI_SE . "/oauth/oa/{$action}", 129 | 'version' => '', 130 | 'requestId' => ID::get(), 131 | 'userAgent' => SightConfig::USER_AGENT_2, 132 | 'form' => $formData, 133 | ]) 134 | ); 135 | } 136 | 137 | // todo - convert to: https://github.com/xivapi/companion-php 138 | preg_match('/(.*)action="(?P[^"]+)">/', $html, $matches); 139 | $action = html_entity_decode($matches['action']); 140 | preg_match('/(.*)name="cis_sessid" type="hidden" value="(?P[^"]+)">/', $html, $matches); 141 | $cis_sessid = trim($matches['cis_sessid']); 142 | 143 | $formData = [ 144 | 'cis_sessid' => $cis_sessid, 145 | 'provision' => '', // ??? - Don't know what this is but doesn't seem to matter 146 | '_c' => 1 // ??? - Don't know what this is but doesn't seem to matter 147 | ]; 148 | 149 | // submit to companion to confirm cis_sessid 150 | $req = new CompanionRequest([ 151 | 'method' => Method::POST, 152 | 'uri' => $action, 153 | 'form' => $formData, 154 | 'version' => '', 155 | 'requestId' => ID::get(), 156 | 'userAgent' => SightConfig::USER_AGENT_2, 157 | 'return202' => true, 158 | ]); 159 | 160 | // this will be another form with some other bits that the app just forcefully submits via js 161 | if ($this->statusCode($req) !== 202) { 162 | throw new \Exception('Login status could not be validated.'); 163 | } 164 | } 165 | 166 | /** 167 | * Build the Login uri 168 | */ 169 | private function buildLoginUri() 170 | { 171 | return CompanionRequest::SQEX_AUTH_URI .'?'. http_build_query([ 172 | 'client_id' => 'ffxiv_comapp', 173 | 'lang' => 'en-gb', 174 | 'response_type' => 'code', 175 | 'redirect_uri' => $this->buildCompanionOAuthRedirectUri(), 176 | ]); 177 | } 178 | 179 | /** 180 | * Build the login redirect uri 181 | */ 182 | private function buildCompanionOAuthRedirectUri() 183 | { 184 | $uid = PBKDF2::encrypt( 185 | CompanionTokenManager::getToken()->userId, 186 | CompanionTokenManager::getToken()->salt 187 | ); 188 | 189 | CompanionTokenManager::getToken()->uid = $uid; 190 | CompanionTokenManager::saveTokens(); 191 | 192 | return CompanionRequest::OAUTH_CALLBACK .'?'. http_build_query([ 193 | 'token' => CompanionTokenManager::getToken()->token, 194 | 'uid' => $uid, 195 | 'request_id' => ID::get(), 196 | ]); 197 | } 198 | 199 | /** 200 | * Clean up static set data, this is required if you login multiple times during the same PHP runtime. 201 | */ 202 | private function cleanup() 203 | { 204 | // delete all cookies 205 | Cookies::clear(); 206 | 207 | // refresh static ID 208 | ID::refresh(); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9c29a0198d3845f6e7edecc1ece65963", 8 | "packages": [ 9 | { 10 | "name": "guzzlehttp/guzzle", 11 | "version": "6.3.3", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/guzzle/guzzle.git", 15 | "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", 20 | "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "guzzlehttp/promises": "^1.0", 25 | "guzzlehttp/psr7": "^1.4", 26 | "php": ">=5.5" 27 | }, 28 | "require-dev": { 29 | "ext-curl": "*", 30 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", 31 | "psr/log": "^1.0" 32 | }, 33 | "suggest": { 34 | "psr/log": "Required for using the Log middleware" 35 | }, 36 | "type": "library", 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "6.3-dev" 40 | } 41 | }, 42 | "autoload": { 43 | "files": [ 44 | "src/functions_include.php" 45 | ], 46 | "psr-4": { 47 | "GuzzleHttp\\": "src/" 48 | } 49 | }, 50 | "notification-url": "https://packagist.org/downloads/", 51 | "license": [ 52 | "MIT" 53 | ], 54 | "authors": [ 55 | { 56 | "name": "Michael Dowling", 57 | "email": "mtdowling@gmail.com", 58 | "homepage": "https://github.com/mtdowling" 59 | } 60 | ], 61 | "description": "Guzzle is a PHP HTTP client library", 62 | "homepage": "http://guzzlephp.org/", 63 | "keywords": [ 64 | "client", 65 | "curl", 66 | "framework", 67 | "http", 68 | "http client", 69 | "rest", 70 | "web service" 71 | ], 72 | "time": "2018-04-22T15:46:56+00:00" 73 | }, 74 | { 75 | "name": "guzzlehttp/promises", 76 | "version": "v1.3.1", 77 | "source": { 78 | "type": "git", 79 | "url": "https://github.com/guzzle/promises.git", 80 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" 81 | }, 82 | "dist": { 83 | "type": "zip", 84 | "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", 85 | "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", 86 | "shasum": "" 87 | }, 88 | "require": { 89 | "php": ">=5.5.0" 90 | }, 91 | "require-dev": { 92 | "phpunit/phpunit": "^4.0" 93 | }, 94 | "type": "library", 95 | "extra": { 96 | "branch-alias": { 97 | "dev-master": "1.4-dev" 98 | } 99 | }, 100 | "autoload": { 101 | "psr-4": { 102 | "GuzzleHttp\\Promise\\": "src/" 103 | }, 104 | "files": [ 105 | "src/functions_include.php" 106 | ] 107 | }, 108 | "notification-url": "https://packagist.org/downloads/", 109 | "license": [ 110 | "MIT" 111 | ], 112 | "authors": [ 113 | { 114 | "name": "Michael Dowling", 115 | "email": "mtdowling@gmail.com", 116 | "homepage": "https://github.com/mtdowling" 117 | } 118 | ], 119 | "description": "Guzzle promises library", 120 | "keywords": [ 121 | "promise" 122 | ], 123 | "time": "2016-12-20T10:07:11+00:00" 124 | }, 125 | { 126 | "name": "guzzlehttp/psr7", 127 | "version": "1.6.1", 128 | "source": { 129 | "type": "git", 130 | "url": "https://github.com/guzzle/psr7.git", 131 | "reference": "239400de7a173fe9901b9ac7c06497751f00727a" 132 | }, 133 | "dist": { 134 | "type": "zip", 135 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", 136 | "reference": "239400de7a173fe9901b9ac7c06497751f00727a", 137 | "shasum": "" 138 | }, 139 | "require": { 140 | "php": ">=5.4.0", 141 | "psr/http-message": "~1.0", 142 | "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" 143 | }, 144 | "provide": { 145 | "psr/http-message-implementation": "1.0" 146 | }, 147 | "require-dev": { 148 | "ext-zlib": "*", 149 | "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" 150 | }, 151 | "suggest": { 152 | "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" 153 | }, 154 | "type": "library", 155 | "extra": { 156 | "branch-alias": { 157 | "dev-master": "1.6-dev" 158 | } 159 | }, 160 | "autoload": { 161 | "psr-4": { 162 | "GuzzleHttp\\Psr7\\": "src/" 163 | }, 164 | "files": [ 165 | "src/functions_include.php" 166 | ] 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "authors": [ 173 | { 174 | "name": "Michael Dowling", 175 | "email": "mtdowling@gmail.com", 176 | "homepage": "https://github.com/mtdowling" 177 | }, 178 | { 179 | "name": "Tobias Schultze", 180 | "homepage": "https://github.com/Tobion" 181 | } 182 | ], 183 | "description": "PSR-7 message implementation that also provides common utility methods", 184 | "keywords": [ 185 | "http", 186 | "message", 187 | "psr-7", 188 | "request", 189 | "response", 190 | "stream", 191 | "uri", 192 | "url" 193 | ], 194 | "time": "2019-07-01T23:21:34+00:00" 195 | }, 196 | { 197 | "name": "paragonie/random_compat", 198 | "version": "v9.99.99", 199 | "source": { 200 | "type": "git", 201 | "url": "https://github.com/paragonie/random_compat.git", 202 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" 203 | }, 204 | "dist": { 205 | "type": "zip", 206 | "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 207 | "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", 208 | "shasum": "" 209 | }, 210 | "require": { 211 | "php": "^7" 212 | }, 213 | "require-dev": { 214 | "phpunit/phpunit": "4.*|5.*", 215 | "vimeo/psalm": "^1" 216 | }, 217 | "suggest": { 218 | "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." 219 | }, 220 | "type": "library", 221 | "notification-url": "https://packagist.org/downloads/", 222 | "license": [ 223 | "MIT" 224 | ], 225 | "authors": [ 226 | { 227 | "name": "Paragon Initiative Enterprises", 228 | "email": "security@paragonie.com", 229 | "homepage": "https://paragonie.com" 230 | } 231 | ], 232 | "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", 233 | "keywords": [ 234 | "csprng", 235 | "polyfill", 236 | "pseudorandom", 237 | "random" 238 | ], 239 | "time": "2018-07-02T15:55:56+00:00" 240 | }, 241 | { 242 | "name": "phpseclib/phpseclib", 243 | "version": "2.0.21", 244 | "source": { 245 | "type": "git", 246 | "url": "https://github.com/phpseclib/phpseclib.git", 247 | "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d" 248 | }, 249 | "dist": { 250 | "type": "zip", 251 | "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9f1287e68b3f283339a9f98f67515dd619e5bf9d", 252 | "reference": "9f1287e68b3f283339a9f98f67515dd619e5bf9d", 253 | "shasum": "" 254 | }, 255 | "require": { 256 | "php": ">=5.3.3" 257 | }, 258 | "require-dev": { 259 | "phing/phing": "~2.7", 260 | "phpunit/phpunit": "^4.8.35|^5.7|^6.0", 261 | "sami/sami": "~2.0", 262 | "squizlabs/php_codesniffer": "~2.0" 263 | }, 264 | "suggest": { 265 | "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", 266 | "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", 267 | "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", 268 | "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." 269 | }, 270 | "type": "library", 271 | "autoload": { 272 | "files": [ 273 | "phpseclib/bootstrap.php" 274 | ], 275 | "psr-4": { 276 | "phpseclib\\": "phpseclib/" 277 | } 278 | }, 279 | "notification-url": "https://packagist.org/downloads/", 280 | "license": [ 281 | "MIT" 282 | ], 283 | "authors": [ 284 | { 285 | "name": "Jim Wigginton", 286 | "email": "terrafrost@php.net", 287 | "role": "Lead Developer" 288 | }, 289 | { 290 | "name": "Patrick Monnerat", 291 | "email": "pm@datasphere.ch", 292 | "role": "Developer" 293 | }, 294 | { 295 | "name": "Andreas Fischer", 296 | "email": "bantu@phpbb.com", 297 | "role": "Developer" 298 | }, 299 | { 300 | "name": "Hans-Jürgen Petrich", 301 | "email": "petrich@tronic-media.com", 302 | "role": "Developer" 303 | }, 304 | { 305 | "name": "Graham Campbell", 306 | "email": "graham@alt-three.com", 307 | "role": "Developer" 308 | } 309 | ], 310 | "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", 311 | "homepage": "http://phpseclib.sourceforge.net", 312 | "keywords": [ 313 | "BigInteger", 314 | "aes", 315 | "asn.1", 316 | "asn1", 317 | "blowfish", 318 | "crypto", 319 | "cryptography", 320 | "encryption", 321 | "rsa", 322 | "security", 323 | "sftp", 324 | "signature", 325 | "signing", 326 | "ssh", 327 | "twofish", 328 | "x.509", 329 | "x509" 330 | ], 331 | "time": "2019-07-12T12:53:49+00:00" 332 | }, 333 | { 334 | "name": "psr/http-message", 335 | "version": "1.0.1", 336 | "source": { 337 | "type": "git", 338 | "url": "https://github.com/php-fig/http-message.git", 339 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 340 | }, 341 | "dist": { 342 | "type": "zip", 343 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 344 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 345 | "shasum": "" 346 | }, 347 | "require": { 348 | "php": ">=5.3.0" 349 | }, 350 | "type": "library", 351 | "extra": { 352 | "branch-alias": { 353 | "dev-master": "1.0.x-dev" 354 | } 355 | }, 356 | "autoload": { 357 | "psr-4": { 358 | "Psr\\Http\\Message\\": "src/" 359 | } 360 | }, 361 | "notification-url": "https://packagist.org/downloads/", 362 | "license": [ 363 | "MIT" 364 | ], 365 | "authors": [ 366 | { 367 | "name": "PHP-FIG", 368 | "homepage": "http://www.php-fig.org/" 369 | } 370 | ], 371 | "description": "Common interface for HTTP messages", 372 | "homepage": "https://github.com/php-fig/http-message", 373 | "keywords": [ 374 | "http", 375 | "http-message", 376 | "psr", 377 | "psr-7", 378 | "request", 379 | "response" 380 | ], 381 | "time": "2016-08-06T14:39:51+00:00" 382 | }, 383 | { 384 | "name": "ralouphie/getallheaders", 385 | "version": "3.0.3", 386 | "source": { 387 | "type": "git", 388 | "url": "https://github.com/ralouphie/getallheaders.git", 389 | "reference": "120b605dfeb996808c31b6477290a714d356e822" 390 | }, 391 | "dist": { 392 | "type": "zip", 393 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 394 | "reference": "120b605dfeb996808c31b6477290a714d356e822", 395 | "shasum": "" 396 | }, 397 | "require": { 398 | "php": ">=5.6" 399 | }, 400 | "require-dev": { 401 | "php-coveralls/php-coveralls": "^2.1", 402 | "phpunit/phpunit": "^5 || ^6.5" 403 | }, 404 | "type": "library", 405 | "autoload": { 406 | "files": [ 407 | "src/getallheaders.php" 408 | ] 409 | }, 410 | "notification-url": "https://packagist.org/downloads/", 411 | "license": [ 412 | "MIT" 413 | ], 414 | "authors": [ 415 | { 416 | "name": "Ralph Khattar", 417 | "email": "ralph.khattar@gmail.com" 418 | } 419 | ], 420 | "description": "A polyfill for getallheaders.", 421 | "time": "2019-03-08T08:55:37+00:00" 422 | }, 423 | { 424 | "name": "ramsey/uuid", 425 | "version": "3.8.0", 426 | "source": { 427 | "type": "git", 428 | "url": "https://github.com/ramsey/uuid.git", 429 | "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3" 430 | }, 431 | "dist": { 432 | "type": "zip", 433 | "url": "https://api.github.com/repos/ramsey/uuid/zipball/d09ea80159c1929d75b3f9c60504d613aeb4a1e3", 434 | "reference": "d09ea80159c1929d75b3f9c60504d613aeb4a1e3", 435 | "shasum": "" 436 | }, 437 | "require": { 438 | "paragonie/random_compat": "^1.0|^2.0|9.99.99", 439 | "php": "^5.4 || ^7.0", 440 | "symfony/polyfill-ctype": "^1.8" 441 | }, 442 | "replace": { 443 | "rhumsaa/uuid": "self.version" 444 | }, 445 | "require-dev": { 446 | "codeception/aspect-mock": "^1.0 | ~2.0.0", 447 | "doctrine/annotations": "~1.2.0", 448 | "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ~2.1.0", 449 | "ircmaxell/random-lib": "^1.1", 450 | "jakub-onderka/php-parallel-lint": "^0.9.0", 451 | "mockery/mockery": "^0.9.9", 452 | "moontoast/math": "^1.1", 453 | "php-mock/php-mock-phpunit": "^0.3|^1.1", 454 | "phpunit/phpunit": "^4.7|^5.0|^6.5", 455 | "squizlabs/php_codesniffer": "^2.3" 456 | }, 457 | "suggest": { 458 | "ext-ctype": "Provides support for PHP Ctype functions", 459 | "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", 460 | "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", 461 | "ircmaxell/random-lib": "Provides RandomLib for use with the RandomLibAdapter", 462 | "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", 463 | "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", 464 | "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." 465 | }, 466 | "type": "library", 467 | "extra": { 468 | "branch-alias": { 469 | "dev-master": "3.x-dev" 470 | } 471 | }, 472 | "autoload": { 473 | "psr-4": { 474 | "Ramsey\\Uuid\\": "src/" 475 | } 476 | }, 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "MIT" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "Marijn Huizendveld", 484 | "email": "marijn.huizendveld@gmail.com" 485 | }, 486 | { 487 | "name": "Thibaud Fabre", 488 | "email": "thibaud@aztech.io" 489 | }, 490 | { 491 | "name": "Ben Ramsey", 492 | "email": "ben@benramsey.com", 493 | "homepage": "https://benramsey.com" 494 | } 495 | ], 496 | "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", 497 | "homepage": "https://github.com/ramsey/uuid", 498 | "keywords": [ 499 | "guid", 500 | "identifier", 501 | "uuid" 502 | ], 503 | "time": "2018-07-19T23:38:55+00:00" 504 | }, 505 | { 506 | "name": "rct567/dom-query", 507 | "version": "v0.8", 508 | "source": { 509 | "type": "git", 510 | "url": "https://github.com/Rct567/DomQuery.git", 511 | "reference": "15208bd61f0b87d3befa795a38e5b47a3dbebb97" 512 | }, 513 | "dist": { 514 | "type": "zip", 515 | "url": "https://api.github.com/repos/Rct567/DomQuery/zipball/15208bd61f0b87d3befa795a38e5b47a3dbebb97", 516 | "reference": "15208bd61f0b87d3befa795a38e5b47a3dbebb97", 517 | "shasum": "" 518 | }, 519 | "require": { 520 | "ext-dom": "*", 521 | "ext-json": "*", 522 | "ext-xml": "*", 523 | "php": "^7.0.0" 524 | }, 525 | "require-dev": { 526 | "phpstan/phpstan": "^0.8", 527 | "phpunit/php-code-coverage": "^5.3", 528 | "phpunit/phpunit": "^6.5", 529 | "squizlabs/php_codesniffer": "^3.1" 530 | }, 531 | "type": "library", 532 | "autoload": { 533 | "psr-4": { 534 | "Rct567\\": "src/Rct567" 535 | } 536 | }, 537 | "notification-url": "https://packagist.org/downloads/", 538 | "license": [ 539 | "MIT" 540 | ], 541 | "authors": [ 542 | { 543 | "name": "Rct567", 544 | "email": "rct999@gmail.com" 545 | } 546 | ], 547 | "description": "DomQuery is a PHP library that allows easy 'jQuery like' DOM traversing and manipulation", 548 | "keywords": [ 549 | "css", 550 | "dom", 551 | "html", 552 | "jquery", 553 | "selector", 554 | "xml" 555 | ], 556 | "time": "2019-05-13T13:28:13+00:00" 557 | }, 558 | { 559 | "name": "symfony/polyfill-ctype", 560 | "version": "v1.11.0", 561 | "source": { 562 | "type": "git", 563 | "url": "https://github.com/symfony/polyfill-ctype.git", 564 | "reference": "82ebae02209c21113908c229e9883c419720738a" 565 | }, 566 | "dist": { 567 | "type": "zip", 568 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", 569 | "reference": "82ebae02209c21113908c229e9883c419720738a", 570 | "shasum": "" 571 | }, 572 | "require": { 573 | "php": ">=5.3.3" 574 | }, 575 | "suggest": { 576 | "ext-ctype": "For best performance" 577 | }, 578 | "type": "library", 579 | "extra": { 580 | "branch-alias": { 581 | "dev-master": "1.11-dev" 582 | } 583 | }, 584 | "autoload": { 585 | "psr-4": { 586 | "Symfony\\Polyfill\\Ctype\\": "" 587 | }, 588 | "files": [ 589 | "bootstrap.php" 590 | ] 591 | }, 592 | "notification-url": "https://packagist.org/downloads/", 593 | "license": [ 594 | "MIT" 595 | ], 596 | "authors": [ 597 | { 598 | "name": "Symfony Community", 599 | "homepage": "https://symfony.com/contributors" 600 | }, 601 | { 602 | "name": "Gert de Pagter", 603 | "email": "BackEndTea@gmail.com" 604 | } 605 | ], 606 | "description": "Symfony polyfill for ctype functions", 607 | "homepage": "https://symfony.com", 608 | "keywords": [ 609 | "compatibility", 610 | "ctype", 611 | "polyfill", 612 | "portable" 613 | ], 614 | "time": "2019-02-06T07:57:58+00:00" 615 | } 616 | ], 617 | "packages-dev": [], 618 | "aliases": [], 619 | "minimum-stability": "stable", 620 | "stability-flags": [], 621 | "prefer-stable": false, 622 | "prefer-lowest": false, 623 | "platform": [], 624 | "platform-dev": [] 625 | } 626 | --------------------------------------------------------------------------------