diff --git a/src/applications/phragment/controller/PhragmentBrowseController.php b/src/applications/phragment/controller/PhragmentBrowseController.php index adb4c0d79..852f1e864 100644 --- a/src/applications/phragment/controller/PhragmentBrowseController.php +++ b/src/applications/phragment/controller/PhragmentBrowseController.php @@ -1,85 +1,89 @@ <?php final class PhragmentBrowseController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $current = nonempty(last($parents), null); $path = ''; if ($current !== null) { $path = $current->getPath(); } $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Fragment')) ->setHref($this->getApplicationURI('/create/'.$path)) ->setIcon('create')); $current_box = $this->createCurrentFragmentView($current, false); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); $fragments = null; if ($current === null) { // Find all root fragments. $fragments = id(new PhragmentFragmentQuery()) ->setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withDepths(array(1)) ->execute(); } else { // Find all child fragments. $fragments = id(new PhragmentFragmentQuery()) ->setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withLeadingPath($current->getPath().'/') ->withDepths(array($current->getDepth() + 1)) ->execute(); } foreach ($fragments as $fragment) { $item = id(new PHUIObjectItemView()); $item->setHeader($fragment->getName()); $item->setHref($this->getApplicationURI('/browse/'.$fragment->getPath())); if (!$fragment->isDirectory()) { $item->addAttribute(pht( 'Last Updated %s', phabricator_datetime( $fragment->getLatestVersion()->getDateCreated(), $viewer))); $item->addAttribute(pht( 'Latest Version %s', $fragment->getLatestVersion()->getSequence())); + if ($fragment->isDeleted()) { + $item->setDisabled(true); + $item->addAttribute(pht('Deleted')); + } } else { $item->addAttribute('Directory'); } $list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $current_box, $list), array( 'title' => pht('Browse Fragments'), 'device' => true)); } } diff --git a/src/applications/phragment/controller/PhragmentController.php b/src/applications/phragment/controller/PhragmentController.php index d156745f4..ac5d43c03 100644 --- a/src/applications/phragment/controller/PhragmentController.php +++ b/src/applications/phragment/controller/PhragmentController.php @@ -1,154 +1,160 @@ <?php abstract class PhragmentController extends PhabricatorController { protected function loadParentFragments($path) { $components = explode('/', $path); $combinations = array(); $current = ''; foreach ($components as $component) { $current .= '/'.$component; $current = trim($current, '/'); if (trim($current) === '') { continue; } $combinations[] = $current; } $fragments = array(); $results = id(new PhragmentFragmentQuery()) ->setViewer($this->getRequest()->getUser()) ->needLatestVersion(true) ->withPaths($combinations) ->execute(); foreach ($combinations as $combination) { $found = false; foreach ($results as $fragment) { if ($fragment->getPath() === $combination) { $fragments[] = $fragment; $found = true; break; } } if (!$found) { return null; } } return $fragments; } protected function buildApplicationCrumbsWithPath(array $fragments) { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('/') ->setHref('/phragment/')); foreach ($fragments as $parent) { $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($parent->getName()) ->setHref('/phragment/browse/'.$parent->getPath())); } return $crumbs; } protected function createCurrentFragmentView($fragment, $is_history_view) { if ($fragment === null) { return null; } $viewer = $this->getRequest()->getUser(); $phids = array(); $phids[] = $fragment->getLatestVersionPHID(); $this->loadHandles($phids); $file = null; $file_uri = null; if (!$fragment->isDirectory()) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($fragment->getLatestVersion()->getFilePHID())) ->executeOne(); if ($file !== null) { $file_uri = $file->getBestURI(); } } $header = id(new PHUIHeaderView()) ->setHeader($fragment->getName()) ->setPolicyObject($fragment) ->setUser($viewer); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($fragment) ->setObjectURI($fragment->getURI()); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Fragment')) ->setHref($file_uri) ->setDisabled($file === null) ->setIcon('download')); $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Contents as ZIP')) ->setHref($this->getApplicationURI("zip/".$fragment->getPath())) ->setDisabled(false) // TODO: Policy ->setIcon('zip')); if (!$fragment->isDirectory()) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Update Fragment')) ->setHref($this->getApplicationURI("update/".$fragment->getPath())) ->setDisabled(false) // TODO: Policy ->setIcon('edit')); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Convert to File')) ->setHref($this->getApplicationURI("update/".$fragment->getPath())) ->setDisabled(false) // TODO: Policy ->setIcon('edit')); } if ($is_history_view) { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('View Child Fragments')) ->setHref($this->getApplicationURI("browse/".$fragment->getPath())) ->setIcon('browse')); } else { $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setHref($this->getApplicationURI("history/".$fragment->getPath())) ->setIcon('history')); } $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($fragment) ->setActionList($actions); if (!$fragment->isDirectory()) { - $properties->addProperty( - pht('Type'), - pht('File')); + if ($fragment->isDeleted()) { + $properties->addProperty( + pht('Type'), + pht('File (Deleted)')); + } else { + $properties->addProperty( + pht('Type'), + pht('File')); + } $properties->addProperty( pht('Latest Version'), $this->renderHandlesForPHIDs(array($fragment->getLatestVersionPHID()))); } else { $properties->addProperty( pht('Type'), pht('Directory')); } return id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); } } diff --git a/src/applications/phragment/controller/PhragmentHistoryController.php b/src/applications/phragment/controller/PhragmentHistoryController.php index 4ca50c0f3..53cac0c36 100644 --- a/src/applications/phragment/controller/PhragmentHistoryController.php +++ b/src/applications/phragment/controller/PhragmentHistoryController.php @@ -1,78 +1,83 @@ <?php final class PhragmentHistoryController extends PhragmentController { private $dblob; public function willProcessRequest(array $data) { $this->dblob = idx($data, "dblob", ""); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $current = idx($parents, count($parents) - 1, null); $path = $current->getPath(); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Fragment')) ->setHref($this->getApplicationURI('/create/'.$path)) ->setIcon('create')); $current_box = $this->createCurrentFragmentView($current, true); $versions = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($current->getPHID())) ->execute(); $list = id(new PHUIObjectItemListView()) ->setUser($viewer); $file_phids = mpull($versions, 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($versions as $version) { $item = id(new PHUIObjectItemView()); $item->setHeader('Version '.$version->getSequence()); $item->setHref($version->getURI()); $item->addAttribute(phabricator_datetime( $version->getDateCreated(), $viewer)); + if ($version->getFilePHID() === null) { + $item->setDisabled(true); + $item->addAttribute('Deletion'); + } + $disabled = !isset($files[$version->getFilePHID()]); $action = id(new PHUIListItemView()) ->setIcon('download') ->setDisabled($disabled) ->setRenderNameAsTooltip(true) ->setName(pht("Download")); if (!$disabled) { $action->setHref($files[$version->getFilePHID()]->getBestURI()); } $item->addAction($action); $list->addItem($item); } return $this->buildApplicationPage( array( $crumbs, $current_box, $list), array( 'title' => pht('Fragment History'), 'device' => true)); } } diff --git a/src/applications/phragment/storage/PhragmentFragment.php b/src/applications/phragment/storage/PhragmentFragment.php index 87caf483e..446c306ac 100644 --- a/src/applications/phragment/storage/PhragmentFragment.php +++ b/src/applications/phragment/storage/PhragmentFragment.php @@ -1,302 +1,306 @@ <?php final class PhragmentFragment extends PhragmentDAO implements PhabricatorPolicyInterface { protected $path; protected $depth; protected $latestVersionPHID; protected $viewPolicy; protected $editPolicy; private $latestVersion = self::ATTACHABLE; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhragmentPHIDTypeFragment::TYPECONST); } public function getURI() { return '/phragment/fragment/'.$this->getID().'/'; } public function getName() { return basename($this->path); } public function getFile() { return $this->assertAttached($this->file); } public function attachFile(PhabricatorFile $file) { return $this->file = $file; } public function isDirectory() { return $this->latestVersionPHID === null; } + public function isDeleted() { + return $this->getLatestVersion()->getFilePHID() === null; + } + public function getLatestVersion() { if ($this->latestVersionPHID === null) { return null; } return $this->assertAttached($this->latestVersion); } public function attachLatestVersion(PhragmentFragmentVersion $version) { return $this->latestVersion = $version; } /* -( Updating ) --------------------------------------------------------- */ /** * Create a new fragment from a file. */ public static function createFromFile( PhabricatorUser $viewer, PhabricatorFile $file = null, $path, $view_policy, $edit_policy) { $fragment = id(new PhragmentFragment()); $fragment->setPath($path); $fragment->setDepth(count(explode('/', $path))); $fragment->setLatestVersionPHID(null); $fragment->setViewPolicy($view_policy); $fragment->setEditPolicy($edit_policy); $fragment->save(); // Directory fragments have no versions associated with them, so we // just return the fragment at this point. if ($file === null) { return $fragment; } if ($file->getMimeType() === "application/zip") { $fragment->updateFromZIP($viewer, $file); } else { $fragment->updateFromFile($viewer, $file); } return $fragment; } /** * Set the specified file as the next version for the fragment. */ public function updateFromFile( PhabricatorUser $viewer, PhabricatorFile $file) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID($file->getPHID()); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); } /** * Apply the specified ZIP archive onto the fragment, removing * and creating fragments as needed. */ public function updateFromZIP( PhabricatorUser $viewer, PhabricatorFile $file) { if ($file->getMimeType() !== "application/zip") { throw new Exception("File must have mimetype 'application/zip'"); } // First apply the ZIP as normal. $this->updateFromFile($viewer, $file); // Ensure we have ZIP support. $zip = null; try { $zip = new ZipArchive(); } catch (Exception $e) { // The server doesn't have php5-zip, so we can't do recursive updates. return; } $temp = new TempFile(); Filesystem::writeFile($temp, $file->loadFileData()); if (!$zip->open($temp)) { throw new Exception("Unable to open ZIP"); } // Get all of the paths and their data from the ZIP. $mappings = array(); for ($i = 0; $i < $zip->numFiles; $i++) { $path = trim($zip->getNameIndex($i), '/'); $stream = $zip->getStream($path); $data = null; // If the stream is false, then it is a directory entry. We leave // $data set to null for directories so we know not to create a // version entry for them. if ($stream !== false) { $data = stream_get_contents($stream); fclose($stream); } $mappings[$path] = $data; } // We need to detect any directories that are in the ZIP folder that // aren't explicitly noted in the ZIP. This can happen if the file // entries in the ZIP look like: // // * something/blah.png // * something/other.png // * test.png // // Where there is no explicit "something/" entry. foreach ($mappings as $path_key => $data) { if ($data === null) { continue; } $directory = dirname($path_key); while ($directory !== ".") { if (!array_key_exists($directory, $mappings)) { $mappings[$directory] = null; } if (dirname($directory) === $directory) { // dirname() will not reduce this directory any further; to // prevent infinite loop we just break out here. break; } $directory = dirname($directory); } } // Adjust the paths relative to this fragment so we can look existing // fragments up in the DB. $base_path = $this->getPath(); $paths = array(); foreach ($mappings as $p => $data) { $paths[] = $base_path.'/'.$p; } // FIXME: What happens when a child exists, but the current user // can't see it. We're going to create a new child with the exact // same path and then bad things will happen. $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($this->getPath().'/') ->execute(); $children = mpull($children, null, 'getPath'); // Iterate over the existing fragments. foreach ($children as $full_path => $child) { $path = substr($full_path, strlen($base_path) + 1); if (array_key_exists($path, $mappings)) { if ($child->isDirectory() && $mappings[$path] === null) { // Don't create a version entry for a directory // (unless it's been converted into a file). continue; } // The file is being updated. $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); $child->updateFromFile($viewer, $file); } else { // The file is being deleted. $child->deleteFile($viewer); } } // Iterate over the mappings to find new files. foreach ($mappings as $path => $data) { if (!array_key_exists($base_path.'/'.$path, $children)) { // The file is being created. If the data is null, // then this is explicitly a directory being created. $file = null; if ($mappings[$path] !== null) { $file = PhabricatorFile::newFromFileData( $mappings[$path], array('name' => basename($path))); } PhragmentFragment::createFromFile( $viewer, $file, $base_path.'/'.$path, $this->getViewPolicy(), $this->getEditPolicy()); } } } /** * Delete the contents of the specified fragment. */ public function deleteFile(PhabricatorUser $viewer) { $existing = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withFragmentPHIDs(array($this->getPHID())) ->execute(); $sequence = count($existing); $this->openTransaction(); $version = id(new PhragmentFragmentVersion()); $version->setSequence($sequence); $version->setFragmentPHID($this->getPHID()); $version->setFilePHID(null); $version->save(); $this->setLatestVersionPHID($version->getPHID()); $this->save(); $this->saveTransaction(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } }