Difference between revisions of "Extension:RecordAdmin"

From Organic Design wiki
(sortable table and change row classes to names)
(moved to svn)
Line 1: Line 1:
<?php
+
[[Category:Extensions|RecordAdmin]]
/**
+
{{svn|http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/RecordAdmin}}
* Extension:RecordAdmin - MediaWiki extension
 
*{{Category:Extensions|RecordAdmin}}{{php}}{{Category:Extensions created with Template:SpecialPage}}
 
* @package MediaWiki
 
* @subpackage Extensions
 
* @author Aran Dunkley [http://www.organicdesign.co.nz/nad User:Nad]
 
* @licence GNU General Public Licence 2.0 or later
 
*/
 
 
 
if (!defined('MEDIAWIKI')) die('Not an entry point.');
 
 
 
define('RECORDADMIN_VERSION','0.1.4, 2008-10-29');
 
 
 
$wgRecordAdminCategory      = 'Records'; # Category which contains the templates used as records and having corresponding forms
 
$wgRecordAdminUseNamespaces = false;    # Whether record articles should be in a namespace of the same name as their type
 
 
 
$wgExtensionFunctions[] = 'wfSetupRecordAdmin';
 
 
 
$wgExtensionCredits['specialpage'][] = array(
 
'name'        => 'Record administration',
 
'author'      => '[http://www.organicdesign.co.nz/nad User:Nad]',
 
'description' => 'A special page for finding and editing record articles using a form',
 
'url'        => 'http://www.organicdesign.co.nz/Extension:SpecialExample',
 
'version'    => RECORDADMIN_VERSION
 
);
 
 
 
require_once "$IP/includes/SpecialPage.php";
 
 
 
/**
 
* Define a new class based on the SpecialPage class
 
*/
 
