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 
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  }
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  $config = MediaWikiServices::getInstance()->getMainConfig();
121  $secretKey = $config->get( 'SecretKey' );
122 
123  if ( $secretKey ) {
124  // See if the compiled PHP code is stored in the server-local cache.
125  $key = $this->cache->makeKey(
126  'lightncandy-compiled',
127  self::CACHE_VERSION,
128  $this->compileFlags,
129  $this->templateDir,
130  $templateName
131  );
132  $compiledTemplate = $this->cache->get( $key );
133 
134  // 1. Has the template changed since the compiled template was cached? If so, don't use
135  // the cached code.
136  if ( $compiledTemplate ) {
137  $filesHash = FileContentsHasher::getFileContentsHash( $compiledTemplate['files'] );
138 
139  if ( $filesHash !== $compiledTemplate['filesHash'] ) {
140  $compiledTemplate = null;
141  }
142  }
143 
144  // 2. Is the integrity of the cached PHP code compromised? If so, don't use the cached
145  // code.
146  if ( $compiledTemplate ) {
147  $integrityHash = hash_hmac( 'sha256', $compiledTemplate['phpCode'], $secretKey );
148 
149  if ( $integrityHash !== $compiledTemplate['integrityHash'] ) {
150  $compiledTemplate = null;
151  }
152  }
153 
154  // We're not using the cached code for whathever reason. Recompile the template and
155  // cache it.
156  if ( !$compiledTemplate ) {
157  $compiledTemplate = $this->compile( $templateName );
158 
159  $compiledTemplate['integrityHash'] = hash_hmac(
160  'sha256',
161  $compiledTemplate['phpCode'],
162  $secretKey
163  );
164 
165  $this->cache->set( $key, $compiledTemplate, self::CACHE_TTL );
166  }
167 
168  // If there is no secret key available, don't use cache
169  } else {
170  $compiledTemplate = $this->compile( $templateName );
171  }
172 
173  $renderer = eval( $compiledTemplate['phpCode'] );
174  if ( !is_callable( $renderer ) ) {
175  throw new RuntimeException( "Compiled template `{$templateName}` is not callable" );
176  }
177  $this->renderers[$templateKey] = $renderer;
178  return $renderer;
179  }
180 
212  protected function compile( $templateName ) {
213  $filename = $this->getTemplateFilename( $templateName );
214 
215  if ( !file_exists( $filename ) ) {
216  throw new RuntimeException( "Could not find template `{$templateName}` at {$filename}" );
217  }
218 
219  $files = [ $filename ];
220  $contents = file_get_contents( $filename );
221  $compiled = LightnCandy::compile(
222  $contents,
223  [
224  'flags' => $this->compileFlags,
225  'basedir' => $this->templateDir,
226  'fileext' => '.mustache',
227  'partialresolver' => function ( $cx, $partialName ) use ( $templateName, &$files ) {
228  $filename = "{$this->templateDir}/{$partialName}.mustache";
229  if ( !file_exists( $filename ) ) {
230  throw new RuntimeException( sprintf(
231  'Could not compile template `%s`: Could not find partial `%s` at %s',
232  $templateName,
233  $partialName,
234  $filename
235  ) );
236  }
237 
238  $fileContents = file_get_contents( $filename );
239 
240  if ( $fileContents === false ) {
241  throw new RuntimeException( sprintf(
242  'Could not compile template `%s`: Could not find partial `%s` at %s',
243  $templateName,
244  $partialName,
245  $filename
246  ) );
247  }
248 
249  $files[] = $filename;
250 
251  return $fileContents;
252  }
253  ]
254  );
255  if ( !$compiled ) {
256  // This shouldn't happen because LightnCandy::FLAG_ERROR_EXCEPTION is set
257  // Errors should throw exceptions instead of returning false
258  // Check anyway for paranoia
259  throw new RuntimeException( "Could not compile template `{$filename}`" );
260  }
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 }
TemplateParser\__construct
__construct( $templateDir=null, $cache=null)
Definition: TemplateParser.php:57
EmptyBagOStuff
A BagOStuff object with no objects in it.
Definition: EmptyBagOStuff.php:29
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:149
TemplateParser\compile
compile( $templateName)
Compile the Mustache template into PHP code using LightnCandy.
Definition: TemplateParser.php:212
BagOStuff
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:70
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:289
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
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
$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:74
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:90
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:109
TemplateParser\$cache
BagOStuff $cache
Definition: TemplateParser.php:35
TemplateParser\getTemplateFilename
getTemplateFilename( $templateName)
Constructs the location of the source Mustache template.
Definition: TemplateParser.php:88
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:254