Extension:PageProtection/Sourcecode

From MediaWiki.org
Jump to navigation Jump to search
Extension:PageProtection PageProtection extension (Source code)
This is the source code for the MediaWiki PageProtection extension.


<?php
/**
* MediaWiki extension for page-protection. This extension allows protecting
* single areas by spanning them with <protect>-tags. The tag has the
following
* parameters:
* users: Comma-separated list of usernames who are allowed to read and edit
*        the area.
* groups: Comma-separated lsit of groupnames who are allowed to read and edit
*         the area.
* show: Defines, what to do when user has no access to the area.
*       Possible Values: warning:   Error-message with list of permissions
*                        none:      Nothing is displayed
*                        crypt:     RSA-encrpyted text is shown
*                        page       Page in "errorpage" is displayed.
* errorpage: Page to be shown when show=page is set. 
*
* The text between the opening and closing 'protect'-tags will be hidden for
* users not in the specified lists. Members of 'sysop' always have access.
*
* Example:
*   <protect users="admin1,admin2" groups"sysop">
*   Database-Settings
*   ;Server
*   : localhost
*   ...
*   </protect>
*
* Requirements:
*   Crypt_RSA http://pear.php.net/package/Crypt_RSA
*   Pear-Base http://pear.php.net/package/PEAR
* 
*
* With this extension, it is possible to mark single sections of pages as
* protected and thus leaving the other sections editable to all users.
* Access to sections containing <protect>-tags is only permitted for users
* having access to all protected areas in the specified section  / article.
*
* @author Copyright (C) 2006 Fabian Schmitt (fs@u4m.de)
* @version 1.1
*
* 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 2 of the License, or
* (at your option) any later version.
*
* 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, write to the Free Software Foundation, Inc.,
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @todo Editing sections within protected areas is possible by supplying
*       section=X to URL! So we need to check permissions of protect-tags
*       surrounding the current section.
*
* Changelog:
*  1.1
*   - PEM-file for storing PEM-data now is automatically created
*   - Bugfix
*  1.0
*   - Texts are stored RSA-encrypted in database
*   - When saving articles, the current user is automatically added
*       to the permitted users.
*   - Added parameter show=[warning, none, crypt, page] and parameter
*               errorpage
*/

define("RSA_PEM_FILE",  "private.pem");
define("PROTECT_TAG",           "protect");
define("VAR_USERS",     "{{{USERS}}}");
define("VAR_GROUPS",    "{{{GROUPS}}}");
define(MSG_NO_ACCESS,
" This area is [[Page Protection|protected]].
 You are not allowed to read or edit this area.<br/>
 Allowed users: ".VAR_USERS.
" <br/>Allowed groups: ".VAR_GROUPS);

$wgExtensionFunctions[] = "wfPageProtection";

$wgExtensionCredits['parserhook'][] = array(
    'name' => 'ProtectPage',
    'author' => 'Fabian Schmitt (http://meta.wikimedia.org/wiki/User:Sirius_gd)',
    'version' => '0.6',
    'url' => 'http://www.mediawiki.org/wiki/Extension:PageProtection'
);

/**
* Extension-function. Registers parser, hook, messages.
*/
function wfPageProtection() {
    global $wgParser;

    $msgs = array (
            'ProtectedSite' => MSG_NO_ACCESS
        );

    global $wgMessageCache;
    $wgMessageCache->addMessages($msgs);

    $wgParser->setHook( PROTECT_TAG, "protectPage" );

    global $wgHooks;
    $wgHooks['AlternateEdit'][] = 'protectedEdit';
    $wgHooks['ArticleSave'][] = 'protectSave';
}

/**
* Callback function for the hook to the protect-tag.
* @param text Text to be protected
* @param params Parameters supplied to the tag
* @param parser Global parser-object
* @return If current user is allowed to read the page, $text will be returned.
*         Otherwise,  an error-page will be returned.
*/
function protectPage( $text, $params, &$parser) {
    $parser->disableCache();

    global $wgUser;
    global $wgOut;
    
    $protect = createProtectPage();    
    $protect->initShow($params);
    
    if ($protect->hasAccess(&$wgUser)) {
        return $wgOut->parse($protect->decrypt($text));
    }
    
    return $protect->showError($params);
}

