MediaWiki REL1_39
TemplateParser.php
Go to the documentation of this file.
1<?php
2
3use LightnCandy\LightnCandy;
6
29
30 private const CACHE_VERSION = '2.2.0';
31 private const CACHE_TTL = BagOStuff::TTL_WEEK;
32
36 private $cache;
37
41 protected $templateDir;
42
46 protected $renderers;
47
51 protected $compileFlags;
52
57 public function __construct( $templateDir = null, ?BagOStuff $cache = null ) {
58 $this->templateDir = $templateDir ?: __DIR__ . '/templates';
59 $this->cache = $cache ?: ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
60
61 // Do not add more flags here without discussion.
62 // If you do add more flags, be sure to update unit tests as well.
63 $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
64 }
65
70 public function enableRecursivePartials( $enable ) {
71 if ( $enable ) {
72 $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
73 } else {
74 $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
75 }
76 }
77
84 protected function getTemplateFilename( $templateName ) {
85 // Prevent path traversal. Based on Language::isValidCode().
86 // This is for paranoia. The $templateName should never come from
87 // untrusted input.
88 if (
89 strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName )
90 ) {
91 throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
92 }
93
94 return "{$this->templateDir}/{$templateName}.mustache";
95 }
96
105 protected function getTemplate( $templateName ) {
106 $templateKey = $templateName . '|' . $this->compileFlags;
107
108 // If a renderer has already been defined for this template, reuse it
109 if ( isset( $this->renderers[$templateKey] ) &&
110 is_callable( $this->renderers[$templateKey] )
111 ) {
112 return $this->renderers[$templateKey];
113 }
114
115 // Fetch a secret key for building a keyed hash of the PHP code.
116 // Note that this may be called before MediaWiki is fully initialized.
117 $secretKey = MediaWikiServices::hasInstance()
118 ? MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::SecretKey )
119 : null;
120
121 if ( $secretKey ) {
122 // See if the compiled PHP code is stored in the server-local cache.
123 $key = $this->cache->makeKey(
124 'lightncandy-compiled',
125 self::CACHE_VERSION,
126 $this->compileFlags,
127 $this->templateDir,
128 $templateName
129 );
130 $compiledTemplate = $this->cache->get( $key );
131
132 // 1. Has the template changed since the compiled template was cached? If so, don't use
133 // the cached code.
134 if ( $compiledTemplate ) {
135 $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
136
137 if ( $filesHash !== $compiledTemplate['filesHash'] ) {
138 $compiledTemplate = null;
139 }
140 }
141
142 // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
143 // code.
144 if ( $compiledTemplate ) {
145 $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
146
147 if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
148 $compiledTemplate = null;
149 }
150 }
151
152 // We're not using the cached code for whatever reason. Recompile the template and
153 // cache it.
154 if ( !$compiledTemplate ) {
155 $compiledTemplate = $this->compile( $templateName );
156
157 $compiledTemplate['integrityHash'] = hash_hmac(
158 'sha256',
159 $compiledTemplate['phpCode'],
160 $secretKey
161 );
162
163 $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
164 }
165
166 // If there is no secret key available, don't use cache
167 } else {
168 $compiledTemplate = $this->compile( $templateName );
169 }
170
171 $renderer = eval( $compiledTemplate['phpCode'] );
172 if ( !is_callable( $renderer ) ) {
173 throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
174 }
175 $this->renderers[$templateKey] = $renderer;
176 return $renderer;
177 }
178
210 protected function compile( $templateName ) {
211 $filename = $this->getTemplateFilename( $templateName );
212
213 if ( !file_exists( $filename ) ) {
214 throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
215 }
216
217 $files = [ $filename ];
218 $contents = file_get_contents( $filename );
219 $compiled = LightnCandy::compile(
220 $contents,
221 [
222 'flags' => $this->compileFlags,
223 'basedir' => $this->templateDir,
224 'fileext' => '.mustache',
225 'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
226 $filename = "{$this->templateDir}/{$partialName}.mustache";
227 if ( !file_exists( $filename ) ) {
228 throw new RuntimeException( sprintf(
229 'Could not compile template `%s`: Could not find partial `%s` at %s',
230 $templateName,
231 $partialName,
232 $filename
233 ) );
234 }
235
236 $fileContents = file_get_contents( $filename );
237
238 if ( $fileContents === false ) {
239 throw new RuntimeException( sprintf(
240 'Could not compile template `%s`: Could not find partial `%s` at %s',
241 $templateName,
242 $partialName,
243 $filename
244 ) );
245 }
246
247 $files[] = $filename;
248
249 return $fileContents;
250 }
251 ]
252 );
253 if ( !$compiled ) {
254 // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
255 // Errors should throw exceptions instead of returning false
256 // Check anyway for paranoia
257 throw new RuntimeException( "Could not compile template `{$filename}`" );
258 }
259
260 $files = array_values( array_unique( $files ) );
261
262 return [
263 'phpCode' => $compiled,
264 'files' => $files,
265 'filesHash' => FileContentsHasher::getFileContentsHash( $files ),
266 ];
267 }
268
289 public function processTemplate( $templateName, $args, array $scopes = [] ) {
290 $template = $this->getTemplate( $templateName );
291 return $template( $args, $scopes );
292 }
293}
const CACHE_ANYTHING
Definition Defines.php:85
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
processTemplate( $templateName, $args, array $scopes=[])
Returns HTML for a given template by calling the template function with the given args.
__construct( $templateDir=null, ?BagOStuff $cache=null)
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.
compile( $templateName)
Compile the Mustache template into PHP code using LightnCandy.
enableRecursivePartials( $enable)
Enable/disable the use of recursive partials.
getTemplateFilename( $templateName)
Constructs the location of the source Mustache template.
callable[] $renderers
Array of cached rendering functions.
$cache
Definition mcc.php:33
if( $line===false) $args
Definition mcc.php:124