Passed
Push — master ( ef4891...235f7d )
by Stefan
01:23
created

GContacts::setContactPhotoFile()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 9
nop 2
dl 0
loc 24
rs 9.3554
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\Google;
5
6
/**
7
 * Class to manage the contacts of a google account.
8
 *
9
 * This class encapsulates the following Google People API resources:
10
 *  - people
11
 *  - people.connections
12
 *
13
 * If one of the methods that calls the google API fails (if it returns `false`),
14
 * the last responsecode and furter informations can be retrieved through
15
 * following methods of the `GClient` instance this object was created with:
16
 *  - `$oClient->getLastResponseCode()`
17
 *  - `$oClient->getLastError()`
18
 *  - `$oClient->getLastStatus()`
19
 *
20
 * @see \SKien\Google\GClient::getLastResponseCode()
21
 * @see \SKien\Google\GClient::getLastError()
22
 * @see \SKien\Google\GClient::getLastStatus()
23
 *
24
 * @link https://developers.google.com/people/api/rest/v1/people
25
 * @link https://developers.google.com/people/api/rest/v1/people.connections
26
 *
27
 * @author Stefanius <[email protected]>
28
 * @copyright MIT License - see the LICENSE file for details
29
 */
30
class GContacts
31
{
32
    /** full access to the users contacts */
33
    public const CONTACTS = "https://www.googleapis.com/auth/contacts";
34
    /** readonly access to the users contacts */
35
    public const CONTACTS_READONLY = "https://www.googleapis.com/auth/contacts.readonly";
36
    /** readonly access to the users other contacts */
37
    public const CONTACTS_OTHER_READONLY ="https://www.googleapis.com/auth/contacts.other.readonly";
38
39
    /**	Sort people by when they were changed; older entries first. */
40
    public const SO_LAST_MODIFIED_ASCENDING ='LAST_MODIFIED_ASCENDING';
41
    /**	Sort people by when they were changed; newer entries first. */
42
    public const SO_LAST_MODIFIED_DESCENDING = 'LAST_MODIFIED_DESCENDING';
43
    /**	Sort people by first name. */
44
    public const SO_FIRST_NAME_ASCENDING = 'FIRST_NAME_ASCENDING';
45
    /**	Sort people by last name. */
46
    public const SO_LAST_NAME_ASCENDING = 'LAST_NAME_ASCENDING';
47
48
    /** the default personFields for detail view    */
49
    public const DEF_DETAIL_PERSON_FIELDS = [
50
        GContact::PF_NAMES,
51
        GContact::PF_ORGANIZATIONS,
52
        GContact::PF_NICKNAMES,
53
        GContact::PF_BIRTHDAYS,
54
        GContact::PF_PHOTOS,
55
        GContact::PF_ADDRESSES,
56
        GContact::PF_EMAIL_ADDRESSES,
57
        GContact::PF_PHONE_NUMBERS,
58
        GContact::PF_GENDERS,
59
        GContact::PF_MEMBERSHIPS,
60
        GContact::PF_METADATA,
61
        GContact::PF_BIOGRAPHIES,
62
        GContact::PF_URLS,
63
    ];
64
65
    /** the default personFields for detail view    */
66
    public const DEF_LIST_PERSON_FIELDS = [
67
        GContact::PF_NAMES,
68
        GContact::PF_ORGANIZATIONS,
69
        GContact::PF_NICKNAMES,
70
        GContact::PF_BIRTHDAYS,
71
        GContact::PF_ADDRESSES,
72
        GContact::PF_EMAIL_ADDRESSES,
73
        GContact::PF_PHONE_NUMBERS,
74
        GContact::PF_MEMBERSHIPS,
75
        GContact::PF_METADATA,
76
    ];
77
78
    /** max. pagesize for the list request */
79
    protected const CONTACTS_MAX_PAGESIZE = 1000;
80
    /** max. pagesize for a search */
81
    protected const SEARCH_MAX_PAGESIZE = 30;
82
83
    /** @var array<string>  personFields/readMask to be returned by the request */
84
    protected array $aPersonFields = [];
85
    /** pagesize for a request */
86
    protected int $iPageSize = 200;
87
88
    /** @var GClient    clients to perform the requests to the api     */
89
    protected GClient $oClient;
90
91
    /**
92
     * Create instance an pass the clinet for the requests.
93
     * @param GClient $oClient
94
     */
95
    public function __construct(GClient $oClient)
96
    {
97
        $this->oClient = $oClient;
98
    }
99
100
    /**
101
     * Add personFields/readMask for next request.
102
     * Can be called multiple and/or by passing an array of personFields. All
103
     * const `GContact::PF_xxxx` can be specified.
104
     * @param string|array<string>|null $fields; if set to null, the internal array is cleared
105
     */
106
    public function addPersonFields($fields) : void
107
    {
108
        if ($fields === null) {
109
            $this->aPersonFields = [];
110
        } else if (is_array($fields)) {
111
            $this->aPersonFields = array_merge($this->aPersonFields, $fields);
112
        } else if (!in_array($fields, $this->aPersonFields)) {
113
            $this->aPersonFields[] = $fields;
114
        }
115
    }
116
117
    /**
118
     * Set the pagesize for reading lists.
119
     * May be limited to the max. pgaesize for the request.
120
     * - contact list: max. pagesize is 1000
121
     * - search: max. pagesize is 30
122
     * @param int $iPageSize
123
     */
124
    public function setPageSize(int $iPageSize) : void
125
    {
126
        $this->iPageSize = $iPageSize;
127
    }
128
129
    /**
130
     * Get the whole contact list.
131
     * @link https://developers.google.com/people/api/rest/v1/people.connections/list
132
     * @param string $strSortOrder  one of the self::SO_xxx constants
133
     * @param string $strGroupResourceName
134
     * @return array<mixed>|false
135
     */
136
    public function list(string $strSortOrder = self::SO_LAST_NAME_ASCENDING, string $strGroupResourceName = '')
137
    {
138
        $aHeader = [$this->oClient->getAuthHeader()];
139
140
        if (count($this->aPersonFields) == 0) {
141
            $this->aPersonFields = self::DEF_LIST_PERSON_FIELDS;
142
        }
143
144
        $aParams = [
145
            'personFields' => implode(',', $this->aPersonFields),
146
            'sortOrder' => $strSortOrder,
147
            'pageSize' => ($this->iPageSize > self::CONTACTS_MAX_PAGESIZE ? self::CONTACTS_MAX_PAGESIZE : $this->iPageSize),
148
        ];
149
150
        $bEndOfList = false;
151
        $aContactList = [];
152
        while ($bEndOfList === false) {
153
            $strURI = 'https://people.googleapis.com/v1/people/me/connections?' . http_build_query($aParams);
154
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
155
            if ($strResponse === false) {
156
                return false;
157
            }
158
            $oResponse = json_decode($strResponse, true);
159
            if (is_array($oResponse)) {
160
                if (isset($oResponse['nextPageToken']) && !empty($oResponse['nextPageToken'])) {
161
                    $aParams['pageToken'] = $oResponse['nextPageToken'];
162
                } else {
163
                    $bEndOfList = true;
164
                }
165
                foreach ($oResponse['connections'] as $aContact) {
166
                    if (empty($strGroupResourceName) || GContact::fromArray($aContact)->belongsToGroup($strGroupResourceName)) {
167
                        $aContactList[] = $aContact;
168
                    }
169
                }
170
            } else {
171
                break;
172
            }
173
        }
174
        return $aContactList;
175
    }
176
177
    /**
178
     * Search within the contacts.
179
     * The query matches on a contact's `names`, `nickNames`, `emailAddresses`,
180
     * `phoneNumbers`, and `organizations` fields. The search for phone numbers only
181
     * works, if leading '+' and contained '(', ')' or spaces are omitted in the query!
182
     * The query is used to match <b>prefix</B> phrases of the fields on a person.
183
     * For example, a person with name "foo name" matches queries such as "f", "fo",
184
     * "foo", "foo n", "nam", etc., but not "oo n".
185
     * > <b>Note:</b>
186
     * > The count of contacts, the search request returns is limitetd to the
187
     * > pageSize (which is limited itself to max. 30 at all). If there are
188
     * > more contacts in the list that matches the query, unfortunately NO
189
     * > further information about that additional contacts - and how many - are
190
     * > available!
191
     * @link https://developers.google.com/people/api/rest/v1/people/searchContacts
192
     * @param string $strQuery  the query to search for
193
     * @return array<mixed>|false
194
     */
195
    public function search(string $strQuery)
196
    {
197
        $aHeader = [$this->oClient->getAuthHeader()];
198
199
        if (count($this->aPersonFields) == 0) {
200
            $this->aPersonFields = self::DEF_LIST_PERSON_FIELDS;
201
        }
202
203
        $aParams = [
204
            'query' => '',
205
            'readMask' => implode(',', $this->aPersonFields),
206
            'pageSize' => ($this->iPageSize > self::SEARCH_MAX_PAGESIZE ? self::SEARCH_MAX_PAGESIZE : $this->iPageSize),
207
        ];
208
        $strURI = 'https://people.googleapis.com/v1/people:searchContacts?' . http_build_query($aParams);;
209
210
        // 'warmup' request
211
        // Note from google documentation:
212
        // Before searching, clients should send a warmup request with an empty query to update the cache.
213
        // https://developers.google.com/people/v1/contacts#search_the_users_contacts
214
        $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
215
        $aParams['query'] = $strQuery;
216
        $strURI = 'https://people.googleapis.com/v1/people:searchContacts?' . http_build_query($aParams);;
217
218
        $aContactList = false;
219
        if (($strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader)) !== false) {
220
            $aContactList = [];
221
            $oResponse = json_decode($strResponse, true);
222
            if (is_array($oResponse) && isset($oResponse['results']) && count($oResponse['results']) > 0) {
223
                foreach ($oResponse['results'] as $aContact) {
224
                    $aContactList[] = $aContact['person'];
225
                }
226
            }
227
        }
228
        return $aContactList;
229
    }
