Extension:Workflow.php
<?php /**
* Extension:Workflow *
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 * @url http://www.organicdesign.co.nz/Extension:Workflow.php * Started: 2007-10-06 */
if (!defined('MEDIAWIKI')) die("Not an entry point.");
define('WORKFLOW_VERSION', "1.0.4, 2009-07-09");
$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*(.+)$%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"; if ($state == $current) $wikitext .= "[[{$this->cat}:$state]]"; $content = $parser->parse($wikitext, $wgTitle, $parser->mOptions, false, false)->getText(); $stitle = Title::newFromText($state, NS_CATEGORY); $surl = $stitle->getLocalUrl();
# Append the menu to the state
$html .= "$content | ||
<a href='$surl' title='{$this->cat}:$state'>$state</a> |
}
$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 "
";
} }');
# 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 * - this is a bit of a hack which is needed for the direct catlinks updating */ function updateCatLinks() { global $wgOut, $wgUser; if (isset($wgOut->mCategoryLinks['normal'])) $links = &$wgOut->mCategoryLinks['normal']; else $links = &$wgOut->mCategoryLinks; $data = &$this->workflowData[$this->name]; $cats = join('|', array_map('preg_quote', array_keys($data['states']))); $tmp = array(); foreach ($links 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()); $links = $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]; $state = 'No states defined!'; if ($this->name == $name) $state = $this->state; elseif (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_map('preg_quote', array_keys($data['states'])));
# Template specified, extract state from template parameter if ($data['template']) { $tmpl = preg_quote($data['template']); $par = preg_quote($data['parameter']); $state = "%\s*\{\{$tmpl.+?\|\s*$par\s*=\s*(\w+)%si"; if (preg_match("%\s*\{\{$tmpl.*?\|\s*$par\s*=\s*(\w+)%si", $text, $m)) $state = $m[1]; }
# No template specified, extract state from category links elseif (preg_match("%\s*\[\[".preg_quote($this->cat).":($cats)\]\]\s*%i", $text, $m)) $state = $m[1];
# Otherwise default to first defined state else { $state = array_keys($data['states']); if (count($state)) $state = $state[0]; } } 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_map('preg_quote', 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*\[\[".preg_quote($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"); header("Content-Type: text/html; charset=utf-8"); 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('workflowlayer')); } 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();