MediaWiki master
TemplateParser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Html;
4
5use BagOStuff;
7use LightnCandy\LightnCandy;
10use ObjectCache;
11use RuntimeException;
12use UnexpectedValueException;
13
36
37 private const CACHE_VERSION = '2.2.0';
38 private const CACHE_TTL = BagOStuff::TTL_WEEK;
39
43 private $cache;
44
48 protected $templateDir;
49
53 protected $renderers;
54
58 protected $compileFlags;
59
64 public function __construct( $templateDir = null, ?BagOStuff $cache = null ) {
65 $this->templateDir = $templateDir ?: __DIR__ . '/../templates';
66 $this->cache = $cache ?: ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
67
68 // Do not add more flags here without discussion.
69 // If you do add more flags, be sure to update unit tests as well.
70 $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
71 }
72
77 public function enableRecursivePartials( $enable ) {
78 if ( $enable ) {
79 $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
80 } else {
81 $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
82 }
83 }
84
91 protected function getTemplateFilename( $templateName ) {
92 // Prevent path traversal. Based on LanguageNameUtils::isValidCode().
93 // This is for paranoia. The $templateName should never come from
94 // untrusted input.
95 if ( strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName ) ) {
96 throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
97 }
98
99 return "{$this->templateDir}/{$templateName}.mustache";
100 }
101
110 protected function getTemplate( $templateName ) {
111 $templateKey = $templateName . '|' . $this->compileFlags;
112
113 // If a renderer has already been defined for this template, reuse it
114 if ( isset( $this->renderers[$templateKey] ) &&
115 is_callable( $this->renderers[$templateKey] )
116 ) {
117 return $this->renderers[$templateKey];
118 }
119
120 // Fetch a secret key for building a keyed hash of the PHP code.
121 // Note that this may be called before MediaWiki is fully initialized.
122 $secretKey = MediaWikiServices::hasInstance()
124 : null;
125
126 if ( $secretKey ) {
127 // See if the compiled PHP code is stored in the server-local cache.
128 $key = $this->cache->makeKey(
129 'lightncandy-compiled',
130 self::CACHE_VERSION,
131 $this->compileFlags,
132 $this->templateDir,
133 $templateName
134 );
135 $compiledTemplate = $this->cache->get( $key );
136
137 // 1. Has the template changed since the compiled template was cached? If so, don't use
138 // the cached code.
139 if ( $compiledTemplate ) {
140 $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
141
142 if ( $filesHash !== $compiledTemplate['filesHash'] ) {
143 $compiledTemplate = null;
144 }
145 }
146
147 // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
148 // code.
149 if ( $compiledTemplate ) {
150 $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
151
152 if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
153 $compiledTemplate = null;
154 }
155 }
156
157 // We're not using the cached code for whatever reason. Recompile the template and
158 // cache it.
159 if ( !$compiledTemplate ) {
160 $compiledTemplate = $this->compile( $templateName );
161
162 $compiledTemplate['integrityHash'] = hash_hmac(
163 'sha256',
164 $compiledTemplate['phpCode'],
165 $secretKey
166 );
167
168 $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
169 }
170
171 // If there is no secret key available, don't use cache
172 } else {
173 $compiledTemplate = $this->compile( $templateName );
174 }
175
176 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
177 $renderer = eval( $compiledTemplate['phpCode'] );
178 if ( !is_callable( $renderer ) ) {
179 throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
180 }
181 $this->renderers[$templateKey] = $renderer;
182 return $renderer;
183 }
184
216 protected function compile( $templateName ) {
217 $filename = $this->getTemplateFilename( $templateName );
218
219 if ( !file_exists( $filename ) ) {
220 throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
221 }
222
223 $files = [ $filename ];
224 $contents = file_get_contents( $filename );
225 $compiled = LightnCandy::compile(
226 $contents,
227 [
228 'flags' => $this->compileFlags,
229 'basedir' => $this->templateDir,
230 'fileext' => '.mustache',
231 'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
232 $filename = "{$this->templateDir}/{$partialName}.mustache";
233 if ( !file_exists( $filename ) ) {
234 throw new RuntimeException( sprintf(
235 'Could not compile template `%s`: Could not find partial `%s` at %s',
236 $templateName,
237 $partialName,
238 $filename
239 ) );
240 }
241
242 $fileContents = file_get_contents( $filename );
243
244 if ( $fileContents === false ) {
245 throw new RuntimeException( sprintf(
246 'Could not compile template `%s`: Could not find partial `%s` at %s',
247 $templateName,
248 $partialName,
249 $filename
250 ) );
251 }
252
253 $files[] = $filename;
254
255 return $fileContents;
256 }
257 ]
258 );
259 if ( !$compiled ) {
260 // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
261 // Errors should throw exceptions instead of returning false
262 // Check anyway for paranoia
263 throw new RuntimeException( "Could not compile template `{$filename}`" );
264 }
265
266 $files = array_values( array_unique( $files ) );
267
268 return [
269 'phpCode' => $compiled,
270 'files' => $files,
271 'filesHash' => FileContentsHasher::getFileContentsHash( $files ),
272 ];
273 }
274
295 public function processTemplate( $templateName, $args, array $scopes = [] ) {
296 $template = $this->getTemplate( $templateName );
297 return $template( $args, $scopes );
298 }
299}
300
302class_alias( TemplateParser::class, 'TemplateParser' );
const CACHE_ANYTHING
Definition Defines.php:85
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
static getFileContentsHash( $filePaths)
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
__construct( $templateDir=null, ?BagOStuff $cache=null)
compile( $templateName)
Compile the Mustache template into PHP code using LightnCandy.
string $templateDir
The path to the Mustache templates.
getTemplate( $templateName)
Returns a given template function if found, otherwise throws an exception.
int $compileFlags
Compilation flags passed to LightnCandy.
getTemplateFilename( $templateName)
Constructs the location of the source Mustache template.
enableRecursivePartials( $enable)
Enable/disable the use of recursive partials.
callable[] $renderers
Array of cached rendering functions.
processTemplate( $templateName, $args, array $scopes=[])
Returns HTML for a given template by calling the template function with the given args.
A class containing constants representing the names of configuration variables.
const SecretKey
Name constant for the SecretKey setting, for use with Config::get()
Service locator for MediaWiki core services.
static hasInstance()
Returns true if an instance has already been initialized.
static getInstance()
Returns the global default instance of the top level service locator.