BugSquish

From TestWiki
Jump to: navigation, search
This extension is not currently installed on this wiki.
BugSquish.png

This extension checks your bug tracker installation and adds a strikethrough to interwiki links for closed bugs. BugSquish currently supports the Bugzilla, RT and Trac bug trackers, running on either MySQL or PostgreSQL databases.


Changes[edit]

MediaWiki logo.png
This extension is also documented at
Extension:BugSquish
on MediaWiki.org.

If the pages are not in sync. then this version on my test wiki is always the most up-to-date.

  • 2018-09-13: Updated the code to use the 'mysqli' PHP extension, rather than the older 'mysql' extension. This improves compatibility with PHP 7 and above. The 'mysql' extension is still used if 'mysqli' is unavailable (to ensure this doesn't break backwards-compatibility with old PHP/MediaWiki versions).
  • 2016-02-07: Fixed an issue whereby the CSS was not being output on MediaWiki >= 1.19.
  • 2012-01-08: Fixed a bug whereby the extension would cause CSS styles added by other extensions to be lost.
  • 2011-10-18: Added the CSS styling to the extension, so it is available by default, therefore you no longer need to add it manually. You can still over-ride it in MediaWiki:Common.css, though.
  • 2011-10-02: BugSquish now supports Trac. Thanks to Braden Ehrat for the patch.
  • 2009-11-12: Updated extension so that it now works correctly on MW 1.14 and above.

Credits[edit]

© Copyright 2006-2016, Mark Clements. Released under a creative-commons CC BY-SA 2.5 (or later) license.

Many thanks to Dax Kelson for providing the RT and PostgreSQL code and to Braden Ehrat for integrating with Trac.

Installation[edit]

Simply copy and paste the code (below) into a new file in your 'extensions' directory and include() it from your LocalSettings.php file in the usual way.

Requirements[edit]

Firstly, links to your bug tracker need to be setup in your interwiki table. For example you might setup an interwiki link called bug with the URL http://www.example.com/bugzilla/show_bug.cgi?id=$1 (though see known issues, below, if you plan to use this prefix). This means you can enter the wiki code [[bug:2314]] to create a link to bug 2314 in your Bugzilla install (resulting in a link such as: bug:2314). For links to other bug-trackers, the URL format will change, but the principal is the same.

Note that setting up links to your bug-tracker in this manner is something that MediaWiki supports natively - it is not functionality added by this extension. BugSquish simply adds the ability to shown these links crossed out (or otherwise styled) based on their status.

For BugSquish to work are you need to have an interwiki prefix that accepts just a single number, and that the DB containing the bugs must be accessible locally (though the code is designed to allow for alternative access to the bug tracker data (e.g. via WebDAV)... once someone has written the necessary code!).

Multiple interwiki prefixes to different bug tracker installations can be defined on the same wiki.

Configuration[edit]

For each interwiki link that you want to use, you need to add an entry into $wgBugSquishSources. Each entry has the following required elements:

  • interwiki => The interwiki prefix this definition applies to.
  • type => The type of connection. Currently only BUGSQUISH_DBConnect is valid for this field.
  • tracker => The type of bug tracker you are linking to. Currently the following values are recognised: "bugzilla", "rt" and "trac".

The following elements are only required if necessary:

  • prefix => DB prefix (as setup in your tracker installation - can be omitted if this does not apply to your tracker, or no prefix is being used).


The following optional elements can be used to define the database connection. If any of the elements are omitted, then the values used for your MediaWiki installation are used. Therefore if Bugzilla shares a database with MediaWiki (and the MediaWiki user has appropriate access privileges) then you can omit all of the settings. If not, then fill in anything that differs from your MediaWiki install.

  • dbtype => The database type. Currently only "mysql" and "postgresql" are supported.
  • host => The database host.
  • database => The database name.
  • user => Username for DB connection.
  • password => Password for DB connection.

Examples[edit]

Example 1 - shared DB[edit]

If MediaWiki and Bugzilla are installed in a single DB, then you only need to specify a minimal set of components:

include("extensions/BugSquish.php");
 
$wgBugSquishSources[] = array(
	'interwiki'	=> "bug",
	'type'		=> BUGSQUISH_DBConnect,
    'tracker'   => "bugzilla",
	'prefix'	=> "bugzilla_",
);

Example 2 - separate DB[edit]

If Bugzilla is stored in a separate DB, then you need to supply details. In most cases 'host' will be the same as for MediaWiki, in which case it can be omitted (as it has been here). Also if no table prefix is used in your Bugzilla install, then that field can be omitted.

include("extensions/BugSquish.php");
 
$wgBugSquishSources[] = array(
	'interwiki' => "bug",
	'type'		=> BUGSQUISH_DBConnect,
    'tracker'   => "bugzilla",
	'database'	=> "dbname",
	'user'		=> "dbuser",
	'password'	=> "dbpass",
);

Known issues[edit]

  • Doesn't handle interwiki links created dynamically via templates, (e.g. [[bug:{{{1}}}]]. It should handle links that don't contain variables though - let me know if you spot any other problems with transclusion, however!
  • In MediaWiki v1.6 the 'Buginese' language was added, which has the ISO code 'bug'. This means that in the default configuration for version 1.6 upwards, you can't use 'bug' for the links (or rather, you can, but they appear in the left side-bar rather than in the text as expected). There are two work-arounds to this.
    1. Use a different interwiki prefix.
    2. If you don't use inter-language links (and if you're unsure, you probably don't) then you can disable them by setting $wgInterwikiMagic to false in your LocalSettings.php file.

Source Code[edit]

Live sourcecode viewer: BugSquish.php
Last modified: 2018-09-13 09:52:43

<?php
if (!defined('MEDIAWIKI'))
	die("MediaWiki extensions cannot be run directly.");
/**
 * An extension that checks your Bugzilla installation
 * and adds a strikethrough to interwiki links for closed bugs.
 *
 * @package MediaWiki
 * @subpackage Extensions
 *
 * @author Mark Clements <mclements at kennel17 dot co dot uk>
 * @copyright copyright  2006-2018, Mark Clements.
 * @license http://creativecommons.org/licenses/by-sa/2.5/ cc-by-sa 2.5 or later
 * @version $Rev: 1495 $
 */
 
///////////////////////////////////// SETUP
 
// Setup version number
	$pMCExt_Version = '$Rev: 1495 $';
	$pMCExt_Version = substr($pMCExt_Version, 6, -2);
// Setup extension credits
	$wgExtensionCredits['other'][] = array(
		'name' => "BugSquish",
		'version' => "r" . $pMCExt_Version,
		'author' => "Mark Clements<br><i>(with contributions from "
				  . "Dax Kelson (RT and PostgreSQL) and Braden Ehrat (Trac))</i>",
		'description' => "For striking-out links to closed bugs",
		'url' => "http://www.kennel17.co.uk/testwiki/BugSquish",
	);
// Tidy up
	unset($pMCExt_Version);
 
// Initialise extension.
	$wgExtensionFunctions[] = "wfBugSquish";
 
	if (!isset($wgBugSquishSources))
		$wgBugSquishSources = array();
 
// Constants for the 'type' variable in the sources array.
// Only 1 so far.
	define("BUGSQUISH_DBConnect", 1);
 
	function wfBugSquish() {
		global $wgHooks, $wgVersion;
 
	// Application hooks
	// If v1.14 or above, we need to use the LinkBegin/LinkEnd hooks to style
	// the individual links.
	// For earlier versions we need to parse the whole page using ParserAfterStrip
	// (which has been 'deprecated' from v1.14 onwards, and now behaves the same as
	// ParserBeforeStrip, i.e. it comments/pre/nowiki/etc. are still in place, so
	// can no longer be used for our purposes on these versions).
		if (version_compare($wgVersion, '1.14.0', '<'))
			$wgHooks['ParserAfterStrip'][] = "wfBugSquish_ParserAfterStrip";
		else {
			$wgHooks['LinkBegin'][] = "wfBugSquish_LinkBegin";
			$wgHooks['LinkEnd'][] = "wfBugSquish_LinkEnd";
		}
 
	// Add some default CSS styling.
	// In early versions of MediaWiki, this was handled via the
	// SkinTemplateSetupPageCss hook, but this was removed in MW 1.19, with the
	// correct way now being to add the CSS in the BeforePageDisplay hook.
	// This hook was introduced in MW 1.7, but it wasn't until some later version
	// that the necessary addInlineStyle() method was added to the OutputPage class
	// (it is present in 1.17, but not sure when it was introduced).
	// We use the old method if that method is not available, or the new method if
	// it is.
		if (method_exists("OutputPage", "addInlineStyle"))
			$wgHooks['BeforePageDisplay'][] = "wfBugSquish_AddCSS";
		else
			$wgHooks['SkinTemplateSetupPageCss'][] = "wfBugSquish_AddCSS";
	}
 
/////////////////////////////////////  HOOKS
 
// Method 1: Parse the page (ParserAfterStrip)
// This function takes in the whole text of the article and wraps any links to
// a defined bug source in appropriate markup to style them correctly based on the
// specified bug's status.  The result is the article text with the above
// modifications, ready to be put through the MW parser (i.e. links are still in
// the wikitext format, e.g. "[[bug:5]]").
// In MW < 1.14, ParserAfterStrip receives the full article text after <nowiki>,
// <pre> and other such tags have been stripped out, which means we can update the
// links safe in the knowledge that anything that shouldn't be altered (e.g. because
// it is inside a nowiki block) will not be affected.
// In version 1.14, this behaviour changed, so that we get the full text including
// the nowiki content, therefore we were affecting these items that we shouldn't be
// affecting.  This function is therefore not used in 1.14 and above (we use the
// LinkBegin/LinkEnd hooks instead).
	function wfBugSquish_ParserAfterStrip(&$parser, &$text, &$strip_state) {
		global $wgBugSquishSources;
 
		foreach ($wgBugSquishSources as $Key => $Value) {
			$Regex = '/(\[\[' . $Value['interwiki'] . ':)([^]|]*)((?:|.*)?\]\])/ei';
			$NewText = preg_replace($Regex, "wfSquishBug('\\1','\\2','\\3','" . $Key
					 . "')", $text);
 
		// If changes were made to the page text then we want to disable caching for
		// the page, so that it is automatically updated when the DB changes.
			if ($NewText != $text)
				$parser->disableCache();
 
		// Replace the original article text with the modified text, before
		// continuing to loop through any remaining bug sources.
			$text = $NewText;
		}
 
		return true;
	}
 
// Method 2: Parse individual links (LinkBegin/LinkEnd)
// In MW 1.14 and above, these two hooks are run before and after a link is created.
// We use LinkBegin to set a flag on items that need parsing, and then LinkEnd
// to actually wrap up the link once it has been created.
	function wfBugSquish_LinkBegin($skin, $target, &$text, &$customAttribs, &$query,
								   &$options, &$ret)
	{
		global $wgBugSquishSources;
 
		if ($target->mInterwiki) {
			foreach ($wgBugSquishSources as $Key => $Value) {
				if ($target->mInterwiki == $Value['interwiki']) {
				// Set some custom attributes so that LinkEnd knows what action needs
				// to be performed for this link.
					$customAttribs['BugSquishSource'] = $Key;
					$customAttribs['BugSquishID'] = $target->mTextform;
 
				// If we've found the source of the link, break out of the loop - no
				// point checking any other sources.
					break;
				}
			}
		}
		return true;
	}
 
	function wfBugSquish_LinkEnd($skin, $target, $options, &$text, &$attribs, &$ret)
	{
		global $wgParser;
 
	// If this is a bugsquish link, then process it.
		if (isset($attribs['BugSquishSource'])) {
		// Disable parser cache, so that links are updated when the bug database
		// changes.
			$wgParser->disableCache();
 
		// Retrieve the source/ID of the bug from the attributes...
			$Source = $attribs['BugSquishSource'];
			$ID = $attribs['BugSquishID'];
		// ...and remove them so they are not output to the HTML.
			unset($attribs['BugSquishSource'], $attribs['BugSquishID']);
 
		// Generate the wrapping for the bug.  We use the asterisks to ensure
		// we match the bug string correctly when we replace it with the actual
		// link.
			$ret = wfSquishBug("**", $ID, "**", $Source);
 
		// Create the link.
			$Link = Xml::openElement('a', $attribs) . $text . Xml::closeElement('a');
 
		// Replace the dummy string (**ID**) with the actual link in order
		// to get the full HTML string we want to output for this link.
			$ret = str_replace("**" . $ID . "**", $Link, $ret);
 
		// Return false to stop MW doing any further processing of this link.
			return false;
		}
 
		return true;
	}
 
// wfBugSquish_AddCSS()
// Adds the extension's default styling to the skin's CSS output.
// Currently done in-line, to keep the extension self-contained.
	function wfBugSquish_AddCSS(&$Output) {
		$NewCSS = <<<EOF
 
.bsBugLink a {
	font-size: 0.9em;
	font-style: normal;
	font-weight: normal;
	border: 1px solid #E0E0FF;
	background-color: #F0F0FF !important;
 
	margin: 0 0.2em;
	padding: 0 0.5em !important;
 
	border-radius: 5px;
	-moz-border-radius: 5px;
	-khtml-border-radius: 5px;
	-webkit-border-radius: 5px;
}
 
.bsBugLink a:hover {
	border-color: #9999FF;
	background-color: #D0E0FF !important;
	color: #0000FF !important;
	text-decoration: none;
}
 
s.bsFixedBug {
	text-decoration: none !important;
}
 
.bsFixedBug a, .bsFixedBug a:hover {
	text-decoration: line-through;
}
 
EOF;
 
	// If $Output is an object, then this is being called from the BeforePageDisplay
	// hook, in which case we can add the CSS directly to the object.
		if (is_object($Output)) {
			$Output->addInlineStyle($NewCSS);
		}
	// Otherwise, this is the string of output text, and we need to explicitly
	// add the CSS to that string.
		else {
		// As of 1.16 the CSS data is automatically wrapped in CDATA tags when
		// necessary.  For earlier versions, we do this ourselves.  The method for
		// applying this has been copied from Html::inlineStyle().  As the class
		// was added in 1.16 we can simply check for its existence, rather than doing
		// a version check.
			if (class_exists("Html") && preg_match( '/[<&]/', $NewCSS)) {
				$NewCSS = "/*<![CDATA[*/\n" . $NewCSS . "\n/*]]>*/";
			}
 
		// Add the CSS to the output.  We include a new-line so that multiple additions
		// are separated out clearly.
			$Output .= "\n" . $NewCSS;
		}
 
	// Return true, to allow other hooks to run.
		return true;
	}
 
/////////////////////////////////////  INTERNALS
 
	define("pBUGSQUISH_NoConnection", 	0);
	define("pBUGSQUISH_QueryError", 	1);
	define("pBUGSQUISH_BugNotFound", 	2);
	define("pBUGSQUISH_BadType", 		3);
	define("pBUGSQUISH_DBNotFound", 	4);
	define("pBUGSQUISH_BadDBType", 		5);
	define("pBUGSQUISH_BadTrackerType",	6);
 
	function wfSquishBug($Pre, $ID, $Post, $Key) {
		global $wgBugSquishSources;
 
		$Source = $wgBugSquishSources[$Key];
		if (!isset($Source['type']))
			$Source['type'] = "";
 
		switch ($Source['type']) {
			case BUGSQUISH_DBConnect:
				$BugData = wfSquishBug_GetDataFromDB($ID, $Key);
				break;
			default:
				$BugData = pBUGSQUISH_BadType;
				break;
		}
 
		$Result = $Pre . $ID . $Post;
		$Class = "bsBugLink";
 
	// Error occurred.  Indicate this in the CSS class.
		if (!is_array($BugData)) {
			$Class .= " bsError";
			switch ($BugData) {
				case pBUGSQUISH_NoConnection:
					$Class .= " bsError-NoConnection";
					break;
				case pBUGSQUISH_QueryError:
					$Class .= " bsError-QueryError";
					break;
				case pBUGSQUISH_BugNotFound:
					$Class .= " bsError-BugNotFound";
					break;
				case pBUGSQUISH_BadType:
					$Class .= " bsError-BadConnectionType";
					break;
				case pBUGSQUISH_DBNotFound:
					$Class .= " bsError-DBNotFound";
					break;
				case pBUGSQUISH_BadDBType:
					$Class .= " bsError-BadDBType";
					break;
				case pBUGSQUISH_BadTrackerType:
					$Class .= " bsError-BadTrackerType";
					break;
			}
		}
 
	// Otherwise, cross out the fixed bugs, and add the current
	// bug status to the CSS class.
		else {
			if ($BugData['IsClosed'])
				 $Result = '<s class="bsFixedBug">' . $Result . '</s>';
 
			if (isset($BugData['Status']) && $BugData['Status'] != "")
				$Class .= " bsStatus_" . $BugData['Status'];
			if (isset($BugData['Resolution']) && $BugData['Resolution'] != "")
				$Class .= " bsResolution_" . $BugData['Resolution'];
			if (isset($BugData['Owner']) && $BugData['Owner'] != "")
				$Class .= " bsOwner_" . $BugData['Owner'];
 
		// NOTE: Although 'Summary' is returned in the $BugData array, it is
		//		 currently unused.  This is for two reasons.  Firstly, the best
		//		 place for it would be as a tooltip (title="..." attribute), but
		//		 this will be over-ridden by the tooltip for the <a> tag so would
		//		 require extra parsing of the $Pre text in order to replace the
		//		 existing value.  In addition, in situations where an alternative
		//		 text is used for the link (e.g. [[bug:152|click here]]), this
		//		 tooltip is the only place where the user can see the actual bug ID
		//		 being referenced, so we probably don't want to replace it (though I
		//		 guess adding the summary to the end of the title would work).
		//		 The second reason is that the summary may contain sensitive
		//		 information e.g. "Give Jake a $5000 raise on next payroll" which we
		//		 shouldn't be exposing. If you are happy with exposing this
		//		 information to all users of your wiki, then feel free to hack away,
		//		 but I don't plan to add this natively to the extension for security
		//		 reasons (or at least, if I ever do it will be something that will
		//		 be disabled by default).
		}
 
	// Return the result wrapped in a span tag, with the appropriate classes.
		return '<span class="' . $Class . '">' . $Result . '</span>';
	}
 
	function wfSquishBug_GetDataFromDB($ID, $Key) {
		global $wgDBtype, $wgDBserver, $wgDBname, $wgDBuser, $wgDBpassword;
		global $wgBugSquishSources;
 
		$Source = $wgBugSquishSources[$Key];
 
		if (!isset($Source['dbtype']) || $Source['dbtype'] == "")
			$Source['dbtype'] = $wgDBtype;
		if (!isset($Source['host']) || $Source['host'] == "")
			$Source['host'] = $wgDBserver;
		if (!isset($Source['database']) || $Source['database'] == "")
			$Source['database'] = $wgDBname;
		if (!isset($Source['user']) || $Source['user'] == "") {
			$Source['user'] = $wgDBuser;
			$Source['password'] = $wgDBpassword;
		}
		elseif (!isset($Source['password']))
			$Source['password'] = "";
		if (!isset($Source['prefix']))
			$Source['prefix'] = "";
 
	// To avoid risk of SQL injection, we convert $ID to an integer.  This means
	// that we can't currently support trackers that use a non-numeric ID.
		$ID = intval($ID);
 
		switch ($Source['dbtype']) {
			case 'mysql':
			// Use the MySQLi extension if available (all modern versions of PHP)
			// of the MySQL extension if not (old versions of PHP).
				if (function_exists("mysqli_connect"))
					$FuncName = "wfSquishBug_GetStatusFromDB_MySQLi";
				else
					$FuncName = "wfSquishBug_GetStatusFromDB_MySQL";
				$fnSplit = "SUBSTRING_INDEX";
				break;
			case 'postgresql':
				$FuncName = "wfSquishBug_GetStatusFromDB_PostgreSQL";
				$fnSplit = "split_part";
				break;
			default:
				return pBUGSQUISH_BadDBType;
				break;
		}
 
		switch ($Source['tracker']) {
			case 'bugzilla':
				$SQL = "SELECT resolution, short_desc, bug_status FROM "
					 . $Source['prefix'] . "bugs" . " WHERE bug_id = " . $ID . ";";
				break;
			case 'trac':
				$SQL = "SELECT status, resolution, summary FROM "
					 . $Source['prefix'] . "ticket" . " WHERE id = " . $ID . ";";
				break;
			case 'rt':
				$SQL = "SELECT status, subject, " . $fnSplit . "("
					. $Source['prefix'] . "users.name,'@',1) AS owner" . " FROM "
					. $Source['prefix'] . "tickets, " .  $Source['prefix'] . "users"
					. " WHERE " . $Source['prefix'] . "tickets.owner = "
					.  $Source['prefix'] . "users.id AND " . $Source['prefix']
					. "tickets.id = " . $ID;
				break;
			default:
				return pBUGSQUISH_BadTrackerType;
		}
 
		$Row = call_user_func($FuncName, $SQL, $Source['host'], $Source['database'],
							  $Source['user'], $Source['password']);
 
		if (!is_array($Row))
			return $Row;
 
		$ReturnVal = array();
		switch ($Source['tracker']) {
			case 'bugzilla':
				if ($Row['resolution'] == "")
					$ReturnVal['IsClosed'] = false;
				else
					$ReturnVal['IsClosed'] = true;
 
				$ReturnVal['Summary'] = $Row['short_desc'];
				$ReturnVal['Status'] = $Row['bug_status'];
				$ReturnVal['Resolution'] = $Row['resolution'];
 
				break;
 
			case 'trac':
				if ($Row['status'] == "closed")
					$ReturnVal['IsClosed'] = true;
				else
					$ReturnVal['IsClosed'] = false;
				$ReturnVal['Status'] = $Row['status'];
				$ReturnVal['Resolution'] = $Row['resolution'];
				$ReturnVal['Summary'] = $Row['summary'];
 
				break;
 
			case 'rt':
				if (in_array($Row['status'],
							 array('resolved', 'rejected', 'deleted'), true))
				{
						$ReturnVal['IsClosed'] = true;
						$ReturnVal['Resolution'] = $Row['status'];
						$ReturnVal['Status'] = 'closed';
				}
				else {
						$ReturnVal['IsClosed'] = false;
						$ReturnVal['Status'] = $Row['status'];
				}
 
				$ReturnVal['Summary'] = $Row['subject'];
				$ReturnVal['Owner'] = $Row['owner'];
 
				break;
		}
 
		return $ReturnVal;
	}
 
	function wfSquishBug_GetStatusFromDB_MySQL($SQL, $Host, $DB, $User, $Pass) {
		$Link = @mysql_connect($Host, $User, $Pass, true);
		if (!$Link)
			return pBUGSQUISH_NoConnection;
 
		if (!mysql_select_db($DB, $Link)) {
			mysql_close($Link);
			return pBUGSQUISH_DBNotFound;
		}
 
		$Result = @mysql_query($SQL, $Link);
		if (!$Result) {
			mysql_close($Link);
			return pBUGSQUISH_QueryError;
		}
 
		$Row = mysql_fetch_assoc($Result);
 
		mysql_free_result($Result);
		mysql_close($Link);
 
		if ($Row)
			return $Row;
		else
			return pBUGSQUISH_BugNotFound;
	}
 
	function wfSquishBug_GetStatusFromDB_MySQLi($SQL, $Host, $DB, $User, $Pass) {
		$Link = @mysqli_connect($Host, $User, $Pass);
		if (!$Link)
			return pBUGSQUISH_NoConnection;
 
		if (!mysqli_select_db($Link, $DB)) {
			mysqli_close($Link);
			return pBUGSQUISH_DBNotFound;
		}
 
		$Result = @mysqli_query($Link, $SQL);
		if (!$Result) {
			mysqli_close($Link);
			return pBUGSQUISH_QueryError;
		}
 
		$Row = mysqli_fetch_assoc($Result);
 
		mysqli_free_result($Result);
		mysqli_close($Link);
 
		if ($Row)
			return $Row;
		else
			return pBUGSQUISH_BugNotFound;
	}
 
	function wfSquishBug_GetStatusFromDB_PostgreSQL($SQL, $Host, $DB, $User, $Pass) {
		$Link = @pg_pconnect("host=" . $Host . " user=" . $User . " password="
							 . $Pass . " dbname=" . $DB);
		if (!$Link)
			return pBUGSQUISH_NoConnection;
 
		$Result = @pg_query($Link, $SQL);
		if (!$Result) {
			pg_close($Link);
			return pBUGSQUISH_QueryError;
		}
 
		$Row = pg_fetch_assoc($Result);
 
		pg_free_result($Result);
		pg_close($Link);
 
		if ($Row)
			return $Row;
		else
			return pBUGSQUISH_BugNotFound;
	}