r98414 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r98413‎ | r98414 | r98415 >
Date:16:17, 29 September 2011
Author:mah
Status:deferred (Comments)
Tags:
Comment:
Add LilyPond module
Modified paths:
  • /trunk/extensions/LilyPond (added) (history)
  • /trunk/extensions/LilyPond/LilyPond.php (added) (history)

Diff [purge]

Index: trunk/extensions/LilyPond/LilyPond.php
@@ -0,0 +1,335 @@
 2+<?php
 3+/**
 4+ * MediaWiki extension: LilyPond
 5+ * =============================
 6+ *
 7+ * To activate, edit your LocalSettings.php, add
 8+ * require_once("$IP/extensions/LilyPond.php");
 9+ * and make sure that the images/ directory is writable.
 10+ *
 11+ * Example wiki code: <lilypond>\relative c' { c d e f g }</lilypond>
 12+ * If you want to typeset a fragment with clickable midi, use
 13+ * <lilymidi>...</lilymidi>
 14+ * If you want write a complete lilypond file, use
 15+ * <lilybook>...</lilybook>
 16+ *
 17+ * Tested with Lilypond version 2.12.2.
 18+ *
 19+ * @file
 20+ * @ingroup Extensions
 21+ * @version 0.01
 22+ * @author Johannes E. Schindelin
 23+ * @link http://www.mediawiki.org/wiki/Extension:LilyPond
 24+ */
 25+
 26+# User Settings
 27+
 28+# The following variables can be set in LocalSettings.php
 29+# before the line:
 30+# require_once("$IP/extensions/LilyPond.php");
 31+
 32+# You can set the variable $wgLilypond if you want/need to override the
 33+# path to the Lilypond executable. For example:
 34+# $wgLilypond = "/home/username/bin/lilypond";
 35+
 36+# Add a text link to prompt user to listen to midi, before and/or after
 37+# the image. Remember line breaks
 38+# $wgLilypondPreMidi = "Listen<br>";
 39+# $wgLilypondPostMidi = "<br>Listen";
 40+
 41+# If you want to avoid trimming the resulting image, set $wgLilypondTrim
 42+# to false.
 43+# $wgLilypondTrim = false;
 44+
 45+# You can put a white border around the image if you like.
 46+# $wgLilypondBorderX = 10;
 47+# $wgLilypondBorderY = 0;
 48+
 49+# End User Settings
 50+
 51+# Defaulting of user settings
 52+if( !isset( $wgLilypond ) )
 53+ $wgLilypond = "PATH=\$PATH:/usr/local/bin /usr/local/bin/lilypond";
 54+
 55+if( !isset( $wgLilypondPreMidi ) )
 56+ $wgLilypondPreMidi = "";
 57+
 58+if( !isset( $wgLilypondPostMidi ) )
 59+ $wgLilypondPostMidi = "";
 60+
 61+if( !isset( $wgLilypondTrim ) ) {
 62+ $wgLilypondTrim = true;
 63+}
 64+
 65+if( !isset( $wgLilypondBorderX ) ) {
 66+ $wgLilypondBorderX = 0;
 67+}
 68+
 69+if( !isset( $wgLilypondBorderY ) ) {
 70+ $wgLilypondBorderY = 0;
 71+}
 72+
 73+$wgExtensionFunctions[] = "wfLilyPondExtension";
 74+
 75+function wfLilyPondExtension() {
 76+ global $wgParser;
 77+ $wgParser->setHook( "lilypond", "renderLilyPondFragment" );
 78+ $wgParser->setHook( "lilymidi", "renderLilyPondMidiFragment" );
 79+ $wgParser->setHook( "lilybook", "renderLilyPond" );
 80+}
 81+
 82+function renderLilyPondMidiFragment( $lilypond_code )
 83+{
 84+ return renderLilyPondFragment( $lilypond_code, true );
 85+}
 86+
 87+function renderLilyPondFragment( $lilypond_code, $midi=false )
 88+{
 89+ return renderLilyPond( "\\header {\n"
 90+ . "\ttagline = ##f\n"
 91+ . "}\n"
 92+ . "\\paper {\n"
 93+ . "\traggedright = ##t\n"
 94+ . "\traggedbottom = ##t\n"
 95+ . "\tindent = 0\mm\n"
 96+ . "}\n"
 97+ . "\\score {\n"
 98+ . $lilypond_code
 99+ . "\t\\layout { }\n"
 100+ . ($midi?"\t\\midi { }\n":"")
 101+ . "}\n", $lilypond_code );
 102+}
 103+
 104+function renderLilyPond( $lilypond_code, $short_code=false )
 105+{
 106+ global $wgMathPath, $wgMathDirectory, $wgTmpDirectory, $wgLilypond, $wgLilypondPreMidi, $wgLilypondPostMidii, $wgLilypondTrim, $wgLilypondBorderX, $wgLilypondBorderY;
 107+
 108+ $mf = wfMsg( "math_failure" );
 109+ $munk = wfMsg( "math_unknown_error" );
 110+
 111+ $fname = "renderMusic";
 112+
 113+ $md5 = md5($lilypond_code);
 114+
 115+ if( file_exists( $wgMathDirectory."/".$md5.".midi" ) ) {
 116+ $pre = "<a href=\"".$wgMathPath."/".$md5.".midi\"> " . $wgLilypondPreMidi;
 117+ $post = $wgLilypondPostMidi . " </a>";
 118+ } else {
 119+ $pre = "";
 120+ $post = "";
 121+ }
 122+
 123+ # if short_code is supplied, this is a fragment
 124+ if( $short_code ) {
 125+ $link = "<img src=\"".$wgMathPath."/".$md5.".png\" alt=\""
 126+ .htmlspecialchars( $short_code )."\">";
 127+
 128+ if( file_exists( "$wgMathDirectory/$md5.png" ) ) {
 129+ return $pre.$link.$post;
 130+ }
 131+ } else {
 132+ if( file_exists( "$wgMathDirectory/$md5-1.png" ) ) {
 133+ $link="";
 134+ for($i=1; file_exists( $wgMathDirectory . "/" .
 135+ $md5 . "-" . $i . ".png" );
 136+ $i++) {
 137+
 138+ $link .= "<img src=\"" . $wgMathPath . "/" .
 139+ $md5 . "-" . $i . ".png\" alt=\"" .
 140+ htmlspecialchars( "page ".$i )."\">";
 141+ }
 142+ return $pre.$link.$post;
 143+ }
 144+ }
 145+
 146+ # Ensure that the temp and output dirs are available before continuing.
 147+ if( !file_exists( $wgMathDirectory ) ) {
 148+ if( !@mkdir( $wgMathDirectory ) ) {
 149+ return "<b>$mf (" . wfMsg( "math_bad_output" ) .
 150+ $wgMathDirectory . ")</b>";
 151+ }
 152+ } elseif( !is_dir( $wgMathDirectory ) ||
 153+ !is_writable( $wgMathDirectory ) ) {
 154+ return "<b>$mf (" . wfMsg( "math_bad_output" ) . ")</b>";
 155+ }
 156+ if( !file_exists( $wgTmpDirectory ) ) {
 157+ if( !@mkdir( $wgTmpDirectory ) ) {
 158+ return "<b>$mf (" . wfMsg( "math_bad_tmpdir" )
 159+ . ")</b>";
 160+ }
 161+ } elseif( !is_dir( $wgTmpDirectory ) ||
 162+ !is_writable( $wgTmpDirectory ) ) {
 163+ return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";
 164+ }
 165+
 166+ $lyFile = $md5.".ly";
 167+ $out = fopen( $wgTmpDirectory."/".$lyFile, "w" );
 168+ if( $out === false ) {
 169+ return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";
 170+ }
 171+ fwrite( $out, $lilypond_code );
 172+ fclose( $out );
 173+
 174+ $cmd = $wgLilypond .
 175+ " -dsafe='#t' -dbackend=eps --png --header=texidoc " .
 176+ escapeshellarg($lyFile) . " 2>&1";
 177+
 178+ wfDebug( "Lilypond: $cmd\n" );
 179+ $oldcwd = getcwd();
 180+ chdir( $wgTmpDirectory );
 181+ $contents = exec( $cmd, $output, $ret );
 182+ chdir( $oldcwd );
 183+
 184+ if( $ret != 0 ) {
 185+ return "<br><b>LilyPond error:</b><br><i>"
 186+ . str_replace( array( $md5, " " ),
 187+ array( "<b>your code</b>", "&nbsp;" ),
 188+ nl2br( htmlentities( join( "\n", $output ) ) ) )
 189+ . "</i><br>";
 190+ }
 191+
 192+ if($short_code) {
 193+ $outputFile = $wgTmpDirectory."/".$md5.".png";
 194+
 195+ if( !file_exists( $outputFile ) ) {
 196+ return "<b>$mf (" . wfMsg( "math_image_error" )
 197+ . ")</b>";
 198+ }
 199+
 200+ rename( $outputFile, $wgMathDirectory."/".$md5.".png");
 201+ }
 202+
 203+ # remove all temporary files
 204+ $files = opendir( $wgTmpDirectory );
 205+ $last_page = 0;
 206+
 207+ while( false !== ($file = readdir( $files ))) {
 208+ if( substr( $file, 0, 32 ) != $md5 )
 209+ continue;
 210+
 211+ $file_absolute = $wgTmpDirectory . "/" . $file;
 212+ if( !$short_code && preg_match( '/-page(\d+)\.png$/',
 213+ $file, $matches ) ) {
 214+ if($matches[1]>$last_page)
 215+ $last_page = $matches[1];
 216+ rename( $file_absolute, $wgMathDirectory . "/" .
 217+ $md5 . "-" . $matches[1] . ".png" );
 218+ continue;
 219+ }
 220+
 221+ if( preg_match( '/.png$/', $file ) ) {
 222+ rename( $file_absolute, $wgMathDirectory."/".$md5.".png" );
 223+ continue;
 224+ }
 225+
 226+ if( preg_match( '/.midi$/', $file ) ) {
 227+ rename( $file_absolute, $wgMathDirectory . "/" .
 228+ $md5 . ".midi" );
 229+ $pre = "<a href=\"".$wgMathPath."/".$md5.".midi\"> " . $wgLilypondPreMidi;
 230+ $post = $wgLilypondPostMidi . " </a>";
 231+ continue;
 232+ }
 233+
 234+ if( !is_file( $file_absolute ) )
 235+ continue;
 236+ unlink( $file_absolute );
 237+ }
 238+ closedir( $files );
 239+
 240+ if( $short_code ) {
 241+ if( !file_exists( $wgMathDirectory."/".$md5.".png" ) ) {
 242+ $errmsg = wfMsg( "math_image_error" );
 243+ return "<h3>$mf ($errmsg): " .
 244+ htmlspecialchars($lilypond_code) . "</h3>";
 245+ }
 246+ } else {
 247+ $link .= "<img src=\"".$wgMathPath."/".$md5.".png\" alt=\""
 248+ . htmlspecialchars( "page " )."\">";
 249+ }
 250+
 251+ if( $wgLilypondTrim ) {
 252+ $imgFile = $wgMathDirectory ."/" .$md5 . ".png";
 253+ trimImage( $imgFile, $imgFile, 0xFFFFFF );
 254+ };
 255+
 256+ if( $wgLilypondBorderX > 0 || $wgLilypondBorderY > 0 ) {
 257+ $imgFile = $wgMathDirectory ."/" .$md5 . ".png";
 258+ frameImage( $imgFile, $imgFile, 0xFFFFFF, $wgLilypondBorderX, $wgLilypondBorderY );
 259+ };
 260+
 261+ return $pre . $link . $post;
 262+}
 263+
 264+function trimImage( $source, $dest, $bgColour )
 265+{
 266+ $srcImage = imagecreatefrompng( $source );
 267+ $width = imagesx( $srcImage );
 268+ $height = imagesy( $srcImage );
 269+
 270+ $xmin = 0;
 271+ $found = false;
 272+ for( $x = 0; $x < $width && !$found; $x++ ) {
 273+ for( $y = 0; $y < $height && !$found; $y++ ) {
 274+ $rgb = imagecolorat( $srcImage, $x, $y );
 275+ if( $rgb != $bgColour ) {
 276+ $xmin = $x;
 277+ $found = true;
 278+ }
 279+ }
 280+ }
 281+
 282+ $xmax = $xmin;
 283+ $found = false;
 284+ for( $x = $width-1; $x > $xmin && !$found; $x-- ) {
 285+ for( $y = 0; $y < $height && !$found; $y++ ) {
 286+ $rgb = imagecolorat( $srcImage, $x, $y );
 287+ if( $rgb != $bgColour ) {
 288+ $xmax = $x;
 289+ $found = true;
 290+ }
 291+ }
 292+ }
 293+
 294+ $ymin = 0;
 295+ $found = false;
 296+ for( $y = 0; $y < $height && !$found; $y++ ) {
 297+ for( $x = 0; $x < $width && !$found; $x++ ) {
 298+ $rgb = imagecolorat( $srcImage, $x, $y );
 299+ if( $rgb != $bgColour ) {
 300+ $ymin = $y;
 301+ $found = true;
 302+ }
 303+ }
 304+ }
 305+
 306+ $ymax = $ymin;
 307+ $found = false;
 308+ for( $y = $height-1; $y > $ymin && !$found; $y-- ) {
 309+ for( $x = 0; $x < $width && !$found; $x++ ) {
 310+ $rgb = imagecolorat( $srcImage, $x, $y );
 311+ if( $rgb != $bgColour ) {
 312+ $ymax = $y;
 313+ $found = true;
 314+ }
 315+ }
 316+ }
 317+
 318+ $newWidth = $xmax - $xmin + 1;
 319+ $newHeight = $ymax - $ymin + 1;
 320+
 321+ $dstImage = imagecreatetruecolor( $newWidth, $newHeight );
 322+ imagecopy( $dstImage, $srcImage, 0, 0, $xmin, $ymin, $newWidth, $newHeight );
 323+ imagepng( $dstImage, $dest );
 324+}
 325+
 326+function frameImage( $source, $dest, $bgColour, $borderWidth, $borderHeight )
 327+{
 328+ $srcImage = imagecreatefrompng( $source );
 329+ $width = imagesx( $srcImage );
 330+ $height = imagesy( $srcImage );
 331+ $dstImage = imagecreatetruecolor( $width + 2*$borderWidth, $height + 2*$borderHeight );
 332+ $allocatedBgColour = imagecolorallocate( $dstImage, ($bgColour >> 16) & 0xFF, ($bgColour >> 8) & 0xFF, $bgColour & 0xFF);
 333+ imagefill( $dstImage, 0, 0, $allocatedBgColour );
 334+ imagecopy( $dstImage, $srcImage, $borderWidth, $borderHeight, 0, 0, $width, $height );
 335+ imagepng( $dstImage, $dest );
 336+}
