1<?php
2/**
3 * $Header$
4 * $Horde: horde/lib/Log/sql.php,v 1.12 2000/08/16 20:27:34 chuck Exp $
5 *
6 * @version $Revision$
7 * @package Log
8 */
9
10/**
11 * We require the PEAR DB class.  This is generally defined in the DB.php file,
12 * but it's possible that the caller may have provided the DB class, or a
13 * compatible wrapper (such as the one shipped with MDB2), so we first check
14 * for an existing 'DB' class before including 'DB.php'.
15 */
16if (!class_exists('DB')) {
17    require_once 'DB.php';
18}
19
20/**
21 * The Log_sql class is a concrete implementation of the Log::
22 * abstract class which sends messages to an SQL server.  Each entry
23 * occupies a separate row in the database.
24 *
25 * This implementation uses PHP's PEAR database abstraction layer.
26 *
27 * CREATE TABLE log_table (
28 *  id          INT NOT NULL,
29 *  logtime     TIMESTAMP NOT NULL,
30 *  ident       CHAR(16) NOT NULL,
31 *  priority    INT NOT NULL,
32 *  message     VARCHAR(200),
33 *  PRIMARY KEY (id)
34 * );
35 *
36 * @author  Jon Parise <jon@php.net>
37 * @since   Horde 1.3
38 * @since   Log 1.0
39 * @package Log
40 *
41 * @example sql.php     Using the SQL handler.
42 */
43class Log_sql extends Log
44{
45    /**
46     * Variable containing the DSN information.
47     * @var mixed
48     * @access private
49     */
50    var $_dsn = '';
51
52    /**
53     * String containing the SQL insertion statement.
54     *
55     * @var string
56     * @access private
57     */
58    var $_sql = '';
59
60    /**
61     * Array containing our set of DB configuration options.
62     * @var array
63     * @access private
64     */
65    var $_options = array('persistent' => true);
66
67    /**
68     * Object holding the database handle.
69     * @var object
70     * @access private
71     */
72    var $_db = null;
73
74    /**
75     * Resource holding the prepared statement handle.
76     * @var resource
77     * @access private
78     */
79    var $_statement = null;
80
81    /**
82     * Flag indicating that we're using an existing database connection.
83     * @var boolean
84     * @access private
85     */
86    var $_existingConnection = false;
87
88    /**
89     * String holding the database table to use.
90     * @var string
91     * @access private
92     */
93    var $_table = 'log_table';
94
95    /**
96     * String holding the name of the ID sequence.
97     * @var string
98     * @access private
99     */
100    var $_sequence = 'log_id';
101
102    /**
103     * Maximum length of the $ident string.  This corresponds to the size of
104     * the 'ident' column in the SQL table.
105     * @var integer
106     * @access private
107     */
108    var $_identLimit = 16;
109
110    /**
111     * Constructs a new sql logging object.
112     *
113     * @param string $name         The target SQL table.
114     * @param string $ident        The identification field.
115     * @param array $conf          The connection configuration array.
116     * @param int $level           Log messages up to and including this level.
117     * @access public
118     */
119    public function __construct($name, $ident = '', $conf = array(),
120                                $level = PEAR_LOG_DEBUG)
121    {
122        $this->_id = md5(microtime().rand());
123        $this->_table = $name;
124        $this->_mask = Log::UPTO($level);
125
126        /* Now that we have a table name, assign our SQL statement. */
127        if (!empty($conf['sql'])) {
128            $this->_sql = $conf['sql'];
129        } else {
130            $this->_sql = 'INSERT INTO ' . $this->_table .
131                          ' (id, logtime, ident, priority, message)' .
132                          ' VALUES(?, CURRENT_TIMESTAMP, ?, ?, ?)';
133        }
134
135        /* If an options array was provided, use it. */
136        if (isset($conf['options']) && is_array($conf['options'])) {
137            $this->_options = $conf['options'];
138        }
139
140        /* If a specific sequence name was provided, use it. */
141        if (!empty($conf['sequence'])) {
142            $this->_sequence = $conf['sequence'];
143        }
144
145        /* If a specific sequence name was provided, use it. */
146        if (isset($conf['identLimit'])) {
147            $this->_identLimit = $conf['identLimit'];
148        }
149
150        /* Now that the ident limit is confirmed, set the ident string. */
151        $this->setIdent($ident);
152
153        /* If an existing database connection was provided, use it. */
154        if (isset($conf['db'])) {
155            $this->_db = &$conf['db'];
156            $this->_existingConnection = true;
157            $this->_opened = true;
158        } else {
159            $this->_dsn = $conf['dsn'];
160        }
161    }
162
163    /**
164     * Opens a connection to the database, if it has not already
165     * been opened. This is implicitly called by log(), if necessary.
166     *
167     * @return boolean   True on success, false on failure.
168     * @access public
169     */
170    function open()
171    {
172        if (!$this->_opened) {
173            /* Use the DSN and options to create a database connection. */
174            $this->_db = &DB::connect($this->_dsn, $this->_options);
175            if (DB::isError($this->_db)) {
176                return false;
177            }
178
179            /* Create a prepared statement for repeated use in log(). */
180            if (!$this->_prepareStatement()) {
181                return false;
182            }
183
184            /* We now consider out connection open. */
185            $this->_opened = true;
186        }
187
188        return $this->_opened;
189    }
190
191    /**
192     * Closes the connection to the database if it is still open and we were
193     * the ones that opened it.  It is the caller's responsible to close an
194     * existing connection that was passed to us via $conf['db'].
195     *
196     * @return boolean   True on success, false on failure.
197     * @access public
198     */
199    function close()
200    {
201        if ($this->_opened && !$this->_existingConnection) {
202            $this->_opened = false;
203            $this->_db->freePrepared($this->_statement);
204            return $this->_db->disconnect();
205        }
206
207        return ($this->_opened === false);
208    }
209
210    /**
211     * Sets this Log instance's identification string.  Note that this
212     * SQL-specific implementation will limit the length of the $ident string
213     * to sixteen (16) characters.
214     *
215     * @param string    $ident      The new identification string.
216     *
217     * @access  public
218     * @since   Log 1.8.5
219     */
220    function setIdent($ident)
221    {
222        $this->_ident = substr($ident, 0, $this->_identLimit);
223    }
224
225    /**
226     * Inserts $message to the currently open database.  Calls open(),
227     * if necessary.  Also passes the message along to any Log_observer
228     * instances that are observing this Log.
229     *
230     * @param mixed  $message  String or object containing the message to log.
231     * @param string $priority The priority of the message.  Valid
232     *                  values are: PEAR_LOG_EMERG, PEAR_LOG_ALERT,
233     *                  PEAR_LOG_CRIT, PEAR_LOG_ERR, PEAR_LOG_WARNING,
234     *                  PEAR_LOG_NOTICE, PEAR_LOG_INFO, and PEAR_LOG_DEBUG.
235     * @return boolean  True on success or false on failure.
236     * @access public
237     */
238    function log($message, $priority = null)
239    {
240        /* If a priority hasn't been specified, use the default value. */
241        if ($priority === null) {
242            $priority = $this->_priority;
243        }
244
245        /* Abort early if the priority is above the maximum logging level. */
246        if (!$this->_isMasked($priority)) {
247            return false;
248        }
249
250        /* If the connection isn't open and can't be opened, return failure. */
251        if (!$this->_opened && !$this->open()) {
252            return false;
253        }
254
255        /* If we don't already have our statement object yet, create it. */
256        if (!is_object($this->_statement) && !$this->_prepareStatement()) {
257            return false;
258        }
259
260        /* Extract the string representation of the message. */
261        $message = $this->_extractMessage($message);
262
263        /* Build our set of values for this log entry. */
264        $id = $this->_db->nextId($this->_sequence);
265        $values = array($id, $this->_ident, $priority, $message);
266
267        /* Execute the SQL query for this log entry insertion. */
268        $result =& $this->_db->execute($this->_statement, $values);
269        if (DB::isError($result)) {
270            return false;
271        }
272
273        $this->_announce(array('priority' => $priority, 'message' => $message));
274
275        return true;
276    }
277
278    /**
279     * Prepare the SQL insertion statement.
280     *
281     * @return boolean  True if the statement was successfully created.
282     *
283     * @access  private
284     * @since   Log 1.9.1
285     */
286    function _prepareStatement()
287    {
288        $this->_statement = $this->_db->prepare($this->_sql);
289
290        /* Return success if we didn't generate an error. */
291        return (DB::isError($this->_statement) === false);
292    }
293}
294