Advanced: Creating a Custom Access Entity

Concrete CMS ships with a number of useful access entities for permissions:

  • Group
  • User
  • Group Set
  • Group Combination
  • File Uploader
  • Page Owner

and more. These can be applied to all of the relevant permissions across a Concrete site, giving administrators lots of flexibility as to how they define their site's permissions. But the most powerful part of access entities isn't just that these are built-in and useful, but that it's easy for developers to create their own.

eCommerce

Let's continue with our eCommerce example. Say we've got a product in our site: "Protected Website Access", and we'd like to give users who purchased this product access to part of our website. We could add functionality to our eCommerce offering that put users in groups based on what they bought – but what if we didn't want to do that? What if we wanted something more custom and something that couldn't be faked by simply putting a user in a particular group? We need a custom "Product Purchaser" Access Entity. Let's add one.

Install the Access Entity

First, we're going to need to install a database record telling Concrete that the access entity exists, and what permission categories it covers. First, add the access entity type and category to the top of your controller, if they're not already there:

use \Concrete\Core\Permission\Access\Entity\Type\Type;
use \Concrete\Core\Permission\Category;

Additionally, makes sure your controller uses the proper autoloader:

$pkgAutoloaderMapCoreExtensions = true;

Then, in your install() method:

$type = Type::getByHandle('product_purchaser');
if (!is_object($type)) {
    $type = Type::add('product_purchaser', 'Product Purchaser', $pkg);
}
$category = Category::getByHandle('page');
$category->associateAccessEntityType($type);

This block of code takes care or creating an access entity type with the name Product Purchaser, and making it available to all objects in the Page attribute category. That way, when working with pages, we'll be able to assign our "Product Purchaser" access entity to different permissions.

Create the Backend Code

Now you're going to need create the access entity type class, which is automatically used based on its handle and the package that it's in. That will be added here:

packages/super_store/src/Concrete/Permission/Access/Entity/ProductPurchaserEntity.php

Here's the contents of the file:

<?php
namespace Concrete\Package\SuperStore\Permission\Access\Entity;

use Concrete\Core\Permission\Access\Entity\Entity;
use Concrete\Core\Permission\Access\Access;
use SuperStore\Product\Product;
use Config;

class ProductPurchaserEntity extends Entity
{

    protected $product = false;

    public function getProductObject()
    {
        return $this->product;
    }

    public function getAccessEntityUsers(Access $pa)
    {
        return $this->product->getProductPurchasers();
    }

    public function getAccessEntityTypeLinkHTML()
    {
        $html = '<a href="' . URL::to('/ccm/super_store/dialogs/product/search') . '" class="dialog-launch" dialog-width="640" dialog-height="480" dialog-modal="false" dialog-title="' . t('Add Product Purchaser') . '">' . tc('PermissionAccessEntityTypeName',
                'Product Purchaser') . '</a>';
        return $html;
    }

    public static function getAccessEntitiesForUser($user)
    {
        $entities = array();
        $inProductIDs = array();
        $db = Loader::db();
        $products = Product::getProductsPurchasedByUser($user);
        foreach($products as $product) {
            $inProductIDs[] = $product->getProductID();
        }
        $instr = implode(',', $inProductIDs);
        $peIDs = $db->GetCol('select pae.peID from PermissionAccessEntities pae inner join PermissionAccessEntityTypes paet on pae.petID = paet.petID inner join PermissionAccessEntityProducts paep on pae.peID = paep.peID where petHandle = \'product\' and paep.productID in (' . $instr . ')');
        if (is_array($peIDs)) {
            foreach ($peIDs as $peID) {
                $entity = \Concrete\Core\Permission\Access\Entity\Entity::getByID($peID);
                if (is_object($entity)) {
                    $entities[] = $entity;
                }
            }
        }

        return $entities;
    }

    public static function getOrCreate(Product $p)
    {
        $db = Loader::db();
        $petID = $db->GetOne('select petID from PermissionAccessEntityTypes where petHandle = \'product\'');
        $peID = $db->GetOne('select pae.peID from PermissionAccessEntities pae inner join PermissionAccessEntityProducts paep on pae.peID = paep.peID where petID = ? and paep.productID = ?',
            array($petID, $p->getProductID()));
        if (!$peID) {
            $db->Execute("insert into PermissionAccessEntities (petID) values(?)", array($petID));
            $peID = $db->Insert_ID();
            Config::save('concrete.misc.access_entity_updated', time());
            $db->Execute('insert into PermissionAccessEntityProducts (peID, productID) values (?, ?)',
                array($peID, $p->getProductID()));
        }
        return \Concrete\Core\Permission\Access\Entity\Entity::getByID($peID);
    }

    public function load()
    {
        $db = Loader::db();
        $productID = $db->GetOne('select productID from PermissionAccessEntityProducts where peID = ?', array($this->peID));
        if ($productID) {
            $p = Product::getByID($productID);
            if (is_object($p)) {
                $this->product = $p;
                $this->label = $p->getProductName();
            } else {
                $this->label = t('(Deleted Product)');
            }
        }
    }

}

Let's break down what's going on here.

