- WIKI wrote:
- PHP Source Code
- Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- Source Code
- JavaScript Source Code
- CSS Source Code
- HTML Source Code
- Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- PHP Source Code
- SQL-Query
- SQL-Query
- Source Code
- SQL-Query
- Source Code
- SQL-Query
- Source Code
Voorwoord
Oorspronkelijk was ik van plan om in het tweede deel een herbruikbaar formulierelement te introduceren voor het bouwen van rechtenbomen. In dit deel zou ook een MySQL-implementatie behandeld worden. Echter, tijdens het schrijven van het eerste deel raakten er wat ideeën in een stroomversnelling wat uiteindelijk uitmondde in een nieuwe, flexibelere versie waarbij het wel mogelijk is om een willekeurig aantal operatoren en operanden (rechten) aan een operator te koppelen. Ook het formulierelement heeft inmiddels een metamorfose ondergaan. Al met al ben ik een stuk tevredener over deze nieuwe variant. De oude variant was wel een noodzakelijke iteratie en vormde een prima uitgangspunt voor verdere ontwikkeling, dus deze was zeker niet overbodig of onaf.
Dit tweede deel zal daarom niet meer gaan over het restant van deel #1, maar zal de nieuwe variant volledig behandelen waarbij onderdelen die reeds besproken waren kort behandeld zullen worden in hun nieuwe opzet. Mocht je toch nog geïnteresseerd zijn in de eerste variant dan is dat artikel, weliswaar in het engels, beschikbaar via mijn website. Misschien is het qua historie ook wel interessant om te zien hoe dit zo gegroeid is.
Allereerst wat historie van deze nieuwe variant.
Een nieuw idee
Op de WIKI-pagina over de Poolse Notatie staat de volgende passage:
CitaatWIKI wrote:
Assuming a given arity of all involved operators ... any well formed prefix representation thereof is unambiguous, and brackets within the prefix expression are unnecessary.
Als het dus vaststaat op hoeveel operanden een operator acteert (de ariteit), dan is deze notatiewijze (Poolse Notatie of prefix notatie) ondubbelzinnig en is er geen noodzaak om (sub)expressies te groeperen met haken. Dit wisten we in feite al. Maar als we dit wat beter lezen dan staat er nergens dat de ariteit moet vastliggen, maar enkel dat deze op een of andere manier bekend moet zijn. Oftewel, deze mag best variëren.
Maar hoe weet je dan op hoeveel items een operator zou moeten acteren? Welnu, dat is eenvoudig: je vertelt dit simpelweg.
We pakken ons voorbeeld uit het vorige deel en breiden deze uit:
PHP Source Code
Edit Source Code
- <?php
- /*
- AND--+--1
- .....|
- .....+--OR--+--2
- .....|......|
- .....|......+--NOT--3
- .....|
- .....+--4
- */
- ?>
De AND operator acteert nu op drie items (het recht 1, de operator OR, het recht 4) in plaats van twee.
De OR operator acteert nog steeds op twee items (het recht 2, de operator NOT) maar dit aantal staat niet langer op voorhand vast.
De NOT operator is nog steeds op precies één item actief.
Het zou ook geen hout snijden als de AND of de OR operator op slechts één item zou werken, dus mogelijk is het verstandig om in gedachten te houden dat er nog steeds bepaalde restricties gelden.
We stellen de ariteit van de operator in door een getal toe te voegen aan de operator. Waarschijnlijk is het ook handig (zoals later zal blijken) dat dit aantal en de operator worden gescheiden met een of ander symbool. We gebruiken hiervoor de dubbele punt ( : ). Omdat de ariteit niet langer op voorhand vastligt, en voor uniformiteit, is het zaak dat alle operatoren deze opmaak volgen, dus ook de NOT operator waarbij de ariteit altijd 1 zal zijn.
Onze nieuwe geserialiseerde expressie, gebaseerd op het bovenstaande voorbeeld en met toevoeging van de ariteiten, wordt dan:
Source Code
Edit Source Code
- &:3,1,|:2,2,!:1,3,4
Separation of concerns
Voordat we verdergaan is het zeer belangrijk om twee zaken, die in zekere zin nauw samenhangen, te onderkennen.
Allereerst: security. Het is héél belangrijk dat als we een expressie willen evalueren aan de hand van een set rechten, dat deze syntactisch en semantisch klopt. De functionaliteit die deze evaluatie uitvoert gaat hier ook vanuit. Daarom is het zaak dat ergens voor gebruik de expressie wordt gevalideerd. Een ideale plek om dit te doen is het moment voor opslaan bij de resource waar deze expressie betrekking op heeft. En het is in het algemeen waarschijnlijk ook een goed idee om enkel kloppende expressies op te slaan zodat je hier geen omkijken meer naar hebt.
Ten tweede: performance. Het evalueren van expressies zou zo efficiënt mogelijk moeten verlopen. Door het valideren en evalueren te scheiden kun je hier al een hoop winst boeken. De code die de expressie evalueert gaat er vanuit dat deze klopt en kan daardoor heel kort en to-the-point blijven. Dit houdt dus wel in dat je er echt 100% van uit moet kunnen gaan dat deze expressie aan alle kanten klopt.
Voorwaarts, en terug
We zijn in principe weer terug bij de start: gegeven de definitie van een functie moeten we komen tot een implementatie.
PHP Source Code
Edit Source Code
- <?php
- // @return bool indicating whether a user with $rights is allowed access based on $expression
- function isAllowed($expression, $rights) {
- // do magic
- }
- ?>
De nieuwe implementatie is als volgt:
PHP Source Code
Edit Source Code
- <?php
- function isAllowed($expression, $rights) {
- if ($expression == '') {
- return true;
- }
- $stack = [];
- foreach (array_reverse(explode(',', $expression)) as $token) {
- if (isset($token[1]) && $token[1] == ':') {
- $args = [];
- $operator = substr($token, 0, 1);
- $count = substr($token, 2);
- while ($count--) {
- $args[] = array_pop($stack);
- }
- switch ($operator) {
- case '&':
- foreach ($args as $val) {
- if (!$val) {
- $stack[] = false;
- break 2;
- }
- }
- $stack[] = true;
- break;
- case '|':
- foreach ($args as $val) {
- if ($val) {
- $stack[] = true;
- break 2;
- }
- }
- $stack[] = false;
- break;
- case '!':
- $stack[] = !$args[0];
- break;
- }
- } else {
- $stack[] = isset($rights[$token]);
- }
- }
- return $stack[0];
- }
- ?>
Nogmaals ten overvloede: deze functie gaat er vanuit dat $expression een geldige expressie (een geserialiseerde rechtenboom in Poolse Notatie) is.
De functie isAllowed() is in grote lijnen hetzelfde. Het enige wat verschilt is het operator token wat nu is opgebouwd uit een symbool ($operator) en een ariteit ($count). Daarnaast halen we in plaats van een vast aantal items -voorheen afhankelijk van de operator- nu $count items van de stack af, berekenen we op grond van de operator het tussenresultaat, en plaatsen we deze terug op de stack. En uiteindelijk retourneren we zoals voorheen het enige overgebleven resultaat op $stack.
Deze implementatie is ontstaan na redelijk wat benchmarks en is eigenlijk nooit af, in die zin dat er vast nog ergens wat performancewinst te boeken valt.
Omdat de AND en OR operatoren nu op een willekeurig aantal items (ten minste twee) van toepassing kan zijn is de berekening van het eindresultaat wat veranderd. In eerste instantie leek dit moeilijk te tacklen maar de uiteindelijke vorm is eigenlijk verbazingwekkend eenvoudig. We kunnen zelfs wat lazy evaluation toepassen bij de berekening van het (tussen)resultaat. We bespreken beide cases kort.
PHP Source Code
Edit Source Code
- <?php
- case '&':
- foreach ($args as $val) {
- if (!$val) {
- $stack[] = false;
- break 2;
- }
- }
- $stack[] = true;
- break;
- ?>
Wanneer we de AND operatie evalueren moeten alle items gelijk zijn aan true om het eindresultaat ook true te laten zijn. De werking hiervan zou je kunnen vergelijken met de universele kwantor ∀. Als we een item tegenkomen die false is zijn we klaar. Op dat moment kunnen we zowel de uitvoering van de foreach-loop alsook het restant van het case-statement afbreken. Anders, als alle items doorlopen zijn betekent dit dat deze alle true waren en dit is dan ook het enige geval waarin het eindresultaat true is.
PHP Source Code
Edit Source Code
- <?php
- case '|':
- foreach ($args as $val) {
- if ($val) {
- $stack[] = true;
- break 2;
- }
- }
- $stack[] = false;
- break;
- ?>
Voor de OR operator geldt iets soortgelijks en de werking zou je kunnen vergelijken met de existentiekwantor ∃. Op het moment dat we een item vinden die true is zijn we klaar. Op dat moment breken we zowel de foreach-loop als het case-statement af. Als we alle items doorlopen hebben houdt dat in dat er geen item was die true was. In dat geval retourneren we dan ook false.
Het is prima mogelijk om meer operatoren toe te voegen. Dit omvat het verzinnen van een uniek symbool, het schrijven van een nieuwe case en iets soortgelijks te doen voor het formulierelement in JavaScript, waarover later meer.
Verdere optimalisatie
De bovenstaande code kan verder geoptimaliseerd worden. Zo zou het onderdeel dat betrekking heeft op operatoren (regel 9-37) vervangen kunnen worden door de volgende code:
PHP Source Code
Edit Source Code
- <?php
- switch (substr($token, 0, 1)) {
- case '!':
- $result = !array_pop($stack);
- break;
- case '&':
- $result = true;
- for ($i = substr($token, 2); $i > 0; $i--) {
- if (!array_pop($stack)) {
- $result = false;
- }
- }
- break;
- case '|':
- $result = false;
- for ($i = substr($token, 2); $i > 0; $i--) {
- if (array_pop($stack)) {
- $result = true;
- }
- }
- break;
- }
- $stack[] = $result;
- ?>
In deze variant is het $args hulparray komen te vervallen. Dit resulteert in minder variabelen en schrijfacties en is daardoor iets sneller. Let hierbij op dat we in deze variant de loop niet voortijdig kunnen afbreken omdat er nog steeds een voorgeschreven aantal items (ariteit van de operator) van $stack gehaald moeten worden. Het verschil met de oude variant is hier dat het eindresultaat dat teruggeplaatst wordt op $stack terloops wordt berekend.
Slagen we voor de test?
Het is tijd om wat dingen te gaan testen. Om het overzicht te bewaren over wat de uitkomst van isAllowed() zou moeten zijn loont het de moeite om een waarheidstabel op te stellen.
PHP Source Code
Edit Source Code
- <?php
- /*
- 4 3 2 1 | AND(1,OR(2,NOT(3)),4)
- ---------+----------------------
- 0 0 0 0 | 0
- 0 0 0 1 | 0
- 0 0 1 0 | 0
- 0 0 1 1 | 0
- 0 1 0 0 | 0
- 0 1 0 1 | 0
- 0 1 1 0 | 0
- 0 1 1 1 | 0
- 1 0 0 0 | 0
- 1 0 0 1 | 1
- 1 0 1 0 | 0
- 1 0 1 1 | 1
- 1 1 0 0 | 0
- 1 1 0 1 | 0
- 1 1 1 0 | 0
- 1 1 1 1 | 1
- */
- ?>
We verzinnen wat combinatie om deze code te testen:
PHP Source Code
Edit Source Code
- ?php
- $myRights = array(
- 4 => true,
- );
- if (isAllowed($expression, $myRights)) {
- echo '[error] rights: 4; should not be allowed<br />';
- } else {
- echo 'rights: 4; not allowed<br />';
- }
- $myRights = array(
- 1 => true,
- 3 => true,
- );
- if (isAllowed($expression, $myRights)) {
- echo '[error] rights: 1, 3; should not be allowed<br />';
- } else {
- echo 'rights: 1,3; not allowed<br />';
- }
- $myRights = array(
- 1 => true,
- 4 => true,
- );
- if (isAllowed($expression, $myRights)) {
- echo 'rights: 1,4; is allowed<br />';
- } else {
- echo '[error] rights: 1,4; should be allowed<br />';
- }
- $myRights = array(
- 2 => true,
- 4 => true,
- );
- if (isAllowed($expression, $myRights)) {
- echo '[error] rights: 2,4; should not be allowed<br />';
- } else {
- echo 'rights: 2,4; not allowed<br />';
- }
- $myRights = array(
- 1 => true,
- 2 => true,
- 3 => true,
- );
- if (isAllowed($expression, $myRights)) {
- echo '[error] rights: 1,2,3; should not be allowed<br />';
- } else {
- echo 'rights: 1,2,3; not allowed<br />';
- }
- ?>
Dit levert het volgende resultaat:
Source Code
Edit Source Code
- rights: 4; not allowed
- rights: 1,3; not allowed
- rights: 1,4; is allowed
- rights: 2,4; not allowed
- rights: 1,2,3; not allowed
Het bouwen van een nieuwe expressie
We zijn aanbeland bij een van de wat moeilijkere onderdelen: het op een gebruiksvriendelijke manier bouwen van een nieuwe expressie. In deze variant, in tegenstelling tot zijn voorganger, is het element volledig met de muis te bedienen, het is niet langer nodig om te typen. We maken hierbij gebruik van jQuery (gebouwd met versie 3.3.1 en verder niet getest met oudere versies maar dit zou geen problemen moeten opleveren) voor eenvoudige(re) manipulatie van het DOM. We hanteren hierbij hetzelfde principe als voorheen: we manipuleren een bulleted list en slaan het resultaat op in een verborgen invoerveld. De code is ook wederom zo opgezet dat deze zich leent voor het makkelijk creëren van meerdere elementen.
Allereerst de JavaScript voor het bouwen van de rechtenboom:
JavaScript Source Code
Edit Source Code
- function Rights() {
- this.$container = false;
- this.$tree = false;
- this.$output = false;
- this.operators = {};
- this.rights = {};
- var that = this;
- this.init = function(options) {
- this.$container = $('#'+options.container);
- this.$tree = this.$container.find('ul:first');
- this.$output = $('#'+options.output);
- this.operators = options.operators;
- this.rights = options.rights;
- this.$tree.on('click', 'a.js-add-operand', function(e) {
- var html =
- '<li class="operand">\
- <span style="display: none;"></span>\
- <select>\
- <option value="">- select -</option>';
- for (i in that.rights) {
- html += '<option value="'+i+'">'+that.rights[i]+'</option>';
- }
- html +=
- '</select>\
- <a href="javascript:void(0)" class="button js-edit">✱</a><a class="button js-remove" href="javascript:void(0);">✖</a>\
- <span class="message"></span>\
- </li>';
- $(this).parent().before(html);
- if ($(this).parent().hasClass('js-first-level')) {
- $(this).parent().hide();
- }
- that.generateOutput();
- });
- this.$tree.on('click', 'a.js-add-operator', function(e) {
- var html =
- '<li class="operator">\
- <span style="display: none;"></span>\
- <select>\
- <option value="">- select -</option>'
- for (i in that.operators) {
- html += '<option value="'+i+'">'+that.operators[i]+'</option>';
- }
- html +=
- '</select>\
- <a href="javascript:void(0)" class="button js-edit">✱</a><a class="button js-remove" href="javascript:void(0);">✖</a>\
- <span class="message"></span>\
- <ul>\
- <li class="add"><a class="button js-add-operator" href="javascript:void(0)">+operator</a><a class="button js-add-operand" href="javascript:void(0)">+right</a></li>\
- </ul>\
- </li>';
- $(this).parent().before(html);
- if ($(this).parent().hasClass('js-first-level')) {
- $(this).parent().hide();
- }
- that.generateOutput();
- });
- this.$tree.on('click', 'a.js-remove', function(e) {
- $(this).parent().remove();
- if (that.$tree.find('> li').length == 1) {
- that.$tree.find('li.js-first-level').show();
- }
- that.generateOutput();
- });
- this.$tree.on('click', 'a.js-edit', function(e) {
- var $span = $(this).parent().children(':first');
- var $select = $span.next();
- var value = $span.attr('data-value');
- if ($select.is(':visible') === false) {
- if ($span.attr('data-value')) {
- $select.find("option[value='"+value+"']").prop('selected', true);
- }
- } else {
- if (typeof value === 'undefined') {
- $span.html('<i class="error">empty</i>');
- }
- }
- $span.toggle();
- $select.toggle();
- });
- this.$tree.on('change', 'select', function(e) {
- var $span = $(this).prev();
- if ($(this).val()) {
- $span.attr('data-value', $(this).val());
- $span.html($(this).find('option:selected').text());
- $span.toggle();
- $(this).toggle();
- that.generateOutput();
- }
- });
- this.$container.find('a.js-toggle').click(function(e) {
- that.$tree.find('span').show();
- that.$tree.find('select').hide();
- that.$tree.find('li.add').toggle();
- if (that.$tree.find('> li').length > 1) {
- that.$tree.find('li.js-first-level').hide();
- }
- that.$tree.find('a.js-edit').toggle();
- that.$tree.find('a.js-remove').toggle();
- that.$tree.find('li').find('span:first').each(function() {
- if (typeof $(this).attr('data-value') === 'undefined') {
- $(this).html('<i class="error">empty</i>');
- }
- });
- });
- this.$tree.on({
- 'mouseenter': function() {
- $(this).parent().addClass('delete');
- },
- 'mouseleave': function() {
- $(this).parent().removeClass('delete');
- }
- }, 'a.js-remove');
- } // init
- this.generateOutput = function() {
- var out = '';
- this.$tree.find('li').each(function() {
- if ($(this).hasClass('operator')) {
- $span = $(this).children(':first');
- var value = $span.attr('data-value');
- if (false === (value in that.operators)) {
- value = 'O'; // placeholder value for operator
- }
- var children = $(this).find('> ul > li.operator').length + $(this).find('> ul > li.operand').length;
- out = out + (out == '' ? '' : ',') + value + ':' + children;
- } else if ($(this).hasClass('operand')) {
- $span = $(this).children(':first');
- var value = $span.attr('data-value');
- value = parseInt(value);
- if (isNaN(value)) {
- value = 'R'; // placeholder value for right
- }
- out = out + (out == '' ? '' : ',') + value;
- }
- });
- this.$output.val(out);
- }
- } // function Rights
CSS voor de opmaak:
CSS Source Code
Edit Source Code
- div.rights { font-family: sans-serif; font-size: 10pt; color: #000000; }
- div.rights ul.tree li { line-height: 25px; list-style: disc; }
- div.rights ul.tree span { font-weight: bold; }
- div.rights a.button { color: #000000; background-color: #ffcccc; border-radius: 5px; font-weight: bold; text-decoration: none; padding: 2px 5px; margin-left: 5px; line-height: 25px; }
- div.rights a.button:hover { background-color: #ffaaaa; }
- div.rights ul.tree .delete span { color: #ff0000; }
- div.rights ul.tree .delete select { color: #ff0000; }
- div.rights ul.tree .delete i { color: #ff0000; }
- div.rights ul.tree .delete a { color: #ff0000; }
- div.rights ul.tree .delete li { color: #ff0000; }
- div.rights ul.tree li.delete { color: #ff0000; }
- div.rights ul.tree span.message { font-weight: normal; font-style: italic; margin-left: 5px; color: #ff0000; }
- div.rights ul.tree .error { color: #ff0000; }
- div.rights div.toggle { padding: 5px 0; }
- div.rights div.validation { width: 300px; border-radius: 15px; padding: 5px; font-weight: bold; text-align: center; }
- div.rights div.pass { background-color: #ccffcc; }
- div.rights div.fail { background-color: #ffcccc; }
Daarbij een minimale lap HTML voor het opzetten van (placeholders) voor dit formulierelement:
HTML Source Code
Edit Source Code
- <div class="rights" id="rights">
- <ul class="tree">
- <li class="add js-first-level"><a class="button js-add-operator" href="javascript:void(0)">+operator</a><a class="button js-add-operand" href="javascript:void(0)">+right</a></li>
- </ul>
- <div class="toggle"><a href="javascript:void(0)" class="button js-toggle">toggle</a></div>
- <input type="text" name="expression" id="rights_out" value="" size="50" autocomplete="off">
- </div>
En uiteindelijk een stukje JavaScript voor het koppelen van de functionaliteit aan dit element:
Source Code
Edit Source Code
- <script type="text/javascript">
- //<![CDATA[
- $().ready(function() {
- var rights = new Rights();
- rights.init({'container': 'rights', 'output': 'rights_out', 'operators': {"&":"AND","|":"OR","!":"NOT"}, 'rights': {"1":"one","2":"two","3":"three","4":"four"}});
- });
- //]]>
- </script>
Op een soortgelijke manier kun je zoveel elementen creëren als je wilt.
Alle bovenstaande code kun je combineren zodat dit resulteert in een werkend voorbeeld. Als we de expressie uit het begin van dit artikel gebruiken kunnen we bijvoorbeeld de volgende boom bouwen:
Het valideren van de expressie in PHP
Omdat de expressies flexibeler zijn dan voorheen kan er ook meer misgaan. Het is om deze reden dat we de validatiefunctie veel en gedetailleerde meldingen laten retourneren.
Naast het vaststellen van de kloppendheid van de expressie doet deze functie zijn best om aan te geven waar dingen precies fout gaan als er fouten optreden. Uiteraard moeten de mensen die werken met deze formulierelementen wel enigszins grip hebben op wat zij aan het doen zijn.
PHP Source Code
Edit Source Code
- <?php
- function validate($expression, $validOperators, $validRights=[]) {
- if ($expression == '') {
- return array('valid' => true);
- }
- $stack = [];
- $tokens = array_reverse(explode(',', $expression));
- $tokenIndex = count($tokens);
- // First pass: syntax
- foreach ($tokens as $token) {
- $isRight = isRight($token);
- $isOperator = isOperator($token);
- if ($isRight === false && $isOperator === false) {
- return array(
- 'valid' => false,
- 'syntaxErrors' => true,
- 'errors' => array(
- 0 => 'could not identify token as either a right id or valid operator on index '.$tokenIndex,
- ),
- );
- }
- if ($isOperator) {
- list($operator, $count) = explode(':', $token);
- if (count($stack) < $count) {
- return array(
- 'valid' => false,
- 'syntaxErrors' => true,
- 'errors' => array(
- 0 => 'ran out of rights to inspect for operator on index '.$tokenIndex,
- ),
- );
- }
- $args = [];
- for ($i=0; $i < $count; $i++) {
- $args[] = array_pop($stack);
- }
- }
- $stack[] = false; // just put some result boolean back on the stack
- $tokenIndex--;
- }
- if (count($stack) != 1) {
- return array(
- 'valid' => false,
- 'syntaxErrors' => true,
- 'errors' => array(
- 0 => 'too many rights left on stack; if you have multiple rights on a level an operator should always encapsulate these',
- ),
- );
- }
- // Second pass: semantics
- $tokenIndex = count($tokens);
- $return = array(
- 'valid' => false,
- 'syntaxErrors' => false,
- 'errors' => array(),
- );
- foreach ($tokens as $token) {
- if (strpos($token, ':') == 1) {
- // operator
- list($operator, $count) = explode(':', $token);
- // when you extend this functionality with your own operators you need to add them to $operatorMap
- if (array_key_exists($operator, $validOperators) === false) {
- $return['errors'][$tokenIndex] = 'operator not set or unknown';
- }
- switch ($operator) {
- case '&':
- case '|':
- if ($count < 2) {
- $return['errors'][$tokenIndex] = 'illegal item count for '.$validOperators[$operator].' operator, need at least two';
- }
- break;
- case '!':
- if ($count != 1) {
- $return['errors'][$tokenIndex] = 'illegal item count for NOT operator, need exactly one';
- }
- break;
- }
- } else {
- // right
- if (count($validRights) > 0) {
- if (isset($validRights[$token]) === false) {
- $return['errors'][$tokenIndex] = 'invalid right id';
- }
- }
- if (isIndex($token) === false) {
- $return['errors'][$tokenIndex] = 'right not set';
- }
- }
- $tokenIndex--;
- }
- if (count($return['errors']) == 0) {
- $return['valid'] = true;
- }
- return $return;
- }
- ?>
Deze functie gebruikt ook de volgende hulpfuncties:
PHP Source Code
Edit Source Code
- <?php
- function isOperator($in) {
- return preg_match('#^\D\:(0|[1-9][0-9]*)$#', $in) === 1;
- }
- function isIndex($in) {
- return preg_match('#^[1-9][0-9]*$#', $in) === 1;
- }
- function isRight($in) {
- return preg_match('#^(R|[1-9][0-9]*)$#', $in) === 1;
- }
- ?>
Merk op dat zowel isOperator() alsook iSRight() alleen controleren of het token syntactisch correct is, maar niet expliciet of de waarden (symbolen van de operatoren en de id's van de rechten) geldig zijn. Dit zijn dus rudimentaire checks om vast te stellen of de expressie er "goed genoeg" uitziet om een boom (verder uit te) bouwen. Uiteraard worden de symbolen voor de operatoren en optioneel de id's van de rechten op een zeker moment expliciet geïnspecteerd.
De functie validate() valt uiteen in twee delen. Eerst wordt een syntactische controle uitgevoerd. Dit om vast te stellen dat de expressie op zijn minst syntactisch correct is zodat gegarandeerd kan worden dat er een (weliswaar mogelijk incomplete) rechtenboom gerenderd kan worden. Wanneer er fouten in dit onderdeel optreden wordt de controle meteen gestaakt. Dit doen we omdat als er in dit onderdeel sprake is van een fout dan houdt dat in dat de expressie echt ongeldig is en niet te repareren valt. Dit betekent ook dat vanaf dat moment niet meer gegarandeerd kan worden dat de rechtenboom correct gerenderd kan worden. Als we dit onderdeel zonder fouten hebben doorlopen garandeert dit nog steeds niet dat de expressie geldig is, maar het is er op zijn minst een waarmee we verder kunnen werken.
Dit onderdeel controleert:
- of de tokens (tot op zekere hoogte) geïdentificeerd kunnen worden als ofwel een operator, ofwel een recht id (placeholder waarden worden geaccepteerd)
- of er op elk moment voldoende items op de stack aanwezig zijn om aan de volgende operator te voeren
- of er na afloop van het consumeren van de volledige expressie er nog een enkel item op de stack aanwezig is
In het tweede deel wordt de expressie nogmaals doorlopen en worden er een aantal semantische checks uitgevoerd. Deze controles in combinatie met de controles uit het eerste deel garanderen tezamen dat de expressie geldig is. Zoals gezegd, als we door het eerste deel heen waren gekomen dan houdt dit in dat we in ieder geval te maken hebben met een syntactisch correcte, maar wellicht incomplete, expressie. Dus in dat opzicht is het hier niet langer noodzakelijk om het validatieproces te staken op het moment dat we een fout constateren. Deze fouten kunnen we opsparen en in één keer retourneren zodat de gebruiker deze ook in één bewerkingsronde kan oplossen.
Dit onderdeel controleert:
- of alle operator-symbolen bekend zijn, deze kunnen doorgegeven worden via de parameter $validOperators (array met key-value paren)
- of alle operatoren acteren op een geldig aantal items
- (optioneel) of alle rechten id's geldig zijn, deze kunnen opgegeven worden via de optionele parameter $validRights
De functie retourneert een array die altijd een valid index heeft. De waarde van deze index geeft aan of de expressie geldig is. Indien er aan het einde van de rit (na de syntactische en semantische controles) geen fouten zijn opgetreden zal deze index de waarde true krijgen.
Nota bene: de JavaScript functionaliteit zou je in grote lijnen moeten helpen bij het wegsturen van situaties waarin de expressie syntactisch ongeldig is, dus het zou niet mogelijk moeten zijn om met klikacties in de boom een syntactisch ongeldige expressie te genereren die je op den duur zou kunnen buitensluiten van het aanpassen van deze boom.
Echter, als deze waarde false is dan betekent dit dat de expressie ongeldig is. In dit geval zou de expressie niet gebruikt mogen worden voor de evaluatie van gebruikersrechten. Het wordt ook met klem afgeraden niet-kloppende expressies op te slaan in de database. Idealiter zou je altijd ergens de expressie gevalideerd moeten hebben voordat je hier isAllowed() op loslaat.
Zoals al eerder is aangehaald opereert isAllowed() onder de aanname dat de expressie geldig is. Als je probeert een niet-kloppende expressie te evalueren dan is het niet gegarandeerd dat dit de juiste resultaten oplevert. In het ergste geval zal er true geretourneerd worden op het moment dat dit echt niet de bedoeling is.
Indien de status index gelijk is aan false zal het geretourneerde array ook een errors en syntaxErrors index bevatten. De syntaxErrors index bevat een Boolse waarde die aangeeft of er syntaxfouten zijn opgetreden. Dit vormt een mogelijk stopsignaal voor het renderen van de rechtenboom. De errors index bevat een subarray die een lijst van volgnummer-foutmelding paren bevat indien er semantische fouten waren.
Het wijzigen van een bestaande expressie
Het is nog steeds een goed idee om enkel geldige expressies op te slaan zodat als je deze later bewerkt de voorafgedrukte boom ook juist wordt weergegeven.
PHP Source Code
Edit Source Code
- <?php
- function printTree($expression, $operators, $rights, $messages=array()) {
- ?><ul class="tree"><?php
- if (empty($expression) === false) {
- $stack = [0 => 1]; // depth => #items
- $index = 1;
- $level = 0;
- foreach (explode(',', $expression) as $token) {
- if (strpos($token, ':') == 1) {
- // operator
- list($operator, $count) = explode(':', $token);
- $stack[$level]--;
- $level++;
- $stack[$level] = (int) $count;
- if (isset($operators[$operator])) {
- $operatorValue = $operator;
- $operatorHTML = escape($operators[$operator]);
- } else {
- $operatorValue = 'O'; // placeholder value
- $operatorHTML = '<i class="error">empty</i>';
- }
- ?><li class="operator">
- <span data-value="<?php echo escape($operatorValue); ?>"><?php echo $operatorHTML; ?></span>
- <select style="display: none;" rel="display: none;">
- <option value="">- select -</option><?php
- foreach ($operators as $k => $v) {
- ?><option value="<?php echo escape($k); ?>"><?php echo escape($v); ?></option><?php
- }
- ?></select>
- <a href="javascript:void(0)" class="button js-edit">✱</a><a class="button js-remove" href="javascript:void(0);">✖</a>
- <span class="message"><?php
- if (isset($messages['errors'][$index])) {
- echo escape($messages['errors'][$index]);
- }
- ?></span>
- <ul><?php
- } else {
- // right/operand
- if (isset($rights[$token])) {
- $rightValue = $token;
- $rightHTML = escape($rights[$token]);
- } else {
- $rightValue = 'R'; // placeholder value
- $rightHTML = '<i class="error">empty</i>';
- }
- ?><li class="operand">
- <span data-value="<?php echo escape($rightValue); ?>"><?php echo $rightHTML; ?></span>
- <select style="display: none;">
- <option value="">- select -</option><?php
- foreach ($rights as $k => $v) {
- ?><option value="<?php echo escape($k); ?>"><?php echo escape($v); ?></option><?php
- }
- ?></select>
- <a href="javascript:void(0)" class="button js-edit">✱</a><a class="button js-remove" href="javascript:void(0);">✖</a>
- <span class="message"><?php
- if (isset($messages['errors'][$index])) {
- echo escape($messages['errors'][$index]);
- }
- ?></span>
- </li><?php
- // we printed an item, lower stack count of current level
- $stack[$level]--;
- }
- // close levels that have no more items to print
- while ($level > 0 && $stack[$level] == 0) {
- array_pop($stack);
- ?><li class="add">
- <a class="button js-add-operator" href="javascript:void(0);">+operator</a><a class="button js-add-operand" href="javascript:void(0);">+right</a>
- </li>
- </ul></li><?php
- $level--;
- }
- $index++;
- }
- if (empty($stack) === false) {
- while ($level > 0) {
- array_pop($stack);
- ?><li class="add">
- <a class="button js-add-operator" href="javascript:void(0);">+operator</a><a class="button js-add-operand" href="javascript:void(0);">+right</a>
- </li>
- </ul></li><?php
- $level--;
- }
- }
- }
- // if the expression is empty, we have visible controls on the top level, otherwise, just hide them
- $style = ($expression == '' ? '' : ' style="display: none;"');
- ?><li class="add js-first-level"<?php echo $style; ?>>
- <a class="button js-add-operator" href="javascript:void(0);">+operator</a><a class="button js-add-operand" href="javascript:void(0);">+right</a>
- </li>
- </ul>
- <div class="toggle"><a href="javascript:void(0)" class="button js-toggle">toggle</a></div><?php
- }
- ?>
Hiermee kun je een eerder opgeslagen, kloppende, expressie als volgt initialiseren:
PHP Source Code
Edit Source Code
- ?php
- $expression = '&:3,1,|:2,2,!:1,3,4'; // some valid expression
- $operators = ['&' => 'AND', '|' => 'OR', '!' => 'NOT'];
- $rights = [1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four'];
- $messages = validate($expression, $operators);
- $printTree = true;
- ?><div class="rights" id="rights"><?php
- if ($messages['valid'] === false) {
- if ($messages['syntaxErrors']) {
- ?><div class="validation fail">
- <p>Syntax errors occurred, unable to print tree.</p>
- </div>
- <p>Message: <?php echo escape($messages['errors'][0]) ?></p><p>Expression: <?php echo escape($expression) ?></p><?php
- $printTree = false;
- } else {
- ?><div class="validation fail">
- <p>Validation failed, check errors below.</p>
- </div><?php
- }
- } else {
- ?><div class="validation pass">
- <p>Validation passed!</p>
- </div><?php
- }
- if ($printTree) {
- printTree($expression, $operators, $rights, $messages);
- }
- ?><input type="text" name="expression" id="rights_out" value="<?php echo escape($expression); ?>" size="50" autocomplete="off">
- </div>
- <script type="text/javascript">
- //<![CDATA[
- $().ready(function() {
- var rights = new Rights();
- rights.init({'container': 'rights', 'output': 'rights_out', 'operators': <?php echo json_encode($operators); ?>, 'rights': <?php echo json_encode($rights); ?>});
- });
- //]]>
- </script>
Het bovenstaande fragment bevat de escape() hulpfunctie voor het escapen van data binnen de HTML-context:
PHP Source Code
Edit Source Code
- <?php
- function escape($s) {
- return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
- }
- ?>
MySQL implementatie
Het is ook zinnig om een MySQL implementatie van deze functionaliteit te hebben. Bijvoorbeeld voor het genereren van gepagineerde lijsten van items waar jij toegang toe hebt. Als je dit niet aan de database-kant zou kunnen filteren dan zou je dit alsnog aan de PHP-kant moeten doen. Ook zou je dan alle items op moeten halen omdat je niet op voorhand weet wat je zou mogen inzien of gebruiken. Daarnaast wordt het berekenen van alle pagina's een moeilijke klus waarbij je eigenlijk alle pagina's zou moeten genereren, in plaats van enkel de actuele. Zonder een implementatie aan de MySQL-kant dreigt dus het gevaar dat het gebruik van deze rechtenfunctionaliteit traag en complex wordt.
Uiteindelijk willen we een MySQL FUNCTION die we aan kunnen roepen waarbij de expressie (expression) wordt toegepast op een verschafte (geserialiseerde) lijst van rechten (rights). Het is echter vrij onmogelijk om fatsoenlijke feedback te krijgen tijdens de ontwikkeling van zo'n FUNCTION. Daartoe gebruiken we voor de ontwikkeling in plaats van een FUNCTION een PROCEDURE. Dit stelt ons continu in staat om rechtstreeks inzicht te verkrijgen over de interne toestand wat het vervolgens vele malen makkelijker maakt om SQL-code te ontwikkelen en te debuggen. Dit omdat je toch enige zekerheid wilt hebben dat deze functionaliteit enigszins klopt ;).
Disclaimer: ik heb er wel vertrouwen in, maar deze PROCEDURE en FUNCTION zijn in zekere zin nog wat experimenteel. Ga hier voorzichtig mee om.
Allereerst bouwen we dus een PROCEDURE met een hele hoop terugkoppeling:
SQL-Query
Edit Source Code
- DELIMITER ;;
- DROP PROCEDURE IF EXISTS is_allowed;;
- CREATE PROCEDURE is_allowed(expression VARCHAR(255), rights VARCHAR(255))
- BEGIN
- DECLARE stack VARCHAR(255);
- DECLARE token VARCHAR(25);
- DECLARE count INT(3) UNSIGNED;
- DECLARE i INT(3) UNSIGNED;
- DECLARE result BOOL;
- IF (expression = '') THEN
- SET stack = '1';
- SELECT CONCAT('empty expression') AS feedback;
- ELSE
- SET stack = '';
- SET expression = REVERSE(expression);
- END IF;
- WHILE (LENGTH(expression) > 0) DO
- SET token = REVERSE(SUBSTRING_INDEX(expression, ',', 1));
- SET expression = SUBSTRING(expression, LENGTH(token) + 2);
- IF (LOCATE(':', token) = 0) THEN
- SET stack = CONCAT(FIND_IN_SET(token, rights) > 0, stack);
- SELECT CONCAT('right found: ', token,', stack: ', stack) AS feedback;
- ELSE
- SET count = SUBSTRING_INDEX(token, ':', -1);
- SET token = SUBSTRING(token, 1, 1);
- SELECT CONCAT('operator found: ', token, ', count: ', count) AS feedback;
- IF (token = '!') THEN
- SET result = 1 - SUBSTRING(stack, 1, 1);
- ELSEIF (token = '&') THEN
- SET result = TRUE;
- SET i = 1;
- WHILE (i <= count AND result) DO
- IF (SUBSTRING(stack, i, 1) = 0) THEN
- SELECT CONCAT('found FALSE on index ', i, ' exiting') AS feedback;
- SET result = FALSE;
- END IF;
- SET i = i + 1;
- END WHILE;
- ELSEIF (token = '|') THEN
- SET result = FALSE;
- SET i = 1;
- WHILE (i <= count AND result = FALSE) DO
- IF (SUBSTRING(stack, i, 1) = 1) THEN
- SELECT CONCAT('found TRUE on index ', i, ' exiting') AS feedback;
- SET result = TRUE;
- END IF;
- SET i = i + 1;
- END WHILE;
- END IF;
- SET stack = CONCAT(result, SUBSTRING(stack, count + 1));
- SELECT CONCAT('stack after processing: ', stack) AS feedback;
- END IF;
- END WHILE;
- SELECT CONCAT('final result: ', stack) AS feedback;
- END;;
- DELIMITER ;
Op deze manier kunnen we exact volgen welk intern pad wordt bewandeld binnen deze code. Roepen we deze PROCEDURE bijvoorbeeld als volgt aan:
SQL-Query
Edit Source Code
- CALL is_allowed('&:3,51,|:2,52,!:1,53,54', '51,54');
Dan resulteert dit (met enige opschoning) de volgende feedback:
Source Code
Edit Source Code
- right found: 54, stack: 1
- right found: 53, stack: 01
- operator found: !, count: 1
- stack after processing: 11
- right found: 52, stack: 011
- operator found: |, count: 2
- found TRUE on index 2 exiting
- stack after processing: 11
- right found: 51, stack: 111
- operator found: &, count: 3
- stack after processing: 1
- final result: 1
Een aanroep van:
SQL-Query
Edit Source Code
- CALL is_allowed('&:3,51,|:2,52,!:1,53,54', '51,53');
Levert de volgende feedback op:
Source Code
Edit Source Code
- right found: 54, stack: 0
- right found: 53, stack: 10
- operator found: !, count: 1
- stack after processing: 00
- right found: 52, stack: 000
- operator found: |, count: 2
- stack after processing: 00
- right found: 51, stack: 100
- operator found: &, count: 3
- found FALSE on index 2 exiting
- stack after processing: 0
- final result: 0
Na een aantal iteraties kun je dit vrijwel 1:1 converteren naar een vrijwel identieke FUNCTION:
SQL-Query
Edit Source Code
- DELIMITER ;;
- DROP FUNCTION IF EXISTS is_allowed;;
- CREATE FUNCTION is_allowed(expression VARCHAR(255), rights VARCHAR(255)) RETURNS BOOL DETERMINISTIC
- BEGIN
- DECLARE stack VARCHAR(255);
- DECLARE token VARCHAR(25);
- DECLARE count INT(3) UNSIGNED;
- DECLARE i INT(3) UNSIGNED;
- DECLARE result BOOL;
- IF (expression = '') THEN
- RETURN TRUE;
- END IF;
- SET stack = '';
- SET expression = REVERSE(expression);
- WHILE (LENGTH(expression) > 0) DO
- SET token = REVERSE(SUBSTRING_INDEX(expression, ',', 1));
- SET expression = SUBSTRING(expression, LENGTH(token) + 2);
- IF (LOCATE(':', token) = 0) THEN
- SET stack = CONCAT(FIND_IN_SET(token, rights) > 0, stack);
- ELSE
- SET count = SUBSTRING_INDEX(token, ':', -1);
- SET token = SUBSTRING(token, 1, 1);
- IF (token = '!') THEN
- SET result = 1 - SUBSTRING(stack, 1, 1);
- ELSEIF (token = '&') THEN
- SET result = TRUE;
- SET i = 1;
- WHILE (i <= count AND result) DO
- IF (SUBSTRING(stack, i, 1) = 0) THEN
- SET result = FALSE;
- END IF;
- SET i = i + 1;
- END WHILE;
- ELSEIF (token = '|') THEN
- SET result = FALSE;
- SET i = 1;
- WHILE (i <= count AND result = FALSE) DO
- IF (SUBSTRING(stack, i, 1) = 1) THEN
- SET result = TRUE;
- END IF;
- SET i = i + 1;
- END WHILE;
- END IF;
- SET stack = CONCAT(result, SUBSTRING(stack, count + 1));
- END IF;
- END WHILE;
- RETURN stack;
- END;;
- DELIMITER ;
Daarna zou je nog wat tests kunnen uitvoeren net als bij de PHP-variant:
Source Code
Edit Source Code
- > SELECT is_allowed('&:3,51,|:2,52,!:1,53,54', '54') AS test;
- +------+
- | test |
- +------+
- | 0 |
- +------+
- 1 row in set (0.01 sec)
- > SELECT is_allowed('&:3,51,|:2,52,!:1,53,54', '51,53') AS test;
- +------+
- | test |
- +------+
- | 0 |
- +------+
- 1 row in set (0.00 sec)
- > SELECT is_allowed('&:3,51,|:2,52,!:1,53,54', '51,54') AS test;
- +------+
- | test |
- +------+
- | 1 |
- +------+
- 1 row in set (0.00 sec)
- > SELECT is_allowed('&:3,51,|:2,52,!:1,53,54', '52,54') AS test;
- +------+
- | test |
- +------+
- | 0 |
- +------+
- 1 row in set (0.00 sec)
- > SELECT is_allowed('&:3,51,|:2,52,!:1,53,54', '51,52,53') AS test;
- +------+
- | test |
- +------+
- | 0 |
- +------+
- 1 row in set (0.00 sec)
Merk op dat ik hier expres wat grotere rechten id's (groter dan 9) test in MySQL. Dit omdat wanneer je REVERSE() toepast op een string in MySQL, dit de hele string omdraait, dus ook de cijfers waaruit de rechten zijn opgebouwd, en niet enkel de volgorde van items in een lijst zoals bij array_reverse() in PHP het geval is. MySQL kent het type array niet wat voor wat uitdagingen zorgt.