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