MediaWiki master
TemplateParser.php
Go to the documentation of this file.
1<?php
2
7namespace MediaWiki\Html;
8
9use Exception;
10use LightnCandy\LightnCandy;
14use RuntimeException;
15use UnexpectedValueException;
17
24
25 private const CACHE_VERSION = '2.2.0';
26 private const CACHE_TTL = BagOStuff::TTL_WEEK;
27
28 private readonly BagOStuff $cache;
29
33 protected $templateDir;
34
38 protected $renderers;
39
43 protected $compileFlags;
44
49 public function __construct( $templateDir = null, ?BagOStuff $cache = null ) {
50 $this->templateDir = $templateDir ?: __DIR__ . '/../../resources/templates';
51 $this->cache = $cache ?? MediaWikiServices::getInstance()->getObjectCacheFactory()
52 ->getLocalServerInstance( CACHE_ANYTHING );
53
54 // Do not add more flags here without discussion.
55 // If you do add more flags, be sure to update unit tests as well.
56 $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
57 }
58
63 public function enableRecursivePartials( $enable ) {
64 if ( $enable ) {
65 $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
66 } else {
67 $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
68 }
69 }
70
77 protected function getTemplateFilename( $templateName ) {
78 // Prevent path traversal. Based on LanguageNameUtils::isValidCode().
79 // This is for paranoia. The $templateName should never come from
80 // untrusted input.
81 if ( strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName ) ) {
82 throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
83 }
84
85 return "{$this->templateDir}/{$templateName}.mustache";
86 }
87
96 protected function getTemplate( $templateName ) {
97 $templateKey = $templateName . '|' . $this->compileFlags;
98
99 // If a renderer has already been defined for this template, reuse it
100 if ( isset( $this->renderers[$templateKey] ) &&
101 is_callable( $this->renderers[$templateKey] )
102 ) {
103 return $this->renderers[$templateKey];
104 }
105
106 // Fetch a secret key for building a keyed hash of the PHP code.
107 // Note that this may be called before MediaWiki is fully initialized.
108 $secretKey = MediaWikiServices::hasInstance()
110 : null;
111
112 if ( $secretKey ) {
113 // See if the compiled PHP code is stored in the server-local cache.
114 $key = $this->cache->makeKey(
115 'lightncandy-compiled',
116 self::CACHE_VERSION,
117 $this->compileFlags,
118 $this->templateDir,
119 $templateName
120 );
121 $compiledTemplate = $this->cache->get( $key );
122
123 // 1. Has the template changed since the compiled template was cached? If so, don't use
124 // the cached code.
125 if ( $compiledTemplate ) {
126 $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
127
128 if ( $filesHash !== $compiledTemplate['filesHash'] ) {
129 $compiledTemplate = null;
130 }
131 }
132
133 // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
134 // code.
135 if ( $compiledTemplate ) {
136 $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
137
138 if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
139 $compiledTemplate = null;
140 }
141 }
142
143 // We're not using the cached code for whatever reason. Recompile the template and
144 // cache it.
145 if ( !$compiledTemplate ) {
146 $compiledTemplate = $this->compile( $templateName );
147
148 $compiledTemplate['integrityHash'] = hash_hmac(
149 'sha256',
150 $compiledTemplate['phpCode'],
151 $secretKey
152 );
153
154 $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
155 }
156
157 // If there is no secret key available, don't use cache
158 } else {
159 $compiledTemplate = $this->compile( $templateName );
160 }
161
162 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
163 $renderer = eval( $compiledTemplate['phpCode'] );
164 if ( !is_callable( $renderer ) ) {
165 throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
166 }
167 $this->renderers[$templateKey] = $renderer;
168 return $renderer;
169 }
170
202 protected function compile( $templateName ) {
203 $filename = $this->getTemplateFilename( $templateName );
204
205 if ( !file_exists( $filename ) ) {
206 throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
207 }
208
209 $files = [ $filename ];
210 $contents = file_get_contents( $filename );
211 $compiled = LightnCandy::compile(
212 $contents,
213 [
214 'flags' => $this->compileFlags,
215 'basedir' => $this->templateDir,
216 'fileext' => '.mustache',
217 'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
218 $filename = "{$this->templateDir}/{$partialName}.mustache";
219 if ( !file_exists( $filename ) ) {
220 throw new RuntimeException( sprintf(
221 'Could not compile template `%s`: Could not find partial `%s` at %s',
222 $templateName,
223 $partialName,
224 $filename
225 ) );
226 }
227
228 $fileContents = file_get_contents( $filename );
229
230 if ( $fileContents === false ) {
231 throw new RuntimeException( sprintf(
232 'Could not compile template `%s`: Could not find partial `%s` at %s',
233 $templateName,
234 $partialName,
235 $filename
236 ) );
237 }
238
239 $files[] = $filename;
240
241 return $fileContents;
242 }
243 ]
244 );
245 if ( !$compiled ) {
246 // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
247 // Errors should throw exceptions instead of returning false
248 // Check anyway for paranoia
249 throw new RuntimeException( "Could not compile template `{$filename}`" );
250 }
251
252 $files = array_values( array_unique( $files ) );
253
254 return [
255 'phpCode' => $compiled,
256 'files' => $files,
257 'filesHash' => FileContentsHasher::getFileContentsHash( $files ),
258 ];
259 }
260
281 public function processTemplate( $templateName, $args, array $scopes = [] ) {
282 $template = $this->getTemplate( $templateName );
283 return $template( $args, $scopes );
284 }
285}
const CACHE_ANYTHING
Definition Defines.php:72
Handles compiling Mustache templates into PHP rendering functions.
__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.
Generate hash digests of file contents to help with cache invalidation.
static getFileContentsHash( $filePaths)
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
Abstract class for any ephemeral data store.
Definition BagOStuff.php:73