/[drupal]/contributions/modules/private_number/private_number.module
ViewVC logotype

Contents of /contributions/modules/private_number/private_number.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.3 - (show annotations) (download) (as text)
Wed Apr 29 20:34:30 2009 UTC (6 months, 3 weeks ago) by gestaltware
Branch: MAIN
CVS Tags: DRUPAL-5--2-1, HEAD
Changes since 1.2: +8 -8 lines
File MIME type: text/x-php
- fixed bug in view own private number permission
1 <?php
2 // $Id: private_number.module,v 1.2 2009/04/06 20:37:32 gestaltware Exp $
3
4 /**
5 * @file
6 * Defines a CCK field type for numbers which should be kept private, such as
7 * government-issued identification or account numbers.
8 */
9
10 /**
11 * Implementation of hook_perm()
12 */
13 function private_number_perm() {
14 return array('view private number', 'view own private number');
15 }
16
17 /**
18 * Implementation of hook_menu().
19 */
20 function private_number_menu($may_cache) {
21 $items = array();
22 if ($may_cache) {
23 $items[] = array(
24 'path' => 'admin/settings/private_number',
25 'title' => 'Private Number',
26 'description' => t('Defines a CCK field type for numbers which should be kept private, such as government-issued identification or account numbers.'),
27 'callback' => 'drupal_get_form',
28 'callback arguments' => 'private_number_admin_settings',
29 'type' => MENU_NORMAL_ITEM,
30 'access' => user_access('administer site configuration'),
31 );
32 }
33 return $items;
34 }
35
36 /**
37 * Implementation of hook_help().
38 */
39 function private_number_help($section) {
40 switch ($section) {
41 case 'admin/settings/private_number':
42 case 'admin/help#private_number':
43 return '<p>' . t('Private Number defines a CCK field type for numbers which should be kept private, such as government-issued identification or account numbers. The module enables two of the industry standard best practices for handling confidential data: masking the number when viewed by users without !view_access access permissions and encrypting the number with a md5 !cipher when stored.', array(
44 '!view_access' => l('view private number', 'admin/user/access', NULL, NULL, 'module-private_number'),
45 '!cipher' => l('block cipher in 128 bit CFB mode', 'http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation'),
46 )) . '<p>';
47 }
48 }
49
50 /**
51 * Implementation of hook_requirements().
52 */
53 function private_number_requirements($phase) {
54 // Ensure translations don't break at install time
55 $t = get_t();
56
57 if ($phase == 'runtime') {
58 $requirements['private_number_key']['title'] = $t('Private Number');
59
60 // Check if legacy cipher key var is enabled.
61 if (variable_get('private_number_cipher_key', '')) {
62 $requirements['private_number_key']['value'] = $t('Private Number key is not secure. The !update.php will secure your existing key in settings.php.', array('!update.php' => l(t('database update script'), 'update.php')));
63 $requirements['private_number_key']['severity'] = REQUIREMENT_ERROR;
64 }
65
66 // Check if var in $conf array.
67 else if (variable_get('private_number_key', '')) {
68 $requirements['private_number_key']['value'] = $t('Private Number key enabled.');
69 $requirements['private_number_key']['severity'] = REQUIREMENT_OK;
70 }
71
72 // Check if file path in $conf array.
73 else if (variable_get('private_number_key_file', '')) {
74 $requirements['private_number_key']['value'] = $t('Private Number key file enabled.');
75 $requirements['private_number_key']['severity'] = REQUIREMENT_OK;
76 }
77
78 // No key found.
79 else {
80 $requirements['private_number_key']['value'] = $t('Private Number key not configured. Please consult README.txt for more information on key management.');
81 $requirements['private_number_key']['severity'] = REQUIREMENT_ERROR;
82 }
83
84 return $requirements;
85 }
86 }
87
88 /**
89 * Menu callback
90 *
91 * @return
92 * array of form content.
93 */
94 function private_number_admin_settings() {
95 // Check if legacy key is enabled
96 if (variable_get('private_number_cipher_key', '')) {
97 drupal_set_message(t('Private Number key is not secure. The !update.php will secure your existing key in settings.php.', array('!update.php' => l(t('database update script'), 'update.php'))), 'error');
98 }
99
100 $form['private_number_digits_shown'] = array(
101 '#type' => 'select',
102 '#title' => t('Number of ending digits unmasked'),
103 '#options' => array(0, 1, 2, 3, 4, 5),
104
105 '#default_value' => variable_get('private_number_digits_shown', 3),
106 '#description' => t('Users without !view_access access permissions will be shown %last_three_digits. Set to 0 to show !hidden.', array(
107 '!view_access' => l('view private number', 'admin/user/access', NULL, NULL, 'module-private_number'),
108 '%last_three_digits' => t('Ending in') . ' XXX',
109 '!hidden' => '<em>' . t('&lt;hidden&gt;') . '</em>',
110 )),
111 );
112
113 $form['enable']['private_number_encrypt'] = array(
114 '#type' => 'radios',
115 '#title' => t('Enable encryption of Private Numbers'),
116 '#default_value' => variable_get('private_number_encrypt', 1),
117 '#options' => array('1' => t('On'), '0' => t('Off')),
118 '#description' => t(' Recommended enabled. Note that changing this setting will make already stored Private Numbers unreadable.')
119 );
120
121 return system_settings_form($form);
122 }
123
124 //============= DATA LAYER ==================================
125
126 /**
127 * Declare information about a field type.
128 */
129 function private_number_field_info() {
130 return array(
131 'private_number' => array('label' => t('Private Number')),
132 );
133 }
134
135 /**
136 * Handle the parameters for a field.
137 */
138 function private_number_field_settings($op, $field) {
139 switch ($op) {
140 // case 'form':
141 // return $form;
142 // case 'validate':
143 // break;
144 // case 'save':
145 // return array('');
146
147 case 'database columns':
148 $columns = array(
149 'private_number' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE),
150 );
151 return $columns;
152 }
153 }
154
155 /**
156 * Define the behavior of a field type.
157 */
158 function private_number_field($op, &$node, $field, &$items) {
159 switch ($op) {
160 // case 'load':
161 // case 'validate':
162 // case 'submit':
163 // case 'insert':
164 // case 'update':
165 // case 'delete':
166 // case 'view':
167 // break;
168 }
169
170 return $output;
171 }
172
173 //============= PRESENTATION LAYER ==========================
174
175 /**
176 * Declare information about a formatter.
177 * - define one or more ways to display information
178 * - The 'default' formatter is used for the standard node display.
179 * - All the available formatters are displayed as options when the field is used in Views.
180 */
181 function private_number_field_formatter_info() {
182 return array(
183 'default' => array(
184 'label' => 'Default',
185 'field types' => array('private_number'),
186 ),
187 );
188 }
189
190 /**
191 * Prepare an individual item for viewing in a browser.
192 */
193 function private_number_field_formatter($field, $item, $formatter, $node) {
194 if (!isset($item['private_number'])) return;
195
196 $private_number = $item['private_number'];
197
198 // If number is encrypted, decrypt it.
199 if (variable_get('private_number_encrypt', 1)) {
200 $private_number = _private_number_md5_decrypt($private_number, _private_number_get_key());
201 }
202 $private_number = check_plain($private_number); //run after decrypt just to be safe
203
204 switch ($formatter) {
205 default:
206 // Check for view private number permission with a version of user_access
207 // that does not automaitcally grant the superuser. If that fails, check
208 // for view own private number if user is viewing their own node.
209 if (_private_number_access($node, $field)) {
210 return $private_number;
211 }
212
213 // Check if digits should be shown.
214 if (intval(variable_get('private_number_digits_shown', 3)) > 0) {
215 return t('Ending in') . ' ' . substr($private_number, -intval(variable_get('private_number_digits_shown', 3)));
216 }
217
218 return t('&lt;hidden&gt;');
219 }
220 }
221
222 //============= FORM LAYER ==================================
223
224 /**
225 * Declare information about a widget.
226 */
227 function private_number_widget_info() {
228 return array(
229 'private_number' => array(
230 'label' => t('Textfield'),
231 'field types' => array('private_number'),
232 ),
233 );
234 }
235
236 /**
237 * Handle the parameters for a widget.
238 */
239 function private_number_widget_settings($op, $widget) {
240 switch ($op) {
241 case 'form':
242 $form = array();
243 $form['size'] = array(
244 '#type' => 'textfield',
245 '#title' => t('Size'),
246 '#default_value' => isset($widget['size']) ? $widget['size'] : 20,
247 '#required' => FALSE,
248 '#description' => t('Size of textfield'),
249 );
250
251 $form['pattern'] = array(
252 '#type' => 'textfield',
253 '#title' => t('Validation pattern'),
254 '#default_value' => isset($widget['pattern']) ? $widget['pattern'] : '',
255 '#required' => FALSE,
256 '#description' => t('Optional pattern for field validation and formatting. Example for a U.S. social security number: \d{3}\-\d{2}\-\d{4}'),
257 );
258
259 $form['human_regex'] = array(
260 '#type' => 'checkbox',
261 '#title' => t('Disable human regex validation error message'),
262 '#default_value' => isset($widget['human_regex']) ? $widget['human_regex'] : 0,
263 '#required' => FALSE,
264 );
265 return $form;
266
267 case 'validate':
268 if (!empty($widget['size']) && (!is_numeric($widget['size']) || intval($widget['size']) != $widget['size'] || $widget['size'] <= 0)) {
269 form_set_error('size', t('"Size" must be a positive integer.'));
270 }
271 break;
272
273 case 'save':
274 return array('size', 'pattern', 'human_regex');
275 }
276 }
277
278 /**
279 * Define the behavior of a widget.
280 */
281 function private_number_widget($op, &$node, $field, &$node_field) {
282 switch ($op) {
283 case 'prepare form values':
284 break;
285
286 case 'form':
287 $form = array();
288 $form[$field['field_name']] = array('#tree' => TRUE);
289
290 if ($field['multiple']) {
291 $access = _private_number_access($node, $field, 'edit');
292
293 if ($access) {
294 $form[$field['field_name']]['#type'] = 'fieldset';
295 $form[$field['field_name']]['#title'] = t($field['widget']['label']);
296 }
297
298 foreach (range(0,2) as $delta) {
299 if ($access) {
300 $default_value = '';
301 if (isset($node_field[$delta]['private_number'])) {
302 if (variable_get('private_number_encrypt', 1)) {
303 $default_value = _private_number_md5_decrypt($node_field[$delta]['private_number'], _private_number_get_key());
304 }
305 else {
306 $default_value = $node_field[$delta]['private_number'];
307 }
308 }
309
310 $form[$field['field_name']][$delta]['private_number'] = array(
311 '#type' => 'textfield',
312 '#title' => '',
313 '#default_value' => $default_value,
314 '#required' => $field['required'] ? $field['required'] : FALSE,
315 '#maxlength' => 255,
316 '#weight' => $field['widget']['weight'],
317 '#size' => isset($field['widget']['size']) ? $field['widget']['size'] : 20,
318 '#description' => isset($field['widget']['description']) ? $field['widget']['description'] : '',
319 );
320 }
321
322 // no access to view
323 else {
324 $form[$field['field_name']][$delta]['private_number'] = array(
325 '#type' => 'hidden',
326 '#value' => 'md5___' . $node_field[$delta]['private_number'],
327 );
328 }
329 }
330 }
331
332 // Single value field.
333 else {
334 if (_private_number_access($node, $field, 'edit')) {
335 $default_value = '';
336 if (isset($node_field[0]['private_number'])) {
337 if (variable_get('private_number_encrypt', 1)) {
338 $default_value = _private_number_md5_decrypt($node_field[0]['private_number'], _private_number_get_key());
339 }
340 else {
341 $default_value = $node_field[0]['private_number'];
342 }
343 }
344
345 $form[$field['field_name']][0]['private_number'] = array(
346 '#type' => 'textfield',
347 '#title' => $field['widget']['label'],
348 '#required' => $field['required'] ? $field['required'] : FALSE,
349 '#default_value' => $default_value,
350 '#maxlength' => 255,
351 '#weight' => $field['widget']['weight'],
352 '#size' => isset($field['widget']['size']) ? $field['widget']['size'] : 20,
353 '#description' => isset($field['widget']['description']) ? $field['widget']['description'] : '',
354 );
355 }
356
357 // no access to view
358 else {
359 $form[$field['field_name']][0]['private_number'] = array(
360 '#type' => 'hidden',
361 '#value' => 'md5___' . $node_field[0]['private_number'],
362 );
363 }
364 }
365
366 return $form;
367
368 case 'validate':
369 //widget display_settings
370 if (is_array($node_field)) {
371 foreach ($node_field as $delta => $item) {
372 if ($item['private_number'] != '') {
373 if (substr($item['private_number'], 0, 6) != 'md5___') {
374 if (!_private_number_widget_validate($item['private_number'], $field['widget']['pattern'])) {
375 form_set_error($field['field_name'], t('%number is not a valid !label. !pattern_text %pattern', array(
376 '%number' => $item['private_number'],
377 '!label' => $field['widget']['label'],
378 '!pattern_text' => (!$field['widget']['human_regex'] ? ($field['widget']['pattern'] ? t('Expected input format is') : '') : ''),
379 '%pattern' => (!$field['widget']['human_regex'] ? ($field['widget']['pattern'] ? _private_number_human_regex($field['widget']['pattern']) : '') : '')
380 )));
381 }
382 }
383 }
384 }
385 }
386 break;
387
388 case 'process form values':
389 if (is_array($node_field)) {
390 foreach ($node_field as $delta => $item) {
391 //format the id_number
392 if ($item['private_number'] != '') {
393 $node_field[$delta]['private_number'] = _private_number_widget_process($node_field[$delta]['private_number']);
394 }
395 }
396 }
397 break;
398
399 case 'submit':
400 break;
401 }
402 }
403
404 //============= INERNAL FUNCTIONS ===========================
405
406 /**
407 * Perform widget form input validation
408 */
409 function _private_number_widget_validate($private_number, $pattern) {
410 if (!$pattern) return true;
411
412 $private_number = trim($private_number);
413 if (!preg_match("/^".$pattern."$/i", $private_number)) return false;
414 return true;
415 }
416
417 /**
418 * Perform widget process form values manipulation
419 */
420 function _private_number_widget_process($private_number) {
421 if (substr($private_number, 0, 6) == 'md5___') {
422 return substr($private_number, 6);
423 }
424 if (variable_get('private_number_encrypt', 1)) {
425 return _private_number_md5_encrypt($private_number, _private_number_get_key());
426 }
427 return $private_number;
428 }
429
430 /**
431 * Implementation of human-readable regex
432 *
433 * @return string Returns human-readable regex
434 */
435 function _private_number_human_regex($pattern) {
436 if (!$pattern) return;
437 $pattern = str_replace('\\d', '!break!%d', $pattern);
438 $parts = explode("!break!", $pattern);
439
440 foreach ($parts as $part) {
441 $delimiter = '';
442
443 //min-max
444 if (substr($part,0,3) == '%d{') {
445 preg_match('/%d{(\d*)(,?)(\d*)?}(.*)/i', $part, $matches);
446 if ($matches[2] == ',') { //range
447 $human_regex .= $matches[1] . ' to ' . $matches[3] . ' digits';
448 }
449 else {
450 $human_regex .= str_repeat('9', $matches[1]);
451 }
452 $delimiter = $matches[4];
453 }
454
455 //optional
456 elseif (substr($part,0,3) == '%d?') {
457 $human_regex .= '0 or 1 digit';
458 $delimiter = substr($part, 3);
459 }
460
461 //1 or more
462 elseif (substr($part,0,3) == '%d+') {
463 $human_regex .= '1 or more digits';
464 $delimiter = substr($part, 3);
465 }
466
467 //0 or more
468 elseif (substr($part,0,3) == '%d*') {
469 $human_regex .= '0 or more digits';
470 $delimiter = substr($part, 3);
471 }
472
473 //single instance
474 elseif (substr($part,0,2) == '%d') {
475 $human_regex .= '9';
476 $delimiter = substr($part, 2);
477 }
478
479 //leading delimeter
480 else {
481 $delimiter = $part;
482 }
483
484 //process delimiters
485 $delimiter = str_replace('\\', '%%', $delimiter);
486 $meta = array(
487 "/(?<!%%)\(/",
488 "/(?<!%%)\)/",
489 '/(?<!%%)\{/',
490 '/(?<!%%)\}/',
491 '/(?<!%%)\|/',
492 '/(?<!%%)\[/',
493 '/(?<!%%)\]/',
494 '/(?<!%%)\^/',
495 '/(?<!%%)\$/',
496 "/(?<!%%)\?/",
497 "/(?<!%%)\./",
498 '/(?<!%%)\+/',
499 "/(?<!%%)\*/"
500 );
501 $replace = array('', '', '', '', '', '', '', '', '', '', '', '', '');
502 $clean_meta = preg_replace($meta , $replace, $delimiter);
503 $human_regex .= str_replace('%%', '', $clean_meta);
504 }
505
506 return $human_regex;
507 }
508
509 /**
510 * Implementation of md5 block cipher encrpytion
511 *
512 * @credits http://us.php.net/manual/en/function.md5.php
513 */
514 function _private_number_md5_encrypt($plain_text, $password, $iv_len = 16) {
515 $plain_text .= "\x13";
516 $n = strlen($plain_text);
517 if ($n % 16) $plain_text .= str_repeat("\0", 16 - ($n % 16));
518 $i = 0;
519 $enc_text = _private_number_get_iv($iv_len);
520 $iv = substr($password ^ $enc_text, 0, 512);
521 while ($i < $n) {
522 $block = substr($plain_text, $i, 16) ^ pack('H*', md5($iv));
523 $enc_text .= $block;
524 $iv = substr($block . $iv, 0, 512) ^ $password;
525 $i += 16;
526 }
527 return base64_encode($enc_text);
528 }
529
530 /**
531 * Implementation of md5 block cipher decrpytion
532 *
533 * @credits http://us.php.net/manual/en/function.md5.php
534 */
535 function _private_number_md5_decrypt($enc_text, $password, $iv_len = 16) {
536 $enc_text = base64_decode($enc_text);
537 $n = strlen($enc_text);
538 $i = $iv_len;
539 $plain_text = '';
540 $iv = substr($password ^ substr($enc_text, 0, $iv_len), 0, 512);
541 while ($i < $n) {
542 $block = substr($enc_text, $i, 16);
543 $plain_text .= $block ^ pack('H*', md5($iv));
544 $iv = substr($block . $iv, 0, 512) ^ $password;
545 $i += 16;
546 }
547 return preg_replace('/\\x13\\x00*$/', '', $plain_text);
548 }
549
550 /**
551 * Get initialization vector.
552 */
553 function _private_number_get_iv($len) {
554 while ($len-- > 0) {
555 $output .= chr(mt_rand() & 0xff);
556 }
557 return $output;
558 }
559
560 /**
561 * Get Private Number cipher key.
562 */
563 function _private_number_get_key() {
564 static $key;
565 if (isset($key)) return $key;
566
567 // Check if legacy cipher key var is enabled.
568 if ($var = variable_get('private_number_cipher_key', '')) {
569 $key = $var;
570 }
571
572 // Check if var in $conf array.
573 else if ($var = variable_get('private_number_key', '')) {
574 $key = $var;
575 }
576
577 // Check if file path in $conf array.
578 else if ($path = variable_get('private_number_key_file', '')) {
579 $file = realpath(conf_path() . '/' . $path);
580 if (file_exists($file)) {
581 if ($fp = @fopen($file, 'r')) {
582 $var = trim(fgets($fp));
583 fclose($fp);
584 }
585
586 if ($var) {
587 $key = $var;
588 }
589 }
590 }
591
592 if (isset($key)) {
593 return $key;
594 }
595
596 drupal_set_message(t('Private Number key not configured. Please consult README.txt for more information on key management.'), 'error');
597 return FALSE;
598 }
599
600 /**
601 * Determine whether the user has a privilege to view Private Number.
602 *
603 * The superuser should not automatically receive View Private Number
604 * access permissions.
605 */
606 function _private_number_access(&$node, $field, $op = 'view') {
607 global $user;
608 static $private_number_perm = array();
609
610 // To reduce the number of SQL queries, we cache the user's permissions
611 // in a static variable.
612 if (!isset($private_number_perm[$user->uid . '_' . $node->uid])) {
613 $rids = array_keys($user->roles);
614 $placeholders = implode(',', array_fill(0, count($rids), '%d'));
615 $result = db_query("SELECT DISTINCT(p.perm) FROM {role} r INNER JOIN {permission} p ON p.rid = r.rid WHERE r.rid IN ($placeholders)", $rids);
616 $private_number_perm[$user->uid . '_' . $node->uid] = '';
617 while ($row = db_fetch_object($result)) {
618 $private_number_perm[$user->uid . '_' . $node->uid] .= "$row->perm, ";
619 }
620 }
621
622 if (isset($private_number_perm[$user->uid . '_' . $node->uid])) {
623 // Check for view private number permission with a version of user_access
624 // that does not automatically grant the superuser.
625 if (strpos($private_number_perm[$user->uid . '_' . $node->uid], "view private number, ") !== FALSE) {
626 return TRUE;
627 }
628
629 // Check for view own private number if user is viewing their own node.
630 if ($node->uid && $user->uid == $node->uid) {
631 if ($op == 'edit') {
632 return TRUE;
633 }
634 if (strpos($private_number_perm[$user->uid . '_' . $node->uid], "view own private number, ") !== FALSE) {
635 return TRUE;
636 }
637 }
638 }
639
640 // Invoke a callback indicating a node with private number field is being
641 // viewed or edited. Modules may enable the operation by returning TRUE.
642 $callback = module_invoke_all('private_number_access', $op, $node, $field, $user);
643 if (in_array(TRUE, $callback)) {
644 return TRUE;
645 }
646
647 return FALSE;
648 }

  ViewVC Help
Powered by ViewVC 1.1.2