1: <?php
2: /**
3: * Horde_Data implementation for LDAP Data Interchange Format (LDIF).
4: *
5: * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
6: *
7: * See the enclosed file LICENSE for license information (ASL). If you
8: * did not receive this file, see http://www.horde.org/licenses/apache.
9: *
10: * @author Rita Selsky <ritaselsky@gmail.com>
11: * @package Horde_Data
12: */
13: class Turba_Data_Ldif extends Horde_Data_Base
14: {
15: protected $_extension = 'ldif';
16:
17: protected $_contentType = 'text/ldif';
18:
19: /**
20: * Useful Mozilla address book attribute names.
21: *
22: * @var array
23: */
24: private $_mozillaAttr = array('cn', 'givenName', 'sn', 'mail', 'mozillaNickname',
25: 'homeStreet', 'mozillaHomeStreet2', 'mozillaHomeLocalityName',
26: 'mozillaHomeState', 'mozillaHomePostalCode',
27: 'mozillaHomeCountryName', 'street',
28: 'mozillaWorkStreet2', 'l', 'st', 'postalCode',
29: 'c', 'homePhone', 'telephoneNumber', 'mobile',
30: 'fax', 'title', 'company', 'description', 'mozillaWorkUrl',
31: 'department', 'mozillaNickname');
32:
33: /**
34: * Useful Turba address book attribute names.
35: *
36: * @var array
37: */
38: private $_turbaAttr = array('name', 'firstname', 'lastname', 'email', 'alias',
39: 'homeAddress', 'homeStreet', 'homeCity',
40: 'homeProvince', 'homePostalCode', 'homeCountry',
41: 'workAddress', 'workStreet', 'workCity', 'workProvince',
42: 'workPostalCode', 'workCountry',
43: 'homePhone', 'workPhone', 'cellPhone',
44: 'fax', 'title', 'company', 'notes', 'website',
45: 'department', 'nickname');
46: /**
47: * Turba address book attribute names and the corresponding Mozilla name.
48: *
49: * @var array
50: */
51: private $_turbaMozillaMap = array('name' => 'cn',
52: 'firstname' => 'givenName',
53: 'lastname' => 'sn',
54: 'email' => 'mail',
55: 'alias' => 'mozillaNickname',
56: 'homePhone' => 'homePhone',
57: 'workPhone' => 'telephoneNumber',
58: 'cellPhone' => 'mobile',
59: 'fax' => 'fax',
60: 'title' => 'title',
61: 'company' => 'company',
62: 'notes' => 'description',
63: 'homeAddress' => 'homeStreet',
64: 'homeStreet' => 'mozillaHomeStreet2',
65: 'homeCity' => 'mozillaHomeLocalityName',
66: 'homeProvince' => 'mozillaHomeState',
67: 'homePostalCode' => 'mozillaHomePostalCode',
68: 'homeCountry' => 'mozillaHomeCountryName',
69: 'workAddress' => 'street',
70: 'workStreet' => 'mozillaWorkStreet2',
71: 'workCity' => 'l',
72: 'workProvince' => 'st',
73: 'workPostalCode' => 'postalCode',
74: 'workCountry' => 'c',
75: 'website' => 'mozillaWorkUrl',
76: 'department' => 'department',
77: 'nickname' => 'mozillaNickname');
78:
79: /**
80: * Check if a string is safe according to RFC 2849, or if it needs to be
81: * base64 encoded.
82: *
83: * @private
84: *
85: * @param string $str The string to check.
86: *
87: * @return boolean True if the string is safe, false otherwise.
88: */
89: private function _is_safe_string($str)
90: {
91: /* SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-7F
92: * ; any value <= 127 decimal except NUL, LF,
93: * ; and CR
94: *
95: * SAFE-INIT-CHAR = %x01-09 / %x0B-0C / %x0E-1F /
96: * %x21-39 / %x3B / %x3D-7F
97: * ; any value <= 127 except NUL, LF, CR,
98: * ; SPACE, colon (":", ASCII 58 decimal)
99: * ; and less-than ("<" , ASCII 60 decimal) */
100: if (!strlen($str)) {
101: return true;
102: }
103: if ($str[0] == ' ' || $str[0] == ':' || $str[0] == '<') {
104: return false;
105: }
106: for ($i = 0; $i < strlen($str); ++$i) {
107: if (ord($str[$i]) > 127 || $str[$i] == NULL || $str[$i] == "\n" ||
108: $str[$i] == "\r") {
109: return false;
110: }
111: }
112:
113: return true;
114: }
115:
116: public function importData($contents, $header = false)
117: {
118: $data = array();
119: $records = preg_split('/(\r?\n){2}/', $contents);
120: foreach ($records as $record) {
121: if (trim($record) == '') {
122: /* Ignore empty records */
123: continue;
124: }
125: /* one key:value pair per line */
126: $lines = preg_split('/\r?\n/', $record);
127: $hash = array();
128: foreach ($lines as $line) {
129: // [0] = key, [1] = delimiter, [2] = value
130: $res = preg_split('/(:[:<]?) */', $line, 2, PREG_SPLIT_DELIM_CAPTURE);
131: if ((count($res) == 3) &&
132: in_array($res[0], $this->_mozillaAttr)) {
133: $hash[$res[0]] = ($res[1] == '::')
134: ? base64_decode($res[2])
135: : $res[2];
136: }
137: }
138: $data[] = $hash;
139: }
140:
141: return $data;
142: }
143:
144: /**
145: * Builds a LDIF file from a given data structure and triggers its download.
146: * It DOES NOT exit the current script but only outputs the correct headers
147: * and data.
148: *
149: * @param string $filename The name of the file to be downloaded.
150: * @param array $data A two-dimensional array containing the data
151: * set.
152: * @param boolean $header If true, the rows of $data are associative
153: * arrays with field names as their keys.
154: */
155: public function exportFile($filename, $data, $header = false)
156: {
157: $export = $this->exportData($data, $header);
158: $GLOBALS['browser']->downloadHeaders($filename, 'text/ldif', false, strlen($export));
159: echo $export;
160: }
161:
162: /**
163: * Builds a LDIF file from a given data structure and returns it as a
164: * string.
165: *
166: * @param array $data A two-dimensional array containing the data set.
167: * @param boolean $header If true, the rows of $data are associative
168: * arrays with field names as their keys.
169: *
170: * @return string The LDIF data.
171: */
172: public function exportData($data, $header = false)
173: {
174: if (!is_array($data) || !count($data)) {
175: return '';
176: }
177: $export = '';
178: $mozillaTurbaMap = array_flip($this->_turbaMozillaMap) ;
179: foreach ($data as $row) {
180: $recordData = '';
181: foreach ($this->_mozillaAttr as $value) {
182: if (isset($row[$mozillaTurbaMap[$value]])) {
183: // Base64 encode each value as necessary and store it.
184: // Store cn and mail separately for use in record dn
185: if (!$this->_is_safe_string($row[$mozillaTurbaMap[$value]])) {
186: $recordData .= $value . ':: ' . base64_encode($row[$mozillaTurbaMap[$value]]) . "\n";
187: } else {
188: $recordData .= $value . ': ' . $row[$mozillaTurbaMap[$value]] . "\n";
189: }
190: }
191: }
192:
193: $dn = 'cn=' . $row[$mozillaTurbaMap['cn']] . ',mail=' . $row[$mozillaTurbaMap['mail']];
194: if (!$this->_is_safe_string($dn)) {
195: $export .= 'dn:: ' . base64_encode($dn) . "\n";
196: } else {
197: $export .= 'dn: ' . $dn . "\n";
198: }
199:
200: $export .= "objectclass: top\n"
201: . "objectclass: person\n"
202: . "objectclass: organizationalPerson\n"
203: . "objectclass: inetOrgPerson\n"
204: . "objectclass: mozillaAbPersonAlpha\n"
205: . $recordData . "modifytimestamp: 0Z\n\n";
206: }
207:
208: return $export;
209: }
210:
211: /**
212: * Takes all necessary actions for the given import step, parameters and
213: * form values and returns the next necessary step.
214: *
215: * @param integer $action The current step. One of the IMPORT_* constants.
216: * @param array $param An associative array containing needed
217: * parameters for the current step.
218: *
219: * @return mixed Either the next step as an integer constant or imported
220: * data set after the final step.
221: * @throws Horde_Data_Exception
222: */
223: public function nextStep($action, $param = array())
224: {
225: switch ($action) {
226: case Horde_Data::IMPORT_FILE:
227: parent::nextStep($action, $param);
228:
229: $f_data = $this->importFile($_FILES['import_file']['tmp_name']);
230:
231: $data = array();
232: foreach ($f_data as $record) {
233: $turbaHash = array();
234: foreach ($this->_turbaAttr as $value) {
235: switch ($value) {
236: case 'homeAddress':
237: // These are the keys we're interested in.
238: $keys = array('homeStreet', 'mozillaHomeStreet2',
239: 'mozillaHomeLocalityName', 'mozillaHomeState',
240: 'mozillaHomePostalCode', 'mozillaHomeCountryName');
241:
242: // Grab all of them that exist in $record.
243: $values = array_intersect_key($record, array_flip($keys));
244:
245: // Special handling for State if both State
246: // and Locality Name are set.
247: if (isset($values['mozillaHomeLocalityName'])
248: && isset($values['mozillaHomeState'])) {
249: $values['mozillaHomeLocalityName'] .= ', ' . $values['mozillaHomeState'];
250: unset($values['mozillaHomeState']);
251: }
252:
253: if ($values) {
254: $turbaHash[$value] = implode("\n", $values);
255: }
256: break;
257:
258: case 'workAddress':
259: // These are the keys we're interested in.
260: $keys = array('street', 'mozillaWorkStreet2', 'l',
261: 'st', 'postalCode', 'c');
262:
263: // Grab all of them that exist in $record.
264: $values = array_intersect_key($record, array_flip($keys));
265:
266: // Special handling for "st" if both "st" and
267: // "l" are set.
268: if (isset($values['l']) && isset($values['st'])) {
269: $values['l'] .= ', ' . $values['st'];
270: unset($values['st']);
271: }
272:
273: if ($values) {
274: $turbaHash[$value] = implode("\n", $values);
275: }
276: break;
277:
278: default:
279: if (isset($record[$this->_turbaMozillaMap[$value]])) {
280: $turbaHash[$value] = $record[$this->_turbaMozillaMap[$value]];
281: }
282: break;
283: }
284: }
285:
286: $data[] = $turbaHash;
287: }
288:
289: $GLOBALS['session']->remove('horde', 'import_data/data');
290: return $data;
291:
292: default:
293: return parent::nextStep($action, $param);
294: }
295: }
296:
297: }
298: