diff --git a/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php b/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php index 4eb5d9d825..ac51e30386 100644 --- a/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php +++ b/src/applications/files/builtin/PhabricatorFilesOnDiskBuiltinFile.php @@ -1,59 +1,75 @@ name = $name; return $this; } public function getName() { if ($this->name === null) { throw new PhutilInvalidStateException('setName'); } return $this->name; } public function getBuiltinDisplayName() { return $this->getName(); } public function getBuiltinFileKey() { $name = $this->getName(); $desc = "disk(name={$name})"; $hash = PhabricatorHash::digestToLength($desc, 40); return "builtin:{$hash}"; } public function loadBuiltinFileData() { $name = $this->getName(); $available = $this->getAllBuiltinFiles(); if (empty($available[$name])) { throw new Exception(pht('Builtin "%s" does not exist!', $name)); } return Filesystem::readFile($available[$name]); } private function getAllBuiltinFiles() { $root = dirname(phutil_get_library_root('phabricator')); $root = $root.'/resources/builtin/'; $map = array(); $list = id(new FileFinder($root)) ->withType('f') ->withFollowSymlinks(true) ->find(); foreach ($list as $file) { $map[$file] = $root.$file; } return $map; } + public function getProjectBuiltinFiles() { + $root = dirname(phutil_get_library_root('phabricator')); + $root = $root.'/resources/builtin/projects/'; + + $map = array(); + $list = id(new FileFinder($root)) + ->withType('f') + ->withFollowSymlinks(true) + ->find(); + + foreach ($list as $file) { + $map[$file] = $root.$file; + } + return $map; + } + } diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php index 36b6f09d86..c61faa64fb 100644 --- a/src/applications/project/config/PhabricatorProjectConfigOptions.php +++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php @@ -1,106 +1,108 @@ deformat(pht(<< Icons and Images}. Configure a list of icon specifications. Each icon specification should be a dictionary, which may contain these keys: - `key` //Required string.// Internal key identifying the icon. - `name` //Required string.// Human-readable icon name. - `icon` //Required string.// Specifies which actual icon image to use. + - `image` //Optional string.// Selects a default image. Select an image from + `resources/builtins/projects/`. - `default` //Optional bool.// Selects a default icon. Exactly one icon must be selected as the default. - `disabled` //Optional bool.// If true, this icon will no longer be available for selection when creating or editing projects. - `special` //Optional string.// Marks an icon as a special icon: - `milestone` This is the icon for milestones. Exactly one icon must be selected as the milestone icon. You can look at the default configuration below for an example of a valid configuration. EOTEXT )); $default_colors = PhabricatorProjectIconSet::getDefaultColorMap(); $colors_type = 'project.colors'; $colors_description = $this->deformat(pht(<< true, ); foreach ($default_fields as $key => $enabled) { $default_fields[$key] = array( 'disabled' => !$enabled, ); } $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; return array( $this->newOption('projects.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Projects fields.')) ->setDescription( pht( 'Array of custom fields for Projects.')) ->addExample( '{"mycompany:motto": {"name": "Project Motto", '. '"type": "text"}}', pht('Valid Setting')), $this->newOption('projects.fields', $custom_field_type, $default_fields) ->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass()) ->setDescription(pht('Select and reorder project fields.')), $this->newOption('projects.icons', $icons_type, $default_icons) ->setSummary(pht('Adjust project icons.')) ->setDescription($icons_description), $this->newOption('projects.colors', $colors_type, $default_colors) ->setSummary(pht('Adjust project colors.')) ->setDescription($colors_description), ); } } diff --git a/src/applications/project/controller/PhabricatorProjectEditPictureController.php b/src/applications/project/controller/PhabricatorProjectEditPictureController.php index f8f1d532a9..97b7fe1792 100644 --- a/src/applications/project/controller/PhabricatorProjectEditPictureController.php +++ b/src/applications/project/controller/PhabricatorProjectEditPictureController.php @@ -1,372 +1,375 @@ getViewer(); $id = $request->getURIData('id'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $this->setProject($project); $manage_uri = $this->getApplicationURI('manage/'.$project->getID().'/'); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_file = true; $errors = array(); if ($request->isFormPost()) { $phid = $request->getStr('phid'); $is_default = false; if ($phid == PhabricatorPHIDConstants::PHID_VOID) { $phid = null; $is_default = true; } else if ($phid) { $file = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); } else { if ($request->getFileExists('picture')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['picture'], array( 'authorPHID' => $viewer->getPHID(), 'canCDN' => true, )); } else { $e_file = pht('Required'); $errors[] = pht( 'You must choose a file when uploading a new project picture.'); } } if (!$errors && !$is_default) { if (!$file->isTransformableImage()) { $e_file = pht('Not Supported'); $errors[] = pht( 'This server only supports these image formats: %s.', implode(', ', $supported_formats)); } else { $xform = PhabricatorFileTransform::getTransformByKey( PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE); $xformed = $xform->executeTransform($file); } } if (!$errors) { if ($is_default) { $new_value = null; } else { $new_value = $xformed->getPHID(); } $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType( PhabricatorProjectImageTransaction::TRANSACTIONTYPE) ->setNewValue($new_value); $editor = id(new PhabricatorProjectTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $editor->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse())->setURI($manage_uri); } } $title = pht('Edit Project Picture'); $form = id(new PHUIFormLayoutView()) ->setUser($viewer); - $default_image = PhabricatorFile::loadBuiltin($viewer, 'project.png'); + $builtin = PhabricatorProjectIconSet::getIconImage( + $project->getIcon()); + $default_image = PhabricatorFile::loadBuiltin($this->getViewer(), + 'projects/'.$builtin); $images = array(); $current = $project->getProfileImagePHID(); $has_current = false; if ($current) { $files = id(new PhabricatorFileQuery()) ->setViewer($viewer) ->withPHIDs(array($current)) ->execute(); if ($files) { $file = head($files); if ($file->isTransformableImage()) { $has_current = true; $images[$current] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Current Picture'), ); } } } $builtins = array( 'projects/v3/book.png', 'projects/v3/bug.png', 'projects/v3/calendar.png', 'projects/v3/clipboard.png', 'projects/v3/cloud.png', 'projects/v3/creditcard.png', 'projects/v3/database.png', 'projects/v3/desktop.png', 'projects/v3/experimental.png', 'projects/v3/flag.png', 'projects/v3/folder.png', 'projects/v3/lightbulb.png', 'projects/v3/lock.png', 'projects/v3/mail.png', 'projects/v3/marker.png', 'projects/v3/mobile.png', 'projects/v3/organization.png', 'projects/v3/people.png', 'projects/v3/piechart.png', 'projects/v3/robot.png', 'projects/v3/rocket.png', 'projects/v3/servers.png', 'projects/v3/sitemap.png', 'projects/v3/tag.png', 'projects/v3/trash.png', 'projects/v3/truck.png', 'projects/v3/umbrella.png', ); foreach ($builtins as $builtin) { $file = PhabricatorFile::loadBuiltin($viewer, $builtin); $images[$file->getPHID()] = array( 'uri' => $file->getBestURI(), 'tip' => pht('Builtin Image'), ); } $images[PhabricatorPHIDConstants::PHID_VOID] = array( 'uri' => $default_image->getBestURI(), 'tip' => pht('Default Picture'), ); require_celerity_resource('people-profile-css'); Javelin::initBehavior('phabricator-tooltips', array()); $buttons = array(); foreach ($images as $phid => $spec) { $button = javelin_tag( 'button', array( 'class' => 'button-grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $spec['tip'], 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $spec['uri'], ))); $button = array( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'phid', 'value' => $phid, )), $button, ); $button = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), $button); $buttons[] = $button; } if ($has_current) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Current Picture')) ->setValue(array_shift($buttons))); } $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Use Picture')) ->setValue( array( $this->renderDefaultForm($project), $buttons, ))); $launch_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); Javelin::initBehavior( 'launch-icon-composer', array( 'launchID' => $launch_id, 'inputID' => $input_id, )); $compose_button = javelin_tag( 'button', array( 'class' => 'button-grey', 'id' => $launch_id, 'sigil' => 'icon-composer', ), pht('Choose Icon and Color...')); $compose_input = javelin_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => 'phid', )); $compose_form = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', ), array( $compose_input, $compose_button, )); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Custom')) ->setValue($compose_form)); $upload_form = id(new AphrontFormView()) ->setUser($viewer) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormFileControl()) ->setName('picture') ->setLabel(pht('Upload Picture')) ->setError($e_file) ->setCaption( pht('Supported formats: %s', implode(', ', $supported_formats)))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($manage_uri) ->setValue(pht('Upload Picture'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $upload_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New Picture')) ->setForm($upload_form); $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorProject::ITEM_MANAGE); return $this->newPage() ->setTitle($title) ->setNavigation($nav) ->appendChild( array( $form_box, $upload_box, )); } private function renderDefaultForm(PhabricatorProject $project) { $viewer = $this->getViewer(); $compose_color = $project->getDisplayIconComposeColor(); $compose_icon = $project->getDisplayIconComposeIcon(); $default_builtin = id(new PhabricatorFilesComposeIconBuiltinFile()) ->setColor($compose_color) ->setIcon($compose_icon); $file_builtins = PhabricatorFile::loadBuiltins( $viewer, array($default_builtin)); $file_builtin = head($file_builtins); $default_button = javelin_tag( 'button', array( 'class' => 'button-grey profile-image-button', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => pht('Use Icon and Color'), 'size' => 300, ), ), phutil_tag( 'img', array( 'height' => 50, 'width' => 50, 'src' => $file_builtin->getBestURI(), ))); $inputs = array( 'projectPHID' => $project->getPHID(), 'icon' => $compose_icon, 'color' => $compose_color, ); foreach ($inputs as $key => $value) { $inputs[$key] = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } $default_form = phabricator_form( $viewer, array( 'class' => 'profile-image-form', 'method' => 'POST', 'action' => '/file/compose/', ), array( $inputs, $default_button, )); return $default_form; } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 393c4569f6..7702b74bd6 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -1,510 +1,478 @@ isMilestone = $is_milestone; return $this; } public function getIsMilestone() { return $this->isMilestone; } public function getEditorApplicationClass() { return 'PhabricatorProjectApplication'; } public function getEditorObjectsDescription() { return pht('Projects'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; $types[] = PhabricatorTransactions::TYPE_JOIN_POLICY; return $types; } protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { $errors = array(); // Prevent creating projects which are both subprojects and milestones, // since this does not make sense, won't work, and will break everything. $parent_xaction = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectParentTransaction::TRANSACTIONTYPE: case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: if ($xaction->getNewValue() === null) { continue; } if (!$parent_xaction) { $parent_xaction = $xaction; continue; } $errors[] = new PhabricatorApplicationTransactionValidationError( $xaction->getTransactionType(), pht('Invalid'), pht( 'When creating a project, specify a maximum of one parent '. 'project or milestone project. A project can not be both a '. 'subproject and a milestone.'), $xaction); break; break; } } $is_milestone = $this->getIsMilestone(); $is_parent = $object->getHasSubprojects(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: $type = $xaction->getMetadataValue('edge:type'); if ($type != PhabricatorProjectProjectHasMemberEdgeType::EDGECONST) { break; } if ($is_parent) { $errors[] = new PhabricatorApplicationTransactionValidationError( $xaction->getTransactionType(), pht('Invalid'), pht( 'You can not change members of a project with subprojects '. 'directly. Members of any subproject are automatically '. 'members of the parent project.'), $xaction); } if ($is_milestone) { $errors[] = new PhabricatorApplicationTransactionValidationError( $xaction->getTransactionType(), pht('Invalid'), pht( 'You can not change members of a milestone. Members of the '. 'parent project are automatically members of the milestone.'), $xaction); } break; } } return $errors; } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectNameTransaction::TRANSACTIONTYPE: case PhabricatorProjectStatusTransaction::TRANSACTIONTYPE: case PhabricatorProjectImageTransaction::TRANSACTIONTYPE: case PhabricatorProjectIconTransaction::TRANSACTIONTYPE: case PhabricatorProjectColorTransaction::TRANSACTIONTYPE: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); return; case PhabricatorProjectLockTransaction::TRANSACTIONTYPE: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), newv($this->getEditorApplicationClass(), array()), ProjectCanLockProjectsCapability::CAPABILITY); return; case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); $actor_phid = $this->requireActor()->getPHID(); $is_join = (($add === array($actor_phid)) && !$rem); $is_leave = (($rem === array($actor_phid)) && !$add); if ($is_join) { // You need CAN_JOIN to join a project. PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_JOIN); } else if ($is_leave) { // You usually don't need any capabilities to leave a project. if ($object->getIsMembershipLocked()) { // you must be able to edit though to leave locked projects PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); } } else { // You need CAN_EDIT to change members other than yourself. PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, PhabricatorPolicyCapability::CAN_EDIT); } return; } break; } return parent::requireCapabilities($object, $xaction); } protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { // NOTE: We're using the omnipotent user here because the original actor // may no longer have permission to view the object. return id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($object->getPHID())) ->needAncestorMembers(true) ->executeOne(); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function getMailSubjectPrefix() { return pht('[Project]'); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $this->getActingAsPHID(), ); } protected function getMailCc(PhabricatorLiskDAO $object) { return array(); } public function getMailTagsMap() { return array( PhabricatorProjectTransaction::MAILTAG_METADATA => pht('Project name, hashtags, icon, image, or color changes.'), PhabricatorProjectTransaction::MAILTAG_MEMBERS => pht('Project membership changes.'), PhabricatorProjectTransaction::MAILTAG_WATCHERS => pht('Project watcher list changes.'), PhabricatorProjectTransaction::MAILTAG_OTHER => pht('Other project activity not listed above occurs.'), ); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ProjectReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $name = $object->getName(); return id(new PhabricatorMetaMTAMail()) ->setSubject("{$name}") ->addHeader('Thread-Topic', "Project {$id}"); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $uri = '/project/profile/'.$object->getID().'/'; $body->addLinkSection( pht('PROJECT DETAIL'), PhabricatorEnv::getProductionURI($uri)); return $body; } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function supportsSearch() { return true; } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { $materialize = false; $new_parent = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: switch ($xaction->getMetadataValue('edge:type')) { case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: $materialize = true; break; } break; case PhabricatorProjectParentTransaction::TRANSACTIONTYPE: case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: $materialize = true; $new_parent = $object->getParentProject(); break; } } if ($new_parent) { // If we just created the first subproject of this parent, we want to // copy all of the real members to the subproject. if (!$new_parent->getHasSubprojects()) { $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $project_members = PhabricatorEdgeQuery::loadDestinationPHIDs( $new_parent->getPHID(), $member_type); if ($project_members) { $editor = id(new PhabricatorEdgeEditor()); foreach ($project_members as $phid) { $editor->addEdge($object->getPHID(), $member_type, $phid); } $editor->save(); } } } - if ($this->getIsNewObject()) { - $this->setDefaultProfilePicture($object); - } - // TODO: We should dump an informational transaction onto the parent // project to show that we created the sub-thing. if ($materialize) { id(new PhabricatorProjectsMembershipIndexEngineExtension()) ->rematerialize($object); } if ($new_parent) { id(new PhabricatorProjectsMembershipIndexEngineExtension()) ->rematerialize($new_parent); } return parent::applyFinalEffects($object, $xactions); } public function addSlug(PhabricatorProject $project, $slug, $force) { $slug = PhabricatorSlug::normalizeProjectSlug($slug); $table = new PhabricatorProjectSlug(); $project_phid = $project->getPHID(); if ($force) { // If we have the `$force` flag set, we only want to ignore an existing // slug if it's for the same project. We'll error on collisions with // other projects. $current = $table->loadOneWhere( 'slug = %s AND projectPHID = %s', $slug, $project_phid); } else { // Without the `$force` flag, we'll just return without doing anything // if any other project already has the slug. $current = $table->loadOneWhere( 'slug = %s', $slug); } if ($current) { return; } return id(new PhabricatorProjectSlug()) ->setSlug($slug) ->setProjectPHID($project_phid) ->save(); } public function removeSlugs(PhabricatorProject $project, array $slugs) { if (!$slugs) { return; } // We're going to try to delete both the literal and normalized versions // of all slugs. This allows us to destroy old slugs that are no longer // valid. foreach ($this->normalizeSlugs($slugs) as $slug) { $slugs[] = $slug; } $objects = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID = %s AND slug IN (%Ls)', $project->getPHID(), $slugs); foreach ($objects as $object) { $object->delete(); } } public function normalizeSlugs(array $slugs) { foreach ($slugs as $key => $slug) { $slugs[$key] = PhabricatorSlug::normalizeProjectSlug($slug); } $slugs = array_unique($slugs); $slugs = array_values($slugs); return $slugs; } protected function adjustObjectForPolicyChecks( PhabricatorLiskDAO $object, array $xactions) { $copy = parent::adjustObjectForPolicyChecks($object, $xactions); $type_edge = PhabricatorTransactions::TYPE_EDGE; $edgetype_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $member_xaction = null; foreach ($xactions as $xaction) { if ($xaction->getTransactionType() !== $type_edge) { continue; } $edgetype = $xaction->getMetadataValue('edge:type'); if ($edgetype !== $edgetype_member) { continue; } $member_xaction = $xaction; } if ($member_xaction) { $object_phid = $object->getPHID(); if ($object_phid) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($object_phid)) ->needMembers(true) ->executeOne(); $members = $project->getMemberPHIDs(); } else { $members = array(); } $clone_xaction = clone $member_xaction; $hint = $this->getPHIDTransactionNewValue($clone_xaction, $members); $rule = new PhabricatorProjectMembersPolicyRule(); $hint = array_fuse($hint); PhabricatorPolicyRule::passTransactionHintToRule( $copy, $rule, $hint); } return $copy; } protected function expandTransactions( PhabricatorLiskDAO $object, array $xactions) { $actor = $this->getActor(); $actor_phid = $actor->getPHID(); $results = parent::expandTransactions($object, $xactions); $is_milestone = $object->isMilestone(); foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorProjectMilestoneTransaction::TRANSACTIONTYPE: if ($xaction->getNewValue() !== null) { $is_milestone = true; } break; } } $this->setIsMilestone($is_milestone); return $results; } - private function setDefaultProfilePicture(PhabricatorProject $project) { - if ($project->isMilestone()) { - return; - } - - $compose_color = $project->getDisplayIconComposeColor(); - $compose_icon = $project->getDisplayIconComposeIcon(); - - $builtin = id(new PhabricatorFilesComposeIconBuiltinFile()) - ->setColor($compose_color) - ->setIcon($compose_icon); - - $data = $builtin->loadBuiltinFileData(); - - $file = PhabricatorFile::newFromFileData( - $data, - array( - 'name' => $builtin->getBuiltinDisplayName(), - 'profile' => true, - 'canCDN' => true, - )); - - $project - ->setProfileImagePHID($file->getPHID()) - ->save(); - } - - protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { // Herald rules may run on behalf of other users and need to execute // membership checks against ancestors. $project = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($object->getPHID())) ->needAncestorMembers(true) ->executeOne(); return id(new PhabricatorProjectHeraldAdapter()) ->setProject($project); } } diff --git a/src/applications/project/icon/PhabricatorProjectIconSet.php b/src/applications/project/icon/PhabricatorProjectIconSet.php index 2283026598..f487b216f3 100644 --- a/src/applications/project/icon/PhabricatorProjectIconSet.php +++ b/src/applications/project/icon/PhabricatorProjectIconSet.php @@ -1,465 +1,507 @@ 'project', 'icon' => 'fa-briefcase', 'name' => pht('Project'), 'default' => true, + 'image' => 'v3/briefcase.png', ), array( 'key' => 'tag', 'icon' => 'fa-tags', 'name' => pht('Tag'), + 'image' => 'v3/tag.png', ), array( 'key' => 'policy', 'icon' => 'fa-lock', 'name' => pht('Policy'), + 'image' => 'v3/lock.png', ), array( 'key' => 'group', 'icon' => 'fa-users', 'name' => pht('Group'), + 'image' => 'v3/people.png', ), array( 'key' => 'folder', 'icon' => 'fa-folder', 'name' => pht('Folder'), + 'image' => 'v3/folder.png', ), array( 'key' => 'timeline', 'icon' => 'fa-calendar', 'name' => pht('Timeline'), + 'image' => 'v3/calendar.png', ), array( 'key' => 'goal', 'icon' => 'fa-flag-checkered', 'name' => pht('Goal'), + 'image' => 'v3/flag.png', ), array( 'key' => 'release', 'icon' => 'fa-truck', 'name' => pht('Release'), + 'image' => 'v3/truck.png', ), array( 'key' => 'bugs', 'icon' => 'fa-bug', 'name' => pht('Bugs'), + 'image' => 'v3/bug.png', ), array( 'key' => 'cleanup', 'icon' => 'fa-trash-o', 'name' => pht('Cleanup'), + 'image' => 'v3/trash.png', ), array( 'key' => 'umbrella', 'icon' => 'fa-umbrella', 'name' => pht('Umbrella'), + 'image' => 'v3/umbrella.png', ), array( 'key' => 'communication', 'icon' => 'fa-envelope', 'name' => pht('Communication'), + 'image' => 'v3/mail.png', ), array( 'key' => 'organization', 'icon' => 'fa-building', 'name' => pht('Organization'), + 'image' => 'v3/organization.png', ), array( 'key' => 'infrastructure', 'icon' => 'fa-cloud', 'name' => pht('Infrastructure'), + 'image' => 'v3/cloud.png', ), array( 'key' => 'account', 'icon' => 'fa-credit-card', 'name' => pht('Account'), + 'image' => 'v3/creditcard.png', ), array( 'key' => 'experimental', 'icon' => 'fa-flask', 'name' => pht('Experimental'), + 'image' => 'v3/experimental.png', ), array( 'key' => 'milestone', 'icon' => 'fa-map-marker', 'name' => pht('Milestone'), 'special' => self::SPECIAL_MILESTONE, + 'image' => 'v3/marker.png', ), ); } protected function newIcons() { $map = self::getIconSpecifications(); $icons = array(); foreach ($map as $spec) { $special = idx($spec, 'special'); if ($special === self::SPECIAL_MILESTONE) { continue; } $icons[] = id(new PhabricatorIconSetIcon()) ->setKey($spec['key']) ->setIsDisabled(idx($spec, 'disabled')) ->setIcon($spec['icon']) ->setLabel($spec['name']); } return $icons; } private static function getIconSpecifications() { return PhabricatorEnv::getEnvConfig('projects.icons'); } public static function getDefaultIconKey() { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'default')) { return $icon['key']; } } return null; } public static function getIconIcon($key) { $spec = self::getIconSpec($key); return idx($spec, 'icon', null); } public static function getIconName($key) { $spec = self::getIconSpec($key); return idx($spec, 'name', null); } + public static function getIconImage($key) { + $spec = self::getIconSpec($key); + return idx($spec, 'image', 'v3/briefcase.png'); + } + private static function getIconSpec($key) { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'key') === $key) { return $icon; } } return array(); } public static function getMilestoneIconKey() { $icons = self::getIconSpecifications(); foreach ($icons as $icon) { if (idx($icon, 'special') === self::SPECIAL_MILESTONE) { return idx($icon, 'key'); } } return null; } public static function validateConfiguration($config) { if (!is_array($config)) { throw new Exception( pht('Configuration must be a list of project icon specifications.')); } foreach ($config as $idx => $value) { if (!is_array($value)) { throw new Exception( pht( 'Value for index "%s" should be a dictionary.', $idx)); } PhutilTypeSpec::checkMap( $value, array( 'key' => 'string', 'name' => 'string', 'icon' => 'string', + 'image' => 'optional string', 'special' => 'optional string', 'disabled' => 'optional bool', 'default' => 'optional bool', )); if (!preg_match('/^[a-z]{1,32}\z/', $value['key'])) { throw new Exception( pht( 'Icon key "%s" is not a valid icon key. Icon keys must be 1-32 '. 'characters long and contain only lowercase letters. For example, '. '"%s" and "%s" are reasonable keys.', 'tag', 'group')); } $special = idx($value, 'special'); $valid = array( self::SPECIAL_MILESTONE => true, ); if ($special !== null) { if (empty($valid[$special])) { throw new Exception( pht( 'Icon special attribute "%s" is not valid. Recognized special '. 'attributes are: %s.', $special, implode(', ', array_keys($valid)))); } } } $default = null; $milestone = null; $keys = array(); foreach ($config as $idx => $value) { $key = $value['key']; if (isset($keys[$key])) { throw new Exception( pht( 'Project icons must have unique keys, but two icons share the '. 'same key ("%s").', $key)); } else { $keys[$key] = true; } $is_disabled = idx($value, 'disabled'); + if (idx($value, 'image')) { + $builtin = idx($value, 'image'); + $builtin_map = id(new PhabricatorFilesOnDiskBuiltinFile()) + ->getProjectBuiltinFiles(); + $builtin_map = array_flip($builtin_map); + + $root = dirname(phutil_get_library_root('phabricator')); + $image = $root.'/resources/builtin/projects/'.$builtin; + + if (!array_key_exists($image, $builtin_map)) { + throw new Exception( + pht( + 'The project image ("%s") specified for ("%s") '. + 'was not found in the folder "resources/builtin/projects/".', + $builtin, + $key)); + } + } + if (idx($value, 'default')) { if ($default === null) { if ($is_disabled) { throw new Exception( pht( 'The project icon marked as the default icon ("%s") must not '. 'be disabled.', $key)); } $default = $value; } else { $original_key = $default['key']; throw new Exception( pht( 'Two different icons ("%s", "%s") are marked as the default '. 'icon. Only one icon may be marked as the default.', $key, $original_key)); } } $special = idx($value, 'special'); if ($special === self::SPECIAL_MILESTONE) { if ($milestone === null) { if ($is_disabled) { throw new Exception( pht( 'The project icon ("%s") with special attribute "%s" must '. 'not be disabled', $key, self::SPECIAL_MILESTONE)); } $milestone = $value; } else { $original_key = $milestone['key']; throw new Exception( pht( 'Two different icons ("%s", "%s") are marked with special '. 'attribute "%s". Only one icon may be marked with this '. 'attribute.', $key, $original_key, self::SPECIAL_MILESTONE)); } } } if ($default === null) { throw new Exception( pht( 'Project icons must include one icon marked as the "%s" icon, '. 'but no such icon exists.', 'default')); } if ($milestone === null) { throw new Exception( pht( 'Project icons must include one icon marked with special attribute '. '"%s", but no such icon exists.', self::SPECIAL_MILESTONE)); } } private static function getColorSpecifications() { return PhabricatorEnv::getEnvConfig('projects.colors'); } public static function getColorMap() { $specifications = self::getColorSpecifications(); return ipull($specifications, 'name', 'key'); } public static function getDefaultColorKey() { $specifications = self::getColorSpecifications(); foreach ($specifications as $specification) { if (idx($specification, 'default')) { return $specification['key']; } } return null; } private static function getAvailableColorKeys() { $list = array(); $specifications = self::getDefaultColorMap(); foreach ($specifications as $specification) { $list[] = $specification['key']; } return $list; } public static function getColorName($color_key) { $map = self::getColorMap(); return idx($map, $color_key); } public static function getDefaultColorMap() { return array( array( 'key' => PHUITagView::COLOR_RED, 'name' => pht('Red'), ), array( 'key' => PHUITagView::COLOR_ORANGE, 'name' => pht('Orange'), ), array( 'key' => PHUITagView::COLOR_YELLOW, 'name' => pht('Yellow'), ), array( 'key' => PHUITagView::COLOR_GREEN, 'name' => pht('Green'), ), array( 'key' => PHUITagView::COLOR_BLUE, 'name' => pht('Blue'), 'default' => true, ), array( 'key' => PHUITagView::COLOR_INDIGO, 'name' => pht('Indigo'), ), array( 'key' => PHUITagView::COLOR_VIOLET, 'name' => pht('Violet'), ), array( 'key' => PHUITagView::COLOR_PINK, 'name' => pht('Pink'), ), array( 'key' => PHUITagView::COLOR_GREY, 'name' => pht('Grey'), ), array( 'key' => PHUITagView::COLOR_CHECKERED, 'name' => pht('Checkered'), ), ); } public static function validateColorConfiguration($config) { if (!is_array($config)) { throw new Exception( pht('Configuration must be a list of project color specifications.')); } $available_keys = self::getAvailableColorKeys(); $available_keys = array_fuse($available_keys); foreach ($config as $idx => $value) { if (!is_array($value)) { throw new Exception( pht( 'Value for index "%s" should be a dictionary.', $idx)); } PhutilTypeSpec::checkMap( $value, array( 'key' => 'string', 'name' => 'string', 'default' => 'optional bool', )); $key = $value['key']; if (!isset($available_keys[$key])) { throw new Exception( pht( 'Color key "%s" is not a valid color key. The supported color '. 'keys are: %s.', $key, implode(', ', $available_keys))); } } $default = null; $keys = array(); foreach ($config as $idx => $value) { $key = $value['key']; if (isset($keys[$key])) { throw new Exception( pht( 'Project colors must have unique keys, but two icons share the '. 'same key ("%s").', $key)); } else { $keys[$key] = true; } if (idx($value, 'default')) { if ($default === null) { $default = $value; } else { $original_key = $default['key']; throw new Exception( pht( 'Two different colors ("%s", "%s") are marked as the default '. 'color. Only one color may be marked as the default.', $key, $original_key)); } } } if ($default === null) { throw new Exception( pht( 'Project colors must include one color marked as the "%s" color, '. 'but no such color exists.', 'default')); } } } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 44055b7260..780f072678 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -1,804 +1,800 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; } public function withWatcherPHIDs(array $watcher_phids) { $this->watcherPHIDs = $watcher_phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withNames(array $names) { $this->names = $names; return $this; } public function withNamePrefixes(array $prefixes) { $this->namePrefixes = $prefixes; return $this; } public function withNameTokens(array $tokens) { $this->nameTokens = array_values($tokens); return $this; } public function withIcons(array $icons) { $this->icons = $icons; return $this; } public function withColors(array $colors) { $this->colors = $colors; return $this; } public function withParentProjectPHIDs($parent_phids) { $this->parentPHIDs = $parent_phids; return $this; } public function withAncestorProjectPHIDs($ancestor_phids) { $this->ancestorPHIDs = $ancestor_phids; return $this; } public function withIsMilestone($is_milestone) { $this->isMilestone = $is_milestone; return $this; } public function withHasSubprojects($has_subprojects) { $this->hasSubprojects = $has_subprojects; return $this; } public function withDepthBetween($min, $max) { $this->minDepth = $min; $this->maxDepth = $max; return $this; } public function withMilestoneNumberBetween($min, $max) { $this->minMilestoneNumber = $min; $this->maxMilestoneNumber = $max; return $this; } public function needMembers($need_members) { $this->needMembers = $need_members; return $this; } public function needAncestorMembers($need_ancestor_members) { $this->needAncestorMembers = $need_ancestor_members; return $this; } public function needWatchers($need_watchers) { $this->needWatchers = $need_watchers; return $this; } public function needImages($need_images) { $this->needImages = $need_images; return $this; } public function needSlugs($need_slugs) { $this->needSlugs = $need_slugs; return $this; } public function newResultObject() { return new PhabricatorProject(); } protected function getDefaultOrderVector() { return array('name'); } public function getBuiltinOrders() { return array( 'name' => array( 'vector' => array('name'), 'name' => pht('Name'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'name' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'name', 'reverse' => true, 'type' => 'string', 'unique' => true, ), 'milestoneNumber' => array( 'table' => $this->getPrimaryTableAlias(), 'column' => 'milestoneNumber', 'type' => 'int', ), ); } protected function getPagingValueMap($cursor, array $keys) { $project = $this->loadCursorObject($cursor); return array( 'id' => $project->getID(), 'name' => $project->getName(), ); } public function getSlugMap() { if ($this->slugMap === null) { throw new PhutilInvalidStateException('execute'); } return $this->slugMap; } protected function willExecute() { $this->slugMap = array(); $this->slugNormals = array(); $this->allSlugs = array(); if ($this->slugs) { foreach ($this->slugs as $slug) { if (PhabricatorSlug::isValidProjectSlug($slug)) { $normal = PhabricatorSlug::normalizeProjectSlug($slug); $this->slugNormals[$slug] = $normal; $this->allSlugs[$normal] = $normal; } // NOTE: At least for now, we query for the normalized slugs but also // for the slugs exactly as entered. This allows older projects with // slugs that are no longer valid to continue to work. $this->allSlugs[$slug] = $slug; } } } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $projects) { $ancestor_paths = array(); foreach ($projects as $project) { foreach ($project->getAncestorProjectPaths() as $path) { $ancestor_paths[$path] = $path; } } if ($ancestor_paths) { $ancestors = id(new PhabricatorProject())->loadAllWhere( 'projectPath IN (%Ls)', $ancestor_paths); } else { $ancestors = array(); } $projects = $this->linkProjectGraph($projects, $ancestors); $viewer_phid = $this->getViewer()->getPHID(); $material_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST; $watcher_type = PhabricatorObjectHasWatcherEdgeType::EDGECONST; $types = array(); $types[] = $material_type; if ($this->needWatchers) { $types[] = $watcher_type; } $all_graph = $this->getAllReachableAncestors($projects); // NOTE: Although we may not need much information about ancestors, we // always need to test if the viewer is a member, because we will return // ancestor projects to the policy filter via ExtendedPolicy calls. If // we skip populating membership data on a parent, the policy framework // will think the user is not a member of the parent project. $all_sources = array(); foreach ($all_graph as $project) { // For milestones, we need parent members. if ($project->isMilestone()) { $parent_phid = $project->getParentProjectPHID(); $all_sources[$parent_phid] = $parent_phid; } $phid = $project->getPHID(); $all_sources[$phid] = $phid; } $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($all_sources) ->withEdgeTypes($types); $need_all_edges = $this->needMembers || $this->needWatchers || $this->needAncestorMembers; // If we only need to know if the viewer is a member, we can restrict // the query to just their PHID. $any_edges = true; if (!$need_all_edges) { if ($viewer_phid) { $edge_query->withDestinationPHIDs(array($viewer_phid)); } else { // If we don't need members or watchers and don't have a viewer PHID // (viewer is logged-out or omnipotent), they'll never be a member // so we don't need to issue this query at all. $any_edges = false; } } if ($any_edges) { $edge_query->execute(); } $membership_projects = array(); foreach ($all_graph as $project) { $project_phid = $project->getPHID(); if ($project->isMilestone()) { $source_phids = array($project->getParentProjectPHID()); } else { $source_phids = array($project_phid); } if ($any_edges) { $member_phids = $edge_query->getDestinationPHIDs( $source_phids, array($material_type)); } else { $member_phids = array(); } if (in_array($viewer_phid, $member_phids)) { $membership_projects[$project_phid] = $project; } if ($this->needMembers || $this->needAncestorMembers) { $project->attachMemberPHIDs($member_phids); } if ($this->needWatchers) { $watcher_phids = $edge_query->getDestinationPHIDs( array($project_phid), array($watcher_type)); $project->attachWatcherPHIDs($watcher_phids); $project->setIsUserWatcher( $viewer_phid, in_array($viewer_phid, $watcher_phids)); } } // If we loaded ancestor members, we've already populated membership // lists above, so we can skip this step. if (!$this->needAncestorMembers) { $member_graph = $this->getAllReachableAncestors($membership_projects); foreach ($all_graph as $phid => $project) { $is_member = isset($member_graph[$phid]); $project->setIsUserMember($viewer_phid, $is_member); } } return $projects; } protected function didFilterPage(array $projects) { if ($this->needImages) { - $default = null; - $file_phids = mpull($projects, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); if ($file_phids) { $files = id(new PhabricatorFileQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($file_phids) ->execute(); $files = mpull($files, null, 'getPHID'); } else { $files = array(); } foreach ($projects as $project) { $file = idx($files, $project->getProfileImagePHID()); if (!$file) { - if (!$default) { - $default = PhabricatorFile::loadBuiltin( - $this->getViewer(), - 'project.png'); - } - $file = $default; + $builtin = PhabricatorProjectIconSet::getIconImage( + $project->getIcon()); + $file = PhabricatorFile::loadBuiltin($this->getViewer(), + 'projects/'.$builtin); } $project->attachProfileImageFile($file); } } $this->loadSlugs($projects); return $projects; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->status != self::STATUS_ANY) { switch ($this->status) { case self::STATUS_OPEN: case self::STATUS_ACTIVE: $filter = array( PhabricatorProjectStatus::STATUS_ACTIVE, ); break; case self::STATUS_CLOSED: case self::STATUS_ARCHIVED: $filter = array( PhabricatorProjectStatus::STATUS_ARCHIVED, ); break; default: throw new Exception( pht( "Unknown project status '%s'!", $this->status)); } $where[] = qsprintf( $conn, 'status IN (%Ld)', $filter); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'status IN (%Ls)', $this->statuses); } if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->memberPHIDs !== null) { $where[] = qsprintf( $conn, 'e.dst IN (%Ls)', $this->memberPHIDs); } if ($this->watcherPHIDs !== null) { $where[] = qsprintf( $conn, 'w.dst IN (%Ls)', $this->watcherPHIDs); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'slug.slug IN (%Ls)', $this->allSlugs); } if ($this->names !== null) { $where[] = qsprintf( $conn, 'name IN (%Ls)', $this->names); } if ($this->namePrefixes) { $parts = array(); foreach ($this->namePrefixes as $name_prefix) { $parts[] = qsprintf( $conn, 'name LIKE %>', $name_prefix); } $where[] = '('.implode(' OR ', $parts).')'; } if ($this->icons !== null) { $where[] = qsprintf( $conn, 'icon IN (%Ls)', $this->icons); } if ($this->colors !== null) { $where[] = qsprintf( $conn, 'color IN (%Ls)', $this->colors); } if ($this->parentPHIDs !== null) { $where[] = qsprintf( $conn, 'parentProjectPHID IN (%Ls)', $this->parentPHIDs); } if ($this->ancestorPHIDs !== null) { $ancestor_paths = queryfx_all( $conn, 'SELECT projectPath, projectDepth FROM %T WHERE phid IN (%Ls)', id(new PhabricatorProject())->getTableName(), $this->ancestorPHIDs); if (!$ancestor_paths) { throw new PhabricatorEmptyQueryException(); } $sql = array(); foreach ($ancestor_paths as $ancestor_path) { $sql[] = qsprintf( $conn, '(projectPath LIKE %> AND projectDepth > %d)', $ancestor_path['projectPath'], $ancestor_path['projectDepth']); } $where[] = '('.implode(' OR ', $sql).')'; $where[] = qsprintf( $conn, 'parentProjectPHID IS NOT NULL'); } if ($this->isMilestone !== null) { if ($this->isMilestone) { $where[] = qsprintf( $conn, 'milestoneNumber IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'milestoneNumber IS NULL'); } } if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, 'hasSubprojects = %d', (int)$this->hasSubprojects); } if ($this->minDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth >= %d', $this->minDepth); } if ($this->maxDepth !== null) { $where[] = qsprintf( $conn, 'projectDepth <= %d', $this->maxDepth); } if ($this->minMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber >= %d', $this->minMilestoneNumber); } if ($this->maxMilestoneNumber !== null) { $where[] = qsprintf( $conn, 'milestoneNumber <= %d', $this->maxMilestoneNumber); } return $where; } protected function shouldGroupQueryResultRows() { if ($this->memberPHIDs || $this->watcherPHIDs || $this->nameTokens) { return true; } return parent::shouldGroupQueryResultRows(); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->memberPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T e ON e.src = p.phid AND e.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorProjectMaterializedMemberEdgeType::EDGECONST); } if ($this->watcherPHIDs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T w ON w.src = p.phid AND w.type = %d', PhabricatorEdgeConfig::TABLE_NAME_EDGE, PhabricatorObjectHasWatcherEdgeType::EDGECONST); } if ($this->slugs !== null) { $joins[] = qsprintf( $conn, 'JOIN %T slug on slug.projectPHID = p.phid', id(new PhabricatorProjectSlug())->getTableName()); } if ($this->nameTokens !== null) { foreach ($this->nameTokens as $key => $token) { $token_table = 'token_'.$key; $joins[] = qsprintf( $conn, 'JOIN %T %T ON %T.projectID = p.id AND %T.token LIKE %>', PhabricatorProject::TABLE_DATASOURCE_TOKEN, $token_table, $token_table, $token_table, $token); } } return $joins; } public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } protected function getPrimaryTableAlias() { return 'p'; } private function linkProjectGraph(array $projects, array $ancestors) { $ancestor_map = mpull($ancestors, null, 'getPHID'); $projects_map = mpull($projects, null, 'getPHID'); $all_map = $projects_map + $ancestor_map; $done = array(); foreach ($projects as $key => $project) { $seen = array($project->getPHID() => true); if (!$this->linkProject($project, $all_map, $done, $seen)) { $this->didRejectResult($project); unset($projects[$key]); continue; } foreach ($project->getAncestorProjects() as $ancestor) { $seen[$ancestor->getPHID()] = true; } } return $projects; } private function linkProject($project, array $all, array $done, array $seen) { $parent_phid = $project->getParentProjectPHID(); // This project has no parent, so just attach `null` and return. if (!$parent_phid) { $project->attachParentProject(null); return true; } // This project has a parent, but it failed to load. if (empty($all[$parent_phid])) { return false; } // Test for graph cycles. If we encounter one, we're going to hide the // entire cycle since we can't meaningfully resolve it. if (isset($seen[$parent_phid])) { return false; } $seen[$parent_phid] = true; $parent = $all[$parent_phid]; $project->attachParentProject($parent); if (!empty($done[$parent_phid])) { return true; } return $this->linkProject($parent, $all, $done, $seen); } private function getAllReachableAncestors(array $projects) { $ancestors = array(); $seen = mpull($projects, null, 'getPHID'); $stack = $projects; while ($stack) { $project = array_pop($stack); $phid = $project->getPHID(); $ancestors[$phid] = $project; $parent_phid = $project->getParentProjectPHID(); if (!$parent_phid) { continue; } if (isset($seen[$parent_phid])) { continue; } $seen[$parent_phid] = true; $stack[] = $project->getParentProject(); } return $ancestors; } private function loadSlugs(array $projects) { // Build a map from primary slugs to projects. $primary_map = array(); foreach ($projects as $project) { $primary_slug = $project->getPrimarySlug(); if ($primary_slug === null) { continue; } $primary_map[$primary_slug] = $project; } // Link up all of the queried slugs which correspond to primary // slugs. If we can link up everything from this (no slugs were queried, // or only primary slugs were queried) we don't need to load anything // else. $unknown = $this->slugNormals; foreach ($unknown as $input => $normal) { if (isset($primary_map[$input])) { $match = $input; } else if (isset($primary_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $primary_map[$match]->getPHID(), ); unset($unknown[$input]); } // If we need slugs, we have to load everything. // If we still have some queried slugs which we haven't mapped, we only // need to look for them. // If we've mapped everything, we don't have to do any work. $project_phids = mpull($projects, 'getPHID'); if ($this->needSlugs) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls)', $project_phids); } else if ($unknown) { $slugs = id(new PhabricatorProjectSlug())->loadAllWhere( 'projectPHID IN (%Ls) AND slug IN (%Ls)', $project_phids, $unknown); } else { $slugs = array(); } // Link up any slugs we were not able to link up earlier. $extra_map = mpull($slugs, 'getProjectPHID', 'getSlug'); foreach ($unknown as $input => $normal) { if (isset($extra_map[$input])) { $match = $input; } else if (isset($extra_map[$normal])) { $match = $normal; } else { continue; } $this->slugMap[$input] = array( 'slug' => $match, 'projectPHID' => $extra_map[$match], ); unset($unknown[$input]); } if ($this->needSlugs) { $slug_groups = mgroup($slugs, 'getProjectPHID'); foreach ($projects as $project) { $project_slugs = idx($slug_groups, $project->getPHID(), array()); $project->attachSlugs($project_slugs); } } } }