From: kaldari Date: Fri, 30 Jan 2015 19:31:44 +0000 (-0800) Subject: Adding TemplateParser class providing interface to Mustache templates X-Git-Tag: 1.31.0-rc.0~12329^2~1 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=2ec027221884a887da6114a429b4e7a8af91ad36;p=lhc%2Fweb%2Fwiklou.git Adding TemplateParser class providing interface to Mustache templates The TemplateParser class provides a server-side interface to cachable dynamically-compiled Mustache templates. It currently uses the lightncandy library to do compilation (which is already included in the vendor repo). Also converting NoLocalSettings.php to use it as a proof-of-concept. Bug: T379 Change-Id: I28cd13d4d1132bd386e2ae2f4f0d1dd88ad9162b --- diff --git a/RELEASE-NOTES-1.25 b/RELEASE-NOTES-1.25 index 10e49ae1e0..6d816fe9d0 100644 --- a/RELEASE-NOTES-1.25 +++ b/RELEASE-NOTES-1.25 @@ -99,6 +99,8 @@ production. 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. diff --git a/autoload.php b/autoload.php index 01dba44784..bdfbee2d48 100644 --- a/autoload.php +++ b/autoload.php @@ -1180,6 +1180,7 @@ $wgAutoloadLocalClasses = array( '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', diff --git a/includes/NoLocalSettings.php b/includes/NoLocalSettings.php new file mode 100644 index 0000000000..6de9bfcde0 --- /dev/null +++ b/includes/NoLocalSettings.php @@ -0,0 +1,63 @@ +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() ); +} diff --git a/includes/TemplateParser.php b/includes/TemplateParser.php new file mode 100644 index 0000000000..57fcc24882 --- /dev/null +++ b/includes/TemplateParser.php @@ -0,0 +1,175 @@ +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 "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 ); + } +} diff --git a/includes/WebStart.php b/includes/WebStart.php index 125e54430d..da4bc87920 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -98,7 +98,7 @@ if ( defined( 'MW_CONFIG_CALLBACK' ) ) { # 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(); } diff --git a/includes/templates/NoLocalSettings.mustache b/includes/templates/NoLocalSettings.mustache new file mode 100644 index 0000000000..1649e3b75c --- /dev/null +++ b/includes/templates/NoLocalSettings.mustache @@ -0,0 +1,39 @@ + + + + + MediaWiki {{wgVersion}} + + + + The MediaWiki logo + +

MediaWiki {{wgVersion}}

+
+ {{#localSettingsExists}} +

LocalSettings.php not readable.

+

Please correct file permissions and try again.

+ {{/localSettingsExists}} + {{^localSettingsExists}} +

LocalSettings.php not found.

+ {{#installerStarted}} +

Please complete the installation and download LocalSettings.php.

+ {{/installerStarted}} + {{^installerStarted}} +

Please set up the wiki first!

+ {{/installerStarted}} + {{/localSettingsExists}} +
+ + \ No newline at end of file diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php deleted file mode 100644 index 824a3156d5..0000000000 --- a/includes/templates/NoLocalSettings.php +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?> - - - - The MediaWiki logo - -

MediaWiki

-
- -

LocalSettings.php not found.

-

- complete the installation and download LocalSettings.php."; - } else { - echo "Please set up the wiki first."; - } - ?> -

- -

LocalSettings.php not readable.

-

Please correct file permissions and try again.

- - -
- - diff --git a/tests/phpunit/includes/TemplateParserTest.php b/tests/phpunit/includes/TemplateParserTest.php new file mode 100644 index 0000000000..ccfccd1d75 --- /dev/null +++ b/tests/phpunit/includes/TemplateParserTest.php @@ -0,0 +1,30 @@ +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' + ); + } +}