WIP Mail Rest API: UI for application passwords/tokens for admin

This commit is contained in:
ralf 2023-07-03 17:09:26 +02:00
parent e210d4b3c6
commit 07300704bc
10 changed files with 625 additions and 25 deletions

View File

@ -72,6 +72,7 @@ class admin_hooks
if (! $GLOBALS['egw']->acl->check('account_access',16,'admin'))
{
$file['Bulk password reset'] = Egw::link('/index.php','menuaction=admin.admin_passwordreset.index&ajax=true');
$file['Application passwords'] = Egw::link('/index.php', 'menuaction=admin.EGroupware\\Admin\\Token.index&ajax=true');
}
if (! $GLOBALS['egw']->acl->check('group_access',1,'admin'))

View File

@ -19,6 +19,7 @@ import {egw} from "../../api/js/jsapi/egw_global.js";
import {egwAction, egwActionObject} from '../../api/js/egw_action/egw_action.js';
import {LitElement} from "@lion/core";
import {et2_nextmatch} from "../../api/js/etemplate/et2_extension_nextmatch";
import {et2_DOMWidget} from "../../api/js/etemplate/et2_core_DOMWidget";
/**
* UI for Admin
@ -1643,6 +1644,24 @@ class AdminApp extends EgwApp
}
}, this).sendRequest();
}
/**
* Clickhandler to copy given text or widget content to clipboard
* @param _widget
* @param _text default widget content
*/
copyClipboard(_widget : et2_DOMWidget, _text? : string, _event? : Event)
{
let value = _text || (typeof _widget.get_value === 'function' ? _widget.get_value() : _widget.options.value);
let node = _widget.getDOMNode() !== _widget ? _widget.getDOMNode() : _widget;
this.egw.copyTextToClipboard(value, node, _event).then((success) =>
{
if(success !== false)
{
this.egw.message(this.egw.lang("Copied '%1' to clipboard", value), 'success');
}
});
}
}
app.classes.admin = AdminApp;

268
admin/src/Token.php Normal file
View File

