MediaWiki REL1_37
TemplateParser.php
Go to the documentation of this file.
1<?php
2
3use LightnCandy\LightnCandy;
5
28
29 private const CACHE_VERSION = '2.2.0';
30 private const CACHE_TTL = BagOStuff::TTL_WEEK;
31
35 private $cache;
36
40 protected $templateDir;
41
45 protected $renderers;
46
50 protected $compileFlags;
51
57 public function __construct( $templateDir = null, $cache = null ) {
58 $this->templateDir = $templateDir ?: __DIR__ . '/templates';
59 if ( $cache === true ) {
60 wfDeprecated( __CLASS__ . ' with $forceRecompile', '1.35' );
61 $cache = new EmptyBagOStuff();
62 }
63 $this->cache = $cache ?: ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
64
65 // Do not add more flags here without discussion.
66 // If you do add more flags, be sure to update unit tests as well.
67 $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
68 }
69
74 public function enableRecursivePartials( $enable ) {
75 if ( $enable ) {
76 $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
77 } else {
78 $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
79 }
80 }
81
88 protected function getTemplateFilename( $templateName ) {
89 // Prevent path traversal. Based on Language::isValidCode().
90 // This is for paranoia. The $templateName should never come from
91 // untrusted input.
92 if (
93 strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName )
94 ) {
95 throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
96 }
97
98 return "{$this->templateDir}/{$templateName}.mustache";
99 }
100
109 protected function getTemplate( $templateName ) {
110 $templateKey = $templateName . '|' . $this->compileFlags;
111
112 // If a renderer has already been defined for this template, reuse it
113 if ( isset( $this->renderers[$templateKey] ) &&
114 is_callable( $this->renderers[$templateKey] )
115 ) {
116 return $this->renderers[$templateKey];
117 }
118
119 // Fetch a secret key for building a keyed hash of the PHP code.
120 // Note that this may be called before MediaWiki is fully initialized.
121 $secretKey = MediaWikiServices::hasInstance()
122 ? MediaWikiServices::getInstance()->getMainConfig()->get( 'SecretKey' )
123 : null;
124
125 if ( $secretKey ) {
126 // See if the compiled PHP code is stored in the server-local cache.
127 $key = $this->cache->makeKey(
128 'lightncandy-compiled',
129 self::CACHE_VERSION,
130 $this->compileFlags,
131 $this->templateDir,
132 $templateName
133 );
134 $compiledTemplate = $this->cache->get( $key );
135
136 // 1. Has the template changed since the compiled template was cached? If so, don't use
137 // the cached code.
138 if ( $compiledTemplate ) {
139 $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
140
141 if ( $filesHash !== $compiledTemplate['filesHash'] ) {
142 $compiledTemplate = null;
143 }
144 }
145
146 // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
147 // code.
148 if ( $compiledTemplate ) {
149 $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
150
151 if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
152 $compiledTemplate = null;
153 }
154 }
155
156 // We're not using the cached code for whathever reason. Recompile the template and
157 // cache it.
158 if ( !$compiledTemplate ) {
159 $compiledTemplate = $this->compile( $templateName );
160
161 $compiledTemplate['integrityHash'] = hash_hmac(
162 'sha256',
163 $compiledTemplate['phpCode'],
164 $secretKey
165 );
166
167 $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
168 }
169
170 // If there is no secret key available, don't use cache
171 } else {
172 $compiledTemplate = $this->compile( $templateName );
173 }
174
175 $renderer = eval( $compiledTemplate['phpCode'] );
176 if ( !is_callable( $renderer ) ) {
177 throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
178 }
179 $this->renderers[$templateKey] = $renderer;
180 return $renderer;
181 }
182
214 protected function compile( $templateName ) {
215 $filename = $this->getTemplateFilename( $templateName );
216
217 if ( !file_exists( $filename ) ) {
218 throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
219 }
220
221 $files = [ $filename ];
222 $contents = file_get_contents( $filename );
223 $compiled = LightnCandy::compile(
224 $contents,
225 [
226 'flags' => $this->compileFlags,
227 'basedir' => $this->templateDir,
228 'fileext' => '.mustache',
229 'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
230 $filename = "{$this->templateDir}/{$partialName}.mustache";
231 if ( !file_exists( $filename ) ) {
232 throw new RuntimeException( sprintf(
233 'Could not compile template `%s`: Could not find partial `%s` at %s',
234 $templateName,
235 $partialName,
236 $filename
237 ) );
238 }
239
240 $fileContents = file_get_contents( $filename );
241
242 if ( $fileContents === false ) {
243 throw new RuntimeException( sprintf(
244 'Could not compile template `%s`: Could not find partial `%s` at %s',
245 $templateName,
246 $partialName,
247 $filename
248 ) );
249 }
250
251 $files[] = $filename;
252
253 return $fileContents;
254 }
255 ]
256 );
257 if ( !$compiled ) {
258 // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
259 // Errors should throw exceptions instead of returning false
260 // Check anyway for paranoia
261 throw new RuntimeException( "Could not compile template `{$filename}`" );
262 }
263
264 return [
265 'phpCode' => $compiled,
266 'files' => $files,
267 'filesHash' => FileContentsHasher::getFileContentsHash( $files ),
268 ];
269 }
270
291 public function processTemplate( $templateName, $args, array $scopes = [] ) {
292 $template = $this->getTemplate( $templateName );
293 return $template( $args, $scopes );
294 }
295}
const CACHE_ANYTHING
Definition Defines.php:85
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:86
A BagOStuff object with no objects in it.
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
MediaWikiServices is the service locator for the application scope of MediaWiki.
processTemplate( $templateName, $args, array $scopes=[])
Returns HTML for a given template by calling the template function with the given args.
string $templateDir
The path to the Mustache templates.
getTemplate( $templateName)
Returns a given template function if found, otherwise throws an exception.
__construct( $templateDir=null, $cache=null)
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.
if( $line===false) $args
Definition mcc.php:124