<?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;
}
}
?>