tags.
* Added 'ChangeTagsListActive' hook, to separate the concepts of "defined" and
"active" formerly conflated by the 'ListDefinedTags' hook.
+* Added TemplateParser class that provides a server-side interface to cachable
+ dynamically-compiled Mustache templates (currently uses lightncandy library).
* Clickable anchors for each section heading in the content are now generated
and appear in the gutter on hovering over the heading.
'TablePager' => __DIR__ . '/includes/pager/TablePager.php',
'TempFSFile' => __DIR__ . '/includes/filebackend/TempFSFile.php',
'TempFileRepo' => __DIR__ . '/includes/filerepo/FileRepo.php',
+ 'TemplateParser' => __DIR__ . '/includes/TemplateParser.php',
'TestFileOpPerformance' => __DIR__ . '/maintenance/fileOpPerfTest.php',
'TextContent' => __DIR__ . '/includes/content/TextContent.php',
'TextContentHandler' => __DIR__ . '/includes/content/TextContentHandler.php',
--- /dev/null
+<?php
+/**
+ * Display an error page when there is no LocalSettings.php file.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+# bug 30219 : can not use pathinfo() on URLs since slashes do not match
+$matches = array();
+$ext = 'php';
+$path = '/';
+foreach ( array_filter( explode( '/', $_SERVER['PHP_SELF'] ) ) as $part ) {
+ if ( !preg_match( '/\.(php5?)$/', $part, $matches ) ) {
+ $path .= "$part/";
+ } else {
+ $ext = $matches[1] == 'php5' ? 'php5' : 'php';
+ break;
+ }
+}
+
+# Check to see if the installer is running
+if ( !function_exists( 'session_name' ) ) {
+ $installerStarted = false;
+} else {
+ session_name( 'mw_installer_session' );
+ $oldReporting = error_reporting( E_ALL & ~E_NOTICE );
+ $success = session_start();
+ error_reporting( $oldReporting );
+ $installerStarted = ( $success && isset( $_SESSION['installData'] ) );
+}
+
+$templateParser = new TemplateParser();
+
+# Render error page if no LocalSettings file can be found
+try {
+ echo $templateParser->processTemplate(
+ 'NoLocalSettings',
+ array(
+ 'wgVersion' => ( isset( $wgVersion ) ? $wgVersion : 'VERSION' ),
+ 'path' => $path,
+ 'ext' => $ext,
+ 'localSettingsExists' => file_exists( MW_CONFIG_FILE ),
+ 'installerStarted' => $installerStarted
+ )
+ );
+} catch ( Exception $e ) {
+ echo 'Error: ' . htmlspecialchars( $e->getMessage() );
+}
--- /dev/null
+<?php
+/**
+ * Handles compiling Mustache templates into PHP rendering functions
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+class TemplateParser {
+ /**
+ * @var string The path to the Mustache templates
+ */
+ protected $templateDir;
+
+ /**
+ * @var callable[] Array of cached rendering functions
+ */
+ protected $renderers;
+
+ /**
+ * @var bool Always compile template files
+ */
+ protected $forceRecompile = false;
+
+ /**
+ * @param string $templateDir
+ * @param boolean $forceRecompile
+ */
+ public function __construct( $templateDir = null, $forceRecompile = false ) {
+ $this->templateDir = $templateDir ? $templateDir : __DIR__.'/templates';
+ $this->forceRecompile = $forceRecompile;
+ }
+
+ /**
+ * Constructs the location of the the source Mustache template
+ * @param string $templateName The name of the template
+ * @return string
+ * @throws Exception Disallows upwards directory traversal via $templateName
+ */
+ public function getTemplateFilename( $templateName ) {
+ // Prevent upwards directory traversal using same methods as Title::secureAndSplit
+ if (
+ strpos( $templateName, '.' ) !== false &&
+ (
+ $templateName === '.' || $templateName === '..' ||
+ strpos( $templateName, './' ) === 0 ||
+ strpos( $templateName, '../' ) === 0 ||
+ strpos( $templateName, '/./' ) !== false ||
+ strpos( $templateName, '/../' ) !== false ||
+ substr( $templateName, -2 ) === '/.' ||
+ substr( $templateName, -3 ) === '/..'
+ )
+ ) {
+ throw new Exception( "Malformed \$templateName: $templateName" );
+ }
+
+ return "{$this->templateDir}/{$templateName}.mustache";
+ }
+
+ /**
+ * Returns a given template function if found, otherwise throws an exception.
+ * @param string $templateName The name of the template (without file suffix)
+ * @return Function
+ * @throws Exception
+ */
+ public function getTemplate( $templateName ) {
+ global $wgSecretKey;
+
+ // If a renderer has already been defined for this template, reuse it
+ if ( isset( $this->renderers[$templateName] ) ) {
+ return $this->renderers[$templateName];
+ }
+
+ $filename = $this->getTemplateFilename( $templateName );
+
+ if ( !file_exists( $filename ) ) {
+ throw new Exception( "Could not locate template: {$filename}" );
+ }
+
+ // Read the template file
+ $fileContents = file_get_contents( $filename );
+
+ // Generate a quick hash for cache invalidation
+ $fastHash = md5( $fileContents );
+
+ // See if the compiled PHP code is stored in cache.
+ // CACHE_ACCEL throws an exception if no suitable object cache is present, so fall
+ // back to CACHE_ANYTHING.
+ try {
+ $cache = wfGetCache( CACHE_ACCEL );
+ } catch ( Exception $e ) {
+ $cache = wfGetCache( CACHE_ANYTHING );
+ }
+ $key = wfMemcKey( 'template', $templateName, $fastHash );
+ $code = $this->forceRecompile ? null : $cache->get( $key );
+
+ if ( !$code ) {
+ // Compile the template into PHP code
+ $code = self::compile( $fileContents );
+
+ if ( !$code ) {
+ throw new Exception( "Could not compile template: {$filename}" );
+ }
+
+ // Strip the "<?php" added by lightncandy so that it can be eval()ed
+ if ( substr( $code, 0, 5 ) === '<?php' ) {
+ $code = substr( $code, 5 );
+ }
+
+ $renderer = eval( $code );
+
+ // Prefix the code with a keyed hash (64 hex chars) as an integrity check
+ $code = hash_hmac( 'sha256', $code, $wgSecretKey ) . $code;
+
+ // Cache the compiled PHP code
+ $cache->set( $key, $code );
+ } else {
+ // Verify the integrity of the cached PHP code
+ $keyedHash = substr( $code, 0, 64 );
+ $code = substr( $code, 64 );
+ if ( $keyedHash === hash_hmac( 'sha256', $code, $wgSecretKey ) ) {
+ $renderer = eval( $code );
+ } else {
+ throw new Exception( "Template failed integrity check: {$filename}" );
+ }
+ }
+
+ return $this->renderers[$templateName] = $renderer;
+ }
+
+ /**
+ * Compile the Mustache code into PHP code using LightnCandy
+ * @param string $code Mustache code
+ * @return string PHP code
+ * @throws Exception
+ */
+ public static function compile( $code ) {
+ if ( !class_exists( 'LightnCandy' ) ) {
+ throw new Exception( 'LightnCandy class not defined' );
+ }
+ return LightnCandy::compile(
+ $code,
+ array(
+ // Do not add more flags here without discussion.
+ // If you do add more flags, be sure to update unit tests as well.
+ 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION
+ )
+ );
+ }
+
+ /**
+ * Returns HTML for a given template by calling the template function with the given args
+ * @param string $templateName The name of the template
+ * @param mixed $args
+ * @param array $scopes
+ * @return string
+ */
+ public function processTemplate( $templateName, $args, array $scopes = array() ) {
+ $template = $this->getTemplate( $templateName );
+ return call_user_func( $template, $args, $scopes );
+ }
+}
# the wiki installer needs to be launched or the generated file uploaded to
# the root wiki directory. Give a hint, if it is not readable by the server.
if ( !is_readable( MW_CONFIG_FILE ) ) {
- require_once "$IP/includes/templates/NoLocalSettings.php";
+ require_once "$IP/includes/NoLocalSettings.php";
die();
}
--- /dev/null
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+ <head>
+ <meta charset="UTF-8" />
+ <title>MediaWiki {{wgVersion}}</title>
+ <style media="screen">
+ html, body {
+ color: #000;
+ background-color: #fff;
+ font-family: sans-serif;
+ text-align: center;
+ }
+
+ h1 {
+ font-size: 150%;
+ }
+ </style>
+ </head>
+ <body>
+ <img src="{{path}}resources/assets/mediawiki.png" alt="The MediaWiki logo" />
+
+ <h1>MediaWiki {{wgVersion}}</h1>
+ <div class="error">
+ {{#localSettingsExists}}
+ <p>LocalSettings.php not readable.</p>
+ <p>Please correct file permissions and try again.</p>
+ {{/localSettingsExists}}
+ {{^localSettingsExists}}
+ <p>LocalSettings.php not found.</p>
+ {{#installerStarted}}
+ <p>Please <a href="{{path}}mw-config/index{{ext}}">complete the installation</a> and download LocalSettings.php.</p>
+ {{/installerStarted}}
+ {{^installerStarted}}
+ <p>Please <a href="{{path}}mw-config/index{{ext}}">set up the wiki</a> first!</p>
+ {{/installerStarted}}
+ {{/localSettingsExists}}
+ </div>
+ </body>
+</html>
\ No newline at end of file
+++ /dev/null
-<?php
-// @codingStandardsIgnoreFile
-/**
- * Template used when there is no LocalSettings.php file.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Templates
- */
-
-if ( !defined( 'MEDIAWIKI' ) ) {
- die( "NoLocalSettings.php is not a valid MediaWiki entry point\n" );
-}
-
-if ( !isset( $wgVersion ) ) {
- $wgVersion = 'VERSION';
-}
-
-# bug 30219 : can not use pathinfo() on URLs since slashes do not match
-$matches = array();
-$ext = 'php';
-$path = '/';
-foreach ( array_filter( explode( '/', $_SERVER['PHP_SELF'] ) ) as $part ) {
- if ( !preg_match( '/\.(php5?)$/', $part, $matches ) ) {
- $path .= "$part/";
- } else {
- $ext = $matches[1] == 'php5' ? 'php5' : 'php';
- break;
- }
-}
-
-# Check to see if the installer is running
-if ( !function_exists( 'session_name' ) ) {
- $installerStarted = false;
-} else {
- session_name( 'mw_installer_session' );
- $oldReporting = error_reporting( E_ALL & ~E_NOTICE );
- $success = session_start();
- error_reporting( $oldReporting );
- $installerStarted = ( $success && isset( $_SESSION['installData'] ) );
-}
-?>
-<!DOCTYPE html>
-<html lang="en" dir="ltr">
- <head>
- <meta charset="UTF-8" />
- <title>MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?></title>
- <style media='screen'>
- html, body {
- color: #000;
- background-color: #fff;
- font-family: sans-serif;
- text-align: center;
- }
-
- h1 {
- font-size: 150%;
- }
- </style>
- </head>
- <body>
- <img src="<?php echo htmlspecialchars( $path ) ?>resources/assets/mediawiki.png" alt='The MediaWiki logo' />
-
- <h1>MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?></h1>
- <div class='error'>
- <?php if ( !file_exists( MW_CONFIG_FILE ) ) { ?>
- <p>LocalSettings.php not found.</p>
- <p>
- <?php
- if ( $installerStarted ) {
- echo "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\">complete the installation</a> and download LocalSettings.php.";
- } else {
- echo "Please <a href=\"" . htmlspecialchars( $path ) . "mw-config/index." . htmlspecialchars( $ext ) . "\">set up the wiki</a> first.";
- }
- ?>
- </p>
- <?php } else { ?>
- <p>LocalSettings.php not readable.</p>
- <p>Please correct file permissions and try again.</p>
- <?php } ?>
-
- </div>
- </body>
-</html>
--- /dev/null
+<?php
+
+/**
+ * @group Templates
+ */
+class TemplateParserTest extends MediaWikiTestCase {
+ /**
+ * @covers TemplateParser::compile
+ */
+ public function testTemplateCompilation() {
+ $this->assertRegExp(
+ '/^<\?php return function/',
+ TemplateParser::compile( "test" ),
+ 'compile a simple mustache template'
+ );
+ }
+
+ /**
+ * @covers TemplateParser::compile
+ */
+ public function testTemplateCompilationWithVariable() {
+ $this->assertRegExp(
+ '/return \'\'\.htmlentities\(\(string\)\(\(isset\(\$in\[\'value\'\]\) && '
+ . 'is_array\(\$in\)\) \? \$in\[\'value\'\] : null\), ENT_QUOTES, '
+ . '\'UTF-8\'\)\.\'\';/',
+ TemplateParser::compile( "{{value}}" ),
+ 'compile a mustache template with an escaped variable'
+ );
+ }
+}