Difference between revisions of "Extension:Workflow.php"

From Organic Design wiki
(almost there but not quite working yet)
(1.0.0 - new way which can optionally work with templates)
Line 11: Line 11:
 
if (!defined('MEDIAWIKI')) die("Not an entry point.");
 
if (!defined('MEDIAWIKI')) die("Not an entry point.");
  
define('WORKFLOW_VERSION', "0.1.1, 2008-11-08");
+
define('WORKFLOW_VERSION', "1.0.0, 2008-12-01");
  
 +
$wgWorkflowMagic      = 'Workflow';
 
$wgWorkflowUpdateDelay = 1000; # Delay in milliseconds after clicking state before Ajax update is made
 
$wgWorkflowUpdateDelay = 1000; # Delay in milliseconds after clicking state before Ajax update is made
  
Line 22: Line 23:
 
'url'        => "http://www.mediawiki.org/wiki/Extension:Workflow",
 
'url'        => "http://www.mediawiki.org/wiki/Extension:Workflow",
 
'version'    => WORKFLOW_VERSION
 
'version'    => WORKFLOW_VERSION
);
+
);
  
 
/**
 
/**
Line 48: Line 49:
 
class Workflow {
 
class Workflow {
  
var $magic;   # magic word used for the workflow parser-function (set from messages)
+
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 $cat;                     # local lang name for "Category"
var $name;    # name of the workflow to update
+
var $workflowData = array();   # Stores lists of state-content, current state and args for each workflow
var $pagename; # the name of the current page (in which the workflows reside)
+
var $state = false;           # state to update a workflow to in an ajax update request
var $cat;     # lcoal lang name for "Category"
+
var $name;                    # name of the workflow to update
var $title;
 
var $workflowData = array(); # Stores lists of states for used workflows
 
var $currentStates = array(); # Stores current state for used workflows
 
  
 
/**
 
/**
Line 61: Line 59:
 
*/
 
*/
 
