~ubuntu-branches/ubuntu/hardy/gallery2/hardy-security

« back to all changes in this revision

Viewing changes to modules/core/classes/GalleryStorage/GalleryStorageExtras.class

  • Committer: Bazaar Package Importer
  • Author(s): Michael C. Schultheiss
  • Date: 2006-04-16 16:42:35 UTC
  • mfrom: (1.1.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20060416164235-8uy0u4bfjdxpge2o
Tags: 2.1.1-1
* New upstream release (Closes: #362936)
  + Bugfixes for Postgres7 (Closes: #359000, #362152)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/*
 
3
 * $RCSfile: GalleryStorageExtras.class,v $
 
4
 *
 
5
 * Gallery - a web based photo album viewer and editor
 
6
 * Copyright (C) 2000-2006 Bharat Mediratta
 
7
 *
 
8
 * This program is free software; you can redistribute it and/or modify
 
9
 * it under the terms of the GNU General Public License as published by
 
10
 * the Free Software Foundation; either version 2 of the License, or (at
 
11
 * your option) any later version.
 
12
 *
 
13
 * This program is distributed in the hope that it will be useful, but
 
14
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
16
 * General Public License for more details.
 
17
 *
 
18
 * You should have received a copy of the GNU General Public License
 
19
 * along with this program; if not, write to the Free Software
 
20
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 
21
 */
 
22
/**
 
23
 * @version $Revision: 1.10 $ $Date: 2006/03/21 07:06:13 $
 
24
 * @package GalleryCore
 
25
 * @author Bharat Mediratta <bharat@menalto.com>
 
26
 */
 
27
 
 
28
/**
 
29
 * Extended functionality in GalleryStorage that's not generally required for
 
30
 * simply viewing albums and photos.
 
31
 *
 
32
 * @package GalleryCore
 
33
 * @subpackage Storage
 
34
 */
 
35
class GalleryStorageExtras /* the other half of GalleryStorage */ {
 
36
    /**
 
37
     * Constructor
 
38
     * @param object GalleryStorage the database storage instance
 
39
     */
 
40
    function GalleryStorageExtras(&$galleryStorage) {
 
41
        $this->_gs =& $galleryStorage;
 
42
    }
 
43
 
 
44
    /**
 
45
     * Return a non transactional database connection
 
46
     *
 
47
     * @return array object GalleryStatus a status code
 
48
     *               object ADOdb a database connection
 
49
     */
 
50
    function _getNonTransactionalDatabaseConnection() {
 
51
        /*
 
52
         * If we're transactional, then we need another connection to
 
53
         * manipulate our locks, since they have to operate outside of a
 
54
         * transaction.
 
55
         */
 
56
        if ($this->_gs->_isTransactional) {
 
57
            if (empty($this->_gs->_nonTransactionalDb)) {
 
58
                list ($ret, $this->_gs->_nonTransactionalDb) = $this->_gs->_getConnection(true);
 
59
                if ($ret) {
 
60
                    return array($ret->wrap(__FILE__, __LINE__), null);
 
61
                }
 
62
            }
 
63
            return array(null, $this->_gs->_nonTransactionalDb);
 
64
        } else {
 
65
            $ret = $this->_dbInit();
 
66
            if ($ret) {
 
67
                return array($ret->wrap(__FILE__, __LINE__), null);
 
68
            }
 
69
            return array(null, $this->_gs->_db);
 
70
        }
 
71
    }
 
72
 
 
73
    /**
 
74
     * Connect to database if needed and optionally guarantee db transaction.
 
75
     * @return object GalleryStatus a status code
 
76
     * @access private
 
77
     */
 
78
    function _dbInit($transaction=false) {
 
79
        if (!isset($this->_gs->_db)) {
 
80
            list ($ret, $this->_gs->_db) = $this->_gs->_getConnection();
 
81
            if ($ret) {
 
82
                return $ret->wrap(__FILE__, __LINE__);
 
83
            }
 
84
        }
 
85
        if ($transaction) {
 
86
            $ret = $this->_gs->_guaranteeTransaction();
 
87
            if ($ret) {
 
88
                return $ret->wrap(__FILE__, __LINE__);
 
89
            }
 
90
        }
 
91
        return null;
 
92
    }
 
93
 
 
94
    /**
 
95
     * @see GalleryStorage::loadEntities
 
96
     */
 
97
    function loadEntities($ids) {
 
98
        global $gallery;
 
99
        $ret = $this->_dbInit();
 
100
        if ($ret) {
 
101
            return array($ret->wrap(__FILE__, __LINE__), null);
 
102
        }
 
103
 
 
104
        foreach ($ids as $idx => $id) {
 
105
            $ids[$idx] = (int)$id;
 
106
        }
 
107
 
 
108
        /* Identify all the ids at once */
 
109
        list ($ret, $types) = $this->_identifyEntities($ids);
 
110
        if ($ret) {
 
111
            return array($ret->wrap(__FILE__, __LINE__), null);
 
112
        }
 
113
 
 
114
        /* Separate the ids by type */
 
115
        $classNames = array();
 
116
        $gallery->guaranteeTimeLimit(5);
 
117
        for ($i = 0; $i < count($ids); $i++) {
 
118
            if (empty($types[$i])) {
 
119
                return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
 
120
                                                  "Missing object for id $ids[$i]"), null);
 
121
            }
 
122
            $classNames[$types[$i]][$ids[$i]] = 1;
 
123
        }
 
124
 
 
125
        /* Load them in groups */
 
126
        foreach ($classNames as $className => $targetIdHash) {
 
127
            $gallery->guaranteeTimeLimit(5);
 
128
 
 
129
            /* Get unique target ids */
 
130
            $targetIds = array_keys($targetIdHash);
 
131
 
 
132
            /* Get our member info for this class */
 
133
            list ($ret, $memberInfo) = $this->describeEntity($className);
 
134
            if ($ret) {
 
135
                return array($ret->wrap(__FILE__, __LINE__), null);
 
136
            }
 
137
 
 
138
            $idCol = $this->_gs->_translateColumnName('id');
 
139
 
 
140
            /* Build up our query */
 
141
            $columns = $tables = $where = $memberData = $callbacks = array();
 
142
            $markers = GalleryUtilities::makeMarkers(count($targetIds));
 
143
            $target = $className;
 
144
            while ($target) {
 
145
                foreach ($memberInfo[$target]['members'] as $columnName => $columnInfo) {
 
146
                    list ($tableName, $unused) = $this->_gs->_translateTableName($target);
 
147
                    $memberData[] = $columnInfo;
 
148
                    $callbacks[] = $columnName;
 
149
                    $columns[$tableName . '.' . $this->_gs->_translateColumnName($columnName)] = 1;
 
150
                    $tables[$tableName] = 1;
 
151
                }
 
152
                $target = $memberInfo[$target]['parent'];
 
153
            }
 
154
            $tables = array_keys($tables);
 
155
            $columns = array_keys($columns);
 
156
 
 
157
            for ($i = 0; $i < count($tables); $i++) {
 
158
                if ($i == 0) {
 
159
                    $where[] = $tables[$i] . '.' . $idCol . ' IN (' . $markers . ')';
 
160
                } else {
 
161
                    $where[] = $tables[$i] . '.' . $idCol . '=' . $tables[0] . '.' . $idCol;
 
162
                }
 
163
            }
 
164
 
 
165
            $query = 'SELECT ';
 
166
            $query .= implode(', ', $columns);
 
167
            $query .= ' FROM ';
 
168
            $query .= implode(', ', $tables);
 
169
            $query .= ' WHERE ';
 
170
            $query .= implode(' AND ', $where);
 
171
 
 
172
            /* Execute the query */
 
173
            $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
174
 
 
175
            $this->_gs->_traceStart();
 
176
            $recordSet = $this->_gs->_db->Execute($query, $targetIds);
 
177
            $this->_gs->_traceStop();
 
178
            if ($recordSet) {
 
179
                if ($recordSet->RecordCount() != count($targetIds)) {
 
180
                    return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__),
 
181
                                 null);
 
182
                }
 
183
 
 
184
                /* Process all the results */
 
185
                $j = 0;
 
186
                while ($row = $recordSet->FetchRow()) {
 
187
                    if (++$j % 20 == 0) {
 
188
                        $gallery->guaranteeTimeLimit(5);
 
189
                    }
 
190
 
 
191
                    if (!class_exists($className)) {
 
192
                        GalleryCoreApi::requireOnce(
 
193
                            "modules/{$memberInfo[$className]['module']}/classes/$className.class");
 
194
                    }
 
195
                    $entity = new $className;
 
196
 
 
197
                    if (empty($entity)) {
 
198
                        return array(GalleryCoreApi::error(ERROR_BAD_DATA_TYPE, __FILE__, __LINE__),
 
199
                                     null);
 
200
                    }
 
201
 
 
202
                    for ($i = 0; $i < count($callbacks); $i++) {
 
203
                        $value = $this->_gs->_normalizeValue($row[$i], $memberData[$i], true);
 
204
 
 
205
                        /* Store the value in the object */
 
206
                        $entity->$callbacks[$i] = $value;
 
207
                        $entity->_persistentStatus['originalValue'][$callbacks[$i]] = $value;
 
208
                    }
 
209
 
 
210
                    $entity->resetOriginalValues();
 
211
                    $entities[$entity->id] = $entity;
 
212
                }
 
213
 
 
214
                $recordSet->Close();
 
215
            } else {
 
216
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
217
                             null);
 
218
            }
 
219
        }
 
220
 
 
221
        /* Assemble the entities in the right order and return them */
 
222
        $result = array();
 
223
        foreach ($ids as $id) {
 
224
            $result[] = $entities[$id];
 
225
        }
 
226
 
 
227
        return array(null, $result);
 
228
    }
 
229
 
 
230
    /**
 
231
     * @see GalleryStorage::saveEntity
 
232
     */
 