@ -0,0 +1,268 @@
<?php
/**
* EGroupware - Admin - Application passwords / tokens
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb-AT-egroupware.org>
* @package admin
* @copyright (c) 2023 by Ralf Becker <rb-AT-egroupware.org>
* @license https://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
*/
namespace EGroupware\Admin;
use EGroupware\Api;
class Token
{
const APP = 'admin';
/**
* Methods callable via menuaction GET parameter
*
* @var array
*/
public $public_functions = [
'index' => true,
'edit' => true,
];
/**
* Instance of our business object
*
* @var Api\Auth\Token
*/
protected $token;
/**
* Constructor
*/
public function __construct()
{
$this->token = new Api\Auth\Token();
}
/**
* Edit a host
*
* @param array $content =null
*/
public function edit(array $content=null)
{
if (!is_array($content))
{
if (!empty($_GET['token_id']))
{
if (!($content = $this->token->read(['token_id' => $_GET['token_id']])))
{
Api\Framework::window_close(lang('Token not found!'));
}
}
else
{
$content = $this->token->init();
if (empty($GLOBALS['egw_info']['user']['apps']['admin']))
{
$content['account_id'] = $GLOBALS['egw_info']['user']['account_id'];
}
}
}
elseif (!empty($content['button']))
{
try {
$button = key($content['button']);
unset($content['button']);
switch($button)
{
case 'save':
case 'apply':
$content['token_limits'] = Api\Auth\Token::apps2limits($content['token_apps']);
if (empty($content['token_id']))
{
$content = Api\Auth\Token::create($content['account_id'] ?: 0, $content['token_valid_until'], $content['token_remark'],
$content['token_limits']);
Api\Framework::refresh_opener(lang('Token created.'),
self::APP, $this->token->data['token_id'],'add');
$button = 'apply'; // must not close window to show token
}
elseif (!$this->token->save($content))
{
Api\Framework::refresh_opener(lang('Token saved.'),
self::APP, $this->token->data['token_id'],'edit');
$content = array_merge($content, $this->token->data);
}
else
{
throw new \Exception(lang('Error storing token!'));
}
if ($button === 'save')
{
Api\Framework::window_close(); // does NOT return
}
break;
case 'delete':
if (!$this->token->revoke($content['token_id']))
{
Api\Framework::message(lang('Error revoking token!'));
}
else
{
Api\Framework::refresh_opener(lang('Token revoked.'),
Bo::APP, $content['token_id'], 'update');
Api\Framework::window_close(); // does NOT return
}
}
}
catch(\Exception $e) {
Api\Framework::message($e->getMessage(), 'error');
}
}
$content['token_apps'] = Api\Auth\Token::limits2apps($content['token_limits']);
if (empty($content['account_id'])) $content['account_id'] = '';
$readonlys = [
'button[delete]' => !$content['token_id'],
'account_id' => empty($GLOBALS['egw_info']['user']['apps']['admin']),
];
$tmpl = new Api\Etemplate(self::APP.'.token.edit');
$tmpl->exec(self::APP.'.'.self::class.'.edit', $content, [], $readonlys, $content, 2);
}
/**
* Fetch rows to display
*
* @param array $query
* @param array& $rows =null
* @param array& $readonlys =null
*/
public function get_rows($query, array &$rows=null, array &$readonlys=null)
{
$total = $this->token->get_rows($query, $rows, $readonlys);
foreach($rows as &$row)
{
$row['token_apps'] = Api\Auth\Token::limits2apps($row['token_limits']);
if ($row['token_revoked'])
{
$row['class'] = 'revoked';
}
}
return $total;
}
/**
* Index
*
* @param array $content =null
*/
public function index(array $content=null)
{
if (!is_array($content) || empty($content['nm']))
{
$content = [
'nm' => [
'get_rows' => self::APP.'.'.__CLASS__.'.get_rows',
'no_filter' => true, // disable the diverse filters we not (yet) use
'no_filter2' => true,
'no_cat' => true,
'order' => 'token_id',// IO name of the column to sort after (optional for the sortheaders)
'sort' => 'DESC',// IO direction of the sort: 'ASC' or 'DESC'
'row_id' => 'token_id',
'actions' => $this->get_actions(),
'placeholder_actions' => array('add'),
'add_link' => Api\Egw::link('/index.php', 'menuaction='.self::APP.'.'.self::class.'.edit'),
]
];
}
elseif(!empty($content['nm']['action']))
{
try {
Api\Framework::message($this->action($content['nm']['action'],
$content['nm']['selected'], $content['nm']['select_all']));
}
catch (\Exception $ex) {
Api\Framework::message($ex->getMessage(), 'error');
}
}
$tmpl = new Api\Etemplate(self::APP.'.tokens');
$tmpl->exec(self::APP.'.'.self::class.'.index', $content, [
'account_id' => ['0' => lang('All users')]
], [], ['nm' => $content['nm']]);
}
/**
* Return actions for cup list
*
* @param array $cont values for keys license_(nation|year|cat)
* @return array
*/
protected function get_actions()
{
return [
'edit' => [
'caption' => 'Edit',
'default' => true,
'allowOnMultiple' => false,
'url' => 'menuaction='.self::APP.'.'.self::class.'.edit&token_id=$id',
'popup' => '640x480',
'group' => $group=0,
],
'add' => [
'caption' => 'Create',
'url' => 'menuaction='.self::APP.'.'.self::class.'.edit',
'popup' => '640x400',
'group' => $group,
],
'activate' => [
'caption' => 'Activate',
'confirm' => 'Active this token again',
'enableClass' => 'revoked',
'group' => $group=5,
],
'revoke' => [
'caption' => 'Revoke',
'confirm' => 'Revoke this token',
'icon' => 'delete',
'disableClass' => 'revoked',
'group' => $group,
],
];
}
/**
* Execute action on list
*
* @param string $action
* @param array|int $selected
* @param boolean $select_all
* @returns string with success message
* @throws Api\Exception\AssertionFailed
*/
protected function action($action, $selected, $select_all)
{
$success = 0;
try {
switch($action)
{
case 'revoke':
case 'activate':
$revoke = $action === 'revoke';
foreach($selected as $token_id)
{
Api\Auth\Token::revoke($token_id, $revoke);
++$success;
}
return lang('%1 token %2.', $success, $revoke ? lang('revoked') : lang('activated again'));
default:
throw new Api\Exception\AssertionFailed('To be implemented ;)');
}
}
catch(\Exception $e) {
if ($success) {
$e = new \Exception($e->getMessage().', '.lang('%1 successful', $success), $e);
}
throw $e;
}
}
}