function Workflow() {
 
function Workflow() {
global $wgHooks, $wgSiteNotice, $wgExtensionFunctions, $wgWorkflowMagic, $wgExtraNamespaces, $wgAjaxExportList;
+
global $wgHooks, $wgExtensionFunctions, $wgWorkflowMagic;
 
 
# 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]
 
# 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 = $wgWorkflowMagic;
$this->magic = $wgExtraNamespaces[NS_WORKFLOW]; # $wgContLang doesn't exist yet
 
 
 
$wgAjaxExportList[] = $this->magic;
 
 
 
 
# Reserve the magic word for use as a parser-function
 
# Reserve the magic word for use as a parser-function
Line 79: Line 68:
 
 
 
# If it's a workflow-related ajax call, don't use dispatcher (because we need the catlinks generated by normal page render)
 
# 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 sets the $this->state and $this->name so that OutputPageBeforeHTML calls updateState()
 
$this->bypassAjaxDispatcher();
 
$this->bypassAjaxDispatcher();
 
 
Line 97: Line 86:
 
if ($wgLanguageCode == 'en') {
 
if ($wgLanguageCode == 'en') {
 
$wgMessageCache->addMessages(array(
 
$wgMessageCache->addMessages(array(
'workflow' => $this->magic, # NOTE: the parser-function, special-page and link-rendering all use this word
+
'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 [[{$this->cat}:$2|$2]]."
+
'workflowMissingStates' => "No workflow states defined",
 +
'workflowStateUpdated' => "$1 {$this->magic} state set to [[{$this->cat}:$2|$2]]."
 
));
 
));
 
}
 
}
Line 114: Line 104:
 
/**
 
/**
 
* Expand the #workflow to reveal the current state and hide the others and add javascript
 
* 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) {
 
function expandMagic(&$parser) {
global $wgUser, $wgJsMimeType, $wgContLang;
+
global $wgTitle, $wgJsMimeType;
 
$parser->disableCache();
 
$parser->disableCache();
$tmpl = $wgContLang->getNsText(NS_TEMPLATE);
 
$wf = $this->magic;
 
  
 
# Extend catlinks information to include workflows
 
# Extend catlinks information to include workflows
 
$this->extendCatlinks();
 
$this->extendCatlinks();
  
# Populate $argv with both named and numeric parameters
+
# Extract workflow info from args
$args ='';
+
$name  = 'Untitled';
$argv  = array();
+
$states = array();
$items = array();
+
$tmpl  = false;
$name = 'Untitled';
+
$par    = 'state';
 
foreach (func_get_args() as $arg) if (!is_object($arg)) {
 
foreach (func_get_args() as $arg) if (!is_object($arg)) {
$args .= "|$arg";
+
if (preg_match("%^(.+?)\\s*=\\s*(.+)$%", $arg, $match)) {
if (preg_match("%^(.+?)\\s*=\\s*(.+)$%", $arg, $match)) $argv[$match[1]] = $match[2]; else $argv[] = $arg;
+
list($arg, $k, $v) = $match;
 +
switch ($k) {
 +
case 'template'  : $tmpl = $v; break;
 +
case 'parameter' : $par  = $v; break;
 +
default: $states[$k] = $v;
 +
}
 +
} else $name = $arg;
 
}
 
}
$name = $argv[0];
 
  
# Get state list and current state of this workflow
+
# Store the data for this workflow
$current = $this->loadStates($name);
+
$this->workflowData[$name] = array('parameter' => $par, 'template' => $tmpl, 'states' => $states);
$states = $this->workflowData[$name];
+
$current = $this->getCurrentState($name);
 
 
# Make a clone of the parser for parsing without affecting LinkHolder or catlinks arrays
 
$psr = new parser;
 
$opt = ParserOptions::newFromUser($wgUser);
 
  
 
# Build links for workflow table
 
# Build links for workflow table
$title  = Title::newFromText($name, NS_WORKFLOW);
+
$edit  = $wgTitle->userCan('edit');
$edit  = $title->userCan('edit');
+
$url    = $wgTitle->getLocalUrl();
$url    = $title->getLocalUrl();
+
$anchor = $name;
$href  = "href='$url'";
+
if (count($states) < 1) $anchor .= " (".wfMsg('workflowMissingStates').")";
$anchor = "$wf:$name";
+
$html = "<a href='$url' title='$anchor'>$anchor</a>";
if (is_object($title) && $title->exists()) {
 
if (count($states)<1) $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)
+
# Render the states in their div elements with only current one visible
 
if (count($states)) {
 
if (count($states)) {
 
if ($edit) {
 
if ($edit) {
Line 162: Line 146:
 
}
 
}
 
else $left = $right = "<td></td>"; # no menu buttons if not allowed to edit
 
else $left = $right = "<td></td>"; # no menu buttons if not allowed to edit
$html = "<div class='workflow' id='workflow-$name'>";
+
$html = "<div class='workflow' id='workflow-$name'>";
$data = "";
+
$data = array_search($current, array_keys($states))+1;
$ci    = 0;
+
foreach ($states as $state => $wikitext) {
foreach ($states as $i => $state) {
+
$data   .= ",'$state'";
$stitle = Title::newFromText($state, NS_TEMPLATE);
+
$style   = $state == $current ? "" : "display:none";
$surl  = $stitle->getLocalUrl();
+
$content = $parser->parse($wikitext, $wgTitle, $parser->mOptions, false, $state != $current)->getText();
$data .= ",'$state'";
+
$stitle  = Title::newFromText($state, NS_CATEGORY);
$style = "display:none";
+
$surl    = $stitle->getLocalUrl();
if (empty($current)) $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 .= "[[{$this->cat}:$state]]"; # if state is current, add a category link
 
$style = "";
 
$ci = $i;
 
}
 
$content = $parser->parse($wikitext, $title, $opt, false, $state != $current)->getText();
 
  
 
# Append the menu to the state
 
# Append the menu to the state
$html .= "<div class='workflow-state' id='workflow-$name-$i' style='$style'>
+
$html .= "<div class='workflow-state' id='workflow-$name-$state' style='$style'>
<table cellpadding='0' cellspacing='0'><tr><td id='content' colspan='3'>$content</td></tr><tr>$left
+
<table cellpadding='0' cellspacing='0'><tr><td id='workflow-content' colspan='3'>$content</td></tr><tr>$left
<td class='menu' id='title'><a href='$surl' title='$tmpl:$state'>$anchor</a></td>$right
+
<td class='menu' id='title'><a href='$surl' title='{$this->cat}:$state'>$state</a></td>$right
 
</tr></table></div>\n";
 
</tr></table></div>\n";
 
}
 
}
$html .= "</div><script type='$wgJsMimeType'>workflowData['$name']=[$ci$data];</script>\n";
+
$html .= "</div><script type='$wgJsMimeType'>workflowData['$name']=[$data];</script>\n";
 
}
 
}
 
 
 
return array($html, 'isHTML' => true, 'noparse' => true);
 
return array($html, 'isHTML' => true, 'noparse' => true);
}
 
 
/**
 
* Extract states from Worklow:Name content if we don't have the list yet
 
* - returns the current state (but only when first loaded)
 
*/
 
function loadStates($name) {
 
$current = '';
 
if (isset($this->workflowData[$name])) $states = $this->workflowData[$name];
 
else {
 
$this->workflowData[$name]  = array();
 
$this->currentStates[$name] = '';
 
$title = Title::newFromText($name, NS_WORKFLOW);
 
if (is_object($title) && $title->exists()) {
 
 
# Extract the links from the workflow article
 
$article = new Article($title);
 
if (preg_match_all("%^\\*\\s*\\[{2}\\s*(.+?:)?\\s*(.+?)\\s*\\]{2}%m", $article->getContent(), $match))
 
$this->workflowData[$name] = $match[2];
 
 
# Extract the current state from the first matching category in the article
 
global $wgTitle;
 
$article = new Article($wgTitle);
 
$cats = join('|', $this->workflowData[$name]);
 
if (preg_match("%\s*\[\[{$this->cat}:($cats)\]\]\s*%", $article->getContent(), $match))
 
$this->currentStates[$name] = $match[1];
 
}
 
}
 
return $this->currentStates[$name];
 
}
 
 
/**
 
* Update the current state of a workflow item in the requested article
 
* - the actual article edit is done in returnCatlinks after all parsing finished
 
* - this disables the parser-cache
 
*/
 
function updateState(&$parser, &$text) {
 
$this->loadStates($this->name);
 
static $i = 0;
 
if ($i++ < 1 && isset($this->workflowData[$this->name]) && count($this->workflowData[$this->name]) > 1) {
 
$cats = join('|', $this->workflowData[$this->name]);
 
$this->text = preg_replace("%\s*\[\[{$this->cat}:($cats)\]\]\s*%", "", $text)."\n[[{$this->cat}:{$this->state}]]";
 
$parser->disableCache();
 
}
 
return true;
 
 
}
 
}
  
Line 250: Line 181:
 
function getCategories() {
 
function getCategories() {
 
global $wgWorkflow;
 
global $wgWorkflow;
 +
if ($wgWorkflow->state) $wgWorkflow->updateCatLinks();
 
return $wgWorkflow->renderWorkflowInfo(parent::getCategories());
 
return $wgWorkflow->renderWorkflowInfo(parent::getCategories());
 
}
 
}
Line 257: Line 189:
 
$wgUser->mSkin = new WorkflowSkin();
 
$wgUser->mSkin = new WorkflowSkin();
 
foreach (array_keys(get_class_vars($class)) as $k) $wgUser->mSkin->$k = $skin->$k;
 
foreach (array_keys(get_class_vars($class)) as $k) $wgUser->mSkin->$k = $skin->$k;
 +
}
 +
 +