Property changes on: trunk/extensions/LilyPond/LilyPond.php
___________________________________________________________________
Added: svn:eol-syle
1337 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r98424re r98414 -- add , check defined MEDIAWIKI, don't use shell varsmah18:18, 29 September 2011
r98435Add braces, fix whitespace...reedy19:23, 29 September 2011
r98443re r97414 — move parser functions to a seperate class file.mah20:30, 29 September 2011
r106459Commit Lilypond replacement from https://github.com/TheCount/score...mah18:47, 16 December 2011

Comments

#Comment by Siebrand (talk | contribs)   16:36, 29 September 2011
  • $wgExtensionCredits credits are missing (and a localisable extension description for Special:Version)
  • Should probably use ParserFirstCallInit instead of $wgExtensionFunctions[]
  • parser tag methods should be in a separate class file.
#Comment by Siebrand (talk | contribs)   16:38, 29 September 2011

Guard for register globals exploits is missing:

if ( !defined( 'MEDIAWIKI' ) ) {
	die( "This is not a valid entry point.\n" );
}
#Comment by MaxSem (talk | contribs)   16:43, 29 September 2011

And even it is insufficient for stuff like

if( !isset( $wgLilypond ) )
		$wgLilypond = "PATH=\$PATH:/usr/local/bin /usr/local/bin/lilypond";
