816 lines
26 KiB
Text
816 lines
26 KiB
Text
|
<?php namespace ProcessWire;
|
|||
|
|
|||
|
/**
|
|||
|
* ProcessWire PageRender Module
|
|||
|
*
|
|||
|
* Adds a render method to Page, as used by the PageView Process.
|
|||
|
* This module is also able to cache page renders.
|
|||
|
* It hooks into Pages and Fieldtypes to ensure cache files are cleaned/deleted when pages are saved/deleted.
|
|||
|
*
|
|||
|
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
|||
|
* https://processwire.com
|
|||
|
*
|
|||
|
* @method renderPage(HookEvent $event)
|
|||
|
* @method void clearCacheFileAll(Page $page)
|
|||
|
* @method void clearCacheFilePages(PageArray $items, Page $page)
|
|||
|
* @method string saveCacheFileReady(Page $page, $data)
|
|||
|
*
|
|||
|
*/
|
|||
|
|
|||
|
class PageRender extends WireData implements Module, ConfigurableModule {
|
|||
|
|
|||
|
const cacheDirName = 'Page';
|
|||
|
|
|||
|
public static function getModuleInfo() {
|
|||
|
return array(
|
|||
|
'title' => __('Page Render', __FILE__), // Module Title
|
|||
|
'summary' => __('Adds a render method to Page and caches page output.', __FILE__), // Module Summary
|
|||
|
'version' => 105,
|
|||
|
'permanent' => true,
|
|||
|
'singular' => true,
|
|||
|
'autoload' => true,
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Instance of Config, cached wire('config')
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $config;
|
|||
|
|
|||
|
/**
|
|||
|
* Stack of pages when rendering recursively
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $pageStack = array();
|
|||
|
|
|||
|
/**
|
|||
|
* Keeps track of recursion level when rendering recursively
|
|||
|
*
|
|||
|
* Used to determine when pageStack should be maintained
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $renderRecursionLevel = 0;
|
|||
|
|
|||
|
/**
|
|||
|
* Page that get() method should pull properties from for rendering fields
|
|||
|
*
|
|||
|
* Note: every get() call sets this back to NULL after it has executed.
|
|||
|
*
|
|||
|
* @var null|Page
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $propertyPage = null;
|
|||
|
|
|||
|
/**
|
|||
|
* Initialize the hooks
|
|||
|
*
|
|||
|
*/
|
|||
|
public function init() {
|
|||
|
$this->useFuel(false);
|
|||
|
$this->config = $this->wire('config');
|
|||
|
$this->addHook('Page::render', $this, 'renderPage');
|
|||
|
// $this->addHook('Page::renderField', $this, 'renderField');
|
|||
|
$this->wire('pages')->addHookAfter('save', $this, 'clearCacheFile');
|
|||
|
$this->wire('pages')->addHookAfter('delete', $this, 'clearCacheFile');
|
|||
|
// $this->addHookAfter('Fieldtype::savePageField', $this, 'savePageField'); // removed, see note in commented function
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* API ready
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ready() {
|
|||
|
// todo 3.0.190: remove template!=admin condition: https://github.com/processwire/processwire-issues/issues/1424
|
|||
|
if($this->wire()->page->template != 'admin') {
|
|||
|
$this->addHookBefore('Page::render', $this, 'beforeRenderPage', array('priority' => 1));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set page for get() properties / field rendering
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setPropertyPage(Page $page) {
|
|||
|
$this->propertyPage = $page;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Handle page field renders like $page->render->title
|
|||
|
*
|
|||
|
* @param string $key
|
|||
|
* @return string|mixed
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __get($key) {
|
|||
|
if(!$this->propertyPage) return parent::__get($key);
|
|||
|
$out = $this->propertyPage->renderField($key);
|
|||
|
$this->propertyPage = null;
|
|||
|
return $out;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Is the page render cache allowed for this request?
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
public function isCacheAllowed($page) {
|
|||
|
|
|||
|
if(!$page->template || ((int) $page->template->cache_time) < 1) return false;
|
|||
|
|
|||
|
if(!$this->wire('user')->isGuest()) {
|
|||
|
if(!$page->template->useCacheForUsers) return false;
|
|||
|
if($page->editable()) return false;
|
|||
|
}
|
|||
|
|
|||
|
$allowed = true;
|
|||
|
|
|||
|
if(count($_GET) && $page->template->noCacheGetVars) {
|
|||
|
if(strpos($page->template->noCacheGetVars, '*') !== false) {
|
|||
|
$allowed = false;
|
|||
|
} else {
|
|||
|
$vars = explode(' ', $page->template->noCacheGetVars);
|
|||
|
foreach($vars as $name) if($name && isset($_GET[$name])) $allowed = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($allowed && count($_POST) && $page->template->noCachePostVars) {
|
|||
|
if(strpos($page->template->noCachePostVars, '*') !== false) {
|
|||
|
$allowed = false;
|
|||
|
} else {
|
|||
|
$vars = explode(' ', $page->template->noCachePostVars);
|
|||
|
foreach($vars as $name) if($name && isset($_POST[$name])) $allowed = false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// NOTE: other modules may set a session var of PageRenderNoCachePage containing a page ID to temporarily
|
|||
|
// remove caching for some page, if necessary.
|
|||
|
if($this->wire('session')->PageRenderNoCachePage && $this->wire('session')->PageRenderNoCachePage == $page->id) $allowed = false;
|
|||
|
|
|||
|
return $allowed;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get a CacheFile object corresponding to this Page
|
|||
|
*
|
|||
|
* Note that this does not check if the page is cachable. This is so that if a cachable setting changes the cache can still be removed.
|
|||
|
*
|
|||
|
* @param int|Page $page May provide page id (int) only if using for deleting a cache file. Must provide Page object otherwise.
|
|||
|
* @param array $options
|
|||
|
* @return CacheFile
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getCacheFile($page, array $options = array()) {
|
|||
|
|
|||
|
$config = $this->config;
|
|||
|
$defaults = array(
|
|||
|
'prependFile' => '',
|
|||
|
'appendFile' => '',
|
|||
|
'filename' => '',
|
|||
|
);
|
|||
|
|
|||
|
$options = array_merge($defaults, $options);
|
|||
|
$path = $config->paths->cache . self::cacheDirName . "/";
|
|||
|
if(is_object($page)) {
|
|||
|
$id = $page->id;
|
|||
|
$cacheTime = (int) $page->template->cache_time;
|
|||
|
} else {
|
|||
|
$id = (int) $page;
|
|||
|
$cacheTime = 3600;
|
|||
|
}
|
|||
|
|
|||
|
if(!is_dir($path)) {
|
|||
|
if(!$this->wire('files')->mkdir($path, true)) throw new WireException("Cache path does not exist: $path");
|
|||
|
if($config->chmodDir) chmod($path, octdec($config->chmodDir));
|
|||
|
}
|
|||
|
|
|||
|
$cacheFile = new CacheFile($path, $id, $cacheTime);
|
|||
|
|
|||
|
if($this->wire('page') === $page) {
|
|||
|
// this part is skipped if arguments provided an id (int) rather than a Page object
|
|||
|
$secondaryID = '';
|
|||
|
$pageNum = $this->wire('input')->pageNum;
|
|||
|
$urlSegments = $this->wire('input')->urlSegments;
|
|||
|
|
|||
|
if(count($urlSegments)) {
|
|||
|
foreach($urlSegments as $urlSegment) {
|
|||
|
$secondaryID .= $this->wire('sanitizer')->pageName($urlSegment) . '+';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($options['prependFile'] || $options['appendFile'] || $options['filename']) {
|
|||
|
$secondaryID .= md5($options['prependFile'] . '+' . $options['appendFile'] . '+' . $options['filename']) . '+';
|
|||
|
}
|
|||
|
if($config->ajax) $secondaryID .= 'ajax+'; // #1262
|
|||
|
if($config->https) $secondaryID .= 'https+';
|
|||
|
if($pageNum > 1) $secondaryID .= "page{$pageNum}";
|
|||
|
$secondaryID = rtrim($secondaryID, '+');
|
|||
|
if($this->wire('languages')) {
|
|||
|
$language = $this->wire('user')->language;
|
|||
|
if($language && $language->id && !$language->isDefault()) $secondaryID .= "_" . $language->id;
|
|||
|
}
|
|||
|
if($secondaryID) $cacheFile->setSecondaryID($secondaryID);
|
|||
|
}
|
|||
|
|
|||
|
return $cacheFile;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Clear all cached pages
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___clearCacheFileAll(Page $page) {
|
|||
|
if($page->template->cache_time > 0) {
|
|||
|
$cacheFile = $this->getCacheFile($page);
|
|||
|
$cacheFile->expireAll();
|
|||
|
}
|
|||
|
|
|||
|
if($this->config->debug && $page->template->cache_time != 0) {
|
|||
|
$this->message($this->_('Expired page cache for entire site'));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Clear cache for multiple pages by ID
|
|||
|
*
|
|||
|
* @param PageArray $items
|
|||
|
* @param Page $page Page that initiated the clear
|
|||
|
* @throws WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___clearCacheFilePages(PageArray $items, Page $page) {
|
|||
|
if($page) {}
|
|||
|
foreach($items as $p) {
|
|||
|
if(((int) $p->template->cache_time) < 1) continue;
|
|||
|
$cf = $this->getCacheFile($p);
|
|||
|
if($cf->exists()) $cf->remove();
|
|||
|
// if($this->config->debug) $this->message(sprintf($this->_('Cleared cache file: %s'), $cf));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook to clear the cache file after a Pages::save or Pages::delete call
|
|||
|
*
|
|||
|
* @param HookEvent $event
|
|||
|
*
|
|||
|
*/
|
|||
|
public function clearCacheFile($event) {
|
|||
|
|
|||
|
$page = $event->arguments[0];
|
|||
|
if(((int) $page->template->cache_time) == 0) return;
|
|||
|
$cacheExpire = $page->template->cacheExpire;
|
|||
|
|
|||
|
if($cacheExpire == Template::cacheExpireNone) {
|
|||
|
if($event->method == 'delete') $cacheExpire = Template::cacheExpirePage;
|
|||
|
else return;
|
|||
|
}
|
|||
|
|
|||
|
if($cacheExpire == Template::cacheExpireSite) {
|
|||
|
// expire entire cache
|
|||
|
$this->clearCacheFileAll($page);
|
|||
|
|
|||
|
} else {
|
|||
|
|
|||
|
// clear the page that was saved
|
|||
|
if($page->template->cache_time > 0) {
|
|||
|
$cacheFile = $this->getCacheFile($page);
|
|||
|
if($cacheFile->exists()) {
|
|||
|
$cacheFile->remove();
|
|||
|
$this->message($this->_('Cleared cache file:') . " $cacheFile", Notice::debug);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$pageIDs = array();
|
|||
|
|
|||
|
if($cacheExpire == Template::cacheExpireParents || $cacheExpire == Template::cacheExpireSpecific) {
|
|||
|
// expire specific pages or parents
|
|||
|
if($cacheExpire == Template::cacheExpireParents) {
|
|||
|
foreach($page->parents as $parent) $pageIDs[] = $parent->id;
|
|||
|
|
|||
|
} else if(is_array($page->template->cacheExpirePages) && count($page->template->cacheExpirePages)) {
|
|||
|
$pageIDs = $page->template->cacheExpirePages;
|
|||
|
}
|
|||
|
} else if($cacheExpire == Template::cacheExpireSelector && $page->template->cacheExpireSelector) {
|
|||
|
// expire pages matching a selector
|
|||
|
$finder = $this->wire(new PageFinder());
|
|||
|
$selectors = new Selectors();
|
|||
|
$selectors->init($page->template->cacheExpireSelector);
|
|||
|
$pageIDs = $finder->findIDs($selectors, array(
|
|||
|
'getTotal' => false,
|
|||
|
'findHidden' => true
|
|||
|
));
|
|||
|
}
|
|||
|
|
|||
|
if(count($pageIDs)) {
|
|||
|
$items = $this->wire('pages')->getById($pageIDs, array(
|
|||
|
'cache' => false,
|
|||
|
'getNumChildren' => false,
|
|||
|
'autojoin' => false,
|
|||
|
'findTemplates' => false,
|
|||
|
'joinSortfield' => false
|
|||
|
));
|
|||
|
if(!$items->has($page)) $items->add($page);
|
|||
|
} else {
|
|||
|
$items = new PageArray();
|
|||
|
$items->add($page);
|
|||
|
}
|
|||
|
if(count($items)) {
|
|||
|
$this->clearCacheFilePages($items, $page);
|
|||
|
$this->message(sprintf($this->_('Cleared cache file for %d page(s)'), count($items)), Notice::debug);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called when cache file is about to be saved for a Page
|
|||
|
*
|
|||
|
* #pw-hooker
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @param string $data Data that will be saved to cache file
|
|||
|
* @return string Data to save to cache file
|
|||
|
* @since 3.0.178
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___saveCacheFileReady(Page $page, $data) {
|
|||
|
if($page) {} // ignore
|
|||
|
return $data;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called before any other hooks to Page::render
|
|||
|
*
|
|||
|
* We use this to determine if Page::render() should be a render() or a renderField()
|
|||
|
*
|
|||
|
* @param HookEvent $event
|
|||
|
*
|
|||
|
*/
|
|||
|
public function beforeRenderPage(HookEvent $event) {
|
|||
|
$fieldName = $event->arguments(0);
|
|||
|
if($fieldName && is_string($fieldName) && $this->wire('sanitizer')->fieldName($fieldName) === $fieldName) {
|
|||
|
// render field requested, cancel page render and hooks, and delegate to renderField
|
|||
|
$file = $event->arguments(1); // optional basename of file to use for render
|
|||
|
if(!is_string($file)) $file = null;
|
|||
|
$event->cancelHooks = true;
|
|||
|
$event->replace = true;
|
|||
|
/** @var Page $page */
|
|||
|
$page = $event->object;
|
|||
|
$event->return = $page->renderField($fieldName, $file);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return a string with the rendered output of this Page from its template file
|
|||
|
*
|
|||
|
* This method provides the implementation for `$page->render()` and you sould only call this method as `render()` from Page objects.
|
|||
|
* You may optionally specify 1-2 arguments to the method. The first argument may be an array of options OR filename (string) to render.
|
|||
|
* If specifying a filename in the first argument, you can optionally specify the $options array as the 2nd argument.
|
|||
|
* If using the options argument, you may specify your own variables to pass along to your template file, and those values will be
|
|||
|
* available in a variable named `$options` within the scope of the template file (see examples below).
|
|||
|
*
|
|||
|
* In addition, the following options are present and recognized by the core:
|
|||
|
*
|
|||
|
* - `forceBuildCache` (bool): If true, the cache will be re-created for this page, regardless of whether it’s expired or not. (default=false)
|
|||
|
* - `allowCache` (bool): Allow cache to be used when template settings ask for it. (default=true)
|
|||
|
* - `filename` (string): Filename to render, optionally relative to /site/templates/. Absolute paths must resolve somewhere in PW’s install. (default=blank)
|
|||
|
* - `prependFile` (string): Filename to prepend to output, must be in /site/templates/.
|
|||
|
* - `prependFiles` (array): Array of additional filenames to prepend to output, must be relative to /site/templates/.
|
|||
|
* - `appendFile` (string): Filename to append to output, must be in /site/templates/.
|
|||
|
* - `appendFiles` (array): Array of additional filenames to append to output, must be relative to /site/templates/.
|
|||
|
* - `pageStack` (array): An array of pages, when recursively rendering. Used internally. You can examine it but not change it.
|
|||
|
*
|
|||
|
* Note that the prepend and append options above have default values that come values configured in `$config` or the Template object.
|
|||
|
*
|
|||
|
* ~~~~~
|
|||
|
* // regular page render call
|
|||
|
* $output = $page->render();
|
|||
|
*
|
|||
|
* // render using given file (in /site/templates/)
|
|||
|
* $output = $page->render('basic-page.php');
|
|||
|
*
|
|||
|
* // render while passing in custom variables
|
|||
|
* $output = $page->render([
|
|||
|
* 'firstName' => 'Ryan',
|
|||
|
* 'lastName' => 'Cramer'
|
|||
|
* ]);
|
|||
|
*
|
|||
|
* // in your template file, you can access the passed-in variables like this:
|
|||
|
* echo "<p>Full name: $options[firstName] $options[lastName]</p>";
|
|||
|
* ~~~~~
|
|||
|
*
|
|||
|
* Note: If the page’s template has caching enabled, then this method will return a cached page render (when available)
|
|||
|
* or save a new cache. Caches are only saved on guest users.
|
|||
|
*
|
|||
|
*
|
|||
|
* @param HookEvent $event
|
|||
|
* @throws WirePermissionException|Wire404Exception|WireException
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___renderPage($event) {
|
|||
|
|
|||
|
/** @var Page $page */
|
|||
|
$page = $event->object;
|
|||
|
|
|||
|
/** @var Template $template */
|
|||
|
$template = $page->template;
|
|||
|
|
|||
|
$this->wire('pages')->setOutputFormatting(true);
|
|||
|
|
|||
|
if($page->status >= Page::statusUnpublished && !$page->viewable()) {
|
|||
|
throw new WirePermissionException("Page '{$page->url}' is not currently viewable.");
|
|||
|
}
|
|||
|
|
|||
|
$_page = $this->wire('page'); // just in case one page is rendering another, save the previous
|
|||
|
$config = $this->wire('config');
|
|||
|
$compiler = null;
|
|||
|
$compilerOptions = array();
|
|||
|
if($config->templateCompile && $template->compile) {
|
|||
|
$compilerOptions = array(
|
|||
|
'namespace' => strlen(__NAMESPACE__) > 0,
|
|||
|
'includes' => $template->compile >= 2 ? true : false,
|
|||
|
'modules' => true,
|
|||
|
'skipIfNamespace' => $template->compile == 3 ? true : false,
|
|||
|
);
|
|||
|
$compiler = $this->wire(new FileCompiler($config->paths->templates, $compilerOptions));
|
|||
|
}
|
|||
|
$this->renderRecursionLevel++;
|
|||
|
|
|||
|
// set the context of the new page to be system-wide
|
|||
|
// only applicable if rendering a page within a page
|
|||
|
if(!$_page || $page->id != $_page->id) $this->wire('page', $page);
|
|||
|
if($this->renderRecursionLevel > 1) $this->pageStack[] = $_page;
|
|||
|
|
|||
|
// arguments to $page->render() may be a string with filename to render or array of options
|
|||
|
$options = $event->arguments(0);
|
|||
|
$options2 = $event->arguments(1);
|
|||
|
|
|||
|
// normalize options to array
|
|||
|
if(is_string($options) && strlen($options)) $options = array('filename' => $options); // arg1 is filename
|
|||
|
if(!is_array($options)) $options = array(); // no args specified
|
|||
|
if(is_array($options2)) $options = array_merge($options2, $options); // arg2 is $options
|
|||
|
|
|||
|
$defaultOptions = array(
|
|||
|
'filename' => '', // default blank means filename comes from $page
|
|||
|
'prependFile' => $template->noPrependTemplateFile ? null : $config->prependTemplateFile,
|
|||
|
'prependFiles' => $template->prependFile ? array($template->prependFile) : array(),
|
|||
|
'appendFile' => $template->noAppendTemplateFile ? null : $config->appendTemplateFile,
|
|||
|
'appendFiles' => $template->appendFile ? array($template->appendFile) : array(),
|
|||
|
'allowCache' => true,
|
|||
|
'forceBuildCache' => false,
|
|||
|
'pageStack' => array(), // set after array_merge
|
|||
|
);
|
|||
|
|
|||
|
$options = array_merge($defaultOptions, $options);
|
|||
|
$options['pageStack'] = $this->pageStack;
|
|||
|
|
|||
|
$cacheAllowed = $options['allowCache'] && $this->isCacheAllowed($page);
|
|||
|
$cacheFile = null;
|
|||
|
|
|||
|
if($cacheAllowed) {
|
|||
|
$cacheFile = $this->getCacheFile($page, $options);
|
|||
|
if(!$options['forceBuildCache'] && ($data = $cacheFile->get()) !== false) {
|
|||
|
$event->return = $data;
|
|||
|
if($_page) $this->wire('page', $_page);
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
$of = $page->of();
|
|||
|
if(!$of) $page->of(true);
|
|||
|
|
|||
|
$data = '';
|
|||
|
$output = $page->output(true);
|
|||
|
if($output) {
|
|||
|
|
|||
|
// global prepend/append include files apply only to user-defined templates, not system templates
|
|||
|
if(!($template->flags & Template::flagSystem)) {
|
|||
|
foreach(array('prependFile' => 'prependFiles', 'appendFile' => 'appendFiles') as $singular => $plural) {
|
|||
|
if($options[$singular]) array_unshift($options[$plural], $options[$singular]);
|
|||
|
foreach($options[$plural] as $file) {
|
|||
|
if(!ctype_alnum(str_replace(array(".", "-", "_", "/"), "", $file))) continue;
|
|||
|
if(strpos($file, '..') !== false || strpos($file, '/.') !== false) continue;
|
|||
|
$file = $config->paths->templates . trim($file, '/');
|
|||
|
if(!is_file($file)) continue;
|
|||
|
if($compiler && $compilerOptions['includes']) {
|
|||
|
$file = $compiler->compile($file);
|
|||
|
}
|
|||
|
if($plural == 'prependFiles') {
|
|||
|
$output->setPrependFilename($file);
|
|||
|
} else {
|
|||
|
$output->setAppendFilename($file);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// option to change the filename that is used for output rendering
|
|||
|
if($options['filename'] && strpos($options['filename'], '..') === false) {
|
|||
|
$filename = $config->paths->templates . ltrim($options['filename'], '/');
|
|||
|
$setFilename = '';
|
|||
|
if(is_file($filename)) {
|
|||
|
// path relative from /site/templates/
|
|||
|
$setFilename = $filename;
|
|||
|
} else {
|
|||
|
// absolute path, ensure it is somewhere within web root
|
|||
|
$filename = $options['filename'];
|
|||
|
if(strpos($filename, $config->paths->root) === 0 && is_file($filename)) $setFilename = $filename;
|
|||
|
}
|
|||
|
|
|||
|
if($setFilename) {
|
|||
|
if($compiler) {
|
|||
|
$output->setChdir(dirname($setFilename));
|
|||
|
$setFilename = $compiler->compile($setFilename);
|
|||
|
}
|
|||
|
$output->setFilename($setFilename);
|
|||
|
$options['filename'] = $setFilename;
|
|||
|
} else {
|
|||
|
throw new WireException("Invalid output file location or specified file does not exist. $setFilename");
|
|||
|
}
|
|||
|
} else {
|
|||
|
if($compiler) {
|
|||
|
$options['filename'] = $compiler->compile($template->filename);
|
|||
|
$output->setFilename($options['filename']);
|
|||
|
$output->setChdir(dirname($template->filename));
|
|||
|
} else {
|
|||
|
$options['filename'] = $template->filename;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// pass along the $options as a local variable to the template so that one can provide their
|
|||
|
// own additional variables in it if they want to
|
|||
|
$output->set('options', $options);
|
|||
|
|
|||
|
$profiler = $this->wire('profiler');
|
|||
|
$profilerEvent = $profiler ? $profiler->start($page->path, $this, array('page' => $page)) : null;
|
|||
|
$data = $output->render();
|
|||
|
if($profilerEvent) $profiler->stop($profilerEvent);
|
|||
|
if(!strlen($data) && $page->template->name === 'admin' && !is_readable($options['filename'])) {
|
|||
|
throw new WireException('Missing or non-readable template file: ' . basename($options['filename']));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($this->wire('config')->useMarkupRegions) {
|
|||
|
$contentType = $template->contentType;
|
|||
|
if(empty($contentType) || stripos($contentType, 'html') !== false) {
|
|||
|
$this->populateMarkupRegions($data);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($data && $cacheAllowed && $cacheFile) {
|
|||
|
$data = $this->saveCacheFileReady($page, $data);
|
|||
|
$cacheFile->save($data);
|
|||
|
}
|
|||
|
|
|||
|
$event->return = $data;
|
|||
|
|
|||
|
if(!$of) $page->of($of);
|
|||
|
if($_page && $_page->id != $page->id) {
|
|||
|
$this->wire('page', $_page);
|
|||
|
}
|
|||
|
if(count($this->pageStack)) array_pop($this->pageStack);
|
|||
|
$this->renderRecursionLevel--;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Populate markup regions directly to $html
|
|||
|
*
|
|||
|
* @param $html
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function populateMarkupRegions(&$html) {
|
|||
|
|
|||
|
$markupRegions = new WireMarkupRegions();
|
|||
|
$this->wire($markupRegions);
|
|||
|
|
|||
|
$pos = stripos($html, '<!DOCTYPE html');
|
|||
|
|
|||
|
if($pos === false) {
|
|||
|
// if no doctype match, attempt an html tag match
|
|||
|
$pos = stripos($html, '<html');
|
|||
|
}
|
|||
|
|
|||
|
// if no document start, or document starts at pos==0, then nothing to populate
|
|||
|
if(!$pos) {
|
|||
|
// there still may be region related stuff that needs to be removed like <region> tags
|
|||
|
$markupRegions->removeRegionTags($html);
|
|||
|
$html = $markupRegions->stripOptional($html);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// split document at doctype/html boundary
|
|||
|
$htmlBefore = substr($html, 0, $pos);
|
|||
|
$html = substr($html, $pos);
|
|||
|
$options = array('useClassActions' => true);
|
|||
|
$config = $this->wire('config');
|
|||
|
$version = (int) $config->useMarkupRegions;
|
|||
|
|
|||
|
if($config->installed >= 1498132609 || $version >= 2) {
|
|||
|
// If PW installed after June 21, 2017 do not use legacy class actions
|
|||
|
// as they are no longer part of the current WireMarkupRegions spec.
|
|||
|
// Can also force this behavior by setting $config->useMarkupRegions = 2;
|
|||
|
$options['useClassActions'] = false;
|
|||
|
}
|
|||
|
|
|||
|
$markupRegions->populate($html, $htmlBefore, $options);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Renders a field value
|
|||
|
*
|
|||
|
* if $fieldName is omitted (blank), a $file and $value must be provided
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @param string $fieldName
|
|||
|
* @param string $file
|
|||
|
* @param mixed $value
|
|||
|
* @return string|mixed
|
|||
|
*
|
|||
|
*/
|
|||
|
public function renderField(Page $page, $fieldName, $file = '', $value = null) {
|
|||
|
|
|||
|
/*
|
|||
|
if(strpos($fieldName, '/') && empty($file)) {
|
|||
|
$file = $fieldName;
|
|||
|
$fieldName = '';
|
|||
|
}
|
|||
|
*/
|
|||
|
if(strlen($fieldName)) {
|
|||
|
$fieldName = $this->wire('sanitizer')->fieldName($fieldName);
|
|||
|
}
|
|||
|
|
|||
|
if(is_null($value) && $fieldName) $value = $page->getFormatted($fieldName);
|
|||
|
if(is_null($value)) return '';
|
|||
|
|
|||
|
if($fieldName) {
|
|||
|
$field = $page->getField($fieldName);
|
|||
|
if(!$field) $field = $this->wire('fields')->get($fieldName);
|
|||
|
$fieldtypeName = $field && $field->type ? $field->type->className() : '';
|
|||
|
} else {
|
|||
|
$field = null;
|
|||
|
$fieldtypeName = '';
|
|||
|
}
|
|||
|
|
|||
|
$path = $this->wire('config')->paths->fieldTemplates;
|
|||
|
$files = array();
|
|||
|
|
|||
|
if($file) {
|
|||
|
// a render file or path was specified
|
|||
|
if(strpos($file, '\\') !== false) $file = str_replace('\\', '/', $file);
|
|||
|
$hasTrailingSlash = substr($file, -1) == '/';
|
|||
|
$hasLeadingSlash = strpos($file, '/') === 0;
|
|||
|
$file = trim($file, '/');
|
|||
|
if(substr($file, -4) == '.php') $file = substr($file, 0, -4);
|
|||
|
|
|||
|
if($hasLeadingSlash && $file) {
|
|||
|
// VERY SPECIFIC
|
|||
|
// use only what was specified
|
|||
|
$files[] = $file;
|
|||
|
|
|||
|
} else if(!$hasTrailingSlash && strpos($file, '/') !== false) {
|
|||
|
// SPECIFIC RENDER FILE
|
|||
|
// file includes a directory off of fields/[dir]
|
|||
|
$parts = explode('/', $file);
|
|||
|
foreach($parts as $key => $part) {
|
|||
|
$parts[$key] = $this->wire('sanitizer')->name($part);
|
|||
|
}
|
|||
|
$file = implode('/', $parts);
|
|||
|
$file = str_replace('..', '', $file);
|
|||
|
// i.e. fields/custom_dir/custom_name.php
|
|||
|
$files[] = $file;
|
|||
|
|
|||
|
} else if($hasTrailingSlash && $fieldName) {
|
|||
|
// GROUP DIRECTORY
|
|||
|
// specifies a group name, referring to a directory, i.e. "group_name/"
|
|||
|
// i.e. fields/custom_name/field_name.php
|
|||
|
$files[] = "$file/$fieldName";
|
|||
|
|
|||
|
} else if($fieldName) {
|
|||
|
// FIELD DIRECTORY WITH CUSTOM NAMED RENDER FILE
|
|||
|
// i.e. fields/field_name/custom_name.php
|
|||
|
$files[] = "$fieldName/$file";
|
|||
|
|
|||
|
// GROUP DIRECTORY WITH FIELD NAMED RENDER FILE
|
|||
|
// i.e. fields/field_name/custom_name.php
|
|||
|
$files[] = "$file/$fieldName";
|
|||
|
|
|||
|
// CUSTOM NAMED RENDER FILE ONLY (NO GROUP)
|
|||
|
// i.e. fields/custom_name.php
|
|||
|
$files[] = $file;
|
|||
|
} else {
|
|||
|
$files[] = $file;
|
|||
|
}
|
|||
|
|
|||
|
} else if($fieldName) {
|
|||
|
// no render file was specified, check for possible template context files
|
|||
|
|
|||
|
if(strpos($fieldtypeName, 'Repeater') === false) {
|
|||
|
// FIELD DIRECTORY WITH TEMPLATE NAME
|
|||
|
// i.e. fields/field_name/template_name.php
|
|||
|
$files[] = "$fieldName/{$page->template->name}";
|
|||
|
}
|
|||
|
|
|||
|
// TEMPLATE DIRECTORY WITH FIELD NAME
|
|||
|
// i.e. fields/template_name/field_name.php
|
|||
|
$files[] = "{$page->template->name}/$fieldName";
|
|||
|
|
|||
|
// FIELD NAME WITH TEMPLATE NAME
|
|||
|
// i.e. fields/field_name.template_name.php
|
|||
|
$files[] = "$fieldName.{$page->template->name}";
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
// LAST FALLBACK/DEFAULT
|
|||
|
// i.e. fields/field_name.php
|
|||
|
if($fieldName) $files[] = $fieldName;
|
|||
|
|
|||
|
$renderFile = '';
|
|||
|
|
|||
|
foreach($files as $f) {
|
|||
|
$file = "$path$f.php";
|
|||
|
if(!is_file($file)) continue;
|
|||
|
$renderFile = $file;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
if(!$renderFile) {
|
|||
|
if($fieldName) {
|
|||
|
return $page->getMarkup($fieldName);
|
|||
|
} else {
|
|||
|
return '';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($this->wire('config')->templateCompile) {
|
|||
|
$renderFile = $this->wire('files')->compile($renderFile, array('skipIfNamespace' => true));
|
|||
|
}
|
|||
|
|
|||
|
$tpl = $this->wire(new TemplateFile($renderFile));
|
|||
|
$tpl->set('page', $page);
|
|||
|
$tpl->set('value', $value);
|
|||
|
$tpl->set('field', $field);
|
|||
|
|
|||
|
return $tpl->render();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Provide a disk cache clearing capability within the module's configuration screen
|
|||
|
*
|
|||
|
* @param array $data
|
|||
|
* @return InputfieldWrapper
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getModuleConfigInputfields(array $data) {
|
|||
|
|
|||
|
if($data) {}
|
|||
|
$path = $this->wire('config')->paths->cache . self::cacheDirName . '/';
|
|||
|
$numPages = 0;
|
|||
|
$numFiles = 0;
|
|||
|
$inputfields = $this->wire(new InputfieldWrapper());
|
|||
|
$dir = null;
|
|||
|
$clearNow = $this->wire('input')->post->clearCache ? true : false;
|
|||
|
|
|||
|
try { $dir = new \DirectoryIterator($path); } catch(\Exception $e) { }
|
|||
|
|
|||
|
if($dir) foreach($dir as $file) {
|
|||
|
if(!$file->isDir() || $file->isDot() || !ctype_digit($file->getFilename())) continue;
|
|||
|
$numPages++;
|
|||
|
if(!$clearNow) continue;
|
|||
|
$d = new \DirectoryIterator($file->getPathname());
|
|||
|
foreach($d as $f) {
|
|||
|
if(!$f->isDir() && preg_match('/\.cache$/D', $f->getFilename())) {
|
|||
|
$numFiles++;
|
|||
|
$this->wire('files')->unlink($f->getPathname());
|
|||
|
}
|
|||
|
}
|
|||
|
$this->wire('files')->rmdir($file->getPathname());
|
|||
|
}
|
|||
|
|
|||
|
if($clearNow) {
|
|||
|
$inputfields->message(sprintf($this->_('Cleared %d cache files for %d pages'), $numFiles, $numPages));
|
|||
|
$numPages = 0;
|
|||
|
}
|
|||
|
|
|||
|
$name = "clearCache";
|
|||
|
$f = $this->wire('modules')->get('InputfieldCheckbox');
|
|||
|
$f->attr('name', $name);
|
|||
|
$f->attr('value', 1);
|
|||
|
$f->label = $this->_('Clear the Page Render Disk Cache?');
|
|||
|
$f->description = sprintf($this->_('There are currently %d pages cached in %s'), $numPages, $path);
|
|||
|
|
|||
|
$inputfields->append($f);
|
|||
|
|
|||
|
return $inputfields;
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|