1
<?php
2

3
declare(strict_types=1);
4

5
namespace Kreait\Firebase;
6

7
use Firebase\Auth\Token\Cache\InMemoryCache;
8
use Firebase\Auth\Token\Domain\Generator;
9
use Firebase\Auth\Token\Domain\Verifier;
10
use Firebase\Auth\Token\Generator as CustomTokenGenerator;
11
use Firebase\Auth\Token\HttpKeyStore;
12
use Firebase\Auth\Token\Verifier as LegacyIdTokenVerifier;
13
use Google\Auth\ApplicationDefaultCredentials;
14
use Google\Auth\Cache\MemoryCacheItemPool;
15
use Google\Auth\Credentials\AppIdentityCredentials;
16
use Google\Auth\Credentials\GCECredentials;
17
use Google\Auth\Credentials\ServiceAccountCredentials;
18
use Google\Auth\Credentials\UserRefreshCredentials;
19
use Google\Auth\CredentialsLoader;
20
use Google\Auth\FetchAuthTokenCache;
21
use Google\Auth\HttpHandler\HttpHandlerFactory;
22
use Google\Auth\Middleware\AuthTokenMiddleware;
23
use Google\Auth\ProjectIdProviderInterface;
24
use Google\Cloud\Firestore\FirestoreClient;
25
use Google\Cloud\Storage\StorageClient;
26
use GuzzleHttp\Client;
27
use GuzzleHttp\HandlerStack;
28
use function GuzzleHttp\Psr7\uri_for;
29
use Kreait\Clock;
30
use Kreait\Clock\SystemClock;
31
use Kreait\Firebase;
32
use Kreait\Firebase\Auth\CustomTokenViaGoogleIam;
33
use Kreait\Firebase\Auth\DisabledLegacyCustomTokenGenerator;
34
use Kreait\Firebase\Auth\DisabledLegacyIdTokenVerifier;
35
use Kreait\Firebase\Auth\IdTokenVerifier;
36
use Kreait\Firebase\Exception\InvalidArgumentException;
37
use Kreait\Firebase\Exception\RuntimeException;
38
use Kreait\Firebase\Http\Middleware;
39
use Kreait\Firebase\Project\ProjectId;
40
use Kreait\Firebase\Value\Email;
41
use Kreait\Firebase\Value\Url;
42
use Psr\Cache\CacheItemPoolInterface;
43
use Psr\Http\Message\UriInterface;
44
use Psr\SimpleCache\CacheInterface;
45
use Throwable;
46