View File

@ -187,4 +187,15 @@ Admin command
border-left: 0;
border-bottom: 0;
background: white;
}
/**
* Application passwords / tokens
*/
/**
* EPL application Firewall
*/
tr.revoked > td * {
color: grey !important;
font-style: italic;
}

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
<overlay>
<template id="admin.token.edit" template="" lang="" group="0" version="19.1">
<grid width="100%">
<columns>
<column width="100"/>
<column/>
</columns>
<rows>
<row disabled="!@token">
<et2-description value="Token"></et2-description>
<et2-textbox id="token" readonly="true" onclick="app.admin.copyClipboard(this)"></et2-textbox>
</row>
<row>
<et2-description for="account_id" value="User"></et2-description>
<et2-select-account id="account_id" accountType="user" emptyLabel="All users"></et2-select-account>
</row>
<row valign="top">
<et2-description for="token_limits" value="Applications"></et2-description>
<et2-vbox>
<et2-select-app id="token_apps" multiple="true" placeholder="All applications of the user"></et2-select-app>
<et2-description value="Select the applications you want the token to be limited to, or leave the default of all applications."></et2-description>
</et2-vbox>
</row>
<row>
<et2-description for="token_valid_until" value="Expiration"></et2-description>
<et2-date id="token_valid_until" dataFormat="object"></et2-date>
</row>
<row>
<et2-description for="token_remark" value="Remark"></et2-description>
<et2-textarea id="token_remark" rows="5"></et2-textarea>
</row>
<row disabled="!@token_id">
<et2-description value="Creator"></et2-description>
<et2-hbox>
<et2-select-account id="token_created_by" readonly="true"></et2-select-account>
<et2-date-time id="token_created" readonly="true" align="right"></et2-date-time>
</et2-hbox>
</row>
<row disabled="!@token_updated_by">
<et2-description value="Last updated"></et2-description>
<et2-hbox>
<et2-select-account id="token_updated_by" readonly="true"></et2-select-account>
<et2-date-time id="token_updated" readonly="true" align="right"></et2-date-time>
</et2-hbox>
</row>
<row>
<et2-hbox span="all">
<et2-button accesskey="s" label="Save" id="button[save]"></et2-button>
<et2-button label="Apply" id="button[apply]"></et2-button>
<et2-button label="Cancel" id="button[cancel]" onclick="window.close(); return false;"></et2-button>
<et2-button align="right" label="Revoke" id="button[delete]"
onclick="et2_dialog.confirm(widget,'Do you really want to revoke this token?','Revoke')"></et2-button>
</et2-hbox>
</row>
</rows>
</grid>
</template>
</overlay>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2.0//EN" "https://www.egroupware.org/etemplate2.0.dtd">
<overlay>
<template id="admin.tokens.rows" template="" lang="" group="0" version="1.9.001">
<grid width="100%" height="100%">
<columns>
<column width="30"/>
<column width="120"/> <!-- User / All user -->
<column width="25%"/> <!-- Applications -->
<column width="120"/> <!-- Expiration -->
<column width="120"/> <!-- Revoked / By -->
<column width="120"/> <!-- Created / By -->
<column width="120"/> <!-- Updated / By -->
<column width="30%"/> <!-- remark -->
</columns>
<rows>
<row>
<nextmatch-sortheader label="ID" id="token_id"/>
<et2-nextmatch-header-account id="account_id" emptyLabel="User" accountType="user">
<option value="0">All users</option>
</et2-nextmatch-header-account>
<nextmatch-header id="token_apps" label="Applications"/>
<nextmatch-sortheader label="Expiration" id="token_valid_until"/>
<et2-vbox>
<nextmatch-sortheader label="Revoked" id="token_revoked"/>
<et2-nextmatch-header-account label="Revoked by" id="token_revoked_by"></et2-nextmatch-header-account>
</et2-vbox>
<et2-vbox>
<nextmatch-sortheader label="Created" id="token_created"/>
<et2-nextmatch-header-account label="Created by" id="token_created_by"></et2-nextmatch-header-account>
</et2-vbox>
<et2-vbox>
<nextmatch-sortheader label="Updated" id="token_created"/>
<et2-nextmatch-header-account label="Updated by" id="token_created_by"></et2-nextmatch-header-account>
</et2-vbox>
<nextmatch-header label="Remark" id="token_remark"/>
</row>
<row class="$row_cont[class]">
<et2-description id="${row}[token_id]" noLang="1"></et2-description>
<et2-select-account id="${row}[account_id]" readonly="true"></et2-select-account>
<et2-select-app id="${row}[token_apps]" readonly="true" multiple="true"></et2-select-app>
<et2-date id="${row}[token_valid_until]" readonly="true"></et2-date>
<et2-vbox>
<et2-date-time id="${row}[token_revoked]" readonly="true"></et2-date-time>
<et2-select-account id="${row}[token_revoked_by]" readonly="true"></et2-select-account>
</et2-vbox>
<et2-vbox>
<et2-date-time id="${row}[token_created]" readonly="true"></et2-date-time>
<et2-select-account id="${row}[token_created_by]" readonly="true"></et2-select-account>
</et2-vbox>
<et2-vbox>
<et2-date-time id="${row}[token_updated]" readonly="true"></et2-date-time>
<et2-select-account id="${row}[token_updated_by]" readonly="true"></et2-select-account>
</et2-vbox>
<et2-description id="${row}[token_remark]"></et2-description>
</row>
</rows>
</grid>
</template>
<template id="admin.tokens.add" template="" lang="" group="0" version="1.9.001">
<et2-button label="Add" id="add" onclick="window.open('$cont[add_link]','_blank','dependent=yes,width=600,height=380,scrollbars=yes,status=yes'); return false;" noSubmit="true"></et2-button>
</template>
<template id="admin.tokens" template="" lang="" group="0" version="1.9.001">
<nextmatch id="nm" template="admin.tokens.rows" header_left="admin.tokens.add"/>
</template>
</overlay>

