* @version $Revision: 17589 $ */ class ItemAddController extends GalleryController { /** * ItemAddOption instances to use when handling this request. Only used by test code. * * @var array (optionId => ItemAddOption) $_optionInstances * @access private */ var $_optionInstances; /** * Tests can use this method to hardwire a specific set of option instances to use. * This avoids situations where some of the option instances will do unpredictable * things and derail the tests. * * @param array $optionInstances (optionId => ItemAddOption, ...) */ function setOptionInstances($optionInstances) { $this->_optionInstances = $optionInstances; } /** * @see GalleryController::handleRequest */ function handleRequest($form) { global $gallery; $templateAdapter =& $gallery->getTemplateAdapter(); $addPluginId = GalleryUtilities::getRequestVariables('addPlugin'); /** * Special case for backwards-compatibility with the webdav module * @todo Remove on next major API change */ if ($addPluginId == 'ItemAddWebDav') { /* WebDAV used to do a static ::handleRequest() call. We need an instance. */ $controller = new ItemAddController(); } else { $controller = $this; } list ($ret, $item) = $controller->getItem(); if ($ret) { return array($ret, null); } $itemId = $item->getId(); /* Make sure we have permission to add to this item */ $ret = GalleryCoreApi::assertHasItemPermission($itemId, 'core.addDataItem'); if ($ret) { return array($ret, null); } if (!$item->getCanContainChildren()) { return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER), null); } /* Load the correct add plugin */ list ($ret, $addPlugin) = GalleryCoreApi::newFactoryInstanceById('ItemAddPlugin', $addPluginId); if ($ret) { return array($ret, null); } if (!isset($addPlugin)) { return array(GalleryCoreApi::error(ERROR_BAD_PARAMETER), null); } /** * Special case for backwards-compatibility with the webdav module * @todo Remove on next major API change */ if ($addPluginId == 'ItemAddWebDav') { /* Don't output any HTML (progress-bar) */ ob_start(); $ret = $controller->handleRequestWithProgressBar($form, $item, $addPlugin); ob_end_clean(); if ($ret) { return array($ret, null); } $session =& $gallery->getSession(); $results['status'] = $session->getStatus(); $results['redirect'] = array('view' => 'core.ItemAdmin', 'subView' => 'core.ItemAddConfirmation', 'itemId' => $item->getId()); $results['error'] = array(); return array(null, $results); } /* Do the actual work in callback of a progress-bar view */ $templateAdapter->registerTrailerCallback( array($this, 'handleRequestWithProgressBar'), array($form, $item, $addPlugin)); return array(null, array('delegate' => array('view' => 'core.ProgressBar'), 'status' => array(), 'error' => array())); } /** * Handles the add item request and is expected to be called as a progress-bar callback. * @param array $form * @param GalleryItem $item The container to which we're adding child-items * @param ItemAddPlugin $addPlugin The plugin that handles this add item request * @return GalleryStatus */ function handleRequestWithProgressBar($form, $item, $addPlugin) { global $gallery; $templateAdapter =& $gallery->getTemplateAdapter(); $urlGenerator =& $gallery->getUrlGenerator(); $phpVm = $gallery->getPhpVm(); $session =& $gallery->getSession(); $startTime = $phpVm->time(); /* Auto-redirect if we complete the request within this period. Else show the continueURL */ $autoRedirectSeconds = 15; list ($ret, $this->_coreModule) = GalleryCoreApi::loadPlugin('module', 'core'); if ($ret) { return $ret; } $templateAdapter->updateProgressBar($this->_coreModule->translate('Adding items'), '', 0); $error = array(); $addPluginId = GalleryUtilities::getRequestVariables('addPlugin'); list ($ret, $lockId) = GalleryCoreApi::acquireReadLock($item->getId()); if ($ret) { return $ret; } /* Start the add process */ list ($ret, $error, $status) = $addPlugin->handleRequest($form, $item, $this); if ($ret) { GalleryCoreApi::releaseLocks($lockId); return $ret; } if (empty($error)) { list ($ret, $error) = $this->postprocessItems($form, $status); if ($ret) { GalleryCoreApi::releaseLocks($lockId); return $ret; } } $ret = GalleryCoreApi::releaseLocks($lockId); if ($ret) { return $ret; } if (!empty($error)) { /** @todo Should we remove all added items in case of a late form validation error? */ if (!empty($status['addedFiles'])) { $error[] = 'form[error][itemsAddedDespiteFormErrors]'; } $session->put('itemAdd.error', $error); $doRedirect = true; $continueUrl = $urlGenerator->generateUrl( array('view' => 'core.ItemAdmin', 'subView' => 'core.ItemAdd', 'addPlugin' => $addPluginId, 'itemId' => $item->getId()), array('forceFullUrl' => true)); } else { $session->putStatus($status); $doRedirect = ($phpVm->time() - $startTime) <= $autoRedirectSeconds; if (empty($status['addedFiles'])) { /* * Append all form parameters for the next view request. Some plugins submit a * first form to the controller only to forward the request to the view which might * depend on the same form parameters. */ $continueUrl = $urlGenerator->generateUrl( array('view' => 'core.ItemAdmin', 'subView' => 'core.ItemAdd', 'addPlugin' => $addPluginId, 'itemId' => $item->getId(), 'form' => $form), array('forceFullUrl' => true)); $templateAdapter->updateProgressBar( $this->_coreModule->translate('Adding items'), '', 1); } else { $continueUrl = $urlGenerator->generateUrl( array('view' => 'core.ItemAdmin', 'subView' => 'core.ItemAddConfirmation', 'itemId' => $item->getId()), array('forceFullUrl' => true)); } } $templateAdapter->completeProgressBar($continueUrl, $doRedirect); return null; } /** * Do post-processing which includes extracting archive-items and letting all ItemAddOption * instances handle the added items. * * If called from an ItemAddPlugin, the plugin should stop adding items if $error is non-empty. * * @param array $form * @param array $status An array including the list of all added items * @see ItemAddPlugin::handleRequest() for the structure of the $status array * @return array GalleryStatus * array $error request parameter errors */ function postprocessItems($form, &$status) { global $gallery; $this->_templateAdapter =& $gallery->getTemplateAdapter(); $this->_storage =& $gallery->getStorage(); if (!isset($this->_coreModule)) { list ($ret, $this->_coreModule) = GalleryCoreApi::loadPlugin('module', 'core'); if ($ret) { return array($ret, null); } } $this->_processingItemsMessage = $this->_coreModule->translate('Processing items'); if (!isset($this->_optionInstances)) { list ($ret, $this->_optionInstances) = ItemAddOption::getAllAddOptions(); if ($ret) { return array($ret, null); } } if (!isset($this->_extractionToolkitMap)) { list ($ret, $extractToolkits) = GalleryCoreApi::getToolkitOperationMimeTypes('extract'); if ($ret) { return array($ret, null); } $this->_extractionToolkitMap = array(); foreach ($extractToolkits as $mimeType => $toolkitList) { if (!empty($toolkitList)) { list ($ret, $this->_extractionToolkitMap[$mimeType]) = GalleryCoreApi::getToolkitByOperation($mimeType, 'extract'); if ($ret) { return array($ret, null); } } } } if (empty($status['addedFiles']) || empty($this->_extractionToolkitMap) && empty($this->_optionInstances)) { /* Nothing to do */ return array(null, array()); } $this->_templateAdapter->updateProgressBar($this->_processingItemsMessage, '', 0); $ret = $this->_storage->checkPoint(); if ($ret) { return array($ret, null); } $gallery->guaranteeTimeLimit(60); if (!isset($this->_processedItems)) { $this->_processedItems = array(); } $errors = array(); /* The number of items a ItemAddOption should be able to process in less than 30 seconds */ $batchSize = 20; /* Extract all archive-type items and call the ItemAddOption instances for postprocessing */ $itemsToProcess = $itemsToProcessKeyMap = array(); $i = 0; do { $file =& $status['addedFiles'][$i]; if (empty($file['id']) || isset($this->_processedItems[$i])) { /* We couldn't add this file for whatever reason or it has been processed already */ continue; } list ($ret, $addedItem) = GalleryCoreApi::loadEntitiesById($file['id'], 'GalleryItem'); if ($ret) { return array($ret, null); } /* Check if we should extract individual files out of an archive */ if (GalleryUtilities::isA($addedItem, 'GalleryDataItem') && isset($this->_extractionToolkitMap[$addedItem->getMimeType()])) { list ($ret, $extractedItems) = $this->_extractAndAddFiles( $addedItem, $this->_extractionToolkitMap[$addedItem->getMimeType()]); if ($ret) { return array($ret, null); } $ret = GalleryCoreApi::deleteEntityById($addedItem->getId(), 'GalleryItem'); if ($ret) { return array($ret, null); } /* * Remove this element from the status and use array_merge to append the extracted * items and to reindex the whole array to fill the gap we just created. */ unset($status['addedFiles'][$i--]); $status['addedFiles'] = array_merge($status['addedFiles'], $extractedItems); $gallery->guaranteeTimeLimit(30); $ret = $this->_storage->checkPoint(); if ($ret) { return array($ret, null); } } else { /* This is not an archive, add it to our array of item objects */ $itemsToProcess[] = $addedItem; /* * We can't index $itemsToProcess directly by $i because some options expect it to * be indexed from 0..n without holes. */ $itemsToProcessKeyMap[] = $i; } if (count($itemsToProcess) % $batchSize == 0 || !isset($status['addedFiles'][$i+1]) && count($itemsToProcess)) { /* Allow ItemAddOptions to process added item(s) */ $optionNumber = 0; foreach ($this->_optionInstances as $option) { $this->_templateAdapter->updateProgressBar( $this->_processingItemsMessage, '', $optionNumber++ / count($this->_optionInstances)); $gallery->guaranteeTimeLimit(60); list ($ret, $optionErrors, $optionWarnings) = $option->handleRequestAfterAdd($form, $itemsToProcess); if ($ret) { return array($ret, null); } $errors = array_merge($errors, $optionErrors); /* For each item, put the items warnings into our status array */ foreach ($optionWarnings as $j => $messages) { $key = $itemsToProcessKeyMap[$j]; if (!isset($status['addedFiles'][$key]['warnings'])) { $status['addedFiles'][$key]['warnings'] = array(); } $status['addedFiles'][$key]['warnings'] = array_merge($status['addedFiles'][$key]['warnings'], $messages); } $ret = $this->_storage->checkPoint(); if ($ret) { return array($ret, null); } } foreach ($itemsToProcessKeyMap as $j) { $this->_processedItems[$j] = 1; } $itemsToProcess = $itemsToProcessKeyMap = array(); $gallery->guaranteeTimeLimit(60); $this->_templateAdapter->updateProgressBar($this->_processingItemsMessage, '', 1); } } while (isset($status['addedFiles'][++$i])); $this->_templateAdapter->updateProgressBar($this->_processingItemsMessage, '', 1); return array(null, $errors); } /** * Extract files from an archive item and add new items to the same album. * @param GalleryDataItem $archiveItem archive * @param GalleryToolkit $toolkit toolkit that supports extract operation * @return array GalleryStatus a status code * array of array('fileName' => '..', 'id' => ##, 'warnings' => array of string) * @access private */ function _extractAndAddFiles($archiveItem, $toolkit) { global $gallery; $this->_platform =& $gallery->getPlatform(); $this->_extractingArchiveMessage = $this->_coreModule->translate('Extracting archive'); $this->_templateAdapter->updateProgressBar($this->_extractingArchiveMessage, '', 0); $gallery->guaranteeTimeLimit(120); $parentId = $archiveItem->getParentId(); list ($ret, $hasAddAlbumPermission) = GalleryCoreApi::hasItemPermission($parentId, 'core.addAlbumItem'); if ($ret) { return array($ret, null); } list ($ret, $file) = $archiveItem->fetchPath(); if ($ret) { return array($ret, null); } $base = $this->_platform->tempnam($gallery->getConfig('data.gallery.tmp'), 'tmp_'); $tmpDir = $base . '.dir'; if (!$this->_platform->mkdir($tmpDir)) { return array(GalleryCoreApi::error(ERROR_PLATFORM_FAILURE), null); } list ($ret) = $toolkit->performOperation($archiveItem->getMimeType(), 'extract', $file, $tmpDir, array()); if ($ret) { @$this->_platform->recursiveRmdir($tmpDir); @$this->_platform->unlink($base); return array($ret, null); } /* * If archive title matches the filename or base filename then name new items * with the same strategy; otherwise just use the archive title. */ $archiveTitle = $archiveItem->getTitle(); $archiveName = $archiveItem->getPathComponent(); list ($archiveBase) = GalleryUtilities::getFileNameComponents($archiveName); if ($archiveTitle == $archiveName) { $titleMode = 'file'; } else if ($archiveTitle == $archiveBase) { $titleMode = 'base'; } else { $titleMode = 'archive'; } $this->_templateAdapter->updateProgressBar($this->_extractingArchiveMessage, '', 0.1); $gallery->guaranteeTimeLimit(30); $addedFiles = array(); $ret = $this->_recursiveAddDir( $tmpDir, $parentId, $addedFiles, $archiveItem, $titleMode, $hasAddAlbumPermission); @$this->_platform->recursiveRmdir($tmpDir); @$this->_platform->unlink($base); if ($ret) { return array($ret, null); } $this->_templateAdapter->updateProgressBar($this->_extractingArchiveMessage, '', 1); return array(null, $addedFiles); } /** * Recursively add files from extracted archive. * @return GalleryStatus a status code * @access private */ function _recursiveAddDir($dir, $parentId, &$addedFiles, &$archiveItem, $titleMode, $canAddAlbums) { global $gallery; $list = array(); $dh = $this->_platform->opendir($dir); while (($file = $this->_platform->readdir($dh)) !== false) { if ($file != '.' && $file != '..') { $list[] = $file; } } $this->_platform->closedir($dh); foreach ($list as $filename) { $path = "$dir/$filename"; if ($this->_platform->is_dir($path)) { if ($canAddAlbums) { $title = $filename; GalleryUtilities::sanitizeInputValues($title); list ($ret, $album) = GalleryCoreApi::createAlbum( $parentId, $filename, $title, '', '', ''); if ($ret) { return $ret; } list ($ret, $lockId) = GalleryCoreApi::acquireReadLock($album->getId()); if ($ret) { return $ret; } $ret = $this->_recursiveAddDir($path, $album->getId(), $addedFiles, $archiveItem, $titleMode, $canAddAlbums); if ($ret) { GalleryCoreApi::releaseLocks($lockId); return $ret; } $ret = GalleryCoreApi::releaseLocks($lockId); if ($ret) { return $ret; } $newItem =& $album; } else { /* * Flattening folder structure since we're not allowed to create albums. * Adding files but ignoring directories. */ $ret = $this->_recursiveAddDir( $path, $parentId, $addedFiles, $archiveItem, $titleMode, $canAddAlbums); if ($ret) { return $ret; } $newItem = null; } } else { list ($ret, $mimeType) = GalleryCoreApi::getMimeType($filename); if ($ret) { return $ret; } if ($titleMode == 'file') { $title = $filename; GalleryUtilities::sanitizeInputValues($title); } else if ($titleMode == 'base') { list ($title) = GalleryUtilities::getFileNameComponents($filename); GalleryUtilities::sanitizeInputValues($title); } else { $title = $archiveItem->getTitle(); } list ($ret, $newItem) = GalleryCoreApi::addItemToAlbum( $path, $filename, $title, $archiveItem->getSummary(), $archiveItem->getDescription(), $mimeType, $parentId); if ($ret) { return $ret; } } if ($newItem) { $sanitizedFilename = $filename; GalleryUtilities::sanitizeInputValues($sanitizedFilename); $addedFiles[] = array('fileName' => $sanitizedFilename, 'id' => $newItem->getId(), 'warnings' => array()); } if (count($addedFiles) % 10 == 0) { /* The percentage isn't accurate at all, we just keep the visual feedback going */ $this->_templateAdapter->updateProgressBar($this->_extractingArchiveMessage, '', 0.1 + 0.9 * count($addedFiles) / (count($list) + count($addedFiles))); $gallery->guaranteeTimeLimit(30); $ret = $this->_storage->checkPoint(); if ($ret) { return $ret; } } } return null; } } /** * This view will show the selected plugin for adding items to the gallery */ class ItemAddView extends GalleryView { /** * @see GalleryView::loadTemplate */ function loadTemplate(&$template, &$form) { global $gallery; $session =& $gallery->getSession(); $addPlugin = GalleryUtilities::getRequestVariables('addPlugin'); list ($ret, $item) = $this->getItem(); if ($ret) { return array($ret, null); } $itemId = $item->getId(); /* Make sure we have permission to add to this item */ $ret = GalleryCoreApi::assertHasItemPermission($itemId, 'core.addDataItem'); if ($ret) { return array($ret, null); } list ($ret, $isAdmin) = GalleryCoreApi::isUserInSiteAdminGroup(); if ($ret) { return array($ret, null); } /* Get all the add plugins */ list ($ret, $allPluginIds) = GalleryCoreApi::getAllFactoryImplementationIds('ItemAddPlugin'); if ($ret) { return array($ret, null); } $pluginInstances = array(); foreach (array_keys($allPluginIds) as $pluginId) { list ($ret, $plugin) = GalleryCoreApi::newFactoryInstanceById('ItemAddPlugin', $pluginId); if ($ret) { return array($ret, null); } list ($ret, $isAppropriate) = $plugin->isAppropriate(); if ($ret) { return array($ret, null); } if ($isAppropriate) { $pluginInstances[$pluginId] = $plugin; } } /* Get all the add options */ list ($ret, $optionInstances) = ItemAddOption::getAllAddOptions(); if ($ret) { return array($ret, null); } /* * If the plugin is empty get it from the session. If it's empty there, * default to the first plugin we find. Either way, save the user's * preference in the session. */ $addPluginSessionKey = 'core.view.ItemAdd.addPlugin.' . get_class($item); if (empty($addPlugin) || !isset($pluginInstances[$addPlugin])) { $addPlugin = $session->get($addPluginSessionKey); if (empty($addPlugin) || !isset($pluginInstances[$addPlugin])) { $ids = array_keys($pluginInstances); $addPlugin = $ids[0]; } } $session->put($addPluginSessionKey, $addPlugin); $errors = $session->get('itemAdd.error'); if (!empty($errors)) { $session->remove('itemAdd.error'); /* Same logic as in main.php */ foreach ($errors as $error) { GalleryUtilities::putRequestVariable($error, 1); } } /* Get display data for all plugins */ $plugins = array(); foreach ($pluginInstances as $pluginId => $plugin) { list ($ret, $title) = $plugin->getTitle(); if ($ret) { return array($ret, null); } $plugins[] = array('title' => $title, 'id' => $pluginId, 'isSelected' => ($pluginId == $addPlugin)); } $ItemAdd = array(); $ItemAdd['addPlugin'] = $addPlugin; $ItemAdd['plugins'] = $plugins; $ItemAdd['isAdmin'] = $isAdmin; /* Let the plugin load its template data */ list ($ret, $ItemAdd['pluginFile'], $ItemAdd['pluginL10Domain']) = $pluginInstances[$addPlugin]->loadTemplate($template, $form, $item); if ($ret) { return array($ret, null); } /* Now let all options load their template data */ $ItemAdd['options'] = array(); foreach ($optionInstances as $option) { list ($ret, $entry['file'], $entry['l10Domain']) = $option->loadTemplate($template, $form, $item); if ($ret) { return array($ret, null); } if (!empty($entry['file'])) { $ItemAdd['options'][] = $entry; } } /* Make sure that we've got some toolkits */ list ($ret, $operations) = GalleryCoreApi::getToolkitOperations('image/jpeg'); if ($ret) { return array($ret, null); } $ItemAdd['hasToolkit'] = false; for ($i = 0; $i < sizeof($operations); $i++) { if ($operations[$i]['name'] == 'thumbnail') { $ItemAdd['hasToolkit'] = true; break; } } $template->setVariable('ItemAdd', $ItemAdd); $template->setVariable('controller', 'core.ItemAdd'); return array(null, array('body' => 'modules/core/templates/ItemAdd.tpl')); } /** * @see GalleryView::getViewDescription */ function getViewDescription() { list ($ret, $core) = GalleryCoreApi::loadPlugin('module', 'core'); if ($ret) { return array($ret, null); } return array(null, $core->translate('add items')); } } /** * Interface for plugins to the ItemAdd view and controller. * Plugins provide alternate ways to add items into Gallery. * @abstract */ class ItemAddPlugin { /** * Load the template with data from this plugin * @see GalleryView::loadTemplate * * @param GalleryTemplate $template * @param array $form the form values * @param GalleryItem $item * @return array GalleryStatus a status code * string the path to a template file to include * string localization domain for the template file */ function loadTemplate(&$template, &$form, $item) { return array(GalleryCoreApi::error(ERROR_UNIMPLEMENTED), null, null); } /** * Let the plugin handle the incoming request * @see GalleryController::handleRequest * * @param array $form the form values * @param GalleryItem $item * @param GalleryItemAddController $addController A reference to the ItemAddController * to be used for the post-processing calls. * @return array GalleryStatus a status code * array error messages (request parameter errors). Stop processing on errors. * array status data, 'addedFiles' entry should contain: * array(array('fileName' => '...', 'id' => ##, * 'warnings' => array of strings), ...) */ function handleRequest($form, &$item, &$addController) { return array(GalleryCoreApi::error(ERROR_UNIMPLEMENTED), null, null); } /** * Return a localized title for this plugin, suitable for display to the user * * @return array GalleryStatus a status code * return-array (same as GalleryController::handleRequest) */ function getTitle() { return array(GalleryCoreApi::error(ERROR_UNIMPLEMENTED), null); } /** * Is this plugin appropriate at this time? Default is true. * * @return array GalleryStatus a status code * boolean true or false */ function isAppropriate() { return array(null, true); } } /** * Interface for options to the ItemAdd view and controller. * Options allow us to provide extra UI in the views and extra processing in the controller so * that we can add new functionality like watermarking, quotas, etc to every ItemAddPlugin * @abstract */ class ItemAddOption { /** * Return all the available option plugins * * @return array GalleryStatus a status code * array ItemAddOption instances * @static */ function getAllAddOptions() { /* Get all the option plugins */ list ($ret, $allOptionIds) = GalleryCoreApi::getAllFactoryImplementationIds('ItemAddOption'); if ($ret) { return array($ret, null); } $optionInstances = array(); foreach (array_keys($allOptionIds) as $optionId) { list ($ret, $option) = GalleryCoreApi::newFactoryInstanceById('ItemAddOption', $optionId); if ($ret) { return array($ret, null); } list ($ret, $isAppropriate) = $option->isAppropriate(); if ($ret) { return array($ret, null); } if ($isAppropriate) { $optionInstances[$optionId] = $option; } } return array(null, $optionInstances); } /** * Load the template with data from this plugin * @see GalleryView::loadTemplate * * @param GalleryTemplate $template * @param array $form the form values * @param GalleryItem $item * @return array GalleryStatus a status code * string the path to a template file to include * string localization domain for the template file */ function loadTemplate(&$template, &$form, $item) { return array(null, null, null); } /** * Let the plugin handle the incoming request. We expect the $items to be locked. * @see GalleryController::handleRequest * * @param array $form the form values * @param array $items GalleryDataItems * @return array GalleryStatus a status code * array localized error messages. Attempt to continue processing on errors since * the items have already been added and post-processing will continue. * array localized warning messages */ function handleRequestAfterAdd($form, $items) { return array(GalleryCoreApi::error(ERROR_UNIMPLEMENTED), null, null); } /** * Is this option appropriate at this time? * * @return array GalleryStatus a status code * boolean true or false */ function isAppropriate() { return array(null, false); } } ?>