/**
* Callback function for the hook to ArticleSave. Encrypts the
* data between protect-tags and ensures the current user is in 
* list of permitted users.
*/
function protectSave(&$article, &$user, &$text, &$summary, &$minoredit, &$watchthis, &$sectionanchor) {
    $protect = createProtectPage();    
    $protect->encryptTags($text, $user->getName());
    
    return true;
}

/**
* Callback-function for hook to 'AlternateEdit'. Checks if the current user
* is allowed to edit the current article / section and if so returns the
* current editpage-object.
* @param editpage EditPage-object
* @return editpage if user is allowed to edit the section, null otherwise.
*/
function protectedEdit($editpage) {
    global $wgUser;
    
    $protect = createProtectPage();    
    $protect->initEdit($editpage);
    
    if (!$protect->pageIsProtected() ) {
        return $editpage;
    }
    
    if ($protect->hasAccess($wgUser) ) {
        $editpage->mArticle->mContent = $protect->decryptPage($editpage->mArticle->mContent);
        return true;
    } else {    
        $protect->stopEditing();
    }
    
    return false;
}

/**
* Creates a PEM-file holding public and private RSA-key.
*/
function createPemFile() {
    $keypair = new Crypt_RSA_KeyPair(256);
    $str = $keypair->toPEMString();
    file_put_contents(RSA_PEM_FILE, $str);        
}

/**
* Pseudo-constructor for a ProtectPage-object. I get errors when
* the constructor of RSA_KeyPair is called from another class-instance.
* @return NEw ProtectPage-object.
*/
function createProtectPage() {
    require_once("PEAR.php");
    require_once("Crypt/RSA.php");
    if (!file_exists(RSA_PEM_FILE)) {        
        createPemFile();
    }
    $keypair = Crypt_RSA_KeyPair::fromPEMString(file_get_contents(RSA_PEM_FILE));    
    return new ProtectPage(&$keypair);
}

/**
* Handles showing, editing and saving articles that contain <protect>-tags.
*/
class ProtectPage {

    /**
    * Private fields.
    */
    var $mKeypair = null;
    var $mEditpage = null;
    var $mContent = array();
    var $mTags = array();
    var $mParams = array();
    var $mSection = 0;
    var $allowedUsers = array();
    var $allowedGroups = array();
    var $mParsedText = "";
    
    /**
    * Constructor.
    * @param keypair Reference to Crypt_RSA_KeyPair-object with loaded
    *                   keys.   
    */
    function ProtectPage(&$keypair) {
        $this->mKeypair = $keypair;
    }
    
    /**
    * Decrypts an RSA-encrypted text.
    * @param text Encrypted text
    * @return Plaintext
    */
    function decrypt($text) {
        $rsa = new Crypt_RSA;
        return $rsa->decrypt($text, $this->mKeypair->getPrivateKey());
    }
    
    /**
    * Encrypts an text with RSA
    * @param text Plaintext
    * @return RSA-encrypted text.
    */
    function encrypt($text) {
        $rsa = new Crypt_RSA;
        return $rsa->encrypt($text, $this->mKeypair->getPublicKey());
    }

    /**
    * Parses a text for protect-tags and stores the texts in member-fields.
    * The resulting array are mContent, mTags, mParams and mSections.
    * @param text   Text to be parsed.
    */
    function parseText($text) {
        $this->mContent = array();
        $this->mTags = array();
        $this->mParams = array();
        
        $this->mParsedText = Parser::extractTagsAndParams(PROTECT_TAG, $text, &$this->mContent, &$this->mTags,
            &$this->mParams );
    }

    /**
    * Initialises showing a protected area.
    * @param params Parameters of tag.
    */
    function initShow($params) {
        $this->allowedUsers = $this->getAllowedUsers($params);
        $this->allowedGroups = $this->getAllowedGroups($params);
    }

