Files
server/usr/share/psa-horde/turba/lib/Data/Ldif.php
2026-01-07 20:52:11 +01:00

303 lines
11 KiB
PHP

<?php
/**
* Horde_Data implementation for LDAP Data Interchange Format (LDIF).
*
* Copyright 2007-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (ASL). If you
* did not receive this file, see http://www.horde.org/licenses/apache.
*
* @author Rita Selsky <ritaselsky@gmail.com>
* @package Horde_Data
*/
class Turba_Data_Ldif extends Horde_Data_Base
{
protected $_extension = 'ldif';
protected $_contentType = 'text/ldif';
/**
* Useful Mozilla address book attribute names.
*
* @var array
*/
protected $_mozillaAttr = array(
'cn', 'givenName', 'sn', 'mail', 'mozillaNickname',
'homeStreet', 'mozillaHomeStreet2', 'mozillaHomeLocalityName',
'mozillaHomeState', 'mozillaHomePostalCode',
'mozillaHomeCountryName', 'street',
'mozillaWorkStreet2', 'l', 'st', 'postalCode',
'c', 'homePhone', 'telephoneNumber', 'mobile',
'fax', 'title', 'company', 'description', 'mozillaWorkUrl',
'department', 'mozillaNickname'
);
/**
* Useful Turba address book attribute names.
*
* @var array
*/
protected $_turbaAttr = array(
'name', 'firstname', 'lastname', 'email', 'alias',
'homeAddress', 'homeStreet', 'homeCity',
'homeProvince', 'homePostalCode', 'homeCountry',
'workAddress', 'workStreet', 'workCity', 'workProvince',
'workPostalCode', 'workCountry',
'homePhone', 'workPhone', 'cellPhone',
'fax', 'title', 'company', 'notes', 'website',
'department', 'nickname'
);
/**
* Turba address book attribute names and the corresponding Mozilla name.
*
* @var array
*/
protected $_turbaMozillaMap = array(
'name' => 'cn',
'firstname' => 'givenName',
'lastname' => 'sn',
'email' => 'mail',
'alias' => 'mozillaNickname',
'homePhone' => 'homePhone',
'workPhone' => 'telephoneNumber',
'cellPhone' => 'mobile',
'fax' => 'fax',
'title' => 'title',
'company' => 'company',
'notes' => 'description',
'homeAddress' => 'homeStreet',
'homeStreet' => 'mozillaHomeStreet2',
'homeCity' => 'mozillaHomeLocalityName',
'homeProvince' => 'mozillaHomeState',
'homePostalCode' => 'mozillaHomePostalCode',
'homeCountry' => 'mozillaHomeCountryName',
'workAddress' => 'street',
'workStreet' => 'mozillaWorkStreet2',
'workCity' => 'l',
'workProvince' => 'st',
'workPostalCode' => 'postalCode',
'workCountry' => 'c',
'website' => 'mozillaWorkUrl',
'department' => 'department',
'nickname' => 'mozillaNickname'
);
public function importData($contents, $header = false)
{
$data = array();
$records = preg_split('/(\r?\n){2}/', $contents);
foreach ($records as $record) {
if (trim($record) == '') {
/* Ignore empty records */
continue;
}
/* one key:value pair per line */
$lines = preg_split('/\r?\n/', $record);
$hash = array();
foreach ($lines as $line) {
// [0] = key, [1] = delimiter, [2] = value
$res = preg_split('/(:[:<]?) */', $line, 2, PREG_SPLIT_DELIM_CAPTURE);
if ((count($res) == 3) &&
in_array($res[0], $this->_mozillaAttr)) {
$hash[$res[0]] = ($res[1] == '::')
? base64_decode($res[2])
: $res[2];
}
}
$data[] = $hash;
}
return $data;
}
/**
* Builds a LDIF file from a given data structure and triggers its download.
* It DOES NOT exit the current script but only outputs the correct headers
* and data.
*
* @param string $filename The name of the file to be downloaded.
* @param array $data A two-dimensional array containing the data
* set.
* @param boolean $header If true, the rows of $data are associative
* arrays with field names as their keys.
*/
public function exportFile($filename, $data, $header = false)
{
$export = $this->exportData($data, $header);
$GLOBALS['browser']->downloadHeaders($filename, 'text/ldif', false, strlen($export));
echo $export;
}
/**
* Builds a LDIF file from a given data structure and returns it as a
* string.
*
* @param array $data A two-dimensional array containing the data set.
* @param boolean $header If true, the rows of $data are associative
* arrays with field names as their keys.
*
* @return string The LDIF data.
*/
public function exportData($data, $header = false)
{
if (!is_array($data) || !count($data)) {
return '';
}
$export = '';
$mozillaTurbaMap = array_flip($this->_turbaMozillaMap) ;
foreach ($data as $row) {
$recordData = '';
foreach ($this->_mozillaAttr as $value) {
if (isset($row[$mozillaTurbaMap[$value]])) {
// Base64 encode each value as necessary and store it.
// Store cn and mail separately for use in record dn
if (!$this->_is_safe_string($row[$mozillaTurbaMap[$value]])) {
$recordData .= $value . ':: ' . base64_encode($row[$mozillaTurbaMap[$value]]) . "\n";
} else {
$recordData .= $value . ': ' . $row[$mozillaTurbaMap[$value]] . "\n";
}
}
}
$dn = 'cn=' . $row[$mozillaTurbaMap['cn']] . ',mail=' . $row[$mozillaTurbaMap['mail']];
if (!$this->_is_safe_string($dn)) {
$export .= 'dn:: ' . base64_encode($dn) . "\n";
} else {
$export .= 'dn: ' . $dn . "\n";
}
$export .= "objectclass: top\n"
. "objectclass: person\n"
. "objectclass: organizationalPerson\n"
. "objectclass: inetOrgPerson\n"
. "objectclass: mozillaAbPersonAlpha\n"
. $recordData . "modifytimestamp: 0Z\n\n";
}
return $export;
}
/**
* Takes all necessary actions for the given import step, parameters and
* form values and returns the next necessary step.
*
* @param integer $action The current step. One of the IMPORT_* constants.
* @param array $param An associative array containing needed
* parameters for the current step.
*
* @return mixed Either the next step as an integer constant or imported
* data set after the final step.
* @throws Horde_Data_Exception
*/
public function nextStep($action, array $param = array())
{
switch ($action) {
case Horde_Data::IMPORT_FILE:
parent::nextStep($action, $param);
$f_data = $this->importFile($_FILES['import_file']['tmp_name']);
$data = array();
foreach ($f_data as $record) {
$turbaHash = array();
foreach ($this->_turbaAttr as $value) {
switch ($value) {
case 'homeAddress':
// These are the keys we're interested in.
$keys = array('homeStreet', 'mozillaHomeStreet2',
'mozillaHomeLocalityName', 'mozillaHomeState',
'mozillaHomePostalCode', 'mozillaHomeCountryName');
// Grab all of them that exist in $record.
$values = array_intersect_key($record, array_flip($keys));
// Special handling for State if both State
// and Locality Name are set.
if (isset($values['mozillaHomeLocalityName'])
&& isset($values['mozillaHomeState'])) {
$values['mozillaHomeLocalityName'] .= ', ' . $values['mozillaHomeState'];
unset($values['mozillaHomeState']);
}
if ($values) {
$turbaHash[$value] = implode("\n", $values);
}
break;
case 'workAddress':
// These are the keys we're interested in.
$keys = array('street', 'mozillaWorkStreet2', 'l',
'st', 'postalCode', 'c');
// Grab all of them that exist in $record.
$values = array_intersect_key($record, array_flip($keys));
// Special handling for "st" if both "st" and
// "l" are set.
if (isset($values['l']) && isset($values['st'])) {
$values['l'] .= ', ' . $values['st'];
unset($values['st']);
}
if ($values) {
$turbaHash[$value] = implode("\n", $values);
}
break;
default:
if (isset($record[$this->_turbaMozillaMap[$value]])) {
$turbaHash[$value] = $record[$this->_turbaMozillaMap[$value]];
}
break;
}
}
$data[] = $turbaHash;
}
$this->storage->set('data', null);
return $data;
default:
return parent::nextStep($action, $param);
}
}
/**
* Checks if a string is safe according to RFC 2849, or if it needs to be
* base64 encoded.
*
* @param string $str The string to check.
*
* @return boolean True if the string is safe.
*/
protected function _is_safe_string($str)
{
/* SAFE-CHAR = %x01-09 / %x0B-0C / %x0E-7F
* ; any value <= 127 decimal except NUL, LF,
* ; and CR
*
* SAFE-INIT-CHAR = %x01-09 / %x0B-0C / %x0E-1F /
* %x21-39 / %x3B / %x3D-7F
* ; any value <= 127 except NUL, LF, CR,
* ; SPACE, colon (":", ASCII 58 decimal)
* ; and less-than ("<" , ASCII 60 decimal) */
if (!strlen($str)) {
return true;
}
if ($str[0] == ' ' || $str[0] == ':' || $str[0] == '<') {
return false;
}
for ($i = 0; $i < strlen($str); ++$i) {
if (ord($str[$i]) > 127 || $str[$i] == NULL || $str[$i] == "\n" ||
$str[$i] == "\r") {
return false;
}
}
return true;
}
}