Page Cloaking
This issue might possibly be addressed by ticket #178.
Note: For reasons unknown, this patch generates a segfault error when attempting to log in as a user other than the admin user. It appears to be related to the various PHP $_SESSION-related bug reports involving memory corruption.
Wiki pages that are not readable by the current user (or guest) should not have their tags displayed in links, indexes, or other lists. While some wiki purists may cringe at the idea of "information hiding," please keep in mind that page cloaking works in conjunction in ACLs, so those same purists will already have a gripe about ACLs, which means the issue has already been hashed and rehashed.
The basis of page cloaking is simple: Verify that a page is visible (readable) to the current user. If it is not, then not only should the page not be displayed, but the page tag (title) itself shouldn't appear either. This isn't meant as a security feature, but rather a feature of convenience: Users shouldn't have to be tempted by pages they do not have access to. A determined individual could simply try different page tag permutations from the URL. There's really no way around this: Even if a generic message is displayed advising the page is unavailable, attempts to edit the page would fail.
There are two approaches to this problem, both of which rely on a few extra functions in wikka.php:
// Filter out pages for which current user does not have ACL
// "read" permissions
function FilterInvisiblePages(&$pages) {
foreach($pages as $index=>$page) {
$tag = $page['tag'];
if(!$this->IsVisible($page)) {
unset($pages[$index]);
}
}
}
// Determine if a page is visible (readable) to the current user.
// May load ACLs if they haven't been loaded already.
function IsVisible($page) {
$tag = $page['tag'];
$owner = $page['owner'];
$isPublic = 0;
if(eregi("public", $owner)) $isPublic = 1;
// ACLs aren't set until after the LoadPage() call, so we
// need to check and load them if they haven't been already
if(!$this->ACLs_loaded)
$this->ACLs = $this->LoadAllACLs($tag);
if(!$isPublic && !$this->HasAccess("read", $tag)) {
return false;
}
return true;
}
// "read" permissions
function FilterInvisiblePages(&$pages) {
foreach($pages as $index=>$page) {
$tag = $page['tag'];
if(!$this->IsVisible($page)) {
unset($pages[$index]);
}
}
}
// Determine if a page is visible (readable) to the current user.
// May load ACLs if they haven't been loaded already.
function IsVisible($page) {
$tag = $page['tag'];
$owner = $page['owner'];
$isPublic = 0;
if(eregi("public", $owner)) $isPublic = 1;
// ACLs aren't set until after the LoadPage() call, so we
// need to check and load them if they haven't been already
if(!$this->ACLs_loaded)
$this->ACLs = $this->LoadAllACLs($tag);
if(!$isPublic && !$this->HasAccess("read", $tag)) {
return false;
}
return true;
}
Approach #1
Identify each action/handler that retrieves a page from one of the page loading functions in wikka.php, then pass that page to IsVisible(). Or, if multiple pages are retrieved, pass them as an array to FilterInvisiblePages().
For instance, if one desired to cloak pages on the RecentChanges page, the first line in actions/recentchanges.php:
if ($pages = $this->LoadRecentlyChanged())
would be modified to something like this:
$pages = $this->LoadRecentlyChanged();
FilterInvisiblePages($pages);
if($pages)
FilterInvisiblePages($pages);
if($pages)
The advantage here is that cloaking can be selectively applied. The disadvantage is if you want cloaking system-wide, you will have to track down each and every action/handler that loads one or more pages and call FilterInvisiblePages() or IsVisible() as appropriate.
Approach #2
For system-wide page cloaking, it is far less labor-intensive to implement all of the changes in wikka.php (as well as an optional parameter in wikka.config.php). The change to wikka.config.php consists of an additiona l parameter than enables or disables page cloaking:
"display_visible_only" => "1",
Here is a file, suitable for feeding to patch (patch -p0 -l < cloaking.patch), that make an effort to implement cloaking system-wide (tested on version 1.1.6.1):
--- wikka.php.orig Tue Apr 25 07:30:28 2006
+++ wikka.php Thu Apr 27 13:03:05 2006
@@ -348,6 +348,7 @@
// PAGES
function LoadPage($tag, $time = "", $cache = 1) {
+ $filter = $this->config['display_visible_only'];
// retrieve from cache
if (!$time && $cache) {
$page = isset($this->pageCache[$tag]) ? $this->pageCache[$tag] : null;
@@ -355,6 +356,7 @@
}
// load page
if (!isset($page)) $page = $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($tag)."' ".($time ? "and time = '".mysql_real_escape_string($time)."'" : "and latest = 'Y'")." limit 1");
+ if($filter && !$this->IsVisible($page)) return null;
// cache result
if ($page && !$time) {
$this->pageCache[$page["tag"]] = $page;
@@ -369,13 +371,33 @@
function GetCachedPage($tag) { return (isset($this->pageCache[$tag])) ? $this->pageCache[$tag] : null; }
function CachePage($page) { $this->pageCache[$page["tag"]] = $page; }
function SetPage($page) { $this->page = $page; if ($this->page["tag"]) $this->tag = $this->page["tag"]; }
- function LoadPageById($id) { return $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where id = '".mysql_real_escape_string($id)."' limit 1"); }
- function LoadRevisions($page) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($page)."' order by time desc"); }
- function LoadPagesLinkingTo($tag) { return $this->LoadAll("select from_tag as tag from ".$this->config["table_prefix"]."links where to_tag = '".mysql_real_escape_string($tag)."' order by tag"); }
+ function LoadPageById($id) {
+ $filter = $this->config['display_visible_only'];
+ $page = $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where id = '".mysql_real_escape_string($id)."' limit 1");
+ if(!$filter || $this->IsVisible($page)) return $page;
+ return null;
+ }
+ function LoadRevisions($page) {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($page)."' order by time desc");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
+ function LoadPagesLinkingTo($tag) {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select from_tag as tag from ".$this->config["table_prefix"]."links where to_tag = '".mysql_real_escape_string($tag)."' order by tag");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function LoadRecentlyChanged()
{
+ $filter = $this->config['display_visible_only'];
if ($pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by time desc"))
{
+ if($filter)
+ $this->FilterInvisiblePages($pages);
foreach ($pages as $page)
{
$this->CachePage($page);
@@ -383,7 +405,13 @@
return $pages;
}
}
- function LoadWantedPages() { return $this->LoadAll("select distinct ".$this->config["table_prefix"]."links.to_tag as tag,count(".$this->config["table_prefix"]."links.from_tag) as count from ".$this->config["table_prefix"]."links left join ".$this->config["table_prefix"]."pages on ".$this->config["table_prefix"]."links.to_tag = ".$this->config["table_prefix"]."pages.tag where ".$this->config["table_prefix"]."pages.tag is NULL group by tag order by count desc"); }
+ function LoadWantedPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select distinct ".$this->config["table_prefix"]."links.to_tag as tag,count(".$this->config["table_prefix"]."links.from_tag) as count from ".$this->config["table_prefix"]."links left join ".$this->config["table_prefix"]."pages on ".$this->config["table_prefix"]."links.to_tag = ".$this->config["table_prefix"]."pages.tag where ".$this->config["table_prefix"]."pages.tag is NULL group by tag order by count desc");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function IsWantedPage($tag)
{
if ($pages = $this->LoadWantedPages())
@@ -395,9 +423,52 @@
}
return false;
}
- function LoadOrphanedPages() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages left join ".$this->config["table_prefix"]."links on ".$this->config["table_prefix"]."pages.tag = ".$this->config["table_prefix"]."links.to_tag where ".$this->config["table_prefix"]."links.to_tag is NULL order by tag"); }
+ function LoadOrphanedPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages left join ".$this->config["table_prefix"]."links on ".$this->config["table_prefix"]."pages.tag = ".$this->config["table_prefix"]."links.to_tag where ".$this->config["table_prefix"]."links.to_tag is NULL order by tag");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function LoadPageTitles() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages order by tag"); }
- function LoadAllPages() { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by tag"); }
+ function LoadAllPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by tag");
+ if($filter);
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
+
+ // Filter out pages for which current user does not have ACL
+ // "read" permissions
+ function FilterInvisiblePages(&$pages) {
+ foreach($pages as $index=>$page) {
+ $tag = $page['tag'];
+ if(!$this->IsVisible($page)) {
+ unset($pages[$index]);
+ }
+ }
+ }
+
+ // Determine if a page is visible (readable) to the current user.
+ // May load ACLs if they haven't been loaded already.
+ function IsVisible($page) {
+ $tag = $page['tag'];
+ $owner = $page['owner'];
+ $isPublic = 0;
+ if(eregi("public", $owner)) $isPublic = 1;
+
+ // ACLs aren't set until after the LoadPage() call, so we
+ // need to check and load them if they haven't been already
+ if(!$this->ACLs_loaded)
+ $this->ACLs = $this->LoadAllACLs($tag);
+
+ if(!$isPublic && !$this->HasAccess("read", $tag)) {
+ return false;
+ }
+ return true;
+ }
+
// function FullTextSearch($phrase) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' and match(tag, body) against('".mysql_real_escape_string($phrase)."')"); }
function FullTextSearch($phrase)
{
@@ -884,6 +955,7 @@
}
function LoadAllACLs($tag, $useDefaults = 1)
{
+ $this->ACLs_loaded = 1;
if ((!$acl = $this->LoadSingle("SELECT * FROM ".$this->config["table_prefix"]."acls WHERE page_tag = '".mysql_real_escape_string($tag)."' LIMIT 1")) && $useDefaults)
{
$acl = array("page_tag" => $tag, "read_acl" => $this->GetConfigValue("default_read_acl"), "write_acl" => $this->GetConfigValue("default_write_acl"), "comment_acl" => $this->GetConfigValue("default_comment_acl"));
@@ -922,7 +994,7 @@
}
else
{
- $tag_ACLs = $this->LoadAllACLs($tag);
+ $tag_ACLs = $this->LoadAllACLs($tag);
$acl = $tag_ACLs[$privilege."_acl"];
}
@@ -993,7 +1065,8 @@
$this->SetPage($this->LoadPage($tag, (isset($_REQUEST["time"]) ? $_REQUEST["time"] :'')));
$this->LogReferrer();
- $this->ACLs = $this->LoadAllACLs($this->tag);
+ $this->ACLs = $this->LoadAllACLs($this->tag);
+
$this->ReadInterWikiConfig();
if(!($this->GetMicroTime()%3)) $this->Maintenance();
@@ -1205,4 +1278,4 @@
ob_end_clean();
echo $page_output;
+++ wikka.php Thu Apr 27 13:03:05 2006
@@ -348,6 +348,7 @@
// PAGES
function LoadPage($tag, $time = "", $cache = 1) {
+ $filter = $this->config['display_visible_only'];
// retrieve from cache
if (!$time && $cache) {
$page = isset($this->pageCache[$tag]) ? $this->pageCache[$tag] : null;
@@ -355,6 +356,7 @@
}
// load page
if (!isset($page)) $page = $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($tag)."' ".($time ? "and time = '".mysql_real_escape_string($time)."'" : "and latest = 'Y'")." limit 1");
+ if($filter && !$this->IsVisible($page)) return null;
// cache result
if ($page && !$time) {
$this->pageCache[$page["tag"]] = $page;
@@ -369,13 +371,33 @@
function GetCachedPage($tag) { return (isset($this->pageCache[$tag])) ? $this->pageCache[$tag] : null; }
function CachePage($page) { $this->pageCache[$page["tag"]] = $page; }
function SetPage($page) { $this->page = $page; if ($this->page["tag"]) $this->tag = $this->page["tag"]; }
- function LoadPageById($id) { return $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where id = '".mysql_real_escape_string($id)."' limit 1"); }
- function LoadRevisions($page) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($page)."' order by time desc"); }
- function LoadPagesLinkingTo($tag) { return $this->LoadAll("select from_tag as tag from ".$this->config["table_prefix"]."links where to_tag = '".mysql_real_escape_string($tag)."' order by tag"); }
+ function LoadPageById($id) {
+ $filter = $this->config['display_visible_only'];
+ $page = $this->LoadSingle("select * from ".$this->config["table_prefix"]."pages where id = '".mysql_real_escape_string($id)."' limit 1");
+ if(!$filter || $this->IsVisible($page)) return $page;
+ return null;
+ }
+ function LoadRevisions($page) {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where tag = '".mysql_real_escape_string($page)."' order by time desc");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
+ function LoadPagesLinkingTo($tag) {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select from_tag as tag from ".$this->config["table_prefix"]."links where to_tag = '".mysql_real_escape_string($tag)."' order by tag");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function LoadRecentlyChanged()
{
+ $filter = $this->config['display_visible_only'];
if ($pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by time desc"))
{
+ if($filter)
+ $this->FilterInvisiblePages($pages);
foreach ($pages as $page)
{
$this->CachePage($page);
@@ -383,7 +405,13 @@
return $pages;
}
}
- function LoadWantedPages() { return $this->LoadAll("select distinct ".$this->config["table_prefix"]."links.to_tag as tag,count(".$this->config["table_prefix"]."links.from_tag) as count from ".$this->config["table_prefix"]."links left join ".$this->config["table_prefix"]."pages on ".$this->config["table_prefix"]."links.to_tag = ".$this->config["table_prefix"]."pages.tag where ".$this->config["table_prefix"]."pages.tag is NULL group by tag order by count desc"); }
+ function LoadWantedPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select distinct ".$this->config["table_prefix"]."links.to_tag as tag,count(".$this->config["table_prefix"]."links.from_tag) as count from ".$this->config["table_prefix"]."links left join ".$this->config["table_prefix"]."pages on ".$this->config["table_prefix"]."links.to_tag = ".$this->config["table_prefix"]."pages.tag where ".$this->config["table_prefix"]."pages.tag is NULL group by tag order by count desc");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function IsWantedPage($tag)
{
if ($pages = $this->LoadWantedPages())
@@ -395,9 +423,52 @@
}
return false;
}
- function LoadOrphanedPages() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages left join ".$this->config["table_prefix"]."links on ".$this->config["table_prefix"]."pages.tag = ".$this->config["table_prefix"]."links.to_tag where ".$this->config["table_prefix"]."links.to_tag is NULL order by tag"); }
+ function LoadOrphanedPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages left join ".$this->config["table_prefix"]."links on ".$this->config["table_prefix"]."pages.tag = ".$this->config["table_prefix"]."links.to_tag where ".$this->config["table_prefix"]."links.to_tag is NULL order by tag");
+ if($filter)
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
function LoadPageTitles() { return $this->LoadAll("select distinct tag from ".$this->config["table_prefix"]."pages order by tag"); }
- function LoadAllPages() { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by tag"); }
+ function LoadAllPages() {
+ $filter = $this->config['display_visible_only'];
+ $pages = $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' order by tag");
+ if($filter);
+ $this->FilterInvisiblePages($pages);
+ return $pages;
+ }
+
+ // Filter out pages for which current user does not have ACL
+ // "read" permissions
+ function FilterInvisiblePages(&$pages) {
+ foreach($pages as $index=>$page) {
+ $tag = $page['tag'];
+ if(!$this->IsVisible($page)) {
+ unset($pages[$index]);
+ }
+ }
+ }
+
+ // Determine if a page is visible (readable) to the current user.
+ // May load ACLs if they haven't been loaded already.
+ function IsVisible($page) {
+ $tag = $page['tag'];
+ $owner = $page['owner'];
+ $isPublic = 0;
+ if(eregi("public", $owner)) $isPublic = 1;
+
+ // ACLs aren't set until after the LoadPage() call, so we
+ // need to check and load them if they haven't been already
+ if(!$this->ACLs_loaded)
+ $this->ACLs = $this->LoadAllACLs($tag);
+
+ if(!$isPublic && !$this->HasAccess("read", $tag)) {
+ return false;
+ }
+ return true;
+ }
+
// function FullTextSearch($phrase) { return $this->LoadAll("select * from ".$this->config["table_prefix"]."pages where latest = 'Y' and match(tag, body) against('".mysql_real_escape_string($phrase)."')"); }
function FullTextSearch($phrase)
{
@@ -884,6 +955,7 @@
}
function LoadAllACLs($tag, $useDefaults = 1)
{
+ $this->ACLs_loaded = 1;
if ((!$acl = $this->LoadSingle("SELECT * FROM ".$this->config["table_prefix"]."acls WHERE page_tag = '".mysql_real_escape_string($tag)."' LIMIT 1")) && $useDefaults)
{
$acl = array("page_tag" => $tag, "read_acl" => $this->GetConfigValue("default_read_acl"), "write_acl" => $this->GetConfigValue("default_write_acl"), "comment_acl" => $this->GetConfigValue("default_comment_acl"));
@@ -922,7 +994,7 @@
}
else
{
- $tag_ACLs = $this->LoadAllACLs($tag);
+ $tag_ACLs = $this->LoadAllACLs($tag);
$acl = $tag_ACLs[$privilege."_acl"];
}
@@ -993,7 +1065,8 @@
$this->SetPage($this->LoadPage($tag, (isset($_REQUEST["time"]) ? $_REQUEST["time"] :'')));
$this->LogReferrer();
- $this->ACLs = $this->LoadAllACLs($this->tag);
+ $this->ACLs = $this->LoadAllACLs($this->tag);
+
$this->ReadInterWikiConfig();
if(!($this->GetMicroTime()%3)) $this->Maintenance();
@@ -1205,4 +1278,4 @@
ob_end_clean();
echo $page_output;