    /**
    * Shows error-message.
    * @todo document
    */
    function showError($params) {
        if ($this->showErrorMessage($params)) {
            return $this->getErrorMessage(true);
        } else if ($this->showCrypted($params)) {
            return $text;
        } else if ($this->showErrorPage($params)) {
            return $this->getErrorPage($params);
        }
    
        return "";
    }
    
    /**
    * Removes empty fields from an array.
    * @param arr Array.
    * @return Array without fields that contain empty strings.
    */
    function removeEmpty($arr) {
        foreach ($arr as $i => $a) {
            if ($a == "") {
                unset($arr[$i]);
            }
        }
        return $arr;
    }

    /**
    * Encrypts all protected tags within a text-block and 
    * ensures a given username is listed in the list of
    * allowed users.
    * @param text Text to encrypt protect-tags in.
    * @param userName Name of user to ensure to be in permitted list
    *                   (usually current username)
    */
    function encryptTags(&$text, $userName) {
        $this->parseText($text);
        
        foreach ($this->mContent as $rand => $cnt) {
            $users = $this->removeEmpty($this->getAllowedUsers($this->mParams[$rand]));
            $groups = $this->removeEmpty($this->getAllowedGroups($this->mParams[$rand]));
            
            if (!in_array($userName,  $users) ) {
                $users[] = $userName;
            }
            
            $strUsers = "";
            if (count($users) > 0) {
                $strUsers = "users=\"".implode(",", $users)."\"";
            }
            
            $strGroups = "";
            if (count($groups) > 0) {
                $strGroups = "groups=\"".implode(",", $groups)."\"";
            }
            
            $strShow = "";
            if ($this->mParams[$rand]["show"] != "") {
                $strShow = "show=\"".$this->mParams[$rand]["show"]."\"";
            }
            
            $strPage = "";
            if ($this->mParams[$rand]["errorpage"] != "") {
                $strPage = "errorpage=\"".$this->mParams[$rand]["errorpage"]."\"";
            }
            
            $starttag = "<protect $strUsers $strGroups $strShow $strPage>";
            
            $this->mParsedText = str_replace($rand, $starttag."\n".$this->encrypt($cnt)."\n</protect>", $this->mParsedText);
        }
    
        $text =  $this->mParsedText."";
    }
    
    /**
    * Initialises editing-process by parsing the editpage and getting
    * permissions.
    * @editpage EditPage object
    */
    function initEdit($editpage) {
        $this->mEditpage  = $editpage;
        
        $mSection = $this->getSection();
    
        // Get text to be edited
        $text = $editpage->mArticle->getContent();
        
        if ($mSection > 0) {
            $text = $editpage->mArticle->getSection($text, $mSection);
        }
    
        $this->parseText($text);
        
        foreach ($this->mParams as $param) {
            $this->restrictPermissions($param);
        }
    }
    
    /**
    * Restricts the current permissions so that only users currently allowed and
    * allowed in $param are allowed.
    */
    function restrictPermissions($param) {
        $this->allowedUsers = $this->intersect($this->allowedUsers, $this->getAllowedUsers($param));
        $this->allowedGroups = $this->intersect($this->allowedGroups, $this->getAllowedGroups($param));
    }
    
    /**
    * Checks if the currently edited page contains protect-tags
    * @return true if Page is protected.
    */
    function pageIsProtected() {
        if (count($this->mTags) != 0) {
            return true;
        }
        return false;
    }
    
    /**
    * Splits the users that are allowed to acces the protected area from a
    * parameter.
    * @param params The parameters as given to the hook-function
    * @return Array with user-names
    */
    function getAllowedUsers($params) {
        return explode(",", $params["users"]);
    }
    
    /**
    * Splits the groups that are allowed to acces the protected area from a
    * parameter. If the group sysop is not supplied as parameter, it will be
    * added automatically.
    * @param params The parameters as given to the hook-function
    * @return Array with group-names including sysop.
    */
    function getAllowedGroups($params) {
        $groups = explode(",", $params["groups"]);
        // sysops always have access
        if (!in_array("sysop", $groups )) {
            $groups[] = "sysop";
        }
        return $groups;
    }
    