View File

@ -11,7 +11,7 @@
/* Basic information about this app */
$setup_info['api']['name'] = 'api';
$setup_info['api']['title'] = 'EGroupware API';
$setup_info['api']['version'] = '23.1.001';
$setup_info['api']['version'] = '23.1.002';
$setup_info['api']['versions']['current_header'] = '1.29';
// maintenance release in sync with changelog in doc/rpm-build/debian.changes
$setup_info['api']['versions']['maintenance_release'] = '23.1.20230620';
@ -139,4 +139,4 @@ $setup_info['groupdav']['author'] = $setup_info['groupdav']['maintainer'] = arra
);
$setup_info['groupdav']['license'] = 'GPL';
$setup_info['groupdav']['hooks']['preferences'] = 'EGroupware\\Api\\CalDAV\\Hooks::menus';
$setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings';
$setup_info['groupdav']['hooks']['settings'] = 'EGroupware\\Api\\CalDAV\\Hooks::settings';

View File

@ -528,7 +528,7 @@ $phpgw_baseline = array(
),
'egw_tokens' => array(
'fd' => array(
'token_id' => array('type' => 'int','precision' => '4','nullable' => False),
'token_id' => array('type' => 'auto','precision' => '4','nullable' => False),
'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => '0=all users'),
'token_hash' => array('type' => 'ascii','precision' => '128','nullable' => False,'comment' => 'hash of token'),
'token_limits' => array('type' => 'ascii','meta' => 'json','precision' => '4096','comment' => 'limit run rights of session'),
@ -537,11 +537,13 @@ $phpgw_baseline = array(
'token_valid_until' => array('type' => 'timestamp'),
'token_revoked' => array('type' => 'timestamp'),
'token_revoked_by' => array('type' => 'int','meta' => 'user','precision' => '4'),
'token_remark' => array('type' => 'varchar','precision' => 255)
'token_remark' => array('type' => 'varchar','precision' => '1024'),
'token_updated' => array('type' => 'timestamp'),
'token_updated_by' => array('type' => 'int','meta' => 'user','precision' => '4')
),
'pk' => array('token_id'),
'fk' => array(),
'ix' => array('account_id'),
'uc' => array()
)
);
);

View File

