Merge branch '26-acl' into 'dev'
Resolve "Acl" Closes #26 See merge request pnx/httpcb!24
This commit is contained in:
commit
52f17efed8
9 changed files with 286 additions and 177 deletions
42
app/config/acl.yml
Normal file
42
app/config/acl.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
# ACL in this system is defined as follows:
|
||||||
|
#
|
||||||
|
# - Roles:
|
||||||
|
# Roles define a group of user. like Author, Admin, Guest etc.
|
||||||
|
# Each role can inherit other roles with the "inherit" key.
|
||||||
|
# Each role can gain access to a zone (explained later) by the
|
||||||
|
# "allowed-zones" key. Per default a role is denied access to all zones.
|
||||||
|
#
|
||||||
|
# - Resources:
|
||||||
|
# Resources maps directly to controller names.
|
||||||
|
# There a 2 controllers/resources that are a bit special,
|
||||||
|
# index and error resources are always accessible by everyone (e.g. they
|
||||||
|
# are not part of the ACL).
|
||||||
|
#
|
||||||
|
# - Access levels.
|
||||||
|
# These are not used in this system. a hardcoded "All" level is used.
|
||||||
|
#
|
||||||
|
# Zones
|
||||||
|
#
|
||||||
|
# Zones defines a group of resources. for example an "backend" zone can
|
||||||
|
# have 2 controllers/resources (site-config, user-manager)
|
||||||
|
#
|
||||||
|
# Zones might be implemented using modules later.
|
||||||
|
|
||||||
|
acl:
|
||||||
|
roles:
|
||||||
|
guest:
|
||||||
|
allowed-zones: public
|
||||||
|
description: Non logged in users
|
||||||
|
user:
|
||||||
|
inherits: guest
|
||||||
|
allowed-zones: user
|
||||||
|
description: Logged in users
|
||||||
|
#admin:
|
||||||
|
# inherits: user
|
||||||
|
# allowed-zones: backend
|
||||||
|
|
||||||
|
zones:
|
||||||
|
public: [ auth, api ]
|
||||||
|
user: [ user, callback ]
|
||||||
|
#backend: [ site, user-man ]
|
||||||
|
|
@ -20,7 +20,7 @@ router:
|
||||||
cb-endpoint:
|
cb-endpoint:
|
||||||
pattern: '/cb/{id}/:params'
|
pattern: '/cb/{id}/:params'
|
||||||
path:
|
path:
|
||||||
controller: callback
|
controller: api
|
||||||
action: endpoint
|
action: endpoint
|
||||||
login:
|
login:
|
||||||
pattern: '/login'
|
pattern: '/login'
|
||||||
|
|
@ -54,5 +54,5 @@ router:
|
||||||
activation-link:
|
activation-link:
|
||||||
pattern: '/activate/{link}'
|
pattern: '/activate/{link}'
|
||||||
path:
|
path:
|
||||||
controller: user
|
controller: api
|
||||||
action: activationlink
|
action: activationlink
|
||||||
|
|
|
||||||
90
app/controllers/ApiController.php
Normal file
90
app/controllers/ApiController.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Controller\ControllerBase,
|
||||||
|
App\Model\Data\Callback as CallbackModel,
|
||||||
|
App\Model\Data\Request as RequestModel,
|
||||||
|
App\Model\Data\RequestMeta as RequestMetaModel,
|
||||||
|
App\Model\Data\UserActivation;
|
||||||
|
|
||||||
|
class ApiController extends ControllerBase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This is the action that the API to be
|
||||||
|
* tested should make it's callback to. So we can catch it.
|
||||||
|
*
|
||||||
|
* @param int $id The test session id so
|
||||||
|
* we know what test it belongs to.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function endpointAction($id)
|
||||||
|
{
|
||||||
|
$this->view->disable();
|
||||||
|
|
||||||
|
$allowed_methods = array('GET', 'POST');
|
||||||
|
if ($this->request->isMethod($allowed_methods)) {
|
||||||
|
|
||||||
|
$callback = CallbackModel::get($id);
|
||||||
|
|
||||||
|
$request = new RequestModel();
|
||||||
|
|
||||||
|
$request->setHeaders($this->request->getHeaders());
|
||||||
|
$request->setBody($this->request->getRawBody());
|
||||||
|
|
||||||
|
$dt = new \DateTime();
|
||||||
|
|
||||||
|
$callback->setLastRequest($dt->format('Y-m-d H:i:s'));
|
||||||
|
|
||||||
|
$meta = new RequestMetaModel();
|
||||||
|
$meta->Callback = $callback;
|
||||||
|
$meta->RequestObject = $request;
|
||||||
|
|
||||||
|
$meta->setSourceIp($this->request->getClientAddress());
|
||||||
|
$meta->setMethod($this->request->isPost() ? 'POST' : 'GET');
|
||||||
|
$meta->setUri($this->request->getServer('REQUEST_URI'));
|
||||||
|
|
||||||
|
$result = $meta->save();
|
||||||
|
if ($result == false) {
|
||||||
|
var_dump($meta->getMessages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account/Password activation.
|
||||||
|
*
|
||||||
|
* @param $id
|
||||||
|
*/
|
||||||
|
public function activationLinkAction($id)
|
||||||
|
{
|
||||||
|
$link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [ $id ]]);
|
||||||
|
|
||||||
|
if ($link) {
|
||||||
|
if ($link->isValid()) {
|
||||||
|
|
||||||
|
$user = $link->getUser();
|
||||||
|
|
||||||
|
// Save password if any is set.
|
||||||
|
if (strlen($link->getPassword()) > 0) {
|
||||||
|
$user->setPassword($link->getPassword());
|
||||||
|
$this->flash->success('Your password has been activated.');
|
||||||
|
} else {
|
||||||
|
$user->setStatus(User::STATUS_ACTIVE);
|
||||||
|
$this->flash->success('Your account has been activated.');
|
||||||
|
|
||||||
|
// Also login the user.
|
||||||
|
$this->auth->systemLogin($user);
|
||||||
|
}
|
||||||
|
$user->save();
|
||||||
|
} else {
|
||||||
|
$this->flash->error('This link has expired or has already been used.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the link is deleted.
|
||||||
|
$link->delete();
|
||||||
|
} else {
|
||||||
|
$this->flash->error('This does not seem to be an active link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,7 @@ namespace App\Controller;
|
||||||
|
|
||||||
use App\Controller\ControllerBase,
|
use App\Controller\ControllerBase,
|
||||||
App\Form\CallbackCreate as CreateCallbackForm,
|
App\Form\CallbackCreate as CreateCallbackForm,
|
||||||
App\Model\Data\Callback as CallbackModel,
|
App\Model\Data\Callback as CallbackModel;
|
||||||
App\Model\Data\Request as RequestModel,
|
|
||||||
App\Model\Data\RequestMeta as RequestMetaModel;
|
|
||||||
|
|
||||||
class CallbackController extends ControllerBase
|
class CallbackController extends ControllerBase
|
||||||
{
|
{
|
||||||
|
|
@ -116,48 +114,4 @@ class CallbackController extends ControllerBase
|
||||||
$this->view->page = $paginator->getPaginate();
|
$this->view->page = $paginator->getPaginate();
|
||||||
$this->view->pagination_url = '/callback/show/' . $id . '/';
|
$this->view->pagination_url = '/callback/show/' . $id . '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the action that the API to be
|
|
||||||
* tested should make it's callback to. So we can catch it.
|
|
||||||
*
|
|
||||||
* @Acl(resource="api")
|
|
||||||
* @param int $id The test session id so
|
|
||||||
* we know what test it belongs to.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function endpointAction($id)
|
|
||||||
{
|
|
||||||
$this->view->disable();
|
|
||||||
|
|
||||||
$allowed_methods = array('GET', 'POST');
|
|
||||||
if ($this->request->isMethod($allowed_methods)) {
|
|
||||||
|
|
||||||
$callback = CallbackModel::get($id);
|
|
||||||
|
|
||||||
$request = new RequestModel();
|
|
||||||
|
|
||||||
$request->setHeaders($this->request->getHeaders());
|
|
||||||
$request->setBody($this->request->getRawBody());
|
|
||||||
|
|
||||||
$dt = new \DateTime();
|
|
||||||
|
|
||||||
$callback->setLastRequest($dt->format('Y-m-d H:i:s'));
|
|
||||||
|
|
||||||
$meta = new RequestMetaModel();
|
|
||||||
$meta->Callback = $callback;
|
|
||||||
$meta->RequestObject = $request;
|
|
||||||
|
|
||||||
$meta->setSourceIp($this->request->getClientAddress());
|
|
||||||
$meta->setMethod($this->request->isPost() ? 'POST' : 'GET');
|
|
||||||
$meta->setUri($this->request->getServer('REQUEST_URI'));
|
|
||||||
|
|
||||||
$result = $meta->save();
|
|
||||||
if ($result == false) {
|
|
||||||
var_dump($meta->getMessages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,44 +106,6 @@ class UserController extends ControllerBase
|
||||||
$this->response->redirect('/settings');
|
$this->response->redirect('/settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Account/Password activation.
|
|
||||||
*
|
|
||||||
* @Acl(resource="auth")
|
|
||||||
* @param $id
|
|
||||||
*/
|
|
||||||
public function activationLinkAction($id)
|
|
||||||
{
|
|
||||||
$link = UserActivation::findFirst(['activation_key = ?0', 'bind' => [ $id ]]);
|
|
||||||
|
|
||||||
if ($link) {
|
|
||||||
if ($link->isValid()) {
|
|
||||||
|
|
||||||
$user = $link->getUser();
|
|
||||||
|
|
||||||
// Save password if any is set.
|
|
||||||
if (strlen($link->getPassword()) > 0) {
|
|
||||||
$user->setPassword($link->getPassword());
|
|
||||||
$this->flash->success('Your password has been activated.');
|
|
||||||
} else {
|
|
||||||
$user->setStatus(User::STATUS_ACTIVE);
|
|
||||||
$this->flash->success('Your account has been activated.');
|
|
||||||
|
|
||||||
// Also login the user.
|
|
||||||
$this->auth->systemLogin($user);
|
|
||||||
}
|
|
||||||
$user->save();
|
|
||||||
} else {
|
|
||||||
$this->flash->error('This link has expired or has already been used.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the link is deleted.
|
|
||||||
$link->delete();
|
|
||||||
} else {
|
|
||||||
$this->flash->error('This does not seem to be an active link');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function activityAction($page = 1)
|
public function activityAction($page = 1)
|
||||||
{
|
{
|
||||||
$user = $this->_getAuth()->getUser();
|
$user = $this->_getAuth()->getUser();
|
||||||
|
|
|
||||||
|
|
@ -2,50 +2,93 @@
|
||||||
|
|
||||||
namespace Httpcb;
|
namespace Httpcb;
|
||||||
|
|
||||||
use Phalcon\Acl\Role,
|
use Phalcon\Config,
|
||||||
Phalcon\Acl\Adapter\Memory as AclList;
|
Phalcon\Acl\Role,
|
||||||
|
Phalcon\Acl\Adapter\Memory as Adapter;
|
||||||
|
|
||||||
class Acl extends AclList
|
class Acl
|
||||||
{
|
{
|
||||||
const ROLE_USER = 'user';
|
const ROLE_USER = 'user';
|
||||||
const ROLE_GUEST = 'guest';
|
const ROLE_GUEST = 'guest';
|
||||||
|
|
||||||
public function __construct()
|
/**
|
||||||
|
* @var Adapter
|
||||||
|
*/
|
||||||
|
protected $_adapter = null;
|
||||||
|
|
||||||
|
public function __construct(Config $config)
|
||||||
{
|
{
|
||||||
|
$this->_adapter = new Adapter();
|
||||||
|
|
||||||
// Deny access to everything by default.
|
// Deny access to everything by default.
|
||||||
$this->setDefaultAction(\Phalcon\Acl::DENY);
|
$this->_adapter->setDefaultAction(\Phalcon\Acl::DENY);
|
||||||
|
|
||||||
// Roles
|
$this->fromConfig($config);
|
||||||
$guest = new Role(self::ROLE_GUEST);
|
|
||||||
$user = new Role(self::ROLE_USER);
|
|
||||||
|
|
||||||
$this->addRole($guest);
|
|
||||||
$this->addRole($user, $guest);
|
|
||||||
|
|
||||||
// Public Resources
|
|
||||||
$public = array(
|
|
||||||
'index',
|
|
||||||
'error',
|
|
||||||
'auth',
|
|
||||||
'api',
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->_grant($guest, $public);
|
|
||||||
|
|
||||||
// Protected Resources
|
|
||||||
$protected = array(
|
|
||||||
'callback',
|
|
||||||
'user',
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->_grant($user, $protected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _grant(Role $role, array $resources)
|
/**
|
||||||
|
* @param $role
|
||||||
|
* @param $resource
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isAllowed($role, $resource)
|
||||||
{
|
{
|
||||||
foreach($resources as $resource) {
|
return $this->_adapter->isAllowed($role, $resource, 'All') == \Phalcon\Acl::ALLOW;
|
||||||
$this->addResource($resource, 'Read');
|
}
|
||||||
$this->allow($role->getName(), $resource, 'Read');
|
|
||||||
|
/**
|
||||||
|
* @param string $resource
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function hasResource($resource)
|
||||||
|
{
|
||||||
|
return $this->_adapter->isResource($resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fromConfig(Config $config)
|
||||||
|
{
|
||||||
|
// Add roles.
|
||||||
|
foreach($config->roles as $name => $def) {
|
||||||
|
|
||||||
|
$inherits = null;
|
||||||
|
$description = null;
|
||||||
|
|
||||||
|
if ($def instanceof Config) {
|
||||||
|
$inherits = $def->get('inherits');
|
||||||
|
$description = $def->get('description');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = new Role($name, $description);
|
||||||
|
$this->_adapter->addRole($role, $inherits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zones
|
||||||
|
foreach($config->zones as $name => $resources) {
|
||||||
|
|
||||||
|
if (!($resources instanceof Config)) {
|
||||||
|
$resources = new Config([ $resources ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($resources as $resource) {
|
||||||
|
$this->_adapter->addResource($resource, 'All');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant access for roles and resources.
|
||||||
|
foreach($config->roles as $name => $def) {
|
||||||
|
|
||||||
|
$zones = $def->get('allowed-zones', []);
|
||||||
|
|
||||||
|
if (is_string($zones)) {
|
||||||
|
$zones = [ $zones ];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($zones as $zone) {
|
||||||
|
foreach($config->zones->get($zone) as $resource) {
|
||||||
|
$this->_adapter->allow($name, $resource, 'All');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ use Httpcb\Auth,
|
||||||
Httpcb\Mail as MailService,
|
Httpcb\Mail as MailService,
|
||||||
Httpcb\ViewHelper\Volt\Extension as ViewHelperVoltExtension;
|
Httpcb\ViewHelper\Volt\Extension as ViewHelperVoltExtension;
|
||||||
|
|
||||||
use App\Listener\AclListener,
|
use App\Listener\AccessListener,
|
||||||
App\Listener\DispatchListener,
|
App\Listener\DispatchListener,
|
||||||
App\Listener\ActivityLog;
|
App\Listener\ActivityLog;
|
||||||
|
|
||||||
|
|
@ -70,7 +70,8 @@ class Services extends DiDefault
|
||||||
'app.yml',
|
'app.yml',
|
||||||
'routes.yml',
|
'routes.yml',
|
||||||
'menu.yml',
|
'menu.yml',
|
||||||
'local.yml'
|
'local.yml',
|
||||||
|
'acl.yml'
|
||||||
);
|
);
|
||||||
|
|
||||||
$basePath = APP_PATH . '/app/config/';
|
$basePath = APP_PATH . '/app/config/';
|
||||||
|
|
@ -124,7 +125,7 @@ class Services extends DiDefault
|
||||||
$eventsManager->attach('dispatch:beforeException', new DispatchListener());
|
$eventsManager->attach('dispatch:beforeException', new DispatchListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
$eventsManager->attach('dispatch:beforeExecuteRoute', new AclListener());
|
$eventsManager->attach('dispatch:beforeExecuteRoute', new AccessListener());
|
||||||
|
|
||||||
$dispatcher = new \Phalcon\Mvc\Dispatcher();
|
$dispatcher = new \Phalcon\Mvc\Dispatcher();
|
||||||
$dispatcher->setEventsManager($eventsManager);
|
$dispatcher->setEventsManager($eventsManager);
|
||||||
|
|
@ -351,7 +352,7 @@ class Services extends DiDefault
|
||||||
|
|
||||||
protected function _initAcl()
|
protected function _initAcl()
|
||||||
{
|
{
|
||||||
return new Acl();
|
return new Acl($this->get('config')->acl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function _initSharedLogger()
|
protected function _initSharedLogger()
|
||||||
|
|
|
||||||
70
app/listeners/AccessListener.php
Normal file
70
app/listeners/AccessListener.php
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listener;
|
||||||
|
|
||||||
|
use Phalcon\Events\Event,
|
||||||
|
Phalcon\Mvc\Dispatcher,
|
||||||
|
Phalcon\Mvc\User\Plugin,
|
||||||
|
Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
|
||||||
|
|
||||||
|
use Httpcb\Acl;
|
||||||
|
|
||||||
|
class AccessListener extends Plugin
|
||||||
|
{
|
||||||
|
protected $_ignored_resources = [
|
||||||
|
'index',
|
||||||
|
'error'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Event $event
|
||||||
|
* @param Dispatcher $dispatcher
|
||||||
|
* @return bool
|
||||||
|
* @throws DispatcherException
|
||||||
|
*/
|
||||||
|
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool
|
||||||
|
{
|
||||||
|
// We only have two roles for now, authenticated users and guests.
|
||||||
|
if ($this->auth->hasIdentity()) {
|
||||||
|
$role = Acl::ROLE_USER;
|
||||||
|
} else {
|
||||||
|
$role = Acl::ROLE_GUEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the resource from controller name.
|
||||||
|
$resource = $dispatcher->getControllerName();
|
||||||
|
|
||||||
|
// Ignore checks for error resource.
|
||||||
|
if (in_array($resource, $this->_ignored_resources)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, check and redirect user to login page if
|
||||||
|
// this role does not have access to this resource.
|
||||||
|
if ($this->acl->isAllowed($role, $resource) === false) {
|
||||||
|
|
||||||
|
// Has identity or acl_redirect flag set.
|
||||||
|
// Throw a "handler not found" exception in this case.
|
||||||
|
if ($this->auth->hasIdentity() || $this->session->has('acl_redirect')) {
|
||||||
|
|
||||||
|
// Unset redirect flag first.
|
||||||
|
unset($this->session->acl_redirect);
|
||||||
|
|
||||||
|
$msg = sprintf("Role '%s' not allowed access to resource '%s'", $role, $resource);
|
||||||
|
throw new DispatcherException($msg, Dispatcher::EXCEPTION_HANDLER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login page
|
||||||
|
$this->response->redirect(['for' => 'login']);
|
||||||
|
|
||||||
|
// And set a flag in session. if we do not have access to that
|
||||||
|
// resource either. we should not redirect again.
|
||||||
|
$this->session->set('acl_redirect', true);
|
||||||
|
|
||||||
|
|
||||||
|
// Return false to stop the dispatch loop.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Listener;
|
|
||||||
|
|
||||||
use Phalcon\Events\Event,
|
|
||||||
Phalcon\Mvc\Dispatcher,
|
|
||||||
Phalcon\Mvc\User\Plugin;
|
|
||||||
|
|
||||||
use Httpcb\Acl;
|
|
||||||
|
|
||||||
class AclListener extends Plugin
|
|
||||||
{
|
|
||||||
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher) : bool
|
|
||||||
{
|
|
||||||
// We only have two roles for now, authenticated users and guests.
|
|
||||||
if ($this->auth->hasIdentity()) {
|
|
||||||
$role = Acl::ROLE_USER;
|
|
||||||
} else {
|
|
||||||
$role = Acl::ROLE_GUEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support annotations for actions to define custom resources.
|
|
||||||
$controllerClass = $dispatcher->getControllerClass();
|
|
||||||
$activeMethod = $dispatcher->getActiveMethod();
|
|
||||||
|
|
||||||
$annotation = $this->annotations->getMethod($controllerClass, $activeMethod);
|
|
||||||
|
|
||||||
// ACL annotation found. use that.
|
|
||||||
if ($annotation->has('Acl')) {
|
|
||||||
$resource = $annotation->get('Acl')->getArgument('resource');
|
|
||||||
}
|
|
||||||
// Otherwise, default to controller name.
|
|
||||||
else {
|
|
||||||
$resource = $dispatcher->getControllerName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, check and redirect user to login page if
|
|
||||||
// this role does not have access to this resource.
|
|
||||||
if ($this->acl->isAllowed($role, $resource, 'Read') == \Phalcon\Acl::DENY) {
|
|
||||||
|
|
||||||
// Forward to login page.
|
|
||||||
$dispatcher->forward(array(
|
|
||||||
'controller' => 'auth',
|
|
||||||
'action' => 'index',
|
|
||||||
));
|
|
||||||
|
|
||||||
// Return false to stop the dispatch loop.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in a new issue