233
    function saveEntity(&$entity) {
 
234
        $ret = $this->_dbInit(true);
 
235
        if ($ret) {
 
236
            return $ret->wrap(__FILE__, __LINE__);
 
237
        }
 
238
 
 
239
        /* Update the serial number, but remember the original one */
 
240
        $originalSerialNumber = (int)$entity->serialNumber++;
 
241
 
 
242
        /* Get our member info for this class */
 
243
        list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
 
244
        if ($ret) {
 
245
            return $ret->wrap(__FILE__, __LINE__);
 
246
        }
 
247
        $idColumn = null;
 
248
 
 
249
        /*
 
250
         * Build up a complete picture of all the various changed fields, so
 
251
         * that we can do an insert or update.
 
252
         */
 
253
        $dataTable = array();
 
254
        $id = array();
 
255
        $target = $entity->getEntityType();
 
256
 
 
257
        while ($target) {
 
258
            foreach ($memberInfo[$target]['members'] as $memberName => $memberData) {
 
259
                $type = $memberData['type'];
 
260
                list ($tableName, $unused) = $this->_gs->_translateTableName($target);
 
261
 
 
262
                /* If the member is modified, record the new value in our table */
 
263
                if ($entity->isModified($memberName)) {
 
264
                    $value = $entity->$memberName;
 
265
 
 
266
                    $entity->$memberName = $value =
 
267
                        $this->_gs->_normalizeValue($value, $memberData);
 
268
 
 
269
                    $columnName = $this->_gs->_translateColumnName($memberName);
 
270
                    $dataTable[$tableName][$columnName] = $value;
 
271
                } else {
 
272
                    /*
 
273
                     * If we haven't set up a table for this class, do so now.
 
274
                     * Otherwise we don't have a complete list of tables that we
 
275
                     * need to insert into in order for this class to be completely
 
276
                     * serialized.
 
277
                     */
 
278
                    if (!isset($dataTable[$tableName])) {
 
279
                        $dataTable[$tableName] = array();
 
280
                    }
 
281
                }
 
282
 
 
283
                if ($type & STORAGE_TYPE_ID) {
 
284
                    $value = $entity->$memberName;
 
285
 
 
286
                    $id['column'] = $this->_gs->_translateColumnName($memberName);
 
287
                    $id['value'] = $value;
 
288
                }
 
289
            }
 
290
            $target = $memberInfo[$target]['parent'];
 
291
        }
 
292
 
 
293
        if ($entity->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
 
294
            /*
 
295
             * Iterate through the data table and make up an INSERT statement
 
296
             * for each table that requires one.
 
297
             */
 
298
            foreach ($dataTable as $tableName => $columnChanges) {
 
299
 
 
300
                /* Make sure that the id column is set for each table */
 
301
                if (empty($columnChanges[$id['column']])) {
 
302
                    $columnChanges[$id['column']] = $id['value'];
 
303
                }
 
304
 
 
305
                $columns = array_keys($columnChanges);
 
306
                $data = array_values($columnChanges);
 
307
                $markers = GalleryUtilities::makeMarkers(count($columnChanges));
 
308
                $query = 'INSERT INTO ' . $tableName . ' (';
 
309
                $query .= implode(', ', $columns);
 
310
                $query .= ') VALUES (' . $markers . ')';
 
311
 
 
312
                $this->_gs->_traceStart();
 
313
                $recordSet = $this->_gs->_db->Execute($query, $data);
 
314
                $this->_gs->_traceStop();
 
315
 
 
316
                if (!$recordSet) {
 
317
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
318
                }
 
319
            }
 
320
        } else {
 
321
            /*
 
322
             * Iterate through the data table and make an UPDATE statement for
 
323
             * each table that requires one.  Make sure that we do the table
 
324
             * that has the serial number in it first, as we use the serial
 
325
             * number to make sure that we're not hitting a concurrency issue.
 
326
             */
 
327
            list ($serialNumberTable) = $this->_gs->_translateTableName('GalleryEntity');
 
328
 
 
329
            $queryList = array();
 
330
            foreach ($dataTable as $tableName => $columnChanges) {
 
331
                $changeList = array();
 
332
                $data = array();
 
333
 
 
334
                foreach ($columnChanges as $columnName => $value) {
 
335
                    $changeList[] = $columnName . '=?';
 
336
                    $data[] = $value;
 
337
                }
 
338
 
 
339
                if (count($changeList)) {
 
340
                    $query = 'UPDATE ' . $tableName  .  ' SET';
 
341
                    $query .= ' ' . implode(',', $changeList);
 
342
                    $query .= ' WHERE ' . $id['column'] . '=?';
 
343
                    $data[] = $id['value'];
 
344
 
 
345
                    if (!strcmp($tableName, $serialNumberTable)) {
 
346
                        $query .= ' AND ' .
 
347
                                $this->_gs->_translateColumnName('serialNumber') .
 
348
                                '=?';
 
349
                        $data[] = $originalSerialNumber;
 
350
                        array_unshift($queryList, array($query, $data));
 
351
                    } else {
 
352
                        array_push($queryList, array($query, $data));
 
353
                    }
 
354
                }
 
355
            }
 
356
 
 
357
            /*
 
358
             * Now apply each UPDATE statement in turn.  Make sure that we're
 
359
             * only affecting one row each time.
 
360
             */
 
361
            foreach ($queryList as $queryAndData) {
 
362
                list ($query, $data) = $queryAndData;
 
363
 
 
364
                $this->_gs->_traceStart();
 
365
                $recordSet = $this->_gs->_db->Execute($query, $data);
 
366
                $this->_gs->_traceStop();
 
367
 
 
368
                if (!$recordSet) {
 
369
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
370
                } else {
 
371
                    $affectedRows = $this->_gs->_db->Affected_Rows();
 
372
                    if ($affectedRows == 0) {
 
373
                        return GalleryCoreApi::error(ERROR_OBSOLETE_DATA, __FILE__, __LINE__,
 
374
                                                    "$query (" . implode('|', $data) . ')');
 
375
                    } else if ($affectedRows > 1) {
 
376
                        /* Holy shit, we just updated more than one row!  What do we do now? */
 
377
                        return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
 
378
                                              "$query (" . implode('|', $data) . ") $affectedRows");
 
379
                    }
 
380
                }
 
381
            }
 
382
        }
 
383
 
 
384
        $entity->clearPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
 
385
        $entity->resetOriginalValues();
 
386
        $ret = $entity->onSave();
 
387
        if ($ret) {
 
388
            return $ret->wrap(__FILE__, __LINE__);
 
389
        }
 
390
 
 
391
        return null;
 
392
    }
 
393
 
 
394
    /**
 
395
     * @see GalleryStorage::deleteEntity
 
396
     */
 
397
    function deleteEntity(&$entity) {
 
398
        $ret = $this->_dbInit(true);
 
399
        if ($ret) {
 
400
            return $ret->wrap(__FILE__, __LINE__);
 
401
        }
 
402
 
 
403
        /* If this object has not yet been saved in the database, don't bother saving it. */
 
404
        if ($entity->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
 
405
            $entity->clearPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
 
406
            $entity->setPersistentFlag(STORAGE_FLAG_DELETED);
 
407
            return null;
 
408
        }
 
409
 
 
410
        /* Get our persistent and member info for this class */
 
411
        list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
 
412
        if ($ret) {
 
413
            return $ret->wrap(__FILE__, __LINE__);
 
414
        }
 
415
 
 
416
        $idCol = $this->_gs->_translateColumnName('id');
 
417
 
 
418
        $tables = array();
 
419
        $target = $entity->entityType;
 
420
        while ($target) {
 
421
            foreach ($memberInfo[$target]['members'] as $columnName => $columnInfo) {
 
422
                list ($tableName, $unused) = $this->_gs->_translateTableName($target);
 
423
                $tables[$tableName] = 1;
 
424
            }
 
425
            $target = $memberInfo[$target]['parent'];
 
426
        }
 
427
 
 
428
        /*
 
429
         * XXX OPT:  Override this for specific database implementations that
 
430
         * allow multi-table delete.
 
431
         */
 
432
        foreach ($tables as $tableName => $junk) {
 
433
            $query = 'DELETE FROM ' . $tableName . ' WHERE ' . $idCol . '=?';
 
434
            $data = array((int)$entity->getId());
 
435
 
 
436
            $this->_gs->_traceStart();
 
437
            $recordSet = $this->_gs->_db->Execute($query, $data);
 
438
            $this->_gs->_traceStop();
 
439
            if (!$recordSet) {
 
440
                return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
441
            }
 
442
        }
 
443
 
 
444
        $entity->setPersistentFlag(STORAGE_FLAG_DELETED);
 
445
 
 
446
        return null;
 
447
    }
 
448
 
 
449
    /**
 
450
     * @see GalleryStorage::newEntity
 
451
     */
 
452
    function newEntity(&$entity) {
 
453
        /* getUniqueId does _dbInit */
 
454
        list ($ret, $id) = $this->getUniqueId();
 
455
        if ($ret) {
 
456
            return $ret->wrap(__FILE__, __LINE__);
 
457
        }
 
458
 
 
459
        $entity->id = $id;
 
460
        $entity->serialNumber = 0;
 
461
        $entity->setPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
 
462
 
 
463
        return null;
 
464
    }
 
465
 
 
466
    /**
 
467
     * @see GalleryStorage::getUniqueId
 
468
     */
 
469
    function getUniqueId() {
 
470
        $ret = $this->_dbInit();
 
471
        if ($ret) {
 
472
            return array($ret->wrap(__FILE__, __LINE__), null);
 
473
        }
 
474
 
 
475
        /* In case we're embedded in an app that sets adodb hasGenID to false (xaraya/postnuke) */
 
476
        if (isset($this->_gs->_db->hasGenID) && !$this->_gs->_db->hasGenID) {
 
477
            $this->_gs->_db->hasGenID = $setGenID = true;
 
478
        }
 
479
 
 
480
        /* Get the id of the next object from our sequence */
 
481
        $this->_gs->_traceStart();
 
482
        $id = (int)$this->_gs->_db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_ID);
 
483
        $this->_gs->_traceStop();
 
484
 
 
485
        if (isset($setGenID)) {
 
486
            $this->_gs->_db->hasGenID = false;
 
487
        }
 
488
 
 
489
        return array(null, $id);
 
490
    }
 
491
 
 
492
    /**
 
493
     * @see GalleryStorage::refreshEntity
 
494
     */
 
495
    function refreshEntity($entity) {
 
496
        $ret = $this->_dbInit();
 
497
        if ($ret) {
 
498
            return array($ret->wrap(__FILE__, __LINE__), null);
 
499
        }
 
500
 
 
501
        /*
 
502
         * We could check the serial number against the database, or check to
 
503
         * see if the entity is modified in order to figure out whether or not
 
504
         * we should refresh.  But either way that requires a database hit so we
 
505
         * might as well just retrieve the record every time
 
506
         */
 
507
        list ($ret, list ($freshEntity)) = $this->_gs->loadEntities(array($entity->id));
 
508
        if ($ret) {
 
509
            return array($ret->wrap(__FILE__, __LINE__), null);
 
510
        }
 
511
 
 
512
        /* Let entity do its post-load procedure */
 
513
        $ret = $freshEntity->onLoad();
 
514
        if ($ret) {
 
515
            return array($ret->wrap(__FILE__, __LINE__), null);
 
516
        }
 
517
 
 
518
        return array(null, $freshEntity);
 
519
    }
 
