MediaWiki master
TemplateParser.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Html;
4
5use Exception;
7use LightnCandy\LightnCandy;
10use RuntimeException;
11use 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 ?: MediaWikiServices::getInstance()->getObjectCacheFactory()
67 ->getLocalServerInstance( CACHE_ANYTHING );
68
69 // Do not add more flags here without discussion.
70 // If you do add more flags, be sure to update unit tests as well.
71 $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
72 }
73
78 public function enableRecursivePartials( $enable ) {
79 if ( $enable ) {
80 $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
81 } else {
82 $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
83 }
84 }
85
92 protected function getTemplateFilename( $templateName ) {
93 // Prevent path traversal. Based on LanguageNameUtils::isValidCode().
94 // This is for paranoia. The $templateName should never come from
95 // untrusted input.
96 if ( strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName ) ) {
97 throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
98 }
99
100 return "{$this->templateDir}/{$templateName}.mustache";
101 }
102
111 protected function getTemplate( $templateName ) {
112 $templateKey = $templateName . '|' . $this->compileFlags;
113
114 // If a renderer has already been defined for this template, reuse it
115 if ( isset( $this->renderers[$templateKey] ) &&
116 is_callable( $this->renderers[$templateKey] )
117 ) {
118 return $this->renderers[$templateKey];
119 }
120
121 // Fetch a secret key for building a keyed hash of the PHP code.
122 // Note that this may be called before MediaWiki is fully initialized.
123 $secretKey = MediaWikiServices::hasInstance()
125 : null;
126
127 if ( $secretKey ) {
128 // See if the compiled PHP code is stored in the server-local cache.
129 $key = $this->cache->makeKey(
130 'lightncandy-compiled',
131 self::CACHE_VERSION,
132 $this->compileFlags,
133 $this->templateDir,
134 $templateName
135 );
136 $compiledTemplate = $this->cache->get( $key );
137
138 // 1. Has the template changed since the compiled template was cached? If so, don't use
139 // the cached code.
140 if ( $compiledTemplate ) {
141 $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
142
143 if ( $filesHash !== $compiledTemplate['filesHash'] ) {
144 $compiledTemplate = null;
145 }
146 }
147
148 // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
149 // code.
150 if ( $compiledTemplate ) {
151 $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
152
153 if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
154 $compiledTemplate = null;
155 }
156 }
157
158 // We're not using the cached code for whatever reason. Recompile the template and
159 // cache it.
160 if ( !$compiledTemplate ) {
161 $compiledTemplate = $this->compile( $templateName );
162
163 $compiledTemplate['integrityHash'] = hash_hmac(
164 'sha256',
165 $compiledTemplate['phpCode'],
166 $secretKey
167 );
168
169 $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
170 }
171
172 // If there is no secret key available, don't use cache
173 } else {
174 $compiledTemplate = $this->compile( $templateName );
175 }
176
177 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
178 $renderer = eval( $compiledTemplate['phpCode'] );
179 if ( !is_callable( $renderer ) ) {
180 throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
181 }
182 $this->renderers[$templateKey] = $renderer;
183 return $renderer;
184 }
185
217 protected function compile( $templateName ) {
218 $filename = $this->getTemplateFilename( $templateName );
219
220 if ( !file_exists( $filename ) ) {
221 throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
222 }
223
224 $files = [ $filename ];
225 $contents = file_get_contents( $filename );
226 $compiled = LightnCandy::compile(
227 $contents,
228 [
229 'flags' => $this->compileFlags,
230 'basedir' => $this->templateDir,
231 'fileext' => '.mustache',
232 'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
233 $filename = "{$this->templateDir}/{$partialName}.mustache";
234 if ( !file_exists( $filename ) ) {
235 throw new RuntimeException( sprintf(
236 'Could not compile template `%s`: Could not find partial `%s` at %s',
237 $templateName,
238 $partialName,
239 $filename
240 ) );
241 }
242
243 $fileContents = file_get_contents( $filename );
244
245 if ( $fileContents === false ) {
246 throw new RuntimeException( sprintf(
247 'Could not compile template `%s`: Could not find partial `%s` at %s',
248 $templateName,
249 $partialName,
250 $filename
251 ) );
252 }
253
254 $files[] = $filename;
255
256 return $fileContents;
257 }
258 ]
259 );
260 if ( !$compiled ) {
261 // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
262 // Errors should throw exceptions instead of returning false
263 // Check anyway for paranoia
264 throw new RuntimeException( "Could not compile template `{$filename}`" );
265 }
266
267 $files = array_values( array_unique( $files ) );
268
269 return [
270 'phpCode' => $compiled,
271 'files' => $files,
272 'filesHash' => FileContentsHasher::getFileContentsHash( $files ),
273 ];
274 }
275
296 public function processTemplate( $templateName, $args, array $scopes = [] ) {
297 $template = $this->getTemplate( $templateName );
298 return $template( $args, $scopes );
299 }
300}
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 and can be obtained from getInstance().
static getInstance()
Returns the global default instance of the top level service locator.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:87