230
231
    /**
232
     * Get the contact specified by its resourceName.
233
     * @link https://developers.google.com/people/api/rest/v1/people/get
234
     * @param string $strResourceName
235
     * @return GContact|false
236
     */
237
    public function getContact(string $strResourceName)
238
    {
239
        $aHeader = [$this->oClient->getAuthHeader()];
240
241
        if (count($this->aPersonFields) == 0) {
242
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
243
        }
244
245
        $aParams = [
246
            'personFields' => implode(',', $this->aPersonFields),
247
        ];
248
249
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . '?' . http_build_query($aParams);;
250
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
251
252
        $result = false;
253
        if ($strResponse !== false) {
254
            $result = GContact::fromJSON($strResponse, $this->aPersonFields);
255
        }
256
        return $result;
257
    }
258
259
    /**
260
     * Creates a new contact.
261
     * @link https://developers.google.com/people/api/rest/v1/people/createContact
262
     * @param GContact $oContact
263
     * @return GContact|false
264
     */
265
    public function createContact(GContact $oContact)
266
    {
267
        $aHeader = [
268
            $this->oClient->getAuthHeader(),
269
            'Content-Type: application/json',
270
        ];
271
272
        if (count($this->aPersonFields) == 0) {
273
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
274
        }
275
        $aParams = ['personFields' => implode(',', $this->getUpdatePersonFields())];
276
277
        $result = false;
278
        $data = json_encode($oContact);
279
        if ($data !== false) {
280
            $strURI = 'https://people.googleapis.com/v1/people:createContact/?' . http_build_query($aParams);
281
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::POST, $aHeader, $data);
282
283
            if ($strResponse !== false) {
284
                $result = GContact::fromJSON($strResponse, $this->aPersonFields);
285
            }
286
        }
