diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php index b2cc472341..d8a7923b04 100644 --- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php +++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php @@ -1,67 +1,67 @@ /)' => 'PhrictionDocumentController', // Match "/w/x/y/z/" with slug "x/y/z/". '/w/(?P.+/)' => 'PhrictionDocumentController', '/phriction/' => array( '(?:query/(?P[^/]+)/)?' => 'PhrictionListController', 'history(?P/)' => 'PhrictionHistoryController', 'history/(?P.+/)' => 'PhrictionHistoryController', 'edit/(?:(?P[1-9]\d*)/)?' => 'PhrictionEditController', 'delete/(?P[1-9]\d*)/' => 'PhrictionDeleteController', 'new/' => 'PhrictionNewController', - 'move/(?:(?P[1-9]\d*)/)?' => 'PhrictionMoveController', + 'move/(?P[1-9]\d*)/' => 'PhrictionMoveController', 'preview/' => 'PhabricatorMarkupPreviewController', 'diff/(?P[1-9]\d*)/' => 'PhrictionDiffController', ), ); } public function getApplicationOrder() { return 0.140; } } diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index ce4dbe240c..21c65f0495 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -1,121 +1,114 @@ id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); - - if ($this->id) { - $document = id(new PhrictionDocumentQuery()) - ->setViewer($user) - ->withIDs(array($this->id)) - ->needContent(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - } else { - $slug = PhabricatorSlug::normalize( - $request->getStr('slug')); - if (!$slug) { - return new Aphront404Response(); - } - - $document = id(new PhrictionDocumentQuery()) - ->setViewer($user) - ->withSlugs(array($slug)) - ->needContent(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - } - + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $document = id(new PhrictionDocumentQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->needContent(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$document) { return new Aphront404Response(); } - if (!isset($slug)) { - $slug = $document->getSlug(); - } + $slug = $document->getSlug(); + $cancel_uri = PhrictionDocument::getSlugURI($slug); - $target_slug = PhabricatorSlug::normalize( - $request->getStr('new-slug', $slug)); + $v_slug = $slug; + $e_slug = null; - $submit_uri = $request->getRequestURI()->getPath(); - $cancel_uri = PhrictionDocument::getSlugURI($slug); + $v_note = ''; - $e_url = true; $validation_exception = null; - $content = $document->getContent(); - if ($request->isFormPost()) { + $v_note = $request->getStr('description'); + $v_slug = $request->getStr('slug'); + + // If what the user typed isn't what we're actually using, warn them + // about it. + if (strlen($v_slug)) { + $normal_slug = PhabricatorSlug::normalize($v_slug); + if ($normal_slug !== $v_slug) { + return $this->newDialog() + ->setTitle(pht('Adjust Path')) + ->appendParagraph( + pht( + 'The path you entered (%s) is not a valid wiki document '. + 'path. Paths may not contain special characters.', + phutil_tag('strong', array(), $v_slug))) + ->appendParagraph( + pht( + 'Would you like to use the path %s instead?', + phutil_tag('strong', array(), $normal_slug))) + ->addHiddenInput('slug', $normal_slug) + ->addHiddenInput('description', $v_note) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Accept Path')); + } + } $editor = id(new PhrictionTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setDescription($request->getStr('description')); + ->setDescription($v_note); $xactions = array(); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_MOVE_TO) ->setNewValue($document); $target_document = PhrictionDocument::initializeNewDocument( - $user, - $target_slug); + $viewer, + $v_slug); try { $editor->applyTransactions($target_document, $xactions); $redir_uri = PhrictionDocument::getSlugURI( $target_document->getSlug()); return id(new AphrontRedirectResponse())->setURI($redir_uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; - $e_url = $ex->getShortMessage(PhrictionTransaction::TYPE_MOVE_TO); + $e_slug = $ex->getShortMessage(PhrictionTransaction::TYPE_MOVE_TO); } } - $form = id(new PHUIFormLayoutView()) - ->setUser($user) + + $form = id(new AphrontFormView()) + ->setUser($viewer) ->appendChild( id(new AphrontFormStaticControl()) - ->setLabel(pht('Title')) - ->setValue($content->getTitle())) + ->setLabel(pht('Title')) + ->setValue($document->getContent()->getTitle())) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('New URI')) - ->setValue($target_slug) - ->setError($e_url) - ->setName('new-slug') - ->setCaption(pht('The new location of the document.'))) + ->setLabel(pht('Current Path')) + ->setDisabled(true) + ->setValue($slug)) ->appendChild( id(new AphrontFormTextControl()) - ->setLabel(pht('Edit Notes')) - ->setValue(pht('Moving document to a new location.')) - ->setError(null) - ->setName('description')); + ->setLabel(pht('New Path')) + ->setValue($v_slug) + ->setError($e_slug) + ->setName('slug')) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Edit Notes')) + ->setValue($v_note) + ->setName('description')); - $dialog = id(new AphrontDialogView()) - ->setUser($user) - ->setValidationException($validation_exception) + return $this->newDialog() ->setTitle(pht('Move Document')) - ->appendChild($form) - ->setSubmitURI($submit_uri) + ->setValidationException($validation_exception) + ->appendForm($form) ->addSubmitButton(pht('Move Document')) ->addCancelButton($cancel_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index eb85ef0169..d8c8bc1730 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -1,709 +1,720 @@ description = $description; return $this; } private function getDescription() { return $this->description; } private function setOldContent(PhrictionContent $content) { $this->oldContent = $content; return $this; } private function getOldContent() { return $this->oldContent; } private function setNewContent(PhrictionContent $content) { $this->newContent = $content; return $this; } private function getNewContent() { return $this->newContent; } public function setSkipAncestorCheck($bool) { $this->skipAncestorCheck = $bool; return $this; } public function getSkipAncestorCheck() { return $this->skipAncestorCheck; } public function setContentVersion($version) { $this->contentVersion = $version; return $this; } public function getContentVersion() { return $this->contentVersion; } public function setProcessContentVersionError($process) { $this->processContentVersionError = $process; return $this; } public function getProcessContentVersionError() { return $this->processContentVersionError; } public function getEditorApplicationClass() { return 'PhabricatorPhrictionApplication'; } public function getEditorObjectsDescription() { return pht('Phriction Documents'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhrictionTransaction::TYPE_TITLE; $types[] = PhrictionTransaction::TYPE_CONTENT; $types[] = PhrictionTransaction::TYPE_DELETE; $types[] = PhrictionTransaction::TYPE_MOVE_TO; $types[] = PhrictionTransaction::TYPE_MOVE_AWAY; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getTitle(); case PhrictionTransaction::TYPE_CONTENT: if ($this->getIsNewObject()) { return null; } return $this->getOldContent()->getContent(); case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_TO: case PhrictionTransaction::TYPE_MOVE_AWAY: return null; } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: return $xaction->getNewValue(); case PhrictionTransaction::TYPE_MOVE_TO: $document = $xaction->getNewValue(); // grab the real object now for the sub-editor to come $this->moveAwayDocument = $document; $dict = array( 'id' => $document->getID(), 'phid' => $document->getPHID(), 'content' => $document->getContent()->getContent(),); return $dict; case PhrictionTransaction::TYPE_MOVE_AWAY: $document = $xaction->getNewValue(); $dict = array( 'id' => $document->getID(), 'phid' => $document->getPHID(), 'content' => $document->getContent()->getContent(),); return $dict; } } protected function shouldApplyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_TO: case PhrictionTransaction::TYPE_MOVE_AWAY: return true; } } return parent::shouldApplyInitialEffects($object, $xactions); } protected function applyInitialEffects( PhabricatorLiskDAO $object, array $xactions) { $this->setOldContent($object->getContent()); $this->setNewContent($this->buildNewContentTemplate($object)); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_MOVE_TO: $object->setStatus(PhrictionDocumentStatus::STATUS_EXISTS); return; case PhrictionTransaction::TYPE_MOVE_AWAY: $object->setStatus(PhrictionDocumentStatus::STATUS_MOVED); return; case PhrictionTransaction::TYPE_DELETE: $object->setStatus(PhrictionDocumentStatus::STATUS_DELETED); return; } } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_CONTENT: if ($this->getIsNewObject()) { break; } $content = $xaction->getNewValue(); if ($content === '') { $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_DELETE) ->setNewValue(true); } break; case PhrictionTransaction::TYPE_MOVE_TO: $document = $xaction->getNewValue(); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($document->getViewPolicy()); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($document->getEditPolicy()); break; default: break; } return $xactions; } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: $this->getNewContent()->setTitle($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_CONTENT: $this->getNewContent()->setContent($xaction->getNewValue()); break; case PhrictionTransaction::TYPE_DELETE: $this->getNewContent()->setContent(''); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_DELETE); break; case PhrictionTransaction::TYPE_MOVE_TO: $dict = $xaction->getNewValue(); $this->getNewContent()->setContent($dict['content']); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_MOVE_HERE); $this->getNewContent()->setChangeRef($dict['id']); break; case PhrictionTransaction::TYPE_MOVE_AWAY: $dict = $xaction->getNewValue(); $this->getNewContent()->setContent(''); $this->getNewContent()->setChangeType( PhrictionChangeType::CHANGE_MOVE_AWAY); $this->getNewContent()->setChangeRef($dict['id']); break; default: break; } } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $save_content = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_TITLE: case PhrictionTransaction::TYPE_CONTENT: case PhrictionTransaction::TYPE_DELETE: case PhrictionTransaction::TYPE_MOVE_AWAY: case PhrictionTransaction::TYPE_MOVE_TO: $save_content = true; break; default: break; } } if ($save_content) { $content = $this->getNewContent(); $content->setDocumentID($object->getID()); $content->save(); $object->setContentID($content->getID()); $object->save(); $object->attachContent($content); } if ($this->getIsNewObject() && !$this->getSkipAncestorCheck()) { // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocumentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSlugs($ancestral_slugs) ->needContent(true) ->execute(); $ancestors = mpull($ancestors, null, 'getSlug'); $stub_type = PhrictionChangeType::CHANGE_STUB; foreach ($ancestral_slugs as $slug) { $ancestor_doc = idx($ancestors, $slug); // We check for change type to prevent near-infinite recursion if (!$ancestor_doc && $content->getChangeType() != $stub_type) { $ancestor_doc = PhrictionDocument::initializeNewDocument( $this->getActor(), $slug); $stub_xactions = array(); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_TITLE) ->setNewValue(PhabricatorSlug::getDefaultTitle($slug)) ->setMetadataValue('stub:create:phid', $object->getPHID()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_CONTENT) ->setNewValue('') ->setMetadataValue('stub:create:phid', $object->getPHID()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($object->getViewPolicy()); $stub_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($object->getEditPolicy()); $sub_editor = id(new PhrictionTransactionEditor()) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setSkipAncestorCheck(true) ->setDescription(pht('Empty Parent Document')) ->applyTransactions($ancestor_doc, $stub_xactions); } } } } if ($this->moveAwayDocument !== null) { $move_away_xactions = array(); $move_away_xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_MOVE_AWAY) ->setNewValue($object); $sub_editor = id(new PhrictionTransactionEditor()) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setDescription($this->getDescription()) ->applyTransactions($this->moveAwayDocument, $move_away_xactions); } return $xactions; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return '[Phriction]'; } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getContent()->getAuthorPHID(), $this->getActingAsPHID(), ); } protected function getMailCC(PhabricatorLiskDAO $object) { $phids = array(); foreach ($this->heraldEmailPHIDs as $phid) { $phids[] = $phid; } return $phids; } public function getMailTagsMap() { return array( PhrictionTransaction::MAILTAG_TITLE => pht("A document's title changes."), PhrictionTransaction::MAILTAG_CONTENT => pht("A document's content changes."), PhrictionTransaction::MAILTAG_DELETE => pht('A document is deleted.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new PhrictionReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getContent()->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject($title) ->addHeader('Thread-Topic', $object->getPHID()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addTextSection( pht('DOCUMENT CONTENT'), $object->getContent()->getContent()); } $body->addLinkSection( pht('DOCUMENT DETAIL'), PhabricatorEnv::getProductionURI( PhrictionDocument::getSlugURI($object->getSlug()))); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldSendMail($object, $xactions); } protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { $phids = parent::getFeedRelatedPHIDs($object, $xactions); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionTransaction::TYPE_MOVE_TO: $dict = $xaction->getNewValue(); $phids[] = $dict['phid']; break; } } return $phids; } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case PhrictionTransaction::TYPE_TITLE: $title = $object->getContent()->getTitle(); $missing = $this->validateIsEmptyTextField( $title, $xactions); if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Document title is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } else if ($this->getProcessContentVersionError()) { $error = $this->validateContentVersion($object, $type, $xaction); if ($error) { $this->setProcessContentVersionError(false); $errors[] = $error; } } break; case PhrictionTransaction::TYPE_CONTENT: if ($xaction->getMetadataValue('stub:create:phid')) { continue; } $missing = false; if ($this->getIsNewObject()) { $content = $object->getContent()->getContent(); $missing = $this->validateIsEmptyTextField( $content, $xactions); } if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('Document content is required.'), nonempty(last($xactions), null)); $error->setIsMissingFieldError(true); $errors[] = $error; } else if ($this->getProcessContentVersionError()) { $error = $this->validateContentVersion($object, $type, $xaction); if ($error) { $this->setProcessContentVersionError(false); $errors[] = $error; } } break; case PhrictionTransaction::TYPE_MOVE_TO: $source_document = $xaction->getNewValue(); switch ($source_document->getStatus()) { case PhrictionDocumentStatus::STATUS_DELETED: $e_text = pht('A deleted document can not be moved.'); break; case PhrictionDocumentStatus::STATUS_MOVED: $e_text = pht('A moved document can not be moved again.'); break; case PhrictionDocumentStatus::STATUS_STUB: $e_text = pht('A stub document can not be moved.'); break; default: $e_text = null; break; } if ($e_text) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Can not move document.'), $e_text, $xaction); $errors[] = $error; } // NOTE: We use the ominpotent user because we can't let users // overwrite documents even if they can't see them. $target_document = id(new PhrictionDocumentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSlugs(array($object->getSlug())) ->needContent(true) ->executeOne(); - // Considering to overwrite existing docs? Nuke this! + // Prevent overwrites and no-op moves. $exists = PhrictionDocumentStatus::STATUS_EXISTS; - if ($target_document && $target_document->getStatus() == $exists) { + if ($target_document) { + if ($target_document->getSlug() == $source_document->getSlug()) { + $message = pht( + 'You can not move a document to its existing location. '. + 'Choose a different location to move the document to.'); + } else if ($target_document->getStatus() == $exists) { + $message = pht( + 'You can not move this document there, because it would '. + 'overwrite an existing document which is already at that '. + 'location. Move or delete the existing document first.'); + } + $error = new PhabricatorApplicationTransactionValidationError( $type, - pht('Can not move document.'), - pht('Can not overwrite existing target document.'), + pht('Invalid'), + $message, $xaction); $errors[] = $error; } break; case PhrictionTransaction::TYPE_DELETE: switch ($object->getStatus()) { case PhrictionDocumentStatus::STATUS_DELETED: $e_text = pht('An already deleted document can not be deleted.'); break; case PhrictionDocumentStatus::STATUS_MOVED: $e_text = pht('A moved document can not be deleted.'); break; case PhrictionDocumentStatus::STATUS_STUB: $e_text = pht('A stub document can not be deleted.'); break; default: break 2; } $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Can not delete document.'), $e_text, $xaction); $errors[] = $error; break; } } return $errors; } private function validateContentVersion( PhabricatorLiskDAO $object, $type, PhabricatorApplicationTransaction $xaction) { $error = null; if ($this->getContentVersion() && ($object->getContent()->getVersion() != $this->getContentVersion())) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Edit Conflict'), pht( 'Another user made changes to this document after you began '. 'editing it. Do you want to overwrite their changes? '. '(If you choose to overwrite their changes, you should review '. 'the document edit history to see what you overwrote, and '. 'then make another edit to merge the changes if necessary.)'), $xaction); } return $error; } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { /* * New objects have a special case. If a user can't see * x/y * then definitely don't let them make some * x/y/z * We need to load the direct parent to handle this case. */ if ($this->getIsNewObject()) { $actor = $this->requireActor(); $parent_doc = null; $ancestral_slugs = PhabricatorSlug::getAncestry($object->getSlug()); // No ancestral slugs is "/"; the first person gets to play with "/". if ($ancestral_slugs) { $parent = end($ancestral_slugs); $parent_doc = id(new PhrictionDocumentQuery()) ->setViewer($actor) ->withSlugs(array($parent)) ->executeOne(); // If the $actor can't see the $parent_doc then they can't create // the child $object; throw a policy exception. if (!$parent_doc) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->raisePolicyExceptions(true) ->rejectObject( $object, $object->getEditPolicy(), PhabricatorPolicyCapability::CAN_EDIT); } // If the $actor can't edit the $parent_doc then they can't create // the child $object; throw a policy exception. if (!PhabricatorPolicyFilter::hasCapability( $actor, $parent_doc, PhabricatorPolicyCapability::CAN_EDIT)) { id(new PhabricatorPolicyFilter()) ->setViewer($actor) ->raisePolicyExceptions(true) ->rejectObject( $object, $object->getEditPolicy(), PhabricatorPolicyCapability::CAN_EDIT); } } } return parent::requireCapabilities($object, $xaction); } protected function supportsSearch() { return true; } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new PhrictionDocumentHeraldAdapter()) ->setDocument($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $xactions = array(); $cc_phids = $adapter->getCcPHIDs(); if ($cc_phids) { $value = array_fuse($cc_phids); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('+' => $value)); } $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); return $xactions; } private function buildNewContentTemplate( PhrictionDocument $document) { $new_content = id(new PhrictionContent()) ->setSlug($document->getSlug()) ->setAuthorPHID($this->getActor()->getPHID()) ->setChangeType(PhrictionChangeType::CHANGE_EDIT) ->setTitle($this->getOldContent()->getTitle()) ->setContent($this->getOldContent()->getContent()); if (strlen($this->getDescription())) { $new_content->setDescription($this->getDescription()); } $new_content->setVersion($this->getOldContent()->getVersion() + 1); return $new_content; } } diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php index 17e961ddbd..4e6d761dc2 100644 --- a/src/infrastructure/util/PhabricatorSlug.php +++ b/src/infrastructure/util/PhabricatorSlug.php @@ -1,76 +1,86 @@ ]+@", '_', $slug); $slug = preg_replace('@_+@', '_', $slug); - $slug = trim($slug, '_'); + + // Remove leading and trailing underscores from each component, if the + // component has not been reduced to a single underscore. For example, "a?" + // converts to "a", but "??" converts to "_". + $parts = explode('/', $slug); + foreach ($parts as $key => $part) { + if ($part != '_') { + $parts[$key] = trim($part, '_'); + } + } + $slug = implode('/', $parts); // Specifically rewrite these slugs. It's OK to have a slug like "a..b", // but not a slug which is only "..". // NOTE: These are explicitly not pht()'d, because they should be stable // across languages. $replace = array( '.' => 'dot', '..' => 'dotdot', ); foreach ($replace as $pattern => $replacement) { $pattern = preg_quote($pattern, '@'); $slug = preg_replace( '@(^|/)'.$pattern.'(\z|/)@', '\1'.$replacement.'\2', $slug); } return $slug.'/'; } public static function getDefaultTitle($slug) { $parts = explode('/', trim($slug, '/')); $default_title = end($parts); $default_title = str_replace('_', ' ', $default_title); $default_title = phutil_utf8_ucwords($default_title); $default_title = nonempty($default_title, pht('Untitled Document')); return $default_title; } public static function getAncestry($slug) { $slug = self::normalize($slug); if ($slug == '/') { return array(); } $ancestors = array( '/', ); $slug = explode('/', $slug); array_pop($slug); array_pop($slug); $accumulate = ''; foreach ($slug as $part) { $accumulate .= $part.'/'; $ancestors[] = $accumulate; } return $ancestors; } public static function getDepth($slug) { $slug = self::normalize($slug); if ($slug == '/') { return 0; } else { return substr_count($slug, '/'); } } } diff --git a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php index faefb51382..3c820a085e 100644 --- a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php +++ b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php @@ -1,70 +1,77 @@ '/', '/' => '/', '//' => '/', - '&&&' => '/', + '&&&' => '_/', '/derp/' => 'derp/', 'derp' => 'derp/', 'derp//derp' => 'derp/derp/', 'DERP//DERP' => 'derp/derp/', 'a B c' => 'a_b_c/', '-1~2.3abcd' => '-1~2.3abcd/', "T\x00O\x00D\x00O" => 't_o_d_o/', 'x#%&+=\\?<> y' => 'x_y/', "\xE2\x98\x83" => "\xE2\x98\x83/", '..' => 'dotdot/', '../' => 'dotdot/', '/../' => 'dotdot/', 'a/b' => 'a/b/', 'a//b' => 'a/b/', 'a/../b/' => 'a/dotdot/b/', '/../a' => 'dotdot/a/', '../a' => 'dotdot/a/', 'a/..' => 'a/dotdot/', 'a/../' => 'a/dotdot/', + 'a?' => 'a/', + '??' => '_/', + 'a/?' => 'a/_/', + '??/a/??' => '_/a/_/', + 'a/??/c' => 'a/_/c/', + 'a/?b/c' => 'a/b/c/', + 'a/b?/c' => 'a/b/c/', ); foreach ($slugs as $slug => $normal) { $this->assertEqual( $normal, PhabricatorSlug::normalize($slug), "Normalization of '{$slug}'"); } } public function testSlugAncestry() { $slugs = array( '/' => array(), 'pokemon/' => array('/'), 'pokemon/squirtle/' => array('/', 'pokemon/'), ); foreach ($slugs as $slug => $ancestry) { $this->assertEqual( $ancestry, PhabricatorSlug::getAncestry($slug), "Ancestry of '{$slug}'"); } } public function testSlugDepth() { $slugs = array( '/' => 0, 'a/' => 1, 'a/b/' => 2, 'a////b/' => 2, ); foreach ($slugs as $slug => $depth) { $this->assertEqual( $depth, PhabricatorSlug::getDepth($slug), "Depth of '{$slug}'"); } } }