PageRenderTime 39ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/horde-3.3.13/lib/Horde/History.php

#
PHP | 407 lines | 239 code | 41 blank | 127 comment | 46 complexity | 9003cd8773d5bb9aad2d70eb4c5f23f1 MD5 | raw file
Possible License(s): LGPL-2.0
  1. <?php
  2. /**
  3. * The History:: class provides a method of tracking changes in Horde
  4. * objects, stored in a SQL table.
  5. *
  6. * $Horde: framework/History/History.php,v 1.28.2.25 2010/11/08 16:17:28 jan Exp $
  7. *
  8. * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
  9. *
  10. * See the enclosed file COPYING for license information (LGPL). If you
  11. * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
  12. *
  13. * @author Chuck Hagenbuch <chuck@horde.org>
  14. * @since Horde 2.1
  15. * @package Horde_History
  16. */
  17. class Horde_History {
  18. /**
  19. * Pointer to a DB instance to manage the history.
  20. *
  21. * @var DB
  22. */
  23. var $_db;
  24. /**
  25. * Handle for the current database connection, used for writing. Defaults
  26. * to the same handle as $_db if a separate write database is not required.
  27. *
  28. * @var DB
  29. */
  30. var $_write_db;
  31. /**
  32. * Attempts to return a reference to a concrete History instance.
  33. * It will only create a new instance if no History instance
  34. * currently exists.
  35. *
  36. * This method must be invoked as: $var = &History::singleton()
  37. *
  38. * @return Horde_History The concrete History reference, or false on an
  39. * error.
  40. */
  41. function &singleton()
  42. {
  43. static $history;
  44. if (!isset($history)) {
  45. $history = new Horde_History();
  46. }
  47. return $history;
  48. }
  49. /**
  50. * Constructor.
  51. */
  52. function Horde_History()
  53. {
  54. global $conf;
  55. if (empty($conf['sql']['phptype']) ||
  56. $conf['sql']['phptype'] == 'none') {
  57. $this->_db = $this->_write_db =
  58. PEAR::raiseError(_("The History system is disabled."));
  59. return;
  60. }
  61. require_once 'DB.php';
  62. $this->_write_db = &DB::connect($conf['sql']);
  63. /* Set DB portability options. */
  64. if (is_a($this->_write_db, 'DB_common')) {
  65. switch ($this->_write_db->phptype) {
  66. case 'mssql':
  67. $this->_write_db->setOption('portability',
  68. DB_PORTABILITY_LOWERCASE |
  69. DB_PORTABILITY_ERRORS |
  70. DB_PORTABILITY_RTRIM);
  71. break;
  72. default:
  73. $this->_write_db->setOption('portability',
  74. DB_PORTABILITY_LOWERCASE |
  75. DB_PORTABILITY_ERRORS);
  76. }
  77. }
  78. /* Check if we need to set up the read DB connection
  79. * seperately. */
  80. if (!empty($conf['sql']['splitread'])) {
  81. $params = array_merge($conf['sql'], $conf['sql']['read']);
  82. $this->_db = &DB::connect($params);
  83. /* Set DB portability options. */
  84. if (is_a($this->_db, 'DB_common')) {
  85. switch ($this->_db->phptype) {
  86. case 'mssql':
  87. $this->_db->setOption('portability',
  88. DB_PORTABILITY_LOWERCASE |
  89. DB_PORTABILITY_ERRORS |
  90. DB_PORTABILITY_RTRIM);
  91. break;
  92. default:
  93. $this->_db->setOption('portability',
  94. DB_PORTABILITY_LOWERCASE |
  95. DB_PORTABILITY_ERRORS);
  96. }
  97. }
  98. } else {
  99. /* Default to the same DB handle for reads. */
  100. $this->_db =& $this->_write_db;
  101. }
  102. }
  103. /**
  104. * Logs an event to an item's history log. The item must be uniquely
  105. * identified by $guid. Any other details about the event are passed in
  106. * $attributes. Standard suggested attributes are:
  107. *
  108. * 'who' => The id of the user that performed the action (will be added
  109. * automatically if not present).
  110. *
  111. * 'ts' => Timestamp of the action (this will be added automatically if
  112. * it is not present).
  113. *
  114. * @param string $guid The unique identifier of the entry to
  115. * add to.
  116. * @param array $attributes The hash of name => value entries that
  117. * describe this event.
  118. * @param boolean $replaceAction If $attributes['action'] is already
  119. * present in the item's history log,
  120. * update that entry instead of creating a
  121. * new one.
  122. *
  123. * @return boolean|PEAR_Error True on success, PEAR_Error on failure.
  124. */
  125. function log($guid, $attributes = array(), $replaceAction = false)
  126. {
  127. if (is_a($this->_write_db, 'PEAR_Error')) {
  128. return $this->_write_db;
  129. }
  130. $history = &$this->getHistory($guid);
  131. if (!$history || is_a($history, 'PEAR_Error')) {
  132. return $history;
  133. }
  134. if (!isset($attributes['who'])) {
  135. $attributes['who'] = Auth::getAuth();
  136. }
  137. if (!isset($attributes['ts'])) {
  138. $attributes['ts'] = time();
  139. }
  140. /* If we want to replace an entry with the same action, try and find
  141. * one. Track whether or not we succeed in $done, so we know whether
  142. * or not to add the entry later. */
  143. $done = false;
  144. if ($replaceAction && !empty($attributes['action'])) {
  145. $count = count($history->data);
  146. for ($i = 0; $i < $count; $i++) {
  147. if (!empty($history->data[$i]['action']) &&
  148. $history->data[$i]['action'] == $attributes['action']) {
  149. $values = array($attributes['ts'],
  150. $attributes['who'],
  151. isset($attributes['desc']) ? $attributes['desc'] : null);
  152. unset($attributes['ts']);
  153. unset($attributes['who']);
  154. unset($attributes['desc']);
  155. unset($attributes['action']);
  156. if ($attributes) {
  157. $values[] = serialize($attributes);
  158. } else {
  159. $values[] = null;
  160. }
  161. $values[] = $history->data[$i]['id'];
  162. $query = 'UPDATE horde_histories SET history_ts = ?, history_who = ?, history_desc = ?, history_extra = ? WHERE history_id = ?';
  163. Horde::logMessage('SQL query by Horde_History::log(): ' . $query . ', values: ' . implode(',', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
  164. $r = $this->_write_db->query($query, $values);
  165. if (is_a($r, 'PEAR_Error')) {
  166. Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
  167. return $r;
  168. }
  169. $done = true;
  170. break;
  171. }
  172. }
  173. }
  174. /* If we're not replacing by action, or if we didn't find an entry to
  175. * replace, insert a new row. */
  176. if (!$done) {
  177. $history_id = $this->_write_db->nextId('horde_histories');
  178. if (is_a($history_id, 'PEAR_Error')) {
  179. Horde::logMessage($history_id, __FILE__, __LINE__, PEAR_LOG_ERR);
  180. return $history_id;
  181. }
  182. $values = array($history_id,
  183. $guid,
  184. $attributes['ts'],
  185. $attributes['who'],
  186. isset($attributes['desc']) ? $attributes['desc'] : null,
  187. isset($attributes['action']) ? $attributes['action'] : null);
  188. unset($attributes['ts']);
  189. unset($attributes['who']);
  190. unset($attributes['desc']);
  191. unset($attributes['action']);
  192. if ($attributes) {
  193. $values[] = serialize($attributes);
  194. } else {
  195. $values[] = null;
  196. }
  197. $query = 'INSERT INTO horde_histories (history_id, object_uid, history_ts, history_who, history_desc, history_action, history_extra) VALUES (?, ?, ?, ?, ?, ?, ?)';
  198. Horde::logMessage('SQL query by Horde_History::log(): ' . $query . ', values: ' . implode(',', $values), __FILE__, __LINE__, PEAR_LOG_DEBUG);
  199. $r = $this->_write_db->query($query, $values);
  200. if (is_a($r, 'PEAR_Error')) {
  201. Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
  202. return $r;
  203. }
  204. }
  205. return true;
  206. }
  207. /**
  208. * Returns a HistoryObject corresponding to the named history
  209. * entry, with the data retrieved appropriately. $autocreate has
  210. * no affect.
  211. *
  212. * @param string $guid The name of the history entry to retrieve.
  213. * @param boolean $autocreate Deprecated.
  214. */
  215. function &getHistory($guid, $autocreate = null)
  216. {
  217. if (is_a($this->_db, 'PEAR_Error')) {
  218. $false = false;
  219. return $false;
  220. }
  221. $query = 'SELECT * FROM horde_histories WHERE object_uid = ?';
  222. Horde::logMessage('SQL query by Horde_History::getHistory(): ' . $query . ', values: ' . $guid, __FILE__, __LINE__, PEAR_LOG_DEBUG);
  223. $rows = $this->_db->getAll($query, array($guid), DB_FETCHMODE_ASSOC);
  224. if (is_a($rows, 'PEAR_Error')) {
  225. Horde::logMessage($rows, __FILE__, __LINE__, PEAR_LOG_ERR);
  226. return $rows;
  227. }
  228. $history = &new HistoryObject($guid, $rows);
  229. return $history;
  230. }
  231. /**
  232. * Finds history objects by timestamp, and optionally filter on other
  233. * fields as well.
  234. *
  235. * @param string $cmp The comparison operator (<, >, <=, >=, or =) to
  236. * check the timestamps with.
  237. * @param integer $ts The timestamp to compare against.
  238. * @param array $filters An array of additional (ANDed) criteria.
  239. * Each array value should be an array with 3
  240. * entries:
  241. * <pre>
  242. * 'op' - the operator to compare this field
  243. * with.
  244. * 'field' - the history field being compared
  245. * (i.e. 'action').
  246. * 'value' - the value to check for (i.e. 'add').
  247. * </pre>
  248. * @param string $parent The parent history to start searching at. If non-empty,
  249. * will be searched for with a LIKE '$parent:%' clause.
  250. *
  251. * @return array An array of history object ids, or an empty array if
  252. * none matched the criteria.
  253. */
  254. function getByTimestamp($cmp, $ts, $filters = array(), $parent = null)
  255. {
  256. if (is_a($this->_db, 'PEAR_Error')) {
  257. return false;
  258. }
  259. /* Build the timestamp test. */
  260. $where = array("history_ts $cmp $ts");
  261. /* Add additional filters, if there are any. */
  262. if ($filters) {
  263. foreach ($filters as $filter) {
  264. $where[] = 'history_' . $filter['field'] . ' ' . $filter['op'] . ' ' . $this->_db->quote($filter['value']);
  265. }
  266. }
  267. if ($parent) {
  268. $where[] = 'object_uid LIKE ' . $this->_db->quote($parent . ':%');
  269. }
  270. $query = 'SELECT DISTINCT object_uid, history_id FROM horde_histories WHERE ' . implode(' AND ', $where);
  271. Horde::logMessage('SQL query by Horde_History::getByTimestamp(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
  272. $r = $this->_db->getAssoc($query);
  273. if (is_a($r, 'PEAR_Error')) {
  274. Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
  275. }
  276. return $r;
  277. }
  278. /**
  279. * Gets the timestamp of the most recent change to $guid.
  280. *
  281. * @param string $guid The name of the history entry to retrieve.
  282. * @param string $action An action: 'add', 'modify', 'delete', etc.
  283. *
  284. * @return integer The timestamp, or 0 if no matching entry is found.
  285. */
  286. function getActionTimestamp($guid, $action)
  287. {
  288. /* This implementation still works, but we should be able to
  289. * get much faster now with a SELECT MAX(history_ts)
  290. * ... query. */
  291. $history = &$this->getHistory($guid);
  292. if (!$history || is_a($history, 'PEAR_Error')) {
  293. return 0;
  294. }
  295. $last = 0;
  296. if (is_array($history->data)) {
  297. foreach ($history->data as $entry) {
  298. if ($entry['action'] == $action && $entry['ts'] > $last) {
  299. $last = $entry['ts'];
  300. }
  301. }
  302. }
  303. return (int)$last;
  304. }
  305. /**
  306. * Remove one or more history entries by name.
  307. *
  308. * @param array $names The history entries to remove.
  309. */
  310. function removeByNames($names)
  311. {
  312. if (is_a($this->_write_db, 'PEAR_Error')) {
  313. return false;
  314. }
  315. if (!count($names)) {
  316. return true;
  317. }
  318. $ids = array();
  319. foreach ($names as $name) {
  320. $ids[] = $this->_write_db->quote($name);
  321. }
  322. $query = 'DELETE FROM horde_histories WHERE object_uid IN (' . implode(',', $ids) . ')';
  323. Horde::logMessage('SQL query by Horde_History::removeByNames(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG);
  324. $r = $this->_write_db->query($query);
  325. if (is_a($r, 'PEAR_Error')) {
  326. Horde::logMessage($r, __FILE__, __LINE__, PEAR_LOG_ERR);
  327. }
  328. return $r;
  329. }
  330. }
  331. /**
  332. * Class for presenting History information.
  333. *
  334. * @author Chuck Hagenbuch <chuck@horde.org>
  335. * @since Horde 2.1
  336. * @package Horde_History
  337. */
  338. class HistoryObject {
  339. var $uid;
  340. var $data = array();
  341. function HistoryObject($uid, $data = array())
  342. {
  343. $this->uid = $uid;
  344. if (!$data || is_a($data, 'PEAR_Error')) {
  345. return;
  346. }
  347. foreach ($data as $row) {
  348. $history = array('action' => $row['history_action'],
  349. 'desc' => $row['history_desc'],
  350. 'who' => $row['history_who'],
  351. 'id' => $row['history_id'],
  352. 'ts' => $row['history_ts']);
  353. if ($row['history_extra']) {
  354. $extra = @unserialize($row['history_extra']);
  355. if ($extra) {
  356. $history = array_merge($history, $extra);
  357. }
  358. }
  359. $this->data[] = $history;
  360. }
  361. }
  362. function getData()
  363. {
  364. return $this->data;
  365. }
  366. }