3
* $RCSfile: GalleryStorageExtras.class,v $
5
* Gallery - a web based photo album viewer and editor
6
* Copyright (C) 2000-2006 Bharat Mediratta
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.
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.
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.
23
* @version $Revision: 1.10 $ $Date: 2006/03/21 07:06:13 $
24
* @package GalleryCore
25
* @author Bharat Mediratta <bharat@menalto.com>
29
* Extended functionality in GalleryStorage that's not generally required for
30
* simply viewing albums and photos.
32
* @package GalleryCore
35
class GalleryStorageExtras /* the other half of GalleryStorage */ {
38
* @param object GalleryStorage the database storage instance
40
function GalleryStorageExtras(&$galleryStorage) {
41
$this->_gs =& $galleryStorage;
45
* Return a non transactional database connection
47
* @return array object GalleryStatus a status code
48
* object ADOdb a database connection
50
function _getNonTransactionalDatabaseConnection() {
52
* If we're transactional, then we need another connection to
53
* manipulate our locks, since they have to operate outside of a
56
if ($this->_gs->_isTransactional) {
57
if (empty($this->_gs->_nonTransactionalDb)) {
58
list ($ret, $this->_gs->_nonTransactionalDb) = $this->_gs->_getConnection(true);
60
return array($ret->wrap(__FILE__, __LINE__), null);
63
return array(null, $this->_gs->_nonTransactionalDb);
65
$ret = $this->_dbInit();
67
return array($ret->wrap(__FILE__, __LINE__), null);
69
return array(null, $this->_gs->_db);
74
* Connect to database if needed and optionally guarantee db transaction.
75
* @return object GalleryStatus a status code
78
function _dbInit($transaction=false) {
79
if (!isset($this->_gs->_db)) {
80
list ($ret, $this->_gs->_db) = $this->_gs->_getConnection();
82
return $ret->wrap(__FILE__, __LINE__);
86
$ret = $this->_gs->_guaranteeTransaction();
88
return $ret->wrap(__FILE__, __LINE__);
95
* @see GalleryStorage::loadEntities
97
function loadEntities($ids) {
99
$ret = $this->_dbInit();
101
return array($ret->wrap(__FILE__, __LINE__), null);
104
foreach ($ids as $idx => $id) {
105
$ids[$idx] = (int)$id;
108
/* Identify all the ids at once */
109
list ($ret, $types) = $this->_identifyEntities($ids);
111
return array($ret->wrap(__FILE__, __LINE__), null);
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);
122
$classNames[$types[$i]][$ids[$i]] = 1;
125
/* Load them in groups */
126
foreach ($classNames as $className => $targetIdHash) {
127
$gallery->guaranteeTimeLimit(5);
129
/* Get unique target ids */
130
$targetIds = array_keys($targetIdHash);
132
/* Get our member info for this class */
133
list ($ret, $memberInfo) = $this->describeEntity($className);
135
return array($ret->wrap(__FILE__, __LINE__), null);
138
$idCol = $this->_gs->_translateColumnName('id');
140
/* Build up our query */
141
$columns = $tables = $where = $memberData = $callbacks = array();
142
$markers = GalleryUtilities::makeMarkers(count($targetIds));
143
$target = $className;
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;
152
$target = $memberInfo[$target]['parent'];
154
$tables = array_keys($tables);
155
$columns = array_keys($columns);
157
for ($i = 0; $i < count($tables); $i++) {
159
$where[] = $tables[$i] . '.' . $idCol . ' IN (' . $markers . ')';
161
$where[] = $tables[$i] . '.' . $idCol . '=' . $tables[0] . '.' . $idCol;
166
$query .= implode(', ', $columns);
168
$query .= implode(', ', $tables);
170
$query .= implode(' AND ', $where);
172
/* Execute the query */
173
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
175
$this->_gs->_traceStart();
176
$recordSet = $this->_gs->_db->Execute($query, $targetIds);
177
$this->_gs->_traceStop();
179
if ($recordSet->RecordCount() != count($targetIds)) {
180
return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__),
184
/* Process all the results */
186
while ($row = $recordSet->FetchRow()) {
187
if (++$j % 20 == 0) {
188
$gallery->guaranteeTimeLimit(5);
191
if (!class_exists($className)) {
192
GalleryCoreApi::requireOnce(
193
"modules/{$memberInfo[$className]['module']}/classes/$className.class");
195
$entity = new $className;
197
if (empty($entity)) {
198
return array(GalleryCoreApi::error(ERROR_BAD_DATA_TYPE, __FILE__, __LINE__),
202
for ($i = 0; $i < count($callbacks); $i++) {
203
$value = $this->_gs->_normalizeValue($row[$i], $memberData[$i], true);
205
/* Store the value in the object */
206
$entity->$callbacks[$i] = $value;
207
$entity->_persistentStatus['originalValue'][$callbacks[$i]] = $value;
210
$entity->resetOriginalValues();
211
$entities[$entity->id] = $entity;
216
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
221
/* Assemble the entities in the right order and return them */
223
foreach ($ids as $id) {
224
$result[] = $entities[$id];
227
return array(null, $result);
231
* @see GalleryStorage::saveEntity
233
function saveEntity(&$entity) {
234
$ret = $this->_dbInit(true);
236
return $ret->wrap(__FILE__, __LINE__);
239
/* Update the serial number, but remember the original one */
240
$originalSerialNumber = (int)$entity->serialNumber++;
242
/* Get our member info for this class */
243
list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
245
return $ret->wrap(__FILE__, __LINE__);
250
* Build up a complete picture of all the various changed fields, so
251
* that we can do an insert or update.
253
$dataTable = array();
255
$target = $entity->getEntityType();
258
foreach ($memberInfo[$target]['members'] as $memberName => $memberData) {
259
$type = $memberData['type'];
260
list ($tableName, $unused) = $this->_gs->_translateTableName($target);
262
/* If the member is modified, record the new value in our table */
263
if ($entity->isModified($memberName)) {
264
$value = $entity->$memberName;
266
$entity->$memberName = $value =
267
$this->_gs->_normalizeValue($value, $memberData);
269
$columnName = $this->_gs->_translateColumnName($memberName);
270
$dataTable[$tableName][$columnName] = $value;
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
278
if (!isset($dataTable[$tableName])) {
279
$dataTable[$tableName] = array();
283
if ($type & STORAGE_TYPE_ID) {
284
$value = $entity->$memberName;
286
$id['column'] = $this->_gs->_translateColumnName($memberName);
287
$id['value'] = $value;
290
$target = $memberInfo[$target]['parent'];
293
if ($entity->testPersistentFlag(STORAGE_FLAG_NEWLY_CREATED)) {
295
* Iterate through the data table and make up an INSERT statement
296
* for each table that requires one.
298
foreach ($dataTable as $tableName => $columnChanges) {
300
/* Make sure that the id column is set for each table */
301
if (empty($columnChanges[$id['column']])) {
302
$columnChanges[$id['column']] = $id['value'];
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 . ')';
312
$this->_gs->_traceStart();
313
$recordSet = $this->_gs->_db->Execute($query, $data);
314
$this->_gs->_traceStop();
317
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
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.
327
list ($serialNumberTable) = $this->_gs->_translateTableName('GalleryEntity');
329
$queryList = array();
330
foreach ($dataTable as $tableName => $columnChanges) {
331
$changeList = array();
334
foreach ($columnChanges as $columnName => $value) {
335
$changeList[] = $columnName . '=?';
339
if (count($changeList)) {
340
$query = 'UPDATE ' . $tableName . ' SET';
341
$query .= ' ' . implode(',', $changeList);
342
$query .= ' WHERE ' . $id['column'] . '=?';
343
$data[] = $id['value'];
345
if (!strcmp($tableName, $serialNumberTable)) {
347
$this->_gs->_translateColumnName('serialNumber') .
349
$data[] = $originalSerialNumber;
350
array_unshift($queryList, array($query, $data));
352
array_push($queryList, array($query, $data));
358
* Now apply each UPDATE statement in turn. Make sure that we're
359
* only affecting one row each time.
361
foreach ($queryList as $queryAndData) {
362
list ($query, $data) = $queryAndData;
364
$this->_gs->_traceStart();
365
$recordSet = $this->_gs->_db->Execute($query, $data);
366
$this->_gs->_traceStop();
369
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
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");
384
$entity->clearPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
385
$entity->resetOriginalValues();
386
$ret = $entity->onSave();
388
return $ret->wrap(__FILE__, __LINE__);
395
* @see GalleryStorage::deleteEntity
397
function deleteEntity(&$entity) {
398
$ret = $this->_dbInit(true);
400
return $ret->wrap(__FILE__, __LINE__);
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);
410
/* Get our persistent and member info for this class */
411
list ($ret, $memberInfo) = $this->describeEntity($entity->entityType);
413
return $ret->wrap(__FILE__, __LINE__);
416
$idCol = $this->_gs->_translateColumnName('id');
419
$target = $entity->entityType;
421
foreach ($memberInfo[$target]['members'] as $columnName => $columnInfo) {
422
list ($tableName, $unused) = $this->_gs->_translateTableName($target);
423
$tables[$tableName] = 1;
425
$target = $memberInfo[$target]['parent'];
429
* XXX OPT: Override this for specific database implementations that
430
* allow multi-table delete.
432
foreach ($tables as $tableName => $junk) {
433
$query = 'DELETE FROM ' . $tableName . ' WHERE ' . $idCol . '=?';
434
$data = array((int)$entity->getId());
436
$this->_gs->_traceStart();
437
$recordSet = $this->_gs->_db->Execute($query, $data);
438
$this->_gs->_traceStop();
440
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
444
$entity->setPersistentFlag(STORAGE_FLAG_DELETED);
450
* @see GalleryStorage::newEntity
452
function newEntity(&$entity) {
453
/* getUniqueId does _dbInit */
454
list ($ret, $id) = $this->getUniqueId();
456
return $ret->wrap(__FILE__, __LINE__);
460
$entity->serialNumber = 0;
461
$entity->setPersistentFlag(STORAGE_FLAG_NEWLY_CREATED);
467
* @see GalleryStorage::getUniqueId
469
function getUniqueId() {
470
$ret = $this->_dbInit();
472
return array($ret->wrap(__FILE__, __LINE__), null);
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;
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();
485
if (isset($setGenID)) {
486
$this->_gs->_db->hasGenID = false;
489
return array(null, $id);
493
* @see GalleryStorage::refreshEntity
495
function refreshEntity($entity) {
496
$ret = $this->_dbInit();
498
return array($ret->wrap(__FILE__, __LINE__), null);
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
507
list ($ret, list ($freshEntity)) = $this->_gs->loadEntities(array($entity->id));
509
return array($ret->wrap(__FILE__, __LINE__), null);
512
/* Let entity do its post-load procedure */
513
$ret = $freshEntity->onLoad();
515
return array($ret->wrap(__FILE__, __LINE__), null);
518
return array(null, $freshEntity);
522
* @see GalleryStorage::acquireReadLock
524
function acquireReadLock($entityIds, $timeout) {
525
/* It's ok to pass in a single id */
526
if (!is_array($entityIds)) {
527
$entityIds = array($entityIds);
530
foreach ($entityIds as $idx => $id) {
531
$entityIds[$idx] = (int)$id;
534
/* Acquire a non-transactional connection to use for this request */
535
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
537
return array($ret->wrap(__FILE__, __LINE__), null);
540
/* Know when to call it quits */
541
$cutoffTime = time() + $timeout;
543
/* Get the true name of the lock table */
544
list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
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
554
list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
556
return array($ret->wrap(__FILE__, __LINE__), null);
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 . ')';
566
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
568
$this->_gs->_traceStart();
569
$recordSet = $db->Execute($query, $data);
570
$this->_gs->_traceStop();
572
$this->releaseLocks($lockId);
573
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
577
$row = $recordSet->FetchRow();
582
/* An entity that we want is write locked */
583
$this->releaseLocks($lockId);
585
if (time() > $cutoffTime) {
586
return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__),
590
/* Wait a second and try again */
593
/* Expire any bogus locks */
594
$ret = $this->_expireLocks();
596
return array($ret->wrap(__FILE__, __LINE__), null);
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;
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);
612
$this->_gs->_traceStart();
613
$recordSet = $db->Execute($query, $data);
614
$this->_gs->_traceStop();
616
$this->releaseLocks($lockId);
617
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
620
$lockInfo[$entityId] = true;
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);
629
$this->_gs->_traceStart();
630
$recordSet = $db->Execute($query, $data);
631
$this->_gs->_traceStop();
633
$this->releaseLocks($lockId);
634
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
638
array('lockId' => $lockId, 'type' => LOCK_READ, 'ids' => $lockInfo));
642
* @see GalleryStorage::acquireWriteLock
644
function acquireWriteLock($entityIds, $timeout) {
645
/* It's ok to pass in a single id */
646
if (!is_array($entityIds)) {
647
$entityIds = array($entityIds);
650
foreach ($entityIds as $idx => $id) {
651
$entityIds[$idx] = (int)$id;
654
/* Acquire a non-transactional connection to use for this request */
655
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
657
return array($ret->wrap(__FILE__, __LINE__), null);
660
/* Know when to call it quits */
661
$cutoffTime = time() + $timeout;
663
/* Get the true name of the lock table */
664
list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
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
674
list ($ret, $lockId) = $this->_getLockClearance($cutoffTime);
676
return array($ret->wrap(__FILE__, __LINE__), null);
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 . ')';
687
$data = array_merge($data, $entityIds);
689
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
691
$this->_gs->_traceStart();
692
$recordSet = $db->Execute($query, $data);
693
$this->_gs->_traceStop();
695
$this->releaseLocks($lockId);
696
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
700
$row = $recordSet->FetchRow();
705
/* An entity that we want is still locked */
706
$this->releaseLocks($lockId);
708
if (time() > $cutoffTime) {
709
return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__),
713
/* Wait a second and try again */
716
/* Expire any bogus locks */
717
$ret = $this->_expireLocks();
719
return array($ret->wrap(__FILE__, __LINE__), null);
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;
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);
735
$this->_gs->_traceStart();
736
$recordSet = $db->Execute($query, $data);
737
$this->_gs->_traceStop();
739
$this->releaseLocks($lockId);
740
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
743
$lockInfo[$entityId] = true;
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);
752
$this->_gs->_traceStart();
753
$recordSet = $db->Execute($query, $data);
754
$this->_gs->_traceStop();
756
$this->releaseLocks($lockId);
757
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
761
array('lockId' => $lockId, 'type' => LOCK_WRITE, 'ids' => $lockInfo));
765
* @see GalleryStorage::refreshLocks
767
function refreshLocks($lockIds, $freshUntil) {
768
if (!empty($lockIds)) {
769
foreach ($lockIds as $idx => $id) {
770
$lockIds[$idx] = (int)$id;
773
/* Acquire a non-transactional connection to use for this request */
774
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
776
return $ret->wrap(__FILE__, __LINE__);
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);
786
$this->_gs->_traceStart();
787
$data = array_merge(array($freshUntil), $lockIds);
788
$recordSet = $db->Execute($query, $data);
789
$this->_gs->_traceStop();
792
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
800
* Delete all not-so-fresh locks.
802
* @return object GalleryStatus a status code
805
function _expireLocks() {
806
/* Acquire a non-transactional connection to use for this request */
807
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
809
return $ret->wrap(__FILE__, __LINE__);
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);
817
$this->_gs->_traceStart();
818
$recordSet = $db->Execute($query, array(time()));
819
$this->_gs->_traceStop();
822
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
829
* @see GalleryStorage::releaseLocks
831
function releaseLocks($lockIds) {
832
if (!is_array($lockIds)) {
833
$lockIds = array($lockIds);
835
foreach ($lockIds as $idx => $id) {
836
$lockIds[$idx] = (int)$id;
839
/* Acquire a non-transactional connection to use for this request */
840
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
842
return $ret->wrap(__FILE__, __LINE__);
845
/* Get the true name of the lock table */
846
list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
848
$lockIdCol = $this->_gs->_translateColumnName('lockId');
849
$markers = GalleryUtilities::makeMarkers(count($lockIds));
850
$query = 'DELETE FROM ' . $lockTable . ' WHERE ' . $lockIdCol . ' IN (' . $markers . ')';
852
$this->_gs->_traceStart();
853
$recordSet = $db->Execute($query, $lockIds);
854
$this->_gs->_traceStop();
858
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
863
* @see GalleryStorage::removeIdsFromLock
865
function removeIdsFromLock($lock, $ids) {
866
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
868
return $ret->wrap(__FILE__, __LINE__);
874
AND [::' . ($lock['type'] == LOCK_WRITE ? 'write' : 'read') . 'EntityId] IN ('
875
. GalleryUtilities::makeMarkers(count($ids)) . ')';
876
$query = $this->_gs->_translateQuery($query);
878
$this->_gs->_traceStart();
879
$recordSet = $db->Execute($query, array_merge(array($lock['lockId']), $ids));
880
$this->_gs->_traceStop();
884
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
889
* @see GalleryStorage::moveIdsBetweenLocks
891
function moveIdsBetweenLocks($relock, $newLockId, $lockType) {
892
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
894
return $ret->wrap(__FILE__, __LINE__);
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();
907
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
914
* @see GalleryStorage::newLockId
916
function newLockId() {
917
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
919
return array($ret->wrap(__FILE__, __LINE__), null);
922
$this->_gs->_traceStart();
923
$lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
924
$this->_gs->_traceStop();
926
return array(null, $lockId);
930
* @see GalleryStorage::execute
932
function execute($statement, $data=array()) {
933
$ret = $this->_dbInit(true);
935
return $ret->wrap(__FILE__, __LINE__);
938
$statement = $this->_gs->_translateQuery($statement);
940
$this->_gs->_traceStart();
941
$recordSet = $this->_gs->_db->Execute($statement, $data);
942
$this->_gs->_traceStop();
944
/* Direct SQL commands can undermine our memory cache, so reset it */
945
GalleryDataCache::reset();
947
return $recordSet ? null
948
: GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
952
* @see GalleryStorage::addMapEntry
954
function addMapEntry($mapName, $entry) {
955
$ret = $this->_dbInit(true);
957
return $ret->wrap(__FILE__, __LINE__);
960
list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
962
return $ret->wrap(__FILE__, __LINE__);
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);
972
if (is_array($entry[$memberName])) {
973
return $this->_addMapEntries($mapInfo, $tableName, $entry);
975
$columns[] = $this->_gs->_translateColumnName($memberName);
976
$data[] = $this->_gs->_normalizeValue($entry[$memberName], $memberData);
979
$markers = GalleryUtilities::makeMarkers(count($columns));
980
$query = 'INSERT INTO ' . $tableName . ' (';
981
$query .= implode(', ', $columns);
982
$query .= ') VALUES (' . $markers . ')';
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__);
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) ...
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
1006
function _addMapEntries($mapInfo, $tableName, $entry) {
1008
foreach ($mapInfo as $memberName => $memberData) {
1009
$columns[$memberName] = $this->_gs->_translateColumnName($memberName);
1012
/* Now we transpose the entry matrix */
1013
$rows = count($entry[$memberName]);
1015
for ($ind = 0; $ind < $rows; $ind++) {
1016
foreach ($mapInfo as $memberName => $memberData) {
1017
$data[] =$this->_gs->_normalizeValue($entry[$memberName][$ind], $memberData);
1021
list ($ret, $query) =
1022
$this->_gs->getFunctionSql('MULTI_INSERT', array($tableName, $columns, $rows));
1024
return $ret->wrap(__FILE__, __LINE__);
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__);
1038
* @see GalleryStorage::removeMapEntry
1040
function removeMapEntry($mapName, $match) {
1041
$ret = $this->_dbInit(true);
1043
return $ret->wrap(__FILE__, __LINE__);
1046
list ($ret, $mapInfo) = $this->_gs->describeMap($mapName);
1048
return $ret->wrap(__FILE__, __LINE__);
1050
list ($tableName, $unused) = $this->_gs->_translateTableName($mapName);
1051
$data = $where = array();
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) {
1061
} else if (is_array($match[$memberName])) {
1063
foreach ($match[$memberName] as $value) {
1065
$data[] = $this->_gs->_normalizeValue($value, $memberData);
1067
$where[] = $this->_gs->_translateColumnName($memberName) . ' IN ('
1068
. implode(',', $qs) . ')';
1070
$value = $this->_gs->_normalizeValue($match[$memberName], $memberData);
1071
if (is_null($value)) {
1072
$where[] = $this->_gs->_translateColumnName($memberName) . ' IS NULL';
1074
$where[] = $this->_gs->_translateColumnName($memberName) . '=?';
1081
if (empty($where)) {
1082
return GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
1083
'Missing where clause');
1086
$query = 'DELETE FROM ' . $tableName . ' WHERE ' . implode(' AND ', $where);
1088
$this->_gs->_traceStart();
1089
$recordSet = $this->_gs->_db->Execute($query, $data);
1090
$this->_gs->_traceStop();
1092
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
1099
* @see GalleryStorage::removeAllMapEntries
1101
function removeAllMapEntries($mapName) {
1102
$ret = $this->_dbInit(true);
1104
return $ret->wrap(__FILE__, __LINE__);
1107
list ($tableName) = $this->_gs->_translateTableName($mapName);
1108
$query = 'DELETE FROM ' . $tableName;
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__);
1121
* Load up the table creation and alteration SQL files for the given module
1124
function _getModuleSql($moduleId) {
1126
$platform =& $gallery->getPlatform();
1127
$sqlFile = sprintf('%smodules/%s/classes/GalleryStorage/schema.tpl',
1128
GalleryCoreApi::getPluginBaseDir('module', $moduleId), $moduleId);
1130
if ($platform->file_exists($sqlFile)) {
1131
$sqlData = $platform->file($sqlFile);
1132
$moduleSql = GalleryStorageExtras::parseSqlTemplate($sqlData, $this->_gs->getType());
1134
$moduleSql = array('table' => array(), 'alter' => array(),
1135
'remove' => array(), 'test' => array());
1138
return array(null, $moduleSql);
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
1146
* @param array the raw template data
1147
* @param string the database type
1148
* @return array the parsed results
1151
function parseSqlTemplate($sqlData, $dbType) {
1152
$info = array('table' => array(), 'alter' => array(),
1153
'remove' => array(), 'test' => array());
1154
$dbname = $tablename = null;
1156
foreach ($sqlData as $line) {
1157
$line = rtrim($line);
1158
if (preg_match('/^## (.*)$/', $line, $matches)) {
1159
$record = ($matches[1] == $dbType);
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]] = '';
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]] = '';
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]] = '';
1182
$insertPointer =& $info['remove'][$matches[1]][$matches[2]][$matches[3]];
1184
if (!isset($info['table'][$tablename])) {
1185
$info['table'][$tablename] = '';
1187
$insertPointer =& $info['table'][$tablename];
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.
1197
$insertPointer .= $line . "\n";
1199
if (!empty($insertPointer)) {
1200
$insertPointer .= "\n";
1202
$insertPointer .= $line;
1210
* @see GalleryStorage::configureStore
1212
function configureStore($moduleId, $upgradeInfo=array()) {
1214
$gallery->guaranteeTimeLimit(20);
1216
$this->_clearEntityAndMapCache();
1218
$ret = $this->_dbInit(true);
1220
return $ret->wrap(__FILE__, __LINE__);
1223
list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
1225
return $ret->wrap(__FILE__, __LINE__);
1228
/* Get the metabase info about this database */
1229
$this->_gs->_traceStart();
1230
$metatables = $this->_gs->_db->MetaTables();
1231
$this->_gs->_traceStop();
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.
1239
for ($i = 0; $i < count($metatables); $i++) {
1240
$metatables[$i] = strtolower($metatables[$i]);
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']);
1248
return $ret->wrap(__FILE__, __LINE__);
1250
unset($moduleSql['table']['Schema']);
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__);
1263
/* Load all table versions */
1264
list ($ret, $tableVersions) = $this->_loadTableVersions();
1266
return $ret->wrap(__FILE__, __LINE__);
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.
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);
1282
return $ret->wrap(__FILE__, __LINE__);
1286
/* The table exists -- see if we have an upgrade for it */
1287
if (empty($tableVersions[$tableNameInSchema])) {
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.
1293
if ($gallery->getDebug()) {
1294
$gallery->debug("Table $rawTableName: missing entry in Schema table");
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);
1306
return $ret->wrap(__FILE__, __LINE__);
1309
/* Reload all table versions, cause one has now changed */
1310
list ($ret, $tableVersions) = $this->_loadTableVersions();
1312
return $ret->wrap(__FILE__, __LINE__);
1315
/* No upgrade available */
1326
* @see GalleryStorage::configureStoreCleanup
1328
function configureStoreCleanup($moduleId) {
1331
$ret = $this->_dbInit(true);
1333
return $ret->wrap(__FILE__, __LINE__);
1336
list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
1338
return $ret->wrap(__FILE__, __LINE__);
1341
/* Get the metabase info about this database */
1342
$this->_gs->_traceStart();
1343
$metatables = $this->_gs->_db->MetaTables();
1344
$this->_gs->_traceStop();
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.
1352
for ($i = 0; $i < count($metatables); $i++) {
1353
$metatables[$i] = strtolower($metatables[$i]);
1356
/* Load all table versions */
1357
list ($ret, $tableVersions) = $this->_loadTableVersions();
1359
return $ret->wrap(__FILE__, __LINE__);
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') {
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])) {
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.
1378
if ($gallery->getDebug()) {
1379
$gallery->debug("Table $rawTableName: missing entry in Schema table");
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]);
1388
return $ret->wrap(__FILE__, __LINE__);
1399
* @see GalleryStorage::unconfigureStore
1401
function unconfigureStore($moduleId) {
1403
$gallery->guaranteeTimeLimit(20);
1405
$ret = $this->_dbInit(true);
1410
list ($ret, $moduleSql) = $this->_getModuleSql($moduleId);
1412
return $ret->wrap(__FILE__, __LINE__);
1415
/* Get the metabase info about this database */
1416
$this->_gs->_traceStart();
1417
$metatables = $this->_gs->_db->MetaTables();
1418
$this->_gs->_traceStop();
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.
1426
for ($i = 0; $i < count($metatables); $i++) {
1427
$metatables[$i] = strtolower($metatables[$i]);
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.
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') {
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__);
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__);
1462
$this->_gs->_traceStop();
1469
* Examine the schema table and return the version of all the Gallery tables
1471
* @return array object GalleryStatus a status code
1472
* array (name => (major, minor))
1475
function _loadTableVersions() {
1476
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
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;
1483
$this->_gs->_traceStart();
1484
$recordSet = $this->_gs->_db->Execute($query);
1485
$this->_gs->_traceStop();
1487
if (empty($recordSet)) {
1488
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
1489
'Error reading schema table'), null);
1492
$tableVersions = array();
1493
while ($row = $recordSet->FetchRow()) {
1494
$tableVersions[$row[0]] = array($row[1], $row[2]);
1497
return array(null, $tableVersions);
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.
1504
* @return object GalleryStatus a status code
1507
function _executeSql($buffer) {
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.
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);
1518
/* Perform database specific replacements */
1519
foreach ($this->_gs->_getSqlReplacements() as $key => $value) {
1520
$query = str_replace($key, $value, $query);
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");
1537
* @see GalleryStorage::_executeSqlFile
1539
function executeSqlFile($fileName) {
1541
$platform =& $gallery->getPlatform();
1543
if (!$platform->file_exists($fileName)) {
1544
return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
1545
"File $fileName does not exist");
1548
if (($buffer = $platform->file_get_contents($fileName)) === false) {
1549
return GalleryCoreApi::error(ERROR_BAD_PATH, __FILE__, __LINE__,
1550
"Unable to read file $fileName");
1553
$ret = $this->_dbInit(true);
1555
return $ret->wrap(__FILE__, __LINE__);
1558
$ret = $this->_executeSql($buffer);
1560
return $ret->wrap(__FILE__, __LINE__);
1567
* @see GalleryStorage::cleanStore
1569
function cleanStore() {
1571
$gallery->guaranteeTimeLimit(20);
1573
$ret = $this->_dbInit(true);
1575
return $ret->wrap(__FILE__, __LINE__);
1578
/* Get the metabase info about this database */
1579
$this->_gs->_traceStart();
1580
$metatables = $this->_gs->_db->MetaTables();
1581
$this->_gs->_traceStop();
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.
1589
for ($i = 0; $i < count($metatables); $i++) {
1590
$metatables[$i] = strtolower($metatables[$i]);
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();
1599
return $ret->wrap(__FILE__, __LINE__);
1602
foreach (array_keys($tableVersions) as $rawTableName) {
1603
list ($tableName, $unused) = $this->_gs->_translateTableName($rawTableName);
1604
$query = sprintf('DROP TABLE %s', $tableName);
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__);
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__);
1629
* @see GalleryStorage::getProfilingHtml
1631
function getProfilingHtml() {
1632
if (!isset($this->_gs->_db)) {
1635
$this->_gs->_traceStart();
1636
$perf =& NewPerfMonitor($this->_gs->_db);
1637
$buf = $perf->SuspiciousSQL();
1638
$buf .= $perf->ExpensiveSQL();
1639
$this->_gs->_traceStop();
1644
* @see GalleryStorage::isInstalled
1646
function isInstalled() {
1647
$ret = $this->_dbInit();
1649
return array($ret->wrap(__FILE__, __LINE__), null);
1652
/* Get the metabase info about this database */
1653
$this->_gs->_traceStart();
1654
$metatables = $this->_gs->_db->MetaTables();
1655
$this->_gs->_traceStop();
1657
list ($schemaTableName) = $this->_gs->_translateTableName('Schema');
1658
$isInstalled = preg_match("/\b$schemaTableName\b/i", implode(' ', $metatables));
1659
return array(null, $isInstalled);
1663
* @see GalleryStorage::optimize
1665
function optimize() {
1666
$ret = $this->_dbInit();
1668
return $ret->wrap(__FILE__, __LINE__);
1671
/* Load all table versions */
1672
list ($ret, $tableVersions) = $this->_loadTableVersions();
1674
return $ret->wrap(__FILE__, __LINE__);
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();
1686
return GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__);
1695
* @see GalleryStorage::getAffectedRows
1697
function getAffectedRows() {
1698
$ret = $this->_dbInit(true);
1700
return array($ret->wrap(__FILE__, __LINE__), null);
1703
return array(null, $this->_gs->_db->Affected_Rows());
1707
* Internal function to get clearance to acquire locks
1709
* Request clearance to acquire locks and then wait until it's our turn.
1711
* @param int the time to stop trying to get clearance
1712
* @return object GalleryStatus a status code
1714
function _getLockClearance($cutoffTime) {
1715
/* Get the true name of the lock table */
1716
list ($lockTable, $unused) = $this->_gs->_translateTableName('Lock');
1718
/* Acquire a non-transactional connection to use for this request */
1719
list ($ret, $db) = $this->_getNonTransactionalDatabaseConnection();
1721
return array($ret->wrap(__FILE__, __LINE__), null);
1724
/* Get a new lock id */
1725
$this->_gs->_traceStart();
1726
$lockId = $db->GenId($this->_gs->_tablePrefix . DATABASE_SEQUENCE_LOCK);
1727
$this->_gs->_traceStop();
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);
1737
$this->_gs->_traceStart();
1738
$recordSet = $db->Execute($query, $data);
1739
$this->_gs->_traceStop();
1741
$this->releaseLocks($lockId);
1742
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__), null);
1745
/* Wait till it's our turn */
1747
$query = 'SELECT ' . $lockIdCol . ' FROM ' . $lockTable
1748
. ' WHERE ' . $requestCol . '=1 ORDER BY ' . $lockIdCol . ' ASC';
1750
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
1751
$this->_gs->_traceStart();
1752
$recordSet = $db->SelectLimit($query, 1);
1753
$this->_gs->_traceStop();
1755
$this->releaseLocks($lockId);
1756
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
1760
$row = $recordSet->FetchRow();
1761
if ($row[0] == $lockId) {
1765
/* Wait a second and try again */
1768
/* Expire any bogus locks */
1769
$ret = $this->_expireLocks();
1771
return array($ret->wrap(__FILE__, __LINE__), null);
1774
if (time() > $cutoffTime) {
1775
$this->releaseLocks($lockId);
1776
return array(GalleryCoreApi::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__), null);
1780
return array(null, $lockId);
1784
* Identify the type of entity associated with the id provided
1786
* @param int a object id
1787
* @return array a GalleryStatus and a string class name
1789
function _identifyEntities($ids) {
1790
assert('!empty($ids)');
1792
if (!is_array($ids)) {
1794
$returnArray = false;
1796
$returnArray = true;
1799
$checkIds = array();
1800
foreach ($ids as $id) {
1801
if (!GalleryDataCache::containsKey("GalleryStorage::_identifyEntities($id)")) {
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 . ')';
1815
$GLOBALS['ADODB_FETCH_MODE'] = ADODB_FETCH_NUM;
1817
$this->_gs->_traceStart();
1818
$recordSet = $this->_gs->_db->Execute($query, $checkIds);
1819
$this->_gs->_traceStop();
1822
while ($row = $recordSet->FetchRow()) {
1823
if (empty($row[1])) {
1825
GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__), null);
1828
* Save a copy locally, in case the global cache is disabled
1829
* (like in the upgrader)
1831
$local[$row[0]] = $row[1];
1832
GalleryDataCache::put("GalleryStorage::_identifyEntities($row[0])",
1837
return array(GalleryCoreApi::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__),
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)");
1851
return array(GalleryCoreApi::error(ERROR_MISSING_OBJECT, __FILE__, __LINE__,
1852
"Missing object for $id"), null);
1856
$results = GalleryDataCache::get("GalleryStorage::_identifyEntities($ids[0])");
1859
return array(null, $results);
1863
* Describe the members, modules and parent of an entity
1865
* @param string a class name
1866
* @param boolean true if we should scan all modules, not just the active ones
1868
* @return array object GalleryStatus a status code
1869
* entity associative array
1871
function describeEntity($entityName, $tryAllModules=false) {
1874
/* Note: keep these cache keys in sync with _clearEntityAndMapCache() */
1875
$cacheKey = "GalleryStorage::describeEntity()";
1876
$cacheParams = array('type' => 'module',
1877
'itemId' => 'GalleryStorage_describeEntity',
1880
/* We only cache the results for active modules */
1881
if (!$tryAllModules) {
1882
if (!GalleryDataCache::containsKey($cacheKey)) {
1883
$entityInfo = GalleryDataCache::getFromDisk($cacheParams);
1885
$entityInfo = GalleryDataCache::get($cacheKey);
1889
if (!isset($entityInfo)) {
1890
list ($ret, $moduleStatus) = GalleryCoreApi::fetchPluginStatus('module');
1892
return array($ret->wrap(__FILE__, __LINE__), null);
1895
$entityInfo = array();
1896
foreach ($moduleStatus as $moduleId => $moduleInfo) {
1897
if (!$tryAllModules && empty($moduleInfo['active'])) {
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.
1906
$moduleDir = GalleryCoreApi::getPluginBaseDir('module', $moduleId);
1908
return array($ret->wrap(__FILE__, __LINE__), null);
1911
$entitiesFile = "$moduleDir/modules/$moduleId/classes/Entities.inc";
1912
if (file_exists($entitiesFile)) {
1913
include($entitiesFile);
1917
if (!$tryAllModules) {
1918
GalleryDataCache::putToDisk($cacheParams, $entityInfo);
1919
GalleryDataCache::put($cacheKey, $entityInfo);
1923
/* Fall back to all available modules */
1924
if (!$tryAllModules && !isset($entityInfo[$entityName])) {
1925
list ($ret, $entityInfo) = $this->describeEntity($entityName, true);
1927
return array($ret->wrap(__FILE__, __LINE__), null);
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.
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');
1951
if (!isset($entityInfo[$entityName])) {
1952
return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER, __FILE__, __LINE__,
1953
"Unknown entity type: $entityName"), null);
1956
return array(null, $entityInfo);
1960
* Clear out the entity and map caches, which we should do any time that we add or
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',
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',