start.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. /**
  3. * Linkup -- Simple markup to link entities across applications.
  4. *
  5. * It recognizes: @username, !group-alias, !group-guid, #task-guid, #hashtag,
  6. * and *entity-guid.
  7. *
  8. * @package Lorea
  9. * @subpackage Linkup
  10. * @homepage http://lorea.org/plugin/linkup
  11. * @copyright 2012,2013 Lorea Faeries <federation@lorea.org>
  12. * @license COPYING, http://www.gnu.org/licenses/agpl
  13. *
  14. * Copyright 2012-2013 Lorea Faeries <federation@lorea.org>
  15. *
  16. * This program is free software: you can redistribute it and/or
  17. * modify it under the terms of the GNU Affero General Public License
  18. * as published by the Free Software Foundation, either version 3 of
  19. * the License, or (at your option) any later version.
  20. *
  21. * This program is distributed in the hope that it will be useful, but
  22. * WITHOUT ANY WARRANTY; without even the implied warranty of
  23. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  24. * Affero General Public License for more details.
  25. *
  26. * You should have received a copy of the GNU Affero General Public
  27. * License along with this program. If not, see
  28. * <http://www.gnu.org/licenses/>.
  29. */
  30. elgg_register_event_handler('init', 'system', 'linkup_init');
  31. function linkup_init() {
  32. // Register tests
  33. elgg_register_plugin_hook_handler('unit_test', 'system', 'linkup_test');
  34. // Register CSS
  35. elgg_extend_view('css.elgg', 'linkup/css');
  36. // Register hook
  37. elgg_register_plugin_hook_handler('output:before', 'layout', 'linkup_event_handler');
  38. // Extend CSS
  39. elgg_extend_view('css/elgg', 'linkup/css');
  40. }
  41. /**
  42. * @todo I'm ashamed
  43. */
  44. function linkup_test($hook, $type, $value, $params) {
  45. // $value[] = elgg_get_config('pluginspath') . "linkup/tests/linkup_test.php";
  46. return $value;
  47. }
  48. /**
  49. * Markup function
  50. *
  51. * Detect linkup patterns and make links where needed.
  52. *
  53. * Matches a marker character followed by printable characters, and
  54. * prefixed to avoid matching CSS DOM IDs. First match is the prefix,
  55. * second the marker, and third is the subject.
  56. */
  57. function linkup_event_handler($hook, $type, $returnvalue, $params) {
  58. if (elgg_get_viewtype() != 'default') {
  59. // only mess with html views
  60. return $returnvalue;
  61. }
  62. $html = mb_convert_encoding($returnvalue['content'], 'HTML-ENTITIES', 'UTF-8');
  63. if (empty($html)) {
  64. return $returnvalue;
  65. }
  66. libxml_use_internal_errors(TRUE);
  67. try {
  68. $dom = new DOMDocument();
  69. $dom->loadHTML($html);
  70. foreach ($dom->childNodes AS $node) {
  71. linkup_dom_recurse($node, 'linkup_dom_replace');
  72. }
  73. // Remove tags added by DOMDocument
  74. $matches = array('/^\<\!DOCTYPE.*?<html[^>]*><body[^>]*>/isu',
  75. '|</body></html>$|isu');
  76. $content = preg_replace($matches, '', $dom->saveHTML());
  77. // convert back to original encoding
  78. $returnvalue['content'] = mb_convert_encoding($content, 'UTF-8', 'HTML-ENTITIES');
  79. } catch (Exception $e) {
  80. error_log("===== linkup error $e->class $e->message");
  81. }
  82. return $returnvalue;
  83. }
  84. /**
  85. * Utility to traverse a DOMDocument and execute a callback function
  86. * on all children.
  87. *
  88. * @param $root a DOMNode
  89. * @param $callback a function
  90. * @return void
  91. */
  92. function linkup_dom_recurse($root, $callback) {
  93. if ($root->hasChildNodes()) {
  94. foreach ($root->childNodes AS $node) {
  95. linkup_dom_recurse($node, $callback);
  96. }
  97. } else {
  98. if (is_callable($callback)) {
  99. $callback($root);
  100. }
  101. }
  102. }
  103. /**
  104. * linkup marking callback will markup only a safe set of conditions:
  105. *
  106. * - only TextNodes will be affected
  107. * - only if their parent is not an attribute (e.g., title, href)
  108. * - only if the parent tag can contain an A tag
  109. *
  110. * @internal
  111. * @see linkup_skip_tags()
  112. */
  113. function linkup_dom_replace($node) {
  114. if ($node->nodeType == XML_TEXT_NODE
  115. && $node->parentNode->nodeType != XML_ATTRIBUTE_NODE
  116. && !in_array($node->parentNode->tagName, linkup_skip_tags())) {
  117. $html = linkup_regexp($node->nodeValue);
  118. if (!empty($html) && $node->nodeValue != $html) {
  119. // Get what's inside the formatted <body> node from a new DOMDocument
  120. $fix = new DOMDocument();
  121. $fix->loadHTML(trim($html));
  122. // get <body>(.*)</body>
  123. $fix = $fix->documentElement->firstChild;
  124. if (!is_null($fix)) {
  125. $parent = $node->parentNode;
  126. $new_node = $parent->ownerDocument->importNode($fix, TRUE);
  127. $parent->replaceChild($new_node, $node);
  128. }
  129. }
  130. }
  131. }
  132. /**
  133. * linkup_regexp -- markup text with HTML links
  134. *
  135. * This function expects plain text with a specific markup.
  136. * It is called internally by the event handler to execute on selected
  137. * strings: that means, do not try it on any HTML.
  138. *
  139. * @see README.org for markup details
  140. *
  141. * @internal
  142. * @param String $text
  143. * @return String HTML markup as a string
  144. */
  145. function linkup_regexp($text) {
  146. return preg_replace_callback("/(^|[^\w&\"\]])([@!\#\*:])([a-z0-9+-]+)(\b)/u",
  147. "linkup_markup", $text);
  148. }
  149. /**
  150. * linkup_skip_tags -- tags to skip wjen looking for markup
  151. *
  152. * @todo the whole range of tag that can contain A are 'flow elements'
  153. * and 'phrasing elements' as defined at
  154. *
  155. * http://dev.w3.org/html5/markup/common-models.html
  156. */
  157. function linkup_skip_tags() {
  158. static $skip = array('a',
  159. 'button',
  160. 'canvas',
  161. 'class',
  162. 'code',
  163. 'embed',
  164. 'head',
  165. 'iframe',
  166. 'input',
  167. 'link',
  168. 'meta',
  169. 'object',
  170. 'param',
  171. 'pre',
  172. 'script',
  173. 'style',
  174. 'textarea',
  175. '');
  176. return $skip;
  177. }
  178. /**
  179. * Markup callback
  180. *
  181. * Generate HTML markup from linkup patterns, for a single matched
  182. * pattern. It is used as a regular expression callback.
  183. *
  184. * It also takes care of permissions and entity validity, so that no
  185. * link will be made to unauthorized or non-existing entities.
  186. *
  187. * @return String, the resulting markup
  188. */
  189. function linkup_markup($matches) {
  190. $prefix = $matches[1];
  191. $marker = $matches[2];
  192. $subject = $matches[3];
  193. $text = "$marker$subject";
  194. switch($marker) {
  195. case "@": // user
  196. if (preg_match("/^[a-z][\w\.-]+/i", $subject)) {
  197. $entity = get_user_by_username($subject);
  198. $text = linkup_markup_user($entity, $text);
  199. // TODO elgg_trigger_plugin_hook('mention', 'user');
  200. }
  201. break;
  202. case "!": // group
  203. if (preg_match("/^[0-9]+$/", $subject)) {
  204. // Get group by GUID
  205. $entity = get_entity($subject);
  206. // TODO if user can access group
  207. } else if (preg_match("/^[a-z][\w+-]+$/i", $subject)) {
  208. // Get group by alias
  209. if (!elgg_is_active_plugin('group_alias')) { break; }
  210. $entity = get_group_from_group_alias($subject);
  211. }
  212. $text = linkup_markup_group($entity, $text);
  213. break;
  214. case "#": // hashtag or task
  215. if (preg_match("/^[0-9]+$/", $subject) && elgg_is_active_plugin('tasks')) {
  216. // Get task by GUID
  217. $entity = get_entity($subject);
  218. if (elgg_instanceof($entity, 'object', 'task') ||
  219. elgg_instanceof($entity, 'object', 'tasklist') ||
  220. elgg_instanceof($entity, 'object', 'tasklist_top')) {
  221. // Linkup task
  222. $text = linkup_markup_task($entity, $text);
  223. break;
  224. }
  225. }
  226. // Linkup hashtag
  227. $text = linkup_link($text, "hashtag", "/tag/$subject");
  228. break;
  229. case "*": // entity
  230. // Check that the subject is a GUID and current user can access
  231. // the corresponding entity
  232. if (!preg_match("/^[0-9]+$/", $subject)) { break; }
  233. $entity = get_entity($subject);
  234. if (elgg_instanceof($entity, 'user')) {
  235. $text = linkup_markup_user($entity);
  236. }
  237. else if (elgg_instanceof($entity, 'group')) {
  238. $text = linkup_markup_group($entity);
  239. }
  240. else if (elgg_instanceof($entity, 'object', 'task') ||
  241. elgg_instanceof($entity, 'object', 'tasklist') ||
  242. elgg_instanceof($entity, 'object', 'tasklist_top')) {
  243. $text = linkup_markup_task($entity, $text);
  244. }
  245. else if (elgg_instanceof($entity, 'object')) {
  246. // get subtype, check permissions, linkup
  247. if (!elgg_trigger_plugin_hook('linkup', "object:{$entity->getSubtype()}", array('entity' => $entity))) {
  248. // Default object view
  249. $text = linkup_markup_object($entity);
  250. }
  251. }
  252. break;
  253. }
  254. return "$prefix$text";
  255. }
  256. // TODO Move the following to a library
  257. /**
  258. * Generic linkup markup
  259. *
  260. */
  261. function linkup_link($anchor, $css, $url, $title = "") {
  262. $vars = array('class' => "linkup $css", 'href' => $url, 'text' => $anchor);
  263. if (!empty($title)) {
  264. $vars['title'] = $title;
  265. }
  266. return elgg_view('output/url', $vars);
  267. }
  268. /**
  269. * Markup for a group
  270. */
  271. function linkup_markup_group($entity, $default = "") {
  272. if (elgg_instanceof($entity, 'group')) {
  273. if (elgg_is_active_plugin('group_alias')) {
  274. $anchor = "!{$entity->alias}";
  275. } else {
  276. $anchor = "!{$entity->username}";
  277. }
  278. $css = "group";
  279. $url = $entity->getURL();
  280. return linkup_link($anchor, $css, $url, elgg_echo('group') . ": {$entity->name}");
  281. }
  282. return $default;
  283. }
  284. /**
  285. * Markup for a generic object
  286. */
  287. function linkup_markup_object($entity, $default = "") {
  288. if (elgg_instanceof($entity, 'object') && $entity->isEnabled()) {
  289. $subtype = $entity->getSubtype();
  290. if (empty($subtype)) {
  291. $subtype = 'default';
  292. }
  293. $anchor = "{$entity->name}";
  294. $css = elgg_get_friendly_title("object-{$subtype}");
  295. $url = $entity->getURL();
  296. return linkup_link($anchor, $css, $url, elgg_echo("linkup:object:$subtype", array($entity->guid)));
  297. }
  298. return $default;
  299. }
  300. /**
  301. * Markup for a task
  302. */
  303. function linkup_markup_task($entity, $default = "") {
  304. if (!elgg_instanceof($entity, 'object')) {
  305. return $default;
  306. }
  307. $subtypes = array('task', 'tasklist', 'tasklist_top');
  308. $subtype = $entity->getSubtype();
  309. if (!in_array($subtype, $subtypes)) {
  310. return $default;
  311. }
  312. $anchor = "#$entity->guid";
  313. $css = "$subtype";
  314. if ('task' == $subtype) {
  315. $css.= " task-status-{$entity->status}";
  316. }
  317. $url = $entity->getURL();
  318. $title = elgg_echo('task') . " $entity->title";
  319. return linkup_link($anchor, $css, $url, $title);
  320. }
  321. /**
  322. * Markup for a user
  323. */
  324. function linkup_markup_user($entity, $default = "") {
  325. if (elgg_instanceof($entity, 'user') && !$entity->isBanned()) {
  326. $anchor = "@{$entity->username}";
  327. $css = "user";
  328. $url = $entity->getURL();
  329. return linkup_link($anchor, $css, $url, $entity->name);
  330. }
  331. return $default;
  332. }