|
|
(15 intermediate revisions by 3 users not shown) |
Line 1: |
Line 1: |
− | [[Category:Extensions created with Template:Extension]][[Category:Extensions|Workflow]]{{voodoo}}[[Category:Extensions in progress|Workflow]]
| + | {{svn|extensions|Workflow/Workflow.php}} |
− | <php>
| |
− | <?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
| |
− | # State should be loaded current category currently assigned from article rather than from the state arguement in the parser function
| |
− | # Regex needs to check for all categorizations and remove redunant ones for that particular workflow. Updated article needs the new
| |
− | # state appended to the end of the article
| |
− | | |
− | 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>: </td><td>";
| |
− | $current = 0;
| |
− | $sep = ' ';
| |
− | 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 = ' → ';
| |
− | }
| |
− | 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();
| |
− | | |
− | </php>
| |