diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index a7f40496ee..714981ef0f 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -1,140 +1,153 @@ identifiers = $identifiers; return $this; } + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + public function loadPage() { $table = new PhabricatorRepositoryCommit(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } public function willFilterPage(array $commits) { if (!$commits) { return array(); } $repository_ids = mpull($commits, 'getRepositoryID', 'getRepositoryID'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withIDs($repository_ids) ->execute(); foreach ($commits as $key => $commit) { $repo = idx($repos, $commit->getRepositoryID()); if ($repo) { $commit->attachRepository($repo); } else { unset($commits[$key]); } } return $commits; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->identifiers) { $min_unqualified = PhabricatorRepository::MINIMUM_UNQUALIFIED_HASH; $min_qualified = PhabricatorRepository::MINIMUM_QUALIFIED_HASH; $refs = array(); $bare = array(); foreach ($this->identifiers as $identifier) { $matches = null; preg_match('/^(?:r([A-Z]+))?(.*)$/', $identifier, $matches); $repo = nonempty($matches[1], null); $identifier = nonempty($matches[2], null); if ($repo === null) { if (strlen($identifier) < $min_unqualified) { continue; } $bare[] = $identifier; } else { $refs[] = array( 'callsign' => $repo, 'identifier' => $identifier, ); } } $sql = array(); foreach ($bare as $identifier) { $sql[] = qsprintf( $conn_r, '(commitIdentifier LIKE %> AND LENGTH(commitIdentifier) = 40)', $identifier); } if ($refs) { $callsigns = ipull($refs, 'callsign'); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->withCallsigns($callsigns) ->execute(); $repos = mpull($repos, null, 'getCallsign'); foreach ($refs as $key => $ref) { $repo = idx($repos, $ref['callsign']); if (!$repo) { continue; } if ($repo->isSVN()) { $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier = %d)', $repo->getID(), $ref['identifier']); } else { if (strlen($ref['identifier']) < $min_qualified) { continue; } $sql[] = qsprintf( $conn_r, '(repositoryID = %d AND commitIdentifier LIKE %>)', $repo->getID(), $ref['identifier']); } } } if ($sql) { $where[] = '('.implode(' OR ', $sql).')'; } else { // If we discarded all possible identifiers (e.g., they all referenced // bogus repositories or were all too short), make sure the query finds // nothing. $where[] = qsprintf($conn_r, '1 = 0'); } } + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + return $this->formatWhereClause($where); } } diff --git a/src/applications/phid/handle/PhabricatorObjectHandleData.php b/src/applications/phid/handle/PhabricatorObjectHandleData.php index 87e43d7ce3..94f6a02ad3 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/PhabricatorObjectHandleData.php @@ -1,664 +1,658 @@ phids = array_unique($phids); } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public static function loadOneHandle($phid, $viewer = null) { $query = new PhabricatorObjectHandleData(array($phid)); if ($viewer) { $query->setViewer($viewer); } $handles = $query->loadHandles(); return $handles[$phid]; } public function loadObjects() { $types = phid_group_by_type($this->phids); $objects = array(); foreach ($types as $type => $phids) { $objects += $this->loadObjectsOfType($type, $phids); } return $objects; } private function loadObjectsOfType($type, array $phids) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $user_dao = new PhabricatorUser(); $users = $user_dao->loadAllWhere( 'phid in (%Ls)', $phids); return mpull($users, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_CMIT: - $commit_dao = new PhabricatorRepositoryCommit(); - $commits = $commit_dao->putInSet(new LiskDAOSet())->loadAllWhere( - 'phid IN (%Ls)', - $phids); + $commits = id(new DiffusionCommitQuery()) + ->setViewer($this->viewer) + ->withPHIDs($phids) + ->execute(); return mpull($commits, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_TASK: $task_dao = new ManiphestTask(); $tasks = $task_dao->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($tasks, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_CONF: $config_dao = new PhabricatorConfigEntry(); $entries = $config_dao->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($entries, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_FILE: $object = new PhabricatorFile(); $files = $object->loadAllWhere('phid IN (%Ls)', $phids); return mpull($files, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_PROJ: $object = new PhabricatorProject(); if ($this->viewer) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->viewer) ->withPHIDs($phids) ->execute(); } else { $projects = $object->loadAllWhere('phid IN (%Ls)', $phids); } return mpull($projects, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_REPO: $object = new PhabricatorRepository(); $repositories = $object->loadAllWhere('phid in (%Ls)', $phids); return mpull($repositories, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_OPKG: $object = new PhabricatorOwnersPackage(); $packages = $object->loadAllWhere('phid in (%Ls)', $phids); return mpull($packages, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_APRJ: $project_dao = new PhabricatorRepositoryArcanistProject(); $projects = $project_dao->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($projects, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_MLST: $object = new PhabricatorMetaMTAMailingList(); $lists = $object->loadAllWhere('phid IN (%Ls)', $phids); return mpull($lists, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_DREV: $revision_dao = new DifferentialRevision(); $revisions = $revision_dao->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($revisions, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_WIKI: $document_dao = new PhrictionDocument(); $documents = $document_dao->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($documents, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_QUES: $questions = id(new PonderQuestionQuery()) ->setViewer($this->viewer) ->withPHIDs($phids) ->execute(); return mpull($questions, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_MOCK: $mocks = id(new PholioMockQuery()) ->setViewer($this->viewer) ->withPHIDs($phids) ->execute(); return mpull($mocks, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_XACT: $subtypes = array(); foreach ($phids as $phid) { $subtypes[phid_get_subtype($phid)][] = $phid; } $xactions = array(); foreach ($subtypes as $subtype => $subtype_phids) { // TODO: Do this magically. switch ($subtype) { case PhabricatorPHIDConstants::PHID_TYPE_MOCK: $results = id(new PholioTransactionQuery()) ->setViewer($this->viewer) ->withPHIDs($subtype_phids) ->execute(); $xactions += mpull($results, null, 'getPHID'); break; case PhabricatorPHIDConstants::PHID_TYPE_MCRO: $results = id(new PhabricatorMacroTransactionQuery()) ->setViewer($this->viewer) ->withPHIDs($subtype_phids) ->execute(); $xactions += mpull($results, null, 'getPHID'); break; } } return mpull($xactions, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_MCRO: $macros = id(new PhabricatorFileImageMacro())->loadAllWhere( 'phid IN (%Ls)', $phids); return mpull($macros, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_PSTE: $pastes = id(new PhabricatorPasteQuery()) ->withPHIDs($phids) ->setViewer($this->viewer) ->execute(); return mpull($pastes, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_BLOG: $blogs = id(new PhameBlogQuery()) ->withPHIDs($phids) ->setViewer($this->viewer) ->execute(); return mpull($blogs, null, 'getPHID'); case PhabricatorPHIDConstants::PHID_TYPE_POST: $posts = id(new PhamePostQuery()) ->withPHIDs($phids) ->setViewer($this->viewer) ->execute(); return mpull($posts, null, 'getPHID'); } } public function loadHandles() { $types = phid_group_by_type($this->phids); $handles = array(); $external_loaders = PhabricatorEnv::getEnvConfig('phid.external-loaders'); foreach ($types as $type => $phids) { $objects = $this->loadObjectsOfType($type, $phids); switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_MAGIC: // Black magic! foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); switch ($phid) { case ManiphestTaskOwner::OWNER_UP_FOR_GRABS: $handle->setName('Up For Grabs'); $handle->setFullName('upforgrabs (Up For Grabs)'); $handle->setComplete(true); break; case ManiphestTaskOwner::PROJECT_NO_PROJECT: $handle->setName('No Project'); $handle->setFullName('noproject (No Project)'); $handle->setComplete(true); break; default: $handle->setName('Foul Magicks'); break; } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_USER: $image_phids = mpull($objects, 'getProfileImagePHID'); $image_phids = array_unique(array_filter($image_phids)); $images = array(); if ($image_phids) { $images = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $image_phids); $images = mpull($images, 'getBestURI', 'getPHID'); } $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses( $phids); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown User'); } else { $user = $objects[$phid]; $handle->setName($user->getUsername()); $handle->setURI('/p/'.$user->getUsername().'/'); $handle->setFullName( $user->getUsername().' ('.$user->getRealName().')'); $handle->setAlternateID($user->getID()); $handle->setComplete(true); if (isset($statuses[$phid])) { $handle->setStatus($statuses[$phid]->getTextStatus()); if ($this->viewer) { $handle->setTitle( $statuses[$phid]->getTerseSummary($this->viewer)); } } $handle->setDisabled($user->getIsDisabled()); $img_uri = idx($images, $user->getProfileImagePHID()); if ($img_uri) { $handle->setImageURI($img_uri); } else { $handle->setImageURI( PhabricatorUser::getDefaultProfileImageURI()); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_MLST: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Mailing List'); } else { $list = $objects[$phid]; $handle->setName($list->getName()); $handle->setURI($list->getURI()); $handle->setFullName($list->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_DREV: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Revision'); } else { $rev = $objects[$phid]; $handle->setName($rev->getTitle()); $handle->setURI('/D'.$rev->getID()); $handle->setFullName('D'.$rev->getID().': '.$rev->getTitle()); $handle->setComplete(true); $status = $rev->getStatus(); if (($status == ArcanistDifferentialRevisionStatus::CLOSED) || ($status == ArcanistDifferentialRevisionStatus::ABANDONED)) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); - $repository = null; - if (!empty($objects[$phid])) { - $repository = $objects[$phid]->loadOneRelative( - new PhabricatorRepository(), - 'id', - 'getRepositoryID'); - } - - if (!$repository) { + if (empty($objects[$phid])) { $handle->setName('Unknown Commit'); } else { + $repository = $objects[$phid]->getRepository(); $commit = $objects[$phid]; $callsign = $repository->getCallsign(); $commit_identifier = $commit->getCommitIdentifier(); $name = $repository->formatCommitName($commit_identifier); $handle->setName($name); $summary = $commit->getSummary(); if (strlen($summary)) { $handle->setFullName($name.': '.$summary); } else { $handle->setFullName($name); } $handle->setURI('/r'.$callsign.$commit_identifier); $handle->setTimestamp($commit->getEpoch()); $handle->setComplete(true); } + $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Task'); } else { $task = $objects[$phid]; $handle->setName($task->getTitle()); $handle->setURI('/T'.$task->getID()); $handle->setFullName('T'.$task->getID().': '.$task->getTitle()); $handle->setComplete(true); $handle->setAlternateID($task->getID()); if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_CONF: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Config Entry'); } else { $entry = $objects[$phid]; $handle->setName($entry->getKey()); $handle->setURI('/config/edit/'.$entry->getKey()); $handle->setFullName($entry->getKey()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_FILE: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown File'); } else { $file = $objects[$phid]; $handle->setName($file->getName()); $handle->setURI($file->getBestURI()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_PROJ: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Project'); } else { $project = $objects[$phid]; $handle->setName($project->getName()); $handle->setURI('/project/view/'.$project->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_REPO: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Repository'); } else { $repository = $objects[$phid]; $handle->setName($repository->getCallsign()); $handle->setFullName("r" . $repository->getCallsign() . " (" . $repository->getName() . ")"); $handle->setURI('/diffusion/'.$repository->getCallsign().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_OPKG: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Package'); } else { $package = $objects[$phid]; $handle->setName($package->getName()); $handle->setURI('/owners/package/'.$package->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_APRJ: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Arcanist Project'); } else { $project = $objects[$phid]; $handle->setName($project->getName()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_WIKI: $document_dao = new PhrictionDocument(); $content_dao = new PhrictionContent(); $conn = $document_dao->establishConnection('r'); $documents = queryfx_all( $conn, 'SELECT * FROM %T document JOIN %T content ON document.contentID = content.id WHERE document.phid IN (%Ls)', $document_dao->getTableName(), $content_dao->getTableName(), $phids); $documents = ipull($documents, null, 'phid'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($documents[$phid])) { $handle->setName('Unknown Document'); } else { $info = $documents[$phid]; $handle->setName($info['title']); $handle->setURI(PhrictionDocument::getSlugURI($info['slug'])); $handle->setComplete(true); if ($info['status'] != PhrictionDocumentStatus::STATUS_EXISTS) { $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $handle->setStatus($closed); } } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_QUES: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Ponder Question'); } else { $question = $objects[$phid]; $handle->setName(phutil_utf8_shorten($question->getTitle(), 60)); $handle->setURI(new PhutilURI('/Q' . $question->getID())); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_PSTE: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Paste'); } else { $paste = $objects[$phid]; $handle->setName($paste->getTitle()); $handle->setFullName($paste->getFullName()); $handle->setURI('/P'.$paste->getID()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_BLOG: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Blog'); } else { $blog = $objects[$phid]; $handle->setName($blog->getName()); $handle->setFullName($blog->getName()); $handle->setURI('/phame/blog/view/'.$blog->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_POST: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Post'); } else { $post = $objects[$phid]; $handle->setName($post->getTitle()); $handle->setFullName($post->getTitle()); $handle->setURI('/phame/post/view/'.$post->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_MOCK: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Mock'); } else { $mock = $objects[$phid]; $handle->setName($mock->getName()); $handle->setFullName('M'.$mock->getID().': '.$mock->getName()); $handle->setURI('/M'.$mock->getID()); $handle->setComplete(true); } $handles[$phid] = $handle; } break; case PhabricatorPHIDConstants::PHID_TYPE_MCRO: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setType($type); if (empty($objects[$phid])) { $handle->setName('Unknown Macro'); } else { $macro = $objects[$phid]; $handle->setName($macro->getName()); $handle->setFullName('Image Macro "'.$macro->getName().'"'); $handle->setURI('/macro/view/'.$macro->getID().'/'); $handle->setComplete(true); } $handles[$phid] = $handle; } break; default: $loader = null; if (isset($external_loaders[$type])) { $loader = $external_loaders[$type]; } else if (isset($external_loaders['*'])) { $loader = $external_loaders['*']; } if ($loader) { $object = newv($loader, array()); assert_instances_of(array($type => $object), 'ObjectHandleLoader'); $handles += $object->loadHandles($phids); break; } foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setType($type); $handle->setPHID($phid); $handle->setName('Unknown Object'); $handle->setFullName('An Unknown Object'); $handles[$phid] = $handle; } break; } } return $handles; } } diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index e7db410e22..31007e4359 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -1,542 +1,542 @@ 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; - private $version = 5; + private $version = 6; /* -( 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, 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).'@'.$this->version; } /** * @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) { try { $blocks = id(new PhabricatorMarkupCache())->loadAllWhere( 'cacheKey IN (%Ls)', array_keys($use_cache)); $blocks = mpull($blocks, null, 'getCacheKey'); } catch (Exception $ex) { phlog($ex); } } 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(); $blocks[$key]->replace(); 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, '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'), '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'), 'preserve-linebreaks' => true, ); } /** * @task engine */ public static function newMarkupEngine(array $options) { $options += self::getMarkupEngineDefaultConfiguration(); $engine = new PhutilRemarkupEngine(); $engine->setConfig('preserve-linebreaks', $options['preserve-linebreaks']); $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['youtube']) { $rules[] = new PhabricatorRemarkupRuleYoutube(); } $rules[] = new PhutilRemarkupRuleHyperlink(); $rules[] = new PhrictionRemarkupRule(); $rules[] = new PhabricatorRemarkupRuleEmbedFile(); $rules[] = new PhabricatorCountdownRemarkupRule(); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { foreach ($application->getRemarkupRules() as $rule) { $rules[] = $rule; } } if ($options['macros']) { $rules[] = new PhabricatorRemarkupRuleImageMacro(); $rules[] = new PhabricatorRemarkupRuleMeme(); } $rules[] = new DivinerRemarkupRuleSymbol(); $rules[] = new PhabricatorRemarkupRuleMention(); $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 PhutilRemarkupEngineRemarkupSimpleTableBlockRule(); $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 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; } public static function extractFilePHIDsFromEmbeddedFiles( array $content_blocks) { $files = array(); $engine = self::newDifferentialMarkupEngine(); foreach ($content_blocks as $content_block) { $engine->markupText($content_block); $ids = $engine->getTextMetadata( PhabricatorRemarkupRuleEmbedFile::KEY_EMBED_FILE_PHIDS, array()); $files += $ids; } return $files; } /** * Produce a corpus summary, in a way that shortens the underlying text * without truncating it somewhere awkward. * * TODO: We could do a better job of this. * * @param string Remarkup corpus to summarize. * @return string Summarized corpus. */ public static function summarize($corpus) { // Major goals here are: // - Don't split in the middle of a character (utf-8). // - Don't split in the middle of, e.g., **bold** text, since // we end up with hanging '**' in the summary. // - Try not to pick an image macro, header, embedded file, etc. // - Hopefully don't return too much text. We don't explicitly limit // this right now. $blocks = preg_split("/\n *\n\s*/", trim($corpus)); $best = null; foreach ($blocks as $block) { // This is a test for normal spaces in the block, i.e. a heuristic to // distinguish standard paragraphs from things like image macros. It may // not work well for non-latin text. We prefer to summarize with a // paragraph of normal words over an image macro, if possible. $has_space = preg_match('/\w\s\w/', $block); // This is a test to find embedded images and headers. We prefer to // summarize with a normal paragraph over a header or an embedded object, // if possible. $has_embed = preg_match('/^[{=]/', $block); if ($has_space && !$has_embed) { // This seems like a good summary, so return it. return $block; } if (!$best) { // This is the first block we found; if everything is garbage just // use the first block. $best = $block; } } return $best; } } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php index 38084b7dac..dcd5235b52 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleObject.php @@ -1,191 +1,194 @@ getEngine()->getConfig('viewer'); if ($viewer) { $query->setViewer($viewer); } $handles = $query->loadHandles(); $result = array(); foreach ($objects as $id => $object) { $result[$id] = $handles[$object->getPHID()]; } return $result; } protected function renderObjectRef($object, $handle, $anchor, $id) { $href = $handle->getURI(); $text = $this->getObjectNamePrefix().$id; if ($anchor) { $matches = null; - if (preg_match('@^#(?:comment-)?(\d{1,7})$@', $anchor, $matches)) { + if (preg_match('@^(?:comment-)?(\d{1,7})$@', $anchor, $matches)) { // Maximum length is 7 because 12345678 could be a file hash in // Differential. - $href = $href."#comment-".$matches[1]; - $text = $text."#".$matches[1]; + $href = $href.'#comment-'.$matches[1]; + $text = $text.'#'.$matches[1]; } else { - $href = $href.$anchor; - $text = $text.$anchor; + $href = $href.'#'.$anchor; + $text = $text.'#'.$anchor; } } $status_closed = PhabricatorObjectHandleStatus::STATUS_CLOSED; $attr = array( 'phid' => $handle->getPHID(), 'closed' => ($handle->getStatus() == $status_closed), ); return $this->renderHovertag($text, $href, $attr); } protected function renderObjectEmbed($object, $handle, $options) { $name = $handle->getFullName(); $href = $handle->getURI(); $attr = array( 'phid' => $handle->getPHID(), ); return $this->renderHovertag($name, $href, $attr); } protected function renderHovertag($name, $href, array $attr = array()) { return id(new PhabricatorTagView()) ->setName($name) ->setHref($href) ->setType(PhabricatorTagView::TYPE_OBJECT) ->setPHID(idx($attr, 'phid')) ->setClosed(idx($attr, 'closed')) ->render(); } public function apply($text) { $prefix = $this->getObjectNamePrefix(); $prefix = preg_quote($prefix, '@'); $id = $this->getObjectIDPattern(); $text = preg_replace_callback( '@\B{'.$prefix.'('.$id.')((?:[^}\\\\]|\\\\.)*)}\B@', array($this, 'markupObjectEmbed'), $text); + // NOTE: The "(?markupObject(array( 'type' => 'embed', 'id' => $matches[1], 'options' => idx($matches, 2), 'original' => $matches[0], )); } public function markupObjectReference($matches) { return $this->markupObject(array( 'type' => 'ref', 'id' => $matches[1], 'anchor' => idx($matches, 2), 'original' => $matches[0], )); } private function markupObject(array $params) { if (!$this->shouldMarkupObject($params)) { return $params['original']; } $engine = $this->getEngine(); $token = $engine->storeText('x'); $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix(); $metadata = $engine->getTextMetadata($metadata_key, array()); $metadata[] = array( 'token' => $token, ) + $params; $engine->setTextMetadata($metadata_key, $metadata); return $token; } public function didMarkupText() { $engine = $this->getEngine(); $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix(); $metadata = $engine->getTextMetadata($metadata_key, array()); if (!$metadata) { return; } $ids = ipull($metadata, 'id'); $objects = $this->loadObjects($ids); // For objects that are invalid or which the user can't see, just render // the original text. // TODO: We should probably distinguish between these cases and render a // "you can't see this" state for nonvisible objects. foreach ($metadata as $key => $spec) { if (empty($objects[$spec['id']])) { $engine->overwriteStoredText( $spec['token'], $spec['original']); unset($metadata[$key]); } } $handles = $this->loadHandles($objects); foreach ($metadata as $key => $spec) { $handle = $handles[$spec['id']]; $object = $objects[$spec['id']]; switch ($spec['type']) { case 'ref': $view = $this->renderObjectRef( $object, $handle, $spec['anchor'], $spec['id']); break; case 'embed': $view = $this->renderObjectEmbed($object, $handle, $spec['options']); break; } $engine->overwriteStoredText($spec['token'], $view); } $engine->setTextMetadata($metadata_key, array()); } }