MediaWiki  master
TemplateParser.php
Go to the documentation of this file.
1 <?php
2 
3 use 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 
56  public function __construct( $templateDir = null, ?BagOStuff $cache = null ) {
57  $this->templateDir = $templateDir ?: __DIR__ . '/templates';
59 
60  // Do not add more flags here without discussion.
61  // If you do add more flags, be sure to update unit tests as well.
62  $this->compileFlags = LightnCandy::FLAG_ERROR_EXCEPTION | LightnCandy::FLAG_MUSTACHELOOKUP;
63  }
64 
69  public function enableRecursivePartials( $enable ) {
70  if ( $enable ) {
71  $this->compileFlags |= LightnCandy::FLAG_RUNTIMEPARTIAL;
72  } else {
73  $this->compileFlags &= ~LightnCandy::FLAG_RUNTIMEPARTIAL;
74  }
75  }
76 
83  protected function getTemplateFilename( $templateName ) {
84  // Prevent path traversal. Based on Language::isValidCode().
85  // This is for paranoia. The $templateName should never come from
86  // untrusted input.
87  if (
88  strcspn( $templateName, ":/\\\000&<>'\"%" ) !== strlen( $templateName )
89  ) {
90  throw new UnexpectedValueException( "Malformed \$templateName: $templateName" );
91  }
92 
93  return "{$this->templateDir}/{$templateName}.mustache";
94  }
95 
104  protected function getTemplate( $templateName ) {
105  $templateKey = $templateName . '|' . $this->compileFlags;
106 
107  // If a renderer has already been defined for this template, reuse it
108  if ( isset( $this->renderers[$templateKey] ) &&
109  is_callable( $this->renderers[$templateKey] )
110  ) {
111  return $this->renderers[$templateKey];
112  }
113 
114  // Fetch a secret key for building a keyed hash of the PHP code
115  $config = MediaWikiServices::getInstance()->getMainConfig();
116  $secretKey = $config->get( 'SecretKey' );
117 
118  if ( $secretKey ) {
119  // See if the compiled PHP code is stored in the server-local cache.
120  $key = $this->cache->makeKey(
121  'lightncandy-compiled',
122  self::CACHE_VERSION,
123  $this->compileFlags,
124  $this->templateDir,
125  $templateName
126  );
127  $compiledTemplate = $this->cache->get( $key );
128 
129  // 1. Has the template changed since the compiled template was cached? If so, don't use
130  // the cached code.
131  if ( $compiledTemplate ) {
132  $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
133 
134  if ( $filesHash !== $compiledTemplate['filesHash'] ) {
135  $compiledTemplate = null;
136  }
137  }
138 
139  // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
140  // code.
141  if ( $compiledTemplate ) {
142  $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
143 
144  if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
145  $compiledTemplate = null;
146  }
147  }
148 
149  // We're not using the cached code for whathever reason. Recompile the template and
150  // cache it.
151  if ( !$compiledTemplate ) {
152  $compiledTemplate = $this->compile( $templateName );
153 
154  $compiledTemplate['integrityHash'] = hash_hmac(
155  'sha256',
156  $compiledTemplate['phpCode'],
157  $secretKey
158  );
159 
160  $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
161  }
162 
163  // If there is no secret key available, don't use cache
164  } else {
165  $compiledTemplate = $this->compile( $templateName );
166  }
167 
168  $renderer = eval( $compiledTemplate['phpCode'] );
169  if ( !is_callable( $renderer ) ) {
170  throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
171  }
172  $this->renderers[$templateKey] = $renderer;
173  return $renderer;
174  }
175 
207  protected function compile( $templateName ) {
208  $filename = $this->getTemplateFilename( $templateName );
209 
210  if ( !file_exists( $filename ) ) {
211  throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
212  }
213 
214  $files = [ $filename ];
215  $contents = file_get_contents( $filename );
216  $compiled = LightnCandy::compile(
217  $contents,
218  [
219  'flags' => $this->compileFlags,
220  'basedir' => $this->templateDir,
221  'fileext' => '.mustache',
222  'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
223  $filename = "{$this->templateDir}/{$partialName}.mustache";
224  if ( !file_exists( $filename ) ) {
225  throw new RuntimeException( sprintf(
226  'Could not compile template `%s`: Could not find partial `%s` at %s',
227  $templateName,
228  $partialName,
229  $filename
230  ) );
231  }
232 
233  $fileContents = file_get_contents( $filename );
234 
235  if ( $fileContents === false ) {
236  throw new RuntimeException( sprintf(
237  'Could not compile template `%s`: Could not find partial `%s` at %s',
238  $templateName,
239  $partialName,
240  $filename
241  ) );
242  }
243 
244  $files[] = $filename;
245 
246  return $fileContents;
247  }
248  ]
249  );
250  if ( !$compiled ) {
251  // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
252  // Errors should throw exceptions instead of returning false
253  // Check anyway for paranoia
254  throw new RuntimeException( "Could not compile template `{$filename}`" );
255  }
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 }
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:85
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
TemplateParser\compile
compile( $templateName)
Compile the Mustache template into PHP code using LightnCandy.
Definition: TemplateParser.php:207
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:86
TemplateParser\processTemplate
processTemplate( $templateName, $args, array $scopes=[])
Returns HTML for a given template by calling the template function with the given args.
Definition: TemplateParser.php:284
TemplateParser\__construct
__construct( $templateDir=null, ?BagOStuff $cache=null)
Definition: TemplateParser.php:56
FileContentsHasher\getFileContentsHash
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
Definition: FileContentsHasher.php:88
TemplateParser\CACHE_VERSION
const CACHE_VERSION
Definition: TemplateParser.php:29
$args
if( $line===false) $args
Definition: mcc.php:124
TemplateParser\$compileFlags
int $compileFlags
Compilation flags passed to LightnCandy.
Definition: TemplateParser.php:50
TemplateParser\enableRecursivePartials
enableRecursivePartials( $enable)
Enable/disable the use of recursive partials.
Definition: TemplateParser.php:69
TemplateParser\$renderers
callable[] $renderers
Array of cached rendering functions.
Definition: TemplateParser.php:45
TemplateParser\getTemplate
getTemplate( $templateName)
Returns a given template function if found, otherwise throws an exception.
Definition: TemplateParser.php:104
TemplateParser\$cache
BagOStuff $cache
Definition: TemplateParser.php:35
TemplateParser\getTemplateFilename
getTemplateFilename( $templateName)
Constructs the location of the source Mustache template.
Definition: TemplateParser.php:83
TemplateParser\CACHE_TTL
const CACHE_TTL
Definition: TemplateParser.php:30
TemplateParser
Definition: TemplateParser.php:27
TemplateParser\$templateDir
string $templateDir
The path to the Mustache templates.
Definition: TemplateParser.php:40
ObjectCache\getLocalServerInstance
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
Definition: ObjectCache.php:255