KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

389 lines
18KB

  1. <?php
  2. class Aggregator extends Modules {
  3. static function __install() {
  4. $config = Config::current();
  5. $config->set("last_aggregation", 0);
  6. $config->set("aggregate_every", 30);
  7. $config->set("disable_aggregation", false);
  8. $config->set("aggregates", array());
  9. Group::add_permission("add_aggregate", "Add Aggregate");
  10. Group::add_permission("edit_aggregate", "Edit Aggregate");
  11. Group::add_permission("delete_aggregate", "Delete Aggregate");
  12. }
  13. static function __uninstall() {
  14. $config = Config::current();
  15. $config->remove("last_aggregation");
  16. $config->remove("aggregate_every");
  17. $config->remove("disable_aggregation");
  18. $config->remove("aggregates");
  19. Group::remove_permission("add_aggregate");
  20. Group::remove_permission("edit_aggregate");
  21. Group::remove_permission("delete_aggregate");
  22. }
  23. public function main_index($main) {
  24. $config = Config::current();
  25. if ($config->disable_aggregation or time() - $config->last_aggregation < ($config->aggregate_every * 60))
  26. return;
  27. $aggregates = (array) $config->aggregates;
  28. if (empty($aggregates))
  29. return;
  30. foreach ($aggregates as $name => $feed) {
  31. $xml_contents = preg_replace(array("/<(\/?)dc:date>/", "/xmlns=/"),
  32. array("<\\1date>", "a="),
  33. get_remote($feed["url"]));
  34. $xml = simplexml_load_string($xml_contents, "SimpleXMLElement", LIBXML_NOCDATA);
  35. if ($xml === false)
  36. continue;
  37. # Flatten namespaces recursively
  38. $this->flatten($xml);
  39. $items = array();
  40. if (isset($xml->entry))
  41. foreach ($xml->entry as $entry)
  42. array_unshift($items, $entry);
  43. elseif (isset($xml->item))
  44. foreach ($xml->item as $item)
  45. array_unshift($items, $item);
  46. else
  47. foreach ($xml->channel->item as $item)
  48. array_unshift($items, $item);
  49. foreach ($items as $item) {
  50. $date = oneof(@$item->pubDate, @$item->date, @$item->updated, 0);
  51. $updated = strtotime($date);
  52. if ($updated > $feed["last_updated"]) {
  53. # Get creation date ('created' in Atom)
  54. $created = @$item->created ? strtotime($item->created) : 0;
  55. if ($created <= 0)
  56. $created = $updated;
  57. # Construct the post data from the user-defined XPath mapping:
  58. $data = array("aggregate" => $name);
  59. foreach ($feed["data"] as $attr => $field) {
  60. $field = (!empty($field) ? $this->parse_field($field, $item) : "");
  61. $data[$attr] = (is_string($field) ? $field : YAML::dump($field));
  62. }
  63. $clean = sanitize(oneof(@$data["title"], @$data["name"], ""));
  64. Post::add($data, $clean, null, $feed["feather"], $feed["author"],
  65. false,
  66. "public",
  67. datetime($created),
  68. datetime($updated));
  69. $aggregates[$name]["last_updated"] = $updated;
  70. }
  71. }
  72. }
  73. $config->set("aggregates", $aggregates);
  74. $config->set("last_aggregation", time());
  75. }
  76. public function admin_manage_aggregates($admin) {
  77. $aggregates = array();
  78. foreach ((array) Config::current()->aggregates as $name => $aggregate)
  79. $aggregates[] = array_merge(array("name" => $name), array("user" => new User($aggregate["author"])), $aggregate);
  80. $admin->display("manage_aggregates", array("aggregates" => new Paginator($aggregates, 25)));
  81. }
  82. public function manage_nav($navs) {
  83. if (!Visitor::current()->group->can("edit_aggregate", "delete_aggregate"))
  84. return $navs;
  85. $navs["manage_aggregates"] = array("title" => __("Aggregates", "aggregator"),
  86. "selected" => array("edit_aggregate", "delete_aggregate", "new_aggregate"));
  87. return $navs;
  88. }
  89. public function manage_nav_pages($pages) {
  90. array_push($pages, "manage_aggregates", "edit_aggregate", "delete_aggregate", "new_aggregate");
  91. return $pages;
  92. }
  93. public function manage_nav_show($possibilities) {
  94. $possibilities[] = Visitor::current()->group->can("edit_aggregate", "delete_aggregate");
  95. return $possibilities;
  96. }
  97. public function determine_action($action) {
  98. if ($action != "manage") return;
  99. if (Visitor::current()->group->can("edit_aggregate", "delete_aggregate"))
  100. return "manage_aggregates";
  101. }
  102. public function settings_nav($navs) {
  103. if (Visitor::current()->group->can("change_settings"))
  104. $navs["aggregation_settings"] = array("title" => __("Aggregation", "aggregator"));
  105. return $navs;
  106. }
  107. private function flatten(&$start) {
  108. foreach ($start as $key => $val) {
  109. if (count($val) and !is_string($val)) {
  110. foreach ($val->getNamespaces(true) as $namespace => $url) {
  111. if (empty($namespace))
  112. continue;
  113. foreach ($val->children($url) as $attr => $child) {
  114. $name = $namespace.":".$attr;
  115. $val->$name = $child;
  116. foreach ($child->attributes() as $attr => $value)
  117. $val->$name->addAttribute($attr, $value);
  118. }
  119. }
  120. $this->flatten($val);
  121. }
  122. }
  123. }
  124. static function image_from_content($html) {
  125. preg_match("/img src=('|\")([^ \\1]+)\\1/", $html, $image);
  126. return $image[2];
  127. }
  128. static function upload_image_from_content($html) {
  129. return upload_from_url(self::image_from_content($html));
  130. }
  131. public function parse_field($value, $item, $basic = true) {
  132. if (is_array($value)) {
  133. $parsed = array();
  134. foreach ($value as $key => $val)
  135. $parsed[$this->parse_field($key, $item, false)] = $this->parse_field($val, $item, false);
  136. return $parsed;
  137. } elseif (!is_string($value))
  138. return $value;
  139. if ($basic and preg_match("/^([a-z0-9:\/]+)$/", $value)) {
  140. $xpath = $item->xpath($value);
  141. return html_entity_decode($xpath[0], ENT_QUOTES, "utf-8");
  142. }
  143. if (preg_match("/feed\[(.+)\]\.attr\[([^\]]+)\]/", $value, $matches)) {
  144. $xpath = $item->xpath($matches[1]);
  145. $value = str_replace($matches[0],
  146. html_entity_decode($xpath[0]->attributes()->$matches[2],
  147. ENT_QUOTES,
  148. "utf-8"),
  149. $value);
  150. }
  151. if (preg_match("/feed\[(.+)\]/", $value, $matches)) {
  152. $xpath = $item->xpath($matches[1]);
  153. $value = str_replace($matches[0],
  154. html_entity_decode($xpath[0], ENT_QUOTES, "utf-8"),
  155. $value);
  156. }
  157. if (preg_match_all("/call:([^\(]+)\((.+)\)/", $value, $calls))
  158. foreach ($calls[0] as $index => $full) {
  159. $function = $calls[1][$index];
  160. $arguments = explode(" || ", $calls[2][$index]);
  161. $value = str_replace($full,
  162. call_user_func_array($function, $arguments),
  163. $value);
  164. }
  165. return $value;
  166. }
  167. public function admin_aggregation_settings($admin) {
  168. if (!Visitor::current()->group->can("change_settings"))
  169. show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings."));
  170. if (empty($_POST))
  171. return $admin->display("aggregation_settings");
  172. if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey)
  173. show_403(__("Access Denied"), __("Invalid security key."));
  174. $config = Config::current();
  175. $set = array($config->set("aggregate_every", $_POST['aggregate_every']),
  176. $config->set("disable_aggregation", !empty($_POST['disable_aggregation'])));
  177. if (!in_array(false, $set))
  178. Flash::notice(__("Settings updated."), "/admin/?action=aggregation_settings");
  179. }
  180. public function admin_new_aggregate($admin) {
  181. $admin->context["users"] = User::find();
  182. if (!Visitor::current()->group->can("add_aggregate"))
  183. show_403(__("Access Denied"), __("You do not have sufficient privileges to add aggregates.", "aggregator"));
  184. if (empty($_POST))
  185. return $admin->display("new_aggregate");
  186. if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey)
  187. show_403(__("Access Denied"), __("Invalid security key."));
  188. if (empty($_POST['data']))
  189. return Flash::warning(__("Please enter the attributes for the Feather."));
  190. if (empty($_POST['name']))
  191. return Flash::warning(__("Please enter a name for the aggregate."));
  192. if (empty($_POST['url']))
  193. return Flash::warning(__("What are you, crazy?! I can't create an aggregate without a source!"));
  194. $config = Config::current();
  195. $aggregate = array("url" => $_POST['url'],
  196. "last_updated" => 0,
  197. "feather" => $_POST['feather'],
  198. "author" => $_POST['author'],
  199. "data" => YAML::load($_POST['data']));
  200. $config->aggregates[$_POST['name']] = $aggregate;
  201. $config->set("aggregates", $config->aggregates);
  202. $config->set("last_aggregation", 0); # to force a refresh
  203. Flash::notice(__("Aggregate created.", "aggregator"), "/admin/?action=manage_aggregates");
  204. }
  205. public function admin_edit_aggregate($admin) {
  206. if (empty($_GET['id']))
  207. error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator"));
  208. if (!Visitor::current()->group->can("edit_aggregate"))
  209. show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator"));
  210. $admin->context["users"] = User::find();
  211. $config = Config::current();
  212. $aggregate = $config->aggregates[$_GET['id']];
  213. if (empty($_POST))
  214. return $admin->display("edit_aggregate",
  215. array("users" => User::find(),
  216. "aggregate" => array("name" => $_GET['id'],
  217. "url" => $aggregate["url"],
  218. "feather" => $aggregate["feather"],
  219. "author" => $aggregate["author"],
  220. "data" => preg_replace("/---\n/",
  221. "",
  222. YAML::dump($aggregate["data"])))));
  223. if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey)
  224. show_403(__("Access Denied"), __("Invalid security key."));
  225. $aggregate = array("url" => $_POST['url'],
  226. "last_updated" => 0,
  227. "feather" => $_POST['feather'],
  228. "author" => $_POST['author'],
  229. "data" => YAML::load($_POST['data']));
  230. unset($config->aggregates[$_GET['id']]);
  231. $config->aggregates[$_POST['name']] = $aggregate;
  232. $config->set("aggregates", $config->aggregates);
  233. $config->set("last_aggregation", 0); // to force a refresh
  234. Flash::notice(__("Aggregate updated.", "aggregator"), "/admin/?action=manage_aggregates");
  235. }
  236. public function admin_delete_aggregate($admin) {
  237. if (empty($_GET['id']))
  238. error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator"));
  239. if (!Visitor::current()->group->can("delete_aggregate"))
  240. show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator"));
  241. $config = Config::current();
  242. $aggregate = $config->aggregates[$_GET['id']];
  243. $admin->context["aggregate"] = array("name" => $_GET['id'],
  244. "url" => $aggregate["url"]);
  245. $admin->display("delete_aggregate", array("aggregate" => array("name" => $_GET['id'],
  246. "url" => $aggregate["url"])));
  247. }
  248. public function admin_destroy_aggregate($admin) {
  249. if (empty($_POST['id']))
  250. error(__("No ID Specified"), __("An ID is required to delete an aggregate.", "aggregator"));
  251. if ($_POST['destroy'] == "bollocks")
  252. redirect("/admin/?action=manage_aggregates");
  253. if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey)
  254. show_403(__("Access Denied"), __("Invalid security key."));
  255. if (!Visitor::current()->group->can("delete_aggregate"))
  256. show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this aggregate.", "aggregator"));
  257. $name = $_POST['id'];
  258. if ($_POST["delete_posts"]) {
  259. $this->delete_posts($name);
  260. $notice = __("Aggregate and its posts deleted.", "aggregator");
  261. } else {
  262. $notice = __("Aggregate deleted.", "aggregator");
  263. }
  264. $config = Config::current();
  265. unset($config->aggregates[$name]);
  266. $config->set("aggregates", $config->aggregates);
  267. Flash::notice($notice, "/admin/?action=manage_aggregates");
  268. }
  269. function delete_posts($aggregate_name) {
  270. $sql = SQL::current();
  271. $attrs = $sql->select("post_attributes",
  272. "post_id",
  273. array("name" => "aggregate", "value" => $aggregate_name))->fetchAll();
  274. foreach( $attrs as $attr) {
  275. Post::delete($attr["post_id"]);
  276. }
  277. }
  278. public function help_aggregation_syntax() {
  279. $title = __("Post Values", "aggregator");
  280. $body = "<p>".__("Use <a href=\"http://yaml.org/\">YAML</a> to specify what post attribute holds what value of the feed entry.", "aggregator")."</p>";
  281. $body.= "<h2>".__("XPath", "aggregator")."</h2>";
  282. $body.= "<cite><strong>".__("Usage")."</strong>: <code>feed[xp/ath]</code></cite>\n";
  283. $body.= "<p>".__("You can use XPath to navigate the feed and find the correct attribute.", "aggregator")."</p>";
  284. $body.= "<h2>".__("Attributes", "aggregator")."</h2>";
  285. $body.= "<cite><strong>".__("Usage")."</strong>: <code>feed[xp/ath].attr[foo]</code></cite>\n";
  286. $body.= "<p>".__("To get the attribute of an element, use XPath to find it and the <code>.attr[]</code> syntax to grab an attribute.", "aggregator")."</p>";
  287. $body.= "<h2>".__("Functions", "aggregator")."</h2>";
  288. $body.= "<cite><strong>".__("Usage")."</strong>: <code>call:foo_function(feed[foo] || feed[arg2])</code></cite>\n";
  289. $body.= "<p>".__("To call a function and use its return value for the post's value, use <code>call:</code>. Separate arguments with <code> || </code>.", "aggregator")."</p>";
  290. $body.= "<p>".__("The Aggregator module provides a couple helper functions:", "aggregator")."</p>";
  291. $body.= "<cite><strong>".__("To upload an image from the content", "aggregator")."</strong>: <code>call:Aggregator::upload_image_from_content(feed[content])</code></cite>";
  292. $body.= "<cite><strong>".__("To get the URL of an image in the content", "aggregator")."</strong>: <code>call:Aggregator::image_from_content(feed[content])</code></cite>";
  293. $body.= "<h2>".__("Example", "aggregator")."</h2>";
  294. $body.= "<p>".__("From the Photo feather:", "aggregator")."</pre>";
  295. $body.= "<pre><code>filename: call:upload_from_url(feed[link].attr[href])\ncaption: feed[description] # or just \"description\"</code></pre>";
  296. return array($title, $body);
  297. }
  298. }