Commit e0259a712b9922727d5a3df0778f384081106131

Authored by bshuttle
1 parent 243a1b27

workflow-security control.


git-svn-id: https://kt-dms.svn.sourceforge.net/svnroot/kt-dms/trunk@5920 c91229c3-7414-0410-bfa2-8a42b809f60b
lib/workflow/workflowadminutil.inc.php
... ... @@ -114,5 +114,6 @@ class KTWorkflowAdminUtil {
114 114 }
115 115 return $aRet;
116 116 }
117   -
  117 +
  118 +
118 119 }
... ...
lib/workflow/workflowutil.inc.php
... ... @@ -253,6 +253,8 @@ class KTWorkflowUtil {
253 253 }
254 254 // }}}
255 255  
  256 + // FIXME DEPRECATED
  257 +
256 258 // {{{ setEnabledActionsForState
257 259 /**
258 260 * Sets the actions that are enabled by this workflow state.
... ... @@ -295,6 +297,34 @@ class KTWorkflowUtil {
295 297 }
296 298 // }}}
297 299  
  300 + function setDisabledActionsForState($oState, $aActions) {
  301 + $iStateId = KTUtil::getId($oState);
  302 + $sTable = KTUtil::getTableName('workflow_state_disabled_actions');
  303 +
  304 + $aQuery = array(
  305 + "DELETE FROM $sTable WHERE state_id = ?",
  306 + array($iStateId),
  307 + );
  308 + $res = DBUtil::runQuery($aQuery);
  309 + if (PEAR::isError($res)) {
  310 + return $res;
  311 + }
  312 +
  313 + if(!is_array($aActions)) return;
  314 +
  315 + $aOptions = array('noid' => true);
  316 + foreach ($aActions as $sAction) {
  317 + $res = DBUtil::autoInsert($sTable, array(
  318 + 'state_id' => $iStateId,
  319 + 'action_name' => $sAction,
  320 + ), $aOptions);
  321 + if (PEAR::isError($res)) {
  322 + return $res;
  323 + }
  324 + }
  325 + return;
  326 + }
  327 +
298 328 // {{{ getEnabledActionsForState
299 329 /**
300 330 * Gets the actions that are enabled by this workflow state.
... ... @@ -320,6 +350,17 @@ class KTWorkflowUtil {
320 350 }
321 351 // }}}
322 352  
  353 + function getDisabledActionsForState($oState) {
  354 + $iStateId = KTUtil::getId($oState);
  355 + $sTable = KTUtil::getTableName('workflow_state_disabled_actions');
  356 +
  357 + $aQuery = array(
  358 + "SELECT action_name FROM $sTable WHERE state_id = ?",
  359 + array($iStateId),
  360 + );
  361 + return DBUtil::getResultArrayKey($aQuery, 'action_name');
  362 + }
  363 +
323 364 // {{{ actionEnabledForDocument
324 365 /**
325 366 * Checks if a particular action is enabled to occur on a document
... ... @@ -343,7 +384,7 @@ class KTWorkflowUtil {
343 384 return true;
344 385 }
345 386 $oState =& KTWorkflowState::getByDocument($oDocument);
346   - if (!in_array($sName, KTWorkflowUtil::getEnabledActionsForState($oState))) {
  387 + if (in_array($sName, KTWorkflowUtil::getDisabledActionsForState($oState))) {
347 388 return false;
348 389 }
349 390 return true;
... ...
plugins/ktcore/admin/workflowsv2.php
... ... @@ -1073,40 +1073,300 @@ class KTWorkflowAdminV2 extends KTAdminDispatcher {
1073 1073 }
1074 1074  
1075 1075 $states = KTWorkflowState::getByWorkflow($this->oWorkflow);
  1076 + $action_grid = array();
  1077 + foreach ($states as $oState) {
  1078 + $state_actions = array();
  1079 + $disabled = KTWorkflowUtil::getDisabledActionsForState($oState);
  1080 +
  1081 + foreach ($disabled as $name) {
  1082 + $state_actions[$name] = $name;
  1083 + }
  1084 +
  1085 + $action_grid[$oState->getId()] = $state_actions;
  1086 + }
1076 1087  
1077 1088 $oTemplate->setData(array(
1078 1089 'context' => $this,
1079 1090 'states' => $states,
1080 1091 'actions' => $actions,
  1092 + 'grid' => $action_grid,
1081 1093 ));
1082 1094 return $oTemplate->render();
1083 1095 }
1084 1096  
1085 1097 function do_editactions() {
1086   - $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/actions_overview');
  1098 + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/actions_edit');
1087 1099 $this->oPage->setBreadcrumbDetails(_kt("Actions"));
  1100 + $actions = KTUtil::keyArray(KTDocumentActionUtil::getAllDocumentActions(), 'getName');
  1101 + $blacklist = array('ktcore.actions.document.displaydetails');
  1102 +
  1103 + foreach ($blacklist as $name) {
  1104 + unset($actions[$name]);
  1105 + }
  1106 +
  1107 + $states = KTWorkflowState::getByWorkflow($this->oWorkflow);
  1108 + $action_grid = array();
  1109 + foreach ($states as $oState) {
  1110 + $state_actions = array();
  1111 + $disabled = KTWorkflowUtil::getDisabledActionsForState($oState);
  1112 +
  1113 + foreach ($disabled as $name) {
  1114 + $state_actions[$name] = $name;
  1115 + }
  1116 +
  1117 + $action_grid[$oState->getId()] = $state_actions;
  1118 + }
1088 1119  
1089 1120 $oTemplate->setData(array(
1090 1121 'context' => $this,
1091   - 'workflow_name' => $this->oWorkflow->getName(),
  1122 + 'states' => $states,
  1123 + 'actions' => $actions,
  1124 + 'grid' => $action_grid,
  1125 + 'args' => $this->meldPersistQuery("","saveactions", true),
1092 1126 ));
1093 1127 return $oTemplate->render();
1094 1128 }
1095 1129  
1096 1130 function do_saveactions() {
1097   - $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/actions_overview');
1098   - $this->oPage->setBreadcrumbDetails(_kt("Actions"));
  1131 + $disabled_actions = (array) $_REQUEST['fActions'];
1099 1132  
1100   - $res = KTWorkflowUtil::setEnabledActionsForState($oState, $_REQUEST['fActions']);
  1133 +
  1134 + $states = KTWorkflowState::getByWorkflow($this->oWorkflow);
  1135 + $actions = KTUtil::keyArray(KTDocumentActionUtil::getAllDocumentActions(), 'getName');
  1136 +
  1137 + $this->startTransaction();
  1138 +
  1139 + foreach ($states as $oState) {
  1140 + $disable = array();
  1141 + $state_disabled = (array) $disabled_actions[$oState->getId()];
  1142 + if (!empty($state_disabled)) {
  1143 + foreach ($actions as $name => $oAction) {
  1144 + if ($state_disabled[$name]) {
  1145 + $disable[] = $name;
  1146 + }
  1147 + }
  1148 + }
  1149 +
  1150 + $res = KTWorkflowUtil::setDisabledActionsForState($oState, $disable);
  1151 + }
  1152 +
  1153 + $this->successRedirectTo('actionsoverview', _kt('Disabled actions updated.'));
  1154 + }
  1155 +
  1156 + function do_transitionsecurityoverview() {
  1157 + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/transition_guards_overview');
  1158 + $this->oPage->setBreadcrumbDetails(_kt("Transition Guards"));
  1159 +
  1160 + $transitions = KTWorkflowTransition::getByWorkflow($this->oWorkflow);
1101 1161  
1102 1162 $oTemplate->setData(array(
1103 1163 'context' => $this,
1104   - 'workflow_name' => $this->oWorkflow->getName(),
  1164 + 'transitions' => $transitions,
1105 1165 ));
1106 1166 return $oTemplate->render();
1107 1167 }
1108 1168  
  1169 + // helper
  1170 + function describeTransitionGuards($oTransition) {
  1171 + $restrictions = KTWorkflowUtil::getGuardTriggersForTransition($oTransition);
  1172 +
  1173 + if (empty($restrictions)) {
  1174 + return _kt("No restrictions in place for this transition.");
  1175 + }
  1176 +
  1177 + $restriction_text = array();
  1178 + foreach ($restrictions as $oGuard) {
  1179 + $restriction_text[] = $oGuard->getConfigDescription();
  1180 + }
  1181 +
  1182 + return implode('. ', $restriction_text);
  1183 + }
  1184 +
  1185 + function form_addtransitionguard() {
  1186 + $oForm = new KTForm;
  1187 + $oForm->setOptions(array(
  1188 + 'identifier' => 'ktcore.admin.workflow.addguard',
  1189 + 'label' => _kt("Add New Transition Restriction"),
  1190 + 'action' => 'addguard',
  1191 + 'cancel_action' => 'manageguards',
  1192 + 'fail_action' => 'manageguards',
  1193 + 'submit_label' => _kt("Add Restriction"),
  1194 + 'context' => $this,
  1195 + ));
  1196 +
  1197 + $oTriggerSingleton =& KTWorkflowTriggerRegistry::getSingleton();
  1198 + $aTriggerList = $oTriggerSingleton->listWorkflowTriggers();
  1199 + $vocab = array();
  1200 + foreach ($aTriggerList as $ns => $aTriggerInfo) {
  1201 + $aInfo = $aTriggerInfo; // i am lazy.
  1202 + //var_dump($aInfo);
  1203 + $actions = array();
  1204 + if ($aInfo['guard']) {
  1205 + $actions[] = _kt('Guard');
  1206 + } else {
  1207 + continue;
  1208 + }
  1209 + if ($aInfo['action']) {
  1210 + $actions[] = _kt('Action');
  1211 + }
  1212 + $sActStr = implode(', ', $actions);
  1213 + $vocab[$ns] = sprintf(_kt("%s (%s)"), $aInfo['name'], $sActStr);
  1214 + }
  1215 +
  1216 + $oForm->setWidgets(array(
  1217 + array('ktcore.widgets.selection', array(
  1218 + 'label' => _kt("Restriction Type"),
  1219 + 'name' => 'guard_name',
  1220 + 'vocab' => $vocab,
  1221 + 'simple_select' => false,
  1222 + 'required' => true,
  1223 + )),
  1224 + ));
  1225 +
  1226 + $oForm->setValidators(array(
  1227 + array('ktcore.validators.string', array(
  1228 + 'test' => 'guard_name',
  1229 + 'output' => 'guard_name',
  1230 + )),
  1231 + ));
  1232 + return $oForm;
  1233 + }
  1234 +
  1235 + function do_manageguards() {
  1236 + $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/restrictions_edit');
  1237 + $this->oPage->setBreadcrumbDetails(_kt("Actions"));
  1238 +
  1239 + $restrictions = KTWorkflowUtil::getGuardTriggersForTransition($this->oTransition);
  1240 + $add_form = $this->form_addtransitionguard();
  1241 +
  1242 + $oTemplate->setData(array(
  1243 + 'context' => $this,
  1244 + 'add_form' => $add_form,
  1245 + 'aGuardTriggers' => $restrictions,
  1246 + ));
  1247 + return $oTemplate->render();
  1248 + }
  1249 +
  1250 + function do_addguard() {
  1251 + $oForm = $this->form_addtransitionguard();
  1252 + $res = $oForm->validate();
  1253 + $data = $res['results'];
  1254 + $errors = $res['errors'];
  1255 +
  1256 + if (!empty($errors)) {
  1257 + return $oForm->handleError();
  1258 + }
  1259 +
  1260 + $KTWFTriggerReg =& KTWorkflowTriggerRegistry::getSingleton();
  1261 +
  1262 + $this->startTransaction();
  1263 +
  1264 + $oTrigger = $KTWFTriggerReg->getWorkflowTrigger(KTUtil::arrayGet($data, 'guard_name'));
  1265 + if (PEAR::isError($oTrigger)) {
  1266 + return $oForm->handleError(_kt('Unable to add trigger.'));
  1267 + }
  1268 +
  1269 + $oTriggerConfig = KTWorkflowTriggerInstance::createFromArray(array(
  1270 + 'transitionid' => KTUtil::getId($this->oTransition),
  1271 + 'namespace' => KTUtil::arrayGet($data, 'guard_name'),
  1272 + 'config' => array(),
  1273 + ));
  1274 +
  1275 + if (PEAR::isError($oTriggerConfig)) {
  1276 + return $oForm->handleError(_kt('Unable to add trigger.') . $oTriggerConfig->getMessage());
  1277 + }
  1278 +
  1279 + // now, if the trigger is editable...
  1280 + $oTrigger->loadConfig($oTriggerConfig);
  1281 + if ($oTrigger->bIsConfigurable) {
  1282 + $this->successRedirectTo('editguardtrigger', _kt("New restriction added. This restriction requires configuration: please specify this below."), array('fTriggerInstanceId' => $oTriggerConfig->getId()));
  1283 + } else {
  1284 + $this->successRedirectTo('manageguards', _kt("New restriction added."));
  1285 + }
  1286 + exit(0);
  1287 + }
  1288 +
  1289 +
  1290 + function do_editguardtrigger() {
  1291 + $this->oPage->setBreadcrumbDetails(_kt('editing restriction'));
  1292 + $oTriggerInstance =& KTWorkflowTriggerInstance::get($_REQUEST['fTriggerInstanceId']);
  1293 + if (PEAR::isError($oTriggerInstance)) {
  1294 + return $this->errorRedirectTo('manageguards', _kt('Unable to load trigger.'));
  1295 + }
1109 1296  
  1297 + // grab the transition ns from the request.
  1298 + $KTWFTriggerReg =& KTWorkflowTriggerRegistry::getSingleton();
  1299 +
  1300 + $this->startTransaction();
  1301 +
  1302 + $oTrigger = $KTWFTriggerReg->getWorkflowTrigger($oTriggerInstance->getNamespace());
  1303 + if (PEAR::isError($oTrigger)) {
  1304 + $this->errorRedirectTo('editTransition', _kt('Unable to add trigger.'), 'fWorkflowId=' . $oWorkflow->getId() . '&fTransitionId=' . $oTransition->getId());
  1305 + exit(0);
  1306 + }
  1307 + $oTrigger->loadConfig($oTriggerInstance);
  1308 +
  1309 + return $oTrigger->displayConfiguration($this->meldPersistQuery(array('fTriggerInstanceId' => $oTriggerInstance->getId()), 'saveguardtrigger', true));
  1310 + }
  1311 +
  1312 + // }}}
  1313 +
  1314 + function do_saveguardtrigger() {
  1315 + $oTriggerInstance =& KTWorkflowTriggerInstance::get($_REQUEST['fTriggerInstanceId']);
  1316 + if (PEAR::isError($oTriggerInstance)) {
  1317 + $this->errorRedirectTo('manageguards', _kt('Unable to load trigger.'));
  1318 + exit(0);
  1319 + }
  1320 +
  1321 + $KTWFTriggerReg =& KTWorkflowTriggerRegistry::getSingleton();
  1322 +
  1323 + $this->startTransaction();
  1324 +
  1325 + $oTrigger = $KTWFTriggerReg->getWorkflowTrigger($oTriggerInstance->getNamespace());
  1326 + if (PEAR::isError($oTrigger)) {
  1327 + $this->errorRedirectTo('manageguards', _kt('Unable to load trigger.'));
  1328 + exit(0);
  1329 + }
  1330 + $oTrigger->loadConfig($oTriggerInstance);
  1331 +
  1332 + $res = $oTrigger->saveConfiguration();
  1333 + if (PEAR::isError($res)) {
  1334 + $this->errorRedirectTo('manageguards', _kt('Unable to save trigger: ') . $res->getMessage());
  1335 + exit(0);
  1336 + }
  1337 +
  1338 + $this->successRedirectTo('manageguards', _kt('Trigger saved.'));
  1339 + exit(0);
  1340 + }
  1341 +
  1342 + function do_deleteguardtrigger() {
  1343 + $oTriggerInstance =& KTWorkflowTriggerInstance::get($_REQUEST['fTriggerInstanceId']);
  1344 + if (PEAR::isError($oTriggerInstance)) {
  1345 + return $this->errorRedirectTo('manageguards', _kt('Unable to load trigger.'));
  1346 + }
  1347 +
  1348 + // grab the transition ns from the request.
  1349 + $KTWFTriggerReg =& KTWorkflowTriggerRegistry::getSingleton();
  1350 + $this->startTransaction();
  1351 +
  1352 + $oTrigger = $KTWFTriggerReg->getWorkflowTrigger($oTriggerInstance->getNamespace());
  1353 + if (PEAR::isError($oTrigger)) {
  1354 + $this->errorRedirectTo('manageguards', _kt('Unable to load trigger.'));
  1355 + exit(0);
  1356 + }
  1357 + $oTrigger->loadConfig($oTriggerInstance);
  1358 +
  1359 + $res = $oTriggerInstance->delete();
  1360 + if (PEAR::isError($res)) {
  1361 + $this->errorRedirectTo('editTransition', _kt('Unable to delete trigger: ') . $res->getMessage(), 'fWorkflowId=' . $oWorkflow->getId() . '&fTransitionId=' . $oTransition->getId());
  1362 + exit(0);
  1363 + }
  1364 +
  1365 + $this->successRedirectTo('manageguards', _kt('Trigger deleted.'));
  1366 + exit(0);
  1367 + }
  1368 +
  1369 +
1110 1370 // ----------------- Effects ---------------------
1111 1371 function do_effects() {
1112 1372 $oTemplate = $this->oValidator->validateTemplate('ktcore/workflow/admin/effects_overview');
... ...
sql/mysql/upgrade/3.1.6.2/workflow_state_disabled_actions.sql 0 → 100644
  1 +CREATE TABLE `workflow_state_disabled_actions` (
  2 + `state_id` int(11) NOT NULL default '0',
  3 + `action_name` char(255) NOT NULL default '0',
  4 + KEY `state_id` (`state_id`),
  5 + KEY `action_name` (`action_name`)
  6 +) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  7 +
  8 +
  9 +CREATE TABLE `zseq_workflow_state_disabled_actions` (
  10 + `id` int(10) unsigned NOT NULL auto_increment,
  11 + PRIMARY KEY (`id`)
  12 +) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;
... ...
templates/ktcore/workflow/admin/actions_edit.smarty 0 → 100644
  1 +<h2>Assign blocked actions</h2>
  2 +
  3 +<p class="descriptiveText important">Actions which are checked on this page will
  4 +<strong>not</strong> be available to users.</p>
  5 +
  6 +<form method="POST" action="{$smarty.server.PHP_SELF}">
  7 +<fieldset><legend>Assign Blocked Actions</legend>
  8 +
  9 +{foreach from=$args item=v key=k}
  10 + <input type="hidden" name="{$k}" value="{$v}" />
  11 +{/foreach}
  12 +
  13 +<table class="kt_collection narrow" cellspacing="0">
  14 + <thead>
  15 + <th>State</th>
  16 + {foreach from=$actions item=oAction}
  17 + <th>{$oAction->getDisplayName()}</th>
  18 + {/foreach}
  19 + </thead>
  20 +
  21 + <tbody>
  22 + {foreach from=$states item=oState}
  23 + {assign value=$oState->getId() var=state_id}
  24 + <tr>
  25 + <td>{$oState->getName()}</td>
  26 + {foreach from=$actions item=oAction}
  27 + {assign value=$oAction->getName() var=action_id}
  28 + <td class="centered">
  29 + <input type="checkbox" name="fActions[{$state_id}][{$action_id}]" value="{$action_id}" {if ($grid.$state_id.$action_id)}checked="true"{/if} />
  30 + </td>
  31 + {/foreach}
  32 + </tr>
  33 + {/foreach}
  34 + </tbody>
  35 +</table>
  36 +
  37 +<div class="form_actions">
  38 + <input type="submit" value="{i18n}Block actions{/i18n}" />
  39 +</div>
  40 +
  41 +</fieldset>
  42 +</form>
... ...
templates/ktcore/workflow/admin/actions_overview.smarty
... ... @@ -4,6 +4,9 @@
4 4 possible to block certain <strong>actions</strong> at any given point. Actions
5 5 which are not blocked are still controlled by the usual permissions.</p>
6 6  
  7 +<p><a class="ktAction ktActionDescribed ktEdit" href="{addQS context=$context}action=editactions{/addQS}">Edit Actions</a>
  8 +<a href="{addQS context=$context}action=editactions{/addQS}">Edit Actions</a></p>
  9 +
7 10 <table class="kt_collection narrow" cellspacing="0">
8 11 <thead>
9 12 <th>State</th>
... ... @@ -19,7 +22,11 @@ which are not blocked are still controlled by the usual permissions.&lt;/p&gt;
19 22 <td>{$oState->getName()}</td>
20 23 {foreach from=$actions item=oAction}
21 24 {assign value=$oAction->getName() var=action_id}
22   - <td>&mdash;</td>
  25 + {if $grid.$state_id.$action_id}
  26 + <td class="centered"><span class="ktAction ktDenied">Denied</span></td>
  27 + {else}
  28 + <td class="centered">&mdash;</td>
  29 + {/if}
23 30 {/foreach}
24 31 </tr>
25 32 {/foreach}
... ...
templates/ktcore/workflow/admin/restrictions_edit.smarty 0 → 100644
  1 +<h2>{i18n}Transition Restrictions{/i18n}</h2>
  2 +
  3 +<p class="descriptiveText important">{i18n}All of these must allow the user to perform the transition.{/i18n}</p>
  4 +
  5 +{$add_form->render()}
  6 +
  7 +<br />
  8 +
  9 +{if empty($aGuardTriggers)}
  10 + <div class="ktInfo"><p>{i18n}Anybody (with the ability to see the document) can perform this transition.{/i18n}</p></div>
  11 +{else}
  12 +
  13 +<table class="kt_collection narrow" cellspacing="0">
  14 + <thead>
  15 + <tr>
  16 + <th>{i18n}Restriction{/i18n}</th>
  17 + <th>{i18n}Edit{/i18n}</th>
  18 + <th>{i18n}Delete{/i18n}</th>
  19 + <th>{i18n}Configuration{/i18n}</th>
  20 + </tr>
  21 + </thead>
  22 + <tbody>
  23 + {foreach from=$aGuardTriggers item=oTrigger}
  24 + <tr>
  25 + <td>{$oTrigger->getName()}</td>
  26 + <td>{if $oTrigger->bIsConfigurable}<a class="ktAction ktEdit" href="{addQS context=$context}action=editguardtrigger&fTriggerInstanceId={$oTrigger->getConfigId()}{/addQS}">edit</a>{else}&mdash;{/if}</td>
  27 + <td><a class="ktAction ktDelete" href="{addQS context=$context}action=deleteguardtrigger&fTriggerInstanceId={$oTrigger->getConfigId()}{/addQS}">delete</a></td>
  28 + <td>{$oTrigger->getConfigDescription()}</td>
  29 + </tr>
  30 + {/foreach}
  31 + </tbody>
  32 + </table>
  33 +
  34 + {/if}
... ...
templates/ktcore/workflow/admin/security_overview.smarty
... ... @@ -11,7 +11,7 @@ with the system&#39;s security:&lt;/p&gt;
11 11 <ol>
12 12 <li><a href="{addQS context=$context}action=permissionsoverview{/addQS}">Document Permissions</a> (by state)</li>
13 13 <li><a href="{addQS context=$context}action=actionsoverview{/addQS}">Action Restrictions</a> (by state)</li>
14   - <li><a href="{addQS context=$context}action=transitionrestrictionoverview{/addQS}">Transition Restrictions</a></li>
  14 + <li><a href="{addQS context=$context}action=transitionsecurityoverview{/addQS}">Transition Restrictions</a></li>
15 15  
16 16 </ol>
17 17  
... ...
templates/ktcore/workflow/admin/transition_guards_overview.smarty 0 → 100644
  1 +<h2>Transition Restrictions Overview</h2>
  2 +
  3 +<p class="descriptiveText">In order to ensure that the workflow is followed
  4 +correctly, it is often necessary to restrict the situations in which a
  5 +transition can be followed. This can include things like the permissions the
  6 +user has on the document, the user's groups or roles, or whether the document
  7 +is checked-out or not.</p>
  8 +
  9 +<p class="descriptiveText important">Please note that the plugins that are installed
  10 +will affect the available options</p>
  11 +
  12 +<table class="kt_collection narrow" cellspacing="0">
  13 + <thead>
  14 + <th>Transition</th>
  15 + <th>Edit</th>
  16 + <th>Existing Restrictions</th>
  17 + </thead>
  18 +
  19 + <tbody>
  20 + {foreach from=$transitions item=oTransition}
  21 + {assign value=$oTransition->getId() var=transition_id}
  22 + <tr>
  23 + <td>{$oTransition->getName()}</td>
  24 + <td class="centered"><a href="{addQS context=$context}action=manageguards&fTransitionId={$transition_id}{/addQS}" class="ktAction ktEdit"></a></td>
  25 + <td><span class="descriptiveText">{$context->describeTransitionGuards($oTransition)}</span></td>
  26 + </tr>
  27 + {/foreach}
  28 + </tbody>
  29 +</table>
... ...