@ -859,7 +859,7 @@ function api_upgrade23_1()
{
$GLOBALS['egw_setup']->oProc->CreateTable('egw_tokens',array(
'fd' => array(
'token_id' => array('type' => 'int','precision' => '4','nullable' => False),
'token_id' => array('type' => 'auto','precision' => '4','nullable' => False),
'account_id' => array('type' => 'int','meta' => 'user','precision' => '4','nullable' => False,'comment' => '0=all users'),
'token_hash' => array('type' => 'ascii','precision' => '128','nullable' => False,'comment' => 'hash of token'),
'token_limits' => array('type' => 'ascii','meta' => 'json','precision' => '4096','comment' => 'limit run rights of session'),
@ -868,7 +868,9 @@ function api_upgrade23_1()
'token_valid_until' => array('type' => 'timestamp'),
'token_revoked' => array('type' => 'timestamp'),
'token_revoked_by' => array('type' => 'int','meta' => 'user','precision' => '4'),
'token_remark' => array('type' => 'varchar','precision' => 255)
'token_remark' => array('type' => 'varchar','precision' => 1024),
'token_updated' => array('type' => 'timestamp'),
'token_updated_by' => array('type' => 'int','meta' => 'user','precision' => '4')
),
'pk' => array('token_id'),
'fk' => array(),
@ -876,5 +878,28 @@ function api_upgrade23_1()
'uc' => array()
));
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.001';
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.002';
}
function api_upgrade23_1_001()
{
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_tokens','token_id',array(
'type' => 'auto',
'precision' => '4',
'nullable' => False
));
$GLOBALS['egw_setup']->oProc->AlterColumn('egw_tokens','token_remark',array(
'type' => 'varchar',
'precision' => '1024'
));
$GLOBALS['egw_setup']->oProc->AddColumn('egw_tokens','token_updated',array(
'type' => 'timestamp'
));
$GLOBALS['egw_setup']->oProc->AddColumn('egw_tokens','token_updated_by',array(
'type' => 'int',
'meta' => 'user',
'precision' => '4'
));
return $GLOBALS['setup_info']['api']['currentver'] = '23.1.002';
}

View File