520
 
 
521
    /**
 
522
     * @see GalleryStorage::acquireReadLock
 
523
     */
 
524
    function acquireReadLock($entityIds, $timeout) {
 
525
        /* It's ok to pass in a single id */
 
526
        if (!is_array($entityIds)) {
 
527
            $entityIds = array($entityIds);
 
528
        }
 
529
 
 
530
        foreach ($entityIds as $idx => $id) {
 
531
            $entityIds[$idx] = (int)$id;
 
532
        }
 
533
 
 
534
        /* Acquire a non-transactional connection to use for this request */
 
535
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
536
        if ($ret) {
 
537
            return array($ret->wrap(__FILE__, __LINE__), null);
 
538
        }
 
539
 
 
540
        /* Know when to call it quits */
 
541
        $cutoffTime = time() + $timeout;
 
542
 
 
543
        /* Get the true name of the lock table */
 
544
        list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
545
 
 
546
        /*
 
547
         * Algorithm:
 
548
         * 1. Get clearance to acquire locks (and get the lock id)
 
549
         * 2. If any of the entities that we want to lock are currently write
 
550
         *    locked, then clear the request and go back to step 1.
 
551
         * 3. Acquire our read locks
 
552
         */
 
553
        while (true) {
 
554
            list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
 
555
            if ($ret) {
 
556
                return array($ret->wrap(__FILE__, __LINE__), null);
 
557
            }
 
558
 
 
559
            /* Check to see if any of the ids that we care about are write locked */
 
560
            $writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
 
561
            $markers = GalleryUtilities::makeMarkers(count($entityIds));
 
562
            $query = 'SELECT COUNT(*) FROM ' . $lockTable
 
563
                   . ' WHERE ' . $writeEntityIdCol . ' IN (' . $markers . ')';
 
564
            $data = $entityIds;
 
565
 
 
566
            $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
567
 
 
568
            $this->_gs->_traceStart();
 
569
            $recordSet = $db->Execute($query, $data);
 
570
            $this->_gs->_traceStop();
 
571
            if (!$recordSet) {
 
572
                $this->releaseLocks($lockId);
 
573
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
574
                             null);
 
575
            }
 
576
 
 
577
            $row = $recordSet->FetchRow();
 
578
            if ($row[0] == 0 ) {
 
579
                /* Success */
 
580
                break;
 
581
            } else {
 
582
                /* An entity that we want is write locked */
 
583
                $this->releaseLocks($lockId);
 
584
 
 
585
                if (time() > $cutoffTime) {
 
586
                    return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__),
 
587
                                 null);
 
588
                }
 
589
 
 
590
                /* Wait a second and try again */
 
591
                sleep(1);
 
592
 
 
593
                /* Expire any bogus locks */
 
594
                $ret = $this->_expireLocks();
 
595
                if ($ret) {
 
596
                    return array($ret->wrap(__FILE__, __LINE__), null);
 
597
                }
 
598
            }
 
599
        }
 
600
 
 
601
        /* Put in a read lock for every entity id */
 
602
        $lockIdCol = $this->_gs->_translateColumnName('lockId');
 
603
        $readEntityIdCol = $this->_gs->_translateColumnName('readEntityId');
 
604
        $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
 
605
        $freshUntil = time() + 30;
 
606
        $lockInfo = array();
 
607
        foreach ($entityIds as $entityId) {
 
608
            $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
 
609
                             $lockTable, $lockIdCol, $readEntityIdCol, $freshUntilCol);
 
610
            $data = array($lockId, $entityId, $freshUntil);
 
611
 
 
612
            $this->_gs->_traceStart();
 
613
            $recordSet = $db->Execute($query, $data);
 
614
            $this->_gs->_traceStop();
 
615
            if (!$recordSet) {
 
616
                $this->releaseLocks($lockId);
 
617
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
618
                             null);
 
619
            }
 
620
            $lockInfo[$entityId] = true;
 
621
        }
 
622
 
 
623
        /* Drop the lock request, now that we've got the write locks */
 
624
        $requestCol = $this->_gs->_translateColumnName('request');
 
625
        $query = 'DELETE FROM ' . $lockTable
 
626
               . ' WHERE ' . $lockIdCol . '=? AND ' . $requestCol . '=1';
 
627
        $data = array($lockId);
 
628
 
 
629
        $this->_gs->_traceStart();
 
630
        $recordSet = $db->Execute($query, $data);
 
631
        $this->_gs->_traceStop();
 
632
        if (!$recordSet) {
 
633
            $this->releaseLocks($lockId);
 
634
            return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
 
635
        }
 
636
 
 
637
        return array(null,
 
638
                     array('lockId' => $lockId, 'type' => LOCK_READ, 'ids' => $lockInfo));
 
639
    }
 
640
 
 
641
    /**
 
642
     * @see GalleryStorage::acquireWriteLock
 
643
     */
 
644
    function acquireWriteLock($entityIds, $timeout) {
 
645
        /* It's ok to pass in a single id */
 
646
        if (!is_array($entityIds)) {
 
647
            $entityIds = array($entityIds);
 
648
        }
 
649
 
 
650
        foreach ($entityIds as $idx => $id) {
 
651
            $entityIds[$idx] = (int)$id;
 
652
        }
 
653
 
 
654
        /* Acquire a non-transactional connection to use for this request */
 
655
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
656
        if ($ret) {
 
657
            return array($ret->wrap(__FILE__, __LINE__), null);
 
658
        }
 
659
 
 
660
        /* Know when to call it quits */
 
661
        $cutoffTime = time() + $timeout;
 
662
 
 
663
        /* Get the true name of the lock table */
 
664
        list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
665
 
 
666
        /*
 
667
         * Algorithm:
 
668
         * 1. Get clearance to acquire locks (and get the lock id)
 
669
         * 2. If any of the entities that we want to lock are currently locked,
 
670
         *    then clear the request and go back to step 1.
 
671
         * 3. Acquire our write locks
 
672
         */
 
673
        while (true) {
 
674
            list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
 
675
            if ($ret) {
 
676
                return array($ret->wrap(__FILE__, __LINE__), null);
 
677
            }
 
678
 
 
679
            /* Check to see if any of the ids that we care about are locked */
 
680
            $readEntityIdCol = $this->_gs->_translateColumnName('readEntityId');
 
681
            $writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
 
682
            $markers = GalleryUtilities::makeMarkers(count($entityIds));
 
683
            $query = 'SELECT COUNT(*) FROM ' . $lockTable . ' ' .
 
684
                    'WHERE ' . $readEntityIdCol . ' IN (' . $markers . ') ' .
 
685
                    'OR ' . $writeEntityIdCol . ' IN (' . $markers . ')';
 
686
            $data = $entityIds;
 
687
            $data = array_merge($data, $entityIds);
 
688
 
 
689
            $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
690
 
 
691
            $this->_gs->_traceStart();
 
692
            $recordSet = $db->Execute($query, $data);
 
693
            $this->_gs->_traceStop();
 
694
            if (!$recordSet) {
 
695
                $this->releaseLocks($lockId);
 
696
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
697
                             null);
 
698
            }
 
699
 
 
700
            $row = $recordSet->FetchRow();
 
701
            if ($row[0] == 0 ) {
 
702
                /* Success */
 
703
                break;
 
704
            } else {
 
705
                /* An entity that we want is still locked */
 
706
                $this->releaseLocks($lockId);
 
707
 
 
708
                if (time() > $cutoffTime) {
 
709
                    return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__),
 
710
                                 null);
 
711
                }
 
712
 
 
713
                /* Wait a second and try again */
 
714
                sleep(1);
 
715
 
 
716
                /* Expire any bogus locks */
 
717
                $ret = $this->_expireLocks();
 
718
                if ($ret) {
 
719
                    return array($ret->wrap(__FILE__, __LINE__), null);
 
720
                }
 
721
            }
 
722
        }
 
723
 
 
724
        /* We are approved to acquire our write locks */
 
725
        $lockIdCol = $this->_gs->_translateColumnName('lockId');
 
726
        $writeEntityIdCol = $this->_gs->_translateColumnName('writeEntityId');
 
727
        $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
 
728
        $freshUntil = time() + 30;
 
729
        $lockInfo = array();
 
730
        foreach ($entityIds as $entityId) {
 
731
            $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
 
732
                             $lockTable, $lockIdCol, $writeEntityIdCol, $freshUntilCol);
 
733
            $data = array($lockId, $entityId, $freshUntil);
 
734
 
 
735
            $this->_gs->_traceStart();
 
736
            $recordSet = $db->Execute($query, $data);
 
737
            $this->_gs->_traceStop();
 
738
            if (!$recordSet) {
 
739
                $this->releaseLocks($lockId);
 
740
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
741
                             null);
 
742
            }
 
743
            $lockInfo[$entityId] = true;
 
744
        }
 
745
 
 
746
        /* Drop the lock request, now that we've got the write locks */
 
747
        $requestCol = $this->_gs->_translateColumnName('request');
 
748
        $query = 'DELETE FROM ' . $lockTable
 
749
               . ' WHERE ' . $lockIdCol . '=? AND ' . $requestCol . '=1';
 
750
        $data = array($lockId);
 
751
 
 
752
        $this->_gs->_traceStart();
 
753
        $recordSet = $db->Execute($query, $data);
 
754
        $this->_gs->_traceStop();
 
755
        if (!$recordSet) {
 
756
            $this->releaseLocks($lockId);
 
757
            return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
 
758
        }
 
759
 
 
760
        return array(null,
 
761
                     array('lockId' => $lockId, 'type' => LOCK_WRITE, 'ids' => $lockInfo));
 
762
    }
 
763
 
 
764
    /**
 
765
     * @see GalleryStorage::refreshLocks
 
766
     */
 