First, getAccessEntityUsers() is a method used by all access entities. It works on the current instance of the access entity, and returns all users who are covered by that access entity. So, in this example, we're returning all users who have purchased this particular product.

getAccessEntityTypeLinkHTML() is a method common to all access entities. It is used when listing the access entity in the dropdown. Typically this returns an anchor with some kind of custom JavaScript to add the access entity. In this example, it is responsible for opening a new dialog window where you can choose the product that you wish this particular access entity to cover.

getAccessEntitiesForUser() is run on a particular user, and it's meant to return all access entities of this particular type that apply to the user in question. This is mostly run at one time, when a user logs in. Every access entity in the system is queried, and all access entity objects that apply to that user are returned and stored in session.

Access Entity Example

Let's say we add three parts of to our website. There's "Protected Membership A", "Protected Membership B", and "Protected Membership C". If user 'andrew' purchases the product "Protected Membership A" and "Protected Membership C", he will gain access to those sections. We will set up our custom permission access entity with the relevant product selected for each area of the site. Then, when andrew logs in, he will have two access entities attached to his session – both of which are of the "product_purchaser" type. The first will be for "Protected Membership A" and the second for "Protected Membership C."

Back to the explanation.

getOrCreate(Product $p) is run any time an access entity is saved against a particular permission. The access entity takes a particular product object. If that entity has already been used elsewhere in the site in a particular permission, that particular instance is returned. Otherwise, we create a new instance, save the custom product data in our custom PermissionAccessEntityProducts table (which we will have to create as part of our installation), and return the new object.

load() called automatically by all access entities, load() loads our custom access entity data into our object. In this case, it's responsible for querying our custom product table, and storing that product object against this object.

Create the Frontend Code

Now that we've created the backend for our custom permission access entity, we need to create some front-end code. Add this to a file at

packages/super_store/elements/permission/access/entity/types/product_purchaser.php

This file is automatically included when you have the Product Purchaser access entity type in a permission list.

<?php defined('C5_EXECUTE') or die('Access Denied.');

$url = $type->getAccessEntityTypeToolsURL(); ?>

<script type="text/javascript">
$(function() {
    ConcreteEvent.unsubscribe('SelectSuperStoreProduct.core');
    ConcreteEvent.subscribe('SelectSuperStoreProduct', function(e, data) {
        jQuery.fn.dialog.closeTop();
        $('#ccm-permissions-access-entity-form .btn-group').removeClass('open');
        $.getJSON('<?=$url?>', {
            'gID': data.productID
        }, function(r) {
            $('#ccm-permissions-access-entity-form input[name=peID]').val(r.peID);
            $('#ccm-permissions-access-entity-label').html('<div class="alert alert-info">' + r.label + '</div>');
        });
    });
});
</script>

This is code in the launcher window that listens for an event fired in the Choose Product dialog. Building that dialog is outside the scope of this documentation, but suffice it to say: when you're building the interface that lives at the dialog "/ccm/super_store/dialogs/product/search" you're going to hyperlink the product name, and when a user clicks on that, use the Concrete JavaScript event framework to fire the "SelectSuperStoreProduct" event, with productID passed in as your data. Then you'll be able to listen to this event and fire this JavaScript. The $url that is generated by $type->getAccessEntityTypeToolsURL() will look like this:

http://www.example.com/index.php/tools/packages/super_store/permission/access/entity/types/product_purchaser?task=process

Create the Routing

So we're going to do for this exactly what we did for our custom permission category: set up a router to this tools URL. Add this to your package controller on_start():

\Route::register('/tools/packages/super_store/permissions/access/entity/types/product_purchaser',
    '\Concrete\Package\SuperStore\Controller\Permissions\Access\Entity\Types\ProductPurchaser::process'
);

This will route the tools request into Concrete\Package\SuperStore\Controller\Permissions\Access\Entity\Types\ProductPurchaser::process. So let's create that controller at

packages/super_store/controllers/permissions/access/entity/types/product_purchaser.php

And it contains this:

<?php
namespace Concrete\Package\SuperStore\Controller\Permissions\Access\Entity\Types;

use \Concrete\Package\SuperStore\Access\Entity\ProductPurchaserEntity;
use \SuperStore\Product\Product;

class ProductPurchaser extends \Concrete\Core\Controller\Controller
{
    public function process()
    {
        if (\Core::make('token')->validate('process')) {

            $obj = new stdClass;
            $p = Product::getByID($_REQUEST['productID']);
            if (is_object($p)) {
                $pae = ProductPurchaserEntity::getOrCreate($p);         
                $obj->peID = $pae->getAccessEntityID();
                $obj->label = $pae->getAccessEntityLabel();
            }
            print $js->encode($obj);    
        }
    }
}
 note: \Core::make() is deprecated . Use app() (since 8.5.2) or \Concrete\Core\Support\Facade\Application::getFacadeApplication() . See this tutorial for more details.

And there you see how we use some of the final code in the backend class. This controller is responsible for creating the access entity, and passing it back in the form of JSON, which Concrete then displays in the core permissions labels code.

And with that, you've created a custom permission access entity. You'll be able to attach this entity anywhere in your site, and lock access to it down based specifically on exactly what products have been purchased.