287
        return $result;
288
    }
289
290
    /**
291
     * Updates an existing contact specified by resourceName.
292
     * To prevent the user from data loss, this request fails with an 400 response
293
     * code, if the contact has changed on the server, since it was loaded.
294
     * Reload the data from the server and make the changes again!
295
     * @link https://developers.google.com/people/api/rest/v1/people/updateContact
296
     * @param string $strResourceName
297
     * @param GContact $oContact
298
     * @return GContact|false
299
     */
300
    public function updateContact(string $strResourceName, GContact $oContact)
301
    {
302
        $aHeader = [
303
            $this->oClient->getAuthHeader(),
304
            'Content-Type: application/json',
305
        ];
306
307
        if (count($this->aPersonFields) == 0) {
308
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
309
        }
310
        $aParams = ['updatePersonFields' => implode(',', $this->getUpdatePersonFields())];
311
312
        $result = false;
313
        $data = json_encode($oContact);
314
        if ($data !== false) {
315
            $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContact/?' . http_build_query($aParams);
316
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
317
318
            if ($strResponse !== false) {
319
                $result = GContact::fromJSON($strResponse, $this->aPersonFields);
320
            }
321
        }
322
        return $result;
323
    }
324
325
    /**
326
     * Delete the requested contact.
327
     * @link https://developers.google.com/people/api/rest/v1/people/deleteContact
328
     * @param string $strResourceName
329
     * @return bool
330
     */
