Xmlwiki.php

From Organic Design wiki
Revision as of 09:19, 23 July 2005 by Nad (talk | contribs)

<?php

  1. xmlWiki - MediaWiki XML Hack
  2. Nad - 2005-05-18
  1. INIT
  2. SECURITY
  3. INPUT HOOK
  4. OUTPUT HOOK
  5. PARSER HOOK
  6. SKIN HOOK
  7. ARTICLE FUNCTIONS
  8. UTILITY FUNCTIONS
  1. ---------------------------------------------------------------------------------------------------------------------- #
  1. TODO:
  2. - Must deal with page-move: security and also pseudo-namespace moving (maybe use proper namespace?)
  3. - setProperty needs to create element if not there so <output> not needed in swf properties
  4. - XPath bug with create element (is it the same setProperty bug?)
  5. - also language-override in ming-swf.php can then be simpler too
  6. - move geshi/wiki out to a default language.php (which uses the <language> element)
  1. - TransformArticle:
  2. - VALIDATE: validate article-properties against xmlwiki:properties.dtd (does PHP4 do this?)
  3. - TRAP: fatal errors in php-transform execution
  4. - Undomificate: validation, xslt
  5. - Check (later validate) doctype of user:sys (change users pages on site first!)
  6. - Check (later validate) doctype of user/properties on save (still save though)
  1. BUGS:
  2. - Rob is admin and can't write to Nad pages
  3. - Pages can be moved regardless of perms!!!
  4. - login error goes to old-wiki
  5. - tabReplace doesn't work for /^\t+.+\t.+$/
  1. LATER:
  2. - allow XIncludes to build large docs from others
  3. - allow docBook and DSSSL content
  4. - Maybe only use XML transforms and use PHP via PI's
  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. INIT
  1. Exit if not included from index.php

defined('MEDIAWIKI') or die('xmlwiki.php must be included from MediaWiki\'s index.php!');

  1. Allows Geshi to be installed by PARSER-HOOK if not installed the usual way already
  2. - still needs to be present in ./extensions though

if (!@is_object($wgGeshiSyntaxSettings)) die('xmlWiki requires the Geshi syntax-highlighting extension');

  1. System globals

$xwDebug = false; $xwMessages = array(); $xwArticleCache = array(); $xwStyleSheets = array(); $xwTemplate = null; $xwParserHookCalled = false; $xwSkinHookCalled = false; $xwMsgToken = ;

  1. User globals

$xwUserName = ucwords($wgUser->mName); $xwUserSYS = null; $xwUserGroups = array($xwUserName, 'anyone'); $xwEdit = isset($_REQUEST['action']) && ($_REQUEST['action'] == 'edit'); $xwView = (!isset($_REQUEST['action']) || (isset($_REQUEST['action']) && ($_REQUEST['action'] == 'view'))); $xwRaw = isset($_REQUEST['action']) && ($_REQUEST['action'] == 'raw'); $xwPreview = isset($_REQUEST['wpPreview']); $xwSave = isset($_REQUEST['wpSave']);

  1. Article globals

$xwArticleTitle = $wgTitle->getPrefixedURL(); $xwArticle = $xwPreview ? $wgRequest->gettext('wpTextbox1') : $wgArticle->getContent(false); $xwArticleProperties = null; $xwIsProperties = preg_match('/^xml:.+$/i', $xwArticleTitle); $xwIsSystem = preg_match('/^sys:.+$/i', $xwArticleTitle); $xwIsUser = preg_match('/^user:.+$/i', $xwArticleTitle); $xwIsAdmin = false; $xwArticleReadableBy = array('anyone'); $xwArticleWritableBy = array('anyone'); $xwLanguage = ;

  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. SECURITY
  1. Get user-system-article and extract users' groups from it if any

if ($xwUserSYS = xwArticleContent("sys:$xwUserName")) xwDomificateArticle($xwUserSYS, "sys:$xwUserName"); $xwUserGroups = array_merge($xwUserGroups, xwGetListByTagname($xwUserSYS, 'groups'));

  1. Read user-prefs

