config = $CONFIG; $this->routes = $this->getConfig('routes', array()); $this->actions_path = $this->getConfig('actions_path', '.'); $this->templates_path = $this->getConfig('templates_path', '.'); $this->routes_prefix = $this->getConfig('routes_prefix', ''); $this->enable_cache = $this->getConfig('enable_cache', FALSE); $this->max_cache_age = $this->getConfig('max_cache_age', 5); $this->route_path = FALSE; $this->action_name = FALSE; $this->route_vars = array(); $this->headers_out = array(); $this->captured_output = array(); $this->log = Log::singleton( 'file', $this->getConfig('log_path', '../logs/main.log'), 'main', array(), PEAR_LOG_DEBUG ); $this->log->debug(basename($_SERVER['SCRIPT_FILENAME'])." starting up..."); // Configure the DB_DataObject framework. $options = &PEAR::getStaticProperty("DB_DataObject",'options'); $options = $CONFIG["DB_DataObject"]; // Set up a database connection. $this->db_dsn = $CONFIG['DB_DataObject']['database']; /* TODO: Figure out if a raw DB connection is needed since DB_DataObject is around. $this->db = DB::connect($this->db_dsn); */ // Set up authentication object. $this->auth = new Auth("DB", array( 'enableLogging' => TRUE, 'dsn' => $this->db_dsn, 'table' => 'users', 'usernamecol' => 'username', 'passwordcol' => 'password', 'postUsername' => 'login_username', 'postPassword' => 'login_password', 'cryptType' => 'md5', 'db_fields' => '*' ), '', FALSE); $this->auth->logger = $this->log; } /** * Replacement for native header(). Use this so the framework can decide * whether to immediately emit headers or buffer up for caching. */ function header($out) { // Immediately give up if caching not enabled. if (!$this->enable_cache) return header($out); array_push($this->headers_out, $out); } /** * Fetch a configuration setting value. * @param $name - name of the configuration setting * @param $default - default config value if setting not set */ function getConfig($name, $default=NULL) { return isset($this->config[$name]) ? $this->config[$name] : $default; } /** * Fetch a named route parameter extracted by matchRoute() * @param $name - name of the parameter * @param $default - default value if not set */ function getRouteVar($name, $default=NULL) { return isset($this->route_vars[$name]) ? $this->route_vars[$name] : $default; } /** * Dispatch an incoming web request. Match current request URI against * defined routes, attempt to find an appropriate action, execute it. */ function dispatch() { // First, try matching the request URI against known routers list($this->route_path, $this->action_name, $this->route_vars) = $this->matchRoute($_SERVER['REQUEST_URI']); // Next, locate the path to the action to execute. $this->action_path = $this->findAction($_SERVER['REQUEST_METHOD'], $this->action_name); // Check for available cached output. If none available, execute the // action fresh and cache the output of that. $output = $this->checkCache(); if (!$output) { $output = $this->executeAction($this->action_path); $this->cacheOutput($output); } // Finally, spit it all out, making sure to emit any stored-up headers. $this->emitHeaders(); echo $output; } /** * Find the path to an action. * @param $request_method - The HTTP method of the request (GET/POST/PUT/DELETE/etc) * @param $action_name - Name of the action to locate. */ function findAction($request_method, $action_name) { // Assemble the base path to an action, using 'route404' as action name // if no route was matched. $base_action_path = $this->actions_path.'/'. ( ($this->action_name) ? $this->action_name : 'route404' ); // Try finding an action based on action name and request method. $method_action_path = "$base_action_path.$request_method.action.php"; $action_path = (is_file($method_action_path)) ? $method_action_path : $base_action_path.".action.php"; // If there's ultimately no file at the action path, fall back to // default.action.php. (If that's missing, let things break.) if (!is_file($action_path)) { $action_path = $this->actions_path.'/default.action.php'; } return $action_path; } /** * Execute a PHP action. * @param $action_path - path to the PHP file implementing the action */ function executeAction($action_path) { // Execute the action by including it in the context of this method, // capturing the output so we can possibly cache it. ob_start(); include $action_path; $output = ob_get_contents(); ob_end_clean(); // Finally, return the captured output, whether generated from scratch or // pulled from cache. return $output; } /** * Match a URL path against the known list of routes. * @param $uri - Request URI for matching * @return Route, action, array of extracted vars * @todo Document the simplified routing syntax */ function matchRoute($uri=null) { // Initialize route found and variables. $route_current = FALSE; $route_vars = Array(); $route_wild = FALSE; // First, ensure the URI starts with the configured prefix... $prefix = $this->routes_prefix; if (strpos($uri, $prefix) === 0) { // Now, extract the URI path between the prefix and an optional set // of query parameters after a '?' $qpos = strpos($uri, '?'); if ($qpos === FALSE) { $qpos = strlen($uri); } $plen = strlen($prefix); $path = substr($uri, $plen, $qpos - $plen); // Split the path into '/' delimited parts for matching. $request_parts = explode('/', $path); // Iterate through each defined route, looking for a match. foreach ($this->routes as $path_spec => $action_spec) { $route_current = $path_spec; // Carve the route up into path parts. $route_parts = explode('/', $path_spec); // If there are more route parts to match than request parts // available, consider this a non-match from the start. if (count($route_parts) > count($request_parts)) { $route_current=null; continue; } // If there are more request parts than route parts, and there's // no wildcard to be found in the route, consider this a non-match. if (count($route_parts) < count($request_parts) && !in_array('*', $route_parts)) { $route_current=null; continue; } // Iterate through each of the parts of the route. for ($i=0; $icount($request_parts)) { $route_current=null; break; } // A non-match between the current route and request parts // is a non-match overall. if ($route_parts[$i] != $request_parts[$i]) { $route_current=null; break; } } // If we found a matching route, stop looking. if ($route_current) break; } } // Return the current route spec, handler (if found), and vars captured. return array( $route_current, ($route_current) ? $this->routes[$route_current] : null, $route_vars ); } /** * HTML escape values in an associative array, or a single string. * @param $mixed - An associative array or a single string. * @return An associative array with escaped values, or an escaped string. */ function htmlescape($mixed) { if (is_array($mixed)) { $safe = array(); foreach ($mixed as $n=>$v) { if (is_string($v)) $safe[$n] = htmlentities($v); } return $safe; } else { return htmlentities($mixed); } } /** * Echo some output, performing HTML escaping first. * @param $out - String to echo */ function _($out) { echo htmlentities($out); } /** * Render a template with a namespace, return the results. * @param $name - name of the template to find and render * @param $ns - namespace of variables to use in template. * @return results of the rendered template output */ function renderTemplate($name, $ns=FALSE) { // Find the full path to selected template. $template_path = $this->findTemplate($name); // Import all the namespace variables into this function's scope. if ($ns) extract($ns); // Start buffering, include the template source, return the buffered // output. ob_start(); include $template_path; $output = ob_get_contents(); ob_end_clean(); return $output; } /** * Find the full path to a template file. * @param $template_name - name of the template to find. */ function findTemplate($template_name) { $template_path = $this->templates_path."/$template_name.tmpl.php"; if (!is_file($template_path)) $template_path = $this->templates_path.'/default.tmpl.php'; return $template_path; } /** * Begin capturing template output, for storage with given key. * @param $key - string key used to identify output capture */ function startOutputCapture($key) { ob_start(); } /** * Stop capturing template output, storing the result with given key. * @param $key - string key used to identify output capture * @return The captured output. */ function endOutputCapture($key) { $output = ob_get_contents(); ob_end_clean(); return $this->captured_output[$key] = $output; } /** * Fetch the captured output for the given key. * @param $key - String key used to fetch captured output * @return The captured output. */ function getCapturedOutput($key) { return isset($this->captured_output[$key]) ? $this->captured_output[$key] : FALSE; } /** * Construct a caching hash key for an action from the current request * environment. Called from checkCache() and cacheOutput(). */ function buildCacheKey() { $key = "mfw-".md5($_SERVER['REQUEST_URI']); return $key; } /** * Cache fresh output for the current request environment. */ function cacheOutput($output) { // Immediately give up if caching not enabled. if (!$this->enable_cache) return NULL; $now = time(); $key = $this->buildCacheKey(); $data = array($now, $output, $this->headers_out); apc_store($key, $data, $this->max_cache_age); } /** * Check the cache for fresh output data for the current request * environment. */ function checkCache() { // Immediately give up if caching not enabled. if (!$this->enable_cache) return NULL; $output = NULL; $now = time(); $key = $this->buildCacheKey(); $data = apc_fetch($key); // Is there data available from the cache? if ($data && count($data)==3) { // Grab the timestamp, response data, and buffered headers. list($then, $c_output, $headers_out) = $data; if ($now - $then < $this->max_cache_age) { // The cached data isn't stale yet, so use it. $output = $c_output; $this->headers_out = $headers_out; } else { // Cached data is stale, so ignore and discard it. apc_delete($key); } } return $output; } /** * Emit any buffered-up headers if caching is enabled. */ function emitHeaders() { // Immediately give up if caching not enabled. if (!$this->enable_cache) return; // Spool out the buffered-up headers. foreach ($this->headers_out as $header) { header($header); } } /** * Helper function to construct and append a DOM node all in one shot. Had * some trouble using SimpleXML with cdata containing ampersands, so this * is a homebrewed workaround. * * @param $parent - Parent node or DOMDocument * @param $tagnname - Name of element to construct * @param $attributes - Array of name/value pairs to be used as attributes (optional) * @param $cdata - Character data to be inserted into element (optional) * @return A newly constructed DOM element, added to the parent. */ function appendChild($parent, $tagname, $attributes=NULL, $cdata=NULL) { // Find the owner document, whether it's the actual parent or the owner // of the parent. $doc = ($parent instanceof DOMDocument) ? $parent : $parent->ownerDocument; if (is_array($tagname)) { // If the $tagname is an array, assume that it's a (name, NS) tuple. $el = $doc->createElementNS($tagname[1], $tagname[0]); } else { // Otherwise, it's just a string tag name. $el = $doc->createElement($tagname); } // Add any attributes supplied. if ($attributes) { foreach ($attributes as $name=>$val) { $el->setAttribute($name, $val); } } // Insert any character data supplied. if ($cdata) { $el->appendChild($doc->createTextNode($cdata)); } // Finally, append this element to its parent and return the element itself. $parent->appendChild($el); return $el; } } ?>