/system/classes/core.php <?php/******************************************************************************* * Provider of general built-in features (virtual directories, content-types, * and taxonomies) * * Global input: $index ******************************************************************************/function versionDesignFile($file) { return SYSTEM_BASE.'design/'.$file.';ver='.filemtime("design/$file");}function fuzzyDate($date, $since) { $date_stamp = strtotime($date); if ($since == true) { $now_day = floor(time()/86400); // By extreme good luck, UNIX time $date_day = floor($date_stamp/86400); // ignores leap seconds. if ($now_day == $date_day) return "today"; if ($now_day == $date_day + 1) return "yesterday"; return ($now_day - $date_day)." days ago"; } $now_info = getdate(); $date_info = getdate($date_stamp); if ($now_info['year'].$now_info['yday'] == $date_info['year'].$date_info['yday']) return "Today at ".date('g \o’\c\l\o\ck a', $date_stamp); if ($now_info['year'].$now_info['yday'] == $date_info['year'].($date_info['yday']+1)) return "Yesterday at ".date('g \o’\c\l\o\ck a', $date_stamp); if ($now_info['year'] == $date_info['year'] && $now_info['yday'] <= $date_info['yday']+7) return "Last ".date('l \a\t g \o’\c\l\o\ck a', $date_stamp); if ($now_info['year'].$now_info['mon'] == $date_info['year'].$date_info['mon']) return date('l \t\h\e jS \a\t g \o’\c\l\o\ck a', $date_stamp); if ($now_info['year'] == $date_info['year']) return date('F \t\h\e jS \a\t g \o’\c\l\o\ck a', $date_stamp); return date('F \t\h\e jS Y \a\t g \o’\c\l\o\ck a', strtotime($date));}function fileUpdated($file) { $date = filemtime(SYSTEM_ROOT."cache/$file.xml") + 1; if (!$date) error_log("Could not get mtime for $file"); else return date(DATE_ATOM, $date);}function array_flatten($array, &$result) { $result = (array)$result; foreach($array as $key=>$val) array_flatten_r($val, $key, $result, '');}function array_flatten_r($value, $key, &$data, $place) { if (!is_array($value)) $data[$place.$key.';'] = $value; else foreach($value as $nkey=>$nval) array_flatten_r($nval, $nkey, $data, $place.$key.';'); }function feedQuery() { global $user; return $user->id ? '?openid='.urlencode($user->id).'&token='.$user->token : '';}/** * @param $str the basic URL we are adding to * @param $upstream if true, we are forcing www.nicholaswilson.me.uk to be used * @param $url_base some URLs are relative to a base document; the base document * is a resource id, like 'colophon', or '2010/04/gardening'. */function prependDomain($str, $upstream = false, $url_base = '') { // If there is already a scheme, leave it. if (preg_match('/^\w*:/', $str)) return $str; if (strpos($str, CONTENT_BASE) === 0) $str = '/'.substr($str, strlen(CONTENT_BASE)); $url_base = preg_replace('/[^\/]*$/', '', $url_base, 1); if (substr($str, 0, 1) != '/') $str = $url_base.$str; return ($upstream ? 'http'.(@$_SERVER['HTTPS'] ? 's' : '').'://www.nicholaswilson.me.uk' : '').//$_SERVER['SERVER_NAME'] ($upstream ? CONTENT_BASE_UPSTREAM : CONTENT_BASE). ltrim($str, '/');}class core { /** * Fetches, and caches. Could return false. */static function fetch($url) { //if ($_SERVER['SERVER_NAME'] == 'localhost') return false; $fname = 'cache/md5-'.md5($url); if (file_exists($fname) && time() - filemtime($fname) < 600) return file_get_contents($fname); $curl = curl_init($url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $return = curl_exec($curl); curl_close($curl); if ($return !== false) file_put_contents($fname, $return); return $return;}static function registerVirtualDir($target, $path, $handler) { global $index; /*$index['targets'] spec: we can elect to handle a whole target with one handler by setting it as the string value for that target; or, we can provide more fine-grained control by registering a descending array of paths which determine handling of the files; the deepest path wins. Example: 1) $index['targets']['atom'] = array('' => 'feed') corresponds to the feed content-type registering itself to handle all atom targets. 2) $index['targets'][''] = array('' => 'page', 'blog' => array('' => 'post', 'special-dir' => array('', listings-handler') )); Here we see the pages handler registering itself to manage everything by default, but the post handler grabs the blog directory, and the listings-handler in turn steals handling for its own directory under that. $path syntax: [ (... "/")* ] ("univ" | "val:'...'") */ $tokens = array_reverse(explode('/', $path)); $index['targets'] = (array)@$index['targets'];//Create if needed $index['targets'][$target] = (array)@$index['targets'][$target]; $tree = &$index['targets'][$target]; while (($next = array_pop($tokens)) !== NULL) { if (count($tokens) == 0) $tree[$next] = $handler; else { $tree[$next] = (array)@$tree[$next]; $tree = &$tree[$next]; } }}static function handleVirtualDir($target, $uri) { global $index; if (!is_array(@$index['targets'][$target])) return false; $tree = &$index['targets'][$target]; $tokens = array_reverse($uri); $universal = false; while (($next = array_pop($tokens)) !== NULL) { $universal = @$tree['univ'] ? $tree['univ'] : $universal; if (array_key_exists("val:'$next'", $tree) && is_string($tree["val:'$next'"])) return $tree["val:'$next'"]; if (!array_key_exists($next, $tree)) break; $tree = &$tree[$next]; } return $universal;}static function scanDirs($basedir, $regexp) { $oldcwd = getcwd(); if (!chdir(CONTENT_ROOT.$basedir)) return; if (!function_exists('scanDirsRec')) { function scanDirsRec($dir, $regexp, $filelist) { $odir = ($dir == '') ? opendir('.') : opendir($dir); while (($file = readdir($odir)) !== false) { if (is_file("$dir$file") && preg_match($regexp, $file)) $filelist[] = "$dir$file"; elseif (is_dir("$dir$file") && $file != '.' && $file != '..' && "$dir$file" != 'system') $filelist = scanDirsRec("$dir$file/", $regexp, $filelist); } return $filelist; } } $filelist = scanDirsRec('', $regexp, array()); chdir($oldcwd); return $filelist;}static function loadIndex() { global $index; $index_file = @file_get_contents('cache/db-index'); if ($index_file === false || !is_array($index = @unserialize($index_file))) { $index = array(); foreach (scandir('classes') as $provider) if (preg_match('#^(?P<name>.*)\.php$#', $provider, $match)) { @call_user_func($match['name'].'::buildIndex'); } file_put_contents('cache/db-index', serialize($index)); }}static function cacheResource($res) { global $target, $user; $doc = new DOMDocument(); $handler = (func_num_args() > 1) ? func_get_arg(1) : core::handleVirtualDir($target, explode('/',$res)); if ($handler == false) return false; $cachem = @filemtime(SYSTEM_ROOT."cache/$res.xml"); $origm = @filemtime(CONTENT_ROOT.$res."-$handler.xml"); if (DEBUG) error_log("Caching for $res: cached mtime is $cachem, real mtime is $origm, fresh is ".($cachem >= $origm)); if ($cachem >= $origm && @$doc->load(SYSTEM_ROOT."cache/$res.xml")) return $doc; if (DEBUG) error_log(' (Cache miss; rebuilding)'); //We will notify the hub of each new article posted /* if ($_SERVER['SERVER_NAME'] != 'localhost') { $mh = curl_multi_init();curl_multi_add_handle($mh,$ch1);int curl_multi_select ( resource $mh [, float $timeout = 1.0 ] )curl_multi_remove_handle($mh, $ch1);curl_multi_remove_handle($mh, $ch2); $base = prependDomain('feed/', true); "http://pubsubhubbub.appspot.com/" hub.mode REQUIRED. The literal string "publish". hub.url REQUIRED. The topic URL of the topic that has been updated. This field may be repeated to indicate multiple topics that have been updated. curl_setopt($ch, CURLOPT_URL,'https://graph.facebook.com/1462358159/feed'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE); curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__).'/facebook-ca-file'); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $attachment); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); //to suppress the curl output $result = curl_exec($ch); if ($result === false || curl_errno($ch)) { error_log("Tried to post an article to Facebook, but failed. Got: ".curl_error($ch).", and data was:\n".print_r($attachment,true)); } elseif (!preg_match('/^\s*{"id":"\d*_\d*"}\s*$/', $result)) { error_log("Posted to Facebook and curl happy, but FB said: $result"); } curl_close($ch); }*/ call_user_func("$handler::cache", $res); if (!$doc->load(SYSTEM_ROOT."cache/$res.xml")) { error_log("Caching seems to be broken; $res could not be read from cache straight after generating it"); return false; } $cacheNS = "http://www.nicholaswilson.me.uk/XIV/ns/cache"; $vocNS = "http://www.nicholaswilson.me.uk/XIV/ns/vocabulary"; $hasGroups = false; $groups = $doc->getElementsByTagNameNS($cacheNS, 'groups'); if ($groups->length > 0) { $groups = $groups->item(0); if ($groups->getElementsByTagNameNS($vocNS, 'term')->length > 0) $hasGroups = true; } if ((int)$cachem < 100 && preg_match('/^\d{4}\//', $res) && $_SERVER['SERVER_NAME'] != 'localhost' && !$hasGroups) { //We have just published a shiny new article, which should go to Facebook $data = simplexml_import_dom($doc); $data->registerXPathNamespace('cache',$cacheNS); $str = $data->xpath('cache:strtitle'); $abs = $data->xpath('cache:abstract'); $attachment = array( 'name' => (string)$str[0], 'link' => "http://www.nicholaswilson.me.uk/$res", 'source' => 'http://www.google.com', 'caption'=>' <center></center>', 'description' => facebook::html($abs[0]->asXML()).'<center>✼✼✼</center>', 'properties'=>json_encode(array( "Read more"=>array("text" => "Whole post ➡", "href"=> "http://www.nicholaswilson.me.uk/$res"), "From"=>array("text"=> "Nicholas’ blog of ideas and code", "href"=> "http://www.nicholaswilson.me.uk/"))) ); facebook::send($attachment); } return $doc;}static function applyStylesheets($file, $sheets/*, $params*/) { if (is_string($file)) { $doc = new DOMDocument(); if (!$doc->load(CONTENT_ROOT.$file)) return; } else { $doc = $file; //Crazy fix to bug regarding document normalization: if (!$doc->loadXML($doc->saveXML())) return; } $xsl = new XSLTProcessor(); $style = new DOMDocument(); if (func_num_args() > 2) foreach(func_get_arg(2) as $par => $parval) $xsl->setParameter('', $par, $parval); $xsl->setParameter('', 'CONTENT_BASE', CONTENT_BASE); $xsl->setParameter('', 'SYSTEM_BASE', SYSTEM_BASE ); $xsl->setParameter('', 'SITE_TITLE', SITE_TITLE ); $xsl->setParameter('', 'TITLE_SEP', TITLE_SEP ); $xsl->setParameter('', 'SITE_TAGLINE', SITE_TAGLINE); $xsl->registerPHPFunctions(array('versionDesignFile', 'fuzzyDate', 'fileUpdated', 'feedQuery', 'urlencode', 'prependDomain', 'preg_replace', 'date', 'strtotime', 'file_exists', 'core::assemblePage')); foreach($sheets as $sheet) { $style->load(SYSTEM_ROOT."stylesheets/$sheet"); $xsl->importStylesheet($style); if ($sheet == 'docbook-stage1.xsl') $doc = mathml::convert($doc); //error_log("\n\n\nabout to apply ".$sheet); //error_log($doc->saveXML()); $doc = $xsl->transformToDoc($doc); } return $doc;}/* Here we put together a page for outputting. A DOM is built containing all the data needed for constructing the page, passed in as a list of resources to load and an array of strings as name=>value pairs. The resources are specified in the following format: it is an array, with elements either strings representing resources to load into the default 'posts' container, or arrays representing groups of resources to go in their own container. The output is transformed according to the stylesheet, then gzip'ed and cached, and finally output as appropriate. Mime handling is performed, with the default being to output the data as XHTML. *//** * I should be documenting this a bit more... * @param $static_values usually an array of keys => values which get passed to the stylesheet * inside a root-level element <temp:key>. If value is a string, the elt has a text value; * value can also be a DOMNode or DOMDocumentFragment. * If static_values is a string, it is split by , to get fields, then by ; to make an array. * @param $resources Too complicated for me to work out what I did back then... * @param $stylesheets A single one as a string (eg 'post.xsl') or an array of them */static public function assemblePage($resource, $static_values, $resources, $stylesheets, $cache = true, $final = true, $expire_seconds = 600, $prefmimes = array('application/xhtml+xml' => '1.0', 'application/xml' => '0.9', 'text/html' => '0.5'), $defmimes = array('text/html', 'application/xhtml+xml')) { $mime = conneg::negotiate($prefmimes, $defmimes); if ($final) { header("Content-Type: $mime;charset=UTF-8"); } if (!DEBUG && $final) { header("Cache-Contol: max-age=$expire_seconds,must-revalidate"); header('Expires: '.date('D, d M Y H:i:s', time()+$expire_seconds).' GMT'); header('Vary: Cookie', false); } /* We will assemble the page in this DOMDocument. */ $doc = new DOMDocument('1.0', 'UTF-8'); $tempNS = 'http://www.nicholaswilson.me.uk/';//Leave even if this is not your site /* We will build up in the DOM all the data that will be in the final page by loading all the content on the page from the cache, in raw form, then run a transformation on it to re-jig into its final form.*/ /* Add in the page title */ $root = $doc->createElementNS($tempNS, 'temp:root'); $root = $doc->appendChild($root); if (!is_array($static_values)) { $static_values = (string)$static_values; $pieces = explode(',', $static_values); $static_values = array(); foreach ($pieces as $piece) { $piece = explode(';', $piece, 2); $static_values[$piece[0]] = $piece[1]; } } foreach($static_values as $name=>$value) { $element = $doc->createElementNS($tempNS, "temp:$name"); if (is_string($value)) $element->nodeValue = $value; elseif (is_object($value) && $value instanceof DOMDocumentFragment) { $frag = $doc->importNode($value, true); $element->appendChild($frag); } elseif (is_object($value) && $value instanceof DOMNode) { //More crazy libxml+PHP bugs... $frag = $doc->createDocumentFragment(); $xml = preg_replace('#<\?xml.*>#U', '', simplexml_import_dom($value)->asXML()); $frag->appendXML($xml); $element->appendChild($frag); } $root->appendChild($element); } /* Now we will add in an array of all the resources that feature on the page */ $post_container = $doc->createElementNS($tempNS, 'temp:posts'); $post_container = $root->appendChild($post_container); $current_container = $post_container; if (!is_array($resources)) $resources = array(); foreach($resources as $key=>$reslist) { if (is_array($reslist)) { $current_container = $doc->createElementNS($tempNS, $key); $current_container = $root->appendChild($current_container); } else $reslist = array($reslist); foreach($reslist as $res) { $res = core::cacheResource($res); if ($res === false) continue; $newres = $doc->importNode($res->firstChild, true); if ($newres === false) { error_log("Failed to copy cached post $res into tree for $resource"); continue; } $current_container->appendChild($newres); } $current_container = $post_container; } if (!is_array($stylesheets)) $stylesheets = array($stylesheets); $doc = core::applyStylesheets($doc, $stylesheets); if ($mime == 'text/html') { $xml = $doc->saveXML($doc, LIBXML_NOEMPTYTAG); $xml = substr($xml, strpos($xml, '<!')); } else $xml = $doc->saveXML(); if ($final) conneg::gzoutput($xml); else return $xml;}}