213 lines
7.9 KiB
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);
|
|
}
|
|
}
|