47
class Factory
48
{
49
    public const API_CLIENT_SCOPES = [
50
        'https://www.googleapis.com/auth/iam',
51
        'https://www.googleapis.com/auth/cloud-platform',
52
        'https://www.googleapis.com/auth/firebase',
53
        'https://www.googleapis.com/auth/firebase.database',
54
        'https://www.googleapis.com/auth/firebase.messaging',
55
        'https://www.googleapis.com/auth/firebase.remoteconfig',
56
        'https://www.googleapis.com/auth/userinfo.email',
57
        'https://www.googleapis.com/auth/securetoken',
58
    ];
59

60
    /** @var UriInterface|null */
61
    protected $databaseUri;
62

63
    /** @var string|null */
64
    protected $defaultStorageBucket;
65

66
    /** @var ServiceAccount|null */
67
    protected $serviceAccount;
68

69
    /** @var ServiceAccountCredentials|UserRefreshCredentials|AppIdentityCredentials|GCECredentials|CredentialsLoader|null */
70
    protected $googleAuthTokenCredentials;
71

72
    /** @var ProjectId|null */
73
    protected $projectId;
74

75
    /** @var Email|null */
76
    protected $clientEmail;
77

78
    /** @var CacheInterface */
79
    protected $verifierCache;
80

81
    /** @var CacheItemPoolInterface */
82
    protected $authTokenCache;
83

84
    /** @var bool */
85
    protected $discoveryIsDisabled = false;
86

87
    /** @var bool */
88
    protected $debug = false;
89

90
    /** @var string|null */
91
    protected $httpProxy;
92

93
    /** @var string */
94
    protected static $databaseUriPattern = 'https://%s.firebaseio.com';
95

96
    /** @var string */
97
    protected static $storageBucketNamePattern = '%s.appspot.com';
98

99
    /** @var Clock */
100
    protected $clock;
101

102 12
    public function __construct()
103
    {
104 12
        $this->clock = new SystemClock();
105 12
        $this->verifierCache = new InMemoryCache();
106 12
        $this->authTokenCache = new MemoryCacheItemPool();
107
    }
108

109
    /**
110
     * @param string|array<string, string>|ServiceAccount $value
111
     */
112 12
    public function withServiceAccount($value): self
113
    {
114 12
        $serviceAccount = ServiceAccount::fromValue($value);
115

116 12
        $factory = clone $this;
117 12
        $factory->serviceAccount = $serviceAccount;
118

119
        return $factory
120 12
            ->withProjectId($serviceAccount->getProjectId())
121 12
            ->withClientEmail($serviceAccount->getClientEmail());
122
    }
123

124 12
    public function withProjectId(string $projectId): self
125
    {
126 12
        $factory = clone $this;
127 12
        $factory->projectId = ProjectId::fromString($projectId);
128

129 12
        return $factory;
130
    }
131

132 12
    public function withClientEmail(string $clientEmail): self
133
    {
134 12
        $factory = clone $this;
135 12
        $factory->clientEmail = new Email($clientEmail);
136

137 12
        return $factory;
138
    }
139

140 9
    public function withDisabledAutoDiscovery(): self
141
    {
142 9
        $factory = clone $this;
143 9
        $factory->discoveryIsDisabled = true;
144

145 9
        return $factory;
146
    }
147

148
    /**
149
     * @param UriInterface|string $uri
150
     */
151 9
    public function withDatabaseUri($uri): self
152
    {
153 9
        $factory = clone $this;
154 9
        $factory->databaseUri = uri_for($uri);
155

156 9
        return $factory;
157
    }
158

159 9
    public function withDefaultStorageBucket(string $name): self
160
    {
161 9
        $factory = clone $this;
162 9
        $factory->defaultStorageBucket = $name;
163

164 9
        return $factory;
165
    }
166

167 9
    public function withVerifierCache(CacheInterface $cache): self
168
    {
169 9
        $factory = clone $this;
170 9
        $factory->verifierCache = $cache;
171

172 9
        return $factory;
173
    }
174

175 0
    public function withAuthTokenCache(CacheItemPoolInterface $cache): self
176
    {
177 0
        $factory = clone $this;
178 0
        $factory->authTokenCache = $cache;
179

180 0
        return $factory;
181
    }
182

183 0
    public function withEnabledDebug(): self
184
    {
185 0
        $factory = clone $this;
186 0
        $factory->debug = true;
187

188 0
        return $factory;
189
    }
190

191 0
    public function withHttpProxy(string $proxy): self
192
    {
193 0
        $factory = clone $this;
194 0
        $factory->httpProxy = $proxy;
195

196 0
        return $factory;
197
    }
198

199 9
    public function withClock(Clock $clock): self
200
    {
201 9
        $factory = clone $this;
202 9
        $factory->clock = $clock;
203

204 9
        return $factory;
205
    }
206

207 12
    protected function getServiceAccount(): ?ServiceAccount
208
    {
209 12
        if ($this->serviceAccount) {
210 12
            return $this->serviceAccount;
211
        }
212

213 9
        if ($credentials = Util::getenv('FIREBASE_CREDENTIALS')) {
214 9
            return $this->serviceAccount = ServiceAccount::fromValue($credentials);
215
        }
216

217 9
        if ($this->discoveryIsDisabled) {
218 9
            return null;
219
        }
220

221 9
        if ($credentials = Util::getenv('GOOGLE_APPLICATION_CREDENTIALS')) {
222
            try {
223 9
                return $this->serviceAccount = ServiceAccount::fromValue($credentials);
224 0
            } catch (InvalidArgumentException $e) {
225
                // Do nothing, continue trying
226
            }
227
        }
228

229
        // @codeCoverageIgnoreStart
230
        // We can't reliably test this without re-implementing it ourselves
231
        if ($credentials = CredentialsLoader::fromWellKnownFile()) {
232
            try {
233
                return $this->serviceAccount = ServiceAccount::fromValue($credentials);
234
            } catch (InvalidArgumentException $e) {
235
                // Do nothing, continue trying
236
            }
237
        }
238
        // @codeCoverageIgnoreEnd
239

240
        // ... or don't
241 9
        return null;
242
    }
243

244 12
    protected function getProjectId(): ?ProjectId
245
    {
246 12
        if ($this->projectId) {
247 12
            return $this->projectId;
248
        }
249

250 9
        if ($serviceAccount = $this->getServiceAccount()) {
251 9
            return $this->projectId = ProjectId::fromString($serviceAccount->getProjectId());
252
        }
253

254 9
        if ($this->discoveryIsDisabled) {
255 9
            return null;
256
        }
257

258
        if (
259 9
            ($credentials = $this->getGoogleAuthTokenCredentials())
260 9
            && ($credentials instanceof ProjectIdProviderInterface)
261 9
            && ($projectId = $credentials->getProjectId())
262
        ) {
263 9
            return $this->projectId = ProjectId::fromString($projectId);
264
        }
265

266 9
        if ($projectId = Util::getenv('GOOGLE_CLOUD_PROJECT')) {
267 9
            return $this->projectId = ProjectId::fromString((string) $projectId);
268
        }
269

270 9
        if ($projectId = Util::getenv('GCLOUD_PROJECT')) {
271 9
            return $this->projectId = ProjectId::fromString((string) $projectId);
272
        }
273

274 9
        return null;
275
    }
276

277 12
    protected function getClientEmail(): ?Email
278
    {
279 12
        if ($this->clientEmail) {
280 12
            return $this->clientEmail;
281
        }
282

283 9
        if ($serviceAccount = $this->getServiceAccount()) {
284 9
            return new Email($serviceAccount->getClientEmail());
285
        }
286

287 9
        return null;
288
    }
289

290 12
    protected function getDatabaseUri(): UriInterface
291
    {
292 12
        if ($this->databaseUri) {
293 9
            return $this->databaseUri;
294
        }
295

296 12
        if ($projectId = $this->getProjectId()) {
297 12
            return $this->databaseUri = uri_for(\sprintf(self::$databaseUriPattern, $projectId->sanitizedValue()));
298
        }
299

300 9
        throw new RuntimeException('Unable to build a database URI without a project ID');
301
    }
302

303 12
    protected function getStorageBucketName(): ?string
304
    {
305 12
        if ($this->defaultStorageBucket) {
306 12
            return $this->defaultStorageBucket;
307
        }
308

309 12
        if ($projectId = $this->getProjectId()) {
310 12
            return $this->defaultStorageBucket = \sprintf(self::$storageBucketNamePattern, $projectId->sanitizedValue());
311
        }
312

313 9
        return null;
314
    }
315

316 12
    public function createAuth(): Auth
317
    {
318 12
        $http = $this->createApiClient([
319 12
            'base_uri' => 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/',
320
        ]);
321 12
        $apiClient = new Auth\ApiClient($http);
322

323 12
        $customTokenGenerator = $this->createCustomTokenGenerator();
324 12
        $idTokenVerifier = $this->createIdTokenVerifier();
325

326 12
        $signInHandler = new Firebase\Auth\SignIn\GuzzleHandler($http);
327

328 12
        return new Auth($apiClient, $customTokenGenerator, $idTokenVerifier, $signInHandler);
329
    }
330

331 12
    public function createCustomTokenGenerator(): Generator
332
    {
333 12
        $serviceAccount = $this->getServiceAccount();
334 12
        $clientEmail = $this->getClientEmail();
335 12
        $privateKey = $serviceAccount ? $serviceAccount->getPrivateKey() : '';
336

337 12
        if ($clientEmail && $privateKey !== '') {
338 12
            return new CustomTokenGenerator((string) $clientEmail, $privateKey);
339
        }
340

341 9
        if ($clientEmail) {
342 9
            return new CustomTokenViaGoogleIam((string) $clientEmail, $this->createApiClient());
343
        }
344

345 9
        return new DisabledLegacyCustomTokenGenerator(
346 9
            'Custom Token Generation is disabled because the current credentials do not permit it'
347
        );
348
    }
349

350 12
    public function createIdTokenVerifier(): Verifier
351
    {
352 12
        if (!($projectId = $this->getProjectId())) {
353 9
            return new DisabledLegacyIdTokenVerifier(
354 9
                'ID Token Verification is disabled because no project ID was provided'
355
            );
356
        }
357

358 12
        $keyStore = new HttpKeyStore(new Client(), $this->verifierCache);
359

360 12
        $baseVerifier = new LegacyIdTokenVerifier($projectId->sanitizedValue(), $keyStore);
361

362 12
        return new IdTokenVerifier($baseVerifier, $this->clock);
363
    }
364

365 12
    public function createDatabase(): Database
366
    {
367 12
        $http = $this->createApiClient();
368

369
        /** @var HandlerStack $handler */
370 12
        $handler = $http->getConfig('handler');
371 12
        $handler->push(Firebase\Http\Middleware::ensureJsonSuffix(), 'realtime_database_json_suffix');
372

373 12
        return new Database($this->getDatabaseUri(), new Database\ApiClient($http));
374
    }
375

376 12
    public function createRemoteConfig(): RemoteConfig
377
    {
378 12
        if (!($projectId = $this->getProjectId())) {
379 9
            throw new RuntimeException('Unable to create the messaging service without a project ID');
380
        }
381

382 3
        $http = $this->createApiClient([
383 3
            'base_uri' => "https://firebaseremoteconfig.googleapis.com/v1/projects/{$projectId->value()}/remoteConfig",
384
        ]);
385

386 3
        return new RemoteConfig(new RemoteConfig\ApiClient($http));
387
    }
388

389 12
    public function createMessaging(): Messaging
390
    {
391 12
        if (!($projectId = $this->getProjectId())) {
392 9
            throw new RuntimeException('Unable to create the messaging service without a project ID');
393
        }
394

395 3
        $messagingApiClient = new Messaging\ApiClient(
396 3
            $this->createApiClient([
397 3
                'base_uri' => 'https://fcm.googleapis.com/v1/projects/'.$projectId->value(),
398
            ])
399
        );
400

401 3
        $appInstanceApiClient = new Messaging\AppInstanceApiClient(
402 3
            $this->createApiClient([
403 3
                'base_uri' => 'https://iid.googleapis.com',
404
                'headers' => [
405
                    'access_token_auth' => 'true',
406
                ],
407
            ])
408
        );
409

410 3
        return new Messaging($messagingApiClient, $appInstanceApiClient, $projectId);
411
    }
412

413
    /**
414
     * @param string|Url|UriInterface|mixed $defaultDynamicLinksDomain
415
     */
416 12
    public function createDynamicLinksService($defaultDynamicLinksDomain = null): DynamicLinks
417
    {
418 12
        $apiClient = $this->createApiClient();
419

420 12
        if ($defaultDynamicLinksDomain) {
421 3
            return DynamicLinks::withApiClientAndDefaultDomain($apiClient, $defaultDynamicLinksDomain);
422
        }
423

424 9
        return DynamicLinks::withApiClient($apiClient);
425
    }
426

427 12
    public function createFirestore(): Firestore
428
    {
429 12
        $config = [];
430

431 12
        if ($serviceAccount = $this->getServiceAccount()) {
432 3
            $config['keyFile'] = $serviceAccount->asArray();
433 9
        } elseif ($this->discoveryIsDisabled) {
434 9
            throw new RuntimeException('Unable to create a Firestore Client without credentials');
435
        }
436

437 3
        if ($projectId = $this->getProjectId()) {
438 3
            $config['projectId'] = $projectId->value();
439
        }
440

441 3
        if (!$projectId) {
442
            // This is the case with user refresh credentials
443 0
            $config['suppressKeyFileNotice'] = true;
444
        }
445

446
        try {
447 3
            $firestoreClient = new FirestoreClient($config);
448 0
        } catch (Throwable $e) {
449 0
            throw new RuntimeException('Unable to create a FirestoreClient: '.$e->getMessage(), $e->getCode(), $e);
450
        }
451

452 3
        return Firestore::withFirestoreClient($firestoreClient);
453
    }
454

455 12
    public function createStorage(): Storage
456
    {
457 12
        $config = [];
458

459 12
        if ($serviceAccount = $this->getServiceAccount()) {
460 12
            $config['keyFile'] = $serviceAccount->asArray();
461 9
        } elseif ($this->discoveryIsDisabled) {
462 9
            throw new RuntimeException('Unable to create a Storage Client without credentials');
463
        }
464

465 12
        if ($projectId = $this->getProjectId()) {
466 12
            $config['projectId'] = $projectId->value();
467
        }
468

469 12
        if (!$projectId) {
470
            // This is the case with user refresh credentials
471 9
            $config['suppressKeyFileNotice'] = true;
472
        }
473

474
        try {
475 12
            $storageClient = new StorageClient($config);
476 0
        } catch (Throwable $e) {
477 0
            throw new RuntimeException('Unable to create a Storage Client: '.$e->getMessage(), $e->getCode(), $e);
478
        }
479

480 12
        return new Storage($storageClient, $this->getStorageBucketName());
481
    }
482

483
    /**
484
     * @internal
485
     *
486
     * @param array<string, mixed>|null $config
487
     */
488 12
    public function createApiClient(?array $config = null): Client
489
    {
490 12
        $config = $config ?? [];
491

492 12
        if ($this->debug) {
493 0
            $config['debug'] = true;
494
        }
495

496 12
        if ($this->httpProxy) {
497 0
            $config['proxy'] = $this->httpProxy;
498
        }
499

500 12
        $handler = $config['handler'] ?? null;
501

502 12
        if (!($handler instanceof HandlerStack)) {
503 12
            $handler = HandlerStack::create($handler);
504
        }
505

506 12
        if ($credentials = $this->getGoogleAuthTokenCredentials()) {
507 12
            $credentials = new FetchAuthTokenCache($credentials, null, $this->authTokenCache);
508 12
            $authTokenHandlerConfig = $config;
509 12
            $authTokenHandlerConfig['handler'] = clone $handler;
510

511 12
            $authTokenHandler = HttpHandlerFactory::build(new Client($authTokenHandlerConfig));
512

513 12
            $handler->push(new AuthTokenMiddleware($credentials, $authTokenHandler));
514
        }
515

516 12
        $handler->push(Middleware::responseWithSubResponses());
517

518 12
        $config['handler'] = $handler;
519 12
        $config['auth'] = 'google_auth';
520

521 12
        return new Client($config);
522
    }
523

524
    /**
525
     * @internal
526
     *
527
     * @param ServiceAccountCredentials|UserRefreshCredentials|AppIdentityCredentials|GCECredentials|CredentialsLoader $credentials
528
     */
529 9
    public function withGoogleAuthTokenCredentials($credentials): self
530
    {
531 9
        $factory = clone $this;
532 9
        $factory->googleAuthTokenCredentials = $credentials;
533

534 9
        return $factory;
535
    }
536

537
    /**
538
     * @return ServiceAccountCredentials|UserRefreshCredentials|AppIdentityCredentials|GCECredentials|CredentialsLoader|null
539
     */
540 12
    protected function getGoogleAuthTokenCredentials()
541
    {
542 12
        if ($this->googleAuthTokenCredentials) {
543 12
            return $this->googleAuthTokenCredentials;
544
        }
545

546 12
        if ($serviceAccount = $this->getServiceAccount()) {
547 12
            return $this->googleAuthTokenCredentials = new ServiceAccountCredentials(self::API_CLIENT_SCOPES, $serviceAccount->asArray());
548
        }
549

550 9
        if ($this->discoveryIsDisabled) {
551 9
            return null;
552
        }
553

554
        try {
555 9
            return $this->googleAuthTokenCredentials = ApplicationDefaultCredentials::getCredentials(self::API_CLIENT_SCOPES);
556 9
        } catch (Throwable $e) {
557 9
            return null;
558
        }
559
    }
560
}

Read our documentation on viewing source code .

Loading