Difference between revisions of "Extension:Workflow.php"

From Organic Design wiki
(1.0.4 - bug fixes by User:VitaliyFilippov)
(moving into MW svn)
Line 1: Line 1:
<?php
+
{{svn|http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/Workflow}}
/**
 
* Extension:Workflow
 
* {{Category:Extensions|Workflow}}{{php}}{{Category:Extensions created with Template:Extension}}
 
* @package MediaWiki
 
* @subpackage Extensions
 
* @author Aran Dunkley [http://www.organicdesign.co.nz/nad 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'      => "[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 $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  = "<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 = 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 .= "<div class='workflow-state' id='workflow-$name-$state' style='$style'>
 
<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='{$this->cat}:$state'>$state</a></td>$right
 
</tr></table></div>\n";
 
}
 
$html .= "</div><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 "<div id=\'workflowlayer\'>" . $wgWorkflow->renderWorkflowInfo(parent::getCategories()) . "</div>";
 
}
 
}');
 
 
 
# 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 = "<table cellpadding='0' cellspacing='0'><tr><td></td><td align='right'>";
 
foreach ($this->workflowData as $name => $data) {
 
$current = $this->getCurrentState($name);
 
$catlinks .= "$table<b>$name</b>:&nbsp;</td><td>";
 
$sep = "&nbsp;";
 
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 = " &rarr; ";
 
}
 
$table = "</td></tr><tr><td></td><td align='right'>";
 
}
 
$catlinks .= "</td></tr></table>\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();
 

Revision as of 23:36, 4 May 2010

Info.svg This code is in our Git repository here.

Note: If there is no information in this page about this code and it's a MediaWiki extension, there may be something at mediawiki.org.