767
    function refreshLocks($lockIds, $freshUntil) {
 
768
        if (!empty($lockIds)) {
 
769
            foreach ($lockIds as $idx => $id) {
 
770
                $lockIds[$idx] = (int)$id;
 
771
            }
 
772
 
 
773
            /* Acquire a non-transactional connection to use for this request */
 
774
            list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
775
            if ($ret) {
 
776
                return $ret->wrap(__FILE__, __LINE__);
 
777
            }
 
778
 
 
779
            list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
780
            $lockIdCol = $this->_gs->_translateColumnName('lockId');
 
781
            $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
 
782
            $lockIdMarkers = GalleryUtilities::makeMarkers(count($lockIds));
 
783
            $query = sprintf('UPDATE %s SET %s = ? WHERE %s in (%s)',
 
784
                             $lockTable, $freshUntilCol, $lockIdCol, $lockIdMarkers);
 
785
 
 
786
            $this->_gs->_traceStart();
 
787
            $data = array_merge(array($freshUntil), $lockIds);
 
788
            $recordSet = $db->Execute($query, $data);
 
789
            $this->_gs->_traceStop();
 
790
 
 
791
            if (!$recordSet) {
 
792
                return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
793
            }
 
794
        }
 
795
 
 
796
        return null;
 
797
    }
 
798
 
 
799
    /**
 
800
     * Delete all not-so-fresh locks.
 
801
     *
 
802
     * @return object GalleryStatus a status code
 
803
     * @access private
 
804
     */
 
805
    function _expireLocks() {
 
806
        /* Acquire a non-transactional connection to use for this request */
 
807
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
808
        if ($ret) {
 
809
            return $ret->wrap(__FILE__, __LINE__);
 
810
        }
 
811
 
 
812
        list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
813
        $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
 
814
        $query = sprintf('DELETE FROM %s WHERE %s < ?',
 
815
                         $lockTable, $freshUntilCol);
 
816
 
 
817
        $this->_gs->_traceStart();
 
818
        $recordSet = $db->Execute($query, array(time()));
 
819
        $this->_gs->_traceStop();
 
820
 
 
821
        if (!$recordSet) {
 
822
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
823
        }
 
824
 
 
825
        return null;
 
826
    }
 
827
 
 
828
    /**
 
829
     * @see GalleryStorage::releaseLocks
 
830
     */
 
831
    function releaseLocks($lockIds) {
 
832
        if (!is_array($lockIds)) {
 
833
            $lockIds = array($lockIds);
 
834
        }
 
835
        foreach ($lockIds as $idx => $id) {
 
836
            $lockIds[$idx] = (int)$id;
 
837
        }
 
838
 
 
839
        /* Acquire a non-transactional connection to use for this request */
 
840
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
841
        if ($ret) {
 
842
            return $ret->wrap(__FILE__, __LINE__);
 
843
        }
 
844
 
 
845
        /* Get the true name of the lock table */
 
846
        list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
847
 
 
848
        $lockIdCol = $this->_gs->_translateColumnName('lockId');
 
849
        $markers = GalleryUtilities::makeMarkers(count($lockIds));
 
850
        $query = 'DELETE FROM ' . $lockTable . ' WHERE ' . $lockIdCol . ' IN (' . $markers . ')';
 
851
 
 
852
        $this->_gs->_traceStart();
 
853
        $recordSet = $db->Execute($query, $lockIds);
 
854
        $this->_gs->_traceStop();
 
855
        if ($recordSet) {
 
856
            return null;
 
857
        } else {
 
858
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
859
        }
 
860
    }
 
861
 
 
862
    /**
 
863
     * @see GalleryStorage::removeIdsFromLock
 
864
     */
 
865
    function removeIdsFromLock($lock, $ids) {
 
866
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
867
        if ($ret) {
 
868
            return $ret->wrap(__FILE__, __LINE__);
 
869
        }
 
870
 
 
871
        $query = '
 
872
        DELETE FROM [Lock]
 
873
        WHERE [::lockId] = ?
 
874
          AND [::' . ($lock['type'] == LOCK_WRITE ? 'write' : 'read') . 'EntityId] IN ('
 
875
            . GalleryUtilities::makeMarkers(count($ids)) . ')';
 
876
        $query = $this->_gs->_translateQuery($query);
 
877
 
 
878
        $this->_gs->_traceStart();
 
879
        $recordSet = $db->Execute($query, array_merge(array($lock['lockId']), $ids));
 
880
        $this->_gs->_traceStop();
 
881
        if ($recordSet) {
 
882
            return null;
 
883
        } else {
 
884
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
885
        }
 
886
    }
 
887
 
 
888
    /**
 
889
     * @see GalleryStorage::moveIdsBetweenLocks
 
890
     */
 
891
    function moveIdsBetweenLocks($relock, $newLockId, $lockType) {
 
892
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
893
        if ($ret) {
 
894
            return $ret->wrap(__FILE__, __LINE__);
 
895
        }
 
896
 
 
897
        $query = '
 
898
        UPDATE [Lock] SET [::lockId] = ? WHERE [::lockId] = ? AND [::'
 
899
            . ($lockType == LOCK_WRITE ? 'write' : 'read') . 'EntityId] IN (';
 
900
        $query = $this->_gs->_translateQuery($query);
 
901
        foreach ($relock as $lockId => $ids) {
 
902
            $this->_gs->_traceStart();
 
903
            $recordSet = $db->Execute($query . GalleryUtilities::makeMarkers(count($ids)) . ')',
 
904
                                      array_merge(array($newLockId, $lockId), $ids));
 
905
            $this->_gs->_traceStop();
 
906
            if (!$recordSet) {
 
907
                return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
908
            }
 
909
        }
 
910
        return null;
 
911
    }
 
912
 
 
913
    /**
 
914
     * @see GalleryStorage::newLockId
 
915
     */
 
916
    function newLockId() {
 
917
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
918
        if ($ret) {
 
919
            return array($ret->wrap(__FILE__, __LINE__), null);
 
920
        }
 
921
 
 
922
        $this->_gs->_traceStart();
 
923
        $lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
 
924
        $this->_gs->_traceStop();
 
925
 
 
926
        return array(null, $lockId);
 
927
    }
 
928
 
 
929
    /**
 
930
     * @see GalleryStorage::execute
 
931
     */
 
932
    function execute($statement, $data=array()) {
 
933
        $ret = $this->_dbInit(true);
 
934
        if ($ret) {
 
935
            return $ret->wrap(__FILE__, __LINE__);
 
936
        }
 
937
 
 
938
        $statement = $this->_gs->_translateQuery($statement);
 
939
 
 
940
        $this->_gs->_traceStart();
 
941
        $recordSet = $this->_gs->_db->Execute($statement, $data);
 
942
        $this->_gs->_traceStop();
 
943
 
 
944
        /* Direct SQL commands can undermine our memory cache, so reset it */
 
945
        GalleryDataCache::reset();
 
946
 
 
947
        return $recordSet ? null
 
948
                          : GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
949
    }
 
950
 
 
951
    /**
 
952
     * @see GalleryStorage::addMapEntry
 
953
     */
 
954
    function addMapEntry($mapName, $entry) {
 
955
        $ret = $this->_dbInit(true);
 
956
        if ($ret) {
 
957
            return $ret->wrap(__FILE__, __LINE__);
 
958
        }
 
959
 
 
960
        list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
 
961
        if ($ret) {
 
962
            return $ret->wrap(__FILE__, __LINE__);
 
963
        }
 
964
        list ($tableName) = $this->_gs->_translateTableName($mapName);
 
965
        $data = $columns = array();
 
966
        foreach ($mapInfo as $memberName => $memberData) {
 
967
            if (!array_key_exists($memberName, $entry)) {
 
968
                return GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
 
969
                                             'Missing parameter: ' . $memberName);
 
970
            }
 
971
 
 
972
            if (is_array($entry[$memberName])) {
 
973
                return $this->_addMapEntries($mapInfo, $tableName, $entry);
 
974
            }
 
975
            $columns[] = $this->_gs->_translateColumnName($memberName);
 
976
            $data[] = $this->_gs->_normalizeValue($entry[$memberName], $memberData);
 
977
        }
 
978
 
 
979
        $markers = GalleryUtilities::makeMarkers(count($columns));
 
980
        $query = 'INSERT INTO ' . $tableName . ' (';
 
981
        $query .= implode(', ', $columns);
 
982
        $query .= ') VALUES (' . $markers . ')';
 
983
 
 
984
        $this->_gs->_traceStart();
 
985
        $recordSet = $this->_gs->_db->Execute($query, $data);
 
986
        $this->_gs->_traceStop();
 
987
        if ($recordSet === false) {
 
988
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
989
        }
 
990
 
 
991
        return null;
 
992
    }
 
993
 
 
994
    /**
 
995
     * Add new entries to a map. This utility takes the values from entry array
 
996
     * e.g., (parm1 => (p1val1, p1val2, p1val3), parm2 => (p2val1, p2val2, p2val3))
 
997
     * and inserts them into the map similar to the following:
 
998
     * INSERT INTO ... (PARM1, PARM2) VALUES (p1val1, p2val1), (p1val2, p2val2) ...
 
999
     *
 
1000
     * @param object the map we're working on
 
1001
     * @param string the translated table name
 
1002
     * @param array an associative array of data about the entry
 
1003
     *              each data element is an array of values
 
1004
     * @return object GalleryStatus a status code
 
1005
     */
 
1006
    function _addMapEntries($mapInfo, $tableName, $entry) {
 
1007
        $columns = array();
 
1008
        foreach ($mapInfo as $memberName => $memberData) {
 
1009
            $columns[$memberName] = $this->_gs->_translateColumnName($memberName);
 
1010
        }
 
1011
 
 
1012
        /* Now we transpose the entry matrix */
 
1013
        $rows = count($entry[$memberName]);
 
1014
        $data = array();
 
1015
        for ($ind = 0; $ind < $rows; $ind++) {
 
1016
            foreach ($mapInfo as $memberName => $memberData) {
 
1017
                $data[] =$this->_gs->_normalizeValue($entry[$memberName][$ind], $memberData);
 
1018
            }
 
1019
        }
 
1020
 
 
1021
        list ($ret, $query) =
 
1022
            $this->_gs->getFunctionSql('MULTI_INSERT', array($tableName, $columns, $rows));
 
1023
        if ($ret) {
 
1024
            return $ret->wrap(__FILE__, __LINE__);
 
1025
        }
 
1026
 
 
1027
        $this->_gs->_traceStart();
 
1028
        $recordSet = $this->_gs->_db->Execute($query, $data);
 
1029
        $this->_gs->_traceStop();
 
1030
        if ($recordSet === false) {
 
1031
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1032
        }
 
1033
 
 
1034
        return null;
 
1035
    }
 
1036
 
 
1037
    /**
 
1038
     * @see GalleryStorage::removeMapEntry
 
1039
     */
 
1040
    function removeMapEntry($mapName, $match) {
 
1041
        $ret = $this->_dbInit(true);
 
1042
        if ($ret) {
 
1043
            return $ret->wrap(__FILE__, __LINE__);
 
1044
        }
 
1045
 
 
1046
        list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
 
1047
        if ($ret) {
 
1048
            return $ret->wrap(__FILE__, __LINE__);
 
1049
        }
 
1050
        list ($tableName, $unused) = $this->_gs->_translateTableName($mapName);
 
1051
        $data = $where = array();
 
1052
 
 
1053
        foreach ($mapInfo as $memberName => $memberData) {
 
1054
            if (array_key_exists($memberName, $match)) {
 
1055
                if (GalleryUtilities::isA($match[$memberName], 'GallerySqlFragment')) {
 
1056
                    $where[] = $this->_gs->_translateColumnName($memberName) . ' '
 
1057
                             . $this->_gs->_translateQuery($match[$memberName]->getFragment());
 
1058
                    foreach ($match[$memberName]->getValues() as $value) {
 
1059
                        $data[] = $value;
 
1060
                    }
 
1061
                } else if (is_array($match[$memberName])) {
 
1062
                    $qs = array();
 
1063
                    foreach ($match[$memberName] as $value) {
 
1064
                        $qs[] = '?';
 
1065
                        $data[] = $this->_gs->_normalizeValue($value, $memberData);
 
1066
                    }
 
1067
                    $where[] = $this->_gs->_translateColumnName($memberName) . ' IN ('
 
1068
                             . implode(',', $qs) . ')';
 
1069
                } else {
 
1070
                    $value = $this->_gs->_normalizeValue($match[$memberName], $memberData);
 
1071
                    if (is_null($value)) {
 
1072
                        $where[] = $this->_gs->_translateColumnName($memberName) . ' IS NULL';
 
1073
                    } else {
 
1074
                        $where[] = $this->_gs->_translateColumnName($memberName) . '=?';
 
1075
                        $data[] = $value;
 
1076
                    }
 
1077
                }
 
1078
            }
 
1079
        }
 
1080
 
 
1081
        if (empty($where)) {
 
1082
            return GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
 
1083
                                        'Missing where clause');
 
1084
        }
 
1085
 
 
1086
        $query = 'DELETE FROM ' . $tableName . ' WHERE '  . implode(' AND ', $where);
 
1087
 
 
1088
        $this->_gs->_traceStart();
 
1089
        $recordSet = $this->_gs->_db->Execute($query, $data);
 
1090
        $this->_gs->_traceStop();
 
1091
        if (!$recordSet) {
 
1092
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1093
        }
 
1094
 
 
1095
        return null;
 
1096
    }
 
