start.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. <?php
  2. /**
  3. * Notifier
  4. *
  5. * @package Notifier
  6. */
  7. elgg_register_event_handler('init', 'system', 'notifier_init');
  8. /**
  9. * Initialize the plugin
  10. *
  11. * @return void
  12. */
  13. function notifier_init () {
  14. notifier_set_view_listener();
  15. // Add hidden popup module to topbar
  16. elgg_extend_view('page/elements/topbar', 'notifier/popup');
  17. elgg_require_js('notifier/notifier');
  18. // Must always have lightbox loaded because views needing it come via AJAX
  19. elgg_load_js('lightbox');
  20. elgg_load_css('lightbox');
  21. elgg_register_page_handler('notifier', 'notifier_page_handler');
  22. // Add css
  23. elgg_extend_view('css/elgg', 'notifier/css');
  24. elgg_register_notification_method('notifier');
  25. elgg_register_plugin_hook_handler('send', 'notification:notifier', 'notifier_notification_send');
  26. elgg_register_plugin_hook_handler('route', 'friendsof', 'notifier_read_friends_notification');
  27. elgg_register_event_handler('create', 'relationship', 'notifier_relationship_notifications');
  28. elgg_register_event_handler('delete', 'relationship', 'notifier_read_group_invitation_notification');
  29. // Hook handler for cron that removes old messages
  30. elgg_register_plugin_hook_handler('cron', 'daily', 'notifier_cron');
  31. elgg_register_plugin_hook_handler('register', 'menu:topbar', 'notifier_topbar_menu_setup');
  32. elgg_register_event_handler('create', 'user', 'notifier_enable_for_new_user');
  33. elgg_register_event_handler('join', 'group', 'notifier_enable_for_new_group_member');
  34. $action_path = elgg_get_plugins_path() . 'notifier/actions/notifier/';
  35. elgg_register_action('notifier/dismiss', $action_path . 'dismiss.php');
  36. elgg_register_action('notifier/clear', $action_path . 'clear.php');
  37. elgg_register_action('notifier/delete', $action_path . 'delete.php');
  38. }
  39. /**
  40. * Add notifier icon to topbar menu
  41. *
  42. * The menu item opens a popup module defined in view notifier/popup
  43. *
  44. * @param string $hook Hook name
  45. * @param string $type Hook type
  46. * @param ElggMenuItem[] $return Array of menu items
  47. * @param array $params Hook parameters
  48. * @return ElggMenuItem[] $return
  49. */
  50. function notifier_topbar_menu_setup ($hook, $type, $return, $params) {
  51. if (elgg_is_logged_in()) {
  52. // Get amount of unread notifications
  53. $count = (int)notifier_count_unread();
  54. $text = elgg_view_icon('attention');
  55. $tooltip = elgg_echo("notifier:unreadcount", array($count));
  56. if ($count > 0) {
  57. if ($count > 99) {
  58. // Don't allow the counter to grow endlessly
  59. $count = '99+';
  60. }
  61. $hidden = '';
  62. } else {
  63. $hidden = 'class="hidden"';
  64. }
  65. $text .= "<span id=\"notifier-new\" $hidden>$count</span>";
  66. $item = ElggMenuItem::factory(array(
  67. 'name' => 'notifier',
  68. 'href' => '#notifier-popup',
  69. 'text' => $text,
  70. 'priority' => 600,
  71. 'title' => $tooltip,
  72. 'rel' => 'popup',
  73. 'id' => 'notifier-popup-link'
  74. ));
  75. $return[] = $item;
  76. }
  77. return $return;
  78. }
  79. /**
  80. * Dispatches notifier pages
  81. *
  82. * URLs take the form of
  83. * All notifications: notifier/all
  84. * Subjects of a notification: notifier/subjects/<notification guid>
  85. *
  86. * @param array $page Array of URL segments
  87. * @return bool Was the page handled successfully
  88. */
  89. function notifier_page_handler ($page) {
  90. gatekeeper();
  91. if (empty($page[0])) {
  92. $page[0] = 'all';
  93. }
  94. $path = elgg_get_plugins_path() . 'notifier/pages/notifier/';
  95. switch ($page[0]) {
  96. case 'popup':
  97. include_once($path . 'popup.php');
  98. break;
  99. case 'subjects':
  100. set_input('guid', $page[1]);
  101. include_once($path . 'subjects.php');
  102. break;
  103. case 'all':
  104. default:
  105. include_once($path . 'list.php');
  106. break;
  107. }
  108. return true;
  109. }
  110. /**
  111. * Create a notification
  112. *
  113. * @param string $hook Hook name
  114. * @param string $type Hook type
  115. * @param bool $result Has the notification been sent
  116. * @param array $params Hook parameters
  117. * @return bool Was the notification handled successfully
  118. */
  119. function notifier_notification_send($hook, $type, $result, $params) {
  120. $notification = $params['notification'];
  121. /* @var Elgg_Notifications_Notification $notification */
  122. $event = $params['event'];
  123. /* @var Elgg_Notifications_Event $event */
  124. if (!$event) {
  125. // Plugin is calling notify_user() so stop here and let
  126. // the NotificationService handle the notification later.
  127. return false;
  128. }
  129. $ia = elgg_set_ignore_access(true);
  130. $action = $event->getAction();
  131. $object = $event->getObject();
  132. $string = "river:{$action}:{$object->getType()}:{$object->getSubtype()}";
  133. $recipient = $notification->getRecipient();
  134. $actor = $event->getActor();
  135. switch ($object->getType()) {
  136. case 'annotation':
  137. // Get the entity that was annotated
  138. $entity = $object->getEntity();
  139. break;
  140. case 'relationship':
  141. $entity = get_entity($object->guid_two);
  142. break;
  143. default:
  144. if ($object instanceof ElggComment) {
  145. // Use comment's container as notification target
  146. $entity = $object->getContainerEntity();
  147. // Check the action because this isn't necessarily a new comment,
  148. // but e.g. someone being mentioned in a comment
  149. if ($action == 'create') {
  150. $string = "river:comment:{$entity->getType()}:{$entity->getSubtype()}";
  151. }
  152. // TODO How about discussion replies?
  153. } else {
  154. // This covers all other entities
  155. $entity = $object;
  156. }
  157. }
  158. if ($object->getType() == 'annotation' || $object->getType() == 'relationship' || ($object instanceof ElggComment && $action == 'create')) {
  159. // Check if similar notification already exists
  160. $existing = notifier_get_similar($event->getDescription(), $entity, $recipient);
  161. if ($existing) {
  162. // Update the existing notification
  163. $existing->setSubject($actor);
  164. $existing->markUnread();
  165. // time_created must be used because time_updated gets updated
  166. // automatically and it won't therefore match the time_created
  167. // of the object triggering the notification
  168. $existing->time_created = $object->time_created;
  169. return $existing->save();
  170. }
  171. }
  172. // If the river string is not available, fall back to summary or subject
  173. if ($string == elgg_echo($string)) {
  174. if ($notification->summary) {
  175. $string = $notification->summary;
  176. } else {
  177. $string = $notification->subject;
  178. }
  179. }
  180. $note = new ElggNotification();
  181. $note->title = $string;
  182. $note->owner_guid = $recipient->getGUID();
  183. $note->container_guid = $recipient->getGUID();
  184. $note->event = $event->getDescription();
  185. // The notification may be being created later than the event took
  186. // place, so use the original time_created instead of time()
  187. $note->time_created = $object->time_created;
  188. if ($note->save()) {
  189. $note->setSubject($actor);
  190. $note->setTarget($entity);
  191. }
  192. elgg_set_ignore_access($ia);
  193. if ($note) {
  194. return true;
  195. }
  196. }
  197. /**
  198. * Get the count of all unread notifications
  199. *
  200. * @return integer
  201. */
  202. function notifier_count_unread () {
  203. return notifier_get_unread(array('count' => true));
  204. }
  205. /**
  206. * Get all unread messages for logged in users
  207. *
  208. * @param array $options Options passed to elgg_get_entities_from_metadata
  209. * @return ElggNotification[]
  210. */
  211. function notifier_get_unread ($options = array()) {
  212. $defaults = array(
  213. 'type' => 'object',
  214. 'subtype' => 'notification',
  215. 'limit' => false,
  216. 'owner_guid' => elgg_get_logged_in_user_guid(),
  217. 'metadata_name_value_pairs' => array(
  218. 'name' => 'status',
  219. 'value' => 'unread'
  220. )
  221. );
  222. $options = array_merge($defaults, $options);
  223. return elgg_get_entities_from_metadata($options);
  224. }
  225. /**
  226. * Remove over week old notifications that have been read
  227. *
  228. * @param string $hook Hook name
  229. * @param string $type Hook type
  230. * @param string $return Old stdOut contents
  231. * @param array $params Array containing the time when cron was triggered
  232. * @return void
  233. */
  234. function notifier_cron ($hook, $type, $return, $params) {
  235. // One week ago
  236. $time = time() - 60 * 60 * 24 * 7;
  237. $options = array(
  238. 'type' => 'object',
  239. 'subtype' => 'notification',
  240. 'wheres' => array("e.time_created < $time"),
  241. 'metadata_name_value_pairs' => array(
  242. 'name' => 'status',
  243. 'value' => 'read'
  244. ),
  245. 'limit' => false
  246. );
  247. $ia = elgg_set_ignore_access(true);
  248. $notifications = elgg_get_entities_from_metadata($options);
  249. $options['count'] = true;
  250. $count = elgg_get_entities_from_metadata($options);
  251. foreach ($notifications as $notification) {
  252. $notification->delete();
  253. }
  254. echo "<p>Removed $count notifications.</p>";
  255. elgg_set_ignore_access($ia);
  256. }
  257. /**
  258. * Add view listener to views that may be the targets of notifications
  259. *
  260. * @return void
  261. */
  262. function notifier_set_view_listener () {
  263. $dbprefix = elgg_get_config('dbprefix');
  264. $types = get_data("SELECT * FROM {$dbprefix}entity_subtypes");
  265. // These subtypes do not have notifications so they can be skipped
  266. $skip = array(
  267. 'plugin',
  268. 'widget',
  269. 'admin_notice',
  270. 'notification',
  271. 'messages',
  272. 'reported_content',
  273. 'site_notification'
  274. );
  275. foreach ($types as $type) {
  276. if (in_array($type->subtype, $skip)) {
  277. continue;
  278. }
  279. elgg_extend_view("object/{$type->subtype}", 'notifier/view_listener');
  280. }
  281. // Some manual additions
  282. elgg_extend_view('profile/wrapper', 'notifier/view_listener');
  283. }
  284. /**
  285. * Enable notifier by default for new users according to plugin settings.
  286. *
  287. * We do this using 'create, user' event instead of 'register, user' plugin
  288. * hook so that it affects also users created by an admin.
  289. *
  290. * @param string $event 'create'
  291. * @param string $type 'user'
  292. * @param ElggUser $user The user that was created
  293. * @return boolean
  294. */
  295. function notifier_enable_for_new_user ($event, $type, $user) {
  296. $personal = (boolean) elgg_get_plugin_setting('enable_personal', 'notifier');
  297. $collections = (boolean) elgg_get_plugin_setting('enable_collections', 'notifier');
  298. if ($personal) {
  299. $prefix = "notification:method:notifier";
  300. $user->$prefix = true;
  301. }
  302. if ($collections) {
  303. /**
  304. * This function is triggered before invite code is checked so it's
  305. * enough just to add the setting. Notifications plugin will take care
  306. * of adding the 'notifynotifier' relationship in case user was invited.
  307. */
  308. $user->collections_notifications_preferences_notifier = '-1';
  309. }
  310. $user->save();
  311. return true;
  312. }
  313. /**
  314. * Enable notifier as notification method when joining a group.
  315. *
  316. * @param string $event 'join'
  317. * @param string $type 'group'
  318. * @param array $params Array containing ElggUser and ElggGroup
  319. */
  320. function notifier_enable_for_new_group_member ($event, $type, $params) {
  321. $group = $params['group'];
  322. $user = $params['user'];
  323. $enabled = (boolean) elgg_get_plugin_setting('enable_groups', 'notifier');
  324. if ($enabled) {
  325. if (elgg_instanceof($group, 'group') && elgg_instanceof($user, 'user')) {
  326. add_entity_relationship($user->guid, 'notifynotifier', $group->guid);
  327. }
  328. }
  329. }
  330. /**
  331. * Get existing notifications that match the given parameters.
  332. *
  333. * This can be used when we want to update an old notification.
  334. * E.g. "A likes X" and "B likes X" become "A and B like X".
  335. *
  336. * @param string $event_name String like "action:type:subtype"
  337. * @param ElggEntity $entity Entity being notified about
  338. * @param ElggUser $recipient User being notified
  339. * @return ElggNotification|null
  340. */
  341. function notifier_get_similar($event_name, $entity, $recipient) {
  342. $db_prefix = elgg_get_config('dbprefix');
  343. $ia = elgg_set_ignore_access(true);
  344. $object_relationship = ElggNotification::HAS_OBJECT;
  345. // Notification (guid_one) has relationship 'hasObject' to target (guid_two)
  346. $options = array(
  347. 'type' => 'object',
  348. 'subtype' => 'notification',
  349. 'owner_guid' => $recipient->guid,
  350. 'metadata_name_value_pairs' => array(
  351. 'name' => 'event',
  352. 'value' => $event_name,
  353. ),
  354. 'joins' => array(
  355. "JOIN {$db_prefix}entity_relationships er ON e.guid = er.guid_one", // Object relationship
  356. ),
  357. 'wheres' => array(
  358. "er.guid_two = {$entity->guid}",
  359. "er.relationship = '$object_relationship'",
  360. ),
  361. );
  362. $notification = elgg_get_entities_from_metadata($options);
  363. if ($notification) {
  364. $notification = $notification[0];
  365. }
  366. elgg_set_ignore_access($ia);
  367. return $notification;
  368. }
  369. /**
  370. * Mark unread friend notifications as read.
  371. *
  372. * This hook is triggered when user goes to the "friendsof/<username>" page.
  373. *
  374. * @param string $hook Hook name
  375. * @param string $type Hook type
  376. * @param array $return Array containing 'identifier' and 'segments'
  377. * @param array $params This is empty
  378. * @return void
  379. */
  380. function notifier_read_friends_notification ($hook, $type, $return, $params) {
  381. // Get unread notifications that match the friending event
  382. $options = array(
  383. 'metadata_name_value_pairs' => array(
  384. 'name' => 'event',
  385. 'value' => 'create:relationship:friend',
  386. )
  387. );
  388. $notifications = notifier_get_unread($options);
  389. foreach ($notifications as $note) {
  390. $note->markRead();
  391. }
  392. }
  393. /**
  394. * Create notifications about new relationships
  395. *
  396. * The Elgg 1.9 notifications system does not yet process relationships
  397. * so we create the notifications manually on the 'create', 'relationship'
  398. * event instead.
  399. *
  400. * @param string $event 'create'
  401. * @param string $type 'relationship'
  402. * @param ElggRelationship $object The created relationships
  403. * @return boolean Always returns true
  404. */
  405. function notifier_relationship_notifications ($event, $type, $object) {
  406. $guid_one = $object->guid_one;
  407. $guid_two = $object->guid_two;
  408. $relationship = $object->relationship;
  409. switch ($relationship) {
  410. case 'friend':
  411. // Notification about a new friend
  412. $actor = get_user($guid_one);
  413. $recipient = get_user($guid_two);
  414. $target = $recipient;
  415. $string = 'friend:notifications:summary';
  416. break;
  417. case 'invited':
  418. // Notification about a group membership invitation
  419. $actor = elgg_get_logged_in_user_entity(); // User who invited
  420. $target = get_entity($guid_one); // The group
  421. $recipient = get_user($guid_two); // The invited user
  422. $string = 'groups:notifications:invitation';
  423. break;
  424. case 'membership_request':
  425. // Notification about a group membership invitation
  426. $actor = get_user($guid_one); // User who requested
  427. $target = get_entity($guid_two); // The group
  428. $recipient = get_user($target->owner_guid); // The group_owner
  429. $string = 'groups:notifications:membership_request';
  430. break;
  431. default;
  432. return true;
  433. }
  434. if (!$actor) {
  435. return true;
  436. }
  437. if (!$recipient) {
  438. return true;
  439. }
  440. if (!$target) {
  441. return true;
  442. }
  443. $ia = elgg_set_ignore_access(true);
  444. $event_name = "create:relationship:{$relationship}";
  445. $note = notifier_get_similar($event_name, $target, $recipient);
  446. if (!$note) {
  447. $note = new ElggNotification();
  448. $note->title = $string;
  449. $note->owner_guid = $recipient->guid;
  450. $note->container_guid = $recipient->guid;
  451. $note->event = $event_name;
  452. $note->save();
  453. $note->setTarget($target);
  454. } else {
  455. // Mark the existing notification as unread
  456. $note->markUnread();
  457. }
  458. $note->setSubject($actor);
  459. elgg_set_ignore_access($ia);
  460. // Returning false would delete the relationship
  461. return true;
  462. }
  463. /**
  464. * Delete notification about a group invitation when user accepts/deletes it
  465. *
  466. * @param string $event 'delete'
  467. * @param string $type 'relationship'
  468. * @param ElggRelationship $object The relationship being deleted
  469. * @return boolean
  470. */
  471. function notifier_read_group_invitation_notification($event, $type, $object) {
  472. $relationship = $object->relationship;
  473. // Proceed only if the relationship is an invitation
  474. if ($relationship != 'invited' && $relationship != 'membership_request') {
  475. return true;
  476. }
  477. // The group may be hidden, so ignore access
  478. $ia = elgg_set_ignore_access(true);
  479. if ($relationship === 'invited') {
  480. $group = get_entity($object->guid_one);
  481. } else {
  482. $group = get_entity($object->guid_two);
  483. }
  484. elgg_set_ignore_access($ia);
  485. // Proceed only if the invitation is for a group
  486. if (!$group instanceof ElggGroup) {
  487. return true;
  488. }
  489. $dbprefix = elgg_get_config('dbprefix');
  490. $has_object = ElggNotification::HAS_OBJECT;
  491. $options = array(
  492. 'joins' => array("JOIN {$dbprefix}entity_relationships er ON e.guid = er.guid_one"),
  493. 'wheres' => array("er.relationship = '{$has_object}' AND er.guid_two = {$group->guid}"),
  494. );
  495. // Get unread notifications
  496. $notifications = notifier_get_unread($options);
  497. foreach ($notifications as $note) {
  498. if ($note->event === "create:relationship:$relationship") {
  499. $note->markRead();
  500. }
  501. }
  502. // Returning true means that the relationship deletion can now proceed
  503. return true;
  504. }