From: Michael M Slusarz Date: Wed, 23 Dec 2009 01:25:08 +0000 (-0700) Subject: Import un-converted framework libs from CVS HEAD X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=9cb1db2414871d3c98a0ebed0d11288c84cb384f;p=horde.git Import un-converted framework libs from CVS HEAD --- diff --git a/framework/Alarm/Alarm.php b/framework/Alarm/Alarm.php new file mode 100644 index 000000000..35410e046 --- /dev/null +++ b/framework/Alarm/Alarm.php @@ -0,0 +1,580 @@ + + * @package Horde_Alarm + */ +class Horde_Alarm { + + /** + * Hash containing connection parameters. + * + * @var array + */ + var $_params = array('ttl' => 300); + + /** + * An error message to throw when something is wrong. + * + * @var string + */ + var $_errormsg; + + /** + * Constructor - just store the $params in our newly-created object. All + * other work is done by initialize(). + * + * @param array $params Any parameters needed for this driver. + */ + function Horde_Alarm($params = array(), $errormsg = null) + { + $this->_params = array_merge($this->_params, $params); + if ($errormsg === null) { + $this->_errormsg = _("The alarm backend is not currently available."); + } else { + $this->_errormsg = $errormsg; + } + } + + /** + * Returns an alarm hash from the backend. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * + * @return array An alarm hash. + */ + function get($id, $user) + { + $alarm = $this->_get($id, $user); + if (is_a($alarm, 'PEAR_Error')) { + return $alarm; + } + if (isset($alarm['mail']['body'])) { + $alarm['mail']['body'] = $this->_fromDriver($alarm['mail']['body']); + } + return $alarm; + } + + /** + * Stores an alarm hash in the backend. + * + * The alarm will be added if it doesn't exist, and updated otherwise. + * + * @param array $alarm An alarm hash. + */ + function set($alarm) + { + if (isset($alarm['mail']['body'])) { + $alarm['mail']['body'] = $this->_toDriver($alarm['mail']['body']); + } + if ($this->exists($alarm['id'], isset($alarm['user']) ? $alarm['user'] : '')) { + return $this->_update($alarm); + } else { + return $this->_add($alarm); + } + } + + /** + * Returns whether an alarm with the given id exists already. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * + * @return boolean True if the specified alarm exists. + */ + function exists($id, $user) + { + $exists = $this->_exists($id, $user); + return $exists && !is_a($exists, 'PEAR_Error'); + } + + /** + * Delays (snoozes) an alarm for a certain period. + * + * @param string $id The alarm's unique id. + * @param string $user The notified user. + * @param integer $minutes The delay in minutes. A negative value + * dismisses the alarm completely. + */ + function snooze($id, $user, $minutes) + { + $alarm = $this->get($id, $user); + if (is_a($alarm, 'PEAR_Error')) { + return $alarm; + } + if (empty($user)) { + return PEAR::raiseError(_("This alarm cannot be snoozed.")); + } + if ($alarm) { + if ($minutes > 0) { + $alarm['snooze'] = new Horde_Date(time()); + $alarm['snooze']->min += $minutes; + return $this->_snooze($id, $user, $alarm['snooze']); + } else { + return $this->_dismiss($id, $user); + } + } + } + + /** + * Returns whether an alarm is snoozed. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * @param Horde_Date $time The time when the alarm may be snoozed. + * Defaults to now. + * + * @return boolean True if the alarm is snoozed. + */ + function isSnoozed($id, $user, $time = null) + { + if (is_null($time)) { + $time = new Horde_Date(time()); + } + return (bool)$this->_isSnoozed($id, $user, $time); + } + + /** + * Deletes an alarm from the backend. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user. All users' alarms if null. + */ + function delete($id, $user = null) + { + return $this->_delete($id, $user); + } + + /** + * Retrieves active alarms from all applications and stores them in the + * backend. + * + * The applications will only be called once in the configured time span, + * by default 5 minutes. + * + * @param string $user Retrieve alarms for this user, or for all users + * if null. + * @param boolean $preload Preload alarms that go off within the next + * ttl time span? + */ + function load($user = null, $preload = true) + { + if (isset($_SESSION['horde']['alarm']['loaded']) && + time() - $_SESSION['horde']['alarm']['loaded'] < $this->_params['ttl']) { + return; + } + + $apps = $GLOBALS['registry']->listApps(null, false, Horde_Perms::READ); + if (is_a($apps, 'PEAR_Error')) { + return false; + } + foreach ($apps as $app) { + if ($GLOBALS['registry']->hasMethod('listAlarms', $app)) { + try { + $pushed = $GLOBALS['registry']->pushApp($app, array('check_perms' => false)); + } catch (Horde_Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + continue; + } + /* Preload alarms that happen in the next ttl seconds. */ + if ($preload) { + try { + $alarms = $GLOBALS['registry']->callByPackage($app, 'listAlarms', array(time() + $this->_params['ttl'], $user)); + } catch (Horde_Exception $e) { + if ($pushed) { + $GLOBALS['registry']->popApp(); + } + continue; + } + } else { + $alarms = array(); + } + + /* Load current alarms if no preloading requested or if this + * is the first call in this session. */ + if (!$preload || !isset($_SESSION['horde']['alarm']['loaded'])) { + try { + $app_alarms = $GLOBALS['registry']->callByPackage($app, 'listAlarms', array(time(), $user)); + } catch (Horde_Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + $app_alarms = array(); + } + $alarms = array_merge($alarms, $app_alarms); + } + + if ($pushed) { + $GLOBALS['registry']->popApp(); + } + + if (empty($alarms)) { + continue; + } + + foreach ($alarms as $alarm) { + $alarm['start'] = new Horde_Date($alarm['start']); + if (!empty($alarm['end'])) { + $alarm['end'] = new Horde_Date($alarm['end']); + } + $this->set($alarm); + } + } + } + + $_SESSION['horde']['alarm']['loaded'] = time(); + } + + /** + * Returns a list of alarms from the backend. + * + * @param string $user Return alarms for this user, all users if + * null, or global alarms if empty. + * @param Horde_Date $time The time when the alarms should be active. + * Defaults to now. + * @param boolean $load Update active alarms from all applications? + * @param boolean $preload Preload alarms that go off within the next + * ttl time span? + * + * @return array A list of alarm hashes. + */ + function listAlarms($user = null, $time = null, $load = false, + $preload = true) + { + if (empty($time)) { + $time = new Horde_Date(time()); + } + if ($load) { + $this->load($user, $preload); + } + + $alarms = $this->_list($user, $time); + if (is_a($alarms, 'PEAR_Error')) { + return $alarms; + } + + foreach (array_keys($alarms) as $alarm) { + if (isset($alarms[$alarm]['mail']['body'])) { + $alarms[$alarm]['mail']['body'] = $this->_fromDriver($alarms[$alarm]['mail']['body']); + } + } + return $alarms; + } + + /** + * Notifies the user about any active alarms. + * + * @param string $user Notify this user, all users if null, or guest + * users if empty. + * @param boolean $load Update active alarms from all applications? + * @param boolean $preload Preload alarms that go off within the next + * ttl time span? + * @param array $exclude Don't notify with these methods. + */ + function notify($user = null, $load = true, $preload = true, + $exclude = array()) + { + $alarms = $this->listAlarms($user, null, $load, $preload); + if (is_a($alarms, 'PEAR_Error')) { + Horde::logMessage($alarms, __FILE__, __LINE__, PEAR_LOG_ERR); + return $alarms; + } + if (empty($alarms)) { + return; + } + + $methods = array_keys($this->notificationMethods()); + foreach ($alarms as $alarm) { + foreach ($alarm['methods'] as $alarm_method) { + if (in_array($alarm_method, $methods) && + !in_array($alarm_method, $exclude)) { + $result = $this->{'_' . $alarm_method}($alarm); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + } + } + } + } + } + + /** + * Notifies about an alarm through Horde_Notification. + * + * @param array $alarm An alarm hash. + */ + function _notify($alarm) + { + static $sound_played; + + $GLOBALS['notification']->push($alarm['title'], 'horde.alarm', array('alarm' => $alarm)); + if (!empty($alarm['params']['notify']['sound']) && + !isset($sound_played[$alarm['params']['notify']['sound']])) { + require_once 'Horde/Notification/Listener/Audio.php'; + $GLOBALS['notification']->attach('audio'); + $GLOBALS['notification']->push($alarm['params']['notify']['sound'], 'audio'); + $sound_played[$alarm['params']['notify']['sound']] = true; + } + } + + /** + * Notifies about an alarm by email. + * + * @param array $alarm An alarm hash. + */ + function _mail($alarm) + { + if (!empty($alarm['internal']['mail']['sent'])) { + return; + } + + if (empty($alarm['params']['mail']['email'])) { + if (empty($alarm['user'])) { + return; + } + $identity = Horde_Prefs_Identity::singleton('none', $alarm['user']); + $email = $identity->getDefaultFromAddress(true); + } else { + $email = $alarm['params']['mail']['email']; + } + + $mail = new Horde_Mime_Mail(array( + 'subject' => $alarm['title'], + 'body' => empty($alarm['params']['mail']['body']) ? $alarm['text'] : $alarm['params']['mail']['body'], + 'to' => $email, + 'from' => $email, + 'charset' => Horde_Nls::getCharset())); + $mail->addHeader('Auto-Submitted', 'auto-generated'); + $mail->addHeader('X-Horde-Alarm', $alarm['title'], Horde_Nls::getCharset()); + $sent = $mail->send(Horde::getMailerConfig()); + if (is_a($sent, 'PEAR_Error')) { + return $sent; + } + + $alarm['internal']['mail']['sent'] = true; + $this->_internal($alarm['id'], $alarm['user'], $alarm['internal']); + } + + /** + * Notifies about an alarm with an SMS through the sms/send API method. + * + * @param array $alarm An alarm hash. + */ + function _sms($alarm) + { + } + + /** + * Returns a list of available notification methods and method parameters. + * + * The returned list is a hash with method names as the keys and + * optionally associated parameters as values. The parameters are hashes + * again with parameter names as keys and parameter information as + * values. The parameter information is hash with the following keys: + * 'desc' contains a parameter description; 'required' specifies whether + * this parameter is required. + * + * @return array List of methods and parameters. + */ + function notificationMethods() + { + static $methods; + + if (!isset($methods)) { + $methods = array('notify' => array( + '__desc' => _("Inline Notification"), + 'sound' => array('type' => 'sound', + 'desc' => _("Play a sound?"), + 'required' => false)), + 'mail' => array( + '__desc' => _("Email Notification"), + 'email' => array('type' => 'text', + 'desc' => _("Email address (optional)"), + 'required' => false))); + /* + if ($GLOBALS['registry']->hasMethod('sms/send')) { + $methods['sms'] = array( + 'phone' => array('type' => 'text', + 'desc' => _("Cell phone number"), + 'required' => true)); + } + */ + } + + return $methods; + } + + /** + * Garbage collects old alarms in the backend. + */ + function gc() + { + /* A 1% chance we will run garbage collection during a call. */ + if (rand(0, 99) != 0) { + return; + } + + return $this->_gc(); + } + + /** + * Attempts to return a concrete Horde_Alarm instance based on $driver. + * + * @param string $driver The type of concrete Horde_Alarm subclass to + * return. The class name is based on the storage + * driver ($driver). The code is dynamically + * included. + * @param array $params A hash containing any additional configuration + * or connection parameters a subclass might need. + * + * @return mixed The newly created concrete Horde_Alarm instance, or false + * on an error. + */ + static function factory($driver = null, $params = null) + { + if (is_null($driver)) { + $driver = empty($GLOBALS['conf']['alarms']['driver']) ? 'sql' : $GLOBALS['conf']['alarms']['driver']; + } + + $driver = basename($driver); + + if (is_null($params)) { + $params = Horde::getDriverConfig('alarms', $driver); + } + + $class = 'Horde_Alarm_' . $driver; + if (class_exists($class)) { + $alarm = new $class($params); + $result = $alarm->initialize(); + if (is_a($result, 'PEAR_Error')) { + $alarm = new Horde_Alarm($params, sprintf(_("The alarm backend is not currently available: %s"), $result->getMessage())); + } else { + $alarm->gc(); + } + } else { + $alarm = new Horde_Alarm($params, sprintf(_("Unable to load the definition of %s."), $class)); + } + + return $alarm; + } + + /** + * Converts a value from the driver's charset. + * + * @param mixed $value Value to convert. + * + * @return mixed Converted value. + */ + function _fromDriver($value) + { + return $value; + } + + /** + * Converts a value to the driver's charset. + * + * @param mixed $value Value to convert. + * + * @return mixed Converted value. + */ + function _toDriver($value) + { + return $value; + } + + /** + * @abstract + */ + function _get() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _list() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _add() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _update() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _internal() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _exists() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _snooze() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _isSnoozed() + { + return PEAR::raiseError($this->_errormsg); + } + + /** + * @abstract + */ + function _delete() + { + return PEAR::raiseError($this->_errormsg); + } + +} diff --git a/framework/Alarm/Alarm/sql.php b/framework/Alarm/Alarm/sql.php new file mode 100644 index 000000000..2581e6ab5 --- /dev/null +++ b/framework/Alarm/Alarm/sql.php @@ -0,0 +1,477 @@ + + * 'phptype' The database type (e.g. 'pgsql', 'mysql', etc.). + * 'charset' The database's internal charset. + * + * Optional values for $params:
+ *      'table'         The name of the foo table in 'database'.
+ *
+ * Required by some database implementations:
+ *      'database'      The name of the database.
+ *      'hostspec'      The hostname of the database server.
+ *      'protocol'      The communication protocol ('tcp', 'unix', etc.).
+ *      'username'      The username with which to connect to the database.
+ *      'password'      The password associated with 'username'.
+ *      'options'       Additional options to pass to the database.
+ *      'tty'           The TTY on which to connect to the database.
+ *      'port'          The port on which to connect to the database.
+ * + * The table structure can be created by the scripts/sql/horde_alarm.sql + * script. + * + * @author Jan Schneider + * @since Horde 3.2 + * @package Horde_Alarm + */ +class Horde_Alarm_sql extends Horde_Alarm { + + /** + * Handle for the current database connection. + * + * @var DB + */ + var $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $_db if a separate write database is not required. + * + * @var DB + */ + var $_write_db; + + /** + * Constructs a new SQL storage object. + * + * @param array $params A hash containing connection parameters. + */ + function Horde_Alarm_sql($params = array()) + { + $this->_params = array_merge($this->_params, $params); + } + + /** + * Converts a value from the driver's charset. + * + * @param mixed $value Value to convert. + * + * @return mixed Converted value. + */ + function _fromDriver($value) + { + return Horde_String::convertCharset($value, $this->_params['charset']); + } + + /** + * Converts a value to the driver's charset. + * + * @param mixed $value Value to convert. + * + * @return mixed Converted value. + */ + function _toDriver($value) + { + return Horde_String::convertCharset($value, Horde_Nls::getCharset(), $this->_params['charset']); + } + + /** + * Returns an alarm hash from the backend. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * + * @return array An alarm hash. + */ + function _get($id, $user) + { + $query = sprintf('SELECT alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze, alarm_internal FROM %s WHERE alarm_id = ? AND %s', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + Horde::logMessage('SQL query by Horde_Alarm_sql::_get(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $alarm = $this->_db->getRow($query, array($id, $user), DB_FETCHMODE_ASSOC); + if (is_a($alarm, 'PEAR_Error')) { + Horde::logMessage($alarm, __FILE__, __LINE__); + return $alarm; + } + if (empty($alarm)) { + return PEAR::raiseError(_("Alarm not found")); + } + $alarm = array( + 'id' => $alarm['alarm_id'], + 'user' => $alarm['alarm_uid'], + 'start' => new Horde_Date($alarm['alarm_start']), + 'end' => empty($alarm['alarm_end']) ? null : new Horde_Date($alarm['alarm_end']), + 'methods' => @unserialize($alarm['alarm_methods']), + 'params' => @unserialize($alarm['alarm_params']), + 'title' => $this->_fromDriver($alarm['alarm_title']), + 'text' => $this->_fromDriver($alarm['alarm_text']), + 'snooze' => empty($alarm['alarm_snooze']) ? null : new Horde_Date($alarm['alarm_snooze']), + 'internal' => empty($alarm['alarm_internal']) ? null : @unserialize($alarm['alarm_internal'])); + return $alarm; + } + + /** + * Returns a list of alarms from the backend. + * + * @param Horde_Date $time The time when the alarms should be active. + * @param string $user Return alarms for this user, all users if + * null, or global alarms if empty. + * + * @return array A list of alarm hashes. + */ + function _list($user, $time) + { + $query = sprintf('SELECT alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze, alarm_internal FROM %s WHERE alarm_dismissed = 0 AND ((alarm_snooze IS NULL AND alarm_start <= ?) OR alarm_snooze <= ?) AND (alarm_end IS NULL OR alarm_end >= ?)%s ORDER BY alarm_start, alarm_end', + $this->_params['table'], + is_null($user) ? '' : ' AND (alarm_uid IS NULL OR alarm_uid = ? OR alarm_uid = ?)'); + $dt = $time->format('Y-m-d\TH:i:s'); + $values = array($dt, $dt, $dt); + if (!is_null($user)) { + $values[] = ''; + $values[] = (string)$user; + } + Horde::logMessage('SQL query by Horde_Alarm_sql::_list(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $alarms = array(); + $result = $this->_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + return $result; + } + while ($alarm = $result->fetchRow(DB_FETCHMODE_ASSOC)) { + if (is_a($alarm, 'PEAR_Error')) { + Horde::logMessage($alarm, __FILE__, __LINE__); + return $alarm; + } + $alarms[$alarm['alarm_id']] = array( + 'id' => $alarm['alarm_id'], + 'user' => $alarm['alarm_uid'], + 'start' => new Horde_Date($alarm['alarm_start']), + 'end' => empty($alarm['alarm_end']) ? null : new Horde_Date($alarm['alarm_end']), + 'methods' => @unserialize($alarm['alarm_methods']), + 'params' => @unserialize($alarm['alarm_params']), + 'title' => $this->_fromDriver($alarm['alarm_title']), + 'text' => $this->_fromDriver($alarm['alarm_text']), + 'snooze' => empty($alarm['alarm_snooze']) ? null : new Horde_Date($alarm['alarm_snooze']), + 'internal' => empty($alarm['alarm_internal']) ? null : @unserialize($alarm['alarm_internal'])); + } + + return $alarms; + } + + /** + * Adds an alarm hash to the backend. + * + * @param array $alarm An alarm hash. + */ + function _add($alarm) + { + $query = sprintf('INSERT INTO %s (alarm_id, alarm_uid, alarm_start, alarm_end, alarm_methods, alarm_params, alarm_title, alarm_text, alarm_snooze) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', $this->_params['table']); + $values = array($alarm['id'], + isset($alarm['user']) ? $alarm['user'] : '', + (string)$alarm['start'], + empty($alarm['end']) ? null : (string)$alarm['end'], + serialize($alarm['methods']), + serialize($alarm['params']), + $this->_toDriver($alarm['title']), + empty($alarm['text']) ? null : $this->_toDriver($alarm['text']), + null); + Horde::logMessage('SQL query by Horde_Alarm_sql::_add(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Updates an alarm hash in the backend. + * + * @param array $alarm An alarm hash. + */ + function _update($alarm) + { + $query = sprintf('UPDATE %s set alarm_start = ?, alarm_end = ?, alarm_methods = ?, alarm_params = ?, alarm_title = ?, alarm_text = ? WHERE alarm_id = ? AND %s', + $this->_params['table'], + isset($alarm['user']) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + $values = array((string)$alarm['start'], + empty($alarm['end']) ? null : (string)$alarm['end'], + serialize($alarm['methods']), + serialize($alarm['params']), + $this->_toDriver($alarm['title']), + empty($alarm['text']) + ? null + : $this->_toDriver($alarm['text']), + $alarm['id'], + isset($alarm['user']) ? $alarm['user'] : ''); + Horde::logMessage('SQL query by Horde_Alarm_sql::_update(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Updates internal alarm properties, i.e. properties not determined by + * the application setting the alarm. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * @param array $internal A hash with the internal data. + */ + function _internal($id, $user, $internal) + { + $query = sprintf('UPDATE %s set alarm_internal = ? WHERE alarm_id = ? AND %s', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + $values = array(serialize($internal), $id, $user); + Horde::logMessage('SQL query by Horde_Alarm_sql::_internal(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Returns whether an alarm with the given id exists already. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * + * @return boolean True if the specified alarm exists. + */ + function _exists($id, $user) + { + $query = sprintf('SELECT 1 FROM %s WHERE alarm_id = ? AND %s', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + Horde::logMessage('SQL query by Horde_Alarm_sql::_exists(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->getOne($query, array($id, $user)); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Delays (snoozes) an alarm for a certain period. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * @param Horde_Date $snooze The snooze time. + */ + function _snooze($id, $user, $snooze) + { + $query = sprintf('UPDATE %s set alarm_snooze = ? WHERE alarm_id = ? AND %s', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + $values = array((string)$snooze, $id, $user); + Horde::logMessage('SQL query by Horde_Alarm_sql::_snooze(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Dismisses an alarm. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + */ + function _dismiss($id, $user) + { + $query = sprintf('UPDATE %s set alarm_dismissed = 1 WHERE alarm_id = ? AND %s', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + $values = array($id, $user); + Horde::logMessage('SQL query by Horde_Alarm_sql::_dismiss(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Returns whether an alarm is snoozed. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user + * @param Horde_Date $time The time when the alarm may be snoozed. + * + * @return boolean True if the alarm is snoozed. + */ + function _isSnoozed($id, $user, $time) + { + $query = sprintf('SELECT 1 FROM %s WHERE alarm_id = ? AND %s AND (alarm_dismissed = 1 OR (alarm_snooze IS NOT NULL AND alarm_snooze >= ?))', + $this->_params['table'], + !empty($user) ? 'alarm_uid = ?' : '(alarm_uid = ? OR alarm_uid IS NULL)'); + Horde::logMessage('SQL query by Horde_Alarm_sql::_isSnoozed(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->getOne($query, array($id, $user, (string)$time)); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Deletes an alarm from the backend. + * + * @param string $id The alarm's unique id. + * @param string $user The alarm's user. All users' alarms if null. + */ + function _delete($id, $user = null) + { + $query = sprintf('DELETE FROM %s WHERE alarm_id = ?', $this->_params['table']); + $values = array($id); + if (!is_null($user)) { + $query .= empty($user) + ? ' AND (alarm_uid IS NULL OR alarm_uid = ?)' + : ' AND alarm_uid = ?'; + $values[] = $user; + } + Horde::logMessage('SQL query by Horde_Alarm_sql::_delete(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Garbage collects old alarms in the backend. + */ + function _gc() + { + $query = sprintf('DELETE FROM %s WHERE alarm_end IS NOT NULL AND alarm_end < ?', $this->_params['table']); + Horde::logMessage('SQL query by Horde_Alarm_sql::_gc(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $end = new Horde_Date(time()); + $result = $this->_write_db->query($query, (string)$end); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__); + } + return $result; + } + + /** + * Attempts to open a connection to the SQL server. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function initialize() + { + Horde::assertDriverConfig($this->_params, 'sql', + array('phptype', 'charset')); + + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + if (!isset($this->_params['table'])) { + $this->_params['table'] = 'horde_alarms'; + } + + /* Connect to the SQL server using the supplied parameters. */ + require_once 'DB.php'; + $this->_write_db = &DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']), + 'ssl' => !empty($this->_params['ssl']))); + if (is_a($this->_write_db, 'PEAR_Error')) { + Horde::logMessage($this->_write_db, __FILE__, __LINE__); + return $this->_write_db; + } + $this->_initConn($this->_write_db); + + /* Check if we need to set up the read DB connection seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = &DB::connect($params, + array('persistent' => !empty($params['persistent']), + 'ssl' => !empty($params['ssl']))); + if (is_a($this->_db, 'PEAR_Error')) { + Horde::logMessage($this->_db, __FILE__, __LINE__); + return $this->_db; + } + $this->_initConn($this->_db); + } else { + /* Default to the same DB handle for the writer too. */ + $this->_db = &$this->_write_db; + } + + return true; + } + + /** + */ + function _initConn(&$db) + { + // Set DB portability options. + switch ($db->phptype) { + case 'mssql': + $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + + default: + $db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + /* Handle any database specific initialization code to run. */ + switch ($db->dbsyntax) { + case 'oci8': + $query = "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"; + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('SQL connection setup for Alarms, query = "%s"', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $db->query($query); + break; + + case 'pgsql': + $query = "SET datestyle TO 'iso'"; + + /* Log the query at a DEBUG log level. */ + Horde::logMessage(sprintf('SQL connection setup for Alarms, query = "%s"', $query), + __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $db->query($query); + break; + } + } + +} diff --git a/framework/Alarm/package.xml b/framework/Alarm/package.xml new file mode 100644 index 000000000..79c5968dc --- /dev/null +++ b/framework/Alarm/package.xml @@ -0,0 +1,56 @@ + + + Horde_Alarm + pear.horde.org + Horde alarm libraries + This package provides an interface to deal with reminders, + alarms and notifications through a standardized API. The following + notification methods are available at the moment: standard Horde + notifications, popups, emails, sms. + + Jan Schneider + jan + jan@horde.org + yes + + 2007-02-01 + + 0.1.0 + 0.1.0 + + + beta + beta + + LGPL + * Initial release. + + + + + + + + + + + + + + + 4.3.0 + + + 1.4.0b1 + + + Date + pear.horde.org + + + + + diff --git a/framework/Alarm/tests/001.phpt b/framework/Alarm/tests/001.phpt new file mode 100644 index 000000000..814c234f3 --- /dev/null +++ b/framework/Alarm/tests/001.phpt @@ -0,0 +1,487 @@ +--TEST-- +Horde_Alarm tests. +--SKIPIF-- + +--FILE-- + 'personalalarm', + 'user' => 'john', + 'start' => $date, + 'end' => $end, + 'methods' => array(), + 'params' => array(), + 'title' => 'This is a personal alarm.'); + +var_dump($alarm->set($hash)); +var_dump($alarm->exists('personalalarm', 'john')); +$saved = $alarm->get('personalalarm', 'john'); +var_dump($saved); +var_dump($saved['start']->compareDateTime($date)); +$hash['title'] = 'Changed alarm text'; +var_dump($alarm->set($hash)); +$date->min--; +$alarm->set(array('id' => 'publicalarm', + 'start' => $date, + 'end' => $end, + 'methods' => array(), + 'params' => array(), + 'title' => 'This is a public alarm.')); +var_dump($alarm->listAlarms('john')); +var_dump($alarm->delete('publicalarm', '')); +var_dump($alarm->listAlarms('john')); +$error = $alarm->snooze('personalalarm', 'jane', 30); +var_dump($error->getMessage()); +var_dump($alarm->snooze('personalalarm', 'john', 30)); +var_dump($alarm->isSnoozed('personalalarm', 'john')); +var_dump($alarm->listAlarms('john')); +var_dump($alarm->listAlarms('john', $end)); +var_dump($alarm->set(array('id' => 'noend', + 'user' => 'john', + 'start' => $date, + 'methods' => array('notify'), + 'params' => array(), + 'title' => 'This is an alarm without end.'))); +var_dump($alarm->listAlarms('john', $end)); +var_dump($alarm->delete('noend', 'john')); +var_dump($alarm->delete('personalalarm', 'john')); + +?> +--EXPECTF-- +int(1) +bool(true) +array(10) { + ["id"]=> + string(13) "personalalarm" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(25) "This is a personal alarm." + ["text"]=> + NULL + ["snooze"]=> + NULL + ["internal"]=> + NULL +} +int(0) +int(1) +array(2) { + ["publicalarm"]=> + array(10) { + ["id"]=> + string(11) "publicalarm" + ["user"]=> + string(0) "" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(23) "This is a public alarm." + ["text"]=> + NULL + ["snooze"]=> + NULL + ["internal"]=> + NULL + } + ["personalalarm"]=> + array(10) { + ["id"]=> + string(13) "personalalarm" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(18) "Changed alarm text" + ["text"]=> + NULL + ["snooze"]=> + NULL + ["internal"]=> + NULL + } +} +int(1) +array(1) { + ["personalalarm"]=> + array(10) { + ["id"]=> + string(13) "personalalarm" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(18) "Changed alarm text" + ["text"]=> + NULL + ["snooze"]=> + NULL + ["internal"]=> + NULL + } +} +string(15) "Alarm not found" +int(1) +bool(true) +array(0) { +} +array(1) { + ["personalalarm"]=> + array(10) { + ["id"]=> + string(13) "personalalarm" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(18) "Changed alarm text" + ["text"]=> + NULL + ["snooze"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["internal"]=> + NULL + } +} +int(1) +array(2) { + ["noend"]=> + array(10) { + ["id"]=> + string(5) "noend" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + NULL + ["methods"]=> + array(1) { + [0]=> + string(6) "notify" + } + ["params"]=> + array(0) { + } + ["title"]=> + string(29) "This is an alarm without end." + ["text"]=> + NULL + ["snooze"]=> + NULL + ["internal"]=> + NULL + } + ["personalalarm"]=> + array(10) { + ["id"]=> + string(13) "personalalarm" + ["user"]=> + string(4) "john" + ["start"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["end"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["methods"]=> + array(0) { + } + ["params"]=> + array(0) { + } + ["title"]=> + string(18) "Changed alarm text" + ["text"]=> + NULL + ["snooze"]=> + object(horde_date)(7) { + ["year"]=> + int(%d%d%d%d) + ["month"]=> + int(%d) + ["mday"]=> + int(%d) + ["hour"]=> + int(%d) + ["min"]=> + int(%d) + ["sec"]=> + int(%d) + ["_supportedSpecs"]=> + string(21) "%CdDeHImMnRStTyYbBpxX" + } + ["internal"]=> + NULL + } +} +int(1) +int(1) diff --git a/framework/Alarm/tests/setup.inc.dist b/framework/Alarm/tests/setup.inc.dist new file mode 100644 index 000000000..996437ca9 --- /dev/null +++ b/framework/Alarm/tests/setup.inc.dist @@ -0,0 +1,14 @@ + 'mysql', + 'database' => 'horde', + 'username' => 'horde', + 'password' => 'horde', + 'charset' => 'iso-8859-1'); + +require_once 'Log.php'; +$conf['log'] = array('priority' => PEAR_LOG_DEBUG, + 'ident' => 'HEADHORDE', + 'params' => array('append' => true), + 'name' => '/tmp/horde.log', + 'type' => 'file', + 'enabled' => true); diff --git a/framework/DOM/DOM.php b/framework/DOM/DOM.php new file mode 100644 index 000000000..e80fd203f --- /dev/null +++ b/framework/DOM/DOM.php @@ -0,0 +1,640 @@ + + * @author Michael J. Rubinsky + * @since Horde 3.2 + * @package Horde_DOM + */ + +/** PEAR */ +require_once 'PEAR.php'; + +/** Validate against the DTD */ +define('HORDE_DOM_LOAD_VALIDATE', 1); + +/** Recover from load errors */ +define('HORDE_DOM_LOAD_RECOVER', 2); + +/** Remove redundant whitespace */ +define('HORDE_DOM_LOAD_REMOVE_BLANKS', 4); + +/** Substitute XML entities */ +define('HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES', 8); + +class Horde_DOM_Document extends Horde_DOM_Node { + + /** + * Creates an appropriate object based on the version of PHP that is + * running and the requested xml source. This function should be passed an + * array containing either 'filename' => $filename | 'xml' => $xmlstring + * depending on the source of the XML document. + * + * You can pass an optional 'options' parameter to enable extra + * features like DTD validation or removal of whitespaces. + * For a list of available features see the HORDE_DOM_LOAD defines. + * Multiple options are added by bitwise OR. + * + * @param array $params Array containing either 'filename' | 'xml' keys. + * You can specify an optional 'options' key. + * + * @return mixed PHP 4 domxml document | Horde_DOM_Document | PEAR_Error + */ + function factory($params = array()) + { + if (!isset($params['options'])) { + $params['options'] = 0; + } + + if (version_compare(PHP_VERSION, '5', '>=')) { + // PHP 5 with Horde_DOM. Let Horde_DOM determine + // if we are a file or string. + $doc = new Horde_DOM_Document($params); + if (isset($doc->error)) { + return $doc->error; + } + return $doc; + } + + // Load mode + if ($params['options'] & HORDE_DOM_LOAD_VALIDATE) { + $options = DOMXML_LOAD_VALIDATING; + } elseif ($params['options'] & HORDE_DOM_LOAD_RECOVER) { + $options = DOMXML_LOAD_RECOVERING; + } else { + $options = DOMXML_LOAD_PARSING; + } + + // Load options + if ($params['options'] & HORDE_DOM_LOAD_REMOVE_BLANKS) { + $options |= DOMXML_LOAD_DONT_KEEP_BLANKS; + } + if ($params['options'] & HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES) { + $options |= DOMXML_LOAD_SUBSTITUTE_ENTITIES; + } + + if (isset($params['filename'])) { + if (function_exists('domxml_open_file')) { + // PHP 4 with domxml and XML file + return domxml_open_file($params['filename'], $options); + } + } elseif (isset($params['xml'])) { + if (function_exists('domxml_open_mem')) { + // PHP 4 with domxml and XML string. + $result = @domxml_open_mem($params['xml'], $options); + if (!$result) { + return PEAR::raiseError('Failed loading XML document.'); + } + return $result; + } + } elseif (function_exists('domxml_new_doc')) { + // PHP 4 creating a blank doc. + return domxml_new_doc('1.0'); + } + + // No DOM support - raise error. + return PEAR::raiseError('DOM support not present.'); + } + + /** + * Constructor. Determine if we are trying to load a file or xml string + * based on the parameters. + * + * @param array $params Array with key 'filename' | 'xml' + */ + function Horde_DOM_Document($params = array()) + { + $this->node = new DOMDocument(); + + // Load mode + if ($params['options'] & HORDE_DOM_LOAD_VALIDATE) { + $this->node->validateOnParse = true; + } elseif ($params['options'] & HORDE_DOM_LOAD_RECOVER) { + $this->node->recover = true; + } + + // Load options + if ($params['options'] & HORDE_DOM_LOAD_REMOVE_BLANKS) { + $this->node->preserveWhiteSpace = false; + } + if ($params['options'] & HORDE_DOM_LOAD_SUBSTITUTE_ENTITIES) { + $this->node->substituteEntities = true; + } + + if (isset($params['xml'])) { + $result = @$this->node->loadXML($params['xml']); + if (!$result) { + $this->error = PEAR::raiseError('Failed loading XML document.'); + return; + } + } elseif (isset($params['filename'])) { + $this->node->load($params['filename']); + } + $this->document = $this; + } + + /** + * Return the root document element. + */ + function root() + { + return new Horde_DOM_Element($this->node->documentElement, $this); + } + + /** + * Return the document element. + */ + function document_element() + { + return $this->root(); + } + + /** + * Return the node represented by the requested tagname. + * + * @param string $name The tagname requested. + * + * @return array The nodes matching the tag name + */ + function get_elements_by_tagname($name) + { + $list = $this->node->getElementsByTagName($name); + $nodes = array(); + $i = 0; + if (isset($list)) { + while ($node = $list->item($i)) { + $nodes[] = $this->_newDOMElement($node, $this); + ++$i; + } + return $nodes; + } + } + + /** + * Return the document as a text string. + * + * @param bool $format Specifies whether the output should be + * neatly formatted, or not + * @param string $encoding Sets the encoding attribute in line + * + * + * @return string The xml document as a string + */ + function dump_mem($format = false, $encoding = false) + { + $format0 = $this->node->formatOutput; + $this->node->formatOutput = $format; + + $encoding0 = $this->node->encoding; + if ($encoding) { + $this->node->encoding=$encoding; + } + + $dump = $this->node->saveXML(); + + $this->node->formatOutput = $format0; + + // UTF-8 is the default encoding for XML. + if ($encoding) { + $this->node->encoding = $encoding0 == '' ? 'UTF-8' : $encoding0; + } + + return $dump; + } + + /** + * Create a new element for this document + * + * @param string $name Name of the new element + * + * @return Horde_DOM_Element New element + */ + function create_element($name) + { + return new Horde_DOM_Element($this->node->createElement($name), $this); + } + + /** + * Create a new text node for this document + * + * @param string $content The content of the text element + * + * @return Horde_DOM_Node New node + */ + function create_text_node($content) + { + return new Horde_DOM_Text($this->node->createTextNode($content), $this); + } + + /** + * Create a new attribute for this document + * + * @param string $name The name of the attribute + * @param string $value The value of the attribute + * + * @return Horde_DOM_Attribute New attribute + */ + function create_attribute($name, $value) + { + $attr = $this->node->createAttribute($name); + $attr->value = $value; + return new Horde_DOM_Attribute($attr, $this); + } + + function xpath_new_context() + { + return Horde_DOM_XPath::factory($this->node); + } +} + +/** + * @package Horde_DOM + */ +class Horde_DOM_Node { + + var $node; + var $document; + + /** + * Wrap a DOMNode into the Horde_DOM_Node class. + * + * @param DOMNode $node The DOMXML node + * @param Horde_DOM_Document $document The parent document + * + * @return Horde_DOM_Node The wrapped node + */ + function Horde_DOM_Node($domNode, $domDocument = null) + { + $this->node = $domNode; + $this->document = $domDocument; + } + + function __get($name) + { + switch ($name) { + case 'type': + return $this->node->nodeType; + + case 'tagname': + return $this->node->tagName; + + case 'content': + return $this->node->textContent; + + default: + return false; + } + } + + /** + * Set the content of this node. + * + * @param string $text The new content of this node. + * + * @return DOMNode The modified node. + */ + function set_content($text) + { + return $this->node->appendChild($this->node->ownerDocument->createTextNode($text)); + } + + /** + * Return the type of this node. + * + * @return integer Type of this node. + */ + function node_type() + { + return $this->node->nodeType; + } + + function child_nodes() + { + $nodes = array(); + + $nodeList = $this->node->childNodes; + if (isset($nodeList)) { + $i = 0; + while ($node = $nodeList->item($i)) { + $nodes[] = $this->_newDOMElement($node, $this->document); + ++$i; + } + } + + return $nodes; + } + + /** + * Return the attributes of this node. + * + * @return array Attributes of this node. + */ + function attributes() + { + $attributes = array(); + + $attrList = $this->node->attributes; + if (isset($attrList)) { + $i = 0; + while ($attribute = $attrList->item($i)) { + $attributes[] = new Horde_DOM_Attribute($attribute, $this->document); + ++$i; + } + } + + return $attributes; + } + + function first_child() + { + return $this->_newDOMElement($this->node->firstChild, $this->document); + } + + /** + * Return the content of this node. + * + * @return string Text content of this node. + */ + function get_content() + { + return $this->node->textContent; + } + + function has_child_nodes() + { + return $this->node->hasChildNodes(); + } + + function next_sibling() + { + if ($this->node->nextSibling === null) { + return null; + } + + return $this->_newDOMElement($this->node->nextSibling, $this->document); + } + + function node_value() + { + return $this->node->nodeValue; + } + + function node_name() + { + if ($this->node->nodeType == XML_ELEMENT_NODE) { + return $this->node->localName; + } else { + return $this->node->nodeName; + } + } + + function clone_node() + { + return new Horde_DOM_Node($this->node->cloneNode()); + } + + /** + * Append a new node + * + * @param Horde_DOM_Node $newnode The child to be added + * + * @return Horde_DOM_Node The resulting node + */ + function append_child($newnode) + { + return $this->_newDOMElement($this->node->appendChild($this->_importNode($newnode)), $this->document); + } + + /** + * Remove a child node + * + * @param Horde_DOM_Node $oldchild The child to be removed + * + * @return Horde_DOM_Node The resulting node + */ + function remove_child($oldchild) + { + return $this->_newDOMElement($this->node->removeChild($oldchild->node), $this->document); + } + + /** + * Return a node of this class type. + * + * @param DOMNode $node The DOMXML node + * @param Horde_DOM_Document $document The parent document + * + * @return Horde_DOM_Node The wrapped node + */ + function _newDOMElement($node, $document) + { + if ($node == null) { + return null; + } + + switch ($node->nodeType) { + case XML_ELEMENT_NODE: + return new Horde_DOM_Element($node, $document); + case XML_TEXT_NODE: + return new Horde_DOM_Text($node, $document); + case XML_ATTRIBUTE_NODE: + return new Horde_DOM_Attribute($node, $document); + // case XML_PI_NODE: + // return new Horde_DOM_ProcessingInstruction($node, $document); + default: + return new Horde_DOM_Node($node, $document); + } + } + + /** + * Private function to import DOMNode from another DOMDocument. + * + * @param Horde_DOM_Node $newnode The node to be imported + * + * @return Horde_DOM_Node The wrapped node + */ + function _importNode($newnode) + { + if ($this->document === $newnode->document) { + return $newnode->node; + } else { + return $this->document->node->importNode($newnode->node, true); + } + } + +} + +/** + * @package Horde_DOM + */ +class Horde_DOM_Element extends Horde_DOM_Node { + + /** + * Get the value of specified attribute. + * + * @param string $name Name of the attribute + * + * @return string Indicates if the attribute was set. + */ + function get_attribute($name) + { + return $this->node->getAttribute($name); + } + + /** + * Determine if an attribute of this name is present on the node. + * + * @param string $name Name of the attribute + * + * @return bool Indicates if such an attribute is present. + */ + function has_attribute($name) + { + return $this->node->hasAttribute($name); + } + + /** + * Set the specified attribute on this node. + * + * @param string $name Name of the attribute + * @param string $value The new value of this attribute. + * + * @return mixed Indicates if the attribute was set. + */ + function set_attribute($name, $value) + { + return $this->node->setAttribute($name, $value); + } + + /** + * Return the node represented by the requested tagname. + * + * @param string $name The tagname requested. + * + * @return array The nodes matching the tag name + */ + function get_elements_by_tagname($name) + { + $list = $this->node->getElementsByTagName($name); + $nodes = array(); + $i = 0; + if (isset($list)) { + while ($node = $list->item($i)) { + $nodes[] = $this->_newDOMElement($node, $this->document); + $i++; + } + } + + return $nodes; + } + +} + +/** + * @package Horde_DOM + */ +class Horde_DOM_Attribute extends Horde_DOM_Node { + + /** + * Return a node of this class type. + * + * @param DOMNode $node The DOMXML node + * @param Horde_DOM_Document $document The parent document + * + * @return Horde_DOM_Attribute The wrapped attribute + */ + function _newDOMElement($node, $document) + { + return new Horde_DOM_Attribute($node, $document); + } + +} + +/** + * @package Horde_DOM + */ +class Horde_DOM_Text extends Horde_DOM_Node { + + function __get($name) + { + if ($name == 'tagname') { + return '#text'; + } else { + return parent::__get($name); + } + } + + function tagname() + { + return '#text'; + } + + /** + * Set the content of this node. + * + * @param string $text The new content of this node. + */ + function set_content($text) + { + $this->node->nodeValue = $text; + } + +} + +/** + * @package Horde_DOM + */ +class Horde_DOM_XPath { + + /** + * @var DOMXPath + */ + var $_xpath; + + function factory($dom) + { + if (version_compare(PHP_VERSION, '5', '>=')) { + // PHP 5 with Horde_DOM. + return new Horde_DOM_XPath($dom); + } + + return $dom->xpath_new_context(); + } + + function Horde_DOM_XPath($dom) + { + $this->_xpath = new DOMXPath($dom); + } + + function xpath_register_ns($prefix, $uri) + { + $this->_xpath->registerNamespace($prefix, $uri); + } + + function xpath_eval($expression, $context = null) + { + if (is_null($context)) { + $nodeset = $this->_xpath->query($expression); + } else { + $nodeset = $this->_xpath->query($expression, $context); + } + $result = new stdClass(); + $result->nodeset = array(); + for ($i = 0; $i < $nodeset->length; $i++) { + $result->nodeset[] = new Horde_DOM_Element($nodeset->item($i)); + } + return $result; + } + +} diff --git a/framework/DOM/package.xml b/framework/DOM/package.xml new file mode 100644 index 000000000..b62beb3fa --- /dev/null +++ b/framework/DOM/package.xml @@ -0,0 +1,106 @@ + + + DOM + pear.horde.org + Wrapper classes for PHP4 domxml compatibility using PHP5's dom functions. + These classes allow the use of code written for PHP4's domxml +implementation to work using PHP5's dom implementation. + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + + Jan Schneider + jan + jan@horde.org + yes + + + Michael Rubinsky + mrubinsk + mrubinsk@horde.org + yes + + 2008-12-31 + + 0.2.0 + 0.2.0 + + + beta + beta + + LGPL + * Add Horde_DOM_Text class. +* Add Horde_DOM_XPath class for XPath wrapping. +* Return correct object types when iterating through child nodes. +* Pass unit test with PHP 4. + + + + + + + + + 4.0.0 + + + 1.4.0b1 + + + + + + + + 0.1.0 + 0.1.0 + + + alpha + alpha + + 2008-01-21 + LGPL + * Allow creation of empty documents +* Add Horde_DOM_Document::document_element() +* Add Horde_DOM_Node::clone_node() +* Add configuration options (Request #5370, Thomas Jarosch <thomas.jarosch@intra2net.com>) + + + + 0.0.2 + 0.0.2 + + + alpha + alpha + + 2006-05-08 + LGPL + * Converted to package.xml 2.0 for pear.horde.org + + + + 0.0.1 + 0.0.1 + + + alpha + alpha + + 2006-01-30 + LGPL + Move out of Horde_Config and added DOM_Node::getElementsByTagName, +and DOM_Node::Node_Name methods. + + + + diff --git a/framework/DOM/tests/dom_load_error.phpt b/framework/DOM/tests/dom_load_error.phpt new file mode 100644 index 000000000..9116d5fb6 --- /dev/null +++ b/framework/DOM/tests/dom_load_error.phpt @@ -0,0 +1,34 @@ +--TEST-- +Check that Horde::DOM handles load errors gracefully. +--FILE-- + $xml, 'options' => HORDE_DOM_LOAD_REMOVE_BLANKS); + +$dom = Horde_DOM_Document::factory($params); + +// Check that the xml loading elicits an error +var_dump(is_a($dom, 'PEAR_Error')); + +// Load XML +$xml = file_get_contents(dirname(__FILE__) . '/fixtures/load_ok.xml'); + +$params = array('xml' => $xml, 'options' => HORDE_DOM_LOAD_REMOVE_BLANKS); + +$dom = Horde_DOM_Document::factory($params); + +// Check that the xml loading elicits an error +var_dump(is_a($dom, 'PEAR_Error')); + +--EXPECT-- +bool(true) +bool(false) diff --git a/framework/DOM/tests/fixtures/load_error.xml b/framework/DOM/tests/fixtures/load_error.xml new file mode 100644 index 000000000..50e397eb9 --- /dev/null +++ b/framework/DOM/tests/fixtures/load_error.xml @@ -0,0 +1,5 @@ + + + 0 + Abständen führen wirþedback + diff --git a/framework/DOM/tests/fixtures/load_ok.xml b/framework/DOM/tests/fixtures/load_ok.xml new file mode 100644 index 000000000..42d0bf2ad --- /dev/null +++ b/framework/DOM/tests/fixtures/load_ok.xml @@ -0,0 +1,5 @@ + + + 0 + Abständen führen wir Feedback + diff --git a/framework/Data/Data.php b/framework/Data/Data.php new file mode 100644 index 000000000..09987b5ba --- /dev/null +++ b/framework/Data/Data.php @@ -0,0 +1,428 @@ + + * @author Chuck Hagenbuch + * @since Horde 1.3 + * @package Horde_Data + */ +class Horde_Data extends PEAR { + +// Import constants +/** Import already mapped csv data. */ const IMPORT_MAPPED = 1; +/** Map date and time entries of csv data. */ const IMPORT_DATETIME = 2; +/** Import generic CSV data. */ const IMPORT_CSV = 3; +/** Import MS Outlook data. */ const IMPORT_OUTLOOK = 4; +/** Import vCalendar/iCalendar data. */ const IMPORT_ICALENDAR = 5; +/** Import vCards. */ const IMPORT_VCARD = 6; +/** Import generic tsv data. */ const IMPORT_TSV = 7; +/** Import Mulberry address book data */ const IMPORT_MULBERRY = 8; +/** Import Pine address book data. */ const IMPORT_PINE = 9; +/** Import file. */ const IMPORT_FILE = 11; +/** Import data. */ const IMPORT_DATA = 12; + +// Export constants +/** Export generic CSV data. */ const EXPORT_CSV = 100; +/** Export iCalendar data. */ const EXPORT_ICALENDAR = 101; +/** Export vCards. */ const EXPORT_VCARD = 102; +/** Export TSV data. */ const EXPORT_TSV = 103; +/** Export Outlook CSV data. */ const EXPORT_OUTLOOKCSV = 104; + + /** + * File extension. + * + * @var string + */ + var $_extension; + + /** + * MIME content type. + * + * @var string + */ + var $_contentType = 'text/plain'; + + /** + * A list of warnings raised during the last operation. + * + * @var array + * @since Horde 3.1 + */ + var $_warnings = array(); + + /** + * Stub to import passed data. + */ + function importData() + { + } + + /** + * Stub to return exported data. + */ + function exportData() + { + } + + /** + * Stub to import a file. + */ + function importFile($filename, $header = false) + { + $data = file_get_contents($filename); + return $this->importData($data, $header); + } + + /** + * Stub to export data to a file. + */ + function exportFile() + { + } + + /** + * Tries to determine the expected newline character based on the + * platform information passed by the browser's agent header. + * + * @return string The guessed expected newline characters, either \n, \r + * or \r\n. + */ + function getNewline() + { + require_once 'Horde/Browser.php'; + $browser = &Horde_Browser::singleton(); + + switch ($browser->getPlatform()) { + case 'win': + return "\r\n"; + + case 'mac': + return "\r"; + + case 'unix': + default: + return "\n"; + } + } + + /** + * Returns the full filename including the basename and extension. + * + * @param string $basename Basename for the file. + * + * @return string The file name. + */ + function getFilename($basename) + { + return $basename . '.' . $this->_extension; + } + + /** + * Returns the content type. + * + * @return string The content type. + */ + function getContentType() + { + return $this->_contentType; + } + + /** + * Returns a list of warnings that have been raised during the last + * operation. + * + * @since Horde 3.1 + * + * @return array A (possibly empty) list of warnings. + */ + function warnings() + { + return $this->_warnings; + } + + /** + * Attempts to return a concrete Horde_Data instance based on $format. + * + * @param mixed $format The type of concrete Horde_Data subclass to + * return. If $format is an array, then we will look + * in $format[0]/lib/Data/ for the subclass + * implementation named $format[1].php. + * + * @return Horde_Data The newly created concrete Horde_Data instance, or + * false on an error. + */ + function &factory($format) + { + if (is_array($format)) { + $app = $format[0]; + $format = $format[1]; + } + + $format = basename($format); + + if (empty($format) || (strcmp($format, 'none') == 0)) { + $data = new Horde_Data(); + return $data; + } + + if (!empty($app)) { + require_once $GLOBALS['registry']->get('fileroot', $app) . '/lib/Data/' . $format . '.php'; + } else { + require_once 'Horde/Data/' . $format . '.php'; + } + $class = 'Horde_Data_' . $format; + if (class_exists($class)) { + $data = new $class(); + } else { + $data = PEAR::raiseError('Class definition of ' . $class . ' not found.'); + } + + return $data; + } + + /** + * Attempts to return a reference to a concrete Horde_Data instance + * based on $format. It will only create a new instance if no Horde_Data + * instance with the same parameters currently exists. + * + * This should be used if multiple data sources (and, thus, multiple + * Horde_Data instances) are required. + * + * This method must be invoked as: $var = &Horde_Data::singleton() + * + * @param string $format The type of concrete Horde_Data subclass to + * return. + * + * @return Horde_Data The concrete Horde_Data reference, or false on an + * error. + */ + function &singleton($format) + { + static $instances = array(); + + $signature = serialize($format); + if (!isset($instances[$signature])) { + $instances[$signature] = &Horde_Data::factory($format); + } + + return $instances[$signature]; + } + + /** + * Maps a date/time string to an associative array. + * + * The method signature has changed in Horde 3.1.3. + * + * @access private + * + * @param string $date The date. + * @param string $type One of 'date', 'time' or 'datetime'. + * @param array $params Two-dimensional array with additional information + * about the formatting. Possible keys are: + * - delimiter - The character that seperates the + * different date/time parts. + * - format - If 'ampm' and $date contains a time we + * assume that it is in AM/PM format. + * - order - If $type is 'datetime' the order of the + * day and time parts: -1 (timestamp), 0 + * (day/time), 1 (time/day). + * @param integer $key The key to use for $params. + * + * @return string The date or time in ISO format. + */ + function mapDate($date, $type, $params, $key) + { + switch ($type) { + case 'date': + case 'monthday': + case 'monthdayyear': + $dates = explode($params['delimiter'][$key], $date); + if (count($dates) != 3) { + return $date; + } + $index = array_flip(explode('/', $params['format'][$key])); + return $dates[$index['year']] . '-' . $dates[$index['month']] . '-' . $dates[$index['mday']]; + + case 'time': + $dates = explode($params['delimiter'][$key], $date); + if (count($dates) < 2 || count($dates) > 3) { + return $date; + } + if ($params['format'][$key] == 'ampm') { + if (strpos(strtolower($dates[count($dates)-1]), 'pm') !== false) { + if ($dates[0] !== '12') { + $dates[0] += 12; + } + } elseif ($dates[0] == '12') { + $dates[0] = '0'; + } + $dates[count($dates) - 1] = sprintf('%02d', $dates[count($dates)-1]); + } + return $dates[0] . ':' . $dates[1] . (count($dates) == 3 ? (':' . $dates[2]) : ':00'); + + case 'datetime': + switch ($params['order'][$key]) { + case -1: + return (string)(int)$date == $date + ? date('Y-m-d H:i:s', $date) + : $date; + case 0: + list($day, $time) = explode(' ', $date, 2); + break; + case 1: + list($time, $day) = explode(' ', $date, 2); + break; + } + $date = $this->mapDate($day, 'date', + array('delimiter' => $params['day_delimiter'], + 'format' => $params['day_format']), + $key); + $time = $this->mapDate($time, 'time', + array('delimiter' => $params['time_delimiter'], + 'format' => $params['time_format']), + $key); + return $date . ' ' . $time; + + } + } + + /** + * 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. + */ + function nextStep($action, $param = array()) + { + /* First step. */ + if (is_null($action)) { + $_SESSION['import_data'] = array(); + return self::IMPORT_FILE; + } + + switch ($action) { + case self::IMPORT_FILE: + /* Sanitize uploaded file. */ + $import_format = Horde_Util::getFormData('import_format'); + $check_upload = Horde_Browser::wasFileUploaded('import_file', $param['file_types'][$import_format]); + if (is_a($check_upload, 'PEAR_Error')) { + return $check_upload; + } + if ($_FILES['import_file']['size'] <= 0) { + return PEAR::raiseError(_("The file contained no data.")); + } + $_SESSION['import_data']['format'] = $import_format; + break; + + case self::IMPORT_MAPPED: + $dataKeys = Horde_Util::getFormData('dataKeys', ''); + $appKeys = Horde_Util::getFormData('appKeys', ''); + if (empty($dataKeys) || empty($appKeys)) { + global $registry; + return PEAR::raiseError(sprintf(_("You didn't map any fields from the imported file to the corresponding fields in %s."), + $registry->get('name'))); + } + $dataKeys = explode("\t", $dataKeys); + $appKeys = explode("\t", $appKeys); + $map = array(); + $dates = array(); + foreach ($appKeys as $key => $app) { + $map[$dataKeys[$key]] = $app; + if (isset($param['time_fields']) && + isset($param['time_fields'][$app])) { + $dates[$dataKeys[$key]]['type'] = $param['time_fields'][$app]; + $dates[$dataKeys[$key]]['values'] = array(); + $i = 0; + /* Build an example array of up to 10 date/time fields. */ + while ($i < count($_SESSION['import_data']['data']) && count($dates[$dataKeys[$key]]['values']) < 10) { + if (!empty($_SESSION['import_data']['data'][$i][$dataKeys[$key]])) { + $dates[$dataKeys[$key]]['values'][] = $_SESSION['import_data']['data'][$i][$dataKeys[$key]]; + } + $i++; + } + } + } + $_SESSION['import_data']['map'] = $map; + if (count($dates) > 0) { + foreach ($dates as $key => $data) { + if (count($data['values'])) { + $_SESSION['import_data']['dates'] = $dates; + return self::IMPORT_DATETIME; + } + } + } + return $this->nextStep(self::IMPORT_DATA, $param); + + case self::IMPORT_DATETIME: + case self::IMPORT_DATA: + if ($action == self::IMPORT_DATETIME) { + $params = array('delimiter' => Horde_Util::getFormData('delimiter'), + 'format' => Horde_Util::getFormData('format'), + 'order' => Horde_Util::getFormData('order'), + 'day_delimiter' => Horde_Util::getFormData('day_delimiter'), + 'day_format' => Horde_Util::getFormData('day_format'), + 'time_delimiter' => Horde_Util::getFormData('time_delimiter'), + 'time_format' => Horde_Util::getFormData('time_format')); + } + if (!isset($_SESSION['import_data']['data'])) { + return PEAR::raiseError(_("The uploaded data was lost since the previous step.")); + } + /* Build the result data set as an associative array. */ + $data = array(); + foreach ($_SESSION['import_data']['data'] as $row) { + $data_row = array(); + foreach ($row as $key => $val) { + if (isset($_SESSION['import_data']['map'][$key])) { + $mapped_key = $_SESSION['import_data']['map'][$key]; + if ($action == self::IMPORT_DATETIME && + !empty($val) && + isset($param['time_fields']) && + isset($param['time_fields'][$mapped_key])) { + $val = $this->mapDate($val, $param['time_fields'][$mapped_key], $params, $key); + } + $data_row[$_SESSION['import_data']['map'][$key]] = $val; + } + } + $data[] = $data_row; + } + return $data; + } + } + + /** + * Cleans the session data up and removes any uploaded and moved + * files. If a function called "_cleanup()" exists, this gets + * called too. + * + * @return mixed If _cleanup() was called, the return value of this call. + * This should be the value of the first import step. + */ + function cleanup() + { + if (isset($_SESSION['import_data']['file_name'])) { + @unlink($_SESSION['import_data']['file_name']); + } + $_SESSION['import_data'] = array(); + if (function_exists('_cleanup')) { + return _cleanup(); + } + } + +} diff --git a/framework/Data/Data/csv.php b/framework/Data/Data/csv.php new file mode 100644 index 000000000..4e242879c --- /dev/null +++ b/framework/Data/Data/csv.php @@ -0,0 +1,274 @@ + + * @author Chuck Hagenbuch + * @since Horde 1.3 + * @package Horde_Data + */ +class Horde_Data_csv extends Horde_Data { + + var $_extension = 'csv'; + var $_contentType = 'text/comma-separated-values'; + + /** + * Tries to discover the CSV file's parameters. + * + * @param string $filename The name of the file to investigate. + * + * @return array An associative array with the following possible keys: + *
+     * 'sep':    The field separator
+     * 'quote':  The quoting character
+     * 'fields': The number of fields (columns)
+     * 
+ */ + function discoverFormat($filename) + { + return File_CSV::discoverFormat($filename); + } + + /** + * Imports and parses a CSV file. + * + * @param string $filename The name of the file to parse. + * @param boolean $header Does the first line contain the field/column + * names? + * @param string $sep The field/column separator. + * @param string $quote The quoting character. + * @param integer $fields The number or fields/columns. + * @param string $charset The file's charset. @since Horde 3.1. + * @param string $crlf The file's linefeed characters. @since Horde 3.1. + * + * @return array A two-dimensional array of all imported data rows. If + * $header was true the rows are associative arrays with the + * field/column names as the keys. + */ + function importFile($filename, $header = false, $sep = '', $quote = '', + $fields = null, $import_mapping = array(), + $charset = null, $crlf = null) + { + /* File_CSV is a bit picky at what parameters it expects. */ + $conf = array(); + if ($fields) { + $conf['fields'] = $fields; + } else { + return array(); + } + if (!empty($quote)) { + $conf['quote'] = $quote; + } + if (empty($sep)) { + $conf['sep'] = ','; + } else { + $conf['sep'] = $sep; + } + if (!empty($crlf)) { + $conf['crlf'] = $crlf; + } + + /* Strip and keep the first line if it contains the field + * names. */ + if ($header) { + $head = File_CSV::read($filename, $conf); + if (is_a($head, 'PEAR_Error')) { + return $head; + } + if (!empty($charset)) { + $head = Horde_String::convertCharset($head, $charset, Horde_Nls::getCharset()); + } + } + + $data = array(); + while ($line = File_CSV::read($filename, $conf)) { + if (is_a($line, 'PEAR_Error')) { + return $line; + } + if (!empty($charset)) { + $line = Horde_String::convertCharset($line, $charset, Horde_Nls::getCharset()); + } + if (!isset($head)) { + $data[] = $line; + } else { + $newline = array(); + for ($i = 0; $i < count($head); $i++) { + if (isset($import_mapping[$head[$i]])) { + $head[$i] = $import_mapping[$head[$i]]; + } + $cell = $line[$i]; + $cell = preg_replace("/\"\"/", "\"", $cell); + $newline[$head[$i]] = empty($cell) ? '' : $cell; + } + $data[] = $newline; + } + } + + $fp = File_CSV::getPointer($filename, $conf); + if ($fp && !is_a($fp, 'PEAR_Error')) { + rewind($fp); + } + + $this->_warnings = File_CSV::warning(); + return $data; + } + + /** + * Builds a CSV 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 CSV data. + */ + function exportData($data, $header = false, $export_mapping = array()) + { + if (!is_array($data) || count($data) == 0) { + return ''; + } + + $export = ''; + $eol = "\n"; + $head = array_keys(current($data)); + if ($header) { + foreach ($head as $key) { + if (!empty($key)) { + if (isset($export_mapping[$key])) { + $key = $export_mapping[$key]; + } + $export .= '"' . $key . '"'; + } + $export .= ','; + } + $export = substr($export, 0, -1) . $eol; + } + + foreach ($data as $row) { + foreach ($head as $key) { + $cell = $row[$key]; + if (!empty($cell) || $cell === 0) { + $export .= '"' . $cell . '"'; + } + $export .= ','; + } + $export = substr($export, 0, -1) . $eol; + } + + return $export; + } + + /** + * Builds a CSV 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. + */ + function exportFile($filename, $data, $header = false, + $export_mapping = array()) + { + $export = $this->exportData($data, $header, $export_mapping); + $GLOBALS['browser']->downloadHeaders($filename, 'application/csv', false, strlen($export)); + echo $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. + */ + function nextStep($action, $param = array()) + { + switch ($action) { + case self::IMPORT_FILE: + $next_step = parent::nextStep($action, $param); + if (is_a($next_step, 'PEAR_Error')) { + return $next_step; + } + + /* Move uploaded file so that we can read it again in the next + step after the user gave some format details. */ + $file_name = Horde::getTempFile('import', false); + if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) { + return PEAR::raiseError(_("The uploaded file could not be saved.")); + } + $_SESSION['import_data']['file_name'] = $file_name; + + /* Try to discover the file format ourselves. */ + $conf = $this->discoverFormat($file_name); + if (!$conf) { + $conf = array('sep' => ','); + } + $_SESSION['import_data'] = array_merge($_SESSION['import_data'], $conf); + + /* Check if charset was specified. */ + $_SESSION['import_data']['charset'] = Horde_Util::getFormData('charset'); + + /* Read the file's first two lines to show them to the user. */ + $_SESSION['import_data']['first_lines'] = ''; + $fp = @fopen($file_name, 'r'); + if ($fp) { + $line_no = 1; + while ($line_no < 3 && $line = fgets($fp)) { + if (!empty($_SESSION['import_data']['charset'])) { + $line = Horde_String::convertCharset($line, $_SESSION['import_data']['charset'], Horde_Nls::getCharset()); + } + $newline = Horde_String::length($line) > 100 ? "\n" : ''; + $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline; + $line_no++; + } + } + return self::IMPORT_CSV; + + case self::IMPORT_CSV: + $_SESSION['import_data']['header'] = Horde_Util::getFormData('header'); + $import_mapping = array(); + if (isset($param['import_mapping'])) { + $import_mapping = $param['import_mapping']; + } + $import_data = $this->importFile($_SESSION['import_data']['file_name'], + $_SESSION['import_data']['header'], + Horde_Util::getFormData('sep'), + Horde_Util::getFormData('quote'), + Horde_Util::getFormData('fields'), + $import_mapping, + $_SESSION['import_data']['charset'], + $_SESSION['import_data']['crlf']); + $_SESSION['import_data']['data'] = $import_data; + unset($_SESSION['import_data']['map']); + return self::IMPORT_MAPPED; + + default: + return parent::nextStep($action, $param); + } + } + +} diff --git a/framework/Data/Data/icalendar.php b/framework/Data/Data/icalendar.php new file mode 100644 index 000000000..8211d6e7e --- /dev/null +++ b/framework/Data/Data/icalendar.php @@ -0,0 +1,23 @@ + + * @author Karsten Fourmont + * @package Horde_Data + * @since Horde 3.0 + */ +class Horde_Data_icalendar extends Horde_Data_imc { + +} diff --git a/framework/Data/Data/imc.php b/framework/Data/Data/imc.php new file mode 100644 index 000000000..3945b553e --- /dev/null +++ b/framework/Data/Data/imc.php @@ -0,0 +1,106 @@ + + * @package Horde_Data + * @since Horde 3.0 + */ +class Horde_Data_imc extends Horde_Data { + + var $_iCal = false; + + function importData($text) + { + $this->_iCal = new Horde_iCalendar(); + if (!$this->_iCal->parsevCalendar($text)) { + return PEAR::raiseError(_("There was an error importing the iCalendar data.")); + } + + return $this->_iCal->getComponents(); + } + + /** + * Builds an iCalendar file from a given data structure and + * returns it as a string. + * + * @param array $data An array containing Horde_iCalendar_vevent + * objects + * @param string $method The iTip method to use. + * + * @return string The iCalendar data. + */ + function exportData($data, $method = 'REQUEST') + { + $this->_iCal = new Horde_iCalendar(); + $this->_iCal->setAttribute('METHOD', $method); + + foreach ($data as $event) { + $this->_iCal->addComponent($event); + } + + return $this->_iCal->exportvCalendar(); + } + + /** + * Builds an iCalendar 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 An array containing Horde_iCalendar_vevents + */ + function exportFile($filename, $data) + { + $export = $this->exportData($data); + $GLOBALS['browser']->downloadHeaders($filename, 'text/calendar', false, strlen($export)); + echo $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. + */ + function nextStep($action, $param = array()) + { + switch ($action) { + case self::IMPORT_FILE: + $next_step = parent::nextStep($action, $param); + if (is_a($next_step, 'PEAR_Error')) { + return $next_step; + } + + $import_data = $this->importFile($_FILES['import_file']['tmp_name']); + if (is_a($import_data, 'PEAR_Error')) { + return $import_data; + } + + return $this->_iCal->getComponents(); + break; + + default: + return parent::nextStep($action, $param); + break; + } + } + +} diff --git a/framework/Data/Data/outlookcsv.php b/framework/Data/Data/outlookcsv.php new file mode 100644 index 000000000..b686e199a --- /dev/null +++ b/framework/Data/Data/outlookcsv.php @@ -0,0 +1,62 @@ + + * @author Chuck Hagenbuch + * @since Horde 1.3 + * @package Horde_Data + */ +class Horde_Data_tsv extends Horde_Data { + + var $_extension = 'tsv'; + var $_contentType = 'text/tab-separated-values'; + + /** + * Convert data file contents to list of data records. + * + * @param string $contents Data file contents. + * @param boolean $header True if a header row is present. + * @param string $delimiter Field delimiter. + * + * @return array List of data records. + */ + function importData($contents, $header = false, $delimiter = "\t") + { + if ($_SESSION['import_data']['format'] == 'pine') { + $contents = preg_replace('/\n +/', '', $contents); + } + $contents = explode("\n", $contents); + $data = array(); + if ($header) { + $head = explode($delimiter, array_shift($contents)); + } + foreach ($contents as $line) { + if (trim($line) == '') { + continue; + } + $line = explode($delimiter, $line); + if (!isset($head)) { + $data[] = $line; + } else { + $newline = array(); + for ($i = 0; $i < count($head); $i++) { + $newline[$head[$i]] = empty($line[$i]) ? '' : $line[$i]; + } + $data[] = $newline; + } + } + return $data; + } + + /** + * Builds a TSV 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 TSV data. + */ + function exportData($data, $header = false) + { + if (!is_array($data) || count($data) == 0) { + return ''; + } + $export = ''; + $head = array_keys(current($data)); + if ($header) { + $export = implode("\t", $head) . "\n"; + } + foreach ($data as $row) { + foreach ($head as $key) { + $cell = $row[$key]; + if (!empty($cell) || $cell === 0) { + $export .= $cell; + } + $export .= "\t"; + } + $export = substr($export, 0, -1) . "\n"; + } + return $export; + } + + /** + * Builds a TSV 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. + */ + function exportFile($filename, $data, $header = false) + { + $export = $this->exportData($data, $header); + $GLOBALS['browser']->downloadHeaders($filename, 'text/tab-separated-values', false, strlen($export)); + echo $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. + */ + function nextStep($action, $param = array()) + { + switch ($action) { + case self::IMPORT_FILE: + $next_step = parent::nextStep($action, $param); + if (is_a($next_step, 'PEAR_Error')) { + return $next_step; + } + + if ($_SESSION['import_data']['format'] == 'mulberry' || + $_SESSION['import_data']['format'] == 'pine') { + $_SESSION['import_data']['data'] = $this->importFile($_FILES['import_file']['tmp_name']); + $format = $_SESSION['import_data']['format']; + if ($format == 'mulberry') { + $appKeys = array('alias', 'name', 'email', 'company', 'workAddress', 'workPhone', 'homePhone', 'fax', 'notes'); + $dataKeys = array(0, 1, 2, 3, 4, 5, 6, 7, 9); + } elseif ($format == 'pine') { + $appKeys = array('alias', 'name', 'email', 'notes'); + $dataKeys = array(0, 1, 2, 4); + } + foreach ($appKeys as $key => $app) { + $map[$dataKeys[$key]] = $app; + } + $data = array(); + foreach ($_SESSION['import_data']['data'] as $row) { + $hash = array(); + if ($format == 'mulberry') { + if (preg_match("/^Grp:/", $row[0]) || empty($row[1])) { + continue; + } + $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]); + foreach ($dataKeys as $key) { + if (array_key_exists($key, $row)) { + $hash[$key] = stripslashes(preg_replace('/\\\\r/', "\n", $row[$key])); + } + } + } elseif ($format == 'pine') { + if (count($row) < 3 || preg_match("/^#DELETED/", $row[0]) || preg_match("/[()]/", $row[2])) { + continue; + } + $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]); + /* Address can be a full RFC822 address */ + require_once 'Horde/MIME.php'; + $addr_arr = MIME::parseAddressList($row[2]); + if (is_a($addr_arr, 'PEAR_Error') || + empty($addr_arr[0]->mailbox)) { + continue; + } + $row[2] = $addr_arr[0]->mailbox . '@' . $addr_arr[0]->host; + if (empty($row[1]) && !empty($addr_arr[0]->personal)) { + $row[1] = $addr_arr[0]->personal; + } + foreach ($dataKeys as $key) { + if (array_key_exists($key, $row)) { + $hash[$key] = $row[$key]; + } + } + } + $data[] = $hash; + } + $_SESSION['import_data']['data'] = $data; + $_SESSION['import_data']['map'] = $map; + $ret = $this->nextStep(self::IMPORT_DATA, $param); + return $ret; + } + + /* Move uploaded file so that we can read it again in the next step + after the user gave some format details. */ + $uploaded = Horde_Browser::wasFileUploaded('import_file', _("TSV file")); + if (is_a($uploaded, 'PEAR_Error')) { + return PEAR::raiseError($uploaded->getMessage()); + } + $file_name = Horde::getTempFile('import', false); + if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) { + return PEAR::raiseError(_("The uploaded file could not be saved.")); + } + $_SESSION['import_data']['file_name'] = $file_name; + + /* Read the file's first two lines to show them to the user. */ + $_SESSION['import_data']['first_lines'] = ''; + $fp = @fopen($file_name, 'r'); + if ($fp) { + $line_no = 1; + while ($line_no < 3 && $line = fgets($fp)) { + $newline = Horde_String::length($line) > 100 ? "\n" : ''; + $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline; + $line_no++; + } + } + return self::IMPORT_TSV; + break; + + case self::IMPORT_TSV: + $_SESSION['import_data']['header'] = Horde_Util::getFormData('header'); + $import_data = $this->importFile($_SESSION['import_data']['file_name'], + $_SESSION['import_data']['header']); + $_SESSION['import_data']['data'] = $import_data; + unset($_SESSION['import_data']['map']); + return self::IMPORT_MAPPED; + break; + + default: + return parent::nextStep($action, $param); + break; + } + } + +} diff --git a/framework/Data/Data/vcard.php b/framework/Data/Data/vcard.php new file mode 100644 index 000000000..794b6eaab --- /dev/null +++ b/framework/Data/Data/vcard.php @@ -0,0 +1,41 @@ + + * @since Horde 3.0 + * @package Horde_Data + */ +class Horde_Data_vcard extends Horde_Data_imc { + + /** + * Exports vcalendar data as a string. Unlike vEvent, vCard data + * is not enclosed in BEGIN|END:vCalendar. + * + * @param array $data An array containing Horde_iCalendar_vcard + * objects. + * @param string $method The iTip method to use. + * + * @return string The iCalendar data. + */ + function exportData($data, $method = 'REQUEST') + { + $s = ''; + foreach ($data as $vcard) { + $s.= $vcard->exportvCalendar(); + } + return $s; + } + +} diff --git a/framework/Data/Data/vnote.php b/framework/Data/Data/vnote.php new file mode 100644 index 000000000..55c02d1d8 --- /dev/null +++ b/framework/Data/Data/vnote.php @@ -0,0 +1,46 @@ + + * @author Chuck Hagenbuch + * @package Horde_Data + * @since Horde 3.0 + */ +class Horde_Data_vnote extends Horde_Data_imc { + + /** + * Exports vcalendar data as a string. Unlike vEvent, vNote data + * is not enclosed in BEGIN|END:vCalendar. + * + * @param array $data An array containing Horde_iCalendar_vnote + * objects. + * @param string $method The iTip method to use. + * + * @return string The iCalendar data. + */ + function exportData($data, $method = 'REQUEST') + { + global $prefs; + + $this->_iCal = new Horde_iCalendar(); + + $this->_iCal->setAttribute('METHOD', $method); + $s = ''; + foreach ($data as $event) { + $s.= $event->exportvCalendar(); + } + return $s; + } + +} diff --git a/framework/Data/Data/vtodo.php b/framework/Data/Data/vtodo.php new file mode 100644 index 000000000..6d56d6f27 --- /dev/null +++ b/framework/Data/Data/vtodo.php @@ -0,0 +1,23 @@ + + * @author Chuck Hagenbuch + * @package Horde_Data + * @since Horde 3.0 + */ +class Horde_Data_vtodo extends Horde_Data_imc { + +} diff --git a/framework/Data/docs/examples/Eudora.txt b/framework/Data/docs/examples/Eudora.txt new file mode 100644 index 000000000..bdc7f041d --- /dev/null +++ b/framework/Data/docs/examples/Eudora.txt @@ -0,0 +1,2 @@ +alias firman firman@php.net +note firman It's a notes. diff --git a/framework/Data/docs/examples/Gmail.csv b/framework/Data/docs/examples/Gmail.csv new file mode 100644 index 000000000..b371bed41 --- /dev/null +++ b/framework/Data/docs/examples/Gmail.csv @@ -0,0 +1,3 @@ +Name,E-mail,Notes,Section 1 - Description,Section 1 - Email,Section 1 - IM,Section 1 - Phone,Section 1 - Mobile,Section 1 - Pager,Section 1 - Fax,Section 1 - Company,Section 1 - Title,Section 1 - Other,Section 1 - Address +John Doe,john@example.com,,,john.doe@example.com,,9999999,9999999,9999999,9999999,"Example, Corp.",Mr,,Foo Street +Jeniffer Doe,jeniffer@example.com,,,jeniffer.doe@example.com,,9999999,9999999,9999999,9999999,"Example, Corp.",Mrs,,Foo Street diff --git a/framework/Data/docs/examples/KMail.csv b/framework/Data/docs/examples/KMail.csv new file mode 100644 index 000000000..bf400a493 --- /dev/null +++ b/framework/Data/docs/examples/KMail.csv @@ -0,0 +1,2 @@ +"Formatted Name","Family Name","Given Name","Additional Names","Honorific Prefixes","Honorific Suffixes","Nick Name","Birthday","Home Address Street","Home Address Locality","Home Address Region","Home Address Postal Code","Home Address Country","Home Address Label","Business Address Street","Business Address Locality","Business Address Region","Business Address Postal Code","Business Address Country","Business Address Label","Home Phone","Business Phone","Mobile Phone","Home Fax","Business Fax","Car Phone","ISDN","Pager","Email Address","Mail Client","Title","Role","Organization","Note","URL","Department","Profession","Assistant's Name","Manager's Name","Spouse's Name","Office","IM Address","Anniversary" +"Firman","","Firman Wandayandi","Wandayandi","","","firman","","Foo Street","","","99999","Indonesia","","Foo Street","","","99999","Indonesia","","99999999","99999999","999999999","99999999","99999999","","","99999999","firman@php.net","","","Web Developer","Foo","It's a notes.","http://php.hm/~firman","Foo Net","","","","","","","" diff --git a/framework/Data/docs/examples/Outlook.csv b/framework/Data/docs/examples/Outlook.csv new file mode 100644 index 000000000..2dd1c1490 --- /dev/null +++ b/framework/Data/docs/examples/Outlook.csv @@ -0,0 +1,4 @@ +Title,First Name,Middle Name,Last Name,Suffix,Company,Department,Job Title,Business Street,Business Street 2,Business Street 3,Business City,Business State,Business Postal Code,Business Country,Home Street,Home Street 2,Home Street 3,Home City,Home State,Home Postal Code,Home Country,Other Street,Other Street 2,Other Street 3,Other City,Other State,Other Postal Code,Other Country,Assistant's Phone,Business Fax,Business Phone,Business Phone 2,Callback,Car Phone,Company Main Phone,Home Fax,Home Phone,Home Phone 2,ISDN,Mobile Phone,Other Fax,Other Phone,Pager,Primary Phone,Radio Phone,TTY/TDD Phone,Telex,Account,Anniversary,Assistant's Name,Billing Information,Birthday,Business Address PO Box,Categories,Children,Directory Server,E-mail Address,E-mail Type,E-mail Display Name,E-mail 2 Address,E-mail 2 Type,E-mail 2 Display Name,E-mail 3 Address,E-mail 3 Type,E-mail 3 Display Name,Gender,Government ID Number,Hobby,Home Address PO Box,Initials,Internet Free Busy,Keywords,Language,Location,Manager's Name,Mileage,Notes,Office Location,Organizational ID Number,Other Address PO Box,Priority,Private,Profession,Referred By,Sensitivity,Spouse,User 1,User 2,User 3,User 4,Web Page +,Firman,,Wandayandi,,Foo,Foo Net,Web Developer,Foo Street,,,Bandung,West Java,99999,Indonesia,Foo Street,,,Bandung,West Java,99999,Indonesia,,,,,,,,,99999999,,,,,99999999,99999999,99999999,,,999999999,,,99999999,,,,,,,,,,,,,,firman@php.net,,Firman Wandayandi,,,,,,,,,,,,,,,,,,"It's +multiline +memo.",,,,,,,,,,,,,,http://php.hm/~firman diff --git a/framework/Data/docs/examples/Palm.csv b/framework/Data/docs/examples/Palm.csv new file mode 100644 index 000000000..4fbf4728c --- /dev/null +++ b/framework/Data/docs/examples/Palm.csv @@ -0,0 +1,2 @@ +"Shumway1","Gordon","President","Alf Ltd","408-555-1254","408-555-1255","408-555-1256","408-555-1257","Gordon!@usa.com","123 Anywhere Street","San Jose","CA","95128","USA","","","","","","0","Family" +"Shumway2","Gordon","VP","Alf Ltd","405-555-1234","408-555-1235","408-555-1236","408-555-1237","Gordon2@usa.com","12345 Lost Street","San Jose","CA","95129","USA","","","","","","1","Family" diff --git a/framework/Data/docs/examples/Thunderbird.csv b/framework/Data/docs/examples/Thunderbird.csv new file mode 100644 index 000000000..0f953cdd0 --- /dev/null +++ b/framework/Data/docs/examples/Thunderbird.csv @@ -0,0 +1 @@ +Firman,Wandayandi,Firman Wandayandi,firman,firman@php.net,,99999999,99999999,99999999,99999999,999999999,Foo Street,,Bandung,West Java,99999,,Foo Street,,Bandung,West Java,99999,Indonesia,Web Developer,Foo Net,Foo,,http://php.hm/~firman,,,,,,,,It's a notes., diff --git a/framework/Data/docs/examples/WAB-selectable.csv b/framework/Data/docs/examples/WAB-selectable.csv new file mode 100644 index 000000000..f3d5974ae --- /dev/null +++ b/framework/Data/docs/examples/WAB-selectable.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Name,Nickname,E-mail Address,Home Street,Home City,Home Postal Code,Home State,Home Country/Region,Home Phone,Home Fax,Mobile Phone,Personal Web Page,Business Street,Business City,Business Postal Code,Business State,Business Country/Region,Business Web Page,Business Phone,Business Fax,Pager,Company,Job Title,Department,Office Location,Notes +foo,bar,foo,foo,foo@bar.com,"Foo Street 8, 20 Building",foo,9999999,foo,Afghanistan,Foo Street 8,99999999,999999999,http://foo.bar.com,Foo Street 8,Foo,99999,Foo,Foo/Foo,,Foo Street 8,99999999,99999999,foo,Web Developer,Foo Net,Foo Building,"I'm +multiline +memo." diff --git a/framework/Data/docs/examples/WAB.csv b/framework/Data/docs/examples/WAB.csv new file mode 100644 index 000000000..4e95b35ab --- /dev/null +++ b/framework/Data/docs/examples/WAB.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Middle Name,Name,Nickname,E-mail Address,Home Street,Home City,Home Postal Code,Home State,Home Country/Region,Home Phone,Home Fax,Mobile Phone,Personal Web Page,Business Street,Business City,Business Postal Code,Business State,Business Country/Region,Business Web Page,Business Phone,Business Fax,Pager,Company,Job Title,Department,Office Location,Notes +Firman,Wandayandi,,Firman Wandayandi,firman,firman@php.net,Foo Street,Bandung,99999,West Java,Indonesia,99999999,99999999,999999999,http://php.hm/~firman,Foo Street,Bandung,99999,West Java,Indonesia,,99999999,99999999,99999999,Foo,Web Developer,Foo Net,,"It's +multiline +memo." diff --git a/framework/Data/docs/examples/Yahoo.csv b/framework/Data/docs/examples/Yahoo.csv new file mode 100644 index 000000000..43e09dd9d --- /dev/null +++ b/framework/Data/docs/examples/Yahoo.csv @@ -0,0 +1,4 @@ +"First","Middle","Last","Nickname","Email","Category","Distribution Lists","Yahoo! ID","Home","Work","Pager","Fax","Mobile","Other","Yahoo! Phone","Primary","Alternate Email 1","Alternate Email 2","Personal Website","Business Website","Title","Company","Work Address","Work City","Work County","Work Post Code","Work Country","Home Address","Home Town","Home County","Home Post Code","Home Country","Birthday","Anniversary","Custom 1","Custom 2","Custom 3","Custom 4","Comments" +"Firman","","Wandayandi","firman","firman@php.net","","","","","tarzillamax","99999999","","999999999","","","","","","http://php.hm/~firman","","","Foo","Foo Street","Bandung","West Java","99999","Indonesia","Foo Street","Bandung","West Java","99999","Indonesia","","","","","","","It's +multiline +memo." diff --git a/framework/Data/package.xml b/framework/Data/package.xml new file mode 100644 index 000000000..3601f63e5 --- /dev/null +++ b/framework/Data/package.xml @@ -0,0 +1,121 @@ + + + Horde_Data + pear.horde.org + Horde Data API + This package provides a data import and export API, with backends for: +* CSV +* TSV +* iCalendar +* vCard +* vNote +* vTodo + + + + Jan Schneider + jan + jan@horde.org + yes + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2006-05-08 + + + 0.0.3 + 0.0.3 + + + beta + beta + + LGPL + Converted to package.xml 2.0 for pear.horde.org + + + + + + + + + + + + + + + + + + + + + + + + + 4.2.0 + + + 1.4.0b1 + + + iCalendar + pear.horde.org + + + Horde_MIME + pear.horde.org + + + Util + pear.horde.org + + + + + gettext + + + + + + + + 0.0.2 + 0.0.2 + + + beta + beta + + 2004-04-21 + LGPL + Bugfixes, much better vCard support + + + + + 0.0.1 + 0.0.1 + + + alpha + alpha + + 2003-07-03 + LGPL + Initial release as a PEAR package + + + + diff --git a/framework/Data/tests/csv_importFile_01.phpt b/framework/Data/tests/csv_importFile_01.phpt new file mode 100644 index 000000000..7999b3df0 --- /dev/null +++ b/framework/Data/tests/csv_importFile_01.phpt @@ -0,0 +1,90 @@ +--TEST-- +Simple CSV files +--FILE-- +importFile(dirname(__FILE__) . '/simple_dos.csv', false, '', '', 4)); +var_dump($data->importFile(dirname(__FILE__) . '/simple_unix.csv', false, '', '', 4)); +var_dump($data->importFile(dirname(__FILE__) . '/simple_dos.csv', true, '', '', 4)); +var_dump($data->importFile(dirname(__FILE__) . '/simple_unix.csv', true, '', '', 4)); + +?> +--EXPECT-- +array(2) { + [0]=> + array(4) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(10) "three four" + [3]=> + string(4) "five" + } + [1]=> + array(4) { + [0]=> + string(3) "six" + [1]=> + string(5) "seven" + [2]=> + string(10) "eight nine" + [3]=> + string(4) " ten" + } +} +array(2) { + [0]=> + array(4) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(10) "three four" + [3]=> + string(4) "five" + } + [1]=> + array(4) { + [0]=> + string(3) "six" + [1]=> + string(5) "seven" + [2]=> + string(10) "eight nine" + [3]=> + string(4) " ten" + } +} +array(1) { + [0]=> + array(4) { + ["one"]=> + string(3) "six" + ["two"]=> + string(5) "seven" + ["three four"]=> + string(10) "eight nine" + ["five"]=> + string(4) " ten" + } +} +array(1) { + [0]=> + array(4) { + ["one"]=> + string(3) "six" + ["two"]=> + string(5) "seven" + ["three four"]=> + string(10) "eight nine" + ["five"]=> + string(4) " ten" + } +} \ No newline at end of file diff --git a/framework/Data/tests/simple_dos.csv b/framework/Data/tests/simple_dos.csv new file mode 100644 index 000000000..7cc025df3 --- /dev/null +++ b/framework/Data/tests/simple_dos.csv @@ -0,0 +1,2 @@ +one,two,three four,five +six,seven,eight nine, ten diff --git a/framework/Data/tests/simple_unix.csv b/framework/Data/tests/simple_unix.csv new file mode 100644 index 000000000..7cc025df3 --- /dev/null +++ b/framework/Data/tests/simple_unix.csv @@ -0,0 +1,2 @@ +one,two,three four,five +six,seven,eight nine, ten diff --git a/framework/DataTree/DataTree.php b/framework/DataTree/DataTree.php new file mode 100644 index 000000000..241d8314e --- /dev/null +++ b/framework/DataTree/DataTree.php @@ -0,0 +1,1635 @@ + + * 'group' -- Define each group of objects we want to build.
+ * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Stephane Huther + * @author Chuck Hagenbuch + * @package Horde_DataTree + */ +class DataTree { + + /** + * Array of all data: indexed by id. The format is: + * array(id => 'name' => name, 'parent' => parent). + * + * @var array + */ + var $_data = array(); + + /** + * A hash that can be used to map a full object name + * (parent:child:object) to that object's unique ID. + * + * @var array + */ + var $_nameMap = array(); + + /** + * Actual attribute sorting hash. + * + * @var array + */ + var $_sortHash = null; + + /** + * Hash containing connection parameters. + * + * @var array + */ + var $_params = array(); + + /** + * Constructor. + * + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * We always need 'group', a string that defines the + * prefix for each set of hierarchical data. + */ + function DataTree($params = array()) + { + $this->_params = $params; + } + + /** + * Returns a parameter of this DataTree instance. + * + * @param string $param The parameter to return. + * + * @return mixed The parameter's value or null if it doesn't exist. + */ + function getParam($param) + { + return isset($this->_params[$param]) ? $this->_params[$param] : null; + } + + /** + * Removes an object. + * + * @param string $object The object to remove. + * @param boolean $force Force removal of every child object? + * + * @return TODO + */ + function remove($object, $force = false) + { + if (is_a($object, 'DataTreeObject')) { + $object = $object->getName(); + } + + if (!$this->exists($object)) { + return PEAR::raiseError($object . ' does not exist'); + } + + $children = $this->getNumberOfChildren($object); + if ($children) { + /* TODO: remove children if $force == true */ + return PEAR::raiseError(sprintf(ngettext("Cannot remove, %d child exists.", "Cannot remove, %d children exist.", count($children)), count($children))); + } + + $id = $this->getId($object); + $pid = $this->getParent($object); + $order = $this->_data[$id]['order']; + unset($this->_data[$id], $this->_nameMap[$id]); + + // Shift down the order positions. + $this->_reorder($pid, $order); + + return $id; + } + + /** + * Removes all DataTree objects owned by a certain user. + * + * @abstract + * + * @param string $user A user name. + * + * @return TODO + */ + function removeUserData($user) + { + return PEAR::raiseError('not supported'); + } + + /** + * Move an object to a new parent. + * + * @param mixed $object The object to move. + * @param string $newparent The new parent object. Defaults to the root. + * + * @return mixed True on success, PEAR_Error on error. + */ + function move($object, $newparent = null) + { + $cid = $this->getId($object); + if (is_a($cid, 'PEAR_Error')) { + return PEAR::raiseError(sprintf('Object to move does not exist: %s', $cid->getMessage())); + } + + if (!is_null($newparent)) { + $pid = $this->getId($newparent); + if (is_a($pid, 'PEAR_Error')) { + return PEAR::raiseError(sprintf('New parent does not exist: %s', $pid->getMessage())); + } + } else { + $pid = DATATREE_ROOT; + } + + $this->_data[$cid]['parent'] = $pid; + + return true; + } + + /** + * Change an object's name. + * + * @param mixed $old_object The old object. + * @param string $new_object_name The new object name. + * + * @return mixed True on success, PEAR_Error on error. + */ + function rename($old_object, $new_object_name) + { + /* Check whether the object exists at all */ + if (!$this->exists($old_object)) { + return PEAR::raiseError($old_object . ' does not exist'); + } + + /* Check for duplicates - get parent and create new object + * name */ + $parent = $this->getName($this->getParent($old_object)); + if ($this->exists($parent . ':' . $new_object_name)) { + return PEAR::raiseError('Duplicate name ' . $new_object_name); + } + + /* Replace the old name with the new one in the cache */ + $old_object_id = $this->getId($old_object); + $this->_data[$old_object_id]['name'] = $new_object_name; + + return true; + } + + /** + * Changes the order of the children of an object. + * + * @abstract + * + * @param string $parent The full id path of the parent object. + * @param mixed $order If an array it specifies the new positions for + * all child objects. + * If an integer and $cid is specified, the position + * where the child specified by $cid is inserted. If + * $cid is not specified, the position gets deleted, + * causing the following positions to shift up. + * @param integer $cid See $order. + * + * @return TODO + */ + function reorder($parents, $order = null, $cid = null) + { + return PEAR::raiseError('not supported'); + } + + /** + * Change order of children of an object. + * + * @param string $pid The parent object id string path. + * @param mixed $order Specific new order position or an array containing + * the new positions for the given parent. + * @param integer $cid If provided indicates insertion of a new child to + * the parent to avoid incrementing it when + * shifting up all other children's order. If not + * provided indicates deletion, so shift all other + * positions down one. + */ + function _reorder($pid, $order = null, $cid = null) + { + if (!is_array($order) && !is_null($order)) { + // Single update (add/del). + if (is_null($cid)) { + // No id given so shuffle down. + foreach ($this->_data as $c_key => $c_val) { + if ($this->_data[$c_key]['parent'] == $pid && + $this->_data[$c_key]['order'] > $order) { + --$this->_data[$c_key]['order']; + } + } + } else { + // We have an id so shuffle up. + foreach ($this->_data as $c_key => $c_val) { + if ($c_key != $cid && + $this->_data[$c_key]['parent'] == $pid && + $this->_data[$c_key]['order'] >= $order) { + ++$this->_data[$c_key]['order']; + } + } + } + } elseif (is_array($order) && count($order)) { + // Multi update. + foreach ($order as $order_position => $cid) { + $this->_data[$cid]['order'] = $order_position; + } + } + } + + /** + * Explicitly set the order for a datatree object. + * + * @abstract + * + * @param integer $id The datatree object id to change. + * @param integer $order The new order. + * + * @return TODO + */ + function setOrder($id, $order) + { + return PEAR::raiseError('not supported'); + } + + /** + * Dynamically determines the object class. + * + * @param array $attributes The set of attributes that contain the class + * information. Defaults to DataTreeObject. + * + * @return TODO + */ + function _defineObjectClass($attributes) + { + $class = 'DataTreeObject'; + if (!is_array($attributes)) { + return $class; + } + + foreach ($attributes as $attr) { + if ($attr['name'] == 'DataTree') { + switch ($attr['key']) { + case 'objectClass': + $class = $attr['value']; + break; + + case 'objectType': + $result = explode('/', $attr['value']); + $class = $GLOBALS['registry']->callByPackage($result[0], 'defineClass', array('type' => $result[1])); + break; + } + } + } + + return $class; + } + + /** + * Returns a DataTreeObject (or subclass) object of the data in the + * object defined by $object. + * + * @param string $object The object to fetch: 'parent:sub-parent:name'. + * @param string $class Subclass of DataTreeObject to use. Defaults to + * DataTreeObject. Null forces the driver to look + * into the attributes table to determine the + * subclass to use. If none is found it uses + * DataTreeObject. + * + * @return TODO + */ + function &getObject($object, $class = 'DataTreeObject') + { + if (empty($object)) { + $error = PEAR::raiseError('No object requested.'); + return $error; + } + + $this->_load($object); + if (!$this->exists($object)) { + $error = PEAR::raiseError($object . ' not found.'); + return $error; + } + + return $this->_getObject($this->getId($object), $object, $class); + } + + /** + * Returns a DataTreeObject (or subclass) object of the data in the + * object with the ID $id. + * + * @param integer $id An object id. + * @param string $class Subclass of DataTreeObject to use. Defaults to + * DataTreeObject. Null forces the driver to look + * into the attributes table to determine the + * subclass to use. If none is found it uses + * DataTreeObject. + * + * @return TODO + */ + function &getObjectById($id, $class = 'DataTreeObject') + { + if (empty($id)) { + $object = PEAR::raiseError('No id requested.'); + return $object; + } + + $result = $this->_loadById($id); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + return $this->_getObject($id, $this->getName($id), $class); + } + + /** + * Helper function for getObject() and getObjectById(). + * + * @access private + */ + function &_getObject($id, $name, $class) + { + $use_attributes = is_null($class) || is_callable(array($class, '_fromAttributes')); + if ($use_attributes) { + $attributes = $this->getAttributes($id); + if (is_a($attributes, 'PEAR_Error')) { + return $attributes; + } + + if (is_null($class)) { + $class = $this->_defineObjectClass($attributes); + } + } + + if (!class_exists($class)) { + $error = PEAR::raiseError($class . ' not found.'); + return $error; + } + + $dataOb = new $class($name); + $dataOb->setDataTree($this); + + /* If the class has a _fromAttributes method, load data from + * the attributes backend. */ + if ($use_attributes) { + $dataOb->_fromAttributes($attributes); + } else { + /* Otherwise load it from the old data storage field. */ + $dataOb->setData($this->getData($id)); + } + + $dataOb->setOrder($this->getOrder($name)); + return $dataOb; + } + + /** + * Returns an array of DataTreeObject (or subclass) objects + * corresponding to the objects in $ids, with the object + * names as the keys of the array. + * + * @param array $ids An array of object ids. + * @param string $class Subclass of DataTreeObject to use. Defaults to + * DataTreeObject. Null forces the driver to look + * into the attributes table to determine the + * subclass to use. If none is found it uses + * DataTreeObject. + * + * @return TODO + */ + function &getObjects($ids, $class = 'DataTreeObject') + { + $result = $this->_loadById($ids); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $defineClass = is_null($class); + $attributes = $defineClass || is_callable(array($class, '_fromAttributes')); + + if ($attributes) { + $data = $this->getAttributes($ids); + } else { + $data = $this->getData($ids); + } + + $objects = array(); + foreach ($ids as $id) { + $name = $this->getName($id); + if (!empty($name) && !empty($data[$id])) { + if ($defineClass) { + $class = $this->_defineObjectClass($data[$id]); + } + + if (!class_exists($class)) { + return PEAR::raiseError($class . ' not found.'); + } + + $objects[$name] = new $class($name); + $objects[$name]->setDataTree($this); + if ($attributes) { + $objects[$name]->_fromAttributes($data[$id]); + } else { + $objects[$name]->setData($data[$id]); + } + $objects[$name]->setOrder($this->getOrder($name)); + } + } + + return $objects; + } + + /** + * Export a list of objects. + * + * @param constant $format Format of the export + * @param string $startleaf The name of the leaf from which we start + * the export tree. + * @param boolean $reload Re-load the requested chunk? Defaults to + * false (only what is currently loaded). + * @param string $rootname The label to use for the root element. + * Defaults to DATATREE_ROOT. + * @param integer $maxdepth The maximum number of levels to return. + * Defaults to DATATREE_ROOT, which is no + * limit. + * @param boolean $loadTree Load a tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return mixed The tree representation of the objects, or a PEAR_Error + * on failure. + */ + function get($format, $startleaf = DATATREE_ROOT, $reload = false, + $rootname = DATATREE_ROOT, $maxdepth = -1, $loadTree = false, + $sortby_name = null, $sortby_key = null, $direction = 0) + { + $out = array(); + + /* Set sorting hash */ + if (!is_null($sortby_name)) { + $this->_sortHash = DataTree::sortHash($startleaf, $sortby_name, $sortby_key, $direction); + } + + $this->_load($startleaf, $loadTree, $reload, $sortby_name, $sortby_key, $direction); + + switch ($format) { + case DATATREE_FORMAT_TREE: + $startid = $this->getId($startleaf, $maxdepth); + if (is_a($startid, 'PEAR_Error')) { + return $startid; + } + $this->_extractAllLevelTree($out, $startid, $maxdepth); + break; + + case DATATREE_FORMAT_FLAT: + $startid = $this->getId($startleaf); + if (is_a($startid, 'PEAR_Error')) { + return $startid; + } + $this->_extractAllLevelList($out, $startid, $maxdepth); + if (!empty($out[DATATREE_ROOT])) { + $out[DATATREE_ROOT] = $rootname; + } + break; + + default: + return PEAR::raiseError('Not supported'); + } + + if (!is_null($this->_sortHash)) { + /* Reset sorting hash. */ + $this->_sortHash = null; + + /* Reverse since the attribute sorting combined with tree up-ward + * sorting produces a reversed object order. */ + $out = array_reverse($out, true); + } + + return $out; + } + + /** + * Counts objects. + * + * @param string $startleaf The name of the leaf from which we start + * counting. + * + * @return integer The number of the objects below $startleaf. + */ + function count($startleaf = DATATREE_ROOT) + { + return $this->_count($startleaf); + } + + /** + * Create attribute sort hash + * + * @since Horde 3.1 + * + * @param string $root The name of the leaf from which we start + * the export tree. + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return string The sort hash. + */ + function sortHash($root, $sortby_name = null, $sortby_key = null, + $direction = 0) + { + return sprintf('%s-%s-%s-%s', $root, $sortby_name, $sortby_key, $direction); + } + + /** + * Export a list of objects just like get() above, but uses an + * object id to fetch the list of objects. + * + * @param constant $format Format of the export. + * @param string $startleaf The id of the leaf from which we start the + * export tree. + * @param boolean $reload Reload the requested chunk? Defaults to + * false (only what is currently loaded). + * @param string $rootname The label to use for the root element. + * Defaults to DATATREE_ROOT. + * @param integer $maxdepth The maximum number of levels to return + * Defaults to -1, which is no limit. + * + * @return mixed The tree representation of the objects, or a PEAR_Error + * on failure. + */ + function getById($format, $startleaf = DATATREE_ROOT, $reload = false, + $rootname = DATATREE_ROOT, $maxdepth = -1) + { + $this->_loadById($startleaf); + $out = array(); + + switch ($format) { + case DATATREE_FORMAT_TREE: + $this->_extractAllLevelTree($out, $startleaf, $maxdepth); + break; + + case DATATREE_FORMAT_FLAT: + $this->_extractAllLevelList($out, $startleaf, $maxdepth); + if (!empty($out[DATATREE_ROOT])) { + $out[DATATREE_ROOT] = $rootname; + } + break; + + default: + return PEAR::raiseError('Not supported'); + } + + return $out; + } + + /** + * Returns a list of all groups (root nodes) of the data tree. + * + * @abstract + * + * @return mixed The group IDs or PEAR_Error on error. + */ + function getGroups() + { + return PEAR::raiseError('not supported'); + } + + /** + * Retrieve data for an object from the datatree_data field. + * + * @abstract + * + * @param integer $cid The object id to fetch, or an array of object ids. + * + * @return TODO + */ + function getData($cid) + { + return PEAR::raiseError('not supported'); + } + + /** + * Import a list of objects. Used by drivers to populate the internal + * $_data array. + * + * @param array $data The data to import. + * @param string $charset The charset to convert the object name from. + * + * @return TODO + */ + function set($data, $charset = null) + { + $cids = array(); + foreach ($data as $id => $cat) { + if (!is_null($charset)) { + $cat[1] = Horde_String::convertCharset($cat[1], $charset); + } + $cids[$cat[0]] = $cat[1]; + $cparents[$cat[0]] = $cat[2]; + $corders[$cat[0]] = $cat[3]; + $sorders[$cat[0]] = $id; + } + + foreach ($cids as $id => $name) { + $this->_data[$id]['name'] = $name; + $this->_data[$id]['order'] = $corders[$id]; + if (!is_null($this->_sortHash)) { + $this->_data[$id]['sorter'][$this->_sortHash] = $sorders[$id]; + } + if (!empty($cparents[$id])) { + $parents = explode(':', substr($cparents[$id], 1)); + $par = $parents[count($parents) - 1]; + $this->_data[$id]['parent'] = $par; + + if (!empty($this->_nameMap[$par])) { + // If we've already loaded the direct parent of + // this object, use that to find the full name. + $this->_nameMap[$id] = $this->_nameMap[$par] . ':' . $name; + } else { + // Otherwise, run through parents one by one to + // build it up. + $this->_nameMap[$id] = ''; + foreach ($parents as $parID) { + if (!empty($cids[$parID])) { + $this->_nameMap[$id] .= ':' . $cids[$parID]; + } + } + $this->_nameMap[$id] = substr($this->_nameMap[$id], 1) . ':' . $name; + } + } else { + $this->_data[$id]['parent'] = DATATREE_ROOT; + $this->_nameMap[$id] = $name; + } + } + + return true; + } + + /** + * Extract one level of data for a parent leaf, sorted first by + * their order and then by name. This function is a way to get a + * collection of $leaf's children. + * + * @param string $leaf Name of the parent from which to start. + * + * @return array TODO + */ + function _extractOneLevel($leaf = DATATREE_ROOT) + { + $out = array(); + foreach ($this->_data as $id => $vals) { + if ($vals['parent'] == $leaf) { + $out[$id] = $vals; + } + } + + uasort($out, array($this, (is_null($this->_sortHash)) ? '_cmp' : '_cmpSorted')); + + return $out; + } + + /** + * Extract all levels of data, starting from a given parent + * leaf in the datatree. + * + * @access private + * + * @note If nothing is returned that means there is no child, but + * don't forget to add the parent if any subsequent operations are + * required! + * + * @param array $out This is an iterating function, so $out is + * passed by reference to contain the result. + * @param string $parent The name of the parent from which to begin. + * @param integer $maxdepth Max of levels of depth to check. + * + * @return TODO + */ + function _extractAllLevelTree(&$out, $parent = DATATREE_ROOT, + $maxdepth = -1) + { + if ($maxdepth == 0) { + return false; + } + + $out[$parent] = true; + + $k = $this->_extractOneLevel($parent); + foreach (array_keys($k) as $object) { + if (!is_array($out[$parent])) { + $out[$parent] = array(); + } + $out[$parent][$object] = true; + $this->_extractAllLevelTree($out[$parent], $object, $maxdepth - 1); + } + } + + /** + * Extract all levels of data, starting from any parent in + * the tree. + * + * Returned array format: array(parent => array(child => true)) + * + * @access private + * + * @param array $out This is an iterating function, so $out is + * passed by reference to contain the result. + * @param string $parent The name of the parent from which to begin. + * @param integer $maxdepth Max number of levels of depth to check. + * + * @return TODO + */ + function _extractAllLevelList(&$out, $parent = DATATREE_ROOT, + $maxdepth = -1) + { + if ($maxdepth == 0) { + return false; + } + + // This is redundant most of the time, so make sure we need to + // do it. + if (empty($out[$parent])) { + $out[$parent] = $this->getName($parent); + } + + foreach (array_keys($this->_extractOneLevel($parent)) as $object) { + $out[$object] = $this->getName($object); + $this->_extractAllLevelList($out, $object, $maxdepth - 1); + } + } + + /** + * Returns a child's direct parent ID. + * + * @param mixed $child Either the object, an array containing the + * path elements, or the object name for which + * to look up the parent's ID. + * + * @return mixed The unique ID of the parent or PEAR_Error on error. + */ + function getParent($child) + { + if (is_a($child, 'DataTreeObject')) { + $child = $child->getName(); + } + $id = $this->getId($child); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + return $this->getParentById($id); + } + + /** + * Get a $child's direct parent ID. + * + * @param integer $childId Get the parent of this object. + * + * @return mixed The unique ID of the parent or PEAR_Error on error. + */ + function getParentById($childId) + { + $this->_loadById($childId); + return isset($this->_data[$childId]) ? + $this->_data[$childId]['parent'] : + PEAR::raiseError($childId . ' not found'); + } + + /** + * Get a list of parents all the way up to the root object for + * $child. + * + * @param mixed $child The name of the child + * @param boolean $getids If true, return parent IDs; otherwise, return + * names. + * + * @return mixed [child] [parent] in a tree format or PEAR_Error. + */ + function getParents($child, $getids = false) + { + $pid = $this->getParent($child); + if (is_a($pid, 'PEAR_Error')) { + return PEAR::raiseError('Parents not found: ' . $pid->getMessage()); + } + $pname = $this->getName($pid); + $parents = ($getids) ? array($pid => true) : array($pname => true); + + if ($pid != DATATREE_ROOT) { + if ($getids) { + $parents[$pid] = $this->getParents($pname, $getids); + } else { + $parents[$pname] = $this->getParents($pname, $getids); + } + } + + return $parents; + } + + /** + * Get a list of parents all the way up to the root object for + * $child. + * + * @param integer $childId The id of the child. + * @param array $parents The array, as we build it up. + * + * @return array A flat list of all of the parents of $child, + * hashed in $id => $name format. + */ + function getParentList($childId, $parents = array()) + { + $pid = $this->getParentById($childId); + if (is_a($pid, 'PEAR_Error')) { + return PEAR::raiseError('Parents not found: ' . $pid->getMessage()); + } + + if ($pid != DATATREE_ROOT) { + $parents[$pid] = $this->getName($pid); + $parents = $this->getParentList($pid, $parents); + } + + return $parents; + } + + /** + * Get a parent ID string (id:cid format) for the specified object. + * + * @param mixed $object The object to return a parent string for. + * + * @return string|PEAR_Error The ID "path" to the parent object or + * PEAR_Error on failure. + */ + function getParentIdString($object) + { + $ptree = $this->getParents($object, true); + if (is_a($ptree, 'PEAR_Error')) { + return $ptree; + } + + $pids = ''; + while ((list($id, $parent) = each($ptree)) && is_array($parent)) { + $pids = ':' . $id . $pids; + $ptree = $parent; + } + + return $pids; + } + + /** + * Get the number of children an object has, only counting immediate + * children, not grandchildren, etc. + * + * @param mixed $parent Either the object or the name for which to count + * the children, defaults to the root + * (DATATREE_ROOT). + * + * @return integer + */ + function getNumberOfChildren($parent = DATATREE_ROOT) + { + if (is_a($parent, 'DataTreeObject')) { + $parent = $parent->getName(); + } + $this->_load($parent); + $out = $this->_extractOneLevel($this->getId($parent)); + + return is_array($out) ? count($out) : 0; + } + + /** + * Check if an object exists or not. The root element DATATREE_ROOT always + * exists. + * + * @param mixed $object The name of the object. + * + * @return boolean True if the object exists, false otherwise. + */ + function exists($object) + { + if (empty($object)) { + return false; + } + + if (is_a($object, 'DataTreeObject')) { + $object = $object->getName(); + } elseif (is_array($object)) { + $object = implode(':', $object); + } + + if ($object == DATATREE_ROOT) { + return true; + } + + if (array_search($object, $this->_nameMap) !== false) { + return true; + } + + // Consult the backend directly. + return $this->_exists($object); + } + + /** + * Get the name of an object from its id. + * + * @param integer $id The id for which to look up the name. + * + * @return string TODO + */ + function getName($id) + { + /* If no id or if id is a PEAR error, return null. */ + if (empty($id) || is_a($id, 'PEAR_Error')) { + return null; + } + + /* If checking name of root, return DATATREE_ROOT. */ + if ($id == DATATREE_ROOT) { + return DATATREE_ROOT; + } + + /* If found in the name map, return the name. */ + if (isset($this->_nameMap[$id])) { + return $this->_nameMap[$id]; + } + + /* Not found in name map, consult the backend. */ + return $this->_getName($id); + } + + /** + * Get the id of an object from its name. + * + * @param mixed $name Either the object, an array containing the + * path elements, or the object name for which + * to look up the id. + * + * @return string + */ + function getId($name) + { + /* Check if $name is not a string. */ + if (is_a($name, 'DataTreeObject')) { + /* DataTreeObject, get the string name. */ + $name = $name->getName(); + } elseif (is_array($name)) { + /* Path array, implode to get the string name. */ + $name = implode(':', $name); + } + + /* If checking id of root, return DATATREE_ROOT. */ + if ($name == DATATREE_ROOT) { + return DATATREE_ROOT; + } + + /* Flip the name map to look up the id using the name as key. */ + if (($id = array_search($name, $this->_nameMap)) !== false) { + return $id; + } + + /* Not found in name map, consult the backend. */ + $id = $this->_getId($name); + if (is_null($id)) { + return PEAR::raiseError($name . ' does not exist'); + } + return $id; + } + + /** + * Get the order position of an object. + * + * @param mixed $child Either the object or the name. + * + * @return mixed The object's order position or a PEAR error on failure. + */ + function getOrder($child) + { + if (is_a($child, 'DataTreeObject')) { + $child = $child->getName(); + } + $id = $this->getId($child); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + $this->_loadById($id); + + return isset($this->_data[$id]['order']) ? + $this->_data[$id]['order'] : + null; + } + + /** + * Replace all occurences of ':' in an object name with '.'. + * + * @param string $name The name of the object. + * + * @return string The encoded name. + */ + function encodeName($name) + { + return str_replace(':', '.', $name); + } + + /** + * Get the short name of an object, returns only the last portion of the + * full name. For display purposes only. + * + * @static + * + * @param string $name The name of the object. + * + * @return string The object's short name. + */ + function getShortName($name) + { + /* If there are several components to the name, explode and get the + * last one, otherwise just return the name. */ + if (strpos($name, ':') !== false) { + $name = explode(':', $name); + $name = array_pop($name); + } + return $name; + } + + /** + * Returns a tree sorted by the specified attribute name and/or key. + * + * @abstract + * + * @since Horde 3.1 + * + * @param string $root Which portion of the tree to sort. + * Defaults to all of it. + * @param boolean $loadTree Sort the tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array TODO + */ + function getSortedTree($root, $loadTree = false, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + return PEAR::raiseError('not supported'); + } + + /** + * Adds an object. + * + * @abstract + * + * @param mixed $object The object to add (string or + * DataTreeObject). + * @param boolean $id_as_name True or false to indicate if object ID is to + * be used as object name. Used in situations + * where there is no available unique input for + * object name. + * + * @return TODO + */ + function add($object, $id_as_name = false) + { + return PEAR::raiseError('not supported'); + } + + /** + * Add an object. + * + * @private + * + * @param string $name The short object name. + * @param integer $id The new object's unique ID. + * @param integer $pid The unique ID of the object's parent. + * @param integer $order The ordering data for the object. + * + * @access protected + * + * @return TODO + */ + function _add($name, $id, $pid, $order = '') + { + $this->_data[$id] = array('name' => $name, + 'parent' => $pid, + 'order' => $order); + $this->_nameMap[$id] = $name; + + /* Shift along the order positions. */ + $this->_reorder($pid, $order, $id); + + return true; + } + + /** + * Retrieve data for an object from the horde_datatree_attributes + * table. + * + * @abstract + * + * @param integer | array $cid The object id to fetch, + * or an array of object ids. + * + * @return array A hash of attributes, or a multi-level hash + * of object ids => their attributes. + */ + function getAttributes($cid) + { + return PEAR::raiseError('not supported'); + } + + /** + * Returns the number of objects matching a set of attribute criteria. + * + * @abstract + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * + * @return TODO + */ + function countByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name') + { + return PEAR::raiseError('not supported'); + } + + /** + * Returns a set of object ids based on a set of attribute criteria. + * + * @abstract + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param integer $from The object to start to fetching + * @param integer $count The number of objects to fetch + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return TODO + */ + function getByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name', $from = 0, + $count = 0, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + return PEAR::raiseError('not supported'); + } + + /** + * Sorts IDs by attribute values. IDs without attributes will be added to + * the end of the sorted list. + * + * @abstract + * + * @param array $unordered_ids Array of ids to sort. + * @param array $sortby_name Attribute name to use for sorting. + * @param array $sortby_key Attribute key to use for sorting. + * @param array $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array Sorted ids. + */ + function sortByAttributes($unordered_ids, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + return PEAR::raiseError('not supported'); + } + + /** + * Update the data in an object. Does not change the object's + * parent or name, just serialized data or attributes. + * + * @abstract + * + * @param DataTree $object A DataTree object. + * + * @return TODO + */ + function updateData($object) + { + return PEAR::raiseError('not supported'); + } + + /** + * Sort two objects by their order field, and if that is the same, + * alphabetically (case insensitive) by name. + * + * You never call this function; it's used in uasort() calls. Do + * NOT use usort(); you'll lose key => value associations. + * + * @private + * + * @param array $a The first object + * @param array $b The second object + * + * @return integer 1 if $a should be first, + * -1 if $b should be first, + * 0 if they are entirely equal. + */ + function _cmp($a, $b) + { + if ($a['order'] > $b['order']) { + return 1; + } elseif ($a['order'] < $b['order']) { + return -1; + } else { + return strcasecmp($a['name'], $b['name']); + } + } + + /** + * Sorts two objects by their sorter hash field. + * + * You never call this function; it's used in uasort() calls. Do NOT use + * usort(); you'll lose key => value associations. + * + * @since Horde 3.1 + * + * @private + * + * @param array $a The first object + * @param array $b The second object + * + * @return integer 1 if $a should be first, + * -1 if $b should be first, + * 0 if they are entirely equal. + */ + function _cmpSorted($a, $b) + { + return intval($a['sorter'][$this->_sortHash] < $b['sorter'][$this->_sortHash]); + } + + /** + * Attempts to return a concrete DataTree instance based on $driver. + * + * @param mixed $driver The type of concrete DataTree subclass to return. + * This is based on the storage driver ($driver). The + * code is dynamically included. If $driver is an array, + * then we will look in $driver[0]/lib/DataTree/ for + * the subclass implementation named $driver[1].php. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * Here, we need 'group' = a string that defines + * top-level groups of objects. + * + * @return DataTree The newly created concrete DataTree instance, or false + * on an error. + */ + function &factory($driver, $params = null) + { + $driver = basename($driver); + + if (is_null($params)) { + $params = Horde::getDriverConfig('datatree', $driver); + } + + if (empty($driver)) { + $driver = 'null'; + } + + include_once 'Horde/DataTree/' . $driver . '.php'; + $class = 'DataTree_' . $driver; + if (class_exists($class)) { + $dt = new $class($params); + $result = $dt->_init(); + if (is_a($result, 'PEAR_Error')) { + include_once 'Horde/DataTree/null.php'; + $dt = new DataTree_null($params); + } + } else { + $dt = PEAR::raiseError('Class definition of ' . $class . ' not found.'); + } + + return $dt; + } + + /** + * Attempts to return a reference to a concrete DataTree instance based on + * $driver. + * + * It will only create a new instance if no DataTree instance with the same + * parameters currently exists. + * + * This should be used if multiple DataTree sources (and, thus, multiple + * DataTree instances) are required. + * + * This method must be invoked as: $var = &DataTree::singleton(); + * + * @param mixed $driver Type of concrete DataTree subclass to return, + * based on storage driver ($driver). The code is + * dynamically included. If $driver is an array, then + * look in $driver[0]/lib/DataTree/ for subclass + * implementation named $driver[1].php. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return DataTree The concrete DataTree reference, or false on an error. + */ + function &singleton($driver, $params = null) + { + static $instances = array(); + + if (is_null($params)) { + $params = Horde::getDriverConfig('datatree', $driver); + } + + $signature = serialize(array($driver, $params)); + if (!isset($instances[$signature])) { + $instances[$signature] = &DataTree::factory($driver, $params); + } + + return $instances[$signature]; + } + +} + +/** + * Class that can be extended to save arbitrary information as part of a stored + * object. + * + * @author Stephane Huther + * @author Chuck Hagenbuch + * @since Horde 2.1 + * @package Horde_DataTree + */ +class DataTreeObject { + + /** + * This object's DataTree instance. + * + * @var DataTree + */ + var $datatree; + + /** + * Key-value hash that will be serialized. + * + * @see getData() + * @var array + */ + var $data = array(); + + /** + * The unique name of this object. + * These names have the same requirements as other object names - they must + * be unique, etc. + * + * @var string + */ + var $name; + + /** + * If this object has ordering data, store it here. + * + * @var integer + */ + var $order = null; + + /** + * DataTreeObject constructor. + * Just sets the $name parameter. + * + * @param string $name The object name. + */ + function DataTreeObject($name) + { + $this->setName($name); + } + + /** + * Sets the {@link DataTree} instance used to retrieve this object. + * + * @param DataTree $datatree A {@link DataTree} instance. + */ + function setDataTree(&$datatree) + { + $this->datatree = &$datatree; + } + + /** + * Gets the name of this object. + * + * @return string The object name. + */ + function getName() + { + return $this->name; + } + + /** + * Sets the name of this object. + * + * NOTE: Use with caution. This may throw out of sync the cached datatree + * tables if not used properly. + * + * @param string $name The name to set this object's name to. + */ + function setName($name) + { + $this->name = $name; + } + + /** + * Gets the short name of this object. + * For display purposes only. + * + * @return string The object's short name. + */ + function getShortName() + { + return DataTree::getShortName($this->name); + } + + /** + * Gets the ID of this object. + * + * @return string The object's ID. + */ + function getId() + { + return $this->datatree->getId($this); + } + + /** + * Gets the data array. + * + * @return array The internal data array. + */ + function getData() + { + return $this->data; + } + + /** + * Sets the data array. + * + * @param array The data array to store internally. + */ + function setData($data) + { + $this->data = $data; + } + + /** + * Sets the order of this object in its object collection. + * + * @param integer $order + */ + function setOrder($order) + { + $this->order = $order; + } + + /** + * Returns this object's parent. + * + * @param string $class Subclass of DataTreeObject to use. Defaults to + * DataTreeObject. Null forces the driver to look + * into the attributes table to determine the + * subclass to use. If none is found it uses + * DataTreeObject. + * + * @return DataTreeObject This object's parent + */ + function &getParent($class = 'DataTreeObject') + { + $id = $this->datatree->getParent($this); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + return $this->datatree->getObjectById($id, $class); + } + + /** + * Returns a child of this object. + * + * @param string $name The child's name. + * @param boolean $autocreate If true and no child with the given name + * exists, one gets created. + */ + function &getChild($name, $autocreate = true) + { + $name = $this->getShortName() . ':' . $name; + + /* If the child shouldn't get created, we don't check for its + * existance to return the "not found" error of + * getObject(). */ + if (!$autocreate || $this->datatree->exists($name)) { + $child = &$this->datatree->getObject($name); + } else { + $child = new DataTreeObject($name); + $child->setDataTree($this->datatree); + $this->datatree->add($child); + } + + return $child; + } + + /** + * Saves any changes to this object to the backend permanently. New objects + * are added instead. + * + * @return boolean|PEAR_Error PEAR_Error on failure. + */ + function save() + { + if ($this->datatree->exists($this)) { + return $this->datatree->updateData($this); + } else { + return $this->datatree->add($this); + } + } + + /** + * Delete this object from the backend permanently. + * + * @return boolean|PEAR_Error PEAR_Error on failure. + */ + function delete() + { + return $this->datatree->remove($this); + } + + /** + * Gets one of the attributes of the object, or null if it isn't defined. + * + * @param string $attribute The attribute to get. + * + * @return mixed The value of the attribute, or null. + */ + function get($attribute) + { + return isset($this->data[$attribute]) + ? $this->data[$attribute] + : null; + } + + /** + * Sets one of the attributes of the object. + * + * @param string $attribute The attribute to set. + * @param mixed $value The value for $attribute. + */ + function set($attribute, $value) + { + $this->data[$attribute] = $value; + } + +} diff --git a/framework/DataTree/DataTree/null.php b/framework/DataTree/DataTree/null.php new file mode 100644 index 000000000..729f001b6 --- /dev/null +++ b/framework/DataTree/DataTree/null.php @@ -0,0 +1,417 @@ + + * @author Chuck Hagenbuch + * @since Horde 3.0 + * @package Horde_DataTree + */ +class DataTree_null extends DataTree { + + /** + * Cache of attributes for any objects created during this page request. + * + * @var array + */ + var $_attributeCache = array(); + + /** + * Cache of data for any objects created during this page request. + * + * @var array + */ + var $_dataCache = array(); + + /** + * Load (a subset of) the datatree into the $_data array. Part of the + * DataTree API that must be overridden by subclasses. + * + * @param string $root Which portion of the tree to load. Defaults to + * all of it. + * @param boolean $reload Re-load already loaded values? + * + * @return mixed True on success or a PEAR_Error on failure. + * + * @access private + */ + function _load($root = null, $reload = false) + { + } + + /** + * Load a specific object identified by its unique ID ($id), and + * its parents, into the $_data array. + * + * @param integer $cid The unique ID of the object to load. + * + * @return mixed True on success or a PEAR_Error on failure. + * + * @access private + */ + function _loadById($cid) + { + } + + /** + * Check for existance of an object in a backend-specific manner. + * + * @param string $object_name Object name to check for. + * + * @return boolean True if the object exists, false otherwise. + */ + function _exists($object_name) + { + return false; + } + + /** + * Look up a datatree id by name. + * + * @param string $name + * + * @return integer DataTree id + */ + function _getId($name) + { + return null; + } + + /** + * Look up a datatree name by id. + * + * @param integer $id + * + * @return string DataTree name + */ + function _getName($id) + { + return null; + } + + /** + * Get a tree sorted by the specified attribute name and/or key. + * + * @since Horde 3.1 + * + * @param string $root Which portion of the tree to sort. + * Defaults to all of it. + * @param boolean $loadTree Sort the tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param array $sortby_name Attribute name to use for sorting. + * @param array $sortby_key Attribute key to use for sorting. + * @param array $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array TODO + */ + function getSortedTree($root, $loadTree = false, $sortby_name = null, $sortby_key = null, $direction = 0) + { + return array(); + } + + /** + * Add an object. Part of the DataTree API that must be + * overridden by subclasses. + * + * @param mixed $fullname The object to add (string or DataTreeObject). + */ + function add($object) + { + if (is_a($object, 'DataTreeObject')) { + $fullname = $object->getName(); + $order = $object->order; + } else { + $fullname = $object; + $order = null; + } + + $id = md5(mt_rand()); + if (strpos($fullname, ':') !== false) { + $parts = explode(':', $fullname); + $name = array_pop($parts); + $parent = implode(':', $parts); + $pid = $this->getId($parent); + if (is_a($pid, 'PEAR_Error')) { + $this->add($parent); + } + } else { + $pid = DATATREE_ROOT; + } + + if (parent::exists($fullname)) { + return PEAR::raiseError('Already exists'); + } + + $added = parent::_add($fullname, $id, $pid, $order); + if (is_a($added, 'PEAR_Error')) { + return $added; + } + return $this->updateData($object); + } + + /** + * Change order of the children of an object. + * + * @param string $parents The parent id string path. + * @param mixed $order A specific new order position or an array + * containing the new positions for the given + * $parents object. + * @param integer $cid If provided indicates insertion of a new child + * to the object, and will be used to avoid + * incrementing it when shifting up all other + * children's order. If not provided indicates + * deletion, hence shift all other positions down + * one. + */ + function reorder($parents, $order = null, $cid = null) + { + if (is_array($order) && !empty($order)) { + // Multi update. + $this->_reorder($pid, $order); + } + } + + /** + * Explicitly set the order for a datatree object. + * + * @param integer $id The datatree object id to change. + * @param integer $order The new order. + */ + function setOrder($id, $order) + { + } + + /** + * Removes an object. + * + * @param mixed $object The object to remove. + * @param boolean $force Force removal of every child object? + */ + function remove($object, $force = false) + { + } + + /** + * Remove one or more objects by id. This function does *not* do + * the validation, reordering, etc. that remove() does. If you + * need to check for children, re-do ordering, etc., then you must + * remove() objects one-by-one. This is for code that knows it's + * dealing with single (non-parented) objects and needs to delete + * a batch of them quickly. + * + * @param array $ids The objects to remove. + */ + function removeByIds($ids) + { + } + + /** + * Remove one or more objects by name. This function does *not* do + * the validation, reordering, etc. that remove() does. If you + * need to check for children, re-do ordering, etc., then you must + * remove() objects one-by-one. This is for code that knows it's + * dealing with single (non-parented) objects and needs to delete + * a batch of them quickly. + * + * @param array $names The objects to remove. + */ + function removeByNames($names) + { + } + + /** + * Move an object to a new parent. + * + * @param mixed $object The object to move. + * @param string $newparent The new parent object. Defaults to the root. + */ + function move($object, $newparent = null) + { + } + + /** + * Change an object's name. + * + * @param mixed $old_object The old object. + * @param string $new_object_name The new object name. + */ + function rename($old_object, $new_object_name) + { + } + + /** + * Retrieve data for an object from the datatree_data field. + * + * @param integer $cid The object id to fetch, or an array of object ids. + */ + function getData($cid) + { + return isset($this->_dataCache[$cid]) ? + $this->_dataCache[$cid] : + array(); + } + + /** + * Retrieve data for an object. + * + * @param integer $cid The object id to fetch. + */ + function getAttributes($cid) + { + if (is_array($cid)) { + $data = array(); + foreach ($cid as $id) { + if (isset($this->_attributeCache[$id])) { + $data[$id] = $this->_attributeCache[$id]; + } + } + + return $data; + } else { + return isset($this->_attributeCache[$cid]) ? + $this->_attributeCache[$cid] : + array(); + } + } + + /** + * Returns the number of objects matching a set of attribute + * criteria. + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + */ + function countByAttributes($criteria, $parent = DATATREE_ROOT, $allLevels = true, $restrict = 'name') + { + if (!count($criteria)) { + return 0; + } + + return count($this->_attributeCache); + } + + /** + * Returns a set of object ids based on a set of attribute criteria. + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param integer $from The object to start to fetching + * @param integer $count The number of objects to fetch + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + */ + function getByAttributes($criteria, $parent = DATATREE_ROOT, $allLevels = true, $restrict = 'name', $from = 0, $count = 0, + $sortby_name = null, $sortby_key = null, $direction = 0) + { + if (!count($criteria)) { + return PEAR::raiseError('no criteria'); + } + + $cids = array(); + foreach (array_keys($this->_attributeCache) as $cid) { + $cids[$cid] = null; + } + return $cids; + } + + /** + * Sorts IDs by attribute values. IDs without attributes will be + * added to the end of the sorted list. + * + * @param array $unordered_ids Array of ids to sort. + * @param array $sortby_name Attribute name to use for sorting. + * @param array $sortby_key Attribute key to use for sorting. + * @param array $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array Sorted ids. + */ + function sortByAttributes($unordered_ids, $sortby_name = null, $sortby_key = null, $direction = 0) + { + return $unordered_ids; + } + + /** + * Returns a list of all of the available values of the given + * attribute name/key combination. Either attribute_name or + * attribute_key MUST be supplied, and both MAY be supplied. + * + * @param string $attribute_name The name of the attribute. + * @param string $attribute_key The key value of the attribute. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? + * + * @return array An array of all of the available values. + */ + function getAttributeValues($attribute_name = null, $attribute_key = null, $parent = DATATREE_ROOT, $allLevels = true) + { + return array(); + } + + /** + * Update the data in an object. Does not change the object's + * parent or name, just serialized data. + * + * @param string $object The object. + */ + function updateData($object) + { + if (!is_a($object, 'DataTreeObject')) { + return true; + } + + $cid = $this->getId($object->getName()); + if (is_a($cid, 'PEAR_Error')) { + return $cid; + } + + // We handle data differently if we can map it to + // attributes. + if (method_exists($object, '_toAttributes')) { + $this->_attributeCache[$cid] = $object->_toAttributes(); + } else { + $this->_dataCache[$cid] = $object->getData(); + } + + return true; + } + + /** + * Init the object. + * + * @return boolean True. + */ + function _init() + { + return true; + } + +} diff --git a/framework/DataTree/DataTree/sql.php b/framework/DataTree/DataTree/sql.php new file mode 100644 index 000000000..0e5c961b1 --- /dev/null +++ b/framework/DataTree/DataTree/sql.php @@ -0,0 +1,1919 @@ + + * 'phptype' The database type (ie. 'pgsql', 'mysql', etc.). + * 'charset' The charset used by the database. + * + * Optional parameters:
+ *   'table'        The name of the data table in 'database'.
+ *                  DEFAULT: 'horde_datatree'
+ * + * Required by some database implementations:
+ *   'database'     The name of the database.
+ *   'username'     The username with which to connect to the database.
+ *   'password'     The password associated with 'username'.
+ *   'hostspec'     The hostname of the database server.
+ *   'protocol'     The communication protocol ('tcp', 'unix', etc.).
+ *   'options'      Additional options to pass to the database.
+ *   'port'         The port on which to connect to the database.
+ *   'tty'          The TTY on which to connect to the database.
+ * + * Optional values when using separate reading and writing servers, for example + * in replication settings:
+ *   'splitread'   Boolean, whether to implement the separation or not.
+ *   'read'        Array containing the parameters which are different for
+ *                 the read database connection, currently supported
+ *                 only 'hostspec' and 'port' parameters.
+ * + * The table structure for the DataTree system is in + * scripts/sql/horde_datatree.sql. + * + * $Horde: framework/DataTree/DataTree/sql.php,v 1.251 2009/07/09 08:17:54 slusarz Exp $ + * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If + * you did not receive this file, see + * http://www.fsf.org/copyleft/lgpl.html. + * + * @author Stephane Huther + * @author Chuck Hagenbuch + * @author Jan Schneider + * @since Horde 2.1 + * @package Horde_DataTree + */ +class DataTree_sql extends DataTree { + + /** + * Handle for the current database connection, used for reading. + * + * @var DB + */ + var $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $_db if a separate write database is not required. + * + * @var DB + */ + var $_write_db; + + /** + * The number of copies of the horde_datatree_attributes table + * that we need to join on in the current query. + * + * @var integer + */ + var $_tableCount = 1; + + /** + * Returns a list of all groups (root nodes) of the data tree. + * + * @return array The the group IDs + */ + function getGroups() + { + $query = 'SELECT DISTINCT group_uid FROM ' . $this->_params['table']; + + Horde::logMessage('SQL Query by DataTree_sql::getGroups(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + return $this->_db->getCol($query); + } + + /** + * Loads (a subset of) the datatree into the $_data array. + * + * @access private + * + * @param string $root Which portion of the tree to load. + * Defaults to all of it. + * @param boolean $loadTree Load a tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param boolean $reload Re-load already loaded values? + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return mixed True on success or a PEAR_Error on failure. + */ + function _load($root = DATATREE_ROOT, $loadTree = false, $reload = false, + $sortby_name = null, $sortby_key = null, $direction = 0) + { + /* Do NOT use DataTree::exists() here; that would cause an infinite + * loop. */ + if (!$reload && + (in_array($root, $this->_nameMap) || + (count($this->_data) && $root == DATATREE_ROOT)) || + (!is_null($this->_sortHash) && + isset($this->_data[$root]['sorter'][$this->_sortHash]))) { + return true; + } + + $query = $this->_buildLoadQuery($root, + $loadTree, + DATATREE_BUILD_SELECT, + $sortby_name, + $sortby_key, + $direction); + if (empty($query)) { + return true; + } + + Horde::logMessage('SQL Query by DataTree_sql::_load(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $data = $this->_db->getAll($query); + if (is_a($data, 'PEAR_Error')) { + return $data; + } + return $this->set($data, $this->_params['charset']); + } + + /** + * Counts (a subset of) the datatree which would be loaded into the $_data + * array if _load() is called with the same value of $root. + * + * @access private + * + * @param string $root Which portion of the tree to load. Defaults to all + * of it. + * + * @return integer Number of objects + */ + function _count($root = DATATREE_ROOT) + { + $query = $this->_buildLoadQuery($root, true, DATATREE_BUILD_COUNT); + if (empty($query)) { + return 0; + } + Horde::logMessage('SQL Query by DataTree_sql::_count(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + return (int)$this->_db->getOne($query); + } + + /** + * Loads (a subset of) the datatree into the $_data array. + * + * @access private + * + * @param string $root Which portion of the tree to load. + * Defaults to all of it. + * @param boolean $loadTree Load a tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param integer $operation Type of query to build + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return mixed True on success or a PEAR_Error on failure. + */ + function _buildLoadQuery($root = DATATREE_ROOT, $loadTree = false, + $operation = DATATREE_BUILD_SELECT, + $sortby_name = null, $sortby_key = null, + $direction = 0) + { + $sorted = false; + $where = sprintf('c.group_uid = %s ', $this->_db->quote($this->_params['group'])); + + if (!empty($root) && $root != DATATREE_ROOT) { + $parent_where = $this->_buildParentIds($root, $loadTree, 'c.'); + if (empty($parent_where)) { + return ''; + } elseif (!is_a($parent_where, 'PEAR_Error')) { + $where = sprintf('%s AND (%s)', $where, $parent_where); + } + } + if (!is_null($sortby_name)) { + $where = sprintf('%s AND a.attribute_name = %s ', $where, $this->_db->quote($sortby_name)); + $sorted = true; + } + if (!is_null($sortby_key)) { + $where = sprintf('%s AND a.attribute_key = %s ', $where, $this->_db->quote($sortby_key)); + $sorted = true; + } + + switch ($operation) { + case DATATREE_BUILD_COUNT: + $what = 'COUNT(*)'; + break; + + default: + $what = 'c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order'; + break; + } + + if ($sorted) { + $query = sprintf('SELECT %s FROM %s c LEFT JOIN %s a ON (c.datatree_id = a.datatree_id OR c.datatree_name=%s) '. + 'WHERE %s GROUP BY c.datatree_id, c.datatree_name, c.datatree_parents, c.datatree_order ORDER BY a.attribute_value %s', + $what, + $this->_params['table'], + $this->_params['table_attributes'], + $this->_db->quote($root), + $where, + ($direction == 1) ? 'DESC' : 'ASC'); + } else { + $query = sprintf('SELECT %s FROM %s c WHERE %s', + $what, + $this->_params['table'], + $where); + } + + return $query; + } + + /** + * Builds parent ID string for selecting trees. + * + * @access private + * + * @param string $root Which portion of the tree to load. + * @param boolean $loadTree Load a tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param string $join_name Table join name + * + * @return string Id list. + */ + function _buildParentIds($root, $loadTree = false, $join_name = '') + { + if (strpos($root, ':') !== false) { + $parts = explode(':', $root); + $root = array_pop($parts); + } + $root = (string)$root; + + $query = 'SELECT datatree_id, datatree_parents' . + ' FROM ' . $this->_params['table'] . + ' WHERE datatree_name = ? AND group_uid = ?' . + ' ORDER BY datatree_id'; + $values = array($root, + $this->_params['group']); + + Horde::logMessage('SQL Query by DataTree_sql::_buildParentIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $root = $this->_db->getAssoc($query, false, $values); + if (is_a($root, 'PEAR_Error') || !count($root)) { + return ''; + } + + $where = ''; + $first_time = true; + foreach ($root as $object_id => $object_parents) { + $pstring = $object_parents . ':' . $object_id . '%'; + $pquery = ''; + if (!empty($object_parents)) { + $ids = substr($object_parents, 1); + $pquery = ' OR ' . $join_name . 'datatree_id IN (' . str_replace(':', ', ', $ids) . ')'; + } + if ($loadTree) { + $pquery .= ' OR ' . $join_name . 'datatree_parents = ' . $this->_db->quote(substr($pstring, 0, -1)); + } + + if (!$first_time) { + $where .= ' OR '; + } + $where .= sprintf($join_name . 'datatree_parents LIKE %s OR ' . $join_name . 'datatree_id = %s%s', + $this->_db->quote($pstring), + $object_id, + $pquery); + + $first_time = false; + } + + return $where; + } + + /** + * Loads a set of objects identified by their unique IDs, and their + * parents, into the $_data array. + * + * @access private + * + * @param mixed $cids The unique ID of the object to load, or an array of + * object ids. + * + * @return mixed True on success or a PEAR_Error on failure. + */ + function _loadById($cids) + { + /* Make sure we have an array. */ + if (!is_array($cids)) { + $cids = array((int)$cids); + } else { + array_walk($cids, 'intval'); + } + + /* Bail out now if there's nothing to load. */ + if (!count($cids)) { + return true; + } + + /* Don't load any that are already loaded. Also, make sure that + * everything in the $ids array that we are building is an integer. */ + $ids = array(); + foreach ($cids as $cid) { + /* Do NOT use DataTree::exists() here; that would cause an + * infinite loop. */ + if (!isset($this->_data[$cid])) { + $ids[] = (int)$cid; + } + } + + /* If there are none left to load, return. */ + if (!count($ids)) { + return true; + } + + $in = array_search(DATATREE_ROOT, $ids) === false ? sprintf('datatree_id IN (%s) AND ', implode(', ', $ids)) : ''; + $query = sprintf('SELECT datatree_id, datatree_parents FROM %s' . + ' WHERE %sgroup_uid = %s' . + ' ORDER BY datatree_id', + $this->_params['table'], + $in, + $this->_db->quote($this->_params['group'])); + Horde::logMessage('SQL Query by DataTree_sql::_loadById(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $parents = $this->_db->getAssoc($query); + if (is_a($parents, 'PEAR_Error')) { + return $parents; + } + if (empty($parents)) { + return PEAR::raiseError(_("Object not found."), null, null, null, 'DataTree ids ' . implode(', ', $ids) . ' not found.'); + } + + $ids = array(); + foreach ($parents as $cid => $parent) { + $ids[(int)$cid] = (int)$cid; + + $pids = explode(':', substr($parent, 1)); + foreach ($pids as $pid) { + $pid = (int)$pid; + if (!isset($this->_data[$pid])) { + $ids[$pid] = $pid; + } + } + } + + /* If $ids is empty, we have nothing to load. */ + if (!count($ids)) { + return true; + } + + $query = 'SELECT datatree_id, datatree_name, datatree_parents, datatree_order' . + ' FROM ' . $this->_params['table'] . + ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')' . + ' AND group_uid = ? ORDER BY datatree_id'; + $values = array_merge($ids, array($this->_params['group'])); + + Horde::logMessage('SQL Query by DataTree_sql::_loadById(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $data = $this->_db->getAll($query, $values); + if (is_a($data, 'PEAR_Error')) { + return $data; + } + + return $this->set($data, $this->_params['charset']); + } + + /** + * Check for existance of an object in a backend-specific manner. + * + * @param string $object_name Object name to check for. + * + * @return boolean True if the object exists, false otherwise. + */ + function _exists($object_name) + { + $query = 'SELECT datatree_id FROM ' . $this->_params['table'] . + ' WHERE group_uid = ? AND datatree_name = ? AND datatree_parents = ?'; + + $object_names = explode(':', $object_name); + $object_parents = ''; + foreach ($object_names as $name) { + $values = array($this->_params['group'], $name, $object_parents); + Horde::logMessage('SQL Query by DataTree_sql::_exists(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->getOne($query, $values); + if (is_a($result, 'PEAR_Error') || !$result) { + return false; + } + + $object_parents .= ':' . $result; + } + + return true; + } + + /** + * Look up a datatree id by name. + * + * @param string $name + * + * @return integer DataTree id + */ + function _getId($name) + { + $query = 'SELECT datatree_id FROM ' . $this->_params['table'] + . ' WHERE group_uid = ? AND datatree_name = ?' + . ' AND datatree_parents = ?'; + + $ids = array(); + $parts = explode(':', $name); + foreach ($parts as $part) { + $result = $this->_db->getOne($query, array($this->_params['group'], $part, count($ids) ? ':' . implode(':', $ids) : '')); + if (is_a($result, 'PEAR_Error') || !$result) { + return null; + } else { + $ids[] = $result; + } + } + + return (int)array_pop($ids); + } + + /** + * Look up a datatree name by id. + * + * @param integer $id + * + * @return string DataTree name + */ + function _getName($id) + { + $query = 'SELECT datatree_name FROM ' . $this->_params['table'] . + ' WHERE group_uid = ? AND datatree_id = ?'; + $values = array($this->_params['group'], (int)$id); + Horde::logMessage('SQL Query by DataTree_sql::_getName(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $name = $this->_db->getOne($query, $values); + if (is_a($name, 'PEAR_Error')) { + return null; + } else { + $name = Horde_String::convertCharset($name, $this->_params['charset'], + Horde_Nls::getCharset()); + // Get the parent names, if any. + $parent = $this->getParentById($id); + if ($parent && !is_a($parent, 'PEAR_Error') && + $parent != DATATREE_ROOT) { + return $this->getName($parent) . ':' . $name; + } else { + return $name; + } + } + } + + /** + * Returns a tree sorted by the specified attribute name and/or key. + * + * @since Horde 3.1 + * + * @param string $root Which portion of the tree to sort. + * Defaults to all of it. + * @param boolean $loadTree Sort the tree starting at $root, or just the + * requested level and direct parents? + * Defaults to single level. + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array TODO + */ + function getSortedTree($root, $loadTree = false, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + $query = $this->_buildLoadQuery($root, + $loadTree, + DATATREE_BUILD_SELECT, + $sortby_name, + $sortby_key, + $direction); + + if (empty($query)) { + return array(); + } + return $this->_db->getAll($query); + } + + /** + * Adds an object. + * + * @param mixed $object The object to add (string or + * DataTreeObject). + * @param boolean $id_as_name Whether the object ID is to be used as + * object name. Used in situations where + * there is no available unique input for + * object name. + */ + function add($object, $id_as_name = false) + { + $attributes = false; + if (is_a($object, 'DataTreeObject')) { + $fullname = $object->getName(); + $order = $object->order; + + /* We handle data differently if we can map it to the + * horde_datatree_attributes table. */ + if (method_exists($object, '_toAttributes')) { + $data = ''; + $ser = null; + + /* Set a flag for later so that we know to insert the + * attribute rows. */ + $attributes = true; + } else { + require_once 'Horde/Serialize.php'; + $ser = Horde_Serialize::UTF7_BASIC; + $data = Horde_Serialize::serialize($object->getData(), $ser, Horde_Nls::getCharset()); + } + } else { + $fullname = $object; + $order = null; + $data = ''; + $ser = null; + } + + /* Get the next unique ID. */ + $id = $this->_write_db->nextId($this->_params['table']); + if (is_a($id, 'PEAR_Error')) { + Horde::logMessage($id, __FILE__, __LINE__, PEAR_LOG_ERR); + return $id; + } + + if (strpos($fullname, ':') !== false) { + $parts = explode(':', $fullname); + $parents = ''; + $pstring = ''; + if ($id_as_name) { + /* Requested use of ID as name, so discard current name. */ + array_pop($parts); + /* Set name to ID. */ + $name = $id; + /* Modify fullname to reflect new name. */ + $fullname = implode(':', $parts) . ':' . $id; + if (is_a($object, 'DataTreeObject')) { + $object->setName($fullname); + } else { + $object = $fullname; + } + } else { + $name = array_pop($parts); + } + foreach ($parts as $par) { + $pstring .= (empty($pstring) ? '' : ':') . $par; + $pid = $this->getId($pstring); + if (is_a($pid, 'PEAR_Error')) { + /* Auto-create parents. */ + $pid = $this->add($pstring); + if (is_a($pid, 'PEAR_Error')) { + return $pid; + } + } + $parents .= ':' . $pid; + } + } else { + if ($id_as_name) { + /* Requested use of ID as name, set fullname and name to ID. */ + $fullname = $id; + $name = $id; + if (is_a($object, 'DataTreeObject')) { + $object->setName($fullname); + } else { + $object = $fullname; + } + } else { + $name = $fullname; + } + $parents = ''; + $pid = DATATREE_ROOT; + } + + if (parent::exists($fullname)) { + return PEAR::raiseError(sprintf(_("\"%s\" already exists"), $fullname)); + } + + $query = 'INSERT INTO ' . $this->_params['table'] . + ' (datatree_id, group_uid, datatree_name, datatree_order,' . + ' datatree_data, user_uid, datatree_serialized,' . + ' datatree_parents)' . + ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; + $values = array((int)$id, + $this->_params['group'], + Horde_String::convertCharset($name, Horde_Nls::getCharset(), $this->_params['charset']), + is_null($order) ? NULL : (int)$order, + $data, + (string)Horde_Auth::getAuth(), + (int)$ser, + $parents); + + Horde::logMessage('SQL Query by DataTree_sql::add(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $reorder = $this->reorder($parents, $order, $id); + if (is_a($reorder, 'PEAR_Error')) { + Horde::logMessage($reorder, __FILE__, __LINE__, PEAR_LOG_ERR); + return $reorder; + } + + $result = parent::_add($fullname, $id, $pid, $order); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + /* If we succesfully inserted the object and it supports + * being mapped to the attributes table, do that now: */ + if (!empty($attributes)) { + $result = $this->updateData($object); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + return $id; + } + + /** + * Changes the order of the children of an object. + * + * @param string $parent The full id path of the parent object. + * @param mixed $order If an array it specifies the new positions for + * all child objects. + * If an integer and $cid is specified, the position + * where the child specified by $cid is inserted. If + * $cid is not specified, the position gets deleted, + * causing the following positions to shift up. + * @param integer $cid See $order. + */ + function reorder($parent, $order = null, $cid = null) + { + if (!$parent || is_a($parent, 'PEAR_Error')) { + // Abort immediately if the parent string is empty; we + // cannot safely reorder all top-level elements. + return; + } + + $pquery = ''; + if (!is_array($order) && !is_null($order)) { + /* Single update (add/del). */ + if (is_null($cid)) { + /* No object id given so shuffle down. */ + $direction = '-'; + } else { + /* We have an object id so shuffle up. */ + $direction = '+'; + + /* Leaving the newly inserted object alone. */ + $pquery = sprintf(' AND datatree_id != %s', (int)$cid); + } + $query = sprintf('UPDATE %s SET datatree_order = datatree_order %s 1 WHERE group_uid = %s AND datatree_parents = %s AND datatree_order >= %s', + $this->_params['table'], + $direction, + $this->_write_db->quote($this->_params['group']), + $this->_write_db->quote($parent), + is_null($order) ? 'NULL' : (int)$order) . $pquery; + + Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query); + } elseif (is_array($order)) { + /* Multi update. */ + $query = 'SELECT COUNT(datatree_id)' . + ' FROM ' . $this->_params['table'] . + ' WHERE group_uid = ? AND datatree_parents = ?' . + ' GROUP BY datatree_parents'; + $values = array($this->_params['group'], + $parent); + + Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->getOne($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } elseif (count($order) != $result) { + return PEAR::raiseError(_("Cannot reorder, number of entries supplied for reorder does not match number stored.")); + } + + $o_key = 0; + foreach ($order as $o_cid) { + $query = 'UPDATE ' . $this->_params['table'] . + ' SET datatree_order = ? WHERE datatree_id = ?'; + $values = array($o_key, is_null($o_cid) ? NULL : (int)$o_cid); + + Horde::logMessage('SQL Query by DataTree_sql::reorder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $o_key++; + } + + $pid = $this->getId($parent); + + /* Re-order our cache. */ + return $this->_reorder($pid, $order); + } + } + + /** + * Explicitly set the order for a datatree object. + * + * @param integer $id The datatree object id to change. + * @param integer $order The new order. + */ + function setOrder($id, $order) + { + $query = 'UPDATE ' . $this->_params['table'] . + ' SET datatree_order = ? WHERE datatree_id = ?'; + $values = array(is_null($order) ? NULL : (int)$order, + (int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::setOrder(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $this->_write_db->query($query, $values); + } + + /** + * Removes an object. + * + * @param mixed $object The object to remove. + * @param boolean $force Force removal of every child object? + */ + function remove($object, $force = false) + { + $id = $this->getId($object); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + $order = $this->getOrder($object); + + $query = 'SELECT datatree_id FROM ' . $this->_params['table'] . + ' WHERE group_uid = ? AND datatree_parents LIKE ?' . + ' ORDER BY datatree_id'; + $values = array($this->_params['group'], + '%:' . (int)$id . ''); + + Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $children = $this->_db->getAll($query, $values, DB_FETCHMODE_ASSOC); + + if (count($children)) { + if ($force) { + foreach ($children as $child) { + $cat = $this->getName($child['datatree_id']); + $result = $this->remove($cat, true); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + } else { + return PEAR::raiseError(sprintf(_("Cannot remove, %d children exist."), count($children))); + } + } + + /* Remove attributes for this object. */ + $query = 'DELETE FROM ' . $this->_params['table_attributes'] . + ' WHERE datatree_id = ?'; + $values = array((int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $query = 'DELETE FROM ' . $this->_params['table'] . + ' WHERE datatree_id = ?'; + $values = array((int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::remove(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $parents = $this->getParentIdString($object); + if (is_a($parents, 'PEAR_Error')) { + return $parents; + } + + $reorder = $this->reorder($parents, $order); + if (is_a($reorder, 'PEAR_Error')) { + return $reorder; + } + + return is_a(parent::remove($object), 'PEAR_Error') ? $id : true; + } + + /** + * Removes one or more objects by id. + * + * This function does *not* do the validation, reordering, etc. that + * remove() does. If you need to check for children, re-do ordering, etc., + * then you must remove() objects one-by-one. This is for code that knows + * it's dealing with single (non-parented) objects and needs to delete a + * batch of them quickly. + * + * @param array $ids The objects to remove. + */ + function removeByIds($ids) + { + /* Sanitize input. */ + if (!is_array($ids)) { + $ids = array((int)$ids); + } else { + array_walk($ids, 'intval'); + } + + /* Removing zero objects always succeeds. */ + if (!$ids) { + return true; + } + + /* Remove attributes for $ids. */ + $query = 'DELETE FROM ' . $this->_params['table_attributes'] . + ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')'; + $values = $ids; + + Horde::logMessage('SQL Query by DataTree_sql::removeByIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $query = 'DELETE FROM ' . $this->_params['table'] . + ' WHERE datatree_id IN (?' . str_repeat(', ?', count($ids) - 1) . ')'; + $values = $ids; + + Horde::logMessage('SQL Query by DataTree_sql::removeByIds(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + return $this->_write_db->query($query, $values); + } + + /** + * Removes one or more objects by name. + * + * This function does *not* do the validation, reordering, etc. that + * remove() does. If you need to check for children, re-do ordering, etc., + * then you must remove() objects one-by-one. This is for code that knows + * it's dealing with single (non-parented) objects and needs to delete a + * batch of them quickly. + * + * @param array $names The objects to remove. + */ + function removeByNames($names) + { + if (!is_array($names)) { + $names = array($names); + } + + /* Removing zero objects always succeeds. */ + if (!$names) { + return true; + } + + $query = 'SELECT datatree_id FROM ' . $this->_params['table'] . + ' WHERE datatree_name IN (?' . str_repeat(', ?', count($names) - 1) . ')'; + $values = $names; + + Horde::logMessage('SQL Query by DataTree_sql::removeByNames(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $ids = $this->_db->getCol($query, 0, $values); + if (is_a($ids, 'PEAR_Error')) { + return $ids; + } + + return $this->removeByIds($ids); + } + + /** + * Move an object to a new parent. + * + * @param mixed $object The object to move. + * @param string $newparent The new parent object. Defaults to the root. + */ + function move($object, $newparent = null) + { + $old_parent_path = $this->getParentIdString($object); + $result = parent::move($object, $newparent); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $id = $this->getId($object); + $new_parent_path = $this->getParentIdString($object); + + /* Fetch the object being moved and all of its children, since + * we also need to update their parent paths to avoid creating + * orphans. */ + $query = 'SELECT datatree_id, datatree_parents' . + ' FROM ' . $this->_params['table'] . + ' WHERE datatree_parents = ? OR datatree_parents LIKE ?' . + ' OR datatree_id = ?'; + $values = array($old_parent_path . ':' . $id, + $old_parent_path . ':' . $id . ':%', + (int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::move(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $rowset = $this->_db->query($query, $values); + if (is_a($rowset, 'PEAR_Error')) { + return $rowset; + } + + /* Update each object, replacing the old parent path with the + * new one. */ + while ($row = $rowset->fetchRow(DB_FETCHMODE_ASSOC)) { + if (is_a($row, 'PEAR_Error')) { + return $row; + } + + $oquery = ''; + if ($row['datatree_id'] == $id) { + $oquery = ', datatree_order = 0 '; + } + + /* Do str_replace() only if this is not a first level + * object. */ + if (!empty($row['datatree_parents'])) { + $ppath = str_replace($old_parent_path, $new_parent_path, $row['datatree_parents']); + } else { + $ppath = $new_parent_path; + } + $query = sprintf('UPDATE %s SET datatree_parents = %s' . $oquery . ' WHERE datatree_id = %s', + $this->_params['table'], + $this->_write_db->quote($ppath), + (int)$row['datatree_id']); + + Horde::logMessage('SQL Query by DataTree_sql::move(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + $order = $this->getOrder($object); + + /* Shuffle down the old order positions. */ + $reorder = $this->reorder($old_parent_path, $order); + + /* Shuffle up the new order positions. */ + $reorder = $this->reorder($new_parent_path, 0, $id); + + return true; + } + + /** + * Change an object's name. + * + * @param mixed $old_object The old object. + * @param string $new_object_name The new object name. + */ + function rename($old_object, $new_object_name) + { + /* Do the cache renaming first */ + $result = parent::rename($old_object, $new_object_name); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + /* Get the object id and set up the sql query. */ + $id = $this->getId($old_object); + $query = 'UPDATE ' . $this->_params['table'] . + ' SET datatree_name = ? WHERE datatree_id = ?'; + $values = array(Horde_String::convertCharset($new_object_name, Horde_Nls::getCharset(), $this->_params['charset']), + (int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::rename(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + + return is_a($result, 'PEAR_Error') ? $result : true; + } + + /** + * Retrieves data for an object from the datatree_data field. + * + * @param integer $cid The object id to fetch, or an array of object ids. + */ + function getData($cid) + { + require_once 'Horde/Serialize.php'; + + if (is_array($cid)) { + if (!count($cid)) { + return array(); + } + + $query = sprintf('SELECT datatree_id, datatree_data, datatree_serialized FROM %s WHERE datatree_id IN (%s)', + $this->_params['table'], + implode(', ', $cid)); + + Horde::logMessage('SQL Query by DataTree_sql::getData(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->getAssoc($query); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $data = array(); + foreach ($result as $id => $row) { + $data[$id] = Horde_Serialize::unserialize($row[0], $row[1], + Horde_Nls::getCharset()); + /* Convert old data to the new format. */ + if ($row[1] == Horde_Serialize::BASIC) { + $data[$id] = Horde_String::convertCharset($data[$id], + Horde_Nls::getCharset(true)); + } + + $data[$id] = (is_null($data[$id]) || !is_array($data[$id])) + ? array() + : $data[$id]; + } + + return $data; + } else { + $query = 'SELECT datatree_data, datatree_serialized' . + ' FROM ' . $this->_params['table'] . + ' WHERE datatree_id = ?'; + $values = array((int)$cid); + + Horde::logMessage('SQL Query by DataTree_sql::getData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $row = $this->_db->getRow($query, $values, DB_FETCHMODE_ASSOC); + + $data = Horde_Serialize::unserialize($row['datatree_data'], + $row['datatree_serialized'], + Horde_Nls::getCharset()); + /* Convert old data to the new format. */ + if ($row['datatree_serialized'] == Horde_Serialize::BASIC) { + $data = Horde_String::convertCharset($data, Horde_Nls::getCharset(true)); + } + return (is_null($data) || !is_array($data)) ? array() : $data; + } + } + + /** + * Retrieves data for an object from the horde_datatree_attributes table. + * + * @param integer|array $cid The object id to fetch, or an array of + * object ids. + * @param array $keys The attributes keys to fetch. + * + * @return array A hash of attributes, or a multi-level hash of object + * ids => their attributes. + */ + function getAttributes($cid, $keys = false) + { + if (empty($cid)) { + return array(); + } + + if ($keys) { + $filter = sprintf(' AND attribute_key IN (\'%s\')', + implode("', '", $keys)); + } else { + $filter = ''; + } + + if (is_array($cid)) { + $query = sprintf('SELECT datatree_id, attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id IN (%s)%s', + $this->_params['table_attributes'], + implode(', ', $cid), + $filter); + + Horde::logMessage('SQL Query by DataTree_sql::getAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC); + if (is_a($rows, 'PEAR_Error')) { + return $rows; + } + + $data = array(); + foreach ($rows as $row) { + if (empty($data[$row['datatree_id']])) { + $data[$row['datatree_id']] = array(); + } + $data[$row['datatree_id']][] = array('name' => $row['name'], + 'key' => $row['key'], + 'value' => Horde_String::convertCharset($row['value'], $this->_params['charset'], Horde_Nls::getCharset())); + } + return $data; + } else { + $query = sprintf('SELECT attribute_name AS name, attribute_key AS "key", attribute_value AS value FROM %s WHERE datatree_id = %s%s', + $this->_params['table_attributes'], + (int)$cid, + $filter); + + Horde::logMessage('SQL Query by DataTree_sql::getAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $rows = $this->_db->getAll($query, DB_FETCHMODE_ASSOC); + for ($i = 0; $i < count($rows); $i++) { + $rows[$i]['value'] = Horde_String::convertCharset($rows[$i]['value'], + $this->_params['charset'], + Horde_Nls::getCharset()); + } + return $rows; + } + } + + /** + * Returns the number of objects matching a set of attribute criteria. + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + */ + function countByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name') + { + if (!count($criteria)) { + return 0; + } + + $aq = $this->buildAttributeQuery($criteria, + $parent, + $allLevels, + $restrict, + DATATREE_BUILD_COUNT); + if (is_a($aq, 'PEAR_Error')) { + return $aq; + } + list($query, $values) = $aq; + + Horde::logMessage('SQL Query by DataTree_sql::countByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + $row = $result->fetchRow(); + if (is_a($row, 'PEAR_Error')) { + Horde::logMessage($row, __FILE__, __LINE__, PEAR_LOG_ERR); + return $row; + } + + return $row[0]; + } + + /** + * Returns a set of object ids based on a set of attribute criteria. + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param integer $from The object to start to fetching + * @param integer $count The number of objects to fetch + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + */ + function getByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name', $from = 0, + $count = 0, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + if (!count($criteria)) { + return PEAR::raiseError('no criteria'); + } + + // If there are top-level OR criteria, process one at a time + // and return any results as soon as they're found...but only if + // there is no LIMIT requested. + if ($count == 0 && $from == 0) { + foreach ($criteria as $key => $vals) { + if ($key == 'OR') { + $rows = array(); + $num_or_statements = count($criteria[$key]); + for ($i = 0; $i < $num_or_statements; $i++) { + $criteria_or = $criteria['OR'][$i]; + list($query, $values) = $this->buildAttributeQuery( + $criteria_or, + $parent, + $allLevels, + $restrict, + DATATREE_BUILD_SELECT, + $sortby_name, + $sortby_key, + $direction); + if ($count) { + $query = $this->_db->modifyLimitQuery($query, $from, $count); + } + + Horde::logMessage('SQL Query by DataTree_sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + while ($row = $result->fetchRow()) { + $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset']); + } + } + + return $rows; + } + } + } + // Process AND or other complex queries. + $aq = $this->buildAttributeQuery($criteria, + $parent, + $allLevels, + $restrict, + DATATREE_BUILD_SELECT, + $sortby_name, + $sortby_key, + $direction); + if (is_a($aq, 'PEAR_Error')) { + return $aq; + } + + list($query, $values) = $aq; + + if ($count) { + $query = $this->_db->modifyLimitQuery($query, $from, $count); + } + + Horde::logMessage('SQL Query by DataTree_sql::getByAttributes(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $rows = array(); + while ($row = $result->fetchRow()) { + $rows[$row[0]] = Horde_String::convertCharset($row[1], $this->_params['charset']); + } + + return $rows; + } + + /** + * Sorts IDs by attribute values. IDs without attributes will be added to + * the end of the sorted list. + * + * @param array $unordered_ids Array of ids to sort. + * @param array $sortby_name Attribute name to use for sorting. + * @param array $sortby_key Attribute key to use for sorting. + * @param array $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array Sorted ids. + */ + function sortByAttributes($unordered_ids, $sortby_name = null, + $sortby_key = null, $direction = 0) + { + /* Select ids ordered by attribute value. */ + $where = ''; + if (!is_null($sortby_name)) { + $where = sprintf(' AND attribute_name = %s ', + $this->_db->quote($sortby_name)); + } + if (!is_null($sortby_key)) { + $where = sprintf('%s AND attribute_key = %s ', + $where, + $this->_db->quote($sortby_key)); + } + + $query = sprintf('SELECT datatree_id FROM %s WHERE datatree_id IN (%s) %s ORDER BY attribute_value %s', + $this->_params['table_attributes'], + implode(',', $unordered_ids), + $where, + ($direction == 1) ? 'DESC' : 'ASC'); + + Horde::logMessage('SQL Query by DataTree_sql::sortByAttributes(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + $ordered_ids = $this->_db->getCol($query); + + /* Make sure that some ids didn't get lost because has no such + * attribute name/key. Append them to the end. */ + if (count($ordered_ids) != count($unordered_ids)) { + $ordered_ids = array_keys(array_flip(array_merge($ordered_ids, $unordered_ids))); + } + + return $ordered_ids; + } + + /** + * Returns the number of all of the available values matching the + * given criteria. Either attribute_name or attribute_key MUST be + * supplied, and both MAY be supplied. + * + * @since Horde 3.2 + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param string $attribute_name The name of the attribute. + * @param string $attribute_key The key value of the attribute. + */ + function countValuesByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name', + $key = null, $name = null) + { + if (!count($criteria)) { + return PEAR::raiseError('no criteria'); + } + + $aq = $this->buildAttributeQuery($criteria, + $parent, + $allLevels, + $restrict, + DATATREE_BUILD_VALUES_COUNT); + + $aq[0] .= ' AND a.datatree_id = c.datatree_id'; + + if ($key !== null) { + $aq[0] .= ' AND a.attribute_key = ?'; + $aq[1][] = $key; + } + + if ($name !== null) { + $aq[0] .= ' AND a.attribute_name = ?'; + $aq[1][] = $name; + } + + return $this->_db->getOne($aq[0], $aq[1]); + } + + /** + * Returns a list of all of the available values of the given criteria + * Either attribute_name or attribute_key MUST be + * supplied, and both MAY be supplied. + * + * @since Horde 3.2 + * + * @see buildAttributeQuery() + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param integer $from The object to start to fetching + * @param integer $count The number of objects to fetch + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * @param string $attribute_name The name of the attribute. + * @param string $attribute_key The key value of the attribute. + */ + function getValuesByAttributes($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name', $from = 0, + $count = 0, $sortby_name = null, + $sortby_key = null, $direction = 0, + $key = null, $name = null) + { + if (!count($criteria)) { + return PEAR::raiseError('no criteria'); + } + + $aq = $this->buildAttributeQuery($criteria, + $parent, + $allLevels, + $restrict, + DATATREE_BUILD_VALUES, + $sortby_name, + $sortby_key, + $direction); + + $aq[0] .= ' AND a.datatree_id = c.datatree_id'; + + if ($key !== null) { + $aq[0] .= ' AND a.attribute_key = ?'; + $aq[1][] = $key; + } + + if ($name !== null) { + $aq[0] .= ' AND a.attribute_name = ?'; + $aq[1][] = $name; + } + + if ($count) { + $aq[0] = $this->_db->modifyLimitQuery($aq[0], $from, $count); + } + + return $this->_db->getCol($aq[0], 0, $aq[1]); + } + + /** + * Returns a list of all of the available values of the given attribute + * name/key combination. Either attribute_name or attribute_key MUST be + * supplied, and both MAY be supplied. + * + * @param string $attribute_name The name of the attribute. + * @param string $attribute_key The key value of the attribute. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all + * levels. + * + * @return array An array of all of the available values. + */ + function getAttributeValues($attribute_name = null, $attribute_key = null, + $parent = DATATREE_ROOT, $allLevels = true) + { + // Build the name/key filter. + $where = ''; + if (!is_null($attribute_name)) { + $where .= 'a.attribute_name = ' . $this->_db->quote($attribute_name); + } + if (!is_null($attribute_key)) { + if ($where) { + $where .= ' AND '; + } + $where .= 'a.attribute_key = ' . $this->_db->quote($attribute_key); + } + + // Return if we have no criteria. + if (!$where) { + return PEAR::raiseError('no criteria'); + } + + // Add filtering by parent, and for one or all levels. + $levelQuery = ''; + if ($parent != DATATREE_ROOT) { + $parts = explode(':', $parent); + $parents = ''; + $pstring = ''; + foreach ($parts as $part) { + $pstring .= (empty($pstring) ? '' : ':') . $part; + $pid = $this->getId($pstring); + if (is_a($pid, 'PEAR_Error')) { + return $pid; + } + $parents .= ':' . $pid; + } + + if ($allLevels) { + $levelQuery = sprintf('AND (datatree_parents = %s OR datatree_parents LIKE %s)', + $this->_db->quote($parents), + $this->_db->quote($parents . ':%')); + } else { + $levelQuery = sprintf('AND datatree_parents = %s', + $this->_db->quote($parents)); + } + } elseif (!$allLevels) { + $levelQuery = "AND datatree_parents = ''"; + } + + // Build the FROM/JOIN clauses. + $joins = 'LEFT JOIN ' . $this->_params['table'] . + ' c ON a.datatree_id = c.datatree_id'; + + $query = sprintf('SELECT DISTINCT a.attribute_value FROM %s a %s WHERE c.group_uid = %s AND %s %s', + $this->_params['table_attributes'], + $joins, + $this->_db->quote($this->_params['group']), + $where, + $levelQuery); + + Horde::logMessage('SQL Query by DataTree_sql::getAttributeValues(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $rows = $this->_db->getCol($query); + if (is_a($rows, 'PEAR_Error')) { + Horde::logMessage($rows, __FILE__, __LINE__, PEAR_LOG_ERR); + } + + return $rows; + } + + /** + * Builds an attribute query. Here is an example $criteria array: + * + * + * $criteria['OR'] = array( + * array('AND' => array( + * array('field' => 'name', + * 'op' => '=', + * 'test' => 'foo'), + * array('field' => 'key', + * 'op' => '=', + * 'test' => 'abc'))), + * array('AND' => array( + * array('field' => 'name', + * 'op' => '=', + * 'test' => 'bar'), + * array('field' => 'key', + * 'op' => '=', + * 'test' => 'xyz')))); + * + * + * This would fetch all object ids where attribute name is "foo" AND key + * is "abc", OR "bar" AND "xyz". + * + * @param array $criteria The array of criteria. + * @param string $parent The parent node to start searching from. + * @param boolean $allLevels Return all levels, or just the direct + * children of $parent? Defaults to all levels. + * @param string $restrict Only return attributes with the same + * attribute_name or attribute_id. + * @param integer $operation Type of query to build + * @param string $sortby_name Attribute name to use for sorting. + * @param string $sortby_key Attribute key to use for sorting. + * @param integer $direction Sort direction: + * 0 - ascending + * 1 - descending + * + * @return array An SQL query and a list of values suitable for binding + * as an array. + */ + function buildAttributeQuery($criteria, $parent = DATATREE_ROOT, + $allLevels = true, $restrict = 'name', + $operation = DATATREE_BUILD_SELECT, + $sortby_name = null, $sortby_key = null, + $direction = 0) + { + if (!count($criteria)) { + return array('', array()); + } + + /* Build the query. */ + $this->_tableCount = 1; + $query = ''; + $values = array(); + foreach ($criteria as $key => $vals) { + if ($key == 'OR' || $key == 'AND') { + if (!empty($query)) { + $query .= ' ' . $key . ' '; + } + $binds = $this->_buildAttributeQuery($key, $vals); + $query .= '(' . $binds[0] . ')'; + $values += $binds[1]; + } + } + + // Add filtering by parent, and for one or all levels. + $levelQuery = ''; + $levelValues = array(); + if ($parent != DATATREE_ROOT) { + $parts = explode(':', $parent); + $parents = ''; + $pstring = ''; + foreach ($parts as $part) { + $pstring .= (empty($pstring) ? '' : ':') . $part; + $pid = $this->getId($pstring); + if (is_a($pid, 'PEAR_Error')) { + return $pid; + } + $parents .= ':' . $pid; + } + + if ($allLevels) { + $levelQuery = 'AND (datatree_parents = ? OR datatree_parents LIKE ?)'; + $levelValues = array($parents, $parents . ':%'); + } else { + $levelQuery = 'AND datatree_parents = ?'; + $levelValues = array($parents); + } + } elseif (!$allLevels) { + $levelQuery = "AND datatree_parents = ''"; + } + + // Build the FROM/JOIN clauses. + $joins = array(); + $pairs = array(); + for ($i = 1; $i <= $this->_tableCount; $i++) { + $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] . + ' a' . $i . ' ON a' . $i . '.datatree_id = c.datatree_id'; + + if ($i != 1) { + if ($restrict == 'name') { + $pairs[] = 'AND a1.attribute_name = a' . $i . '.attribute_name'; + } elseif ($restrict == 'id') { + $pairs[] = 'AND a1.datatree_id = a' . $i . '.datatree_id'; + } + } + } + + // Override sorting. + $sort = array(); + if (!is_null($sortby_name) || !is_null($sortby_key)) { + $order_table = 'a' . $i; + $joins[] = 'LEFT JOIN ' . $this->_params['table_attributes'] . + ' ' . $order_table . ' ON ' . $order_table . + '.datatree_id = c.datatree_id'; + + if (!is_null($sortby_name)) { + $pairs[] = sprintf('AND %s.attribute_name = ? ', $order_table); + $sort[] = $sortby_name; + } + if (!is_null($sortby_key)) { + $pairs[] = sprintf('AND %s.attribute_key = ? ', $order_table); + $sort[] = $sortby_key; + } + + $order = sprintf('%s.attribute_value %s', + $order_table, + ($direction == 1) ? 'DESC' : 'ASC'); + $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order, ' . + $order_table . '.attribute_value'; + } else { + $order = 'c.datatree_order, c.datatree_name, c.datatree_id'; + $group_by = 'c.datatree_id, c.datatree_name, c.datatree_order'; + } + + $joins = implode(' ', $joins); + $pairs = implode(' ', $pairs); + + switch ($operation) { + + case DATATREE_BUILD_VALUES_COUNT: + $what = 'COUNT(DISTINCT(a.attribute_value))'; + $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table']; + $tail = ''; + break; + + case DATATREE_BUILD_VALUES: + $what = 'DISTINCT(a.attribute_value)'; + $from = ' ' . $this->_params['table_attributes'] . ' a, ' . $this->_params['table']; + $tail = ''; + break; + + case DATATREE_BUILD_COUNT: + $what = 'COUNT(DISTINCT c.datatree_id)'; + $from = $this->_params['table']; + $tail = ''; + break; + + default: + $what = 'c.datatree_id, c.datatree_name'; + $from = $this->_params['table']; + $tail = sprintf('GROUP BY %s ORDER BY %s', $group_by, $order); + break; + } + + return array(sprintf('SELECT %s FROM %s c %s WHERE c.group_uid = ? AND %s %s %s %s', + $what, + $from, + $joins, + $query, + $levelQuery, + $pairs, + $tail), + array_merge(array($this->_params['group']), + $values, + $levelValues, + $sort)); + } + + /** + * Builds a piece of an attribute query. + * + * @param string $glue The glue to join the criteria (OR/AND). + * @param array $criteria The array of criteria. + * @param boolean $join Should we join on a clean + * horde_datatree_attributes table? Defaults to + * false. + * + * @return array An SQL fragment and a list of values suitable for binding + * as an array. + */ + function _buildAttributeQuery($glue, $criteria, $join = false) + { + require_once 'Horde/SQL.php'; + + // Initialize the clause that we're building. + $clause = ''; + $values = array(); + + // Get the table alias to use for this set of criteria. + $alias = $this->_getAlias($join); + + foreach ($criteria as $key => $vals) { + if (!empty($clause)) { + $clause .= ' ' . $glue . ' '; + } + if (!empty($vals['OR']) || !empty($vals['AND'])) { + $binds = $this->_buildAttributeQuery($glue, $vals); + $clause .= '(' . $binds[0] . ')'; + $values = array_merge($values, $binds[1]); + } elseif (!empty($vals['JOIN'])) { + $binds = $this->_buildAttributeQuery($glue, $vals['JOIN'], true); + $clause .= $binds[0]; + $values = array_merge($values, $binds[1]); + } else { + if (isset($vals['field'])) { + // All of the attribute_* fields are text, so make + // sure we send strings to the database. + if (is_array($vals['test'])) { + for ($i = 0, $iC = count($vals['test']); $i < $iC; ++$i) { + $vals['test'][$i] = (string)$vals['test'][$i]; + } + } else { + $vals['test'] = (string)$vals['test']; + } + + $binds = Horde_SQL::buildClause($this->_db, $alias . '.attribute_' . $vals['field'], $vals['op'], $vals['test'], true); + $clause .= $binds[0]; + $values = array_merge($values, $binds[1]); + } else { + $binds = $this->_buildAttributeQuery($key, $vals); + $clause .= $binds[0]; + $values = array_merge($values, $binds[1]); + } + } + } + + return array($clause, $values); + } + + /** + * Get an alias to horde_datatree_attributes, incrementing it if + * necessary. + * + * @param boolean $increment Increment the alias count? Defaults to no. + */ + function _getAlias($increment = false) + { + static $seen = array(); + + if ($increment && !empty($seen[$this->_tableCount])) { + $this->_tableCount++; + } + + $seen[$this->_tableCount] = true; + return 'a' . $this->_tableCount; + } + + /** + * Update the data in an object. Does not change the object's + * parent or name, just serialized data or attributes. + * + * @param DataTree $object A DataTree object. + */ + function updateData($object) + { + if (!is_a($object, 'DataTreeObject')) { + /* Nothing to do for non objects. */ + return true; + } + + /* Get the object id. */ + $id = $this->getId($object->getName()); + if (is_a($id, 'PEAR_Error')) { + return $id; + } + + /* See if we can break the object out to datatree_attributes table. */ + if (method_exists($object, '_toAttributes')) { + /* If we can, clear out the datatree_data field to make sure it + * doesn't get picked up by getData(). Intentionally don't check + * for errors here in case datatree_data goes away in the + * future. */ + $query = 'UPDATE ' . $this->_params['table'] . + ' SET datatree_data = ? WHERE datatree_id = ?'; + $values = array(NULL, (int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $this->_write_db->query($query, $values); + + /* Start a transaction. */ + $this->_write_db->autoCommit(false); + + /* Delete old attributes. */ + $query = 'DELETE FROM ' . $this->_params['table_attributes'] . + ' WHERE datatree_id = ?'; + $values = array((int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + if (is_a($result, 'PEAR_Error')) { + $this->_write_db->rollback(); + $this->_write_db->autoCommit(true); + return $result; + } + + /* Get the new attribute set, and insert each into the DB. If + * anything fails in here, rollback the transaction, return the + * relevant error, and bail out. */ + $attributes = $object->_toAttributes(); + $query = 'INSERT INTO ' . $this->_params['table_attributes'] . + ' (datatree_id, attribute_name, attribute_key, attribute_value)' . + ' VALUES (?, ?, ?, ?)'; + $statement = $this->_write_db->prepare($query); + foreach ($attributes as $attr) { + $values = array((int)$id, + $attr['name'], + $attr['key'], + Horde_String::convertCharset($attr['value'], Horde_Nls::getCharset(), $this->_params['charset'])); + + Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_write_db->execute($statement, $values); + if (is_a($result, 'PEAR_Error')) { + $this->_write_db->rollback(); + $this->_write_db->autoCommit(true); + return $result; + } + } + + /* Commit the transaction, and turn autocommit back on. */ + $result = $this->_write_db->commit(); + $this->_write_db->autoCommit(true); + + return is_a($result, 'PEAR_Error') ? $result : true; + } else { + /* Write to the datatree_data field. */ + require_once 'Horde/Serialize.php'; + $ser = Horde_Serialize::UTF7_BASIC; + $data = Horde_Serialize::serialize($object->getData(), $ser, Horde_Nls::getCharset()); + + $query = 'UPDATE ' . $this->_params['table'] . + ' SET datatree_data = ?, datatree_serialized = ?' . + ' WHERE datatree_id = ?'; + $values = array($data, + (int)$ser, + (int)$id); + + Horde::logMessage('SQL Query by DataTree_sql::updateData(): ' . $query . ', ' . var_export($values, true), __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $this->_write_db->query($query, $values); + + return is_a($result, 'PEAR_Error') ? $result : true; + } + } + + /** + * Attempts to open a connection to the SQL server. + * + * @return boolean True. + */ + function _init() + { + Horde::assertDriverConfig($this->_params, 'sql', + array('phptype', 'charset'), + 'DataTree SQL'); + + $default = array( + 'database' => '', + 'username' => '', + 'password' => '', + 'hostspec' => '', + 'table' => 'horde_datatree', + 'table_attributes' => 'horde_datatree_attributes', + ); + $this->_params = array_merge($default, $this->_params); + + /* Connect to the SQL server using the supplied parameters. */ + require_once 'DB.php'; + $this->_write_db = DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']), + 'ssl' => !empty($this->_params['ssl']))); + if (is_a($this->_write_db, 'PEAR_Error')) { + return $this->_write_db; + } + + // Set DB portability options. + $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS; + if ($this->_write_db->phptype == 'mssql') { + $portability |= DB_PORTABILITY_RTRIM; + } + $this->_write_db->setOption('portability', $portability); + + /* Check if we need to set up the read DB connection + * seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = DB::connect($params, + array('persistent' => !empty($params['persistent']), + 'ssl' => !empty($params['ssl']))); + if (is_a($this->_db, 'PEAR_Error')) { + return $this->_db; + } + + // Set DB portability options + $portability = DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS; + if ($this->_db->phptype == 'mssql') { + $portability |= DB_PORTABILITY_RTRIM; + } + $this->_db->setOption('portability', $portability); + } else { + /* Default to the same DB handle for reads. */ + $this->_db = $this->_write_db; + } + + return true; + } + +} diff --git a/framework/DataTree/docs/find-datatree-attribute-orphans.sql b/framework/DataTree/docs/find-datatree-attribute-orphans.sql new file mode 100644 index 000000000..6e2962c4d --- /dev/null +++ b/framework/DataTree/docs/find-datatree-attribute-orphans.sql @@ -0,0 +1 @@ +SELECT DISTINCT da.datatree_id FROM horde_datatree_attributes da LEFT JOIN horde_datatree d ON da.datatree_id = d.datatree_id WHERE d.datatree_id IS NULL diff --git a/framework/DataTree/package.xml b/framework/DataTree/package.xml new file mode 100644 index 000000000..3fb98405b --- /dev/null +++ b/framework/DataTree/package.xml @@ -0,0 +1,102 @@ + + + Horde_DataTree + pear.horde.org + DataTree API + TODO + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + 2006-05-08 + + + 0.0.3 + 0.0.3 + + + alpha + alpha + + LGPL + Converted to package.xml 2.0 for pear.horde.org + + + + + + + + + + + + + + 4.2.0 + + + 1.4.0b1 + + + Horde_Framework + pear.horde.org + + + Serialize + pear.horde.org + + + Horde_SQL + pear.horde.org + + + Util + pear.horde.org + + + + + gettext + + + + + + + + 0.0.2 + 0.0.2 + + + alpha + alpha + + 2004-01-01 + LGPL + * Add failover functionality, if the sql driver is not available will fall back to null driver. +* Add support for separate read and write DB servers for the sql driver. + + + + + 0.0.1 + 0.0.1 + + + alpha + alpha + + 2003-02-11 + LGPL + Initial packaging. + + + + diff --git a/framework/File_CSV/CSV.php b/framework/File_CSV/CSV.php new file mode 100644 index 000000000..77bb811a5 --- /dev/null +++ b/framework/File_CSV/CSV.php @@ -0,0 +1,571 @@ + + * Copyright 2005-2009 The Horde Project (http://www.horde.org/) + * + * This source file is subject to version 2.0 of the PHP license, that is + * bundled with this package in the file LICENSE, and is available at through + * the world-wide-web at http://www.php.net/license/2_02.txt. If you did not + * receive a copy of the PHP license and are unable to obtain it through the + * world-wide-web, please send a note to license@php.net so we can mail you a + * copy immediately. + * + * @author Tomas Von Veschler Cox + * @author Jan Schneider + * @since Horde 3.1 + * @package File_CSV + */ +class File_CSV { + + /** + * Discovers the format of a CSV file (the number of fields, the separator, + * the quote string, and the line break). + * + * We can't use the auto_detect_line_endings PHP setting, because it's not + * supported by fgets() contrary to what the manual says. + * + * @static + * + * @param string The CSV file name + * @param array Extra separators that should be checked for. + * + * @return array The format hash. + */ + function discoverFormat($file, $extraSeps = array()) + { + if (!$fp = @fopen($file, 'r')) { + return PEAR::raiseError('Could not open file: ' . $file); + } + + $seps = array("\t", ';', ':', ',', '~'); + $seps = array_merge($seps, $extraSeps); + $matches = array(); + $crlf = null; + $conf = array(); + + /* Take the first 10 lines and store the number of ocurrences for each + * separator in each line. */ + for ($i = 0; ($i < 10) && ($line = fgets($fp));) { + /* Do we have Mac line endings? */ + $lines = preg_split('/\r(?!\n)/', $line, 10); + $j = 0; + $c = count($lines); + if ($c > 1) { + $crlf = "\r"; + } + while ($i < 10 && $j < $c) { + $line = $lines[$j]; + if (!isset($crlf)) { + foreach (array("\r\n", "\n") as $c) { + if (substr($line, -strlen($c)) == $c) { + $crlf = $c; + break; + } + } + } + $i++; + $j++; + foreach ($seps as $sep) { + $matches[$sep][$i] = substr_count($line, $sep); + } + } + } + if (isset($crlf)) { + $conf['crlf'] = $crlf; + } + + /* Group the results by amount of equal occurrences. */ + $fields = array(); + $amount = array(); + foreach ($matches as $sep => $lines) { + $times = array(); + $times[0] = 0; + foreach ($lines as $num) { + if ($num > 0) { + $times[$num] = (isset($times[$num])) ? $times[$num] + 1 : 1; + } + } + arsort($times); + $fields[$sep] = key($times); + $amount[$sep] = $times[key($times)]; + } + arsort($amount); + $sep = key($amount); + + $conf['fields'] = $fields[$sep] + 1; + $conf['sep'] = $sep; + + /* Test if there are fields with quotes around in the first 10 + * lines. */ + $quotes = '"\''; + $quote = ''; + rewind($fp); + for ($i = 0; ($i < 10) && ($line = fgets($fp)); $i++) { + if (preg_match("|$sep([$quotes]).*([$quotes])$sep|U", $line, $match)) { + if ($match[1] == $match[2]) { + $quote = $match[1]; + break; + } + } + if (preg_match("|^([$quotes]).*([$quotes])$sep|", $line, $match) || + preg_match("|([$quotes]).*([$quotes])$sep\s$|Us", $line, $match)) { + if ($match[1] == $match[2]) { + $quote = $match[1]; + break; + } + } + } + $conf['quote'] = $quote; + + fclose($fp); + + // XXX What about trying to discover the "header"? + return $conf; + } + + /** + * Reads a row from a CSV file and returns it as an array. + * + * This method normalizes linebreaks to single newline characters (0x0a). + * + * @param string $file The name of the CSV file. + * @param array $conf The configuration for the CSV file. + * + * @return array|boolean The CSV data or false if no more data available. + */ + function read($file, &$conf) + { + $fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_READ); + if (is_a($fp, 'PEAR_Error')) { + return $fp; + } + + $line = fgets($fp); + $line_length = strlen($line); + + /* Use readQuoted() if we have Mac line endings. */ + if (preg_match('/\r(?!\n)/', $line)) { + fseek($fp, -$line_length, SEEK_CUR); + return File_CSV::readQuoted($file, $conf); + } + + /* Normalize line endings. */ + $line = str_replace("\r\n", "\n", $line); + if (!strlen(trim($line))) { + return false; + } + + File_CSV::_line(File_CSV::_line() + 1); + + if ($conf['fields'] == 1) { + return array($line); + } + + $fields = explode($conf['sep'], $line); + if ($conf['quote']) { + $last = ltrim($fields[count($fields) - 1]); + /* Fallback to read the line with readQuoted() if we assume that + * the simple explode won't work right. */ + $last_len = strlen($last); + if (($last_len && + $last[$last_len - 1] == "\n" && + $last[0] == $conf['quote'] && + $last[strlen(rtrim($last)) - 1] != $conf['quote']) || + (count($fields) != $conf['fields']) + // XXX perhaps there is a separator inside a quoted field + // preg_match("|{$conf['quote']}.*{$conf['sep']}.*{$conf['quote']}|U", $line) + ) { + fseek($fp, -$line_length, SEEK_CUR); + return File_CSV::readQuoted($file, $conf); + } else { + foreach ($fields as $k => $v) { + $fields[$k] = File_CSV::unquote(trim($v), $conf['quote']); + } + } + } else { + foreach ($fields as $k => $v) { + $fields[$k] = trim($v); + } + } + + if (count($fields) < $conf['fields']) { + File_CSV::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), File_CSV::_line(), $conf['fields'], count($fields))); + $fields = array_merge($fields, array_fill(0, $conf['fields'] - count($fields), '')); + } elseif (count($fields) > $conf['fields']) { + File_CSV::warning(sprintf(_("More fields found in line %d than the expected %d."), File_CSV::_line(), $conf['fields'])); + array_splice($fields, $conf['fields']); + } + + return $fields; + } + + /** + * Reads a row from a CSV file and returns it as an array. + * + * This method is able to read fields with multiline data and normalizes + * linebreaks to single newline characters (0x0a). + * + * @param string $file The name of the CSV file. + * @param array $conf The configuration for the CSV file. + * + * @return array|boolean The CSV data or false if no more data available. + */ + function readQuoted($file, &$conf) + { + $fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_READ); + if (is_a($fp, 'PEAR_Error')) { + return $fp; + } + + /* A buffer with all characters of the current field read so far. */ + $buff = ''; + /* The current character. */ + $c = null; + /* The read fields. */ + $ret = false; + /* The number of the current field. */ + $i = 0; + /* Are we inside a quoted field? */ + $in_quote = false; + /* Did we just process an escaped quote? */ + $quote_escaped = false; + /* Is the last processed quote the first of a field? */ + $first_quote = false; + + while (($ch = fgetc($fp)) !== false) { + /* Normalize line breaks. */ + if ($ch == $conf['crlf']) { + $ch = "\n"; + } elseif (strlen($conf['crlf']) == 2 && $ch == $conf['crlf'][0]) { + $next = fgetc($fp); + if (!$next) { + break; + } + if ($next == $conf['crlf'][1]) { + $ch = "\n"; + } + } + + /* Previous character. */ + $prev = $c; + /* Current character. */ + $c = $ch; + + /* Simple character. */ + if ($c != $conf['quote'] && + $c != $conf['sep'] && + $c != "\n") { + $buff .= $c; + if (!$i) { + $i = 1; + } + $quote_escaped = false; + $first_quote = false; + continue; + } + + if ($c == $conf['quote'] && !$in_quote) { + /* Quoted field begins. */ + $in_quote = true; + $buff = ''; + if (!$i) { + $i = 1; + } + } elseif ($in_quote) { + /* We do NOT check for the closing quote immediately, but when + * we got the character AFTER the closing quote. */ + if ($c == $conf['quote'] && $prev == $conf['quote'] && + !$quote_escaped) { + /* Escaped (double) quotes. */ + $quote_escaped = true; + $first_quote = true; + $prev = null; + /* Simply skip the second quote. */ + continue; + } elseif ($c == $conf['sep'] && $prev == $conf['quote']) { + /* Quoted field ends with a delimiter. */ + $in_quote = false; + $quote_escaped = false; + $first_quote = true; + } elseif ($c == "\n") { + /* We have a linebreak inside the quotes. */ + if (strlen($buff) == 1 && + $buff[0] == $conf['quote'] && + $quote_escaped && $first_quote) { + /* A line break after a closing quote of an empty + * field, field and row end here. */ + $in_quote = false; + } elseif (strlen($buff) >= 1 && + $buff[strlen($buff) - 1] == $conf['quote'] && + !$quote_escaped && !$first_quote) { + /* A line break after a closing quote, field and row + * end here. This is NOT the closing quote if we + * either process an escaped (double) quote, or if the + * quote before the line break was the opening + * quote. */ + $in_quote = false; + } else { + /* Only increment the line number. Line breaks inside + * quoted fields are part of the field content. */ + File_CSV::_line(File_CSV::_line() + 1); + } + $quote_escaped = false; + $first_quote = true; + } + } + + if (!$in_quote && + ($c == $conf['sep'] || $c == "\n")) { + /* End of line or end of field. */ + if ($c == $conf['sep'] && + (count($ret) + 1) == $conf['fields']) { + /* More fields than expected. Forward the line pointer to + * the EOL and drop the remainder. */ + while ($c !== false && $c != "\n") { + $c = fgetc($fp); + } + File_CSV::warning(sprintf(_("More fields found in line %d than the expected %d."), File_CSV::_line(), $conf['fields'])); + } + + if ($c == "\n" && + $i != $conf['fields']) { + /* Less fields than expected. */ + if ($i == 0) { + /* Skip empty lines. */ + return $ret; + } + File_CSV::warning(sprintf(_("Wrong number of fields in line %d. Expected %d, found %d."), File_CSV::_line(), $conf['fields'], $i)); + + $ret[] = File_CSV::unquote($buff, $conf['quote']); + $ret = array_merge($ret, array_fill(0, $conf['fields'] - $i, '')); + return $ret; + } + + /* Remove surrounding quotes from quoted fields. */ + if ($buff == '"') { + $ret[] = ''; + } else { + $ret[] = File_CSV::unquote($buff, $conf['quote']); + } + if (count($ret) == $conf['fields']) { + return $ret; + } + + $buff = ''; + $i++; + continue; + } + $buff .= $c; + } + + return $ret; + } + + /** + * Writes a hash into a CSV file. + * + * @param string $file The name of the CSV file. + * @param array $fields The CSV data. + * @param array $conf The configuration for the CSV file. + * + * @return boolean True on success, PEAR_Error on failure. + */ + function write($file, $fields, &$conf) + { + if (is_a($fp = File_CSV::getPointer($file, $conf, HORDE_FILE_CSV_MODE_WRITE), 'PEAR_Error')) { + return $fp; + } + + if (count($fields) != $conf['fields']) { + return PEAR::raiseError(sprintf(_("Wrong number of fields. Expected %d, found %d."), $conf['fields'], count($fields))); + } + + $write = ''; + for ($i = 0; $i < count($fields); $i++) { + if (!is_numeric($fields[$i]) && $conf['quote']) { + $write .= $conf['quote'] . $fields[$i] . $conf['quote']; + } else { + $write .= $fields[$i]; + } + if ($i < (count($fields) - 1)) { + $write .= $conf['sep']; + } else { + $write .= $conf['crlf']; + } + } + + if (!fwrite($fp, $write)) { + return PEAR::raiseError(sprintf(_("Cannot write to file \"%s\""), $file)); + } + + return true; + } + + /** + * Removes surrounding quotes from a string and normalizes linebreaks. + * + * @param string $field The string to unquote. + * @param string $quote The quote character. + * @param string $crlf The linebreak character. + * + * @return string The unquoted data. + */ + function unquote($field, $quote, $crlf = null) + { + /* Skip empty fields (form: ;;) */ + if (!strlen($field)) { + return $field; + } + if ($quote && $field[0] == $quote && + $field[strlen($field) - 1] == $quote) { + /* Normalize only for BC. */ + if ($crlf) { + $field = str_replace($crlf, "\n", $field); + } + return substr($field, 1, -1); + } + return $field; + } + + /** + * Sets or gets the current line being parsed. + * + * @param integer $line If specified, the current line. + * + * @return integer The current line. + */ + function _line($line = null) + { + static $current_line = 0; + + if (!is_null($line)) { + $current_line = $line; + } + + return $current_line; + } + + /** + * Adds a warning to or retrieves and resets the warning stack. + * + * @param string A warning string. If not specified, the existing + * warnings will be returned instead and the warning stack + * gets emptied. + * + * @return array If no parameter has been specified, the list of existing + * warnings. + */ + function warning($warning = null) + { + static $warnings = array(); + + if (is_null($warning)) { + $return = $warnings; + $warnings = array(); + return $return; + } + + $warnings[] = $warning; + } + + /** + * Returns or creates the file descriptor associated with a file. + * + * @static + * + * @param string $file The name of the file + * @param array $conf The configuration + * @param string $mode The open mode. HORDE_FILE_CSV_MODE_READ or + * HORDE_FILE_CSV_MODE_WRITE. + * + * @return resource The file resource or PEAR_Error on error. + */ + function getPointer($file, &$conf, $mode = HORDE_FILE_CSV_MODE_READ) + { + static $resources = array(); + static $config = array(); + + if (isset($resources[$file])) { + $conf = $config[$file]; + return $resources[$file]; + } + if (is_a($error = File_CSV::_checkConfig($conf), 'PEAR_Error')) { + return $error; + } + $config[$file] = $conf; + + $fp = @fopen($file, $mode); + if (!is_resource($fp)) { + return PEAR::raiseError(sprintf(_("Cannot open file \"%s\"."), $file)); + } + $resources[$file] = $fp; + File_CSV::_line(0); + + if ($mode == HORDE_FILE_CSV_MODE_READ && !empty($conf['header'])) { + if (is_a($header = File_CSV::read($file, $conf), 'PEAR_Error')) { + return $header; + } + } + + return $fp; + } + + /** + * Checks the configuration given by the user. + * + * @param array $conf The configuration assoc array + * @param string $error The error will be written here if any + */ + function _checkConfig(&$conf) + { + // check conf + if (!is_array($conf)) { + return PEAR::raiseError('Invalid configuration.'); + } + + if (!isset($conf['fields']) || !is_numeric($conf['fields'])) { + return PEAR::raiseError(_("The number of fields must be numeric.")); + } + + if (isset($conf['sep'])) { + if (strlen($conf['sep']) != 1) { + return PEAR::raiseError(_("The separator must be one single character.")); + } + } elseif ($conf['fields'] > 1) { + return PEAR::raiseError(_("No separator specified.")); + } + + if (!empty($conf['quote'])) { + if (strlen($conf['quote']) != 1) { + return PEAR::raiseError(_("The quote character must be one single character.")); + } + } else { + $conf['quote'] = ''; + } + + if (!isset($conf['crlf'])) { + $conf['crlf'] = "\n"; + } + } + +} diff --git a/framework/File_CSV/package.xml b/framework/File_CSV/package.xml new file mode 100644 index 000000000..d898fb308 --- /dev/null +++ b/framework/File_CSV/package.xml @@ -0,0 +1,92 @@ + + + File_CSV + pear.horde.org + Reads and writes CSV files + The File_CSV package allows reading and creating of CSV data and files. It +is a fork of the File_CSV class of PEAR's File package. + + + Jan Schneider + jan + jan@horde.org + yes + + 2006-05-08 + + + 0.1.1 + 0.1.1 + + + beta + beta + + PHP + +* Converted to package.xml 2.0 for pear.horde.org +* Close Horde bug #6372 (ritaselsky@gmail.com) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4.3.0 + + + 1.4.0b1 + + + PEAR + pear.php.net + + + pcre + + + + + + + + 0.1.0 + 0.1.0 + + + beta + beta + + 2004-01-01 + PHP + First release. + + + + diff --git a/framework/File_CSV/tests/001.csv b/framework/File_CSV/tests/001.csv new file mode 100755 index 000000000..d217fb0fc --- /dev/null +++ b/framework/File_CSV/tests/001.csv @@ -0,0 +1,4 @@ +"Field 1-1", "Field 1-2", "Field 1-3", "Field 1-4" +"Field 2-1", "Field 2-2", "Field 2-3" +"Field 3-1", "Field 3-2" +"Field 4-1" diff --git a/framework/File_CSV/tests/001.phpt b/framework/File_CSV/tests/001.phpt new file mode 100755 index 000000000..33ff355e7 --- /dev/null +++ b/framework/File_CSV/tests/001.phpt @@ -0,0 +1,84 @@ +--TEST-- +File_CSV Test Case 001: Fields count less than expected +--FILE-- + +--EXPECT-- +array(4) { + ["crlf"]=> + string(1) " +" + ["fields"]=> + int(4) + ["sep"]=> + string(1) "," + ["quote"]=> + string(1) """ +} +array(4) { + [0]=> + array(4) { + [0]=> + string(9) "Field 1-1" + [1]=> + string(9) "Field 1-2" + [2]=> + string(9) "Field 1-3" + [3]=> + string(9) "Field 1-4" + } + [1]=> + array(4) { + [0]=> + string(9) "Field 2-1" + [1]=> + string(9) "Field 2-2" + [2]=> + string(9) "Field 2-3" + [3]=> + string(0) "" + } + [2]=> + array(4) { + [0]=> + string(9) "Field 3-1" + [1]=> + string(9) "Field 3-2" + [2]=> + string(0) "" + [3]=> + string(0) "" + } + [3]=> + array(4) { + [0]=> + string(9) "Field 4-1" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" + } +} diff --git a/framework/File_CSV/tests/002.csv b/framework/File_CSV/tests/002.csv new file mode 100755 index 000000000..bcd6ce0ca --- /dev/null +++ b/framework/File_CSV/tests/002.csv @@ -0,0 +1,4 @@ +"Field 1-1", "Field 1-2", "Field 1-3", "Field 1-4" +"Field 2-1", "Field 2-2", "Field 2-3", "Field 2-4", "Extra Field" +"Field 3-1", "Field 3-2" +"Field 4-1" diff --git a/framework/File_CSV/tests/002.phpt b/framework/File_CSV/tests/002.phpt new file mode 100755 index 000000000..ccc536761 --- /dev/null +++ b/framework/File_CSV/tests/002.phpt @@ -0,0 +1,84 @@ +--TEST-- +File_CSV Test Case 002: Fields count more than expected +--FILE-- + +--EXPECT-- +array(4) { + ["crlf"]=> + string(1) " +" + ["fields"]=> + int(4) + ["sep"]=> + string(1) "," + ["quote"]=> + string(1) """ +} +array(4) { + [0]=> + array(4) { + [0]=> + string(9) "Field 1-1" + [1]=> + string(9) "Field 1-2" + [2]=> + string(9) "Field 1-3" + [3]=> + string(9) "Field 1-4" + } + [1]=> + array(4) { + [0]=> + string(9) "Field 2-1" + [1]=> + string(9) "Field 2-2" + [2]=> + string(9) "Field 2-3" + [3]=> + string(9) "Field 2-4" + } + [2]=> + array(4) { + [0]=> + string(9) "Field 3-1" + [1]=> + string(9) "Field 3-2" + [2]=> + string(0) "" + [3]=> + string(0) "" + } + [3]=> + array(4) { + [0]=> + string(9) "Field 4-1" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" + } +} diff --git a/framework/File_CSV/tests/003.csv b/framework/File_CSV/tests/003.csv new file mode 100644 index 000000000..62cb0f7a6 --- /dev/null +++ b/framework/File_CSV/tests/003.csv @@ -0,0 +1,4 @@ +"Field 1-1","Field 1-2","Field 1-3","Field 1-4" +"Field 2-1","Field 2-2","Field 2-3","I'm multiline +Field" +"Field 3-1","Field 3-2","Field 3-3" diff --git a/framework/File_CSV/tests/003.phpt b/framework/File_CSV/tests/003.phpt new file mode 100644 index 000000000..26407594a --- /dev/null +++ b/framework/File_CSV/tests/003.phpt @@ -0,0 +1,67 @@ +--TEST-- +File_CSV Test Case 003: Windows EOL +--FILE-- + +--EXPECT-- +Format: +Array +( + [crlf] => + + [fields] => 4 + [sep] => , + [quote] => " +) + +Data: +Array +( + [0] => Array + ( + [0] => Field 1-1 + [1] => Field 1-2 + [2] => Field 1-3 + [3] => Field 1-4 + ) + + [1] => Array + ( + [0] => Field 2-1 + [1] => Field 2-2 + [2] => Field 2-3 + [3] => I'm multiline +Field + ) + + [2] => Array + ( + [0] => Field 3-1 + [1] => Field 3-2 + [2] => Field 3-3 + [3] => + ) + +) diff --git a/framework/File_CSV/tests/004.csv b/framework/File_CSV/tests/004.csv new file mode 100644 index 000000000..62cb0f7a6 --- /dev/null +++ b/framework/File_CSV/tests/004.csv @@ -0,0 +1,4 @@ +"Field 1-1","Field 1-2","Field 1-3","Field 1-4" +"Field 2-1","Field 2-2","Field 2-3","I'm multiline +Field" +"Field 3-1","Field 3-2","Field 3-3" diff --git a/framework/File_CSV/tests/004.phpt b/framework/File_CSV/tests/004.phpt new file mode 100644 index 000000000..e85be8180 --- /dev/null +++ b/framework/File_CSV/tests/004.phpt @@ -0,0 +1,67 @@ +--TEST-- +File_CSV Test Case 004: Unix EOL +--FILE-- + +--EXPECT-- +Format: +Array +( + [crlf] => + + [fields] => 4 + [sep] => , + [quote] => " +) + +Data: +Array +( + [0] => Array + ( + [0] => Field 1-1 + [1] => Field 1-2 + [2] => Field 1-3 + [3] => Field 1-4 + ) + + [1] => Array + ( + [0] => Field 2-1 + [1] => Field 2-2 + [2] => Field 2-3 + [3] => I'm multiline +Field + ) + + [2] => Array + ( + [0] => Field 3-1 + [1] => Field 3-2 + [2] => Field 3-3 + [3] => + ) + +) diff --git a/framework/File_CSV/tests/005.csv b/framework/File_CSV/tests/005.csv new file mode 100644 index 000000000..3b26aa998 --- /dev/null +++ b/framework/File_CSV/tests/005.csv @@ -0,0 +1 @@ +"Field 1-1","Field 1-2","Field 1-3","Field 1-4" "Field 2-1","Field 2-2","Field 2-3","I'm multiline Field" "Field 3-1","Field 3-2","Field 3-3" \ No newline at end of file diff --git a/framework/File_CSV/tests/005.phpt b/framework/File_CSV/tests/005.phpt new file mode 100644 index 000000000..78701c00c --- /dev/null +++ b/framework/File_CSV/tests/005.phpt @@ -0,0 +1,67 @@ +--TEST-- +File_CSV Test Case 005: Mac EOL +--FILE-- + +--EXPECT-- +Format: +Array +( + [crlf] => + [fields] => 4 + [sep] => , + [quote] => " +) + +Data: +Array +( + [0] => Array + ( + [0] => Field 1-1 + [1] => Field 1-2 + [2] => Field 1-3 + [3] => Field 1-4 + ) + + [1] => Array + ( + [0] => Field 2-1 + [1] => Field 2-2 + [2] => Field 2-3 + [3] => I'm multiline +Field + ) + + [2] => Array + ( + [0] => Field 3-1 + [1] => Field 3-2 + [2] => Field 3-3 + [3] => + ) + +) diff --git a/framework/File_CSV/tests/bug_3839.csv b/framework/File_CSV/tests/bug_3839.csv new file mode 100644 index 000000000..7deda38f2 --- /dev/null +++ b/framework/File_CSV/tests/bug_3839.csv @@ -0,0 +1,22 @@ +Subject~Start Date~Start Time~End Date~End Time~All day event~Reminder on/off~Reminder Date~Reminder Time~Category~Description~Priority +"Inservice on new resource: ""CPNP Toolkit"""~2004-11-08~10:30 AM~2004-11-08~11:30 AM~FALSE~FALSE~~~Training~"CPN Program ... +Inservice on new resource: ""CPNP Toolkit"" + +Registration Deadline: October 27, 2004, noon + + + Session Evaluation - Eligibility for Prize! + + + Dial In Numbers for Sites Registered + + + Poster and Registration Form + +Facilitator: Manager + +preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb ""CPNP Toolkit"". postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb . + +postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb + +Come check out the new resource!"~Normal diff --git a/framework/File_CSV/tests/bug_3839.phpt b/framework/File_CSV/tests/bug_3839.phpt new file mode 100644 index 000000000..4371409fe --- /dev/null +++ b/framework/File_CSV/tests/bug_3839.phpt @@ -0,0 +1,110 @@ +--TEST-- +File_CSV: test for Bug #3839 +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(12) { + [0]=> + string(7) "Subject" + [1]=> + string(10) "Start Date" + [2]=> + string(10) "Start Time" + [3]=> + string(8) "End Date" + [4]=> + string(8) "End Time" + [5]=> + string(13) "All day event" + [6]=> + string(15) "Reminder on/off" + [7]=> + string(13) "Reminder Date" + [8]=> + string(13) "Reminder Time" + [9]=> + string(8) "Category" + [10]=> + string(11) "Description" + [11]=> + string(8) "Priority" + } + [1]=> + array(12) { + [0]=> + string(41) "Inservice on new resource: "CPNP Toolkit"" + [1]=> + string(10) "2004-11-08" + [2]=> + string(8) "10:30 AM" + [3]=> + string(10) "2004-11-08" + [4]=> + string(8) "11:30 AM" + [5]=> + string(5) "FALSE" + [6]=> + string(5) "FALSE" + [7]=> + string(0) "" + [8]=> + string(0) "" + [9]=> + string(8) "Training" + [10]=> + string(1109) "CPN Program ... +Inservice on new resource: "CPNP Toolkit" + +Registration Deadline: October 27, 2004, noon + + + Session Evaluation - Eligibility for Prize! + + + Dial In Numbers for Sites Registered + + + Poster and Registration Form + +Facilitator: Manager + +preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb preblurb "CPNP Toolkit". postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb . + +postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb postblurb + +Come check out the new resource!" + [11]=> + string(6) "Normal" + } +} diff --git a/framework/File_CSV/tests/bug_4025.csv b/framework/File_CSV/tests/bug_4025.csv new file mode 100644 index 000000000..cd65cc0c4 --- /dev/null +++ b/framework/File_CSV/tests/bug_4025.csv @@ -0,0 +1,3 @@ +"Betreff","Beginnt am","Beginnt um","Endet am","Endet um","Ganztägiges Ereignis","Erinnerung Ein/Aus","Erinnerung am","Erinnerung um","Besprechungsplanung","Erforderliche Teilnehmer","Optionale Teilnehmer","Besprechungsressourcen","Abrechnungsinformationen","Beschreibung","Kategorien","Ort","Priorität","Privat","Reisekilometer","Vertraulichkeit","Zeitspanne zeigen als" +"Burger Download Session","2.5.2006","11:50:00","2.5.2006","13:00:00","Aus","Ein","2.5.2006","11:35:00","Haas, Jörg","Kuhl, Oliver",,,," +",,"Burger Upload Station (Burger King)","Normal","Aus",,"Normal","1" diff --git a/framework/File_CSV/tests/bug_4025.phpt b/framework/File_CSV/tests/bug_4025.phpt new file mode 100644 index 000000000..edde443cb --- /dev/null +++ b/framework/File_CSV/tests/bug_4025.phpt @@ -0,0 +1,131 @@ +--TEST-- +File_CSV: test for Bug #4025 +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(22) { + [0]=> + string(7) "Betreff" + [1]=> + string(10) "Beginnt am" + [2]=> + string(10) "Beginnt um" + [3]=> + string(8) "Endet am" + [4]=> + string(8) "Endet um" + [5]=> + string(20) "Ganztägiges Ereignis" + [6]=> + string(18) "Erinnerung Ein/Aus" + [7]=> + string(13) "Erinnerung am" + [8]=> + string(13) "Erinnerung um" + [9]=> + string(19) "Besprechungsplanung" + [10]=> + string(24) "Erforderliche Teilnehmer" + [11]=> + string(20) "Optionale Teilnehmer" + [12]=> + string(22) "Besprechungsressourcen" + [13]=> + string(24) "Abrechnungsinformationen" + [14]=> + string(12) "Beschreibung" + [15]=> + string(10) "Kategorien" + [16]=> + string(3) "Ort" + [17]=> + string(9) "Priorität" + [18]=> + string(6) "Privat" + [19]=> + string(14) "Reisekilometer" + [20]=> + string(15) "Vertraulichkeit" + [21]=> + string(21) "Zeitspanne zeigen als" + } + [1]=> + array(22) { + [0]=> + string(23) "Burger Download Session" + [1]=> + string(8) "2.5.2006" + [2]=> + string(8) "11:50:00" + [3]=> + string(8) "2.5.2006" + [4]=> + string(8) "13:00:00" + [5]=> + string(3) "Aus" + [6]=> + string(3) "Ein" + [7]=> + string(8) "2.5.2006" + [8]=> + string(8) "11:35:00" + [9]=> + string(10) "Haas, Jörg" + [10]=> + string(12) "Kuhl, Oliver" + [11]=> + string(0) "" + [12]=> + string(0) "" + [13]=> + string(0) "" + [14]=> + string(1) " +" + [15]=> + string(0) "" + [16]=> + string(35) "Burger Upload Station (Burger King)" + [17]=> + string(6) "Normal" + [18]=> + string(3) "Aus" + [19]=> + string(0) "" + [20]=> + string(6) "Normal" + [21]=> + string(1) "1" + } +} diff --git a/framework/File_CSV/tests/bug_6311.csv b/framework/File_CSV/tests/bug_6311.csv new file mode 100644 index 000000000..ef4f25f0e --- /dev/null +++ b/framework/File_CSV/tests/bug_6311.csv @@ -0,0 +1,5 @@ +"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page" +"","John","","Smith","","International Inc","","","",,,"","","","","",,,"","","","","",,,"","","","","","(123) 555-1111","(123) 555-2222","","","","","","","","","(123) 555-3333","","","","","","","","","0/0/00","",,"0/0/00",,"Programming",,,"john@example.com","SMTP","John Smith (john@example.com)",,,,,,,"Unspecified","",,,"J.S.","","","","","",,"PHP +Perl +Python +","","",,"Normal","False","",,"Normal","","","","","","" diff --git a/framework/File_CSV/tests/bug_6311.phpt b/framework/File_CSV/tests/bug_6311.phpt new file mode 100644 index 000000000..b2f4428a8 --- /dev/null +++ b/framework/File_CSV/tests/bug_6311.phpt @@ -0,0 +1,413 @@ +--TEST-- +File_CSV: test for Bug #6311 +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(92) { + [0]=> + string(5) "Title" + [1]=> + string(10) "First Name" + [2]=> + string(11) "Middle Name" + [3]=> + string(9) "Last Name" + [4]=> + string(6) "Suffix" + [5]=> + string(7) "Company" + [6]=> + string(10) "Department" + [7]=> + string(9) "Job Title" + [8]=> + string(15) "Business Street" + [9]=> + string(17) "Business Street 2" + [10]=> + string(17) "Business Street 3" + [11]=> + string(13) "Business City" + [12]=> + string(14) "Business State" + [13]=> + string(20) "Business Postal Code" + [14]=> + string(23) "Business Country/Region" + [15]=> + string(11) "Home Street" + [16]=> + string(13) "Home Street 2" + [17]=> + string(13) "Home Street 3" + [18]=> + string(9) "Home City" + [19]=> + string(10) "Home State" + [20]=> + string(16) "Home Postal Code" + [21]=> + string(19) "Home Country/Region" + [22]=> + string(12) "Other Street" + [23]=> + string(14) "Other Street 2" + [24]=> + string(14) "Other Street 3" + [25]=> + string(10) "Other City" + [26]=> + string(11) "Other State" + [27]=> + string(17) "Other Postal Code" + [28]=> + string(20) "Other Country/Region" + [29]=> + string(17) "Assistant's Phone" + [30]=> + string(12) "Business Fax" + [31]=> + string(14) "Business Phone" + [32]=> + string(16) "Business Phone 2" + [33]=> + string(8) "Callback" + [34]=> + string(9) "Car Phone" + [35]=> + string(18) "Company Main Phone" + [36]=> + string(8) "Home Fax" + [37]=> + string(10) "Home Phone" + [38]=> + string(12) "Home Phone 2" + [39]=> + string(4) "ISDN" + [40]=> + string(12) "Mobile Phone" + [41]=> + string(9) "Other Fax" + [42]=> + string(11) "Other Phone" + [43]=> + string(5) "Pager" + [44]=> + string(13) "Primary Phone" + [45]=> + string(11) "Radio Phone" + [46]=> + string(13) "TTY/TDD Phone" + [47]=> + string(5) "Telex" + [48]=> + string(7) "Account" + [49]=> + string(11) "Anniversary" + [50]=> + string(16) "Assistant's Name" + [51]=> + string(19) "Billing Information" + [52]=> + string(8) "Birthday" + [53]=> + string(23) "Business Address PO Box" + [54]=> + string(10) "Categories" + [55]=> + string(8) "Children" + [56]=> + string(16) "Directory Server" + [57]=> + string(14) "E-mail Address" + [58]=> + string(11) "E-mail Type" + [59]=> + string(19) "E-mail Display Name" + [60]=> + string(16) "E-mail 2 Address" + [61]=> + string(13) "E-mail 2 Type" + [62]=> + string(21) "E-mail 2 Display Name" + [63]=> + string(16) "E-mail 3 Address" + [64]=> + string(13) "E-mail 3 Type" + [65]=> + string(21) "E-mail 3 Display Name" + [66]=> + string(6) "Gender" + [67]=> + string(20) "Government ID Number" + [68]=> + string(5) "Hobby" + [69]=> + string(19) "Home Address PO Box" + [70]=> + string(8) "Initials" + [71]=> + string(18) "Internet Free Busy" + [72]=> + string(8) "Keywords" + [73]=> + string(8) "Language" + [74]=> + string(8) "Location" + [75]=> + string(14) "Manager's Name" + [76]=> + string(7) "Mileage" + [77]=> + string(5) "Notes" + [78]=> + string(15) "Office Location" + [79]=> + string(24) "Organizational ID Number" + [80]=> + string(20) "Other Address PO Box" + [81]=> + string(8) "Priority" + [82]=> + string(7) "Private" + [83]=> + string(10) "Profession" + [84]=> + string(11) "Referred By" + [85]=> + string(11) "Sensitivity" + [86]=> + string(6) "Spouse" + [87]=> + string(6) "User 1" + [88]=> + string(6) "User 2" + [89]=> + string(6) "User 3" + [90]=> + string(6) "User 4" + [91]=> + string(8) "Web Page" + } + [1]=> + array(92) { + [0]=> + string(0) "" + [1]=> + string(4) "John" + [2]=> + string(0) "" + [3]=> + string(5) "Smith" + [4]=> + string(0) "" + [5]=> + string(17) "International Inc" + [6]=> + string(0) "" + [7]=> + string(0) "" + [8]=> + string(0) "" + [9]=> + string(0) "" + [10]=> + string(0) "" + [11]=> + string(0) "" + [12]=> + string(0) "" + [13]=> + string(0) "" + [14]=> + string(0) "" + [15]=> + string(0) "" + [16]=> + string(0) "" + [17]=> + string(0) "" + [18]=> + string(0) "" + [19]=> + string(0) "" + [20]=> + string(0) "" + [21]=> + string(0) "" + [22]=> + string(0) "" + [23]=> + string(0) "" + [24]=> + string(0) "" + [25]=> + string(0) "" + [26]=> + string(0) "" + [27]=> + string(0) "" + [28]=> + string(0) "" + [29]=> + string(0) "" + [30]=> + string(14) "(123) 555-1111" + [31]=> + string(14) "(123) 555-2222" + [32]=> + string(0) "" + [33]=> + string(0) "" + [34]=> + string(0) "" + [35]=> + string(0) "" + [36]=> + string(0) "" + [37]=> + string(0) "" + [38]=> + string(0) "" + [39]=> + string(0) "" + [40]=> + string(14) "(123) 555-3333" + [41]=> + string(0) "" + [42]=> + string(0) "" + [43]=> + string(0) "" + [44]=> + string(0) "" + [45]=> + string(0) "" + [46]=> + string(0) "" + [47]=> + string(0) "" + [48]=> + string(0) "" + [49]=> + string(6) "0/0/00" + [50]=> + string(0) "" + [51]=> + string(0) "" + [52]=> + string(6) "0/0/00" + [53]=> + string(0) "" + [54]=> + string(11) "Programming" + [55]=> + string(0) "" + [56]=> + string(0) "" + [57]=> + string(16) "john@example.com" + [58]=> + string(4) "SMTP" + [59]=> + string(29) "John Smith (john@example.com)" + [60]=> + string(0) "" + [61]=> + string(0) "" + [62]=> + string(0) "" + [63]=> + string(0) "" + [64]=> + string(0) "" + [65]=> + string(0) "" + [66]=> + string(11) "Unspecified" + [67]=> + string(0) "" + [68]=> + string(0) "" + [69]=> + string(0) "" + [70]=> + string(4) "J.S." + [71]=> + string(0) "" + [72]=> + string(0) "" + [73]=> + string(0) "" + [74]=> + string(0) "" + [75]=> + string(0) "" + [76]=> + string(0) "" + [77]=> + string(16) "PHP +Perl +Python +" + [78]=> + string(0) "" + [79]=> + string(0) "" + [80]=> + string(0) "" + [81]=> + string(6) "Normal" + [82]=> + string(5) "False" + [83]=> + string(0) "" + [84]=> + string(0) "" + [85]=> + string(6) "Normal" + [86]=> + string(0) "" + [87]=> + string(0) "" + [88]=> + string(0) "" + [89]=> + string(0) "" + [90]=> + string(0) "" + [91]=> + string(0) "" + } +} diff --git a/framework/File_CSV/tests/bug_6370.csv b/framework/File_CSV/tests/bug_6370.csv new file mode 100644 index 000000000..9a9690209 --- /dev/null +++ b/framework/File_CSV/tests/bug_6370.csv @@ -0,0 +1,3 @@ +"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page" +"","","","","","","","","Big Tower'"", 1"" Floor +123 Main Street",,,"","","","","",,,"","","","","",,,"","","","","","","","","","","","","","","","","","","","","","","","","0/0/00","",,"0/0/00",,,,,"","","",,,,,,,"Unspecified","",,,"","","","","","",,,"","",,"Normal","False","",,"Normal","","","","","","" diff --git a/framework/File_CSV/tests/bug_6370.phpt b/framework/File_CSV/tests/bug_6370.phpt new file mode 100644 index 000000000..a322e2708 --- /dev/null +++ b/framework/File_CSV/tests/bug_6370.phpt @@ -0,0 +1,411 @@ +--TEST-- +File_CSV: test for Bug #6370 +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(92) { + [0]=> + string(5) "Title" + [1]=> + string(10) "First Name" + [2]=> + string(11) "Middle Name" + [3]=> + string(9) "Last Name" + [4]=> + string(6) "Suffix" + [5]=> + string(7) "Company" + [6]=> + string(10) "Department" + [7]=> + string(9) "Job Title" + [8]=> + string(15) "Business Street" + [9]=> + string(17) "Business Street 2" + [10]=> + string(17) "Business Street 3" + [11]=> + string(13) "Business City" + [12]=> + string(14) "Business State" + [13]=> + string(20) "Business Postal Code" + [14]=> + string(23) "Business Country/Region" + [15]=> + string(11) "Home Street" + [16]=> + string(13) "Home Street 2" + [17]=> + string(13) "Home Street 3" + [18]=> + string(9) "Home City" + [19]=> + string(10) "Home State" + [20]=> + string(16) "Home Postal Code" + [21]=> + string(19) "Home Country/Region" + [22]=> + string(12) "Other Street" + [23]=> + string(14) "Other Street 2" + [24]=> + string(14) "Other Street 3" + [25]=> + string(10) "Other City" + [26]=> + string(11) "Other State" + [27]=> + string(17) "Other Postal Code" + [28]=> + string(20) "Other Country/Region" + [29]=> + string(17) "Assistant's Phone" + [30]=> + string(12) "Business Fax" + [31]=> + string(14) "Business Phone" + [32]=> + string(16) "Business Phone 2" + [33]=> + string(8) "Callback" + [34]=> + string(9) "Car Phone" + [35]=> + string(18) "Company Main Phone" + [36]=> + string(8) "Home Fax" + [37]=> + string(10) "Home Phone" + [38]=> + string(12) "Home Phone 2" + [39]=> + string(4) "ISDN" + [40]=> + string(12) "Mobile Phone" + [41]=> + string(9) "Other Fax" + [42]=> + string(11) "Other Phone" + [43]=> + string(5) "Pager" + [44]=> + string(13) "Primary Phone" + [45]=> + string(11) "Radio Phone" + [46]=> + string(13) "TTY/TDD Phone" + [47]=> + string(5) "Telex" + [48]=> + string(7) "Account" + [49]=> + string(11) "Anniversary" + [50]=> + string(16) "Assistant's Name" + [51]=> + string(19) "Billing Information" + [52]=> + string(8) "Birthday" + [53]=> + string(23) "Business Address PO Box" + [54]=> + string(10) "Categories" + [55]=> + string(8) "Children" + [56]=> + string(16) "Directory Server" + [57]=> + string(14) "E-mail Address" + [58]=> + string(11) "E-mail Type" + [59]=> + string(19) "E-mail Display Name" + [60]=> + string(16) "E-mail 2 Address" + [61]=> + string(13) "E-mail 2 Type" + [62]=> + string(21) "E-mail 2 Display Name" + [63]=> + string(16) "E-mail 3 Address" + [64]=> + string(13) "E-mail 3 Type" + [65]=> + string(21) "E-mail 3 Display Name" + [66]=> + string(6) "Gender" + [67]=> + string(20) "Government ID Number" + [68]=> + string(5) "Hobby" + [69]=> + string(19) "Home Address PO Box" + [70]=> + string(8) "Initials" + [71]=> + string(18) "Internet Free Busy" + [72]=> + string(8) "Keywords" + [73]=> + string(8) "Language" + [74]=> + string(8) "Location" + [75]=> + string(14) "Manager's Name" + [76]=> + string(7) "Mileage" + [77]=> + string(5) "Notes" + [78]=> + string(15) "Office Location" + [79]=> + string(24) "Organizational ID Number" + [80]=> + string(20) "Other Address PO Box" + [81]=> + string(8) "Priority" + [82]=> + string(7) "Private" + [83]=> + string(10) "Profession" + [84]=> + string(11) "Referred By" + [85]=> + string(11) "Sensitivity" + [86]=> + string(6) "Spouse" + [87]=> + string(6) "User 1" + [88]=> + string(6) "User 2" + [89]=> + string(6) "User 3" + [90]=> + string(6) "User 4" + [91]=> + string(8) "Web Page" + } + [1]=> + array(92) { + [0]=> + string(0) "" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" + [4]=> + string(0) "" + [5]=> + string(0) "" + [6]=> + string(0) "" + [7]=> + string(0) "" + [8]=> + string(37) "Big Tower'", 1" Floor +123 Main Street" + [9]=> + string(0) "" + [10]=> + string(0) "" + [11]=> + string(0) "" + [12]=> + string(0) "" + [13]=> + string(0) "" + [14]=> + string(0) "" + [15]=> + string(0) "" + [16]=> + string(0) "" + [17]=> + string(0) "" + [18]=> + string(0) "" + [19]=> + string(0) "" + [20]=> + string(0) "" + [21]=> + string(0) "" + [22]=> + string(0) "" + [23]=> + string(0) "" + [24]=> + string(0) "" + [25]=> + string(0) "" + [26]=> + string(0) "" + [27]=> + string(0) "" + [28]=> + string(0) "" + [29]=> + string(0) "" + [30]=> + string(0) "" + [31]=> + string(0) "" + [32]=> + string(0) "" + [33]=> + string(0) "" + [34]=> + string(0) "" + [35]=> + string(0) "" + [36]=> + string(0) "" + [37]=> + string(0) "" + [38]=> + string(0) "" + [39]=> + string(0) "" + [40]=> + string(0) "" + [41]=> + string(0) "" + [42]=> + string(0) "" + [43]=> + string(0) "" + [44]=> + string(0) "" + [45]=> + string(0) "" + [46]=> + string(0) "" + [47]=> + string(0) "" + [48]=> + string(0) "" + [49]=> + string(6) "0/0/00" + [50]=> + string(0) "" + [51]=> + string(0) "" + [52]=> + string(6) "0/0/00" + [53]=> + string(0) "" + [54]=> + string(0) "" + [55]=> + string(0) "" + [56]=> + string(0) "" + [57]=> + string(0) "" + [58]=> + string(0) "" + [59]=> + string(0) "" + [60]=> + string(0) "" + [61]=> + string(0) "" + [62]=> + string(0) "" + [63]=> + string(0) "" + [64]=> + string(0) "" + [65]=> + string(0) "" + [66]=> + string(11) "Unspecified" + [67]=> + string(0) "" + [68]=> + string(0) "" + [69]=> + string(0) "" + [70]=> + string(0) "" + [71]=> + string(0) "" + [72]=> + string(0) "" + [73]=> + string(0) "" + [74]=> + string(0) "" + [75]=> + string(0) "" + [76]=> + string(0) "" + [77]=> + string(1) "" + [78]=> + string(0) "" + [79]=> + string(0) "" + [80]=> + string(0) "" + [81]=> + string(6) "Normal" + [82]=> + string(5) "False" + [83]=> + string(0) "" + [84]=> + string(0) "" + [85]=> + string(6) "Normal" + [86]=> + string(0) "" + [87]=> + string(0) "" + [88]=> + string(0) "" + [89]=> + string(0) "" + [90]=> + string(0) "" + [91]=> + string(0) "" + } +} diff --git a/framework/File_CSV/tests/bug_6372.csv b/framework/File_CSV/tests/bug_6372.csv new file mode 100644 index 000000000..ec31094f5 --- /dev/null +++ b/framework/File_CSV/tests/bug_6372.csv @@ -0,0 +1,4 @@ +"Title","First Name","Middle Name","Last Name","Suffix","Company","Department","Job Title","Business Street","Business Street 2","Business Street 3","Business City","Business State","Business Postal Code","Business Country/Region","Home Street","Home Street 2","Home Street 3","Home City","Home State","Home Postal Code","Home Country/Region","Other Street","Other Street 2","Other Street 3","Other City","Other State","Other Postal Code","Other Country/Region","Assistant's Phone","Business Fax","Business Phone","Business Phone 2","Callback","Car Phone","Company Main Phone","Home Fax","Home Phone","Home Phone 2","ISDN","Mobile Phone","Other Fax","Other Phone","Pager","Primary Phone","Radio Phone","TTY/TDD Phone","Telex","Account","Anniversary","Assistant's Name","Billing Information","Birthday","Business Address PO Box","Categories","Children","Directory Server","E-mail Address","E-mail Type","E-mail Display Name","E-mail 2 Address","E-mail 2 Type","E-mail 2 Display Name","E-mail 3 Address","E-mail 3 Type","E-mail 3 Display Name","Gender","Government ID Number","Hobby","Home Address PO Box","Initials","Internet Free Busy","Keywords","Language","Location","Manager's Name","Mileage","Notes","Office Location","Organizational ID Number","Other Address PO Box","Priority","Private","Profession","Referred By","Sensitivity","Spouse","User 1","User 2","User 3","User 4","Web Page" +"","","","","","","","","123, 12th Floor, +Main Street",,,"","","","","",,,"","","","","",,,"","","","","","","","","","","","","","","","","","","","","","","","","0/0/00","",,"0/0/00",,"",,,"","","","","","",,,,"Unspecified","",,,"","","","","","",," +","","",,"Normal","False","",,"Normal","","","","","","" diff --git a/framework/File_CSV/tests/bug_6372.phpt b/framework/File_CSV/tests/bug_6372.phpt new file mode 100644 index 000000000..589164757 --- /dev/null +++ b/framework/File_CSV/tests/bug_6372.phpt @@ -0,0 +1,412 @@ +--TEST-- +File_CSV: test for Bug #6372 +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(92) { + [0]=> + string(5) "Title" + [1]=> + string(10) "First Name" + [2]=> + string(11) "Middle Name" + [3]=> + string(9) "Last Name" + [4]=> + string(6) "Suffix" + [5]=> + string(7) "Company" + [6]=> + string(10) "Department" + [7]=> + string(9) "Job Title" + [8]=> + string(15) "Business Street" + [9]=> + string(17) "Business Street 2" + [10]=> + string(17) "Business Street 3" + [11]=> + string(13) "Business City" + [12]=> + string(14) "Business State" + [13]=> + string(20) "Business Postal Code" + [14]=> + string(23) "Business Country/Region" + [15]=> + string(11) "Home Street" + [16]=> + string(13) "Home Street 2" + [17]=> + string(13) "Home Street 3" + [18]=> + string(9) "Home City" + [19]=> + string(10) "Home State" + [20]=> + string(16) "Home Postal Code" + [21]=> + string(19) "Home Country/Region" + [22]=> + string(12) "Other Street" + [23]=> + string(14) "Other Street 2" + [24]=> + string(14) "Other Street 3" + [25]=> + string(10) "Other City" + [26]=> + string(11) "Other State" + [27]=> + string(17) "Other Postal Code" + [28]=> + string(20) "Other Country/Region" + [29]=> + string(17) "Assistant's Phone" + [30]=> + string(12) "Business Fax" + [31]=> + string(14) "Business Phone" + [32]=> + string(16) "Business Phone 2" + [33]=> + string(8) "Callback" + [34]=> + string(9) "Car Phone" + [35]=> + string(18) "Company Main Phone" + [36]=> + string(8) "Home Fax" + [37]=> + string(10) "Home Phone" + [38]=> + string(12) "Home Phone 2" + [39]=> + string(4) "ISDN" + [40]=> + string(12) "Mobile Phone" + [41]=> + string(9) "Other Fax" + [42]=> + string(11) "Other Phone" + [43]=> + string(5) "Pager" + [44]=> + string(13) "Primary Phone" + [45]=> + string(11) "Radio Phone" + [46]=> + string(13) "TTY/TDD Phone" + [47]=> + string(5) "Telex" + [48]=> + string(7) "Account" + [49]=> + string(11) "Anniversary" + [50]=> + string(16) "Assistant's Name" + [51]=> + string(19) "Billing Information" + [52]=> + string(8) "Birthday" + [53]=> + string(23) "Business Address PO Box" + [54]=> + string(10) "Categories" + [55]=> + string(8) "Children" + [56]=> + string(16) "Directory Server" + [57]=> + string(14) "E-mail Address" + [58]=> + string(11) "E-mail Type" + [59]=> + string(19) "E-mail Display Name" + [60]=> + string(16) "E-mail 2 Address" + [61]=> + string(13) "E-mail 2 Type" + [62]=> + string(21) "E-mail 2 Display Name" + [63]=> + string(16) "E-mail 3 Address" + [64]=> + string(13) "E-mail 3 Type" + [65]=> + string(21) "E-mail 3 Display Name" + [66]=> + string(6) "Gender" + [67]=> + string(20) "Government ID Number" + [68]=> + string(5) "Hobby" + [69]=> + string(19) "Home Address PO Box" + [70]=> + string(8) "Initials" + [71]=> + string(18) "Internet Free Busy" + [72]=> + string(8) "Keywords" + [73]=> + string(8) "Language" + [74]=> + string(8) "Location" + [75]=> + string(14) "Manager's Name" + [76]=> + string(7) "Mileage" + [77]=> + string(5) "Notes" + [78]=> + string(15) "Office Location" + [79]=> + string(24) "Organizational ID Number" + [80]=> + string(20) "Other Address PO Box" + [81]=> + string(8) "Priority" + [82]=> + string(7) "Private" + [83]=> + string(10) "Profession" + [84]=> + string(11) "Referred By" + [85]=> + string(11) "Sensitivity" + [86]=> + string(6) "Spouse" + [87]=> + string(6) "User 1" + [88]=> + string(6) "User 2" + [89]=> + string(6) "User 3" + [90]=> + string(6) "User 4" + [91]=> + string(8) "Web Page" + } + [1]=> + array(92) { + [0]=> + string(0) "" + [1]=> + string(0) "" + [2]=> + string(0) "" + [3]=> + string(0) "" + [4]=> + string(0) "" + [5]=> + string(0) "" + [6]=> + string(0) "" + [7]=> + string(0) "" + [8]=> + string(28) "123, 12th Floor, +Main Street" + [9]=> + string(0) "" + [10]=> + string(0) "" + [11]=> + string(0) "" + [12]=> + string(0) "" + [13]=> + string(0) "" + [14]=> + string(0) "" + [15]=> + string(0) "" + [16]=> + string(0) "" + [17]=> + string(0) "" + [18]=> + string(0) "" + [19]=> + string(0) "" + [20]=> + string(0) "" + [21]=> + string(0) "" + [22]=> + string(0) "" + [23]=> + string(0) "" + [24]=> + string(0) "" + [25]=> + string(0) "" + [26]=> + string(0) "" + [27]=> + string(0) "" + [28]=> + string(0) "" + [29]=> + string(0) "" + [30]=> + string(0) "" + [31]=> + string(0) "" + [32]=> + string(0) "" + [33]=> + string(0) "" + [34]=> + string(0) "" + [35]=> + string(0) "" + [36]=> + string(0) "" + [37]=> + string(0) "" + [38]=> + string(0) "" + [39]=> + string(0) "" + [40]=> + string(0) "" + [41]=> + string(0) "" + [42]=> + string(0) "" + [43]=> + string(0) "" + [44]=> + string(0) "" + [45]=> + string(0) "" + [46]=> + string(0) "" + [47]=> + string(0) "" + [48]=> + string(0) "" + [49]=> + string(6) "0/0/00" + [50]=> + string(0) "" + [51]=> + string(0) "" + [52]=> + string(6) "0/0/00" + [53]=> + string(0) "" + [54]=> + string(0) "" + [55]=> + string(0) "" + [56]=> + string(0) "" + [57]=> + string(0) "" + [58]=> + string(0) "" + [59]=> + string(0) "" + [60]=> + string(0) "" + [61]=> + string(0) "" + [62]=> + string(0) "" + [63]=> + string(0) "" + [64]=> + string(0) "" + [65]=> + string(0) "" + [66]=> + string(11) "Unspecified" + [67]=> + string(0) "" + [68]=> + string(0) "" + [69]=> + string(0) "" + [70]=> + string(0) "" + [71]=> + string(0) "" + [72]=> + string(0) "" + [73]=> + string(0) "" + [74]=> + string(0) "" + [75]=> + string(0) "" + [76]=> + string(0) "" + [77]=> + string(1) " +" + [78]=> + string(0) "" + [79]=> + string(0) "" + [80]=> + string(0) "" + [81]=> + string(6) "Normal" + [82]=> + string(5) "False" + [83]=> + string(0) "" + [84]=> + string(0) "" + [85]=> + string(6) "Normal" + [86]=> + string(0) "" + [87]=> + string(0) "" + [88]=> + string(0) "" + [89]=> + string(0) "" + [90]=> + string(0) "" + [91]=> + string(0) "" + } +} diff --git a/framework/File_CSV/tests/columns.phpt b/framework/File_CSV/tests/columns.phpt new file mode 100644 index 000000000..c88b57450 --- /dev/null +++ b/framework/File_CSV/tests/columns.phpt @@ -0,0 +1,98 @@ +--TEST-- +File_CSV: column count tests +--FILE-- + +--EXPECT-- +array(4) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(0) "" + } + [2]=> + array(3) { + [0]=> + string(3) "six" + [1]=> + string(5) "seven" + [2]=> + string(5) "eight" + } + [3]=> + array(3) { + [0]=> + string(4) "nine" + [1]=> + string(3) "ten" + [2]=> + string(6) "eleven" + } +} +array(2) { + [0]=> + string(54) "Wrong number of fields in line 2. Expected 3, found 2." + [1]=> + string(48) "More fields found in line 4 than the expected 3." +} +array(4) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(0) "" + } + [2]=> + array(3) { + [0]=> + string(3) "six" + [1]=> + string(5) "seven" + [2]=> + string(5) "eight" + } + [3]=> + array(3) { + [0]=> + string(4) "nine" + [1]=> + string(3) "ten" + [2]=> + string(6) "eleven" + } +} +array(2) { + [0]=> + string(54) "Wrong number of fields in line 2. Expected 3, found 2." + [1]=> + string(48) "More fields found in line 4 than the expected 3." +} diff --git a/framework/File_CSV/tests/columns1.csv b/framework/File_CSV/tests/columns1.csv new file mode 100644 index 000000000..be92c82ea --- /dev/null +++ b/framework/File_CSV/tests/columns1.csv @@ -0,0 +1,4 @@ +one,two,three +four,five +six,seven,eight +nine,ten,eleven,twelve diff --git a/framework/File_CSV/tests/columns2.csv b/framework/File_CSV/tests/columns2.csv new file mode 100644 index 000000000..da81f1e07 --- /dev/null +++ b/framework/File_CSV/tests/columns2.csv @@ -0,0 +1,4 @@ +"one","two","three" +"four","five" +"six","seven","eight" +"nine","ten","eleven","twelve" diff --git a/framework/File_CSV/tests/common.php b/framework/File_CSV/tests/common.php new file mode 100644 index 000000000..7677dd898 --- /dev/null +++ b/framework/File_CSV/tests/common.php @@ -0,0 +1,31 @@ + +--EXPECT-- +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(3) "six" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(3) "six" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(3) "six" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(3) "six" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(4) "five" + [2]=> + string(3) "six" + } +} diff --git a/framework/File_CSV/tests/multiline.phpt b/framework/File_CSV/tests/multiline.phpt new file mode 100644 index 000000000..fc43c41f8 --- /dev/null +++ b/framework/File_CSV/tests/multiline.phpt @@ -0,0 +1,52 @@ +--TEST-- +File_CSV: multiline tests +--FILE-- + +--EXPECT-- +array(4) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(10) "three +four" + } + [1]=> + array(3) { + [0]=> + string(4) "five" + [1]=> + string(9) "six +seven" + [2]=> + string(5) "eight" + } + [2]=> + array(3) { + [0]=> + string(4) "nine" + [1]=> + string(3) "ten" + [2]=> + string(14) "eleven +twelve" + } + [3]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(11) "three + four" + } +} diff --git a/framework/File_CSV/tests/multiline1.csv b/framework/File_CSV/tests/multiline1.csv new file mode 100644 index 000000000..5e836a7df --- /dev/null +++ b/framework/File_CSV/tests/multiline1.csv @@ -0,0 +1,8 @@ +"one","two","three +four" +"five","six +seven","eight" +"nine","ten","eleven +twelve" +"one","two","three + four" diff --git a/framework/File_CSV/tests/notrailing_crlf.csv b/framework/File_CSV/tests/notrailing_crlf.csv new file mode 100644 index 000000000..5b60c9cc4 --- /dev/null +++ b/framework/File_CSV/tests/notrailing_crlf.csv @@ -0,0 +1,2 @@ +one,two,three +four,five,six diff --git a/framework/File_CSV/tests/notrailing_lf.csv b/framework/File_CSV/tests/notrailing_lf.csv new file mode 100644 index 000000000..5b60c9cc4 --- /dev/null +++ b/framework/File_CSV/tests/notrailing_lf.csv @@ -0,0 +1,2 @@ +one,two,three +four,five,six diff --git a/framework/File_CSV/tests/quote1.csv b/framework/File_CSV/tests/quote1.csv new file mode 100644 index 000000000..e2fe57d10 --- /dev/null +++ b/framework/File_CSV/tests/quote1.csv @@ -0,0 +1,2 @@ +"one",two,"three" +four,"five six",seven diff --git a/framework/File_CSV/tests/quote2.csv b/framework/File_CSV/tests/quote2.csv new file mode 100644 index 000000000..e2fe57d10 --- /dev/null +++ b/framework/File_CSV/tests/quote2.csv @@ -0,0 +1,2 @@ +"one",two,"three" +four,"five six",seven diff --git a/framework/File_CSV/tests/quote3.csv b/framework/File_CSV/tests/quote3.csv new file mode 100644 index 000000000..c44409128 --- /dev/null +++ b/framework/File_CSV/tests/quote3.csv @@ -0,0 +1,3 @@ +"one two","three, four",five +six,"seven ",eight + diff --git a/framework/File_CSV/tests/quote4.csv b/framework/File_CSV/tests/quote4.csv new file mode 100644 index 000000000..c44409128 --- /dev/null +++ b/framework/File_CSV/tests/quote4.csv @@ -0,0 +1,3 @@ +"one two","three, four",five +six,"seven ",eight + diff --git a/framework/File_CSV/tests/quote5.csv b/framework/File_CSV/tests/quote5.csv new file mode 100644 index 000000000..0629c42b4 --- /dev/null +++ b/framework/File_CSV/tests/quote5.csv @@ -0,0 +1,2 @@ +"one two","three, four","five" +"six","seven ","eight" diff --git a/framework/File_CSV/tests/quotes.phpt b/framework/File_CSV/tests/quotes.phpt new file mode 100644 index 000000000..c027d9a8d --- /dev/null +++ b/framework/File_CSV/tests/quotes.phpt @@ -0,0 +1,110 @@ +--TEST-- +File_CSV: quote tests +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(8) "five six" + [2]=> + string(5) "seven" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(3) "one" + [1]=> + string(3) "two" + [2]=> + string(5) "three" + } + [1]=> + array(3) { + [0]=> + string(4) "four" + [1]=> + string(8) "five six" + [2]=> + string(5) "seven" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(7) "one two" + [1]=> + string(11) "three, four" + [2]=> + string(4) "five" + } + [1]=> + array(3) { + [0]=> + string(3) "six" + [1]=> + string(6) "seven " + [2]=> + string(5) "eight" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(7) "one two" + [1]=> + string(11) "three, four" + [2]=> + string(4) "five" + } + [1]=> + array(3) { + [0]=> + string(3) "six" + [1]=> + string(6) "seven " + [2]=> + string(5) "eight" + } +} +array(2) { + [0]=> + array(3) { + [0]=> + string(7) "one two" + [1]=> + string(11) "three, four" + [2]=> + string(4) "five" + } + [1]=> + array(3) { + [0]=> + string(3) "six" + [1]=> + string(6) "seven " + [2]=> + string(5) "eight" + } +} diff --git a/framework/File_CSV/tests/simple_cr.csv b/framework/File_CSV/tests/simple_cr.csv new file mode 100644 index 000000000..beb72ce15 --- /dev/null +++ b/framework/File_CSV/tests/simple_cr.csv @@ -0,0 +1 @@ +one,two,three four,five,six \ No newline at end of file diff --git a/framework/File_CSV/tests/simple_crlf.csv b/framework/File_CSV/tests/simple_crlf.csv new file mode 100644 index 000000000..5b60c9cc4 --- /dev/null +++ b/framework/File_CSV/tests/simple_crlf.csv @@ -0,0 +1,2 @@ +one,two,three +four,five,six diff --git a/framework/File_CSV/tests/simple_lf.csv b/framework/File_CSV/tests/simple_lf.csv new file mode 100644 index 000000000..5b60c9cc4 --- /dev/null +++ b/framework/File_CSV/tests/simple_lf.csv @@ -0,0 +1,2 @@ +one,two,three +four,five,six diff --git a/framework/File_PDF/PDF.php b/framework/File_PDF/PDF.php new file mode 100644 index 000000000..12c885548 --- /dev/null +++ b/framework/File_PDF/PDF.php @@ -0,0 +1,3178 @@ + + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Olivier Plathey + * @author Marko Djukic + * @author Jan Schneider + * @package File_PDF + * @category Fileformats + */ + +/** + * This hack works around Horde bug #4094 + * (http://bugs.horde.org/ticket/?id=4094) + * + * Once this package does not need to support PHP < 4.3.10 anymore the + * following definiton can be removed and the ugly code can be removed + * using + * + * sed -i -e 's/\' \. FILE_PDF_FLOAT \. \'/F/g' PDF.php + */ +if (version_compare(PHP_VERSION, '4.3.10', '>=')) { + define('FILE_PDF_FLOAT', 'F'); +} else { + define('FILE_PDF_FLOAT', 'f'); +} + +class File_PDF { + + /** + * Current page number. + * + * @var integer + */ + var $_page = 0; + + /** + * Current object number. + * + * @var integer + */ + var $_n = 2; + + /** + * Array of object offsets. + * + * @var array + */ + var $_offsets = array(); + + /** + * Buffer holding in-memory PDF. + * + * @var string + */ + var $_buffer = ''; + + /** + * Buffer length, including already flushed content. + * + * @var integer + */ + var $_buflen = 0; + + /** + * Whether the buffer has been flushed already. + * + * @var boolean + */ + var $_flushed = false; + + /** + * Array containing the pages. + * + * @var array + */ + var $_pages = array(); + + /** + * Current document state.
+     *   0 - initial state
+     *   1 - document opened
+     *   2 - page opened
+     *   3 - document closed
+     * 
+ * + * @var integer + */ + var $_state = 0; + + /** + * Flag indicating if PDF file is to be compressed or not. + * + * @var boolean + */ + var $_compress; + + /** + * The default page orientation. + * + * @var string + */ + var $_default_orientation; + + /** + * The current page orientation. + * + * @var string + */ + var $_current_orientation; + + /** + * Array indicating orientation changes. + * + * @var array + */ + var $_orientation_changes = array(); + + /** + * Current width of page format in points. + * + * @var float + */ + var $fwPt; + + /** + * Current height of page format in points. + * + * @var float + */ + var $fhPt; + + /** + * Current width of page format in user units. + * + * @var float + */ + var $fw; + + /** + * Current height of page format in user units. + * + * @var float + */ + var $fh; + + /** + * Current width of page in points. + * + * @var float + */ + var $wPt; + + /** + * Current height of page in points. + * + * @var float + */ + var $hPt; + + /** + * Current width of page in user units + * + * @var float + */ + var $w; + + /** + * Current height of page in user units + * + * @var float + */ + var $h; + + /** + * Scale factor (number of points in user units). + * + * @var float + */ + var $_scale; + + /** + * Left page margin size. + * + * @var float + */ + var $_left_margin; + + /** + * Top page margin size. + * + * @var float + */ + var $_top_margin; + + /** + * Right page margin size. + * + * @var float + */ + var $_right_margin; + + /** + * Break page margin size, the bottom margin which triggers a page break. + * + * @var float + */ + var $_break_margin; + + /** + * Cell margin size. + * + * @var float + */ + var $_cell_margin; + + /** + * The current horizontal position for cell positioning. + * Value is set in user units and is calculated from the top left corner + * as origin. + * + * @var float + */ + var $x; + + /** + * The current vertical position for cell positioning. + * Value is set in user units and is calculated from the top left corner + * as origin. + * + * @var float + */ + var $y; + + /** + * The height of the last cell printed. + * + * @var float + */ + var $_last_height; + + /** + * Line width in user units. + * + * @var float + */ + var $_line_width; + + /** + * An array of standard font names. + * + * @var array + */ + var $_core_fonts = array('courier' => 'Courier', + 'courierB' => 'Courier-Bold', + 'courierI' => 'Courier-Oblique', + 'courierBI' => 'Courier-BoldOblique', + 'helvetica' => 'Helvetica', + 'helveticaB' => 'Helvetica-Bold', + 'helveticaI' => 'Helvetica-Oblique', + 'helveticaBI' => 'Helvetica-BoldOblique', + 'times' => 'Times-Roman', + 'timesB' => 'Times-Bold', + 'timesI' => 'Times-Italic', + 'timesBI' => 'Times-BoldItalic', + 'symbol' => 'Symbol', + 'zapfdingbats' => 'ZapfDingbats'); + + /** + * An array of used fonts. + * + * @var array + */ + var $_fonts = array(); + + /** + * An array of font files. + * + * @var array + */ + var $_font_files = array(); + + /** + * An array of encoding differences. + * + * @var array + */ + var $_diffs = array(); + + /** + * An array of used images. + * + * @var array + */ + var $_images = array(); + + /** + * An array of links in pages. + * + * @var array + */ + var $_page_links; + + /** + * An array of internal links. + * + * @var array + */ + var $_links = array(); + + /** + * Current font family. + * + * @var string + */ + var $_font_family = ''; + + /** + * Current font style. + * + * @var string + */ + var $_font_style = ''; + + /** + * Underlining flag. + * + * @var boolean + */ + var $_underline = false; + + /** + * An array containing current font info. + * + * @var array + */ + var $_current_font; + + /** + * Current font size in points. + * + * @var float + */ + var $_font_size_pt = 12; + + /** + * Current font size in user units. + * + * @var float + */ + var $_font_size = 12; + + /** + * Commands for filling color. + * + * @var string + */ + var $_fill_color = '0 g'; + + /** + * Commands for text color. + * + * @var string + */ + var $_text_color = '0 g'; + + /** + * Whether text color is different from fill color. + * + * @var boolean + */ + var $_color_flag = false; + + /** + * Commands for drawing color. + * + * @var string + */ + var $_draw_color = '0 G'; + + /** + * Word spacing. + * + * @var integer + */ + var $_word_spacing = 0; + + /** + * Automatic page breaking. + * + * @var boolean + */ + var $_auto_page_break; + + /** + * Threshold used to trigger page breaks. + * + * @var float + */ + var $_page_break_trigger; + + /** + * Flag set when processing footer. + * + * @var boolean + */ + var $_in_footer = false; + + /** + * Zoom display mode. + * + * @var string + */ + var $_zoom_mode; + + /** + * Layout display mode. + * + * @var string + */ + var $_layout_mode; + + /** + * An array containing the document info, consisting of: + * - title + * - subject + * - author + * - keywords + * - creator + * + * @var array + */ + var $_info = array(); + + /** + * Alias for total number of pages. + * + * @var string + */ + var $_alias_nb_pages = '{nb}'; + + /** + * Attempts to return a conrete PDF instance. + * + * It allows to set up the page format, the orientation and the units of + * measurement used in all the methods (except for the font sizes). + * + * Example: + * + * $pdf = File_PDF::factory(array('orientation' => 'P', + * 'unit' => 'mm', + * 'format' => 'A4')); + * + * + * @param array $params A hash with parameters for the created PDF object. + * Possible parameters are: + * - orientation - Default page orientation. Possible + * values are (case insensitive): + * - P or Portrait (default) + * - L or Landscape + * - unit - User measure units. Possible values + * values are: + * - pt: point + * - mm: millimeter (default) + * - cm: centimeter + * - in: inch + * A point equals 1/72 of inch, that is to say + * about 0.35 mm (an inch being 2.54 cm). This is a + * very common unit in typography; font sizes are + * expressed in that unit. + * - format - The format used for pages. It can be + * either one of the following values (case + * insensitive): + * - A3 + * - A4 (default) + * - A5 + * - Letter + * - Legal + * or a custom format in the form of a two-element + * array containing the width and the height + * (expressed in the unit given by the unit + * parameter). + * @param string $class The concrete class name to return an instance of. + * Defaults to File_PDF. + */ + function &factory($params = array(), $class = 'File_PDF') + { + /* Default parameters. */ + $defaults = array('orientation' => 'P', + 'unit' => 'mm', + 'format' => 'A4'); + + /* Backward compatibility with old method signature. */ + /* Should be removed a few versions later. */ + if (!is_array($params)) { + $class = 'File_PDF'; + $params = $defaults; + $names = array_keys($defaults); + for ($i = 0; $i < func_num_args(); $i++) { + $params[$names[$i]] = func_get_arg($i); + } + } else { + $params = array_merge($defaults, $params); + } + + /* Create the PDF object. */ + $pdf = new $class($params); + + /* Scale factor. */ + if ($params['unit'] == 'pt') { + $pdf->_scale = 1; + } elseif ($params['unit'] == 'mm') { + $pdf->_scale = 72 / 25.4; + } elseif ($params['unit'] == 'cm') { + $pdf->_scale = 72 / 2.54; + } elseif ($params['unit'] == 'in') { + $pdf->_scale = 72; + } else { + $error = File_PDF::raiseError(sprintf('Incorrect units: %s', $params['unit'])); + return $error; + } + /* Page format. */ + if (is_string($params['format'])) { + $params['format'] = strtolower($params['format']); + if ($params['format'] == 'a3') { + $params['format'] = array(841.89, 1190.55); + } elseif ($params['format'] == 'a4') { + $params['format'] = array(595.28, 841.89); + } elseif ($params['format'] == 'a5') { + $params['format'] = array(420.94, 595.28); + } elseif ($params['format'] == 'letter') { + $params['format'] = array(612, 792); + } elseif ($params['format'] == 'legal') { + $params['format'] = array(612, 1008); + } else { + $error = File_PDF::raiseError(sprintf('Unknown page format: %s', $params['format'])); + return $error; + } + $pdf->fwPt = $params['format'][0]; + $pdf->fhPt = $params['format'][1]; + } else { + $pdf->fwPt = $params['format'][0] * $pdf->_scale; + $pdf->fhPt = $params['format'][1] * $pdf->_scale; + } + $pdf->fw = $pdf->fwPt / $pdf->_scale; + $pdf->fh = $pdf->fhPt / $pdf->_scale; + + /* Page orientation. */ + $params['orientation'] = strtolower($params['orientation']); + if ($params['orientation'] == 'p' || $params['orientation'] == 'portrait') { + $pdf->_default_orientation = 'P'; + $pdf->wPt = $pdf->fwPt; + $pdf->hPt = $pdf->fhPt; + } elseif ($params['orientation'] == 'l' || $params['orientation'] == 'landscape') { + $pdf->_default_orientation = 'L'; + $pdf->wPt = $pdf->fhPt; + $pdf->hPt = $pdf->fwPt; + } else { + $error = File_PDF::raiseError(sprintf('Incorrect orientation: %s', $params['orientation'])); + return $error; + } + $pdf->_current_orientation = $pdf->_default_orientation; + $pdf->w = $pdf->wPt / $pdf->_scale; + $pdf->h = $pdf->hPt / $pdf->_scale; + + /* Page margins (1 cm) */ + $margin = 28.35 / $pdf->_scale; + $pdf->setMargins($margin, $margin); + + /* Interior cell margin (1 mm) */ + $pdf->_cell_margin = $margin / 10; + + /* Line width (0.2 mm) */ + $pdf->_line_width = .567 / $pdf->_scale; + + /* Automatic page break */ + $pdf->setAutoPageBreak(true, 2 * $margin); + + /* Full width display mode */ + $pdf->setDisplayMode('fullwidth'); + + /* Compression */ + $pdf->setCompression(true); + + return $pdf; + } + + /** + * Returns a PEAR_Error object. + * + * Wraps around PEAR::raiseError() to avoid having to include PEAR.php + * unless an error occurs. + * + * @param mixed $error The error message. + * + * @return object PEAR_Error + */ + function raiseError($error) + { + require_once 'PEAR.php'; + return PEAR::raiseError($error); + } + + /** + * Defines the left, top and right margins. + * + * By default, they equal 1 cm. Call this method to change them. + * + * @param float $left Left margin. + * @param float $top Top margin. + * @param float $right Right margin. If not specified default to the value + * of the left one. + * + * @see setAutoPageBreak() + * @see setLeftMargin() + * @see setRightMargin() + * @see setTopMargin() + */ + function setMargins($left, $top, $right = null) + { + /* Set left and top margins. */ + $this->_left_margin = $left; + $this->_top_margin = $top; + /* If no right margin set default to same as left. */ + $this->_right_margin = (is_null($right) ? $left : $right); + } + + /** + * Defines the left margin. + * + * The method can be called before creating the first page. If the + * current abscissa gets out of page, it is brought back to the margin. + * + * @param float $margin The margin. + * + * @see setAutoPageBreak() + * @see setMargins() + * @see setRightMargin() + * @see setTopMargin() + */ + function setLeftMargin($margin) + { + $this->_left_margin = $margin; + /* If there is a current page and the current X position is less than + * margin set the X position to the margin value. */ + if ($this->_page > 0 && $this->x < $margin) { + $this->x = $margin; + } + } + + /** + * Defines the top margin. + * + * The method can be called before creating the first page. + * + * @param float $margin The margin. + */ + function setTopMargin($margin) + { + $this->_top_margin = $margin; + } + + /** + * Defines the right margin. + * + * The method can be called before creating the first page. + * + * @param float $margin The margin. + */ + function setRightMargin($margin) + { + $this->_right_margin = $margin; + } + + /** + * Returns the actual page width. + * + * @since File_PDF 0.2.0 + * @since Horde 3.2 + * + * @return float The page width. + */ + function getPageWidth() + { + return ($this->w - $this->_right_margin - $this->_left_margin); + } + + /** + * Returns the actual page height. + * + * @since File_PDF 0.2.0 + * @since Horde 3.2 + * + * @return float The page height. + */ + function getPageHeight() + { + return ($this->h - $this->_top_margin - $this->_break_margin); + } + + /** + * Enables or disables the automatic page breaking mode. + * + * When enabling, the second parameter is the distance from the bottom of + * the page that defines the triggering limit. By default, the mode is on + * and the margin is 2 cm. + * + * @param boolean $auto Boolean indicating if mode should be on or off. + * @param float $margin Distance from the bottom of the page. + */ + function setAutoPageBreak($auto, $margin = 0) + { + $this->_auto_page_break = $auto; + $this->_break_margin = $margin; + $this->_page_break_trigger = $this->h - $margin; + } + + /** + * Defines the way the document is to be displayed by the viewer. + * + * The zoom level can be set: pages can be displayed entirely on screen, + * occupy the full width of the window, use real size, be scaled by a + * specific zooming factor or use viewer default (configured in the + * Preferences menu of Acrobat). The page layout can be specified too: + * single at once, continuous display, two columns or viewer default. By + * default, documents use the full width mode with continuous display. + * + * @param mixed $zoom The zoom to use. It can be one of the following + * string values: + * - fullpage: entire page on screen + * - fullwidth: maximum width of window + * - real: uses real size (100% zoom) + * - default: uses viewer default mode + * or a number indicating the zooming factor. + * @param string layout The page layout. Possible values are: + * - single: one page at once + * - continuous: pages in continuously + * - two: two pages on two columns + * - default: uses viewer default mode + * Default value is continuous. + */ + function setDisplayMode($zoom, $layout = 'continuous') + { + $zoom = strtolower($zoom); + if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' + || $zoom == 'default' || !is_string($zoom)) { + $this->_zoom_mode = $zoom; + } elseif ($zoom == 'zoom') { + $this->_zoom_mode = $layout; + } else { + return $this->raiseError(sprintf('Incorrect zoom display mode: %s', $zoom)); + } + + $layout = strtolower($layout); + if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' + || $layout == 'default') { + $this->_layout_mode = $layout; + } elseif ($zoom != 'zoom') { + return $this->raiseError(sprintf('Incorrect layout display mode: %s', $layout)); + } + } + + /** + * Activates or deactivates page compression. + * + * When activated, the internal representation of each page is compressed, + * which leads to a compression ratio of about 2 for the resulting + * document. Compression is on by default. + * + * Note: the {@link http://www.php.net/zlib/ zlib extension} is required + * for this feature. If not present, compression will be turned off. + * + * @param boolean $compress Boolean indicating if compression must be + * enabled or not. + */ + function setCompression($compress) + { + /* If no gzcompress function is available then default to false. */ + $this->_compress = (function_exists('gzcompress') ? $compress : false); + } + + /** + * Set the info to a document. + * + * Possible info settings are: + * - title + * - subject + * - author + * - keywords + * - creator + * + * @param array|string $info If passed as an array then the complete hash + * containing the info to be inserted into the + * document. Otherwise the name of setting to be + * set. + * @param string $value The value of the setting. + */ + function setInfo($info, $value = '') + { + if (is_array($info)) { + $this->_info = $info; + } else { + $this->_info[$info] = $value; + } + } + + /** + * Defines an alias for the total number of pages. + * + * It will be substituted as the document is closed. + * + * Example: + * + * class My_File_PDF extends File_PDF { + * function footer() + * { + * // Go to 1.5 cm from bottom + * $this->setY(-15); + * // Select Arial italic 8 + * $this->setFont('Arial', 'I', 8); + * // Print current and total page numbers + * $this->cell(0, 10, 'Page ' . $this->getPageNo() . '/{nb}', 0, + * 0, 'C'); + * } + * } + * $pdf = My_File_PDF::factory(); + * $pdf->aliasNbPages(); + * + * + * @param string $alias The alias. + * + * @see getPageNo() + * @see footer() + */ + function aliasNbPages($alias = '{nb}') + { + $this->_alias_nb_pages = $alias; + } + + /** + * This method begins the generation of the PDF document; it must be + * called before any output commands. + * + * No page is created by this method, therefore it is necessary to call + * {@link addPage()}. + * + * @see addPage() + * @see close() + */ + function open() + { + $this->_beginDoc(); + } + + /** + * Terminates the PDF document. It is not necessary to call this method + * explicitly because {@link output()} does it automatically. + * + * If the document contains no page, {@link addPage()} is called to + * prevent from getting an invalid document. + * + * @see open() + * @see output() + */ + function close() + { + /* Terminate document */ + if ($this->_page == 0) { + $result = $this->addPage(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + /* Page footer */ + $this->_in_footer = true; + $this->x = $this->_left_margin; + $this->footer(); + $this->_in_footer = false; + /* Close page */ + $this->_endPage(); + /* Close document */ + $this->_endDoc(); + } + + /** + * Adds a new page to the document. + * + * If a page is already present, the {@link footer()} method is called + * first to output the footer. Then the page is added, the current + * position set to the top-left corner according to the left and top + * margins, and {@link header()} is called to display the header. + * + * The font which was set before calling is automatically restored. There + * is no need to call {@link setFont()} again if you want to continue with + * the same font. The same is true for colors and line width. The origin + * of the coordinate system is at the top-left corner and increasing + * ordinates go downwards. + * + * @param string $orientation Page orientation. Possible values + * are (case insensitive): + * - P or Portrait + * - L or Landscape + * The default value is the one passed to the + * constructor. + * + * @see header() + * @see footer() + * @see setMargins() + */ + function addPage($orientation = '') + { + /* For good measure make sure this is called. */ + $this->_beginDoc(); + + /* Save style settings so that they are not overridden by footer() or + * header(). */ + $lw = $this->_line_width; + $dc = $this->_draw_color; + $fc = $this->_fill_color; + $tc = $this->_text_color; + $cf = $this->_color_flag; + $font_family = $this->_font_family; + $font_style = $this->_font_style . ($this->_underline ? 'U' : ''); + $font_size = $this->_font_size_pt; + + /* Close old page. */ + if ($this->_page > 0) { + /* Page footer. */ + $this->_in_footer = true; + $this->x = $this->_left_margin; + $this->footer(); + $this->_in_footer = false; + + /* Close page. */ + $this->_endPage(); + } + + /* Start new page. */ + $this->_beginPage($orientation); + /* Set line cap style to square. */ + $this->_out('2 J'); + /* Set line width. */ + $this->_line_width = $lw; + $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $lw * $this->_scale)); + + /* Force the setting of the font. Each new page requires a new + * call. */ + if ($font_family) { + $result = $this->setFont($font_family, $font_style, $font_size, true); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + /* Restore styles. */ + if ($this->_fill_color != $fc) { + $this->_fill_color = $fc; + $this->_out($this->_fill_color); + } + if ($this->_draw_color != $dc) { + $this->_draw_color = $dc; + $this->_out($this->_draw_color); + } + $this->_text_color = $tc; + $this->_color_flag = $cf; + + /* Page header. */ + $this->header(); + + /* Restore styles. */ + if ($this->_line_width != $lw) { + $this->_line_width = $lw; + $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $lw * $this->_scale)); + } + $result = $this->setFont($font_family, $font_style, $font_size); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + if ($this->_fill_color != $fc) { + $this->_fill_color = $fc; + $this->_out($this->_fill_color); + } + if ($this->_draw_color != $dc) { + $this->_draw_color = $dc; + $this->_out($this->_draw_color); + } + $this->_text_color = $tc; + $this->_color_flag = $cf; + } + + /** + * This method is used to render the page header. + * + * It is automatically called by {@link addPage()} and should not be + * called directly by the application. The implementation in File_PDF:: is + * empty, so you have to subclass it and override the method if you want a + * specific processing. + * + * Example: + * + * class My_File_PDF extends File_PDF { + * function header() + * { + * // Select Arial bold 15 + * $this->setFont('Arial', 'B', 15); + * // Move to the right + * $this->cell(80); + * // Framed title + * $this->cell(30, 10, 'Title', 1, 0, 'C'); + * // Line break + * $this->newLine(20); + * } + * } + * + * + * @see footer() + */ + function header() + { + /* To be implemented in your own inherited class. */ + } + + /** + * This method is used to render the page footer. + * + * It is automatically called by {@link addPage()} and {@link close()} and + * should not be called directly by the application. The implementation in + * File_PDF:: is empty, so you have to subclass it and override the method + * if you want a specific processing. + * + * Example: + * + * class My_File_PDF extends File_PDF { + * function footer() + * { + * // Go to 1.5 cm from bottom + * $this->setY(-15); + * // Select Arial italic 8 + * $this->setFont('Arial', 'I', 8); + * // Print centered page number + * $this->cell(0, 10, 'Page ' . $this->getPageNo(), 0, 0, 'C'); + * } + * } + * + * + * @see header() + */ + function footer() + { + /* To be implemented in your own inherited class. */ + } + + /** + * Returns the current page number. + * + * @return integer + * + * @see aliasNbPages() + */ + function getPageNo() + { + return $this->_page; + } + + /** + * Sets the fill color. + * + * Depending on the colorspace called, the number of color component + * parameters required can be either 1, 3 or 4. The method can be called + * before the first page is created and the color is retained from page to + * page. + * + * @param string $cs Indicates the colorspace which can be either 'rgb', + * 'cmyk' or 'gray'. Defaults to 'rgb'. + * @param float $c1 First color component, floating point value between 0 + * and 1. Required for gray, rgb and cmyk. + * @param float $c2 Second color component, floating point value + * between 0 and 1. Required for rgb and cmyk. + * @param float $c3 Third color component, floating point value between 0 + * and 1. Required for rgb and cmyk. + * @param float $c4 Fourth color component, floating point value + * between 0 and 1. Required for cmyk. + * + * @see setTextColor() + * @see setDrawColor() + * @see rect() + * @see cell() + * @see multiCell() + */ + function setFillColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0, $c4 = 0) + { + $cs = strtolower($cs); + if ($cs == 'rgb') { + $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' rg', $c1, $c2, $c3); + } elseif ($cs == 'cmyk') { + $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' k', $c1, $c2, $c3, $c4); + } else { + $this->_fill_color = sprintf('%.3' . FILE_PDF_FLOAT . ' g', $c1); + } + if ($this->_page > 0) { + $this->_out($this->_fill_color); + } + $this->_color_flag = $this->_fill_color != $this->_text_color; + } + + /** + * Sets the text color. + * + * Depending on the colorspace called, the number of color component + * parameters required can be either 1, 3 or 4. The method can be called + * before the first page is created and the color is retained from page to + * page. + * + * @param string $cs Indicates the colorspace which can be either 'rgb', + * 'cmyk' or 'gray'. Defaults to 'rgb'. + * @param float $c1 First color component, floating point value between 0 + * and 1. Required for gray, rgb and cmyk. + * @param float $c2 Second color component, floating point value + * between 0 and 1. Required for rgb and cmyk. + * @param float $c3 Third color component, floating point value between 0 + * and 1. Required for rgb and cmyk. + * @param float $c4 Fourth color component, floating point value + * between 0 and 1. Required for cmyk. + * + * @since File_PDF 0.2.0 + * @since Horde 3.2 + * @see setFillColor() + * @see setDrawColor() + * @see rect() + * @see cell() + * @see multiCell() + */ + function setTextColor($cs, $c1, $c2 = 0, $c3 = 0, $c4 = 0) + { + $cs = strtolower($cs); + if ($cs == 'rgb') { + $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' rg', $c1, $c2, $c3); + } elseif ($cs == 'cmyk') { + $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' k', $c1, $c2, $c3, $c4); + } else { + $this->_text_color = sprintf('%.3' . FILE_PDF_FLOAT . ' g', $c1); + } + $this->_color_flag = $this->_fill_color != $this->_text_color; + } + + /** + * Sets the draw color, used when drawing lines. + * + * Depending on the colorspace called, the number of color component + * parameters required can be either 1, 3 or 4. The method can be called + * before the first page is created and the color is retained from page to + * page. + * + * @param string $cs Indicates the colorspace which can be either 'rgb', + * 'cmyk' or 'gray'. Defaults to 'rgb'. + * @param float $c1 First color component, floating point value between 0 + * and 1. Required for gray, rgb and cmyk. + * @param float $c2 Second color component, floating point value + * between 0 and 1. Required for rgb and cmyk. + * @param float $c3 Third color component, floating point value between 0 + * and 1. Required for rgb and cmyk. + * @param float $c4 Fourth color component, floating point value + * between 0 and 1. Required for cmyk. + * + * @see setFillColor() + * @see line() + * @see rect() + * @see cell() + * @see multiCell() + */ + function setDrawColor($cs = 'rgb', $c1, $c2 = 0, $c3 = 0, $c4 = 0) + { + $cs = strtolower($cs); + if ($cs == 'rgb') { + $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' RG', $c1, $c2, $c3); + } elseif ($cs == 'cmyk') { + $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' %.3' . FILE_PDF_FLOAT . ' K', $c1, $c2, $c3, $c4); + } else { + $this->_draw_color = sprintf('%.3' . FILE_PDF_FLOAT . ' G', $c1); + } + if ($this->_page > 0) { + $this->_out($this->_draw_color); + } + } + + /** + * Returns the length of a text string. A font must be selected. + * + * @param string $text The text whose length is to be computed. + * @param boolean $pt Whether the width should be returned in points or + * user units. + * + * @return float + */ + function getStringWidth($text, $pt = false) + { + $text = (string)$text; + $width = 0; + $length = strlen($text); + for ($i = 0; $i < $length; $i++) { + $width += $this->_current_font['cw'][$text{$i}]; + } + + /* Adjust for word spacing. */ + $width += $this->_word_spacing * substr_count($text, ' ') * $this->_current_font['cw'][' ']; + + if ($pt) { + return $width * $this->_font_size_pt / 1000; + } else { + return $width * $this->_font_size / 1000; + } + } + + /** + * Defines the line width. + * + * By default, the value equals 0.2 mm. The method can be called before + * the first page is created and the value is retained from page to page. + * + * @param float $width The width. + * + * @see line() + * @see rect() + * @see cell() + * @see multiCell() + */ + function setLineWidth($width) + { + $this->_line_width = $width; + if ($this->_page > 0) { + $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' w', $width * $this->_scale)); + } + } + + /** + * Draws a line between two points. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param float $x1 Abscissa of first point. + * @param float $y1 Ordinate of first point. + * @param float $x2 Abscissa of second point. + * @param float $y2 Ordinate of second point. + * + * @see setLineWidth() + * @see setDrawColor() + */ + function line($x1, $y1, $x2, $y2) + { + if ($x1 < 0) { + $x1 += $this->w; + } + if ($y1 < 0) { + $y1 += $this->h; + } + if ($x2 < 0) { + $x2 += $this->w; + } + if ($y2 < 0) { + $y2 += $this->h; + } + + $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S', $x1 * $this->_scale, ($this->h - $y1) * $this->_scale, $x2 * $this->_scale, ($this->h - $y2) * $this->_scale)); + } + + /** + * Outputs a rectangle. + * + * It can be drawn (border only), filled (with no border) or both. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param float $x Abscissa of upper-left corner. + * @param float $y Ordinate of upper-left corner. + * @param float $width Width. + * @param float $height Height. + * @param float $style Style of rendering. Possible values are: + * - D or empty string: draw (default) + * - F: fill + * - DF or FD: draw and fill + * + * @see setLineWidth() + * @see setDrawColor() + * @see setFillColor() + */ + function rect($x, $y, $width, $height, $style = '') + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + $style = strtoupper($style); + if ($style == 'F') { + $op = 'f'; + } elseif ($style == 'FD' || $style == 'DF') { + $op = 'B'; + } else { + $op = 'S'; + } + + $x = $this->_toPt($x); + $y = $this->_toPt($y); + $width = $this->_toPt($width); + $height = $this->_toPt($height); + + $this->_out(sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re %s', $x, $this->hPt - $y, $width, -$height, $op)); + } + + /** + * Outputs a circle. It can be drawn (border only), filled (with no + * border) or both. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param float $x Abscissa of the center of the circle. + * @param float $y Ordinate of the center of the circle. + * @param float $r Circle radius. + * @param string $style Style of rendering. Possible values are: + * - D or empty string: draw (default) + * - F: fill + * - DF or FD: draw and fill + */ + function circle($x, $y, $r, $style = '') + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + $style = strtolower($style); + if ($style == 'f') { + $op = 'f'; // Style is fill only. + } elseif ($style == 'fd' || $style == 'df') { + $op = 'B'; // Style is fill and stroke. + } else { + $op = 'S'; // Style is stroke only. + } + + $x = $this->_toPt($x); + $y = $this->_toPt($y); + $r = $this->_toPt($r); + + /* Invert the y scale. */ + $y = $this->hPt - $y; + /* Length of the Bezier control. */ + $b = $r * 0.552; + + /* Move from the given origin and set the current point + * to the start of the first Bezier curve. */ + $c = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m', $x - $r, $y); + $x = $x - $r; + /* First circle quarter. */ + $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c', + $x, $y + $b, // First control point. + $x + $r - $b, $y + $r, // Second control point. + $x + $r, $y + $r); // Final point. + /* Set x/y to the final point. */ + $x = $x + $r; + $y = $y + $r; + /* Second circle quarter. */ + $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c', + $x + $b, $y, + $x + $r, $y - $r + $b, + $x + $r, $y - $r); + /* Set x/y to the final point. */ + $x = $x + $r; + $y = $y - $r; + /* Third circle quarter. */ + $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c', + $x, $y - $b, + $x - $r + $b, $y - $r, + $x - $r, $y - $r); + /* Set x/y to the final point. */ + $x = $x - $r; + $y = $y - $r; + /* Fourth circle quarter. */ + $c .= sprintf(' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' c %s', + $x - $b, $y, + $x - $r, $y + $r - $b, + $x - $r, $y + $r, + $op); + /* Output the whole string. */ + $this->_out($c); + } + + /** + * Imports a TrueType or Type1 font and makes it available. It is + * necessary to generate a font definition file first with the + * makefont.php utility. + * The location of the definition file (and the font file itself when + * embedding) must be found at the full path name included. + * + * Example: + * + * $pdf->addFont('Comic', 'I'); + * is equivalent to: + * $pdf->addFont('Comic', 'I', 'comici.php'); + * + * + * @param string $family Font family. The name can be chosen arbitrarily. + * If it is a standard family name, it will + * override the corresponding font. + * @param string $style Font style. Possible values are (case + * insensitive): + * - empty string: regular (default) + * - B: bold + * - I: italic + * - BI or IB: bold italic + * @param string $file The font definition file. By default, the name is + * built from the family and style, in lower case + * with no space. + * + * @see setFont() + */ + function addFont($family, $style = '', $file = '') + { + $family = strtolower($family); + if ($family == 'arial') { + $family = 'helvetica'; + } + + $style = strtoupper($style); + if ($style == 'IB') { + $style = 'BI'; + } + if (isset($this->_fonts[$family . $style])) { + return $this->raiseError(sprintf('Font already added: %s %s', $family, $style)); + } + if ($file == '') { + $file = str_replace(' ', '', $family) . strtolower($style) . '.php'; + } + include($file); + if (!isset($name)) { + return $this->raiseError('Could not include font definition file'); + } + $i = count($this->_fonts) + 1; + $this->_fonts[$family . $style] = array('i' => $i, 'type' => $type, 'name' => $name, 'desc' => $desc, 'up' => $up, 'ut' => $ut, 'cw' => $cw, 'enc' => $enc, 'file' => $file); + if ($diff) { + /* Search existing encodings. */ + $d = 0; + $nb = count($this->_diffs); + for ($i = 1; $i <= $nb; $i++) { + if ($this->_diffs[$i] == $diff) { + $d = $i; + break; + } + } + if ($d == 0) { + $d = $nb + 1; + $this->_diffs[$d] = $diff; + } + $this->_fonts[$family . $style]['diff'] = $d; + } + if ($file) { + if ($type == 'TrueType') { + $this->_font_files[$file] = array('length1' => $originalsize); + } else { + $this->_font_files[$file] = array('length1' => $size1, 'length2' => $size2); + } + } + } + + /** + * Sets the font used to print character strings. + * + * It is mandatory to call this method at least once before printing text + * or the resulting document would not be valid. The font can be either a + * standard one or a font added via the {@link addFont()} method. Standard + * fonts use Windows encoding cp1252 (Western Europe). + * + * The method can be called before the first page is created and the font + * is retained from page to page. + * + * If you just wish to change the current font size, it is simpler to call + * {@link setFontSize()}. + * + * @param string $family Family font. It can be either a name defined by + * {@link addFont()} or one of the standard families + * (case insensitive): + * - Courier (fixed-width) + * - Helvetica or Arial (sans serif) + * - Times (serif) + * - Symbol (symbolic) + * - ZapfDingbats (symbolic) + * It is also possible to pass an empty string. In + * that case, the current family is retained. + * @param string $style Font style. Possible values are (case + * insensitive): + * - empty string: regular + * - B: bold + * - I: italic + * - U: underline + * or any combination. Bold and italic styles do not + * apply to Symbol and ZapfDingbats. + * @param integer $size Font size in points. The default value is the + * current size. If no size has been specified since + * the beginning of the document, the value taken + * is 12. + * @param boolean $force Force the setting of the font. Each new page will + * require a new call to {@link setFont()} and + * settings this to true will make sure that the + * checks for same font calls will be skipped. + * + * @see addFont() + * @see setFontSize() + * @see cell() + * @see multiCell() + * @see write() + */ + function setFont($family, $style = '', $size = null, $force = false) + { + $family = strtolower($family); + if (empty($family)) { + $family = $this->_font_family; + } + if ($family == 'arial') { + /* Use helvetica instead of arial. */ + $family = 'helvetica'; + } elseif ($family == 'symbol' || $family == 'zapfdingbats') { + /* These two fonts do not have styles available. */ + $style = ''; + } + + $style = strtoupper($style); + + /* Underline is handled separately, if specified in the style var + * remove it from the style and set the underline flag. */ + if (strpos($style, 'U') !== false) { + $this->_underline = true; + $style = str_replace('U', '', $style); + } else { + $this->_underline = false; + } + + if ($style == 'IB') { + $style = 'BI'; + } + + /* If no size specified, use current size. */ + if (is_null($size)) { + $size = $this->_font_size_pt; + } + + /* If font requested is already the current font and no force setting + * of the font is requested (eg. when adding a new page) don't bother + * with the rest of the function and simply return. */ + if ($this->_font_family == $family && $this->_font_style == $style && + $this->_font_size_pt == $size && !$force) { + return; + } + + /* Set the font key. */ + $fontkey = $family . $style; + + /* Test if already cached. */ + if (!isset($this->_fonts[$fontkey])) { + /* Get the character width definition file. */ + $font_widths = File_PDF::_getFontFile($fontkey); + if (is_a($font_widths, 'PEAR_Error')) { + return $font_widths; + } + + $i = count($this->_fonts) + 1; + $this->_fonts[$fontkey] = array( + 'i' => $i, + 'type' => 'core', + 'name' => $this->_core_fonts[$fontkey], + 'up' => -100, + 'ut' => 50, + 'cw' => $font_widths[$fontkey]); + } + + /* Store font information as current font. */ + $this->_font_family = $family; + $this->_font_style = $style; + $this->_font_size_pt = $size; + $this->_font_size = $size / $this->_scale; + $this->_current_font = &$this->_fonts[$fontkey]; + + /* Output font information if at least one page has been defined. */ + if ($this->_page > 0) { + $this->_out(sprintf('BT /F%d %.2' . FILE_PDF_FLOAT . ' Tf ET', $this->_current_font['i'], $this->_font_size_pt)); + } + } + + /** + * Defines the size of the current font. + * + * @param float $size The size (in points). + * + * @see setFont() + */ + function setFontSize($size) + { + /* If the font size is already the current font size, just return. */ + if ($this->_font_size_pt == $size) { + return; + } + /* Set the current font size, both in points and scaled to user + * units. */ + $this->_font_size_pt = $size; + $this->_font_size = $size / $this->_scale; + + /* Output font information if at least one page has been defined. */ + if ($this->_page > 0) { + $this->_out(sprintf('BT /F%d %.2' . FILE_PDF_FLOAT . ' Tf ET', $this->_current_font['i'], $this->_font_size_pt)); + } + } + + /** + * Defines the style of the current font. + * + * @param string $style The font style. + * + * @since File_PDF 0.2.0 + * @since Horde 3.2 + * @see setFont() + */ + function setFontStyle($style) + { + return $this->setFont($this->_font_family, $style); + } + + /** + * Creates a new internal link and returns its identifier. + * + * An internal link is a clickable area which directs to another place + * within the document. + * + * The identifier can then be passed to {@link cell()}, {@link()} write, + * {@link image()} or {@link link()}. The destination is defined with + * {@link setLink()}. + * + * @see cell() + * @see write() + * @see image() + * @see link() + * @see setLink() + */ + function addLink() + { + $n = count($this->_links) + 1; + $this->_links[$n] = array(0, 0); + return $n; + } + + /** + * Defines the page and position a link points to. + * + * @param integer $link The link identifier returned by {@link addLink()}. + * @param float $y Ordinate of target position; -1 indicates the + * current position. The default value is 0 (top of + * page). + * @param integer $page Number of target page; -1 indicates the current + * page. + * + * @see addLink() + */ + function setLink($link, $y = 0, $page = -1) + { + if ($y == -1) { + $y = $this->y; + } + if ($page == -1) { + $page = $this->_page; + } + $this->_links[$link] = array($page, $y); + } + + /** + * Puts a link on a rectangular area of the page. + * + * Text or image links are generally put via {@link cell()}, {@link + * write()} or {@link image()}, but this method can be useful for instance + * to define a clickable area inside an image. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param float $x Abscissa of the upper-left corner of the + * rectangle. + * @param float $y Ordinate of the upper-left corner of the + * rectangle. + * @param float $width Width of the rectangle. + * @param float $height Height of the rectangle. + * @param mixed $link URL or identifier returned by {@link addLink()}. + * + * @see addLink() + * @see cell() + * @see write() + * @see image() + */ + function link($x, $y, $width, $height, $link) + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + /* Set up the coordinates with correct scaling in pt. */ + $x = $this->_toPt($x); + $y = $this->hPt - $this->_toPt($y); + $width = $this->_toPt($width); + $height = $this->_toPt($height); + + /* Save link to page links array. */ + $this->_link($x, $y, $width, $height, $link); + } + + /** + * Prints a character string. + * + * The origin is on the left of the first character, on the baseline. This + * method allows to place a string precisely on the page, but it is + * usually easier to use {@link cell()}, {@link multiCell()} or {@link + * write()} which are the standard methods to print text. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param float $x Abscissa of the origin. + * @param float $y Ordinate of the origin. + * @param string $text String to print. + * + * @see setFont() + * @see cell() + * @see multiCell() + * @see write() + */ + function text($x, $y, $text) + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + /* Scale coordinates into points and set correct Y position. */ + $x = $this->_toPt($x); + $y = $this->hPt - $this->_toPt($y); + + /* Escape any potentially harmful characters. */ + $text = $this->_escape($text); + + $out = sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Td (%s) Tj ET', $x, $y, $text); + if ($this->_underline && $text != '') { + $out .= ' ' . $this->_doUnderline($x, $y, $text); + } + if ($this->_color_flag) { + $out = sprintf('q %s %s Q', $this->_text_color, $out); + } + $this->_out($out); + } + + /** + * Whenever a page break condition is met, the method is called, and the + * break is issued or not depending on the returned value. The default + * implementation returns a value according to the mode selected by + * {@link setAutoPageBreak()}. + * This method is called automatically and should not be called directly + * by the application. + * + * @return boolean + * + * @see setAutoPageBreak() + */ + function acceptPageBreak() + { + return $this->_auto_page_break; + } + + /** + * Prints a cell (rectangular area) with optional borders, background + * color and character string. + * + * The upper-left corner of the cell corresponds to the current + * position. The text can be aligned or centered. After the call, the + * current position moves to the right or to the next line. It is possible + * to put a link on the text. If automatic page breaking is enabled and + * the cell goes beyond the limit, a page break is done before outputting. + * + * @param float $width Cell width. If 0, the cell extends up to the right + * margin. + * @param float $height Cell height. + * @param string $text String to print. + * @param mixed $border Indicates if borders must be drawn around the + * cell. The value can be either a number: + * - 0: no border (default) + * - 1: frame + * or a string containing some or all of the + * following characters (in any order): + * - L: left + * - T: top + * - R: right + * - B: bottom + * @param integer $ln Indicates where the current position should go + * after the call. Possible values are: + * - 0: to the right (default) + * - 1: to the beginning of the next line + * - 2: below + * Putting 1 is equivalent to putting 0 and calling + * {@link newLine()} just after. + * @param string $align Allows to center or align the text. Possible + * values are: + * - L or empty string: left (default) + * - C: center + * - R: right + * @param integer $fill Indicates if the cell fill type. Possible values + * are: + * - 0: transparent (default) + * - 1: painted + * @param string $link URL or identifier returned by {@link addLink()}. + * + * @see setFont() + * @see setDrawColor() + * @see setFillColor() + * @see setLineWidth() + * @see addLink() + * @see newLine() + * @see multiCell() + * @see write() + * @see setAutoPageBreak() + */ + function cell($width, $height = 0, $text = '', $border = 0, $ln = 0, + $align = '', $fill = 0, $link = '') + { + $k = $this->_scale; + if ($this->y + $height > $this->_page_break_trigger && + !$this->_in_footer && $this->acceptPageBreak()) { + $x = $this->x; + $ws = $this->_word_spacing; + if ($ws > 0) { + $this->_word_spacing = 0; + $this->_out('0 Tw'); + } + $result = $this->addPage($this->_current_orientation); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $this->x = $x; + if ($ws > 0) { + $this->_word_spacing = $ws; + $this->_out(sprintf('%.3' . FILE_PDF_FLOAT . ' Tw', $ws * $k)); + } + } + if ($width == 0) { + $width = $this->w - $this->_right_margin - $this->x; + } + $s = ''; + if ($fill == 1 || $border == 1) { + if ($fill == 1) { + $op = ($border == 1) ? 'B' : 'f'; + } else { + $op = 'S'; + } + $s = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re %s ', $this->x * $k, ($this->h - $this->y) * $k, $width * $k, -$height * $k, $op); + } + if (is_string($border)) { + if (strpos($border, 'L') !== false) { + $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - $this->y) * $k, $this->x * $k, ($this->h - ($this->y + $height)) * $k); + } + if (strpos($border, 'T') !== false) { + $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - $this->y) * $k); + } + if (strpos($border, 'R') !== false) { + $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', ($this->x + $width) * $k, ($this->h - $this->y) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k); + } + if (strpos($border, 'B') !== false) { + $s .= sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' m %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' l S ', $this->x * $k, ($this->h - ($this->y + $height)) * $k, ($this->x + $width) * $k, ($this->h - ($this->y + $height)) * $k); + } + } + if ($text != '') { + if ($align == 'R') { + $dx = $width - $this->_cell_margin - $this->getStringWidth($text); + } elseif ($align == 'C') { + $dx = ($width - $this->getStringWidth($text)) / 2; + } else { + $dx = $this->_cell_margin; + } + if ($this->_color_flag) { + $s .= 'q ' . $this->_text_color . ' '; + } + $text = str_replace(')', '\\)', str_replace('(', '\\(', str_replace('\\', '\\\\', $text))); + $test2 = ((.5 * $height) + (.3 * $this->_font_size)); + $test1 = $this->fhPt - (($this->y + $test2) * $k); + $x = ($this->x + $dx) * $k; + $y = ($this->h - ($this->y + .5 * $height + .3 * $this->_font_size)) * $k; + $s .= sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Td (%s) Tj ET', $x, $y, $text); + if ($this->_underline) { + $s .= ' ' . $this->_doUnderline($x, $y, $text); + } + if ($this->_color_flag) { + $s .= ' Q'; + } + if ($link) { + $this->link($this->x + $dx, $this->y + .5 * $height- .5 * $this->_font_size, $this->getStringWidth($text), $this->_font_size, $link); + } + } + if ($s) { + $this->_out($s); + } + $this->_last_height = $height; + if ($ln > 0) { + /* Go to next line. */ + $this->y += $height; + if ($ln == 1) { + $this->x = $this->_left_margin; + } + } else { + $this->x += $width; + } + } + + /** + * This method allows printing text with line breaks. + * + * They can be automatic (as soon as the text reaches the right border of + * the cell) or explicit (via the \n character). As many cells as + * necessary are output, one below the other. Text can be aligned, + * centered or justified. The cell block can be framed and the background + * painted. + * + * @param float $width Width of cells. If 0, they extend up to the right + * margin of the page. + * @param float $height Height of cells. + * @param string $text String to print. + * @param mixed $border Indicates if borders must be drawn around the cell + * block. The value can be either a number: + * - 0: no border (default) + * - 1: frame + * or a string containing some or all of the + * following characters (in any order): + * - L: left + * - T: top + * - R: right + * - B: bottom + * @param string $align Sets the text alignment. Possible values are: + * - L: left alignment + * - C: center + * - R: right alignment + * - J: justification (default value) + * @param integer $fill Indicates if the cell background must: + * - 0: transparent (default) + * - 1: painted + * + * @see setFont() + * @see setDrawColor() + * @see setFillColor() + * @see setLineWidth() + * @see cell() + * @see write() + * @see setAutoPageBreak() + */ + function multiCell($width, $height, $text, $border = 0, $align = 'J', + $fill = 0) + { + $cw = &$this->_current_font['cw']; + if ($width == 0) { + $width = $this->w - $this->_right_margin - $this->x; + } + $wmax = ($width-2 * $this->_cell_margin) * 1000 / $this->_font_size; + $s = str_replace("\r", '', $text); + $nb = strlen($s); + if ($nb > 0 && $s[$nb-1] == "\n") { + $nb--; + } + $b = 0; + if ($border) { + if ($border == 1) { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } else { + $b2 = ''; + if (strpos($border, 'L') !== false) { + $b2 .= 'L'; + } + if (strpos($border, 'R') !== false) { + $b2 .= 'R'; + } + $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while ($i < $nb) { + /* Get next character. */ + $c = $s[$i]; + if ($c == "\n") { + /* Explicit line break. */ + if ($this->_word_spacing > 0) { + $this->_word_spacing = 0; + $this->_out('0 Tw'); + } + $result = $this->cell($width, $height, substr($s, $j, $i-$j), + $b, 2, $align, $fill); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) { + $b = $b2; + } + continue; + } + if ($c == ' ') { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if ($l > $wmax) { + /* Automatic line break. */ + if ($sep == -1) { + if ($i == $j) { + $i++; + } + if ($this->_word_spacing > 0) { + $this->_word_spacing = 0; + $this->_out('0 Tw'); + } + $result = $this->cell($width, $height, + substr($s, $j, $i - $j), $b, 2, + $align, $fill); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } else { + if ($align == 'J') { + $this->_word_spacing = ($ns>1) + ? ($wmax - $ls) / 1000 * $this->_font_size / ($ns - 1) + : 0; + $this->_out(sprintf('%.3' . FILE_PDF_FLOAT . ' Tw', + $this->_word_spacing * $this->_scale)); + } + $result = $this->cell($width, $height, + substr($s, $j, $sep - $j), + $b, 2, $align, $fill); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) { + $b = $b2; + } + } else { + $i++; + } + } + /* Last chunk. */ + if ($this->_word_spacing > 0) { + $this->_word_spacing = 0; + $this->_out('0 Tw'); + } + if ($border && strpos($border, 'B') !== false) { + $b .= 'B'; + } + $result = $this->cell($width, $height, substr($s, $j, $i), $b, 2, + $align, $fill); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $this->x = $this->_left_margin; + } + + /** + * This method prints text from the current position. + * + * When the right margin is reached (or the \n character is met) a line + * break occurs and text continues from the left margin. Upon method exit, + * the current position is left just at the end of the text. + * + * It is possible to put a link on the text. + * + * Example: + * + * // Begin with regular font + * $pdf->setFont('Arial', '', 14); + * $pdf->write(5, 'Visit '); + * // Then put a blue underlined link + * $pdf->setTextColor(0, 0, 255); + * $pdf->setFont('', 'U'); + * $pdf->write(5, 'www.fpdf.org', 'http://www.fpdf.org'); + * + * + * @param float $height Line height. + * @param string $text String to print. + * @param mixed $link URL or identifier returned by {@link addLink()}. + * + * @see setFont() + * @see addLink() + * @see multiCell() + * @see setAutoPageBreak() + */ + function write($height, $text, $link = '') + { + $cw = &$this->_current_font['cw']; + $width = $this->w - $this->_right_margin - $this->x; + $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size; + $s = str_replace("\r", '', $text); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + /* Get next character. */ + $c = $s{$i}; + if ($c == "\n") { + /* Explicit line break. */ + $result = $this->cell($width, $height, substr($s, $j, $i - $j), + 0, 2, '', 0, $link); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $i++; + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->_left_margin; + $width = $this->w - $this->_right_margin - $this->x; + $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size; + } + $nl++; + continue; + } + if ($c == ' ') { + $sep = $i; + $ls = $l; + } + $l += (isset($cw[$c]) ? $cw[$c] : 0); + if ($l > $wmax) { + /* Automatic line break. */ + if ($sep == -1) { + if ($this->x > $this->_left_margin) { + /* Move to next line. */ + $this->x = $this->_left_margin; + $this->y += $height; + $width = $this->w - $this->_right_margin - $this->x; + $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size; + $i++; + $nl++; + continue; + } + if ($i == $j) { + $i++; + } + $result = $this->cell($width, $height, + substr($s, $j, $i - $j), + 0, 2, '', 0, $link); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } else { + $result = $this->cell($width, $height, + substr($s, $j, $sep - $j), + 0, 2, '', 0, $link); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->_left_margin; + $width = $this->w - $this->_right_margin - $this->x; + $wmax = ($width - 2 * $this->_cell_margin) * 1000 / $this->_font_size; + } + $nl++; + } else { + $i++; + } + } + /* Last chunk. */ + if ($i != $j) { + $result = $this->cell($l / 1000 * $this->_font_size, $height, + substr($s, $j, $i), 0, 0, '', 0, $link); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + } + + /** + * Writes text at an angle. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * @param integer $x X coordinate. + * @param integer $y Y coordinate. + * @param string $text Text to write. + * @param float $text_angle Angle to rotate (Eg. 90 = bottom to top). + * @param float $font_angle Rotate characters as well as text. + * + * @see setFont() + */ + function writeRotated($x, $y, $text, $text_angle, $font_angle = 0) + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + /* Escape text. */ + $text = $this->_escape($text); + + $font_angle += 90 + $text_angle; + $text_angle *= M_PI / 180; + $font_angle *= M_PI / 180; + + $text_dx = cos($text_angle); + $text_dy = sin($text_angle); + $font_dx = cos($font_angle); + $font_dy = sin($font_angle); + + $s= sprintf('BT %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' Tm (%s) Tj ET', + $text_dx, $text_dy, $font_dx, $font_dy, + $x * $this->_scale, ($this->h-$y) * $this->_scale, $text); + + if ($this->_draw_color) { + $s = 'q ' . $this->_draw_color . ' ' . $s . ' Q'; + } + $this->_out($s); + } + + /** + * Prints an image in the page. + * + * The upper-left corner and at least one of the dimensions must be + * specified; the height or the width can be calculated automatically in + * order to keep the image proportions. Supported formats are JPEG and + * PNG. + * + * All coordinates can be negative to provide values from the right or + * bottom edge of the page (since File_PDF 0.2.0, Horde 3.2). + * + * For JPEG, all flavors are allowed: + * - gray scales + * - true colors (24 bits) + * - CMYK (32 bits) + * + * For PNG, are allowed: + * - gray scales on at most 8 bits (256 levels) + * - indexed colors + * - true colors (24 bits) + * but are not supported: + * - Interlacing + * - Alpha channel + * + * If a transparent color is defined, it will be taken into account (but + * will be only interpreted by Acrobat 4 and above). + * The format can be specified explicitly or inferred from the file + * extension. + * It is possible to put a link on the image. + * + * Remark: if an image is used several times, only one copy will be + * embedded in the file. + * + * @param string $file Name of the file containing the image. + * @param float $x Abscissa of the upper-left corner. + * @param float $y Ordinate of the upper-left corner. + * @param float $width Width of the image in the page. If equal to zero, + * it is automatically calculated to keep the + * original proportions. + * @param float $height Height of the image in the page. If not specified + * or equal to zero, it is automatically calculated + * to keep the original proportions. + * @param string $type Image format. Possible values are (case + * insensitive): JPG, JPEG, PNG. If not specified, + * the type is inferred from the file extension. + * @param mixed $link URL or identifier returned by {@link addLink()}. + * + * @see addLink() + */ + function image($file, $x, $y, $width = 0, $height = 0, $type = '', + $link = '') + { + if ($x < 0) { + $x += $this->w; + } + if ($y < 0) { + $y += $this->h; + } + + if (!isset($this->_images[$file])) { + /* First use of image, get some file info. */ + if ($type == '') { + $pos = strrpos($file, '.'); + if ($pos === false) { + return $this->raiseError(sprintf('Image file has no extension and no type was specified: %s', $file)); + } + $type = substr($file, $pos + 1); + } + $type = strtolower($type); + $mqr = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + if ($type == 'jpg' || $type == 'jpeg') { + $info = $this->_parseJPG($file); + } elseif ($type == 'png') { + $info = $this->_parsePNG($file); + } else { + return $this->raiseError(sprintf('Unsupported image file type: %s', $type)); + } + if (is_a($info, 'PEAR_Error')) { + return $info; + } + set_magic_quotes_runtime($mqr); + $info['i'] = count($this->_images) + 1; + $this->_images[$file] = $info; + } else { + $info = $this->_images[$file]; + } + + /* Make sure all vars are converted to pt scale. */ + $x = $this->_toPt($x); + $y = $this->hPt - $this->_toPt($y); + $width = $this->_toPt($width); + $height = $this->_toPt($height); + + /* If not specified do automatic width and height calculations. */ + if (empty($width) && empty($height)) { + $width = $info['w']; + $height = $info['h']; + } elseif (empty($width)) { + $width = $height * $info['w'] / $info['h']; + } elseif (empty($height)) { + $height = $width * $info['h'] / $info['w']; + } + + $this->_out(sprintf('q %.2' . FILE_PDF_FLOAT . ' 0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' cm /I%d Do Q', $width, $height, $x, $y - $height, $info['i'])); + + /* Set any link if requested. */ + if ($link) { + $this->_link($x, $y, $width, $height, $link); + } + } + + /** + * Performs a line break. + * + * The current abscissa goes back to the left margin and the ordinate + * increases by the amount passed in parameter. + * + * @param float $height The height of the break. By default, the value + * equals the height of the last printed cell. + * + * @see cell() + */ + function newLine($height = '') + { + $this->x = $this->_left_margin; + if (is_string($height)) { + $this->y += $this->_last_height; + } else { + $this->y += $height; + } + } + + /** + * Returns the abscissa of the current position in user units. + * + * @return float + * + * @see setX() + * @see getY() + * @see setY() + */ + function getX() + { + return $this->x; + } + + /** + * Defines the abscissa of the current position. + * + * If the passed value is negative, it is relative to the right of the + * page. + * + * @param float $x The value of the abscissa. + * + * @see getX() + * @see getY() + * @see setY() + * @see setXY() + */ + function setX($x) + { + if ($x >= 0) { + /* Absolute value. */ + $this->x = $x; + } else { + /* Negative, so relative to right edge of the page. */ + $this->x = $this->w + $x; + } + } + + /** + * Returns the ordinate of the current position in user units. + * + * @return float + * + * @see setY() + * @see getX() + * @see setX() + */ + function getY() + { + return $this->y; + } + + /** + * Defines the ordinate of the current position. + * + * If the passed value is negative, it is relative to the bottom of the + * page. + * + * @param float $y The value of the ordinate. + * + * @see getX() + * @see getY() + * @see setY() + * @see setXY() + */ + function setY($y) + { + if ($y >= 0) { + /* Absolute value. */ + $this->y = $y; + } else { + /* Negative, so relative to bottom edge of the page. */ + $this->y = $this->h + $y; + } + } + + /** + * Defines the abscissa and ordinate of the current position. + * + * If the passed values are negative, they are relative respectively to + * the right and bottom of the page. + * + * @param float $x The value of the abscissa. + * @param float $y The value of the ordinate. + * + * @see setX() + * @see setY() + */ + function setXY($x, $y) + { + $this->setY($y); + $this->setX($x); + } + + /** + * Returns the current buffer content and resets the buffer. + * + * Use this method when creating large files to avoid memory problems. + * This method doesn't work in combination with the output() or save() + * methods, use getOutput() at the end. Calling this method doubles the + * memory usage during the call. + * + * @since File_PDF 0.2.0 + * @since Horde 3.2 + * @see getOutput() + */ + function flush() + { + // Make sure we have the file header. + $this->_beginDoc(); + + $buffer = $this->_buffer; + $this->_buffer = ''; + $this->_flushed = true; + $this->_buflen += strlen($buffer); + + return $buffer; + } + + /** + * Returns the raw PDF file. + * + * @see output() + * @see flush() + */ + function getOutput() + { + /* Check whether file has been closed. */ + if ($this->_state < 3) { + $result = $this->close(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + return $this->_buffer; + } + + /** + * Sends the buffered data to the browser. + * + * @param string $filename The filename for the output file. + * @param boolean $inline True if inline, false if attachment. + */ + function output($filename = 'unknown.pdf', $inline = false) + { + /* Check whether the buffer has been flushed already. */ + if ($this->_flushed) { + return $this->raiseError('The buffer has been flushed already, don\'t use output() in combination with flush().'); + } + + /* Check whether file has been closed. */ + if ($this->_state < 3) { + $result = $this->close(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + /* Check if headers have been sent. */ + if (headers_sent()) { + return $this->raiseError('Unable to send PDF file, some data has already been output to browser'); + } + + /* If HTTP_Download is not available return a PEAR_Error. */ + if (!include_once 'HTTP/Download.php') { + return $this->raiseError('Missing PEAR package HTTP_Download'); + } + + /* Params for the output. */ + $disposition = $inline ? HTTP_DOWNLOAD_INLINE : HTTP_DOWNLOAD_ATTACHMENT; + $params = array('data' => $this->_buffer, + 'contenttype' => 'application/pdf', + 'contentdisposition' => array($disposition, $filename)); + /* Output the file. */ + return HTTP_Download::staticSend($params); + } + + /** + * Saves the PDF file on the filesystem. + * + * @param string $filename The filename for the output file. + */ + function save($filename = 'unknown.pdf') + { + /* Check whether the buffer has been flushed already. */ + if ($this->_flushed) { + return $this->raiseError('The buffer has been flushed already, don\'t use save() in combination with flush().'); + } + + /* Check whether file has been closed. */ + if ($this->_state < 3) { + $result = $this->close(); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + } + + $f = fopen($filename, 'wb'); + if (!$f) { + return $this->raiseError(sprintf('Unable to save PDF file: %s', $filename)); + } + fwrite($f, $this->_buffer, strlen($this->_buffer)); + fclose($f); + } + + function _toPt($val) + { + return $val * $this->_scale; + } + + function _getFontFile($fontkey, $path = '') + { + static $font_widths = array(); + + if (!isset($font_widths[$fontkey])) { + if (!empty($path)) { + $file = $path . strtolower($fontkey) . '.php'; + } else { + $file = 'File/PDF/fonts/' . strtolower($fontkey) . '.php'; + } + include $file; + if (!isset($font_widths[$fontkey])) { + return $this->raiseError(sprintf('Could not include font metric file: %s', $file)); + } + } + + return $font_widths; + } + + function _link($x, $y, $width, $height, $link) + { + /* Save link to page links array. */ + $this->_page_links[$this->_page][] = array($x, $y, $width, $height, $link); + } + + function _beginDoc() + { + /* Start document, but only if not yet started. */ + if ($this->_state < 1) { + $this->_state = 1; + $this->_out('%PDF-1.3'); + } + } + + function _putPages() + { + $nb = $this->_page; + if (!empty($this->_alias_nb_pages)) { + /* Replace number of pages. */ + for ($n = 1; $n <= $nb; $n++) { + $this->_pages[$n] = str_replace($this->_alias_nb_pages, $nb, $this->_pages[$n]); + } + } + if ($this->_default_orientation == 'P') { + $wPt = $this->fwPt; + $hPt = $this->fhPt; + } else { + $wPt = $this->fhPt; + $hPt = $this->fwPt; + } + $filter = ($this->_compress) ? '/Filter /FlateDecode ' : ''; + for ($n = 1; $n <= $nb; $n++) { + /* Page */ + $this->_newobj(); + $this->_out('<_out('/Parent 1 0 R'); + if (isset($this->_orientation_changes[$n])) { + $this->_out(sprintf('/MediaBox [0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ']', $hPt, $wPt)); + } + $this->_out('/Resources 2 0 R'); + if (isset($this->_page_links[$n])) { + /* Links */ + $annots = '/Annots ['; + foreach ($this->_page_links[$n] as $pl) { + $rect = sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . '', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); + $annots .= '<_textString($pl[4]) . '>>>>'; + } else { + $l = $this->_links[$pl[4]]; + $height = isset($this->_orientation_changes[$l[0]]) ? $wPt : $hPt; + $annots .= sprintf('/Dest [%d 0 R /XYZ 0 %.2' . FILE_PDF_FLOAT . ' null]>>', 1 + 2 * $l[0], $height - $l[1] * $this->_scale); + } + } + $this->_out($annots . ']'); + } + $this->_out('/Contents ' . ($this->_n + 1) . ' 0 R>>'); + $this->_out('endobj'); + /* Page content */ + $p = ($this->_compress) ? gzcompress($this->_pages[$n]) : $this->_pages[$n]; + $this->_newobj(); + $this->_out('<<' . $filter . '/Length ' . strlen($p) . '>>'); + $this->_putStream($p); + $this->_out('endobj'); + } + /* Pages root */ + $this->_offsets[1] = $this->_buflen + strlen($this->_buffer); + $this->_out('1 0 obj'); + $this->_out('<_out($kids . ']'); + $this->_out('/Count ' . $nb); + $this->_out(sprintf('/MediaBox [0 0 %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ']', $wPt, $hPt)); + $this->_out('>>'); + $this->_out('endobj'); + } + + function _putFonts() + { + $nf = $this->_n; + foreach ($this->_diffs as $diff) { + /* Encodings */ + $this->_newobj(); + $this->_out('<>'); + $this->_out('endobj'); + } + $mqr = get_magic_quotes_runtime(); + set_magic_quotes_runtime(0); + foreach ($this->_font_files as $file => $info) { + /* Font file embedding. */ + $this->_newobj(); + $this->_font_files[$file]['n'] = $this->_n; + $size = filesize($file); + if (!$size) { + return $this->raiseError('Font file not found'); + } + $this->_out('<_out('/Filter /FlateDecode'); + } + $this->_out('/Length1 ' . $info['length1']); + if (isset($info['length2'])) { + $this->_out('/Length2 ' . $info['length2'] . ' /Length3 0'); + } + $this->_out('>>'); + $f = fopen($file, 'rb'); + $this->_putStream(fread($f, $size)); + fclose($f); + $this->_out('endobj'); + } + set_magic_quotes_runtime($mqr); + foreach ($this->_fonts as $k => $font) { + /* Font objects */ + $this->_newobj(); + $this->_fonts[$k]['n'] = $this->_n; + $name = $font['name']; + $this->_out('<_out('/BaseFont /' . $name); + if ($font['type'] == 'core') { + /* Standard font. */ + $this->_out('/Subtype /Type1'); + if ($name != 'Symbol' && $name != 'ZapfDingbats') { + $this->_out('/Encoding /WinAnsiEncoding'); + } + } else { + /* Additional font. */ + $this->_out('/Subtype /' . $font['type']); + $this->_out('/FirstChar 32'); + $this->_out('/LastChar 255'); + $this->_out('/Widths ' . ($this->_n + 1) . ' 0 R'); + $this->_out('/FontDescriptor ' . ($this->_n + 2) . ' 0 R'); + if ($font['enc']) { + if (isset($font['diff'])) { + $this->_out('/Encoding ' . ($nf + $font['diff']) . ' 0 R'); + } else { + $this->_out('/Encoding /WinAnsiEncoding'); + } + } + } + $this->_out('>>'); + $this->_out('endobj'); + if ($font['type'] != 'core') { + /* Widths. */ + $this->_newobj(); + $cw = &$font['cw']; + $s = '['; + for ($i = 32; $i <= 255; $i++) { + $s .= $cw[chr($i)] . ' '; + } + $this->_out($s . ']'); + $this->_out('endobj'); + /* Descriptor. */ + $this->_newobj(); + $s = '< $v) { + $s .= ' /' . $k . ' ' . $v; + } + $file = $font['file']; + if ($file) { + $s .= ' /FontFile' . ($font['type'] == 'Type1' ? '' : '2') . ' ' . $this->_font_files[$file]['n'] . ' 0 R'; + } + $this->_out($s . '>>'); + $this->_out('endobj'); + } + } + } + + function _putImages() + { + $filter = ($this->_compress) ? '/Filter /FlateDecode ' : ''; + foreach ($this->_images as $file => $info) { + $this->_newobj(); + $this->_images[$file]['n'] = $this->_n; + $this->_out('<_out('/Subtype /Image'); + $this->_out('/Width ' . $info['w']); + $this->_out('/Height ' . $info['h']); + if ($info['cs'] == 'Indexed') { + $this->_out('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal'])/3 - 1) . ' ' . ($this->_n + 1) . ' 0 R]'); + } else { + $this->_out('/ColorSpace /' . $info['cs']); + if ($info['cs'] == 'DeviceCMYK') { + $this->_out('/Decode [1 0 1 0 1 0 1 0]'); + } + } + $this->_out('/BitsPerComponent ' . $info['bpc']); + $this->_out('/Filter /' . $info['f']); + if (isset($info['parms'])) { + $this->_out($info['parms']); + } + if (isset($info['trns']) && is_array($info['trns'])) { + $trns = ''; + $i_max = count($info['trns']); + for ($i = 0; $i < $i_max; $i++) { + $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' '; + } + $this->_out('/Mask [' . $trns . ']'); + } + $this->_out('/Length ' . strlen($info['data']) . '>>'); + $this->_putStream($info['data']); + $this->_out('endobj'); + + /* Palette. */ + if ($info['cs'] == 'Indexed') { + $this->_newobj(); + $pal = ($this->_compress) ? gzcompress($info['pal']) : $info['pal']; + $this->_out('<<' . $filter . '/Length ' . strlen($pal) . '>>'); + $this->_putStream($pal); + $this->_out('endobj'); + } + } + } + + function _putResources() + { + $this->_putFonts(); + $this->_putImages(); + /* Resource dictionary */ + $this->_offsets[2] = $this->_buflen + strlen($this->_buffer); + $this->_out('2 0 obj'); + $this->_out('<_out('/Font <<'); + foreach ($this->_fonts as $font) { + $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + } + $this->_out('>>'); + if (count($this->_images)) { + $this->_out('/XObject <<'); + foreach ($this->_images as $image) { + $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + $this->_out('>>'); + } + $this->_out('>>'); + $this->_out('endobj'); + } + + function _putInfo() + { + $this->_out('/Producer ' . $this->_textString('Horde PDF')); + if (!empty($this->_info['title'])) { + $this->_out('/Title ' . $this->_textString($this->_info['title'])); + } + if (!empty($this->_info['subject'])) { + $this->_out('/Subject ' . $this->_textString($this->_info['subject'])); + } + if (!empty($this->_info['author'])) { + $this->_out('/Author ' . $this->_textString($this->_info['author'])); + } + if (!empty($this->keywords)) { + $this->_out('/Keywords ' . $this->_textString($this->keywords)); + } + if (!empty($this->creator)) { + $this->_out('/Creator ' . $this->_textString($this->creator)); + } + $this->_out('/CreationDate ' . $this->_textString('D:' . date('YmdHis'))); + } + + function _putCatalog() + { + $this->_out('/Type /Catalog'); + $this->_out('/Pages 1 0 R'); + if ($this->_zoom_mode == 'fullpage') { + $this->_out('/OpenAction [3 0 R /Fit]'); + } elseif ($this->_zoom_mode == 'fullwidth') { + $this->_out('/OpenAction [3 0 R /FitH null]'); + } elseif ($this->_zoom_mode == 'real') { + $this->_out('/OpenAction [3 0 R /XYZ null null 1]'); + } elseif (!is_string($this->_zoom_mode)) { + $this->_out('/OpenAction [3 0 R /XYZ null null ' . ($this->_zoom_mode / 100) . ']'); + } + if ($this->_layout_mode == 'single') { + $this->_out('/PageLayout /SinglePage'); + } elseif ($this->_layout_mode == 'continuous') { + $this->_out('/PageLayout /OneColumn'); + } elseif ($this->_layout_mode == 'two') { + $this->_out('/PageLayout /TwoColumnLeft'); + } + } + + function _putTrailer() + { + $this->_out('/Size ' . ($this->_n + 1)); + $this->_out('/Root ' . $this->_n . ' 0 R'); + $this->_out('/Info ' . ($this->_n - 1) . ' 0 R'); + } + + function _endDoc() + { + $this->_putPages(); + $this->_putResources(); + /* Info */ + $this->_newobj(); + $this->_out('<<'); + $this->_putInfo(); + $this->_out('>>'); + $this->_out('endobj'); + /* Catalog */ + $this->_newobj(); + $this->_out('<<'); + $this->_putCatalog(); + $this->_out('>>'); + $this->_out('endobj'); + /* Cross-ref */ + $o = $this->_buflen + strlen($this->_buffer); + $this->_out('xref'); + $this->_out('0 ' . ($this->_n + 1)); + $this->_out('0000000000 65535 f '); + for ($i = 1; $i <= $this->_n; $i++) { + $this->_out(sprintf('%010d 00000 n ', $this->_offsets[$i])); + } + /* Trailer */ + $this->_out('trailer'); + $this->_out('<<'); + $this->_putTrailer(); + $this->_out('>>'); + $this->_out('startxref'); + $this->_out($o); + $this->_out('%%EOF'); + $this->_state = 3; + } + + function _beginPage($orientation) + { + $this->_page++; + $this->_pages[$this->_page] = ''; + $this->_state = 2; + $this->x = $this->_left_margin; + $this->y = $this->_top_margin; + $this->_last_height = 0; + /* Page orientation */ + if (!$orientation) { + $orientation = $this->_default_orientation; + } else { + $orientation = strtoupper($orientation[0]); + if ($orientation != $this->_default_orientation) { + $this->_orientation_changes[$this->_page] = true; + } + } + if ($orientation != $this->_current_orientation) { + /* Change orientation */ + if ($orientation == 'P') { + $this->wPt = $this->fwPt; + $this->hPt = $this->fhPt; + $this->w = $this->fw; + $this->h = $this->fh; + } else { + $this->wPt = $this->fhPt; + $this->hPt = $this->fwPt; + $this->w = $this->fh; + $this->h = $this->fw; + } + $this->_page_break_trigger = $this->h - $this->_break_margin; + $this->_current_orientation = $orientation; + } + } + + function _endPage() + { + /* End of page contents */ + $this->_state = 1; + } + + function _newobj() + { + /* Begin a new object */ + $this->_n++; + $this->_offsets[$this->_n] = $this->_buflen + strlen($this->_buffer); + $this->_out($this->_n . ' 0 obj'); + } + + function _doUnderline($x, $y, $text) + { + /* Set the rectangle width according to text width. */ + $width = $this->getStringWidth($text, true); + + /* Set rectangle position and height, using underline position and + * thickness settings scaled by the font size. */ + $y = $y + ($this->_current_font['up'] * $this->_font_size_pt / 1000); + $height = -$this->_current_font['ut'] * $this->_font_size_pt / 1000; + + return sprintf('%.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' %.2' . FILE_PDF_FLOAT . ' re f', $x, $y, $width, $height); + } + + function _parseJPG($file) + { + /* Extract info from a JPEG file. */ + $img = @getimagesize($file); + if (!$img) { + return $this->raiseError(sprintf('Missing or incorrect image file: %s', $file)); + } + if ($img[2] != 2) { + return $this->raiseError(sprintf('Not a JPEG file: %s', $file)); + } + if (!isset($img['channels']) || $img['channels'] == 3) { + $colspace = 'DeviceRGB'; + } elseif ($img['channels'] == 4) { + $colspace = 'DeviceCMYK'; + } else { + $colspace = 'DeviceGray'; + } + $bpc = isset($img['bits']) ? $img['bits'] : 8; + + /* Read whole file. */ + $f = fopen($file, 'rb'); + $data = fread($f, filesize($file)); + fclose($f); + + return array('w' => $img[0], 'h' => $img[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data); + } + + function _parsePNG($file) + { + /* Extract info from a PNG file. */ + $f = fopen($file, 'rb'); + if (!$f) { + return $this->raiseError(sprintf('Unable to open image file: %s', $file)); + } + + /* Check signature. */ + if (fread($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) { + return $this->raiseError(sprintf('Not a PNG file: %s', $file)); + } + + /* Read header chunk. */ + fread($f, 4); + if (fread($f, 4) != 'IHDR') { + return $this->raiseError(sprintf('Incorrect PNG file: %s', $file)); + } + $width = $this->_freadInt($f); + $height = $this->_freadInt($f); + $bpc = ord(fread($f, 1)); + if ($bpc > 8) { + return $this->raiseError(sprintf('16-bit depth not supported: %s', $file)); + } + $ct = ord(fread($f, 1)); + if ($ct == 0) { + $colspace = 'DeviceGray'; + } elseif ($ct == 2) { + $colspace = 'DeviceRGB'; + } elseif ($ct == 3) { + $colspace = 'Indexed'; + } else { + return $this->raiseError(sprintf('Alpha channel not supported: %s', $file)); + } + if (ord(fread($f, 1)) != 0) { + return $this->raiseError(sprintf('Unknown compression method: %s', $file)); + } + if (ord(fread($f, 1)) != 0) { + return $this->raiseError(sprintf('Unknown filter method: %s', $file)); + } + if (ord(fread($f, 1)) != 0) { + return $this->raiseError(sprintf('Interlacing not supported: %s', $file)); + } + fread($f, 4); + $parms = '/DecodeParms <>'; + /* Scan chunks looking for palette, transparency and image data. */ + $pal = ''; + $trns = ''; + $data = ''; + do { + $n = $this->_freadInt($f); + $type = fread($f, 4); + if ($type == 'PLTE') { + /* Read palette */ + $pal = fread($f, $n); + fread($f, 4); + } elseif ($type == 'tRNS') { + /* Read transparency info */ + $t = fread($f, $n); + if ($ct == 0) { + $trns = array(ord(substr($t, 1, 1))); + } elseif ($ct == 2) { + $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))); + } else { + $pos = strpos($t, chr(0)); + if (is_int($pos)) { + $trns = array($pos); + } + } + fread($f, 4); + } elseif ($type == 'IDAT') { + /* Read image data block */ + $data .= fread($f, $n); + fread($f, 4); + } elseif ($type == 'IEND') { + break; + } else { + fread($f, $n + 4); + } + } while ($n); + + if ($colspace == 'Indexed' && empty($pal)) { + return $this->raiseError(sprintf('Missing palette in: %s', $file)); + } + fclose($f); + + return array('w' => $width, 'h' => $height, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'parms' => $parms, 'pal' => $pal, 'trns' => $trns, 'data' => $data); + } + + function _freadInt($f) + { + /* Read a 4-byte integer from file. */ + $i = ord(fread($f, 1)) << 24; + $i += ord(fread($f, 1)) << 16; + $i += ord(fread($f, 1)) << 8; + $i += ord(fread($f, 1)); + return $i; + } + + function _textString($s) + { + /* Format a text string */ + return '(' . $this->_escape($s) . ')'; + } + + function _escape($s) + { + /* Add \ before \, ( and ) */ + return str_replace(array('\\', ')', '('), + array('\\\\', '\\)', '\\('), + $s); + } + + function _putStream($s) + { + $this->_out('stream'); + $this->_out($s); + $this->_out('endstream'); + } + + function _out($s) + { + /* Add a line to the document. */ + if ($this->_state == 2) { + $this->_pages[$this->_page] .= $s . "\n"; + } else { + $this->_buffer .= $s . "\n"; + } + } + +} diff --git a/framework/File_PDF/PDF/fonts/courier.php b/framework/File_PDF/PDF/fonts/courier.php new file mode 100644 index 000000000..7223e61fa --- /dev/null +++ b/framework/File_PDF/PDF/fonts/courier.php @@ -0,0 +1,10 @@ + 278, + chr(1) => 278, + chr(2) => 278, + chr(3) => 278, + chr(4) => 278, + chr(5) => 278, + chr(6) => 278, + chr(7) => 278, + chr(8) => 278, + chr(9) => 278, + chr(10) => 278, + chr(11) => 278, + chr(12) => 278, + chr(13) => 278, + chr(14) => 278, + chr(15) => 278, + chr(16) => 278, + chr(17) => 278, + chr(18) => 278, + chr(19) => 278, + chr(20) => 278, + chr(21) => 278, + + chr(22) => 278, + chr(23) => 278, + chr(24) => 278, + chr(25) => 278, + chr(26) => 278, + chr(27) => 278, + chr(28) => 278, + chr(29) => 278, + chr(30) => 278, + chr(31) => 278, + ' ' => 278, + '!' => 278, + '"' => 355, + '#' => 556, + '$' => 556, + '%' => 889, + '&' => 667, + '\'' => 191, + '(' => 333, + ')' => 333, + '*' => 389, + '+' => 584, + + ',' => 278, + '-' => 333, + '.' => 278, + '/' => 278, + '0' => 556, + '1' => 556, + '2' => 556, + '3' => 556, + '4' => 556, + '5' => 556, + '6' => 556, + '7' => 556, + '8' => 556, + '9' => 556, + ':' => 278, + ';' => 278, + '<' => 584, + '=' => 584, + '>' => 584, + '?' => 556, + '@' => 1015, + 'A' => 667, + + 'B' => 667, + 'C' => 722, + 'D' => 722, + 'E' => 667, + 'F' => 611, + 'G' => 778, + 'H' => 722, + 'I' => 278, + 'J' => 500, + 'K' => 667, + 'L' => 556, + 'M' => 833, + 'N' => 722, + 'O' => 778, + 'P' => 667, + 'Q' => 778, + 'R' => 722, + 'S' => 667, + 'T' => 611, + 'U' => 722, + 'V' => 667, + 'W' => 944, + + 'X' => 667, + 'Y' => 667, + 'Z' => 611, + '[' => 278, + '\\' => 278, + ']' => 278, + '^' => 469, + '_' => 556, + '`' => 333, + 'a' => 556, + 'b' => 556, + 'c' => 500, + 'd' => 556, + 'e' => 556, + 'f' => 278, + 'g' => 556, + 'h' => 556, + 'i' => 222, + 'j' => 222, + 'k' => 500, + 'l' => 222, + 'm' => 833, + + 'n' => 556, + 'o' => 556, + 'p' => 556, + 'q' => 556, + 'r' => 333, + 's' => 500, + 't' => 278, + 'u' => 556, + 'v' => 500, + 'w' => 722, + 'x' => 500, + 'y' => 500, + 'z' => 500, + '{' => 334, + '|' => 260, + '}' => 334, + '~' => 584, + chr(127) => 350, + chr(128) => 556, + chr(129) => 350, + chr(130) => 222, + chr(131) => 556, + + chr(132) => 333, + chr(133) => 1000, + chr(134) => 556, + chr(135) => 556, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 667, + chr(139) => 333, + chr(140) => 1000, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 222, + chr(146) => 222, + chr(147) => 333, + chr(148) => 333, + chr(149) => 350, + chr(150) => 556, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 500, + chr(155) => 333, + chr(156) => 944, + chr(157) => 350, + chr(158) => 500, + chr(159) => 667, + chr(160) => 278, + chr(161) => 333, + chr(162) => 556, + chr(163) => 556, + chr(164) => 556, + chr(165) => 556, + chr(166) => 260, + chr(167) => 556, + chr(168) => 333, + chr(169) => 737, + chr(170) => 370, + chr(171) => 556, + chr(172) => 584, + chr(173) => 333, + chr(174) => 737, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 584, + chr(178) => 333, + chr(179) => 333, + chr(180) => 333, + chr(181) => 556, + chr(182) => 537, + chr(183) => 278, + chr(184) => 333, + chr(185) => 333, + chr(186) => 365, + chr(187) => 556, + chr(188) => 834, + chr(189) => 834, + chr(190) => 834, + chr(191) => 611, + chr(192) => 667, + chr(193) => 667, + chr(194) => 667, + chr(195) => 667, + chr(196) => 667, + chr(197) => 667, + + chr(198) => 1000, + chr(199) => 722, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 278, + chr(205) => 278, + chr(206) => 278, + chr(207) => 278, + chr(208) => 722, + chr(209) => 722, + chr(210) => 778, + chr(211) => 778, + chr(212) => 778, + chr(213) => 778, + chr(214) => 778, + chr(215) => 584, + chr(216) => 778, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 667, + chr(222) => 667, + chr(223) => 611, + chr(224) => 556, + chr(225) => 556, + chr(226) => 556, + chr(227) => 556, + chr(228) => 556, + chr(229) => 556, + chr(230) => 889, + chr(231) => 500, + chr(232) => 556, + chr(233) => 556, + chr(234) => 556, + chr(235) => 556, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 556, + chr(241) => 556, + + chr(242) => 556, + chr(243) => 556, + chr(244) => 556, + chr(245) => 556, + chr(246) => 556, + chr(247) => 584, + chr(248) => 611, + chr(249) => 556, + chr(250) => 556, + chr(251) => 556, + chr(252) => 556, + chr(253) => 500, + chr(254) => 556, + chr(255) => 500); diff --git a/framework/File_PDF/PDF/fonts/helveticab.php b/framework/File_PDF/PDF/fonts/helveticab.php new file mode 100644 index 000000000..8bed545bb --- /dev/null +++ b/framework/File_PDF/PDF/fonts/helveticab.php @@ -0,0 +1,272 @@ + 278, + chr(1) => 278, + chr(2) => 278, + chr(3) => 278, + chr(4) => 278, + chr(5) => 278, + chr(6) => 278, + chr(7) => 278, + chr(8) => 278, + chr(9) => 278, + chr(10) => 278, + chr(11) => 278, + chr(12) => 278, + chr(13) => 278, + chr(14) => 278, + chr(15) => 278, + chr(16) => 278, + chr(17) => 278, + chr(18) => 278, + chr(19) => 278, + chr(20) => 278, + chr(21) => 278, + + chr(22) => 278, + chr(23) => 278, + chr(24) => 278, + chr(25) => 278, + chr(26) => 278, + chr(27) => 278, + chr(28) => 278, + chr(29) => 278, + chr(30) => 278, + chr(31) => 278, + ' ' => 278, + '!' => 333, + '"' => 474, + '#' => 556, + '$' => 556, + '%' => 889, + '&' => 722, + '\'' => 238, + '(' => 333, + ')' => 333, + '*' => 389, + '+' => 584, + + ',' => 278, + '-' => 333, + '.' => 278, + '/' => 278, + '0' => 556, + '1' => 556, + '2' => 556, + '3' => 556, + '4' => 556, + '5' => 556, + '6' => 556, + '7' => 556, + '8' => 556, + '9' => 556, + ':' => 333, + ';' => 333, + '<' => 584, + '=' => 584, + '>' => 584, + '?' => 611, + '@' => 975, + 'A' => 722, + + 'B' => 722, + 'C' => 722, + 'D' => 722, + 'E' => 667, + 'F' => 611, + 'G' => 778, + 'H' => 722, + 'I' => 278, + 'J' => 556, + 'K' => 722, + 'L' => 611, + 'M' => 833, + 'N' => 722, + 'O' => 778, + 'P' => 667, + 'Q' => 778, + 'R' => 722, + 'S' => 667, + 'T' => 611, + 'U' => 722, + 'V' => 667, + 'W' => 944, + + 'X' => 667, + 'Y' => 667, + 'Z' => 611, + '[' => 333, + '\\' => 278, + ']' => 333, + '^' => 584, + '_' => 556, + '`' => 333, + 'a' => 556, + 'b' => 611, + 'c' => 556, + 'd' => 611, + 'e' => 556, + 'f' => 333, + 'g' => 611, + 'h' => 611, + 'i' => 278, + 'j' => 278, + 'k' => 556, + 'l' => 278, + 'm' => 889, + + 'n' => 611, + 'o' => 611, + 'p' => 611, + 'q' => 611, + 'r' => 389, + 's' => 556, + 't' => 333, + 'u' => 611, + 'v' => 556, + 'w' => 778, + 'x' => 556, + 'y' => 556, + 'z' => 500, + '{' => 389, + '|' => 280, + '}' => 389, + '~' => 584, + chr(127) => 350, + chr(128) => 556, + chr(129) => 350, + chr(130) => 278, + chr(131) => 556, + + chr(132) => 500, + chr(133) => 1000, + chr(134) => 556, + chr(135) => 556, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 667, + chr(139) => 333, + chr(140) => 1000, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 278, + chr(146) => 278, + chr(147) => 500, + chr(148) => 500, + chr(149) => 350, + chr(150) => 556, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 556, + chr(155) => 333, + chr(156) => 944, + chr(157) => 350, + chr(158) => 500, + chr(159) => 667, + chr(160) => 278, + chr(161) => 333, + chr(162) => 556, + chr(163) => 556, + chr(164) => 556, + chr(165) => 556, + chr(166) => 280, + chr(167) => 556, + chr(168) => 333, + chr(169) => 737, + chr(170) => 370, + chr(171) => 556, + chr(172) => 584, + chr(173) => 333, + chr(174) => 737, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 584, + chr(178) => 333, + chr(179) => 333, + chr(180) => 333, + chr(181) => 611, + chr(182) => 556, + chr(183) => 278, + chr(184) => 333, + chr(185) => 333, + chr(186) => 365, + chr(187) => 556, + chr(188) => 834, + chr(189) => 834, + chr(190) => 834, + chr(191) => 611, + chr(192) => 722, + chr(193) => 722, + chr(194) => 722, + chr(195) => 722, + chr(196) => 722, + chr(197) => 722, + + chr(198) => 1000, + chr(199) => 722, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 278, + chr(205) => 278, + chr(206) => 278, + chr(207) => 278, + chr(208) => 722, + chr(209) => 722, + chr(210) => 778, + chr(211) => 778, + chr(212) => 778, + chr(213) => 778, + chr(214) => 778, + chr(215) => 584, + chr(216) => 778, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 667, + chr(222) => 667, + chr(223) => 611, + chr(224) => 556, + chr(225) => 556, + chr(226) => 556, + chr(227) => 556, + chr(228) => 556, + chr(229) => 556, + chr(230) => 889, + chr(231) => 556, + chr(232) => 556, + chr(233) => 556, + chr(234) => 556, + chr(235) => 556, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 611, + chr(241) => 611, + + chr(242) => 611, + chr(243) => 611, + chr(244) => 611, + chr(245) => 611, + chr(246) => 611, + chr(247) => 584, + chr(248) => 611, + chr(249) => 611, + chr(250) => 611, + chr(251) => 611, + chr(252) => 611, + chr(253) => 556, + chr(254) => 611, + chr(255) => 556); diff --git a/framework/File_PDF/PDF/fonts/helveticabi.php b/framework/File_PDF/PDF/fonts/helveticabi.php new file mode 100644 index 000000000..495d08740 --- /dev/null +++ b/framework/File_PDF/PDF/fonts/helveticabi.php @@ -0,0 +1,272 @@ + 278, + chr(1) => 278, + chr(2) => 278, + chr(3) => 278, + chr(4) => 278, + chr(5) => 278, + chr(6) => 278, + chr(7) => 278, + chr(8) => 278, + chr(9) => 278, + chr(10) => 278, + chr(11) => 278, + chr(12) => 278, + chr(13) => 278, + chr(14) => 278, + chr(15) => 278, + chr(16) => 278, + chr(17) => 278, + chr(18) => 278, + chr(19) => 278, + chr(20) => 278, + chr(21) => 278, + + chr(22) => 278, + chr(23) => 278, + chr(24) => 278, + chr(25) => 278, + chr(26) => 278, + chr(27) => 278, + chr(28) => 278, + chr(29) => 278, + chr(30) => 278, + chr(31) => 278, + ' ' => 278, + '!' => 333, + '"' => 474, + '#' => 556, + '$' => 556, + '%' => 889, + '&' => 722, + '\'' => 238, + '(' => 333, + ')' => 333, + '*' => 389, + '+' => 584, + + ',' => 278, + '-' => 333, + '.' => 278, + '/' => 278, + '0' => 556, + '1' => 556, + '2' => 556, + '3' => 556, + '4' => 556, + '5' => 556, + '6' => 556, + '7' => 556, + '8' => 556, + '9' => 556, + ':' => 333, + ';' => 333, + '<' => 584, + '=' => 584, + '>' => 584, + '?' => 611, + '@' => 975, + 'A' => 722, + + 'B' => 722, + 'C' => 722, + 'D' => 722, + 'E' => 667, + 'F' => 611, + 'G' => 778, + 'H' => 722, + 'I' => 278, + 'J' => 556, + 'K' => 722, + 'L' => 611, + 'M' => 833, + 'N' => 722, + 'O' => 778, + 'P' => 667, + 'Q' => 778, + 'R' => 722, + 'S' => 667, + 'T' => 611, + 'U' => 722, + 'V' => 667, + 'W' => 944, + + 'X' => 667, + 'Y' => 667, + 'Z' => 611, + '[' => 333, + '\\' => 278, + ']' => 333, + '^' => 584, + '_' => 556, + '`' => 333, + 'a' => 556, + 'b' => 611, + 'c' => 556, + 'd' => 611, + 'e' => 556, + 'f' => 333, + 'g' => 611, + 'h' => 611, + 'i' => 278, + 'j' => 278, + 'k' => 556, + 'l' => 278, + 'm' => 889, + + 'n' => 611, + 'o' => 611, + 'p' => 611, + 'q' => 611, + 'r' => 389, + 's' => 556, + 't' => 333, + 'u' => 611, + 'v' => 556, + 'w' => 778, + 'x' => 556, + 'y' => 556, + 'z' => 500, + '{' => 389, + '|' => 280, + '}' => 389, + '~' => 584, + chr(127) => 350, + chr(128) => 556, + chr(129) => 350, + chr(130) => 278, + chr(131) => 556, + + chr(132) => 500, + chr(133) => 1000, + chr(134) => 556, + chr(135) => 556, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 667, + chr(139) => 333, + chr(140) => 1000, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 278, + chr(146) => 278, + chr(147) => 500, + chr(148) => 500, + chr(149) => 350, + chr(150) => 556, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 556, + chr(155) => 333, + chr(156) => 944, + chr(157) => 350, + chr(158) => 500, + chr(159) => 667, + chr(160) => 278, + chr(161) => 333, + chr(162) => 556, + chr(163) => 556, + chr(164) => 556, + chr(165) => 556, + chr(166) => 280, + chr(167) => 556, + chr(168) => 333, + chr(169) => 737, + chr(170) => 370, + chr(171) => 556, + chr(172) => 584, + chr(173) => 333, + chr(174) => 737, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 584, + chr(178) => 333, + chr(179) => 333, + chr(180) => 333, + chr(181) => 611, + chr(182) => 556, + chr(183) => 278, + chr(184) => 333, + chr(185) => 333, + chr(186) => 365, + chr(187) => 556, + chr(188) => 834, + chr(189) => 834, + chr(190) => 834, + chr(191) => 611, + chr(192) => 722, + chr(193) => 722, + chr(194) => 722, + chr(195) => 722, + chr(196) => 722, + chr(197) => 722, + + chr(198) => 1000, + chr(199) => 722, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 278, + chr(205) => 278, + chr(206) => 278, + chr(207) => 278, + chr(208) => 722, + chr(209) => 722, + chr(210) => 778, + chr(211) => 778, + chr(212) => 778, + chr(213) => 778, + chr(214) => 778, + chr(215) => 584, + chr(216) => 778, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 667, + chr(222) => 667, + chr(223) => 611, + chr(224) => 556, + chr(225) => 556, + chr(226) => 556, + chr(227) => 556, + chr(228) => 556, + chr(229) => 556, + chr(230) => 889, + chr(231) => 556, + chr(232) => 556, + chr(233) => 556, + chr(234) => 556, + chr(235) => 556, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 611, + chr(241) => 611, + + chr(242) => 611, + chr(243) => 611, + chr(244) => 611, + chr(245) => 611, + chr(246) => 611, + chr(247) => 584, + chr(248) => 611, + chr(249) => 611, + chr(250) => 611, + chr(251) => 611, + chr(252) => 611, + chr(253) => 556, + chr(254) => 611, + chr(255) => 556); diff --git a/framework/File_PDF/PDF/fonts/helveticai.php b/framework/File_PDF/PDF/fonts/helveticai.php new file mode 100644 index 000000000..681c8e83f --- /dev/null +++ b/framework/File_PDF/PDF/fonts/helveticai.php @@ -0,0 +1,272 @@ + 278, + chr(1) => 278, + chr(2) => 278, + chr(3) => 278, + chr(4) => 278, + chr(5) => 278, + chr(6) => 278, + chr(7) => 278, + chr(8) => 278, + chr(9) => 278, + chr(10) => 278, + chr(11) => 278, + chr(12) => 278, + chr(13) => 278, + chr(14) => 278, + chr(15) => 278, + chr(16) => 278, + chr(17) => 278, + chr(18) => 278, + chr(19) => 278, + chr(20) => 278, + chr(21) => 278, + + chr(22) => 278, + chr(23) => 278, + chr(24) => 278, + chr(25) => 278, + chr(26) => 278, + chr(27) => 278, + chr(28) => 278, + chr(29) => 278, + chr(30) => 278, + chr(31) => 278, + ' ' => 278, + '!' => 278, + '"' => 355, + '#' => 556, + '$' => 556, + '%' => 889, + '&' => 667, + '\'' => 191, + '(' => 333, + ')' => 333, + '*' => 389, + '+' => 584, + + ',' => 278, + '-' => 333, + '.' => 278, + '/' => 278, + '0' => 556, + '1' => 556, + '2' => 556, + '3' => 556, + '4' => 556, + '5' => 556, + '6' => 556, + '7' => 556, + '8' => 556, + '9' => 556, + ':' => 278, + ';' => 278, + '<' => 584, + '=' => 584, + '>' => 584, + '?' => 556, + '@' => 1015, + 'A' => 667, + + 'B' => 667, + 'C' => 722, + 'D' => 722, + 'E' => 667, + 'F' => 611, + 'G' => 778, + 'H' => 722, + 'I' => 278, + 'J' => 500, + 'K' => 667, + 'L' => 556, + 'M' => 833, + 'N' => 722, + 'O' => 778, + 'P' => 667, + 'Q' => 778, + 'R' => 722, + 'S' => 667, + 'T' => 611, + 'U' => 722, + 'V' => 667, + 'W' => 944, + + 'X' => 667, + 'Y' => 667, + 'Z' => 611, + '[' => 278, + '\\' => 278, + ']' => 278, + '^' => 469, + '_' => 556, + '`' => 333, + 'a' => 556, + 'b' => 556, + 'c' => 500, + 'd' => 556, + 'e' => 556, + 'f' => 278, + 'g' => 556, + 'h' => 556, + 'i' => 222, + 'j' => 222, + 'k' => 500, + 'l' => 222, + 'm' => 833, + + 'n' => 556, + 'o' => 556, + 'p' => 556, + 'q' => 556, + 'r' => 333, + 's' => 500, + 't' => 278, + 'u' => 556, + 'v' => 500, + 'w' => 722, + 'x' => 500, + 'y' => 500, + 'z' => 500, + '{' => 334, + '|' => 260, + '}' => 334, + '~' => 584, + chr(127) => 350, + chr(128) => 556, + chr(129) => 350, + chr(130) => 222, + chr(131) => 556, + + chr(132) => 333, + chr(133) => 1000, + chr(134) => 556, + chr(135) => 556, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 667, + chr(139) => 333, + chr(140) => 1000, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 222, + chr(146) => 222, + chr(147) => 333, + chr(148) => 333, + chr(149) => 350, + chr(150) => 556, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 500, + chr(155) => 333, + chr(156) => 944, + chr(157) => 350, + chr(158) => 500, + chr(159) => 667, + chr(160) => 278, + chr(161) => 333, + chr(162) => 556, + chr(163) => 556, + chr(164) => 556, + chr(165) => 556, + chr(166) => 260, + chr(167) => 556, + chr(168) => 333, + chr(169) => 737, + chr(170) => 370, + chr(171) => 556, + chr(172) => 584, + chr(173) => 333, + chr(174) => 737, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 584, + chr(178) => 333, + chr(179) => 333, + chr(180) => 333, + chr(181) => 556, + chr(182) => 537, + chr(183) => 278, + chr(184) => 333, + chr(185) => 333, + chr(186) => 365, + chr(187) => 556, + chr(188) => 834, + chr(189) => 834, + chr(190) => 834, + chr(191) => 611, + chr(192) => 667, + chr(193) => 667, + chr(194) => 667, + chr(195) => 667, + chr(196) => 667, + chr(197) => 667, + + chr(198) => 1000, + chr(199) => 722, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 278, + chr(205) => 278, + chr(206) => 278, + chr(207) => 278, + chr(208) => 722, + chr(209) => 722, + chr(210) => 778, + chr(211) => 778, + chr(212) => 778, + chr(213) => 778, + chr(214) => 778, + chr(215) => 584, + chr(216) => 778, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 667, + chr(222) => 667, + chr(223) => 611, + chr(224) => 556, + chr(225) => 556, + chr(226) => 556, + chr(227) => 556, + chr(228) => 556, + chr(229) => 556, + chr(230) => 889, + chr(231) => 500, + chr(232) => 556, + chr(233) => 556, + chr(234) => 556, + chr(235) => 556, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 556, + chr(241) => 556, + + chr(242) => 556, + chr(243) => 556, + chr(244) => 556, + chr(245) => 556, + chr(246) => 556, + chr(247) => 584, + chr(248) => 611, + chr(249) => 556, + chr(250) => 556, + chr(251) => 556, + chr(252) => 556, + chr(253) => 500, + chr(254) => 556, + chr(255) => 500); diff --git a/framework/File_PDF/PDF/fonts/symbol.php b/framework/File_PDF/PDF/fonts/symbol.php new file mode 100644 index 000000000..dd1f660df --- /dev/null +++ b/framework/File_PDF/PDF/fonts/symbol.php @@ -0,0 +1,272 @@ + 250, + chr(1) => 250, + chr(2) => 250, + chr(3) => 250, + chr(4) => 250, + chr(5) => 250, + chr(6) => 250, + chr(7) => 250, + chr(8) => 250, + chr(9) => 250, + chr(10) => 250, + chr(11) => 250, + chr(12) => 250, + chr(13) => 250, + chr(14) => 250, + chr(15) => 250, + chr(16) => 250, + chr(17) => 250, + chr(18) => 250, + chr(19) => 250, + chr(20) => 250, + chr(21) => 250, + + chr(22) => 250, + chr(23) => 250, + chr(24) => 250, + chr(25) => 250, + chr(26) => 250, + chr(27) => 250, + chr(28) => 250, + chr(29) => 250, + chr(30) => 250, + chr(31) => 250, + ' ' => 250, + '!' => 333, + '"' => 713, + '#' => 500, + '$' => 549, + '%' => 833, + '&' => 778, + '\'' => 439, + '(' => 333, + ')' => 333, + '*' => 500, + '+' => 549, + + ',' => 250, + '-' => 549, + '.' => 250, + '/' => 278, + '0' => 500, + '1' => 500, + '2' => 500, + '3' => 500, + '4' => 500, + '5' => 500, + '6' => 500, + '7' => 500, + '8' => 500, + '9' => 500, + ':' => 278, + ';' => 278, + '<' => 549, + '=' => 549, + '>' => 549, + '?' => 444, + '@' => 549, + 'A' => 722, + + 'B' => 667, + 'C' => 722, + 'D' => 612, + 'E' => 611, + 'F' => 763, + 'G' => 603, + 'H' => 722, + 'I' => 333, + 'J' => 631, + 'K' => 722, + 'L' => 686, + 'M' => 889, + 'N' => 722, + 'O' => 722, + 'P' => 768, + 'Q' => 741, + 'R' => 556, + 'S' => 592, + 'T' => 611, + 'U' => 690, + 'V' => 439, + 'W' => 768, + + 'X' => 645, + 'Y' => 795, + 'Z' => 611, + '[' => 333, + '\\' => 863, + ']' => 333, + '^' => 658, + '_' => 500, + '`' => 500, + 'a' => 631, + 'b' => 549, + 'c' => 549, + 'd' => 494, + 'e' => 439, + 'f' => 521, + 'g' => 411, + 'h' => 603, + 'i' => 329, + 'j' => 603, + 'k' => 549, + 'l' => 549, + 'm' => 576, + + 'n' => 521, + 'o' => 549, + 'p' => 549, + 'q' => 521, + 'r' => 549, + 's' => 603, + 't' => 439, + 'u' => 576, + 'v' => 713, + 'w' => 686, + 'x' => 493, + 'y' => 686, + 'z' => 494, + '{' => 480, + '|' => 200, + '}' => 480, + '~' => 549, + chr(127) => 0, + chr(128) => 0, + chr(129) => 0, + chr(130) => 0, + chr(131) => 0, + + chr(132) => 0, + chr(133) => 0, + chr(134) => 0, + chr(135) => 0, + chr(136) => 0, + chr(137) => 0, + chr(138) => 0, + chr(139) => 0, + chr(140) => 0, + chr(141) => 0, + chr(142) => 0, + chr(143) => 0, + chr(144) => 0, + chr(145) => 0, + chr(146) => 0, + chr(147) => 0, + chr(148) => 0, + chr(149) => 0, + chr(150) => 0, + chr(151) => 0, + chr(152) => 0, + chr(153) => 0, + + chr(154) => 0, + chr(155) => 0, + chr(156) => 0, + chr(157) => 0, + chr(158) => 0, + chr(159) => 0, + chr(160) => 750, + chr(161) => 620, + chr(162) => 247, + chr(163) => 549, + chr(164) => 167, + chr(165) => 713, + chr(166) => 500, + chr(167) => 753, + chr(168) => 753, + chr(169) => 753, + chr(170) => 753, + chr(171) => 1042, + chr(172) => 987, + chr(173) => 603, + chr(174) => 987, + chr(175) => 603, + + chr(176) => 400, + chr(177) => 549, + chr(178) => 411, + chr(179) => 549, + chr(180) => 549, + chr(181) => 713, + chr(182) => 494, + chr(183) => 460, + chr(184) => 549, + chr(185) => 549, + chr(186) => 549, + chr(187) => 549, + chr(188) => 1000, + chr(189) => 603, + chr(190) => 1000, + chr(191) => 658, + chr(192) => 823, + chr(193) => 686, + chr(194) => 795, + chr(195) => 987, + chr(196) => 768, + chr(197) => 768, + + chr(198) => 823, + chr(199) => 768, + chr(200) => 768, + chr(201) => 713, + chr(202) => 713, + chr(203) => 713, + chr(204) => 713, + chr(205) => 713, + chr(206) => 713, + chr(207) => 713, + chr(208) => 768, + chr(209) => 713, + chr(210) => 790, + chr(211) => 790, + chr(212) => 890, + chr(213) => 823, + chr(214) => 549, + chr(215) => 250, + chr(216) => 713, + chr(217) => 603, + chr(218) => 603, + chr(219) => 1042, + + chr(220) => 987, + chr(221) => 603, + chr(222) => 987, + chr(223) => 603, + chr(224) => 494, + chr(225) => 329, + chr(226) => 790, + chr(227) => 790, + chr(228) => 786, + chr(229) => 713, + chr(230) => 384, + chr(231) => 384, + chr(232) => 384, + chr(233) => 384, + chr(234) => 384, + chr(235) => 384, + chr(236) => 494, + chr(237) => 494, + chr(238) => 494, + chr(239) => 494, + chr(240) => 0, + chr(241) => 329, + + chr(242) => 274, + chr(243) => 686, + chr(244) => 686, + chr(245) => 686, + chr(246) => 384, + chr(247) => 384, + chr(248) => 384, + chr(249) => 384, + chr(250) => 384, + chr(251) => 384, + chr(252) => 494, + chr(253) => 494, + chr(254) => 494, + chr(255) => 0); diff --git a/framework/File_PDF/PDF/fonts/times.php b/framework/File_PDF/PDF/fonts/times.php new file mode 100644 index 000000000..28507c633 --- /dev/null +++ b/framework/File_PDF/PDF/fonts/times.php @@ -0,0 +1,272 @@ + 250, + chr(1) => 250, + chr(2) => 250, + chr(3) => 250, + chr(4) => 250, + chr(5) => 250, + chr(6) => 250, + chr(7) => 250, + chr(8) => 250, + chr(9) => 250, + chr(10) => 250, + chr(11) => 250, + chr(12) => 250, + chr(13) => 250, + chr(14) => 250, + chr(15) => 250, + chr(16) => 250, + chr(17) => 250, + chr(18) => 250, + chr(19) => 250, + chr(20) => 250, + chr(21) => 250, + + chr(22) => 250, + chr(23) => 250, + chr(24) => 250, + chr(25) => 250, + chr(26) => 250, + chr(27) => 250, + chr(28) => 250, + chr(29) => 250, + chr(30) => 250, + chr(31) => 250, + ' ' => 250, + '!' => 333, + '"' => 408, + '#' => 500, + '$' => 500, + '%' => 833, + '&' => 778, + '\'' => 180, + '(' => 333, + ')' => 333, + '*' => 500, + '+' => 564, + + ',' => 250, + '-' => 333, + '.' => 250, + '/' => 278, + '0' => 500, + '1' => 500, + '2' => 500, + '3' => 500, + '4' => 500, + '5' => 500, + '6' => 500, + '7' => 500, + '8' => 500, + '9' => 500, + ':' => 278, + ';' => 278, + '<' => 564, + '=' => 564, + '>' => 564, + '?' => 444, + '@' => 921, + 'A' => 722, + + 'B' => 667, + 'C' => 667, + 'D' => 722, + 'E' => 611, + 'F' => 556, + 'G' => 722, + 'H' => 722, + 'I' => 333, + 'J' => 389, + 'K' => 722, + 'L' => 611, + 'M' => 889, + 'N' => 722, + 'O' => 722, + 'P' => 556, + 'Q' => 722, + 'R' => 667, + 'S' => 556, + 'T' => 611, + 'U' => 722, + 'V' => 722, + 'W' => 944, + + 'X' => 722, + 'Y' => 722, + 'Z' => 611, + '[' => 333, + '\\' => 278, + ']' => 333, + '^' => 469, + '_' => 500, + '`' => 333, + 'a' => 444, + 'b' => 500, + 'c' => 444, + 'd' => 500, + 'e' => 444, + 'f' => 333, + 'g' => 500, + 'h' => 500, + 'i' => 278, + 'j' => 278, + 'k' => 500, + 'l' => 278, + 'm' => 778, + + 'n' => 500, + 'o' => 500, + 'p' => 500, + 'q' => 500, + 'r' => 333, + 's' => 389, + 't' => 278, + 'u' => 500, + 'v' => 500, + 'w' => 722, + 'x' => 500, + 'y' => 500, + 'z' => 444, + '{' => 480, + '|' => 200, + '}' => 480, + '~' => 541, + chr(127) => 350, + chr(128) => 500, + chr(129) => 350, + chr(130) => 333, + chr(131) => 500, + + chr(132) => 444, + chr(133) => 1000, + chr(134) => 500, + chr(135) => 500, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 556, + chr(139) => 333, + chr(140) => 889, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 333, + chr(146) => 333, + chr(147) => 444, + chr(148) => 444, + chr(149) => 350, + chr(150) => 500, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 980, + + chr(154) => 389, + chr(155) => 333, + chr(156) => 722, + chr(157) => 350, + chr(158) => 444, + chr(159) => 722, + chr(160) => 250, + chr(161) => 333, + chr(162) => 500, + chr(163) => 500, + chr(164) => 500, + chr(165) => 500, + chr(166) => 200, + chr(167) => 500, + chr(168) => 333, + chr(169) => 760, + chr(170) => 276, + chr(171) => 500, + chr(172) => 564, + chr(173) => 333, + chr(174) => 760, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 564, + chr(178) => 300, + chr(179) => 300, + chr(180) => 333, + chr(181) => 500, + chr(182) => 453, + chr(183) => 250, + chr(184) => 333, + chr(185) => 300, + chr(186) => 310, + chr(187) => 500, + chr(188) => 750, + chr(189) => 750, + chr(190) => 750, + chr(191) => 444, + chr(192) => 722, + chr(193) => 722, + chr(194) => 722, + chr(195) => 722, + chr(196) => 722, + chr(197) => 722, + + chr(198) => 889, + chr(199) => 667, + chr(200) => 611, + chr(201) => 611, + chr(202) => 611, + chr(203) => 611, + chr(204) => 333, + chr(205) => 333, + chr(206) => 333, + chr(207) => 333, + chr(208) => 722, + chr(209) => 722, + chr(210) => 722, + chr(211) => 722, + chr(212) => 722, + chr(213) => 722, + chr(214) => 722, + chr(215) => 564, + chr(216) => 722, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 722, + chr(222) => 556, + chr(223) => 500, + chr(224) => 444, + chr(225) => 444, + chr(226) => 444, + chr(227) => 444, + chr(228) => 444, + chr(229) => 444, + chr(230) => 667, + chr(231) => 444, + chr(232) => 444, + chr(233) => 444, + chr(234) => 444, + chr(235) => 444, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 500, + chr(241) => 500, + + chr(242) => 500, + chr(243) => 500, + chr(244) => 500, + chr(245) => 500, + chr(246) => 500, + chr(247) => 564, + chr(248) => 500, + chr(249) => 500, + chr(250) => 500, + chr(251) => 500, + chr(252) => 500, + chr(253) => 500, + chr(254) => 500, + chr(255) => 500); diff --git a/framework/File_PDF/PDF/fonts/timesb.php b/framework/File_PDF/PDF/fonts/timesb.php new file mode 100644 index 000000000..422923f10 --- /dev/null +++ b/framework/File_PDF/PDF/fonts/timesb.php @@ -0,0 +1,272 @@ + 250, + chr(1) => 250, + chr(2) => 250, + chr(3) => 250, + chr(4) => 250, + chr(5) => 250, + chr(6) => 250, + chr(7) => 250, + chr(8) => 250, + chr(9) => 250, + chr(10) => 250, + chr(11) => 250, + chr(12) => 250, + chr(13) => 250, + chr(14) => 250, + chr(15) => 250, + chr(16) => 250, + chr(17) => 250, + chr(18) => 250, + chr(19) => 250, + chr(20) => 250, + chr(21) => 250, + + chr(22) => 250, + chr(23) => 250, + chr(24) => 250, + chr(25) => 250, + chr(26) => 250, + chr(27) => 250, + chr(28) => 250, + chr(29) => 250, + chr(30) => 250, + chr(31) => 250, + ' ' => 250, + '!' => 333, + '"' => 555, + '#' => 500, + '$' => 500, + '%' => 1000, + '&' => 833, + '\'' => 278, + '(' => 333, + ')' => 333, + '*' => 500, + '+' => 570, + + ',' => 250, + '-' => 333, + '.' => 250, + '/' => 278, + '0' => 500, + '1' => 500, + '2' => 500, + '3' => 500, + '4' => 500, + '5' => 500, + '6' => 500, + '7' => 500, + '8' => 500, + '9' => 500, + ':' => 333, + ';' => 333, + '<' => 570, + '=' => 570, + '>' => 570, + '?' => 500, + '@' => 930, + 'A' => 722, + + 'B' => 667, + 'C' => 722, + 'D' => 722, + 'E' => 667, + 'F' => 611, + 'G' => 778, + 'H' => 778, + 'I' => 389, + 'J' => 500, + 'K' => 778, + 'L' => 667, + 'M' => 944, + 'N' => 722, + 'O' => 778, + 'P' => 611, + 'Q' => 778, + 'R' => 722, + 'S' => 556, + 'T' => 667, + 'U' => 722, + 'V' => 722, + 'W' => 1000, + + 'X' => 722, + 'Y' => 722, + 'Z' => 667, + '[' => 333, + '\\' => 278, + ']' => 333, + '^' => 581, + '_' => 500, + '`' => 333, + 'a' => 500, + 'b' => 556, + 'c' => 444, + 'd' => 556, + 'e' => 444, + 'f' => 333, + 'g' => 500, + 'h' => 556, + 'i' => 278, + 'j' => 333, + 'k' => 556, + 'l' => 278, + 'm' => 833, + + 'n' => 556, + 'o' => 500, + 'p' => 556, + 'q' => 556, + 'r' => 444, + 's' => 389, + 't' => 333, + 'u' => 556, + 'v' => 500, + 'w' => 722, + 'x' => 500, + 'y' => 500, + 'z' => 444, + '{' => 394, + '|' => 220, + '}' => 394, + '~' => 520, + chr(127) => 350, + chr(128) => 500, + chr(129) => 350, + chr(130) => 333, + chr(131) => 500, + + chr(132) => 500, + chr(133) => 1000, + chr(134) => 500, + chr(135) => 500, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 556, + chr(139) => 333, + chr(140) => 1000, + chr(141) => 350, + chr(142) => 667, + chr(143) => 350, + chr(144) => 350, + chr(145) => 333, + chr(146) => 333, + chr(147) => 500, + chr(148) => 500, + chr(149) => 350, + chr(150) => 500, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 389, + chr(155) => 333, + chr(156) => 722, + chr(157) => 350, + chr(158) => 444, + chr(159) => 722, + chr(160) => 250, + chr(161) => 333, + chr(162) => 500, + chr(163) => 500, + chr(164) => 500, + chr(165) => 500, + chr(166) => 220, + chr(167) => 500, + chr(168) => 333, + chr(169) => 747, + chr(170) => 300, + chr(171) => 500, + chr(172) => 570, + chr(173) => 333, + chr(174) => 747, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 570, + chr(178) => 300, + chr(179) => 300, + chr(180) => 333, + chr(181) => 556, + chr(182) => 540, + chr(183) => 250, + chr(184) => 333, + chr(185) => 300, + chr(186) => 330, + chr(187) => 500, + chr(188) => 750, + chr(189) => 750, + chr(190) => 750, + chr(191) => 500, + chr(192) => 722, + chr(193) => 722, + chr(194) => 722, + chr(195) => 722, + chr(196) => 722, + chr(197) => 722, + + chr(198) => 1000, + chr(199) => 722, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 389, + chr(205) => 389, + chr(206) => 389, + chr(207) => 389, + chr(208) => 722, + chr(209) => 722, + chr(210) => 778, + chr(211) => 778, + chr(212) => 778, + chr(213) => 778, + chr(214) => 778, + chr(215) => 570, + chr(216) => 778, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 722, + chr(222) => 611, + chr(223) => 556, + chr(224) => 500, + chr(225) => 500, + chr(226) => 500, + chr(227) => 500, + chr(228) => 500, + chr(229) => 500, + chr(230) => 722, + chr(231) => 444, + chr(232) => 444, + chr(233) => 444, + chr(234) => 444, + chr(235) => 444, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 500, + chr(241) => 556, + + chr(242) => 500, + chr(243) => 500, + chr(244) => 500, + chr(245) => 500, + chr(246) => 500, + chr(247) => 570, + chr(248) => 500, + chr(249) => 556, + chr(250) => 556, + chr(251) => 556, + chr(252) => 556, + chr(253) => 500, + chr(254) => 556, + chr(255) => 500); diff --git a/framework/File_PDF/PDF/fonts/timesbi.php b/framework/File_PDF/PDF/fonts/timesbi.php new file mode 100644 index 000000000..4d8614f88 --- /dev/null +++ b/framework/File_PDF/PDF/fonts/timesbi.php @@ -0,0 +1,272 @@ + 250, + chr(1) => 250, + chr(2) => 250, + chr(3) => 250, + chr(4) => 250, + chr(5) => 250, + chr(6) => 250, + chr(7) => 250, + chr(8) => 250, + chr(9) => 250, + chr(10) => 250, + chr(11) => 250, + chr(12) => 250, + chr(13) => 250, + chr(14) => 250, + chr(15) => 250, + chr(16) => 250, + chr(17) => 250, + chr(18) => 250, + chr(19) => 250, + chr(20) => 250, + chr(21) => 250, + + chr(22) => 250, + chr(23) => 250, + chr(24) => 250, + chr(25) => 250, + chr(26) => 250, + chr(27) => 250, + chr(28) => 250, + chr(29) => 250, + chr(30) => 250, + chr(31) => 250, + ' ' => 250, + '!' => 389, + '"' => 555, + '#' => 500, + '$' => 500, + '%' => 833, + '&' => 778, + '\'' => 278, + '(' => 333, + ')' => 333, + '*' => 500, + '+' => 570, + + ',' => 250, + '-' => 333, + '.' => 250, + '/' => 278, + '0' => 500, + '1' => 500, + '2' => 500, + '3' => 500, + '4' => 500, + '5' => 500, + '6' => 500, + '7' => 500, + '8' => 500, + '9' => 500, + ':' => 333, + ';' => 333, + '<' => 570, + '=' => 570, + '>' => 570, + '?' => 500, + '@' => 832, + 'A' => 667, + + 'B' => 667, + 'C' => 667, + 'D' => 722, + 'E' => 667, + 'F' => 667, + 'G' => 722, + 'H' => 778, + 'I' => 389, + 'J' => 500, + 'K' => 667, + 'L' => 611, + 'M' => 889, + 'N' => 722, + 'O' => 722, + 'P' => 611, + 'Q' => 722, + 'R' => 667, + 'S' => 556, + 'T' => 611, + 'U' => 722, + 'V' => 667, + 'W' => 889, + + 'X' => 667, + 'Y' => 611, + 'Z' => 611, + '[' => 333, + '\\' => 278, + ']' => 333, + '^' => 570, + '_' => 500, + '`' => 333, + 'a' => 500, + 'b' => 500, + 'c' => 444, + 'd' => 500, + 'e' => 444, + 'f' => 333, + 'g' => 500, + 'h' => 556, + 'i' => 278, + 'j' => 278, + 'k' => 500, + 'l' => 278, + 'm' => 778, + + 'n' => 556, + 'o' => 500, + 'p' => 500, + 'q' => 500, + 'r' => 389, + 's' => 389, + 't' => 278, + 'u' => 556, + 'v' => 444, + 'w' => 667, + 'x' => 500, + 'y' => 444, + 'z' => 389, + '{' => 348, + '|' => 220, + '}' => 348, + '~' => 570, + chr(127) => 350, + chr(128) => 500, + chr(129) => 350, + chr(130) => 333, + chr(131) => 500, + + chr(132) => 500, + chr(133) => 1000, + chr(134) => 500, + chr(135) => 500, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 556, + chr(139) => 333, + chr(140) => 944, + chr(141) => 350, + chr(142) => 611, + chr(143) => 350, + chr(144) => 350, + chr(145) => 333, + chr(146) => 333, + chr(147) => 500, + chr(148) => 500, + chr(149) => 350, + chr(150) => 500, + chr(151) => 1000, + chr(152) => 333, + chr(153) => 1000, + + chr(154) => 389, + chr(155) => 333, + chr(156) => 722, + chr(157) => 350, + chr(158) => 389, + chr(159) => 611, + chr(160) => 250, + chr(161) => 389, + chr(162) => 500, + chr(163) => 500, + chr(164) => 500, + chr(165) => 500, + chr(166) => 220, + chr(167) => 500, + chr(168) => 333, + chr(169) => 747, + chr(170) => 266, + chr(171) => 500, + chr(172) => 606, + chr(173) => 333, + chr(174) => 747, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 570, + chr(178) => 300, + chr(179) => 300, + chr(180) => 333, + chr(181) => 576, + chr(182) => 500, + chr(183) => 250, + chr(184) => 333, + chr(185) => 300, + chr(186) => 300, + chr(187) => 500, + chr(188) => 750, + chr(189) => 750, + chr(190) => 750, + chr(191) => 500, + chr(192) => 667, + chr(193) => 667, + chr(194) => 667, + chr(195) => 667, + chr(196) => 667, + chr(197) => 667, + + chr(198) => 944, + chr(199) => 667, + chr(200) => 667, + chr(201) => 667, + chr(202) => 667, + chr(203) => 667, + chr(204) => 389, + chr(205) => 389, + chr(206) => 389, + chr(207) => 389, + chr(208) => 722, + chr(209) => 722, + chr(210) => 722, + chr(211) => 722, + chr(212) => 722, + chr(213) => 722, + chr(214) => 722, + chr(215) => 570, + chr(216) => 722, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 611, + chr(222) => 611, + chr(223) => 500, + chr(224) => 500, + chr(225) => 500, + chr(226) => 500, + chr(227) => 500, + chr(228) => 500, + chr(229) => 500, + chr(230) => 722, + chr(231) => 444, + chr(232) => 444, + chr(233) => 444, + chr(234) => 444, + chr(235) => 444, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 500, + chr(241) => 556, + + chr(242) => 500, + chr(243) => 500, + chr(244) => 500, + chr(245) => 500, + chr(246) => 500, + chr(247) => 570, + chr(248) => 500, + chr(249) => 556, + chr(250) => 556, + chr(251) => 556, + chr(252) => 556, + chr(253) => 444, + chr(254) => 500, + chr(255) => 444); diff --git a/framework/File_PDF/PDF/fonts/timesi.php b/framework/File_PDF/PDF/fonts/timesi.php new file mode 100644 index 000000000..403b7e1b0 --- /dev/null +++ b/framework/File_PDF/PDF/fonts/timesi.php @@ -0,0 +1,272 @@ + 250, + chr(1) => 250, + chr(2) => 250, + chr(3) => 250, + chr(4) => 250, + chr(5) => 250, + chr(6) => 250, + chr(7) => 250, + chr(8) => 250, + chr(9) => 250, + chr(10) => 250, + chr(11) => 250, + chr(12) => 250, + chr(13) => 250, + chr(14) => 250, + chr(15) => 250, + chr(16) => 250, + chr(17) => 250, + chr(18) => 250, + chr(19) => 250, + chr(20) => 250, + chr(21) => 250, + + chr(22) => 250, + chr(23) => 250, + chr(24) => 250, + chr(25) => 250, + chr(26) => 250, + chr(27) => 250, + chr(28) => 250, + chr(29) => 250, + chr(30) => 250, + chr(31) => 250, + ' ' => 250, + '!' => 333, + '"' => 420, + '#' => 500, + '$' => 500, + '%' => 833, + '&' => 778, + '\'' => 214, + '(' => 333, + ')' => 333, + '*' => 500, + '+' => 675, + + ',' => 250, + '-' => 333, + '.' => 250, + '/' => 278, + '0' => 500, + '1' => 500, + '2' => 500, + '3' => 500, + '4' => 500, + '5' => 500, + '6' => 500, + '7' => 500, + '8' => 500, + '9' => 500, + ':' => 333, + ';' => 333, + '<' => 675, + '=' => 675, + '>' => 675, + '?' => 500, + '@' => 920, + 'A' => 611, + + 'B' => 611, + 'C' => 667, + 'D' => 722, + 'E' => 611, + 'F' => 611, + 'G' => 722, + 'H' => 722, + 'I' => 333, + 'J' => 444, + 'K' => 667, + 'L' => 556, + 'M' => 833, + 'N' => 667, + 'O' => 722, + 'P' => 611, + 'Q' => 722, + 'R' => 611, + 'S' => 500, + 'T' => 556, + 'U' => 722, + 'V' => 611, + 'W' => 833, + + 'X' => 611, + 'Y' => 556, + 'Z' => 556, + '[' => 389, + '\\' => 278, + ']' => 389, + '^' => 422, + '_' => 500, + '`' => 333, + 'a' => 500, + 'b' => 500, + 'c' => 444, + 'd' => 500, + 'e' => 444, + 'f' => 278, + 'g' => 500, + 'h' => 500, + 'i' => 278, + 'j' => 278, + 'k' => 444, + 'l' => 278, + 'm' => 722, + + 'n' => 500, + 'o' => 500, + 'p' => 500, + 'q' => 500, + 'r' => 389, + 's' => 389, + 't' => 278, + 'u' => 500, + 'v' => 444, + 'w' => 667, + 'x' => 444, + 'y' => 444, + 'z' => 389, + '{' => 400, + '|' => 275, + '}' => 400, + '~' => 541, + chr(127) => 350, + chr(128) => 500, + chr(129) => 350, + chr(130) => 333, + chr(131) => 500, + + chr(132) => 556, + chr(133) => 889, + chr(134) => 500, + chr(135) => 500, + chr(136) => 333, + chr(137) => 1000, + chr(138) => 500, + chr(139) => 333, + chr(140) => 944, + chr(141) => 350, + chr(142) => 556, + chr(143) => 350, + chr(144) => 350, + chr(145) => 333, + chr(146) => 333, + chr(147) => 556, + chr(148) => 556, + chr(149) => 350, + chr(150) => 500, + chr(151) => 889, + chr(152) => 333, + chr(153) => 980, + + chr(154) => 389, + chr(155) => 333, + chr(156) => 667, + chr(157) => 350, + chr(158) => 389, + chr(159) => 556, + chr(160) => 250, + chr(161) => 389, + chr(162) => 500, + chr(163) => 500, + chr(164) => 500, + chr(165) => 500, + chr(166) => 275, + chr(167) => 500, + chr(168) => 333, + chr(169) => 760, + chr(170) => 276, + chr(171) => 500, + chr(172) => 675, + chr(173) => 333, + chr(174) => 760, + chr(175) => 333, + + chr(176) => 400, + chr(177) => 675, + chr(178) => 300, + chr(179) => 300, + chr(180) => 333, + chr(181) => 500, + chr(182) => 523, + chr(183) => 250, + chr(184) => 333, + chr(185) => 300, + chr(186) => 310, + chr(187) => 500, + chr(188) => 750, + chr(189) => 750, + chr(190) => 750, + chr(191) => 500, + chr(192) => 611, + chr(193) => 611, + chr(194) => 611, + chr(195) => 611, + chr(196) => 611, + chr(197) => 611, + + chr(198) => 889, + chr(199) => 667, + chr(200) => 611, + chr(201) => 611, + chr(202) => 611, + chr(203) => 611, + chr(204) => 333, + chr(205) => 333, + chr(206) => 333, + chr(207) => 333, + chr(208) => 722, + chr(209) => 667, + chr(210) => 722, + chr(211) => 722, + chr(212) => 722, + chr(213) => 722, + chr(214) => 722, + chr(215) => 675, + chr(216) => 722, + chr(217) => 722, + chr(218) => 722, + chr(219) => 722, + + chr(220) => 722, + chr(221) => 556, + chr(222) => 611, + chr(223) => 500, + chr(224) => 500, + chr(225) => 500, + chr(226) => 500, + chr(227) => 500, + chr(228) => 500, + chr(229) => 500, + chr(230) => 667, + chr(231) => 444, + chr(232) => 444, + chr(233) => 444, + chr(234) => 444, + chr(235) => 444, + chr(236) => 278, + chr(237) => 278, + chr(238) => 278, + chr(239) => 278, + chr(240) => 500, + chr(241) => 500, + + chr(242) => 500, + chr(243) => 500, + chr(244) => 500, + chr(245) => 500, + chr(246) => 500, + chr(247) => 675, + chr(248) => 500, + chr(249) => 500, + chr(250) => 500, + chr(251) => 500, + chr(252) => 500, + chr(253) => 444, + chr(254) => 500, + chr(255) => 444); diff --git a/framework/File_PDF/PDF/fonts/zapfdingbats.php b/framework/File_PDF/PDF/fonts/zapfdingbats.php new file mode 100644 index 000000000..daa1a651d --- /dev/null +++ b/framework/File_PDF/PDF/fonts/zapfdingbats.php @@ -0,0 +1,272 @@ + 0, + chr(1) => 0, + chr(2) => 0, + chr(3) => 0, + chr(4) => 0, + chr(5) => 0, + chr(6) => 0, + chr(7) => 0, + chr(8) => 0, + chr(9) => 0, + chr(10) => 0, + chr(11) => 0, + chr(12) => 0, + chr(13) => 0, + chr(14) => 0, + chr(15) => 0, + chr(16) => 0, + chr(17) => 0, + chr(18) => 0, + chr(19) => 0, + chr(20) => 0, + chr(21) => 0, + + chr(22) => 0, + chr(23) => 0, + chr(24) => 0, + chr(25) => 0, + chr(26) => 0, + chr(27) => 0, + chr(28) => 0, + chr(29) => 0, + chr(30) => 0, + chr(31) => 0, + ' ' => 278, + '!' => 974, + '"' => 961, + '#' => 974, + '$' => 980, + '%' => 719, + '&' => 789, + '\'' => 790, + '(' => 791, + ')' => 690, + '*' => 960, + '+' => 939, + + ',' => 549, + '-' => 855, + '.' => 911, + '/' => 933, + '0' => 911, + '1' => 945, + '2' => 974, + '3' => 755, + '4' => 846, + '5' => 762, + '6' => 761, + '7' => 571, + '8' => 677, + '9' => 763, + ':' => 760, + ';' => 759, + '<' => 754, + '=' => 494, + '>' => 552, + '?' => 537, + '@' => 577, + 'A' => 692, + + 'B' => 786, + 'C' => 788, + 'D' => 788, + 'E' => 790, + 'F' => 793, + 'G' => 794, + 'H' => 816, + 'I' => 823, + 'J' => 789, + 'K' => 841, + 'L' => 823, + 'M' => 833, + 'N' => 816, + 'O' => 831, + 'P' => 923, + 'Q' => 744, + 'R' => 723, + 'S' => 749, + 'T' => 790, + 'U' => 792, + 'V' => 695, + 'W' => 776, + + 'X' => 768, + 'Y' => 792, + 'Z' => 759, + '[' => 707, + '\\' => 708, + ']' => 682, + '^' => 701, + '_' => 826, + '`' => 815, + 'a' => 789, + 'b' => 789, + 'c' => 707, + 'd' => 687, + 'e' => 696, + 'f' => 689, + 'g' => 786, + 'h' => 787, + 'i' => 713, + 'j' => 791, + 'k' => 785, + 'l' => 791, + 'm' => 873, + + 'n' => 761, + 'o' => 762, + 'p' => 762, + 'q' => 759, + 'r' => 759, + 's' => 892, + 't' => 892, + 'u' => 788, + 'v' => 784, + 'w' => 438, + 'x' => 138, + 'y' => 277, + 'z' => 415, + '{' => 392, + '|' => 392, + '}' => 668, + '~' => 668, + chr(127) => 0, + chr(128) => 390, + chr(129) => 390, + chr(130) => 317, + chr(131) => 317, + + chr(132) => 276, + chr(133) => 276, + chr(134) => 509, + chr(135) => 509, + chr(136) => 410, + chr(137) => 410, + chr(138) => 234, + chr(139) => 234, + chr(140) => 334, + chr(141) => 334, + chr(142) => 0, + chr(143) => 0, + chr(144) => 0, + chr(145) => 0, + chr(146) => 0, + chr(147) => 0, + chr(148) => 0, + chr(149) => 0, + chr(150) => 0, + chr(151) => 0, + chr(152) => 0, + chr(153) => 0, + + chr(154) => 0, + chr(155) => 0, + chr(156) => 0, + chr(157) => 0, + chr(158) => 0, + chr(159) => 0, + chr(160) => 0, + chr(161) => 732, + chr(162) => 544, + chr(163) => 544, + chr(164) => 910, + chr(165) => 667, + chr(166) => 760, + chr(167) => 760, + chr(168) => 776, + chr(169) => 595, + chr(170) => 694, + chr(171) => 626, + chr(172) => 788, + chr(173) => 788, + chr(174) => 788, + chr(175) => 788, + + chr(176) => 788, + chr(177) => 788, + chr(178) => 788, + chr(179) => 788, + chr(180) => 788, + chr(181) => 788, + chr(182) => 788, + chr(183) => 788, + chr(184) => 788, + chr(185) => 788, + chr(186) => 788, + chr(187) => 788, + chr(188) => 788, + chr(189) => 788, + chr(190) => 788, + chr(191) => 788, + chr(192) => 788, + chr(193) => 788, + chr(194) => 788, + chr(195) => 788, + chr(196) => 788, + chr(197) => 788, + + chr(198) => 788, + chr(199) => 788, + chr(200) => 788, + chr(201) => 788, + chr(202) => 788, + chr(203) => 788, + chr(204) => 788, + chr(205) => 788, + chr(206) => 788, + chr(207) => 788, + chr(208) => 788, + chr(209) => 788, + chr(210) => 788, + chr(211) => 788, + chr(212) => 894, + chr(213) => 838, + chr(214) => 1016, + chr(215) => 458, + chr(216) => 748, + chr(217) => 924, + chr(218) => 748, + chr(219) => 918, + + chr(220) => 927, + chr(221) => 928, + chr(222) => 928, + chr(223) => 834, + chr(224) => 873, + chr(225) => 828, + chr(226) => 924, + chr(227) => 924, + chr(228) => 917, + chr(229) => 930, + chr(230) => 931, + chr(231) => 463, + chr(232) => 883, + chr(233) => 836, + chr(234) => 836, + chr(235) => 867, + chr(236) => 867, + chr(237) => 696, + chr(238) => 696, + chr(239) => 874, + chr(240) => 0, + chr(241) => 874, + + chr(242) => 760, + chr(243) => 946, + chr(244) => 771, + chr(245) => 865, + chr(246) => 771, + chr(247) => 888, + chr(248) => 967, + chr(249) => 888, + chr(250) => 831, + chr(251) => 873, + chr(252) => 927, + chr(253) => 970, + chr(254) => 918, + chr(255) => 0); diff --git a/framework/File_PDF/package.xml b/framework/File_PDF/package.xml new file mode 100644 index 000000000..a7aa90a54 --- /dev/null +++ b/framework/File_PDF/package.xml @@ -0,0 +1,183 @@ + + + File_PDF + pear.php.net + PDF generation using only PHP. + This package provides PDF generation using only PHP, without requiring any external libraries. + + Marko Djukic + mdjukic + mdjukic@horde.org + no + + + Jan Schneider + yunosh + jan@horde.org + yes + + + Chuck Hagenbuch + chagenbu + chuck@horde.org + yes + + 2008-02-26 + + 0.3.2 + 0.1.0 + + + beta + beta + + LGPL + * Automatically restore font styles after adding headers and footers (PEAR Request #12310). +* Pass $params to the class constructor to allow subclasses to receive and handle additional parameters (PEAR Request #12441). +* Fix creating linked images (Horde Bug #5964). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 4.2.0 + + + 1.4.0b1 + + + + + HTTP_Download + pear.php.net + + + + + + + + 0.3.1 + 0.1.0 + + + beta + beta + + 2007-11-07 + LGPL + * Fixed escaping of parenthesis in PDF documents (PEAR Bug #12092). +* Always reset position to the left margin before starting footers. +* Fixed fill color overwriting text color (PEAR Bug #12310). + + + + 0.3.0 + 0.1.0 + + + beta + beta + + 2007-09-14 + LGPL + * Add flush() method to allow processing of very large PDF files (Request #10077). +* Fix underlined fonts (Bug #11447). +* Workaround BC break in PHP 4.3.10 with some locales (Horde Bug #4094). + + + + 0.2.0 + 0.1.0 + + + beta + beta + + 2007-01-22 + LGPL + * Catch errors from parsing images (Bug #8856). +* Fix font width calculation of comma character (Andrew Teixeira, Bug #9595). +* Add getPageWidth() and getPageHeight() methods (Request #9267). + + + + 0.1.0 + 0.1.0 + + + beta + beta + + 2006-08-28 + LGPL + * Preserve font settings when adding new pages (Bug #2682). +* Add setFontStyle() method (d.baechtold@unico.ch, Request #5230). +* Allow all coordinates to be specified as negative values from the right or bottom edges (Request #5230). +* Add setTextColor() to specify text colors different from fill colors (Request #1767). + + + + 0.0.2 + 0.0.2 + + + beta + beta + + 2005-04-14 + LGPL + * Fixed loading of font metrics in setFont(). +* Fixed typo preventing setFillColor() and setDrawColor() from accepting any other colorspace than 'rgb' (Horde Bug 1276). +* Allow to use factory() method with custom class extended from File_PDF (Bug 1543). +* Fixed typo in link() (Bug 1737). +* Fixed save() method to actually save the whole document (Bug 1768). + + + + + 0.0.1 + 0.0.1 + + + beta + beta + + 2004-06-04 + LGPL + Initial release as a PEAR package + + + + diff --git a/framework/File_PDF/tests/20k_c1.txt b/framework/File_PDF/tests/20k_c1.txt new file mode 100644 index 000000000..6d5b29542 --- /dev/null +++ b/framework/File_PDF/tests/20k_c1.txt @@ -0,0 +1,10 @@ +The year 1866 was marked by a bizarre development, an unexplained and downright inexplicable phenomenon that surely no one has forgotten. Without getting into those rumors that upset civilians in the seaports and deranged the public mind even far inland, it must be said that professional seamen were especially alarmed. Traders, shipowners, captains of vessels, skippers, and master mariners from Europe and America, naval officers from every country, and at their heels the various national governments on these two continents, were all extremely disturbed by the business. +In essence, over a period of time several ships had encountered "an enormous thing" at sea, a long spindle-shaped object, sometimes giving off a phosphorescent glow, infinitely bigger and faster than any whale. +The relevant data on this apparition, as recorded in various logbooks, agreed pretty closely as to the structure of the object or creature in question, its unprecedented speed of movement, its startling locomotive power, and the unique vitality with which it seemed to be gifted. If it was a cetacean, it exceeded in bulk any whale previously classified by science. No naturalist, neither Cuvier nor Lacépède, neither Professor Dumeril nor Professor de Quatrefages, would have accepted the existence of such a monster sight unseen -- specifically, unseen by their own scientific eyes. +Striking an average of observations taken at different times -- rejecting those timid estimates that gave the object a length of 200 feet, and ignoring those exaggerated views that saw it as a mile wide and three long--you could still assert that this phenomenal creature greatly exceeded the dimensions of anything then known to ichthyologists, if it existed at all. +Now then, it did exist, this was an undeniable fact; and since the human mind dotes on objects of wonder, you can understand the worldwide excitement caused by this unearthly apparition. As for relegating it to the realm of fiction, that charge had to be dropped. +In essence, on July 20, 1866, the steamer Governor Higginson, from the Calcutta & Burnach Steam Navigation Co., encountered this moving mass five miles off the eastern shores of Australia. Captain Baker at first thought he was in the presence of an unknown reef; he was even about to fix its exact position when two waterspouts shot out of this inexplicable object and sprang hissing into the air some 150 feet. So, unless this reef was subject to the intermittent eruptions of a geyser, the Governor Higginson had fair and honest dealings with some aquatic mammal, until then unknown, that could spurt from its blowholes waterspouts mixed with air and steam. +Similar events were likewise observed in Pacific seas, on July 23 of the same year, by the Christopher Columbus from the West India & Pacific Steam Navigation Co. Consequently, this extraordinary cetacean could transfer itself from one locality to another with startling swiftness, since within an interval of just three days, the Governor Higginson and the Christopher Columbus had observed it at two positions on the charts separated by a distance of more than 700 nautical leagues. +Fifteen days later and 2,000 leagues farther, the Helvetia from the Compagnie Nationale and the Shannon from the Royal Mail line, running on opposite tacks in that part of the Atlantic lying between the United States and Europe, respectively signaled each other that the monster had been sighted in latitude 42 degrees 15' north and longitude 60 degrees 35' west of the meridian of Greenwich. From their simultaneous observations, they were able to estimate the mammal's minimum length at more than 350 English feet; this was because both the Shannon and the Helvetia were of smaller dimensions, although each measured 100 meters stem to stern. Now then, the biggest whales, those rorqual whales that frequent the waterways of the Aleutian Islands, have never exceeded a length of 56 meters--if they reach even that. +One after another, reports arrived that would profoundly affect public opinion: new observations taken by the transatlantic liner Pereire, the Inman line's Etna running afoul of the monster, an official report drawn up by officers on the French frigate Normandy, dead-earnest reckonings obtained by the general staff of Commodore Fitz-James aboard the Lord Clyde. In lighthearted countries, people joked about this phenomenon, but such serious, practical countries as England, America, and Germany were deeply concerned. +In every big city the monster was the latest rage; they sang about it in the coffee houses, they ridiculed it in the newspapers, they dramatized it in the theaters. The tabloids found it a fine opportunity for hatching all sorts of hoaxes. In those newspapers short of copy, you saw the reappearance of every gigantic imaginary creature, from "Moby Dick," that dreadful white whale from the High Arctic regions, to the stupendous kraken whose tentacles could entwine a 500-ton craft and drag it into the ocean depths. They even reprinted reports from ancient times: the views of Aristotle and Pliny accepting the existence of such monsters, then the Norwegian stories of Bishop Pontoppidan, the narratives of Paul Egede, and finally the reports of Captain Harrington -- whose good faith is above suspicion--in which he claims he saw, while aboard the Castilian in 1857, one of those enormous serpents that, until then, had frequented only the seas of France's old extremist newspaper, The Constitutionalist. diff --git a/framework/File_PDF/tests/20k_c2.txt b/framework/File_PDF/tests/20k_c2.txt new file mode 100644 index 000000000..7b5c56514 --- /dev/null +++ b/framework/File_PDF/tests/20k_c2.txt @@ -0,0 +1,23 @@ +During the period in which these developments were occurring, I had returned from a scientific undertaking organized to explore the Nebraska badlands in the United States. In my capacity as Assistant Professor at the Paris Museum of Natural History, I had been attached to this expedition by the French government. After spending six months in Nebraska, I arrived in New York laden with valuable collections near the end of March. My departure for France was set for early May. In the meantime, then, I was busy classifying my mineralogical, botanical, and zoological treasures when that incident took place with the Scotia. +I was perfectly abreast of this question, which was the big news of the day, and how could I not have been? I had read and reread every American and European newspaper without being any farther along. This mystery puzzled me. Finding it impossible to form any views, I drifted from one extreme to the other. Something was out there, that much was certain, and any doubting Thomas was invited to place his finger on the Scotia's wound. +When I arrived in New York, the question was at the boiling point. The hypothesis of a drifting islet or an elusive reef, put forward by people not quite in their right minds, was completely eliminated. And indeed, unless this reef had an engine in its belly, how could it move about with such prodigious speed? +Also discredited was the idea of a floating hull or some other enormous wreckage, and again because of this speed of movement. +So only two possible solutions to the question were left, creating two very distinct groups of supporters: on one side, those favoring a monster of colossal strength; on the other, those favoring an "underwater boat" of tremendous motor power. +Now then, although the latter hypothesis was completely admissible, it couldn't stand up to inquiries conducted in both the New World and the Old. That a private individual had such a mechanism at his disposal was less than probable. Where and when had he built it, and how could he have built it in secret? +Only some government could own such an engine of destruction, and in these disaster-filled times, when men tax their ingenuity to build increasingly powerful aggressive weapons, it was possible that, unknown to the rest of the world, some nation could have been testing such a fearsome machine. The Chassepot rifle led to the torpedo, and the torpedo has led to this underwater battering ram, which in turn will lead to the world putting its foot down. At least I hope it will. +But this hypothesis of a war machine collapsed in the face of formal denials from the various governments. Since the public interest was at stake and transoceanic travel was suffering, the sincerity of these governments could not be doubted. Besides, how could the assembly of this underwater boat have escaped public notice? Keeping a secret under such circumstances would be difficult enough for an individual, and certainly impossible for a nation whose every move is under constant surveillance by rival powers. +So, after inquiries conducted in England, France, Russia, Prussia, Spain, Italy, America, and even Turkey, the hypothesis of an underwater Monitor was ultimately rejected. +After I arrived in New York, several people did me the honor of consulting me on the phenomenon in question. In France I had published a two-volume work, in quarto, entitled The Mysteries of the Great Ocean Depths. Well received in scholarly circles, this book had established me as a specialist in this pretty obscure field of natural history. My views were in demand. As long as I could deny the reality of the business, I confined myself to a flat "no comment." But soon, pinned to the wall, I had to explain myself straight out. And in this vein, "the honorable Pierre Aronnax, Professor at the Paris Museum," was summoned by The New York Herald to formulate his views no matter what. +I complied. Since I could no longer hold my tongue, I let it wag. I discussed the question in its every aspect, both political and scientific, and this is an excerpt from the well-padded article I published in the issue of April 30. + +"Therefore," I wrote, "after examining these different hypotheses one by one, we are forced, every other supposition having been refuted, to accept the existence of an extremely powerful marine animal. +"The deepest parts of the ocean are totally unknown to us. No soundings have been able to reach them. What goes on in those distant depths? What creatures inhabit, or could inhabit, those regions twelve or fifteen miles beneath the surface of the water? What is the constitution of these animals? It's almost beyond conjecture. +"However, the solution to this problem submitted to me can take the form of a choice between two alternatives. +"Either we know every variety of creature populating our planet, or we do not. +"If we do not know every one of them, if nature still keeps ichthyological secrets from us, nothing is more admissible than to accept the existence of fish or cetaceans of new species or even new genera, animals with a basically 'cast-iron' constitution that inhabit strata beyond the reach of our soundings, and which some development or other, an urge or a whim if you prefer, can bring to the upper level of the ocean for long intervals. +"If, on the other hand, we do know every living species, we must look for the animal in question among those marine creatures already cataloged, and in this event I would be inclined to accept the existence of a giant narwhale. +"The common narwhale, or sea unicorn, often reaches a length of sixty feet. Increase its dimensions fivefold or even tenfold, then give this cetacean a strength in proportion to its size while enlarging its offensive weapons, and you have the animal we're looking for. It would have the proportions determined by the officers of the Shannon, the instrument needed to perforate the Scotia, and the power to pierce a steamer's hull. +"In essence, the narwhale is armed with a sort of ivory sword, or lance, as certain naturalists have expressed it. It's a king-sized tooth as hard as steel. Some of these teeth have been found buried in the bodies of baleen whales, which the narwhale attacks with invariable success. Others have been wrenched, not without difficulty, from the undersides of vessels that narwhales have pierced clean through, as a gimlet pierces a wine barrel. The museum at the Faculty of Medicine in Paris owns one of these tusks with a length of 2.25 meters and a width at its base of forty-eight centimeters! +"All right then! Imagine this weapon to be ten times stronger and the animal ten times more powerful, launch it at a speed of twenty miles per hour, multiply its mass times its velocity, and you get just the collision we need to cause the specified catastrophe. +"So, until information becomes more abundant, I plump for a sea unicorn of colossal dimensions, no longer armed with a mere lance but with an actual spur, like ironclad frigates or those warships called 'rams,' whose mass and motor power it would possess simultaneously. +"This inexplicable phenomenon is thus explained away--unless it's something else entirely, which, despite everything that has been sighted, studied, explored and experienced, is still possible!" diff --git a/framework/File_PDF/tests/auto_break.phpt b/framework/File_PDF/tests/auto_break.phpt new file mode 100644 index 000000000..6ac74317a --- /dev/null +++ b/framework/File_PDF/tests/auto_break.phpt @@ -0,0 +1,122 @@ +--TEST-- +File_PDF: Automatic page break test +--FILE-- + array(50, 50), 'unit' => 'pt')); +// Deactivate compression. +$pdf->setCompression(false); +// Set margins. +$pdf->setMargins(0, 0); +// Enable automatic page breaks. +$pdf->setAutoPageBreak(true); +// Start the document. +$pdf->open(); +// Start a page. +$pdf->addPage(); +// Set font to Courier 8 pt. +$pdf->setFont('Courier', '', 10); +// Write 7 lines +$pdf->write(10, "Hello\nHello\nHello\nHello\nHello\nHello\nHello\n"); +// Print the generated file. +echo $pdf->getOutput(); + +?> +--EXPECTF-- +%PDF-1.3 +3 0 obj +<> +endobj +4 0 obj +<> +stream +2 J +0.57 w +BT /F1 10.00 Tf ET +BT 2.83 42.00 Td (Hello) Tj ET +BT 2.83 32.00 Td (Hello) Tj ET +BT 2.83 22.00 Td (Hello) Tj ET +BT 2.83 12.00 Td (Hello) Tj ET +BT 2.83 2.00 Td (Hello) Tj ET + +endstream +endobj +5 0 obj +<> +endobj +6 0 obj +<> +stream +2 J +0.57 w +BT /F1 10.00 Tf ET +BT 2.83 42.00 Td (Hello) Tj ET +BT 2.83 32.00 Td (Hello) Tj ET + +endstream +endobj +1 0 obj +<> +endobj +7 0 obj +<> +endobj +2 0 obj +<> +>> +endobj +8 0 obj +<< +/Producer (Horde PDF) +/CreationDate (D:%d) +>> +endobj +9 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 10 +0000000000 65535 f +0000000538 00000 n +0000000723 00000 n +0000000009 00000 n +0000000087 00000 n +0000000320 00000 n +0000000398 00000 n +0000000629 00000 n +0000000811 00000 n +0000000887 00000 n +trailer +<< +/Size 10 +/Root 9 0 R +/Info 8 0 R +>> +startxref +990 +%%EOF diff --git a/framework/File_PDF/tests/factory.phpt b/framework/File_PDF/tests/factory.phpt new file mode 100644 index 000000000..987ae14ce --- /dev/null +++ b/framework/File_PDF/tests/factory.phpt @@ -0,0 +1,63 @@ +--TEST-- +File_PDF: factory() test +--FILE-- +_default_orientation); +var_dump($pdf->_scale); +var_dump($pdf->fwPt); +var_dump($pdf->fhPt); +$pdf = &File_PDF::factory('L', 'pt'); +var_dump($pdf->_default_orientation); +var_dump($pdf->_scale); +var_dump($pdf->fwPt); +var_dump($pdf->fhPt); + +/* New signature. */ +$pdf = &File_PDF::factory(array('orientation' => 'L', 'unit' => 'pt', 'format' => 'A3')); +var_dump($pdf->_default_orientation); +var_dump($pdf->_scale); +var_dump($pdf->fwPt); +var_dump($pdf->fhPt); +$pdf = &File_PDF::factory(); +var_dump($pdf->_default_orientation); +var_dump(abs($pdf->_scale - 2.8346456692913) < 0.000001); +var_dump($pdf->fwPt); +var_dump($pdf->fhPt); + +/* Custom class. */ +class MyPDF extends File_PDF {} +$pdf = &File_PDF::factory(array(), 'MyPDF'); +var_dump(strtolower(get_class($pdf))); +var_dump($pdf->_default_orientation); +var_dump(abs($pdf->_scale - 2.8346456692913) < 0.000001); +var_dump($pdf->fwPt); +var_dump($pdf->fhPt); + +?> +--EXPECT-- +string(1) "L" +int(1) +float(841.89) +float(1190.55) +string(1) "L" +int(1) +float(595.28) +float(841.89) +string(1) "L" +int(1) +float(841.89) +float(1190.55) +string(1) "P" +bool(true) +float(595.28) +float(841.89) +string(5) "mypdf" +string(1) "P" +bool(true) +float(595.28) +float(841.89) diff --git a/framework/File_PDF/tests/hello_world.phpt b/framework/File_PDF/tests/hello_world.phpt new file mode 100644 index 000000000..6ee0ff930 Binary files /dev/null and b/framework/File_PDF/tests/hello_world.phpt differ diff --git a/framework/File_PDF/tests/horde-power1.png b/framework/File_PDF/tests/horde-power1.png new file mode 100644 index 000000000..eea229c6b Binary files /dev/null and b/framework/File_PDF/tests/horde-power1.png differ diff --git a/framework/File_PDF/tests/links.phpt b/framework/File_PDF/tests/links.phpt new file mode 100644 index 000000000..aae9ec534 --- /dev/null +++ b/framework/File_PDF/tests/links.phpt @@ -0,0 +1,153 @@ +--TEST-- +File_PDF: Link tests +--FILE-- + 'P', 'format' => 'A4')); +// Start the document. +$pdf->open(); +// Deactivate compression. +$pdf->setCompression(false); +// Start a page. +$pdf->addPage(); +// Set font to Helvetica 12 pt. +$pdf->setFont('Helvetica', 'U', 12); +// Write linked text. +$pdf->write(15, 'Horde', 'http://www.horde.org'); +// Add line break. +$pdf->write(15, "\n"); +// Write linked text. +$link = $pdf->addLink(); +$pdf->write(15, 'here', $link); +// Start next page. +$pdf->addPage(); +// Add link anchor. +$pdf->setLink($link); +// Create linked image. +$pdf->image(dirname(__FILE__) . '/horde-power1.png', 15, 15, 0, 0, '', 'http://pear.horde.org/'); +// Print the generated file. +echo $pdf->getOutput(); + +?> +--EXPECTF-- +%PDF-1.3 +3 0 obj +<>>><>] +/Contents 4 0 R>> +endobj +4 0 obj +<> +stream +2 J +0.57 w +BT /F1 12.00 Tf ET +BT 31.19 788.68 Td (Horde) Tj ET 31.19 787.48 32.68 -0.60 re f +BT 31.19 746.16 Td (here) Tj ET 31.19 744.96 24.01 -0.60 re f + +endstream +endobj +5 0 obj +<>>>] +/Contents 6 0 R>> +endobj +6 0 obj +<> +stream +2 J +0.57 w +BT /F1 12.00 Tf ET +q 84.00 0 0 31.00 42.52 768.37 cm /I1 Do Q + +endstream +endobj +1 0 obj +<> +endobj +7 0 obj +<> +endobj +8 0 obj +<> +/Length 2202>> +stream +%s +%s +%s +%s +%s +%s +%s +%s +%s +endstream +endobj +2 0 obj +<> +/XObject << +/I1 8 0 R +>> +>> +endobj +9 0 obj +<< +/Producer (Horde PDF) +/CreationDate (D:%d) +>> +endobj +10 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 11 +0000000000 65535 f +0000000877 00000 n +0000003507 00000 n +0000000009 00000 n +0000000336 00000 n +0000000540 00000 n +0000000756 00000 n +0000000970 00000 n +0000001066 00000 n +0000003620 00000 n +0000003696 00000 n +trailer +<< +/Size 11 +/Root 10 0 R +/Info 9 0 R +>> +startxref +3800 +%%EOF \ No newline at end of file diff --git a/framework/File_PDF/tests/locale_floats.phpt b/framework/File_PDF/tests/locale_floats.phpt new file mode 100644 index 000000000..ddfe95a87 Binary files /dev/null and b/framework/File_PDF/tests/locale_floats.phpt differ diff --git a/framework/File_PDF/tests/pear12310.phpt b/framework/File_PDF/tests/pear12310.phpt new file mode 100644 index 000000000..61a67d4bc --- /dev/null +++ b/framework/File_PDF/tests/pear12310.phpt @@ -0,0 +1,558 @@ +--TEST-- +PEAR Bug #12310 +--FILE-- +setFont('Arial', 'B', 15); + $w = $this->getStringWidth($this->_info['title']) + 6; + $this->setX((210 - $w) / 2); + $this->setDrawColor('rgb', 0/255, 80/255, 180/255); + $this->setFillColor('rgb', 230/255, 230/255, 0/255); + $this->setTextColor('rgb', 220/255, 50/255, 50/255); + $this->setLineWidth(1); + $this->cell($w, 9, $this->_info['title'], 1, 1, 'C', 1); + $this->newLine(10); + } + + function footer() + { + $this->setY(-15); + $this->setFont('Arial', 'I', 8); + $this->setTextColor('gray', 128/255); + $this->cell(0, 10, 'Page ' . $this->getPageNo(), 0, 0, 'C'); + } + + function chapterTitle($num, $label) + { + $this->setFont('Arial', '', 12); + $this->setFillColor('rgb', 200/255, 220/255, 255/255); + $this->cell(0, 6, "Chapter $num : $label", 0, 1, 'L', 1); + $this->newLine(4); + } + + function chapterBody($file) + { + $text = file_get_contents(dirname(__FILE__) . '/' . $file); + $this->setFont('Times', '', 12); + $this->multiCell(0, 5, $text); + $this->newLine(); + $this->setFont('', 'I'); + $this->cell(0, 5, '(end of extract)'); + } + + function printChapter($num, $title, $file) + { + $this->addPage(); + $this->chapterTitle($num, $title); + $this->chapterBody($file); + } + +} + +$pdf = MyPDF::factory(array('orientation' => 'P', + 'unit' => 'mm', + 'format' => 'A4'), + 'MyPDF'); +$pdf->setCompression(false); +$pdf->setInfo('title', '20000 Leagues Under the Seas'); +$pdf->setInfo('author', 'Jules Verne'); +$pdf->printChapter(1, 'A RUNAWAY REEF', '20k_c1.txt'); +$pdf->printChapter(2, 'THE PROS AND CONS', '20k_c2.txt'); +echo $pdf->getOutput(); + +?> +--EXPECTF-- +%PDF-1.3 +3 0 obj +<> +endobj +4 0 obj +<> +stream +2 J +0.57 w +BT /F1 15.00 Tf ET +0.000 0.314 0.706 RG +0.902 0.902 0.000 rg +2.83 w +179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q +0.57 w +BT /F2 12.00 Tf ET +0 g +0 G +0.784 0.863 1.000 rg +28.35 759.68 538.58 -17.01 re f q 0 g BT 31.19 747.58 Td (Chapter 1 : A RUNAWAY REEF) Tj ET Q +BT /F3 12.00 Tf ET +0.002 Tw +q 0 g BT 31.19 720.65 Td (The year 1866 was marked by a bizarre development, an unexplained and downright inexplicable phenomenon) Tj ET Q +1.585 Tw +q 0 g BT 31.19 706.48 Td (that surely no one has forgotten. Without getting into those rumors that upset civilians in the seaports and) Tj ET Q +1.022 Tw +q 0 g BT 31.19 692.30 Td (deranged the public mind even far inland, it must be said that professional seamen were especially alarmed.) Tj ET Q +2.336 Tw +q 0 g BT 31.19 678.13 Td (Traders, shipowners, captains of vessels, skippers, and master mariners from Europe and America, naval) Tj ET Q +0.314 Tw +q 0 g BT 31.19 663.96 Td (officers from every country, and at their heels the various national governments on these two continents, were) Tj ET Q +0 Tw +q 0 g BT 31.19 649.78 Td (all extremely disturbed by the business.) Tj ET Q +3.660 Tw +q 0 g BT 31.19 635.61 Td (In essence, over a period of time several ships had encountered "an enormous thing" at sea, a long) Tj ET Q +2.306 Tw +q 0 g BT 31.19 621.44 Td (spindle-shaped object, sometimes giving off a phosphorescent glow, infinitely bigger and faster than any) Tj ET Q +0 Tw +q 0 g BT 31.19 607.26 Td (whale.) Tj ET Q +0.589 Tw +q 0 g BT 31.19 593.09 Td (The relevant data on this apparition, as recorded in various logbooks, agreed pretty closely as to the structure) Tj ET Q +0.125 Tw +q 0 g BT 31.19 578.92 Td (of the object or creature in question, its unprecedented speed of movement, its startling locomotive power, and) Tj ET Q +1.556 Tw +q 0 g BT 31.19 564.74 Td (the unique vitality with which it seemed to be gifted. If it was a cetacean, it exceeded in bulk any whale) Tj ET Q +1.192 Tw +q 0 g BT 31.19 550.57 Td (previously classified by science. No naturalist, neither Cuvier nor Lacépède, neither Professor Dumeril nor) Tj ET Q +1.159 Tw +q 0 g BT 31.19 536.40 Td (Professor de Quatrefages, would have accepted the existence of such a monster sight unseen -- specifically,) Tj ET Q +0 Tw +q 0 g BT 31.19 522.22 Td (unseen by their own scientific eyes.) Tj ET Q +1.520 Tw +q 0 g BT 31.19 508.05 Td (Striking an average of observations taken at different times -- rejecting those timid estimates that gave the) Tj ET Q +2.544 Tw +q 0 g BT 31.19 493.88 Td (object a length of 200 feet, and ignoring those exaggerated views that saw it as a mile wide and three) Tj ET Q +1.356 Tw +q 0 g BT 31.19 479.70 Td (long--you could still assert that this phenomenal creature greatly exceeded the dimensions of anything then) Tj ET Q +0 Tw +q 0 g BT 31.19 465.53 Td (known to ichthyologists, if it existed at all.) Tj ET Q +0.232 Tw +q 0 g BT 31.19 451.36 Td (Now then, it did exist, this was an undeniable fact; and since the human mind dotes on objects of wonder, you) Tj ET Q +0.292 Tw +q 0 g BT 31.19 437.18 Td (can understand the worldwide excitement caused by this unearthly apparition. As for relegating it to the realm) Tj ET Q +0 Tw +q 0 g BT 31.19 423.01 Td (of fiction, that charge had to be dropped.) Tj ET Q +3.687 Tw +q 0 g BT 31.19 408.84 Td (In essence, on July 20, 1866, the steamer Governor Higginson, from the Calcutta & Burnach Steam) Tj ET Q +0.332 Tw +q 0 g BT 31.19 394.66 Td (Navigation Co., encountered this moving mass five miles off the eastern shores of Australia. Captain Baker at) Tj ET Q +0.413 Tw +q 0 g BT 31.19 380.49 Td (first thought he was in the presence of an unknown reef; he was even about to fix its exact position when two) Tj ET Q +0.593 Tw +q 0 g BT 31.19 366.32 Td (waterspouts shot out of this inexplicable object and sprang hissing into the air some 150 feet. So, unless this) Tj ET Q +0.177 Tw +q 0 g BT 31.19 352.14 Td (reef was subject to the intermittent eruptions of a geyser, the Governor Higginson had fair and honest dealings) Tj ET Q +0.662 Tw +q 0 g BT 31.19 337.97 Td (with some aquatic mammal, until then unknown, that could spurt from its blowholes waterspouts mixed with) Tj ET Q +0 Tw +q 0 g BT 31.19 323.80 Td (air and steam.) Tj ET Q +2.548 Tw +q 0 g BT 31.19 309.63 Td (Similar events were likewise observed in Pacific seas, on July 23 of the same year, by the Christopher) Tj ET Q +1.355 Tw +q 0 g BT 31.19 295.45 Td (Columbus from the West India & Pacific Steam Navigation Co. Consequently, this extraordinary cetacean) Tj ET Q +0.567 Tw +q 0 g BT 31.19 281.28 Td (could transfer itself from one locality to another with startling swiftness, since within an interval of just three) Tj ET Q +1.163 Tw +q 0 g BT 31.19 267.11 Td (days, the Governor Higginson and the Christopher Columbus had observed it at two positions on the charts) Tj ET Q +0 Tw +q 0 g BT 31.19 252.93 Td (separated by a distance of more than 700 nautical leagues.) Tj ET Q +1.734 Tw +q 0 g BT 31.19 238.76 Td (Fifteen days later and 2,000 leagues farther, the Helvetia from the Compagnie Nationale and the Shannon) Tj ET Q +0.050 Tw +q 0 g BT 31.19 224.59 Td (from the Royal Mail line, running on opposite tacks in that part of the Atlantic lying between the United States) Tj ET Q +0.167 Tw +q 0 g BT 31.19 210.41 Td (and Europe, respectively signaled each other that the monster had been sighted in latitude 42 degrees 15' north) Tj ET Q +0.551 Tw +q 0 g BT 31.19 196.24 Td (and longitude 60 degrees 35' west of the meridian of Greenwich. From their simultaneous observations, they) Tj ET Q +0.341 Tw +q 0 g BT 31.19 182.07 Td (were able to estimate the mammal's minimum length at more than 350 English feet; this was because both the) Tj ET Q +0.146 Tw +q 0 g BT 31.19 167.89 Td (Shannon and the Helvetia were of smaller dimensions, although each measured 100 meters stem to stern. Now) Tj ET Q +0.377 Tw +q 0 g BT 31.19 153.72 Td (then, the biggest whales, those rorqual whales that frequent the waterways of the Aleutian Islands, have never) Tj ET Q +0 Tw +q 0 g BT 31.19 139.55 Td (exceeded a length of 56 meters--if they reach even that.) Tj ET Q +0.293 Tw +q 0 g BT 31.19 125.37 Td (One after another, reports arrived that would profoundly affect public opinion: new observations taken by the) Tj ET Q +0.893 Tw +q 0 g BT 31.19 111.20 Td (transatlantic liner Pereire, the Inman line's Etna running afoul of the monster, an official report drawn up by) Tj ET Q +0.051 Tw +q 0 g BT 31.19 97.03 Td (officers on the French frigate Normandy, dead-earnest reckonings obtained by the general staff of Commodore) Tj ET Q +1.236 Tw +q 0 g BT 31.19 82.85 Td (Fitz-James aboard the Lord Clyde. In lighthearted countries, people joked about this phenomenon, but such) Tj ET Q +0 Tw +q 0 g BT 31.19 68.68 Td (serious, practical countries as England, America, and Germany were deeply concerned.) Tj ET Q +0.060 Tw +0 Tw +BT /F4 8.00 Tf ET +q 0.502 g BT 284.96 25.95 Td (Page 1) Tj ET Q + +endstream +endobj +5 0 obj +<> +endobj +6 0 obj +<> +stream +2 J +0.57 w +BT /F3 12.00 Tf ET +BT /F1 15.00 Tf ET +0.000 0.314 0.706 RG +0.902 0.902 0.000 rg +2.83 w +179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q +0.57 w +BT /F3 12.00 Tf ET +0.784 0.863 1.000 rg +0 G +0.060 Tw +q 0 g BT 31.19 749.00 Td (In every big city the monster was the latest rage; they sang about it in the coffee houses, they ridiculed it in the) Tj ET Q +0.034 Tw +q 0 g BT 31.19 734.82 Td (newspapers, they dramatized it in the theaters. The tabloids found it a fine opportunity for hatching all sorts of) Tj ET Q +1.315 Tw +q 0 g BT 31.19 720.65 Td (hoaxes. In those newspapers short of copy, you saw the reappearance of every gigantic imaginary creature,) Tj ET Q +0.742 Tw +q 0 g BT 31.19 706.48 Td (from "Moby Dick," that dreadful white whale from the High Arctic regions, to the stupendous kraken whose) Tj ET Q +1.315 Tw +q 0 g BT 31.19 692.30 Td (tentacles could entwine a 500-ton craft and drag it into the ocean depths. They even reprinted reports from) Tj ET Q +0.707 Tw +q 0 g BT 31.19 678.13 Td (ancient times: the views of Aristotle and Pliny accepting the existence of such monsters, then the Norwegian) Tj ET Q +0.936 Tw +q 0 g BT 31.19 663.96 Td (stories of Bishop Pontoppidan, the narratives of Paul Egede, and finally the reports of Captain Harrington --) Tj ET Q +0.980 Tw +q 0 g BT 31.19 649.78 Td (whose good faith is above suspicion--in which he claims he saw, while aboard the Castilian in 1857, one of) Tj ET Q +1.389 Tw +q 0 g BT 31.19 635.61 Td (those enormous serpents that, until then, had frequented only the seas of France's old extremist newspaper,) Tj ET Q +0 Tw +q 0 g BT 31.19 621.44 Td (The Constitutionalist. +) Tj ET Q +BT /F5 12.00 Tf ET +q 0 g BT 31.19 593.09 Td (\(end of extract\)) Tj ET Q +BT /F4 8.00 Tf ET +q 0.502 g BT 284.96 25.95 Td (Page 2) Tj ET Q + +endstream +endobj +7 0 obj +<> +endobj +8 0 obj +<> +stream +2 J +0.57 w +BT /F5 12.00 Tf ET +BT /F1 15.00 Tf ET +0.000 0.314 0.706 RG +0.902 0.902 0.000 rg +2.83 w +179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q +0.57 w +BT /F5 12.00 Tf ET +0.784 0.863 1.000 rg +0 G +BT /F2 12.00 Tf ET +0.784 0.863 1.000 rg +28.35 759.68 538.58 -17.01 re f q 0 g BT 31.19 747.58 Td (Chapter 2 : THE PROS AND CONS) Tj ET Q +BT /F3 12.00 Tf ET +1.002 Tw +q 0 g BT 31.19 720.65 Td (During the period in which these developments were occurring, I had returned from a scientific undertaking) Tj ET Q +0.608 Tw +q 0 g BT 31.19 706.48 Td (organized to explore the Nebraska badlands in the United States. In my capacity as Assistant Professor at the) Tj ET Q +1.687 Tw +q 0 g BT 31.19 692.30 Td (Paris Museum of Natural History, I had been attached to this expedition by the French government. After) Tj ET Q +2.020 Tw +q 0 g BT 31.19 678.13 Td (spending six months in Nebraska, I arrived in New York laden with valuable collections near the end of) Tj ET Q +1.649 Tw +q 0 g BT 31.19 663.96 Td (March. My departure for France was set for early May. In the meantime, then, I was busy classifying my) Tj ET Q +0 Tw +q 0 g BT 31.19 649.78 Td (mineralogical, botanical, and zoological treasures when that incident took place with the Scotia.) Tj ET Q +0.471 Tw +q 0 g BT 31.19 635.61 Td (I was perfectly abreast of this question, which was the big news of the day, and how could I not have been? I) Tj ET Q +0.782 Tw +q 0 g BT 31.19 621.44 Td (had read and reread every American and European newspaper without being any farther along. This mystery) Tj ET Q +0.516 Tw +q 0 g BT 31.19 607.26 Td (puzzled me. Finding it impossible to form any views, I drifted from one extreme to the other. Something was) Tj ET Q +1.601 Tw +q 0 g BT 31.19 593.09 Td (out there, that much was certain, and any doubting Thomas was invited to place his finger on the Scotia's) Tj ET Q +0 Tw +q 0 g BT 31.19 578.92 Td (wound.) Tj ET Q +1.249 Tw +q 0 g BT 31.19 564.74 Td (When I arrived in New York, the question was at the boiling point. The hypothesis of a drifting islet or an) Tj ET Q +1.561 Tw +q 0 g BT 31.19 550.57 Td (elusive reef, put forward by people not quite in their right minds, was completely eliminated. And indeed,) Tj ET Q +0 Tw +q 0 g BT 31.19 536.40 Td (unless this reef had an engine in its belly, how could it move about with such prodigious speed?) Tj ET Q +0.779 Tw +q 0 g BT 31.19 522.22 Td (Also discredited was the idea of a floating hull or some other enormous wreckage, and again because of this) Tj ET Q +0 Tw +q 0 g BT 31.19 508.05 Td (speed of movement.) Tj ET Q +1.114 Tw +q 0 g BT 31.19 493.88 Td (So only two possible solutions to the question were left, creating two very distinct groups of supporters: on) Tj ET Q +0.914 Tw +q 0 g BT 31.19 479.70 Td (one side, those favoring a monster of colossal strength; on the other, those favoring an "underwater boat" of) Tj ET Q +0 Tw +q 0 g BT 31.19 465.53 Td (tremendous motor power.) Tj ET Q +3.674 Tw +q 0 g BT 31.19 451.36 Td (Now then, although the latter hypothesis was completely admissible, it couldn't stand up to inquiries) Tj ET Q +0.227 Tw +q 0 g BT 31.19 437.18 Td (conducted in both the New World and the Old. That a private individual had such a mechanism at his disposal) Tj ET Q +0 Tw +q 0 g BT 31.19 423.01 Td (was less than probable. Where and when had he built it, and how could he have built it in secret?) Tj ET Q +0.395 Tw +q 0 g BT 31.19 408.84 Td (Only some government could own such an engine of destruction, and in these disaster-filled times, when men) Tj ET Q +1.331 Tw +q 0 g BT 31.19 394.66 Td (tax their ingenuity to build increasingly powerful aggressive weapons, it was possible that, unknown to the) Tj ET Q +0.106 Tw +q 0 g BT 31.19 380.49 Td (rest of the world, some nation could have been testing such a fearsome machine. The Chassepot rifle led to the) Tj ET Q +0.490 Tw +q 0 g BT 31.19 366.32 Td (torpedo, and the torpedo has led to this underwater battering ram, which in turn will lead to the world putting) Tj ET Q +0 Tw +q 0 g BT 31.19 352.14 Td (its foot down. At least I hope it will.) Tj ET Q +1.078 Tw +q 0 g BT 31.19 337.97 Td (But this hypothesis of a war machine collapsed in the face of formal denials from the various governments.) Tj ET Q +0.251 Tw +q 0 g BT 31.19 323.80 Td (Since the public interest was at stake and transoceanic travel was suffering, the sincerity of these governments) Tj ET Q +0.979 Tw +q 0 g BT 31.19 309.63 Td (could not be doubted. Besides, how could the assembly of this underwater boat have escaped public notice?) Tj ET Q +3.430 Tw +q 0 g BT 31.19 295.45 Td (Keeping a secret under such circumstances would be difficult enough for an individual, and certainly) Tj ET Q +0 Tw +q 0 g BT 31.19 281.28 Td (impossible for a nation whose every move is under constant surveillance by rival powers.) Tj ET Q +0.422 Tw +q 0 g BT 31.19 267.11 Td (So, after inquiries conducted in England, France, Russia, Prussia, Spain, Italy, America, and even Turkey, the) Tj ET Q +0 Tw +q 0 g BT 31.19 252.93 Td (hypothesis of an underwater Monitor was ultimately rejected.) Tj ET Q +2.481 Tw +q 0 g BT 31.19 238.76 Td (After I arrived in New York, several people did me the honor of consulting me on the phenomenon in) Tj ET Q +0.569 Tw +q 0 g BT 31.19 224.59 Td (question. In France I had published a two-volume work, in quarto, entitled The Mysteries of the Great Ocean) Tj ET Q +0.862 Tw +q 0 g BT 31.19 210.41 Td (Depths. Well received in scholarly circles, this book had established me as a specialist in this pretty obscure) Tj ET Q +1.833 Tw +q 0 g BT 31.19 196.24 Td (field of natural history. My views were in demand. As long as I could deny the reality of the business, I) Tj ET Q +0.058 Tw +q 0 g BT 31.19 182.07 Td (confined myself to a flat "no comment." But soon, pinned to the wall, I had to explain myself straight out. And) Tj ET Q +1.637 Tw +q 0 g BT 31.19 167.89 Td (in this vein, "the honorable Pierre Aronnax, Professor at the Paris Museum," was summoned by The New) Tj ET Q +0 Tw +q 0 g BT 31.19 153.72 Td (York Herald to formulate his views no matter what.) Tj ET Q +0.697 Tw +q 0 g BT 31.19 139.55 Td (I complied. Since I could no longer hold my tongue, I let it wag. I discussed the question in its every aspect,) Tj ET Q +0.017 Tw +q 0 g BT 31.19 125.37 Td (both political and scientific, and this is an excerpt from the well-padded article I published in the issue of April) Tj ET Q +0 Tw +q 0 g BT 31.19 111.20 Td (30.) Tj ET Q +2.226 Tw +q 0 g BT 31.19 82.85 Td ("Therefore," I wrote, "after examining these different hypotheses one by one, we are forced, every other) Tj ET Q +0 Tw +q 0 g BT 31.19 68.68 Td (supposition having been refuted, to accept the existence of an extremely powerful marine animal.) Tj ET Q +0.550 Tw +0 Tw +BT /F4 8.00 Tf ET +q 0.502 g BT 284.96 25.95 Td (Page 3) Tj ET Q + +endstream +endobj +9 0 obj +<> +endobj +10 0 obj +<> +stream +2 J +0.57 w +BT /F3 12.00 Tf ET +BT /F1 15.00 Tf ET +0.000 0.314 0.706 RG +0.902 0.902 0.000 rg +2.83 w +179.09 813.54 237.10 -25.51 re B q 0.863 0.196 0.196 rg BT 187.59 796.28 Td (20000 Leagues Under the Seas) Tj ET Q +0.57 w +BT /F3 12.00 Tf ET +0.784 0.863 1.000 rg +0 G +0.550 Tw +q 0 g BT 31.19 749.00 Td ("The deepest parts of the ocean are totally unknown to us. No soundings have been able to reach them. What) Tj ET Q +0.352 Tw +q 0 g BT 31.19 734.82 Td (goes on in those distant depths? What creatures inhabit, or could inhabit, those regions twelve or fifteen miles) Tj ET Q +0 Tw +q 0 g BT 31.19 720.65 Td (beneath the surface of the water? What is the constitution of these animals? It's almost beyond conjecture.) Tj ET Q +3.495 Tw +q 0 g BT 31.19 706.48 Td ("However, the solution to this problem submitted to me can take the form of a choice between two) Tj ET Q +0 Tw +q 0 g BT 31.19 692.30 Td (alternatives.) Tj ET Q +q 0 g BT 31.19 678.13 Td ("Either we know every variety of creature populating our planet, or we do not.) Tj ET Q +1.250 Tw +q 0 g BT 31.19 663.96 Td ("If we do not know every one of them, if nature still keeps ichthyological secrets from us, nothing is more) Tj ET Q +0.231 Tw +q 0 g BT 31.19 649.78 Td (admissible than to accept the existence of fish or cetaceans of new species or even new genera, animals with a) Tj ET Q +3.022 Tw +q 0 g BT 31.19 635.61 Td (basically 'cast-iron' constitution that inhabit strata beyond the reach of our soundings, and which some) Tj ET Q +1.589 Tw +q 0 g BT 31.19 621.44 Td (development or other, an urge or a whim if you prefer, can bring to the upper level of the ocean for long) Tj ET Q +0 Tw +q 0 g BT 31.19 607.26 Td (intervals.) Tj ET Q +0.321 Tw +q 0 g BT 31.19 593.09 Td ("If, on the other hand, we do know every living species, we must look for the animal in question among those) Tj ET Q +1.409 Tw +q 0 g BT 31.19 578.92 Td (marine creatures already cataloged, and in this event I would be inclined to accept the existence of a giant) Tj ET Q +0 Tw +q 0 g BT 31.19 564.74 Td (narwhale.) Tj ET Q +0.008 Tw +q 0 g BT 31.19 550.57 Td ("The common narwhale, or sea unicorn, often reaches a length of sixty feet. Increase its dimensions fivefold or) Tj ET Q +0.352 Tw +q 0 g BT 31.19 536.40 Td (even tenfold, then give this cetacean a strength in proportion to its size while enlarging its offensive weapons,) Tj ET Q +1.251 Tw +q 0 g BT 31.19 522.22 Td (and you have the animal we're looking for. It would have the proportions determined by the officers of the) Tj ET Q +0 Tw +q 0 g BT 31.19 508.05 Td (Shannon, the instrument needed to perforate the Scotia, and the power to pierce a steamer's hull.) Tj ET Q +0.130 Tw +q 0 g BT 31.19 493.88 Td ("In essence, the narwhale is armed with a sort of ivory sword, or lance, as certain naturalists have expressed it.) Tj ET Q +1.326 Tw +q 0 g BT 31.19 479.70 Td (It's a king-sized tooth as hard as steel. Some of these teeth have been found buried in the bodies of baleen) Tj ET Q +3.771 Tw +q 0 g BT 31.19 465.53 Td (whales, which the narwhale attacks with invariable success. Others have been wrenched, not without) Tj ET Q +0.119 Tw +q 0 g BT 31.19 451.36 Td (difficulty, from the undersides of vessels that narwhales have pierced clean through, as a gimlet pierces a wine) Tj ET Q +0.649 Tw +q 0 g BT 31.19 437.18 Td (barrel. The museum at the Faculty of Medicine in Paris owns one of these tusks with a length of 2.25 meters) Tj ET Q +0 Tw +q 0 g BT 31.19 423.01 Td (and a width at its base of forty-eight centimeters!) Tj ET Q +0.467 Tw +q 0 g BT 31.19 408.84 Td ("All right then! Imagine this weapon to be ten times stronger and the animal ten times more powerful, launch) Tj ET Q +0.980 Tw +q 0 g BT 31.19 394.66 Td (it at a speed of twenty miles per hour, multiply its mass times its velocity, and you get just the collision we) Tj ET Q +0 Tw +q 0 g BT 31.19 380.49 Td (need to cause the specified catastrophe.) Tj ET Q +1.067 Tw +q 0 g BT 31.19 366.32 Td ("So, until information becomes more abundant, I plump for a sea unicorn of colossal dimensions, no longer) Tj ET Q +0.631 Tw +q 0 g BT 31.19 352.14 Td (armed with a mere lance but with an actual spur, like ironclad frigates or those warships called 'rams,' whose) Tj ET Q +0 Tw +q 0 g BT 31.19 337.97 Td (mass and motor power it would possess simultaneously.) Tj ET Q +1.992 Tw +q 0 g BT 31.19 323.80 Td ("This inexplicable phenomenon is thus explained away--unless it's something else entirely, which, despite) Tj ET Q +0 Tw +q 0 g BT 31.19 309.63 Td (everything that has been sighted, studied, explored and experienced, is still possible!" +) Tj ET Q +BT /F5 12.00 Tf ET +q 0 g BT 31.19 281.28 Td (\(end of extract\)) Tj ET Q +BT /F4 8.00 Tf ET +q 0.502 g BT 284.96 25.95 Td (Page 4) Tj ET Q + +endstream +endobj +1 0 obj +<> +endobj +11 0 obj +<> +endobj +12 0 obj +<> +endobj +13 0 obj +<> +endobj +14 0 obj +<> +endobj +15 0 obj +<> +endobj +2 0 obj +<> +>> +endobj +16 0 obj +<< +/Producer (Horde PDF) +/Title (20000 Leagues Under the Seas) +/Author (Jules Verne) +/CreationDate (D:%d) +>> +endobj +17 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 18 +0000000000 65535 f +0000020852 00000 n +0000021460 00000 n +0000000009 00000 n +0000000087 00000 n +0000007099 00000 n +0000007177 00000 n +0000009074 00000 n +0000009152 00000 n +0000016015 00000 n +0000016094 00000 n +0000020957 00000 n +0000021059 00000 n +0000021156 00000 n +0000021255 00000 n +0000021360 00000 n +0000021593 00000 n +0000021730 00000 n +trailer +<< +/Size 18 +/Root 17 0 R +/Info 16 0 R +>> +startxref +21834 +%%EOF diff --git a/framework/File_PDF/tests/text_color.phpt b/framework/File_PDF/tests/text_color.phpt new file mode 100644 index 000000000..ba3da1c1f --- /dev/null +++ b/framework/File_PDF/tests/text_color.phpt @@ -0,0 +1,101 @@ +--TEST-- +File_PDF: Text colors. +--FILE-- +setCompression(false); +// Start the document. +$pdf->open(); +// Start a page. +$pdf->addPage(); +// Set font to Helvetica bold 24 pt. +$pdf->setFont('Helvetica', 'B', 48); +// Set colors. +$pdf->setDrawColor('rgb', 50, 0, 0); +$pdf->setTextColor('rgb', 0, 50, 0); +$pdf->setFillColor('rgb', 0, 0, 50); +// Write text. +$pdf->cell(0, 50, 'Hello Colors', 1, 0, 'C', 1); +// Print the generated file. +echo $pdf->getOutput(); + +?> +--EXPECTF-- +%PDF-1.3 +3 0 obj +<> +endobj +4 0 obj +<> +stream +2 J +0.57 w +BT /F1 48.00 Tf ET +50.000 0.000 0.000 RG +0.000 0.000 50.000 rg +28.35 813.54 538.58 -141.73 re B q 0.000 50.000 0.000 rg BT 156.28 728.27 Td (Hello Colors) Tj ET Q + +endstream +endobj +1 0 obj +<> +endobj +5 0 obj +<> +endobj +2 0 obj +<> +>> +endobj +6 0 obj +<< +/Producer (Horde PDF) +/CreationDate (D:%d) +>> +endobj +7 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 8 +0000000000 65535 f +0000000310 00000 n +0000000498 00000 n +0000000009 00000 n +0000000087 00000 n +0000000397 00000 n +0000000586 00000 n +0000000662 00000 n +trailer +<< +/Size 8 +/Root 7 0 R +/Info 6 0 R +>> +startxref +765 +%%EOF diff --git a/framework/File_PDF/tests/underline.phpt b/framework/File_PDF/tests/underline.phpt new file mode 100644 index 000000000..e2fa2b157 --- /dev/null +++ b/framework/File_PDF/tests/underline.phpt @@ -0,0 +1,99 @@ +--TEST-- +File_PDF: Underline test +--FILE-- + 'P', 'format' => 'A4')); +// Start the document. +$pdf->open(); +// Deactivate compression. +$pdf->setCompression(false); +// Start a page. +$pdf->addPage(); +// Set font to Courier 8 pt. +$pdf->setFont('Helvetica', 'U', 12); +// Write underlined text. +$pdf->write(15, "Underlined\n"); +// Write linked text. +$pdf->write(15, 'Horde', 'http://www.horde.org'); +// Print the generated file. +echo $pdf->getOutput(); + +?> +--EXPECTF-- +%PDF-1.3 +3 0 obj +<>>>] +/Contents 4 0 R>> +endobj +4 0 obj +<> +stream +2 J +0.57 w +BT /F1 12.00 Tf ET +BT 31.19 788.68 Td (Underlined) Tj ET 31.19 787.48 58.02 -0.60 re f +BT 31.19 746.16 Td (Horde) Tj ET 31.19 744.96 32.68 -0.60 re f + +endstream +endobj +1 0 obj +<> +endobj +5 0 obj +<> +endobj +2 0 obj +<> +>> +endobj +6 0 obj +<< +/Producer (Horde PDF) +/CreationDate (D:%d) +>> +endobj +7 0 obj +<< +/Type /Catalog +/Pages 1 0 R +/OpenAction [3 0 R /FitH null] +/PageLayout /OneColumn +>> +endobj +xref +0 8 +0000000000 65535 f +0000000432 00000 n +0000000615 00000 n +0000000009 00000 n +0000000222 00000 n +0000000519 00000 n +0000000703 00000 n +0000000779 00000 n +trailer +<< +/Size 8 +/Root 7 0 R +/Info 6 0 R +>> +startxref +882 +%%EOF diff --git a/framework/Form/Form.php b/framework/Form/Form.php new file mode 100644 index 000000000..659eecb23 --- /dev/null +++ b/framework/Form/Form.php @@ -0,0 +1,830 @@ + + * Copyright 2001-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Robert E. Coyle + * @author Chuck Hagenbuch + * @since Horde 3.0 + * @package Horde_Form + */ +class Horde_Form { + + protected $_name = ''; + protected $_title = ''; + protected $_extra = ''; + protected $_vars; + protected $_submit = array(); + protected $_reset = false; + protected $_errors = array(); + protected $_submitted = null; + public $_sections = array(); + protected $_open_section = null; + protected $_currentSection = array(); + protected $_variables = array(); + protected $_hiddenVariables = array(); + protected $_useFormToken = true; + protected $_autofilled = false; + protected $_enctype = null; + public $_help = false; + + function Horde_Form(&$vars, $title = '', $name = null) + { + if (empty($name)) { + $name = Horde_String::lower(get_class($this)); + } + + $this->_vars = &$vars; + $this->_title = $title; + $this->_name = $name; + } + + function __construct($vars, $title = '', $name = null) + { + $this->Horde_Form($vars, $title, $name); + } + + function &singleton($form, &$vars, $title = '', $name = null) + { + static $instances = array(); + + $signature = serialize(array($form, $vars, $title, $name)); + if (!isset($instances[$signature])) { + if (class_exists($form)) { + $instances[$signature] = new $form($vars, $title, $name); + } else { + $instances[$signature] = new Horde_Form($vars, $title, $name); + } + } + + return $instances[$signature]; + } + + function setVars(&$vars) + { + $this->_vars = &$vars; + } + + function getVars() + { + return $this->_vars; + } + + function getTitle() + { + return $this->_title; + } + + function setTitle($title) + { + $this->_title = $title; + } + + function getExtra() + { + return $this->_extra; + } + + function setExtra($extra) + { + $this->_extra = $extra; + } + + function getName() + { + return $this->_name; + } + + /** + * Sets or gets whether the form should be verified by tokens. + * Tokens are used to verify that a form is only submitted once. + * + * @param boolean $token If specified, sets whether to use form tokens. + * + * @return boolean Whether form tokens are being used. + */ + function useToken($token = null) + { + if (!is_null($token)) { + $this->_useFormToken = $token; + } + return $this->_useFormToken; + } + + /** + * Get the renderer for this form, either a custom renderer or the + * standard one. + * + * To use a custom form renderer, your form class needs to + * override this function: + * + * function &getRenderer() + * { + * $r = new CustomFormRenderer(); + * return $r; + * } + * + * + * ... where CustomFormRenderer is the classname of the custom + * renderer class, which should extend Horde_Form_Renderer. + * + * @param array $params A hash of renderer-specific parameters. + * + * @return object Horde_Form_Renderer The form renderer. + */ + function getRenderer($params = array()) + { + require_once 'Horde/Form/Renderer.php'; + $renderer = new Horde_Form_Renderer($params); + return $renderer; + } + + /** + * @throws Horde_Exception + */ + function getType($type, $params = array()) + { + $type_class = 'Horde_Form_Type_' . $type; + if (!class_exists($type_class)) { + throw new Horde_Exception(sprintf('Nonexistant class "%s" for field type "%s"', $type_class, $type)); + } + $type_ob = new $type_class(); + call_user_func_array(array($type_ob, 'init'), $params); + return $type_ob; + } + + function setSection($section = '', $desc = '', $image = '', $expanded = true) + { + $this->_currentSection = $section; + if (!count($this->_sections) && !$this->getOpenSection()) { + $this->setOpenSection($section); + } + $this->_sections[$section]['desc'] = $desc; + $this->_sections[$section]['expanded'] = $expanded; + $this->_sections[$section]['image'] = $image; + } + + function getSectionDesc($section) + { + return $this->_sections[$section]['desc']; + } + + function getSectionImage($section) + { + return $this->_sections[$section]['image']; + } + + function setOpenSection($section) + { + $this->_vars->set('__formOpenSection', $section); + } + + function getOpenSection() + { + return $this->_vars->get('__formOpenSection'); + } + + function getSectionExpandedState($section, $boolean = false) + { + if ($boolean) { + /* Only the boolean value is required. */ + return $this->_sections[$section]['expanded']; + } + + /* Need to return the values for use in styles. */ + if ($this->_sections[$section]['expanded']) { + return 'block'; + } else { + return 'none'; + } + } + + /** + * TODO + */ + function &addVariable($humanName, $varName, $type, $required, + $readonly = false, $description = null, + $params = array()) + { + return $this->insertVariableBefore(null, $humanName, $varName, $type, + $required, $readonly, $description, + $params); + } + + /** + * TODO + */ + function &insertVariableBefore($before, $humanName, $varName, $type, + $required, $readonly = false, + $description = null, $params = array()) + { + $type = &$this->getType($type, $params); + $var = new Horde_Form_Variable($humanName, $varName, $type, + $required, $readonly, $description); + + /* Set the form object reference in the var. */ + $var->setFormOb($this); + + if ($var->getTypeName() == 'enum' && + !strlen($type->getPrompt()) && + count($var->getValues()) == 1) { + $vals = array_keys($var->getValues()); + $this->_vars->add($var->varName, $vals[0]); + $var->_autofilled = true; + } elseif ($var->getTypeName() == 'file' || + $var->getTypeName() == 'image') { + $this->_enctype = 'multipart/form-data'; + } + if (empty($this->_currentSection)) { + $this->_currentSection = '__base'; + } + + if (is_null($before)) { + $this->_variables[$this->_currentSection][] = &$var; + } else { + $num = 0; + while (isset($this->_variables[$this->_currentSection][$num]) && + $this->_variables[$this->_currentSection][$num]->getVarName() != $before) { + $num++; + } + if (!isset($this->_variables[$this->_currentSection][$num])) { + $this->_variables[$this->_currentSection][] = &$var; + } else { + $this->_variables[$this->_currentSection] = array_merge( + array_slice($this->_variables[$this->_currentSection], 0, $num), + array(&$var), + array_slice($this->_variables[$this->_currentSection], $num)); + } + } + + return $var; + } + + /** + * Removes a variable from the form. + * + * As only variables can be passed by reference, you need to call this + * method this way if want to pass a variable name: + * + * $form->removeVariable($var = 'varname'); + * + * + * @param Horde_Form_Variable|string $var Either the variable's name or + * the variable to remove from the + * form. + * + * @return boolean True if the variable was found (and deleted). + */ + function removeVariable(&$var) + { + foreach (array_keys($this->_variables) as $section) { + foreach (array_keys($this->_variables[$section]) as $i) { + if ((is_a($var, 'Horde_Form_Variable') && $this->_variables[$section][$i] === $var) || + ($this->_variables[$section][$i]->getVarName() == $var)) { + // Slice out the variable to be removed. + $this->_variables[$this->_currentSection] = array_merge( + array_slice($this->_variables[$this->_currentSection], 0, $i), + array_slice($this->_variables[$this->_currentSection], $i + 1)); + + return true; + } + } + } + + return false; + } + + /** + * TODO + */ + function &addHidden($humanName, $varName, $type, $required, + $readonly = false, $description = null, + $params = array()) + { + $type = &$this->getType($type, $params); + $var = new Horde_Form_Variable($humanName, $varName, $type, + $required, $readonly, $description); + $var->hide(); + $this->_hiddenVariables[] = &$var; + return $var; + } + + function &getVariables($flat = true, $withHidden = false) + { + if ($flat) { + $vars = array(); + foreach ($this->_variables as $section) { + foreach ($section as $var) { + $vars[] = $var; + } + } + if ($withHidden) { + foreach ($this->_hiddenVariables as $var) { + $vars[] = $var; + } + } + return $vars; + } else { + return $this->_variables; + } + } + + function setButtons($submit, $reset = false) + { + if ($submit === true || is_null($submit) || empty($submit)) { + /* Default to 'Submit'. */ + $submit = array(_("Submit")); + } elseif (!is_array($submit)) { + /* Default to array if not passed. */ + $submit = array($submit); + } + /* Only if $reset is strictly true insert default 'Reset'. */ + if ($reset === true) { + $reset = _("Reset"); + } + + $this->_submit = $submit; + $this->_reset = $reset; + } + + function appendButtons($submit) + { + if (!is_array($submit)) { + $submit = array($submit); + } + + $this->_submit = array_merge($this->_submit, $submit); + } + + function preserveVarByPost(&$vars, $varname, $alt_varname = '') + { + $value = $vars->getExists($varname, $wasset); + + /* If an alternate name is given under which to preserve use that. */ + if ($alt_varname) { + $varname = $alt_varname; + } + + if ($wasset) { + $this->_preserveVarByPost($varname, $value); + } + } + + /** + * @access private + */ + function _preserveVarByPost($varname, $value) + { + if (is_array($value)) { + foreach ($value as $id => $val) { + $this->_preserveVarByPost($varname . '[' . $id . ']', $val); + } + } else { + $varname = htmlspecialchars($varname); + $value = htmlspecialchars($value); + printf('' . "\n", + $varname, + $value); + } + } + + function open(&$renderer, &$vars, $action, $method = 'get', $enctype = null) + { + if (is_null($enctype) && !is_null($this->_enctype)) { + $enctype = $this->_enctype; + } + $renderer->open($action, $method, $this->_name, $enctype); + $renderer->listFormVars($this); + + if (!empty($this->_name)) { + $this->_preserveVarByPost('formname', $this->_name); + } + + if ($this->_useFormToken) { + require_once 'Horde/Token.php'; + $token = Horde_Token::generateId($this->_name); + $_SESSION['horde_form_secrets'][$token] = true; + $this->_preserveVarByPost($this->_name . '_formToken', $token); + } + + /* Loop through vars and check for any special cases to preserve. */ + $variables = $this->getVariables(); + foreach ($variables as $var) { + /* Preserve value if change has to be tracked. */ + if ($var->getOption('trackchange')) { + $varname = $var->getVarName(); + $this->preserveVarByPost($vars, $varname, '__old_' . $varname); + } + } + + foreach ($this->_hiddenVariables as $var) { + $this->preserveVarByPost($vars, $var->getVarName()); + } + } + + function close($renderer) + { + $renderer->close(); + } + + /** + * Renders the form for editing. + * + * @param Horde_Form_Renderer $renderer A renderer instance, optional + * since Horde 3.2. + * @param Variables $vars A Variables instance, optional + * since Horde 3.2. + * @param string $action The form action (url). + * @param string $method The form method, usually either + * 'get' or 'post'. + * @param string $enctype The form encoding type. Determined + * automatically if null. + * @param boolean $focus Focus the first form field? + */ + function renderActive($renderer = null, $vars = null, $action = '', + $method = 'get', $enctype = null, $focus = true) + { + if (is_null($renderer)) { + $renderer = $this->getRenderer(); + } + if (is_null($vars)) { + $vars = $this->_vars; + } + + if (is_null($enctype) && !is_null($this->_enctype)) { + $enctype = $this->_enctype; + } + $renderer->open($action, $method, $this->getName(), $enctype); + $renderer->listFormVars($this); + + if (!empty($this->_name)) { + $this->_preserveVarByPost('formname', $this->_name); + } + + if ($this->_useFormToken) { + require_once 'Horde/Token.php'; + $token = Horde_Token::generateId($this->_name); + $_SESSION['horde_form_secrets'][$token] = true; + $this->_preserveVarByPost($this->_name . '_formToken', $token); + } + + if (count($this->_sections)) { + $this->_preserveVarByPost('__formOpenSection', $this->getOpenSection()); + } + + /* Loop through vars and check for any special cases to + * preserve. */ + $variables = $this->getVariables(); + foreach ($variables as $var) { + /* Preserve value if change has to be tracked. */ + if ($var->getOption('trackchange')) { + $varname = $var->getVarName(); + $this->preserveVarByPost($vars, $varname, '__old_' . $varname); + } + } + + foreach ($this->_hiddenVariables as $var) { + $this->preserveVarByPost($vars, $var->getVarName()); + } + + $renderer->beginActive($this->getTitle(), $this->getExtra()); + $renderer->renderFormActive($this, $vars); + $renderer->submit($this->_submit, $this->_reset); + $renderer->end(); + $renderer->close($focus); + } + + /** + * Renders the form for displaying. + * + * @param Horde_Form_Renderer $renderer A renderer instance, optional + * since Horde 3.2. + * @param Variables $vars A Variables instance, optional + * since Horde 3.2. + */ + function renderInactive($renderer = null, $vars = null) + { + if (is_null($renderer)) { + $renderer = $this->getRenderer(); + } + if (is_null($vars)) { + $vars = $this->_vars; + } + + $renderer->_name = $this->_name; + $renderer->beginInactive($this->getTitle(), $this->getExtra()); + $renderer->renderFormInactive($this, $vars); + $renderer->end(); + } + + function preserve($vars) + { + if ($this->_useFormToken) { + require_once 'Horde/Token.php'; + $token = Horde_Token::generateId($this->_name); + $_SESSION['horde_form_secrets'][$token] = true; + $this->_preserveVarByPost($this->_name . '_formToken', $token); + } + + $variables = $this->getVariables(); + foreach ($variables as $var) { + $varname = $var->getVarName(); + + /* Save value of individual components. */ + switch ($var->getTypeName()) { + case 'passwordconfirm': + case 'emailconfirm': + $this->preserveVarByPost($vars, $varname . '[original]'); + $this->preserveVarByPost($vars, $varname . '[confirm]'); + break; + + case 'monthyear': + $this->preserveVarByPost($vars, $varname . '[month]'); + $this->preserveVarByPost($vars, $varname . '[year]'); + break; + + case 'monthdayyear': + $this->preserveVarByPost($vars, $varname . '[month]'); + $this->preserveVarByPost($vars, $varname . '[day]'); + $this->preserveVarByPost($vars, $varname . '[year]'); + break; + } + + $this->preserveVarByPost($vars, $varname); + } + foreach ($this->_hiddenVariables as $var) { + $this->preserveVarByPost($vars, $var->getVarName()); + } + } + + function unsetVars(&$vars) + { + foreach ($this->getVariables() as $var) { + $vars->remove($var->getVarName()); + } + } + + /** + * Validates the form, checking if it really has been submitted by calling + * isSubmitted() and if true does any onSubmit() calls for variable types + * in the form. The _submitted variable is then rechecked. + * + * @param Variables $vars A Variables instance, optional since Horde + * 3.2. + * @param boolean $canAutofill Can the form be valid without being + * submitted? + * + * @return boolean True if the form is valid. + */ + function validate($vars = null, $canAutoFill = false) + { + if (is_null($vars)) { + $vars = $this->_vars; + } + + /* Get submitted status. */ + if ($this->isSubmitted() || $canAutoFill) { + /* Form was submitted or can autofill; check for any variable + * types' onSubmit(). */ + $this->onSubmit($vars); + + /* Recheck submitted status. */ + if (!$this->isSubmitted() && !$canAutoFill) { + return false; + } + } else { + /* Form has not been submitted; return false. */ + return false; + } + + $message = ''; + $this->_autofilled = true; + + if ($this->_useFormToken) { + global $conf; + require_once 'Horde/Token.php'; + if (isset($conf['token'])) { + /* If there is a configured token system, set it up. */ + $tokenSource = &Horde_Token::singleton($conf['token']['driver'], Horde::getDriverConfig('token', $conf['token']['driver'])); + } else { + /* Default to the file system if no config. */ + $tokenSource = &Horde_Token::singleton('file'); + } + $passedToken = $vars->get($this->_name . '_formToken'); + if (!empty($passedToken) && !$tokenSource->verify($passedToken)) { + $this->_errors['_formToken'] = _("This form has already been processed."); + } + if (empty($_SESSION['horde_form_secrets'][$passedToken])) { + $this->_errors['_formSecret'] = _("Required secret is invalid - potentially malicious request."); + } + } + + foreach ($this->getVariables() as $var) { + $this->_autofilled = $var->_autofilled && $this->_autofilled; + if (!$var->validate($vars, $message)) { + $this->_errors[$var->getVarName()] = $message; + } + } + + if ($this->_autofilled) { + unset($this->_errors['_formToken']); + } + + foreach ($this->_hiddenVariables as $var) { + if (!$var->validate($vars, $message)) { + $this->_errors[$var->getVarName()] = $message; + } + } + + return $this->isValid(); + } + + function clearValidation() + { + $this->_errors = array(); + } + + function getError($var) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + return isset($this->_errors[$name]) ? $this->_errors[$name] : null; + } + + function setError($var, $message) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + $this->_errors[$name] = $message; + } + + function clearError($var) + { + if (is_a($var, 'Horde_Form_Variable')) { + $name = $var->getVarName(); + } else { + $name = $var; + } + unset($this->_errors[$name]); + } + + function isValid() + { + return ($this->_autofilled || count($this->_errors) == 0); + } + + function execute() + { + Horde::logMessage('Warning: Horde_Form::execute() called, should be overridden', __FILE__, __LINE__, PEAR_LOG_DEBUG); + } + + /** + * Fetch the field values of the submitted form. + * + * @param Variables $vars A Variables instance, optional since Horde 3.2. + * @param array $info Array to be filled with the submitted field + * values. + */ + function getInfo($vars, &$info) + { + if (is_null($vars)) { + $vars = $this->_vars; + } + $this->_getInfoFromVariables($this->getVariables(), $vars, $info); + $this->_getInfoFromVariables($this->_hiddenVariables, $vars, $info); + } + + /** + * Fetch the field values from a given array of variables. + * + * @access private + * + * @param array $variables An array of Horde_Form_Variable objects to + * fetch from. + * @param object $vars The Variables object. + * @param array $info The array to be filled with the submitted + * field values. + */ + function _getInfoFromVariables($variables, &$vars, &$info) + { + foreach ($variables as $var) { + if ($var->isArrayVal()) { + $var->getInfo($vars, $values); + if (is_array($values)) { + $varName = str_replace('[]', '', $var->getVarName()); + foreach ($values as $i => $val) { + $info[$i][$varName] = $val; + } + } + } else { + require_once 'Horde/Array.php'; + if (Horde_Array::getArrayParts($var->getVarName(), $base, $keys)) { + if (!isset($info[$base])) { + $info[$base] = array(); + } + $pointer = &$info[$base]; + while (count($keys)) { + $key = array_shift($keys); + if (!isset($pointer[$key])) { + $pointer[$key] = array(); + } + $pointer = &$pointer[$key]; + } + $var->getInfo($vars, $pointer); + } else { + $var->getInfo($vars, $info[$var->getVarName()]); + } + } + } + } + + function hasHelp() + { + return $this->_help; + } + + /** + * Determines if this form has been submitted or not. If the class + * var _submitted is null then it will check for the presence of + * the formname in the form variables. + * + * Other events can explicitly set the _submitted variable to + * false to indicate a form submit but not for actual posting of + * data (eg. onChange events to update the display of fields). + * + * @return boolean True or false indicating if the form has been + * submitted. + */ + function isSubmitted() + { + if (is_null($this->_submitted)) { + if ($this->_vars->get('formname') == $this->getName()) { + $this->_submitted = true; + } else { + $this->_submitted = false; + } + } + + return $this->_submitted; + } + + /** + * Checks if there is anything to do on the submission of the form by + * looping through each variable's onSubmit() function. + * + * @param Horde_Variables $vars + */ + function onSubmit(&$vars) + { + /* Loop through all vars and check if there's anything to do on + * submit. */ + $variables = $this->getVariables(); + foreach ($variables as $var) { + $var->type->onSubmit($var, $vars); + /* If changes to var being tracked don't register the form as + * submitted if old value and new value differ. */ + if ($var->getOption('trackchange')) { + $varname = $var->getVarName(); + if (!is_null($vars->get('formname')) && + $vars->get($varname) != $vars->get('__old_' . $varname)) { + $this->_submitted = false; + } + } + } + } + + /** + * Explicitly sets the state of the form submit. + * + * An event can override the automatic determination of the submit state + * in the isSubmitted() function. + * + * @param boolean $state Whether to set the state of the form as being + * submitted. + */ + function setSubmitted($state = true) + { + $this->_submitted = $state; + } + +} diff --git a/framework/Form/Form/Action.php b/framework/Form/Form/Action.php new file mode 100644 index 000000000..ff5e21112 --- /dev/null +++ b/framework/Form/Form/Action.php @@ -0,0 +1,139 @@ + + * @package Horde_Form + */ +class Horde_Form_Action { + + var $_id; + var $_params; + var $_trigger = null; + + function Horde_Form_Action($params = null) + { + $this->_params = $params; + $this->_id = md5(mt_rand()); + } + + function getTrigger() + { + return $this->_trigger; + } + + function id() + { + return $this->_id; + } + + function getActionScript($form, $renderer, $varname) + { + return ''; + } + + function printJavaScript() + { + } + + function _printJavaScriptStart() + { + echo ''; + } + + function getTarget() + { + return isset($this->_params['target']) ? $this->_params['target'] : null; + } + + function setValues(&$vars, $sourceVal, $index = null, $arrayVal = false) + { + } + + /** + * Attempts to return a concrete Horde_Form_Action instance + * based on $form. + * + * @param mixed $action The type of concrete Horde_Form_Action subclass + * to return. If $action is an array, then we will look + * in $action[0]/lib/Form/Action/ for the subclass + * implementation named $action[1].php. + * @param array $params A hash containing any additional configuration a + * form might need. + * + * @return Horde_Form_Action The concrete Horde_Form_Action reference, or + * false on an error. + */ + function &factory($action, $params = null) + { + if (is_array($action)) { + $app = $action[0]; + $action = $action[1]; + } + + $action = basename($action); + $class = 'Horde_Form_Action_' . $action; + if (!class_exists($class)) { + if (!empty($app)) { + include_once $GLOBALS['registry']->get('fileroot', $app) . '/lib/Form/Action/' . $action . '.php'; + } else { + include_once 'Horde/Form/Action/' . $action . '.php'; + } + } + + if (class_exists($class)) { + $instance = new $class($params); + } else { + $instance = PEAR::raiseError('Class definition of ' . $class . ' not found.'); + } + + return $instance; + } + + /** + * Attempts to return a reference to a concrete + * Horde_Form_Action instance based on $action. It will only + * create a new instance if no Horde_Form_Action instance with + * the same parameters currently exists. + * + * This should be used if multiple types of form renderers (and, + * thus, multiple Horde_Form_Action instances) are required. + * + * This method must be invoked as: $var = + * &Horde_Form_Action::singleton() + * + * @param mixed $action The type of concrete Horde_Form_Action subclass to return. + * The code is dynamically included. If $action is an array, + * then we will look in $action[0]/lib/Form/Action/ for + * the subclass implementation named $action[1].php. + * @param array $params A hash containing any additional configuration a + * form might need. + * + * @return Horde_Form_Action The concrete Horde_Form_Action reference, or + * false on an error. + */ + function &singleton($action, $params = null) + { + static $instances = array(); + + $signature = serialize(array($action, $params)); + if (!isset($instances[$signature])) { + $instances[$signature] = &Horde_Form_Action::factory($action, $params); + } + + return $instances[$signature]; + } + +} diff --git a/framework/Form/Form/Action/ConditionalEnable.php b/framework/Form/Form/Action/ConditionalEnable.php new file mode 100644 index 000000000..ccba9eb65 --- /dev/null +++ b/framework/Form/Form/Action/ConditionalEnable.php @@ -0,0 +1,51 @@ + + * $params = array( + * 'target' => '[name of element this is conditional on]', + * 'enabled' => 'true' | 'false', + * 'values' => array([target values to check]) + * ); + * + * + * So $params = array('foo', 'true', array(1, 2)) will enable the field this + * action is attached to if the value of 'foo' is 1 or 2, and disable it + * otherwise. + * + * $Horde: framework/Form/Form/Action/ConditionalEnable.php,v 1.6 2009/10/06 18:58:57 slusarz Exp $ + * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Matt Kynaston + * @package Horde_Form + */ +class Horde_Form_Action_ConditionalEnable extends Horde_Form_Action { + + var $_trigger = array('onload'); + + function getActionScript(&$form, $renderer, $varname) + { + Horde::addScriptFile('form_helpers.js', 'horde'); + + $form_name = $form->getName(); + $target = $this->_params['target']; + $enabled = $this->_params['enabled']; + if (!is_string($enabled)) { + $enabled = ($enabled) ? 'true' : 'false'; + } + $vals = $this->_params['values']; + $vals = (is_array($vals)) ? $vals : array($vals); + $args = "'$varname', $enabled, '" . implode("','", $vals) . "'"; + + return "if (addEvent(document.getElementById('$form_name').$target, 'onchange', \"checkEnabled(this, $args);\")) { " + . " checkEnabled(document.getElementById('$form_name').$varname, $args); };"; + } + +} diff --git a/framework/Form/Form/Action/ConditionalSetValue.php b/framework/Form/Form/Action/ConditionalSetValue.php new file mode 100644 index 000000000..40a7e8f03 --- /dev/null +++ b/framework/Form/Form/Action/ConditionalSetValue.php @@ -0,0 +1,97 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_ConditionalSetValue extends Horde_Form_Action { + + /** + * Which JS events should trigger this action? + * + * @var array + */ + var $_trigger = array('onchange', 'onload'); + + function getActionScript($form, $renderer, $varname) + { + return 'map(\'' . $renderer->_genID($varname, false) . "', '" . $renderer->_genID($this->getTarget(), false) . '\');'; + } + + function setValues(&$vars, $sourceVal, $arrayVal = false) + { + $map = $this->_params['map']; + $target = $this->getTarget(); + + if ($arrayVal) { + $i = 0; + if (is_array($sourceVal)) { + foreach ($sourceVal as $val) { + if (!empty($map[$val])) { + $vars->set($target, $map[$val], $i); + } + $i++; + } + } + } else { + if (!empty($map[$sourceVal])) { + $vars->set($target, $map[$sourceVal]); + } + } + } + + function printJavaScript() + { + $this->_printJavaScriptStart(); + $map = $this->_params['map']; +?> + +var _map = [ 0) { + echo ', '; + } + echo '"' . $val . '"'; + $i++; +}?>]; + +function map(sourceId, targetId) +{ + var newval; + var source = document.getElementById(sourceId); + var element = document.getElementById(targetId); + if (element) { + if (_map[source.selectedIndex]) { + newval = _map[source.selectedIndex]; + replace = true; + } else { + newval = ''; + replace = false; + for (i = 0; i < _map.length; i++) { + if (element.value == _map[i]) { + replace = true; + break; + } + } + } + + if (replace) { + element.value = newval; + } + } +}_printJavaScriptEnd(); + } + +} diff --git a/framework/Form/Form/Action/conditional_enable.php b/framework/Form/Form/Action/conditional_enable.php new file mode 100644 index 000000000..bb6f9d766 --- /dev/null +++ b/framework/Form/Form/Action/conditional_enable.php @@ -0,0 +1,51 @@ + + * $params = array( + * 'target' => '[name of element this is conditional on]', + * 'enabled' => 'true' | 'false', + * 'values' => array([target values to check]) + * ); + * + * + * So $params = array('foo', 'true', array(1, 2)) will enable the field this + * action is attached to if the value of 'foo' is 1 or 2, and disable it + * otherwise. + * + * $Horde: framework/Form/Form/Action/conditional_enable.php,v 1.13 2009/10/06 18:58:57 slusarz Exp $ + * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Matt Kynaston + * @package Horde_Form + */ +class Horde_Form_Action_conditional_enable extends Horde_Form_Action { + + var $_trigger = array('onload'); + + function getActionScript(&$form, $renderer, $varname) + { + Horde::addScriptFile('form_helpers.js', 'horde'); + + $form_name = $form->getName(); + $target = $this->_params['target']; + $enabled = $this->_params['enabled']; + if (!is_string($enabled)) { + $enabled = ($enabled) ? 'true' : 'false'; + } + $vals = $this->_params['values']; + $vals = (is_array($vals)) ? $vals : array($vals); + $args = "'$varname', $enabled, '" . implode("','", $vals) . "'"; + + return "if (addEvent(document.getElementById('$form_name').$target, 'onchange', \"checkEnabled(this, $args);\")) { " + . " checkEnabled(document.getElementById('$form_name').$varname, $args); };"; + } + +} diff --git a/framework/Form/Form/Action/conditional_setvalue.php b/framework/Form/Form/Action/conditional_setvalue.php new file mode 100644 index 000000000..c08b2c384 --- /dev/null +++ b/framework/Form/Form/Action/conditional_setvalue.php @@ -0,0 +1,97 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_conditional_setvalue extends Horde_Form_Action { + + /** + * Which JS events should trigger this action? + * + * @var array + */ + var $_trigger = array('onchange', 'onload'); + + function getActionScript($form, $renderer, $varname) + { + return 'map(\'' . $renderer->_genID($varname, false) . "', '" . $renderer->_genID($this->getTarget(), false) . '\');'; + } + + function setValues(&$vars, $sourceVal, $arrayVal = false) + { + $map = $this->_params['map']; + $target = $this->getTarget(); + + if ($arrayVal) { + $i = 0; + if (is_array($sourceVal)) { + foreach ($sourceVal as $val) { + if (!empty($map[$val])) { + $vars->set($target, $map[$val], $i); + } + $i++; + } + } + } else { + if (!empty($map[$sourceVal])) { + $vars->set($target, $map[$sourceVal]); + } + } + } + + function printJavaScript() + { + $this->_printJavaScriptStart(); + $map = $this->_params['map']; +?> + +var _map = [ 0) { + echo ', '; + } + echo '"' . $val . '"'; + $i++; +}?>]; + +function map(sourceId, targetId) +{ + var newval; + var source = document.getElementById(sourceId); + var element = document.getElementById(targetId); + if (element) { + if (_map[source.selectedIndex]) { + newval = _map[source.selectedIndex]; + replace = true; + } else { + newval = ''; + replace = false; + for (i = 0; i < _map.length; i++) { + if (element.value == _map[i]) { + replace = true; + break; + } + } + } + + if (replace) { + element.value = newval; + } + } +}_printJavaScriptEnd(); + } + +} diff --git a/framework/Form/Form/Action/reload.php b/framework/Form/Form/Action/reload.php new file mode 100644 index 000000000..0ede7b9cb --- /dev/null +++ b/framework/Form/Form/Action/reload.php @@ -0,0 +1,29 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_reload extends Horde_Form_Action { + + var $_trigger = array('onchange'); + + function getActionScript($form, $renderer, $varname) + { + Horde::addScriptFile('prototype.js', 'horde'); + Horde::addScriptFile('effects.js', 'horde'); + Horde::addScriptFile('redbox.js', 'horde'); + return 'if (this.value) { document.' . $form->getName() . '.formname.value=\'\'; RedBox.loading(); document.' . $form->getName() . '.submit() }'; + } + +} diff --git a/framework/Form/Form/Action/setcursorpos.php b/framework/Form/Form/Action/setcursorpos.php new file mode 100644 index 000000000..b3e6333cd --- /dev/null +++ b/framework/Form/Form/Action/setcursorpos.php @@ -0,0 +1,33 @@ + + * @since Horde 3.2 + * @package Horde_Form + */ +class Horde_Form_Action_setcursorpos extends Horde_Form_Action { + + var $_trigger = array('onload'); + + function getActionScript(&$form, $renderer, $varname) + { + Horde::addScriptFile('form_helpers.js', 'horde'); + + $pos = implode(',', $this->_params); + return 'form_setCursorPosition(document.forms[\'' . + htmlspecialchars($form->getName()) . '\'].elements[\'' . + htmlspecialchars($varname) . '\'].id, ' . $pos . ');'; + } + +} diff --git a/framework/Form/Form/Action/submit.php b/framework/Form/Form/Action/submit.php new file mode 100644 index 000000000..06b031c2c --- /dev/null +++ b/framework/Form/Form/Action/submit.php @@ -0,0 +1,29 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_submit extends Horde_Form_Action { + + var $_trigger = array('onchange'); + + function getActionScript($form, $renderer, $varname) + { + Horde::addScriptFile('prototype.js', 'horde'); + Horde::addScriptFile('effects.js', 'horde'); + Horde::addScriptFile('redbox.js', 'horde'); + return 'RedBox.loading(); document.' . $form->getName() . '.submit()'; + } + +} diff --git a/framework/Form/Form/Action/sum_fields.php b/framework/Form/Form/Action/sum_fields.php new file mode 100644 index 000000000..46896c4b5 --- /dev/null +++ b/framework/Form/Form/Action/sum_fields.php @@ -0,0 +1,44 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_sum_fields extends Horde_Form_Action { + + var $_trigger = array('onload'); + + function getActionScript(&$form, $renderer, $varname) + { + Horde::addScriptFile('form_helpers.js', 'horde'); + + $form_name = $form->getName(); + $fields = "'" . implode("','", $this->_params) . "'"; + $js = array(); + $js[] = sprintf('document.forms[\'%s\'].elements[\'%s\'].disabled = true;', + $form_name, + $varname); + foreach ($this->_params as $field) { + $js[] = sprintf("addEvent(document.forms['%1\$s'].elements['%2\$s'], \"onchange\", \"sumFields(document.forms['%1\$s'], '%3\$s', %4\$s);\");", + $form_name, + $field, + $varname, + $fields); + } + + return implode("\n", $js); + } + +} diff --git a/framework/Form/Form/Action/updatefield.php b/framework/Form/Form/Action/updatefield.php new file mode 100644 index 000000000..53b50eca4 --- /dev/null +++ b/framework/Form/Form/Action/updatefield.php @@ -0,0 +1,63 @@ + + * @package Horde_Form + */ +class Horde_Form_Action_updatefield extends Horde_Form_Action { + + var $_trigger = array('onchange', 'onload', 'onkeyup'); + + function getActionScript(&$form, &$renderer, $varname) + { + return 'updateField' . $this->id() . '();'; + } + + function setValues(&$vars, $sourceVal, $arrayVal = false) + { + } + + function printJavaScript() + { + $this->_printJavaScriptStart(); + $pieces = explode('%s', $this->_params['format']); + $fields = $this->_params['fields']; + $val_first = (substr($this->_params['format'], 0, 2) == '%s'); + if ($val_first) { + array_shift($pieces); + } + if (substr($this->_params['format'], -2) == '%s') { + array_pop($pieces); + } + + $args = array(); + if ($val_first) { + $args[] = "document.getElementById('" . array_shift($fields) . "').value"; + } + while (count($pieces)) { + $args[] = "'" . array_shift($pieces) . "'"; + $args[] = "document.getElementById('" . array_shift($fields) . "').value"; + } +?> +// Updater for getTarget() ?>. +function updateFieldid() ?>() +{ + var target = document.getElementById('getTarget() ?>'); + if (target) { + target.value = ().replace(/(^ +| +$)/, '').replace(/ +/g, ' '); + } +}_printJavaScriptEnd(); + } + +} diff --git a/framework/Form/Form/Renderer.php b/framework/Form/Form/Renderer.php new file mode 100644 index 000000000..ce075d195 --- /dev/null +++ b/framework/Form/Form/Renderer.php @@ -0,0 +1,540 @@ + + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @author Robert E. Coyle + * @package Horde_Form + */ +class Horde_Form_Renderer { + + var $_name; + var $_requiredLegend = false; + var $_requiredMarker = '*'; + var $_helpMarker = '?'; + var $_showHeader = true; + var $_cols = 2; + var $_varRenderer = null; + var $_firstField = null; + var $_stripedRows = true; + + /** + * Does the title of the form contain HTML? If so, you are responsible for + * doing any needed escaping/sanitization yourself. Otherwise the title + * will be run through htmlspecialchars() before being output. + * + * @var boolean + */ + var $_encodeTitle = true; + + /** + * Width of the attributes column. + * + * @access private + * @var string + */ + var $_attrColumnWidth = '15%'; + + /** + * Construct a new Horde_Form_Renderer::. + * + * @param array $params This is a hash of renderer-specific parameters. + * Possible keys: + * 'varrenderer_driver': specifies the driver + * parameter to Horde_Ui_VarRenderer::factory(). + * 'encode_title': @see $_encodeTitle + */ + function Horde_Form_Renderer($params = array()) + { + global $registry; + if (isset($registry) && is_a($registry, 'Registry')) { + /* Registry available, so use a pretty image. */ + $this->_requiredMarker = Horde::img('required.png', '*', '', $registry->getImageDir('horde')); + } else { + /* No registry available, use something plain. */ + $this->_requiredMarker = '*'; + } + + if (isset($params['encode_title'])) { + $this->encodeTitle($params['encode_title']); + } + + $driver = 'html'; + if (isset($params['varrenderer_driver'])) { + $driver = $params['varrenderer_driver']; + } + $this->_varRenderer = Horde_Ui_VarRenderer::factory($driver, $params); + } + + function showHeader($bool) + { + $this->_showHeader = $bool; + } + + /** + * Sets or returns whether the form title should be encoded with + * htmlspecialchars(). + * + * @param boolean $encode If true, the form title gets encoded. If false + * the title can contain HTML, but the class user + * is responsible to encode any special characters. + * + * @return boolean Whether the form title should be encoded. + */ + function encodeTitle($encode = null) + { + if (!is_null($encode)) { + $this->_encodeTitle = $encode; + } + return $this->_encodeTitle = $encode; + } + + /** + * @deprecated + */ + function setAttrColumnWidth($width) + { + } + + function open($action, $method, $name, $enctype = null) + { + $this->_name = $name; + $name = htmlspecialchars($name); + $action = htmlspecialchars($action); + $method = htmlspecialchars($method); + echo "
\n"; + Horde_Util::pformInput(); + } + + function beginActive($name, $extra = null) + { + $this->_renderBeginActive($name, $extra); + } + + function beginInactive($name, $extra = null) + { + $this->_renderBeginInactive($name, $extra); + } + + function _renderSectionTabs(&$form) + { + /* If javascript is not available, do not render tabs. */ + if (!$GLOBALS['browser']->hasFeature('javascript')) { + return; + } + + $open_section = $form->getOpenSection(); + + /* Add the javascript for the toggling the sections. */ + Horde::addScriptFile('form_sections.js', 'horde'); + echo ''; + + /* Loop through the sections and print out a tab for each. */ + echo "
    \n"; + foreach ($form->_sections as $section => $val) { + $class = ($section == $open_section) ? ' class="activeTab"' : ''; + $js = sprintf('onclick="sections_%s.toggle(\'%s\'); return false;"', + $form->getName(), + $section); + printf('%s%s ' . "\n", + $class, htmlspecialchars($form->getName() . '_tab_' . $section), $js, + $form->getSectionImage($section), + $form->getSectionDesc($section)); + } + echo "

\n"; + } + + function _renderSectionBegin(&$form, $section) + { + // Stripe alternate rows if that option is turned on. + if ($this->_stripedRows && class_exists('Horde')) { + Horde::addScriptFile('stripe.js', 'horde'); + $class = ' class="striped"'; + } else { + $class = ''; + } + + $open_section = $form->getOpenSection(); + if (empty($open_section)) { + $open_section = '__base'; + } + printf('
', + htmlspecialchars($form->getName() . '_section_' . $section), + ($open_section == $section ? 'block' : 'none'), + $class); + } + + function _renderSectionEnd() + { + echo '
'; + } + + function end() + { + $this->_renderEnd(); + } + + function close($focus = true) + { + echo "
\n"; + if ($focus && !empty($this->_firstField)) { + echo ' +'; + } + } + + function listFormVars(&$form) + { + $variables = &$form->getVariables(true, true); + $vars = array(); + if ($variables) { + foreach ($variables as $var) { + if (is_object($var)) { + if (!$var->isReadonly()) { + $vars[$var->getVarName()] = 1; + } + } else { + $vars[$var] = 1; + } + } + } + echo ''; + } + + function renderFormActive(&$form, &$vars) + { + $this->_renderForm($form, $vars, true); + } + + function renderFormInactive(&$form, &$vars) + { + $this->_renderForm($form, $vars, false); + } + + function _renderForm(&$form, &$vars, $active) + { + /* If help is present 3 columns are needed. */ + $this->_cols = $form->hasHelp() ? 3 : 2; + + $variables = &$form->getVariables(false); + + /* Check for a form token error. */ + if (($tokenError = $form->getError('_formToken')) !== null) { + echo '

' . htmlspecialchars($tokenError) . '

'; + } + + /* Check for a form secret error. */ + if (($secretError = $form->getError('_formSecret')) !== null) { + echo '

' . htmlspecialchars($secretError) . '

'; + } + + if (count($form->_sections)) { + $this->_renderSectionTabs($form); + } + + $error_section = null; + foreach ($variables as $section_id => $section) { + $this->_renderSectionBegin($form, $section_id); + foreach ($section as $var) { + $type = $var->getTypeName(); + + switch ($type) { + case 'header': + $this->_renderHeader($var->getHumanName(), $form->getError($var->getVarName())); + break; + + case 'description': + $this->_renderDescription($var->getHumanName()); + break; + + case 'spacer': + $this->_renderSpacer(); + break; + + default: + $isInput = ($active && !$var->isReadonly()); + $format = $isInput ? 'Input' : 'Display'; + $begin = "_renderVar${format}Begin"; + $end = "_renderVar${format}End"; + + $this->$begin($form, $var, $vars); + echo $this->_varRenderer->render($form, $var, $vars, $isInput); + $this->$end($form, $var, $vars); + + /* Print any javascript if actions present. */ + if ($var->hasAction()) { + $var->_action->printJavaScript(); + } + + /* Keep first field. */ + if ($active && empty($this->_firstField) && !$var->isReadonly() && !$var->isHidden()) { + $this->_firstField = $var->getVarName(); + } + + /* Keep section with first error. */ + if (is_null($error_section) && $form->getError($var)) { + $error_section = $section_id; + } + } + } + + $this->_renderSectionEnd(); + } + + if (!is_null($error_section) && $form->_sections) { + echo '"; + } + } + + function submit($submit = null, $reset = false) + { + if (is_null($submit) || empty($submit)) { + $submit = _("Submit"); + } + if ($reset === true) { + $reset = _("Reset"); + } + $this->_renderSubmit($submit, $reset); + } + + /** + * Implementation specific begin function. + */ + function _renderBeginActive($name, $extra) + { + echo '
'; + if ($this->_showHeader) { + $this->_sectionHeader($name, $extra); + } + if ($this->_requiredLegend) { + echo '' . $this->_requiredMarker . ' = ' . _("Required Field"); + } + } + + /** + * Implementation specific begin function. + */ + function _renderBeginInactive($name, $extra) + { + echo '
'; + if ($this->_showHeader) { + $this->_sectionHeader($name, $extra); + } + } + + /** + * Implementation specific end function. + */ + function _renderEnd() + { + echo '
' . $this->_varRenderer->renderEnd(); + } + + function _renderHeader($header, $error = '') + { +?>
+

+  +
+ + + + + + +
+getError($var); + $isvalid = empty($message); + echo "\n"; + printf(' %s%s%s%s' . "\n", + empty($this->_attrColumnWidth) ? '' : ' width="' . $this->_attrColumnWidth . '"', + $isvalid ? '' : '', + $var->isRequired() ? '' . $this->_requiredMarker . ' ' : '', + $var->getHumanName(), + $isvalid ? '' : '
' . $message . '
'); + printf(' ', + ((!$var->hasHelp() && $form->hasHelp()) ? ' colspan="2"' : ''), + ($var->isDisabled() ? ' class="form-disabled"' : '')); + } + + function _renderVarInputEnd(&$form, &$var, &$vars) + { + /* Display any description for the field. */ + if ($var->hasDescription()) { + echo '
' . $var->getDescription(); + } + + /* Display any help for the field. */ + if ($var->hasHelp()) { + global $registry; + if (isset($registry) && is_a($registry, 'Registry')) { + $link = Horde_Help::link($GLOBALS['registry']->getApp(), $var->getHelp()); + } else { + $link = '' . $this->_helpMarker . ''; + } + echo "\n $link "; + } + + echo "\n\n"; + } + + // Implementation specifics -- display variables. + function _renderVarDisplayBegin(&$form, &$var, &$vars) + { + $message = $form->getError($var); + $isvalid = empty($message); + echo "\n"; + printf(' %s%s%s' . "\n", + empty($this->_attrColumnWidth) ? '' : ' width="' . $this->_attrColumnWidth . '"', + $isvalid ? '' : '', + $var->getHumanName(), + $isvalid ? '' : '
' . $message . '
'); + echo ' '; + } + + function _renderVarDisplayEnd(&$form, &$var, &$vars) + { + if ($var->hasHelp()) { + echo ' '; + } + echo "\n\n"; + } + + function _sectionHeader($title, $extra = '') + { + if (strlen($title)) { + echo '
'; + if (!empty($extra)) { + echo '' . $extra . ''; + } + echo $this->_encodeTitle ? htmlspecialchars($title) : $title; + echo '
'; + } + } + + /** + * Attempts to return a concrete Horde_Form_Renderer instance based on + * $renderer. + * + * @param mixed $renderer The type of concrete Horde_Form_Renderer + * subclass to return. The code is dynamically + * included. If $renderer is an array, then we will + * look in $renderer[0]/lib/Form/Renderer/ for the + * subclass implementation named $renderer[1].php. + * @param array $params A hash containing any additional configuration a + * form might need. + * + * @return Horde_Form_Renderer The concrete Horde_Form_Renderer reference, + * or false on an error. + */ + function factory($renderer = '', $params = null) + { + if (is_array($renderer)) { + $app = $renderer[0]; + $renderer = $renderer[1]; + } + + /* Return a base Horde_Form_Renderer object if no driver is + * specified. */ + $renderer = basename($renderer); + if (!empty($renderer) && $renderer != 'none') { + $class = 'Horde_Form_Renderer_' . $renderer; + } else { + $class = 'Horde_Form_Renderer'; + } + + if (!class_exists($class)) { + if (!empty($app)) { + include $GLOBALS['registry']->get('fileroot', $app) . '/lib/Form/Renderer/' . $renderer . '.php'; + } else { + include 'Horde/Form/Renderer/' . $renderer . '.php'; + } + } + + if (class_exists($class)) { + return new $class($params); + } else { + return PEAR::raiseError('Class definition of ' . $class . ' not found.'); + } + } + + /** + * Attempts to return a reference to a concrete Horde_Form_Renderer + * instance based on $renderer. It will only create a new instance if no + * Horde_Form_Renderer instance with the same parameters currently exists. + * + * This should be used if multiple types of form renderers (and, + * thus, multiple Horde_Form_Renderer instances) are required. + * + * This method must be invoked as: $var = &Horde_Form_Renderer::singleton() + * + * @param mixed $renderer The type of concrete Horde_Form_Renderer + * subclass to return. The code is dynamically + * included. If $renderer is an array, then we will + * look in $renderer[0]/lib/Form/Renderer/ for the + * subclass implementation named $renderer[1].php. + * @param array $params A hash containing any additional configuration a + * form might need. + * + * @return Horde_Form_Renderer The concrete Horde_Form_Renderer reference, + * or false on an error. + */ + function &singleton($renderer, $params = null) + { + static $instances = array(); + + $signature = serialize(array($renderer, $params)); + if (!isset($instances[$signature])) { + $instances[$signature] = Horde_Form_Renderer::factory($renderer, $params); + } + + return $instances[$signature]; + } + +} diff --git a/framework/Form/Form/Type.php b/framework/Form/Form/Type.php new file mode 100644 index 000000000..5cc7eaec1 --- /dev/null +++ b/framework/Form/Form/Type.php @@ -0,0 +1,3553 @@ + + * @package Horde_Form + */ +class Horde_Form_Type { + + function Horde_Form_Type() + { + } + + function getProperty($property) + { + $prop = '_' . $property; + return isset($this->$prop) ? $this->$prop : null; + } + + function __get($property) + { + return $this->getProperty($property); + } + + function setProperty($property, $value) + { + $prop = '_' . $property; + $this->$prop = $value; + } + + function __set($property, $value) + { + return $this->setProperty($property, $value); + } + + function init() + { + } + + function onSubmit() + { + } + + function isValid(&$var, &$vars, $value, &$message) + { + $message = 'Error: Horde_Form_Type::isValid() called - should be overridden
'; + return false; + } + + function getTypeName() + { + return str_replace('horde_form_type_', '', Horde_String::lower(get_class($this))); + } + + function getValues() + { + return null; + } + + function getInfo(&$vars, &$var, &$info) + { + $info = $var->getValue($vars); + } + +} + +class Horde_Form_Type_spacer extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Spacer")); + } + +} + +class Horde_Form_Type_header extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Header")); + } + +} + +class Horde_Form_Type_description extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Description")); + } + +} + +/** + * Simply renders its raw value in both active and inactive rendering. + */ +class Horde_Form_Type_html extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("HTML")); + } + +} + +class Horde_Form_Type_number extends Horde_Form_Type { + + var $_fraction; + + function init($fraction = null) + { + $this->_fraction = $fraction; + } + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) { + $message = _("This field is required."); + return false; + } elseif (empty($value)) { + return true; + } + + /* If matched, then this is a correct numeric value. */ + if (preg_match($this->_getValidationPattern(), $value)) { + return true; + } + + $message = _("This field must be a valid number."); + return false; + } + + function _getValidationPattern() + { + static $pattern = ''; + if (!empty($pattern)) { + return $pattern; + } + + /* Get current locale information. */ + $linfo = Horde_Nls::getLocaleInfo(); + + /* Build the pattern. */ + $pattern = '(-)?'; + + /* Only check thousands separators if locale has any. */ + if (!empty($linfo['mon_thousands_sep'])) { + /* Regex to check for correct thousands separators (if any). */ + $pattern .= '((\d+)|((\d{0,3}?)([' . $linfo['mon_thousands_sep'] . ']\d{3})*?))'; + } else { + /* No locale thousands separator, check for only digits. */ + $pattern .= '(\d+)'; + } + /* If no decimal point specified default to dot. */ + if (empty($linfo['mon_decimal_point'])) { + $linfo['mon_decimal_point'] = '.'; + } + /* Regex to check for correct decimals (if any). */ + if (empty($this->_fraction)) { + $fraction = '*'; + } else { + $fraction = '{0,' . $this->_fraction . '}'; + } + $pattern .= '([' . $linfo['mon_decimal_point'] . '](\d' . $fraction . '))?'; + + /* Put together the whole regex pattern. */ + $pattern = '/^' . $pattern . '$/'; + + return $pattern; + } + + function getInfo(&$vars, &$var, &$info) + { + $value = $vars->get($var->getVarName()); + $linfo = Horde_Nls::getLocaleInfo(); + $value = str_replace($linfo['mon_thousands_sep'], '', $value); + $info = str_replace($linfo['mon_decimal_point'], '.', $value); + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Number")); + } + +} + +class Horde_Form_Type_int extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-9]+$/', $value)) { + return true; + } + + $message = _("This field may only contain integers."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Integer")); + } + +} + +class Horde_Form_Type_octal extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(int)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-7]+$/', $value)) { + return true; + } + + $message = _("This field may only contain octal values."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Octal")); + } + +} + +class Horde_Form_Type_intlist extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if (empty($value) && $var->isRequired()) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-9 ,]+$/', $value)) { + return true; + } + + $message = _("This field must be a comma or space separated list of integers"); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Integer list")); + } + +} + +class Horde_Form_Type_text extends Horde_Form_Type { + + var $_regex; + var $_size; + var $_maxlength; + + /** + * The initialisation function for the text variable type. + * + * @access private + * + * @param string $regex Any valid PHP PCRE pattern syntax that + * needs to be matched for the field to be + * considered valid. If left empty validity + * will be checked only for required fields + * whether they are empty or not. + * If using this regex test it is advisable + * to enter a description for this field to + * warn the user what is expected, as the + * generated error message is quite generic + * and will not give any indication where + * the regex failed. + * @param integer $size The size of the input field. + * @param integer $maxlength The max number of characters. + */ + function init($regex = '', $size = 40, $maxlength = null) + { + $this->_regex = $regex; + $this->_size = $size; + $this->_maxlength = $maxlength; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $valid = true; + + if (!empty($this->_maxlength) && Horde_String::length($value) > $this->_maxlength) { + $valid = false; + $message = sprintf(_("Value is over the maximum length of %d."), $this->_maxlength); + } elseif ($var->isRequired() && empty($this->_regex)) { + $valid = strlen(trim($value)) > 0; + + if (!$valid) { + $message = _("This field is required."); + } + } elseif (!empty($this->_regex)) { + $valid = preg_match($this->_regex, $value); + + if (!$valid) { + $message = _("You must enter a valid value."); + } + } + + return $valid; + } + + function getSize() + { + return $this->_size; + } + + function getMaxLength() + { + return $this->_maxlength; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Text"), + 'params' => array( + 'regex' => array('label' => _("Regex"), + 'type' => 'text'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'maxlength' => array('label' => _("Maximum length"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_stringlist extends Horde_Form_Type_text { + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("String list"), + 'params' => array( + 'regex' => array('label' => _("Regex"), + 'type' => 'text'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'maxlength' => array('label' => _("Maximum length"), + 'type' => 'int')), + ); + } + +} + +/** + * @since Horde 3.3 + */ +class Horde_Form_Type_stringarray extends Horde_Form_Type_stringlist { + + function getInfo(&$vars, &$var, &$info) + { + $info = array_map('trim', explode(',', $vars->get($var->getVarName()))); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("String list returning an array"), + 'params' => array( + 'regex' => array('label' => _("Regex"), + 'type' => 'text'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'maxlength' => array('label' => _("Maximum length"), + 'type' => 'int')), + ); + } + +} + +/** + * @since Horde 3.2 + */ +class Horde_Form_Type_phone extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if (!strlen(trim($value))) { + if ($var->isRequired()) { + $message = _("This field is required."); + return false; + } + } elseif (!preg_match('/^\+?[\d()\-\/. ]*$/', $value)) { + $message = _("You must enter a valid phone number, digits only with an optional '+' for the international dialing prefix."); + return false; + } + + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Phone number")); + } + +} + +class Horde_Form_Type_cellphone extends Horde_Form_Type_phone { + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Mobile phone number")); + } + +} + +class Horde_Form_Type_ipaddress extends Horde_Form_Type_text { + + function isValid(&$var, &$vars, $value, &$message) + { + $valid = true; + + if (strlen(trim($value)) > 0) { + $ip = explode('.', $value); + $valid = (count($ip) == 4); + if ($valid) { + foreach ($ip as $part) { + if (!is_numeric($part) || + $part > 255 || + $part < 0) { + $valid = false; + break; + } + } + } + + if (!$valid) { + $message = _("Please enter a valid IP address."); + } + } elseif ($var->isRequired()) { + $valid = false; + $message = _("This field is required."); + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("IP address")); + } + +} + +class Horde_Form_Type_longtext extends Horde_Form_Type_text { + + var $_rows; + var $_cols; + var $_helper = array(); + + function init($rows = 8, $cols = 80, $helper = array()) + { + if (!is_array($helper)) { + $helper = array($helper); + } + + $this->_rows = $rows; + $this->_cols = $cols; + $this->_helper = $helper; + } + + function getRows() + { + return $this->_rows; + } + + function getCols() + { + return $this->_cols; + } + + function hasHelper($option = '') + { + if (empty($option)) { + /* No option specified, check if any helpers have been + * activated. */ + return !empty($this->_helper); + } elseif (empty($this->_helper)) { + /* No helpers activated at all, return false. */ + return false; + } else { + /* Check if given helper has been activated. */ + return in_array($option, $this->_helper); + } + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Long text"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'), + 'helper' => array('label' => _("Helpers"), + 'type' => 'array'))); + } + +} + +class Horde_Form_Type_countedtext extends Horde_Form_Type_longtext { + + var $_chars; + + function init($rows = null, $cols = null, $chars = 1000) + { + parent::init($rows, $cols); + $this->_chars = $chars; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $valid = true; + + $length = Horde_String::length(trim($value)); + + if ($var->isRequired() && $length <= 0) { + $valid = false; + $message = _("This field is required."); + } elseif ($length > $this->_chars) { + $valid = false; + $message = sprintf(ngettext("There are too many characters in this field. You have entered %d character; ", "There are too many characters in this field. You have entered %d characters; ", $length), $length) + . sprintf(_("you must enter less than %d."), $this->_chars); + } + + return $valid; + } + + function getChars() + { + return $this->_chars; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Counted text"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'), + 'chars' => array('label' => _("Number of characters"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_address extends Horde_Form_Type_longtext { + + function parse($address) + { + $info = array(); + $aus_state_regex = '(?:ACT|NSW|NT|QLD|SA|TAS|VIC|WA)'; + + if (preg_match('/(?s)(.*?)(?-s)\r?\n(?:(.*?)\s+)?((?:A[BL]|B[ABDHLNRST]?|C[ABFHMORTVW]|D[ADEGHLNTY]|E[CHNX]?|F[KY]|G[LUY]?|H[ADGPRSUX]|I[GMPV]|JE|K[ATWY]|L[ADELNSU]?|M[EKL]?|N[EGNPRW]?|O[LX]|P[AEHLOR]|R[GHM]|S[AEGKLMNOPRSTWY]?|T[ADFNQRSW]|UB|W[ACDFNRSV]?|YO|ZE)\d(?:\d|[A-Z])? \d[A-Z]{2})/', $address, $addressParts)) { + /* UK postcode detected. */ + $info = array('country' => 'uk', 'zip' => $addressParts[3]); + if (!empty($addressParts[1])) { + $info['street'] = $addressParts[1]; + } + if (!empty($addressParts[2])) { + $info['city'] = $addressParts[2]; + } + } elseif (preg_match('/\b' . $aus_state_regex . '\b/', $address)) { + /* Australian state detected. */ + /* Split out the address, line-by-line. */ + $addressLines = preg_split('/\r?\n/', $address); + $info = array('country' => 'au'); + for ($i = 0; $i < count($addressLines); $i++) { + /* See if it's the street number & name. */ + if (preg_match('/(\d+\s*\/\s*)?(\d+|\d+[a-zA-Z])\s+([a-zA-Z ]*)/', $addressLines[$i], $lineParts)) { + $info['street'] = $addressLines[$i]; + $info['streetNumber'] = $lineParts[2]; + $info['streetName'] = $lineParts[3]; + } + /* Look for "Suburb, State". */ + if (preg_match('/([a-zA-Z ]*),?\s+(' . $aus_state_regex . ')/', $addressLines[$i], $lineParts)) { + $info['city'] = $lineParts[1]; + $info['state'] = $lineParts[2]; + } + /* Look for "State <4 digit postcode>". */ + if (preg_match('/(' . $aus_state_regex . ')\s+(\d{4})/', $addressLines[$i], $lineParts)) { + $info['state'] = $lineParts[1]; + $info['zip'] = $lineParts[2]; + } + } + } elseif (preg_match('/(?s)(.*?)(?-s)\r?\n(.*)\s*,\s*(\w+)\.?\s+(\d+|[a-zA-Z]\d[a-zA-Z]\s?\d[a-zA-Z]\d)/', $address, $addressParts)) { + /* American/Canadian address style. */ + $info = array('country' => 'us'); + if (!empty($addressParts[4]) && + preg_match('|[a-zA-Z]\d[a-zA-Z]\s?\d[a-zA-Z]\d|', $addressParts[4])) { + $info['country'] = 'ca'; + } + if (!empty($addressParts[1])) { + $info['street'] = $addressParts[1]; + } + if (!empty($addressParts[2])) { + $info['city'] = $addressParts[2]; + } + if (!empty($addressParts[3])) { + $info['state'] = $addressParts[3]; + } + if (!empty($addressParts[4])) { + $info['zip'] = $addressParts[4]; + } + } elseif (preg_match('/(?:(?s)(.*?)(?-s)(?:\r?\n|,\s*))?(?:([A-Z]{1,3})-)?(\d{4,5})\s+(.*)(?:\r?\n(.*))?/i', $address, $addressParts)) { + /* European address style. */ + $info = array(); + if (!empty($addressParts[1])) { + $info['street'] = $addressParts[1]; + } + if (!empty($addressParts[2])) { + include 'Horde/NLS/carsigns.php'; + $country = array_search(Horde_String::upper($addressParts[2]), $carsigns); + if ($country) { + $info['country'] = $country; + } + } + if (!empty($addressParts[5])) { + include 'Horde/NLS/countries.php'; + $country = array_search($addressParts[5], $countries); + if ($country) { + $info['country'] = Horde_String::lower($country); + } elseif (!isset($info['street'])) { + $info['street'] = trim($addressParts[5]); + } else { + $info['street'] .= "\n" . $addressParts[5]; + } + } + if (!empty($addressParts[3])) { + $info['zip'] = $addressParts[3]; + } + if (!empty($addressParts[4])) { + $info['city'] = trim($addressParts[4]); + } + } + + return $info; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Address"), + 'params' => array( + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_addresslink extends Horde_Form_Type_address { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Address Link")); + } + +} + +/** + * @since Horde 3.3 + */ +class Horde_Form_Type_pgp extends Horde_Form_Type_longtext { + + /** + * Path to the GnuPG binary. + * + * @var string + */ + var $_gpg; + + /** + * A temporary directory. + * + * @var string + */ + var $_temp; + + function init($gpg, $temp_dir = null, $rows = null, $cols = null) + { + $this->_gpg = $gpg; + $this->_temp = $temp_dir; + parent::init($rows, $cols); + } + + /** + * Returns a parameter hash for the Horde_Crypt_pgp constructor. + * + * @return array A parameter hash. + */ + function getPGPParams() + { + return array('program' => $this->_gpg, 'temp' => $this->_temp); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("PGP Key"), + 'params' => array( + 'gpg' => array('label' => _("Path to the GnuPG binary"), + 'type' => 'string'), + 'temp_dir' => array('label' => _("A temporary directory"), + 'type' => 'string'), + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'))); + } + +} + +/** + * @since Horde 3.3 + */ +class Horde_Form_Type_smime extends Horde_Form_Type_longtext { + + /** + * A temporary directory. + * + * @var string + */ + var $_temp; + + function init($temp_dir = null, $rows = null, $cols = null) + { + $this->_temp = $temp_dir; + parent::init($rows, $cols); + } + + /** + * Returns a parameter hash for the Horde_Crypt_smime constructor. + * + * @return array A parameter hash. + */ + function getSMIMEParams() + { + return array('temp' => $this->_temp); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("S/MIME Key"), + 'params' => array( + 'temp_dir' => array('label' => _("A temporary directory"), + 'type' => 'string'), + 'rows' => array('label' => _("Number of rows"), + 'type' => 'int'), + 'cols' => array('label' => _("Number of columns"), + 'type' => 'int'))); + } + +} + +/** + * @since Horde 3.2 + */ +class Horde_Form_Type_country extends Horde_Form_Type_enum { + + function init($prompt = null) + { + parent::init(Horde_Nls::getCountryISO(), $prompt); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Country drop down list"), + 'params' => array( + 'prompt' => array('label' => _("Prompt text"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_file extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired()) { + $uploaded = Horde_Browser::wasFileUploaded($var->getVarName()); + if (is_a($uploaded, 'PEAR_Error')) { + $message = $uploaded->getMessage(); + return false; + } + } + + return true; + } + + function getInfo(&$vars, &$var, &$info) + { + $name = $var->getVarName(); + $uploaded = Horde_Browser::wasFileUploaded($name); + if ($uploaded === true) { + $info['name'] = Horde_Util::dispelMagicQuotes($_FILES[$name]['name']); + $info['type'] = $_FILES[$name]['type']; + $info['tmp_name'] = $_FILES[$name]['tmp_name']; + $info['file'] = $_FILES[$name]['tmp_name']; + $info['error'] = $_FILES[$name]['error']; + $info['size'] = $_FILES[$name]['size']; + } + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("File upload")); + } + +} + +class Horde_Form_Type_image extends Horde_Form_Type { + + /** + * Has a file been uploaded on this form submit? + * + * @var boolean + */ + var $_uploaded = null; + + /** + * Show the upload button? + * + * @var boolean + */ + var $_show_upload = true; + + /** + * Show the option to upload also original non-modified image? + * + * @var boolean + */ + var $_show_keeporig = false; + + /** + * Limit the file size? + * + * @var integer + */ + var $_max_filesize = null; + + /** + * Hash containing the previously uploaded image info. + * + * @var array + */ + var $_img; + + /** + * A random id that identifies the image information in the session data. + * + * @var string + */ + var $_random; + + function init($show_upload = true, $show_keeporig = false, $max_filesize = null) + { + $this->_show_upload = $show_upload; + $this->_show_keeporig = $show_keeporig; + $this->_max_filesize = $max_filesize; + } + + function onSubmit(&$var, &$vars) + { + /* Get the upload. */ + $this->getImage($vars, $var); + + /* If this was done through the upload button override the submitted + * value of the form. */ + if ($vars->get('_do_' . $var->getVarName())) { + $var->form->setSubmitted(false); + if (is_a($this->_uploaded, 'PEAR_Error')) { + $this->_img = array('hash' => $this->getRandomId(), + 'error' => $this->_uploaded->getMessage()); + } + } + } + + function isValid(&$var, &$vars, $value, &$message) + { + /* Get the upload. */ + $this->getImage($vars, $var); + $field = $vars->get($var->getVarName()); + + /* The upload generated a PEAR Error. */ + if (is_a($this->_uploaded, 'PEAR_Error')) { + /* Not required and no image upload attempted. */ + if (!$var->isRequired() && empty($field['hash']) && + $this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) { + return true; + } + + if (($this->_uploaded->getCode() == UPLOAD_ERR_NO_FILE) && + empty($field['hash'])) { + /* Nothing uploaded and no older upload. */ + $message = _("This field is required."); + return false; + } elseif (!empty($field['hash'])) { + if ($this->_img && isset($this->_img['error'])) { + $message = $this->_img['error']; + return false; + } + /* Nothing uploaded but older upload present. */ + return true; + } else { + /* Some other error message. */ + $message = $this->_uploaded->getMessage(); + return false; + } + } elseif (empty($this->_img['img']['size'])) { + $message = _("The image file size could not be determined or it was 0 bytes. The upload may have been interrupted."); + return false; + } elseif ($this->_max_filesize && + $this->_img['img']['size'] > $this->_max_filesize) { + $message = sprintf(_("The image file was larger than the maximum allowed size (%d bytes)."), $this->_max_filesize); + return false; + } + + return true; + } + + function getInfo(&$vars, &$var, &$info) + { + /* Get the upload. */ + $this->getImage($vars, $var); + + /* Get image params stored in the hidden field. */ + $value = $var->getValue($vars); + $info = $this->_img['img']; + if (empty($info['file'])) { + unset($info['file']); + return; + } + if ($this->_show_keeporig) { + $info['keep_orig'] = !empty($value['keep_orig']); + } + + /* Set the uploaded value (either true or PEAR_Error). */ + $info['uploaded'] = &$this->_uploaded; + + /* If a modified file exists move it over the original. */ + if ($this->_show_keeporig && $info['keep_orig']) { + /* Requested the saving of original file also. */ + $info['orig_file'] = Horde::getTempDir() . '/' . $info['file']; + $info['file'] = Horde::getTempDir() . '/mod_' . $info['file']; + /* Check if a modified file actually exists. */ + if (!file_exists($info['file'])) { + $info['file'] = $info['orig_file']; + unset($info['orig_file']); + } + } else { + /* Saving of original not required. */ + $mod_file = Horde::getTempDir() . '/mod_' . $info['file']; + $info['file'] = Horde::getTempDir() . '/' . $info['file']; + + if (file_exists($mod_file)) { + /* Unlink first (has to be done on Windows machines?) */ + unlink($info['file']); + rename($mod_file, $info['file']); + } + } + } + + /** + * Gets the upload and sets up the upload data array. Either + * fetches an upload done with this submit or retries stored + * upload info. + */ + function _getUpload(&$vars, &$var) + { + /* Don't bother with this function if already called and set + * up vars. */ + if (!empty($this->_img)) { + return true; + } + + /* Check if file has been uploaded. */ + $varname = $var->getVarName(); + $this->_uploaded = Horde_Browser::wasFileUploaded($varname . '[new]'); + + if ($this->_uploaded === true) { + /* A file has been uploaded on this submit. Save to temp dir for + * preview work. */ + $this->_img['img']['type'] = $this->getUploadedFileType($varname . '[new]'); + + /* Get the other parts of the upload. */ + require_once 'Horde/Array.php'; + Horde_Array::getArrayParts($varname . '[new]', $base, $keys); + + /* Get the temporary file name. */ + $keys_path = array_merge(array($base, 'tmp_name'), $keys); + $this->_img['img']['file'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get the actual file name. */ + $keys_path = array_merge(array($base, 'name'), $keys); + $this->_img['img']['name'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get the file size. */ + $keys_path = array_merge(array($base, 'size'), $keys); + $this->_img['img']['size'] = Horde_Array::getElement($_FILES, $keys_path); + + /* Get any existing values for the image upload field. */ + $upload = $vars->get($var->getVarName()); + if (!empty($upload['hash'])) { + $upload['img'] = $_SESSION['horde_form'][$upload['hash']]; + unset($_SESSION['horde_form'][$upload['hash']]); + } + + /* Get the temp file if already one uploaded, otherwise create a + * new temporary file. */ + if (!empty($upload['img']['file'])) { + $tmp_file = Horde::getTempDir() . '/' . $upload['img']['file']; + } else { + $tmp_file = Horde::getTempFile('Horde', false); + } + + /* Move the browser created temp file to the new temp file. */ + move_uploaded_file($this->_img['img']['file'], $tmp_file); + $this->_img['img']['file'] = basename($tmp_file); + } elseif ($this->_uploaded) { + /* File has not been uploaded. */ + $upload = $vars->get($var->getVarName()); + if ($this->_uploaded->getCode() == 4 && + !empty($upload['hash']) && + isset($_SESSION['horde_form'][$upload['hash']])) { + $this->_img['img'] = $_SESSION['horde_form'][$upload['hash']]; + unset($_SESSION['horde_form'][$upload['hash']]); + if (isset($this->_img['error'])) { + $this->_uploaded = PEAR::raiseError($this->_img['error']); + } + } + } + if (isset($this->_img['img'])) { + $_SESSION['horde_form'][$this->getRandomId()] = $this->_img['img']; + } + } + + function getUploadedFileType($field) + { + /* Get any index on the field name. */ + $index = Horde_Array::getArrayParts($field, $base, $keys); + + if ($index) { + /* Index present, fetch the mime type var to check. */ + $keys_path = array_merge(array($base, 'type'), $keys); + $type = Horde_Array::getElement($_FILES, $keys_path); + $keys_path = array_merge(array($base, 'tmp_name'), $keys); + $tmp_name = Horde_Array::getElement($_FILES, $keys_path); + } else { + /* No index, simple set up of vars to check. */ + $type = $_FILES[$field]['type']; + $tmp_name = $_FILES[$field]['tmp_name']; + } + + if (empty($type) || ($type == 'application/octet-stream')) { + /* Type wasn't set on upload, try analising the upload. */ + if (!($type = Horde_Mime_Magic::analyzeFile($tmp_name, isset($GLOBALS['conf']['mime']['magic_db']) ? $GLOBALS['conf']['mime']['magic_db'] : null))) { + if ($index) { + /* Get the name value. */ + $keys_path = array_merge(array($base, 'name'), $keys); + $name = Horde_Array::getElement($_FILES, $keys_path); + + /* Work out the type from the file name. */ + $type = Horde_Mime_Magic::filenameToMime($name); + + /* Set the type. */ + $keys_path = array_merge(array($base, 'type'), $keys); + Horde_Array::getElement($_FILES, $keys_path, $type); + } else { + /* Work out the type from the file name. */ + $type = Horde_Mime_Magic::filenameToMime($_FILES[$field]['name']); + + /* Set the type. */ + $_FILES[$field]['type'] = Horde_Mime_Magic::filenameToMime($_FILES[$field]['name']); + } + } + } + + return $type; + } + + /** + * Returns the current image information. + * + * @return array The current image hash. + */ + function getImage($vars, $var) + { + $this->_getUpload($vars, $var); + if (!isset($this->_img)) { + $image = $vars->get($var->getVarName()); + if ($image) { + $this->loadImageData($image); + if (isset($image['img'])) { + $this->_img = $image; + $_SESSION['horde_form'][$this->getRandomId()] = $this->_img['img']; + } + } + } + return $this->_img; + } + + /** + * Loads any existing image data into the image field. Requires that the + * array $image passed to it contains the structure: + * $image['load']['file'] - the filename of the image; + * $image['load']['data'] - the raw image data. + * + * @param array $image The image array. + */ + function loadImageData(&$image) + { + /* No existing image data to load. */ + if (!isset($image['load'])) { + return; + } + + /* Save the data to the temp dir. */ + $tmp_file = Horde::getTempDir() . '/' . $image['load']['file']; + if ($fd = fopen($tmp_file, 'w')) { + fwrite($fd, $image['load']['data']); + fclose($fd); + } + + $image['img'] = array('file' => $image['load']['file']); + unset($image['load']); + } + + function getRandomId() + { + if (!isset($this->_random)) { + $this->_random = uniqid(mt_rand()); + } + return $this->_random; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Image upload"), + 'params' => array( + 'show_upload' => array('label' => _("Show upload?"), + 'type' => 'boolean'), + 'show_keeporig' => array('label' => _("Show option to keep original?"), + 'type' => 'boolean'), + 'max_filesize' => array('label' => _("Maximum file size in bytes"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_boolean extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + function getInfo(&$vars, &$var, &$info) + { + $info = Horde_String::lower($vars->get($var->getVarName())) == 'on'; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("True or false")); + } + +} + +class Horde_Form_Type_link extends Horde_Form_Type { + + /** + * List of hashes containing link parameters. Possible keys: 'url', 'text', + * 'target', 'onclick', 'title', 'accesskey'. + * + * @var array + */ + var $values; + + function init($values) + { + $this->values = $values; + } + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Link"), + 'params' => array( + 'url' => array( + 'label' => _("Link URL"), + 'type' => 'text'), + 'text' => array( + 'label' => _("Link text"), + 'type' => 'text'), + 'target' => array( + 'label' => _("Link target"), + 'type' => 'text'), + 'onclick' => array( + 'label' => _("Onclick event"), + 'type' => 'text'), + 'title' => array( + 'label' => _("Link title attribute"), + 'type' => 'text'), + 'accesskey' => array( + 'label' => _("Link access key"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_email extends Horde_Form_Type { + + /** + * Allow multiple addresses? + * + * @var boolean + */ + var $_allow_multi = false; + + /** + * Protect address from spammers? + * + * @var boolean + */ + var $_strip_domain = false; + + /** + * Link the email address to the compose page when displaying? + * + * @var boolean + */ + var $_link_compose = false; + + /** + * Whether to check the domain's SMTP server whether the address exists. + * + * @var boolean + */ + var $_check_smtp = false; + + /** + * The name to use when linking to the compose page + * + * @var boolean + */ + var $_link_name; + + /** + * A string containing valid delimiters (default is just comma). + * + * @var string + */ + var $_delimiters = ','; + + /** + * @param boolean $allow_multi Allow multiple addresses? + * @param boolean $strip_domain Protect address from spammers? + * @param boolean $link_compose Link the email address to the compose page + * when displaying? + * @param string $link_name The name to use when linking to the + compose page. + * @param string $delimiters Character to split multiple addresses with. + */ + function init($allow_multi = false, $strip_domain = false, + $link_compose = false, $link_name = null, + $delimiters = ',') + { + $this->_allow_multi = $allow_multi; + $this->_strip_domain = $strip_domain; + $this->_link_compose = $link_compose; + $this->_link_name = $link_name; + $this->_delimiters = $delimiters; + } + + /** + */ + function isValid(&$var, &$vars, $value, &$message) + { + // Split into individual addresses. + $emails = $this->splitEmailAddresses($value); + + // Check for too many. + if (!$this->_allow_multi && count($emails) > 1) { + $message = _("Only one email address is allowed."); + return false; + } + + // Check for all valid and at least one non-empty. + $nonEmpty = 0; + foreach ($emails as $email) { + if (!strlen($email)) { + continue; + } + if (!$this->validateEmailAddress($email)) { + $message = sprintf(_("\"%s\" is not a valid email address."), $email); + return false; + } + ++$nonEmpty; + } + + if (!$nonEmpty && $var->isRequired()) { + if ($this->_allow_multi) { + $message = _("You must enter at least one email address."); + } else { + $message = _("You must enter an email address."); + } + return false; + } + + return true; + } + + /** + * Explodes an RFC 2822 string, ignoring a delimiter if preceded + * by a "\" character, or if the delimiter is inside single or + * double quotes. + * + * @param string $string The RFC 822 string. + * + * @return array The exploded string in an array. + */ + function splitEmailAddresses($string) + { + $quotes = array('"', "'"); + $emails = array(); + $pos = 0; + $in_quote = null; + $in_group = false; + $prev = null; + + if (!strlen($string)) { + return array(); + } + + $char = $string[0]; + if (in_array($char, $quotes)) { + $in_quote = $char; + } elseif ($char == ':') { + $in_group = true; + } elseif (strpos($this->_delimiters, $char) !== false) { + $emails[] = ''; + $pos = 1; + } + + for ($i = 1, $iMax = strlen($string); $i < $iMax; ++$i) { + $char = $string[$i]; + if (in_array($char, $quotes)) { + if ($prev !== '\\') { + if ($in_quote === $char) { + $in_quote = null; + } elseif (is_null($in_quote)) { + $in_quote = $char; + } + } + } elseif ($in_group) { + if ($char == ';') { + $emails[] = substr($string, $pos, $i - $pos + 1); + $pos = $i + 1; + $in_group = false; + } + } elseif ($char == ':') { + $in_group = true; + } elseif (strpos($this->_delimiters, $char) !== false && + $prev !== '\\' && + is_null($in_quote)) { + $emails[] = substr($string, $pos, $i - $pos); + $pos = $i + 1; + } + $prev = $char; + } + + if ($pos != $i) { + /* The string ended without a delimiter. */ + $emails[] = substr($string, $pos, $i - $pos); + } + + return $emails; + } + + /** + * RFC(2)822 Email Parser. + * + * By Cal Henderson + * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License + * http://creativecommons.org/licenses/by-sa/2.5/ + * + * http://code.iamcal.com/php/rfc822/ + * + * http://iamcal.com/publish/articles/php/parsing_email + * + * Revision 4 + * + * @param string $email An individual email address to validate. + * + * @return boolean + */ + function validateEmailAddress($email) + { + static $comment_regexp, $email_regexp; + if ($comment_regexp === null) { + $this->_defineValidationRegexps($comment_regexp, $email_regexp); + } + + // We need to strip comments first (repeat until we can't find + // any more). + while (true) { + $new = preg_replace("!$comment_regexp!", '', $email); + if (strlen($new) == strlen($email)){ + break; + } + $email = $new; + } + + // Now match what's left. + $result = (bool)preg_match("!^$email_regexp$!", $email); + if ($result && $this->_check_smtp) { + $result = $this->validateEmailAddressSmtp($email); + } + + return $result; + } + + /** + * Attempt partial delivery of mail to an address to validate it. + * + * @param string $email An individual email address to validate. + * + * @return boolean + */ + function validateEmailAddressSmtp($email) + { + list(, $maildomain) = explode('@', $email, 2); + + // Try to get the real mailserver from MX records. + if (function_exists('getmxrr') && + @getmxrr($maildomain, $mxhosts, $mxpriorities)) { + // MX record found. + array_multisort($mxpriorities, $mxhosts); + $mailhost = $mxhosts[0]; + } else { + // No MX record found, try the root domain as the mail + // server. + $mailhost = $maildomain; + } + + $fp = @fsockopen($mailhost, 25, $errno, $errstr, 5); + if (!$fp) { + return false; + } + + // Read initial response. + fgets($fp, 4096); + + // HELO + fputs($fp, "HELO $mailhost\r\n"); + fgets($fp, 4096); + + // MAIL FROM + fputs($fp, "MAIL FROM: \r\n"); + fgets($fp, 4096); + + // RCPT TO - gets the result we want. + fputs($fp, "RCPT TO: <$email>\r\n"); + $result = trim(fgets($fp, 4096)); + + // QUIT + fputs($fp, "QUIT\r\n"); + fgets($fp, 4096); + fclose($fp); + + return substr($result, 0, 1) == '2'; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Email"), + 'params' => array( + 'allow_multi' => array( + 'label' => _("Allow multiple addresses?"), + 'type' => 'boolean'), + 'strip_domain' => array( + 'label' => _("Protect address from spammers?"), + 'type' => 'boolean'), + 'link_compose' => array( + 'label' => _("Link the email address to the compose page when displaying?"), + 'type' => 'boolean'), + 'link_name' => array( + 'label' => _("The name to use when linking to the compose page"), + 'type' => 'text'), + 'delimiters' => array( + 'label' => _("Character to split multiple addresses with"), + 'type' => 'text'), + ), + ); + } + + /** + * RFC(2)822 Email Parser. + * + * By Cal Henderson + * This code is licensed under a Creative Commons Attribution-ShareAlike 2.5 License + * http://creativecommons.org/licenses/by-sa/2.5/ + * + * http://code.iamcal.com/php/rfc822/ + * + * http://iamcal.com/publish/articles/php/parsing_email + * + * Revision 4 + * + * @param string &$comment The regexp for comments. + * @param string &$addr_spec The regexp for email addresses. + */ + function _defineValidationRegexps(&$comment, &$addr_spec) + { + /** + * NO-WS-CTL = %d1-8 / ; US-ASCII control characters + * %d11 / ; that do not include the + * %d12 / ; carriage return, line feed, + * %d14-31 / ; and white space characters + * %d127 + * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + * DIGIT = %x30-39 + */ + $no_ws_ctl = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]"; + $alpha = "[\\x41-\\x5a\\x61-\\x7a]"; + $digit = "[\\x30-\\x39]"; + $cr = "\\x0d"; + $lf = "\\x0a"; + $crlf = "($cr$lf)"; + + /** + * obs-char = %d0-9 / %d11 / ; %d0-127 except CR and + * %d12 / %d14-127 ; LF + * obs-text = *LF *CR *(obs-char *LF *CR) + * text = %d1-9 / ; Characters excluding CR and LF + * %d11 / + * %d12 / + * %d14-127 / + * obs-text + * obs-qp = "\" (%d0-127) + * quoted-pair = ("\" text) / obs-qp + */ + $obs_char = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]"; + $obs_text = "($lf*$cr*($obs_char$lf*$cr*)*)"; + $text = "([\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|$obs_text)"; + $obs_qp = "(\\x5c[\\x00-\\x7f])"; + $quoted_pair = "(\\x5c$text|$obs_qp)"; + + /** + * obs-FWS = 1*WSP *(CRLF 1*WSP) + * FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space + * obs-FWS + * ctext = NO-WS-CTL / ; Non white space controls + * %d33-39 / ; The rest of the US-ASCII + * %d42-91 / ; characters not including "(", + * %d93-126 ; ")", or "\" + * ccontent = ctext / quoted-pair / comment + * comment = "(" *([FWS] ccontent) [FWS] ")" + * CFWS = *([FWS] comment) (([FWS] comment) / FWS) + * + * @note: We translate ccontent only partially to avoid an + * infinite loop. Instead, we'll recursively strip comments + * before processing the input. + */ + $wsp = "[\\x20\\x09]"; + $obs_fws = "($wsp+($crlf$wsp+)*)"; + $fws = "((($wsp*$crlf)?$wsp+)|$obs_fws)"; + $ctext = "($no_ws_ctl|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])"; + $ccontent = "($ctext|$quoted_pair)"; + $comment = "(\\x28($fws?$ccontent)*$fws?\\x29)"; + $cfws = "(($fws?$comment)*($fws?$comment|$fws))"; + $cfws = "$fws*"; + + /** + * atext = ALPHA / DIGIT / ; Any character except controls, + * "!" / "#" / ; SP, and specials. + * "$" / "%" / ; Used for atoms + * "&" / "'" / + * "*" / "+" / + * "-" / "/" / + * "=" / "?" / + * "^" / "_" / + * "`" / "{" / + * "|" / "}" / + * "~" + * atom = [CFWS] 1*atext [CFWS] + */ + $atext = "($alpha|$digit|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2e\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])"; + $atom = "($cfws?$atext+$cfws?)"; + + /** + * qtext = NO-WS-CTL / ; Non white space controls + * %d33 / ; The rest of the US-ASCII + * %d35-91 / ; characters not including "\" + * %d93-126 ; or the quote character + * qcontent = qtext / quoted-pair + * quoted-string = [CFWS] + * DQUOTE *([FWS] qcontent) [FWS] DQUOTE + * [CFWS] + * word = atom / quoted-string + */ + $qtext = "($no_ws_ctl|[\\x21\\x23-\\x5b\\x5d-\\x7e])"; + $qcontent = "($qtext|$quoted_pair)"; + $quoted_string = "($cfws?\\x22($fws?$qcontent)*$fws?\\x22$cfws?)"; + $word = "($atom|$quoted_string)"; + + /** + * obs-local-part = word *("." word) + * obs-domain = atom *("." atom) + */ + $obs_local_part = "($word(\\x2e$word)*)"; + $obs_domain = "($atom(\\x2e$atom)*)"; + + /** + * dot-atom-text = 1*atext *("." 1*atext) + * dot-atom = [CFWS] dot-atom-text [CFWS] + */ + $dot_atom_text = "($atext+(\\x2e$atext+)*)"; + $dot_atom = "($cfws?$dot_atom_text$cfws?)"; + + /** + * domain-literal = [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS] + * dcontent = dtext / quoted-pair + * dtext = NO-WS-CTL / ; Non white space controls + * + * %d33-90 / ; The rest of the US-ASCII + * %d94-126 ; characters not including "[", + * ; "]", or "\" + */ + $dtext = "($no_ws_ctl|[\\x21-\\x5a\\x5e-\\x7e])"; + $dcontent = "($dtext|$quoted_pair)"; + $domain_literal = "($cfws?\\x5b($fws?$dcontent)*$fws?\\x5d$cfws?)"; + + /** + * local-part = dot-atom / quoted-string / obs-local-part + * domain = dot-atom / domain-literal / obs-domain + * addr-spec = local-part "@" domain + */ + $local_part = "($dot_atom|$quoted_string|$obs_local_part)"; + $domain = "($dot_atom|$domain_literal|$obs_domain)"; + $addr_spec = "($local_part\\x40$domain)"; + } + +} + +class Horde_Form_Type_matrix extends Horde_Form_Type { + + var $_cols; + var $_rows; + var $_matrix; + var $_new_input; + + /** + * Initializes the variable. + * + * Example: + * + * init(array('Column A', 'Column B'), + * array(1 => 'Row One', 2 => 'Row 2', 3 => 'Row 3'), + * array(array(true, true, false), + * array(true, false, true), + * array(fasle, true, false)), + * array('Row 4', 'Row 5')); + * + * + * @param array $cols A list of column headers. + * @param array $rows A hash with row IDs as the keys and row + * labels as the values. + * @param array $matrix A two dimensional hash with the field + * values. + * @param boolean|array $new_input If true, a free text field to add a new + * row is displayed on the top, a select + * box if this parameter is a value. + */ + function init($cols, $rows = array(), $matrix = array(), $new_input = false) + { + $this->_cols = $cols; + $this->_rows = $rows; + $this->_matrix = $matrix; + $this->_new_input = $new_input; + } + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + function getCols() { return $this->_cols; } + function getRows() { return $this->_rows; } + function getMatrix() { return $this->_matrix; } + function getNewInput() { return $this->_new_input; } + + function getInfo(&$vars, &$var, &$info) + { + $values = $vars->get($var->getVarName()); + if (!empty($values['n']['r']) && isset($values['n']['v'])) { + $new_row = $values['n']['r']; + $values['r'][$new_row] = $values['n']['v']; + unset($values['n']); + } + + $info = (isset($values['r']) ? $values['r'] : array()); + } + + function about() + { + return array( + 'name' => _("Field matrix"), + 'params' => array( + 'cols' => array('label' => _("Column titles"), + 'type' => 'stringarray'))); + } + +} + +class Horde_Form_Type_emailConfirm extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value['original'])) { + $message = _("This field is required."); + return false; + } + + if ($value['original'] != $value['confirm']) { + $message = _("Email addresses must match."); + return false; + } else { + $parsed_email = Horde_Mime_Address::parseAddressList($value['original'], + array('validate' => true)); + if (is_a($parsed_email, 'PEAR_Error')) { + $message = $parsed_email->getMessage(); + return false; + } + if (count($parsed_email) > 1) { + $message = _("Only one email address allowed."); + return false; + } + if (empty($parsed_email[0]->mailbox)) { + $message = _("You did not enter a valid email address."); + return false; + } + } + + return true; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Email with confirmation")); + } + +} + +class Horde_Form_Type_password extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + $valid = true; + + if ($var->isRequired()) { + $valid = strlen(trim($value)) > 0; + + if (!$valid) { + $message = _("This field is required."); + } + } + + return $valid; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Password")); + } + +} + +class Horde_Form_Type_passwordconfirm extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value['original'])) { + $message = _("This field is required."); + return false; + } + + if ($value['original'] != $value['confirm']) { + $message = _("Passwords must match."); + return false; + } + + return true; + } + + function getInfo(&$vars, &$var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = $value['original']; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Password with confirmation")); + } + +} + +class Horde_Form_Type_enum extends Horde_Form_Type { + + var $_values; + var $_prompt; + + function init($values, $prompt = null) + { + $this->setValues($values); + + if ($prompt === true) { + $this->_prompt = _("-- select --"); + } else { + $this->_prompt = $prompt; + } + } + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && $value == '' && !isset($this->_values[$value])) { + $message = _("This field is required."); + return false; + } + + if (count($this->_values) == 0 || isset($this->_values[$value]) || + ($this->_prompt && empty($value))) { + return true; + } + + $message = _("Invalid data submitted."); + return false; + } + + function getValues() + { + return $this->_values; + } + + /** + * @since Horde 3.2 + */ + function setValues($values) + { + $this->_values = $values; + } + + function getPrompt() + { + return $this->_prompt; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Drop down list"), + 'params' => array( + 'values' => array('label' => _("Values to select from"), + 'type' => 'stringarray'), + 'prompt' => array('label' => _("Prompt text"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_mlenum extends Horde_Form_Type { + + var $_values; + var $_prompts; + + function init(&$values, $prompts = null) + { + $this->_values = &$values; + + if ($prompts === true) { + $this->_prompts = array(_("-- select --"), _("-- select --")); + } elseif (!is_array($prompts)) { + $this->_prompts = array($prompts, $prompts); + } else { + $this->_prompts = $prompts; + } + } + + function onSubmit(&$var, &$vars) + { + $varname = $var->getVarName(); + $value = $vars->get($varname); + + if ($value['1'] != $value['old']) { + $var->form->setSubmitted(false); + } + } + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && (empty($value['1']) || empty($value['2']))) { + $message = _("This field is required."); + return false; + } + + if (!count($this->_values) || isset($this->_values[$value['1']]) || + (!empty($this->_prompts) && empty($value['1']))) { + return true; + } + + $message = _("Invalid data submitted."); + return false; + } + + function getValues() + { + return $this->_values; + } + + function getPrompts() + { + return $this->_prompts; + } + + function getInfo(&$vars, &$var, &$info) + { + $info = $vars->get($var->getVarName()); + return $info['2']; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Multi-level drop down lists"), + 'params' => array( + 'values' => array('label' => _("Values to select from"), + 'type' => 'stringarray'), + 'prompt' => array('label' => _("Prompt text"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_multienum extends Horde_Form_Type_enum { + + var $size = 5; + + function init($values, $size = null) + { + if (!is_null($size)) { + $this->size = (int)$size; + } + + parent::init($values); + } + + function isValid(&$var, &$vars, $value, &$message) + { + if (is_array($value)) { + foreach ($value as $val) { + if (!$this->isValid($var, $vars, $val, $message)) { + return false; + } + } + return true; + } + + if (empty($value) && ((string)(int)$value !== $value)) { + if ($var->isRequired()) { + $message = _("This field is required."); + return false; + } else { + return true; + } + } + + if (count($this->_values) == 0 || isset($this->_values[$value])) { + return true; + } + + $message = _("Invalid data submitted."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Multiple selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringarray'), + 'size' => array('label' => _("Size"), + 'type' => 'int')) + ); + } + +} + +class Horde_Form_Type_keyval_multienum extends Horde_Form_Type_multienum { + + function getInfo(&$vars, &$var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = array(); + foreach ($value as $key) { + $info[$key] = $this->_values[$key]; + } + } + +} + +class Horde_Form_Type_radio extends Horde_Form_Type_enum { + + /* Entirely implemented by Horde_Form_Type_enum; just a different + * view. */ + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Radio selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringarray'))); + } + +} + +class Horde_Form_Type_set extends Horde_Form_Type { + + var $_values; + var $_checkAll = false; + + function init($values, $checkAll = false) + { + $this->_values = $values; + $this->_checkAll = $checkAll; + } + + function isValid(&$var, &$vars, $value, &$message) + { + if (count($this->_values) == 0 || count($value) == 0) { + return true; + } + foreach ($value as $item) { + if (!isset($this->_values[$item])) { + $error = true; + break; + } + } + if (!isset($error)) { + return true; + } + + $message = _("Invalid data submitted."); + return false; + } + + function getValues() + { + return $this->_values; + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Set"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringarray'))); + } + +} + +class Horde_Form_Type_date extends Horde_Form_Type { + + var $_format; + + function init($format = '%a %d %B') + { + $this->_format = $format; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $valid = true; + + if ($var->isRequired()) { + $valid = strlen(trim($value)) > 0; + + if (!$valid) { + $message = sprintf(_("%s is required"), $var->getHumanName()); + } + } + + return $valid; + } + + /** + * @static + * + * @param mixed $date The date to calculate the difference from. Can be + * either a timestamp integer value, or an array + * with date parts: 'day', 'month', 'year'. + * + * @return string + */ + function getAgo($date) + { + if ($date === null) { + return ''; + } elseif (!is_array($date)) { + /* Date is not array, so assume timestamp. Work out the component + * parts using date(). */ + $date = array('day' => date('j', $date), + 'month' => date('n', $date), + 'year' => date('Y', $date)); + } + + require_once 'Date/Calc.php'; + $diffdays = Date_Calc::dateDiff((int)$date['day'], + (int)$date['month'], + (int)$date['year'], + date('j'), date('n'), date('Y')); + + /* An error occured. */ + if ($diffdays == -1) { + return; + } + + $ago = $diffdays * Date_Calc::compareDates((int)$date['day'], + (int)$date['month'], + (int)$date['year'], + date('j'), date('n'), + date('Y')); + if ($ago < -1) { + return sprintf(_(" (%s days ago)"), $diffdays); + } elseif ($ago == -1) { + return _(" (yesterday)"); + } elseif ($ago == 0) { + return _(" (today)"); + } elseif ($ago == 1) { + return _(" (tomorrow)"); + } else { + return sprintf(_(" (in %s days)"), $diffdays); + } + } + + function getFormattedTime($timestamp, $format = null, $showago = true) + { + if (empty($format)) { + $format = $this->_format; + } + if (!empty($timestamp)) { + return strftime($format, $timestamp) . ($showago ? Horde_Form_Type_date::getAgo($timestamp) : ''); + } else { + return ''; + } + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Date")); + } + +} + +class Horde_Form_Type_time extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value) && ((string)(double)$value !== $value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^[0-2]?[0-9]:[0-5][0-9]$/', $value)) { + return true; + } + + $message = _("This field may only contain numbers and the colon."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Time")); + } + +} + +class Horde_Form_Type_hourminutesecond extends Horde_Form_Type { + + var $_show_seconds; + + function init($show_seconds = false) + { + $this->_show_seconds = $show_seconds; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $time = $vars->get($var->getVarName()); + if (!$this->_show_seconds && count($time) && !isset($time['second'])) { + $time['second'] = 0; + } + + if (!$this->emptyTimeArray($time) && !$this->checktime($time['hour'], $time['minute'], $time['second'])) { + $message = _("Please enter a valid time."); + return false; + } elseif ($this->emptyTimeArray($time) && $var->isRequired()) { + $message = _("This field is required."); + return false; + } + + return true; + } + + function checktime($hour, $minute, $second) + { + if (!isset($hour) || $hour == '' || ($hour < 0 || $hour > 23)) { + return false; + } + if (!isset($minute) || $minute == '' || ($minute < 0 || $minute > 60)) { + return false; + } + if (!isset($second) || $second === '' || ($second < 0 || $second > 60)) { + return false; + } + + return true; + } + + /** + * Return the time supplied as a Horde_Date object. + * + * @param string $time_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch). + * + * @return Date The time object. + */ + function getTimeOb($time_in) + { + require_once 'Horde/Date.php'; + + if (is_array($time_in)) { + if (!$this->emptyTimeArray($time_in)) { + $time_in = sprintf('1970-01-01 %02d:%02d:%02d', $time_in['hour'], $time_in['minute'], $this->_show_seconds ? $time_in['second'] : 0); + } + } + + return new Horde_Date($time_in); + } + + /** + * Return the time supplied split up into an array. + * + * @param string $time_in Time in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch). + * + * @return array Array with three elements - hour, minute and seconds. + */ + function getTimeParts($time_in) + { + if (is_array($time_in)) { + /* This is probably a failed isValid input so just return the + * parts as they are. */ + return $time_in; + } elseif (empty($time_in)) { + /* This is just an empty field so return empty parts. */ + return array('hour' => '', 'minute' => '', 'second' => ''); + } + $time = $this->getTimeOb($time_in); + return array('hour' => $time->hour, + 'minute' => $time->min, + 'second' => $time->sec); + } + + function emptyTimeArray($time) + { + return (is_array($time) + && (!isset($time['hour']) || !strlen($time['hour'])) + && (!isset($time['minute']) || !strlen($time['minute'])) + && (!$this->_show_seconds || !strlen($time['second']))); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Time selection"), + 'params' => array( + 'seconds' => array('label' => _("Show seconds?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_monthyear extends Horde_Form_Type { + + var $_start_year; + var $_end_year; + + function init($start_year = null, $end_year = null) + { + if (empty($start_year)) { + $start_year = 1920; + } + if (empty($end_year)) { + $end_year = date('Y'); + } + + $this->_start_year = $start_year; + $this->_end_year = $end_year; + } + + function isValid(&$var, &$vars, $value, &$message) + { + if (!$var->isRequired()) { + return true; + } + + if (!$vars->get($this->getMonthVar($var)) || + !$vars->get($this->getYearVar($var))) { + $message = _("Please enter a month and a year."); + return false; + } + + return true; + } + + function getMonthVar($var) + { + return $var->getVarName() . '[month]'; + } + + function getYearVar($var) + { + return $var->getVarName() . '[year]'; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Month and year"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'))); + } + +} + +class Horde_Form_Type_monthdayyear extends Horde_Form_Type { + + var $_start_year; + var $_end_year; + var $_picker; + var $_format_in = null; + var $_format_out = '%x'; + + /** + * Return the date supplied as a Horde_Date object. + * + * @param integer $start_year The first available year for input. + * @param integer $end_year The last available year for input. + * @param boolean $picker Do we show the DHTML calendar? + * @param integer $format_in The format to use when sending the date + * for storage. Defaults to Unix epoch. + * Similar to the strftime() function. + * @param integer $format_out The format to use when displaying the + * date. Similar to the strftime() function. + */ + function init($start_year = '', $end_year = '', $picker = true, + $format_in = null, $format_out = '%x') + { + if (empty($start_year)) { + $start_year = date('Y'); + } + if (empty($end_year)) { + $end_year = date('Y') + 10; + } + + $this->_start_year = $start_year; + $this->_end_year = $end_year; + $this->_picker = $picker; + $this->_format_in = $format_in; + $this->_format_out = $format_out; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $date = $vars->get($var->getVarName()); + $empty = $this->emptyDateArray($date); + + if ($empty == 1 && $var->isRequired()) { + $message = _("This field is required."); + return false; + } elseif ($empty == 0 && !checkdate($date['month'], + $date['day'], + $date['year'])) { + $message = _("Please enter a valid date, check the number of days in the month."); + return false; + } elseif ($empty == -1) { + $message = _("Select all date components."); + return false; + } + + return true; + } + + /** + * Determine if the provided date value is completely empty, partially empty + * or non-empty. + * + * @param mixed $date String or date part array representation of date. + * + * @return integer 0 for non-empty, 1 for completely empty or -1 for + * partially empty. + */ + function emptyDateArray($date) + { + if (!is_array($date)) { + return (int)empty($date); + } + $empty = 0; + /* Check each date array component. */ + foreach (array('day', 'month', 'year') as $key) { + if (empty($date[$key])) { + $empty++; + } + } + + /* Check state of empty. */ + if ($empty == 0) { + /* If no empty parts return 0. */ + return 0; + } elseif ($empty == 3) { + /* If all empty parts return 1. */ + return 1; + } else { + /* If some empty parts return -1. */ + return -1; + } + } + + /** + * Return the date supplied split up into an array. + * + * @param string $date_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS + * and UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return array Array with three elements - year, month and day. + */ + function getDateParts($date_in) + { + if (is_array($date_in)) { + /* This is probably a failed isValid input so just return + * the parts as they are. */ + return $date_in; + } elseif (empty($date_in)) { + /* This is just an empty field so return empty parts. */ + return array('year' => '', 'month' => '', 'day' => ''); + } + + $date = $this->getDateOb($date_in); + return array('year' => $date->year, + 'month' => $date->month, + 'day' => $date->mday); + } + + /** + * Return the date supplied as a Horde_Date object. + * + * @param string $date_in Date in one of the three formats supported by + * Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS + * and UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return Date The date object. + */ + function getDateOb($date_in) + { + require_once 'Horde/Date.php'; + + if (is_array($date_in)) { + /* If passed an array change it to the ISO format. */ + if ($this->emptyDateArray($date_in) == 0) { + $date_in = sprintf('%04d-%02d-%02d 00:00:00', + $date_in['year'], + $date_in['month'], + $date_in['day']); + } + } elseif (preg_match('/^\d{4}-?\d{2}-?\d{2}$/', $date_in)) { + /* Fix the date if it is the shortened ISO. */ + $date_in = $date_in . ' 00:00:00'; + } + + return new Horde_Date($date_in); + } + + /** + * Return the date supplied as a Horde_Date object. + * + * @param string $date Either an already set up Horde_Date object or a + * string date in one of the three formats supported + * by Horde_Form and Horde_Date (ISO format + * YYYY-MM-DD HH:MM:SS, timestamp YYYYMMDDHHMMSS and + * UNIX epoch) plus the fourth YYYY-MM-DD. + * + * @return string The date formatted according to the $format_out + * parameter when setting up the monthdayyear field. + */ + function formatDate($date) + { + if (!is_a($date, 'Date')) { + $date = $this->getDateOb($date); + } + + return $date->strftime($this->_format_out); + } + + /** + * Insert the date input through the form into $info array, in the format + * specified by the $format_in parameter when setting up monthdayyear + * field. + */ + function getInfo(&$vars, &$var, &$info) + { + $info = $this->_validateAndFormat($var->getValue($vars), $var); + } + + /** + * Validate/format a date submission. + */ + function _validateAndFormat($value, &$var) + { + /* If any component is empty consider it a bad date and return the + * default. */ + if ($this->emptyDateArray($value) == 1) { + return $var->getDefault(); + } else { + $date = $this->getDateOb($value); + if ($this->_format_in === null) { + return $date->timestamp(); + } else { + return $date->strftime($this->_format_in); + } + } + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Date selection"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'), + 'picker' => array('label' => _("Show picker?"), + 'type' => 'boolean'), + 'format_in' => array('label' => _("Storage format"), + 'type' => 'text'), + 'format_out' => array('label' => _("Display format"), + 'type' => 'text'))); + } + +} + +/** + * @since Horde 3.2 + */ +class Horde_Form_Type_datetime extends Horde_Form_Type { + + var $_mdy; + var $_hms; + var $_show_seconds; + + /** + * Return the date supplied as a Horde_Date object. + * + * @param integer $start_year The first available year for input. + * @param integer $end_year The last available year for input. + * @param boolean $picker Do we show the DHTML calendar? + * @param integer $format_in The format to use when sending the date + * for storage. Defaults to Unix epoch. + * Similar to the strftime() function. + * @param integer $format_out The format to use when displaying the + * date. Similar to the strftime() function. + * @param boolean $show_seconds Include a form input for seconds. + */ + function init($start_year = '', $end_year = '', $picker = true, + $format_in = null, $format_out = '%x', $show_seconds = false) + { + $this->_mdy = new Horde_Form_Type_monthdayyear(); + $this->_mdy->init($start_year, $end_year, $picker, $format_in, $format_out); + + $this->_hms = new Horde_Form_Type_hourminutesecond(); + $this->_hms->init($show_seconds); + $this->_show_seconds = $show_seconds; + } + + function isValid(&$var, &$vars, $value, &$message) + { + $date = $vars->get($var->getVarName()); + if (!$this->_show_seconds && !isset($date['second'])) { + $date['second'] = ''; + } + $mdy_empty = $this->emptyDateArray($date); + $hms_empty = $this->emptyTimeArray($date); + + $valid = true; + + /* Require all fields if one field is not empty */ + if ($var->isRequired() || $mdy_empty != 1 || !$hms_empty) { + $old_required = $var->required; + $var->required = true; + + $mdy_valid = $this->_mdy->isValid($var, $vars, $value, $message); + $hms_valid = $this->_hms->isValid($var, $vars, $value, $message); + $var->required = $old_required; + + $valid = $mdy_valid && $hms_valid; + if ($mdy_valid && !$hms_valid) { + $message = _("You must choose a time."); + } elseif ($hms_valid && !$mdy_valid) { + $message = _("You must choose a date."); + } + } + + return $valid; + } + + function getInfo(&$vars, &$var, &$info) + { + /* If any component is empty consider it a bad date and return the + * default. */ + $value = $var->getValue($vars); + if ($this->emptyDateArray($value) == 1 || $this->emptyTimeArray($value)) { + $info = $var->getDefault(); + return; + } + + $date = $this->getDateOb($value); + $time = $this->getTimeOb($value); + $date->hour = $time->hour; + $date->min = $time->min; + $date->sec = $time->sec; + if ($this->getProperty('format_in') === null) { + $info = $date->timestamp(); + } else { + $info = $date->strftime($this->getProperty('format_in')); + } + } + + function getProperty($property) + { + if ($property == 'show_seconds') { + return $this->_hms->getProperty($property); + } else { + return $this->_mdy->getProperty($property); + } + } + + function setProperty($property, $value) + { + if ($property == 'show_seconds') { + $this->_hms->setProperty($property, $value); + } else { + $this->_mdy->setProperty($property, $value); + } + } + + function checktime($hour, $minute, $second) + { + return $this->_hms->checktime($hour, $minute, $second); + } + + function getTimeOb($time_in) + { + return $this->_hms->getTimeOb($time_in); + } + + function getTimeParts($time_in) + { + return $this->_hms->getTimeParts($time_in); + } + + function emptyTimeArray($time) + { + return $this->_hms->emptyTimeArray($time); + } + + function emptyDateArray($date) + { + return $this->_mdy->emptyDateArray($date); + } + + function getDateParts($date_in) + { + return $this->_mdy->getDateParts($date_in); + } + + function getDateOb($date_in) + { + return $this->_mdy->getDateOb($date_in); + } + + function formatDate($date) + { + if ($this->_mdy->emptyDateArray($date)) { + return ''; + } + return $this->_mdy->formatDate($date); + } + + function about() + { + return array( + 'name' => _("Date and time selection"), + 'params' => array( + 'start_year' => array('label' => _("Start year"), + 'type' => 'int'), + 'end_year' => array('label' => _("End year"), + 'type' => 'int'), + 'picker' => array('label' => _("Show picker?"), + 'type' => 'boolean'), + 'format_in' => array('label' => _("Storage format"), + 'type' => 'text'), + 'format_out' => array('label' => _("Display format"), + 'type' => 'text'), + 'seconds' => array('label' => _("Show seconds?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_colorpicker extends Horde_Form_Type { + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || preg_match('/^#([0-9a-z]){6}$/i', $value)) { + return true; + } + + $message = _("This field must contain a color code in the RGB Hex format, for example '#1234af'."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Colour selection")); + } + +} + +class Horde_Form_Type_sound extends Horde_Form_Type { + + var $_sounds = array(); + + function init() + { + foreach (glob($GLOBALS['registry']->get('themesfs', 'horde') . '/sounds/*.wav') as $sound) { + $this->_sounds[] = basename($sound); + } + } + + function getSounds() + { + return $this->_sounds; + } + + function isValid(&$var, &$vars, $value, &$message) + { + if ($var->isRequired() && empty($value)) { + $message = _("This field is required."); + return false; + } + + if (empty($value) || in_array($value, $this->_sounds)) { + return true; + } + + $message = _("Please choose a sound."); + return false; + } + + /** + * Return info about field type. + */ + function about() + { + return array('name' => _("Sound selection")); + } + +} + +class Horde_Form_Type_sorter extends Horde_Form_Type { + + var $_instance; + var $_values; + var $_size; + var $_header; + + function init($values, $size = 8, $header = '') + { + static $horde_sorter_instance = 0; + + /* Get the next progressive instance count for the horde + * sorter so that multiple sorters can be used on one page. */ + $horde_sorter_instance++; + $this->_instance = 'horde_sorter_' . $horde_sorter_instance; + $this->_values = $values; + $this->_size = $size; + $this->_header = $header; + } + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + function getValues() + { + return $this->_values; + } + + function getSize() + { + return $this->_size; + } + + function getHeader() + { + if (!empty($this->_header)) { + return $this->_header; + } + return ''; + } + + function getOptions($keys = null) + { + $html = ''; + if ($this->_header) { + $html .= ''; + } + + if (empty($keys)) { + $keys = array_keys($this->_values); + } else { + $keys = explode("\t", $keys['array']); + } + foreach ($keys as $sl_key) { + $html .= ''; + } + + return $html; + } + + function getInfo(&$vars, &$var, &$info) + { + $value = $vars->get($var->getVarName()); + $info = explode("\t", $value['array']); + } + + /** + * Return info about field type. + */ + function about() + { + return array( + 'name' => _("Sort order selection"), + 'params' => array( + 'values' => array('label' => _("Values"), + 'type' => 'stringarray'), + 'size' => array('label' => _("Size"), + 'type' => 'int'), + 'header' => array('label' => _("Header"), + 'type' => 'text'))); + } + +} + +class Horde_Form_Type_selectfiles extends Horde_Form_Type { + + /** + * The text to use in the link. + * + * @var string + */ + var $_link_text; + + /** + * The style to use for the link. + * + * @var string + */ + var $_link_style; + + /** + * Create the link with an icon instead of text? + * + * @var boolean + */ + var $_icon; + + /** + * Contains gollem selectfile selectionID + * + * @var string + */ + var $_selectid; + + function init($selectid, $link_text = null, $link_style = '', + $icon = false) + { + $this->_selectid = $selectid; + if (is_null($link_text)) { + $link_text = _("Select Files"); + } + $this->_link_text = $link_text; + $this->_link_style = $link_style; + $this->_icon = $icon; + } + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + function getInfo(&$var, &$vars, &$info) + { + $value = $vars->getValue($var); + $info = $GLOBALS['registry']->call('files/selectlistResults', array($value)); + } + + function about() + { + return array( + 'name' => _("File selection"), + 'params' => array( + 'selectid' => array('label' => _("Id"), + 'type' => 'text'), + 'link_text' => array('label' => _("Link text"), + 'type' => 'text'), + 'link_style' => array('label' => _("Link style"), + 'type' => 'text'), + 'icon' => array('label' => _("Show icon?"), + 'type' => 'boolean'))); + } + +} + +class Horde_Form_Type_assign extends Horde_Form_Type { + + var $_leftValues; + var $_rightValues; + var $_leftHeader; + var $_rightHeader; + var $_size; + var $_width; + + function init($leftValues, $rightValues, $leftHeader = '', + $rightHeader = '', $size = 8, $width = '200px') + { + $this->_leftValues = $leftValues; + $this->_rightValues = $rightValues; + $this->_leftHeader = $leftHeader; + $this->_rightHeader = $rightHeader; + $this->_size = $size; + $this->_width = $width; + } + + function isValid(&$var, &$vars, $value, &$message) + { + return true; + } + + function getValues($side) + { + return $side ? $this->_rightValues : $this->_leftValues; + } + + function setValues($side, $values) + { + if ($side) { + $this->_rightValues = $values; + } else { + $this->_leftValues = $values; + } + } + + function getHeader($side) + { + return $side ? $this->_rightHeader : $this->_leftHeader; + } + + function getSize() + { + return $this->_size; + } + + function getWidth() + { + return $this->_width; + } + + function getOptions($side, $formname, $varname) + { + $html = ''; + $headers = false; + if ($side) { + $values = $this->_rightValues; + if (!empty($this->_rightHeader)) { + $values = array('' => $this->_rightHeader) + $values; + $headers = true; + } + } else { + $values = $this->_leftValues; + if (!empty($this->_leftHeader)) { + $values = array('' => $this->_leftHeader) + $values; + $headers = true; + } + } + + foreach ($values as $key => $val) { + $html .= '