1097
 
 
1098
    /**
 
1099
     * @see GalleryStorage::removeAllMapEntries
 
1100
     */
 
1101
    function removeAllMapEntries($mapName) {
 
1102
        $ret = $this->_dbInit(true);
 
1103
        if ($ret) {
 
1104
            return $ret->wrap(__FILE__, __LINE__);
 
1105
        }
 
1106
 
 
1107
        list ($tableName) = $this->_gs->_translateTableName($mapName);
 
1108
        $query = 'DELETE FROM ' . $tableName;
 
1109
 
 
1110
        $this->_gs->_traceStart();
 
1111
        $recordSet = $this->_gs->_db->Execute($query);
 
1112
        $this->_gs->_traceStop();
 
1113
        if ($recordSet === false) {
 
1114
            return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1115
        }
 
1116
 
 
1117
        return null;
 
1118
    }
 
1119
 
 
1120
    /*
 
1121
     * Load up the table creation and alteration SQL files for the given module
 
1122
     * @access private
 
1123
     */
 
1124
    function _getModuleSql($moduleId) {
 
1125
        global $gallery;
 
1126
        $platform =& $gallery->getPlatform();
 
1127
        $sqlFile = sprintf('%smodules/%s/classes/GalleryStorage/schema.tpl',
 
1128
            GalleryCoreApi::getPluginBaseDir('module', $moduleId), $moduleId);
 
1129
 
 
1130
        if ($platform->file_exists($sqlFile)) {
 
1131
            $sqlData = $platform->file($sqlFile);
 
1132
            $moduleSql = GalleryStorageExtras::parseSqlTemplate($sqlData, $this->_gs->getType());
 
1133
        } else {
 
1134
            $moduleSql = array('table' => array(), 'alter' => array(),
 
1135
                               'remove' => array(), 'test' => array());
 
1136
        }
 
1137
 
 
1138
        return array(null, $moduleSql);
 
1139
    }
 
1140
 
 
1141
    /**
 
1142
     * Parse the SQL template file and break it down by database and sql file type and return the
 
1143
     * results in an array.  The best way to see how this is supposed to work is to look in the
 
1144
     * unit test.
 
1145
     *
 
1146
     * @param array the raw template data
 
1147
     * @param string the database type
 
1148
     * @return array the parsed results
 
1149
     * @static
 
1150
     */
 
1151
    function parseSqlTemplate($sqlData, $dbType) {
 
1152
        $info = array('table' => array(), 'alter' => array(),
 
1153
                      'remove' => array(), 'test' => array());
 
1154
        $dbname = $tablename = null;
 
1155
        $record = false;
 
1156
        foreach ($sqlData as $line) {
 
1157
            $line = rtrim($line);
 
1158
            if (preg_match('/^## (.*)$/', $line, $matches)) {
 
1159
                $record = ($matches[1] == $dbType);
 
1160
                continue;
 
1161
            }
 
1162
            if (!$record) {
 
1163
                continue;
 
1164
            }
 
1165
 
 
1166
            if (preg_match('/^# (.*)$/', $line, $matches)) {
 
1167
                $tablename = $matches[1];
 
1168
                if (preg_match('/^T_(.*)_(\d+)/', $tablename, $matches)) {
 
1169
                    if (!isset($info['test'][$matches[1]][$matches[2]])) {
 
1170
                        $info['test'][$matches[1]][$matches[2]] = '';
 
1171
                    }
 
1172
                    $insertPointer =& $info['test'][$matches[1]][$matches[2]];
 
1173
                } else if (preg_match('/^A_(.*)_(\d+)\.(\d+)/', $tablename, $matches)) {
 
1174
                    if (!isset($info['alter'][$matches[1]][$matches[2]][$matches[3]])) {
 
1175
                        $info['alter'][$matches[1]][$matches[2]][$matches[3]] = '';
 
1176
                    }
 
1177
                    $insertPointer =& $info['alter'][$matches[1]][$matches[2]][$matches[3]];
 
1178
                } else if (preg_match('/^R_(.*)_(\d+)\.(\d+)/', $tablename, $matches)) {
 
1179
                    if (!isset($info['remove'][$matches[1]][$matches[2]][$matches[3]])) {
 
1180
                        $info['remove'][$matches[1]][$matches[2]][$matches[3]] = '';
 
1181
                    }
 
1182
                    $insertPointer =& $info['remove'][$matches[1]][$matches[2]][$matches[3]];
 
1183
                } else {
 
1184
                    if (!isset($info['table'][$tablename])) {
 
1185
                        $info['table'][$tablename] = '';
 
1186
                    }
 
1187
                    $insertPointer =& $info['table'][$tablename];
 
1188
                }
 
1189
                continue;
 
1190
            }
 
1191
            if ($dbType == 'oracle') {
 
1192
                /* TODO: use this line below always, remove the else block */
 
1193
                /* This change is needed for oracle so no statements are executed with
 
1194
                 * trailing semicolon.. other dbs ignore this; but we're too close to
 
1195
                 * 2.1 release, so just making this change for oracle now.
 
1196
                 */
 
1197
                $insertPointer .= $line . "\n";
 
1198
            } else {
 
1199
                if (!empty($insertPointer)) {
 
1200
                    $insertPointer .= "\n";
 
1201
                }
 
1202
                $insertPointer .= $line;
 
1203
            }
 
1204
        }
 
1205
 
 
1206
        return $info;
 
1207
    }
 
1208
 
 
1209
    /**
 
1210
     * @see GalleryStorage::configureStore
 
1211
     */
 
