Personal tools


Extension:Workflow.php

From OrganicDesign Wiki

Jump to: navigation, search
This code exhibits voodoo programming techniques. The most common of these is extending an instance's class at runtime after it has been instantiated, a technique that can be used to provide additional hooks into existing code without requiring modification of code-base files. For a list of all our scripts which exhibit voodoo, see Category:Code that uses voodoo.
<?php
# Extension:Workflow
# - Licenced under LGPL (http://www.gnu.org/copyleft/lesser.html)
# - Author: [http://www.organicdesign.co.nz/nad User:Nad]
# - Started: 2007-10-06
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
define('WORKFLOW_VERSION','0.0.13, 2008-08-28');
 
$wgWorkflowUpdateDelay = 1000; # Delay in milliseconds after clicking state before Ajax update is made
 
$wgExtensionCredits['parserhook'][] = $wgExtensionCredits['specialpage'][] = array(
	'name'        => 'Workflow',
	'author'      => '[http://www.organicdesign.co.nz/nad User:Nad]',
	'description' => 'Adds the ability for articles to be part of workflow sequences and easily moved
dynamically between phases in the sequence using AJAX.',
	'url'         => 'http://www.mediawiki.org/wiki/Extension:Workflow',
	'version'     => WORKFLOW_VERSION
	);
 
# Define a new specialpage for displaying a list of workflows
require_once "$IP/includes/SpecialPage.php";
class SpecialWorkflow extends SpecialPage {
 
	function SpecialWorkflow() {
		SpecialPage::SpecialPage(wfMsg('workflow'), '', true, false, false, false);
	}
 
	# Render list
	function execute($param) {
		global $wgParser, $wgOut;
		$wgParser->disableCache();
		$this->setHeaders();
		$wgOut->addWikiText('Not done yet... discussion at [[Extension talk:Workflow.php]]', true);
	}
}
 
# Define main workflow class containing all the functionality
class Workflow {
 
	var $magic;    # magic word used for the workflow parser-function (set from messages)
	var $state;    # state to update a workflow to in an ajax update request
	var $name;     # name of the workflow to update
	var $pagename; # the name of the current page (in which the workflows reside)
	var $title;
	var $workflowData = array();
 
	# Constructor
	function Workflow() {
		global $wgHooks, $wgSiteNotice, $wgExtensionFunctions, $wgWorkflowMagic, $wgExtraNamespaces;
 
		# Require NS_WORKFLOW to be defined before installing parser-function or javascript etc
		if (!defined('NS_WORKFLOW')) {
			$wgSiteNotice .= '<div class="usermessage">The NS_WORKFLOW namespace must be defined. Extension is disabled.</div>';
			return;
		}
 
		# The parser-function name, special-page name and links all use the word defined in $wgExtraNamespaces[NS_WORKFLOW]
		#$this->magic = $wgContLang->getNsText(NS_WORKFLOW);
		$this->magic = $wgExtraNamespaces[NS_WORKFLOW]; # $wgContLang doesn't exist yet
 
		# Reserve the magic word for use as a parser-function
		$wgHooks['LanguageGetMagic'][] = array($this, 'languageGetMagic');
 
		# If it's a workflow-related ajax call, don't use dispatcher (because we need the catlinks generated by normal page render)
		# - this sets the state, name, pagename and update properties
		$this->bypassAjaxDispatcher();
 
		# Add the extension's setup function to the list to be called after the environment is up and running
		$wgExtensionFunctions[] = array($this, 'setup');
	}
 
	# Setup
	function setup() {
		global $wgParser, $wgLanguageCode, $wgMessageCache, $wgContLang;
		$cat = $wgContLang->getNsText(NS_CATEGORY);
 
		# Add the messages used (todo: move into i18n)
		if ($wgLanguageCode == 'en') {
			$wgMessageCache->addMessages(array(
				'workflow' => $this->magic, # NOTE: the parser-function, special-page and link-rendering all use this word
				'workflowStateUpdated' => "[[{$this->magic}:$1|$1 {$this->magic}]] state set to [[$cat:$2|$2]]."
			));
		}
 
		# Add the specialpage to the environment
		SpecialPage::addPage(new SpecialWorkflow());
 
		# Add the parser-function hook
		$wgParser->setFunctionHook($this->magic, array($this, 'expandMagic'));
 
		# Add the client-side scripts for changing states
		$this->addJS();
	}
 