@ -33,6 +33,7 @@ class Token extends APi\Storage\Base
public function __construct()
{
parent::__construct(self::APP, self::TABLE, null, '', true, 'object');
$this->convert_all_timestamps();
}
/**
@ -42,55 +43,202 @@ class Token extends APi\Storage\Base
* @param string $token must start with "token<token_id>:", or function will return null
* @param ?array& $limits on return limits of token
* @return bool|null null: $token is no token, probably a password, false: invalid token, true: valid token for $user
* @throws \Exception
*/
public static function authenticate(string $user, string $token, array& $limits=null)
{
if (!preg_match(self::TOKEN_REGEXP, $token, $matches))
{
return null; // no a token
return null; // not a token
}
if (!($data = self::getInstance()->read([
try {
$data = self::getInstance()->read([
'token_id' => $matches[1],
'account_id' => [0, Api\Accounts::getInstance()->name2id($user)],
'token_revoked' => null,
'(token_valid_until IS NULL OR token_valid_until > NOW())'
])) || !password_verify($matches[2], $data['token_hash']))
{
return false; // wrong/invalid token
]);
if (!password_verify($matches[2], $data['token_hash']))
{
return false; // invalid token password
}
$limits = $data['token_limits'];
return true;
}
catch (Api\Exception\NotFound $e) {
return false; // token not found
}
$limits = $data['token_limits'] ? json_decode($data['token_limits'], true) : null;
return true;
}
/**
* Create a token and return it
*
* @param int $account_id
* @param ?DateTime $until
* @param ?\DateTimeInterface $until
* @param ?string $remark
* @param ?array $limits app-name => rights pairs, run rights are everything evaluation to true,
* the rights can be an array with more granulate rights, but the app needs to check this itself!
* @return string
* @return array full token record plus token under key "token"
* @throws Api\Exception\NoPermission\Admin if non-admin user tries to create token for anyone else
* @throws Api\Exception\NotFound if token_id does NOT exist
* @throws Api\Db\Exception if token could not be stored
*/
public static function create(int $account_id, DateTime $until=null, string $remark=null, array $limits=null): string
public static function create(int $account_id, \DateTimeInterface $until=null, string $remark=null, array $limits=null): array
{
if (empty($GLOBALS['egw_info']['user']['apps']['admin']))
{
$account_id = $GLOBALS['egw_info']['user']['account_id'];
}
$token = Api\Auth::randomstring(16);
$inst = self::getInstance();
$inst->init([
'account_id' => $account_id,
'token_hash' => password_hash($token, PASSWORD_DEFAULT),
'token_created' => new Api\DateTime(),
'token_created_by' => $GLOBALS['egw_info']['user']['account_id'],
'token_valid_until' => $until,
'token_remark' => $remark,
'token_limits' => $limits ? json_encode($limits) : null,
'token_limits' => $limits,
]);
if (!($token_id = $inst->save()))
$inst->save();
return $inst->data+[
'token' => self::PREFIX.$inst->data['token_id'].'_'.$token,
];
}
/**
* Revoke or (re-)activate a token
*
* @param int $token_id
* @param bool $revoke true: revoke, false: (re-)activate
* @throws Api\Exception\NoPermission\Admin if non-admin user tries to create token for anyone else
* @throws Api\Exception\NotFound if token_id does NOT exist
* @throws Api\Db\Exception if token could not be stored
*/
public static function revoke(int $token_id, bool $revoke=true)
{
$inst = self::getInstance();
$inst->read($token_id);
return $inst->save([
'token_revoked_by' => $GLOBALS['egw_info']['user']['account_id'],
'token_revoked' => $revoke ? $inst->now : null,
]);
}
/**
* saves the content of data to the db
*
* @param array $keys =null if given $keys are copied to data before saveing => allows a save as
* @param string|array $extra_where =null extra where clause, eg. to check an etag, returns true if no affected rows!
* @return int|boolean 0 on success, or errno != 0 on error, or true if $extra_where is given and no rows affected
* @throws Api\Exception\NoPermission\Admin if non-admin user tries to create token for anyone else
* @throws Api\Exception\NotFound if token_id does NOT exist
* @throws Api\Db\Exception if token could not be stored
*/
function save($keys=null,$extra_where=null)
{
if (is_array($keys) && count($keys)) $this->data_merge($keys);
if (empty($GLOBALS['egw_info']['user']['apps']['admin']) && $this->data['account_id'] != $GLOBALS['egw_info']['user']['account_id'])
{
throw new Api\Exception('Error storing token');
throw new Api\Exception\NoPermission\Admin();
}
return self::PREFIX.$token_id.'_'.$token;
if (empty($this->data['token_id']))
{
$this->data['token_created_by'] = $GLOBALS['egw_info']['user']['account_id'];
$this->data['token_created'] = $this->now;
}
else
{
$this->data['token_updated_by'] = $GLOBALS['egw_info']['user']['account_id'];
$this->data['token_updated'] = $this->now;
}
if (($ret = parent::save(null, $extra_where)))
{
throw new Api\Db\Exception(lang('Error storing token'));
}
return $ret;
}
/**
* reads row matched by key and puts all cols in the data array
*
* @param array $keys array with keys in form internalName => value, may be a scalar value if only one key
* @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num"
* @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or
* @return array data if row could be retrieved
* @throws Api\Exception\NotFound if entry was NOT found
*/
function read($keys,$extra_cols='',$join='')
{
if (!($data = parent::read($keys, $extra_cols, $join)))
{
throw new Api\Exception\NotFound();
}
return $data;
}
/**
* Convert limits to allowed apps
*
* @param array|null $limits
* @return array of app-names
*/
public static function limits2apps(array $limits=null): array
{
return $limits ? array_keys(array_filter($limits)) : [];
}
/**
* Convert apps to (default, value === true) limits
*
* @param array $apps
* @return array|null
*/
public static function apps2limits(array $apps): ?array
{
return $apps ? array_combine($apps, array_fill(0, count($apps), true)) : null;
}
/**
* Changes the data from the db-format to your work-format
*
* @param array $data =null if given works on that array and returns result, else works on internal data-array
* @return array
*/
function db2data($data=null)
{
if (($intern = !is_array($data)))
{
$data =& $this->data;
}
if (is_string($data['token_limits']))
{
$data['token_limits'] = json_decode($data['token_limits'], true);
}
return parent::db2data($intern ? null : $data); // important to use null, if $intern!
}
/**
* Changes the data from your work-format to the db-format
*
* @param array $data =null if given works on that array and returns result, else works on internal data-array
* @return array
*/
function data2db($data=null)
{
if (($intern = !is_array($data)))
{
$data =& $this->data;
}
if (is_array($data['token_limits']))
{
$data['token_limits'] = $data['token_limits'] ? json_encode($data['token_limits']) : null;
}
return parent::data2db($intern ? null : $data);
}
private static self $instance;