class SpecialRecordAdmin extends SpecialPage {
 
 
 
var $form = '';
 
var $types = array();
 
var $guid = '';
 
 
 
function __construct() {
 
 
# Name to use for creating a new record either via RecordAdmin or a public form
 
# todo: should add a hook here for custom default-naming
 
$this->guid = strftime('%Y%m%d', time()).'-'.substr(strtoupper(uniqid()), -5);
 
 
SpecialPage::SpecialPage(
 
'RecordAdmin', # name as seen in links etc
 
'sysop',      # user rights required
 
true,          # listed in special:specialpages
 
false,        # function called by execute() - defaults to wfSpecial{$name}
 
false,        # file included by execute() - defaults to Special{$name}.php, only used if no function
 
false          # includable
 
);
 
}
 
 
 
/**
 
* Override SpecialPage::execute()
 
*/
 
function execute($param) {
 
global $wgOut, $wgRequest, $wgRecordAdminCategory, $wgRecordAdminUseNamespaces;
 
$this->setHeaders();
 
$type    = $wgRequest->getText('wpType') or $type = $param;
 
$record  = $wgRequest->getText('wpRecord');
 
$invert  = $wgRequest->getText('wpInvert');
 
$title    = Title::makeTitle(NS_SPECIAL, 'RecordAdmin');
 
$wpTitle  = trim($wgRequest->getText('wpTitle'));
 
 
 
if ($type && $wgRecordAdminUseNamespaces) {
 
if ($wpTitle && !ereg("^$type:.+$", $wpTitle)) $wpTitle = "$type:$wpTitle";
 
}
 
 
 
$wgOut->addHTML("<div class='center'><a href='".$title->getLocalURL()."/$type'>New $type search</a> | "
 
. "<a href='".$title->getLocalURL()."'>Select another record type</a></div><br>\n"
 
);
 
 
# Get posted form values if any
 
$posted = array();
 
foreach ($_POST as $k => $v) if (ereg('^ra_(.+)$', $k, $m)) $posted[$m[1]] = $v;
 
 
 
# Read in and prepare the form for this record type if one has been selected
 
if ($type) $this->preProcessForm($type);
 
 
 
# Extract the input names and types used in the form
 
$this->examineForm();
 
 
 
# Clear any default values
 
$this->populateForm(array());
 
 
 
# If no type selected, render select list of record types from Category:Records
 
if (empty($type)) {
 
$wgOut->addWikiText("== Select the type of record to search for ==\n");
 
 
# Get titles in Category:Records and build option list
 
$options = '';
 
$dbr  = &wfGetDB(DB_SLAVE);
 
$cl  = $dbr->tableName('categorylinks');
 
$cat  = $dbr->addQuotes($wgRecordAdminCategory);
 
$res  = $dbr->select($cl, 'cl_from', "cl_to = $cat", __METHOD__, array('ORDER BY' => 'cl_sortkey'));
 
while ($row = $dbr->fetchRow($res)) $options .= '<option>'.Title::newFromID($row[0])->getText().'</option>';
 
 
 
# Render type-selecting form
 
$wgOut->addHTML(wfElement('form', array('action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null)
 
. "<select name='wpType'>$options</select> "
 
. wfElement('input', array('type' => 'submit', 'value' => 'Submit'))
 
. '</form>'
 
);
 
}
 
 
# Record type known, but no record selected, render form for searching or creating
 
elseif (empty($record)) {
 
$wgOut->addWikiText("== Find or Create a \"$type\" record ==\n");
 
 
# Process Create submission
 
if (count($posted) && $wgRequest->getText('wpCreate')) {
 
if (empty($wpTitle)) {
 
$wpTitle = $this->guid;
 
if ($wgRecordAdminUseNamespaces) $wpTitle = "$type:$wpTitle";
 
}
 
$t = Title::newFromText($wpTitle);
 
if (is_object($t)) {
 
if ($t->exists()) $wgOut->addHTML("<div class='errorbox'>Sorry, \"$wpTitle\" already exists!</div>\n");
 
else {
 
 
 
# Attempt to create the article
 
$article = new Article($t);
 
$summary = "[[Special:RecordAdmin/$type|RecordAdmin]]: New $type created";
 
$text = '';
 
foreach ($posted as $k => $v) if ($v) {
 
if ($this->types[$k] == 'bool') $v = 'yes';
 
$text .= "| $k = $v\n";
 
}
 
$text = $text ? "{{"."$type\n$text}}" : "{{"."$type}}";
 
$success = $article->doEdit($text, $summary, EDIT_NEW);
 
 
# Report success or error
 
if ($success) $wgOut->addHTML("<div class='successbox'>\"$wpTitle\" created successfully</div>\n");
 
else $wgOut->addHTML("<div class='errorbox'>An error occurred while attempting to create the $type!</div>\n");
 
}
 
} else $wgOut->addHTML("<div class='errorbox'>Bad title!</div>\n");
 
$wgOut->addHTML("<br><br><br><br>\n");
 
}
 
 
# Populate the search form with any posted values
 
$this->populateForm($posted);
 
 
# Render the form
 
$wgOut->addHTML(
 
wfElement('form', array('class' => 'recordadmin', 'action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null)
 
.'<b>Record ID:</b> '.wfElement('input', array('name' => 'wpTitle', 'size' => 30, 'value' => $wpTitle))
 
.'&nbsp;&nbsp;&nbsp;'.wfElement('input', array('name' => 'wpInvert', 'type' => 'checkbox')).' Invert selection'
 
."\n<br><br><hr><br>\n{$this->form}"
 
.wfElement('input', array('type' => 'hidden', 'name' => 'wpType', 'value' => $type))
 
.'<br><hr><br><table width="100%"><tr>'
 
.'<td>'.wfElement('input', array('type' => 'submit', 'name' => 'wpFind', 'value' => "Search")).'</td>'
 
.'<td>'.wfElement('input', array('type' => 'submit', 'name' => 'wpCreate', 'value' => "Create")).'</td>'
 
.'<td width="100%" align="left">'.wfElement('input', array('type' => 'reset', 'value' => "Reset")).'</td>'
 
.'</tr></table></form>'
 
);
 
 
# Process Find submission
 
if (count($posted) && $wgRequest->getText('wpFind')) {
 
$wgOut->addWikiText("<br>\n== Search results ==\n");
 
 
 
# Select records which use the template and exhibit a matching title and other fields
 
$records = array();
 
$dbr  = &wfGetDB(DB_SLAVE);
 
$tbl  = $dbr->tableName('templatelinks');
 
$ty  = $dbr->addQuotes($type);
 
$res  = $dbr->select($tbl, 'tl_from', "tl_namespace = 10 AND tl_title = $ty", __METHOD__);
 
while ($row = $dbr->fetchRow($res)) {
 
$t = Title::newFromID($row[0]);
 
if (empty($wpTitle) || eregi($wpTitle, $t->getPrefixedText())) {
 
$a = new Article($t);
 
$text = $a->getContent();
 
$match = true;
 
$r = array($t);
 
foreach (array_keys($this->types) as $k) {
 
$v = isset($posted[$k]) ? ($this->types[$k] == 'bool' ? 'yes' : $posted[$k]) : '';
 
$i = preg_match("|\s*\|\s*$k\s*=\s*(.*?)\s*(?=[\|\}])|si", $text, $m);
 
if ($v && !($i && eregi($v, $m[1]))) $match = false;
 
$r[$k] = isset($m[1]) ? $m[1] : '';
 
}
 
if ($invert) $match = !$match;
 
if ($match) $records[$t->getPrefixedText()] = $r;
 
}
 
}
 
$dbr->freeResult($res);
 
 
 
# Render search results
 
if (count($records)) {
 
 
# Pass1, scan the records to find the create date of each and sort by that
 
$sorted = array();
 
foreach ($records as $k => $r) {
 
$t = $r[0];
 
$id = $t->getArticleID();
 
$r[1] = $k;
 
$tbl = $dbr->tableName('revision');
 
$row = $dbr->selectRow(
 
$tbl,
 
'rev_timestamp',
 
"rev_page = $id",
 
__METHOD__,
 
array('ORDER BY' => 'rev_timestamp')
 
);
 
$sorted[$row->rev_timestamp] = $r;
 
}
 
krsort($sorted);
 
 
$table = "<table class='sortable recordadmin $type-record'>\n<tr>
 
          <th class='col1'>$type<br></th><th class='col2'>Created<br></th>";
 
foreach (array_keys($this->types) as $k) $table .= "<th class='col$k'>$k<br></th>";
 
$table .= "</tr>\n";
 
$stripe = '';
 
foreach ($sorted as $ts => $r) {
 
$ts = preg_replace('|^..(..)(..)(..)(..)(..)..$|', '$3/$2/$1&nbsp;$4:$5', $ts);
 
$t = $r[0];
 
$k = $r[1];
 
$stripe = $stripe ? '' : ' class="stripe"';
 
$table .= "<tr$stripe><td class='col1'>(<a href='".$t->getLocalURL()."'>view</a>)";
 
$table .= "(<a href='".$title->getLocalURL("wpType=$type&wpRecord=$k")."'>edit</a>)</td>\n";
 
$table .= "<td class='col2'>$ts</td>\n";
 
$i = 0;
 
foreach (array_keys($this->types) as $k) {
 
$v = isset($r[$k]) ? $r[$k] : '&nbsp;';
 
$table .= "<td class='col$k'>$v</td>";
 
}
 
$table .= "</tr>\n";
 
}
 
$table .= "</table>\n";
 
$wgOut->addHTML($table);
 
} else $wgOut->addWikiText("''No matching records found!''\n");
 
}
 
}
 
 
# A specific record has been selected, render form for updating
 
else {
 
$wgOut->addWikiText("== Editing \"$record\" ==\n");
 
$article = new Article(Title::newFromText($record));
 
$text = $article->fetchContent();
 
 
 
# Update article if form posted
 
if (count($posted)) {
 
 
# Get the location and length of the record braces to replace
 
foreach ($this->examineBraces($text) as $brace) if ($brace['NAME'] == $type) $braces = $brace;
 
 
 
# Attempt to save the article
 
$summary = "[[Special:RecordAdmin/$type|RecordAdmin]]: $type properties updated";
 
$replace = '';
 
foreach ($posted as $k => $v) if ($v) {
 
if ($this->types[$k] == 'bool') $v = 'yes';
 
$replace .= "| $k = $v\n";
 
}
 
$replace = $replace ? "{{"."$type\n$replace}}" : "{{"."$type}}";
 
$text = substr_replace($text, $replace, $braces['OFFSET'], $braces['LENGTH']);
 
$success = $article->doEdit($text, $summary, EDIT_UPDATE);
 
 
# Report success or error
 
if ($success) $wgOut->addHTML("<div class='successbox'>$type updated successfully</div>\n");
 
else $wgOut->addHTML("<div class='errorbox'>An error occurred during update!</div>\n");
 
$wgOut->addHTML("<br><br><br><br>\n");
 
}
 
 
# Populate the form with the current values in the article
 
foreach ($this->examineBraces($text) as $brace) if ($brace['NAME'] == $type) $braces = $brace;
 
$this->populateForm(substr($text, $braces['OFFSET'], $braces['LENGTH']));
 
 
# Render the form
 
$wgOut->addHTML(wfElement('form', array('class' => 'recordadmin', 'action' => $title->getLocalURL('action=submit'), 'method' => 'post'), null));
 
$wgOut->addHTML($this->form);
 
$wgOut->addHTML(wfElement('input', array('type' => 'hidden', 'name' => 'wpType', 'value' => $type)));
 
$wgOut->addHTML(wfElement('input', array('type' => 'hidden', 'name' => 'wpRecord', 'value' => $record)));
 
$wgOut->addHTML('<br><hr><br><table width="100%"><tr>'
 
.'<td>'.wfElement('input', array('type' => 'submit', 'value' => "Save")).'</td>'
 
.'<td width="100%" align="left">'.wfElement('input', array('type' => 'reset', 'value' => "Reset")).'</td>'
 
.'</tr></table></form>'
 
);
 
}
 
}
 
 
 
/**
 
* Read in and prepare the form (for use as a search filter) for passed record type
 
* - we're using the record's own form as a filter for searching for records
 
* - extract only the content from between the form tags and remove any submit inputs
 
*/
 
function preProcessForm($type) {
 
$title = Title::newFromText($type, NS_FORM);
 
if ($title->exists()) {
 
$form = new Article($title);
 
$form = $form->getContent();
 
$form = preg_replace('#<input.+?type=[\'"]?submit["\']?.+?/(input| *)>#', '', $form);    # remove submits
 
$form = preg_replace('#^.+?<form.+?>#s', '', $form);                                    # remove up to and including form open
 
$form = preg_replace('#</form>.+?$#s', '', $form);                                      # remove form close and everything after
 
$form = preg_replace('#name\s*=\s*([\'"])(.*?)\\1#s', 'name="ra_$2"', $form);            # prefix input names with ra_
 
$form = preg_replace('#(<select.+?>)\s*(?!<option/>)#s', '$1<option selected/>', $form); # ensure all select lists have default blank
 
}
 
 
# Create a red link to the form if it doesn't exist
 
else {
 
$form = "<b>There is no form associated with \"$type\" records!</b>"
 
      ."<br><br>click <a href=\"".$title->getLocalURL('action=edit')."\">here</a> to create one</div>";
 
}
 
$this->form = $form;
 
}
 
 
 
 
 
/**
 
* Populates the form values from the passed values
 
* - $form is HTML text
 
* - $values may be a hash or wikitext template syntax
 
*/
 
function populateForm($values) {
 
 
# If values are wikitext, convert to hash
 
if (!is_array($values)) {
 
$text = $values;
 
$values = array();
 
preg_match_all("|\|\s*(.+?)\s*=\s*(.*?)\s*(?=[\|\}])|s", $text, $m);
 
foreach ($m[1] as $i => $k) $values[$k] = $m[2][$i];
 
}
 
 
 
# Add the values into the form's HTML depending on their type
 
foreach($this->types as $k => $type) {
 
 
 
# Get this input element's html text and position and length
 
preg_match("|<([a-zA-Z]+)[^<]+?name=\"ra_$k\".*?>(.*?</\\1>)?|s", $this->form, $m, PREG_OFFSET_CAPTURE);
 
list($html, $pos) = $m[0];
 
$len = strlen($html);
 
 
 
# Modify the element according to its type
 
# - clears default value, then adds new value
 
$v = isset($values[$k]) ? $values[$k] : '';
 
switch ($type) {
 
case 'text':
 
$html = preg_replace("|value\s*=\s*\".*?\"|", "", $html);
 
if ($v) $html = preg_replace("|(/?>)$|", " value=\"$v\" $1", $html);
 
break;
 
case 'bool':
 
$html = preg_replace("|checked|", "", $html);
 
if ($v) $html = preg_replace("|(/?>)$|", " checked $1", $html);
 
break;
 
case 'list':
 
$html = preg_replace("|(<option[^<>]*) selected|", "$1", $html);
 
if ($v) $html = preg_replace("|(?<=<option)(?=>$v</option>)|s", " selected", $html);
 
break;
 
case 'blob':
 
$html = preg_replace("|>.*?(?=</textarea>)|s", ">$v", $html);
 
break;
 
}
 
 
# Replace the element in the form with the modified html
 
$this->form = substr_replace($this->form, $html, $pos, $len);
 
}
 
}
 
 
 
/**
 
* Returns an array of types used by the passed HTML text form
 
* - supported types, text, select, checkbox, textarea
 
*/
 
function examineForm() {
 
$this->types = array();
 
preg_match_all("|<([a-zA-Z]+)[^<]+?name=\"ra_(.+?)\".*?>|", $this->form, $m);
 
foreach ($m[2] as $i => $k) {
 
$tag = $m[1][$i];
 
$type = preg_match("|type\s*=\s*\"(.+?)\"|", $m[0][$i], $n) ? $n[1] : '';
 
switch ($tag) {
 
case 'input':
 
switch ($type) {
 
case 'checkbox':
 
$this->types[$k] = 'bool';
 
break;
 
default:
 
$this->types[$k] = 'text';
 
break;
 
}
 
break;
 
case 'select':
 
$this->types[$k] = 'list';
 
break;
 
case 'textarea':
 
$this->types[$k] = 'blob';
 
break;
 
}
 
}
 
}
 
 
 
/**
 
* Return array of braces used and the name, position, length and depth
 
* See http://www.organicdesign.co.nz/MediaWiki_code_snippets
 
*/
 
function examineBraces(&$content) {
 
$braces = array();
 
$depths = array();
 
$depth = 1;
 
$index = 0;
 
while (preg_match('/\\{\\{\\s*([#a-z0-9_]*)|\\}\\}/is', $content, $match, PREG_OFFSET_CAPTURE, $index)) {
 
$index = $match[0][1]+2;
 
if ($match[0][0] == '}}') {
 
$brace =& $braces[$depths[$depth-1]];
 
$brace['LENGTH'] = $match[0][1]-$brace['OFFSET']+2;
 
$brace['DEPTH']  = $depth--;
 
}
 
else {
 
$depths[$depth++] = count($braces);
 
$braces[] = array(
 
'NAME'  => $match[1][0],
 
'OFFSET' => $match[0][1]
 
);
 
}
 
}
 
return $braces;
 
}
 
 
 
/**
 
* A callback for processing public forms
 
*/
 
function createRecord() {
 
global $wgRequest, $wgRecordAdminUseNamespaces;
 
$type = $wgRequest->getText('wpType');
 
$title = $wgRequest->getText('wpTitle');
 
 
 
# Get types in this kind of record from form
 
$this->preProcessForm($type);
 
$this->examineForm();
 
 
# Use guid if no title specified
 
if (empty($title)) {
 
$title = $this->guid;
 
if ($wgRecordAdminUseNamespaces) $title = "$type:$title";
 
}
 
 
 
# Attempt to create the article
 
$title = Title::newFromText($title);
 
if (is_object($title) && !$title->exists()) {
 
$article = new Article($title);
 
$summary = "New $type created from public form";
 
$text = '';
 
foreach ($_POST as $k => $v) if ($v && isset($this->types[$k])) {
 
if ($this->types[$k] == 'bool') $v = 'yes';
 
$text .= "| $k = $v\n";
 
}
 
$text = $text ? "{{"."$type\n$text}}" : "{{"."$type}}";
 
$success = $article->doEdit($text, $summary, EDIT_NEW);
 
}
 
}
 
 
 
# If a record was created by a public form, make last 5 digits of ID available via a tag
 
function expandTag($text, $argv, &$parser) {
 
$parser->mOutput->mCacheTime = -1;
 
return $this->guid ? substr($this->guid, -5) : '';
 
}
 
 
 
}
 
 
 
/**
 
* Called from $wgExtensionFunctions array when initialising extensions
 
*/
 
function wfSetupRecordAdmin() {
 
global $wgSpecialRecordAdmin, $wgParser, $wgLanguageCode, $wgMessageCache, $wgRequest;
 
 
 
# Add the messages used by the specialpage
 
if ($wgLanguageCode == 'en') {
 
$wgMessageCache->addMessages(array(
 
'recordadmin' => 'Record administration'
 
));
 
}
 
 
 
# Make a global singleton so methods are accessible as callbacks etc
 
$wgSpecialRecordAdmin = new SpecialRecordAdmin();
 
 
 
# Make recordID's of articles created with public forms available via recordid tag
 
$wgParser->setHook('recordid', array($wgSpecialRecordAdmin, 'expandTag'));
 
 
 
# Check if posting a public creation form
 
$title = Title::newFromText($wgRequest->getText('title'));
 
if (is_object($title) && $title->getNamespace() != NS_SPECIAL && $wgRequest->getText('wpType') && $wgRequest->getText('wpCreate'))
 
$wgSpecialRecordAdmin->createRecord();
 
 
 
# Add the specialpage to the environment
 
SpecialPage::addPage($wgSpecialRecordAdmin);
 
}
 

Revision as of 22:50, 10 November 2008


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.