Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimisation for backend by caching requests for users. [2.2] #3847

Merged
merged 8 commits into from
Jul 21, 2015
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ addons:
postgresql: "9.3"

before_install:
- sudo sh -c "echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf"
- sudo add-apt-repository -y ppa:nginx/stable
- sudo apt-get update
- sudo apt-get --reinstall install -qq language-pack-en language-pack-fr && sudo locale-gen fr_FR
Expand Down
98 changes: 50 additions & 48 deletions src/Permissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -462,27 +462,29 @@ public function getEffectiveRolesForUser($user)
*
* "contenttype:$contenttype:edit or contenttype:$contenttype:view"
*
* @param string $what The desired permission, as elaborated upon above.
* @param mixed $user The user to check permissions against.
* @param string $contenttype Optional: Content type slug. If specified,
* $what is taken to be a relative permission (e.g. 'edit')
* rather than an absolute one (e.g. 'contenttype:pages:edit').
* @param integer $contentid Only used if $contenttype is given, to further
* specifiy the content item.
* @param string $what The desired permission, as elaborated upon above.
* @param mixed $user The user to check permissions against.
* @param string|array|Content $content Optional: Content object/array or ContentType slug.
* If specified, $what is taken to be a relative permission (e.g. 'edit')
* rather than an absolute one (e.g. 'contenttype:pages:edit').
* @param integer $contentId Only used if $content is given, to further specifiy the content item.
*
* @return boolean TRUE if the permission is granted, FALSE if denied.
*/
public function isAllowed($what, $user, $contenttype = null, $contentid = null)
public function isAllowed($what, $user, $content = null, $contentId = null)
{
// $contenttype must be a string, not an array.
if (is_array($contenttype)) {
$contenttype = $contenttype['slug'];
if (is_array($content)) {
$contenttypeSlug = $content['slug'];
} elseif ($content instanceof \Bolt\Content) {
$contenttypeSlug = $content->contenttype['slug'];
} else {
$contenttypeSlug = $content;
}

$this->audit("Checking permission query '$what' for user '{$user['username']}' with contenttype '$contenttype' and contentid '$contentid'");
$this->audit("Checking permission query '$what' for user '{$user['username']}' with contenttype '$contenttypeSlug' and contentid '$contentId'");

// First, let's see if we have the check in the per-request cache.
$rqCacheKey = $user['id'] . '//' . $what . '//' . $contenttype . '//' . $contentid;
$rqCacheKey = $user['id'] . '//' . $what . '//' . $contenttypeSlug . '//' . $contentId;
if (isset($this->rqcache[$rqCacheKey])) {
return $this->rqcache[$rqCacheKey];
}
Expand All @@ -496,7 +498,7 @@ public function isAllowed($what, $user, $contenttype = null, $contentid = null)
$this->app['cache']->save($cacheKey, json_encode($rule));
}
$userRoles = $this->getEffectiveRolesForUser($user);
$isAllowed = $this->isAllowedRule($rule, $user, $userRoles, $contenttype, $contentid);
$isAllowed = $this->isAllowedRule($rule, $user, $userRoles, $content, $contenttypeSlug, $contentId);

// Cache for the current request
$this->rqcache[$rqCacheKey] = $isAllowed;
Expand All @@ -507,36 +509,37 @@ public function isAllowed($what, $user, $contenttype = null, $contentid = null)
/**
* Check if a user is allowed a rule 'type'.
*
* @param array $rule
* @param array $user
* @param array $userRoles
* @param string $contenttype
* @param integer $contentid
* @param array $rule
* @param array $user
* @param array $userRoles
* @param string|array|Content $content
* @param string $contenttype
* @param integer $contentid
*
* @throws \Exception
*
* @return boolean
*/
private function isAllowedRule($rule, $user, $userRoles, $contenttype, $contentid)
private function isAllowedRule($rule, $user, $userRoles, $content, $contenttypeSlug, $contentid)
{
switch ($rule['type']) {
case PermissionParser::P_TRUE:
return true;
case PermissionParser::P_FALSE:
return false;
case PermissionParser::P_SIMPLE:
return $this->isAllowedSingle($rule['value'], $user, $userRoles, $contenttype, $contentid);
return $this->isAllowedSingle($rule['value'], $user, $userRoles, $content, $contenttypeSlug, $contentid);
case PermissionParser::P_OR:
foreach ($rule['value'] as $subrule) {
if ($this->isAllowedRule($subrule, $user, $userRoles, $contenttype, $contentid)) {
if ($this->isAllowedRule($subrule, $user, $userRoles, $content, $contenttypeSlug, $contentid)) {
return true;
}
}

return false;
case PermissionParser::P_AND:
foreach ($rule['value'] as $subrule) {
if (!$this->isAllowedRule($subrule, $user, $userRoles, $contenttype, $contentid)) {
if (!$this->isAllowedRule($subrule, $user, $userRoles, $content, $contenttypeSlug, $contentid)) {
return false;
}
}
Expand All @@ -550,22 +553,23 @@ private function isAllowedRule($rule, $user, $userRoles, $contenttype, $contenti
/**
* Check if a user has a specific role.
*
* @param string $what
* @param array $user
* @param array $userRoles
* @param string $contenttype
* @param integer $contentid
* @param string $what
* @param array $user
* @param array $userRoles
* @param string|array|Content $content
* @param string $contenttype
* @param integer $contentId
*
* @return boolean
*/
private function isAllowedSingle($what, $user, $userRoles, $contenttype = null, $contentid = null)
private function isAllowedSingle($what, $user, $userRoles, $content = null, $contenttypeSlug = null, $contentId = null)
{
if ($contenttype !== null) {
if ($content !== null) {
$parts = array(
'contenttype',
$contenttype,
$contenttypeSlug,
$what,
$contentid,
$contentId,
);
} else {
$parts = explode(':', $what);
Expand Down Expand Up @@ -605,31 +609,29 @@ private function isAllowedSingle($what, $user, $userRoles, $contenttype = null,

case 'contenttype':
$contenttype = $parts[1];
$permission = $contentid = null;
$permission = $contentId = null;
if (isset($parts[2])) {
$permission = $parts[2];
}
if (isset($parts[3])) {
$contentid = $parts[3];
$contentId = $parts[3];
}
if (empty($permission)) {
$permission = 'view';
}

// Handle special case for owner.
// It's a bit unfortunate that we have to fetch the content
// item for this, but since we're in the back-end, we probably
// won't see a lot of traffic here, so it's probably
// forgivable.
if (!empty($contentid)) {
// $contenttype must be a string, not an array.
if (is_array($contenttype)) {
$contenttype = $contenttype['slug'];
}
$content = $this->app['storage']->getContent("$contenttype/$contentid", array('hydrate' => false));
if (intval($content['ownerid']) &&
(intval($content['ownerid']) === intval($user['id']))) {
$userRoles[] = Permissions::ROLE_OWNER;
}
if ($contentId === null) {
break;
}

// If $content was passed in as a string, fetch the Content object
if (is_string($content)) {
$content = $this->app['storage']->getContent("$contenttypeSlug/$contentId", array('hydrate' => false));
}

if (intval($content['ownerid']) && (intval($content['ownerid']) === intval($user['id']))) {
$userRoles[] = Permissions::ROLE_OWNER;
}
break;

Expand Down
2 changes: 1 addition & 1 deletion src/Twig/Handler/UserHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public function isAllowed($what, $content = null)
$contentid = null;
if ($content instanceof \Bolt\Content) {
// It's a content record
$contenttype = $content->contenttype;
$contenttype = $content;
$contentid = $content['id'];
} elseif (is_array($content)) {
// It's a contenttype
Expand Down
10 changes: 9 additions & 1 deletion src/Users.php
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ public function getUsers()
$tempusers = $queryBuilder->execute()->fetchAll();

foreach ($tempusers as $user) {
$key = $user['username'];
$key = $user['id'];
$this->users[$key] = $user;
$this->users[$key]['password'] = '**dontchange**';

Expand Down Expand Up @@ -897,6 +897,14 @@ public function getUser($id)
}
}

// Make sure users have been 'got' already.
$this->getUsers();

// In most cases by far, we'll request an ID, and we can return it here.
if (array_key_exists($id, $this->users)) {
return $this->users[$id];
}

/** @var \Doctrine\DBAL\Query\QueryBuilder $queryBuilder */
$queryBuilder = $this->app['db']->createQueryBuilder()
->select('*')
Expand Down
2 changes: 1 addition & 1 deletion tests/phpunit/unit/Storage/StorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function testGetContentObject()
public function testPreFill()
{
$app = $this->getApp();
$app['users']->users = array(1 => $this->addDefaultUser($app));
$this->addDefaultUser($app);
$prefillMock = new LoripsumMock();
$app['prefill'] = $prefillMock;

Expand Down
4 changes: 0 additions & 4 deletions tests/phpunit/unit/Users/UsersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public function testGetUserById()
{
// Setup test
$users = $this->getMock('Bolt\Users', array('getUsers'), array($this->getApp()));
$users->users = array($this->user);

// Run test
$result = $users->getUser(2);
Expand All @@ -53,7 +52,6 @@ public function testGetUserByUnknownId()
{
// Setup test
$users = $this->getMock('Bolt\Users', array('getUsers'), array($this->getApp()));
$users->users = array($this->user);

// Run test
$result = $users->getUser(0);
Expand All @@ -69,7 +67,6 @@ public function testGetUserByUsername()
{
// Setup test
$users = $this->getMock('Bolt\Users', array('getUsers'), array($this->getApp()));
$users->users = array($this->user);

// Run test
$result = $users->getUser('editor');
Expand All @@ -87,7 +84,6 @@ public function testGetUserByUnknownUsername()
{
// Setup test
$users = $this->getMock('Bolt\Users', array('getUsers'), array($this->getApp()));
$users->users = array($this->user);

// Run test
$result = $users->getUser('anotheruser');
Expand Down