#Comment by Reedy (talk | contribs)   19:45, 29 September 2011

Probably worth adding some wfDebugLog() and maybe some profiling...

#Comment by He7d3r (talk | contribs)   23:04, 29 September 2011

Ignore me (aka bug 22094)

#Comment by Tim Starling (talk | contribs)   23:24, 11 December 2011

Whole extension review as of r100547:

$pre = "<a href=\"" . $wgMathPath . "/" . $md5 . ".midi\"> " . $wgLilypondPreMidi;

Html::openElement() etc. should be used in new code. Several instances. Also I'm not sure what $wgLilypondPreMidi and $wgLilypondPostMidi are meant to be used for, but whatever it is, I'm sure it shouldn't be a global variable.

What is the #t, ##f stuff meant to do, e.g. "-dsafe='#t'"? It looks incomplete, like it's meant to be substituted with something but the substitution code is missing.

		$cmd = $wgLilypond .
			" -dsafe='#t' -dbackend=eps --png --header=texidoc " .
			escapeshellarg( $lyFile ) . " 2>&1";

wfEscapeShellArg() should be used, not escapeshellarg(). This gives Windows compatibility and a workaround for some Linux locale issues.

		exec( $cmd, $output, $ret );

wfShellExec() should be used.

					nl2br( htmlentities( join( "\n", $output ) ) ) )