1212
    function configureStore($moduleId, $upgradeInfo=array()) {
 
1213
        global $gallery;
 
1214
        $gallery->guaranteeTimeLimit(20);
 
1215
 
 
1216
        $this->_clearEntityAndMapCache();
 
1217
 
 
1218
        $ret = $this->_dbInit(true);
 
1219
        if ($ret) {
 
1220
            return $ret->wrap(__FILE__, __LINE__);
 
1221
        }
 
1222
 
 
1223
        list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
 
1224
        if ($ret) {
 
1225
            return $ret->wrap(__FILE__, __LINE__);
 
1226
        }
 
1227
 
 
1228
        /* Get the metabase info about this database */
 
1229
        $this->_gs->_traceStart();
 
1230
        $metatables = $this->_gs->_db->MetaTables();
 
1231
        $this->_gs->_traceStop();
 
1232
 
 
1233
        /*
 
1234
         * Some databases (notably MySQL on Win32) don't support mixed case
 
1235
         * table names.  So, when we get the meta table list back, it's lower
 
1236
         * case.  Force all metatable listings to lower case and then expect
 
1237
         * them to be lowercase so that we're consistent.
 
1238
         */
 
1239
        for ($i = 0; $i < count($metatables); $i++) {
 
1240
            $metatables[$i] = strtolower($metatables[$i]);
 
1241
        }
 
1242
 
 
1243
        /* Do the schema table first */
 
1244
        list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
 
1245
        if (!in_array(strtolower($schemaTableName), $metatables)) {
 
1246
            $ret = $this->_executeSql($moduleSql['table']['Schema']);
 
1247
            if ($ret) {
 
1248
                return $ret->wrap(__FILE__, __LINE__);
 
1249
            }
 
1250
            unset($moduleSql['table']['Schema']);
 
1251
 
 
1252
            /* Create our sequences now */
 
1253
            foreach (array(DATABASE_SEQUENCE_LOCK, DATABASE_SEQUENCE_ID) as $sequenceId) {
 
1254
                $this->_gs->_traceStart();
 
1255
                $result = $this->_gs->_db->CreateSequence($this->_gs->_tablePrefix . $sequenceId);
 
1256
                $this->_gs->_traceStop();
 
1257
                if (empty($result)) {
 
1258
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1259
                }
 
1260
            }
 
1261
        }
 
1262
 
 
1263
        /* Load all table versions */
 
1264
        list ($ret, $tableVersions) = $this->_loadTableVersions();
 
1265
        if ($ret) {
 
1266
            return $ret->wrap(__FILE__, __LINE__);
 
1267
        }
 
1268
 
 
1269
        /*
 
1270
         * Now take care of the rest of the tables.  If the table doesn't
 
1271
         * exist, apply the current table definition.  If it already exists,
 
1272
         * check to see if there is an upgrade available for the given table
 
1273
         * version that we should apply based on $upgradeInfo.
 
1274
         */
 
1275
        foreach ($moduleSql['table'] as $rawTableName => $sql) {
 
1276
            $gallery->guaranteeTimeLimit(20);
 
1277
            list ($tableName, $unused, $tableNameInSchema) =
 
1278
                $this->_gs->_translateTableName($rawTableName);
 
1279
            if (!in_array(strtolower($tableName), $metatables)) {
 
1280
                $ret = $this->_executeSql($sql);
 
1281
                if ($ret) {
 
1282
                    return $ret->wrap(__FILE__, __LINE__);
 
1283
                }
 
1284
            } else {
 
1285
                while (1) {
 
1286
                    /* The table exists -- see if we have an upgrade for it */
 
1287
                    if (empty($tableVersions[$tableNameInSchema])) {
 
1288
                        /*
 
1289
                         * We've found a SQL file that matches a table in the
 
1290
                         * database, but has no matching version info in the
 
1291
                         * schema table.  How can this be?  Leave it alone.
 
1292
                         */
 
1293
                        if ($gallery->getDebug()) {
 
1294
                            $gallery->debug("Table $rawTableName: missing entry in Schema table");
 
1295
                        }
 
1296
                        break;
 
1297
                    }
 
1298
 
 
1299
                    /* If we locate an appropriate upgrade, apply it. */
 
1300
                    list ($major, $minor) = $tableVersions[$tableNameInSchema];
 
1301
                    if (!empty($moduleSql['alter'][$rawTableName][$major][$minor]) &&
 
1302
                            in_array("$rawTableName:$major.$minor", $upgradeInfo)) {
 
1303
                        $sql = $moduleSql['alter'][$rawTableName][$major][$minor];
 
1304
                        $ret = $this->_executeSql($sql);
 
1305
                        if ($ret) {
 
1306
                            return $ret->wrap(__FILE__, __LINE__);
 
1307
                        }
 
1308
 
 
1309
                        /* Reload all table versions, cause one has now changed */
 
1310
                        list ($ret, $tableVersions) = $this->_loadTableVersions();
 
1311
                        if ($ret) {
 
1312
                            return $ret->wrap(__FILE__, __LINE__);
 
1313
                        }
 
1314
                    } else {
 
1315
                        /* No upgrade available */
 
1316
                        break;
 
1317
                    }
 
1318
                }
 
1319
            }
 
1320
        }
 
1321
 
 
1322
        return null;
 
1323
    }
 
1324
 
 
1325
    /**
 
1326
     * @see GalleryStorage::configureStoreCleanup
 
1327
     */
 
1328
    function configureStoreCleanup($moduleId) {
 
1329
        global $gallery;
 
1330
 
 
1331
        $ret = $this->_dbInit(true);
 
1332
        if ($ret) {
 
1333
            return $ret->wrap(__FILE__, __LINE__);
 
1334
        }
 
1335
 
 
1336
        list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
 
1337
        if ($ret) {
 
1338
            return $ret->wrap(__FILE__, __LINE__);
 
1339
        }
 
1340
 
 
1341
        /* Get the metabase info about this database */
 
1342
        $this->_gs->_traceStart();
 
1343
        $metatables = $this->_gs->_db->MetaTables();
 
1344
        $this->_gs->_traceStop();
 
1345
 
 
1346
        /*
 
1347
         * Some databases (notably MySQL on Win32) don't support mixed case
 
1348
         * table names.  So, when we get the meta table list back, it's lower
 
1349
         * case.  Force all metatable listings to lower case and then expect
 
1350
         * them to be lowercase so that we're consistent.
 
1351
         */
 
1352
        for ($i = 0; $i < count($metatables); $i++) {
 
1353
            $metatables[$i] = strtolower($metatables[$i]);
 
1354
        }
 
1355
 
 
1356
        /* Load all table versions */
 
1357
        list ($ret, $tableVersions) = $this->_loadTableVersions();
 
1358
        if ($ret) {
 
1359
            return $ret->wrap(__FILE__, __LINE__);
 
1360
        }
 
1361
 
 
1362
        /* Now locate any existing tables that should be removed and drop them. */
 
1363
        foreach (array_keys($moduleSql['remove']) as $rawTableName) {
 
1364
            if ($rawTableName == 'Schema') {
 
1365
                continue;
 
1366
            }
 
1367
 
 
1368
            list ($tableName, $unused, $tableNameInSchema) =
 
1369
                $this->_gs->_translateTableName($rawTableName);
 
1370
            if (in_array(strtolower($tableName), $metatables)) {
 
1371
                /* The table exists -- see if we should delete it */
 
1372
                if (empty($tableVersions[$tableNameInSchema])) {
 
1373
                    /*
 
1374
                     * We've found a SQL file that matches a table in the
 
1375
                     * database, but has no matching version info in the
 
1376
                     * schema table.  How can this be?  Leave it alone.
 
1377
                     */
 
1378
                    if ($gallery->getDebug()) {
 
1379
                        $gallery->debug("Table $rawTableName: missing entry in Schema table");
 
1380
                    }
 
1381
                } else {
 
1382
                    $gallery->guaranteeTimeLimit(20);
 
1383
                    list ($major, $minor) = $tableVersions[$tableNameInSchema];
 
1384
                    if (!empty($moduleSql['remove'][$rawTableName][$major][$minor])) {
 
1385
                        $ret = $this->_executeSql(
 
1386
                            $moduleSql['remove'][$rawTableName][$major][$minor]);
 
1387
                        if ($ret) {
 
1388
                            return $ret->wrap(__FILE__, __LINE__);
 
1389
                        }
 
1390
                    }
 
1391
                }
 
1392
            }
 
1393
        }
 
1394
 
 
1395
        return null;
 
1396
    }
 
1397
 
 
1398
    /**
 
1399
     * @see GalleryStorage::unconfigureStore
 
1400
     */
 
1401
    function unconfigureStore($moduleId) {
 
1402
        global $gallery;
 
1403
        $gallery->guaranteeTimeLimit(20);
 
1404
 
 
1405
        $ret = $this->_dbInit(true);
 
1406
        if ($ret) {
 
1407
            return $ret;
 
1408
        }
 
1409
 
 
1410
        list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
 
1411
        if ($ret) {
 
1412
            return $ret->wrap(__FILE__, __LINE__);
 
1413
        }
 
1414
 
 
1415
        /* Get the metabase info about this database */
 
1416
        $this->_gs->_traceStart();
 
1417
        $metatables = $this->_gs->_db->MetaTables();
 
1418
        $this->_gs->_traceStop();
 
1419
 
 
1420
        /*
 
1421
         * Some databases (notably MySQL on Win32) don't support mixed case
 
1422
         * table names.  So, when we get the meta table list back, it's lower
 
1423
         * case.  Force all metatable listings to lower case and then expect
 
1424
         * them to be lowercase so that we're consistent.
 
1425
         */
 
1426
        for ($i = 0; $i < count($metatables); $i++) {
 
1427
            $metatables[$i] = strtolower($metatables[$i]);
 
1428
        }
 
1429
 
 
1430
        /*
 
1431
         * Now take care of the rest of the tables.  If the table doesn't
 
1432
         * exist, apply the current table definition.  If it already exists,
 
1433
         * check to see if there is an upgrade available for the given table
 
1434
         * version.  If so, apply it.
 
1435
         */
 
1436
        list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
 
1437
        $schemaColumnName = $this->_gs->_translateColumnName('name');
 
1438
        foreach ($moduleSql['table'] as $rawTableName => $ignored) {
 
1439
            /* Don't drop the schema table, it's part of the core. */
 
1440
            if ($rawTableName == 'Schema') {
 
1441
                continue;
 
1442
            }
 
1443
 
 
1444
            $this->_gs->_traceStart();
 
1445
            list ($tableName, $unused, $tableNameInSchema) =
 
1446
                $this->_gs->_translateTableName($rawTableName);
 
1447
            if (in_array(strtolower($tableName), $metatables)) {
 
1448
                /* Drop the table and yank it from the schema table */
 
1449
                $dropQuery = sprintf('DROP TABLE %s', $tableName);
 
1450
                $recordSet = $this->_gs->_db->Execute($dropQuery);
 
1451
                if (empty($recordSet)) {
 
1452
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1453
                }
 
1454
            }
 
1455
 
 
1456
            $cleanQuery = sprintf('DELETE FROM %s where %s=?',
 
1457
                                  $schemaTableName, $schemaColumnName);
 
1458
            $recordSet = $this->_gs->_db->Execute($cleanQuery, array($tableNameInSchema));
 
1459
            if (empty($recordSet)) {
 
1460
                return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1461
            }
 
1462
            $this->_gs->_traceStop();
 
1463
        }
 
1464
 
 
1465
        return null;
 
1466
    }
 
1467
 
 
1468
    /**
 
1469
     * Examine the schema table and return the version of all the Gallery tables
 
1470
     *
 
1471
     * @return array object GalleryStatus a status code
 
1472
     *               array (name => (major, minor))
 
1473
     * @access private
 
1474
     */
 
1475
    function _loadTableVersions() {
 
1476
        $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
1477
 
 
1478
        list ($schemaTableName) = $this->_gs->_translateTableName('Schema');
 
1479
        $query = 'SELECT ' . $this->_gs->_translateColumnName('name') . ', '
 
1480
               . $this->_gs->_translateColumnName('major') . ', '
 
1481
               . $this->_gs->_translateColumnName('minor') . ' FROM ' . $schemaTableName;
 
1482
 
 
1483
        $this->_gs->_traceStart();
 
1484
        $recordSet = $this->_gs->_db->Execute($query);
 
1485
        $this->_gs->_traceStop();
 
1486
 
 
1487
        if (empty($recordSet)) {
 
1488
            return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
 
1489
                                              'Error reading schema table'), null);
 
1490
        }
 
1491
 
 
1492
        $tableVersions = array();
 
