diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php index 070131b443..f39707618d 100644 --- a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php @@ -1,109 +1,109 @@ getViewer(); $key = $this->newKeyForObjectPHID($request->getStr('objectPHID')); if (!$key) { return new Aphront404Response(); } $cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $cancel_uri); if ($request->isFormPost()) { $default_name = $key->getObject()->getSSHKeyDefaultName(); $keys = PhabricatorSSHKeyGenerator::generateKeypair(); list($public_key, $private_key) = $keys; - $file = PhabricatorFile::buildFromFileDataOrHash( + $file = PhabricatorFile::newFromFileData( $private_key, array( 'name' => $default_name.'.key', 'ttl.relative' => phutil_units('10 minutes in seconds'), 'viewPolicy' => $viewer->getPHID(), )); $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key); $type = $public_key->getType(); $body = $public_key->getBody(); $comment = pht('Generated'); $entire_key = "{$type} {$body} {$comment}"; $type_create = PhabricatorTransactions::TYPE_CREATE; $type_name = PhabricatorAuthSSHKeyTransaction::TYPE_NAME; $type_key = PhabricatorAuthSSHKeyTransaction::TYPE_KEY; $xactions = array(); $xactions[] = id(new PhabricatorAuthSSHKeyTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); $xactions[] = id(new PhabricatorAuthSSHKeyTransaction()) ->setTransactionType($type_name) ->setNewValue($default_name); $xactions[] = id(new PhabricatorAuthSSHKeyTransaction()) ->setTransactionType($type_key) ->setNewValue($entire_key); $editor = id(new PhabricatorAuthSSHKeyEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->applyTransactions($key, $xactions); // NOTE: We're disabling workflow on submit so the download works. We're // disabling workflow on cancel so the page reloads, showing the new // key. return $this->newDialog() ->setTitle(pht('Download Private Key')) ->setDisableWorkflowOnCancel(true) ->setDisableWorkflowOnSubmit(true) ->setSubmitURI($file->getDownloadURI()) ->appendParagraph( pht( 'A keypair has been generated, and the public key has been '. 'added as a recognized key. Use the button below to download '. 'the private key.')) ->appendParagraph( pht( 'After you download the private key, it will be destroyed. '. 'You will not be able to retrieve it if you lose your copy.')) ->addSubmitButton(pht('Download Private Key')) ->addCancelButton($cancel_uri, pht('Done')); } try { PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); return $this->newDialog() ->setTitle(pht('Generate New Keypair')) ->addHiddenInput('objectPHID', $key->getObject()->getPHID()) ->appendParagraph( pht( 'This workflow will generate a new SSH keypair, add the public '. 'key, and let you download the private key.')) ->appendParagraph( pht('Phabricator will not retain a copy of the private key.')) ->addSubmitButton(pht('Generate New Keypair')) ->addCancelButton($cancel_uri); } catch (Exception $ex) { return $this->newDialog() ->setTitle(pht('Unable to Generate Keys')) ->appendParagraph($ex->getMessage()) ->addCancelButton($cancel_uri); } } } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 26ff22838f..6cb39c1510 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1,1109 +1,1109 @@ getViewer(); $this->revisionID = $request->getURIData('id'); $viewer_is_anonymous = !$viewer->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) ->setViewer($viewer) ->needReviewers(true) ->needReviewerAuthority(true) ->executeOne(); if (!$revision) { return new Aphront404Response(); } $diffs = id(new DifferentialDiffQuery()) ->setViewer($viewer) ->withRevisionIDs(array($this->revisionID)) ->execute(); $diffs = array_reverse($diffs, $preserve_keys = true); if (!$diffs) { throw new Exception( pht('This revision has no diffs. Something has gone quite wrong.')); } $revision->attachActiveDiff(last($diffs)); $diff_vs = $request->getInt('vs'); $target_id = $request->getInt('id'); $target = idx($diffs, $target_id, end($diffs)); $target_manual = $target; if (!$target_id) { foreach ($diffs as $diff) { if ($diff->getCreationMethod() != 'commit') { $target_manual = $diff; } } } if (empty($diffs[$diff_vs])) { $diff_vs = null; } $repository = null; $repository_phid = $target->getRepositoryPHID(); if ($repository_phid) { if ($repository_phid == $revision->getRepositoryPHID()) { $repository = $revision->getRepository(); } else { $repository = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); } } list($changesets, $vs_map, $vs_changesets, $rendering_references) = $this->loadChangesetsAndVsMap( $target, idx($diffs, $diff_vs), $repository); if ($request->getExists('download')) { return $this->buildRawDiffResponse( $revision, $changesets, $vs_changesets, $vs_map, $repository); } $map = $vs_map; if (!$map) { $map = array_fill_keys(array_keys($changesets), 0); } $old_ids = array(); $new_ids = array(); foreach ($map as $id => $vs) { if ($vs <= 0) { $old_ids[] = $id; $new_ids[] = $id; } else { $new_ids[] = $id; $new_ids[] = $vs; } } $this->loadDiffProperties($diffs); $props = $target_manual->getDiffProperties(); $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( $revision->getPHID()); $object_phids = array_merge( $revision->getReviewerPHIDs(), $subscriber_phids, $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), $viewer->getPHID(), )); foreach ($revision->getAttached() as $type => $phids) { foreach ($phids as $phid => $info) { $object_phids[] = $phid; } } $field_list = PhabricatorCustomField::getObjectFields( $revision, PhabricatorCustomField::ROLE_VIEW); $field_list->setViewer($viewer); $field_list->readFieldsFromStorage($revision); $warning_handle_map = array(); foreach ($field_list->getFields() as $key => $field) { $req = $field->getRequiredHandlePHIDsForRevisionHeaderWarnings(); foreach ($req as $phid) { $warning_handle_map[$key][] = $phid; $object_phids[] = $phid; } } $handles = $this->loadViewerHandles($object_phids); $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = count($changesets); $warning = new PHUIInfoView(); $warning->setTitle(pht('Very Large Diff')); $warning->setSeverity(PHUIInfoView::SEVERITY_WARNING); $warning->appendChild(hsprintf( '%s %s', pht( 'This diff is very large and affects %s files. '. 'You may load each file individually or ', new PhutilNumber($count)), phutil_tag( 'a', array( 'class' => 'button grey', 'href' => $request_uri ->alter('large', 'true') ->setFragment('toc'), ), pht('Show All Files Inline')))); $warning = $warning->render(); $old = array_select_keys($changesets, $old_ids); $new = array_select_keys($changesets, $new_ids); $query = id(new DifferentialInlineCommentQuery()) ->setViewer($viewer) ->needHidden(true) ->withRevisionPHIDs(array($revision->getPHID())); $inlines = $query->execute(); $inlines = $query->adjustInlinesForChangesets( $inlines, $old, $new, $revision); $visible_changesets = array(); foreach ($inlines as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { $visible_changesets[$changeset_id] = $changesets[$changeset_id]; } } } else { $warning = null; $visible_changesets = $changesets; } $commit_hashes = mpull($diffs, 'getSourceControlBaseRevision'); $local_commits = idx($props, 'local:commits', array()); foreach ($local_commits as $local_commit) { $commit_hashes[] = idx($local_commit, 'tree'); $commit_hashes[] = idx($local_commit, 'local'); } $commit_hashes = array_unique(array_filter($commit_hashes)); if ($commit_hashes) { $commits_for_links = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withIdentifiers($commit_hashes) ->execute(); $commits_for_links = mpull( $commits_for_links, null, 'getCommitIdentifier'); } else { $commits_for_links = array(); } $header = $this->buildHeader($revision); $subheader = $this->buildSubheaderView($revision); $details = $this->buildDetails($revision, $field_list); $curtain = $this->buildCurtain($revision); $whitespace = $request->getStr( 'whitespace', DifferentialChangesetParser::WHITESPACE_IGNORE_MOST); $repository = $revision->getRepository(); if ($repository) { $symbol_indexes = $this->buildSymbolIndexes( $repository, $visible_changesets); } else { $symbol_indexes = array(); } $revision_warnings = $this->buildRevisionWarnings( $revision, $field_list, $warning_handle_map, $handles); $info_view = null; if ($revision_warnings) { $info_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($revision_warnings); } $detail_diffs = array_select_keys( $diffs, array($diff_vs, $target->getID())); $detail_diffs = mpull($detail_diffs, null, 'getPHID'); $this->loadHarbormasterData($detail_diffs); $diff_detail_box = $this->buildDiffDetailView( $detail_diffs, $revision, $field_list); $unit_box = $this->buildUnitMessagesView( $target, $revision); $timeline = $this->buildTransactions( $revision, $diff_vs ? $diffs[$diff_vs] : $target, $target, $old_ids, $new_ids); $timeline->setQuoteRef($revision->getMonogram()); $changeset_view = id(new DifferentialChangesetListView()) ->setChangesets($changesets) ->setVisibleChangesets($visible_changesets) ->setStandaloneURI('/differential/changeset/') ->setRawFileURIs( '/differential/changeset/?view=old', '/differential/changeset/?view=new') ->setUser($viewer) ->setDiff($target) ->setRenderingReferences($rendering_references) ->setVsMap($vs_map) ->setWhitespace($whitespace) ->setSymbolIndexes($symbol_indexes) ->setTitle(pht('Diff %s', $target->getID())) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); if ($repository) { $changeset_view->setRepository($repository); } if (!$viewer_is_anonymous) { $changeset_view->setInlineCommentControllerURI( '/differential/comment/inline/edit/'.$revision->getID().'/'); } $broken_diffs = $this->loadHistoryDiffStatus($diffs); $history = id(new DifferentialRevisionUpdateHistoryView()) ->setUser($viewer) ->setDiffs($diffs) ->setDiffUnitStatuses($broken_diffs) ->setSelectedVersusDiffID($diff_vs) ->setSelectedDiffID($target->getID()) ->setSelectedWhitespace($whitespace) ->setCommitsForLinks($commits_for_links); $local_table = id(new DifferentialLocalCommitsView()) ->setUser($viewer) ->setLocalCommits(idx($props, 'local:commits')) ->setCommitsForLinks($commits_for_links); if ($repository) { $other_revisions = $this->loadOtherRevisions( $changesets, $target, $repository); } else { $other_revisions = array(); } $other_view = null; if ($other_revisions) { $other_view = $this->renderOtherRevisions($other_revisions); } $toc_view = $this->buildTableOfContents( $changesets, $visible_changesets, $target->loadCoverageMap($viewer)); $tab_group = id(new PHUITabGroupView()) ->addTab( id(new PHUITabView()) ->setName(pht('Files')) ->setKey('files') ->appendChild($toc_view)) ->addTab( id(new PHUITabView()) ->setName(pht('History')) ->setKey('history') ->appendChild($history)) ->addTab( id(new PHUITabView()) ->setName(pht('Commits')) ->setKey('commits') ->appendChild($local_table)); $stack_graph = id(new DifferentialRevisionGraph()) ->setViewer($viewer) ->setSeedPHID($revision->getPHID()) ->setLoadEntireGraph(true) ->loadGraph(); if (!$stack_graph->isEmpty()) { $stack_table = $stack_graph->newGraphTable(); $parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; $reachable = $stack_graph->getReachableObjects($parent_type); foreach ($reachable as $key => $reachable_revision) { if ($reachable_revision->isClosed()) { unset($reachable[$key]); } } if ($reachable) { $stack_name = pht('Stack (%s Open)', phutil_count($reachable)); $stack_color = PHUIListItemView::STATUS_FAIL; } else { $stack_name = pht('Stack'); $stack_color = null; } $tab_group->addTab( id(new PHUITabView()) ->setName($stack_name) ->setKey('stack') ->setColor($stack_color) ->appendChild($stack_table)); } if ($other_view) { $tab_group->addTab( id(new PHUITabView()) ->setName(pht('Similar')) ->setKey('similar') ->appendChild($other_view)); } $tab_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Revision Contents')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addTabGroup($tab_group); $signatures = DifferentialRequiredSignaturesField::loadForRevision( $revision); $missing_signatures = false; foreach ($signatures as $phid => $signed) { if (!$signed) { $missing_signatures = true; } } $footer = array(); $signature_message = null; if ($missing_signatures) { $signature_message = id(new PHUIInfoView()) ->setTitle(pht('Content Hidden')) ->appendChild( pht( 'The content of this revision is hidden until the author has '. 'signed all of the required legal agreements.')); } else { $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('toc') ->setNavigationMarker(true); $footer[] = array( $anchor, $warning, $tab_view, $changeset_view, ); } $comment_view = id(new DifferentialRevisionEditEngine()) ->setViewer($viewer) ->buildEditEngineCommentView($revision); $comment_view->setTransactionTimeline($timeline); $review_warnings = array(); foreach ($field_list->getFields() as $field) { $review_warnings[] = $field->getWarningsForDetailView(); } $review_warnings = array_mergev($review_warnings); if ($review_warnings) { $warnings_view = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($review_warnings); $comment_view->setInfoView($warnings_view); } $footer[] = $comment_view; $monogram = $revision->getMonogram(); $operations_box = $this->buildOperationsBox($revision); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($monogram); $crumbs->setBorder(true); $filetree_on = $viewer->compareUserSetting( PhabricatorShowFiletreeSetting::SETTINGKEY, PhabricatorShowFiletreeSetting::VALUE_ENABLE_FILETREE); $nav = null; if ($filetree_on) { $collapsed_key = PhabricatorFiletreeVisibleSetting::SETTINGKEY; $collapsed_value = $viewer->getUserSetting($collapsed_key); $nav = id(new DifferentialChangesetFileTreeSideNavBuilder()) ->setTitle($monogram) ->setBaseURI(new PhutilURI($revision->getURI())) ->setCollapsed((bool)$collapsed_value) ->build($changesets); } Javelin::initBehavior('differential-user-select'); Javelin::initBehavior('differential-keyboard-navigation'); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) ->setCurtain($curtain) ->setMainColumn( array( $operations_box, $info_view, $details, $diff_detail_box, $unit_box, $timeline, $signature_message, )) ->setFooter($footer); $page = $this->newPage() ->setTitle($monogram.' '.$revision->getTitle()) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($revision->getPHID())) ->appendChild($view); if ($nav) { $page->setNavigation($nav); } return $page; } private function buildHeader(DifferentialRevision $revision) { $view = id(new PHUIHeaderView()) ->setHeader($revision->getTitle($revision)) ->setUser($this->getViewer()) ->setPolicyObject($revision) ->setHeaderIcon('fa-cog'); $status = $revision->getStatus(); $status_name = DifferentialRevisionStatus::renderFullDescription($status); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); return $view; } private function buildSubheaderView(DifferentialRevision $revision) { $viewer = $this->getViewer(); $author_phid = $revision->getAuthorPHID(); $author = $viewer->renderHandle($author_phid)->render(); $date = phabricator_datetime($revision->getDateCreated(), $viewer); $author = phutil_tag('strong', array(), $author); $handles = $viewer->loadHandles(array($author_phid)); $image_uri = $handles[$author_phid]->getImageURI(); $image_href = $handles[$author_phid]->getURI(); $content = pht('Authored by %s on %s.', $author, $date); return id(new PHUIHeadThingView()) ->setImage($image_uri) ->setImageHref($image_href) ->setContent($content); } private function buildDetails( DifferentialRevision $revision, $custom_fields) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer); if ($custom_fields) { $custom_fields->appendFieldsToPropertyList( $revision, $viewer, $properties); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Details')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); } private function buildCurtain(DifferentialRevision $revision) { $viewer = $this->getViewer(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $curtain = $this->newCurtainView($revision); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $revision, PhabricatorPolicyCapability::CAN_EDIT); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref("/differential/revision/edit/{$revision_id}/") ->setName(pht('Edit Revision')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-upload') ->setHref("/differential/revision/update/{$revision_id}/") ->setName(pht('Update Diff')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $request_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-download') ->setName(pht('Download Raw Diff')) ->setHref($request_uri->alter('download', 'true'))); $relationship_list = PhabricatorObjectRelationshipList::newForObject( $viewer, $revision); $revision_actions = array( DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY, DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY, ); $revision_submenu = $relationship_list->newActionSubmenu($revision_actions) ->setName(pht('Edit Related Revisions...')) ->setIcon('fa-cog'); $curtain->addAction($revision_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { $curtain->addAction($relationship_submenu); } return $curtain; } private function loadHistoryDiffStatus(array $diffs) { assert_instances_of($diffs, 'DifferentialDiff'); $diff_phids = mpull($diffs, 'getPHID'); $bad_unit_status = array( ArcanistUnitTestResult::RESULT_FAIL, ArcanistUnitTestResult::RESULT_BROKEN, ); $message = new HarbormasterBuildUnitMessage(); $target = new HarbormasterBuildTarget(); $build = new HarbormasterBuild(); $buildable = new HarbormasterBuildable(); $broken_diffs = queryfx_all( $message->establishConnection('r'), 'SELECT distinct a.buildablePHID FROM %T m JOIN %T t ON m.buildTargetPHID = t.phid JOIN %T b ON t.buildPHID = b.phid JOIN %T a ON b.buildablePHID = a.phid WHERE a.buildablePHID IN (%Ls) AND m.result in (%Ls)', $message->getTableName(), $target->getTableName(), $build->getTableName(), $buildable->getTableName(), $diff_phids, $bad_unit_status); $unit_status = array(); foreach ($broken_diffs as $broken) { $phid = $broken['buildablePHID']; $unit_status[$phid] = DifferentialUnitStatus::UNIT_FAIL; } return $unit_status; } private function loadChangesetsAndVsMap( DifferentialDiff $target, DifferentialDiff $diff_vs = null, PhabricatorRepository $repository = null) { $load_diffs = array($target); if ($diff_vs) { $load_diffs[] = $diff_vs; } $raw_changesets = id(new DifferentialChangesetQuery()) ->setViewer($this->getRequest()->getUser()) ->withDiffs($load_diffs) ->execute(); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $refs = array(); $vs_map = array(); $vs_changesets = array(); if ($diff_vs) { $vs_id = $diff_vs->getID(); $vs_changesets_path_map = array(); foreach (idx($changeset_groups, $vs_id, array()) as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff_vs); $vs_changesets_path_map[$path] = $changeset; $vs_changesets[$changeset->getID()] = $changeset; } foreach ($changesets as $key => $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $target); if (isset($vs_changesets_path_map[$path])) { $vs_map[$changeset->getID()] = $vs_changesets_path_map[$path]->getID(); $refs[$changeset->getID()] = $changeset->getID().'/'.$vs_changesets_path_map[$path]->getID(); unset($vs_changesets_path_map[$path]); } else { $refs[$changeset->getID()] = $changeset->getID(); } } foreach ($vs_changesets_path_map as $path => $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; $refs[$changeset->getID()] = $changeset->getID().'/-1'; } } else { foreach ($changesets as $changeset) { $refs[$changeset->getID()] = $changeset->getID(); } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map, $vs_changesets, $refs); } private function buildSymbolIndexes( PhabricatorRepository $repository, array $visible_changesets) { assert_instances_of($visible_changesets, 'DifferentialChangeset'); $engine = PhabricatorSyntaxHighlighter::newEngine(); $langs = $repository->getSymbolLanguages(); $langs = nonempty($langs, array()); $sources = $repository->getSymbolSources(); $sources = nonempty($sources, array()); $symbol_indexes = array(); if ($langs && $sources) { $have_symbols = id(new DiffusionSymbolQuery()) ->existsSymbolsInRepository($repository->getPHID()); if (!$have_symbols) { return $symbol_indexes; } } $repository_phids = array_merge( array($repository->getPHID()), $sources); $indexed_langs = array_fill_keys($langs, true); foreach ($visible_changesets as $key => $changeset) { $lang = $engine->getLanguageFromFilename($changeset->getFilename()); if (empty($indexed_langs) || isset($indexed_langs[$lang])) { $symbol_indexes[$key] = array( 'lang' => $lang, 'repositories' => $repository_phids, ); } } return $symbol_indexes; } private function loadOtherRevisions( array $changesets, DifferentialDiff $target, PhabricatorRepository $repository) { assert_instances_of($changesets, 'DifferentialChangeset'); $paths = array(); foreach ($changesets as $changeset) { $paths[] = $changeset->getAbsoluteRepositoryPath( $repository, $target); } if (!$paths) { return array(); } $path_map = id(new DiffusionPathIDQuery($paths))->loadPathIDs(); if (!$path_map) { return array(); } $recent = (PhabricatorTime::getNow() - phutil_units('30 days in seconds')); $query = id(new DifferentialRevisionQuery()) ->setViewer($this->getRequest()->getUser()) ->withStatus(DifferentialRevisionQuery::STATUS_OPEN) ->withUpdatedEpochBetween($recent, null) ->setOrder(DifferentialRevisionQuery::ORDER_MODIFIED) ->setLimit(10) ->needFlags(true) ->needDrafts(true) ->needReviewers(true); foreach ($path_map as $path => $path_id) { $query->withPath($repository->getID(), $path_id); } $results = $query->execute(); // Strip out *this* revision. foreach ($results as $key => $result) { if ($result->getID() == $this->revisionID) { unset($results[$key]); } } return $results; } private function renderOtherRevisions(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $viewer = $this->getViewer(); $header = id(new PHUIHeaderView()) ->setHeader(pht('Recent Similar Revisions')); $view = id(new DifferentialRevisionListView()) ->setRevisions($revisions) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setNoBox(true) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); $handles = $this->loadViewerHandles($phids); $view->setHandles($handles); return $view; } /** * Note this code is somewhat similar to the buildPatch method in * @{class:DifferentialReviewRequestMail}. * * @return @{class:AphrontRedirectResponse} */ private function buildRawDiffResponse( DifferentialRevision $revision, array $changesets, array $vs_changesets, array $vs_map, PhabricatorRepository $repository = null) { assert_instances_of($changesets, 'DifferentialChangeset'); assert_instances_of($vs_changesets, 'DifferentialChangeset'); $viewer = $this->getViewer(); id(new DifferentialHunkQuery()) ->setViewer($viewer) ->withChangesets($changesets) ->needAttachToChangesets(true) ->execute(); $diff = new DifferentialDiff(); $diff->attachChangesets($changesets); $raw_changes = $diff->buildChangesList(); $changes = array(); foreach ($raw_changes as $changedict) { $changes[] = ArcanistDiffChange::newFromDictionary($changedict); } $loader = id(new PhabricatorFileBundleLoader()) ->setViewer($viewer); $bundle = ArcanistBundle::newFromChanges($changes); $bundle->setLoadFileDataCallback(array($loader, 'loadFileData')); $vcs = $repository ? $repository->getVersionControlSystem() : null; switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $raw_diff = $bundle->toGitPatch(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: default: $raw_diff = $bundle->toUnifiedDiff(); break; } $request_uri = $this->getRequest()->getRequestURI(); // this ends up being something like // D123.diff // or the verbose // D123.vs123.id123.whitespaceignore-all.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; foreach ($request_uri->getQueryParams() as $key => $value) { if ($key == 'download') { continue; } $file_name .= $key.$value.'.'; } $file_name .= 'diff'; - $file = PhabricatorFile::buildFromFileDataOrHash( - $raw_diff, - array( - 'name' => $file_name, - 'ttl.relative' => phutil_units('24 hours in seconds'), - 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, - )); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file = PhabricatorFile::newFromFileData( + $raw_diff, + array( + 'name' => $file_name, + 'ttl.relative' => phutil_units('24 hours in seconds'), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + )); + $file->attachToObject($revision->getPHID()); unset($unguarded); return $file->getRedirectResponse(); } private function buildTransactions( DifferentialRevision $revision, DifferentialDiff $left_diff, DifferentialDiff $right_diff, array $old_ids, array $new_ids) { $timeline = $this->buildTransactionTimeline( $revision, new DifferentialTransactionQuery(), $engine = null, array( 'left' => $left_diff->getID(), 'right' => $right_diff->getID(), 'old' => implode(',', $old_ids), 'new' => implode(',', $new_ids), )); return $timeline; } private function buildRevisionWarnings( DifferentialRevision $revision, PhabricatorCustomFieldList $field_list, array $warning_handle_map, array $handles) { $warnings = array(); foreach ($field_list->getFields() as $key => $field) { $phids = idx($warning_handle_map, $key, array()); $field_handles = array_select_keys($handles, $phids); $field_warnings = $field->getWarningsForRevisionHeader($field_handles); foreach ($field_warnings as $warning) { $warnings[] = $warning; } } return $warnings; } private function buildDiffDetailView( array $diffs, DifferentialRevision $revision, PhabricatorCustomFieldList $field_list) { $viewer = $this->getViewer(); $fields = array(); foreach ($field_list->getFields() as $field) { if ($field->shouldAppearInDiffPropertyView()) { $fields[] = $field; } } if (!$fields) { return null; } $property_lists = array(); foreach ($this->getDiffTabLabels($diffs) as $tab) { list($label, $diff) = $tab; $property_lists[] = array( $label, $this->buildDiffPropertyList($diff, $revision, $fields), ); } $tab_group = id(new PHUITabGroupView()) ->setHideSingleTab(true); foreach ($property_lists as $key => $property_list) { list($tab_name, $list_view) = $property_list; $tab = id(new PHUITabView()) ->setKey($key) ->setName($tab_name) ->appendChild($list_view); $tab_group->addTab($tab); $tab_group->selectTab($key); } return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Diff Detail')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUser($viewer) ->addTabGroup($tab_group); } private function buildDiffPropertyList( DifferentialDiff $diff, DifferentialRevision $revision, array $fields) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($diff); foreach ($fields as $field) { $label = $field->renderDiffPropertyViewLabel($diff); $value = $field->renderDiffPropertyViewValue($diff); if ($value !== null) { $view->addProperty($label, $value); } } return $view; } private function buildOperationsBox(DifferentialRevision $revision) { $viewer = $this->getViewer(); // Save a query if we can't possibly have pending operations. $repository = $revision->getRepository(); if (!$repository || !$repository->canPerformAutomation()) { return null; } $operations = id(new DrydockRepositoryOperationQuery()) ->setViewer($viewer) ->withObjectPHIDs(array($revision->getPHID())) ->withIsDismissed(false) ->withOperationTypes( array( DrydockLandRepositoryOperation::OPCONST, )) ->execute(); if (!$operations) { return null; } $state_fail = DrydockRepositoryOperation::STATE_FAIL; // We're going to show the oldest operation which hasn't failed, or the // most recent failure if they're all failures. $operations = msort($operations, 'getID'); foreach ($operations as $operation) { if ($operation->getOperationState() != $state_fail) { break; } } // If we found a completed operation, don't render anything. We don't want // to show an older error after the thing worked properly. if ($operation->isDone()) { return null; } $box_view = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Active Operations')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); return id(new DrydockRepositoryOperationStatusView()) ->setUser($viewer) ->setBoxView($box_view) ->setOperation($operation); } private function buildUnitMessagesView( $diff, DifferentialRevision $revision) { $viewer = $this->getViewer(); if (!$diff->getBuildable()) { return null; } if (!$diff->getUnitMessages()) { return null; } $interesting_messages = array(); foreach ($diff->getUnitMessages() as $message) { switch ($message->getResult()) { case ArcanistUnitTestResult::RESULT_PASS: case ArcanistUnitTestResult::RESULT_SKIP: break; default: $interesting_messages[] = $message; break; } } if (!$interesting_messages) { return null; } $excuse = null; if ($diff->hasDiffProperty('arc:unit-excuse')) { $excuse = $diff->getProperty('arc:unit-excuse'); } return id(new HarbormasterUnitSummaryView()) ->setUser($viewer) ->setExcuse($excuse) ->setBuildable($diff->getBuildable()) ->setUnitMessages($diff->getUnitMessages()) ->setLimit(5) ->setShowViewAll(true); } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 4136f1fdf8..a7dd6b2e97 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,1562 +1,1524 @@ setViewer(PhabricatorUser::getOmnipotentUser()) ->withClasses(array('PhabricatorFilesApplication')) ->executeOne(); $view_policy = $app->getPolicy( FilesDefaultViewCapability::CAPABILITY); return id(new PhabricatorFile()) ->setViewPolicy($view_policy) ->setIsPartial(0) ->attachOriginalFile(null) ->attachObjects(array()) ->attachObjectPHIDs(array()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255?', 'mimeType' => 'text255?', 'byteSize' => 'uint64', 'storageEngine' => 'text32', 'storageFormat' => 'text32', 'storageHandle' => 'text255', 'authorPHID' => 'phid?', 'secretKey' => 'bytes20?', 'contentHash' => 'bytes40?', 'ttl' => 'epoch?', 'isExplicitUpload' => 'bool?', 'mailKey' => 'bytes20', 'isPartial' => 'bool', 'builtinKey' => 'text64?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'authorPHID' => array( 'columns' => array('authorPHID'), ), 'contentHash' => array( 'columns' => array('contentHash'), ), 'key_ttl' => array( 'columns' => array('ttl'), ), 'key_dateCreated' => array( 'columns' => array('dateCreated'), ), 'key_partial' => array( 'columns' => array('authorPHID', 'isPartial'), ), 'key_builtin' => array( 'columns' => array('builtinKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorFileFilePHIDType::TYPECONST); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function getMonogram() { return 'F'.$this->getID(); } public function scrambleSecret() { return $this->setSecretKey($this->generateSecretKey()); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception(pht('No file was uploaded!')); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception(pht('File is not an uploaded file.')); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception(pht('File size disagrees with uploaded size.')); } return $file_data; } public static function newFromPHPUpload($spec, array $params = array()) { $file_data = self::readUploadedFileData($spec); $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromXHRUpload($data, array $params = array()) { return self::newFromFileData($data, $params); } - /** - * Given a block of data, try to load an existing file with the same content - * if one exists. If it does not, build a new file. - * - * This method is generally used when we have some piece of semi-trusted data - * like a diff or a file from a repository that we want to show to the user. - * We can't just dump it out because it may be dangerous for any number of - * reasons; instead, we need to serve it through the File abstraction so it - * ends up on the CDN domain if one is configured and so on. However, if we - * simply wrote a new file every time we'd potentially end up with a lot - * of redundant data in file storage. - * - * To solve these problems, we use file storage as a cache and reuse the - * same file again if we've previously written it. - * - * NOTE: This method unguards writes. - * - * @param string Raw file data. - * @param dict Dictionary of file information. - */ - public static function buildFromFileDataOrHash( - $data, - array $params = array()) { - - $file = id(new PhabricatorFile())->loadOneWhere( - 'name = %s AND contentHash = %s LIMIT 1', - idx($params, 'name'), - self::hashFileContent($data)); - - if (!$file) { - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $file = self::newFromFileData($data, $params); - unset($unguarded); - } - - return $file; - } - public static function newFileFromContentHash($hash, array $params) { // Check to see if a file with same contentHash exist $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if ($file) { $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_storage_properties = $file->getStorageProperties(); $copy_of_byte_size = $file->getByteSize(); $copy_of_mime_type = $file->getMimeType(); $new_file = self::initializeNewFile(); $new_file->setByteSize($copy_of_byte_size); $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); $new_file->setStorageProperties($copy_of_storage_properties); $new_file->setMimeType($copy_of_mime_type); $new_file->copyDimensions($file); $new_file->readPropertiesFromParameters($params); $new_file->save(); return $new_file; } return $file; } public static function newChunkedFile( PhabricatorFileStorageEngine $engine, $length, array $params) { $file = self::initializeNewFile(); $file->setByteSize($length); // NOTE: Once we receive the first chunk, we'll detect its MIME type and // update the parent file. This matters for large media files like video. $file->setMimeType('application/octet-stream'); $chunked_hash = idx($params, 'chunkedHash'); if ($chunked_hash) { $file->setContentHash($chunked_hash); } else { // See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some // discussion of this. $seed = Filesystem::readRandomBytes(64); $hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput( $seed); $file->setContentHash($hash); } $file->setStorageEngine($engine->getEngineIdentifier()); $file->setStorageHandle(PhabricatorFileChunk::newChunkHandle()); // Chunked files are always stored raw because they do not actually store // data. The chunks do, and can be individually formatted. $file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY); $file->setIsPartial(1); $file->readPropertiesFromParameters($params); return $file; } private static function buildFromFileData($data, array $params = array()) { if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $size = strlen($data); $engines = PhabricatorFileStorageEngine::loadStorageEngines($size); if (!$engines) { throw new Exception( pht( 'No configured storage engine can store this file. See '. '"Configuring File Storage" in the documentation for '. 'information on configuring storage engines.')); } } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception(pht('No valid storage engines are available!')); } $file = self::initializeNewFile(); $aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY; $has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type); if ($has_aes !== null) { $default_key = PhabricatorFileAES256StorageFormat::FORMATKEY; } else { $default_key = PhabricatorFileRawStorageFormat::FORMATKEY; } $key = idx($params, 'format', $default_key); // Callers can pass in an object explicitly instead of a key. This is // primarily useful for unit tests. if ($key instanceof PhabricatorFileStorageFormat) { $format = clone $key; } else { $format = clone PhabricatorFileStorageFormat::requireFormat($key); } $format->setFile($file); $properties = $format->newStorageProperties(); $file->setStorageFormat($format->getStorageFormatKey()); $file->setStorageProperties($properties); $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { list($engine_identifier, $data_handle) = $file->writeToEngine( $engine, $data, $params); // We stored the file somewhere so stop trying to write it to other // places. break; } catch (PhabricatorFileStorageConfigurationException $ex) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } catch (Exception $ex) { phlog($ex); // If an engine doesn't work, keep trying all the other valid engines // in case something else works. $exceptions[$engine_class] = $ex; } } if (!$data_handle) { throw new PhutilAggregateException( pht('All storage engines failed to write file:'), $exceptions); } $file->setByteSize(strlen($data)); $file->setContentHash(self::hashFileContent($data)); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); $file->readPropertiesFromParameters($params); if (!$file->getMimeType()) { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing } $file->save(); return $file; } public static function newFromFileData($data, array $params = array()) { $hash = self::hashFileContent($data); $file = self::newFileFromContentHash($hash, $params); if ($file) { return $file; } return self::buildFromFileData($data, $params); } public function migrateToEngine( PhabricatorFileStorageEngine $engine, $make_copy) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("You can not migrate a file which hasn't yet been saved.")); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); list($new_identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $old_engine = $this->instantiateStorageEngine(); $old_identifier = $this->getStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); if (!$make_copy) { $this->deleteFileDataIfUnused( $old_engine, $old_identifier, $old_handle); } return $this; } public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("You can not migrate a file which hasn't yet been saved.")); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); $engine = $this->instantiateStorageEngine(); $old_handle = $this->getStorageHandle(); $properties = $format->newStorageProperties(); $this->setStorageFormat($format->getStorageFormatKey()); $this->setStorageProperties($properties); list($identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $this->setStorageHandle($new_handle); $this->save(); $this->deleteFileDataIfUnused( $engine, $identifier, $old_handle); return $this; } public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( pht("You can not cycle keys for a file which hasn't yet been saved.")); } $properties = $format->cycleStorageProperties(); $this->setStorageProperties($properties); $this->save(); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $key = $this->getStorageFormat(); $format = id(clone PhabricatorFileStorageFormat::requireFormat($key)) ->setFile($this); $data_iterator = array($data); $formatted_iterator = $format->newWriteIterator($data_iterator); $formatted_data = $this->loadDataFromIterator($formatted_iterator); $data_handle = $engine->writeFile($formatted_data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( pht( "Storage engine '%s' executed %s but did not return a valid ". "handle ('%s') to the data: it must be nonempty and no longer ". "than 255 characters.", $engine_class, 'writeFile()', $data_handle)); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( pht( "Storage engine '%s' returned an improper engine identifier '{%s}': ". "it must be nonempty and no longer than 32 characters.", $engine_class, $engine_identifier)); } return array($engine_identifier, $data_handle); } /** * Download a remote resource over HTTP and save the response body as a file. * * This method respects `security.outbound-blacklist`, and protects against * HTTP redirection (by manually following "Location" headers and verifying * each destination). It does not protect against DNS rebinding. See * discussion in T6755. */ public static function newFromFileDownload($uri, array $params = array()) { $timeout = 5; $redirects = array(); $current = $uri; while (true) { try { if (count($redirects) > 10) { throw new Exception( pht('Too many redirects trying to fetch remote URI.')); } $resolved = PhabricatorEnv::requireValidRemoteURIForFetch( $current, array( 'http', 'https', )); list($resolved_uri, $resolved_domain) = $resolved; $current = new PhutilURI($current); if ($current->getProtocol() == 'http') { // For HTTP, we can use a pre-resolved URI to defuse DNS rebinding. $fetch_uri = $resolved_uri; $fetch_host = $resolved_domain; } else { // For HTTPS, we can't: cURL won't verify the SSL certificate if // the domain has been replaced with an IP. But internal services // presumably will not have valid certificates for rebindable // domain names on attacker-controlled domains, so the DNS rebinding // attack should generally not be possible anyway. $fetch_uri = $current; $fetch_host = null; } $future = id(new HTTPSFuture($fetch_uri)) ->setFollowLocation(false) ->setTimeout($timeout); if ($fetch_host !== null) { $future->addHeader('Host', $fetch_host); } list($status, $body, $headers) = $future->resolve(); if ($status->isRedirect()) { // This is an HTTP 3XX status, so look for a "Location" header. $location = null; foreach ($headers as $header) { list($name, $value) = $header; if (phutil_utf8_strtolower($name) == 'location') { $location = $value; break; } } // HTTP 3XX status with no "Location" header, just treat this like // a normal HTTP error. if ($location === null) { throw $status; } if (isset($redirects[$location])) { throw new Exception( pht('Encountered loop while following redirects.')); } $redirects[$location] = $location; $current = $location; // We'll fall off the bottom and go try this URI now. } else if ($status->isError()) { // This is something other than an HTTP 2XX or HTTP 3XX status, so // just bail out. throw $status; } else { // This is HTTP 2XX, so use the response body to save the // file data. $params = $params + array( 'name' => basename($uri), ); return self::newFromFileData($body, $params); } } catch (Exception $ex) { if ($redirects) { throw new PhutilProxyException( pht( 'Failed to fetch remote URI "%s" after following %s redirect(s) '. '(%s): %s', $uri, phutil_count($redirects), implode(' > ', array_keys($redirects)), $ex->getMessage()), $ex); } else { throw $ex; } } } } public static function normalizeFileName($file_name) { $pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@"; $file_name = preg_replace($pattern, '_', $file_name); $file_name = preg_replace('@_+@', '_', $file_name); $file_name = trim($file_name, '_'); $disallowed_filenames = array( '.' => 'dot', '..' => 'dotdot', '' => 'file', ); $file_name = idx($disallowed_filenames, $file_name, $file_name); return $file_name; } public function delete() { // We want to delete all the rows which mark this file as the transformation // of some other file (since we're getting rid of it). We also delete all // the transformations of this file, so that a user who deletes an image // doesn't need to separately hunt down and delete a bunch of thumbnails and // resizes of it. $outbound_xforms = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTransforms( array( array( 'originalPHID' => $this->getPHID(), 'transform' => true, ), )) ->execute(); foreach ($outbound_xforms as $outbound_xform) { $outbound_xform->delete(); } $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 'transformedPHID = %s', $this->getPHID()); $this->openTransaction(); foreach ($inbound_xforms as $inbound_xform) { $inbound_xform->delete(); } $ret = parent::delete(); $this->saveTransaction(); $this->deleteFileDataIfUnused( $this->instantiateStorageEngine(), $this->getStorageEngine(), $this->getStorageHandle()); return $ret; } /** * Destroy stored file data if there are no remaining files which reference * it. */ public function deleteFileDataIfUnused( PhabricatorFileStorageEngine $engine, $engine_identifier, $handle) { // Check to see if any files are using storage. $usage = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s LIMIT 1', $engine_identifier, $handle); // If there are no files using the storage, destroy the actual storage. if (!$usage) { try { $engine->deleteFile($handle); } catch (Exception $ex) { // In the worst case, we're leaving some data stranded in a storage // engine, which is not a big deal. phlog($ex); } } } public static function hashFileContent($data) { return sha1($data); } public function loadFileData() { $iterator = $this->getFileDataIterator(); return $this->loadDataFromIterator($iterator); } /** * Return an iterable which emits file content bytes. * * @param int Offset for the start of data. * @param int Offset for the end of data. * @return Iterable Iterable object which emits requested data. */ public function getFileDataIterator($begin = null, $end = null) { $engine = $this->instantiateStorageEngine(); $raw_iterator = $engine->getRawFileDataIterator($this, $begin, $end); $key = $this->getStorageFormat(); $format = id(clone PhabricatorFileStorageFormat::requireFormat($key)) ->setFile($this); return $format->newReadIterator($raw_iterator); } public function getURI() { return $this->getInfoURI(); } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( pht('You must save a file before you can generate a view URI.')); } return $this->getCDNURI(); } public function getCDNURI() { $name = self::normalizeFileName($this->getName()); $name = phutil_escape_uri($name); $parts = array(); $parts[] = 'file'; $parts[] = 'data'; // If this is an instanced install, add the instance identifier to the URI. // Instanced configurations behind a CDN may not be able to control the // request domain used by the CDN (as with AWS CloudFront). Embedding the // instance identity in the path allows us to distinguish between requests // originating from different instances but served through the same CDN. $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $this->getSecretKey(); $parts[] = $this->getPHID(); $parts[] = $name; $path = '/'.implode('/', $parts); // If this file is only partially uploaded, we're just going to return a // local URI to make sure that Ajax works, since the page is inevitably // going to give us an error back. if ($this->getIsPartial()) { return PhabricatorEnv::getURI($path); } else { return PhabricatorEnv::getCDNURI($path); } } public function getInfoURI() { return '/'.$this->getMonogram(); } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getDownloadURI() { $uri = id(new PhutilURI($this->getViewURI())) ->setQueryParam('download', true); return (string)$uri; } public function getURIForTransform(PhabricatorFileTransform $transform) { return $this->getTransformedURI($transform->getTransformKey()); } private function getTransformedURI($transform) { $parts = array(); $parts[] = 'file'; $parts[] = 'xform'; $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { $parts[] = '@'.$instance; } $parts[] = $transform; $parts[] = $this->getPHID(); $parts[] = $this->getSecretKey(); $path = implode('/', $parts); $path = $path.'/'; return PhabricatorEnv::getCDNURI($path); } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isViewableImage() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isAudio() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isVideo() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception(pht('Unknown type matched as image MIME type.')); } } public static function getTransformableImageFormats() { $supported = array(); if (function_exists('imagejpeg')) { $supported[] = 'jpg'; } if (function_exists('imagepng')) { $supported[] = 'png'; } if (function_exists('imagegif')) { $supported[] = 'gif'; } return $supported; } public function getDragAndDropDictionary() { return array( 'id' => $this->getID(), 'phid' => $this->getPHID(), 'uri' => $this->getBestURI(), ); } public function instantiateStorageEngine() { return self::buildEngine($this->getStorageEngine()); } public static function buildEngine($engine_identifier) { $engines = self::buildAllEngines(); foreach ($engines as $engine) { if ($engine->getEngineIdentifier() == $engine_identifier) { return $engine; } } throw new Exception( pht( "Storage engine '%s' could not be located!", $engine_identifier)); } public static function buildAllEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorFileStorageEngine') ->execute(); } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function getDisplayIconForMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type, 'fa-file-o'); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } public function setStorageProperties(array $properties) { $this->metadata[self::METADATA_STORAGE] = $properties; return $this; } public function getStorageProperties() { return idx($this->metadata, self::METADATA_STORAGE, array()); } public function getStorageProperty($key, $default = null) { $properties = $this->getStorageProperties(); return idx($properties, $key, $default); } public function loadDataFromIterator($iterator) { $result = ''; foreach ($iterator as $chunk) { $result .= $chunk; } return $result; } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception(pht('This file is not a viewable image.')); } if (!function_exists('imagecreatefromstring')) { throw new Exception(pht('Cannot retrieve image information.')); } $data = $this->loadFileData(); $img = imagecreatefromstring($data); if ($img === false) { throw new Exception(pht('Error when decoding image.')); } $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); if ($save) { $this->save(); } return $this; } public function copyDimensions(PhabricatorFile $file) { $metadata = $file->getMetadata(); $width = idx($metadata, self::METADATA_IMAGE_WIDTH); if ($width) { $this->metadata[self::METADATA_IMAGE_WIDTH] = $width; } $height = idx($metadata, self::METADATA_IMAGE_HEIGHT); if ($height) { $this->metadata[self::METADATA_IMAGE_HEIGHT] = $height; } return $this; } /** * Load (or build) the {@class:PhabricatorFile} objects for builtin file * resources. The builtin mechanism allows files shipped with Phabricator * to be treated like normal files so that APIs do not need to special case * things like default images or deleted files. * * Builtins are located in `resources/builtin/` and identified by their * name. * * @param PhabricatorUser Viewing user. * @param list List of builtin file specs. * @return dict Dictionary of named builtins. */ public static function loadBuiltins(PhabricatorUser $user, array $builtins) { $builtins = mpull($builtins, null, 'getBuiltinFileKey'); // NOTE: Anyone is allowed to access builtin files. $files = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuiltinKeys(array_keys($builtins)) ->execute(); $results = array(); foreach ($files as $file) { $builtin_key = $file->getBuiltinName(); if ($builtin_key !== null) { $results[$builtin_key] = $file; } } $build = array(); foreach ($builtins as $key => $builtin) { if (isset($results[$key])) { continue; } $data = $builtin->loadBuiltinFileData(); $params = array( 'name' => $builtin->getBuiltinDisplayName(), 'ttl.relative' => phutil_units('7 days in seconds'), 'canCDN' => true, 'builtin' => $key, ); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $file = self::newFromFileData($data, $params); } catch (AphrontDuplicateKeyQueryException $ex) { $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuiltinKeys(array($key)) ->executeOne(); if (!$file) { throw new Exception( pht( 'Collided mid-air when generating builtin file "%s", but '. 'then failed to load the object we collided with.', $key)); } } unset($unguarded); $file->attachObjectPHIDs(array()); $file->attachObjects(array()); $results[$key] = $file; } return $results; } /** * Convenience wrapper for @{method:loadBuiltins}. * * @param PhabricatorUser Viewing user. * @param string Single builtin name to load. * @return PhabricatorFile Corresponding builtin file. */ public static function loadBuiltin(PhabricatorUser $user, $name) { $builtin = id(new PhabricatorFilesOnDiskBuiltinFile()) ->setName($name); $key = $builtin->getBuiltinFileKey(); return idx(self::loadBuiltins($user, array($builtin)), $key); } public function getObjects() { return $this->assertAttached($this->objects); } public function attachObjects(array $objects) { $this->objects = $objects; return $this; } public function getObjectPHIDs() { return $this->assertAttached($this->objectPHIDs); } public function attachObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function getOriginalFile() { return $this->assertAttached($this->originalFile); } public function attachOriginalFile(PhabricatorFile $file = null) { $this->originalFile = $file; return $this; } public function getImageHeight() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_HEIGHT); } public function getImageWidth() { if (!$this->isViewableImage()) { return null; } return idx($this->metadata, self::METADATA_IMAGE_WIDTH); } public function getCanCDN() { if (!$this->isViewableImage()) { return false; } return idx($this->metadata, self::METADATA_CAN_CDN); } public function setCanCDN($can_cdn) { $this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0; return $this; } public function isBuiltin() { return ($this->getBuiltinName() !== null); } public function getBuiltinName() { return idx($this->metadata, self::METADATA_BUILTIN); } public function setBuiltinName($name) { $this->metadata[self::METADATA_BUILTIN] = $name; return $this; } public function getIsProfileImage() { return idx($this->metadata, self::METADATA_PROFILE); } public function setIsProfileImage($value) { $this->metadata[self::METADATA_PROFILE] = $value; return $this; } /** * Write the policy edge between this file and some object. * * @param phid Object PHID to attach to. * @return this */ public function attachToObject($phid) { $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST; id(new PhabricatorEdgeEditor()) ->addEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Remove the policy edge between this file and some object. * * @param phid Object PHID to detach from. * @return this */ public function detachFromObject($phid) { $edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST; id(new PhabricatorEdgeEditor()) ->removeEdge($phid, $edge_type, $this->getPHID()) ->save(); return $this; } /** * Configure a newly created file object according to specified parameters. * * This method is called both when creating a file from fresh data, and * when creating a new file which reuses existing storage. * * @param map Bag of parameters, see @{class:PhabricatorFile} * for documentation. * @return this */ private function readPropertiesFromParameters(array $params) { PhutilTypeSpec::checkMap( $params, array( 'name' => 'optional string', 'authorPHID' => 'optional string', 'ttl.relative' => 'optional int', 'ttl.absolute' => 'optional int', 'viewPolicy' => 'optional string', 'isExplicitUpload' => 'optional bool', 'canCDN' => 'optional bool', 'profile' => 'optional bool', 'format' => 'optional string|PhabricatorFileStorageFormat', 'mime-type' => 'optional string', 'builtin' => 'optional string', 'storageEngines' => 'optional list', )); $file_name = idx($params, 'name'); $this->setName($file_name); $author_phid = idx($params, 'authorPHID'); $this->setAuthorPHID($author_phid); $absolute_ttl = idx($params, 'ttl.absolute'); $relative_ttl = idx($params, 'ttl.relative'); if ($absolute_ttl !== null && $relative_ttl !== null) { throw new Exception( pht( 'Specify an absolute TTL or a relative TTL, but not both.')); } else if ($absolute_ttl !== null) { if ($absolute_ttl < PhabricatorTime::getNow()) { throw new Exception( pht( 'Absolute TTL must be in the present or future, but TTL "%s" '. 'is in the past.', $absolute_ttl)); } $this->setTtl($absolute_ttl); } else if ($relative_ttl !== null) { if ($relative_ttl < 0) { throw new Exception( pht( 'Relative TTL must be zero or more seconds, but "%s" is '. 'negative.', $relative_ttl)); } $max_relative = phutil_units('365 days in seconds'); if ($relative_ttl > $max_relative) { throw new Exception( pht( 'Relative TTL must not be more than "%s" seconds, but TTL '. '"%s" was specified.', $max_relative, $relative_ttl)); } $absolute_ttl = PhabricatorTime::getNow() + $relative_ttl; $this->setTtl($absolute_ttl); } $view_policy = idx($params, 'viewPolicy'); if ($view_policy) { $this->setViewPolicy($params['viewPolicy']); } $is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0); $this->setIsExplicitUpload($is_explicit); $can_cdn = idx($params, 'canCDN'); if ($can_cdn) { $this->setCanCDN(true); } $builtin = idx($params, 'builtin'); if ($builtin) { $this->setBuiltinName($builtin); $this->setBuiltinKey($builtin); } $profile = idx($params, 'profile'); if ($profile) { $this->setIsProfileImage(true); } $mime_type = idx($params, 'mime-type'); if ($mime_type) { $this->setMimeType($mime_type); } return $this; } public function getRedirectResponse() { $uri = $this->getBestURI(); // TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI // (if the file is a viewable image) and sometimes a local URI (if not). // For now, just detect which one we got and configure the response // appropriately. In the long run, if this endpoint is served from a CDN // domain, we can't issue a local redirect to an info URI (which is not // present on the CDN domain). We probably never actually issue local // redirects here anyway, since we only ever transform viewable images // right now. $is_external = strlen(id(new PhutilURI($uri))->getDomain()); return id(new AphrontRedirectResponse()) ->setIsExternal($is_external) ->setURI($uri); } public function attachTransforms(array $map) { $this->transforms = $map; return $this; } public function getTransform($key) { return $this->assertAttachedKey($this->transforms, $key); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorFileEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorFileTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: if ($this->isBuiltin()) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->getIsProfileImage()) { return PhabricatorPolicies::getMostOpenPolicy(); } return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return PhabricatorPolicies::POLICY_NOONE; } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid) { if ($this->getAuthorPHID() == $viewer_phid) { return true; } } switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: // If you can see the file this file is a transform of, you can see // this file. if ($this->getOriginalFile()) { return true; } // If you can see any object this file is attached to, you can see // the file. return (count($this->getObjects()) > 0); } return false; } public function describeAutomaticCapability($capability) { $out = array(); $out[] = pht('The user who uploaded a file can always view and edit it.'); switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: $out[] = pht( 'Files attached to objects are visible to users who can view '. 'those objects.'); $out[] = pht( 'Thumbnails are visible only to users who can view the original '. 'file.'); break; } return $out; } /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ public function isAutomaticallySubscribed($phid) { return ($this->authorPHID == $phid); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array( $this->getAuthorPHID(), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the file.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('dataURI') ->setType('string') ->setDescription(pht('Download URI for the file data.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('size') ->setType('int') ->setDescription(pht('File size, in bytes.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'dataURI' => $this->getCDNURI(), 'size' => (int)$this->getByteSize(), ); } public function getConduitSearchAttachments() { return array(); } } diff --git a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php index 0a3736f93a..a030190343 100644 --- a/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php +++ b/src/applications/phragment/conduit/PhragmentGetPatchConduitAPIMethod.php @@ -1,189 +1,192 @@ 'required string', 'state' => 'required dict', ); } protected function defineReturnType() { return 'nonempty dict'; } protected function defineErrorTypes() { return array( 'ERR_BAD_FRAGMENT' => pht('No such fragment exists.'), ); } protected function execute(ConduitAPIRequest $request) { $path = $request->getValue('path'); $state = $request->getValue('state'); // The state is an array mapping file paths to hashes. $patches = array(); // We need to get all of the mappings (like phragment.getstate) first // so that we can detect deletions and creations of files. $fragment = id(new PhragmentFragmentQuery()) ->setViewer($request->getUser()) ->withPaths(array($path)) ->executeOne(); if ($fragment === null) { throw new ConduitException('ERR_BAD_FRAGMENT'); } $mappings = $fragment->getFragmentMappings( $request->getUser(), $fragment->getPath()); $file_phids = mpull(mpull($mappings, 'getLatestVersion'), 'getFilePHID'); $files = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); // Scan all of the files that the caller currently has and iterate // over that. foreach ($state as $path => $hash) { // If $mappings[$path] exists, then the user has the file and it's // also a fragment. if (array_key_exists($path, $mappings)) { $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); if ($file_phid !== null) { // If the file PHID is present, then we need to check the // hashes to see if they are the same. $hash_caller = strtolower($state[$path]); $hash_current = $files[$file_phid]->getContentHash(); if ($hash_caller === $hash_current) { // The user's version is identical to our version, so // there is no update needed. } else { // The hash differs, and the user needs to update. $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => $files[$file_phid], 'hashOld' => $hash_caller, 'hashNew' => $hash_current, 'patchURI' => null, ); } } else { // We have a record of this as a file, but there is no file // attached to the latest version, so we consider this to be // a deletion. $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => null, 'hashOld' => $hash_caller, 'hashNew' => PhragmentPatchUtil::EMPTY_HASH, 'patchURI' => null, ); } } else { // If $mappings[$path] does not exist, then the user has a file, // and we have absolutely no record of it what-so-ever (we haven't // even recorded a deletion). Assuming most applications will store // some form of data near their own files, this is probably a data // file relevant for the application that is not versioned, so we // don't tell the client to do anything with it. } } // Check the remaining files that we know about but the caller has // not reported. foreach ($mappings as $path => $child) { if (array_key_exists($path, $state)) { // We have already evaluated this above. } else { $file_phid = $mappings[$path]->getLatestVersion()->getFilePHID(); if ($file_phid !== null) { // If the file PHID is present, then this is a new file that // we know about, but the caller does not. We need to tell // the caller to create the file. $hash_current = $files[$file_phid]->getContentHash(); $patches[] = array( 'path' => $path, 'fileOld' => null, 'fileNew' => $files[$file_phid], 'hashOld' => PhragmentPatchUtil::EMPTY_HASH, 'hashNew' => $hash_current, 'patchURI' => null, ); } else { // We have a record of deleting this file, and the caller hasn't // reported it, so they've probably deleted it in a previous // update. } } } // Before we can calculate patches, we need to resolve the old versions // of files so we can draw diffs on them. $hashes = array(); foreach ($patches as $patch) { if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) { $hashes[] = $patch['hashOld']; } } $old_files = array(); if (count($hashes) !== 0) { $old_files = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withContentHashes($hashes) ->execute(); } $old_files = mpull($old_files, null, 'getContentHash'); foreach ($patches as $key => $patch) { if ($patch['hashOld'] !== PhragmentPatchUtil::EMPTY_HASH) { if (array_key_exists($patch['hashOld'], $old_files)) { $patches[$key]['fileOld'] = $old_files[$patch['hashOld']]; } else { // We either can't see or can't read the old file. $patches[$key]['hashOld'] = PhragmentPatchUtil::EMPTY_HASH; $patches[$key]['fileOld'] = null; } } } // Now run through all of the patch entries, calculate the patches // and return the results. foreach ($patches as $key => $patch) { $data = PhragmentPatchUtil::calculatePatch( $patches[$key]['fileOld'], $patches[$key]['fileNew']); unset($patches[$key]['fileOld']); unset($patches[$key]['fileNew']); - $file = PhabricatorFile::buildFromFileDataOrHash( - $data, - array( - 'name' => 'patch.dmp', - 'ttl.relative' => phutil_units('24 hours in seconds'), - )); + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file = PhabricatorFile::newFromFileData( + $data, + array( + 'name' => 'patch.dmp', + 'ttl.relative' => phutil_units('24 hours in seconds'), + )); + unset($unguarded); + $patches[$key]['patchURI'] = $file->getDownloadURI(); } return $patches; } } diff --git a/src/applications/phragment/controller/PhragmentPatchController.php b/src/applications/phragment/controller/PhragmentPatchController.php index dade7b2f89..eaa08bc29e 100644 --- a/src/applications/phragment/controller/PhragmentPatchController.php +++ b/src/applications/phragment/controller/PhragmentPatchController.php @@ -1,97 +1,97 @@ getViewer(); $aid = $request->getURIData('aid'); $bid = $request->getURIData('bid'); // If "aid" is "x", then it means the user wants to generate // a patch of an empty file to the version specified by "bid". $ids = array($aid, $bid); if ($aid === 'x') { $ids = array($bid); } $versions = id(new PhragmentFragmentVersionQuery()) ->setViewer($viewer) ->withIDs($ids) ->execute(); $version_a = null; if ($aid !== 'x') { $version_a = idx($versions, $aid, null); if ($version_a === null) { return new Aphront404Response(); } } $version_b = idx($versions, $bid, null); if ($version_b === null) { return new Aphront404Response(); } $file_phids = array(); if ($version_a !== null) { $file_phids[] = $version_a->getFilePHID(); } $file_phids[] = $version_b->getFilePHID(); $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); $file_a = null; if ($version_a != null) { $file_a = idx($files, $version_a->getFilePHID(), null); } $file_b = idx($files, $version_b->getFilePHID(), null); $patch = PhragmentPatchUtil::calculatePatch($file_a, $file_b); if ($patch === null) { // There are no differences between the two files, so we output // an empty patch. $patch = ''; } $a_sequence = 'x'; if ($version_a !== null) { $a_sequence = $version_a->getSequence(); } $name = $version_b->getFragment()->getName().'.'. $a_sequence.'.'. $version_b->getSequence().'.patch'; $return = $version_b->getURI(); if ($request->getExists('return')) { $return = $request->getStr('return'); } - $result = PhabricatorFile::buildFromFileDataOrHash( - $patch, - array( - 'name' => $name, - 'mime-type' => 'text/plain', - 'ttl.relative' => phutil_units('24 hours in seconds'), - )); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $result = PhabricatorFile::newFromFileData( + $patch, + array( + 'name' => $name, + 'mime-type' => 'text/plain', + 'ttl.relative' => phutil_units('24 hours in seconds'), + )); + $result->attachToObject($version_b->getFragmentPHID()); unset($unguarded); return id(new AphrontRedirectResponse()) ->setURI($result->getDownloadURI($return)); } } diff --git a/src/applications/phragment/controller/PhragmentZIPController.php b/src/applications/phragment/controller/PhragmentZIPController.php index 6cb9d3ac77..167a67857f 100644 --- a/src/applications/phragment/controller/PhragmentZIPController.php +++ b/src/applications/phragment/controller/PhragmentZIPController.php @@ -1,153 +1,154 @@ getViewer(); $dblob = $request->getURIData('dblob'); $snapshot = $request->getURIData('snapshot'); $parents = $this->loadParentFragments($dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = idx($parents, count($parents) - 1, null); if ($snapshot !== null) { $snapshot = id(new PhragmentSnapshotQuery()) ->setViewer($viewer) ->withPrimaryFragmentPHIDs(array($fragment->getPHID())) ->withNames(array($snapshot)) ->executeOne(); if ($snapshot === null) { return new Aphront404Response(); } $cache = id(new PhragmentSnapshotChildQuery()) ->setViewer($viewer) ->needFragmentVersions(true) ->withSnapshotPHIDs(array($snapshot->getPHID())) ->execute(); $this->snapshotCache = mpull( $cache, 'getFragmentVersion', 'getFragmentPHID'); } $temp = new TempFile(); $zip = null; try { $zip = new ZipArchive(); } catch (Exception $e) { $dialog = new AphrontDialogView(); $dialog->setUser($viewer); $inst = pht( 'This system does not have the ZIP PHP extension installed. This '. 'is required to download ZIPs from Phragment.'); $dialog->setTitle(pht('ZIP Extension Not Installed')); $dialog->appendParagraph($inst); $dialog->addCancelButton('/phragment/browse/'.$dblob); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!$zip->open((string)$temp, ZipArchive::CREATE)) { throw new Exception(pht('Unable to create ZIP archive!')); } $mappings = $this->getFragmentMappings( $fragment, $fragment->getPath(), $snapshot); $phids = array(); foreach ($mappings as $path => $file_phid) { $phids[] = $file_phid; } $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); $files = mpull($files, null, 'getPHID'); foreach ($mappings as $path => $file_phid) { if (!isset($files[$file_phid])) { // The path is most likely pointing to a deleted fragment, which // hence no longer has a file associated with it. unset($mappings[$path]); continue; } $mappings[$path] = $files[$file_phid]; } foreach ($mappings as $path => $file) { if ($file !== null) { $zip->addFromString($path, $file->loadFileData()); } } $zip->close(); $zip_name = $fragment->getName(); if (substr($zip_name, -4) !== '.zip') { $zip_name .= '.zip'; } $data = Filesystem::readFile((string)$temp); - $file = PhabricatorFile::buildFromFileDataOrHash( - $data, - array( - 'name' => $zip_name, - 'ttl.relative' => phutil_units('24 hours in seconds'), - )); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $file = PhabricatorFile::newFromFileData( + $data, + array( + 'name' => $zip_name, + 'ttl.relative' => phutil_units('24 hours in seconds'), + )); + $file->attachToObject($fragment->getPHID()); unset($unguarded); $return = $fragment->getURI(); if ($request->getExists('return')) { $return = $request->getStr('return'); } return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($file->getDownloadURI($return)); } /** * Returns a list of mappings like array('some/path.txt' => 'file PHID'); */ private function getFragmentMappings( PhragmentFragment $current, $base_path, $snapshot) { $mappings = $current->getFragmentMappings( $this->getRequest()->getUser(), $base_path); $result = array(); foreach ($mappings as $path => $fragment) { $version = $this->getVersion($fragment, $snapshot); if ($version !== null) { $result[$path] = $version->getFilePHID(); } } return $result; } private function getVersion($fragment, $snapshot) { if ($snapshot === null) { return $fragment->getLatestVersion(); } else { return idx($this->snapshotCache, $fragment->getPHID(), null); } } }