$xwDebug = count($results = xwXPathQuery($xwUserSYS, '//debug')) ? $results[0]->get_content() : false;

  1. Get article-meta-file and extract article-perms

if ($xwIsSystem) $xwArticleReadableBy = $xwArticleWritableBy = array('admin'); else { # Get xml:article (self if already xml:*) if ($xwArticleProperties = $xwIsProperties ? $xwArticle : xwArticleContent($id = 'xml:'.ucwords($xwArticleTitle))) xwDomificateArticle($xwArticleProperties, $xwIsProperties ? $xwArticleTitle : $id); $xwArticleReadableBy = xwGetListByTagname($xwArticleProperties, 'read') + array('anyone'); $xwArticleWritableBy = xwGetListByTagname($xwArticleProperties, 'write') + array('anyone'); if ($xwIsProperties) $xwArticleProperties = false; }

  1. Merge this articles properties with default-properties

if (is_object($xwDefaultProperties = xwDomificateArticle(xwArticleContent('default-properties.xml'),'defaults'))) $xwArticleProperties = xwMergeDOM($xwDefaultProperties, $xwArticleProperties);

  1. Get language from properties, or articleType()

$xwLanguage = count($results = xwXPathQuery($xwArticleProperties, '/properties/language')) ? strtolower($results[0]->get_content()) : xwArticleType($xwArticleTitle, $xwArticle);

  1. Set perms for this request

if ($xwDebug) xwMessage('PERMISSIONS:','green'); if (in_array('admin', $xwUserGroups)) $xwIsAdmin = $xwReadable = $xwWritable = true; else { $xwReadable = 0 < count(array_intersect($xwArticleReadableBy, $xwUserGroups)); $xwWritable = 0 < count(array_intersect($xwArticleWritableBy, $xwUserGroups)); } if ($xwDebug) { xwMessage('Groups: '.join(', ', $xwUserGroups)); xwMessage('Readable ('.($xwReadable?'yes':'no').'): '.join(', ', $xwArticleReadableBy)); xwMessage('Writable ('.($xwWritable?'yes':'no').'): '.join(', ', $xwArticleWritableBy)); }

  1. Divert to access-denied article if not readable

if (!$xwReadable) { $action = 'view'; $xwSave = $xwEdit = false; }


  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. INPUT HOOK
  1. Parse and process input from forms

function xwInputHook() {

global $xwArticle, $xwArticleProperties, $xwArticleTitle, $xwWritable, $_REQUEST; global $xwDebug, $xwSave, $xwEdit, $action, $xwUserName, $wgArticle, $xwIsProperties, $xwIsSystem; if ($xwDebug) xwMessage('INPUT-HOOK:','green');

# If not writable, change action to view if (!$xwWritable && ($action != 'view')) { xwMessage('Sorry, article not writable, action changed to "view".', 'red'); $action = 'view'; $xwSave = $xwEdit = false; }

# Scan POST and apply any XPath inputs foreach ($_REQUEST as $query => $value) xwSetProperty($xwArticleProperties, $query, $value);

# if it's a pseudo-namespace... if ($xwIsProperties || $xwIsSystem) { # wpSave should be valid XML to be saved if ($xwSave) { xwDomificateArticle($wpSave, 'POST-DATA'); if (!is_object($wpSave)) { xwMessage('A meta-article must be valid XML! changing action to "edit"', 'red'); $action='edit'; } } }

}

  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. OUTPUT HOOK
  1. Post-process and output article

function xwOutputHook() {

global $wgUser, $wgOut, $xwParserHookCalled, $xwSkinHookCalled, $xwDebug; global $xwArticle, $xwArticleProperties, $xwArticleTitle, $xwUserName; global $xwView, $xwPreview, $xwSave, $xwEdit, $xwRaw, $xwIsProperties, $xwIsSystem; if ($xwDebug) xwMessage('OUTPUT-HOOK:','green');

# Activate the skin-hook and generate wiki's output $wgUser->setOption('skin', 'xwskin'); $wgOut->output();

# If saving, execute the transforms in the onChange list if ($xwSave) { foreach (xwGetListByTagname($xwArticleProperties, $tListName = 'onChange') as $tName) xwApplyTransform($tName, $xwArticle, $xwArticleTitle, $xwArticleProperties, $tListName); }

# If editing an empty sys:article or xml:article, add default content if ($xwEdit && preg_match("/^(.*<textarea .+?>)\\s*(<\\/textarea>.*)$/s", $xwArticle, $m)) { if ($xwIsSystem) { $xwArticle = $m[1]."<?xml version=\"1.0\" standalone=\"yes\"?>\n"; $xwArticle .= "<!DOCTYPE xmlwiki:user SYSTEM \"xmlwiki-user.dtd\">\n"; $xwArticle .= "<user>\n\t<groups></groups>\n</user>\n".$m[2]; } elseif ($xwIsProperties) { $xwArticle = $m[1]."<?xml version=\"1.0\" standalone=\"yes\"?>\n"; $xwArticle .= "<!DOCTYPE xmlwiki:properties SYSTEM \"xmlwiki-properties.dtd\">\n"; $xwArticle .= "<properties>\n\t<read>anyone</read>\n\t<write>$xwUserName</write>\n"; $xwArticle .= "\t\n\t<view></view>\n\t<onChange></onChange>\n</properties>\n".$m[2]; } }

# Insert messages into final html and output if ($xwDebug) { if (!$xwParserHookCalled) xwMessage('PARSER-HOOK was not called','green'); if (!$xwSkinHookCalled) xwMessage('SKIN-HOOK was not called','green'); if (is_object($xwArticleProperties)) xwMessage($xwArticleProperties->dump_mem(true),'purple'); } xwAddMessages(); print $xwArticle; }

  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. PARSER HOOK

function xwParserHook(&$text) {

global $xwArticleTitle, $xwArticle, $xwArticleProperties; global $xwRaw, $xwParserHookCalled, $xwDebug, $xwReadable, $xwLanguage; if ($xwDebug) xwMessage('PARSER-HOOK:','green'); $xwParserHookCalled = true;

# If this text-fragment is our article, apply transforms if (strncmp($text, $xwArticle, 100) == 0 or strncmp($text, xwArticleContent($xwArticleTitle), 100) == 0) { if ($xwDebug) xwMessage("Matching text-fragment intercepted."); if (!$xwReadable) { xwMessage('Sorry, article not readable!','red'); return $text = false; } # Apply data-transforms # - domificate first if xml # - undomificate after if still an object if (preg_match("/^<\\?xml/i", $text)) xwDomificateArticle($text, 'ParserHook/text'); if (is_object($xwArticleProperties)) { $docType = $xwArticleProperties->doctype(); $root = $xwArticleProperties->document_element(); # If xmlwiki:properties doctype, apply data-transforms if (ereg('^xmlwiki:properties', $docType->name)) { if ($xwDebug) xwMessage('Applying "data" transforms.'); while (count($tNodes = $root->get_elements_by_tagname('data'))) { $tName = $tNodes[0]->get_content(); xwApplyTransform($tName, $text, $xwArticleTitle, $xwArticleProperties, 'data'); $tNodes[0]->unlink_node(); } } else xwMessage('Could not transform: article-properties must be xmlwiki:properties doctype!','red'); } if (is_object($text)) xwUndomificateArticle($text, $xwArticleTitle); } elseif ($xwDebug) xwMessage('Parsing other content');

if ($xwDebug) xwMessage('Content language determined as '.($xwLanguage ? strtoupper($xwLanguage) : 'WIKI')); # Return this back to wiki-parser so it can do wiki-markup if it wasn't one of ours return $xwLanguage == ; }


  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. SKIN HOOK
  1. If attempted xml encountered, try to domificate and transform
  2. - Raw requests don't get here

function xwSkinHook(&$tmpl) {

global $xwArticle, $xwArticleTitle, $xwArticleProperties; global $xwTemplate, $xwDebug, $xwSkinHookCalled, $xwReadable, $xwRaw;

# Extract article from existing template structure if ($xwDebug) xwMessage('SKIN-HOOK:','green'); $xwSkinHookCalled = true; $xwTemplate = $tmpl; if ($xwReadable) $xwArticle = $xwTemplate->data['bodytext'];

# Apply view-transforms # - "default-skin.php" builds XML output structure # - "default-skin.xslt" transforms XML to HTML with table-layout # - "default-skin.css" transforms HTML with design if (is_object($xwArticleProperties)) { $docType = $xwArticleProperties->doctype(); $root = $xwArticleProperties->document_element(); # If xmlwiki:properties doctype, apply view-transforms if (ereg('^xmlwiki:properties', $docType->name)) { if ($xwDebug) xwMessage('Applying "view" transforms.'); while (count($tNodes = $root->get_elements_by_tagname('view'))) { $tName = $tNodes[0]->get_content(); xwApplyTransform($tName, $xwArticle, $xwArticleTitle, $xwArticleProperties, 'view'); $tNodes[0]->unlink_node(); } } else xwMessage('Could not transform: article-properties must be xmlwiki:properties doctype!','red'); } }

  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. ARTICLE FUNCTIONS
  1. Retreive wiki-article as raw text

function xwArticleContent($articleTitle) { # Return with results if already cached global $xwArticleCache, $xwDebug; if ($xwDebug) xwMessage("xwArticleContent(\"$articleTitle\")",'blue'); if (isset($xwArticleCache[$articleTitle])) return $xwArticleCache[$articleTitle]; # Get wiki article content (or use global if no article passed) # - using getContentWithoutUsingSoManyDamnGlobals() because getContent() fucks up nested-article-reads if ($article = new Article(Title::newFromText($articleTitle))) $article = $article->getContentWithoutUsingSoManyDamnGlobals(); if ($article == ) { if ($xwDebug) xwMessage("Article \"$articleTitle\" not found."); return false; } elseif ($article == '(There is currently no text in this page)') { if ($xwDebug) xwMessage("Article \"$articleTitle\" not found."); return false; } if ($articleTitle) $xwArticleCache[$articleTitle] = $article; return $article; }

  1. Decide kind of article from content and title

function xwArticleType($title, $article) { if (preg_match("/^<\\?xml.+?\\?>\\s*<xsl:stylesheet/i", $article)) return 'xslt'; if (eregi('^(sys)|(xml):.+$', $title) || ereg('^<\\?xml', $article)) return 'xml'; if (eregi('\\.php$', $title)) return 'php'; if (eregi('\\.css$', $title)) return 'css'; if (eregi('\\.as$', $title)) return 'actionscript'; if (eregi('\\.py$', $title)) return 'python'; if (eregi('\\.java$', $title)) return 'java'; if (eregi('\\.((cpp)|(h))$', $title)) return 'cpp'; if (eregi('\\.js$', $title)) return 'javascript'; if (eregi('\\.css$', $title)) return 'css'; if (eregi('^<\\?html', $article)) return 'html4strict'; if (ereg('^#![/a-zA-Z0-9]+\\/perl', $article)) return 'perl'; if (ereg('^#![/a-zA-Z0-9]+sh', $article)) return 'bash'; if (eregi('\\.dtd$', $title)) return 'xml'; }

  1. Convert passed article to a DOM object
  2. - Article is unchanged if not valid XML

function xwDomificateArticle(&$article, $id = ) { global $xwDebug; if ($xwDebug) xwMessage("xwDomificateArticle(\"$id\")",'blue'); ob_start(); if ($dom = domxml_open_mem($article)) $article = $dom; else { # Could not convert, extract error messages from output if ($xwDebug) xwMessage("Failed :-(", 'red'); xwExtractMessages(); } ob_end_clean(); return $article; }

  1. Reduce article to a string if it's a DOM object
  2. - ie if all xslt's had xml-output-method

function xwUndomificateArticle(&$article, $id = ) { if (!is_object($article)) return false; global $xwDebug; if ($xwDebug) xwMessage("xwUndomificate(\"$id\")",'blue'); # TODO: # Validate, report errors # xwApplyXSLT($article, $xslt) if xml referrs one # Convert DOM back to XML if still an object if (is_object($article)) $article = $article->dump_mem(true); return true; }

  1. Apply a transform

function xwApplyTransform($transform, &$article, $title, &$properties, $listName) { global $xwDebug; if ($transform) { if ($tText = xwArticleContent($transform)) { $tType = xwArticleType($transform, $tText); if ($tType == 'xslt') xwApplyXSLT($article, $tText, $transform); elseif ($tType == 'php') xwApplyPHP($article, $tText, $transform, $title, $properties, $listName); elseif ($tType == 'css') xwApplyCSS($transform); elseif ($tType == 'xml') xwMergeDOM($properties, xwDomificateArticle($tText, $transform)); } else xwMessage("Unknown transform \"$transform\" found in \"$listName\" list!", 'red'); } }

  1. Apply PHP-transform if article perms are only writable by admin or dev

function xwApplyPHP(&$article, &$code, $tName, $title, &$properties, $listName) { # Get transform perms if ($tProperties = xwArticleContent($id = 'xml:'.ucwords($tName))) xwDomificateArticle($tProperties, $id); if (!count(preg_grep('/^(admin)|(dev)$/i', xwGetListByTagname($tProperties, 'write')))) { xwMessage('PHP-Transform will not execute unless it is writable by admin or dev only.','red'); return; } # Permissions ok, execute the transform code and trap output global $_SERVER; $path = '/properties/'.preg_replace("/\\.\\w+$/",,$tName); $script = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']; ob_start(); eval("?>$code<?"); xwExtractMessages(); ob_end_clean(); }

  1. Append CSS stylesheet link to output

function xwApplyCSS($cssName) { global $xwDebug, $xwStyleSheets; if ($xwDebug) xwMessage("xwApplyCSS($cssName)",'blue'); $xwStyleSheets[] = $cssName; }

  1. Apply XSLT to article-dom
  2. - if output-method is html, article will get stringified

function xwApplyXSLT(&$article, &$xslt, $id = ) { global $xwDebug; if ($xwDebug) xwMessage("xwApplyXSLT(DOM,$id)",'blue'); ob_start(); if ($tObject = domxml_xslt_stylesheet($xslt)) { $tResult = $tObject->process($article); # If output-method is html, use XSLT's dump_mem to create an html-string if (preg_match('/<xsl:output +?method *= *"html"/i', $xslt)) $tResult = $tObject->result_dump_mem($tResult); } xwExtractMessages(); ob_end_clean(); if (isset($tResult)) $article = $tResult; else return false; return true; }

  1. Return result set of an XPath query on passed DOM-object

function xwXPathQuery(&$dom, $query) { $result = array(); if (is_object($dom)) { global $xwDebug; if ($xwDebug) xwMessage("xwXPathQuery(\"$query\")",'blue'); ob_start(); $context = $dom->xpath_new_context(); if ($xpath = $context->xpath_eval($query)) $result = @$xpath->nodeset; elseif ($xwDebug) xwMessage("Query \"$query\" failed :-(", 'red'); xwExtractMessages(); ob_end_clean(); } return $result; }

  1. - Syntax: xpath:XPathQuery:[[@]node][+] = value
  2. - if "node" exists, then a new element (or att if "@node") is created for the value
  3. - if "+" exists, content is appended, else replaced

function xwSetProperty(&$properties, $query, $value) { if (!is_object($properties) or !preg_match("/^xpath:(.+?):(@?)(\\w*?)(\\+?)$/", $query, $match)) return false; list(, $xpath, $att, $node, $append) = $match; if (!is_array($results = xwXPathQuery($properties, $xpath))) $results = array(); foreach ($results as $result) { if ($node) { # create new element/attribute in result-node if ($att) $node = $properties->create_attribute($node, $value); else { $node = $properties->create_element($node); $node->set_content($value); } $result->append_child($node); } else { # node not set, replace or append current-result-value if ($append) $result->set_content($value); else { $newnode = $properties->create_element($result->tagname); $newnode->set_content($value); $result->replace_node($newnode); } } } return true; }


  1. ---------------------------------------------------------------------------------------------------------------------- #
  2. UTILITY FUNCTIONS
  1. Return array of comma-separated-items in element content
  2. - if more than one element all are split and appended into the list

function xwGetListByTagname(&$xml, $tag) { $list = array(); if (is_object($xml)) { $root = $xml->document_element(); foreach ($root->get_elements_by_tagname($tag) as $element) if ($csv = $element->get_content()) $list = array_merge($list, split(',', $csv)); } return $list; }

  1. Merge the two passed DOM objects
  2. - appends, does not overwrite

function xwMergeDOM($dom1, $dom2) { global $xwDebug; if ($xwDebug) xwMessage('xwMergeDOM()','blue'); # Return if first DOM not a valid properties article if (!is_object($dom1)) return false; $docType = $dom1->doctype(); if (!ereg('^xmlwiki:properties', $docType->name)) { if ($xwDebug) xwMessage('xwMergeDOM: First parameter is not an xmlwiki:properties object!','red'); return false; } # Return DOM1 if DOM2 not valid properties if (!is_object($dom2)) return $dom1; $docType = $dom2->doctype(); if (!ereg('^xmlwiki:properties', $docType->name)) return $dom1; # Both are ok, perform the merge if ($xwDebug) xwMessage('xwMergeDOM()','blue'); $root1 = $dom1->document_element(); $root2 = $dom2->document_element(); foreach ($root2->child_nodes() as $node) { $root1->append_child($node->clone_node(true)); } return $dom1; }

  1. Append messages to content

function xwAddMessages() { global $xwArticle, $xwMessages, $xwMsgToken; if (count($xwMessages)) {

$msg = '

There are Messages

'; $msg .= '

  • '.join("
  • ", $xwMessages).'

';

$xwArticle = ereg_replace($xwMsgToken, $msg, $xwArticle); } }

  1. Extract error-messages from captured output and put in proper message-queue

function xwExtractMessages() { #if (preg_matcheval()'d code on line 2 $err = preg_replace("/<.+?>/", "", ob_get_contents()); #$err = preg_replace('/Warning.+?\\(\\): /', , $err); foreach (split("\n", $err) as $msg) if (trim($msg)) { if (ereg("eval\\(\\)'d code on line ([0-9]+)", $msg, $m)) $line = " (Line $m[1])"; $msg = preg_replace('/ in .+? on line.+?[0-9]+/', , $msg); if (isset($line)) $msg .= $line; xwMessage($msg, 'red'); } }

  1. Add message to queue

function xwMessage($msg, $col = '#000080') { global $xwMessages; $msg = htmlentities($msg); return $xwMessages[] = "$msg"; }

  1. Print list of passed object's methods and properties

function xwCheckoutObject($obj) { if (is_object($obj)) { print strtoupper("
$obj properties:
"); foreach (get_object_vars($obj) as $k=>$v) print htmlentities("$k => $v")."
"; print strtoupper("
$obj methods:
"); foreach (get_class_methods($obj) as $k=>$v) print "$v
"; } if (is_array($obj)) { print strtoupper("
Array content:
"); foreach ($obj as $k=>$v) print "$k => $v
"; } die; }

?>