|
|
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>: </td><td>";
| |
− | $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 = "</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();
| |