Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"phpunit/phpunit": "*",
"phpstan/phpstan": "^2.0"
},
"suggest": {
"ext-apcu": "Optional: required only by the built-in ApcuSessionCache backend."
},
"autoload": {
"psr-4": {
"INTERMediator\\FileMakerServer\\RESTAPI\\": "src/"
Expand Down
114 changes: 87 additions & 27 deletions src/FMDataAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace INTERMediator\FileMakerServer\RESTAPI;

use INTERMediator\FileMakerServer\RESTAPI\SessionCache\SessionCacheInterface;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\FileMakerLayout;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\FileMakerRelation;
use INTERMediator\FileMakerServer\RESTAPI\Supporting\CommunicationProvider;
Expand Down Expand Up @@ -58,23 +59,33 @@
* Ex. [{"database"=>"<databaseName>", "username"=>"<username>", "password"=>"<password>"}].
* If you use OAuth, "oAuthRequestId" and "oAuthIdentifier" keys have to be specified.
* @param boolean $isUnitTest If it's set to true, the communication provider just works locally.
*/
public function __construct(string $solution,
string $user,
string|null $password,
string|null $host = null,
int|null $port = null,
string|null $protocol = null,
array|null $fmDataSource = null,
bool $isUnitTest = false)
* @param SessionCacheInterface|null $sessionCache Cache backend for persistent sessions.
* If omitted, the library logs in and out on every database operation, or once
* per communication scope when using startCommunication() / endCommunication().
* If specified, session tokens are persisted and reused across requests via
* startCommunication() / endCommunication(), avoiding redundant logins against the FileMaker Server.
* When a session cache is specified, {@see self::setRetryOnAccessTokenInvalidation()} is
* automatically set to true, ensuring the library re-authenticates and retries the request if
* the cached token has expired on the FileMaker Server.
*/
public function __construct(string $solution,
string $user,
string|null $password,
string|null $host = null,
int|null $port = null,
string|null $protocol = null,
array|null $fmDataSource = null,
bool $isUnitTest = false,
SessionCacheInterface|null $sessionCache = null)
{
if (is_null($password)) {
$password = "password"; // For testing purpose.
}

if (!$isUnitTest) {
$this->provider = new Supporting\CommunicationProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource);
$this->provider = new Supporting\CommunicationProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource, $sessionCache);

Check failure on line 86 in src/FMDataAPI.php

View workflow job for this annotation

GitHub Actions / Run PHPStan (5)

Parameter #5 $port of class INTERMediator\FileMakerServer\RESTAPI\Supporting\CommunicationProvider constructor expects string|null, int|null given.
} else {
$this->provider = new Supporting\TestProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource);
$this->provider = new Supporting\TestProvider($solution, $user, $password, $host, $port, $protocol, $fmDataSource, $sessionCache);

Check failure on line 88 in src/FMDataAPI.php

View workflow job for this annotation

GitHub Actions / Run PHPStan (5)

Parameter #5 $port of class INTERMediator\FileMakerServer\RESTAPI\Supporting\TestProvider constructor expects string|null, int|null given.
}
}

Expand Down Expand Up @@ -264,33 +275,42 @@
}

/**
* Start a transaction which is a serial calling of multiple database operations before the single authentication.
* Usually most methods login and logout before/after the database operation, and so a little bit of time is going to
* take.
* The startCommunication() login and endCommunication() logout, and methods between them don't log in/out, and
* it can expect faster operations.
* Start a communication scope with a shared authenticated session.
*
* Usually most methods login and logout before and after each database operation.
* By calling startCommunication() and endCommunication(), methods between them don't
* log in and out every time, and it can expect faster operations.
*
* Without a session cache, one authenticated session is kept for the duration of
* the current communication scope and discarded when endCommunication() is called.
*
* With a session cache, the session token is persisted beyond the current communication
* scope and reused across requests. If no cached token is available, a new session is
* created and stored for future reuse.
*
* @throws Exception
*/
public function startCommunication(): void
{
try {
if ($this->provider->login()) {
$this->provider->keepAuth = true;
}
} catch (Exception $e) {
$this->provider->keepAuth = false;
throw $e;
}
$this->provider->startCommunication();
}

/**
* Finish a transaction which is a serial calling of any database operations, and logout.
* Finish a communication scope.
*
* Without a session cache, the authenticated session for the current communication
* scope is ended and the server session is logged out.
*
* With a session cache, the cached token's TTL is renewed if it still matches the
* token held by this instance. If another process has replaced the cached token in
* the meantime, only this instance's now-stale token is logged out, leaving the
* newer cached token intact.
*
* @throws Exception
*/
public function endCommunication(): void
{
$this->provider->keepAuth = false;
$this->provider->logout();
$this->provider->endCommunication();
}

/**
Expand Down Expand Up @@ -425,4 +445,44 @@
{
$this->provider->excludeTimeStampInException = $value;
}

/**
* Controls whether failed Data API calls are automatically retried after session invalidation.
*
* When enabled and a call fails with error 952 (invalid token) or 112 (window missing), the
* current session is discarded, a new session is established, and the call is retried once.
*
* When a session cache is provided to the constructor, retry on token invalidation is always
* active regardless of this setting. This flag only has an effect when no session cache is
* configured.
*
* Warning: The retry runs in a fresh session. Any session-scoped state from the original session
* is lost — for example, global fields set before the retry will not carry over.
* @param bool $value
*/
public function setRetryOnAccessTokenInvalidation(bool $value = true): void
{
$this->provider->retryOnAccessTokenInvalidation = $value;
}