1493
        while ($row = $recordSet->FetchRow()) {
 
1494
            $tableVersions[$row[0]] = array($row[1], $row[2]);
 
1495
        }
 
1496
 
 
1497
        return array(null, $tableVersions);
 
1498
    }
 
1499
 
 
1500
    /**
 
1501
     * Execute a given SQL against the database.  Prefix table and column names
 
1502
     * as necessary.  Split multiple commands in the file into separate Execute() calls.
 
1503
     *
 
1504
     * @return object GalleryStatus a status code
 
1505
     * @access private
 
1506
     */
 
1507
    function _executeSql($buffer) {
 
1508
        /*
 
1509
         * Split the file where semicolons are followed by a blank line..
 
1510
         * PL/SQL blocks will have other semicolons, so we can't split on every one.
 
1511
         */
 
1512
        foreach (preg_split('/; *\r?\n *\r?\n/s', $buffer) as $query) {
 
1513
            $query = trim($query);
 
1514
            if (!empty($query)) {
 
1515
                $query = str_replace('DB_TABLE_PREFIX', $this->_gs->_tablePrefix, $query);
 
1516
                $query = str_replace('DB_COLUMN_PREFIX', $this->_gs->_columnPrefix, $query);
 
1517
 
 
1518
                /* Perform database specific replacements */
 
1519
                foreach ($this->_gs->_getSqlReplacements() as $key => $value) {
 
1520
                    $query = str_replace($key, $value, $query);
 
1521
                }
 
1522
 
 
1523
                $this->_gs->_traceStart();
 
1524
                $recordSet = $this->_gs->_db->Execute($query);
 
1525
                $this->_gs->_traceStop();
 
1526
                if (empty($recordSet)) {
 
1527
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
 
1528
                                                "Error trying to run query: $query");
 
1529
                }
 
1530
            }
 
1531
        }
 
1532
 
 
1533
        return null;
 
1534
    }
 
1535
 
 
1536
    /**
 
1537
     * @see GalleryStorage::_executeSqlFile
 
1538
     */
 
1539
    function executeSqlFile($fileName) {
 
1540
        global $gallery;
 
1541
        $platform =& $gallery->getPlatform();
 
1542
 
 
1543
        if (!$platform->file_exists($fileName)) {
 
1544
            return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
 
1545
                                         "File $fileName does not exist");
 
1546
        }
 
1547
 
 
1548
        if (($buffer = $platform->file_get_contents($fileName)) === false) {
 
1549
            return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
 
1550
                                         "Unable to read file $fileName");
 
1551
        }
 
1552
 
 
1553
        $ret = $this->_dbInit(true);
 
1554
        if ($ret) {
 
1555
            return $ret->wrap(__FILE__, __LINE__);
 
1556
        }
 
1557
 
 
1558
        $ret = $this->_executeSql($buffer);
 
1559
        if ($ret) {
 
1560
            return $ret->wrap(__FILE__, __LINE__);
 
1561
        }
 
1562
 
 
1563
        return null;
 
1564
    }
 
1565
 
 
1566
    /**
 
1567
     * @see GalleryStorage::cleanStore
 
1568
     */
 
1569
    function cleanStore() {
 
1570
        global $gallery;
 
1571
        $gallery->guaranteeTimeLimit(20);
 
1572
 
 
1573
        $ret = $this->_dbInit(true);
 
1574
        if ($ret) {
 
1575
            return $ret->wrap(__FILE__, __LINE__);
 
1576
        }
 
1577
 
 
1578
        /* Get the metabase info about this database */
 
1579
        $this->_gs->_traceStart();
 
1580
        $metatables = $this->_gs->_db->MetaTables();
 
1581
        $this->_gs->_traceStop();
 
1582
 
 
1583
        /*
 
1584
         * Some databases (notably MySQL on Win32) don't support mixed case
 
1585
         * table names.  So, when we get the meta table list back, it's lower
 
1586
         * case.  Force all metatable listings to lower case and then expect
 
1587
         * them to be lowercase so that we're consistent.
 
1588
         */
 
1589
        for ($i = 0; $i < count($metatables); $i++) {
 
1590
            $metatables[$i] = strtolower($metatables[$i]);
 
1591
        }
 
1592
 
 
1593
        /* If the schema table exists then delete all the tables it lists */
 
1594
        list ($schemaTableName, $unused) = $this->_gs->_translateTableName('Schema');
 
1595
        if (in_array(strtolower($schemaTableName), $metatables)) {
 
1596
            /* Load all table versions */
 
1597
            list ($ret, $tableVersions) = $this->_loadTableVersions();
 
1598
            if ($ret) {
 
1599
                return $ret->wrap(__FILE__, __LINE__);
 
1600
            }
 
1601
 
 
1602
            foreach (array_keys($tableVersions) as $rawTableName) {
 
1603
                list ($tableName, $unused) = $this->_gs->_translateTableName($rawTableName);
 
1604
                $query = sprintf('DROP TABLE %s', $tableName);
 
1605
 
 
1606
                $this->_gs->_traceStart();
 
1607
                $recordSet = $this->_gs->_db->Execute($query);
 
1608
                $this->_gs->_traceStop();
 
1609
                if (empty($recordSet)) {
 
1610
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1611
                }
 
1612
            }
 
1613
 
 
1614
            /* Get rid of our sequences */
 
1615
            foreach (array(DATABASE_SEQUENCE_LOCK, DATABASE_SEQUENCE_ID) as $sequenceId) {
 
1616
                $this->_gs->_traceStart();
 
1617
                $recordSet = $this->_gs->_db->DropSequence($this->_gs->_tablePrefix . $sequenceId);
 
1618
                $this->_gs->_traceStop();
 
1619
                if (empty($recordSet)) {
 
1620
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1621
                }
 
1622
            }
 
1623
        }
 
1624
 
 
1625
        return null;
 
1626
    }
 
1627
 
 
1628
    /**
 
1629
     * @see GalleryStorage::getProfilingHtml
 
1630
     */
 
1631
    function getProfilingHtml() {
 
1632
        if (!isset($this->_gs->_db)) {
 
1633
            return '';
 
1634
        }
 
1635
        $this->_gs->_traceStart();
 
1636
        $perf =& NewPerfMonitor($this->_gs->_db);
 
1637
        $buf = $perf->SuspiciousSQL();
 
1638
        $buf .= $perf->ExpensiveSQL();
 
1639
        $this->_gs->_traceStop();
 
1640
        return $buf;
 
1641
    }
 
1642
 
 
1643
    /**
 
1644
     * @see GalleryStorage::isInstalled
 
1645
     */
 
1646
    function isInstalled() {
 
1647
        $ret = $this->_dbInit();
 
1648
        if ($ret) {
 
1649
            return array($ret->wrap(__FILE__, __LINE__), null);
 
1650
        }
 
1651
 
 
1652
        /* Get the metabase info about this database */
 
1653
        $this->_gs->_traceStart();
 
1654
        $metatables = $this->_gs->_db->MetaTables();
 
1655
        $this->_gs->_traceStop();
 
1656
 
 
1657
        list ($schemaTableName) = $this->_gs->_translateTableName('Schema');
 
1658
        $isInstalled = preg_match("/\b$schemaTableName\b/i", implode(' ', $metatables));
 
1659
        return array(null, $isInstalled);
 
1660
    }
 
1661
 
 
1662
    /**
 
1663
     * @see GalleryStorage::optimize
 
1664
     */
 
1665
    function optimize() {
 
1666
        $ret = $this->_dbInit();
 
1667
        if ($ret) {
 
1668
            return $ret->wrap(__FILE__, __LINE__);
 
1669
        }
 
1670
 
 
1671
        /* Load all table versions */
 
1672
        list ($ret, $tableVersions) = $this->_loadTableVersions();
 
1673
        if ($ret) {
 
1674
            return $ret->wrap(__FILE__, __LINE__);
 
1675
        }
 
1676
 
 
1677
        $statement = $this->_gs->_getOptimizeStatement();
 
1678
        if (!empty($statement)) {
 
1679
            foreach (array_keys($tableVersions) as $tableName) {
 
1680
                $query = sprintf($statement, $this->_gs->_tablePrefix . $tableName);
 
1681
                $this->_gs->_traceStart();
 
1682
                $recordSet = $this->_gs->_db->Execute($query);
 
1683
                $this->_gs->_traceStop();
 
1684
 
 
1685
                if (!$recordSet) {
 
1686
                    return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
 
1687
                }
 
1688
            }
 
1689
        }
 
1690
 
 
1691
        return null;
 
1692
    }
 
1693
 
 
1694
    /**
 
1695
     * @see GalleryStorage::getAffectedRows
 
1696
     */
 
1697
    function getAffectedRows() {
 
1698
        $ret = $this->_dbInit(true);
 
1699
        if ($ret) {
 
1700
            return array($ret->wrap(__FILE__, __LINE__), null);
 
1701
        }
 
1702
 
 
1703
        return array(null, $this->_gs->_db->Affected_Rows());
 
1704
    }
 
1705
 
 
1706
    /**
 
1707
     * Internal function to get clearance to acquire locks
 
1708
     *
 
1709
     * Request clearance to acquire locks and then wait until it's our turn.
 
1710
     *
 
1711
     * @param int the time to stop trying to get clearance
 
1712
     * @return object GalleryStatus a status code
 
1713
     */
 
1714
    function _getLockClearance($cutoffTime) {
 
1715
        /* Get the true name of the lock table */
 
1716
        list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
 
1717
 
 
1718
        /* Acquire a non-transactional connection to use for this request */
 
1719
        list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
 
1720
        if ($ret) {
 
1721
            return array($ret->wrap(__FILE__, __LINE__), null);
 
1722
        }
 
1723
 
 
1724
        /* Get a new lock id */
 
1725
        $this->_gs->_traceStart();
 
1726
        $lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
 
1727
        $this->_gs->_traceStop();
 
1728
 
 
1729
        /* Put in a lock request */
 
1730
        $lockIdCol = $this->_gs->_translateColumnName('lockId');
 
1731
        $requestCol = $this->_gs->_translateColumnName('request');
 
1732
        $freshUntilCol = $this->_gs->_translateColumnName('freshUntil');
 
1733
        $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES(?, 1, ?)',
 
1734
                         $lockTable, $lockIdCol, $requestCol, $freshUntilCol);
 
1735
        $data = array($lockId, time() + 30);
 
1736
 
 
1737
        $this->_gs->_traceStart();
 
1738
        $recordSet = $db->Execute($query, $data);
 
1739
        $this->_gs->_traceStop();
 