331
    public function deleteContact(string $strResourceName) : bool
332
    {
333
        $aHeader = [$this->oClient->getAuthHeader()];
334
335
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':deleteContact';
336
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::DELETE, $aHeader);
337
        return ($strResponse !== false);
338
    }
339
340
    /**
341
     * Set or unset the 'starred' mark for the specified contact.
342
     * The 'starred' mark just means, the contact belongs to the system group `contactGroups/starred`.
343
     * @see GContactGroups::addContactsToGroup()
344
     * @see GContactGroups::removeContactsFromGroup()
345
     * @param string $strResourceName
346
     * @param bool $bSetStarred
347
     * @return bool
348
     */
349
    public function setContactStarred(string $strResourceName, bool $bSetStarred = true) : bool
350
    {
351
        $oContactGroups = new GContactGroups($this->oClient);
352
        if ($bSetStarred) {
353
            $result = $oContactGroups->addContactsToGroup(GContactGroups::GRP_STARRED, [$strResourceName]);
354
        } else {
355
            $result = $oContactGroups->removeContactsFromGroup(GContactGroups::GRP_STARRED, [$strResourceName]);
356
        }
357
        return $result !== false;
358
    }
359
360
361
    /**
362
     * Set contact photo from image file.
363
     * Supported types are JPG, PNG, GIF and BMP.
364
     * @link https://developers.google.com/people/api/rest/v1/people/updateContactPhoto
365
     * @param string $strResourceName
366
     * @param string $strFilename
367
     * @return bool
368
     */
369
    public function setContactPhotoFile(string $strResourceName, string $strFilename) : bool
370
    {
371
        $blobPhoto = '';
372
        if (filter_var($strFilename, FILTER_VALIDATE_URL)) {
373
            $blobPhoto = $this->loadImageFromURL($strFilename);
374
        } elseif (file_exists($strFilename)) {
375
            $blobPhoto = $this->loadImageFromFile($strFilename);
376
        } else {
377
            $this->oClient->setError(0, 'File not found: ' . $strFilename, 'INVALID_ARGUMENT');
378
        }
379
        $result = false;
380
        if (!empty($blobPhoto)) {
381
            $aHeader = [
382
                $this->oClient->getAuthHeader(),
383
                'Content-Type: application/json',
384
            ];
385
            $data = json_encode(['photoBytes' => $blobPhoto]);
386
            if ($data !== false) {
387
                $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContactPhoto';
388
                $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
389
                $result = ($strResponse !== false);
390
            }
391
        }
392
        return $result;
393
    }
394
395
    /**
396
     * Set contact photo from base64 encoded image data.
397
     * @link https://developers.google.com/people/api/rest/v1/people/updateContactPhoto
398
     * @param string $strResourceName
399
     * @param string $blobPhoto  base64 encoded image
400
     * @return bool
401
     */
402
    public function setContactPhoto(string $strResourceName, string $blobPhoto) : bool
403
    {
404
        $result = false;
405
        // check for valid base64 encoded imagedata
406
        $img = base64_decode($blobPhoto);
407
        if ($img !== false && imagecreatefromstring(base64_decode($blobPhoto)) !== false) {
408
            $aHeader = [
409
                $this->oClient->getAuthHeader(),
410
                'Content-Type: application/json',
411
            ];
412
            $data = json_encode(['photoBytes' => $blobPhoto]);
413
            if ($data !== false) {
414
                $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContactPhoto';
415
                $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
416
                $result = ($strResponse !== false);
417
            }
418
        } else {
419
            $this->oClient->setError(0, 'Invalid base64 encoded image data', 'INVALID_ARGUMENT');
420
        }
421
        return $result;
422
    }
