MediaWiki  master
TemplateParser.php
Go to the documentation of this file.
1 <?php
2 
3 use 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
static getFileContentsHash( $filePaths)
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from configuration)
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.
if( $line===false) $args
Definition: mcc.php:124