1740
        if (!$recordSet) {
 
1741
            $this->releaseLocks($lockId);
 
1742
            return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
 
1743
        }
 
1744
 
 
1745
        /* Wait till it's our turn */
 
1746
        while (true) {
 
1747
            $query = 'SELECT ' . $lockIdCol . ' FROM ' . $lockTable
 
1748
                   . ' WHERE ' . $requestCol . '=1 ORDER BY ' . $lockIdCol . ' ASC';
 
1749
 
 
1750
            $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
1751
            $this->_gs->_traceStart();
 
1752
            $recordSet = $db->SelectLimit($query, 1);
 
1753
            $this->_gs->_traceStop();
 
1754
            if (!$recordSet) {
 
1755
                $this->releaseLocks($lockId);
 
1756
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
1757
                             null);
 
1758
            }
 
1759
 
 
1760
            $row = $recordSet->FetchRow();
 
1761
            if ($row[0] == $lockId) {
 
1762
                break;
 
1763
            }
 
1764
 
 
1765
            /* Wait a second and try again */
 
1766
            sleep(1);
 
1767
 
 
1768
            /* Expire any bogus locks */
 
1769
            $ret = $this->_expireLocks();
 
1770
            if ($ret) {
 
1771
                return array($ret->wrap(__FILE__, __LINE__), null);
 
1772
            }
 
1773
 
 
1774
            if (time() > $cutoffTime) {
 
1775
                $this->releaseLocks($lockId);
 
1776
                return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__), null);
 
1777
            }
 
1778
        }
 
1779
 
 
1780
        return array(null, $lockId);
 
1781
    }
 
1782
 
 
1783
    /**
 
1784
     * Identify the type of entity associated with the id provided
 
1785
     *
 
1786
     * @param int a object id
 
1787
     * @return array a GalleryStatus and a string class name
 
1788
     */
 
1789
    function _identifyEntities($ids) {
 
1790
        assert('!empty($ids)');
 
1791
 
 
1792
        if (!is_array($ids)) {
 
1793
            $ids = array($ids);
 
1794
            $returnArray = false;
 
1795
        } else {
 
1796
            $returnArray = true;
 
1797
        }
 
1798
 
 
1799
        $checkIds = array();
 
1800
        foreach ($ids as $id) {
 
1801
            if (!GalleryDataCache::containsKey("GalleryStorage::_identifyEntities($id)")) {
 
1802
                $checkIds[] = $id;
 
1803
            }
 
1804
        }
 
1805
 
 
1806
        $local = array();
 
1807
        if (!empty($checkIds)) {
 
1808
            $idCol = $this->_gs->_translateColumnName('id');
 
1809
            $entityTypeCol = $this->_gs->_translateColumnName('entityType');
 
1810
            list ($table, $unused) = $this->_gs->_translateTableName('GalleryEntity');
 
1811
            $markers = GalleryUtilities::makeMarkers(count($checkIds));
 
1812
            $query = 'SELECT ' . $idCol . ', ' . $entityTypeCol
 
1813
                   . ' FROM ' . $table . ' WHERE ' . $idCol . ' IN (' . $markers . ')';
 
1814
 
 
1815
            $GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
 
1816
 
 
1817
            $this->_gs->_traceStart();
 
1818
            $recordSet = $this->_gs->_db->Execute($query, $checkIds);
 
1819
            $this->_gs->_traceStop();
 
1820
 
 
1821
            if ($recordSet) {
 
1822
                while ($row = $recordSet->FetchRow()) {
 
1823
                    if (empty($row[1])) {
 
1824
                        return array(
 
1825
                            GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__), null);
 
1826
                    } else {
 
1827
                        /*
 
1828
                         * Save a copy locally, in case the global cache is disabled
 
1829
                         * (like in the upgrader)
 
1830
                         */
 
1831
                        $local[$row[0]] = $row[1];
 
1832
                        GalleryDataCache::put("GalleryStorage::_identifyEntities($row[0])",
 
1833
                                              $row[1], true);
 
1834
                    }
 
1835
                }
 
1836
            } else {
 
1837
                return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
 
1838
                             null);
 
1839
            }
 
1840
        }
 
1841
 
 
1842
        if ($returnArray) {
 
1843
            $results = array();
 
1844
            foreach ($ids as $id) {
 
1845
                if (isset($local[$id])) {
 
1846
                    $results[] = $local[$id];
 
1847
                } else if (GalleryDataCache::containsKey(
 
1848
                               "GalleryStorage::_identifyEntities($id)")) {
 
1849
                    $results[] = GalleryDataCache::get("GalleryStorage::_identifyEntities($id)");
 
1850
                } else {
 
1851
                    return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
 
1852
                                                       "Missing object for $id"), null);
 
1853
                }
 
1854
            }
 
1855
        } else {
 
1856
            $results = GalleryDataCache::get("GalleryStorage::_identifyEntities($ids[0])");
 
1857
        }
 
1858
 
 
1859
        return array(null, $results);
 
1860
    }
 
1861
 
 
1862
    /**
 
1863
     * Describe the members, modules and parent of an entity
 
1864
     *
 
1865
     * @param string a class name
 
1866
     * @param boolean true if we should scan all modules, not just the active ones
 
1867
     * @access protected
 
1868
     * @return array object GalleryStatus a status code
 
1869
     *               entity associative array
 
1870
     */
 
1871
    function describeEntity($entityName, $tryAllModules=false) {
 
1872
        global $gallery;
 
1873
 
 
1874
        /* Note: keep these cache keys in sync with _clearEntityAndMapCache() */
 
1875
        $cacheKey = "GalleryStorage::describeEntity()";
 
1876
        $cacheParams = array('type' => 'module',
 
1877
                             'itemId' => 'GalleryStorage_describeEntity',
 
1878
                             'id' => '_all');
 
1879
 
 
1880
        /* We only cache the results for active modules */
 
1881
        if (!$tryAllModules) {
 
1882
            if (!GalleryDataCache::containsKey($cacheKey)) {
 
1883
                $entityInfo = GalleryDataCache::getFromDisk($cacheParams);
 
1884
            }  else {
 
1885
                $entityInfo = GalleryDataCache::get($cacheKey);
 
1886
            }
 
1887
        }
 
1888
 
 
1889
        if (!isset($entityInfo)) {
 
1890
            list ($ret, $moduleStatus) = GalleryCoreApi::fetchPluginStatus('module');
 
1891
            if ($ret) {
 
1892
                return array($ret->wrap(__FILE__, __LINE__), null);
 
1893
            }
 
1894
 
 
1895
            $entityInfo = array();
 
1896
            foreach ($moduleStatus as $moduleId => $moduleInfo) {
 
1897
                if (!$tryAllModules && empty($moduleInfo['active'])) {
 
1898
                    continue;
 
1899
                }
 
1900
 
 
1901
                /*
 
1902
                 * Don't use GalleryPlatform here because it can cause difficult-to-eliminate
 
1903
                 * issues in the testing code when we use mock platforms.  Once we have an
 
1904
                 * abstraction layer around GalleryCoreApi we can use the platform here.
 
1905
                 */
 
1906
                $moduleDir = GalleryCoreApi::getPluginBaseDir('module', $moduleId);
 
1907
                if ($ret) {
 
1908
                    return array($ret->wrap(__FILE__, __LINE__), null);
 
1909
                }
 
1910
 
 
1911
                $entitiesFile = "$moduleDir/modules/$moduleId/classes/Entities.inc";
 
1912
                if (file_exists($entitiesFile)) {
 
1913
                    include($entitiesFile);
 
1914
                }
 
1915
            }
 
1916
 
 
1917
            if (!$tryAllModules) {
 
1918
                GalleryDataCache::putToDisk($cacheParams, $entityInfo);
 
1919
                GalleryDataCache::put($cacheKey, $entityInfo);
 
1920
            }
 
1921
        }
 
1922
 
 
1923
        /* Fall back to all available modules */
 
1924
        if (!$tryAllModules && !isset($entityInfo[$entityName])) {
 
1925
            list ($ret, $entityInfo) = $this->describeEntity($entityName, true);
 
1926
            if ($ret) {
 
1927
                return array($ret->wrap(__FILE__, __LINE__), null);
 
1928
            }
 
1929
        }
 
1930
 
 
1931
        /*
 
1932
         * Fall back on the parent class for any entities we don't recognize.  This is mainly so
 
1933
         * that tests can create lightweight subclasses.  Because PHP4 doesn't have case sensitive
 
1934
         * class names we have to do a linear time lookup.
 
1935
         * Don't use strcasecmp or strtolower because they are affected by locale.
 
1936
         */
 
1937
        if (!isset($entityInfo[$entityName])) {
 
1938
            $parentClass = get_parent_class($entityName);
 
1939
            foreach (array_keys($entityInfo) as $candidate) {
 
1940
                if ($parentClass == $candidate || $parentClass == strtr($candidate,
 
1941
                            'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijlkmnopqrstuvwxyz')) {
 
1942
                    $entityInfo[$entityName] = array(
 
1943
                        'members' => array(),
 
1944
                        'parent' => $candidate,
 
1945
                        'module' => 'unknown');
 
1946
                    break;
 
1947
                }
 
1948
            }
 
1949
        }
 
1950
 
 
1951
        if (!isset($entityInfo[$entityName])) {
 
1952
            return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
 
1953
                                               "Unknown entity type: $entityName"), null);
 
1954
        }
 
1955
 
 
1956
        return array(null, $entityInfo);
 
1957
    }
 
1958
 
 
1959
    /**
 
1960
     * Clear out the entity and map caches, which we should do any time that we add or
 
1961
     * remove a table.
 
1962
     */
 
1963
    function _clearEntityAndMapCache() {
 
1964
        /* Note: keep these cache keys in sync with describeMap() */
 
1965
        GalleryDataCache::remove("GalleryStorage::describeMap()");
 
1966
        GalleryDataCache::removeFromDisk(array('type' => 'module',
 
1967
                                               'itemId' => 'GalleryStorage_describeMap',
 
1968
                                               'id' => '_all'));
 
1969
 
 
1970
        /* Note: keep these cache keys in sync with describeEntity() */
 
1971
        GalleryDataCache::remove("GalleryStorage::describeEntity()");
 
1972
        GalleryDataCache::removeFromDisk(array('type' => 'module',
 
1973
                                               'itemId' => 'GalleryStorage_describeEntity',
 
1974
                                               'id' => '_all'));
 
1975
    }
 
1976
}
 
1977
?>