    /**
    * Checks if a given user has access to a protected area.
    * @param user User-object to check for.
    * @param users Array containing allowed user-names.
    * @param groups Array containing allowed group-names.
    * @return true, if user is in list of users or in one of the allowed groups,
    *         false otherwise.
    */
    function hasAccess(&$user) {
        // Check Group access
        foreach($this->allowedGroups as $group) {
            if(in_array($group, $user->mGroups)){
                return true;
            }
        }
    
        if (in_array($user->getName(), $this->allowedUsers)) {
            return true;
        }
    
        return false;
    }
   
    function createUsersList() {
        $usersString = "";
        $users = array();
        // create links to user-pages
        foreach($this->allowedUsers as $user) {
            $u = User::newFromName($user);
            $userPage = "[[".Title::makeTitle(NS_USER,
                $u)->getNsText().":".$user."|".$user."]]";
            $users[] = $userPage;
        }
        return implode(", ", $users);
    }
    
    function createGroupsList() {
        return implode(", ", $this->allowedGroups);
    }
    
    /**
    * Reads the error-message 'ProtectedSite' from the message-cache and
    replaces
    * all {{{USERS}}} by a comma-separated list of allowed users and all
    * {{{GROUPS}}} by a comma-separated lsit of allowed groups.
    * @param parseWiki If true, the text will be parsed before returning.
    * @return Error-message.
    */
    function getErrorMessage($parseWiki = true) {
        $msg = wfMsg("ProtectedSite");
        return $this->formatMessage($msg, $parseWiki);
    }
    
    function formatMessage($msg, $parseWiki = true) {
        global $wgOut;
        
        $msg = str_replace(VAR_USERS, $this->createUsersList(), $msg);
        $msg = str_replace(VAR_GROUPS, $this->createGroupsList(), $msg);
    
        if ($parseWiki) {
            return $wgOut->parse($msg);
        } else {
            return $msg;
        }
    }
    
    function showErrorMessage($params) {
        if ($params["show"] == "warning") {
            return true;
        }
        return false;
    }
    
    function showErrorPage($params) {
        if ($params["errorpage"] != "") {
            return true;
        }
        return false;
    }
    
    function getErrorPage($params) {
        $tit = Title::newFromText($params["errorpage"]);
        $art = new Article(&$tit);
        return $this->formatMessage($art->getContent());
    }
    
    function showCrypted($params) {
        if ($params["show"] == "crypt") {
            return true;
        }
        return false;
    }
    
    /**
    * Reads currently displayed or edited section from request.
    * @return Current section or 0 if no section is beeing edited.
    */
    function getSection()
    {
        global $wgRequest;
        // section when editing
        $section = $wgRequest->getText('section');
        if (!$section) {
            // for preview and finish editing, section is in wpSection
            $section = $wgRequest->getText('wpSection');
            if (!$section) {
                $section = 0;
            }
        }
        return $section - 1;
    }
    
    /**
    * Intersects two array and returns the result. If one of the given arrays
    * is empty, the other one will be returned.
    * @param a1 First array
    * @param a2 Second array
    * @return Intersection
    */
    function intersect($a1, $a2) {
        if (count($a1) == 0) {
            return $a2;
        }
        if (count($a2) == 0) {
            return $a1;
        }
        return array_intersect($a1, $a2);
    }
    
    /**
    * Cancels the editing and prints error message.
    * @param users List of allowed users
    * @param groups List of allowed groups
    */
    function stopEditing() {
        global $wgOut;
        $wgOut->setPageTitle( "Page is protected" );
        $wgOut->setRobotpolicy( 'noindex,nofollow' );
        $wgOut->setArticleRelated( false );
        $wgOut->addWikiText( $this->getErrorMessage(false) );
        $wgOut->returnToMain( false );
    }
    
    function decryptPage($text) {
        $this->parseText($text);

        foreach ($this->mContent as $rand => $cnt) {
            $this->mParsedText = str_replace($rand, $this->mTags[$rand].$this->decrypt($cnt)."</protect>", $this->mParsedText);
        }
        
        return $this->mParsedText;
    }
}

?>