IfAuthEx Dokuwiki Plugin

Dokuwiki plugin to filter wiki content based on username and groups
Usage guide on Dokuwiki's Wiki.
This Dokuwiki plugin activates and deactivates zones in a wiki page based on the user that is viewing the page. Obviously, the page cannot be cached, but it enables nice effects, such as customizing the wiki landing page.

In fact, this is enabled in this very same wiki. Let's see it in action. At the moment

  • you are not logged in

The message above changes depending on the logged user. Of course, it is always possible to reveal it by looking at the wiki page source, but the purpose is not to keep something safe, just to customize the page.

IfAuthEx Dokuwiki plugin

  • Start: 8 Jan 2019
  • End/release: 11 Jan 2019
  • Status: complete (updates possible)

Source

Page on DokuWiki

There is already a plugin that implements the same behavior, it's called IfAuth (no surprise there). However, this plugin can only combine conditions (e.g. user name or user group) with the OR operator, without associativity, and it's thus not functionally complete (that is, you cannot state conditions such as “in group A and simultaneously in group B”).

Ifauth is a simple plugin, extending it to be functionally complete amounted to a complete rewrite, so here we are.

Among the last changes, the grammar was fixed to support usernames containing dashes and dots, support for multibyte strings was added, and the output was fixed to avoid breaking sections.

Special thanks to Andreas Gohr and annda, for helping in making the output more robust and the general cleanup of the DokuWiki part. One more thanks goes to Andreas for all his work on the DokuWiki project. Thanks to tanakakz-alpha for bringing attention to multibyte strings and to Matthias Keller for spotting the limitations of the literals matching.

I considered extending the plugin to a simple syntax, chaining literal expressions (user or @group, possibly negated) with ANDs and ORs, but personally I do not always remember which one has higher priority, so I thought that parentheses were necessary.

Literal expressions, AND, OR, NOT and parenthesized sub-expressions can build arbitrary boolean expressions, so the best solution would've been having something like Python's Asteval. Asteval can build AST out of Python code, and can evaluate the expression in a relatively secure context. I could not find anything like that for PHP, so I wrote something myself.

To get an AST out of an arbitrary expression, you need a tokenizer, a lexer and a parser.

The tokenizer is implemented using regular expressions (which is maybe excessive and surely not very fast, but super practical to use). The tokenizer consumes characters from the expression and identifies them against a supported token list. It can ignore tokens (such as whitespace), and remembers the matched portion of the string, to generate meaningful error messages.

The lexer loops through a grammar defined directly in the plugin, in order. It supports operators with different “fixing”: infix, prefix, postfix, “wrap” (not exactly fixing, but needed for parentheses), and none (for literal expressions). There are some limitations on the number of tokens it can use for each operator, and each operator has a given arity, also with limitations.

For example, the operator “OR” has a single token, ||, and it's an infix operator with infinite arity (because you can OR consecutively an arbitrary number of expressions). Hence, it can match expressions such as x0 || x1 || … || xn. The limitations on the number of tokens and arity are needed to have a simplified pattern matching; in this case, a single token infixed between several expressions, but could also be prefix or postfix (with the appropriate shift in the first token's position). Basically, it's a matter of finding in a sequence of tokens those that belong to a certain operator. By matching the operators in order of priority, we can guarantee that an expression is well-formed (or we can throw an exception if for example ( is not matched by a corresponding )).

Parser and lexer are actually the same thing in this implementation. As soon as an operator is identified, the relevant tokens are grouped and replaced with an instance of that operator. The grouping encodes the AST; the next operator is then matched against all the tokens, including inside those that were already grouped, by traversing the tree top-down, starting at the root. At the end of the process, you get the AST.

The AST has a functionality to rebuild the original expression (up to whitespace differences), which is used in unit test.

The AST can be evaluated directly by traversing it. The information about the user and the groups are encapsulated in an EvaluationContext class, to be able to simulate them at unit test.

The expression is

(!@A && !B && @C) || !(@D || E || @F)

The tokenizer disassembles it in

OPEN_PAR NOT IN_GROUP <A> AND NOT <B> AND IN_GROUP <C> CLOSE_PAR OR NOT OPEN_PAR IN_GROUP <D> OR <E> OR IN_GROUP <F> CLOSE_PAR

The lexer/parser processes in order literal expressions, parenthesized sub-expressions, in-group test operator, NOT, AND, OR. The expression is transformed as follows:

OPEN_PAR NOT IN_GROUP [
    Lit: <A>
] AND NOT [
    Lit: <B>
] AND IN_GROUP [
    Lit: <C>
] CLOSE_PAR OR NOT OPEN_PAR IN_GROUP [
    Lit: <D>
] OR [
    Lit: <E>
] OR IN_GROUP [
    Lit: <F>
] CLOSE_PAR
[
    Subexpr: NOT IN_GROUP [
        Lit: <A>
    ] AND NOT [
        Lit: <B>
    ] AND IN_GROUP [
        Lit: <C>
    ]
] OR NOT [
    Subexpr: IN_GROUP [
        Lit: <D>
    ] OR [
        Lit: <E>
    ] OR IN_GROUP [
        Lit: <F>
    ]
]
[
    Subexpr: NOT [
        InGroup: [
            Lit: <A>
        ]
    ] AND NOT [
        Lit: <B>
    ] AND [
        InGroup: [
            Lit: <C>
        ]
    ]
] OR NOT [
    Subexpr: [
        InGroup: [
            Lit: <D>
        ]
    ] OR [
        Lit: <E>
    ] OR [
        InGroup: [
            Lit: <F>
        ]
    ]
]
[
    Subexpr: [
        Not: [
            InGroup: [
                Lit: <A>
            ]
        ]
    ] AND [
        Not: [
            Lit: <B>
        ]
    ] AND [
        InGroup: [
            Lit: <C>
        ]
    ]
] OR [
    Not: [
        Subexpr: [
            InGroup: [
                Lit: <D>
            ]
        ] OR [
            Lit: <E>
        ] OR [
            InGroup: [
                Lit: <F>
            ]
        ]
    ]
]
[
    Or:  [
        Subexpr: [
            And: [
                Not: [
                    InGroup: [
                        Lit: <A>
                    ]
                ]
            ], [
                Not: [
                    Lit: <B>
                ]
            ], [
                InGroup: [
                    Lit: <C>
                ]
            ]
        ]
    ], [
        Not: [
            Subexpr: [
                Or: [
                    InGroup: [
                        Lit: <D>
                    ]
                ], [
                    Lit: <E>
                ], [
                    InGroup: [
                        Lit: <F>
                    ]
                ]
            ]
        ]
    ]
]

this corresponds to the following AST, once the subexpressions are dissolved:

Or
+-- And:
|   +-- Not:
|   |   +-- InGroup:
|   |       +-- Lit: <A>
|   +-- Not:
|   |   +-- Lit: <B>
|   +-- InGroup:
|       +-- Lit: <C>
+-- Not:
    +-- Or:
        +-- InGroup:
        |   +-- Lit: <D>
        +-- Lit: <E>
        +-- InGroup:
            +-- Lit: <F>
  • en/progetti/5p4k/ifauthex-dokuwiki-plugin.txt
  • Last modified: 2020/04/19 20:21
  • by 5p4k