840 lines
26 KiB
Text
840 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 2021 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');
|
||
$pages = $this->wire()->pages;
|
||
$pages->addHookAfter('save', $this, 'clearCacheFile');
|
||
$pages->addHookAfter('delete', $this, 'clearCacheFile');
|
||
// $this->addHookAfter('Fieldtype::savePageField', $this, 'savePageField'); // removed, see note in commented function
|
||
}
|
||
|
||
/**
|
||
* API ready
|
||
*
|
||
*/
|
||
public function ready() {
|
||
$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) {
|
||
|
||
$template = $page->template;
|
||
if(!$template || ((int) $template->cache_time) < 1) return false;
|
||
|
||
if(!$this->wire()->user->isGuest()) {
|
||
if(!$template->useCacheForUsers) return false;
|
||
if($page->editable()) return false;
|
||
}
|
||
|
||
$allowed = true;
|
||
$noCacheGetVars = $template->noCacheGetVars;
|
||
$noCachePostVars = $template->noCachePostVars;
|
||
|
||
if($noCacheGetVars && count($_GET)) {
|
||
if(strpos($noCacheGetVars, '*') !== false) {
|
||
$allowed = false;
|
||
} else {
|
||
$vars = explode(' ', $noCacheGetVars);
|
||
foreach($vars as $name) if($name && isset($_GET[$name])) $allowed = false;
|
||
}
|
||
}
|
||
|
||
if($allowed && $noCachePostVars && count($_POST)) {
|
||
if(strpos($noCachePostVars, '*') !== false) {
|
||
$allowed = false;
|
||
} else {
|
||
$vars = explode(' ', $noCachePostVars);
|
||
foreach($vars as $name) if($name && isset($_POST[$name])) $allowed = false;
|
||
}
|
||
}
|
||
|
||
if($allowed) {
|
||
// NOTE: other modules may set a session var of PageRenderNoCachePage containing a page ID to temporarily
|
||
// remove caching for some page, if necessary.
|
||
$id = (int) $this->wire()->session->get('PageRenderNoCachePage');
|
||
if($id && $id === $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");
|
||
}
|
||
|
||
$cacheFile = new CacheFile($path, $id, $cacheTime);
|
||
$this->wire($cacheFile);
|
||
|
||
if($this->wire()->page === $page) {
|
||
// this part is skipped if arguments provided an id (int) rather than a Page object
|
||
$secondaryID = '';
|
||
$input = $this->wire()->input;
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$pageNum = $input->pageNum;
|
||
$urlSegments = $input->urlSegments;
|
||
|
||
if(count($urlSegments)) {
|
||
foreach($urlSegments as $urlSegment) {
|
||
$secondaryID .= $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();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Hook to clear the cache file after a Pages::save or Pages::delete call
|
||
*
|
||
* @param HookEvent $event
|
||
*
|
||
*/
|
||
public function clearCacheFile($event) {
|
||
|
||
/** @var Page $page */
|
||
$page = $event->arguments[0];
|
||
$template = $page->template;
|
||
|
||
if(((int) $template->cache_time) == 0) return;
|
||
|
||
$cacheExpire = $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($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($template->cacheExpirePages) && count($template->cacheExpirePages)) {
|
||
$pageIDs = $template->cacheExpirePages;
|
||
}
|
||
} else if($cacheExpire == Template::cacheExpireSelector && $template->cacheExpireSelector) {
|
||
// expire pages matching a selector
|
||
/** @var PageFinder $finder */
|
||
$finder = $this->wire(new PageFinder());
|
||
$selectors = new Selectors();
|
||
$this->wire($selectors);
|
||
$selectors->init($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();
|
||
$this->wire($items);
|
||
$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;
|
||
$timerKey = $config->debug ? "page.$page.render" : "";
|
||
$compiler = null; /** @var FileCompiler|null $compiler */
|
||
$compilerOptions = array();
|
||
|
||
if($timerKey) Debug::timer($timerKey);
|
||
|
||
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);
|
||
if($timerKey) Debug::saveTimer($timerKey);
|
||
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($config->useMarkupRegions) {
|
||
$contentType = $template->contentType;
|
||
if(empty($contentType) || stripos($contentType, 'html') !== false) {
|
||
$this->populateMarkupRegions($data);
|
||
}
|
||
}
|
||
|
||
if($timerKey) Debug::saveTimer($timerKey);
|
||
|
||
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) {
|
||
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$config = $this->wire()->config;
|
||
|
||
if(strlen($fieldName)) {
|
||
$fieldName = $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 = $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] = $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($config->templateCompile) {
|
||
$renderFile = $this->wire()->files->compile($renderFile, array('skipIfNamespace' => true));
|
||
}
|
||
|
||
/** @var TemplateFile $tpl */
|
||
$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) {
|
||
|
||
$config = $this->wire()->config;
|
||
$input = $this->wire()->input;
|
||
$files = $this->wire()->files;
|
||
$modules = $this->wire()->modules;
|
||
|
||
if($data) {}
|
||
$path = $config->paths->cache . self::cacheDirName . '/';
|
||
$numPages = 0;
|
||
$numFiles = 0;
|
||
$inputfields = $this->wire(new InputfieldWrapper());
|
||
$dir = null;
|
||
$clearNow = $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++;
|
||
$files->unlink($f->getPathname());
|
||
}
|
||
}
|
||
$files->rmdir($file->getPathname());
|
||
}
|
||
|
||
if($clearNow) {
|
||
$inputfields->message(sprintf($this->_('Cleared %d cache files for %d pages'), $numFiles, $numPages));
|
||
$numPages = 0;
|
||
}
|
||
|
||
/** @var InputfieldCheckbox $f */
|
||
$f = $modules->get('InputfieldCheckbox');
|
||
$f->attr('name', '_clearCache');
|
||
$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;
|
||
}
|
||
|
||
}
|