RestrictAccessByCategoryAndGroup.php

From publications

The original extension was written by Andrés Orencio Ramirez Perez[1] and has been customized by User:Ralph to function as outlined in category:Access Control for this wiki.

<?php
 
// History:
// 2022-09-20 Ralph Holland - include Special:Categories white-listing.
// 2022-09-20 Ralph Holland - now permit quite a few more Special: page titles.
// 2022-09-20 Ralph Holland - fixed a comparison typo after r
// 2022-09-19 Ralph Holland - made category:edit: markings inclusive while excluding users that have no corresponding marking for the edit action.
// 2022-09-18 Ralph Holland - Now User:category is not completely exclusive, other User:category can be put on page to share with another user
//                            public is not public when a private has been placed on the page or an exclusive user:category is on the page.
//                            white-listed pages cannot override an exclusive category denying access
// 2022-08-15 Ralph Holland - made pages without a category not public, made private exclusive, implemented category:User:<name> as exclusive page access
//
 
if ( !defined( 'MEDIAWIKI' ) ) {
        die( 'Not a valid entry point.' );
}
 
$wgExtensionCredits['parserhook'][] = array(
        'name' => 'Restrict access by category and group',
        'author' => 'Andrés Orencio Ramirez Perez & Ralph Holland',
        'url' => 'https://www.mediawiki.org/wiki/Extension:Restrict_access_by_category_and_group',
        'description' => 'Allows to restrict access to pages by users groups and page categories: See [[Security]]',
        'version' => '3.1.0-adapted-by-ralph-holland'
);
 
$wgHooks['userCan'][] = 'restrictAccessByCategoryAndGroup';
//$wgHooks['getUserPermissionsErrors'][] = 'restrictAccessByCategoryAndGroup';
 
function debug( $a ) {
    for ($i =0; $i < 35; $i++) {
        print " ";
    }
    print "$a";
}
 
function restrictAccessByCategoryAndGroup( $title, $user, $action, $result ) {
 
    # debug("Ralph is debugging access controls ATM title=".$title." user=".$user." action=".$action."<br/>");
 
    global $wgGroupPermissions;
    global $wgWhitelistRead;
    global $wgLang;
    global $wgHooks;
    global $wgContLang;
    global $wgVersion;
 
    // strip the prefix of Talk etc off
    $posn = strpos( $title,":");
    $prefix = $posn > -1 ? substr( $title, 0, $posn ) : "";
    $page = $posn > -1 ? substr( $title, $posn+1 ) : $title;
 
    #debug( "RBH is debugging page=".$page." prefix=".$prefix." action=".$action." user=".$user."</br>" );
 
    //These are special pages to be white-listed 
    if (strpos($title,'Special:')===0) {
        switch($title) {
        case 'Special:Login':
        case 'Special:Logout':
        case 'Special:UserLogin':
        case 'Special:UserLogout':
        case 'Special:Badtitle':
        case 'Special:Random':
        // case 'Special:RecentChanges':
        case 'Special:Version':
        case 'Special:Categories':
        case 'Special:AllPages':
                    # debug( "*** $title permitted" );
                    return true;
 
        default:
            if ( strpos($title,"Special:WhatLinksHere")===0 ) { // || strpos($title,"Special:RecentChangesLinked")===0 ) {
                # debug("*** $title must be permitted");
                return true;
            }
            break;
        }
        }
    
    if ( in_array('sysop',$user->getGroups()) ) {
        # debug( "*** sysop can see all pages" );
        return true;
    }
 
    //Build up System categories from the title's categories
    $systemCategory = array();
 
    // get the users groups
    $userGroups = $user->getGroups();
    $hasUserCategory = false;
    $userCategoryMatched = false;
 
    // now check the page categories against the system groups
    $publicPage = false;
    $privatePage = false;
    $editUserMatched = false;
    $editGroupMatched = false;
 
    // now process page categories
    foreach ( array_change_key_case( $title->getParentCategories(), CASE_LOWER ) as $key => $value ) {
        $formatedKey = substr( $key, ( strpos( $key, ":" ) + 1 ) );
 
        # debug( "category:".$formatedKey."<br/>" );
 
        // check for the exclusive category public
        if ( $formatedKey=='public' ) {
            $publicPage = true;
        }
        // check for the exclusive category private
        elseif ( $formattedKey==='private' ) {
            // check that the user holds private group
            if (! in_array($formatedKey,$userGroups) ) {
                $privatePage = true;
            }
        }
        // check for the exclusive category user:*
        elseif ( strpos( $formatedKey, 'user:' ) === 0  ) {
            // these are special user categories e.g. [[User:Ralph]]
            $hasUserCategory = true;
            $name = 'user:'.strtolower($user->getName());
            # debug( $formatedKey.' '.$name.'<-name<br/>' );
            if ( $name && $formatedKey===$name ) {
                # debug( '**** user permitted by user category'.$formatedKey.' '.'allowed<br/>' );
                $userCategoryMatched = true;
            }
        }
        elseif ( strpos( $formatedKey, 'edit:user:' ) === 0 ) {
            // check if user is not permitted to edit
            if ($action === 'edit') {
                $hasEdit = true;
                $name = 'edit:user:'.strtolower($user->getName());
                if ($name === $formatedKey) {
                    # debug('*** edit user matched'.$name);
                    $editUserMatched = true;
                }
            }    
        }
        elseif ( strpos( $formatedKey, 'edit:' ) === 0 ) {
            //  check is user does not have groups to permit edit
            if ($action === 'edit') {
                $hasEdit = true;
                $group = substr( $formatedKey, ( strpos( $formatedKey, ":" ) + 1 ) );
                if ( in_array($group,$userGroups) ) {
                    # debug('*** edit group matched '.$group);
                    $editGroupMatched = true;
                }
            }
        }    
        else {
            // build up non-exclusive categories
            $systemCategory[ $formatedKey ] = $value;
        }
    }
 
    // check if category:edit: marking found
    if ($hasEdit) {
        if ($editGroupMatched) {
            # debug('*** matched a category:edit:group marking');
            return true;
        }
        else if ($editUserMatched) {
            # debug('*** matched a category:edit:user: markingi');
            return true;
        }
        else {
            # debug('--- denied user by category:edit marking');
            return false;
        }
    }
 
    // check if page marked with the user:category
    if ($userCategoryMatched) {
        # debug('*** user allowed by user:category');
        return true;
    }
 
    // check if user is excluded by page marked private
    if ($privatePage) {
        # debug('--- denied because the page was marked private');
        return false;
    }
 
    // check if user is excluded by another user:category
    if ($hasUserCategory) {
        # debug('--- denied by user:category');
        return false;
    }
 
    // check if page was marked as public
    if ($publicPage) {
        # debug('*** permitted by public page');
        return true;
    }
 
    // honour the existing white-list mechanism
    if ( count( $wgWhitelistRead ) != 0 ) {
        if ( in_array( $title, $wgWhitelistRead ) ) {
                # debug("*** white listed" );
                return true;
        }
    }
 
    // check if page has any remaining categories
    if ( count( $systemCategory ) === 0 ) {
        if ( count($userGroups) > 0 ) {
            // Note this code insists on logged-on users being a member of at least one group
            # debug('*** logged in users can access pages without categories');
            return true;
        }
        # debug('-- anonymous user cannot access pages without categories');
        return false;
    } else {
        // check remaining page categories for inclusive private permissions
        // users must be logged in to process remaining category markings
        if ( count($userGroups) != 0) {
            
            // for each group permission ...
            $hasPrivateCategories = false;
            foreach ( $wgGroupPermissions as $key => $value ) {
 
                # debug( 'group->'.$key.' '.$action.' '.$title.'<br/>' );
 
                //  ... if the group permission is marked as 'private' then check ...
                if ( isset( $wgGroupPermissions[$key]['private'] ) ) {
 
                    $hasPrivateCategories = true;
        
                    // ... if page is marked with a category that corresponds to the private group 
                    if ( array_key_exists( strtolower( str_replace( " ", "_", $key ) ), $systemCategory ) ) {
 
                        // ... permit access if user is assigned the group
                        if ( in_array( $key, $userGroups) ) {
                            # debug( '*** permitted user holds the group '.$key );
                            return true;
                        }
                    }
                }
            }
            if (!$hasPrivateCategories) {
                # debug('*** user permitted to access a page that does not contain private categories');
                return true;
            }
        }
    }
    # debug("--- user does not hold an appropriate private category and page is not marked for their access");
    return false;
}

