1: <?php
2: require_once 'MDB2.php';
3:
4: /**
5: * Klutz Driver implementation for comics as files with SUM info stored
6: * in SQL database.
7: *
8: * Required parameters:<pre>
9: * 'directory' The main directory the comics are stored in</pre>
10: *
11: * @author Marcus I. Ryan <marcus@riboflavin.net>
12: * @author Florian Steinel <fsteinel@klutz.horde.flonet.net>
13: * @package Klutz
14: */
15: class Klutz_Driver_Sql extends Klutz_Driver
16: {
17: /**
18: * The base directory we store comics in.
19: *
20: * @var string
21: */
22: var $basedir = null;
23:
24: /**
25: * The format for the various subdirectories.
26: * WARNING: DO NOT CHANGE THIS!
27: *
28: * @var string
29: */
30: var $subdir = 'Ymd';
31:
32: /**
33: * The MDB2 database object
34: *
35: * @var
36: */
37: var $_db = null;
38:
39: /**
40: * Constructs a new SQL storage object.
41: *
42: * @param array $params A hash containing connection parameters.
43: */
44: function Klutz_Driver_sql($params = array())
45: {
46: if (empty($params['basedir'])) {
47: return null;
48: }
49:
50: $this->basedir = $params['basedir'];
51: if (substr($this->basedir, -1, 1) != "/") {
52: $this->basedir .= "/";
53: }
54:
55: /* Setup the database */
56: $config = $GLOBALS['conf']['sql'];
57: unset($config['charset']);
58: $this->_db = MDB2::factory($config);
59: $this->_db->setOption('seqcol_name', 'id');
60:
61: }
62:
63: /**
64: * We do nothing in this function for the SQL driver since we grab
65: * the info on demand from the database. We keep the function here,
66: * however to honor our 'interface' since we call this function from
67: * various places in the client code.
68: */
69: function loadSums()
70: {
71: }
72:
73: /**
74: * Rebuild the table of unique identifiers.
75: *
76: * @return void
77: */
78: function rebuildSums()
79: {
80: /* First, wipe out the existing SUMS */
81: $this->removeSum();
82:
83: $d = dir($this->basedir);
84: while (false !== ($entry = $d->read())) {
85: if (is_dir($d->path . $entry) && strlen($entry) == 8
86: && is_numeric($entry)) {
87: // we're reasonably sure this is a valid dir.
88: $sd = dir($this->basedir . $entry);
89: while (false !== ($file = $sd->read())) {
90: $comicname = $file;
91: $file = $sd->path . '/' . $file;
92: if (is_file($file)) {
93: ob_start();
94: readfile($file);
95:
96: // We need to strtotime() the date since we are grabing
97: // it from the directory, and it's in the $subdir
98: // format.
99: $this->addSum($comicname, strtotime($entry), md5(ob_get_contents()));
100: ob_end_clean();
101: }
102: }
103: }
104: }
105: $d->close();
106: }
107:
108: /**
109: * Add a unique identifier for a given image.
110: *
111: * @param string $index The index for the comic
112: * @param timestamp $date The date of the comic
113: * @param string $data The md5 of the raw (binary) image data
114: *
115: * @return boolean|PEAR_Error True on success, PEAR_Error on failure.
116: */
117: function addSum($index, $date, $data)
118: {
119: $id = $this->_db->nextId('klutz_comics');
120: $key = $this->basedir . date($this->subdir, $date) . '/' . $index;
121:
122: /* Build the SQL query. */
123: $query = $this->_db->prepare('INSERT INTO ' . 'klutz_comics (comicpic_id, comicpic_date, comicpic_key, comicpic_hash) VALUES (?, ?, ?, ?)');
124: $values = array($id, $date, $key, $data);
125:
126: /* Log the query at a DEBUG log level. */
127: Horde::logMessage(sprintf("Klutz_Driver_sql::addSum(): %s values: %s", $query->query, print_r($values, true)),
128: __FILE__, __LINE__, PEAR_LOG_DEBUG);
129:
130: /* Execute the query. */
131: $result = $query->execute($values);
132: if (is_a($result, 'PEAR_Error')) {
133: return $result;
134: }
135:
136: return true;
137: }
138:
139: /**
140: * Remove the unique identifier for the given comic and/or
141: * date. If both are passed, removes the uid for that comic and
142: * date. If only a comic is passed, removes all uids for that
143: * comic. If only a date is passed removes uids for all comics on
144: * that date. If neither is passed, all uids are wiped out.
145: *
146: * @param string $index Index for the comic to delete. If left out all
147: * comics will be assumed.
148: * @param timestamp $date Date to remove. If left out, assumes all dates.
149: *
150: * @return int|PEAR_Error number of affected Comics on success, PEAR_Error on failure.
151: */
152: function removeSum($index = null, $date = null)
153: {
154: $sql = 'DELETE FROM klutz_comics';
155:
156: if (is_null($index) && is_null($date)) {
157: $values = array();
158: } elseif (is_null($date) && !is_null($index)) {
159: $sql .= ' WHERE comicpic_key = ?';
160: $values = array($index);
161: } elseif (is_null($index) && !is_null($date)) {
162: $sql .= ' WHERE comicpic_date = ?';
163: $values = array($date);
164: } else {
165: $sql .= ' WHERE comicpic_key = ? AND comicpic_date = ?';
166: $values = array($index, $date);
167: }
168: $query = $this->_db->prepare($sql);
169:
170: /* Log the query at a DEBUG log level. */
171: Horde::logMessage('Klutz_Driver_sql::removeSum(): ' . $sql,
172: __FILE__, __LINE__, PEAR_LOG_DEBUG);
173:
174: /* Execute the query. */
175: $result = $query->execute($values);
176:
177: if (isset($result) && !is_a($result, 'PEAR_Error')) {
178: return $result->numRows();
179: $result->free();
180: } else {
181: return $result;
182: }
183: }
184:
185: /**
186: * Determine if the image passed is a unique image (one we don't already
187: * have).
188: *
189: * This allows for $days = random, etc., but keeps us from getting the same
190: * comic day after day.
191: *
192: * @param Klutz_Image $image Raw (binary) image data.
193: *
194: * @return boolean True if unique, false otherwise.
195: */
196: function isUnique($image)
197: {
198: if (!is_a($image, 'Klutz_Image')) {
199: return null;
200: }
201:
202: /* Build the SQL query. */
203: $query = $this->_db->prepare('SELECT COUNT(*) FROM klutz_comics WHERE comicpic_hash = ?');
204: $params = array(md5($image->data));
205:
206: /* Log the query at a DEBUG log level. */
207: Horde::logMessage('Klutz_Driver_sql::isUnique(): ' . $query->query,
208: __FILE__, __LINE__, PEAR_LOG_DEBUG);
209:
210: /* Execute the query. */
211: $result = $query->execute($params);
212: $result = $result->fetchOne();
213: if (!is_a($result, 'PEAR_Error') && $result > 0) {
214: return false;
215: } else {
216: return true;
217: }
218: }
219:
220: /**
221: * Get a list of the dates for which we have comics between
222: * $oldest and $newest. Only returns dates we have at least one
223: * comic for.
224: *
225: * @param timestamp $date The reference date (default today)
226: * @param timestamp $oldest The earliest possible date to return (default
227: * first of the month)
228: * @param timestamp $newest The latest possible date to return (default
229: * last date of the month)
230: *
231: * @return mixed An array of dates in $subdir format between $oldest and
232: * $newest that we have comics for | PEAR_Error
233: */
234: function listDates($date = null, $oldest = null, $newest = null)
235: {
236:
237: if (is_null($date)) {
238: $date = mktime(0, 0, 0);
239: }
240:
241: $return = array();
242:
243: // Using Date as a reference, return all dates for the same
244: // time of day.
245: $d = getdate($date);
246:
247: if (is_null($oldest)) {
248: $oldest = mktime(0, 0, 0, $d['mon'], 1, $d['year']);
249: }
250:
251: if (is_null($newest)) {
252: $newest = mktime(0, 0, 0, $d['mon'] + 1, 0, $d['year']);
253: }
254:
255: /* Build the SQL query. */
256: $query = $this->_db->prepare('SELECT DISTINCT comicpic_date FROM klutz_comics WHERE comicpic_date >= ? AND comicpic_date <= ?');
257: $values = array($oldest, $newest);
258:
259: /* Log the query at a DEBUG log level. */
260: Horde::logMessage(sprintf('Klutz_Driver_sql::listDates($date = %s, $oldest = %s, $newest = %s): %s',
261: $date, $oldest, $newest, $query->query),
262: __FILE__, __LINE__, PEAR_LOG_DEBUG);
263:
264: /* Execute the query. */
265: $result = $query->execute($values);
266:
267:
268: if (isset($result) && !is_a($result, 'PEAR_Error')) {
269: $row = $result->fetchRow(MDB2_FETCHMODE_ASSOC);
270: if (is_a($row, 'PEAR_Error')) {
271: return $row;
272: }
273:
274: /* Store the retrieved values in the $return variable. */
275: while ($row && !is_a($row, 'PEAR_Error')) {
276: /* Add this new date to the $return list. */
277: $comicdate = date($this->subdir, $row['comicpic_date']);
278: $return[] = mktime(0, 0, 0, substr($comicdate, 4, 2),
279: substr($comicdate, -2), substr($comicdate, 0, 4));
280:
281: /* Advance to the new row in the result set. */
282: $row = $result->fetchRow(MDB2_FETCHMODE_ASSOC);
283: }
284: $result->free();
285: } else {
286: return $result;
287: }
288:
289: /* Fallback solution, if the query db doesn't return a result */
290: if (count($return) == 0) {
291: $d = dir($this->basedir);
292: while (false !== ($entry = $d->read())) {
293: if (is_dir($d->path . $entry) && strlen($entry) == 8
294: && is_numeric($entry)) {
295: // We're reasonably sure this is a valid dir.
296: $time = mktime(0, 0, 0, substr($entry, 4, 2),
297: substr($entry, -2), substr($entry, 0, 4));
298: if ($time >= $oldest && $time <= $newest) {
299: $return[] = $time;
300: }
301: }
302: }
303: $d->close();
304: }
305: sort($return, SORT_NUMERIC);
306: return $return;
307: }
308:
309: /**
310: * Get the image dimensions for the requested image.
311: *
312: * @param string $index The index of the comic to check
313: * @param timestamp $date The date of the comic to check (default today)
314: *
315: * @return string Attributes for an <img> tag giving height and width
316: */
317: function imageSize($index, $date = null)
318: {
319: $image = $this->retrieveImage($index, $date);
320: if (get_class($image) == 'Klutz_Image' && !empty($image->size)) {
321: return $image->size;
322: } else {
323: return '';
324: }
325: }
326:
327: /**
328: * Find out if we already have a local copy of this image.
329: *
330: * @param string $index The index of the comic to check
331: * @param timestamp $date The date of the comic to check (default today)
332: *
333: * @return boolean False in this driver
334: */
335: function imageExists($index, $date = null)
336: {
337: if (is_null($date)) {
338: $date = mktime(0, 0, 0);
339: }
340:
341: $dir = $this->basedir . date($this->subdir, $date);
342: return (file_exists($dir . '/' . $index) && is_file($dir . '/' . $index));
343: }
344:
345: /**
346: * Retrieve an image from storage. Make sure the image exists
347: * first with imageExists().
348: *
349: * @param string $index The index of the comic to retrieve
350: * @param timestamp $date The date for which we want $comic
351: *
352: * @return mixed If the image exists locally, return a Klutz_Image object.
353: * If it doesn't, return a string with the URL pointing to
354: * the comic.
355: */
356: function retrieveImage($index, $date = null)
357: {
358: if (is_null($date)) {
359: $date = mktime(0, 0, 0);
360: }
361:
362: $dir = $this->basedir . date($this->subdir, $date);
363: return new Klutz_Image($dir . '/' . $index);
364: }
365:
366: /**
367: * Store an image for later retrieval.
368: *
369: * @param string $index The index of the comic to retrieve
370: * @param string $image Raw (binary) image data to store
371: * @param timestamp $data Date to store it under (default today)
372: *
373: * @return boolean True on success, false otherwise
374: */
375: function storeImage($index, $image, $date = null)
376: {
377: if (!is_object($image) || get_class($image) != "Klutz_Image") {
378: return false;
379: }
380:
381: if (is_null($date)) {
382: $date = mktime(0, 0, 0);
383: }
384:
385: // Make sure $this->basedir exists and is writeable.
386: if (!file_exists($this->basedir)) {
387: if (!file_exists(dirname($this->basedir))) { return false; }
388: if (!mkdir($this->basedir, 0700)) { return false; }
389: }
390: if (!is_writable($this->basedir)) {
391: return false;
392: }
393:
394: $dir = $this->basedir . date($this->subdir, $date);
395:
396: if (!file_exists($dir)) {
397: mkdir($dir, 0700);
398: } elseif (!is_writable($dir)) {
399: return false;
400: }
401:
402: $fp = fopen($dir . '/' . $index, 'w+');
403: fwrite($fp, $image->data);
404: fclose($fp);
405: $this->addSum($index, $date, md5($image->data));
406: return true;
407: }
408:
409: /**
410: * Remove an image from the storage system (including its unique
411: * ID).
412: *
413: * @param string $index The index of the comic to remove
414: * @param timestamp $date The date of the comic to remove (default today)
415: *
416: * @return boolean True on success, else false
417: */
418: function removeImage($index, $date = null)
419: {
420: if (is_null($date)) {
421: $date = mktime(0, 0, 0);
422: }
423:
424: if ($this->imageExists($index, $date)) {
425: $file = $this->basedir . date($this->subdir, $date) . '/' . $index;
426: if (unlink($file)) {
427: $this->removeSum($index, $date);
428: return true;
429: }
430: }
431: return false;
432: }
433:
434: /**
435: * Remove all images from the storage system (including unique
436: * IDs) for a given date.
437: *
438: * @param timestamp $date The date to remove comics for (default today)
439: *
440: * @return boolean True on success, else false
441: */
442: function removeDate($date = null)
443: {
444: if (is_null($date)) {
445: $date = mktime(0, 0, 0);
446: }
447:
448: $dir = $this->basedir . date($this->subdir, $date);
449: if (file_exists($dir) && is_dir($dir)) {
450: $d = dir($dir);
451: while (false !== ($file = $d->read())) {
452: $file = $d->path . '/' . $file;
453: if (is_file($file)) {
454: unlink($file);
455: }
456: }
457: $d->close();
458: if (@rmdir($dir)) {
459: $this->removeSum(null, $date);
460: return true;
461: }
462: }
463: return false;
464: }
465:
466: }
467: