diff --git a/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php b/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php index 6d8d975a79..892776ad51 100644 --- a/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php +++ b/src/applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php @@ -1,75 +1,76 @@ 'Content may not be empty.', 'ERR-INVALID-ENGINE' => 'Invalid markup engine.', ); } public function defineParamTypes() { $available_contexts = array_keys($this->getEngineContexts()); $available_contexts = implode(', ', $available_contexts); return array( 'context' => 'required enum<'.$available_contexts.'>', 'content' => 'required string', ); } protected function execute(ConduitAPIRequest $request) { $content = $request->getValue('content'); $context = $request->getValue('context'); $engine_class = idx($this->getEngineContexts(), $context); if (!$engine_class) { throw new ConduitException('ERR-INVALID_ENGINE'); } $engine = PhabricatorMarkupEngine::$engine_class(); + $engine->setConfig('viewer', $request->getUser()); $result = array( 'content' => $engine->markupText($content), ); return $result; } private function getEngineContexts() { return array( 'phriction' => 'newPhrictionMarkupEngine', 'maniphest' => 'newManiphestMarkupEngine', 'differential' => 'newDifferentialMarkupEngine', ); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php b/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php index eda546d22b..d28f209ab6 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php @@ -1,87 +1,88 @@ transactionID = $transaction_id; return $this; } public function getTransactionID() { return $this->transactionID; } public function willProcessRequest(array $data) { $this->setTransactionID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $is_show_more = false; if (!$this->getTransactionID()) { $this->setTransactionID($this->getRequest()->getStr('ref')); $is_show_more = true; } $transaction_id = $this->getTransactionID(); $transaction = id(new ManiphestTransaction())->load($transaction_id); if (!$transaction) { return new Aphront404Response(); } $transactions = array($transaction); $phids = array(); foreach ($transactions as $xaction) { foreach ($xaction->extractPHIDs() as $phid) { $phids[$phid] = $phid; } } $handles = $this->loadViewerHandles($phids); $engine = new PhabricatorMarkupEngine(); + $engine->setViewer($user); $engine->addObject($transaction, ManiphestTransaction::MARKUP_FIELD_BODY); $engine->process(); $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($transactions); $view->setHandles($handles); $view->setUser($user); $view->setMarkupEngine($engine); $view->setRenderSummaryOnly(true); $view->setRenderFullSummary(true); $view->setRangeSpecification($request->getStr('range')); if ($is_show_more) { return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($view->render()); } else { return id(new AphrontAjaxResponse())->setContent($view->render()); } } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php index 17a89f6991..35989c9e57 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php @@ -1,45 +1,45 @@ getRequest(); $description = $request->getStr('description'); $task = new ManiphestTask(); $task->setDescription($description); $output = PhabricatorMarkupEngine::renderOneObject( $task, - ManiphestTask::MARKUP_FIELD_DESCRIPTION); + ManiphestTask::MARKUP_FIELD_DESCRIPTION, + $request->getUser()); $content = '
'. $output. '
'; return id(new AphrontAjaxResponse()) ->setContent($content); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index cf1f15165a..12615caac5 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -1,555 +1,556 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $e_title = null; $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $task = id(new ManiphestTask())->load($this->id); if (!$task) { return new Aphront404Response(); } $workflow = $request->getStr('workflow'); $parent_task = null; if ($workflow && is_numeric($workflow)) { $parent_task = id(new ManiphestTask())->load($workflow); } $transactions = id(new ManiphestTransaction())->loadAllWhere( 'taskID = %d ORDER BY id ASC', $task->getID()); $e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT; $e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK; $e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK; $e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV; $phid = $task->getPHID(); $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($phid)) ->withEdgeTypes( array( $e_commit, $e_dep_on, $e_dep_by, $e_rev, )); $edges = $query->execute(); $commit_phids = array_keys($edges[$phid][$e_commit]); $dep_on_tasks = array_keys($edges[$phid][$e_dep_on]); $dep_by_tasks = array_keys($edges[$phid][$e_dep_by]); $revs = array_keys($edges[$phid][$e_rev]); $phids = array_fill_keys($query->getDestinationPHIDs(), true); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } foreach ($task->getCCPHIDs() as $phid) { $phids[$phid] = true; } foreach ($task->getProjectPHIDs() as $phid) { $phids[$phid] = true; } if ($task->getOwnerPHID()) { $phids[$task->getOwnerPHID()] = true; } $phids[$task->getAuthorPHID()] = true; $attached = $task->getAttached(); foreach ($attached as $type => $list) { foreach ($list as $phid => $info) { $phids[$phid] = true; } } if ($parent_task) { $phids[$parent_task->getPHID()] = true; } $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $dict = array(); $dict['Status'] = ''. ManiphestTaskStatus::getTaskStatusFullName($task->getStatus()). ''; $dict['Assigned To'] = $task->getOwnerPHID() ? $handles[$task->getOwnerPHID()]->renderLink() : 'None'; $dict['Priority'] = ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()); $cc = $task->getCCPHIDs(); if ($cc) { $cc_links = array(); foreach ($cc as $phid) { $cc_links[] = $handles[$phid]->renderLink(); } $dict['CC'] = implode(', ', $cc_links); } else { $dict['CC'] = 'None'; } $dict['Author'] = $handles[$task->getAuthorPHID()]->renderLink(); $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); $dict['From Email'] = phutil_render_tag( 'a', array( 'href' => 'mailto:'.$source.'?subject='.$subject ), phutil_escape_html($source)); } $projects = $task->getProjectPHIDs(); if ($projects) { $project_links = array(); foreach ($projects as $phid) { $project_links[] = $handles[$phid]->renderLink(); } $dict['Projects'] = implode(', ', $project_links); } else { $dict['Projects'] = 'None'; } $extensions = ManiphestTaskExtensions::newExtensions(); $aux_fields = $extensions->getAuxiliaryFieldSpecifications(); if ($aux_fields) { $task->loadAndAttachAuxiliaryAttributes(); foreach ($aux_fields as $aux_field) { $aux_key = $aux_field->getAuxiliaryKey(); $aux_field->setValue($task->getAuxiliaryAttribute($aux_key)); $value = $aux_field->renderForDetailView(); if (strlen($value)) { $dict[$aux_field->getLabel()] = $value; } } } if ($dep_by_tasks) { $dict['Dependent Tasks'] = $this->renderHandleList( array_select_keys($handles, $dep_by_tasks)); } if ($dep_on_tasks) { $dict['Depends On'] = $this->renderHandleList( array_select_keys($handles, $dep_on_tasks)); } if ($revs) { $dict['Revisions'] = $this->renderHandleList( array_select_keys($handles, $revs)); } if ($commit_phids) { $dict['Commits'] = $this->renderHandleList( array_select_keys($handles, $commit_phids)); } $file_infos = idx($attached, PhabricatorPHIDConstants::PHID_TYPE_FILE); if ($file_infos) { $file_phids = array_keys($file_infos); $files = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $file_phids); $views = array(); foreach ($files as $file) { $view = new AphrontFilePreviewView(); $view->setFile($file); $views[] = $view->render(); } $dict['Files'] = implode('', $views); } $context_bar = null; if ($parent_task) { $context_bar = new AphrontContextBarView(); $context_bar->addButton( phutil_render_tag( 'a', array( 'href' => '/maniphest/task/create/?parent='.$parent_task->getID(), 'class' => 'green button', ), 'Create Another Subtask')); $context_bar->appendChild( 'Created a subtask of '. $handles[$parent_task->getPHID()]->renderLink(). ''); } else if ($workflow == 'create') { $context_bar = new AphrontContextBarView(); $context_bar->addButton(''); $context_bar->addButton( phutil_render_tag( 'a', array( 'href' => '/maniphest/task/create/?template='.$task->getID(), 'class' => 'green button', ), 'Similar Task')); $context_bar->addButton( phutil_render_tag( 'a', array( 'href' => '/maniphest/task/create/', 'class' => 'green button', ), 'Empty Task')); $context_bar->appendChild('New task created.'); } $actions = array(); $action = new AphrontHeadsupActionView(); $action->setName('Edit Task'); $action->setURI('/maniphest/task/edit/'.$task->getID().'/'); $action->setClass('action-edit'); $actions[] = $action; require_celerity_resource('phabricator-flag-css'); $flag = PhabricatorFlagQuery::loadUserFlag($user, $task->getPHID()); if ($flag) { $class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $color = PhabricatorFlagColor::getColorName($flag->getColor()); $action = new AphrontHeadsupActionView(); $action->setClass('flag-clear '.$class); $action->setURI('/flag/delete/'.$flag->getID().'/'); $action->setName('Remove '.$color.' Flag'); $action->setWorkflow(true); $actions[] = $action; } else { $action = new AphrontHeadsupActionView(); $action->setClass('phabricator-flag-ghost'); $action->setURI('/flag/edit/'.$task->getPHID().'/'); $action->setName('Flag Task'); $action->setWorkflow(true); $actions[] = $action; } require_celerity_resource('phabricator-object-selector-css'); require_celerity_resource('javelin-behavior-phabricator-object-selector'); $action = new AphrontHeadsupActionView(); $action->setName('Merge Duplicates'); $action->setURI('/search/attach/'.$task->getPHID().'/TASK/merge/'); $action->setWorkflow(true); $action->setClass('action-merge'); $actions[] = $action; $action = new AphrontHeadsupActionView(); $action->setName('Create Subtask'); $action->setURI('/maniphest/task/create/?parent='.$task->getID()); $action->setClass('action-branch'); $actions[] = $action; $action = new AphrontHeadsupActionView(); $action->setName('Edit Dependencies'); $action->setURI('/search/attach/'.$task->getPHID().'/TASK/dependencies/'); $action->setWorkflow(true); $action->setClass('action-dependencies'); $actions[] = $action; $action = new AphrontHeadsupActionView(); $action->setName('Edit Differential Revisions'); $action->setURI('/search/attach/'.$task->getPHID().'/DREV/'); $action->setWorkflow(true); $action->setClass('action-attach'); $actions[] = $action; $action_list = new AphrontHeadsupActionListView(); $action_list->setActions($actions); $headsup_panel = new AphrontHeadsupView(); $headsup_panel->setObjectName('T'.$task->getID()); $headsup_panel->setHeader($task->getTitle()); $headsup_panel->setActionList($action_list); $headsup_panel->setProperties($dict); $engine = new PhabricatorMarkupEngine(); + $engine->setViewer($user); $engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION); foreach ($transactions as $xaction) { if ($xaction->hasComments()) { $engine->addObject($xaction, ManiphestTransaction::MARKUP_FIELD_BODY); } } $engine->process(); $headsup_panel->appendChild( '
'. $engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION). '
'); $transaction_types = ManiphestTransactionType::getTransactionTypeMap(); $resolution_types = ManiphestTaskStatus::getTaskStatusMap(); if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) { $resolution_types = array_select_keys( $resolution_types, array( ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, ManiphestTaskStatus::STATUS_CLOSED_INVALID, ManiphestTaskStatus::STATUS_CLOSED_SPITE, )); } else { $resolution_types = array( ManiphestTaskStatus::STATUS_OPEN => 'Reopened', ); $transaction_types[ManiphestTransactionType::TYPE_STATUS] = 'Reopen Task'; unset($transaction_types[ManiphestTransactionType::TYPE_PRIORITY]); unset($transaction_types[ManiphestTransactionType::TYPE_OWNER]); } $default_claim = array( $user->getPHID() => $user->getUsername().' ('.$user->getRealName().')', ); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if ($draft) { $draft_text = $draft->getDraft(); } else { $draft_text = null; } $panel_id = celerity_generate_unique_node_id(); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { // Prevent tasks from being closed "out of spite" in serious business // installs. unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]); } $remarkup_href = PhabricatorEnv::getDoclink( 'article/Remarkup_Reference.html'); $comment_form = new AphrontFormView(); $comment_form ->setUser($user) ->setAction('/maniphest/transaction/save/') ->setEncType('multipart/form-data') ->addHiddenInput('taskID', $task->getID()) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Action') ->setName('action') ->setOptions($transaction_types) ->setID('transaction-action')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Resolution') ->setName('resolution') ->setControlID('resolution') ->setControlStyle('display: none') ->setOptions($resolution_types)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Assign To') ->setName('assign_to') ->setControlID('assign_to') ->setControlStyle('display: none') ->setID('assign-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('CCs') ->setName('ccs') ->setControlID('ccs') ->setControlStyle('display: none') ->setID('cc-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Priority') ->setName('priority') ->setOptions($priority_map) ->setControlID('priority') ->setControlStyle('display: none') ->setValue($task->getPriority())) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Projects') ->setName('projects') ->setControlID('projects') ->setControlStyle('display: none') ->setID('projects-tokenizer') ->setDisableBehavior(true)) ->appendChild( id(new AphrontFormFileControl()) ->setLabel('File') ->setName('file') ->setControlID('file') ->setControlStyle('display: none')) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Comments') ->setName('comments') ->setValue($draft_text) ->setCaption( phutil_render_tag( 'a', array( 'href' => $remarkup_href, 'tabindex' => '-1', 'target' => '_blank', ), 'Formatting Reference')) ->setID('transaction-comments')) ->appendChild( id(new AphrontFormDragAndDropUploadControl()) ->setLabel('Attached Files') ->setName('files') ->setDragAndDropTarget($panel_id) ->setActivatedClass('aphront-panel-view-drag-and-drop')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($is_serious ? 'Submit' : 'Avast!')); $control_map = array( ManiphestTransactionType::TYPE_STATUS => 'resolution', ManiphestTransactionType::TYPE_OWNER => 'assign_to', ManiphestTransactionType::TYPE_CCS => 'ccs', ManiphestTransactionType::TYPE_PRIORITY => 'priority', ManiphestTransactionType::TYPE_PROJECTS => 'projects', ManiphestTransactionType::TYPE_ATTACH => 'file', ); $tokenizer_map = array( ManiphestTransactionType::TYPE_PROJECTS => array( 'id' => 'projects-tokenizer', 'src' => '/typeahead/common/projects/', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => 'Type a project name...', ), ManiphestTransactionType::TYPE_OWNER => array( 'id' => 'assign-tokenizer', 'src' => '/typeahead/common/users/', 'value' => $default_claim, 'limit' => 1, 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => 'Type a user name...', ), ManiphestTransactionType::TYPE_CCS => array( 'id' => 'cc-tokenizer', 'src' => '/typeahead/common/mailable/', 'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'), 'placeholder' => 'Type a user or mailing list...', ), ); Javelin::initBehavior('maniphest-transaction-controls', array( 'select' => 'transaction-action', 'controlMap' => $control_map, 'tokenizers' => $tokenizer_map, )); Javelin::initBehavior('maniphest-transaction-preview', array( 'uri' => '/maniphest/transaction/preview/'.$task->getID().'/', 'preview' => 'transaction-preview', 'comments' => 'transaction-comments', 'action' => 'transaction-action', 'map' => $control_map, 'tokenizers' => $tokenizer_map, )); $comment_panel = new AphrontPanelView(); $comment_panel->appendChild($comment_form); $comment_panel->setID($panel_id); $comment_panel->addClass('aphront-panel-accent'); $comment_panel->setHeader($is_serious ? 'Add Comment' : 'Weigh In'); $preview_panel = '
Loading preview...
'; $transaction_view = new ManiphestTransactionListView(); $transaction_view->setTransactions($transactions); $transaction_view->setHandles($handles); $transaction_view->setUser($user); $transaction_view->setAuxiliaryFields($aux_fields); $transaction_view->setMarkupEngine($engine); PhabricatorFeedStoryNotification::updateObjectNotificationViews( $user, $task->getPHID()); return $this->buildStandardPageResponse( array( $context_bar, $headsup_panel, $transaction_view, $comment_panel, $preview_panel, ), array( 'title' => 'T'.$task->getID().' '.$task->getTitle(), 'pageObjects' => array($task->getPHID()), )); } private function renderHandleList(array $handles) { $links = array(); foreach ($handles as $handle) { $links[] = $handle->renderLink(); } return implode('
', $links); } } diff --git a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php index 82c41f6ea4..2206933e78 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php @@ -1,136 +1,137 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $comments = $request->getStr('comments'); $task = id(new ManiphestTask())->load($this->id); if (!$task) { return new Aphront404Response(); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if (!$draft) { $draft = new PhabricatorDraft(); $draft->setAuthorPHID($user->getPHID()); $draft->setDraftKey($task->getPHID()); } $draft->setDraft($comments); $draft->save(); $action = $request->getStr('action'); $transaction = new ManiphestTransaction(); $transaction->setAuthorPHID($user->getPHID()); $transaction->setComments($comments); $transaction->setTransactionType($action); $value = $request->getStr('value'); // grab phids for handles and set transaction values based on action and // value (empty or control-specific format) coming in from the wire switch ($action) { case ManiphestTransactionType::TYPE_PRIORITY: $transaction->setOldValue($task->getPriority()); $transaction->setNewValue($value); break; case ManiphestTransactionType::TYPE_OWNER: if ($value) { $value = current(json_decode($value)); $phids = array($value); } else { $phids = array(); } $transaction->setNewValue($value); break; case ManiphestTransactionType::TYPE_CCS: if ($value) { $value = json_decode($value); $phids = $value; foreach ($task->getCCPHIDs() as $cc_phid) { $phids[] = $cc_phid; $value[] = $cc_phid; } $transaction->setNewValue($value); } else { $phids = array(); $transaction->setNewValue(array()); } $transaction->setOldValue($task->getCCPHIDs()); break; case ManiphestTransactionType::TYPE_PROJECTS: if ($value) { $value = json_decode($value); $phids = $value; foreach ($task->getProjectPHIDs() as $project_phid) { $phids[] = $project_phid; $value[] = $project_phid; } $transaction->setNewValue($value); } else { $phids = array(); $transaction->setNewValue(array()); } $transaction->setOldValue($task->getProjectPHIDs()); break; default: $phids = array(); $transaction->setNewValue($value); break; } $phids[] = $user->getPHID(); $handles = $this->loadViewerHandles($phids); $transactions = array(); $transactions[] = $transaction; $engine = new PhabricatorMarkupEngine(); + $engine->setViewer($user); $engine->addObject($transaction, ManiphestTransaction::MARKUP_FIELD_BODY); $engine->process(); $transaction_view = new ManiphestTransactionListView(); $transaction_view->setTransactions($transactions); $transaction_view->setHandles($handles); $transaction_view->setUser($user); $transaction_view->setMarkupEngine($engine); $transaction_view->setPreview(true); return id(new AphrontAjaxResponse()) ->setContent($transaction_view->render()); } } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 5f3253d44e..3ff4ea3b60 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,399 +1,399 @@ slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $slug = PhabricatorSlug::normalize($this->slug); if ($slug != $this->slug) { $uri = PhrictionDocument::getSlugURI($slug); // Canonicalize pages to their one true URI. return id(new AphrontRedirectResponse())->setURI($uri); } require_celerity_resource('phriction-document-css'); $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $breadcrumbs = $this->renderBreadcrumbs($slug); $version_note = null; if (!$document) { if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProject())->loadOneWhere( 'phrictionSlug = %s', PhrictionDocument::getProjectSlugIdentifier($slug)); if (!$project) { return new Aphront404Response(); } } $create_uri = '/phriction/edit/?slug='.$slug; $create_sentence = 'You can '. phutil_render_tag( 'a', array( 'href' => $create_uri, ), 'create a new document'). '.'; $button = phutil_render_tag( 'a', array( 'href' => $create_uri, 'class' => 'green button', ), 'Create Page'); $page_content = '
'. 'No content here!
'. 'No document found at '.phutil_escape_html($slug).'. '. $create_sentence. '
'; $page_title = 'Page Not Found'; $buttons = $button; } else { $version = $request->getInt('v'); if ($version) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $version); if (!$content) { return new Aphront404Response(); } if ($content->getID() != $document->getContentID()) { $version_note = new AphrontErrorView(); $version_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $version_note->setTitle('Older Version'); $version_note->appendChild( 'You are viewing an older version of this document, as it '. 'appeared on '. phabricator_datetime($content->getDateCreated(), $user).'.'); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } $page_title = $content->getTitle(); $project_phid = null; if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProject())->loadOneWhere( 'phrictionSlug = %s', PhrictionDocument::getProjectSlugIdentifier($slug)); if ($project) { $project_phid = $project->getPHID(); } } $phids = array_filter( array( $content->getAuthorPHID(), $project_phid, )); $handles = $this->loadViewerHandles($phids); $age = time() - $content->getDateCreated(); $age = floor($age / (60 * 60 * 24)); if ($age < 1) { $when = 'today'; } else if ($age == 1) { $when = 'yesterday'; } else { $when = "{$age} days ago"; } $project_info = null; if ($project_phid) { $project_info = '
This document is about the project '. $handles[$project_phid]->renderLink().'.'; } $byline = '
'. "Last updated {$when} by ". $handles[$content->getAuthorPHID()]->renderLink().'.'. $project_info. '
'; $doc_status = $document->getStatus(); if ($doc_status == PhrictionDocumentStatus::STATUS_EXISTS) { - $core_content = $content->renderContent(); + $core_content = $content->renderContent($user); } else if ($doc_status == PhrictionDocumentStatus::STATUS_DELETED) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle('Document Deleted'); $notice->appendChild( 'This document has been deleted. You can edit it to put new content '. 'here, or use history to revert to an earlier version.'); $core_content = $notice->render(); } else { throw new Exception("Unknown document status '{$doc_status}'!"); } $page_content = '
'. $byline. $core_content. '
'; $edit_button = phutil_render_tag( 'a', array( 'href' => '/phriction/edit/'.$document->getID().'/', 'class' => 'button', ), 'Edit Document'); $history_button = phutil_render_tag( 'a', array( 'href' => PhrictionDocument::getSlugURI($slug, 'history'), 'class' => 'button grey', ), 'View History'); // these float right so history_button which is right most goes first $buttons = $history_button.$edit_button; } if ($version_note) { $version_note = $version_note->render(); } $children = $this->renderChildren($slug); $page = '
'. $buttons. '

'.phutil_escape_html($page_title).'

'. $breadcrumbs. '
'. $version_note. $page_content. $children; return $this->buildStandardPageResponse( $page, array( 'title' => 'Phriction - '.$page_title, )); } private function renderBreadcrumbs($slug) { $ancestor_handles = array(); $ancestral_slugs = PhabricatorSlug::getAncestry($slug); $ancestral_slugs[] = $slug; if ($ancestral_slugs) { $empty_slugs = array_fill_keys($ancestral_slugs, null); $ancestors = id(new PhrictionDocument())->loadAllWhere( 'slug IN (%Ls)', $ancestral_slugs); $ancestors = mpull($ancestors, null, 'getSlug'); $ancestor_phids = mpull($ancestors, 'getPHID'); $handles = array(); if ($ancestor_phids) { $handles = $this->loadViewerHandles($ancestor_phids); } $ancestor_handles = array(); foreach ($ancestral_slugs as $slug) { if (isset($ancestors[$slug])) { $ancestor_handles[] = $handles[$ancestors[$slug]->getPHID()]; } else { $handle = new PhabricatorObjectHandle(); $handle->setName(PhabricatorSlug::getDefaultTitle($slug)); $handle->setURI(PhrictionDocument::getSlugURI($slug)); $ancestor_handles[] = $handle; } } } $breadcrumbs = array(); foreach ($ancestor_handles as $ancestor_handle) { $breadcrumbs[] = $ancestor_handle->renderLink(); } $list = phutil_render_tag( 'a', array( 'href' => '/phriction/', ), 'Document Index'); return '
'. $list.' · '. ''. implode(" \xC2\xBB ", $breadcrumbs). ''. '
'; } private function renderChildren($slug) { $document_dao = new PhrictionDocument(); $content_dao = new PhrictionContent(); $conn = $document_dao->establishConnection('r'); $limit = 50; $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; // Select children and grandchildren. $children = queryfx_all( $conn, 'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c ON d.contentID = c.id WHERE d.slug LIKE %> AND d.depth IN (%d, %d) AND d.status = %d ORDER BY d.depth, c.title LIMIT %d', $document_dao->getTableName(), $content_dao->getTableName(), ($slug == '/' ? '' : $slug), $d_child, $d_grandchild, PhrictionDocumentStatus::STATUS_EXISTS, $limit); if (!$children) { return; } // We're going to render in one of three modes to try to accommodate // different information scales: // // - If we found fewer than $limit rows, we know we have all the children // and grandchildren and there aren't all that many. We can just render // everything. // - If we found $limit rows but the results included some grandchildren, // we just throw them out and render only the children, as we know we // have them all. // - If we found $limit rows and the results have no grandchildren, we // have a ton of children. Render them and then let the user know that // this is not an exhaustive list. if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { if ($child['depth'] == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } $grandchildren = array(); foreach ($children as $key => $child) { if ($child['depth'] == $d_child) { continue; } else { unset($children[$key]); if ($show_grandchildren) { $ancestors = PhabricatorSlug::getAncestry($child['slug']); $grandchildren[end($ancestors)][] = $child; } } } // Fill in any missing children. $known_slugs = ipull($children, null, 'slug'); foreach ($grandchildren as $slug => $ignored) { if (empty($known_slugs[$slug])) { $children[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } $children = isort($children, 'title'); $list = array(); $list[] = ''; $list = implode("\n", $list); return '
'. '
Document Hierarchy
'. $list. '
'; } private function renderChildDocumentLink(array $info) { $title = nonempty($info['title'], '(Untitled Document)'); $item = phutil_render_tag( 'a', array( 'href' => PhrictionDocument::getSlugURI($info['slug']), ), phutil_escape_html($title)); if (isset($info['empty'])) { $item = ''.$item.''; } return '
  • '.$item.'
  • '; } } diff --git a/src/applications/phriction/controller/PhrictionDocumentPreviewController.php b/src/applications/phriction/controller/PhrictionDocumentPreviewController.php index 3790a67cf2..e5583cc890 100644 --- a/src/applications/phriction/controller/PhrictionDocumentPreviewController.php +++ b/src/applications/phriction/controller/PhrictionDocumentPreviewController.php @@ -1,51 +1,49 @@ getRequest(); $document = $request->getStr('document'); $draft_key = $request->getStr('draftkey'); if ($draft_key) { $table = new PhabricatorDraft(); queryfx( $table->establishConnection('w'), 'INSERT INTO %T (authorPHID, draftKey, draft) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE draft = VALUES(draft)', $table->getTableName(), $request->getUser()->getPHID(), $draft_key, $document); } $content_obj = new PhrictionContent(); $content_obj->setContent($document); - - $engine = PhabricatorMarkupEngine::newPhrictionMarkupEngine(); - $content = $content_obj->renderContent(); + $content = $content_obj->renderContent($request->getUser()); return id(new AphrontAjaxResponse())->setContent($content); } } diff --git a/src/applications/phriction/storage/PhrictionContent.php b/src/applications/phriction/storage/PhrictionContent.php index da84fb1ce0..18d907db69 100644 --- a/src/applications/phriction/storage/PhrictionContent.php +++ b/src/applications/phriction/storage/PhrictionContent.php @@ -1,118 +1,119 @@ shouldUseMarkupCache($field)) { $id = $this->getID(); } else { $id = PhabricatorHash::digest($this->getMarkupText($field)); } return "phriction:{$field}:{$id}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getContent(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newPhrictionMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { $toc = PhutilRemarkupEngineRemarkupHeaderBlockRule::renderTableOfContents( $engine); if ($toc) { $toc = '
    '. '
    '. 'Table of Contents'. '
    '. $toc. '
    '; } return '
    '. $toc. $output. '
    '; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } } diff --git a/src/applications/ponder/view/PonderCommentBodyView.php b/src/applications/ponder/view/PonderCommentBodyView.php index 4bf0a57ad3..b35407414f 100644 --- a/src/applications/ponder/view/PonderCommentBodyView.php +++ b/src/applications/ponder/view/PonderCommentBodyView.php @@ -1,149 +1,150 @@ question = $question; return $this; } public function setTarget($target) { $this->target = $target; return $this; } public function setAction($action) { $this->action = $action; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function render() { if (!$this->user) { throw new Exception("Call setUser() before rendering!"); } require_celerity_resource('phabricator-remarkup-css'); require_celerity_resource('ponder-post-css'); $user = $this->user; $question = $this->question; $target = $this->target; $content = $target->getContent(); $info = array(); $content = PhabricatorMarkupEngine::renderOneObject( $target, - $target->getMarkupField()); + $target->getMarkupField(), + $this->user); $content = '
    '. $content. '
    '; $author = $this->handles[$target->getAuthorPHID()]; $actions = array($author->renderLink().' '.$this->action); $author_link = $author->renderLink(); $xaction_view = id(new PhabricatorTransactionView()) ->setUser($user) ->setImageURI($author->getImageURI()) ->setContentSource($target->getContentSource()) ->setActions($actions); if ($this->target instanceof PonderAnswer) { $xaction_view->addClass("ponder-answer"); } else { $xaction_view->addClass("ponder-question"); } if ($this->preview) { $xaction_view->setIsPreview($this->preview); } else { $xaction_view->setEpoch($target->getDateCreated()); if ($this->target instanceof PonderAnswer) { $anchor_text = 'Q' . $question->getID(). '#A' . $target->getID(); $xaction_view->setAnchor('A'.$target->getID(), $anchor_text); $xaction_view->addClass("ponder-answer"); } } $xaction_view->appendChild( '
    '. $content. '
    ' ); $outerview = $xaction_view; if (!$this->preview) { $outerview = id(new PonderVotableView()) ->setPHID($target->getPHID()) ->setCount($target->getVoteCount()) ->setVote($target->getUserVote()); if ($this->target instanceof PonderAnswer) { $outerview->setURI('/ponder/answer/vote/'); } else { $outerview->setURI('/ponder/question/vote/'); } $outerview->appendChild($xaction_view); } return $outerview->render(); } private function renderHandleList(array $phids) { $result = array(); foreach ($phids as $phid) { $result[] = $this->handles[$phid]->renderLink(); } return implode(', ', $result); } } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index f9e11d520d..3a3c297d1e 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -1,479 +1,498 @@ addObject($comment, $field); * } * * Now, call @{method:process} to perform the actual cache/rendering * step. This is a heavyweight call which does batched data access and * transforms the markup into output. * * $engine->process(); * * Finally, do something with the results: * * $results = array(); * foreach ($comments as $comment) { * $results[] = $engine->getOutput($comment, $field); * } * * If you have a single object to render, you can use the convenience method * @{method:renderOneObject}. * * @task markup Markup Pipeline * @task engine Engine Construction */ final class PhabricatorMarkupEngine { private $objects = array(); + private $viewer; /* -( Markup Pipeline )---------------------------------------------------- */ /** * Convenience method for pushing a single object through the markup * pipeline. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. + * @param PhabricatorUser User viewing the markup. * @return string Marked up output. * @task markup */ public static function renderOneObject( PhabricatorMarkupInterface $object, - $field) { + $field, + PhabricatorUser $viewer) { return id(new PhabricatorMarkupEngine()) + ->setViewer($viewer) ->addObject($object, $field) ->process() ->getOutput($object, $field); } /** * Queue an object for markup generation when @{method:process} is * called. You can retrieve the output later with @{method:getOutput}. * * @param PhabricatorMarkupInterface The object to render. * @param string The field to render. * @return this * @task markup */ public function addObject(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); $this->objects[$key] = array( 'object' => $object, 'field' => $field, ); return $this; } /** * Process objects queued with @{method:addObject}. You can then retrieve * the output with @{method:getOutput}. * * @return this * @task markup */ public function process() { $keys = array(); foreach ($this->objects as $key => $info) { if (!isset($info['markup'])) { $keys[] = $key; } } if (!$keys) { return; } $objects = array_select_keys($this->objects, $keys); // Build all the markup engines. We need an engine for each field whether // we have a cache or not, since we still need to postprocess the cache. $engines = array(); foreach ($objects as $key => $info) { $engines[$key] = $info['object']->newMarkupEngine($info['field']); + $engines[$key]->setConfig('viewer', $this->viewer); } // Load or build the preprocessor caches. $blocks = $this->loadPreprocessorCaches($engines, $objects); // Finalize the output. foreach ($objects as $key => $info) { $data = $blocks[$key]->getCacheData(); $engine = $engines[$key]; $field = $info['field']; $object = $info['object']; $output = $engine->postprocessText($data); $output = $object->didMarkupText($field, $output, $engine); $this->objects[$key]['output'] = $output; } return $this; } /** * Get the output of markup processing for a field queued with * @{method:addObject}. Before you can call this method, you must call * @{method:process}. * * @param PhabricatorMarkupInterface The object to retrieve. * @param string The field to retrieve. * @return string Processed output. * @task markup */ public function getOutput(PhabricatorMarkupInterface $object, $field) { $key = $this->getMarkupFieldKey($object, $field); if (empty($this->objects[$key])) { throw new Exception( "Call addObject() before getOutput() (key = '{$key}')."); } if (!isset($this->objects[$key]['output'])) { throw new Exception( "Call process() before getOutput()."); } return $this->objects[$key]['output']; } /** * @task markup */ private function getMarkupFieldKey( PhabricatorMarkupInterface $object, $field) { return $object->getMarkupFieldKey($field); } /** * @task markup */ private function loadPreprocessorCaches(array $engines, array $objects) { $blocks = array(); $use_cache = array(); foreach ($objects as $key => $info) { if ($info['object']->shouldUseMarkupCache($info['field'])) { $use_cache[$key] = true; } } if ($use_cache) { $blocks = id(new PhabricatorMarkupCache())->loadAllWhere( 'cacheKey IN (%Ls)', array_keys($use_cache)); $blocks = mpull($blocks, null, 'getCacheKey'); } foreach ($objects as $key => $info) { if (isset($blocks[$key])) { // If we already have a preprocessing cache, we don't need to rebuild // it. continue; } $text = $info['object']->getMarkupText($info['field']); $data = $engines[$key]->preprocessText($text); // NOTE: This is just debugging information to help sort out cache issues. // If one machine is misconfigured and poisoning caches you can use this // field to hunt it down. $metadata = array( 'host' => php_uname('n'), ); $blocks[$key] = id(new PhabricatorMarkupCache()) ->setCacheKey($key) ->setCacheData($data) ->setMetadata($metadata); if (isset($use_cache[$key])) { // This is just filling a cache and always safe, even on a read pathway. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $blocks[$key]->save(); } catch (AphrontQueryDuplicateKeyException $ex) { // Ignore this, we just raced to write the cache. } unset($unguarded); } } return $blocks; } + /** + * Set the viewing user. Used to implement object permissions. + * + * @param PhabricatorUser The viewing user. + * @return this + * @task markup + */ + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + /* -( Engine Construction )------------------------------------------------ */ + /** * @task engine */ public static function newManiphestMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newPhrictionMarkupEngine() { return self::newMarkupEngine(array( 'header.generate-toc' => true, )); } /** * @task engine */ public static function newPhameMarkupEngine() { return self::newMarkupEngine(array( 'macros' => false, )); } /** * @task engine */ public static function newFeedMarkupEngine() { return self::newMarkupEngine( array( 'macros' => false, 'fileproxy' => false, 'youtube' => false, )); } /** * @task engine */ public static function newDifferentialMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( 'custom-inline' => PhabricatorEnv::getEnvConfig( 'differential.custom-remarkup-rules'), 'custom-block' => PhabricatorEnv::getEnvConfig( 'differential.custom-remarkup-block-rules'), 'differential.diff' => idx($options, 'differential.diff'), )); } /** * @task engine */ public static function newDiffusionMarkupEngine(array $options = array()) { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newProfileMarkupEngine() { return self::newMarkupEngine(array( )); } /** * @task engine */ public static function newSlowvoteMarkupEngine() { return self::newMarkupEngine(array( )); } public static function newPonderMarkupEngine(array $options = array()) { return self::newMarkupEngine($options); } /** * @task engine */ private static function getMarkupEngineDefaultConfiguration() { return array( 'pygments' => PhabricatorEnv::getEnvConfig('pygments.enabled'), 'fileproxy' => PhabricatorEnv::getEnvConfig('files.enable-proxy'), 'youtube' => PhabricatorEnv::getEnvConfig( 'remarkup.enable-embedded-youtube'), 'custom-inline' => array(), 'custom-block' => array(), 'differential.diff' => null, 'header.generate-toc' => false, 'macros' => true, 'uri.allowed-protocols' => PhabricatorEnv::getEnvConfig( 'uri.allowed-protocols'), 'syntax-highlighter.engine' => PhabricatorEnv::getEnvConfig( 'syntax-highlighter.engine'), ); } /** * @task engine */ private static function newMarkupEngine(array $options) { $options += self::getMarkupEngineDefaultConfiguration(); $engine = new PhutilRemarkupEngine(); $engine->setConfig('preserve-linebreaks', true); $engine->setConfig('pygments.enabled', $options['pygments']); $engine->setConfig( 'uri.allowed-protocols', $options['uri.allowed-protocols']); $engine->setConfig('differential.diff', $options['differential.diff']); $engine->setConfig('header.generate-toc', $options['header.generate-toc']); $engine->setConfig( 'syntax-highlighter.engine', $options['syntax-highlighter.engine']); $rules = array(); $rules[] = new PhutilRemarkupRuleEscapeRemarkup(); $rules[] = new PhutilRemarkupRuleMonospace(); $custom_rule_classes = $options['custom-inline']; if ($custom_rule_classes) { foreach ($custom_rule_classes as $custom_rule_class) { $rules[] = newv($custom_rule_class, array()); } } $rules[] = new PhutilRemarkupRuleDocumentLink(); if ($options['fileproxy']) { $rules[] = new PhabricatorRemarkupRuleProxyImage(); } if ($options['youtube']) { $rules[] = new PhabricatorRemarkupRuleYoutube(); } $rules[] = new PhutilRemarkupRuleHyperlink(); $rules[] = new PhabricatorRemarkupRulePhriction(); $rules[] = new PhabricatorRemarkupRuleDifferentialHandle(); $rules[] = new PhabricatorRemarkupRuleManiphestHandle(); $rules[] = new PhabricatorRemarkupRuleEmbedFile(); $rules[] = new PhabricatorRemarkupRuleDifferential(); $rules[] = new PhabricatorRemarkupRuleDiffusion(); $rules[] = new PhabricatorRemarkupRuleManiphest(); $rules[] = new PhabricatorRemarkupRulePaste(); $rules[] = new PhabricatorRemarkupRuleCountdown(); $rules[] = new PonderRuleQuestion(); if ($options['macros']) { $rules[] = new PhabricatorRemarkupRuleImageMacro(); } $rules[] = new PhabricatorRemarkupRuleMention(); $rules[] = new PhutilRemarkupRuleEscapeHTML(); $rules[] = new PhutilRemarkupRuleBold(); $rules[] = new PhutilRemarkupRuleItalic(); $rules[] = new PhutilRemarkupRuleDel(); $blocks = array(); $blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupLiteralBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupListBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupNoteBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupTableBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupDefaultBlockRule(); $custom_block_rule_classes = $options['custom-block']; if ($custom_block_rule_classes) { foreach ($custom_block_rule_classes as $custom_block_rule_class) { $blocks[] = newv($custom_block_rule_class, array()); } } foreach ($blocks as $block) { if ($block instanceof PhutilRemarkupEngineRemarkupLiteralBlockRule) { $literal_rules = array(); $literal_rules[] = new PhutilRemarkupRuleEscapeHTML(); $literal_rules[] = new PhutilRemarkupRuleLinebreaks(); $block->setMarkupRules($literal_rules); } else if ( !($block instanceof PhutilRemarkupEngineRemarkupCodeBlockRule)) { $block->setMarkupRules($rules); } } $engine->setBlockRules($blocks); return $engine; } public static function extractPHIDsFromMentions(array $content_blocks) { $mentions = array(); $engine = self::newDifferentialMarkupEngine(); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $phids = $engine->getTextMetadata( PhabricatorRemarkupRuleMention::KEY_MENTIONED, array()); $mentions += $phids; } return $mentions; } }