Use htmlspecialchars() not htmlentities().

			return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";

In new code, messages should generally be plain text, not HTML, so escaping is recommended here.

		$out = fopen( $wgTmpDirectory . "/" . $lyFile, "w" );
...
		fwrite( $out, $lilypond_code );
		fclose( $out );

file_put_contents() would probably be more convenient here.

		$files = opendir( $wgTmpDirectory );
		$last_page = 0;

		while ( false !== ( $file = readdir( $files ) ) ) {

This might be a very large list indeed, depending on what else is running on the server. I suggest creating a separate directory for each LilyPond run, and then deleting all the files in it. If the directory has a random name instead of being a guessable hash, that avoids the symlink vulnerability in /tmp which the current code appears to suffer from.

$wgMathPath and $wgMathDirectory should not be used, the extension should be separately configured.

		if ( $wgLilypondTrim ) {
			$imgFile = $wgMathDirectory . "/" . $md5 . ".png";
			self::trimImage( $imgFile, $imgFile, 0xFFFFFF );
		}

For efficiency, trimming should be done while the image is still in the temporary directory.

		$ymin = self::findMin( $height, $width, $srcImage, $bgColour );

This won't have the intended result. findMin() and findMax() will only find bounds in the X direction, and the first parameter is unconditionally a width, just swapping the parameters won't work.

You may want to consider adding an option to use ImageMagick "convert -trim" instead.

#Comment by MarkAHershberger (talk | contribs)   19:11, 14 December 2011

Thanks for the review! I'll try to get these addressed.

#Comment by MarkAHershberger (talk | contribs)   21:47, 16 December 2011

set to deferred since development has moved to the Score extension. Also added a security warning to the Lilypond page.

#Comment by P858snake (talk | contribs)   00:30, 18 December 2011

If no one is going to main this and development has moved to another extension, this whole extension should be deleted from the repo.

#Comment by Reedy (talk | contribs)   00:51, 18 December 2011

I agree. Not a lot of point keeping a non working, unsecure extension about if there is a better alternative available

#Comment by He7d3r (talk | contribs)   00:56, 18 December 2011

Is there an equivalent of w:Wikipedia:How to fix cut-and-paste moves for code revisions? I mean, is it posible to move the old one to the new name and merge its histories?

#Comment by Reedy (talk | contribs)   01:16, 18 December 2011

You can svn move and "replace a file". Or just paste the new files over the old, and fget a diff commit

I don't think there is a way to completely merge histories...

Status & tagging log