user_reference.module 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208
  1. <?php
  2. /**
  3. * @file
  4. * Defines a field type for referencing a user from a node.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function user_reference_menu() {
  10. $items['user_reference/autocomplete/%/%/%'] = array(
  11. 'page callback' => 'user_reference_autocomplete',
  12. 'page arguments' => array(2, 3, 4),
  13. 'access callback' => 'reference_autocomplete_access',
  14. 'access arguments' => array(2, 3, 4),
  15. 'type' => MENU_CALLBACK,
  16. );
  17. return $items;
  18. }
  19. /**
  20. * Implements hook_field_info().
  21. */
  22. function user_reference_field_info() {
  23. return array(
  24. 'user_reference' => array(
  25. 'label' => t('User reference'),
  26. 'description' => t('This field stores the ID of a related user as an integer value.'),
  27. 'settings' => array(
  28. 'referenceable_roles' => array(),
  29. 'referenceable_status' => array(),
  30. 'view' => array(
  31. 'view_name' => '',
  32. 'display_name' => '',
  33. 'args' => array(),
  34. ),
  35. ),
  36. 'default_widget' => 'user_reference_autocomplete',
  37. 'default_formatter' => 'user_reference_default',
  38. // Support hook_entity_property_info() from contrib "Entity API".
  39. 'property_type' => 'user',
  40. // Support default token formatter for field tokens.
  41. 'default_token_formatter' => 'user_reference_plain',
  42. ),
  43. );
  44. }
  45. /**
  46. * Implements hook_field_settings_form().
  47. */
  48. function user_reference_field_settings_form($field, $instance, $has_data) {
  49. $settings = $field['settings'];
  50. $form = array();
  51. $form['referenceable_roles'] = array(
  52. '#type' => 'checkboxes',
  53. '#title' => t('User roles that can be referenced'),
  54. '#default_value' => is_array($settings['referenceable_roles'])
  55. ? array_filter($settings['referenceable_roles'])
  56. : array(),
  57. '#options' => user_roles(TRUE),
  58. );
  59. $form['referenceable_status'] = array(
  60. '#type' => 'checkboxes',
  61. '#title' => t('User status that can be referenced'),
  62. '#default_value' => is_array($settings['referenceable_status'])
  63. ? array_filter($settings['referenceable_status'])
  64. : array(1),
  65. '#options' => array(1 => t('Active'), 0 => t('Blocked')),
  66. );
  67. if (module_exists('views')) {
  68. $view_settings = $settings['view'];
  69. $description = '<p>' . t('The list of users that can be referenced can provided by a view (Views module) using the "References" display type.') . '</p>';
  70. // Special note for legacy fields migrated from D6.
  71. if (!empty($view_settings['view_name']) && $view_settings['display_name'] == 'default') {
  72. $description .= '<p><strong><span class="admin-missing">'. t("Important D6 migration note:") . '</span></strong>';
  73. $description .= '<br/>' . t("The field is currently configured to use the 'Master' display of the view %view_name.", array('%view_name' => $view_settings['view_name']));
  74. $description .= '<br/>' . t("It is highly recommended that you: <br/>- edit this view and create a new display using the 'References' display type, <br/>- update the field settings to explicitly select the correct view and display.");
  75. $description .= '<br/>' . t("The field will work correctly until then, but submitting this form might inadvertently change the field settings.") . '</p>';
  76. }
  77. $form['view'] = array(
  78. '#type' => 'fieldset',
  79. '#title' => t('Views - Users that can be referenced'),
  80. '#collapsible' => TRUE,
  81. '#collapsed' => empty($view_settings['view_name']),
  82. '#description' => $description,
  83. );
  84. $views_options = references_get_views_options('user');
  85. if ($views_options) {
  86. // The value of the 'view_and_display' select below will need to be split
  87. // into 'view_name' and 'view_display' in the final submitted values, so
  88. // we massage the data at validate time on the wrapping element (not
  89. // ideal).
  90. $form['view']['#element_validate'] = array('_user_reference_view_settings_validate');
  91. $views_options = array('' => '<' . t('none') . '>') + $views_options;
  92. $default = empty($view_settings['view_name']) ? '' : $view_settings['view_name'] . ':' .$view_settings['display_name'];
  93. $form['view']['view_and_display'] = array(
  94. '#type' => 'select',
  95. '#title' => t('View used to select the users'),
  96. '#options' => $views_options,
  97. '#default_value' => $default,
  98. '#description' => '<p>' . t('Choose the view and display that select the nodes that can be referenced.<br />Only views with a display of type "References" are eligible.') . '</p>' .
  99. t('Note:<ul><li>This will discard the "Referenceable Roles" and "Referenceable Status" settings above. Use the view\'s "filters" section instead.</li><li>Use the view\'s "fields" section to display additional informations about candidate users on user creation/edition form.</li><li>Use the view\'s "sort criteria" section to determine the order in which candidate users will be displayed.</li></ul>'),
  100. );
  101. $default = implode(', ', $view_settings['args']);
  102. $form['view']['args'] = array(
  103. '#type' => 'textfield',
  104. '#title' => t('View arguments'),
  105. '#default_value' => $default,
  106. '#required' => FALSE,
  107. '#description' => t('Provide a comma separated list of arguments to pass to the view.'),
  108. );
  109. }
  110. else {
  111. $form['view']['no_view_help'] = array(
  112. '#markup' => '<p>' . t('No eligible view was found.') .'</p>',
  113. );
  114. }
  115. }
  116. return $form;
  117. }
  118. /**
  119. * Validate callback for the 'view settings' fieldset.
  120. *
  121. * Puts back the various form values in the expected shape.
  122. */
  123. function _user_reference_view_settings_validate($element, &$form_state, $form) {
  124. // Split view name and display name from the 'view_and_display' value.
  125. if (!empty($element['view_and_display']['#value'])) {
  126. list($view, $display) = explode(':', $element['view_and_display']['#value']);
  127. }
  128. else {
  129. $view = '';
  130. $display = '';
  131. }
  132. // Explode the 'args' string into an actual array. Beware, explode() turns an
  133. // empty string into an array with one empty string. We'll need an empty array
  134. // instead.
  135. $args_string = trim($element['args']['#value']);
  136. $args = ($args_string === '') ? array() : array_map('trim', explode(',', $args_string));
  137. $value = array('view_name' => $view, 'display_name' => $display, 'args' => $args);
  138. form_set_value($element, $value, $form_state);
  139. }
  140. /**
  141. * Implements hook_field_validate().
  142. *
  143. * Possible error codes:
  144. * - 'invalid_uid': uid is not valid for the field (not a valid user id, or the user is not referenceable).
  145. */
  146. function user_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  147. // Extract uids to check.
  148. $ids = array();
  149. // First check non-numeric uid's to avoid losing time with them.
  150. foreach ($items as $delta => $item) {
  151. if (is_array($item) && !empty($item['uid'])) {
  152. if (is_numeric($item['uid'])) {
  153. $ids[] = $item['uid'];
  154. }
  155. else {
  156. $errors[$field['field_name']][$langcode][$delta][] = array(
  157. 'error' => 'invalid_uid',
  158. 'message' => t('%name: invalid input.',
  159. array('%name' => $instance['label'])),
  160. );
  161. }
  162. }
  163. }
  164. // Prevent performance hog if there are no ids to check.
  165. if ($ids) {
  166. $options = array(
  167. 'ids' => $ids,
  168. );
  169. $refs = user_reference_potential_references($field, $options);
  170. foreach ($items as $delta => $item) {
  171. if (is_array($item)) {
  172. if (!empty($item['uid']) && !isset($refs[$item['uid']])) {
  173. $errors[$field['field_name']][$langcode][$delta][] = array(
  174. 'error' => 'invalid_uid',
  175. 'message' => t("%name: this user can't be referenced.",
  176. array('%name' => $instance['label'])),
  177. );
  178. }
  179. }
  180. }
  181. }
  182. }
  183. /**
  184. * Implements hook_field_prepare_view().
  185. */
  186. function user_reference_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
  187. $checked_ids = &drupal_static(__FUNCTION__, array());
  188. // Set an 'access' property on each item (TRUE if the user exists).
  189. // Extract ids to check.
  190. $ids = array();
  191. foreach ($items as $id => $entity_items) {
  192. foreach ($entity_items as $delta => $item) {
  193. if (is_array($item)) {
  194. // Default to 'not accessible'.
  195. $items[$id][$delta]['access'] = FALSE;
  196. if (!empty($item['uid']) && is_numeric($item['uid'])) {
  197. $ids[$item['uid']] = $item['uid'];
  198. }
  199. }
  200. }
  201. }
  202. if ($ids) {
  203. // Load information about ids that we haven't already loaded during this
  204. // page request.
  205. $ids_to_check = array_diff($ids, array_keys($checked_ids));
  206. if (!empty($ids_to_check)) {
  207. $query = db_select('users', 'u')
  208. ->addMetaData('id', 'user_reference_field_prepare_view')
  209. ->addMetaData('field', $field)
  210. ->fields('u', array('uid'))
  211. ->condition('u.uid', $ids_to_check, 'IN');
  212. $accessible_ids = $query->execute()->fetchAllAssoc('uid');
  213. // Populate our static list so that we do not query on those ids again.
  214. foreach ($ids_to_check as $id) {
  215. $checked_ids[$id] = isset($accessible_ids[$id]);
  216. }
  217. }
  218. foreach ($items as $id => $entity_items) {
  219. foreach ($entity_items as $delta => $item) {
  220. if (is_array($item) && !empty($item['uid']) && !empty($checked_ids[$item['uid']])) {
  221. $items[$id][$delta]['access'] = TRUE;
  222. }
  223. }
  224. }
  225. }
  226. }
  227. /**
  228. * Implements hook_field_is_empty().
  229. */
  230. function user_reference_field_is_empty($item, $field) {
  231. return empty($item['uid']);
  232. }
  233. /**
  234. * Implements hook_field_formatter_info().
  235. */
  236. function user_reference_field_formatter_info() {
  237. return array(
  238. 'user_reference_default' => array(
  239. 'label' => t('Default'),
  240. 'description' => t("Display the name of the referenced user as a link to the user's profile page."),
  241. 'field types' => array('user_reference'),
  242. ),
  243. 'user_reference_plain' => array(
  244. 'label' => t('Plain text'),
  245. 'description' => t('Display the name of the referenced user as plain text.'),
  246. 'field types' => array('user_reference'),
  247. ),
  248. 'user_reference_user' => array(
  249. 'label' => t('Rendered user'),
  250. 'description' => t('Display the referenced user in a specific view mode'),
  251. 'field types' => array('user_reference'),
  252. 'settings' => array('user_reference_view_mode' => 'full'),
  253. ),
  254. 'user_reference_uid' => array(
  255. 'label' => t('User ID'),
  256. 'description' => t('Display the referenced user ID'),
  257. 'field types' => array('user_reference'),
  258. ),
  259. 'user_reference_path' => array(
  260. 'label' => t('URL as plain text'),
  261. 'description' => t('Display the URL of the referenced user'),
  262. 'field types' => array('user_reference'),
  263. 'settings' => array(
  264. 'alias' => TRUE,
  265. 'absolute' => FALSE
  266. ),
  267. ),
  268. );
  269. }
  270. /**
  271. * Implements hook_field_formatter_settings_form().
  272. */
  273. function user_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  274. $display = $instance['display'][$view_mode];
  275. $settings = $display['settings'];
  276. $element = array();
  277. switch ($display['type']) {
  278. case 'user_reference_user':
  279. $entity_info = entity_get_info('user');
  280. $modes = $entity_info['view modes'];
  281. $options = array();
  282. foreach ($modes as $name => $mode) {
  283. $options[$name] = $mode['label'];
  284. }
  285. $element['user_reference_view_mode'] = array(
  286. '#title' => t('View mode'),
  287. '#type' => 'select',
  288. '#options' => $options,
  289. '#default_value' => $settings['user_reference_view_mode'],
  290. // Never empty, so no #empty_option
  291. );
  292. break;
  293. case 'user_reference_path':
  294. $element['alias'] = array(
  295. '#type' => 'checkbox',
  296. '#title' => t('Display the aliased path (if exists) instead of the system path'),
  297. '#default_value' => $settings['alias'],
  298. );
  299. $element['absolute'] = array(
  300. '#type' => 'checkbox',
  301. '#title' => t('Display an absolute URL'),
  302. '#default_value' => $settings['absolute'],
  303. );
  304. break;
  305. }
  306. return $element;
  307. }
  308. /**
  309. * Implements hook_field_formatter_settings_summary().
  310. */
  311. function user_reference_field_formatter_settings_summary($field, $instance, $view_mode) {
  312. $display = $instance['display'][$view_mode];
  313. $settings = $display['settings'];
  314. $summary = array();
  315. switch ($display['type']) {
  316. case 'user_reference_user':
  317. $entity_info = entity_get_info('user');
  318. $modes = $entity_info['view modes'];
  319. $mode = $modes[$settings['user_reference_view_mode']]['label'];
  320. $summary[] = t('View mode: %mode', array('%mode' => $mode));
  321. break;
  322. case 'user_reference_path':
  323. $summary[] = t('Aliased path: %yes_no', array('%yes_no' => $settings['alias'] ? t('Yes') : t('No')));
  324. $summary[] = t('Absolute URL: %yes_no', array('%yes_no' => $settings['absolute'] ? t('Yes') : t('No')));
  325. break;
  326. }
  327. return implode('<br />', $summary);
  328. }
  329. /**
  330. * Implements hook_field_formatter_prepare_view().
  331. *
  332. * Preload all user referenced by items using 'full entity' formatters.
  333. */
  334. function user_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  335. // Load the referenced users, except for the 'user_reference_uid' which does
  336. // not need full objects.
  337. // Collect ids to load.
  338. $ids = array();
  339. foreach ($displays as $id => $display) {
  340. if ($display['type'] != 'user_reference_uid') {
  341. foreach ($items[$id] as $delta => $item) {
  342. if ($item['access']) {
  343. $ids[$item['uid']] = $item['uid'];
  344. }
  345. }
  346. }
  347. }
  348. $entities = user_load_multiple($ids);
  349. // Add the loaded user objects to the items.
  350. foreach ($displays as $id => $display) {
  351. if ($display['type'] != 'user_reference_uid') {
  352. foreach ($items[$id] as $delta => $item) {
  353. if ($item['access']) {
  354. $items[$id][$delta]['user'] = $entities[$item['uid']];
  355. }
  356. }
  357. }
  358. }
  359. }
  360. /**
  361. * Implements hook_field_formatter_view().
  362. */
  363. function user_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  364. $settings = $display['settings'];
  365. $result = array();
  366. // @todo Optimisation: use hook_field_formatter_prepare_view() to load
  367. // user names or full user entities in 'multiple' mode.
  368. // Collect the list of user ids.
  369. $uids = array();
  370. foreach ($items as $delta => $item) {
  371. $uids[$item['uid']] = $item['uid'];
  372. }
  373. switch ($display['type']) {
  374. case 'user_reference_default':
  375. case 'user_reference_plain':
  376. foreach ($items as $delta => $item) {
  377. if ($item['access']) {
  378. $user = $item['user'];
  379. $label = entity_label('user', $user);
  380. if ($display['type'] == 'user_reference_default') {
  381. $uri = entity_uri('user', $user);
  382. $result[$delta] = array(
  383. '#type' => 'link',
  384. '#title' => $label,
  385. '#href' => $uri['path'],
  386. '#options' => $uri['options'],
  387. );
  388. }
  389. else {
  390. $result[$delta] = array(
  391. '#markup' => check_plain($label),
  392. );
  393. }
  394. }
  395. }
  396. break;
  397. case 'user_reference_user':
  398. $view_mode = $display['settings']['user_reference_view_mode'];
  399. // To prevent infinite recursion caused by reference cycles, we store
  400. // diplayed accounts in a recursion queue.
  401. $recursion_queue = &drupal_static(__FUNCTION__, array());
  402. // If no 'referencing entity' is set, we are starting a new 'reference
  403. // thread' and need to reset the queue.
  404. // @todo Bug: $entity->referencing_entity on accounts referenced in a different
  405. // thread on the page. E.g: 1 references 1+2 / 2 references 1+2 / visit homepage.
  406. // We'd need a more accurate way...
  407. if (!isset($entity->referencing_entity)) {
  408. $recursion_queue = array();
  409. }
  410. // The recursion queue only needs to track nodes.
  411. if ($entity_type == 'user') {
  412. list($id) = entity_extract_ids($entity_type, $entity);
  413. $recursion_queue[$id] = $id;
  414. }
  415. // Check the recursion queue to determine which accounts should be fully
  416. // displayed, and which accounts will only be displayed as a username.
  417. $users_display = array();
  418. foreach ($items as $delta => $item) {
  419. if ($item['access'] && !isset($recursion_queue[$item['uid']])) {
  420. $users_display[$item['uid']] = $item['user'];
  421. }
  422. }
  423. // Load and build the fully displayed nodes.
  424. if ($users_display) {
  425. $users_built = array('users' => array('#sorted' => TRUE));
  426. foreach ($users_display as $uid => $account) {
  427. $users_display[$uid]->referencing_entity = $entity;
  428. $users_display[$uid]->referencing_field = $field['field_name'];
  429. $users_built['users'][$account->uid] = user_view($account, $settings['user_reference_view_mode']);
  430. }
  431. }
  432. // Assemble the render array.
  433. foreach ($items as $delta => $item) {
  434. if ($item['access']) {
  435. if (isset($users_display[$item['uid']])) {
  436. $result[$delta] = $users_built['users'][$item['uid']];
  437. }
  438. else {
  439. $account = $item['user'];
  440. $label = entity_label('user', $user);
  441. $uri = entity_uri('user', $account);
  442. $result[$delta] = array(
  443. '#type' => 'link',
  444. '#title' => $label,
  445. '#href' => $uri['path'],
  446. '#options' => $uri['options'],
  447. );
  448. if (!$account->status) {
  449. $result[$delta]['#prefix'] = '<span class="user-unpublished">';
  450. $result[$delta]['#suffix'] = '</span>';
  451. }
  452. }
  453. }
  454. }
  455. break;
  456. case 'user_reference_uid':
  457. foreach ($items as $delta => $item) {
  458. if ($item['access']) {
  459. $result[$delta] = array(
  460. '#markup' => $item['uid'],
  461. );
  462. }
  463. }
  464. break;
  465. case 'user_reference_path':
  466. foreach ($items as $delta => $item) {
  467. if ($item['access']) {
  468. $uri = entity_uri('user', $item['user']);
  469. $options = array(
  470. 'absolute' => $settings['absolute'],
  471. 'alias' => !$settings['alias'],
  472. );
  473. $options += $uri['options'];
  474. $result[$delta] = array(
  475. '#markup' => url($uri['path'], $options),
  476. );
  477. }
  478. }
  479. break;
  480. }
  481. return $result;
  482. }
  483. /**
  484. * Helper function for widgets and formatters.
  485. *
  486. * Store user names collected in the curent request.
  487. */
  488. function _user_reference_get_user_names($uids, $known_titles = array()) {
  489. $titles = &drupal_static(__FUNCTION__, array());
  490. // Save titles we receive.
  491. $titles += $known_titles;
  492. // Collect nids to retrieve from database.
  493. $uids_query = array();
  494. foreach ($uids as $uid) {
  495. if (!isset($titles[$uid])) {
  496. $uids_query[] = $uid;
  497. }
  498. }
  499. if ($uids_query) {
  500. $query = db_select('users', 'u')
  501. ->fields('u', array('uid', 'name'))
  502. ->condition('u.uid', $uids);
  503. $titles += $query->execute()->fetchAllKeyed();
  504. }
  505. // Build the results array.
  506. $return = array();
  507. foreach ($uids as $uid) {
  508. $return[$uid] = isset($titles[$uid]) ? $titles[$uid] : '';
  509. }
  510. return $return;
  511. }
  512. /**
  513. * Implements hook_field_widget_info().
  514. */
  515. function user_reference_field_widget_info() {
  516. return array(
  517. 'user_reference_autocomplete' => array(
  518. 'label' => t('Autocomplete text field'),
  519. 'description' => t('Display the list of referenceable users as a textfield with autocomplete behaviour.'),
  520. 'field types' => array('user_reference'),
  521. 'settings' => array(
  522. 'autocomplete_match' => 'contains',
  523. 'size' => 60,
  524. 'autocomplete_path' => 'user_reference/autocomplete',
  525. ),
  526. ),
  527. );
  528. }
  529. /**
  530. * Implements hook_field_widget_info_alter().
  531. */
  532. function user_reference_field_widget_info_alter(&$info) {
  533. $info['options_select']['field types'][] = 'user_reference';
  534. $info['options_buttons']['field types'][] = 'user_reference';
  535. }
  536. /**
  537. * Implements hook_field_widget_settings_form().
  538. */
  539. function user_reference_field_widget_settings_form($field, $instance) {
  540. $widget = $instance['widget'];
  541. $defaults = field_info_widget_settings($widget['type']);
  542. $settings = array_merge($defaults, $widget['settings']);
  543. $form = array();
  544. if ($widget['type'] == 'user_reference_autocomplete') {
  545. $form['autocomplete_match'] = array(
  546. '#type' => 'select',
  547. '#title' => t('Autocomplete matching'),
  548. '#default_value' => $settings['autocomplete_match'],
  549. '#options' => array(
  550. 'starts_with' => t('Starts with'),
  551. 'contains' => t('Contains'),
  552. ),
  553. '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of users.'),
  554. );
  555. $form['size'] = array(
  556. '#type' => 'textfield',
  557. '#title' => t('Size of textfield'),
  558. '#default_value' => $settings['size'],
  559. '#element_validate' => array('_element_validate_integer_positive'),
  560. '#required' => TRUE,
  561. );
  562. }
  563. return $form;
  564. }
  565. /**
  566. * Implements hook_field_widget_form().
  567. */
  568. function user_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  569. switch ($instance['widget']['type']) {
  570. case 'user_reference_autocomplete':
  571. $element += array(
  572. '#type' => 'textfield',
  573. '#default_value' => isset($items[$delta]['uid']) ? $items[$delta]['uid'] : NULL,
  574. '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'],
  575. '#size' => $instance['widget']['settings']['size'],
  576. '#element_validate' => array('user_reference_autocomplete_validate'),
  577. '#value_callback' => 'user_reference_autocomplete_value',
  578. );
  579. break;
  580. }
  581. return array('uid' => $element);
  582. }
  583. /**
  584. * Value callback for a user_reference autocomplete element.
  585. *
  586. * Substitute in the user name for the uid.
  587. */
  588. function user_reference_autocomplete_value($element, $input = FALSE, $form_state) {
  589. if ($input === FALSE) {
  590. // We're building the displayed 'default value': expand the raw uid into
  591. // "user name [uid:n]".
  592. $uid = $element['#default_value'];
  593. if (!empty($uid)) {
  594. $q = db_select('users', 'u');
  595. $q->addField('u', 'name');
  596. $q->condition('u.uid', $uid)
  597. ->range(0, 1);
  598. $result = $q->execute();
  599. // @todo If no result (user doesn't exist).
  600. $value = $result->fetchField();
  601. $value .= ' [uid:' . $uid . ']';
  602. return $value;
  603. }
  604. }
  605. }
  606. /**
  607. * Validation callback for a user_reference autocomplete element.
  608. */
  609. function user_reference_autocomplete_validate($element, &$form_state, $form) {
  610. $field = field_widget_field($element, $form_state);
  611. $instance = field_widget_instance($element, $form_state);
  612. $value = $element['#value'];
  613. $uid = NULL;
  614. if (!empty($value)) {
  615. // Check whether we have an explicit "[uid:n]" input.
  616. preg_match('/^(?:\s*|(.*) )?\[\s*uid\s*:\s*(\d+)\s*\]$/', $value, $matches);
  617. if (!empty($matches)) {
  618. // Explicit uid. Check that the 'name' part matches the actual name for
  619. // the uid.
  620. list(, $name, $uid) = $matches;
  621. if (!empty($name)) {
  622. $names = _user_reference_get_user_names(array($uid));
  623. if ($name != $names[$uid]) {
  624. form_error($element, t('%name: name mismatch. Please check your selection.', array('%name' => $instance['label'])));
  625. }
  626. }
  627. }
  628. else {
  629. // No explicit uid (the submitted value was not populated by autocomplete
  630. // selection). Get the uid of a referencable user from the entered name.
  631. $options = array(
  632. 'string' => $value,
  633. 'match' => 'equals',
  634. 'limit' => 1,
  635. );
  636. $references = user_reference_potential_references($field, $options);
  637. if ($references) {
  638. // @todo The best thing would be to present the user with an
  639. // additional form, allowing the user to choose between valid
  640. // candidates with the same name. ATM, we pick the first
  641. // matching candidate...
  642. $uid = key($references);
  643. }
  644. else {
  645. form_error($element, t('%name: found no valid user with that name.', array('%name' => $instance['label'])));
  646. }
  647. }
  648. }
  649. // Set the element's value as the user id that was extracted from the entered
  650. // input.
  651. form_set_value($element, $uid, $form_state);
  652. }
  653. /**
  654. * Implements hook_field_widget_error().
  655. */
  656. function user_reference_field_widget_error($element, $error, $form, &$form_state) {
  657. form_error($element['uid'], $error['message']);
  658. }
  659. /**
  660. * Builds a list of referenceable users suitable for the '#option' FAPI property.
  661. *
  662. * Warning: the function does NOT take care of encoding or escaping the user
  663. * names. Proper massaging needs to be performed by the caller, according to
  664. * the destination FAPI '#type' (radios / checkboxes / select).
  665. *
  666. * @param $field
  667. * The field definition.
  668. *
  669. * @return
  670. * An array of referenceable user names, keyed by user id.
  671. */
  672. function _user_reference_options($field, $flat = TRUE) {
  673. $references = user_reference_potential_references($field);
  674. $options = array();
  675. foreach ($references as $key => $value) {
  676. // The label, displayed in selects and checkboxes/radios, should have HTML
  677. // entities unencoded. The widgets (core's options.module) take care of
  678. // applying the relevant filters (strip_tags() or filter_xss()).
  679. $label = html_entity_decode($value['rendered'], ENT_QUOTES);
  680. if (empty($value['group']) || $flat) {
  681. $options[$key] = $label;
  682. }
  683. else {
  684. // The group name, displayed in selects, cannot contain tags, and should
  685. // have HTML entities unencoded.
  686. $group = html_entity_decode(strip_tags($value['group']), ENT_QUOTES);
  687. $options[$group][$key] = $label;
  688. }
  689. }
  690. return $options;
  691. }
  692. /**
  693. * Retrieves an array of candidate referenceable users.
  694. *
  695. * This info is used in various places (aloowed values, autocomplete results,
  696. * input validation...). Some of them only need the uids, others nid + names,
  697. * others yet uid + names + rendered row (for display in widgets).
  698. * The array we return contains all the potentially needed information, and lets
  699. * consumers use the parts they actually need.
  700. *
  701. * @param $field
  702. * The field definition.
  703. * @param $options
  704. * An array of options to limit the scope of the returned list. The following
  705. * key/value pairs are accepted:
  706. * - string: string to filter titles on (used by autocomplete).
  707. * - match: operator to match the above string against, can be any of:
  708. * 'contains', 'equals', 'starts_with'. Defaults to 'contains'.
  709. * - ids: array of specific node ids to lookup.
  710. * - limit: maximum size of the the result set. Defaults to 0 (no limit).
  711. *
  712. * @return
  713. * An array of valid users in the form:
  714. * array(
  715. * uid => array(
  716. * 'title' => The user name,
  717. * 'rendered' => The text to display in widgets (can be HTML)
  718. * ),
  719. * ...
  720. * )
  721. */
  722. function user_reference_potential_references($field, $options = array()) {
  723. // Fill in default options.
  724. $options += array(
  725. 'string' => '',
  726. 'match' => 'contains',
  727. 'ids' => array(),
  728. 'limit' => 0,
  729. );
  730. $results = &drupal_static(__FUNCTION__, array());
  731. // Create unique id for static cache.
  732. $cid = $field['field_name'] . ':' . $options['match'] . ':'
  733. . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids']))
  734. . ':' . $options['limit'];
  735. if (!isset($results[$cid])) {
  736. $references = FALSE;
  737. if (module_exists('views') && !empty($field['settings']['view']['view_name'])) {
  738. $references = _user_reference_potential_references_views($field, $options);
  739. }
  740. if ($references === FALSE) {
  741. $references = _user_reference_potential_references_standard($field, $options);
  742. }
  743. // Store the results.
  744. $results[$cid] = !empty($references) ? $references : array();
  745. }
  746. return $results[$cid];
  747. }
  748. /**
  749. * Helper function for user_reference_potential_references().
  750. *
  751. * Case of Views-defined referenceable users.
  752. */
  753. function _user_reference_potential_references_views($field, $options) {
  754. $settings = $field['settings']['view'];
  755. $options['title_field'] = 'name';
  756. return references_potential_references_view('user', $settings['view_name'], $settings['display_name'], $settings['args'], $options);
  757. }
  758. /**
  759. * Helper function for user_reference_potential_references().
  760. *
  761. * List of referenceable users defined by user role and status.
  762. */
  763. function _user_reference_potential_references_standard($field, $options) {
  764. // Avoid useless work.
  765. $filter_roles = array_filter($field['settings']['referenceable_roles']);
  766. $filter_status = array_filter($field['settings']['referenceable_status']);
  767. if (!count($filter_status) && !count($filter_roles)) {
  768. return array();
  769. }
  770. $query = db_select('users', 'u')
  771. // Select the whole record, so that format_username() has enough
  772. // information.
  773. ->fields('u')
  774. ->addMetaData('id', ' _user_reference_potential_references_standard')
  775. ->addMetaData('field', $field)
  776. ->addMetaData('options', $options);
  777. // Enable this filter only if any statuses checked (and not both).
  778. if (count($filter_status) == 1) {
  779. $query->condition('u.status', array_keys($filter_status), 'IN');
  780. }
  781. // Skip filter when "authenticated user" choosen.
  782. if ($filter_roles && !isset($filter_roles[DRUPAL_AUTHENTICATED_RID])) {
  783. $query->join('users_roles', 'r', 'u.uid = r.uid');
  784. $query->condition('r.rid', array_keys($filter_roles), 'IN');
  785. }
  786. if ($options['string'] !== '') {
  787. switch ($options['match']) {
  788. case 'contains':
  789. $query->condition('u.name', '%' . $options['string'] . '%', 'LIKE');
  790. break;
  791. case 'starts_with':
  792. $query->condition('u.name', $options['string'] . '%', 'LIKE');
  793. break;
  794. case 'equals':
  795. default: // no match type or incorrect match type: use "="
  796. $query->condition('u.name', $options['string'], '=');
  797. break;
  798. }
  799. }
  800. if ($options['ids']) {
  801. $query->condition('u.uid', $options['ids'], 'IN');
  802. }
  803. // Explicitly exclude the anonymous user.
  804. $query->condition('u.uid', 0, '<>');
  805. if ($options['limit']) {
  806. $query->range(0, $options['limit']);
  807. }
  808. $query->orderBy('u.name');
  809. $result = $query->execute()->fetchAll();
  810. $references = array();
  811. foreach ($result as $account) {
  812. $references[$account->uid] = array(
  813. 'title' => $account->name,
  814. 'rendered' => check_plain(format_username($account)),
  815. );
  816. }
  817. return $references;
  818. }
  819. /**
  820. * Menu callback; Retrieve a pipe delimited string of autocomplete suggestions for existing users
  821. */
  822. function user_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
  823. $instance = field_info_instance($entity_type, $field_name, $bundle);
  824. $field = field_info_field($field_name);
  825. $options = array(
  826. 'string' => $string,
  827. 'match' => $instance['widget']['settings']['autocomplete_match'],
  828. 'limit' => 10,
  829. );
  830. $references = user_reference_potential_references($field, $options);
  831. $matches = array();
  832. foreach ($references as $id => $row) {
  833. // Markup is fine in autocompletion results (might happen when rendered
  834. // through Views) but we want to remove hyperlinks.
  835. $suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\/a>/', '$2', $row['rendered']);
  836. // Remove link tags Add a class wrapper for a few required CSS overrides.
  837. $matches[$row['title'] . " [uid:$id]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>';
  838. }
  839. drupal_json_output($matches);
  840. }
  841. /**
  842. * Implements hook_options_list().
  843. */
  844. function user_reference_options_list($field) {
  845. return _user_reference_options($field, FALSE);
  846. }
  847. /**
  848. * Implementation of hook_user_load().
  849. */
  850. /*function user_reference_user_load($accounts) {
  851. // Only add links if we are on the user 'view' page.
  852. if (arg(0) != 'user' || arg(2)) {
  853. return;
  854. }
  855. foreach ($accounts as $uid => $account) {
  856. // find CCK user_reference field tables
  857. // search through them for matching user ids and load those nodes
  858. $additions = array();
  859. $fields = field_info_instances('user');
  860. // TODO : replace with field_attach_query() + synchronize with latest D6 code.
  861. // Find the table and columns to search through, if the same
  862. // table comes up in more than one field type, we only need
  863. // to search it once.
  864. $search_tables = array();
  865. $search_links = array();
  866. foreach ($fields as $field) {
  867. if ($field['type'] == 'user_reference' && !empty($field['widget']['reverse_link'])) {
  868. $db_info = content_database_info($field);
  869. $search_tables[$db_info['table']] = $db_info['columns']['uid']['column'];
  870. $search_links[$db_info['table']] = $field['widget']['reverse_link'];
  871. }
  872. }
  873. foreach ($search_tables as $table => $column) {
  874. $ids = db_query(db_rewrite_sql("SELECT DISTINCT(n.nid) FROM {node} n LEFT JOIN {". $table ."} f ON n.vid = f.vid WHERE f.". $column ."=". $account->uid. " AND n.status = 1"));
  875. while ($data = db_fetch_object($ids)) {
  876. // TODO, do we really want a complete node_load() here? We only need the title to create a link.
  877. $node = node_load($data->nid);
  878. $node->reverse_link = $search_links[$table];
  879. $additions[$node->type][] = $node;
  880. }
  881. }
  882. $accounts[$uid]->user_reference = $additions;
  883. }
  884. return;
  885. }*/
  886. /**
  887. * Implementation of hook_user_view().
  888. */
  889. /*function user_reference_user_view($account, $view_mode, $langcode) {
  890. if (!empty($account->user_reference)) {
  891. $node_types = content_types();
  892. $additions = array();
  893. $values = array();
  894. foreach ($account->user_reference as $node_type => $nodes) {
  895. foreach ($nodes as $node) {
  896. if ($node->reverse_link) {
  897. $values[$node_type][] = l($node->title, 'node/' . $node->nid);
  898. }
  899. }
  900. if (isset($values[$node_type])) {
  901. $additions[] = array(
  902. '#type' => 'user_profile_item',
  903. '#title' => check_plain($node_types[$node_type]['name']),
  904. '#value' => theme('item_list', $values[$node_type]),
  905. );
  906. }
  907. }
  908. if ($additions) {
  909. $account->content['user_reference'] = $additions + array(
  910. '#type' => 'user_profile_category',
  911. '#attributes' => array('class' => array('user-member')),
  912. '#title' => t('Related content'),
  913. '#weight' => 10,
  914. );
  915. }
  916. }
  917. }*/
  918. /**
  919. * Implements hook_content_migrate_field_alter().
  920. *
  921. * Use this to tweak the conversion of field settings
  922. * from the D6 style to the D7 style for specific
  923. * situations not handled by basic conversion,
  924. * as when field types or settings are changed.
  925. *
  926. * $field_value['widget_type'] is available to
  927. * see what widget type was originally used.
  928. */
  929. function user_reference_content_migrate_field_alter(&$field_value, $instance_value) {
  930. switch ($field_value['module']) {
  931. case 'userreference':
  932. $field_value['module'] = 'user_reference';
  933. $field_value['type'] = 'user_reference';
  934. // Translate 'view' settings.
  935. $view_name = isset($field_value['settings']['advanced_view']) ? $field_value['settings']['advanced_view'] : '';
  936. $view_args = isset($field_value['settings']['advanced_view_args']) ? $field_value['settings']['advanced_view_args'] : '';
  937. $view_args = array_map('trim', explode(',', $view_args));
  938. $field_value['settings']['view'] = array(
  939. 'view_name' => $view_name,
  940. 'display_name' => 'default',
  941. 'args' => $view_args,
  942. );
  943. if ($view_name) {
  944. $field_value['messages'][] = t("The field uses the view @view_name to determine referenceable users. You will need to manually edit the view and add a display of type 'References'.", array('@view_name' => $view_name));
  945. }
  946. unset($field_value['settings']['advanced_view']);
  947. unset($field_value['settings']['advanced_view_args']);
  948. break;
  949. }
  950. }
  951. /**
  952. * Implements hook_content_migrate_instance_alter().
  953. *
  954. * Use this to tweak the conversion of instance or widget settings
  955. * from the D6 style to the D7 style for specific
  956. * situations not handled by basic conversion, as when
  957. * formatter or widget names or settings are changed.
  958. */
  959. function user_reference_content_migrate_instance_alter(&$instance_value, $field_value) {
  960. // The module name for the instance was corrected
  961. // by the change in user_reference_content_migrate_field_alter().
  962. switch ($field_value['type']) {
  963. case 'userreference':
  964. // The formatter names changed, all are prefixed
  965. // with 'user_reference_'.
  966. foreach ($instance_value['display'] as $context => $settings) {
  967. $instance_value['display'][$context]['type'] = 'user_reference_' . $settings['type'];
  968. }
  969. // Massage the widget.
  970. switch ($instance_value['widget']['type']) {
  971. case 'userreference_autocomplete':
  972. $instance_value['widget']['type'] = 'user_reference_autocomplete';
  973. $instance_value['widget']['module'] = 'user_reference';
  974. break;
  975. case 'userreference_select':
  976. $instance_value['widget']['type'] = 'options_select';
  977. $instance_value['widget']['module'] = 'options';
  978. break;
  979. case 'userreference_buttons':
  980. $instance_value['widget']['type'] = 'options_buttons';
  981. $instance_value['widget']['module'] = 'options';
  982. }
  983. break;
  984. }
  985. }
  986. /**
  987. * Implements hook_field_views_data().
  988. *
  989. * In addition to the default field information we add the relationship for
  990. * views to connect back to the users table.
  991. */
  992. function user_reference_field_views_data($field) {
  993. // No module_load_include(): this hook is invoked from
  994. // views/modules/field.views.inc, which is where that function is defined.
  995. $data = field_views_field_default_views_data($field);
  996. $storage = $field['storage']['details']['sql'];
  997. foreach ($storage as $age => $table_data) {
  998. $table = key($table_data);
  999. $columns = current($table_data);
  1000. $id_column = $columns['uid'];
  1001. if (isset($data[$table])) {
  1002. // Filter: swap the handler to the 'in' operator. The callback receives
  1003. // the field name instead of the whole $field structure to keep views
  1004. // data to a reasonable size.
  1005. $data[$table][$id_column]['filter']['handler'] = 'views_handler_filter_in_operator';
  1006. $data[$table][$id_column]['filter']['options callback'] = 'user_reference_views_filter_options';
  1007. $data[$table][$id_column]['filter']['options arguments'] = array($field['field_name']);
  1008. // Argument: display users.name in argument titles (handled in our custom
  1009. // handler) and summary lists (handled by the base views_handler_argument
  1010. // handler).
  1011. // Both mechanisms rely on the 'name table' and 'name field' information
  1012. // below, by joining to a separate copy of the base table from the field
  1013. // data table.
  1014. $data[$table][$id_column]['argument']['handler'] = 'references_handler_argument';
  1015. $data[$table][$id_column]['argument']['name table'] = $table . '_reference';
  1016. $data[$table][$id_column]['argument']['name field'] = 'name';
  1017. $data[$table . '_reference']['table']['join'][$table] = array(
  1018. 'left_field' => $id_column,
  1019. 'table' => 'users',
  1020. 'field' => 'uid',
  1021. );
  1022. // Relationship.
  1023. $data[$table][$id_column]['relationship'] = array(
  1024. 'handler' => 'references_handler_relationship',
  1025. 'base' => 'users',
  1026. 'base field' => 'uid',
  1027. 'field' => $id_column,
  1028. 'label' => $field['field_name'],
  1029. 'field_name' => $field['field_name'],
  1030. );
  1031. }
  1032. }
  1033. return $data;
  1034. }
  1035. /**
  1036. * Implements hook_field_views_data_views_data_alter().
  1037. */
  1038. function user_reference_field_views_data_views_data_alter(&$data, $field) {
  1039. foreach ($field['bundles'] as $entity_type => $bundles) {
  1040. $entity_info = entity_get_info($entity_type);
  1041. $pseudo_field_name = 'reverse_' . $field['field_name'] . '_' . $entity_type;
  1042. list($label, $all_labels) = field_views_field_label($field['field_name']);
  1043. $entity = $entity_info['label'];
  1044. if ($entity == t('Node')) {
  1045. $entity = t('Content');
  1046. }
  1047. // Only specify target entity type if the field is used in more than one.
  1048. if (count($field['bundles']) > 1) {
  1049. $title = t('@field (@field_name) - reverse (to @entity)', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
  1050. }
  1051. else {
  1052. $title = t('@field (@field_name) - reverse', array('@entity' => $entity, '@field' => $label, '@field_name' => $field['field_name']));
  1053. }
  1054. $data['users'][$pseudo_field_name]['relationship'] = array(
  1055. 'title' => $title,
  1056. 'help' => t('Relate each @entity referencing the user through @field.', array('@entity' => $entity, '@field' => $label)),
  1057. 'handler' => 'views_handler_relationship_entity_reverse',
  1058. 'field_name' => $field['field_name'],
  1059. 'field table' => _field_sql_storage_tablename($field),
  1060. 'field field' => $field['field_name'] . '_uid',
  1061. 'base' => $entity_info['base table'],
  1062. 'base field' => $entity_info['entity keys']['id'],
  1063. 'label' => t('!field_name', array('!field_name' => $field['field_name'])),
  1064. );
  1065. }
  1066. }
  1067. /**
  1068. * 'options callback' for the views_handler_filter_in_operator filter.
  1069. *
  1070. * @param $field_name
  1071. * The field name.
  1072. *
  1073. * @return
  1074. * The array of allowed options for the filter.
  1075. */
  1076. function user_reference_views_filter_options($field_name) {
  1077. $options = array();
  1078. if ($field = field_info_field($field_name)) {
  1079. $options = _user_reference_options($field, TRUE);
  1080. // The options are displayed in checkboxes within the filter admin form, and
  1081. // in a select within an exposed filter. Checkboxes accept HTML, other
  1082. // entities should be encoded; selects require the exact opposite: no HTML,
  1083. // no encoding. We go for a middle ground: strip tags, leave entities
  1084. // unencoded.
  1085. foreach ($options as $key => $value) {
  1086. $options[$key] = strip_tags($value);
  1087. }
  1088. }
  1089. return $options;
  1090. }