← All changes
|
backend/src/Services/GoogleSheets/GoogleSheetsService.php
+149
-2
1.4
→
1.5
View file →
| @@ -15,8 +15,11 @@ | ||
| 15 | 15 | { |
| 16 | 16 | private const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo'; |
| 17 | 17 | private const DRIVE_FILES_URL = 'https://www.googleapis.com/drive/v3/files'; |
| 18 | 18 | private const SHEETS_API_URL = 'https://sheets.googleapis.com/v4/spreadsheets'; |
| 19 | + private const PICKER_CONFIG_TRANSIENT = 'ivyforms_google_sheets_picker_config'; | |
| 20 | + public const PICKER_NOT_CONFIGURED_CODE = 'google_sheets_picker_not_configured'; | |
| 21 | + public const PICKER_ERROR_CODE = 'google_sheets_picker_error'; | |
| 19 | 22 | |
| 20 | 23 | private SettingsService $settingsService; |
| 21 | 24 | |
| 22 | 25 | public function __construct(SettingsService $settingsService) |
| @@ -237,9 +240,14 @@ | ||
| 237 | 240 | ]); |
| 238 | 241 | } |
| 239 | 242 | |
| 240 | 243 | /** |
| 244 | + * Legacy Drive listing. Spreadsheet selection uses the Google Picker, which | |
| 245 | + * only grants drive.file access to files the user picks. This query does not | |
| 246 | + * see those files and often returns an empty list. Kept for older clients. | |
| 247 | + * | |
| 241 | 248 | * @return array<array{id: string, name: string}> |
| 249 | + * @deprecated Use the Picker flow (picker-config) instead of listing Drive files. | |
| 242 | 250 | */ |
| 243 | 251 | public function listSpreadsheets(): array |
| 244 | 252 | { |
| 245 | 253 | $url = add_query_arg( |
| @@ -320,10 +328,12 @@ | ||
| 320 | 328 | if (!is_array($values)) { |
| 321 | 329 | return []; |
| 322 | 330 | } |
| 323 | 331 | |
| 324 | - return array_values(array_filter(array_map('strval', $values), static function ($value) { | |
| 325 | - return trim($value) !== ''; | |
| 332 | + return array_values(array_filter(array_map(static function ($value) { | |
| 333 | + return trim((string) $value); | |
| 334 | + }, $values), static function ($value) { | |
| 335 | + return $value !== ''; | |
| 326 | 336 | })); |
| 327 | 337 | } |
| 328 | 338 | |
| 329 | 339 | /** |
| @@ -545,8 +555,145 @@ | ||
| 545 | 555 | |
| 546 | 556 | $filtered = apply_filters('ivyforms/google_sheets/middleware_url', ''); |
| 547 | 557 | |
| 548 | 558 | return is_string($filtered) ? rtrim($filtered, '/') : ''; |
| 559 | + } | |
| 560 | + | |
| 561 | + /** | |
| 562 | + * Picker credentials from middleware (cached) plus a short-lived access token | |
| 563 | + * for the Google Picker widget. | |
| 564 | + * | |
| 565 | + * @return array{ | |
| 566 | + * success: bool, | |
| 567 | + * code?: string, | |
| 568 | + * message?: string, | |
| 569 | + * developerKey?: string, | |
| 570 | + * appId?: string, | |
| 571 | + * accessToken?: string | |
| 572 | + * } | |
| 573 | + */ | |
| 574 | + public function getPickerConfig(): array | |
| 575 | + { | |
| 576 | + $accessToken = $this->getValidAccessToken(); | |
| 577 | + if ($accessToken === null) { | |
| 578 | + return [ | |
| 579 | + 'success' => false, | |
| 580 | + 'code' => self::PICKER_ERROR_CODE, | |
| 581 | + 'message' => BackendStrings::getIntegrationsStrings()['google_sheets_not_connected'], | |
| 582 | + ]; | |
| 583 | + } | |
| 584 | + | |
| 585 | + $pickerKeys = $this->getCachedOrFetchedPickerKeys(); | |
| 586 | + if (empty($pickerKeys['success'])) { | |
| 587 | + $code = (string) ($pickerKeys['code'] ?? self::PICKER_ERROR_CODE); | |
| 588 | + | |
| 589 | + return [ | |
| 590 | + 'success' => false, | |
| 591 | + 'code' => $code, | |
| 592 | + 'message' => $code === self::PICKER_NOT_CONFIGURED_CODE | |
| 593 | + ? BackendStrings::getIntegrationsStrings()['google_sheets_picker_not_configured'] | |
| 594 | + : BackendStrings::getIntegrationsStrings()['google_sheets_picker_error'], | |
| 595 | + ]; | |
| 596 | + } | |
| 597 | + | |
| 598 | + return [ | |
| 599 | + 'success' => true, | |
| 600 | + 'developerKey' => $pickerKeys['developerKey'], | |
| 601 | + 'appId' => $pickerKeys['appId'], | |
| 602 | + 'accessToken' => $accessToken, | |
| 603 | + ]; | |
| 604 | + } | |
| 605 | + | |
| 606 | + /** | |
| 607 | + * @return array{success: true, developerKey: string, appId: string}|array{success: false, code: string} | |
| 608 | + */ | |
| 609 | + private function getCachedOrFetchedPickerKeys(): array | |
| 610 | + { | |
| 611 | + $cached = get_transient(self::PICKER_CONFIG_TRANSIENT); | |
| 612 | + if (is_array($cached)) { | |
| 613 | + $developerKey = trim((string) ($cached['developerKey'] ?? '')); | |
| 614 | + $appId = trim((string) ($cached['appId'] ?? '')); | |
| 615 | + if ($developerKey !== '' && $appId !== '') { | |
| 616 | + return [ | |
| 617 | + 'success' => true, | |
| 618 | + 'developerKey' => $developerKey, | |
| 619 | + 'appId' => $appId, | |
| 620 | + ]; | |
| 621 | + } | |
| 622 | + } | |
| 623 | + | |
| 624 | + $fetched = $this->fetchMiddlewarePickerConfig(); | |
| 625 | + if (!empty($fetched['success'])) { | |
| 626 | + set_transient(self::PICKER_CONFIG_TRANSIENT, [ | |
| 627 | + 'developerKey' => $fetched['developerKey'], | |
| 628 | + 'appId' => $fetched['appId'], | |
| 629 | + ], DAY_IN_SECONDS); | |
| 630 | + } | |
| 631 | + | |
| 632 | + return $fetched; | |
| 633 | + } | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * @return array{success: true, developerKey: string, appId: string}|array{success: false, code: string} | |
| 637 | + */ | |
| 638 | + private function fetchMiddlewarePickerConfig(): array | |
| 639 | + { | |
| 640 | + $baseUrl = $this->getMiddlewareApiBaseUrl(); | |
| 641 | + if ($baseUrl === '') { | |
| 642 | + return [ | |
| 643 | + 'success' => false, | |
| 644 | + 'code' => self::PICKER_ERROR_CODE, | |
| 645 | + ]; | |
| 646 | + } | |
| 647 | + | |
| 648 | + $response = wp_remote_get( | |
| 649 | + $baseUrl . '/google-sheets/picker-config', | |
| 650 | + ['timeout' => 15] | |
| 651 | + ); | |
| 652 | + | |
| 653 | + if (is_wp_error($response)) { | |
| 654 | + return [ | |
| 655 | + 'success' => false, | |
| 656 | + 'code' => self::PICKER_ERROR_CODE, | |
| 657 | + ]; | |
| 658 | + } | |
| 659 | + | |
| 660 | + $statusCode = (int) wp_remote_retrieve_response_code($response); | |
| 661 | + $body = json_decode((string) wp_remote_retrieve_body($response), true); | |
| 662 | + $developerKey = ''; | |
| 663 | + $appId = ''; | |
| 664 | + if (is_array($body)) { | |
| 665 | + $developerKey = trim((string) ($body['developerKey'] ?? '')); | |
| 666 | + $appId = trim((string) ($body['appId'] ?? '')); | |
| 667 | + } | |
| 668 | + | |
| 669 | + $keysMissing = $developerKey === '' || $appId === ''; | |
| 670 | + if ($statusCode === 400 && $keysMissing) { | |
| 671 | + return [ | |
| 672 | + 'success' => false, | |
| 673 | + 'code' => self::PICKER_NOT_CONFIGURED_CODE, | |
| 674 | + ]; | |
| 675 | + } | |
| 676 | + | |
| 677 | + if ($statusCode < 200 || $statusCode >= 300 || !is_array($body)) { | |
| 678 | + return [ | |
| 679 | + 'success' => false, | |
| 680 | + 'code' => self::PICKER_ERROR_CODE, | |
| 681 | + ]; | |
| 682 | + } | |
| 683 | + | |
| 684 | + if ($keysMissing) { | |
| 685 | + return [ | |
| 686 | + 'success' => false, | |
| 687 | + 'code' => self::PICKER_NOT_CONFIGURED_CODE, | |
| 688 | + ]; | |
| 689 | + } | |
| 690 | + | |
| 691 | + return [ | |
| 692 | + 'success' => true, | |
| 693 | + 'developerKey' => $developerKey, | |
| 694 | + 'appId' => $appId, | |
| 695 | + ]; | |
| 549 | 696 | } |
| 550 | 697 | |
| 551 | 698 | private function fetchMiddlewareAuthorizationUrl(string $state): string |
| 552 | 699 | { |