	# Expand the #workflow to reveal the current state and hide the others and add javascript
	# - note the hidden states mustn't be rendered because they contain categorisation links which shouldn't be processed
	function expandMagic(&$parser) {
		global $wgUser, $wgJsMimeType, $wgContLang;
		$parser->disableCache();
		$tmpl = $wgContLang->getNsText(NS_TEMPLATE);
		$cat = $wgContLang->getNsText(NS_CATEGORY);
		$wf = $this->magic;
 
		# Extend catlinks information to include workflows
		$this->extendCatlinks();
 
		# Populate $argv with both named and numeric parameters
		$args ='';
		$argv  = array();
		$items = array();
		$name = 'Untitled';
		foreach (func_get_args() as $arg) if (!is_object($arg)) {
			$args .= "|$arg";
			if (preg_match('/^(.+?)\\s*=\\s*(.+)$/',$arg,$match)) $argv[$match[1]] = $match[2]; else $argv[] = $arg;
		}
		$name = $argv[0];
		$this->workflowData[$name] = array(0);
		$current = isset($argv['state']) ? $argv['state'] : false;
 
		# Make a clone of the parser for parsing without affecting LinkHolder or catlinks arrays
		$psr = new parser;
		$opt = ParserOptions::newFromUser($wgUser);
 
		# Get the list of workflow states from the Workflow article
		# NOTE: this method of using the parser to get the links corrupts the order, so a preg_match_all may be preferable
		$states = array();
		$title  = Title::newFromtext($name,NS_WORKFLOW);
		$edit   = $title->userCan('edit');
		$url    = $title->getLocalUrl();
		$href   = "href='$url'";
		$anchor = "$wf:$name";
		if (is_object($title) && $title->exists()) {
			$article = new Article($title);
			if (preg_match_all('/^\\*\\s*\\[{2}\\s*(.+?:)?\\s*(.+?)\\s*\\]{2}/m', $article->getContent(), $match)) $states = $match[2];
			else $anchor .= " (".wfMsg('workflowMissingContent').")";
		}
		else $href .= " class='new'";
		$html = "<a $href title='$anchor'>$anchor</a>";
 
		# Transclude each (use a parser clone for the ones which aren't current to avoid categorisation)
		if (count($states)) {
			if ($edit) {
				$left  = "<td class='menu' id='left' onClick='workflowSwitchState(\"$name\",-1)'><a href='javascript:;'><</a></td>";
				$right = "<td class='menu' id='right' onClick='workflowSwitchState(\"$name\",1)'><a href='javascript:;'>></a></td>";
			}
			else $left = $right = "<td></td>"; # no menu buttons if not allowed to edit
			$html  = "<div class='workflow' id='workflow-$name'>";
			$data  = '';
			$ci    = 0;
			foreach ($states as $i => $dbk) {
				$i++;
				$stitle = Title::newFromText($dbk, NS_TEMPLATE);
				$surl   = $stitle->getLocalUrl();
				$state  = $stitle->getText();
				$anchor = preg_replace('|^.+/|','',$state);
				$data  .= ",'$state'";
				$style  = 'display:none';
				$p      =& $psr; # use local parser by default
				$this->workflowData[$name][] = $state;
				if ($current === false) $current = $state; # make current default to first item name
				$wikitext = $stitle->exists() ? '{'.'{'."$state|state=$state$args}".'}' : "[[$tmpl:$state]]"; # transclude or render a red-link
				if ($state == $current) {
					$wikitext .= "[[$cat:$state]]"; # if state is current, add a category link
					$style = '';
					$p =& $parser; # use the global parser so catlinks are updated
					$ci = $i;
				}
				$content = $p->parse($wikitext,$title,$opt,false,$state != $current)->getText();
 
				# Append the menu to the state
				$html .= "<div class='workflow-state' id='workflow-$name-$i' style='$style'>
					<table cellpadding='0' cellspacing='0'><tr><td id='content' colspan='3'>$content</td></tr><tr>$left
					<td class='menu' id='title'><a href='$surl' title='$tmpl:$state'>$anchor</a></td>$right
					</tr></table></div>\n";
			}
			$html .= "</div><script type='$wgJsMimeType'>workflowData['$name']=[$ci$data];</script>\n";
			$this->workflowData[$name][0] = $ci;
		}
 
		return array($html, 'isHTML' => true, 'noparse' => true);
	}
 
	# Update the current state of a workflow item in the requested article
	# - the actual article edit is done in returnCatlinks after all parsin finished
	# - this disables the parser-cache if the content has changed
	function updateState(&$parser, &$text) {
		$result = preg_replace_callback(
			"/\\{\\{\\s*#\\s*{$this->magic}\\s*:\\s*{$this->name}\\s*(.*?)\\s*\\}\\}/is",
			array($this, 'updateStateCallback'),
			$text,
			1,
			$count
		);
		if ($count) $this->text = $text = $result;
		$parser->disableCache();
		return true;
	}
 
	# Replacement callback for updating tag state
	function updateStateCallback($match) {
		$result = preg_replace("/(state\\s*=\\s*['\"]?)[^'\"\]\|]+/", "$1{$this->state}", $match[1], 1, $count);
		if ($count < 1) $result = "|state={$this->state}".$match[1];
		return '{'.'{'."#{$this->magic}:{$this->name}$result".'}'.'}';
	}
 
	# Extend catlinks information to include workflows
	function extendCatlinks() {
		static $done = 0;
		if ($done++) return;
		global $wgUser;
		$skin = $wgUser->getSkin();
 
		# Create a new Skin class (WorkflowSkin) by extending the existing one with overridden getCategoryLinks method
		$class = get_class($skin);
		eval("class WorkflowSkin extends {$class} ".'{
			function getCategories() {
				global $wgWorkflow;
				return $wgWorkflow->renderWorkflowInfo(parent::getCategories());
			}
		}');
 
		# Replace user's skin with a WorkflowSkin replica
		$wgUser->mSkin = new WorkflowSkin();
		foreach (array_keys(get_class_vars($class)) as $k) $wgUser->mSkin->$k = $skin->$k;
	}
 
