login_destination.module 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. <?php
  2. /**
  3. * @file
  4. * Control where users are directed to, once they login
  5. */
  6. // Page constants.
  7. define('LOGIN_DESTINATION_REDIRECT_NOTLISTED', 0);
  8. define('LOGIN_DESTINATION_REDIRECT_LISTED', 1);
  9. define('LOGIN_DESTINATION_REDIRECT_PHP', 2);
  10. // Destination constants.
  11. define('LOGIN_DESTINATION_STATIC', 0);
  12. define('LOGIN_DESTINATION_SNIPPET', 1);
  13. /**
  14. * Implements hook_help().
  15. */
  16. function login_destination_help($path, $arg) {
  17. switch ($path) {
  18. case 'admin/help#login_destination':
  19. $output = '';
  20. $output .= '<h3>' . t('About') . '</h3>';
  21. $output .= '<p>' . t('The Login Destination module allows you to customize the destination that the user is redirected to after logging in, registering to the site, using a one-time login link or logging out. The destination can be an internal page or an external URL. You may specify certain conditions like pages or user roles and make the destination depend upon them. You may also use a PHP snippets to provide custom conditions and destinations. Note that PHP Filter module has to be enabled and you have to be granted the "Use PHP for settings" permissions to be able to enter PHP code.') . '</p>';
  22. return $output;
  23. case 'admin/config/people/login-destination':
  24. return '<p>' . t('Login destination rules are evaluated each time a user logs in, registers to the site, uses a one-time login link or logs out. Each rule consists of the destination, path conditions and user roles conditions. First matching rule gets executed.') . '</p>';
  25. }
  26. }
  27. /**
  28. * Implements hook_menu().
  29. */
  30. function login_destination_menu() {
  31. $items['admin/config/people/login-destination'] = array(
  32. 'title' => 'Login destinations',
  33. 'description' => 'Customize the destination that the user is redirected to after login.',
  34. 'page callback' => 'drupal_get_form',
  35. 'page arguments' => array('login_destination_overview_form'),
  36. 'access arguments' => array('administer users'),
  37. 'file' => 'login_destination.admin.inc',
  38. 'weight' => 10,
  39. );
  40. $items['admin/config/people/login-destination/add'] = array(
  41. 'title' => 'Add login destination rule',
  42. 'page callback' => 'drupal_get_form',
  43. 'page arguments' => array('login_destination_edit_form'),
  44. 'access arguments' => array('administer users'),
  45. 'type' => MENU_LOCAL_ACTION,
  46. 'weight' => 1,
  47. 'file' => 'login_destination.admin.inc',
  48. );
  49. $items['admin/config/people/login-destination/edit/%login_destination'] = array(
  50. 'title' => 'Edit login destination rule',
  51. 'page callback' => 'drupal_get_form',
  52. 'page arguments' => array('login_destination_edit_form', 5),
  53. 'access arguments' => array('administer users'),
  54. 'file' => 'login_destination.admin.inc',
  55. );
  56. $items['admin/config/people/login-destination/delete/%login_destination'] = array(
  57. 'title' => 'Delete login destination rule',
  58. 'page callback' => 'drupal_get_form',
  59. 'page arguments' => array('login_destination_delete_form', 5),
  60. 'access arguments' => array('administer users'),
  61. 'file' => 'login_destination.admin.inc',
  62. );
  63. $items['admin/config/people/login-destination/list'] = array(
  64. 'title' => 'List',
  65. 'type' => MENU_DEFAULT_LOCAL_TASK,
  66. 'weight' => -10,
  67. );
  68. $items['admin/config/people/login-destination/settings'] = array(
  69. 'title' => 'Settings',
  70. 'description' => 'Change Login Destination settings.',
  71. 'page callback' => 'drupal_get_form',
  72. 'page arguments' => array('login_destination_settings'),
  73. 'access arguments' => array('administer users'),
  74. 'type' => MENU_LOCAL_TASK,
  75. 'file' => 'login_destination.admin.inc',
  76. 'weight' => 10,
  77. );
  78. return $items;
  79. }
  80. /**
  81. * Load a login destination.
  82. */
  83. function login_destination_load($id) {
  84. $result = db_select('login_destination', 'l')
  85. ->fields('l')
  86. ->condition('id', $id)
  87. ->execute()
  88. ->fetchAssoc();
  89. $result['triggers'] = unserialize($result['triggers']);
  90. if (empty($result['triggers'])) {
  91. $result['triggers'] = array();
  92. }
  93. $result['roles'] = unserialize($result['roles']);
  94. if (empty($result['roles'])) {
  95. $result['roles'] = array();
  96. }
  97. return $result;
  98. }
  99. /**
  100. * Implements hook_theme().
  101. */
  102. function login_destination_theme() {
  103. return array(
  104. 'login_destination_destination' => array(
  105. 'variables' => array('destination' => NULL),
  106. 'file' => 'login_destination.admin.inc',
  107. ),
  108. 'login_destination_pages' => array(
  109. 'variables' => array('pages' => NULL, 'pages_type' => 0),
  110. 'file' => 'login_destination.admin.inc',
  111. ),
  112. 'login_destination_triggers' => array(
  113. 'variables' => array('items' => NULL),
  114. 'file' => 'login_destination.admin.inc',
  115. ),
  116. 'login_destination_roles' => array(
  117. 'variables' => array('items' => NULL),
  118. 'file' => 'login_destination.admin.inc',
  119. ),
  120. 'login_destination_overview_form' => array(
  121. 'file' => 'login_destination.admin.inc',
  122. 'render element' => 'form',
  123. ),
  124. );
  125. }
  126. /**
  127. * Implements hook_form_alter().
  128. */
  129. function login_destination_form_alter(&$form, &$form_state, $form_id) {
  130. // We redirect by using the drupal_goto_alter hook. If we simply
  131. // call drupal_goto() it may break compability with other modules. If we set
  132. // the $_GET['destination'] variable we will loose the possibility to redirect
  133. // to an external URL.
  134. // Please note the the system_goto_action() calls drupal_goto()
  135. // More on this issue http://drupal.org/node/732542.
  136. // If we add the $form_state['redirect'] here it will be overriden by the
  137. // user_login_submit(). So we add a submit handler instead and will set the
  138. // redirect later. Our submit handler will be executed after the execution
  139. // of user_login_submit(). This is because form_submit() functions are
  140. // appended to form before hook_form_alter() is executed.
  141. // We will execute also after LoginToboggan's function as it replaces the
  142. // original submit function from user module.
  143. switch ($form_id) {
  144. // User register page and user login page.
  145. case 'user_register_form':
  146. case 'user_login':
  147. $form['#validate'][] = 'login_destination_validate';
  148. break;
  149. }
  150. switch ($form_id) {
  151. // One-time login, password reset.
  152. case 'user_profile_form':
  153. if (isset($_GET['pass-reset-token'])) {
  154. // Redirect only from user_pass_reset
  155. // You have to explicitally turn on the option to always redirect from
  156. // the profile page. This is for constistency.
  157. $form['#submit'][] = 'login_destination_submit';
  158. break;
  159. }
  160. }
  161. }
  162. /**
  163. * Helper submit function.
  164. */
  165. function login_destination_validate($form, &$form_state) {
  166. // LoginToboggan's unified page is rendered dynamically. Fix it.
  167. switch ($form['#form_id']) {
  168. case 'user_register_form':
  169. if (drupal_match_path($_GET['q'], 'user')) {
  170. $_GET['q'] = 'user/register';
  171. }
  172. break;
  173. case 'user_login':
  174. if (drupal_match_path($_GET['q'], 'user/register')) {
  175. $_GET['q'] = 'user';
  176. }
  177. break;
  178. }
  179. // Fix the current page in case of 403 page.
  180. if ($form['#form_id'] == 'user_login') {
  181. if (drupal_get_http_header('Status') == '403 Forbidden') {
  182. $_GET['current'] = $_GET['destination'];
  183. }
  184. }
  185. }
  186. /**
  187. * Helper submit function.
  188. */
  189. function login_destination_submit($form, &$form_state) {
  190. login_destination_perform_redirect('login');
  191. }
  192. /**
  193. * Implements hook_menu_link_alter().
  194. */
  195. function login_destination_menu_link_alter(&$item) {
  196. // Flag a link to be altered by hook_translated_menu_link_alter().
  197. // This is called only on menu rebuild, so we have to add this information
  198. // manually to the database on install. Clearing caches also helps.
  199. $paths = array('user/logout', 'user/login', 'user');
  200. if (in_array($item['link_path'], $paths)) {
  201. $item['options']['alter'] = TRUE;
  202. }
  203. }
  204. /**
  205. * Implements hook_translated_menu_link_alter().
  206. */
  207. function login_destination_translated_menu_link_alter(&$item, $map) {
  208. global $user;
  209. $paths = array('user/login', 'user');
  210. // Append the current path to URL.
  211. if ($item['link_path'] == 'user/logout' || (in_array($item['link_path'], $paths) && user_is_anonymous())) {
  212. $item['localized_options']['query'] = array('current' => $_GET['q']);
  213. }
  214. }
  215. /**
  216. * Implements hook_page_alter().
  217. */
  218. function login_destination_page_alter(&$page) {
  219. // Substitute toolbar's pre_render function to change links.
  220. if (isset($page['page_top']['toolbar']['#pre_render'])) {
  221. $page['page_top']['toolbar']['#pre_render'][0] = 'login_destination_toolbar_pre_render';
  222. }
  223. }
  224. /**
  225. * Helper function to change toolbar's links.
  226. */
  227. function login_destination_toolbar_pre_render($toolbar) {
  228. $toolbar = toolbar_pre_render($toolbar);
  229. // Add current param to be able to evaluate previous page.
  230. $toolbar['toolbar_user']['#links']['logout']['query'] = array('current' => $_GET['q']);
  231. return $toolbar;
  232. }
  233. /**
  234. * Implements hook_user_login().
  235. */
  236. function login_destination_user_login(&$edit, $account) {
  237. if (!isset($_POST['form_id']) || $_POST['form_id'] != 'user_pass_reset' || variable_get('login_destination_immediate_redirect', FALSE)) {
  238. login_destination_perform_redirect('login');
  239. }
  240. }
  241. /**
  242. * Implements hook_user_insert().
  243. */
  244. function login_destination_user_insert(&$edit, $account, $category) {
  245. global $user;
  246. if (!$user->uid) {
  247. // If the user is already logged in, it means probably that they create a
  248. // user account and not the user registers themselves.
  249. login_destination_perform_redirect('login');
  250. }
  251. }
  252. /**
  253. * Implements hook_user_logout().
  254. */
  255. function login_destination_user_logout($account) {
  256. login_destination_perform_redirect('logout', _login_destination_get_current('logout'));
  257. }
  258. /**
  259. * Implements hook_drupal_goto_alter().
  260. */
  261. function login_destination_drupal_goto_alter(&$path, &$options, &$http_response_code) {
  262. // Note that this functionality cannot be backported do 6.x as Drupal 6 does
  263. // not call drupal_alter for drupal_goto.
  264. // This actually may be used also by templates.
  265. if (isset($GLOBALS['destination'])) {
  266. $destination = $GLOBALS['destination'];
  267. // Alter drupal_goto.
  268. if (is_array($destination)) {
  269. $path = $destination[0];
  270. $options = array();
  271. if (count($destination) > 1) {
  272. $options = $destination[1];
  273. }
  274. }
  275. else {
  276. $path = $destination;
  277. }
  278. }
  279. }
  280. /**
  281. * Pass destination to drupal_goto.
  282. */
  283. function login_destination_prepare_goto($destination) {
  284. // Check if $_GET['destination'] should overwrite us.
  285. if (!isset($_GET['destination']) || !variable_get('login_destination_preserve_destination', FALSE)) {
  286. $GLOBALS['destination'] = $destination;
  287. }
  288. }
  289. /**
  290. * Evaluate rules and perform redirect.
  291. *
  292. * This function is intended to be used by external modules.
  293. *
  294. * @param string $trigger
  295. * Action of login destination rule.
  296. * @param string $current
  297. * Path, if null $_GET['q'] is used.
  298. */
  299. function login_destination_perform_redirect($trigger = '', $current = NULL) {
  300. $destination = login_destination_get_destination($trigger, $current);
  301. // Check if we redirect.
  302. if ($destination !== FALSE) {
  303. login_destination_prepare_goto($destination);
  304. }
  305. }
  306. /**
  307. * Process all destination rules and return destination path.
  308. *
  309. * This function is intended to be used by external modules.
  310. */
  311. function login_destination_get_destination($trigger = '', $current = NULL) {
  312. // Get all the login destination rules from the database.
  313. $result = db_select('login_destination', 'l')
  314. ->fields('l', array(
  315. 'triggers',
  316. 'roles',
  317. 'pages_type',
  318. 'pages',
  319. 'destination_type',
  320. 'destination',
  321. 'enabled',
  322. )
  323. )
  324. ->orderBy('weight')
  325. ->execute()
  326. ->fetchAll();
  327. if ($current == NULL) {
  328. $current = $_GET['q'];
  329. }
  330. // Examine path matches.
  331. foreach ($result as $data) {
  332. // Try to match the subsequent rule.
  333. if (_login_destination_match_rule($data, $trigger, $current)) {
  334. // Note: Matching rule with empty destination will cancel redirect.
  335. return _login_destination_evaluate_rule($data, $trigger);
  336. }
  337. }
  338. // No rule matched.
  339. return FALSE;
  340. }
  341. /**
  342. * Evaluate the code with forms context.
  343. *
  344. * This function hides the calling function's scope from eval().
  345. */
  346. function _login_destination_eval($code) {
  347. // We could use the php_eval(), but would not be able get array return value.
  348. // We always check for the existance of PHP Filter module for security.
  349. return eval('?>' . $code);
  350. }
  351. /**
  352. * A helper function to provide role options.
  353. */
  354. function _login_destination_role_options() {
  355. // User role selection, without anonymous user roles.
  356. $role_options = array_map('check_plain', user_roles(TRUE));
  357. return $role_options;
  358. }
  359. /**
  360. * Get the current path (before trigger was invoked).
  361. */
  362. function _login_destination_get_current($trigger = '') {
  363. if (isset($_GET['current'])) {
  364. return check_plain($_GET['current']);
  365. }
  366. if ($trigger == 'login') {
  367. return $_GET['q'];
  368. }
  369. // Front by default.
  370. return '';
  371. }
  372. /**
  373. * A helper function to determine whether redirection should happen.
  374. *
  375. * @return bool
  376. * TRUE - apply redirect, FALSE - not to apply redirect.
  377. */
  378. function _login_destination_match_rule($rule, $trigger = '', $current = NULL) {
  379. global $user;
  380. // Check rule is enabled or not.
  381. if ($rule->enabled == 0) {
  382. return FALSE;
  383. }
  384. $type = $rule->pages_type;
  385. $pages = $rule->pages;
  386. $triggers = unserialize($rule->triggers);
  387. if (empty($triggers)) {
  388. $triggers = array();
  389. }
  390. $roles = unserialize($rule->roles);
  391. if (empty($roles)) {
  392. $roles = array();
  393. }
  394. // Remove non-existent roles.
  395. $roles = array_intersect_key(_login_destination_role_options(), $roles);
  396. // Examine trigger match.
  397. if (!(empty($triggers) || array_key_exists($trigger, $triggers))) {
  398. return FALSE;
  399. }
  400. // Examine role matches.
  401. $roles_intersect = array_intersect_key($roles, $user->roles);
  402. if (!empty($roles) && empty($roles_intersect)) {
  403. return FALSE;
  404. }
  405. if ($type < LOGIN_DESTINATION_REDIRECT_PHP) {
  406. $pages = drupal_strtolower($pages);
  407. $alias = drupal_strtolower(drupal_get_path_alias($current));
  408. $page_match = drupal_match_path($alias, $pages);
  409. if ($alias != $current) {
  410. $page_match = $page_match || drupal_match_path($current, $pages);
  411. }
  412. $page_match = !($type xor $page_match);
  413. }
  414. elseif (module_exists('php')) {
  415. // Do not execute php if the PHP Filter is off.
  416. $page_match = _login_destination_eval($pages);
  417. }
  418. else {
  419. $page_match = FALSE;
  420. }
  421. return $page_match;
  422. }
  423. /**
  424. * A helper function to evaluate destination path.
  425. */
  426. function _login_destination_evaluate_rule($rule, $trigger = '') {
  427. if ($rule->destination_type == LOGIN_DESTINATION_STATIC) {
  428. // Take only 1st line.
  429. if (preg_match("!^(.*?)$!", $rule->destination, $matches) === 1) {
  430. $path = $matches[1];
  431. if (empty($path)) {
  432. return FALSE;
  433. }
  434. // Current path.
  435. elseif ($path == '<current>') {
  436. return _login_destination_get_current($trigger);
  437. }
  438. // External URL.
  439. elseif (strpos($path, '://') !== FALSE) {
  440. return $path;
  441. }
  442. // Internal URL.
  443. else {
  444. $destination = drupal_parse_url($path);
  445. $options = array();
  446. $options['query'] = $destination['query'];
  447. $options['fragment'] = $destination['fragment'];
  448. // Drupal api, drupal_goto cares about <front>.
  449. return array($destination['path'], $options);
  450. }
  451. }
  452. else {
  453. // Error - multiple lines.
  454. return '';
  455. }
  456. }
  457. elseif (module_exists('php')) {
  458. // We cannot use the php_eval because we expect array here, but for the
  459. // matter of consistent UI we don't do it with the PHP Filter module off.
  460. $result = _login_destination_eval($rule->destination);
  461. if (empty($result)) {
  462. return FALSE;
  463. }
  464. return $result;
  465. }
  466. else {
  467. // PHP code and PHP filter disabled.
  468. return FALSE;
  469. }
  470. }