/**
* Overrides the time-to-live (TTL) of the cached FileMaker Data API session token.
*
* WARNING: Setting a TTL that exceeds the FileMaker Data API session timeout (15 minutes)
* will cause the library to use expired tokens, resulting in authentication failures.
* Do not use this method unless you fully understand the implications.
*
* The default TTL is 840 seconds (14 minutes), intentionally set one minute below the
* FileMaker Data API session timeout of 15 minutes to ensure the cached token is
* invalidated before it expires on the FileMaker Server.
* @param int $ttl Time-to-live in seconds. Defaults to 840 seconds (14 minutes).
* @throws Exception If a session cache is not set, an exception is thrown.
*/
public function setSessionCacheTtl(int $ttl = 840): void
{
if ($this->provider->sessionCache === null) {
throw new Exception("setSessionCacheTtl() requires a session cache to be configured via the constructor.");
}
$this->provider->sessionCache->setTtl($ttl);
}
}
73 changes: 73 additions & 0 deletions src/SessionCache/AbstractSessionCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace INTERMediator\FileMakerServer\RESTAPI\SessionCache;

/**
* Base class for session cache implementations.
*
* Provides the cache key and TTL to concrete implementations, both of which
* are managed internally by the library. The cache key and TTL will not change
* during a single PHP request.
*
* As this cache stores FileMaker Data API session tokens, which are sensitive
* credentials granting full API access on behalf of the authenticated user,
* implementors must ensure that the underlying cache storage is secure and
* not accessible to unauthorized parties.
*
* To provide a custom cache backend, extend this class and implement
* {@see SessionCacheInterface::get()}, {@see SessionCacheInterface::set()},
* and {@see SessionCacheInterface::delete()}, using {@see self::$key}
* and {@see self::$ttl} in your implementations.
*
* @see ApcuSessionCache for an example implementation using APCu.
* @see SessionCacheInterface for an alternative way to implement session caching without
* extending this class.
*/
abstract class AbstractSessionCache implements SessionCacheInterface
{
/**
* The cache key for the current session.
*
* Always set by the library via {@see SessionCacheInterface::setKey()} before any cache
* operation is performed. Will not change during a single PHP request.
* Implementing classes should use this property directly in their
* {@see SessionCacheInterface::get()}, {@see SessionCacheInterface::set()},
* and {@see SessionCacheInterface::delete()} implementations.
*/
protected string $key;

/**
* The time-to-live in seconds for cached session tokens.
*
* Set by the library via {@see SessionCacheInterface::setTtl()} before any cache operation
* is performed, defaulting to the value provided at construction time.
* Will not change during a single PHP request. Implementing classes should
* use this property directly in their {@see SessionCacheInterface::set()} implementation.
*
*/
protected int $ttl;

/**
* @param int $defaultTtl Default time-to-live in seconds for cached session tokens.
* Defaults to 840 seconds (14 minutes), reflecting the
* default FileMaker Data API session timeout. Adjust this
* value if your FileMaker Server is configured with a
* different session timeout.
*/
public function __construct(int $defaultTtl = 840)
{
$this->ttl = $defaultTtl;
}

final public function setKey(string $key): void
{
$this->key = $key;
}

final public function setTtl(int $ttl): void
{
$this->ttl = $ttl;
}
}
83 changes: 83 additions & 0 deletions src/SessionCache/ApcuSessionCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace INTERMediator\FileMakerServer\RESTAPI\SessionCache;

use RuntimeException;

/**
* APCu-based session cache implementation.
*
* Caches FileMaker Data API session tokens using APCu, which stores data in
* shared memory on the server. Note that depending on your setup APCu cache
* may be shared across PHP processes on the same server, so cache keys must be
* sufficiently unique to avoid collisions between different users and applications.
*
* Requires that the APCu extension is installed and enabled. See the
* documentation here for more information: https://www.php.net/apcu
*
* As this cache stores sensitive FileMaker Data API session tokens, APCu is
* only appropriate in environments where server memory access is properly
* restricted.
*
* Note that cache operations in this implementation are not atomic. While care
* has been taken to minimize the risk of race conditions, concurrent requests
* sharing the same cache key may occasionally result in redundant
* re-authentication against the FileMaker Server. This is considered an
* acceptable trade-off given the constraints of the current implementation.
*
* @package INTER-Mediator\FileMakerServer\RESTAPI\SessionCache
* @link https://github.com/msyk/FMDataAPI GitHub Repository
* @version 36
*/
class ApcuSessionCache extends AbstractSessionCache
{
/**
* ApcuSessionCache constructor.
* @throws RuntimeException If APCu is not available.
*/
public function __construct()
{
parent::__construct();
if (!function_exists('apcu_enabled') || !apcu_enabled()) {
throw new RuntimeException("APCu is required to use ApcuSessionCache.");
}
}

/**
* Retrieves the cached FileMaker Data API session token for the current session.
*
* @return string|null The cached session token, or null if no token exists
* for the current key.
*/
public function get(): string|null
{
$value = apcu_fetch($this->key, $success);
return $success && is_string($value) ? $value : null;
}

/**
* Persists a FileMaker Data API session token in APCu.
*
* @param string $value The FileMaker Data API session token to store.
* This is a sensitive credential and must be treated as such.
* @return bool True on success, false on failure.
*/
public function set(string $value): bool
{
return apcu_store($this->key, $value, $this->ttl);
}

/**
* Deletes the cached FileMaker Data API session token.
*
* Returns false both when the key does not exist and when deletion fails.
*
* @return bool True on success, false if the key did not exist or deletion failed.
*/
public function delete(): bool
{
return apcu_delete($this->key);
}
}
Loading
Loading