/**
 +
* Update $wgOut's category links if state updated by ajax
 +
*/
 +
function updateCatLinks() {
 +
global $wgOut, $wgUser;
 +
$data =& $this->workflowData[$this->name];
 +
$cats = join('|', array_keys($data['states']));
 +
$tmp = array();
 +
foreach ($wgOut->mCategoryLinks['normal'] as $i => $link) if (!preg_match("%>($cats)</a>%i", $link)) $tmp[] = $link;
 +
$title = Title::newFromText($this->state, NS_CATEGORY);
 +
$tmp[] = $wgUser->getSkin()->makeLinkObj($title, $title->getText());
 +
$wgOut->mCategoryLinks['normal'] = $tmp;
 
}
 
}
 
 
Line 265: Line 211:
 
if (count($this->workflowData)) {
 
if (count($this->workflowData)) {
 
$table = "<table cellpadding='0' cellspacing='0'><tr><td></td><td align='right'>";
 
$table = "<table cellpadding='0' cellspacing='0'><tr><td></td><td align='right'>";
foreach ($this->workflowData as $name => $states) {
+
foreach ($this->workflowData as $name => $data) {
$title = Title::newFromText($name, NS_WORKFLOW);
+
$current = $this->getCurrentState($name);
$anchor = $title->getText();
+
$catlinks .= "$table<b>$name</b>:&nbsp;</td><td>";
$catlinks .= "$table<a href='{$title->getLocalURL()}'>$anchor</a>:&nbsp;</td><td>";
 
$current = $this->currentStates[$name];
 
 
$sep = "&nbsp;";
 
$sep = "&nbsp;";
foreach ($states as $state) {
+
foreach (array_keys($data['states']) as $state) {
 
$title = Title::newFromText($state, NS_CATEGORY);
 
$title = Title::newFromText($state, NS_CATEGORY);
$class = $current == $state ? 'current' : '';
+
$class = ($current == $state ? 'current' : '') . ($title->exists() ? '' : ' new');
$class .= $title->exists() ? '' : ' new';
+
$catlinks .= "$sep<a class='$class' href='{$title->getLocalURL()}'>{$title->getText()}</a>";
$catlinks .= "$sep<a class='$class' href='{$title->getLocalURL()}'>$state</a>";
 
 
$sep = " &rarr; ";
 
$sep = " &rarr; ";
 
}
 
}
Line 286: Line 229:
  
 
/**
 
/**
* Return just the catlinks to the client after updating a tag state
+
* Extract the current state of the named workflow from the current article content
* - the article is updated here if replacement text has been set
 
 
*/
 
*/
function returnCatlinks(&$out, &$text) {
+
function getCurrentState($name) {
global $wgUser, $wgOut, $wgTitle;
+
$data =& $this->workflowData[$name];
if ($this->text) {
+
if (isset($data['current'])) $state = $data['current'];
 +
else {
 +
 
 +
# Get the content of the current article
 +
global $wgTitle;
 
$article = new Article($wgTitle);
 
$article = new Article($wgTitle);
$article->doEdit($this->text, wfMsg('workflowStateUpdated', $this->name, $this->state), EDIT_UPDATE);
+
$text = $article->getContent();
 +
$cats = join('|', array_keys($data['states']));
 +
 +
# Template specified, extract state from template parameter
 +
if ($data['template']) {
 +
$tmpl = $data['template'];
 +
$par  = $data['parameter'];
 +
if (preg_match("%\s*\{\{$tmpl.+?\|\s*$par\s*=\s*(\w+)%s", $text, $m)) $state = $m[1];
 +
}
 +
 +
# No template specified, extract state from category links
 +
elseif (preg_match("%\s*\[\[{$this->cat}:($cats)\]\]\s*%", $text, $m)) $state = $m[1];
 +
 +
# Otherwise default to first defined state
 +
else {
 +
$state = array_keys($data['states']);
 +
$state = count($state) ? $state[0] : 'Error: No states defined!';
 +
}
 
}
 
}
 +
return $state;
 +
}
 +
 +
/**
 +
* Update the current state of a workflow item in the requested article
 +
* - called from OutputPageBeforeHTML if $this->state was set by bypassAjaxDispatcher()
 +
*/
 +
function updateState() {
 +
$cat  = $this->cat;
 +
$name  = $this->name;
 +
$state = $this->state;
 +
$data  = &$this->workflowData[$name];
 +
$cats  = join('|', array_keys($data['states']));
 +
$tmpl  = $data['template'];
 +
$par  = $data['parameter'];
 +
$data['current'] = $state;
 +
 +
# Get current article content
 +
global $wgTitle;
 +
$article = new Article($wgTitle);
 +
$text = $article->getContent();
 +
 +
# Update the state in the article text (as template parameter or direct cat links)
 +
$text = $tmpl
 +
? preg_replace("%(\s*\{\{$tmpl.+?\|\s*$par\s*=\s*)\w+%s", "$1$state", $text)
 +
: preg_replace("%\s*\[\[{$cat}:($cats)\]\]\s*%i", "", $text) . "[[$cat:$state]]";
 +
 +
# Update the article with the new text
 +
$article->doEdit($text, wfMsg('workflowStateUpdated', $this->name, $this->state), EDIT_UPDATE);
 +
}
 +
 +
/**
 +
* Return just the catlinks to the client after updating a tag state
 +
* - this also calls updateState() if $this->state set by bypassAjaxDispatcher()
 +
*/
 +
function onOutputPageBeforeHTML(&$out) {
 +
global $wgUser;
 +
if ($this->state) $this->updateState();
 
$skin = $wgUser->getSkin();
 
$skin = $wgUser->getSkin();
 
$catlinks = is_object($skin) ? $skin->getCategories() : "Error: no skin!";
 
$catlinks = is_object($skin) ? $skin->getCategories() : "Error: no skin!";
$wgOut->disable();
+
$out->disable();
 
wfResetOutputBuffers();
 
wfResetOutputBuffers();
 
header("Cache-Control: no-cache, must-revalidate");
 
header("Cache-Control: no-cache, must-revalidate");
Line 318: Line 319:
 
workflowLastState = workflowData[name][0];
 
workflowLastState = workflowData[name][0];
 
var state = workflowData[name][workflowLastState];
 
var state = workflowData[name][workflowLastState];
sajax_do_call('{$this->magic}',[wgPageName,name,state],document.getElementById('catlinks'));
+
sajax_do_call('workflow_ajax_callback',[wgPageName,name,state],document.getElementById('catlinks'));
 
}
 
}
 
function workflowSwitchState(name,dir) {
 
function workflowSwitchState(name,dir) {
 
clearTimeout(workflowUpdate);
 
clearTimeout(workflowUpdate);
document.getElementById('workflow-'+name+'-'+workflowData[name][0]).setAttribute('style','display:none');
+
var id = 'workflow-'+name+'-'+workflowData[name][workflowData[name][0]];
 +
document.getElementById(id).setAttribute('style','display:none');
 
if (workflowLastState == 0) workflowLastState = workflowData[name][0];
 
if (workflowLastState == 0) workflowLastState = workflowData[name][0];
 
workflowData[name][0] += dir;
 
workflowData[name][0] += dir;
Line 328: Line 330:
 
if (workflowData[name][0] > workflowData[name].length-1) workflowData[name][0] = 1;
 
if (workflowData[name][0] > workflowData[name].length-1) workflowData[name][0] = 1;
 
var state = workflowData[name][0];
 
var state = workflowData[name][0];
document.getElementById('workflow-'+name+'-'+state).setAttribute('style','');
+
id = 'workflow-'+name+'-'+workflowData[name][state];
 +
document.getElementById(id).setAttribute('style','');
 
if (workflowLastState != state) workflowUpdate = setTimeout('workflowUpdateState(\"'+name+'\")',$wgWorkflowUpdateDelay);
 
if (workflowLastState != state) workflowUpdate = setTimeout('workflowUpdateState(\"'+name+'\")',$wgWorkflowUpdateDelay);
 
}
 
}
Line 336: Line 339:
 
/**
 
/**
 
* If it's a workflow-related ajax call, don't use dispatcher (because we need the catlinks generated by normal page render)
 
* 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 sets the $this->state and $this->name so that OutputPageBeforeHTML calls updateState()
 +
* - the ajax dispatcher id bypassed by changing the action to 'view'
 
*/
 
*/
 
function bypassAjaxDispatcher() {
 
function bypassAjaxDispatcher() {
global $wgUseAjax, $wgHooks;
+
global $wgUseAjax, $wgHooks, $wgAjaxExportList;
 
if ($wgUseAjax
 
if ($wgUseAjax
&& isset($_GET['action'])
+
&& isset($_GET['action']) && $_GET['action'] == 'ajax'
&& $_GET['action'] == 'ajax'
+
&& isset($_GET['rs'])     && $_GET['rs'] == 'workflow_ajax_callback'
&& isset($_GET['rs'])
+
&& isset($_GET['rsargs']) && is_array($_GET['rsargs'])
&& $_GET['rs'] == $this->magic
 
&& isset($_GET['rsargs'])
 
&& is_array($_GET['rsargs'])
 
 
) {
 
) {
 
list($title, $this->name, $this->state) = $_GET['rsargs'];
 
list($title, $this->name, $this->state) = $_GET['rsargs'];
$wgHooks['ParserBeforeStrip'][]    = array($this, 'updateState');
+
$wgHooks['OutputPageBeforeHTML'][] = $this;
$wgHooks['OutputPageBeforeHTML'][] = array($this, 'returnCatlinks');
 
 
$_GET = $_REQUEST = array('title' => $title, 'action' => 'view');
 
$_GET = $_REQUEST = array('title' => $title, 'action' => 'view');
 +
$wgAjaxExportList[] = 'workflow_ajax_callback';
 
}
 
}
$this->pagename = isset($_REQUEST['title']) ? $_REQUEST['title'] : '';
 
$this->text = '';
 
 
}
 
}
  
Line 360: Line 359:
 
* Needed in some versions to prevent Special:Version from breaking
 
* Needed in some versions to prevent Special:Version from breaking
 
*/
 
*/
function __toString() { return 'Workflow'; }
+
function __toString() { return __CLASS__; }
  
 
/**
 
/**

Revision as of 04:08, 1 December 2008

<?php /**

* Extension:Workflow
*
Info.svg These are the MediaWiki extensions we're using and/or developing. Please refer to the information on the mediawiki.org wiki for installation and usage details. Extensions here which have no corresponding mediawiki article are either not ready for use or have been superseded. You can also browse our extension code in our local Subversion repository or our GitHub mirror.

Template:PhpCategory:Extensions created with Template:Extension

* @package MediaWiki
* @subpackage Extensions
* @author Aran Dunkley User:Nad
* @licence GNU General Public Licence 2.0 or later
* Started: 2007-10-06
*/

if (!defined('MEDIAWIKI')) die("Not an entry point.");

define('WORKFLOW_VERSION', "1.0.0, 2008-12-01");

$wgWorkflowMagic = 'Workflow'; $wgWorkflowUpdateDelay = 1000; # Delay in milliseconds after clicking state before Ajax update is made

$wgExtensionCredits['parserhook'][] = $wgExtensionCredits['specialpage'][] = array( 'name' => "Workflow", 'author' => "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 $cat; # local lang name for "Category" var $workflowData = array(); # Stores lists of state-content, current state and args for each workflow var $state = false; # state to update a workflow to in an ajax update request var $name; # name of the workflow to update

/** * Constructor */ function Workflow() { global $wgHooks, $wgExtensionFunctions, $wgWorkflowMagic;

# The parser-function name, special-page name and links all use the word defined in $wgExtraNamespaces[NS_WORKFLOW] $this->magic = $wgWorkflowMagic;

# 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 $this->state and $this->name so that OutputPageBeforeHTML calls updateState() $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'); }

/** * Extension setup */ function setup() { global $wgParser, $wgLanguageCode, $wgMessageCache, $wgContLang;

$this->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 'workflowMissingStates' => "No workflow states defined", 'workflowStateUpdated' => "$1 {$this->magic} state set to [[{$this->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 */ function expandMagic(&$parser) { global $wgTitle, $wgJsMimeType; $parser->disableCache();

# Extend catlinks information to include workflows $this->extendCatlinks();

# Extract workflow info from args $name = 'Untitled'; $states = array(); $tmpl = false; $par = 'state'; foreach (func_get_args() as $arg) if (!is_object($arg)) { if (preg_match("%^(.+?)\\s*=\\s*(.+)$%", $arg, $match)) { list($arg, $k, $v) = $match; switch ($k) { case 'template'  : $tmpl = $v; break; case 'parameter' : $par = $v; break; default: $states[$k] = $v; } } else $name = $arg; }

# Store the data for this workflow $this->workflowData[$name] = array('parameter' => $par, 'template' => $tmpl, 'states' => $states); $current = $this->getCurrentState($name);

# Build links for workflow table $edit = $wgTitle->userCan('edit'); $url = $wgTitle->getLocalUrl(); $anchor = $name; if (count($states) < 1) $anchor .= " (".wfMsg('workflowMissingStates').")"; $html = "<a href='$url' title='$anchor'>$anchor</a>";

# Render the states in their div elements with only current one visible if (count($states)) { if ($edit) {

$left = "<a href='javascript:;'><</a>"; $right = "<a href='javascript:;'>></a>";

}

else $left = $right = ""; # no menu buttons if not allowed to edit $html = "

";

$data = array_search($current, array_keys($states))+1; foreach ($states as $state => $wikitext) { $data .= ",'$state'"; $style = $state == $current ? "" : "display:none"; $content = $parser->parse($wikitext, $wgTitle, $parser->mOptions, false, $state != $current)->getText(); $stitle = Title::newFromText($state, NS_CATEGORY); $surl = $stitle->getLocalUrl();

# Append the menu to the state

$html .= "
$left $right
$content
\n";

}

$html .= "

<script type='$wgJsMimeType'>workflowData['$name']=[$data];</script>\n";

}

return array($html, 'isHTML' => true, 'noparse' => true); }

/** * 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; if ($wgWorkflow->state) $wgWorkflow->updateCatLinks(); 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; }

/** * Update $wgOut's category links if state updated by ajax */ function updateCatLinks() { global $wgOut, $wgUser; $data =& $this->workflowData[$this->name]; $cats = join('|', array_keys($data['states'])); $tmp = array(); foreach ($wgOut->mCategoryLinks['normal'] as $i => $link) if (!preg_match("%>($cats)</a>%i", $link)) $tmp[] = $link; $title = Title::newFromText($this->state, NS_CATEGORY); $tmp[] = $wgUser->getSkin()->makeLinkObj($title, $title->getText()); $wgOut->mCategoryLinks['normal'] = $tmp; }

/** * Render the workflow info which appears in the catlinks area */ function renderWorkflowInfo(&$catlinks) { if (count($this->workflowData)) {

$table = "

";

foreach ($this->workflowData as $name => $data) { $current = $this->getCurrentState($name);

$catlinks .= "$table$name
";

$sep = " "; foreach (array_keys($data['states']) as $state) { $title = Title::newFromText($state, NS_CATEGORY); $class = ($current == $state ? 'current' : ) . ($title->exists() ?  : ' new'); $catlinks .= "$sep<a class='$class' href='{$title->getLocalURL()}'>{$title->getText()}</a>"; $sep = " → "; }

$table = "
";

}

$catlinks .= "

\n";

} return $catlinks; }

/** * Extract the current state of the named workflow from the current article content */ function getCurrentState($name) { $data =& $this->workflowData[$name]; if (isset($data['current'])) $state = $data['current']; else {

# Get the content of the current article global $wgTitle; $article = new Article($wgTitle); $text = $article->getContent(); $cats = join('|', array_keys($data['states']));

# Template specified, extract state from template parameter if ($data['template']) { $tmpl = $data['template']; $par = $data['parameter']; if (preg_match("%\s*\{\{$tmpl.+?\|\s*$par\s*=\s*(\w+)%s", $text, $m)) $state = $m[1]; }

# No template specified, extract state from category links elseif (preg_match("%\s*\[\[{$this->cat}:($cats)\]\]\s*%", $text, $m)) $state = $m[1];

# Otherwise default to first defined state else { $state = array_keys($data['states']); $state = count($state) ? $state[0] : 'Error: No states defined!'; } } return $state; }

/** * Update the current state of a workflow item in the requested article * - called from OutputPageBeforeHTML if $this->state was set by bypassAjaxDispatcher() */ function updateState() { $cat = $this->cat; $name = $this->name; $state = $this->state; $data = &$this->workflowData[$name]; $cats = join('|', array_keys($data['states'])); $tmpl = $data['template']; $par = $data['parameter']; $data['current'] = $state;

# Get current article content global $wgTitle; $article = new Article($wgTitle); $text = $article->getContent();

# Update the state in the article text (as template parameter or direct cat links) $text = $tmpl ? preg_replace("%(\s*\{\{$tmpl.+?\|\s*$par\s*=\s*)\w+%s", "$1$state", $text) : preg_replace("%\s*\[\[{$cat}:($cats)\]\]\s*%i", "", $text) . "$cat:$state";

# Update the article with the new text $article->doEdit($text, wfMsg('workflowStateUpdated', $this->name, $this->state), EDIT_UPDATE); }

/** * Return just the catlinks to the client after updating a tag state * - this also calls updateState() if $this->state set by bypassAjaxDispatcher() */ function onOutputPageBeforeHTML(&$out) { global $wgUser; if ($this->state) $this->updateState(); $skin = $wgUser->getSkin(); $catlinks = is_object($skin) ? $skin->getCategories() : "Error: no skin!"; $out->disable(); wfResetOutputBuffers(); header("Cache-Control: no-cache, must-revalidate"); header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); print $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('workflow_ajax_callback',[wgPageName,name,state],document.getElementById('catlinks')); } function workflowSwitchState(name,dir) { clearTimeout(workflowUpdate); var id = 'workflow-'+name+'-'+workflowData[name][workflowData[name][0]]; document.getElementById(id).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]; id = 'workflow-'+name+'-'+workflowData[name][state]; document.getElementById(id).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 $this->state and $this->name so that OutputPageBeforeHTML calls updateState() * - the ajax dispatcher id bypassed by changing the action to 'view' */ function bypassAjaxDispatcher() { global $wgUseAjax, $wgHooks, $wgAjaxExportList; if ($wgUseAjax && isset($_GET['action']) && $_GET['action'] == 'ajax' && isset($_GET['rs']) && $_GET['rs'] == 'workflow_ajax_callback' && isset($_GET['rsargs']) && is_array($_GET['rsargs']) ) { list($title, $this->name, $this->state) = $_GET['rsargs']; $wgHooks['OutputPageBeforeHTML'][] = $this; $_GET = $_REQUEST = array('title' => $title, 'action' => 'view'); $wgAjaxExportList[] = 'workflow_ajax_callback'; } }

/** * Needed in some versions to prevent Special:Version from breaking */ function __toString() { return __CLASS__; }

/** * Set up the magic words */ function languageGetMagic(&$magicWords, $langCode = 0) { $magicWords[$this->magic] = array($langCode, $this->magic); return true; } }

$wgWorkflow = new Workflow();