installation

The extension is installed in the file location:

.../mediawiki/extensions/rabcg/RestrictAccessByCategoryAndGroup.php

The extension is loaded from the:

.../mediawiki/LocalSettings.php

access control checks

The access control reference data is defined in LocalSettings.php as follows:

  • privileges are denied by:
$wgGroupPermissions[<group>]['*'] = false;
  • and the private privilege for a group is defined by:
$wgGroupPermissions[<group>]['private'] = true;

Those groups may be assigned to a user account via the Special:UserRights page, where the privilege groups that were assigned are accessible via $user->getGroups() in this extension.

The Access Control checks are performed in this order as follows:

  1. the special pages:
    are always permitted.
  2. pages marked with any Categories are examined to determine if they are marked with an Access Control marking as follows:
    1. when marked with a category:user: mark (containing a user name) then the page is accessible only to:
    2. when marked with a private [[:category:<group>]] (e.g. category:lesson) then a logged-in user is permitted to access the page when that user is a member of the group (e.g. the lesson group) i.e. if the group has been assigned to the user in the Special:UserRights pages.
    3. when marked with category:private the page is inaccessible to any user that is not a member of the category:private group, except for a sysop who is permitted to access any page
    4. when marked with a category:edit: the page is restricted for edit unless there is:
      • a [[:category:edit:user:<name>]] to permit that user, or
      • a [[:category:edit:<group>]] of which the user is a member i.e. the user has been assigned the group with the particular private privilege.
    5. when marked with category:public the page is visible to any user, provide none of the previous read access checks have failed to grant access.
  3. pages that are not marked with any categories are not accessible to an anonymous user.
  4. only pages marked with category:public or pages that have been white-listed may be viewed by any user, including anonymous users (those who have not logged-in).

references


Note: I am making tweaks to this code as time goes on so I just have to remember to update it here until I upload the code to my github account. I have also made security changes to the /includes/skin/Skin.php to restrict what links are presented to anonymous users. I will post those changes soon. Sorry about the empty pages. I copied and pasted this text from another wiki https://regional-training.org/rt/Category:Access_Control. I maintain 10 mediawikis so far - lol.