misuzu/src/Apps/AppsData.php

213 lines
7.9 KiB
PHP

<?php
namespace Misuzu\Apps;
use stdClass;
use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Db\{DbConnection,DbStatementCache};
use Misuzu\Users\UserInfo;
class AppsData {
private DbStatementCache $cache;
public function __construct(DbConnection $dbConn) {
$this->cache = new DbStatementCache($dbConn);
}
public function getAppInfo(
?string $appId = null,
?string $clientId = null,
?bool $deleted = null
): AppInfo {
$hasAppId = $appId !== null;
$hasClientId = $clientId !== null;
$hasDeleted = $deleted !== null;
if($hasAppId === $hasClientId)
throw new InvalidArgumentException('you must specify either $appId or $clientId');
$values = [];
$query = <<<SQL
SELECT app_id, user_id, app_name, app_summary, app_website, app_type,
app_access_lifetime, app_refresh_lifetime, app_client_id, app_client_secret,
UNIX_TIMESTAMP(app_created), UNIX_TIMESTAMP(app_updated), UNIX_TIMESTAMP(app_deleted)
FROM msz_apps
SQL;
$query .= sprintf(' WHERE %s = ?', $hasAppId ? 'app_id' : 'app_client_id');
if($hasDeleted)
$query .= sprintf(' AND app_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->nextParameter($hasAppId ? $appId : $clientId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Application not found.');
return AppInfo::fromResult($result);
}
public const int CLIENT_ID_LENGTH = 20;
public const string CLIENT_SECRET_ALGO = PASSWORD_ARGON2ID;
public function createApp(
string $name,
AppType $type,
UserInfo|string|null $userInfo = null,
string $summary = '',
string $website = '',
?string $clientId = null,
#[\SensitiveParameter] ?string $clientSecret = null,
?int $accessLifetime = null,
?int $refreshLifetime = null
): AppInfo {
if(trim($name) === '')
throw new InvalidArgumentException('$name may not be empty');
if($clientId === null)
$clientId = XString::random(self::CLIENT_ID_LENGTH);
elseif(trim($clientId) === '')
throw new InvalidArgumentException('$clientId may not be empty');
if($accessLifetime !== null && $accessLifetime < 1)
throw new InvalidArgumentException('$accessLifetime must be null or greater than zero');
if($refreshLifetime !== null && $refreshLifetime < 0)
throw new InvalidArgumentException('$refreshLifetime must be null or a positive integer');
$summary = trim($summary);
$website = trim($website);
$clientSecret = $clientSecret === null ? '' : password_hash($clientSecret, self::CLIENT_SECRET_ALGO);
if($type !== AppType::Public && $clientSecret === '')
throw new InvalidArgumentException('$clientSecret must be specified for confidential clients');
$stmt = $this->cache->get(<<<SQL
INSERT INTO msz_apps (
user_id, app_name, app_summary, app_website, app_type,
app_access_lifetime, app_refresh_lifetime,
app_client_id, app_client_secret
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
SQL);
$stmt->nextParameter($userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->nextParameter($name);
$stmt->nextParameter($summary);
$stmt->nextParameter($website);
$stmt->nextParameter($type);
$stmt->nextParameter($accessLifetime);
$stmt->nextParameter($refreshLifetime);
$stmt->nextParameter($clientId);
$stmt->nextParameter($clientSecret);
$stmt->execute();
return $this->getAppInfo(appId: (string)$stmt->lastInsertId);
}
public function countAppUris(AppInfo|string $appInfo): int {
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_apps_uris WHERE app_id = ?');
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
/** @return iterable<AppUriInfo> */
public function getAppUriInfos(AppInfo|string $appInfo): iterable {
$stmt = $this->cache->get(<<<SQL
SELECT uri_id, app_id, uri_string, UNIX_TIMESTAMP(uri_created)
FROM msz_apps_uris
WHERE app_id = ?
SQL);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->execute();
return $stmt->getResultIterator(AppUriInfo::fromResult(...));
}
public function getAppUriInfo(string $uriId): AppUriInfo {
$stmt = $this->cache->get(<<<SQL
SELECT uri_id, app_id, uri_string, UNIX_TIMESTAMP(uri_created)
FROM msz_apps_uris
WHERE uri_id = ?
SQL);
$stmt->nextParameter($uriId);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('URI not found.');
return AppUriInfo::fromResult($result);
}
public function getAppUriId(AppInfo|string $appInfo, string $uriString): ?string {
$stmt = $this->cache->get('SELECT uri_id FROM msz_apps_uris WHERE app_id = ? AND uri_string = ?');
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($uriString);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getStringOrNull(0) : null;
}
public function getAppScopes(AppInfo|string $appInfo): AppScopesInfo {
$allowed = [];
$denied = [];
$stmt = $this->cache->get(<<<SQL
SELECT (
SELECT scope_string
FROM msz_scopes AS _s
WHERE _s.scope_id = _as.scope_id
), scope_allowed
FROM msz_apps_scopes AS _as
WHERE app_id = ?
SQL);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->execute();
$result = $stmt->getResult();
while($result->next()) {
$scopeStr = $result->getString(0);
if($result->getBoolean(1))
$allowed[] = $scopeStr;
else
$denied[] = $scopeStr;
}
return new AppScopesInfo($allowed, $denied);
}
public function setAppScopeAllow(AppInfo|string $appInfo, ScopeInfo $scopeInfo, ?bool $allowed): void {
if($allowed !== $scopeInfo->restricted) {
$stmt = $this->cache->get('DELETE FROM msz_apps_scopes WHERE app_id = ? AND scope_id = ?');
} else {
$stmt = $this->cache->get('REPLACE INTO msz_apps_scopes (app_id, scope_id, scope_allowed) VALUES (?, ?, ?)');
$stmt->addParameter(3, $allowed ? 1 : 0);
}
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->nextParameter($scopeInfo->id);
$stmt->execute();
}
public function isAppScopeAllowed(AppInfo|string $appInfo, ScopeInfo|string $scopeInfo): bool {
$stmt = $this->cache->get(<<<SQL
SELECT ? AS _scope_id, COALESCE(
(SELECT scope_allowed FROM msz_apps_scopes WHERE app_id = ? AND scope_id = _scope_id),
(SELECT NOT scope_restricted FROM msz_scopes WHERE scope_id = _scope_id)
)
SQL);
$stmt->nextParameter($scopeInfo instanceof ScopeInfo ? $scopeInfo->id : $scopeInfo);
$stmt->nextParameter($appInfo instanceof AppInfo ? $appInfo->id : $appInfo);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('failed to check scope ACL');
return $result->getBoolean(1);
}
}