JFIF $ d d 7 * Z qn )ZP 0 !1 A "#a$2Qr D8 a Ri [f \m I ykI w0 c u F c Rı?lO7 к _f˓ [C $殷WF< _W ԣ s Kc ëI zyQy /_LK ℂ ;C",p F A : /] =H ~ , l s / 9 ć: [=/#f ; ) x { ٛE Q )~ = 𘙲r*2~ a _V = ' ku m F D } KYYC )( { * g& f `툪 r y ` = ^ c J. I ]( * `wq 1dđ #̩ ͑ 0 ; H ] u 搂 @:~ ו K L N s h } O I R* 8 :2 ! lD JV o ( 3 = M ( z Ȱ+i *N A r6K n S l ) ! J J Ӂ* % ݉?|D } d5: eP0 R; {$X' x F @ .ÊB {,WJ uQ ɲRI ;9 Q E琯 6 2fT .DU J ;*c P A\IL Nj! J ۱+ O\ ͔ ] ޒ S J ȧ c%A N ol ՎprU L Zԛe r E2= X D Xg VQeӓ k y P7 U* o m Q I s ,K `) 6\G3t?pgjr m ۛجw luGt f h 9uyP 0D;Uڽ"OXl i f $ ) &|ML 0 Z rm1 [ H XP l P R0 'G=i 2N +0 e 2 ]] 9 VT P O 7 h(F * 癈 ' = Q VZD F,d ߬ ~ T X G[ `le 69C R(!S2! P < 0 x < !1AQ "Raq 02Br #S CTb ? Ζ "]mH5WR 7 k.ۛ ! } Q~ + yԏ z |@ T20 S~ K e k *zF f^2X *( @ 8 r ? C I uI|֓>^Ex LgNU Y+{.R Ѫ τV YT D I 6 2 '8Y2 7'\T P.6 d &˦ @V q i |8 -O Ε ] ʔ U= T L 8= ;6c | !qfF3 a ů& ~$ l} 'NW U s$U k^S V : U#6w + +s &r+n ڐ { @ 29gL u " T Ù M=6 (^ "7 r}=6Yݾ lC u hquymp Ǧ G jhs ǜNl ɻ} o 7 #S6 a w 4 ! OS r D 5 7 % | ?x> L | / nD 6?/8 w # [ ) L 7 +6〼T A T g ! %5 Mm Z /c -{1_Je "| ^$ 'O & ޱմ < H vNqϡQ$ < 6 &Nݛ ֨tǻN (9 n ru~'όN sMG& ,uH Kw *Ī 5@ u6 BԒ i YN * f 0 _ GhKW u lT <nj- 0{ M L~ | /a v\ E = ?1 \ɟuq Ђ Y O a K ޡP q $ H8o Q˼EV ~ k 0 h' L Pp M -M" m^ !6l nմ 쮝C U T . ! :vMǨb h ډ Vc r @~p # !1QAa q ? AL0L x LU 4 d ; x " 16ad0 7.\ߋ 0 = P l v/ " c A Ay A|2pO8"d MV #1] = \ " ˡ ] k v I9 d ۓ & q ) ǐq E ꞅ v "P )q X &Q З non]я u ג./ ; gZ$ M QР 2M ]h ni 0 P $ێ 3CɏGjώy : m c # S_ l@ '_ \m|C 2`K _ 1 cIvGc $? PCQ S+D ? K aײ k Fr b 8 ύ NEدg ĝ U 3 6 #Ik+ v { v i ( ވ ? a e) X xy/*@Q Ǽ Ծ ) $ IWO l c G; !i k{ gOŀ L ('x T rb $ w) R $ & N1E td U3Uȉ1p M"N* (DNy d96.(jQ) X 5 c QɎMy W ? Q *!R > 6=7)Xj 5`J ] e 8%t ! + ' ! 1Q 5 !1 AQaq ё #2 "0BRb ? G t^ ## .l lQ T $ v , ,m 㵜5u bV =sY+ @d { N ! dn O < .- B; _w J t 6;Q Jd. Q c%p{ 1 ,sND dF H I0 Гo X ш e黅X ۢ F :)[ F G X ƹ / w _c MeD ,ʡc c . W D tA $ j@ : ) - #u c1< @ۗ9F ) KJ- hpP] _ x [ q B lb p ʖ w q " LFGd ƶ * s +ډ_Zc " ? % t[I P 6 J ]# =ɺ V v v CGs Gh1 > )6 |e y ? L ӣ m,4 G W Ui ` ] uJVo VDG< SB 6ϏQ @ TiU l y O U 0 kfV ~ ~}S Z @ *WU Ui ## ; s /[ = ! 7}" W N ] ' (L ! ~ y5 g9T̅J kbM' +s :S +B) v @ M j e C f j E 0 Y\Qn z G 1 д~ W o{T 9?` Rm yhsy3 !HAD ] m c1 ~ 2 L S u7x T ;j$ `} 4-> L# vzŏ I L S ֭ T { r jGK C ; b pU= - ` Bs K. S F w4 M q] Z dH S 0 ) tLg
<?php
/**
* View renderer
* @property-read string $_content
* @property-read string|null $_trash
*/
class Loco_mvc_View implements IteratorAggregate {
private Loco_mvc_ViewParams $scope;
/**
* View that is decorating current view
*/
private ?self $parent = null;
/**
* Current template as full path to PHP file
*/
private string $template = '';
/**
* Current working directory for finding templates by relative path
*/
private string $cwd;
/**
* Name of current output buffer
*/
private string $name = '';
/**
* @internal
*/
public function __construct( array $args = [] ){
$this->scope = new Loco_mvc_ViewParams( $args );
$this->cwd = loco_plugin_root().'/tpl';
}
/**
* Change base path for template paths
* @param string $path relative to current directory
*/
public function cd( string $path ):self {
if( $path && '/' === substr($path,0,1) ){
$this->cwd = untrailingslashit( loco_plugin_root().'/tpl'.$path );
}
else {
$this->cwd = untrailingslashit( $this->cwd.'/'.$path );
}
return $this;
}
/**
* @internal
* Clean up if something abruptly stopped rendering before graceful end
*/
public function __destruct(){
if( '' !== $this->name ){
ob_end_clean();
}
}
/**
* Render error screen HTML
*/
public static function renderError( Loco_error_Exception $e ):string {
$view = new Loco_mvc_View;
try {
$view->set( 'error', $e );
return $view->render( $e->getTemplate() );
}
catch( Throwable $e ){
return '<h1>'.esc_html( $e->getMessage() ).'</h1>';
}
}
/**
* Make this view a child of another template. i.e. decorate this with that.
* Parent will have access to original argument scope, but separate from now on
* @return self the parent view
*/
private function extend( string $tpl ):self {
$this->parent = new Loco_mvc_View;
$this->parent->cwd = $this->cwd;
$this->parent->setTemplate( $tpl );
return $this->parent;
}
/**
* After start is called any captured output will be placed in the named variable
*/
private function start( string $name ):void {
$this->stop();
$this->scope[$name] = null;
$this->name = $name;
}
/**
* When stop is called, buffered output is saved into current variable for output by parent template, or at end of script.
*/
private function stop():void {
$content = ob_get_contents();
ob_clean();
$b = $this->name;
if( '' !== $b ){
if( isset($this->scope[$b]) ){
$content = $this->scope[$b].$content;
}
$this->scope[$b] = new _LocoViewBuffer($content);
}
$this->name = '_trash';
}
/**
* Output a captured block buffer, as long as it's valid
*/
private function block( string $name ):void {
if( $this->has($name) ){
$view = $this->get($name);
if( $view instanceof _LocoViewBuffer ){
echo $view->__toString();
}
}
}
/**
* {@inheritDoc}
*/
#[ReturnTypeWillChange]
public function getIterator(){
return $this->scope;
}
/**
* @internal
* @return mixed
*/
public function __get( string $prop ){
return $this->has($prop) ? $this->get($prop) : null;
}
/**
* Test if a view argument exists
*/
public function has( string $prop ):bool {
return $this->scope->offsetExists($prop);
}
/**
* Get property after checking with self::has
* @return mixed
*/
public function get( string $prop ){
return $this->scope[$prop];
}
/**
* Set a view argument
*/
public function set( string $prop, $value ):self {
$this->scope[$prop] = $value;
return $this;
}
/**
* Remove a view argument
*/
public function unset( string $prop ):void {
$this->scope->offsetUnset($prop);
}
/**
* Main entry to rendering complete template
* @param string $tpl template name excluding extension
* @param array|null $args extra arguments to set in view scope
* @param self|null $parent parent view rendering this view
*/
public function render( string $tpl, ?array $args = null, ?Loco_mvc_View $parent = null ):string {
if( '' !== $this->name ){
return $this->fork()->render( $tpl, $args, $this );
}
$this->setTemplate($tpl);
if( $parent && $this->template === $parent->template ){
throw new Loco_error_Exception('Avoiding infinite loop');
}
if( is_array($args) ){
foreach( $args as $prop => $value ){
$this->set($prop, $value);
}
}
ob_start();
$content = $this->buffer();
ob_end_clean();
return $content;
}
/**
* Do actual render of currently validated template path
* @return string content not captured in sub-blocks
*/
private function buffer():string {
$this->start('_trash');
$this->execTemplate( $this->template );
$this->stop();
$this->name = '';
// decorate via parent view if there is one
if( $this->parent ){
$this->parent->scope = clone $this->scope;
$this->parent->set('_content', $this->_trash );
return $this->parent->buffer();
}
// else at the root of view chain
return (string) $this->_trash;
}
/**
* Single entry point for setting current template
* @param string $tpl Path to template, excluding file extension
*/
public function setTemplate( string $tpl ):void {
$file = new Loco_fs_File( $tpl.'.php' );
$path = $file->normalize( $this->cwd );
$root = Loco_fs_File::abs( loco_plugin_root().'/tpl/' );
// User input should be blocked from here, so the following is to catch developer errors.
if( ! str_starts_with($path,$root) ){
throw new Loco_error_Exception('Bad template path: '.$tpl.' => '.$path );
}
else if( ! $file->exists() ){
throw new Loco_error_Exception('Template not found: '.str_replace($root,'',$path) );
}
$this->cwd = $file->dirname();
$this->template = $file->getPath();
}
private function fork():self {
$view = new Loco_mvc_View;
$view->cwd = $this->cwd;
$view->scope = clone $this->scope;
return $view;
}
/**
* Do actual runtime template include
*/
private function execTemplate( string $template ):void {
$params = $this->scope;
// Parameter input should be sanitized, but here we log developer error
if( $params->has('template') || $params->has('params')){
Loco_error_Debug::trace('Template arguments attempted to override local variables');
}
extract( $params->getArrayCopy(), EXTR_SKIP );
include $template;
}
/**
* Link generator
* @param string $route page route, e.g. "config"
* @param array $args optional page arguments
*/
public function route( string $route, array $args = [] ):Loco_mvc_ViewParams {
return new Loco_mvc_ViewParams( [
'href' => Loco_mvc_AdminRouter::generate( $route, $args ),
] );
}
/**
* Shorthand for `echo esc_html( sprintf( ...`
*/
private static function e( string $text ):string {
if( 1 < func_num_args() ){
$args = func_get_args();
$text = call_user_func_array( 'sprintf', $args );
}
echo htmlspecialchars( $text, ENT_COMPAT, 'UTF-8' );
return '';
}
}
/**
* @internal
*/
class _LocoViewBuffer {
private string $s;
public function __construct( string $s ){
$this->s = $s;
}
public function __toString(){
return $this->s;
}
}