1<?php
2/**
3 * Copyright 2017 Facebook, Inc.
4 *
5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6 * use, copy, modify, and distribute this software in source code or binary
7 * form for use in connection with the web services and APIs provided by
8 * Facebook.
9 *
10 * As with any software that integrates with the Facebook platform, your use
11 * of this software is subject to the Facebook Developer Principles and
12 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13 * shall be included in all 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,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 * DEALINGS IN THE SOFTWARE.
22 *
23 */
24namespace Facebook\Authentication;
25
26use Facebook\Exceptions\FacebookSDKException;
27
28/**
29 * Class AccessTokenMetadata
30 *
31 * Represents metadata from an access token.
32 *
33 * @package Facebook
34 * @see     https://developers.facebook.com/docs/graph-api/reference/debug_token
35 */
36class AccessTokenMetadata
37{
38    /**
39     * The access token metadata.
40     *
41     * @var array
42     */
43    protected $metadata = [];
44
45    /**
46     * Properties that should be cast as DateTime objects.
47     *
48     * @var array
49     */
50    protected static $dateProperties = ['expires_at', 'issued_at'];
51
52    /**
53     * @param array $metadata
54     *
55     * @throws FacebookSDKException
56     */
57    public function __construct(array $metadata)
58    {
59        if (!isset($metadata['data'])) {
60            throw new FacebookSDKException('Unexpected debug token response data.', 401);
61        }
62
63        $this->metadata = $metadata['data'];
64
65        $this->castTimestampsToDateTime();
66    }
67
68    /**
69     * Returns a value from the metadata.
70     *
71     * @param string $field   The property to retrieve.
72     * @param mixed  $default The default to return if the property doesn't exist.
73     *
74     * @return mixed
75     */
76    public function getField($field, $default = null)
77    {
78        if (isset($this->metadata[$field])) {
79            return $this->metadata[$field];
80        }
81
82        return $default;
83    }
84
85    /**
86     * Returns a value from the metadata.
87     *
88     * @param string $field   The property to retrieve.
89     * @param mixed  $default The default to return if the property doesn't exist.
90     *
91     * @return mixed
92     *
93     * @deprecated 5.0.0 getProperty() has been renamed to getField()
94     * @todo v6: Remove this method
95     */
96    public function getProperty($field, $default = null)
97    {
98        return $this->getField($field, $default);
99    }
100
101    /**
102     * Returns a value from a child property in the metadata.
103     *
104     * @param string $parentField The parent property.
105     * @param string $field       The property to retrieve.
106     * @param mixed  $default     The default to return if the property doesn't exist.
107     *
108     * @return mixed
109     */
110    public function getChildProperty($parentField, $field, $default = null)
111    {
112        if (!isset($this->metadata[$parentField])) {
113            return $default;
114        }
115
116        if (!isset($this->metadata[$parentField][$field])) {
117            return $default;
118        }
119
120        return $this->metadata[$parentField][$field];
121    }
122
123    /**
124     * Returns a value from the error metadata.
125     *
126     * @param string $field   The property to retrieve.
127     * @param mixed  $default The default to return if the property doesn't exist.
128     *
129     * @return mixed
130     */
131    public function getErrorProperty($field, $default = null)
132    {
133        return $this->getChildProperty('error', $field, $default);
134    }
135
136    /**
137     * Returns a value from the "metadata" metadata. *Brain explodes*
138     *
139     * @param string $field   The property to retrieve.
140     * @param mixed  $default The default to return if the property doesn't exist.
141     *
142     * @return mixed
143     */
144    public function getMetadataProperty($field, $default = null)
145    {
146        return $this->getChildProperty('metadata', $field, $default);
147    }
148
149    /**
150     * The ID of the application this access token is for.
151     *
152     * @return string|null
153     */
154    public function getAppId()
155    {
156        return $this->getField('app_id');
157    }
158
159    /**
160     * Name of the application this access token is for.
161     *
162     * @return string|null
163     */
164    public function getApplication()
165    {
166        return $this->getField('application');
167    }
168
169    /**
170     * Any error that a request to the graph api
171     * would return due to the access token.
172     *
173     * @return bool|null
174     */
175    public function isError()
176    {
177        return $this->getField('error') !== null;
178    }
179
180    /**
181     * The error code for the error.
182     *
183     * @return int|null
184     */
185    public function getErrorCode()
186    {
187        return $this->getErrorProperty('code');
188    }
189
190    /**
191     * The error message for the error.
192     *
193     * @return string|null
194     */
195    public function getErrorMessage()
196    {
197        return $this->getErrorProperty('message');
198    }
199
200    /**
201     * The error subcode for the error.
202     *
203     * @return int|null
204     */
205    public function getErrorSubcode()
206    {
207        return $this->getErrorProperty('subcode');
208    }
209
210    /**
211     * DateTime when this access token expires.
212     *
213     * @return \DateTime|null
214     */
215    public function getExpiresAt()
216    {
217        return $this->getField('expires_at');
218    }
219
220    /**
221     * Whether the access token is still valid or not.
222     *
223     * @return boolean|null
224     */
225    public function getIsValid()
226    {
227        return $this->getField('is_valid');
228    }
229
230    /**
231     * DateTime when this access token was issued.
232     *
233     * Note that the issued_at field is not returned
234     * for short-lived access tokens.
235     *
236     * @see https://developers.facebook.com/docs/facebook-login/access-tokens#debug
237     *
238     * @return \DateTime|null
239     */
240    public function getIssuedAt()
241    {
242        return $this->getField('issued_at');
243    }
244
245    /**
246     * General metadata associated with the access token.
247     * Can contain data like 'sso', 'auth_type', 'auth_nonce'.
248     *
249     * @return array|null
250     */
251    public function getMetadata()
252    {
253        return $this->getField('metadata');
254    }
255
256    /**
257     * The 'sso' child property from the 'metadata' parent property.
258     *
259     * @return string|null
260     */
261    public function getSso()
262    {
263        return $this->getMetadataProperty('sso');
264    }
265
266    /**
267     * The 'auth_type' child property from the 'metadata' parent property.
268     *
269     * @return string|null
270     */
271    public function getAuthType()
272    {
273        return $this->getMetadataProperty('auth_type');
274    }
275
276    /**
277     * The 'auth_nonce' child property from the 'metadata' parent property.
278     *
279     * @return string|null
280     */
281    public function getAuthNonce()
282    {
283        return $this->getMetadataProperty('auth_nonce');
284    }
285
286    /**
287     * For impersonated access tokens, the ID of
288     * the page this token contains.
289     *
290     * @return string|null
291     */
292    public function getProfileId()
293    {
294        return $this->getField('profile_id');
295    }
296
297    /**
298     * List of permissions that the user has granted for
299     * the app in this access token.
300     *
301     * @return array
302     */
303    public function getScopes()
304    {
305        return $this->getField('scopes');
306    }
307
308    /**
309     * The ID of the user this access token is for.
310     *
311     * @return string|null
312     */
313    public function getUserId()
314    {
315        return $this->getField('user_id');
316    }
317
318    /**
319     * Ensures the app ID from the access token
320     * metadata is what we expect.
321     *
322     * @param string $appId
323     *
324     * @throws FacebookSDKException
325     */
326    public function validateAppId($appId)
327    {
328        if ($this->getAppId() !== $appId) {
329            throw new FacebookSDKException('Access token metadata contains unexpected app ID.', 401);
330        }
331    }
332
333    /**
334     * Ensures the user ID from the access token
335     * metadata is what we expect.
336     *
337     * @param string $userId
338     *
339     * @throws FacebookSDKException
340     */
341    public function validateUserId($userId)
342    {
343        if ($this->getUserId() !== $userId) {
344            throw new FacebookSDKException('Access token metadata contains unexpected user ID.', 401);
345        }
346    }
347
348    /**
349     * Ensures the access token has not expired yet.
350     *
351     * @throws FacebookSDKException
352     */
353    public function validateExpiration()
354    {
355        if (!$this->getExpiresAt() instanceof \DateTime) {
356            return;
357        }
358
359        if ($this->getExpiresAt()->getTimestamp() < time()) {
360            throw new FacebookSDKException('Inspection of access token metadata shows that the access token has expired.', 401);
361        }
362    }
363
364    /**
365     * Converts a unix timestamp into a DateTime entity.
366     *
367     * @param int $timestamp
368     *
369     * @return \DateTime
370     */
371    private function convertTimestampToDateTime($timestamp)
372    {
373        $dt = new \DateTime();
374        $dt->setTimestamp($timestamp);
375
376        return $dt;
377    }
378
379    /**
380     * Casts the unix timestamps as DateTime entities.
381     */
382    private function castTimestampsToDateTime()
383    {
384        foreach (static::$dateProperties as $key) {
385            if (isset($this->metadata[$key]) && $this->metadata[$key] !== 0) {
386                $this->metadata[$key] = $this->convertTimestampToDateTime($this->metadata[$key]);
387            }
388        }
389    }
390}
391