set("last_aggregation", 0); $config->set("aggregate_every", 30); $config->set("disable_aggregation", false); $config->set("aggregates", array()); Group::add_permission("add_aggregate", "Add Aggregate"); Group::add_permission("edit_aggregate", "Edit Aggregate"); Group::add_permission("delete_aggregate", "Delete Aggregate"); } static function __uninstall() { $config = Config::current(); $config->remove("last_aggregation"); $config->remove("aggregate_every"); $config->remove("disable_aggregation"); $config->remove("aggregates"); Group::remove_permission("add_aggregate"); Group::remove_permission("edit_aggregate"); Group::remove_permission("delete_aggregate"); } public function main_index($main) { $config = Config::current(); if ($config->disable_aggregation or time() - $config->last_aggregation < ($config->aggregate_every * 60)) return; $aggregates = (array) $config->aggregates; if (empty($aggregates)) return; foreach ($aggregates as $name => $feed) { $xml_contents = preg_replace(array("/<(\/?)dc:date>/", "/xmlns=/"), array("<\\1date>", "a="), get_remote($feed["url"])); $xml = simplexml_load_string($xml_contents, "SimpleXMLElement", LIBXML_NOCDATA); if ($xml === false) continue; # Flatten namespaces recursively $this->flatten($xml); $items = array(); if (isset($xml->entry)) foreach ($xml->entry as $entry) array_unshift($items, $entry); elseif (isset($xml->item)) foreach ($xml->item as $item) array_unshift($items, $item); else foreach ($xml->channel->item as $item) array_unshift($items, $item); foreach ($items as $item) { $date = oneof(@$item->pubDate, @$item->date, @$item->updated, 0); $updated = strtotime($date); if ($updated > $feed["last_updated"]) { # Get creation date ('created' in Atom) $created = @$item->created ? strtotime($item->created) : 0; if ($created <= 0) $created = $updated; # Construct the post data from the user-defined XPath mapping: $data = array("aggregate" => $name); foreach ($feed["data"] as $attr => $field) { $field = (!empty($field) ? $this->parse_field($field, $item) : ""); $data[$attr] = (is_string($field) ? $field : YAML::dump($field)); } $clean = sanitize(oneof(@$data["title"], @$data["name"], "")); Post::add($data, $clean, null, $feed["feather"], $feed["author"], false, "public", datetime($created), datetime($updated)); $aggregates[$name]["last_updated"] = $updated; } } } $config->set("aggregates", $aggregates); $config->set("last_aggregation", time()); } public function admin_manage_aggregates($admin) { $aggregates = array(); foreach ((array) Config::current()->aggregates as $name => $aggregate) $aggregates[] = array_merge(array("name" => $name), array("user" => new User($aggregate["author"])), $aggregate); $admin->display("manage_aggregates", array("aggregates" => new Paginator($aggregates, 25))); } public function manage_nav($navs) { if (!Visitor::current()->group->can("edit_aggregate", "delete_aggregate")) return $navs; $navs["manage_aggregates"] = array("title" => __("Aggregates", "aggregator"), "selected" => array("edit_aggregate", "delete_aggregate", "new_aggregate")); return $navs; } public function manage_nav_pages($pages) { array_push($pages, "manage_aggregates", "edit_aggregate", "delete_aggregate", "new_aggregate"); return $pages; } public function manage_nav_show($possibilities) { $possibilities[] = Visitor::current()->group->can("edit_aggregate", "delete_aggregate"); return $possibilities; } public function determine_action($action) { if ($action != "manage") return; if (Visitor::current()->group->can("edit_aggregate", "delete_aggregate")) return "manage_aggregates"; } public function settings_nav($navs) { if (Visitor::current()->group->can("change_settings")) $navs["aggregation_settings"] = array("title" => __("Aggregation", "aggregator")); return $navs; } private function flatten(&$start) { foreach ($start as $key => $val) { if (count($val) and !is_string($val)) { foreach ($val->getNamespaces(true) as $namespace => $url) { if (empty($namespace)) continue; foreach ($val->children($url) as $attr => $child) { $name = $namespace.":".$attr; $val->$name = $child; foreach ($child->attributes() as $attr => $value) $val->$name->addAttribute($attr, $value); } } $this->flatten($val); } } } static function image_from_content($html) { preg_match("/img src=('|\")([^ \\1]+)\\1/", $html, $image); return $image[2]; } static function upload_image_from_content($html) { return upload_from_url(self::image_from_content($html)); } public function parse_field($value, $item, $basic = true) { if (is_array($value)) { $parsed = array(); foreach ($value as $key => $val) $parsed[$this->parse_field($key, $item, false)] = $this->parse_field($val, $item, false); return $parsed; } elseif (!is_string($value)) return $value; if ($basic and preg_match("/^([a-z0-9:\/]+)$/", $value)) { $xpath = $item->xpath($value); return html_entity_decode($xpath[0], ENT_QUOTES, "utf-8"); } if (preg_match("/feed\[(.+)\]\.attr\[([^\]]+)\]/", $value, $matches)) { $xpath = $item->xpath($matches[1]); $value = str_replace($matches[0], html_entity_decode($xpath[0]->attributes()->$matches[2], ENT_QUOTES, "utf-8"), $value); } if (preg_match("/feed\[(.+)\]/", $value, $matches)) { $xpath = $item->xpath($matches[1]); $value = str_replace($matches[0], html_entity_decode($xpath[0], ENT_QUOTES, "utf-8"), $value); } if (preg_match_all("/call:([^\(]+)\((.+)\)/", $value, $calls)) foreach ($calls[0] as $index => $full) { $function = $calls[1][$index]; $arguments = explode(" || ", $calls[2][$index]); $value = str_replace($full, call_user_func_array($function, $arguments), $value); } return $value; } public function admin_aggregation_settings($admin) { if (!Visitor::current()->group->can("change_settings")) show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); if (empty($_POST)) return $admin->display("aggregation_settings"); if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) show_403(__("Access Denied"), __("Invalid security key.")); $config = Config::current(); $set = array($config->set("aggregate_every", $_POST['aggregate_every']), $config->set("disable_aggregation", !empty($_POST['disable_aggregation']))); if (!in_array(false, $set)) Flash::notice(__("Settings updated."), "/admin/?action=aggregation_settings"); } public function admin_new_aggregate($admin) { $admin->context["users"] = User::find(); if (!Visitor::current()->group->can("add_aggregate")) show_403(__("Access Denied"), __("You do not have sufficient privileges to add aggregates.", "aggregator")); if (empty($_POST)) return $admin->display("new_aggregate"); if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) show_403(__("Access Denied"), __("Invalid security key.")); if (empty($_POST['data'])) return Flash::warning(__("Please enter the attributes for the Feather.")); if (empty($_POST['name'])) return Flash::warning(__("Please enter a name for the aggregate.")); if (empty($_POST['url'])) return Flash::warning(__("What are you, crazy?! I can't create an aggregate without a source!")); $config = Config::current(); $aggregate = array("url" => $_POST['url'], "last_updated" => 0, "feather" => $_POST['feather'], "author" => $_POST['author'], "data" => YAML::load($_POST['data'])); $config->aggregates[$_POST['name']] = $aggregate; $config->set("aggregates", $config->aggregates); $config->set("last_aggregation", 0); # to force a refresh Flash::notice(__("Aggregate created.", "aggregator"), "/admin/?action=manage_aggregates"); } public function admin_edit_aggregate($admin) { if (empty($_GET['id'])) error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator")); if (!Visitor::current()->group->can("edit_aggregate")) show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator")); $admin->context["users"] = User::find(); $config = Config::current(); $aggregate = $config->aggregates[$_GET['id']]; if (empty($_POST)) return $admin->display("edit_aggregate", array("users" => User::find(), "aggregate" => array("name" => $_GET['id'], "url" => $aggregate["url"], "feather" => $aggregate["feather"], "author" => $aggregate["author"], "data" => preg_replace("/---\n/", "", YAML::dump($aggregate["data"]))))); if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) show_403(__("Access Denied"), __("Invalid security key.")); $aggregate = array("url" => $_POST['url'], "last_updated" => 0, "feather" => $_POST['feather'], "author" => $_POST['author'], "data" => YAML::load($_POST['data'])); unset($config->aggregates[$_GET['id']]); $config->aggregates[$_POST['name']] = $aggregate; $config->set("aggregates", $config->aggregates); $config->set("last_aggregation", 0); // to force a refresh Flash::notice(__("Aggregate updated.", "aggregator"), "/admin/?action=manage_aggregates"); } public function admin_delete_aggregate($admin) { if (empty($_GET['id'])) error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator")); if (!Visitor::current()->group->can("delete_aggregate")) show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator")); $config = Config::current(); $aggregate = $config->aggregates[$_GET['id']]; $admin->context["aggregate"] = array("name" => $_GET['id'], "url" => $aggregate["url"]); $admin->display("delete_aggregate", array("aggregate" => array("name" => $_GET['id'], "url" => $aggregate["url"]))); } public function admin_destroy_aggregate($admin) { if (empty($_POST['id'])) error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator")); if ($_POST['destroy'] == "bollocks") redirect("/admin/?action=manage_aggregates"); if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) show_403(__("Access Denied"), __("Invalid security key.")); if (!Visitor::current()->group->can("delete_aggregate")) show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator")); $name = $_POST['id']; if ($_POST["delete_posts"]) { $this->delete_posts($name); $notice = __("Aggregate and its posts deleted.", "aggregator"); } else { $notice = __("Aggregate deleted.", "aggregator"); } $config = Config::current(); unset($config->aggregates[$name]); $config->set("aggregates", $config->aggregates); Flash::notice($notice, "/admin/?action=manage_aggregates"); } function delete_posts($aggregate_name) { $sql = SQL::current(); $attrs = $sql->select("post_attributes", "post_id", array("name" => "aggregate", "value" => $aggregate_name))->fetchAll(); foreach( $attrs as $attr) { Post::delete($attr["post_id"]); } } public function help_aggregation_syntax() { $title = __("Post Values", "aggregator"); $body = "

".__("Use YAML to specify what post attribute holds what value of the feed entry.", "aggregator")."

"; $body.= "

".__("XPath", "aggregator")."

"; $body.= "".__("Usage").": feed[xp/ath]\n"; $body.= "

".__("You can use XPath to navigate the feed and find the correct attribute.", "aggregator")."

"; $body.= "

".__("Attributes", "aggregator")."

"; $body.= "".__("Usage").": feed[xp/ath].attr[foo]\n"; $body.= "

".__("To get the attribute of an element, use XPath to find it and the .attr[] syntax to grab an attribute.", "aggregator")."

"; $body.= "

".__("Functions", "aggregator")."

"; $body.= "".__("Usage").": call:foo_function(feed[foo] || feed[arg2])\n"; $body.= "

".__("To call a function and use its return value for the post's value, use call:. Separate arguments with || .", "aggregator")."

"; $body.= "

".__("The Aggregator module provides a couple helper functions:", "aggregator")."

"; $body.= "".__("To upload an image from the content", "aggregator").": call:Aggregator::upload_image_from_content(feed[content])"; $body.= "".__("To get the URL of an image in the content", "aggregator").": call:Aggregator::image_from_content(feed[content])"; $body.= "

".__("Example", "aggregator")."

"; $body.= "

".__("From the Photo feather:", "aggregator").""; $body.= "

filename: call:upload_from_url(feed[link].attr[href])\ncaption: feed[description] # or just \"description\"
"; return array($title, $body); } }