a tiny mvc framework for php using php-activerecord
at master 822 lines 24 kB view raw
1<?php 2 3namespace HalfMoon; 4 5class ApplicationController { 6 static $DEFAULT_CONTENT_TYPE = "text/html"; 7 8 /* array of methods to call before processing any actions, bailing if any 9 * return false 10 * e.g. static $before_filter = array( 11 * "validate_logged_in_user", 12 * "validate_admin" => array("only" => array("create")), 13 * ... 14 */ 15 static $before_filter = array(); 16 17 /* array of methods to call after processing actions, which will be passed 18 * all buffered output and must return new output */ 19 static $after_filter = array(); 20 21 /* things to verify (like the method used) before processing any actions */ 22 static $verify = array(); 23 24 /* if non-empty, recurse through GET/POST params and filter out the values 25 * of any parameter names that match, replacing them with '[FILTERED]' */ 26 static $filter_parameter_logging = array(); 27 28 /* per-controller session options, can be "off", "on", or a per-action 29 * setting like: array("on" => array("only" => array("foo", "bar"))) */ 30 static $session = ""; 31 32 /* specify a different layout than controller name or application */ 33 static $layout = array(); 34 35 /* helpers to bring in (as "ref" => "filename"), other than 36 * application_helper and a controller-specific helper (if each exists) */ 37 static $helpers = array(); 38 39 /* protect all (or specific actions passed as an array) actions from 40 * forgery */ 41 static $protect_from_forgery = true; 42 43 /* simple array of action names which will perform full page caching */ 44 static $caches_page = array(); 45 46 public $request = array(); 47 public $params = array(); 48 public $locals = array(); 49 50 /* this will be set to a helper object before rendering a template */ 51 public $helper = null; 52 53 /* keep track of the content-type being sent */ 54 public $content_type = null; 55 56 private $did_render = false; 57 private $redirected_to = null; 58 private $did_layout = false; 59 60 /* set while we're processing a view so render() behaves differently */ 61 private $in_view = false; 62 63 /* track what ob_get_level() was when we started buffering */ 64 private $start_ob_level = 0; 65 66 private $etag; 67 68 public function __construct($request) { 69 $this->request = $request; 70 $this->params = &$request->params; 71 72 if (Config::log_level_at_least("full") && array_keys($this->params)) { 73 $params_log = " Parameters: "; 74 75 /* the closure can't access static vars */ 76 $filters = static::$filter_parameter_logging; 77 78 /* build a string of parameters, less the ones matching 79 * $filter_parameter_logging */ 80 $recursor = function($params) use (&$recursor, &$params_log, 81 $filters) { 82 $params_log .= "{"; 83 $done_first = false; 84 foreach ($params as $k => $v) { 85 if ($done_first) 86 $params_log .= ", "; 87 else 88 $done_first = true; 89 90 $params_log .= "\"" . $k . "\"=>"; 91 92 if (is_array($v)) 93 $recursor($v); 94 else { 95 $filter = false; 96 97 if (is_array($filters) && !empty($filters)) { 98 foreach ($filters as $field) 99 if (preg_match("/" . preg_quote($field, "/") 100 . "/i", $k)) { 101 $filter = true; 102 break; 103 } 104 } 105 106 if ($filter) 107 $params_log .= "[FILTERED]"; 108 else 109 $params_log .= "\"" . $v . "\""; 110 } 111 } 112 $params_log .= "}"; 113 }; 114 $recursor($this->params); 115 116 Log::info($params_log); 117 } 118 } 119 120 /* turn local class variables into $variables when rendering views */ 121 public function __set($name, $value) { 122 $this->locals[$name] =& $value; 123 } 124 public function __get($name) { 125 if (isset($this->locals[$name])) 126 return $this->locals[$name]; 127 else 128 return null; 129 } 130 public function __isset($name) { 131 return isset($this->locals[$name]); 132 } 133 public function __unset($name) { 134 unset($this->locals[$name]); 135 } 136 137 /* store an error in the session to be printed on the next view with the 138 * flash_errors() helper */ 139 public function add_flash_error($string) { 140 if (!isset($_SESSION["flash_errors"]) || 141 !is_array($_SESSION["flash_errors"])) 142 $_SESSION["flash_errors"] = array(); 143 144 array_push($_SESSION["flash_errors"], $string); 145 } 146 147 /* store a notice in the session to be printed on the next view with the 148 * flash_notices() helper */ 149 public function add_flash_notice($string) { 150 if (!isset($_SESSION["flash_notices"]) || 151 !is_array($_SESSION["flash_notices"])) 152 $_SESSION["flash_notices"] = array(); 153 154 array_push($_SESSION["flash_notices"], $string); 155 } 156 157 /* store a success message in the session to be printed on the next view 158 * with the flash_success() helper */ 159 public function add_flash_success($string) { 160 if (!isset($_SESSION["flash_successes"]) || 161 !is_array($_SESSION["flash_successes"])) 162 $_SESSION["flash_successes"] = array(); 163 164 array_push($_SESSION["flash_successes"], $string); 165 } 166 167 /* cancel all buffered output, send a location: header and stop processing */ 168 public function redirect_to($obj_or_url) { 169 $link = HTMLHelper::link_from_obj_or_string($obj_or_url); 170 171 /* prevent any content from getting to the user */ 172 while (($l = ob_get_level()) && ($l >= $this->start_ob_level)) 173 ob_end_clean(); 174 175 if (Config::log_level_at_least("full")) 176 Log::info("Redirected to " . $link); 177 178 $this->redirected_to = $link; 179 180 /* end session first so it can write the cookie */ 181 session_write_close(); 182 183 Request::send_status_header(302); 184 header("Location: " . $link); 185 } 186 187 /* render a partial view, an action template, text, etc. */ 188 public function render($template, $vars = array()) { 189 /* render(array("partial" => "somedir/file"), array("v" => $v)) */ 190 191 if (!is_array($template)) 192 $template = array("action" => $template); 193 194 if (isset($template["status"])) 195 Request::send_status_header($template["status"]); 196 197 $collection = array(); 198 if (isset($template["collection"]) && 199 is_array($template["collection"])) { 200 if (isset($template["as"])) 201 $collection = array($template["as"] => 202 $template["collection"]); 203 else { 204 /* figure out the type of things in the collection */ 205 $cl = strtolower(get_class($template["collection"][0])); 206 if ($cl != "") 207 $cl = \ActiveRecord\Utils::singularize($cl); 208 209 if ($cl == "") 210 throw new HalfMoonException("could not figure out type of " 211 . "collection"); 212 213 $collection = array($cl => $template["collection"]); 214 } 215 } 216 217 /* just render text with no layout */ 218 if (is_array($template) && array_key_exists("text", $template)) { 219 if (!$this->content_type_set()) 220 $this->content_type = "text/plain"; 221 222 if (Config::log_level_at_least("full")) 223 Log::info("Rendering text"); 224 225 print $template["text"]; 226 } 227 228 /* just render html with no layout */ 229 elseif (is_array($template) && array_key_exists("html", $template)) { 230 if (!$this->content_type_set()) 231 $this->content_type = "text/html"; 232 233 if (Config::log_level_at_least("full")) 234 Log::info("Rendering HTML"); 235 236 print $template["html"]; 237 } 238 239 /* just render json with no layout */ 240 elseif (is_array($template) && array_key_exists("json", $template)) { 241 if (!$this->content_type_set()) 242 $this->content_type = "application/json"; 243 244 if (Config::log_level_at_least("full")) 245 Log::info("Rendering json"); 246 247 /* there's no way to know if we were passed a json-encoded string, 248 * or a string that needs to be encoded, so just encode everything 249 * and hope the user figures it out */ 250 print json_encode($template["json"]); 251 } 252 253 /* just render javascript with no layout */ 254 elseif (is_array($template) && array_key_exists("js", $template)) { 255 if (!$this->content_type_set()) 256 $this->content_type = "text/javascript"; 257 258 if (Config::log_level_at_least("full")) 259 Log::info("Rendering javascript"); 260 261 print $template["js"]; 262 } 263 264 /* assume we're dealing with files */ 265 else { 266 $tf = ""; 267 268 /* render a partial template */ 269 if (is_array($template) && isset($template["partial"])) 270 $tf = $template["partial"]; 271 272 /* render an action template */ 273 elseif (is_array($template) && isset($template["action"])) 274 $tf = $template["action"]; 275 276 /* just a filename, render it as an action */ 277 elseif (is_array($template)) 278 $tf = join("", array_values($template)); 279 280 /* just a filename, render it as an action */ 281 else 282 $tf = $template; 283 284 if (substr($tf, 0, 1) == "/") 285 /* full path, just use it */ 286 ; 287 elseif (strpos($tf, "/") !== false) 288 /* path relative to base view path */ 289 $tf = HALFMOON_ROOT . "/views/" . $tf; 290 else 291 /* just a file in this controller's directory 292 * (AdminSomethingController -> admin_something) */ 293 $tf = $this->view_template_path() . $tf; 294 295 /* partial template files start with _ */ 296 if (is_array($template) && isset($template["partial"])) 297 $tf = dirname($tf) . "/_" . basename($tf); 298 299 /* do the actual renders */ 300 $filename = null; 301 302 /* regular php/html */ 303 if (file_exists($filename = $tf . ".phtml")) { 304 if (!$this->content_type_set()) 305 $this->content_type = "text/html"; 306 } 307 308 /* xml */ 309 elseif (file_exists($filename = $tf . ".pxml")) { 310 if (!$this->content_type_set()) 311 $this->content_type = "application/xml"; 312 } 313 314 /* php-javascript */ 315 elseif (file_exists($filename = $tf . ".pjs")) { 316 if (!$this->content_type_set()) 317 $this->content_type = "text/javascript"; 318 } 319 320 else 321 throw new MissingTemplate("no template file " . $tf 322 . ".p{html,xml,js}"); 323 324 if (count($collection)) { 325 $ck = Utils::A(array_keys($collection), 0); 326 327 /* it would be nice to be able to just read the template 328 * into a string and eval() it each time to save on i/o, 329 * but php won't let us catch parse errors properly and 330 * there may be some other fallout */ 331 foreach ($collection[$ck] as $cobj) { 332 $vars[$ck] = $cobj; 333 $this->_really_render_file($filename, $vars); 334 } 335 } else 336 $this->_really_render_file($filename, $vars); 337 } 338 339 if (!$this->in_view) { 340 if (is_array($template) && array_key_exists("layout", $template)) 341 $this::$layout = $template["layout"]; 342 343 elseif ($this->content_type_set() && 344 $this->content_type != static::$DEFAULT_CONTENT_TYPE) 345 /* if we were called from the controller, we're not outputting 346 * html, and no layout was explicitly specified, we probably 347 * don't want a layout */ 348 $this::$layout = false; 349 } 350 351 $this->did_render = true; 352 } 353 354 /* do render() but capture all the output and return it */ 355 public function render_to_string($template, $vars = array()) { 356 $old_did_render = $this->did_render; 357 358 /* store current content-type in case render() changes it */ 359 $ct = $this->content_type; 360 361 ob_start(); 362 $this->render($template, $vars); 363 $output = ob_get_contents(); 364 ob_end_clean(); 365 366 $this->did_render = $old_did_render; 367 $this->content_type = $ct; 368 369 return $output; 370 } 371 372 /* a private function to avoid taining the variable space after the 373 * require() */ 374 private function _really_render_file($__file, $__vars) { 375 /* XXX: should this be checking for more special variable names? */ 376 $__special_vars = array("__special_vars", "__vars", "__file", 377 "controller"); 378 379 /* export variables set in the controller to the view */ 380 foreach ((array)$this->locals as $__k => $__v) { 381 if (in_array($__k, $__special_vars)) { 382 Log::warn("tried to redefine \$" . $__k . " passed from " 383 . "controller"); 384 continue; 385 } 386 387 $$__k = $__v; 388 } 389 390 /* and any passed as locals to the render() function */ 391 foreach ((array)$__vars as $__k => $__v) { 392 if (in_array($__k, $__special_vars)) { 393 Log::warn("tried to redefine \$" . $__k . " passed " 394 . "from render() call"); 395 continue; 396 } 397 398 $$__k = $__v; 399 } 400 401 /* make helpers available to the view */ 402 $this->bring_in_helpers(); 403 foreach ($this->_helper_refs as $__hn => $__hk) { 404 $$__hn = $__hk; 405 $$__hn->controller = $this; 406 $$__hn->C = $this; 407 } 408 409 /* define $controller and $C where $this can't be used */ 410 $controller = $this; 411 $C = $this; 412 413 if (Config::log_level_at_least("full")) 414 Log::info("Rendering " . $__file); 415 416 $this->in_view = true; 417 require($__file); 418 $this->in_view = false; 419 } 420 421 /* setup each built-in helper to be $var = VarHelper, and the 422 * application_helper and controller-specific helper to be $helper */ 423 private $_helper_refs = null; 424 private function bring_in_helpers() { 425 if ($this->_helper_refs) 426 return; 427 428 $this->_helper_refs = array(); 429 430 foreach (get_declared_classes() as $class) 431 if (preg_match("/^HalfMoon\\\\(.+)Helper$/", $class, $m)) 432 $this->_helper_refs[strtolower($m[1])] = new $class; 433 434 /* bring in the application-wide helpers */ 435 if (file_exists($f = HALFMOON_ROOT . "/helpers/" 436 . "application_helper.php")) { 437 require_once($f); 438 $this->_helper_refs["helper"] = new \ApplicationHelper; 439 } 440 441 /* if a controller-specific helper exists, hopefully it descends from 442 * ApplicationHelper so the $helper ref we're going to point at it will 443 * still have access to methods in ApplicationHelper */ 444 $controller = preg_replace("/Controller$/", "", 445 Utils::current_controller_name()); 446 447 if (file_exists($f = HALFMOON_ROOT . "/helpers/" 448 . strtolower($controller . "_helper.php"))) { 449 require_once($f); 450 451 $n = $controller . "Helper"; 452 $this->_helper_refs["helper"] = new $n; 453 } 454 455 /* bring in any extra helpers requested by the static $helpers */ 456 foreach (static::$helpers as $ref => $file) { 457 require_once($f = HALFMOON_ROOT . "/helpers/" . $file 458 . "_helper.php"); 459 460 $c = "\\" . $file . "Helper"; 461 462 try { 463 $this->_helper_refs[$ref] = new $c; 464 } catch (Exception $e) { 465 throw new HalfMoonException("loaded helper " . $f . " which " 466 . "did not define " . $c); 467 } 468 } 469 } 470 471 /* the main entry point for the controller, sent by the router */ 472 public function render_action($action) { 473 $this->enable_or_disable_sessions($action); 474 475 $this->verify_method($action); 476 477 $this->protect_from_forgery($action); 478 479 if (!$this->process_before_filters($action)) 480 return false; 481 482 /* start our one output buffer that we'll pass to the after filters */ 483 ob_start(); 484 $this->start_ob_level = ob_get_level(); 485 486 /* we only want to allow calling public methods in controllers, to 487 * avoid users getting directly to before_filters and other utility 488 * functions */ 489 if (!in_array($action, Utils::get_public_class_methods($this))) 490 throw new UndefinedFunction("controller \"" . get_class($this) 491 . "\" does not have an action \"" . $action . "\""); 492 493 call_user_func_array(array($this, $action), array()); 494 495 if (isset($this->redirected_to)) { 496 $this->request->redirected_to = $this->redirected_to; 497 return; 498 } 499 500 if (!$this->did_render) 501 $this->render(array("action" => $action), $this->locals); 502 503 if (!$this->did_layout) 504 $this->render_layout($action); 505 506 $this->process_after_filters($action); 507 508 /* end session first so it can write the cookie */ 509 session_write_close(); 510 511 if (!$this->content_type_set()) 512 $this->content_type = static::$DEFAULT_CONTENT_TYPE; 513 514 if (!$this->content_type_sent()) 515 header("Content-type: " . $this->content_type); 516 517 /* if we're caching this action as a full page, write out what we've 518 * buffered so far */ 519 if (!Utils::is_blank(Config::instance()->cache_store_path) && 520 is_array($this::$caches_page) && 521 in_array($action, $this::$caches_page)) 522 $this->write_cache_output(); 523 524 /* flush out everything, we're done playing with buffers */ 525 ob_end_flush(); 526 } 527 528 /* capture the output of everything rendered and put it within the layout */ 529 public function render_layout($action) { 530 /* get all buffered output and turn them off, except for our one last 531 * buffer needed for after_filters */ 532 $content_for_layout = ""; 533 while (ob_get_level() >= $this->start_ob_level) { 534 $content_for_layout = $content_for_layout . ob_get_contents(); 535 536 if (ob_get_level() == $this->start_ob_level) 537 break; 538 else 539 ob_end_clean(); 540 } 541 542 /* now that we have all of our content, clean our last buffer since 543 * we're going to print the layout (and content inside) into it */ 544 ob_clean(); 545 546 $tlayout = null; 547 if ($this::$layout === false) 548 $tlayout = false; 549 else { 550 $opts = Utils::options_for_key_from_options_hash($action, 551 $this::$layout); 552 if (!empty($opts[0])) 553 $tlayout = $opts[0]; 554 } 555 556 /* if we don't want a layout at all, just print the content */ 557 if ($tlayout === false || $tlayout === "false") { 558 print $content_for_layout; 559 return; 560 } 561 562 /* if no specific layout was set, check for a controller-specific one */ 563 if (!$tlayout && isset($this->params["controller"]) && 564 file_exists(HALFMOON_ROOT . "/views/layouts/" . $this->params["controller"] 565 . ".phtml")) 566 $tlayout = $this->params["controller"]; 567 568 /* otherwise, default to "application" */ 569 if (!$tlayout) 570 $tlayout = "application"; 571 572 $this->did_layout = true; 573 574 if (!file_exists(HALFMOON_ROOT . "/views/layouts/" . $tlayout . 575 ".phtml")) 576 throw new MissingTemplate("no layout file " . $tlayout . ".phtml"); 577 578 /* make helpers available to the layout */ 579 $this->bring_in_helpers(); 580 foreach ($this->_helper_refs as $__hn => $__hk) { 581 $$__hn = $__hk; 582 $$__hn->controller = $this; 583 $$__hn->C = $this; 584 } 585 586 /* define $controller where $this can't be used */ 587 $controller = $this; 588 $C = $this; 589 590 if (Config::log_level_at_least("full")) 591 Log::info("Rendering layout " . $tlayout); 592 593 require(HALFMOON_ROOT . "/views/layouts/" . $tlayout . ".phtml"); 594 } 595 596 public function form_authenticity_token() { 597 /* explicitly enable sessions so we can store/retrieve the token */ 598 $this->start_session(); 599 600 if (@!$_SESSION["_csrf_token"]) 601 $_SESSION["_csrf_token"] = Utils::random_hash(); 602 603 return $_SESSION["_csrf_token"]; 604 } 605 606 public function view_template_path($absolute = true) { 607 $words = preg_split('/(?<=\\w)(?=[A-Z])/', 608 preg_replace("/Controller$/", "", 609 Utils::current_controller_name())); 610 611 $path = strtolower(join("_", $words)) . "/"; 612 if ($absolute) 613 $path = HALFMOON_ROOT . "/views/" . $path; 614 615 return $path; 616 } 617 618 public function expire_page($p) { 619 if (!isset($p["action"])) 620 throw new HalfMoonException("cannot expire page cache without " 621 . "at least an action"); 622 623 $path = Config::instance()->cache_store_path . "/" . 624 $this->view_template_path($abs = false) . "/" . $p["action"]; 625 626 if (isset($p["id"])) 627 $path .= "/" . $p["id"]; 628 629 $path .= ".html"; 630 if (file_exists($path)) { 631 unlink($path); 632 Log::info("Expired cached file " . $path); 633 } 634 } 635 636 /* enable or disable sessions according to $session */ 637 private function enable_or_disable_sessions($action) { 638 if (empty($this::$session) || 639 (!is_array($this::$session) && $this::$session == "off")) 640 $sessions = false; 641 elseif (is_array($this::$session)) { 642 $opts = Utils::options_for_key_from_options_hash($action, 643 $this::$session); 644 645 if ($opts == array("on")) 646 $sessions = true; 647 } else 648 $sessions = true; 649 650 if ($sessions) { 651 session_cache_expire(0); 652 session_cache_limiter("private_no_expire"); 653 $this->start_session(); 654 } else 655 session_cache_limiter("public"); 656 } 657 658 /* verify any options requiring verification */ 659 private function verify_method($action) { 660 $to_verify = Utils::options_for_key_from_options_hash($action, 661 $this::$verify); 662 663 if (empty($to_verify)) 664 return; 665 666 foreach ($to_verify as $v) { 667 if (isset($v["method"])) { 668 if (strtolower($this->request->request_method()) != 669 strtolower($v["method"])) { 670 if (isset($v["redirect_to"])) 671 return $this->redirect_to($v["redirect_to"]); 672 else 673 throw new BadRequest(); 674 } 675 } 676 677 /* TODO: support other verify options from rails 678 * http://railsapi.com/doc/v2.3.2/classes/ActionController/Verification/ClassMethods.html#M000331 679 */ 680 } 681 } 682 683 /* xsrf protection: verify the passed authenticity token for non-GET/HEAD 684 * requests */ 685 private function protect_from_forgery($action) { 686 if (!$this::$protect_from_forgery) 687 return; 688 689 if (strtoupper($this->request->request_method()) == "GET" || 690 strtoupper($this->request->request_method()) == "HEAD") 691 return; 692 693 if (Utils::option_applies_for_key($action, 694 $this::$protect_from_forgery)) { 695 if (@$this->params["authenticity_token"] != 696 $this->form_authenticity_token()) 697 throw new InvalidAuthenticityToken(); 698 } 699 } 700 701 /* return false if any before_filters return false */ 702 private function process_before_filters($action) { 703 $filters = Utils::options_for_key_from_options_hash($action, 704 $this::$before_filter); 705 706 foreach ($filters as $filter) { 707 if (!method_exists($this, $filter)) 708 throw new UndefinedFunction("before_filter \"" . $filter 709 . "\" function does not exist"); 710 711 if (!call_user_func_array(array($this, $filter), array())) { 712 if (Config::log_level_at_least("short")) 713 Log::info("Filter chain halted as " . $filter 714 . " did not return true."); 715 716 return false; 717 } 718 719 if (isset($this->redirected_to)) 720 return false; 721 } 722 723 return true; 724 } 725 726 /* pass all buffered output through after filters */ 727 private function process_after_filters($action) { 728 $filters = Utils::options_for_key_from_options_hash($action, 729 $this::$after_filter); 730 731 foreach ($filters as $filter) { 732 if (!method_exists($this, $filter)) 733 throw new UndefinedFunction("after_filter \"" . $filter 734 . "\" function does not exist"); 735 736 /* get all buffered output, then replace it with the filtered 737 * output */ 738 $output = ob_get_contents(); 739 $output = call_user_func_array(array($this, $filter), 740 array($output)); 741 ob_clean(); 742 print $output; 743 } 744 } 745 746 private function start_session() { 747 try { 748 if (!session_id()) 749 session_start(); 750 } catch (\HalfMoon\InvalidCookieData $e) { 751 /* probably a decryption failure. rather than assume the user is 752 * an attacker, try to invalidate their session so they get a new 753 * cookie. on a reload, they'll at least start with a clean 754 * session instead of continuing to get 500 errors forever. */ 755 session_destroy(); 756 757 /* and then throw the error so they see a 500 and see that 758 * something was wrong, which may help explain their new session on 759 * the reload. */ 760 throw $e; 761 } 762 } 763 764 private function content_type_set() { 765 if (empty($this->content_type)) 766 if ($ct = $this->content_type_sent()) 767 $this->content_type = $ct; 768 769 return !empty($this->content_type); 770 } 771 772 private function content_type_sent() { 773 foreach ((array)headers_list() as $header) 774 if (preg_match("/^Content-type: (.+)/i", $header, $m)) 775 return $m[1]; 776 777 return null; 778 } 779 780 private function write_cache_output() { 781 /* request path is already sanitized of .. and such, so we must 782 * assume it is safe to use relative to our cache store path */ 783 $rp = $this->request->path; 784 if ($rp == "") 785 $rp = "index"; 786 787 $path = Config::instance()->cache_store_path . "/" . $rp; 788 789 /* use .html as the default extension unless it looks like our url 790 * already had a format */ 791 if (!preg_match('/\.[^\/]+/', $rp)) 792 $path .= ".html"; 793 794 /* append .html to our temporary file as a precaution in case it gets 795 * left around, we don't want it being interpreted by the web server as 796 * anything else */ 797 $tmppath = $path . "." . bin2hex(openssl_random_pseudo_bytes(10)) 798 . ".html"; 799 800 if (!is_writable($path)) 801 @mkdir(dirname($path), 0755, $recursive = true); 802 803 $fp = fopen($tmppath, "x"); 804 if ($fp === false) { 805 Log::error("Error creating cache file " . $tmppath); 806 return; 807 } 808 809 fwrite($fp, ob_get_contents()); 810 fclose($fp); 811 812 if (!rename($tmppath, $path)) { 813 Log::error("Error renaming " . $tmppath . " to " . $path); 814 unlink($tmppath); 815 return; 816 } 817 818 Log::info("Cached page output to " . realpath($path)); 819 } 820} 821 822?>