423
424
    /**
425
     * Delete contact photo for given contact.
426
     * @link https://developers.google.com/people/api/rest/v1/people/deleteContactPhoto
427
     * @param string $strResourceName
428
     * @return bool
429
     */
430
    public function deleteContactPhoto(string $strResourceName) : bool
431
    {
432
        $aHeader = [$this->oClient->getAuthHeader()];
433
434
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':deleteContactPhoto';
435
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::DELETE, $aHeader);
436
437
        return ($strResponse !== false);
438
    }
439
440
    /**
441
     * Get updateable personFields.
442
     * @return array<string>
443
     */
444
    private function getUpdatePersonFields() : array
445
    {
446
        $aReadonlyPersonFields = [
447
            GContact::PF_PHOTOS,
448
            GContact::PF_COVER_PHOTOS,
449
            GContact::PF_AGE_RANGES,
450
            GContact::PF_METADATA,
451
        ];
452
        $aUpdatePersonFields = $this->aPersonFields;
453
        foreach ($aReadonlyPersonFields as $strReadonly) {
454
            if (($key = array_search($strReadonly, $aUpdatePersonFields)) !== false) {
455
                unset($aUpdatePersonFields[$key]);
456
            }
457
        }
458
        return $aUpdatePersonFields;
459
    }
460
461
    /**
462
     * Load an image from an URL.
463
     * The method uses curl to be independet of [allow_url_fopen] enabled on the system.
464
     * @param string $strURL
465
     * @return string   base64 encoded imagedata
466
     */
467
    private function loadImageFromURL(string $strURL) : string
468
    {
469
        $blobPhoto = '';
470
471
        $curl = curl_init();
472
        curl_setopt($curl, CURLOPT_URL, $strURL);
473
        curl_setopt($curl, CURLOPT_HEADER, true);
474
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
475
476
        $strResponse = curl_exec($curl);
477
478
        $iResponseCode = intval(curl_getinfo($curl, CURLINFO_RESPONSE_CODE));
479
        $iHeaderSize = intval(curl_getinfo($curl, CURLINFO_HEADER_SIZE));
480
481
        curl_close($curl);
482
483
        if ($iResponseCode == 200 && is_string($strResponse)) {
484
            $aHeader = $this->oClient->parseHttpHeader(substr($strResponse, 0, $iHeaderSize));
485
            $strContentType = $aHeader['content-type'] ?? '';
486
            switch ($strContentType) {
487
                case 'image/jpeg':
488
                case 'image/png':
489
                case 'image/gif':
490
                case 'image/bmp':
491
                    $img = substr($strResponse, $iHeaderSize);
492
                    $blobPhoto = base64_encode($img);
493
                    break;
494
                default:
495
                    $this->oClient->setError(0, 'Unsupported file type: ' . $strContentType, 'INVALID_ARGUMENT');
496
                    break;
497
            }
498
        } else {
499
            $this->oClient->setError(0, 'File not found: ' . $strURL, 'INVALID_ARGUMENT');
500
        }
501
        return $blobPhoto;
502
    }
503
504
    /**
505
     * Load an image from an file.
506
     * In most cases an uploaded imagefile.
507
     * @param string $strFilename
508
     * @return string   base64 encoded imagedata
509
     */
510
    private function loadImageFromFile(string $strFilename) : string
511
    {
512
        $blobPhoto = '';
513
        $iImageType = exif_imagetype($strFilename);
514
        switch ($iImageType) {
515
            case IMAGETYPE_JPEG:
516
            case IMAGETYPE_PNG:
517
            case IMAGETYPE_GIF:
518
            case IMAGETYPE_BMP:
519
                $img = file_get_contents($strFilename);
520
                if ($img !== false) {
521
                    $blobPhoto = base64_encode($img);
522
                }
523
                break;
524
            default:
525
                $this->oClient->setError(0, 'Unsupported image type: ' . image_type_to_mime_type($iImageType), 'INVALID_ARGUMENT');
526
                break;
527
        }
528
        return $blobPhoto;
529
    }
530
}
531