MediaWiki master
TemplateParser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Html;
4
6use LightnCandy\LightnCandy;
9use RuntimeException;
10use UnexpectedValueException;
12
35
36 private const CACHE_VERSION = '2.2.0';
37 private const CACHE_TTL = BagOStuff::TTL_WEEK;
38
42 private $cache;
43
47 protected $templateDir;
48
52 protected $renderers;
53
57 protected $compileFlags;
58
63 public function __construct( $templateDir = null, ?BagOStuff $cache = null ) {
64 $this->templateDir = $templateDir ?: __DIR__ . '/../templates';
65 $this->cache = $cache ?: MediaWikiServices::getInstance()->getObjectCacheFactory()
66 ->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:86
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.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:88