start.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371
  1. <?php
  2. /**
  3. * Elgg groups plugin
  4. *
  5. * @package ElggGroups
  6. */
  7. elgg_register_event_handler('init', 'system', 'groups_init');
  8. // Ensure this runs after other plugins
  9. elgg_register_event_handler('init', 'system', 'groups_fields_setup', 10000);
  10. /**
  11. * Initialize the groups plugin.
  12. */
  13. function groups_init() {
  14. elgg_register_library('elgg:groups', elgg_get_plugins_path() . 'groups/lib/groups.php');
  15. // register group entities for search
  16. elgg_register_entity_type('group', '');
  17. // Set up the menu
  18. $item = new ElggMenuItem('groups', elgg_echo('groups'), 'groups/all');
  19. elgg_register_menu_item('site', $item);
  20. // Register a page handler, so we can have nice URLs
  21. elgg_register_page_handler('groups', 'groups_page_handler');
  22. // Register URL handlers for groups
  23. elgg_register_plugin_hook_handler('entity:url', 'group', 'groups_set_url');
  24. elgg_register_plugin_hook_handler('entity:icon:url', 'group', 'groups_set_icon_url');
  25. // Register an icon handler for groups
  26. elgg_register_page_handler('groupicon', 'groups_icon_handler');
  27. // Register some actions
  28. $action_base = elgg_get_plugins_path() . 'groups/actions/groups';
  29. elgg_register_action("groups/edit", "$action_base/edit.php");
  30. elgg_register_action("groups/delete", "$action_base/delete.php");
  31. elgg_register_action("groups/featured", "$action_base/featured.php", 'admin');
  32. $action_base .= '/membership';
  33. elgg_register_action("groups/invite", "$action_base/invite.php");
  34. elgg_register_action("groups/join", "$action_base/join.php");
  35. elgg_register_action("groups/leave", "$action_base/leave.php");
  36. elgg_register_action("groups/remove", "$action_base/remove.php");
  37. elgg_register_action("groups/killrequest", "$action_base/delete_request.php");
  38. elgg_register_action("groups/killinvitation", "$action_base/delete_invite.php");
  39. elgg_register_action("groups/addtogroup", "$action_base/add.php");
  40. // Add some widgets
  41. elgg_register_widget_type('a_users_groups', elgg_echo('groups:widget:membership'), elgg_echo('groups:widgets:description'));
  42. elgg_register_widget_type(
  43. 'group_activity',
  44. elgg_echo('groups:widget:group_activity:title'),
  45. elgg_echo('groups:widget:group_activity:description'),
  46. array('dashboard'),
  47. true
  48. );
  49. // add group activity tool option
  50. add_group_tool_option('activity', elgg_echo('groups:enableactivity'), true);
  51. elgg_extend_view('groups/tool_latest', 'groups/profile/activity_module');
  52. // add link to owner block
  53. elgg_register_plugin_hook_handler('register', 'menu:owner_block', 'groups_activity_owner_block_menu');
  54. // group entity menu
  55. elgg_register_plugin_hook_handler('register', 'menu:entity', 'groups_entity_menu_setup');
  56. // group user hover menu
  57. elgg_register_plugin_hook_handler('register', 'menu:user_hover', 'groups_user_entity_menu_setup');
  58. // invitation request actions
  59. elgg_register_plugin_hook_handler('register', 'menu:invitationrequest', 'groups_invitationrequest_menu_setup');
  60. //extend some views
  61. elgg_extend_view('css/elgg', 'groups/css');
  62. elgg_extend_view('js/elgg', 'groups/js');
  63. // Access permissions
  64. elgg_register_plugin_hook_handler('access:collections:write', 'all', 'groups_write_acl_plugin_hook');
  65. elgg_register_plugin_hook_handler('default', 'access', 'groups_access_default_override');
  66. // Register profile menu hook
  67. elgg_register_plugin_hook_handler('profile_menu', 'profile', 'activity_profile_menu');
  68. // allow ecml in discussion and profiles
  69. elgg_register_plugin_hook_handler('get_views', 'ecml', 'groups_ecml_views_hook');
  70. elgg_register_plugin_hook_handler('get_views', 'ecml', 'groupprofile_ecml_views_hook');
  71. // Register a handler for create groups
  72. elgg_register_event_handler('create', 'group', 'groups_create_event_listener');
  73. elgg_register_event_handler('join', 'group', 'groups_user_join_event_listener');
  74. elgg_register_event_handler('leave', 'group', 'groups_user_leave_event_listener');
  75. elgg_register_event_handler('pagesetup', 'system', 'groups_setup_sidebar_menus');
  76. elgg_register_plugin_hook_handler('access:collections:add_user', 'collection', 'groups_access_collection_override');
  77. elgg_register_event_handler('upgrade', 'system', 'groups_run_upgrades');
  78. // Add tests
  79. elgg_register_plugin_hook_handler('unit_test', 'system', 'groups_test');
  80. }
  81. /**
  82. * This function loads a set of default fields into the profile, then triggers
  83. * a hook letting other plugins to edit add and delete fields.
  84. *
  85. * Note: This is a system:init event triggered function and is run at a super
  86. * low priority to guarantee that it is called after all other plugins have
  87. * initialized.
  88. */
  89. function groups_fields_setup() {
  90. $profile_defaults = array(
  91. 'description' => 'longtext',
  92. 'briefdescription' => 'text',
  93. 'interests' => 'tags',
  94. //'website' => 'url',
  95. );
  96. $profile_defaults = elgg_trigger_plugin_hook('profile:fields', 'group', NULL, $profile_defaults);
  97. elgg_set_config('group', $profile_defaults);
  98. // register any tag metadata names
  99. foreach ($profile_defaults as $name => $type) {
  100. if ($type == 'tags') {
  101. elgg_register_tag_metadata_name($name);
  102. // only shows up in search but why not just set this in en.php as doing it here
  103. // means you cannot override it in a plugin
  104. add_translation(get_current_language(), array("tag_names:$name" => elgg_echo("groups:$name")));
  105. }
  106. }
  107. }
  108. /**
  109. * Configure the groups sidebar menu. Triggered on page setup
  110. *
  111. */
  112. function groups_setup_sidebar_menus() {
  113. // Get the page owner entity
  114. $page_owner = elgg_get_page_owner_entity();
  115. if (elgg_in_context('group_profile')) {
  116. if (!elgg_instanceof($page_owner, 'group')) {
  117. forward('', '404');
  118. }
  119. if (elgg_is_logged_in() && $page_owner->canEdit() && !$page_owner->isPublicMembership()) {
  120. $url = elgg_get_site_url() . "groups/requests/{$page_owner->getGUID()}";
  121. $count = elgg_get_entities_from_relationship(array(
  122. 'type' => 'user',
  123. 'relationship' => 'membership_request',
  124. 'relationship_guid' => $page_owner->getGUID(),
  125. 'inverse_relationship' => true,
  126. 'count' => true,
  127. ));
  128. if ($count) {
  129. $text = elgg_echo('groups:membershiprequests:pending', array($count));
  130. } else {
  131. $text = elgg_echo('groups:membershiprequests');
  132. }
  133. elgg_register_menu_item('page', array(
  134. 'name' => 'membership_requests',
  135. 'text' => $text,
  136. 'href' => $url,
  137. ));
  138. }
  139. }
  140. if (elgg_get_context() == 'groups' && !elgg_instanceof($page_owner, 'group')) {
  141. elgg_register_menu_item('page', array(
  142. 'name' => 'groups:all',
  143. 'text' => elgg_echo('groups:all'),
  144. 'href' => 'groups/all',
  145. ));
  146. $user = elgg_get_logged_in_user_entity();
  147. if ($user) {
  148. $url = "groups/owner/$user->username";
  149. $item = new ElggMenuItem('groups:owned', elgg_echo('groups:owned'), $url);
  150. elgg_register_menu_item('page', $item);
  151. $url = "groups/member/$user->username";
  152. $item = new ElggMenuItem('groups:member', elgg_echo('groups:yours'), $url);
  153. elgg_register_menu_item('page', $item);
  154. $url = "groups/invitations/$user->username";
  155. $invitation_count = groups_get_invited_groups($user->getGUID(), false, array('count' => true));
  156. if ($invitation_count) {
  157. $text = elgg_echo('groups:invitations:pending', array($invitation_count));
  158. } else {
  159. $text = elgg_echo('groups:invitations');
  160. }
  161. $item = new ElggMenuItem('groups:user:invites', $text, $url);
  162. elgg_register_menu_item('page', $item);
  163. }
  164. }
  165. }
  166. /**
  167. * Groups page handler
  168. *
  169. * URLs take the form of
  170. * All groups: groups/all
  171. * User's owned groups: groups/owner/<username>
  172. * User's member groups: groups/member/<username>
  173. * Group profile: groups/profile/<guid>/<title>
  174. * New group: groups/add/<guid>
  175. * Edit group: groups/edit/<guid>
  176. * Group invitations: groups/invitations/<username>
  177. * Invite to group: groups/invite/<guid>
  178. * Membership requests: groups/requests/<guid>
  179. * Group activity: groups/activity/<guid>
  180. * Group members: groups/members/<guid>
  181. *
  182. * @param array $page Array of url segments for routing
  183. * @return bool
  184. */
  185. function groups_page_handler($page) {
  186. elgg_load_library('elgg:groups');
  187. if (!isset($page[0])) {
  188. $page[0] = 'all';
  189. }
  190. elgg_push_breadcrumb(elgg_echo('groups'), "groups/all");
  191. switch ($page[0]) {
  192. case 'all':
  193. groups_handle_all_page();
  194. break;
  195. case 'search':
  196. groups_search_page();
  197. break;
  198. case 'owner':
  199. groups_handle_owned_page();
  200. break;
  201. case 'member':
  202. set_input('username', $page[1]);
  203. groups_handle_mine_page();
  204. break;
  205. case 'invitations':
  206. set_input('username', $page[1]);
  207. groups_handle_invitations_page();
  208. break;
  209. case 'add':
  210. groups_handle_edit_page('add');
  211. break;
  212. case 'edit':
  213. groups_handle_edit_page('edit', $page[1]);
  214. break;
  215. case 'profile':
  216. groups_handle_profile_page($page[1]);
  217. break;
  218. case 'activity':
  219. groups_handle_activity_page($page[1]);
  220. break;
  221. case 'members':
  222. groups_handle_members_page($page[1]);
  223. break;
  224. case 'invite':
  225. groups_handle_invite_page($page[1]);
  226. break;
  227. case 'requests':
  228. groups_handle_requests_page($page[1]);
  229. break;
  230. default:
  231. return false;
  232. }
  233. return true;
  234. }
  235. /**
  236. * Handle group icons.
  237. *
  238. * @param array $page
  239. * @return bool
  240. */
  241. function groups_icon_handler($page) {
  242. // The username should be the file we're getting
  243. if (isset($page[0])) {
  244. set_input('group_guid', $page[0]);
  245. }
  246. if (isset($page[1])) {
  247. set_input('size', $page[1]);
  248. }
  249. // Include the standard profile index
  250. $plugin_dir = elgg_get_plugins_path();
  251. include("$plugin_dir/groups/icon.php");
  252. return true;
  253. }
  254. /**
  255. * Populates the ->getUrl() method for group objects
  256. *
  257. * @param string $hook
  258. * @param string $type
  259. * @param string $url
  260. * @param array $params
  261. * @return string
  262. */
  263. function groups_set_url($hook, $type, $url, $params) {
  264. $entity = $params['entity'];
  265. $title = elgg_get_friendly_title($entity->name);
  266. return "groups/profile/{$entity->guid}/$title";
  267. }
  268. /**
  269. * Override the default entity icon for groups
  270. *
  271. * @param string $hook
  272. * @param string $type
  273. * @param string $url
  274. * @param array $params
  275. * @return string Relative URL
  276. */
  277. function groups_set_icon_url($hook, $type, $url, $params) {
  278. /* @var ElggGroup $group */
  279. $group = $params['entity'];
  280. $size = $params['size'];
  281. $icontime = $group->icontime;
  282. // handle missing metadata (pre 1.7 installations)
  283. if (null === $icontime) {
  284. $file = new ElggFile();
  285. $file->owner_guid = $group->owner_guid;
  286. $file->setFilename("groups/" . $group->guid . "large.jpg");
  287. $icontime = $file->exists() ? time() : 0;
  288. create_metadata($group->guid, 'icontime', $icontime, 'integer', $group->owner_guid, ACCESS_PUBLIC);
  289. }
  290. if ($icontime) {
  291. // return thumbnail
  292. return "groupicon/$group->guid/$size/$icontime.jpg";
  293. }
  294. return "mod/groups/graphics/default{$size}.gif";
  295. }
  296. /**
  297. * Add owner block link
  298. */
  299. function groups_activity_owner_block_menu($hook, $type, $return, $params) {
  300. if (elgg_instanceof($params['entity'], 'group')) {
  301. if ($params['entity']->activity_enable != "no") {
  302. $url = "groups/activity/{$params['entity']->guid}";
  303. $item = new ElggMenuItem('activity', elgg_echo('groups:activity'), $url);
  304. $return[] = $item;
  305. }
  306. }
  307. return $return;
  308. }
  309. /**
  310. * Add links/info to entity menu particular to group entities
  311. */
  312. function groups_entity_menu_setup($hook, $type, $return, $params) {
  313. if (elgg_in_context('widgets')) {
  314. return $return;
  315. }
  316. /* @var ElggGroup $entity */
  317. $entity = $params['entity'];
  318. $handler = elgg_extract('handler', $params, false);
  319. if ($handler != 'groups') {
  320. return $return;
  321. }
  322. /* @var ElggMenuItem $item */
  323. foreach ($return as $index => $item) {
  324. if (in_array($item->getName(), array('access', 'likes', 'unlike', 'edit', 'delete'))) {
  325. unset($return[$index]);
  326. }
  327. }
  328. // membership type
  329. if ($entity->isPublicMembership()) {
  330. $mem = elgg_echo("groups:open");
  331. } else {
  332. $mem = elgg_echo("groups:closed");
  333. }
  334. $options = array(
  335. 'name' => 'membership',
  336. 'text' => $mem,
  337. 'href' => false,
  338. 'priority' => 100,
  339. );
  340. $return[] = ElggMenuItem::factory($options);
  341. // number of members
  342. $num_members = $entity->getMembers(array('count' => true));
  343. $members_string = elgg_echo('groups:member');
  344. $options = array(
  345. 'name' => 'members',
  346. 'text' => $num_members . ' ' . $members_string,
  347. 'href' => false,
  348. 'priority' => 200,
  349. );
  350. $return[] = ElggMenuItem::factory($options);
  351. // feature link
  352. if (elgg_is_admin_logged_in()) {
  353. $isFeatured = $entity->featured_group == "yes";
  354. $return[] = ElggMenuItem::factory(array(
  355. 'name' => 'feature',
  356. 'text' => elgg_echo("groups:makefeatured"),
  357. 'href' => elgg_add_action_tokens_to_url("action/groups/featured?group_guid={$entity->guid}&action_type=feature"),
  358. 'priority' => 300,
  359. 'item_class' => $isFeatured ? 'hidden' : '',
  360. ));
  361. $return[] = ElggMenuItem::factory(array(
  362. 'name' => 'unfeature',
  363. 'text' => elgg_echo("groups:makeunfeatured"),
  364. 'href' => elgg_add_action_tokens_to_url("action/groups/featured?group_guid={$entity->guid}&action_type=unfeature"),
  365. 'priority' => 300,
  366. 'item_class' => $isFeatured ? '' : 'hidden',
  367. ));
  368. }
  369. return $return;
  370. }
  371. /**
  372. * Add a remove user link to user hover menu when the page owner is a group
  373. */
  374. function groups_user_entity_menu_setup($hook, $type, $return, $params) {
  375. if (elgg_is_logged_in()) {
  376. $group = elgg_get_page_owner_entity();
  377. // Check for valid group
  378. if (!elgg_instanceof($group, 'group')) {
  379. return $return;
  380. }
  381. $entity = $params['entity'];
  382. // Make sure we have a user and that user is a member of the group
  383. if (!elgg_instanceof($entity, 'user') || !$group->isMember($entity)) {
  384. return $return;
  385. }
  386. // Add remove link if we can edit the group, and if we're not trying to remove the group owner
  387. if ($group->canEdit() && $group->getOwnerGUID() != $entity->guid) {
  388. $return[] = ElggMenuItem::factory([
  389. 'name' => 'removeuser',
  390. 'href' => "action/groups/remove?user_guid={$entity->guid}&group_guid={$group->guid}",
  391. 'text' => elgg_echo('groups:removeuser'),
  392. 'confirm' => true,
  393. 'priority' => 999,
  394. ]);
  395. }
  396. }
  397. return $return;
  398. }
  399. /**
  400. * Groups created so create an access list for it
  401. */
  402. function groups_create_event_listener($event, $object_type, $object) {
  403. $ac_name = elgg_echo('groups:group') . ": " . $object->name;
  404. $ac_id = create_access_collection($ac_name, $object->guid);
  405. if ($ac_id) {
  406. $object->group_acl = $ac_id;
  407. } else {
  408. // delete group if access creation fails
  409. return false;
  410. }
  411. return true;
  412. }
  413. /**
  414. * Return the write access for the current group if the user has write access to it.
  415. */
  416. function groups_write_acl_plugin_hook($hook, $entity_type, $returnvalue, $params) {
  417. $user_guid = sanitise_int(elgg_extract('user_id', $params), false);
  418. $user = get_user($user_guid);
  419. if (empty($user)) {
  420. return $returnvalue;
  421. }
  422. $page_owner = elgg_get_page_owner_entity();
  423. if (!($page_owner instanceof ElggGroup)) {
  424. return $returnvalue;
  425. }
  426. if (!$page_owner->canWriteToContainer($user_guid)) {
  427. return $returnvalue;
  428. }
  429. // check group content access rules
  430. $allowed_access = array(
  431. ACCESS_PRIVATE
  432. );
  433. if ($page_owner->getContentAccessMode() !== ElggGroup::CONTENT_ACCESS_MODE_MEMBERS_ONLY) {
  434. $allowed_access[] = ACCESS_LOGGED_IN;
  435. $allowed_access[] = ACCESS_PUBLIC;
  436. }
  437. foreach ($returnvalue as $access_id => $access_string) {
  438. if (!in_array($access_id, $allowed_access)) {
  439. unset($returnvalue[$access_id]);
  440. }
  441. }
  442. // add write access to the group
  443. $returnvalue[$page_owner->group_acl] = elgg_echo('groups:acl', array($page_owner->name));
  444. return $returnvalue;
  445. }
  446. /**
  447. * Listens to a group join event and adds a user to the group's access control
  448. *
  449. */
  450. function groups_user_join_event_listener($event, $object_type, $object) {
  451. $group = $object['group'];
  452. $user = $object['user'];
  453. $acl = $group->group_acl;
  454. add_user_to_access_collection($user->guid, $acl);
  455. return true;
  456. }
  457. /**
  458. * Make sure users are added to the access collection
  459. */
  460. function groups_access_collection_override($hook, $entity_type, $returnvalue, $params) {
  461. if (isset($params['collection'])) {
  462. if (elgg_instanceof(get_entity($params['collection']->owner_guid), 'group')) {
  463. return true;
  464. }
  465. }
  466. }
  467. /**
  468. * Listens to a group leave event and removes a user from the group's access control
  469. *
  470. */
  471. function groups_user_leave_event_listener($event, $object_type, $object) {
  472. $group = $object['group'];
  473. $user = $object['user'];
  474. $acl = $group->group_acl;
  475. remove_user_from_access_collection($user->guid, $acl);
  476. return true;
  477. }
  478. /**
  479. * The default access for members only content is this group only. This makes
  480. * for better display of access (can tell it is group only), but does not change
  481. * access to the content.
  482. *
  483. * @param string $hook Hook name
  484. * @param string $type Hook type
  485. * @param int $access Current default access
  486. * @return int
  487. */
  488. function groups_access_default_override($hook, $type, $access) {
  489. $page_owner = elgg_get_page_owner_entity();
  490. if (elgg_instanceof($page_owner, 'group')) {
  491. if ($page_owner->getContentAccessMode() == ElggGroup::CONTENT_ACCESS_MODE_MEMBERS_ONLY) {
  492. $access = $page_owner->group_acl;
  493. }
  494. }
  495. return $access;
  496. }
  497. /**
  498. * Grabs groups by invitations
  499. * Have to override all access until there's a way override access to getter functions.
  500. *
  501. * @param int $user_guid The user's guid
  502. * @param bool $return_guids Return guids rather than ElggGroup objects
  503. * @param array $options Additional options
  504. *
  505. * @return mixed ElggGroups or guids depending on $return_guids, or count
  506. */
  507. function groups_get_invited_groups($user_guid, $return_guids = false, $options = array()) {
  508. $ia = elgg_set_ignore_access(true);
  509. $defaults = array(
  510. 'relationship' => 'invited',
  511. 'relationship_guid' => (int) $user_guid,
  512. 'inverse_relationship' => true,
  513. 'limit' => 0,
  514. );
  515. $options = array_merge($defaults, $options);
  516. $groups = elgg_get_entities_from_relationship($options);
  517. elgg_set_ignore_access($ia);
  518. if ($return_guids) {
  519. $guids = array();
  520. foreach ($groups as $group) {
  521. $guids[] = $group->getGUID();
  522. }
  523. return $guids;
  524. }
  525. return $groups;
  526. }
  527. /**
  528. * Join a user to a group, add river event, clean-up invitations
  529. *
  530. * @param ElggGroup $group
  531. * @param ElggUser $user
  532. * @return bool
  533. */
  534. function groups_join_group($group, $user) {
  535. // access ignore so user can be added to access collection of invisible group
  536. $ia = elgg_set_ignore_access(TRUE);
  537. $result = $group->join($user);
  538. elgg_set_ignore_access($ia);
  539. if ($result) {
  540. // flush user's access info so the collection is added
  541. get_access_list($user->guid, 0, true);
  542. // Remove any invite or join request flags
  543. remove_entity_relationship($group->guid, 'invited', $user->guid);
  544. remove_entity_relationship($user->guid, 'membership_request', $group->guid);
  545. elgg_create_river_item(array(
  546. 'view' => 'river/relationship/member/create',
  547. 'action_type' => 'join',
  548. 'subject_guid' => $user->guid,
  549. 'object_guid' => $group->guid,
  550. ));
  551. return true;
  552. }
  553. return false;
  554. }
  555. /**
  556. * Function to use on groups for access. It will house private, loggedin, public,
  557. * and the group itself. This is when you don't want other groups or access lists
  558. * in the access options available.
  559. *
  560. * @return array
  561. */
  562. function group_access_options($group) {
  563. $access_array = array(
  564. ACCESS_PRIVATE => 'private',
  565. ACCESS_LOGGED_IN => 'logged in users',
  566. ACCESS_PUBLIC => 'public',
  567. $group->group_acl => elgg_echo('groups:acl', array($group->name)),
  568. );
  569. return $access_array;
  570. }
  571. function activity_profile_menu($hook, $entity_type, $return_value, $params) {
  572. if ($params['owner'] instanceof ElggGroup) {
  573. $return_value[] = array(
  574. 'text' => elgg_echo('groups:activity'),
  575. 'href' => "groups/activity/{$params['owner']->getGUID()}"
  576. );
  577. }
  578. return $return_value;
  579. }
  580. /**
  581. * Parse ECML on group discussion views
  582. */
  583. function groups_ecml_views_hook($hook, $entity_type, $return_value, $params) {
  584. $return_value['forum/viewposts'] = elgg_echo('groups:ecml:discussion');
  585. return $return_value;
  586. }
  587. /**
  588. * Parse ECML on group profiles
  589. */
  590. function groupprofile_ecml_views_hook($hook, $entity_type, $return_value, $params) {
  591. $return_value['groups/groupprofile'] = elgg_echo('groups:ecml:groupprofile');
  592. return $return_value;
  593. }
  594. /**
  595. * Discussion
  596. *
  597. */
  598. elgg_register_event_handler('init', 'system', 'discussion_init');
  599. /**
  600. * Initialize the discussion component
  601. */
  602. function discussion_init() {
  603. elgg_register_library('elgg:discussion', elgg_get_plugins_path() . 'groups/lib/discussion.php');
  604. elgg_register_page_handler('discussion', 'discussion_page_handler');
  605. elgg_register_plugin_hook_handler('entity:url', 'object', 'discussion_set_topic_url');
  606. // commenting not allowed on discussion topics (use a different annotation)
  607. elgg_register_plugin_hook_handler('permissions_check:comment', 'object', 'discussion_comment_override');
  608. elgg_register_plugin_hook_handler('permissions_check', 'object', 'discussion_can_edit_reply');
  609. // discussion reply menu
  610. elgg_register_plugin_hook_handler('register', 'menu:entity', 'discussion_reply_menu_setup');
  611. // allow non-owners to add replies to group discussion
  612. elgg_register_plugin_hook_handler('container_permissions_check', 'object', 'discussion_reply_container_permissions_override');
  613. elgg_register_event_handler('update:after', 'object', 'discussion_update_reply_access_ids');
  614. $action_base = elgg_get_plugins_path() . 'groups/actions/discussion';
  615. elgg_register_action('discussion/save', "$action_base/save.php");
  616. elgg_register_action('discussion/delete', "$action_base/delete.php");
  617. elgg_register_action('discussion/reply/save', "$action_base/reply/save.php");
  618. elgg_register_action('discussion/reply/delete', "$action_base/reply/delete.php");
  619. // add link to owner block
  620. elgg_register_plugin_hook_handler('register', 'menu:owner_block', 'discussion_owner_block_menu');
  621. // Register for search.
  622. elgg_register_entity_type('object', 'groupforumtopic');
  623. elgg_register_plugin_hook_handler('search', 'object:groupforumtopic', 'discussion_search_groupforumtopic');
  624. // because replies are not comments, need of our menu item
  625. elgg_register_plugin_hook_handler('register', 'menu:river', 'discussion_add_to_river_menu');
  626. // add the forum tool option
  627. add_group_tool_option('forum', elgg_echo('groups:enableforum'), true);
  628. elgg_extend_view('groups/tool_latest', 'discussion/group_module');
  629. $discussion_js_path = elgg_get_site_url() . 'mod/groups/views/default/js/discussion/';
  630. elgg_register_js('elgg.discussion', $discussion_js_path . 'discussion.js');
  631. elgg_register_ajax_view('ajax/discussion/reply/edit');
  632. // notifications
  633. elgg_register_plugin_hook_handler('get', 'subscriptions', 'discussion_get_subscriptions');
  634. elgg_register_notification_event('object', 'groupforumtopic');
  635. elgg_register_plugin_hook_handler('prepare', 'notification:create:object:groupforumtopic', 'discussion_prepare_notification');
  636. elgg_register_notification_event('object', 'discussion_reply');
  637. elgg_register_plugin_hook_handler('prepare', 'notification:create:object:discussion_reply', 'discussion_prepare_reply_notification');
  638. }
  639. /**
  640. * Discussion page handler
  641. *
  642. * URLs take the form of
  643. * All topics in site: discussion/all
  644. * List topics in forum: discussion/owner/<guid>
  645. * View discussion topic: discussion/view/<guid>
  646. * Add discussion topic: discussion/add/<guid>
  647. * Edit discussion topic: discussion/edit/<guid>
  648. *
  649. * @param array $page Array of url segments for routing
  650. * @return bool
  651. */
  652. function discussion_page_handler($page) {
  653. elgg_load_library('elgg:discussion');
  654. if (!isset($page[0])) {
  655. $page[0] = 'all';
  656. }
  657. elgg_push_breadcrumb(elgg_echo('discussion'), 'discussion/all');
  658. switch ($page[0]) {
  659. case 'all':
  660. discussion_handle_all_page();
  661. break;
  662. case 'owner':
  663. discussion_handle_list_page(elgg_extract(1, $page));
  664. break;
  665. case 'add':
  666. discussion_handle_edit_page('add', elgg_extract(1, $page));
  667. break;
  668. case 'reply':
  669. switch (elgg_extract(1, $page)) {
  670. case 'edit':
  671. discussion_handle_reply_edit_page('edit', elgg_extract(2, $page));
  672. break;
  673. case 'view':
  674. discussion_redirect_to_reply(elgg_extract(2, $page), elgg_extract(3, $page));
  675. break;
  676. default:
  677. return false;
  678. }
  679. break;
  680. case 'edit':
  681. discussion_handle_edit_page('edit', elgg_extract(1, $page));
  682. break;
  683. case 'view':
  684. discussion_handle_view_page(elgg_extract(1, $page));
  685. break;
  686. default:
  687. return false;
  688. }
  689. return true;
  690. }
  691. /**
  692. * Redirect to the reply in context of the containing topic
  693. *
  694. * @param int $reply_guid GUID of the reply
  695. * @param int $fallback_guid GUID of the topic
  696. *
  697. * @return void
  698. * @access private
  699. */
  700. function discussion_redirect_to_reply($reply_guid, $fallback_guid) {
  701. $fail = function () {
  702. register_error(elgg_echo('discussion:reply:error:notfound'));
  703. forward(REFERER);
  704. };
  705. $reply = get_entity($reply_guid);
  706. if (!$reply) {
  707. // try fallback
  708. $fallback = get_entity($fallback_guid);
  709. if (!elgg_instanceof($fallback, 'object', 'groupforumtopic')) {
  710. $fail();
  711. }
  712. register_error(elgg_echo('discussion:reply:error:notfound_fallback'));
  713. forward($fallback->getURL());
  714. }
  715. if (!$reply instanceof ElggDiscussionReply) {
  716. $fail();
  717. }
  718. // start with topic URL
  719. $topic = $reply->getContainerEntity();
  720. // this won't work with threaded comments, but core doesn't support that yet
  721. $count = elgg_get_entities([
  722. 'type' => 'object',
  723. 'subtype' => $reply->getSubtype(),
  724. 'container_guid' => $topic->guid,
  725. 'count' => true,
  726. 'wheres' => ["e.guid < " . (int)$reply->guid],
  727. ]);
  728. $limit = (int)get_input('limit', 0);
  729. if (!$limit) {
  730. $limit = _elgg_services()->config->get('default_limit');
  731. }
  732. $offset = floor($count / $limit) * $limit;
  733. if (!$offset) {
  734. $offset = null;
  735. }
  736. $url = elgg_http_add_url_query_elements($topic->getURL(), [
  737. 'offset' => $offset,
  738. ]) . "#elgg-object-{$reply->guid}";
  739. forward($url);
  740. }
  741. /**
  742. * Override the url for discussion topics and replies
  743. *
  744. * Discussion replies do not have their own page so their url is
  745. * the same as the topic url.
  746. *
  747. * @param string $hook
  748. * @param string $type
  749. * @param string $url
  750. * @param array $params
  751. * @return string
  752. */
  753. function discussion_set_topic_url($hook, $type, $url, $params) {
  754. $entity = $params['entity'];
  755. if (!$entity instanceof ElggObject) {
  756. return;
  757. }
  758. if ($entity->getSubtype() === 'groupforumtopic') {
  759. $title = elgg_get_friendly_title($entity->title);
  760. return "discussion/view/{$entity->guid}/{$title}";
  761. }
  762. if (!$entity instanceof ElggDiscussionReply) {
  763. return;
  764. }
  765. $topic = $entity->getContainerEntity();
  766. return "discussion/reply/view/{$entity->guid}/{$topic->guid}";
  767. }
  768. /**
  769. * We don't want people commenting on topics in the river
  770. *
  771. * @param string $hook
  772. * @param string $type
  773. * @param string $return
  774. * @param array $params
  775. * @return bool
  776. */
  777. function discussion_comment_override($hook, $type, $return, $params) {
  778. if (elgg_instanceof($params['entity'], 'object', 'groupforumtopic')) {
  779. return false;
  780. }
  781. }
  782. /**
  783. * Add owner block link
  784. *
  785. * @param string $hook 'register'
  786. * @param string $type 'menu:owner_block'
  787. * @param ElggMenuItem[] $return
  788. * @param array $params
  789. * @return ElggMenuItem[] $return
  790. */
  791. function discussion_owner_block_menu($hook, $type, $return, $params) {
  792. if (elgg_instanceof($params['entity'], 'group')) {
  793. if ($params['entity']->forum_enable != "no") {
  794. $url = "discussion/owner/{$params['entity']->guid}";
  795. $item = new ElggMenuItem('discussion', elgg_echo('discussion:group'), $url);
  796. $return[] = $item;
  797. }
  798. }
  799. return $return;
  800. }
  801. /**
  802. * Set up menu items for river items
  803. *
  804. * Add reply button for discussion topic. Remove the possibility
  805. * to comment on a discussion reply.
  806. *
  807. * @param string $hook 'register'
  808. * @param string $type 'menu:river'
  809. * @param ElggMenuItem[] $return
  810. * @param array $params
  811. * @return ElggMenuItem[] $return
  812. */
  813. function discussion_add_to_river_menu($hook, $type, $return, $params) {
  814. if (!elgg_is_logged_in() || elgg_in_context('widgets')) {
  815. return $return;
  816. }
  817. $item = $params['item'];
  818. $object = $item->getObjectEntity();
  819. if (elgg_instanceof($object, 'object', 'groupforumtopic')) {
  820. $group = $object->getContainerEntity();
  821. if ($group && ($group->canWriteToContainer() || elgg_is_admin_logged_in())) {
  822. $options = array(
  823. 'name' => 'reply',
  824. 'href' => "#discussion-reply-{$object->guid}",
  825. 'text' => elgg_view_icon('speech-bubble'),
  826. 'title' => elgg_echo('reply:this'),
  827. 'rel' => 'toggle',
  828. 'priority' => 50,
  829. );
  830. $return[] = ElggMenuItem::factory($options);
  831. }
  832. } else {
  833. if (elgg_instanceof($object, 'object', 'discussion_reply', 'ElggDiscussionReply')) {
  834. // Group discussion replies cannot be commented
  835. foreach ($return as $key => $item) {
  836. if ($item->getName() === 'comment') {
  837. unset($return[$key]);
  838. }
  839. }
  840. }
  841. }
  842. return $return;
  843. }
  844. /**
  845. * Prepare a notification message about a new discussion topic
  846. *
  847. * @param string $hook Hook name
  848. * @param string $type Hook type
  849. * @param Elgg\Notifications\Notification $notification The notification to prepare
  850. * @param array $params Hook parameters
  851. * @return Elgg\Notifications\Notification
  852. */
  853. function discussion_prepare_notification($hook, $type, $notification, $params) {
  854. $entity = $params['event']->getObject();
  855. $owner = $params['event']->getActor();
  856. $recipient = $params['recipient'];
  857. $language = $params['language'];
  858. $method = $params['method'];
  859. $descr = $entity->description;
  860. $title = $entity->title;
  861. $group = $entity->getContainerEntity();
  862. $notification->subject = elgg_echo('discussion:topic:notify:subject', array($title), $language);
  863. $notification->body = elgg_echo('discussion:topic:notify:body', array(
  864. $owner->name,
  865. $group->name,
  866. $title,
  867. $descr,
  868. $entity->getURL()
  869. ), $language);
  870. $notification->summary = elgg_echo('discussion:topic:notify:summary', array($entity->title), $language);
  871. return $notification;
  872. }
  873. /**
  874. * Prepare a notification message about a new discussion reply
  875. *
  876. * @param string $hook Hook name
  877. * @param string $type Hook type
  878. * @param Elgg\Notifications\Notification $notification The notification to prepare
  879. * @param array $params Hook parameters
  880. * @return Elgg\Notifications\Notification
  881. */
  882. function discussion_prepare_reply_notification($hook, $type, $notification, $params) {
  883. $reply = $params['event']->getObject();
  884. $topic = $reply->getContainerEntity();
  885. $poster = $reply->getOwnerEntity();
  886. $group = $topic->getContainerEntity();
  887. $language = elgg_extract('language', $params);
  888. $notification->subject = elgg_echo('discussion:reply:notify:subject', array($topic->title), $language);
  889. $notification->body = elgg_echo('discussion:reply:notify:body', array(
  890. $poster->name,
  891. $topic->title,
  892. $group->name,
  893. $reply->description,
  894. $reply->getURL(),
  895. ), $language);
  896. $notification->summary = elgg_echo('discussion:reply:notify:summary', array($topic->title), $language);
  897. return $notification;
  898. }
  899. /**
  900. * Get subscriptions for group notifications
  901. *
  902. * @param string $hook 'get'
  903. * @param string $type 'subscriptions'
  904. * @param array $subscriptions Array containing subscriptions in the form
  905. * <user guid> => array('email', 'site', etc.)
  906. * @param array $params Hook parameters
  907. * @return array
  908. */
  909. function discussion_get_subscriptions($hook, $type, $subscriptions, $params) {
  910. $reply = $params['event']->getObject();
  911. if (!elgg_instanceof($reply, 'object', 'discussion_reply', 'ElggDiscussionReply')) {
  912. return $subscriptions;
  913. }
  914. $group_guid = $reply->getContainerEntity()->container_guid;
  915. $group_subscribers = elgg_get_subscriptions_for_container($group_guid);
  916. return ($subscriptions + $group_subscribers);
  917. }
  918. /**
  919. * A simple function to see who can edit a group discussion post
  920. *
  921. * @param ElggComment $entity the comment $entity
  922. * @param ELggUser $group_owner user who owns the group $group_owner
  923. * @return boolean
  924. */
  925. function groups_can_edit_discussion($entity, $group_owner) {
  926. //logged in user
  927. $user = elgg_get_logged_in_user_guid();
  928. if (($entity->owner_guid == $user) || $group_owner == $user || elgg_is_admin_logged_in()) {
  929. return true;
  930. } else {
  931. return false;
  932. }
  933. }
  934. /**
  935. * Process upgrades for the groups plugin
  936. */
  937. function groups_run_upgrades() {
  938. $path = elgg_get_plugins_path() . 'groups/upgrades/';
  939. $files = elgg_get_upgrade_files($path);
  940. foreach ($files as $file) {
  941. include "$path{$file}";
  942. }
  943. }
  944. /**
  945. * Allow group owner and discussion owner to edit discussion replies.
  946. *
  947. * @param string $hook 'permissions_check'
  948. * @param string $type 'object'
  949. * @param boolean $return
  950. * @param array $params Array('entity' => ElggEntity, 'user' => ElggUser)
  951. * @return boolean True if user is discussion or group owner
  952. */
  953. function discussion_can_edit_reply($hook, $type, $return, $params) {
  954. /** @var $reply ElggEntity */
  955. $reply = $params['entity'];
  956. $user = $params['user'];
  957. if (!elgg_instanceof($reply, 'object', 'discussion_reply', 'ElggDiscussionReply')) {
  958. return $return;
  959. }
  960. if ($reply->owner_guid == $user->guid) {
  961. return true;
  962. }
  963. $discussion = $reply->getContainerEntity();
  964. if ($discussion->owner_guid == $user->guid) {
  965. return true;
  966. }
  967. $group = $discussion->getContainerEntity();
  968. if (elgg_instanceof($group, 'group') && $group->owner_guid == $user->guid) {
  969. return true;
  970. }
  971. return false;
  972. }
  973. /**
  974. * Allow group members to post to a group discussion
  975. *
  976. * @param string $hook 'container_permissions_check'
  977. * @param string $type 'object'
  978. * @param array $return
  979. * @param array $params Array with container, user and subtype
  980. * @return boolean $return
  981. */
  982. function discussion_reply_container_permissions_override($hook, $type, $return, $params) {
  983. /** @var $container ElggEntity */
  984. $container = $params['container'];
  985. $user = $params['user'];
  986. if (elgg_instanceof($container, 'object', 'groupforumtopic')) {
  987. $group = $container->getContainerEntity();
  988. if ($group->canWriteToContainer($user->guid) && $params['subtype'] === 'discussion_reply') {
  989. return true;
  990. }
  991. }
  992. return $return;
  993. }
  994. /**
  995. * Update access_id of discussion replies when topic access_id is updated.
  996. *
  997. * @param string $event 'update'
  998. * @param string $type 'object'
  999. * @param ElggObject $object ElggObject
  1000. */
  1001. function discussion_update_reply_access_ids($event, $type, $object) {
  1002. if (elgg_instanceof($object, 'object', 'groupforumtopic')) {
  1003. $ia = elgg_set_ignore_access(true);
  1004. $options = array(
  1005. 'type' => 'object',
  1006. 'subtype' => 'discussion_reply',
  1007. 'container_guid' => $object->getGUID(),
  1008. 'limit' => 0,
  1009. );
  1010. $batch = new ElggBatch('elgg_get_entities', $options);
  1011. foreach ($batch as $reply) {
  1012. if ($reply->access_id == $object->access_id) {
  1013. // Assume access_id of the replies is up-to-date
  1014. break;
  1015. }
  1016. // Update reply access_id
  1017. $reply->access_id = $object->access_id;
  1018. $reply->save();
  1019. }
  1020. elgg_set_ignore_access($ia);
  1021. }
  1022. }
  1023. /**
  1024. * Set up discussion reply entity menu
  1025. *
  1026. * @param string $hook 'register'
  1027. * @param string $type 'menu:entity'
  1028. * @param ElggMenuItem[] $return
  1029. * @param array $params
  1030. * @return ElggMenuItem[] $return
  1031. */
  1032. function discussion_reply_menu_setup($hook, $type, $return, $params) {
  1033. /** @var $reply ElggEntity */
  1034. $reply = elgg_extract('entity', $params);
  1035. if (empty($reply) || !elgg_instanceof($reply, 'object', 'discussion_reply')) {
  1036. return $return;
  1037. }
  1038. if (!elgg_is_logged_in()) {
  1039. return $return;
  1040. }
  1041. if (elgg_in_context('widgets')) {
  1042. return $return;
  1043. }
  1044. // Reply has the same access as the topic so no need to view it
  1045. $remove = array('access');
  1046. $user = elgg_get_logged_in_user_entity();
  1047. // Allow discussion topic owner, group owner and admins to edit and delete
  1048. if ($reply->canEdit() && !elgg_in_context('activity')) {
  1049. $return[] = ElggMenuItem::factory(array(
  1050. 'name' => 'edit',
  1051. 'text' => elgg_echo('edit'),
  1052. 'href' => "discussion/reply/edit/{$reply->guid}",
  1053. 'priority' => 150,
  1054. ));
  1055. $return[] = ElggMenuItem::factory(array(
  1056. 'name' => 'delete',
  1057. 'text' => elgg_view_icon('delete'),
  1058. 'href' => "action/discussion/reply/delete?guid={$reply->guid}",
  1059. 'priority' => 150,
  1060. 'is_action' => true,
  1061. 'confirm' => elgg_echo('deleteconfirm'),
  1062. ));
  1063. } else {
  1064. // Edit and delete links can be removed from all other users
  1065. $remove[] = 'edit';
  1066. $remove[] = 'delete';
  1067. }
  1068. // Remove unneeded menu items
  1069. foreach ($return as $key => $item) {
  1070. if (in_array($item->getName(), $remove)) {
  1071. unset($return[$key]);
  1072. }
  1073. }
  1074. return $return;
  1075. }
  1076. /**
  1077. * Runs unit tests for groups
  1078. *
  1079. * @return array
  1080. */
  1081. function groups_test($hook, $type, $value, $params) {
  1082. global $CONFIG;
  1083. $value[] = $CONFIG->pluginspath . 'groups/tests/write_access.php';
  1084. return $value;
  1085. }
  1086. /**
  1087. * Search in both forumtopics and topic replies
  1088. *
  1089. * @param string $hook the name of the hook
  1090. * @param string $type the type of the hook
  1091. * @param mixed $value the current return value
  1092. * @param array $params supplied params
  1093. */
  1094. function discussion_search_groupforumtopic($hook, $type, $value, $params) {
  1095. if (empty($params) || !is_array($params)) {
  1096. return $value;
  1097. }
  1098. $subtype = elgg_extract("subtype", $params);
  1099. if (empty($subtype) || ($subtype !== "groupforumtopic")) {
  1100. return $value;
  1101. }
  1102. unset($params["subtype"]);
  1103. $params["subtypes"] = array("groupforumtopic", "discussion_reply");
  1104. // trigger the 'normal' object search as it can handle the added options
  1105. return elgg_trigger_plugin_hook('search', 'object', $params, array());
  1106. }
  1107. /**
  1108. * Setup invitation request actions
  1109. *
  1110. * @param string $hook "register"
  1111. * @param string $type "menu:invitationrequest"
  1112. * @param array $menu Menu items
  1113. * @param array $params Hook params
  1114. * @return array
  1115. */
  1116. function groups_invitationrequest_menu_setup($hook, $type, $menu, $params) {
  1117. $group = elgg_extract('entity', $params);
  1118. $user = elgg_extract('user', $params);
  1119. if (!$group instanceof \ElggGroup) {
  1120. return $menu;
  1121. }
  1122. if (!$user instanceof \ElggUser || !$user->canEdit()) {
  1123. return $menu;
  1124. }
  1125. $accept_url = elgg_http_add_url_query_elements('action/groups/join', array(
  1126. 'user_guid' => $user->guid,
  1127. 'group_guid' => $group->guid,
  1128. ));
  1129. $menu[] = \ElggMenuItem::factory(array(
  1130. 'name' => 'accept',
  1131. 'href' => $accept_url,
  1132. 'is_action' => true,
  1133. 'text' => elgg_echo('accept'),
  1134. 'link_class' => 'elgg-button elgg-button-submit',
  1135. 'is_trusted' => true,
  1136. ));
  1137. $delete_url = elgg_http_add_url_query_elements('action/groups/killinvitation', array(
  1138. 'user_guid' => $user->guid,
  1139. 'group_guid' => $group->guid,
  1140. ));
  1141. $menu[] = \ElggMenuItem::factory(array(
  1142. 'name' => 'delete',
  1143. 'href' => $delete_url,
  1144. 'is_action' => true,
  1145. 'confirm' => elgg_echo('groups:invite:remove:check'),
  1146. 'text' => elgg_echo('delete'),
  1147. 'link_class' => 'elgg-button elgg-button-delete mlm',
  1148. ));
  1149. return $menu;
  1150. }