	# Render the workflow info which appears in the catlinks area
	function renderWorkflowInfo(&$catlinks) {
		if (count($this->workflowData)) {
			//$title = Title::makeTitle(NS_SPECIAL,wfMsg('workflow'));
			//$table = "<table cellpadding='0' cellspacing='0'><tr><td><a href='{$title->getLocalURL()}'>".wfMsg('workflow')."</a>:</td><td align='right'>";
			$table = "<table cellpadding='0' cellspacing='0'><tr><td></td><td align='right'>";
			foreach ($this->workflowData as $name => $states) {
				$title = Title::makeTitle(NS_WORKFLOW, $name);
				$catlinks .= "$table<a href='{$title->getLocalURL()}'>$name</a>:&nbsp;</td><td>";
				$current = 0;
				$sep = '&nbsp;';
				foreach ($states as $i => $state) {
					if ($current) {
						$title = Title::makeTitle(NS_CATEGORY,$state);
						$class = $current == $i ? 'current' : '';
						$class .= $title->exists() ? '' : ' new';
						$catlinks .= "$sep<a class='$class' href='{$title->getLocalURL()}'>$state</a>";
						$sep = ' &rarr; ';
					}
					else $current = $state;
				}
				$table = "</td></tr><tr><td></td><td align='right'>";
			}
			$catlinks .= "</td></tr></table>\n";
		}
		return $catlinks;
	}
 
	# Return just the catlinks to the client after updating a tag state
	# - the article is updated here if replacement text has been set
	function returnCatlinks() {
		global $wgUser, $wgOut, $wgTitle;
		if ($this->text) {
			$article = new Article($wgTitle);
			$article->doEdit($this->text, wfMsg('workflowStateUpdated', $this->name, $this->state), EDIT_UPDATE);
		}
		$skin = $wgUser->getSkin();
		$catlinks = is_object($skin) ? $skin->getCategories() : 'Error: no skin!';
		$wgOut->disable();
		wfResetOutputBuffers();
		header("Cache-Control: no-cache, must-revalidate");
		header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
		echo($catlinks);
		return false;
	}
 
	# Make necessary Javascript functions available to the page
	function addJS() {
		global $wgOut, $wgJsMimeType, $wgWorkflowUpdateDelay;
		$wgOut->addScript("<script type='$wgJsMimeType'>
			var workflowData = [];
			var workflowUpdate = 0;
			var workflowLastState = 0;
			function workflowUpdateState(name) {
				clearTimeout(workflowUpdate);
				workflowLastState = workflowData[name][0];
				var state = workflowData[name][workflowLastState];
				sajax_do_call('{$this->magic}',[wgPageName,name,state],document.getElementById('catlinks'));
			}
			function workflowSwitchState(name,dir) {
				clearTimeout(workflowUpdate);
				document.getElementById('workflow-'+name+'-'+workflowData[name][0]).setAttribute('style','display:none');
				if (workflowLastState == 0) workflowLastState = workflowData[name][0];
				workflowData[name][0] += dir;
				if (workflowData[name][0] < 1) workflowData[name][0] = workflowData[name].length-1;
				if (workflowData[name][0] > workflowData[name].length-1) workflowData[name][0] = 1;
				var state = workflowData[name][0];
				document.getElementById('workflow-'+name+'-'+state).setAttribute('style','');
				if (workflowLastState != state) workflowUpdate = setTimeout('workflowUpdateState(\"'+name+'\")',$wgWorkflowUpdateDelay);
			}
			</script>");
	}
 
	# If it's a workflow-related ajax call, don't use dispatcher (because we need the catlinks generated by normal page render)
	# - this sets the state, name, pagename and update properties
	function bypassAjaxDispatcher() {
		global $wgUseAjax,$wgHooks;
		if ($wgUseAjax && $_REQUEST['action'] == 'ajax' && $_REQUEST['rs'] == $this->magic && is_array($_REQUEST['rsargs'])) {
			list($_REQUEST['title'], $this->name,$this->state) = $_REQUEST['rsargs'];
			$wgHooks['ParserBeforeStrip'][] = array($this, 'updateState');
			$wgHooks['OutputPageBeforeHTML'][] = array($this, 'returnCatlinks');
			$_REQUEST['action'] = 'render';
		}
		$this->pagename = $_REQUEST['title'];
		$this->text = '';
	}
 
	# Needed in some versions to prevent Special:Version from breaking
	function __toString() { return 'Workflow'; }
 
	# Set up the magic words
	function languageGetMagic(&$magicWords, $langCode = 0) {
		$magicWords[$this->magic] = array($langCode, $this->magic);
		return true;
	}
 
}
 
$wgWorkflow = new Workflow();

The GNU Project Debian Linux Ubuntu Linux Wikipedia online encycopedia MediaWiki