module access management
This commit is contained in:
@@ -28,7 +28,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Thelia\Core\Event\Profile\ProfileEvent;
|
||||
use Thelia\Core\Event\TheliaEvents;
|
||||
use Thelia\Core\Security\AccessManager;
|
||||
use Thelia\Model\ModuleQuery;
|
||||
use Thelia\Model\Profile as ProfileModel;
|
||||
use Thelia\Model\ProfileModule;
|
||||
use Thelia\Model\ProfileModuleQuery;
|
||||
use Thelia\Model\ProfileQuery;
|
||||
use Thelia\Model\ProfileResource;
|
||||
use Thelia\Model\ProfileResourceQuery;
|
||||
@@ -104,6 +107,30 @@ class Profile extends BaseAction implements EventSubscriberInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProfileEvent $event
|
||||
*/
|
||||
public function updateModuleAccess(ProfileEvent $event)
|
||||
{
|
||||
if (null !== $profile = ProfileQuery::create()->findPk($event->getId())) {
|
||||
ProfileModuleQuery::create()->filterByProfileId($event->getId())->delete();
|
||||
foreach($event->getModuleAccess() as $moduleCode => $accesses) {
|
||||
$manager = new AccessManager(0);
|
||||
$manager->build($accesses);
|
||||
|
||||
$profileModule = new ProfileModule();
|
||||
$profileModule->setProfileId($event->getId())
|
||||
->setModule(ModuleQuery::create()->findOneByCode($moduleCode))
|
||||
->setAccess( $manager->getAccessValue() );
|
||||
|
||||
$profileModule->save();
|
||||
|
||||
}
|
||||
|
||||
$event->setProfile($profile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProfileEvent $event
|
||||
*/
|
||||
@@ -129,6 +156,7 @@ class Profile extends BaseAction implements EventSubscriberInterface
|
||||
TheliaEvents::PROFILE_UPDATE => array("update", 128),
|
||||
TheliaEvents::PROFILE_DELETE => array("delete", 128),
|
||||
TheliaEvents::PROFILE_RESOURCE_ACCESS_UPDATE => array("updateResourceAccess", 128),
|
||||
TheliaEvents::PROFILE_MODULE_ACCESS_UPDATE => array("updateModuleAccess", 128),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
<form name="thelia.admin.profile.add" class="Thelia\Form\ProfileCreationForm"/>
|
||||
<form name="thelia.admin.profile.modification" class="Thelia\Form\ProfileModificationForm"/>
|
||||
<form name="thelia.admin.profile.resource-access.modification" class="Thelia\Form\ProfileUpdateResourceAccessForm"/>
|
||||
<form name="thelia.admin.profile.module-access.modification" class="Thelia\Form\ProfileUpdateModuleAccessForm"/>
|
||||
|
||||
<form name="thelia.admin.template.creation" class="Thelia\Form\TemplateCreationForm"/>
|
||||
<form name="thelia.admin.template.modification" class="Thelia\Form\TemplateModificationForm"/>
|
||||
|
||||
@@ -781,6 +781,10 @@
|
||||
<default key="_controller">Thelia\Controller\Admin\ProfileController::processUpdateResourceAccess</default>
|
||||
</route>
|
||||
|
||||
<route id="admin.configuration.profiles.saveModuleAccess" path="/admin/configuration/profiles/saveModuleAccess">
|
||||
<default key="_controller">Thelia\Controller\Admin\ProfileController::processUpdateModuleAccess</default>
|
||||
</route>
|
||||
|
||||
<route id="admin.configuration.profiles.delete" path="/admin/configuration/profiles/delete">
|
||||
<default key="_controller">Thelia\Controller\Admin\ProfileController::deleteAction</default>
|
||||
</route>
|
||||
|
||||
@@ -30,6 +30,7 @@ use Thelia\Core\Event\TheliaEvents;
|
||||
use Thelia\Form\ProfileCreationForm;
|
||||
use Thelia\Form\ProfileModificationForm;
|
||||
use Thelia\Form\ProfileProfileListUpdateForm;
|
||||
use Thelia\Form\ProfileUpdateModuleAccessForm;
|
||||
use Thelia\Form\ProfileUpdateResourceAccessForm;
|
||||
use Thelia\Model\ProfileQuery;
|
||||
|
||||
@@ -128,6 +129,16 @@ class ProfileController extends AbstractCrudController
|
||||
return new ProfileUpdateResourceAccessForm($this->getRequest(), "form", $data);
|
||||
}
|
||||
|
||||
protected function hydrateModuleUpdateForm($object)
|
||||
{
|
||||
$data = array(
|
||||
'id' => $object->getId(),
|
||||
);
|
||||
|
||||
// Setup the object form
|
||||
return new ProfileUpdateModuleAccessForm($this->getRequest(), "form", $data);
|
||||
}
|
||||
|
||||
protected function getObjectFromEvent($event)
|
||||
{
|
||||
return $event->hasProfile() ? $event->getProfile() : null;
|
||||
@@ -246,9 +257,11 @@ class ProfileController extends AbstractCrudController
|
||||
|
||||
// Hydrate the form and pass it to the parser
|
||||
$resourceAccessForm = $this->hydrateResourceUpdateForm($object);
|
||||
$moduleAccessForm = $this->hydrateModuleUpdateForm($object);
|
||||
|
||||
// Pass it to the parser
|
||||
$this->getParserContext()->addForm($resourceAccessForm);
|
||||
$this->getParserContext()->addForm($moduleAccessForm);
|
||||
}
|
||||
|
||||
return parent::updateAction();
|
||||
@@ -264,6 +277,16 @@ class ProfileController extends AbstractCrudController
|
||||
return $event;
|
||||
}
|
||||
|
||||
protected function getUpdateModuleAccessEvent($formData)
|
||||
{
|
||||
$event = new ProfileEvent();
|
||||
|
||||
$event->setId($formData['id']);
|
||||
$event->setModuleAccess($this->getModuleAccess($formData));
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
protected function getResourceAccess($formData)
|
||||
{
|
||||
$requirements = array();
|
||||
@@ -286,6 +309,28 @@ class ProfileController extends AbstractCrudController
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
protected function getModuleAccess($formData)
|
||||
{
|
||||
$requirements = array();
|
||||
foreach($formData as $data => $value) {
|
||||
if(!strstr($data, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$explosion = explode(':', $data);
|
||||
|
||||
$prefix = array_shift ( $explosion );
|
||||
|
||||
if($prefix != ProfileUpdateModuleAccessForm::MODULE_ACCESS_FIELD_PREFIX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$requirements[implode('.', $explosion)] = $value;
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
public function processUpdateResourceAccess()
|
||||
{
|
||||
// Check current user authorization
|
||||
@@ -334,4 +379,53 @@ class ProfileController extends AbstractCrudController
|
||||
// At this point, the form has errors, and should be redisplayed.
|
||||
return $this->renderEditionTemplate();
|
||||
}
|
||||
|
||||
public function processUpdateModuleAccess()
|
||||
{
|
||||
// Check current user authorization
|
||||
if (null !== $response = $this->checkAuth($this->resourceCode, AccessManager::UPDATE)) return $response;
|
||||
|
||||
$error_msg = false;
|
||||
|
||||
// Create the form from the request
|
||||
$changeForm = new ProfileUpdateModuleAccessForm($this->getRequest());
|
||||
|
||||
try {
|
||||
// Check the form against constraints violations
|
||||
$form = $this->validateForm($changeForm, "POST");
|
||||
|
||||
// Get the form field values
|
||||
$data = $form->getData();
|
||||
|
||||
$changeEvent = $this->getUpdateModuleAccessEvent($data);
|
||||
|
||||
$this->dispatch(TheliaEvents::PROFILE_MODULE_ACCESS_UPDATE, $changeEvent);
|
||||
|
||||
if (! $this->eventContainsObject($changeEvent))
|
||||
throw new \LogicException(
|
||||
$this->getTranslator()->trans("No %obj was updated.", array('%obj', $this->objectName)));
|
||||
|
||||
// Log object modification
|
||||
if (null !== $changedObject = $this->getObjectFromEvent($changeEvent)) {
|
||||
$this->adminLogAppend(sprintf("%s %s (ID %s) modified", ucfirst($this->objectName), $this->getObjectLabel($changedObject), $this->getObjectId($changedObject)));
|
||||
}
|
||||
|
||||
if ($response == null) {
|
||||
$this->redirectToEditionTemplate($this->getRequest(), isset($data['country_list'][0]) ? $data['country_list'][0] : null);
|
||||
} else {
|
||||
return $response;
|
||||
}
|
||||
} catch (FormValidationException $ex) {
|
||||
// Form cannot be validated
|
||||
$error_msg = $this->createStandardFormValidationErrorMessage($ex);
|
||||
} catch (\Exception $ex) {
|
||||
// Any other error
|
||||
$error_msg = $ex->getMessage();
|
||||
}
|
||||
|
||||
$this->setupFormErrorContext($this->getTranslator()->trans("%obj modification", array('%obj' => 'taxrule')), $error_msg, $changeForm, $ex);
|
||||
|
||||
// At this point, the form has errors, and should be redisplayed.
|
||||
return $this->renderEditionTemplate();
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ class ProfileEvent extends ActionEvent
|
||||
protected $description = null;
|
||||
protected $postscriptum = null;
|
||||
protected $resourceAccess = null;
|
||||
protected $moduleAccess = null;
|
||||
|
||||
public function __construct(Profile $profile = null)
|
||||
{
|
||||
@@ -139,4 +140,14 @@ class ProfileEvent extends ActionEvent
|
||||
{
|
||||
return $this->resourceAccess;
|
||||
}
|
||||
|
||||
public function setModuleAccess($moduleAccess)
|
||||
{
|
||||
$this->moduleAccess = $moduleAccess;
|
||||
}
|
||||
|
||||
public function getModuleAccess()
|
||||
{
|
||||
return $this->moduleAccess;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +553,7 @@ final class TheliaEvents
|
||||
const PROFILE_UPDATE = "action.updateProfile";
|
||||
const PROFILE_DELETE = "action.deleteProfile";
|
||||
const PROFILE_RESOURCE_ACCESS_UPDATE = "action.updateProfileResourceAccess";
|
||||
const PROFILE_MODULE_ACCESS_UPDATE = "action.updateProfileModuleAccess";
|
||||
|
||||
// -- Tax Rules management ---------------------------------------------
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
namespace Thelia\Core\Template\Loop;
|
||||
|
||||
use Propel\Runtime\ActiveQuery\Criteria;
|
||||
use Thelia\Core\Security\AccessManager;
|
||||
use Thelia\Core\Template\Element\BaseI18nLoop;
|
||||
use Thelia\Core\Template\Element\LoopResult;
|
||||
use Thelia\Core\Template\Element\LoopResultRow;
|
||||
@@ -56,6 +57,13 @@ class Module extends BaseI18nLoop
|
||||
{
|
||||
return new ArgumentCollection(
|
||||
Argument::createIntListTypeArgument('id'),
|
||||
Argument::createIntTypeArgument('profile'),
|
||||
new Argument(
|
||||
'code',
|
||||
new Type\TypeCollection(
|
||||
new Type\AlphaNumStringListType()
|
||||
)
|
||||
),
|
||||
new Argument(
|
||||
'module_type',
|
||||
new Type\TypeCollection(
|
||||
@@ -89,6 +97,20 @@ class Module extends BaseI18nLoop
|
||||
$search->filterById($id, Criteria::IN);
|
||||
}
|
||||
|
||||
$profile = $this->getProfile();
|
||||
|
||||
if (null !== $profile) {
|
||||
$search->leftJoinProfileModule('profile_module')
|
||||
->addJoinCondition('profile_module', 'profile_module.PROFILE_ID=?', $profile, null, \PDO::PARAM_INT)
|
||||
->withColumn('profile_module.access', 'access');
|
||||
}
|
||||
|
||||
$code = $this->getCode();
|
||||
|
||||
if(null !== $code) {
|
||||
$search->filterByCode($code, Criteria::IN);
|
||||
}
|
||||
|
||||
$moduleType = $this->getModule_type();
|
||||
|
||||
if (null !== $moduleType) {
|
||||
@@ -129,6 +151,16 @@ class Module extends BaseI18nLoop
|
||||
->set("CLASS", $module->getFullNamespace())
|
||||
->set("POSITION", $module->getPosition());
|
||||
|
||||
if (null !== $profile) {
|
||||
$accessValue = $module->getVirtualColumn('access');
|
||||
$manager = new AccessManager($accessValue);
|
||||
|
||||
$loopResultRow->set("VIEWABLE", $manager->can(AccessManager::VIEW)? 1 : 0)
|
||||
->set("CREATABLE", $manager->can(AccessManager::CREATE) ? 1 : 0)
|
||||
->set("UPDATABLE", $manager->can(AccessManager::UPDATE)? 1 : 0)
|
||||
->set("DELETABLE", $manager->can(AccessManager::DELETE)? 1 : 0);
|
||||
}
|
||||
|
||||
$loopResult->addRow($loopResultRow);
|
||||
}
|
||||
|
||||
|
||||
99
core/lib/Thelia/Form/ProfileUpdateModuleAccessForm.php
Normal file
99
core/lib/Thelia/Form/ProfileUpdateModuleAccessForm.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/*************************************************************************************/
|
||||
/* */
|
||||
/* Thelia */
|
||||
/* */
|
||||
/* Copyright (c) OpenStudio */
|
||||
/* email : info@thelia.net */
|
||||
/* web : http://www.thelia.net */
|
||||
/* */
|
||||
/* This program is free software; you can redistribute it and/or modify */
|
||||
/* it under the terms of the GNU General Public License as published by */
|
||||
/* the Free Software Foundation; either version 3 of the License */
|
||||
/* */
|
||||
/* This program is distributed in the hope that it will be useful, */
|
||||
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
|
||||
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
|
||||
/* GNU General Public License for more details. */
|
||||
/* */
|
||||
/* You should have received a copy of the GNU General Public License */
|
||||
/* along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
/* */
|
||||
/*************************************************************************************/
|
||||
namespace Thelia\Form;
|
||||
|
||||
use Symfony\Component\Validator\Constraints;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\ExecutionContextInterface;
|
||||
use Thelia\Core\Security\AccessManager;
|
||||
use Thelia\Core\Translation\Translator;
|
||||
use Thelia\Model\ProfileQuery;
|
||||
use Thelia\Model\ModuleQuery;
|
||||
|
||||
/**
|
||||
* Class ProfileUpdateModuleAccessForm
|
||||
* @package Thelia\Form
|
||||
* @author Etienne Roudeix <eroudeix@openstudio.fr>
|
||||
*/
|
||||
class ProfileUpdateModuleAccessForm extends BaseForm
|
||||
{
|
||||
const MODULE_ACCESS_FIELD_PREFIX = "module";
|
||||
|
||||
protected function buildForm($change_mode = false)
|
||||
{
|
||||
$this->formBuilder
|
||||
->add("id", "hidden", array(
|
||||
"required" => true,
|
||||
"constraints" => array(
|
||||
new Constraints\NotBlank(),
|
||||
new Constraints\Callback(
|
||||
array(
|
||||
"methods" => array(
|
||||
array($this, "verifyProfileId"),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
))
|
||||
;
|
||||
|
||||
foreach(ModuleQuery::create()->find() as $module) {
|
||||
$this->formBuilder->add(
|
||||
self::MODULE_ACCESS_FIELD_PREFIX . ':' . str_replace(".", ":", $module->getCode()),
|
||||
"choice",
|
||||
array(
|
||||
"choices" => array(
|
||||
AccessManager::VIEW => AccessManager::VIEW,
|
||||
AccessManager::CREATE => AccessManager::CREATE,
|
||||
AccessManager::UPDATE => AccessManager::UPDATE,
|
||||
AccessManager::DELETE => AccessManager::DELETE,
|
||||
),
|
||||
"attr" => array(
|
||||
"tag" => "modules",
|
||||
"module_code" => $module->getCode(),
|
||||
"module_title" => $module->getTitle(),
|
||||
),
|
||||
"multiple" => true,
|
||||
"constraints" => array(
|
||||
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return "thelia_profile_module_access_modification";
|
||||
}
|
||||
|
||||
public function verifyProfileId($value, ExecutionContextInterface $context)
|
||||
{
|
||||
$profile = ProfileQuery::create()
|
||||
->findPk($value);
|
||||
|
||||
if (null === $profile) {
|
||||
$context->addViolation("Profile ID not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@
|
||||
|
||||
<ul class="nav nav-tabs clearfix">
|
||||
<li {if $oder_tab == 'data'}class="active"{/if}><a href="#data" data-tab-name="cart" data-toggle="tab"><span class="glyphicon glyphicon-shopping-cart"></span> {intl l="Description"}</a></li>
|
||||
<li {if $oder_tab == 'permissions'}class="active"{/if}><a href="#permissions" data-tab-name="bill" data-toggle="tab"><span class="glyphicon glyphicon-list-alt"></span> {intl l="Permissions"}</a></li>
|
||||
<li {if $oder_tab == 'resources'}class="active"{/if}><a href="#resources" data-tab-name="bill" data-toggle="tab"><span class="glyphicon glyphicon-list-alt"></span> {intl l="Resource access rights"}</a></li>
|
||||
<li {if $oder_tab == 'modules'}class="active"{/if}><a href="#modules" data-tab-name="bill" data-toggle="tab"><span class="glyphicon glyphicon-list-alt"></span> {intl l="Module access rights"}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -116,11 +117,11 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade {if $oder_tab == 'permissions'}active in{/if}" id="permissions">
|
||||
<div class="tab-pane fade {if $oder_tab == 'resources'}active in{/if}" id="resources">
|
||||
|
||||
{form name="thelia.admin.profile.resource-access.modification"}
|
||||
|
||||
<form method="POST" action="{url path="/admin/configuration/profiles/saveResourceAccess?tab=permissions"}" {form_enctype form=$form} >
|
||||
<form method="POST" action="{url path="/admin/configuration/profiles/saveResourceAccess?tab=resources"}" {form_enctype form=$form} >
|
||||
|
||||
{form_hidden_fields form=$form}
|
||||
|
||||
@@ -131,7 +132,8 @@
|
||||
|
||||
<table class="table table-striped table-condensed table-left-aligned">
|
||||
<caption>
|
||||
{intl l="Manage permissions"}
|
||||
{intl l="Manage resource rights"}
|
||||
<button type="submit" class="btn btn-default btn-primary pull-right"><span class="glyphicon glyphicon-check"></span> {intl l="Save"}</button>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -184,7 +186,89 @@
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<td colspan="7">
|
||||
<button type="submit" class="btn btn-default btn-primary pull-right"><span class="glyphicon glyphicon-check"></span> {intl l="Save"}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
</form>
|
||||
|
||||
{/form}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade {if $oder_tab == 'modules'}active in{/if}" id="modules">
|
||||
|
||||
{form name="thelia.admin.profile.module-access.modification"}
|
||||
|
||||
<form method="POST" action="{url path="/admin/configuration/profiles/saveModuleAccess?tab=modules"}" {form_enctype form=$form} >
|
||||
|
||||
{form_hidden_fields form=$form}
|
||||
|
||||
{* Be sure to get the product ID, even if the form could not be validated *}
|
||||
<input type="hidden" name="profile_id" value="{$ID}" />
|
||||
|
||||
{if $form_error}<div class="alert alert-danger">{$form_error_message}</div>{/if}
|
||||
|
||||
<table class="table table-striped table-condensed table-left-aligned">
|
||||
<caption>
|
||||
{intl l="Manage module rights"}
|
||||
<button type="submit" class="btn btn-default btn-primary pull-right"><span class="glyphicon glyphicon-check"></span> {intl l="Save"}</button>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">{intl l="Module"}</th>
|
||||
<th rowspan="2">{intl l="Title"}</th>
|
||||
<th colspan="4" class="text-center">{intl l="Rights"}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{intl l="View"}</th>
|
||||
<th>{intl l="Create"}</th>
|
||||
<th>{intl l="Update"}</th>
|
||||
<th>{intl l="Delete"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{form_tagged_fields form=$form tag='modules'}
|
||||
|
||||
{loop type="module" name="module-list" code=$attr_list.module_code profile=$ID backend_context="1"}
|
||||
|
||||
<tr>
|
||||
<td>{$CODE}</td>
|
||||
<td>{$TITLE}</td>
|
||||
<td>
|
||||
<div class="make-switch switch-mini" data-on="success" data-off="danger" data-on-label="<i class='glyphicon glyphicon-ok'></i>" data-off-label="<i class='glyphicon glyphicon-remove'></i>">
|
||||
<input name="{$name}" value="VIEW" type="checkbox" {if $VIEWABLE == 1}checked="checked"{/if}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="make-switch switch-mini" data-on="success" data-off="danger" data-on-label="<i class='glyphicon glyphicon-ok'></i>" data-off-label="<i class='glyphicon glyphicon-remove'></i>">
|
||||
<input name="{$name}" value="CREATE" type="checkbox" {if $CREATABLE == 1}checked="checked"{/if}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="make-switch switch-mini" data-on="success" data-off="danger" data-on-label="<i class='glyphicon glyphicon-ok'></i>" data-off-label="<i class='glyphicon glyphicon-remove'></i>">
|
||||
<input name="{$name}" value="UPDATE" type="checkbox" {if $UPDATABLE == 1}checked="checked"{/if}>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="make-switch switch-mini" data-on="success" data-off="danger" data-on-label="<i class='glyphicon glyphicon-ok'></i>" data-off-label="<i class='glyphicon glyphicon-remove'></i>">
|
||||
<input name="{$name}" value="DELETE" type="checkbox" {if $DELETABLE == 1}checked="checked"{/if}>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/loop}
|
||||
|
||||
{/form_tagged_fields}
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<button type="submit" class="btn btn-default btn-primary pull-right"><span class="glyphicon glyphicon-check"></span> {intl l="Save"}</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user