Creating the Category User Interface

Now that we have the underpinnings of a completely new permission category for our Product object, we need to create a place where these permissions can actually be set. Typically this will be done in a Dashboard page. (Don't know how to create a Dashboard Page? Check out Single Pages documentation.).

Create a Dashboard Page

First, create a Dashboard Page as you normally would in a Package controller. Let's say we've created one at

/dashboard/super_store/product/permissions

This Dashboard page is meant to load a product by its ID:

http://www.yoursite.com/dashboard/super_store/products/permissions/120

Where 120 is the product's ID (productID). The controller will load the product, and then be responsible for setting the product into the product category, and retrieving its permissions. The view() method in the Dashboard controller doesn't have to do much other than retrieving the relevant product, and set it into the Dashboard view.

public function view($productID = null)
{
    $product = \SuperStore\Product\Product::getByID($productID);
    $this->set('product', $product);
}

And here's what the Dashboard view looks like:

<form method="post" action="<?=$view->action('save_permissions')?>">
    <input type="hidden" name="productID" value="<?=$product->getProductID()?>">
    <?=$token->output('save_permissions')?>
    <fieldset>
        <div id="ccm-permission-list-form">
            <?php View::packageElement('permission/lists/product', 'super_store', array(
                "product" => $product)); ?>
        </div>
    </fieldset>

    <div class="form-actions">
        <button type="submit" name="submit" class="btn pull-right btn-primary"><?=t('Save Permissions')?></button>
    </div>

</form>

What's our package element look like? Found in packages/super_store/elements/permission/lists/product.php, it contains this:

<table class="ccm-permission-grid table table-striped">
    <?php
    $permissions = PermissionKey::getList('product');
    foreach($permissions as $pk) {
        $pk->setPermissionObject($product);
        ?>
        <tr>
            <td class="ccm-permission-grid-name" id="ccm-permission-grid-name-<?=$pk->getPermissionKeyID()?>">
                <strong>
                <a dialog-title="<?=$pk->getPermissionKeyDisplayName()?>" 
                data-pkID="<?=$pk->getPermissionKeyID()?>" 
                data-paID="<?=$pk->getPermissionAccessID()?>" 
                onclick="ccm_permissionLaunchDialog(this)" 
                href="javascript:void(0)"><?=$pk->getPermissionKeyDisplayName()?>
                </strong>
            </td>
            <td id="ccm-permission-grid-cell-<?=$pk->getPermissionKeyID()?>" class="ccm-permission-grid-cell">
            <?=View::element('permission/labels', array('pk' => $pk))?>
            </td>
        </tr>
    <?php } ?>
</table>


<script type="text/javascript">
    ccm_permissionLaunchDialog = function(link) {
        var dupe = $(link).attr('data-duplicate');
        if (dupe != 1) {
            dupe = 0;
        }

        jQuery.fn.dialog.open({
            title: $(link).attr('dialog-title'),
            href: '<?=URL::to("/ccm/super_store/permissions/dialogs/product")?>?productID=<?=$product->getID()?>&duplicate=' + dupe + '&pkID=' + $(link).attr('data-pkID') + '&paID=' + $(link).attr('data-paID'),
            modal: false,
            width: 500,
            height: 380
        });
    }
</script>

There's a lot going on here. Let's try and explain it:

  1. First, we're creating a table with a couple classes. The IDs are important.
  2. Next, we loop through all the permission keys provided by our custom permission category.
  3. We set the permission object for each key to the product object that we passed into the element.
  4. Then, we retrieve access data and permission key data for each permission, and print out the permisson row.
  5. We include data attributes on the identifying line, and we include the core permission labels element in the table cell, passing our primed permission key object into the labels element. This takes care of displaying the permissions, with the proper access entities highlighted in the proper way.
  6. Finally, we include some custom JavaScript. This is executed when the user clicks on the link in the table. It is responsible for opening a dialog to a custom route, with our product data passed through, and the value of the selected permission key and access objects.

Let's continue, by checking out the route itself. First, we're going to need to define this route in our package controller:

public function on_start()
{
    \Route::register(
        Router::route(array('/permissions/dialogs/product', 'super_store')),
        '\Concrete\Package\SuperStore\Controller\Permissions\ProductDialog::view'
    );
}

This might look confusing, but it's pretty simple: we define a URL route for whenever someone visits http://www.yoursite.com/index.php/ccm/super_store/permissions/dialogs/product. It will route to the Concrete\Package\SuperStore\Controller\Permissions\ProductDialog controller. So let's create that. We do so by making a the file here:

packages/super_store/controllers/permissions/product_dialog.php

And filling it with content.

<?php
namespace Concrete\Package\SuperStore\Controller\Permissions;

use \SuperStore\Product\Product;
use \Concrete\Controller\Backend\UserInterface as BackendInterfaceController;

class ProductDialog extends BackendInterfaceController
{

    protected $viewPath = '/dialogs/permissions/product_dialog';

    public function canAccess()
    {
        $c = \Page::getByPath('/dashboard/super_store/product/permissions');
        $cp = new \Permissions($c);
        return $cp->canViewPage();
    }
    public function view()
    {
        if ($this->request->query->has('productID')) {
            $product = Product::getByID($this->request->query->get('productID'));
            $this->set('product', $product);
        }
    }
}

So what's going on here? First, we inherit from our backend user interface controller – which requires that we define a method named canAccess(), which determines whether the view can be accessed. This is important; without it, anyone could access this route. As its written in here, only those with access to the relevant dashboard page can access this dialog.

Next, we define with the view() method, which is what our router is pointing to. Finally, we give our path a protected $viewPath variable, which determines a file in the packages/super_store/views/ folder to load. This will be loading packages/super_store/views/dialogs/permissions/product_dialog.php, with a variable named $product for the selected product in scope. That file looks like this:

<?php
defined('C5_EXECUTE') or die("Access Denied.");
View::element('permission/details/product', array('product' => $product), 'super_store');

Now we're including another element, a permission detail element, found in packages/super_store/permission/details/product.php

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

$pk = PermissionKey::getByID($_REQUEST['pkID']);
$pk->setPermissionObject($product);

?>

<?php View::element("permission/detail", array('permissionKey' => $pk)); ?>

<script type="text/javascript">
    var ccm_permissionDialogURL = '<?=URL::to("/ccm/super_store/permissions/product_dialog")?>';
</script>

There's very little in here that isn't boilerplate. We retrieve the select permission key, and we set our specific permission object (the $product object). Next, we define the ccm_permissionDialogURL variable, which is used by javascript contained within the core detail permission element. This is so we can reload these dialogs if necessary.

There's one last piece of our puzzle. We need to define the core backend permission category processor – which is the script that handles adding and removing access entities and saving permissions against our permission access entity. This the script automatically returned by ProductAssignment::getPermissionKeyToolsURL. This is an automatically generated script, using the old-style tools URL syntax. For our category it will be automatically generated as this:

http://www.yoursite.com/index.php/tools/packages/super_store/permissions/categories/product

Unfortunately, packages can't make use of tools URLs. While Concrete CMS will certainly add a nicer syntax to point to this backend processor, in the meantime we're stuck with our automatically generated tools URL. So let's satisfy this requirement by adding a custom route for the generated URL! In our package's on_start() method, we'll add this line of code:

\Route::register('/tools/packages/super_store/permissions/categories/product',
    '\Concrete\Package\SuperStore\Controller\Permissions::process'
);

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

packages/super_store/controllers/permissions.php

And it contains this:

<?php
namespace Concrete\Package\SuperStore\Controller;

use \Concrete\Core\Permission\Access\Entity\Entity as PermissionAccessEntity;
use \Concrete\Core\Permission\Duration as PermissionDuration;

class Permissions extends \Concrete\Core\Controller\Controller
{
    public function process()
    {
        $c = \Page::getByPath('/dashboard/super_store/product/permissions');
        $cp = new Permissions($c);
        if ($cp->canViewPage()) {

            if ($_REQUEST['task'] == 'add_access_entity' && \Loader::helper("validation/token")->validate('add_access_entity')) {
                $pk = \PermissionKey::getByID($_REQUEST['pkID']);
                $pa = \PermissionAccess::getByID($_REQUEST['paID'], $pk);
                $pe = PermissionAccessEntity::getByID($_REQUEST['peID']);
                $pd = PermissionDuration::getByID($_REQUEST['pdID']);
                $pa->addListItem($pe, $pd, $_REQUEST['accessType']);
            }

            if ($_REQUEST['task'] == 'remove_access_entity' && \Loader::helper("validation/token")->validate('remove_access_entity')) {
                $pk = \PermissionKey::getByID($_REQUEST['pkID']);
                $pa = \PermissionAccess::getByID($_REQUEST['paID'], $pk);
                $pe = PermissionAccessEntity::getByID($_REQUEST['peID']);
                $pa->removeListItem($pe);
            }

            if ($_REQUEST['task'] == 'save_permission' && \Loader::helper("validation/token")->validate('save_permission')) {
                $pk = \PermissionKey::getByID($_REQUEST['pkID']);
                $pa = \PermissionAccess::getByID($_REQUEST['paID'], $pk);
                $pa->save($_POST);
            }

            if ($_REQUEST['task'] == 'display_access_cell' && \Loader::helper("validation/token")->validate('display_access_cell')) {
                $pk = \PermissionKey::getByID($_REQUEST['pkID']);
                $pa = \PermissionAccess::getByID($_REQUEST['paID'], $pk);
                \Loader::element('permission/labels', array('pk' => $pk, 'pa' => $pa));
            }

        }
    }
}

That's it! It's not the prettiest code – much of the permissions backend generates legacy URLs with GET parameters. But that's ok! Our controller can support that.

Using the Permissions

And with that, we've built our custom permissions category, housing custom permissions! Now you'll be to do this:

$product = Product::getByID($productID);
$p = new Permissions($product);
if ($p->canViewProduct()) {
    // Can view!
}
if ($p->canEditProduct()) {
    // Can edit!
}
if ($p->canDeleteProduct()) {
    // Can delete!
}

You'll be able to add more permissions to your products over time. Your products will support all Concrete access entities, and even duration-based permissions.