initial import from onelab svn codebase
[plewww.git] / modules / profile.module
1 <?php
2 // $Id: profile.module 144 2007-03-28 07:52:20Z thierry $
3
4 /**
5  * @file
6  * Support for configurable user profiles.
7  */
8
9 /**
10  * Flags to define the visibility of a profile field.
11  */
12 define('PROFILE_PRIVATE', 1);
13 define('PROFILE_PUBLIC', 2);
14 define('PROFILE_PUBLIC_LISTINGS', 3);
15 define('PROFILE_HIDDEN', 4);
16
17 /**
18  * Implementation of hook_help().
19  */
20 function profile_help($section) {
21   switch ($section) {
22     case 'admin/help#profile':
23       $output = '<p>'. t('The profile module allows you to define custom fields (such as country, real name, age, ...) in the user profile. This permits users of a site to share more information about themselves, and can help community-based sites to organize users around profile fields.') .'</p>';
24       $output .= t('<p>The following types of fields can be added to the user profile:</p>
25 <ul>
26 <li>single-line textfield</li>
27 <li>multi-line textfield</li>
28 <li>checkbox</li>
29 <li>list selection</li>
30 <li>freeform list</li>
31 <li>URL</li>
32 <li>date</li>
33 </ul>
34 ');
35       $output .= t('<p>You can</p>
36 <ul>
37 <li>view user <a href="%profile">profiles</a>.</li>
38 <li>administer profile settings: <a href="%admin-settings-profile">administer &gt;&gt; settings &gt;&gt; profiles</a>.</li>
39 </ul>
40 ', array('%profile' => url('profile'), '%admin-settings-profile' => url('admin/settings/profile')));
41       $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%profile">Profile page</a>.', array('%profile' => 'http://drupal.org/handbook/modules/profile/')) .'</p>';
42       return $output;
43     case 'admin/modules#description':
44       return t('Supports configurable user profiles.');
45     case 'admin/settings/profile':
46       return t('<p>Here you can define custom fields that users can fill in in their user profile (such as <em>country</em>, <em>real name</em>, <em>age</em>, ...).</p>');
47   }
48 }
49
50 /**
51  * Implementation of hook_menu().
52  */
53 function profile_menu($may_cache) {
54   $items = array();
55
56   if ($may_cache) {
57     $items[] = array('path' => 'profile',
58       'title' => t('user list'),
59       'callback' => 'profile_browse',
60       'access' => user_access('access user profiles'),
61       'type' => MENU_SUGGESTED_ITEM);
62     $items[] = array('path' => 'admin/settings/profile',
63       'title' => t('profiles'),
64       'callback' => 'profile_admin_overview');
65     $items[] = array('path' => 'admin/settings/profile/add',
66       'title' => t('add field'),
67       'callback' => 'profile_field_form',
68       'type' => MENU_CALLBACK);
69     $items[] = array('path' => 'admin/settings/profile/edit',
70       'title' => t('edit field'),
71       'callback' => 'profile_field_form',
72       'type' => MENU_CALLBACK);
73     $items[] = array('path' => 'admin/settings/profile/delete',
74       'title' => t('delete field'),
75       'callback' => 'profile_field_delete',
76       'type' => MENU_CALLBACK);
77   }
78
79   return $items;
80 }
81
82 /**
83  * Implementation of hook_block().
84  */
85 function profile_block($op = 'list', $delta = 0, $edit = array()) {
86
87   if ($op == 'list') {
88      $blocks[0]['info'] = t('Author information');
89
90      return $blocks;
91   }
92   else if ($op == 'configure' && $delta == 0) {
93     // Compile a list of fields to show
94     $fields = array();
95     $result = db_query('SELECT name, title, weight, visibility FROM {profile_fields} WHERE visibility IN (%d, %d) ORDER BY weight', PROFILE_PUBLIC, PROFILE_PUBLIC_LISTINGS);
96     while ($record = db_fetch_object($result)) {
97       $fields[$record->name] = $record->title;
98     }
99     $fields['user_profile'] = t('Link to full user profile');
100     $form['profile_block_author_fields'] = array('#type' => 'checkboxes',
101       '#title' => t('Profile fields to display'),
102       '#default_value' => variable_get('profile_block_author_fields', NULL),
103       '#options' => $fields,
104       '#description' => t('Select which profile fields you wish to display in the block. Only fields designated as public in the <a href="%profile-admin">profile field configuration</a> are available.', array('%profile-admin' => url('admin/settings/profile'))),
105     );
106     return $form;
107   }
108   else if ($op == 'save' && $delta == 0) {
109     variable_set('profile_block_author_fields', $edit['profile_block_author_fields']);
110   }
111   else if ($op == 'view') {
112     if (user_access('access user profiles')) {
113       if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
114         $node = node_load(arg(1));
115         $account = user_load(array('uid' => $node->uid));
116
117         if ($use_fields = variable_get('profile_block_author_fields', array())) {
118           // Compile a list of fields to show.
119           $fields = array();
120           $result = db_query('SELECT name, title, type, visibility, weight FROM {profile_fields} WHERE visibility IN (%d, %d) ORDER BY weight', PROFILE_PUBLIC, PROFILE_PUBLIC_LISTINGS);
121           while ($record = db_fetch_object($result)) {
122             // Ensure that field is displayed only if it is among the defined block fields and, if it is private, the user has appropriate permissions.
123             if (isset($use_fields[$record->name]) && $use_fields[$record->name]) {
124               $fields[] = $record;
125             }
126           }
127         }
128
129         if ($fields) {
130           $fields = _profile_update_user_fields($fields, $account);
131           $output .= theme('profile_block', $account, $fields, true);
132         }
133
134         if (isset($use_fields['user_profile']) && $use_fields['user_profile']) {
135           $output .= '<div>' . l(t('View full user profile'), 'user/' . $account->uid) . '</div>';
136         }
137       }
138
139       if ($output) {
140          $block['subject'] = t('About %name', array('%name' => $account->name));
141          $block['content'] = $output;
142          return $block;
143       }
144     }
145   }
146 }
147
148 /**
149  * Implementation of hook_user().
150  */
151 function profile_user($type, &$edit, &$user, $category = NULL) {
152   switch ($type) {
153     case 'load':
154       return profile_load_profile($user);
155     case 'register':
156       return profile_form_profile($edit, $user, $category);
157     case 'update':
158     case 'insert':
159       return profile_save_profile($edit, $user, $category);
160     case 'view':
161       return profile_view_profile($user);
162     case 'form':
163       return profile_form_profile($edit, $user, $category);
164     case 'validate':
165       return profile_validate_profile($edit, $category);
166     case 'categories':
167       return profile_categories();
168     case 'delete':
169       db_query('DELETE FROM {profile_values} WHERE uid = %d', $user->uid);
170   }
171 }
172
173 /**
174  * Menu callback: Generate a form to add/edit a user profile field.
175  */
176 function profile_field_form($arg = NULL) {
177   if (arg(3) == 'edit') {
178     if (is_numeric($arg)) {
179       $fid = $arg;
180
181       $edit = db_fetch_array(db_query('SELECT * FROM {profile_fields} WHERE fid = %d', $fid));
182
183       if (!$edit) {
184         drupal_not_found();
185         return;
186       }
187       drupal_set_title(t('edit %title', array('%title' => $edit['title'])));
188       $form['fid'] = array('#type' => 'value',
189         '#value' => $fid,
190       );
191       $type = $edit['type'];
192     }
193     else {
194       drupal_not_found();
195       return;
196     }
197   }
198   else {
199     $types = _profile_field_types();
200     if (!isset($types[$arg])) {
201       drupal_not_found();
202       return;
203     }
204     $type = $arg;
205     drupal_set_title(t('add new %type', array('%type' => $types[$type])));
206     $edit = array('name' => 'profile_');
207     $form['type'] = array('#type' => 'value', '#value' => $type);
208   }
209   $form['fields'] = array('#type' => 'fieldset',
210     '#title' => t('Field settings'),
211   );
212   $form['fields']['category'] = array('#type' => 'textfield',
213     '#title' => t('Category'),
214     '#default_value' => $edit['category'],
215     '#description' => t('The category the new field should be part of. Categories are used to group fields logically. An example category is "Personal information".'),
216     '#required' => TRUE,
217   );
218   $form['fields']['title'] = array('#type' => 'textfield',
219     '#title' => t('Title'),
220     '#default_value' => $edit['title'],
221     '#description' => t('The title of the new field. The title will be shown to the user. An example title is "Favorite color".'),
222     '#required' => TRUE,
223   );
224   $form['fields']['name'] = array('#type' => 'textfield',
225     '#title' => t('Form name'),
226     '#default_value' => $edit['name'],
227     '#description' => t('The name of the field. The form name is not shown to the user but used internally in the HTML code and URLs.
228 Unless you know what you are doing, it is highly recommended that you prefix the form name with <code>profile_</code> to avoid name clashes with other fields. Spaces or any other special characters except dash (-) and underscore (_) are not allowed. An example name is "profile_favorite_color" or perhaps just "profile_color".'),
229     '#required' => TRUE,
230   );
231   $form['fields']['explanation'] = array('#type' => 'textarea',
232     '#title' => t('Explanation'),
233     '#default_value' => $edit['explanation'],
234     '#description' => t('An optional explanation to go with the new field. The explanation will be shown to the user.'),
235   );
236   if ($type == 'selection') {
237     $form['fields']['options'] = array('#type' => 'textarea',
238       '#title' => t('Selection options'),
239       '#default_value' => $edit['options'],
240       '#description' => t('A list of all options. Put each option on a separate line. Example options are "red", "blue", "green", etc.'),
241     );
242   }
243   $form['fields']['weight'] = array('#type' => 'weight',
244     '#title' => t('Weight'),
245     '#default_value' => $edit['weight'],
246     '#delta' => 5,
247     '#description' => t('The weights define the order in which the form fields are shown. Lighter fields "float up" towards the top of the category.'),
248   );
249   $form['fields']['visibility'] = array('#type' => 'radios',
250     '#title' => t('Visibility'),
251     '#default_value' => isset($edit['visibility']) ? $edit['visibility'] : PROFILE_PUBLIC,
252     '#options' => array(PROFILE_HIDDEN => t('Hidden profile field, only accessible by administrators, modules and themes.'), PROFILE_PRIVATE => t('Private field, content only available to privileged users.'), PROFILE_PUBLIC => t('Public field, content shown on profile page but not used on member list pages.'), PROFILE_PUBLIC_LISTINGS => t('Public field, content shown on profile page and on member list pages.')),
253   );
254   if ($type == 'selection' || $type == 'list' || $type == 'textfield') {
255     $form['fields']['page'] = array('#type' => 'textfield',
256       '#title' => t('Page title'),
257       '#default_value' => $edit['page'],
258       '#description' => t('To enable browsing this field by value, enter a title for the resulting page. The word <code>%value</code> will be substituted with the corresponding value. An example page title is "People whose favorite color is %value". This is only applicable for a public field.'),
259     );
260   }
261   else if ($type == 'checkbox') {
262     $form['fields']['page'] = array('#type' => 'textfield',
263       '#title' => t('Page title'),
264       '#default_value' => $edit['page'],
265       '#description' => t('To enable browsing this field by value, enter a title for the resulting page. An example page title is "People who are employed". This is only applicable for a public field.'),
266     );
267   }
268   $form['fields']['required'] = array('#type' => 'checkbox',
269     '#title' => t('The user must enter a value.'),
270     '#default_value' => $edit['required'],
271   );
272   $form['fields']['register'] = array('#type' => 'checkbox',
273     '#title' => t('Visible in user registration form.'),
274     '#default_value' => $edit['register'],
275   );
276   $form['submit'] = array('#type' => 'submit',
277     '#value' => t('Save field'),
278   );
279   return drupal_get_form('profile_field_form', $form);
280 }
281
282 /**
283  * Validate profile_field_form submissions.
284  */
285 function profile_field_form_validate($form_id, $form_values) {
286   // Validate the 'field name':
287   if (eregi('[^a-z0-9_-]', $form_values['name'])) {
288     form_set_error('name', t('The specified form name contains one or more illegal characters. Spaces or any other special characters expect dash (-) and underscore (_) are not allowed.'));
289   }
290
291   if (in_array($form_values['name'], user_fields())) {
292     form_set_error('name', t('The specified form name is reserved for use by Drupal.'));
293   }
294   // Validate the category:
295   if (!$form_values['category']) {
296     form_set_error('category', t('You must enter a category.'));
297   }
298   if ($form_values['category'] == 'account') {
299     form_set_error('category', t('The specified category name is reserved for use by Drupal.'));
300   }
301   $args1 = array($form_values['title'], $form_values['category']);
302   $args2 = array($form_values['name']);
303   $query_suffix = '';
304
305   if (isset($form_values['fid'])) {
306     $args1[] = $args2[] = $form_values['fid'];
307     $query_suffix = ' AND fid != %d';
308   }
309
310   if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE title = '%s' AND category = '%s'". $query_suffix, $args1))) {
311     form_set_error('title', t('The specified title is already in use.'));
312   }
313   if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE name = '%s'". $query_suffix, $args2))) {
314     form_set_error('name', t('The specified name is already in use.'));
315   }
316 }
317
318 /**
319  * Process profile_field_form submissions.
320  */
321 function profile_field_form_submit($form_id, $form_values) {
322   if (!isset($form_values['fid'])) {
323     db_query("INSERT INTO {profile_fields} (title, name, explanation, category, type, weight, required, register, visibility, options, page) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d, %d, '%s', '%s')", $form_values['title'], $form_values['name'], $form_values['explanation'], $form_values['category'], $form_values['type'], $form_values['weight'], $form_values['required'], $form_values['register'], $form_values['visibility'], $form_values['options'], $form_values['page']);
324
325     drupal_set_message(t('The field has been created.'));
326     watchdog('profile', t('Profile field %field added under category %category.', array('%field' => theme('placeholder', $form_values['title']), '%category' => theme('placeholder', $form_values['category']))), WATCHDOG_NOTICE, l(t('view'), 'admin/settings/profile'));
327   }
328   else {
329     db_query("UPDATE {profile_fields} SET title = '%s', name = '%s', explanation = '%s', category = '%s', weight = %d, required = %d, register = %d, visibility = %d, options = '%s', page = '%s' WHERE fid = %d", $form_values['title'], $form_values['name'], $form_values['explanation'], $form_values['category'], $form_values['weight'], $form_values['required'], $form_values['register'], $form_values['visibility'], $form_values['options'], $form_values['page'], $form_values['fid']);
330
331     drupal_set_message(t('The field has been updated.'));
332   }
333   cache_clear_all();
334
335   return 'admin/settings/profile';
336 }
337
338 /**
339  * Menu callback; deletes a field from all user profiles.
340  */
341 function profile_field_delete($fid) {
342   $field = db_fetch_object(db_query("SELECT title FROM {profile_fields} WHERE fid = %d", $fid));
343   if (!$field) {
344     drupal_not_found();
345     return;
346   }
347   $form['fid'] = array('#type' => 'value', '#value' => $fid);
348   $form['title'] = array('#type' => 'value', '#value' => $field->title);
349
350   return confirm_form('profile_field_delete', $form, t('Are you sure you want to delete the field %field?', array('%field' => theme('placeholder', $field->title))), 'admin/settings/profile', t('This action cannot be undone. If users have entered values into this field in their profile, these entries will also be deleted. If you want to keep the user-entered data, instead of deleting the field you may wish to <a href="%edit-field">edit this field</a> and change it to a %hidden-field so that it may only be accessed by administrators.', array('%edit-field' => url('admin/settings/profile/edit/' . $fid), '%hidden-field' => theme('placeholder', t('hidden profile field')))), t('Delete'), t('Cancel'));
351 }
352
353 /**
354  * Process a field delete form submission.
355  */
356 function profile_field_delete_submit($form_id, $form_values) {
357   db_query('DELETE FROM {profile_fields} WHERE fid = %d', $form_values['fid']);
358   db_query('DELETE FROM {profile_values} WHERE fid = %d', $form_values['fid']);
359
360   cache_clear_all();
361
362   drupal_set_message(t('The field %field has been deleted.', array('%field' => theme('placeholder', $form_values['title']))));
363   watchdog('profile', t('Profile field %field deleted.', array('%field' => theme('placeholder', $form_values['title']))), WATCHDOG_NOTICE, l(t('view'), 'admin/settings/profile'));
364
365   return 'admin/settings/profile';
366 }
367
368 /**
369  * Menu callback; display a listing of all editable profile fields.
370  */
371 function profile_admin_overview() {
372
373   $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
374   $rows = array();
375   while ($field = db_fetch_object($result)) {
376     $rows[] = array(check_plain($field->title), $field->name, _profile_field_types($field->type), $field->category, l(t('edit'), "admin/settings/profile/edit/$field->fid"), l(t('delete'), "admin/settings/profile/delete/$field->fid"));
377   }
378   if (count($rows) == 0) {
379     $rows[] = array(array('data' => t('No fields defined.'), 'colspan' => '6'));
380   }
381
382   $header = array(t('Title'), t('Name'), t('Type'), t('Category'), array('data' => t('Operations'), 'colspan' => '2'));
383
384   $output  = theme('table', $header, $rows);
385   $output .= '<h2>'. t('Add new field') .'</h2>';
386   $output .= '<ul>';
387   foreach (_profile_field_types() as $key => $value) {
388     $output .= '<li>'. l($value, "admin/settings/profile/add/$key") .'</li>';
389   }
390   $output .= '</ul>';
391
392   return $output;
393 }
394
395 /**
396  * Menu callback; display a list of user information.
397  */
398 function profile_browse() {
399
400   $name = arg(1);
401   list(,,$value) = explode('/', $_GET['q'], 3);
402
403   $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page, visibility FROM {profile_fields} WHERE name = '%s'", $name));
404
405   if ($name && $field->fid) {
406     // Only allow browsing of fields that have a page title set.
407     if (empty($field->page)) {
408       drupal_not_found();
409       return;
410     }
411     // Do not allow browsing of private fields by non-admins.
412     if (!user_access('administer users') && $field->visibility == PROFILE_PRIVATE) {
413        drupal_access_denied();
414        return;
415     }
416
417     // Compile a list of fields to show.
418     $fields = array();
419     $result = db_query('SELECT name, title, type, weight FROM {profile_fields} WHERE fid != %d AND visibility = %d ORDER BY weight', $field->fid, PROFILE_PUBLIC_LISTINGS);
420     while ($record = db_fetch_object($result)) {
421       $fields[] = $record;
422     }
423
424     // Determine what query to use:
425     $arguments = array($field->fid);
426     switch ($field->type) {
427       case 'checkbox':
428         $query = 'v.value = 1';
429         break;
430       case 'textfield':
431       case 'selection':
432         $query = "v.value = '%s'";
433         $arguments[] = $value;
434         break;
435       case 'list':
436         $query = "v.value LIKE '%%%s%%'";
437         $arguments[] = $value;
438         break;
439       default:
440         drupal_not_found();
441         return;
442     }
443
444     // Extract the affected users:
445     $result = pager_query("SELECT u.uid, u.access FROM {users} u INNER JOIN {profile_values} v ON u.uid = v.uid WHERE v.fid = %d AND $query ORDER BY u.access DESC", 20, 0, NULL, $arguments);
446
447     $output = '<div id="profile">';
448     while ($account = db_fetch_object($result)) {
449       $account = user_load(array('uid' => $account->uid));
450       $profile = _profile_update_user_fields($fields, $account);
451       $output .= theme('profile_listing', $account, $profile);
452     }
453     $output .= theme('pager', NULL, 20);
454
455     if ($field->type == 'selection' || $field->type == 'list' || $field->type == 'textfield') {
456       $title = strtr($field->page, array('%value' => theme('placeholder', $value)));
457     }
458     else {
459       $title = $field->page;
460     }
461     $output .= '</div>';
462
463     drupal_set_title($title);
464     return $output;
465   }
466   else if ($name && !$field->fid) {
467     drupal_not_found();
468   }
469   else {
470     // Compile a list of fields to show.
471     $fields = array();
472     $result = db_query('SELECT name, title, type, weight FROM {profile_fields} WHERE visibility = %d ORDER BY category, weight', PROFILE_PUBLIC_LISTINGS);
473     while ($record = db_fetch_object($result)) {
474       $fields[] = $record;
475     }
476
477     // Extract the affected users:
478     $result = pager_query("SELECT uid, access FROM {users} WHERE uid > 0 AND status != 0 ORDER BY access DESC", 20, 0, NULL);
479
480     $output = '<div id="profile">';
481     while ($account = db_fetch_object($result)) {
482       $account = user_load(array('uid' => $account->uid));
483       $profile = _profile_update_user_fields($fields, $account);
484       $output .= theme('profile_listing', $account, $profile);
485     }
486     $output .= '</div>';
487     $output .= theme('pager', NULL, 20);
488
489     drupal_set_title(t('user list'));
490     return $output;
491   }
492 }
493
494 function profile_load_profile(&$user) {
495   $result = db_query('SELECT f.name, f.type, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
496   while ($field = db_fetch_object($result)) {
497     if (empty($user->{$field->name})) {
498       $user->{$field->name} = _profile_field_serialize($field->type) ? unserialize($field->value) : $field->value;
499     }
500   }
501 }
502
503 function profile_save_profile(&$edit, &$user, $category) {
504   if ((arg(0) == 'user' && arg(1) == 'register') || (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create')) {
505     $result = db_query('SELECT fid, name, type FROM {profile_fields} WHERE register = 1 AND visibility != %d ORDER BY category, weight', PROFILE_HIDDEN);
506   }
507   else {
508     $result = db_query("SELECT fid, name, type FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') AND visibility != %d", $category, PROFILE_HIDDEN);
509     // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
510   }
511   while ($field = db_fetch_object($result)) {
512     if (_profile_field_serialize($field->type)) {
513        $edit[$field->name] = serialize($edit[$field->name]);
514     }
515     db_query("DELETE FROM {profile_values} WHERE fid = %d AND uid = %d", $field->fid, $user->uid);
516     db_query("INSERT INTO {profile_values} (fid, uid, value) VALUES (%d, %d, '%s')", $field->fid, $user->uid, $edit[$field->name]);
517     // Mark field as handled (prevents saving to user->data).
518     $edit[$field->name] = NULL;
519   }
520 }
521
522 function profile_view_field($user, $field) {
523   // Only allow browsing of private fields for admins, if browsing is enabled,
524   // and if a user has permission to view profiles. Note that this check is
525   // necessary because a user may always see their own profile.
526   $browse = user_access('access user profiles')
527          && (user_access('administer users') || $field->visibility != PROFILE_PRIVATE)
528          && !empty($field->page);
529
530   if ($value = $user->{$field->name}) {
531     switch ($field->type) {
532       case 'textarea':
533         return check_markup($value);
534       case 'textfield':
535       case 'selection':
536         return $browse ? l($value, 'profile/'. $field->name .'/'. $value) : check_plain($value);
537       case 'checkbox':
538         return $browse ? l($field->title, 'profile/'. $field->name) : check_plain($field->title);
539       case 'url':
540         return '<a href="'. check_url($value) .'">'. check_plain($value) .'</a>';
541       case 'date':
542         $format = substr(variable_get('date_format_short', 'm/d/Y - H:i'), 0, 5);
543         // Note: Avoid PHP's date() because it does not handle dates before
544         // 1970 on Windows. This would make the date field useless for e.g.
545         // birthdays.
546         $replace = array('d' => sprintf('%02d', $value['day']),
547                          'j' => $value['day'],
548                          'm' => sprintf('%02d', $value['month']),
549                          'M' => map_month($value['month']),
550                          'Y' => $value['year'],
551                          'H:i' => null,
552                          'g:ia' => null);
553         return strtr($format, $replace);
554       case 'list':
555         $values = split("[,\n\r]", $value);
556         $fields = array();
557         foreach ($values as $value) {
558           if ($value = trim($value)) {
559             $fields[] = $browse ? l($value, 'profile/'. $field->name .'/'. $value) : check_plain($value);
560           }
561         }
562         return implode(', ', $fields);
563     }
564   }
565 }
566
567 function profile_view_profile($user) {
568
569   profile_load_profile($user);
570
571   // Show private fields to administrators and people viewing their own account.
572   if (user_access('administer users') || $GLOBALS['user']->uid == $user->uid) {
573     $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_HIDDEN);
574   }
575   else {
576     $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND visibility != %d ORDER BY category, weight', PROFILE_PRIVATE, PROFILE_HIDDEN);
577   }
578
579   while ($field = db_fetch_object($result)) {
580     if ($value = profile_view_field($user, $field)) {
581       $description = ($field->visibility == PROFILE_PRIVATE) ? t('The content of this field is private and only visible to yourself.') : '';
582       $title = ($field->type != 'checkbox') ? check_plain($field->title) : NULL;
583       $item = array('title' => $title,
584         'value' => $value,
585         'class' => $field->name,
586       );
587       $fields[$field->category][] = $item;
588     }
589   }
590   return $fields;
591 }
592
593 function _profile_form_explanation($field) {
594   $output = $field->explanation;
595
596   if ($field->type == 'list') {
597     $output .= ' '. t('Put each item on a separate line or separate them by commas. No HTML allowed.');
598   }
599
600   if ($field->visibility == PROFILE_PRIVATE) {
601     $output .= ' '. t('The content of this field is kept private and will not be shown publicly.');
602   }
603
604   return $output;
605 }
606
607 function profile_form_profile($edit, $user, $category) {
608   if (arg(0) == 'user' && arg(1) == 'register') {
609     $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND register = 1 ORDER BY category, weight', PROFILE_HIDDEN);
610   }
611   elseif (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create') {
612     $result = db_query('SELECT * FROM {profile_fields} WHERE register = 1 ORDER BY category, weight');
613   }
614   elseif (user_access('administer users')) {
615     $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') ORDER BY weight", $category);
616   }
617   else {
618     $result = db_query("SELECT * FROM {profile_fields} WHERE visibility != %d AND LOWER(category) = LOWER('%s') ORDER BY weight", PROFILE_HIDDEN, $category);
619     // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
620   }
621
622   while ($field = db_fetch_object($result)) {
623     $category = $field->category;
624     if (!isset($fields[$category])) {
625       $fields[$category] = array('#type' => 'fieldset', '#title' => $category, '#weight' => $w++);
626     }
627     switch ($field->type) {
628       case 'textfield':
629       case 'url':
630         $fields[$category][$field->name] = array('#type' => 'textfield',
631           '#title' => check_plain($field->title),
632           '#default_value' => $edit[$field->name],
633           '#maxlength' => 255,
634           '#description' => _profile_form_explanation($field),
635           '#required' => $field->required,
636         );
637         break;
638       case 'textarea':
639         $fields[$category][$field->name] = array('#type' => 'textarea',
640           '#title' => check_plain($field->title),
641           '#default_value' => $edit[$field->name],
642           '#description' => _profile_form_explanation($field),
643           '#required' => $field->required,
644         );
645         break;
646       case 'list':
647         $fields[$category][$field->name] = array('#type' => 'textarea',
648           '#title' => check_plain($field->title),
649           '#default_value' => $edit[$field->name],
650           '#description' => _profile_form_explanation($field),
651           '#required' => $field->required,
652         );
653         break;
654       case 'checkbox':
655         $fields[$category][$field->name] = array('#type' => 'checkbox',
656           '#title' => check_plain($field->title),
657           '#default_value' => $edit[$field->name],
658           '#description' => _profile_form_explanation($field),
659           '#required' => $field->required,
660         );
661         break;
662       case 'selection':
663         $options = $field->required ? array() : array('--');
664         $lines = split("[,\n\r]", $field->options);
665         foreach ($lines as $line) {
666           if ($line = trim($line)) {
667             $options[$line] = $line;
668           }
669         }
670         $fields[$category][$field->name] = array('#type' => 'select',
671           '#title' => check_plain($field->title),
672           '#default_value' => $edit[$field->name],
673           '#options' => $options,
674           '#description' => _profile_form_explanation($field),
675           '#required' => $field->required,
676         );
677         break;
678       case 'date':
679         $fields[$category][$field->name] = array('#type' => 'date',
680           '#title' => check_plain($field->title),
681           '#default_value' => $edit[$field->name],
682           '#description' => _profile_form_explanation($field),
683           '#required' => $field->required,
684         );
685         break;
686     }
687   }
688   return $fields;
689 }
690
691 /**
692  * Helper function: update an array of user fields by calling profile_view_field
693  */
694 function _profile_update_user_fields($fields, $account) {
695   foreach ($fields as $key => $field) {
696     $fields[$key]->value = profile_view_field($account, $field);
697   }
698   return $fields;
699 }
700
701 function profile_validate_profile($edit, $category) {
702   if (arg(0) == 'user' && arg(1) == 'register') {
703     $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND register = 1 ORDER BY category, weight', PROFILE_HIDDEN);
704   }
705   elseif (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create') {
706     $result = db_query('SELECT * FROM {profile_fields} WHERE register = 1 ORDER BY category, weight');
707   }
708   elseif (user_access('administer users')) {
709     $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') ORDER BY weight", $category);
710   }
711   else {
712     $result = db_query("SELECT * FROM {profile_fields} WHERE visibility != %d AND LOWER(category) = LOWER('%s') ORDER BY weight", PROFILE_HIDDEN, $category);
713     // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
714   }
715
716   while ($field = db_fetch_object($result)) {
717     if ($edit[$field->name]) {
718       if ($field->type == 'url') {
719         if (!valid_url($edit[$field->name], true)) {
720           form_set_error($field->name, t('The value provided for %field is not a valid URL.', array('%field' => theme('placeholder', $field->title))));
721         }
722       }
723     }
724     else if ($field->required && !user_access('administer users')) {
725       form_set_error($field->name, t('The field %field is required.', array('%field' => theme('placeholder', $field->title))));
726     }
727   }
728
729   return $edit;
730 }
731
732 function profile_categories() {
733   $result = db_query("SELECT DISTINCT(category) FROM {profile_fields}");
734   while ($category = db_fetch_object($result)) {
735     $data[] = array('name' => check_plain($category->category), 'title' => $category->category, 'weight' => 3);
736   }
737   return $data;
738 }
739
740 function theme_profile_block($account, $fields = array()) {
741
742   $output .= theme('user_picture', $account);
743
744   foreach ($fields as $field) {
745     if ($field->value) {
746       if ($field->type == 'checkbox') {
747         $output .= "<p>$field->value</p>\n";
748       }
749       else {
750         $output .= "<p><strong>$field->title</strong><br />$field->value</p>\n";
751       }
752     }
753   }
754
755   return $output;
756 }
757
758 function theme_profile_listing($account, $fields = array()) {
759
760   $output  = "<div class=\"profile\">\n";
761   $output .= theme('user_picture', $account);
762   $output .= ' <div class="name">'. theme('username', $account) ."</div>\n";
763
764   foreach ($fields as $field) {
765     if ($field->value) {
766       $output .= " <div class=\"field\">$field->value</div>\n";
767     }
768   }
769
770   $output .= "</div>\n";
771
772   return $output;
773 }
774
775 function _profile_field_types($type = NULL) {
776   $types = array('textfield' => t('single-line textfield'),
777                  'textarea' => t('multi-line textfield'),
778                  'checkbox' => t('checkbox'),
779                  'selection' => t('list selection'),
780                  'list' => t('freeform list'),
781                  'url' => t('URL'),
782                  'date' => t('date'));
783   return isset($type) ? $types[$type] : $types;
784 }
785
786 function _profile_field_serialize($type = NULL) {
787   return $type == 'date';
788 }