diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php index 608bac675e..df5821665c 100644 --- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php +++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php @@ -1,100 +1,100 @@ <?php final class PhabricatorDaemonsSetupCheck extends PhabricatorSetupCheck { public function getDefaultGroup() { return self::GROUP_IMPORTANT; } protected function executeChecks() { try { $task_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->withDaemonClasses(array('PhabricatorTaskmasterDaemon')) ->setLimit(1) ->execute(); $no_daemons = !$task_daemons; } catch (Exception $ex) { // Just skip this warning if the query fails for some reason. $no_daemons = false; } if ($no_daemons) { $doc_href = PhabricatorEnv::getDoclink('Managing Daemons with phd'); $summary = pht( 'You must start the daemons to send email, rebuild search indexes, '. 'and do other background processing.'); $message = pht( 'The daemons are not running, background processing (including '. 'sending email, rebuilding search indexes, importing commits, '. 'cleaning up old data, and running builds) can not be performed.'. "\n\n". 'Use %s to start daemons. See %s for more information.', phutil_tag('tt', array(), 'bin/phd start'), phutil_tag( 'a', array( 'href' => $doc_href, 'target' => '_blank', ), pht('Managing Daemons with phd'))); $this->newIssue('daemons.not-running') ->setShortName(pht('Daemons Not Running')) ->setName(pht('Daemons Are Not Running')) ->setSummary($summary) ->setMessage($message) ->addCommand('$ ./bin/phd start'); } $expect_user = PhabricatorEnv::getEnvConfig('phd.user'); - if (strlen($expect_user)) { + if (phutil_nonempty_string($expect_user)) { try { $all_daemons = id(new PhabricatorDaemonLogQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) ->execute(); } catch (Exception $ex) { // If this query fails for some reason, just skip this check. $all_daemons = array(); } foreach ($all_daemons as $daemon) { $actual_user = $daemon->getRunningAsUser(); if ($actual_user == $expect_user) { continue; } $summary = pht( 'At least one daemon is currently running as the wrong user.'); $message = pht( 'A daemon is running as user %s, but daemons should be '. 'running as %s.'. "\n\n". 'Either adjust the configuration setting %s or restart the '. 'daemons. Daemons should attempt to run as the proper user when '. 'restarted.', phutil_tag('tt', array(), $actual_user), phutil_tag('tt', array(), $expect_user), phutil_tag('tt', array(), 'phd.user')); $this->newIssue('daemons.run-as-different-user') ->setName(pht('Daemon Running as Wrong User')) ->setSummary($summary) ->setMessage($message) ->addPhabricatorConfig('phd.user') ->addCommand('$ ./bin/phd restart'); break; } } } } diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php index 76aacbe36f..cd65c9a197 100644 --- a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php +++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php @@ -1,106 +1,106 @@ <?php /** * IMPORTANT: If you use this, make sure to implement * * public function isGlobalDragAndDropUploadEnabled() { * return true; * } * * on the controller(s) that render this class...! This is necessary * to make sure Quicksand works properly with the javascript in this * UI. */ final class PhabricatorGlobalUploadTargetView extends AphrontView { private $showIfSupportedID; private $hintText; private $viewPolicy; private $submitURI; public function setShowIfSupportedID($show_if_supported_id) { $this->showIfSupportedID = $show_if_supported_id; return $this; } public function getShowIfSupportedID() { return $this->showIfSupportedID; } public function setHintText($hint_text) { $this->hintText = $hint_text; return $this; } public function getHintText() { return $this->hintText; } public function setViewPolicy($view_policy) { $this->viewPolicy = $view_policy; return $this; } public function getViewPolicy() { return $this->viewPolicy; } public function setSubmitURI($submit_uri) { $this->submitURI = $submit_uri; return $this; } public function getSubmitURI() { return $this->submitURI; } public function render() { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } $instructions_id = 'phabricator-global-drag-and-drop-upload-instructions'; require_celerity_resource('global-drag-and-drop-css'); $hint_text = $this->getHintText(); - if (!strlen($hint_text)) { + if (!phutil_nonempty_string($hint_text)) { $hint_text = "\xE2\x87\xAA ".pht('Drop Files to Upload'); } // Use the configured default view policy. Drag and drop uploads use // a more restrictive view policy if we don't specify a policy explicitly, // as the more restrictive policy is correct for most drop targets (like // Pholio uploads and Remarkup text areas). $view_policy = $this->getViewPolicy(); if ($view_policy === null) { $view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy(); } $submit_uri = $this->getSubmitURI(); $done_uri = '/file/query/authored/'; Javelin::initBehavior('global-drag-and-drop', array( 'ifSupported' => $this->showIfSupportedID, 'instructions' => $instructions_id, 'uploadURI' => '/file/dropupload/', 'submitURI' => $submit_uri, 'browseURI' => $done_uri, 'viewPolicy' => $view_policy, 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), )); return phutil_tag( 'div', array( 'id' => $instructions_id, 'class' => 'phabricator-global-upload-instructions', 'style' => 'display: none;', ), $hint_text); } } diff --git a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php index dbf1586366..c3ebd825db 100644 --- a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php +++ b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php @@ -1,70 +1,70 @@ <?php final class PhabricatorHomeLauncherProfileMenuItem extends PhabricatorProfileMenuItem { const MENUITEMKEY = 'home.launcher.menu'; public function getMenuItemTypeName() { return pht('More Applications'); } private function getDefaultName() { return pht('More Applications'); } public function getMenuItemTypeIcon() { return 'fa-ellipsis-h'; } public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; } public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return false; } public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $name = $config->getMenuItemProperty('name'); - if (strlen($name)) { + if (phutil_nonempty_string($name)) { return $name; } return $this->getDefaultName(); } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setPlaceholder($this->getDefaultName()) ->setValue($config->getMenuItemProperty('name')), ); } protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $name = $this->getDisplayName($config); $icon = 'fa-ellipsis-h'; $uri = '/applications/'; $item = $this->newItemView() ->setURI($uri) ->setName($name) ->setIcon($icon); return array( $item, ); } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index aedbcc787f..3845c0f127 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -1,1361 +1,1361 @@ <?php abstract class PhabricatorProfileMenuEngine extends Phobject { private $viewer; private $profileObject; private $customPHID; private $items; private $controller; private $navigation; private $editMode; private $pageClasses = array(); private $showContentCrumbs = true; const ITEM_CUSTOM_DIVIDER = 'engine.divider'; const ITEM_MANAGE = 'item.configure'; const MODE_COMBINED = 'combined'; const MODE_GLOBAL = 'global'; const MODE_CUSTOM = 'custom'; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setProfileObject($profile_object) { $this->profileObject = $profile_object; return $this; } public function getProfileObject() { return $this->profileObject; } public function setCustomPHID($custom_phid) { $this->customPHID = $custom_phid; return $this; } public function getCustomPHID() { return $this->customPHID; } private function getEditModeCustomPHID() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: $custom_phid = $this->getCustomPHID(); break; case self::MODE_GLOBAL: $custom_phid = null; break; } return $custom_phid; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } public function addContentPageClass($class) { $this->pageClasses[] = $class; return $this; } public function setShowContentCrumbs($show_content_crumbs) { $this->showContentCrumbs = $show_content_crumbs; return $this; } public function getShowContentCrumbs() { return $this->showContentCrumbs; } abstract public function getItemURI($path); abstract protected function isMenuEngineConfigurable(); abstract protected function getBuiltinProfileItems($object); protected function getBuiltinCustomProfileItems( $object, $custom_phid) { return array(); } protected function getEditMode() { return $this->editMode; } public function buildResponse() { $controller = $this->getController(); $viewer = $controller->getViewer(); $this->setViewer($viewer); $request = $controller->getRequest(); $item_action = $request->getURIData('itemAction'); if (!$item_action) { $item_action = 'view'; } $is_view = ($item_action == 'view'); // If the engine is not configurable, don't respond to any of the editing // or configuration routes. if (!$this->isMenuEngineConfigurable()) { if (!$is_view) { return new Aphront404Response(); } } $item_id = $request->getURIData('itemID'); // If we miss on the MenuEngine route, try the EditEngine route. This will // be populated while editing items. if (!$item_id) { $item_id = $request->getURIData('id'); } $view_list = $this->newProfileMenuItemViewList(); if ($is_view) { $selected_item = $this->selectViewItem($view_list, $item_id); } else { if (!strlen($item_id)) { $item_id = self::ITEM_MANAGE; } $selected_item = $this->selectEditItem($view_list, $item_id); } switch ($item_action) { case 'view': // If we were not able to select an item, we're still going to render // a page state. For example, this happens when you create a new // portal for the first time. break; case 'info': case 'hide': case 'default': case 'builtin': if (!$selected_item) { return new Aphront404Response(); } break; case 'edit': if (!$request->getURIData('id')) { // If we continue along the "edit" pathway without an ID, we hit an // unrelated exception because we can not build a new menu item out // of thin air. For menus, new items are created via the "new" // action. Just catch this case and 404 early since there's currently // no clean way to make EditEngine aware of this. return new Aphront404Response(); } break; } $navigation = $view_list->newNavigationView(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if (!$is_view) { $edit_mode = null; if ($selected_item) { if ($selected_item->getBuiltinKey() !== self::ITEM_MANAGE) { if ($selected_item->getCustomPHID()) { $edit_mode = 'custom'; } else { $edit_mode = 'global'; } } } if ($edit_mode === null) { $edit_mode = $request->getURIData('itemEditMode'); } $available_modes = $this->getViewerEditModes(); if ($available_modes) { $available_modes = array_fuse($available_modes); if (isset($available_modes[$edit_mode])) { $this->editMode = $edit_mode; } else { if ($item_action != 'configure') { return new Aphront404Response(); } } } $page_title = pht('Configure Menu'); } else { if ($selected_item) { $page_title = $selected_item->getDisplayName(); } else { $page_title = pht('Empty'); } } switch ($item_action) { case 'view': if ($selected_item) { try { $content = $this->buildItemViewContent($selected_item); } catch (Exception $ex) { $content = id(new PHUIInfoView()) ->setTitle(pht('Unable to Render Dashboard')) ->setErrors(array($ex->getMessage())); } $crumbs->addTextCrumb($selected_item->getDisplayName()); } else { $content = $this->newNoContentView($this->getItems()); } if (!$content) { $content = $this->newEmptyView( pht('Empty'), pht('There is nothing here.')); } break; case 'configure': $mode = $this->getEditMode(); if (!$mode) { $crumbs->addTextCrumb(pht('Configure Menu')); $content = $this->buildMenuEditModeContent(); } else { if (count($available_modes) > 1) { $crumbs->addTextCrumb( pht('Configure Menu'), $this->getItemURI('configure/')); switch ($mode) { case self::MODE_CUSTOM: $crumbs->addTextCrumb(pht('Personal')); break; case self::MODE_GLOBAL: $crumbs->addTextCrumb(pht('Global')); break; } } else { $crumbs->addTextCrumb(pht('Configure Menu')); } $edit_list = $this->loadItems($mode); $content = $this->buildItemConfigureContent($edit_list); } break; case 'reorder': $mode = $this->getEditMode(); $edit_list = $this->loadItems($mode); $content = $this->buildItemReorderContent($edit_list); break; case 'new': $item_key = $request->getURIData('itemKey'); $mode = $this->getEditMode(); $content = $this->buildItemNewContent($item_key, $mode); break; case 'builtin': $content = $this->buildItemBuiltinContent($selected_item); break; case 'hide': $content = $this->buildItemHideContent($selected_item); break; case 'default': if (!$this->isMenuEnginePinnable()) { return new Aphront404Response(); } $content = $this->buildItemDefaultContent($selected_item); break; case 'edit': $content = $this->buildItemEditContent(); break; default: throw new Exception( pht( 'Unsupported item action "%s".', $item_action)); } if ($content instanceof AphrontResponse) { return $content; } if ($content instanceof AphrontResponseProducerInterface) { return $content; } $crumbs->setBorder(true); $page = $controller->newPage() ->setTitle($page_title) ->appendChild($content); if (!$is_view || $this->getShowContentCrumbs()) { $page->setCrumbs($crumbs); } $page->setNavigation($navigation); if ($is_view) { foreach ($this->pageClasses as $class) { $page->addClass($class); } } return $page; } private function getItems() { if ($this->items === null) { $this->items = $this->loadItems(self::MODE_COMBINED); } return $this->items; } private function loadItems($mode) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $items = $this->loadBuiltinProfileItems($mode); $query = id(new PhabricatorProfileMenuItemConfigurationQuery()) ->setViewer($viewer) ->withProfilePHIDs(array($object->getPHID())); switch ($mode) { case self::MODE_GLOBAL: $query->withCustomPHIDs(array(), true); break; case self::MODE_CUSTOM: $query->withCustomPHIDs(array($this->getCustomPHID()), false); break; case self::MODE_COMBINED: $query->withCustomPHIDs(array($this->getCustomPHID()), true); break; } $stored_items = $query->execute(); foreach ($stored_items as $stored_item) { $impl = $stored_item->getMenuItem(); $impl->setViewer($viewer); $impl->setEngine($this); } // Merge the stored items into the builtin items. If a builtin item has // a stored version, replace the defaults with the stored changes. foreach ($stored_items as $stored_item) { if (!$stored_item->shouldEnableForObject($object)) { continue; } $builtin_key = $stored_item->getBuiltinKey(); if ($builtin_key !== null) { // If this builtin actually exists, replace the builtin with the // stored configuration. Otherwise, we're just going to drop the // stored config: it corresponds to an out-of-date or uninstalled // item. if (isset($items[$builtin_key])) { $builtin_item = $items[$builtin_key]; // Copy runtime properties from the builtin item to the stored item. $stored_item->setIsHeadItem($builtin_item->getIsHeadItem()); $stored_item->setIsTailItem($builtin_item->getIsTailItem()); $items[$builtin_key] = $stored_item; } else { continue; } } else { $items[] = $stored_item; } } return $this->arrangeItems($items, $mode); } private function loadBuiltinProfileItems($mode) { $object = $this->getProfileObject(); switch ($mode) { case self::MODE_GLOBAL: $builtins = $this->getBuiltinProfileItems($object); break; case self::MODE_CUSTOM: $builtins = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); break; case self::MODE_COMBINED: $builtins = array(); $builtins[] = $this->getBuiltinCustomProfileItems( $object, $this->getCustomPHID()); $builtins[] = $this->getBuiltinProfileItems($object); $builtins = array_mergev($builtins); break; } $items = PhabricatorProfileMenuItem::getAllMenuItems(); $viewer = $this->getViewer(); $order = 1; $map = array(); foreach ($builtins as $builtin) { $builtin_key = $builtin->getBuiltinKey(); if (!$builtin_key) { throw new Exception( pht( 'Object produced a builtin item with no builtin item key! '. 'Builtin items must have a unique key.')); } if (isset($map[$builtin_key])) { throw new Exception( pht( 'Object produced two items with the same builtin key ("%s"). '. 'Each item must have a unique builtin key.', $builtin_key)); } $item_key = $builtin->getMenuItemKey(); $item = idx($items, $item_key); if (!$item) { throw new Exception( pht( 'Builtin item ("%s") specifies a bad item key ("%s"); there '. 'is no corresponding item implementation available.', $builtin_key, $item_key)); } $item = clone $item; $item->setViewer($viewer); $item->setEngine($this); $builtin ->setProfilePHID($object->getPHID()) ->attachMenuItem($item) ->attachProfileObject($object) ->setMenuItemOrder($order); if (!$builtin->shouldEnableForObject($object)) { continue; } $map[$builtin_key] = $builtin; $order++; } return $map; } private function validateNavigationMenuItem($item) { if (!($item instanceof PHUIListItemView)) { throw new Exception( pht( 'Expected buildNavigationMenuItems() to return a list of '. 'PHUIListItemView objects, but got a surprise.')); } } public function getConfigureURI() { $mode = $this->getEditMode(); switch ($mode) { case self::MODE_CUSTOM: return $this->getItemURI('configure/custom/'); case self::MODE_GLOBAL: return $this->getItemURI('configure/global/'); } return $this->getItemURI('configure/'); } private function buildItemReorderContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); // If you're reordering global items, you need to be able to edit the // object the menu appears on. If you're reordering custom items, you only // need to be able to edit the custom object. Currently, the custom object // is always the viewing user's own user object. $custom_phid = $this->getEditModeCustomPHID(); if (!$custom_phid) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } else { $policy_object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($custom_phid)) ->executeOne(); if (!$policy_object) { throw new Exception( pht( 'Failed to load custom PHID "%s"!', $custom_phid)); } PhabricatorPolicyFilter::requireCapability( $viewer, $policy_object, PhabricatorPolicyCapability::CAN_EDIT); } $controller = $this->getController(); $request = $controller->getRequest(); $request->validateCSRF(); $order = $request->getStrList('order'); $by_builtin = array(); $by_id = array(); foreach ($items as $key => $item) { $id = $item->getID(); if ($id) { $by_id[$id] = $key; continue; } $builtin_key = $item->getBuiltinKey(); if ($builtin_key) { $by_builtin[$builtin_key] = $key; continue; } } $key_order = array(); foreach ($order as $order_item) { if (isset($by_id[$order_item])) { $key_order[] = $by_id[$order_item]; continue; } if (isset($by_builtin[$order_item])) { $key_order[] = $by_builtin[$order_item]; continue; } } $items = array_select_keys($items, $key_order) + $items; $type_order = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_ORDER; $order = 1; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_order) ->setNewValue($order); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); $order++; } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } protected function buildItemViewContent( PhabricatorProfileMenuItemConfiguration $item) { return $item->newPageContent(); } private function getViewerEditModes() { $modes = array(); $viewer = $this->getViewer(); if ($viewer->isLoggedIn() && $this->isMenuEnginePersonalizable()) { $modes[] = self::MODE_CUSTOM; } $object = $this->getProfileObject(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); if ($can_edit) { $modes[] = self::MODE_GLOBAL; } return $modes; } protected function isMenuEnginePersonalizable() { return true; } /** * Does this engine support pinning items? * * Personalizable menus disable pinning by default since it creates a number * of weird edge cases without providing many benefits for current menus. * * @return bool True if items may be pinned as default items. */ public function isMenuEnginePinnable() { return !$this->isMenuEnginePersonalizable(); } private function buildMenuEditModeContent() { $viewer = $this->getViewer(); $modes = $this->getViewerEditModes(); if (!$modes) { return new Aphront404Response(); } if (count($modes) == 1) { $mode = head($modes); return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("configure/{$mode}/")); } $menu = id(new PHUIObjectItemListView()) ->setUser($viewer); $modes = array_fuse($modes); if (isset($modes['custom'])) { $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Personal Menu Items')) ->setHref($this->getItemURI('configure/custom/')) ->setImageURI($viewer->getProfileImageURI()) ->addAttribute(pht('Edit the menu for your personal account.'))); } if (isset($modes['global'])) { $icon = id(new PHUIIconView()) ->setIcon('fa-globe') ->setBackground('bg-blue'); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Global Menu Items')) ->setHref($this->getItemURI('configure/global/')) ->setImageIcon($icon) ->addAttribute(pht('Edit the global default menu for all users.'))); } $box = id(new PHUIObjectBoxView()) ->setObjectList($menu); $header = id(new PHUIHeaderView()) ->setHeader(pht('Manage Menu')) ->setHeaderIcon('fa-list'); return id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter($box); } private function buildItemConfigureContent(array $items) { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $filtered_groups = mgroup($items, 'getMenuItemKey'); foreach ($filtered_groups as $group) { $first_item = head($group); $first_item->willGetMenuItemViewList($group); } // Users only need to be able to edit the object which this menu appears // on if they're editing global menu items. For example, users do not need // to be able to edit the Favorites application to add new items to the // Favorites menu. if (!$this->getCustomPHID()) { PhabricatorPolicyFilter::requireCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); } $list_id = celerity_generate_unique_node_id(); $mode = $this->getEditMode(); Javelin::initBehavior( 'reorder-profile-menu-items', array( 'listID' => $list_id, 'orderURI' => $this->getItemURI("reorder/{$mode}/"), )); $list = id(new PHUIObjectItemListView()) ->setID($list_id) ->setNoDataString(pht('This menu currently has no items.')); $any_draggable = false; foreach ($items as $item) { $id = $item->getID(); $builtin_key = $item->getBuiltinKey(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $item, PhabricatorPolicyCapability::CAN_EDIT); $view = id(new PHUIObjectItemView()); $name = $item->getDisplayName(); $type = $item->getMenuItemTypeName(); if (!strlen(trim($name))) { $name = pht('Untitled "%s" Item', $type); } $view->setHeader($name); $view->addAttribute($type); $icon = $item->getMenuItem()->getMenuItemTypeIcon(); if ($icon !== null) { $view->setStatusIcon($icon); } if ($can_edit) { $can_move = (!$item->getIsHeadItem() && !$item->getIsTailItem()); if ($can_move) { $view ->setGrippable(true) ->addSigil('profile-menu-item') ->setMetadata( array( 'key' => nonempty($id, $builtin_key), )); $any_draggable = true; } else { $view->setGrippable(false); } if ($id) { $default_uri = $this->getItemURI("default/{$id}/"); } else { $default_uri = $this->getItemURI("default/{$builtin_key}/"); } $default_text = null; if ($this->isMenuEnginePinnable()) { if ($item->isDefault()) { $default_icon = 'fa-thumb-tack green'; $default_text = pht('Current Default'); } else if ($item->canMakeDefault()) { $default_icon = 'fa-thumb-tack'; $default_text = pht('Make Default'); } } if ($default_text !== null) { $view->addAction( id(new PHUIListItemView()) ->setHref($default_uri) ->setWorkflow(true) ->setName($default_text) ->setIcon($default_icon)); } if ($id) { $view->setHref($this->getItemURI("edit/{$id}/")); $hide_uri = $this->getItemURI("hide/{$id}/"); } else { $view->setHref($this->getItemURI("builtin/{$builtin_key}/")); $hide_uri = $this->getItemURI("hide/{$builtin_key}/"); } if ($item->isDisabled()) { $hide_icon = 'fa-plus'; $hide_text = pht('Enable'); } else if ($item->getBuiltinKey() !== null) { $hide_icon = 'fa-times'; $hide_text = pht('Disable'); } else { $hide_icon = 'fa-times'; $hide_text = pht('Delete'); } $can_disable = $item->canHideMenuItem(); $view->addAction( id(new PHUIListItemView()) ->setHref($hide_uri) ->setWorkflow(true) ->setDisabled(!$can_disable) ->setName($hide_text) ->setIcon($hide_icon)); } if ($item->isDisabled()) { $view->setDisabled(true); } $list->addItem($view); } $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $object = $this->getProfileObject(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); // See T12167. This makes the "Actions" dropdown button show up in the // page header. $action_list->setID(celerity_generate_unique_node_id()); $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Add New Menu Item...'))); foreach ($item_types as $item_type) { if (!$item_type->canAddToObject($object)) { continue; } $item_key = $item_type->getMenuItemKey(); $edit_mode = $this->getEditMode(); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon($item_type->getMenuItemTypeIcon()) ->setName($item_type->getMenuItemTypeName()) ->setHref($this->getItemURI("new/{$edit_mode}/{$item_key}/")) ->setWorkflow(true)); } $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) ->setName(pht('Documentation'))); $doc_link = PhabricatorEnv::getDoclink('Profile Menu User Guide'); $doc_name = pht('Profile Menu User Guide'); $action_list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-book') ->setHref($doc_link) ->setName($doc_name)); $header = id(new PHUIHeaderView()) ->setHeader(pht('Menu Items')) ->setHeaderIcon('fa-list'); $list_header = id(new PHUIHeaderView()) ->setHeader(pht('Current Menu Items')); if ($any_draggable) { $list_header->setSubheader( pht('Drag items in this list to reorder them.')); } $box = id(new PHUIObjectBoxView()) ->setHeader($list_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); $curtain = id(new PHUICurtainView()) ->setViewer($viewer) ->setActionList($action_list); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn( array( $box, )); return $view; } private function buildItemNewContent($item_key, $mode) { $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $item_type = idx($item_types, $item_key); if (!$item_type) { return new Aphront404Response(); } $object = $this->getProfileObject(); if (!$item_type->canAddToObject($object)) { return new Aphront404Response(); } $custom_phid = $this->getEditModeCustomPHID(); $configuration = PhabricatorProfileMenuItemConfiguration::initializeNewItem( $object, $item_type, $custom_phid); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $controller = $this->getController(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setCustomPHID($custom_phid) ->setController($controller) ->buildResponse(); } private function buildItemEditContent() { $viewer = $this->getViewer(); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setMenuEngine($this) ->setProfileObject($object) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemBuiltinContent( PhabricatorProfileMenuItemConfiguration $configuration) { // If this builtin item has already been persisted, redirect to the // edit page. $id = $configuration->getID(); if ($id) { return id(new AphrontRedirectResponse()) ->setURI($this->getItemURI("edit/{$id}/")); } // Otherwise, act like we're creating a new item, we're just starting // with the builtin template. $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $object = $this->getProfileObject(); $controller = $this->getController(); $custom_phid = $this->getEditModeCustomPHID(); return id(new PhabricatorProfileMenuEditEngine()) ->setIsBuiltin(true) ->setMenuEngine($this) ->setProfileObject($object) ->setNewMenuItemConfiguration($configuration) ->setController($controller) ->setCustomPHID($custom_phid) ->buildResponse(); } private function buildItemHideContent( PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); if (!$configuration->canHideMenuItem()) { return $controller->newDialog() ->setTitle(pht('Mandatory Item')) ->appendParagraph( pht('This menu item is very important, and can not be disabled.')) ->addCancelButton($this->getConfigureURI()); } if ($configuration->getBuiltinKey() === null) { $new_value = null; $title = pht('Delete Menu Item'); $body = pht('Delete this menu item?'); $button = pht('Delete Menu Item'); } else if ($configuration->isDisabled()) { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $title = pht('Enable Menu Item'); $body = pht( 'Enable this menu item? It will appear in the menu again.'); $button = pht('Enable Menu Item'); } else { $new_value = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DISABLED; $title = pht('Disable Menu Item'); $body = pht( 'Disable this menu item? It will no longer appear in the menu, but '. 'you can re-enable it later.'); $button = pht('Disable Menu Item'); } $v_visibility = $configuration->getVisibility(); if ($request->isFormPost()) { if ($new_value === null) { $configuration->delete(); } else { $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($new_value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($configuration, $xactions); } return id(new AphrontRedirectResponse()) ->setURI($this->getConfigureURI()); } return $controller->newDialog() ->setTitle($title) ->appendParagraph($body) ->addCancelButton($this->getConfigureURI()) ->addSubmitButton($button); } private function buildItemDefaultContent( PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $this->getViewer(); PhabricatorPolicyFilter::requireCapability( $viewer, $configuration, PhabricatorPolicyCapability::CAN_EDIT); $done_uri = $this->getConfigureURI(); if (!$configuration->canMakeDefault()) { return $controller->newDialog() ->setTitle(pht('Not Defaultable')) ->appendParagraph( pht( 'This item can not be set as the default item. This is usually '. 'because the item has no page of its own, or links to an '. 'external page.')) ->addCancelButton($done_uri); } if ($configuration->isDefault()) { return $controller->newDialog() ->setTitle(pht('Already Default')) ->appendParagraph( pht( 'This item is already set as the default item for this menu.')) ->addCancelButton($done_uri); } if ($request->isFormPost()) { $key = $configuration->getID(); if (!$key) { $key = $configuration->getBuiltinKey(); } $this->adjustDefault($key); return id(new AphrontRedirectResponse()) ->setURI($done_uri); } return $controller->newDialog() ->setTitle(pht('Make Default')) ->appendParagraph( pht( 'Set this item as the default for this menu? Users arriving on '. 'this page will be shown the content of this item by default.')) ->addCancelButton($done_uri) ->addSubmitButton(pht('Make Default')); } protected function newItem() { return PhabricatorProfileMenuItemConfiguration::initializeNewBuiltin(); } protected function newManageItem() { return $this->newItem() ->setBuiltinKey(self::ITEM_MANAGE) ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) ->setIsTailItem(true); } protected function newDividerItem($key) { return $this->newItem() ->setBuiltinKey($key) ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) ->setIsTailItem(true); } public function getDefaultMenuItemConfiguration() { $configs = $this->getItems(); foreach ($configs as $config) { if ($config->isDefault()) { return $config; } } return null; } public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); $viewer = $request->getViewer(); $items = $this->loadItems(self::MODE_COMBINED); // To adjust the default item, we first change any existing items that // are marked as defaults to "visible", then make the new default item // the default. $default = array(); $visible = array(); foreach ($items as $item) { $builtin_key = $item->getBuiltinKey(); $id = $item->getID(); $is_target = (($builtin_key !== null) && ($builtin_key === $key)) || (($id !== null) && ((int)$id === (int)$key)); if ($is_target) { if (!$item->isDefault()) { $default[] = $item; } } else { if ($item->isDefault()) { $visible[] = $item; } } } $type_visibility = PhabricatorProfileMenuItemConfigurationTransaction::TYPE_VISIBILITY; $v_visible = PhabricatorProfileMenuItemConfiguration::VISIBILITY_VISIBLE; $v_default = PhabricatorProfileMenuItemConfiguration::VISIBILITY_DEFAULT; $apply = array( array($v_visible, $visible), array($v_default, $default), ); foreach ($apply as $group) { list($value, $items) = $group; foreach ($items as $item) { $xactions = array(); $xactions[] = id(new PhabricatorProfileMenuItemConfigurationTransaction()) ->setTransactionType($type_visibility) ->setNewValue($value); $editor = id(new PhabricatorProfileMenuEditor()) ->setContentSourceFromRequest($request) ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); } } return $this; } private function arrangeItems(array $items, $mode) { // Sort the items. $items = msortv($items, 'getSortVector'); $object = $this->getProfileObject(); // If we have some global items and some custom items and are in "combined" // mode, put a hard-coded divider item between them. if ($mode == self::MODE_COMBINED) { $list = array(); $seen_custom = false; $seen_global = false; foreach ($items as $item) { if ($item->getCustomPHID()) { $seen_custom = true; } else { if ($seen_custom && !$seen_global) { $list[] = $this->newItem() ->setBuiltinKey(self::ITEM_CUSTOM_DIVIDER) ->setMenuItemKey(PhabricatorDividerProfileMenuItem::MENUITEMKEY) ->attachProfileObject($object) ->attachMenuItem( new PhabricatorDividerProfileMenuItem()); } $seen_global = true; } $list[] = $item; } $items = $list; } // Normalize keys since callers shouldn't rely on this array being // partially keyed. $items = array_values($items); return $items; } final protected function newEmptyView($title, $message) { return id(new PHUIInfoView()) ->setTitle($title) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->setErrors( array( $message, )); } protected function newNoContentView(array $items) { return $this->newEmptyView( pht('No Content'), pht('No visible menu items can render content.')); } final public function newProfileMenuItemViewList() { $items = $this->getItems(); // Throw away disabled items: they are not allowed to build any views for // the menu. foreach ($items as $key => $item) { if ($item->isDisabled()) { unset($items[$key]); continue; } } // Give each item group a callback so it can load data it needs to render // views. $groups = mgroup($items, 'getMenuItemKey'); foreach ($groups as $group) { $item = head($group); $item->willGetMenuItemViewList($group); } $view_list = id(new PhabricatorProfileMenuItemViewList()) ->setProfileMenuEngine($this); foreach ($items as $item) { $views = $item->getMenuItemViewList(); foreach ($views as $view) { $view_list->addItemView($view); } } return $view_list; } private function selectViewItem( PhabricatorProfileMenuItemViewList $view_list, $item_id) { // Figure out which view's content we're going to render. In most cases, // the URI tells us. If we don't have an identifier in the URI, we'll // render the default view instead. $selected_view = null; - if (strlen($item_id)) { + if (phutil_nonempty_string($item_id)) { $item_views = $view_list->getViewsWithItemIdentifier($item_id); if ($item_views) { $selected_view = head($item_views); } } else { $default_views = $view_list->getDefaultViews(); if ($default_views) { $selected_view = head($default_views); } } if ($selected_view) { $view_list->setSelectedView($selected_view); $selected_item = $selected_view->getMenuItemConfiguration(); } else { $selected_item = null; } return $selected_item; } private function selectEditItem( PhabricatorProfileMenuItemViewList $view_list, $item_id) { // First, try to select a visible item using the normal view selection // pathway. If this works, it also highlights the menu properly. if ($item_id) { $selected_item = $this->selectViewItem($view_list, $item_id); if ($selected_item) { return $selected_item; } } // If we didn't find an item in the view list, we may be enabling an item // which is currently disabled or editing an item which is not generating // any actual items in the menu. foreach ($this->getItems() as $item) { if ($item->matchesIdentifier($item_id)) { return $item; } } return null; } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemView.php b/src/applications/search/engine/PhabricatorProfileMenuItemView.php index d947afcba6..7f0dd1d397 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuItemView.php +++ b/src/applications/search/engine/PhabricatorProfileMenuItemView.php @@ -1,232 +1,232 @@ <?php final class PhabricatorProfileMenuItemView extends Phobject { private $config; private $uri; private $name; private $icon; private $iconImage; private $disabled; private $tooltip; private $actions = array(); private $counts = array(); private $images = array(); private $progressBars = array(); private $isExternalLink; private $specialType; public function setMenuItemConfiguration( PhabricatorProfileMenuItemConfiguration $config) { $this->config = $config; return $this; } public function getMenuItemConfiguration() { return $this->config; } public function setURI($uri) { $this->uri = $uri; return $this; } public function getURI() { return $this->uri; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function getIcon() { return $this->icon; } public function setIconImage($icon_image) { $this->iconImage = $icon_image; return $this; } public function getIconImage() { return $this->iconImage; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function setTooltip($tooltip) { $this->tooltip = $tooltip; return $this; } public function getTooltip() { return $this->tooltip; } public function newAction($uri) { $this->actions[] = $uri; return null; } public function newCount($count) { $this->counts[] = $count; return null; } public function newProfileImage($src) { $this->images[] = $src; return null; } public function newProgressBar($bar) { $this->progressBars[] = $bar; return null; } public function setIsExternalLink($is_external) { $this->isExternalLink = $is_external; return $this; } public function getIsExternalLink() { return $this->isExternalLink; } public function setIsLabel($is_label) { return $this->setSpecialType('label'); } public function getIsLabel() { return $this->isSpecialType('label'); } public function setIsDivider($is_divider) { return $this->setSpecialType('divider'); } public function getIsDivider() { return $this->isSpecialType('divider'); } private function setSpecialType($type) { $this->specialType = $type; return $this; } private function isSpecialType($type) { return ($this->specialType === $type); } public function newListItemView() { $view = id(new PHUIListItemView()) ->setName($this->getName()); $uri = $this->getURI(); - if (strlen($uri)) { + if (phutil_nonempty_string($uri)) { if ($this->getIsExternalLink()) { if (!PhabricatorEnv::isValidURIForLink($uri)) { $uri = '#'; } $view->setRel('noreferrer'); } $view->setHref($uri); } $icon = $this->getIcon(); if ($icon) { $view->setIcon($icon); } $icon_image = $this->getIconImage(); if ($icon_image) { $view->setProfileImage($icon_image); } if ($this->getDisabled()) { $view->setDisabled(true); } if ($this->getIsLabel()) { $view->setType(PHUIListItemView::TYPE_LABEL); } if ($this->getIsDivider()) { $view ->setType(PHUIListItemView::TYPE_DIVIDER) ->addClass('phui-divider'); } $tooltip = $this->getTooltip(); - if (strlen($tooltip)) { + if (phutil_nonempty_string($tooltip)) { $view->setTooltip($tooltip); } if ($this->images) { require_celerity_resource('people-picture-menu-item-css'); foreach ($this->images as $image_src) { $classes = array(); $classes[] = 'people-menu-image'; if ($this->getDisabled()) { $classes[] = 'phui-image-disabled'; } $image = phutil_tag( 'img', array( 'src' => $image_src, 'class' => implode(' ', $classes), )); $image = phutil_tag( 'div', array( 'class' => 'people-menu-image-container', ), $image); $view->appendChild($image); } } foreach ($this->counts as $count) { $view->appendChild( phutil_tag( 'span', array( 'class' => 'phui-list-item-count', ), $count)); } foreach ($this->actions as $action) { $view->setActionIcon('fa-pencil', $action); } foreach ($this->progressBars as $bar) { $view->appendChild($bar); } return $view; } } diff --git a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php index 9d89c52ff8..717bba7c39 100644 --- a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php @@ -1,250 +1,250 @@ <?php final class PhabricatorDashboardProfileMenuItem extends PhabricatorProfileMenuItem { const MENUITEMKEY = 'dashboard'; const FIELD_DASHBOARD = 'dashboardPHID'; private $dashboard; private $dashboardHandle; public function getMenuItemTypeIcon() { return 'fa-dashboard'; } public function getMenuItemTypeName() { return pht('Dashboard'); } public function canAddToObject($object) { return true; } public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return true; } private function attachDashboard(PhabricatorDashboard $dashboard = null) { $this->dashboard = $dashboard; return $this; } private function getDashboard() { return $this->dashboard; } public function getAffectedObjectPHIDs( PhabricatorProfileMenuItemConfiguration $config) { return array( $this->getDashboardPHID($config), ); } public function newPageContent( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $dashboard_phid = $this->getDashboardPHID($config); // Reload the dashboard to attach panels, which we need for rendering. $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs(array($dashboard_phid)) ->executeOne(); if (!$dashboard) { return $this->newEmptyView( pht('Invalid Dashboard'), pht('This dashboard is invalid and could not be loaded.')); } if ($dashboard->isArchived()) { return $this->newEmptyView( pht('Archived Dashboard'), pht('This dashboard has been archived.')); } $engine = id(new PhabricatorDashboardRenderingEngine()) ->setViewer($viewer) ->setDashboard($dashboard); return $engine->renderDashboard(); } public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $dashboard_phids = array(); foreach ($items as $item) { $dashboard_phids[] = $this->getDashboardPHID($item); } $dashboards = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs($dashboard_phids) ->execute(); $handles = $viewer->loadHandles($dashboard_phids); $dashboards = mpull($dashboards, null, 'getPHID'); foreach ($items as $item) { $dashboard_phid = $this->getDashboardPHID($item); $dashboard = idx($dashboards, $dashboard_phid, null); $menu_item = $item->getMenuItem(); $menu_item ->attachDashboard($dashboard) ->setDashboardHandle($handles[$dashboard_phid]); } } public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $dashboard = $this->getDashboard(); if (!$dashboard) { if ($this->getDashboardHandle()->getPolicyFiltered()) { return pht('Restricted Dashboard'); } else { return pht('Invalid Dashboard'); } } if ($dashboard->isArchived()) { return pht('Archived Dashboard'); } - if (strlen($this->getName($config))) { + if (phutil_nonempty_string($this->getName($config))) { return $this->getName($config); } else { return $dashboard->getName(); } } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorDatasourceEditField()) ->setKey(self::FIELD_DASHBOARD) ->setLabel(pht('Dashboard')) ->setIsRequired(true) ->setDatasource(new PhabricatorDashboardDatasource()) ->setSingleValue($this->getDashboardPHID($config)), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setValue($this->getName($config)), ); } private function getName( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('name'); } protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $is_disabled = true; $action_uri = null; $dashboard = $this->getDashboard(); if ($dashboard) { if ($dashboard->isArchived()) { $icon = 'fa-ban'; $name = $this->getDisplayName($config); } else { $icon = $dashboard->getIcon(); $name = $this->getDisplayName($config); $is_disabled = false; $action_uri = $dashboard->getURI(); } } else { $icon = 'fa-ban'; if ($this->getDashboardHandle()->getPolicyFiltered()) { $name = pht('Restricted Dashboard'); } else { $name = pht('Invalid Dashboard'); } } $uri = $this->getItemViewURI($config); $item = $this->newItemView() ->setURI($uri) ->setName($name) ->setIcon($icon) ->setDisabled($is_disabled); if ($action_uri) { $item->newAction($action_uri); } return array( $item, ); } public function validateTransactions( PhabricatorProfileMenuItemConfiguration $config, $field_key, $value, array $xactions) { $viewer = $this->getViewer(); $errors = array(); if ($field_key == self::FIELD_DASHBOARD) { if ($this->isEmptyTransaction($value, $xactions)) { $errors[] = $this->newRequiredError( pht('You must choose a dashboard.'), $field_key); } foreach ($xactions as $xaction) { $new = $xaction['new']; if (!$new) { continue; } if ($new === $value) { continue; } $dashboards = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withPHIDs(array($new)) ->execute(); if (!$dashboards) { $errors[] = $this->newInvalidError( pht( 'Dashboard "%s" is not a valid dashboard which you have '. 'permission to see.', $new), $xaction['xaction']); } } } return $errors; } private function getDashboardPHID( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('dashboardPHID'); } private function getDashboardHandle() { return $this->dashboardHandle; } private function setDashboardHandle(PhabricatorObjectHandle $handle) { $this->dashboardHandle = $handle; return $this; } } diff --git a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php index 71e3d7e8a5..23d5ad3f30 100644 --- a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php @@ -1,177 +1,177 @@ <?php final class PhabricatorEditEngineProfileMenuItem extends PhabricatorProfileMenuItem { const MENUITEMKEY = 'editengine'; const FIELD_FORM = 'formKey'; private $form; public function getMenuItemTypeIcon() { return 'fa-plus'; } public function getMenuItemTypeName() { return pht('Form'); } public function canAddToObject($object) { return true; } public function attachForm($form) { $this->form = $form; return $this; } public function getForm() { $form = $this->form; if (!$form) { return null; } return $form; } public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $engines = PhabricatorEditEngine::getAllEditEngines(); $engine_keys = array_keys($engines); $forms = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys($engine_keys) ->withIsDisabled(false) ->execute(); $form_engines = mgroup($forms, 'getEngineKey'); $form_ids = $forms; $builtin_map = array(); foreach ($form_engines as $engine_key => $form_engine) { $builtin_map[$engine_key] = mpull($form_engine, null, 'getBuiltinKey'); } foreach ($items as $item) { $key = $item->getMenuItemProperty('formKey'); list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey($key); if (is_numeric($form_key)) { $form = idx($form_ids, $form_key, null); $item->getMenuItem()->attachForm($form); } else if (isset($builtin_map[$engine_key][$form_key])) { $form = $builtin_map[$engine_key][$form_key]; $item->getMenuItem()->attachForm($form); } } } public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $form = $this->getForm(); if (!$form) { return pht('(Restricted/Invalid Form)'); } - if (strlen($this->getName($config))) { + if (phutil_nonempty_string($this->getName($config))) { return $this->getName($config); } else { return $form->getName(); } } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorDatasourceEditField()) ->setKey(self::FIELD_FORM) ->setLabel(pht('Form')) ->setIsRequired(true) ->setDatasource(new PhabricatorEditEngineDatasource()) ->setSingleValue($config->getMenuItemProperty('formKey')), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setValue($this->getName($config)), ); } private function getName( PhabricatorProfileMenuItemConfiguration $config) { return $config->getMenuItemProperty('name'); } protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $form = $this->getForm(); if (!$form) { return array(); } $icon = $form->getIcon(); $name = $this->getDisplayName($config); $uri = $form->getCreateURI(); if ($uri === null) { return array(); } $item = $this->newItemView() ->setURI($uri) ->setName($name) ->setIcon($icon); return array( $item, ); } public function validateTransactions( PhabricatorProfileMenuItemConfiguration $config, $field_key, $value, array $xactions) { $viewer = $this->getViewer(); $errors = array(); if ($field_key == self::FIELD_FORM) { if ($this->isEmptyTransaction($value, $xactions)) { $errors[] = $this->newRequiredError( pht('You must choose a form.'), $field_key); } foreach ($xactions as $xaction) { $new = $xaction['new']; if (!$new) { continue; } if ($new === $value) { continue; } list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey( $new); $forms = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($engine_key)) ->withIdentifiers(array($form_key)) ->execute(); if (!$forms) { $errors[] = $this->newInvalidError( pht( 'Form "%s" is not a valid form which you have permission to '. 'see.', $new), $xaction['xaction']); } } } return $errors; } } diff --git a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php index 89ac4a5633..83c3133705 100644 --- a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php @@ -1,76 +1,76 @@ <?php final class PhabricatorManageProfileMenuItem extends PhabricatorProfileMenuItem { const MENUITEMKEY = 'menu.manage'; public function getMenuItemTypeName() { return pht('Manage Menu'); } private function getDefaultName() { return pht('Edit Menu'); } public function getMenuItemTypeIcon() { return 'fa-pencil'; } public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; } public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return false; } public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $name = $config->getMenuItemProperty('name'); - if (strlen($name)) { + if (phutil_nonempty_string($name)) { return $name; } return $this->getDefaultName(); } public function buildEditEngineFields( PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setPlaceholder($this->getDefaultName()) ->setValue($config->getMenuItemProperty('name')), ); } protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return array(); } $engine = $this->getEngine(); $uri = $engine->getItemURI('configure/'); $name = $this->getDisplayName($config); $icon = 'fa-pencil'; $item = $this->newItemView() ->setURI($uri) ->setName($name) ->setIcon($icon); return array( $item, ); } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 6f6ce56cad..17ce1db8bb 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,914 +1,914 @@ <?php /** * This is a standard Phabricator page with menus, Javelin, DarkConsole, and * basic styles. */ final class PhabricatorStandardPageView extends PhabricatorBarePageView implements AphrontResponseProducerInterface { private $baseURI; private $applicationName; private $glyph; private $menuContent; private $showChrome = true; private $classes = array(); private $disableConsole; private $pageObjects = array(); private $applicationMenu; private $showFooter = true; private $showDurableColumn = true; private $quicksandConfig = array(); private $tabs; private $crumbs; private $navigation; private $footer; public function setShowFooter($show_footer) { $this->showFooter = $show_footer; return $this; } public function getShowFooter() { return $this->showFooter; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowChrome($show_chrome) { $this->showChrome = $show_chrome; return $this; } public function getShowChrome() { return $this->showChrome; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setPageObjectPHIDs(array $phids) { $this->pageObjects = $phids; return $this; } public function setShowDurableColumn($show) { $this->showDurableColumn = $show; return $this; } public function getShowDurableColumn() { $request = $this->getRequest(); if (!$request) { return false; } $viewer = $request->getUser(); if (!$viewer->isLoggedIn()) { return false; } $conpherence_installed = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorConpherenceApplication', $viewer); if (!$conpherence_installed) { return false; } if ($this->isQuicksandBlacklistURI()) { return false; } return true; } private function isQuicksandBlacklistURI() { $request = $this->getRequest(); if (!$request) { return false; } $patterns = $this->getQuicksandURIPatternBlacklist(); $path = $request->getRequestURI()->getPath(); foreach ($patterns as $pattern) { if (preg_match('(^'.$pattern.'$)', $path)) { return true; } } return false; } public function getDurableColumnVisible() { $column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY; return (bool)$this->getUserPreference($column_key, false); } public function getDurableColumnMinimize() { $column_key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY; return (bool)$this->getUserPreference($column_key, false); } public function addQuicksandConfig(array $config) { $this->quicksandConfig = $config + $this->quicksandConfig; return $this; } public function getQuicksandConfig() { return $this->quicksandConfig; } public function setCrumbs(PHUICrumbsView $crumbs) { $this->crumbs = $crumbs; return $this; } public function getCrumbs() { return $this->crumbs; } public function setTabs(PHUIListView $tabs) { $tabs->setType(PHUIListView::TABBAR_LIST); $tabs->addClass('phabricator-standard-page-tabs'); $this->tabs = $tabs; return $this; } public function getTabs() { return $this->tabs; } public function setNavigation(AphrontSideNavFilterView $navigation) { $this->navigation = $navigation; return $this; } public function getNavigation() { return $this->navigation; } public function getTitle() { $glyph_key = PhabricatorTitleGlyphsSetting::SETTINGKEY; $glyph_on = PhabricatorTitleGlyphsSetting::VALUE_TITLE_GLYPHS; $glyph_setting = $this->getUserPreference($glyph_key, $glyph_on); $use_glyph = ($glyph_setting == $glyph_on); $title = parent::getTitle(); $prefix = null; if ($use_glyph) { $prefix = $this->getGlyph(); } else { $application_name = $this->getApplicationName(); if (strlen($application_name)) { $prefix = '['.$application_name.']'; } } - if (strlen($prefix)) { + if (phutil_nonempty_string($prefix)) { $title = $prefix.' '.$title; } return $title; } protected function willRenderPage() { $footer = $this->renderFooter(); // NOTE: A cleaner solution would be to let body layout elements implement // some kind of "LayoutInterface" so content can be embedded inside frames, // but there's only really one use case for this for now. $children = $this->renderChildren(); if ($children) { $layout = head($children); if ($layout instanceof PHUIFormationView) { $layout->setFooter($footer); $footer = null; } } $this->footer = $footer; parent::willRenderPage(); if (!$this->getRequest()) { throw new Exception( pht( 'You must set the %s to render a %s.', 'Request', __CLASS__)); } $console = $this->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-zindex-css'); require_celerity_resource('phui-button-css'); require_celerity_resource('phui-spacing-css'); require_celerity_resource('phui-form-css'); require_celerity_resource('phabricator-standard-page-view'); require_celerity_resource('conpherence-durable-column-view'); require_celerity_resource('font-lato'); Javelin::initBehavior('workflow', array()); $request = $this->getRequest(); $user = null; if ($request) { $user = $request->getUser(); } if ($user) { if ($user->isUserActivated()) { $offset = $user->getTimeZoneOffset(); $ignore_key = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY; $ignore = $user->getUserSetting($ignore_key); Javelin::initBehavior( 'detect-timezone', array( 'offset' => $offset, 'uri' => '/settings/timezone/', 'message' => pht( 'Your browser timezone setting differs from the timezone '. 'setting in your profile, click to reconcile.'), 'ignoreKey' => $ignore_key, 'ignore' => $ignore, )); if ($user->getIsAdmin()) { $server_https = $request->isHTTPS(); $server_protocol = $server_https ? 'HTTPS' : 'HTTP'; $client_protocol = $server_https ? 'HTTP' : 'HTTPS'; $doc_name = 'Configuring a Preamble Script'; $doc_href = PhabricatorEnv::getDoclink($doc_name); Javelin::initBehavior( 'setup-check-https', array( 'server_https' => $server_https, 'doc_name' => pht('See Documentation'), 'doc_href' => $doc_href, 'message' => pht( 'This server thinks you are using %s, but your '. 'client is convinced that it is using %s. This is a serious '. 'misconfiguration with subtle, but significant, consequences.', $server_protocol, $client_protocol), )); } } Javelin::initBehavior('lightbox-attachments'); } Javelin::initBehavior('aphront-form-disable-on-submit'); Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('history-install'); Javelin::initBehavior('phabricator-gesture'); $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); } Javelin::initBehavior( 'refresh-csrf', array( 'tokenName' => AphrontRequest::getCSRFTokenName(), 'header' => AphrontRequest::getCSRFHeaderName(), 'viaHeader' => AphrontRequest::getViaHeaderName(), 'current' => $current_token, )); Javelin::initBehavior('device'); Javelin::initBehavior( 'high-security-warning', $this->getHighSecurityWarningConfig()); if (PhabricatorEnv::isReadOnly()) { Javelin::initBehavior( 'read-only-warning', array( 'message' => PhabricatorEnv::getReadOnlyMessage(), 'uri' => PhabricatorEnv::getReadOnlyURI(), )); } // If we aren't showing the page chrome, skip rendering DarkConsole and the // main menu, since they won't be visible on the page. if (!$this->getShowChrome()) { return; } if ($console) { require_celerity_resource('aphront-dark-console-css'); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } Javelin::initBehavior( 'dark-console', $this->getConsoleConfig()); } if ($user) { $viewer = $user; } else { $viewer = new PhabricatorUser(); } $menu = id(new PhabricatorMainMenuView()) ->setUser($viewer); if ($this->getController()) { $menu->setController($this->getController()); } $application_menu = $this->applicationMenu; if ($application_menu) { if ($application_menu instanceof PHUIApplicationMenuView) { $crumbs = $this->getCrumbs(); if ($crumbs) { $application_menu->setCrumbs($crumbs); } $application_menu = $application_menu->buildListView(); } $menu->setApplicationMenu($application_menu); } $this->menuContent = $menu->render(); } protected function getHead() { $monospaced = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user) { $monospaced = $user->getUserSetting( PhabricatorMonospacedFontSetting::SETTINGKEY); } } $response = CelerityAPI::getStaticResourceResponse(); $font_css = null; if (!empty($monospaced)) { // We can't print this normally because escaping quotation marks will // break the CSS. Instead, filter it strictly and then mark it as safe. $monospaced = new PhutilSafeHTML( PhabricatorMonospacedFontSetting::filterMonospacedCSSRule( $monospaced)); $font_css = hsprintf( '<style type="text/css">'. '.PhabricatorMonospaced, '. '.phabricator-remarkup .remarkup-code-block '. '.remarkup-code { font: %s !important; } '. '</style>', $monospaced); } return hsprintf( '%s%s%s', parent::getHead(), $font_css, $response->renderSingleResource('javelin-magical-init', 'phabricator')); } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $request = $this->getRequest(); $response = parent::willSendResponse($response); $console = $request->getApplicationConfiguration()->getConsole(); if ($console) { $response = PhutilSafeHTML::applyFunction( 'str_replace', hsprintf('<darkconsole />'), $console->render($request), $response); } return $response; } protected function getBody() { $user = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); } $header_chrome = null; if ($this->getShowChrome()) { $header_chrome = $this->menuContent; } $classes = array(); $classes[] = 'main-page-frame'; $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { $developer_warning = phutil_tag_div( 'aphront-developer-error-callout', pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); } $main_page = phutil_tag( 'div', array( 'id' => 'phabricator-standard-page', 'class' => 'phabricator-standard-page', ), array( $developer_warning, $header_chrome, phutil_tag( 'div', array( 'id' => 'phabricator-standard-page-body', 'class' => 'phabricator-standard-page-body', ), $this->renderPageBodyContent()), )); $durable_column = null; if ($this->getShowDurableColumn()) { $is_visible = $this->getDurableColumnVisible(); $is_minimize = $this->getDurableColumnMinimize(); $durable_column = id(new ConpherenceDurableColumnView()) ->setSelectedConpherence(null) ->setUser($user) ->setQuicksandConfig($this->buildQuicksandConfig()) ->setVisible($is_visible) ->setMinimize($is_minimize) ->setInitialLoad(true); if ($is_minimize) { $this->classes[] = 'minimize-column'; } } Javelin::initBehavior('quicksand-blacklist', array( 'patterns' => $this->getQuicksandURIPatternBlacklist(), )); return phutil_tag( 'div', array( 'class' => implode(' ', $classes), 'id' => 'main-page-frame', ), array( $main_page, $durable_column, )); } private function renderPageBodyContent() { $console = $this->getConsole(); $body = parent::getBody(); $nav = $this->getNavigation(); $tabs = $this->getTabs(); if ($nav) { $crumbs = $this->getCrumbs(); if ($crumbs) { $nav->setCrumbs($crumbs); } $nav->appendChild($body); $nav->appendFooter($this->footer); $content = phutil_implode_html('', array($nav->render())); } else { $content = array(); $crumbs = $this->getCrumbs(); if ($crumbs) { if ($this->getTabs()) { $crumbs->setBorder(true); } $content[] = $crumbs; } $tabs = $this->getTabs(); if ($tabs) { $content[] = $tabs; } $content[] = $body; $content[] = $this->footer; $content = phutil_implode_html('', $content); } return array( ($console ? hsprintf('<darkconsole />') : null), $content, ); } protected function getTail() { $request = $this->getRequest(); $user = $request->getUser(); $tail = array( parent::getTail(), ); $response = CelerityAPI::getStaticResourceResponse(); if ($request->isHTTPS()) { $with_protocol = 'https'; } else { $with_protocol = 'http'; } $servers = PhabricatorNotificationServerRef::getEnabledClientServers( $with_protocol); if ($servers) { if ($user && $user->isLoggedIn()) { // TODO: We could tell the browser about all the servers and let it // do random reconnects to improve reliability. shuffle($servers); $server = head($servers); $client_uri = $server->getWebsocketURI(); Javelin::initBehavior( 'aphlict-listen', array( 'websocketURI' => (string)$client_uri, ) + $this->buildAphlictListenConfigData()); CelerityAPI::getStaticResourceResponse() ->addContentSecurityPolicyURI('connect-src', $client_uri); } } $tail[] = $response->renderHTMLFooter($this->getFrameable()); return $tail; } protected function getBodyClasses() { $classes = array(); if (!$this->getShowChrome()) { $classes[] = 'phabricator-chromeless-page'; } $agent = AphrontRequest::getHTTPHeader('User-Agent'); // Try to guess the device resolution based on UA strings to avoid a flash // of incorrectly-styled content. $device_guess = 'device-desktop'; if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) { $device_guess = 'device-phone device'; } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) { $device_guess = 'device-tablet device'; } $classes[] = $device_guess; if (preg_match('@Windows@', $agent)) { $classes[] = 'platform-windows'; } else if (preg_match('@Macintosh@', $agent)) { $classes[] = 'platform-mac'; } else if (preg_match('@X11@', $agent)) { $classes[] = 'platform-linux'; } if ($this->getRequest()->getStr('__print__')) { $classes[] = 'printable'; } if ($this->getRequest()->getStr('__aural__')) { $classes[] = 'audible'; } $classes[] = 'phui-theme-'.PhabricatorEnv::getEnvConfig('ui.header-color'); foreach ($this->classes as $class) { $classes[] = $class; } return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } private function getConsoleConfig() { $user = $this->getRequest()->getUser(); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } if ($user) { $setting_tab = PhabricatorDarkConsoleTabSetting::SETTINGKEY; $setting_visible = PhabricatorDarkConsoleVisibleSetting::SETTINGKEY; $tab = $user->getUserSetting($setting_tab); $visible = $user->getUserSetting($setting_visible); } else { $tab = null; $visible = true; } return array( // NOTE: We use a generic label here to prevent input reflection // and mitigate compression attacks like BREACH. See discussion in // T3684. 'uri' => pht('Main Request'), 'selected' => $tab, 'visible' => $visible, 'headers' => $headers, ); } private function getHighSecurityWarningConfig() { $user = $this->getRequest()->getUser(); $show = false; if ($user->hasSession()) { $hisec = ($user->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $show = true; } } return array( 'show' => $show, 'uri' => '/auth/session/downgrade/', 'message' => pht( 'Your session is in high security mode. When you '. 'finish using it, click here to leave.'), ); } private function renderFooter() { if (!$this->getShowChrome()) { return null; } if (!$this->getShowFooter()) { return null; } $items = PhabricatorEnv::getEnvConfig('ui.footer-items'); if (!$items) { return null; } $foot = array(); foreach ($items as $item) { $name = idx($item, 'name', pht('Unnamed Footer Item')); $href = idx($item, 'href'); if (!PhabricatorEnv::isValidURIForLink($href)) { $href = null; } if ($href !== null) { $tag = 'a'; } else { $tag = 'span'; } $foot[] = phutil_tag( $tag, array( 'href' => $href, ), $name); } $foot = phutil_implode_html(" \xC2\xB7 ", $foot); return phutil_tag( 'div', array( 'class' => 'phabricator-standard-page-footer grouped', ), $foot); } public function renderForQuicksand() { parent::willRenderPage(); $response = $this->renderPageBodyContent(); $response = $this->willSendResponse($response); $extra_config = $this->getQuicksandConfig(); return array( 'content' => hsprintf('%s', $response), ) + $this->buildQuicksandConfig() + $extra_config; } private function buildQuicksandConfig() { $viewer = $this->getRequest()->getUser(); $controller = $this->getController(); $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($viewer); $dropdown_query->execute(); $hisec_warning_config = $this->getHighSecurityWarningConfig(); $console_config = null; $console = $this->getConsole(); if ($console) { $console_config = $this->getConsoleConfig(); } $upload_enabled = false; if ($controller) { $upload_enabled = $controller->isGlobalDragAndDropUploadEnabled(); } $application_class = null; $application_search_icon = null; $application_help = null; $controller = $this->getController(); if ($controller) { $application = $controller->getCurrentApplication(); if ($application) { $application_class = get_class($application); if ($application->getApplicationSearchDocumentTypes()) { $application_search_icon = $application->getIcon(); } $help_items = $application->getHelpMenuItems($viewer); if ($help_items) { $help_list = id(new PhabricatorActionListView()) ->setViewer($viewer); foreach ($help_items as $help_item) { $help_list->addAction($help_item); } $application_help = $help_list->getDropdownMenuMetadata(); } } } return array( 'title' => $this->getTitle(), 'bodyClasses' => $this->getBodyClasses(), 'aphlictDropdownData' => array( $dropdown_query->getNotificationData(), $dropdown_query->getConpherenceData(), ), 'globalDragAndDrop' => $upload_enabled, 'hisecWarningConfig' => $hisec_warning_config, 'consoleConfig' => $console_config, 'applicationClass' => $application_class, 'applicationSearchIcon' => $application_search_icon, 'helpItems' => $application_help, ) + $this->buildAphlictListenConfigData(); } private function buildAphlictListenConfigData() { $user = $this->getRequest()->getUser(); $subscriptions = $this->pageObjects; $subscriptions[] = $user->getPHID(); return array( 'pageObjects' => array_fill_keys($this->pageObjects, true), 'subscriptions' => $subscriptions, ); } private function getQuicksandURIPatternBlacklist() { $applications = PhabricatorApplication::getAllApplications(); $blacklist = array(); foreach ($applications as $application) { $blacklist[] = $application->getQuicksandURIPatternBlacklist(); } // See T4340. Currently, Phortune and Auth both require pulling in external // Javascript (for Stripe card management and Recaptcha, respectively). // This can put us in a position where the user loads a page with a // restrictive Content-Security-Policy, then uses Quicksand to navigate to // a page which needs to load external scripts. For now, just blacklist // these entire applications since we aren't giving up anything // significant by doing so. $blacklist[] = array( '/phortune/.*', '/auth/.*', ); return array_mergev($blacklist); } private function getUserPreference($key, $default = null) { $request = $this->getRequest(); if (!$request) { return $default; } $user = $request->getUser(); if (!$user) { return $default; } return $user->getUserSetting($key); } public function produceAphrontResponse() { $controller = $this->getController(); $viewer = $this->getUser(); if ($viewer && $viewer->getPHID()) { $object_phids = $this->pageObjects; foreach ($object_phids as $object_phid) { PhabricatorFeedStoryNotification::updateObjectNotificationViews( $viewer, $object_phid); } } if ($this->getRequest()->isQuicksand()) { $content = $this->renderForQuicksand(); $response = id(new AphrontAjaxResponse()) ->setContent($content); } else { // See T13247. Try to find some navigational menu items to create a // mobile navigation menu from. $application_menu = $controller->buildApplicationMenu(); if (!$application_menu) { $navigation = $this->getNavigation(); if ($navigation) { $application_menu = $navigation->getMenu(); } } $this->applicationMenu = $application_menu; $content = $this->render(); $response = id(new AphrontWebpageResponse()) ->setContent($content) ->setFrameable($this->getFrameable()); } return $response; } } diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index 6a69c4feaa..f0b2bbbe83 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -1,736 +1,736 @@ <?php final class PhabricatorMainMenuView extends AphrontView { private $controller; private $applicationMenu; public function setApplicationMenu(PHUIListView $application_menu) { $this->applicationMenu = $application_menu; return $this; } public function getApplicationMenu() { return $this->applicationMenu; } public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } private static function getFavicons() { $refs = array(); $refs['favicon'] = id(new PhabricatorFaviconRef()) ->setWidth(64) ->setHeight(64); $refs['message_favicon'] = id(new PhabricatorFaviconRef()) ->setWidth(64) ->setHeight(64) ->setEmblems( array( 'dot-pink', null, null, null, )); id(new PhabricatorFaviconRefQuery()) ->withRefs($refs) ->execute(); return mpull($refs, 'getURI'); } public function render() { $viewer = $this->getViewer(); require_celerity_resource('phabricator-main-menu-view'); $header_id = celerity_generate_unique_node_id(); $menu_bar = array(); $alerts = array(); $search_button = ''; $app_button = ''; $aural = null; $is_full = $this->isFullSession($viewer); if ($is_full) { list($menu, $dropdowns, $aural) = $this->renderNotificationMenu(); if (array_filter($menu)) { $alerts[] = $menu; } $menu_bar = array_merge($menu_bar, $dropdowns); $app_button = $this->renderApplicationMenuButton(); $search_button = $this->renderSearchMenuButton($header_id); } else if (!$viewer->isLoggedIn()) { $app_button = $this->renderApplicationMenuButton(); if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { $search_button = $this->renderSearchMenuButton($header_id); } } if ($search_button) { $search_menu = $this->renderPhabricatorSearchMenu(); } else { $search_menu = null; } if ($alerts) { $alerts = javelin_tag( 'div', array( 'class' => 'phabricator-main-menu-alerts', 'aural' => false, ), $alerts); } if ($aural) { $aural = javelin_tag( 'span', array( 'aural' => true, ), phutil_implode_html(' ', $aural)); } $extensions = PhabricatorMainMenuBarExtension::getAllEnabledExtensions(); foreach ($extensions as $extension) { $extension ->setViewer($viewer) ->setIsFullSession($is_full); $controller = $this->getController(); if ($controller) { $extension->setController($controller); $application = $controller->getCurrentApplication(); if ($application) { $extension->setApplication($application); } } } if (!$is_full) { foreach ($extensions as $key => $extension) { if ($extension->shouldRequireFullSession()) { unset($extensions[$key]); } } } foreach ($extensions as $key => $extension) { if (!$extension->isExtensionEnabledForViewer($extension->getViewer())) { unset($extensions[$key]); } } $menus = array(); foreach ($extensions as $extension) { foreach ($extension->buildMainMenus() as $menu) { $menus[] = $menu; } } // Because we display these with "float: right", reverse their order before // rendering them into the document so that the extension order and display // order are the same. $menus = array_reverse($menus); foreach ($menus as $menu) { $menu_bar[] = $menu; } $classes = array(); $classes[] = 'phabricator-main-menu'; $classes[] = 'phabricator-main-menu-background'; return phutil_tag( 'div', array( 'class' => implode(' ', $classes), 'id' => $header_id, ), array( $app_button, $search_button, $this->renderPhabricatorLogo(), $alerts, $aural, $search_menu, $menu_bar, )); } private function renderSearch() { $viewer = $this->getViewer(); $result = null; $keyboard_config = array( 'helpURI' => '/help/keyboardshortcut/', ); if ($viewer->isLoggedIn()) { $show_search = $viewer->isUserActivated(); } else { $show_search = PhabricatorEnv::getEnvConfig('policy.allow-public'); } if ($show_search) { $search = new PhabricatorMainMenuSearchView(); $search->setViewer($viewer); $application = null; $controller = $this->getController(); if ($controller) { $application = $controller->getCurrentApplication(); } if ($application) { $search->setApplication($application); } $result = $search; $keyboard_config['searchID'] = $search->getID(); } $keyboard_config['pht'] = array( '/' => pht('Give keyboard focus to the search box.'), '?' => pht('Show keyboard shortcut help for the current page.'), ); Javelin::initBehavior( 'phabricator-keyboard-shortcuts', $keyboard_config); if ($result) { $result = id(new PHUIListItemView()) ->addClass('phabricator-main-menu-search') ->appendChild($result); } return $result; } public function renderApplicationMenuButton() { $dropdown = $this->renderApplicationMenu(); if (!$dropdown) { return null; } return id(new PHUIButtonView()) ->setTag('a') ->setHref('#') ->setIcon('fa-bars') ->addClass('phabricator-core-user-menu') ->addClass('phabricator-core-user-mobile-menu') ->setNoCSS(true) ->setDropdownMenu($dropdown) ->setAuralLabel(pht('Page Menu')); } private function renderApplicationMenu() { $viewer = $this->getViewer(); $view = $this->getApplicationMenu(); if ($view) { $items = $view->getItems(); $view = id(new PhabricatorActionListView()) ->setViewer($viewer); foreach ($items as $item) { $view->addAction( id(new PhabricatorActionView()) ->setName($item->getName()) ->setHref($item->getHref()) ->setType($item->getType())); } } return $view; } public function renderSearchMenuButton($header_id) { $button_id = celerity_generate_unique_node_id(); return javelin_tag( 'a', array( 'class' => 'phabricator-main-menu-search-button '. 'phabricator-expand-application-menu', 'sigil' => 'jx-toggle-class', 'meta' => array( 'map' => array( $header_id => 'phabricator-search-menu-expanded', $button_id => 'menu-icon-selected', ), ), ), phutil_tag( 'span', array( 'class' => 'phabricator-menu-button-icon phui-icon-view '. 'phui-font-fa fa-search', 'id' => $button_id, ), '')); } private function renderPhabricatorSearchMenu() { $view = new PHUIListView(); $view->addClass('phabricator-search-menu'); $search = $this->renderSearch(); if ($search) { $view->addMenuItem($search); } return $view; } private function renderPhabricatorLogo() { $custom_header = PhabricatorCustomLogoConfigType::getLogoImagePHID(); $logo_style = array(); if ($custom_header) { $cache = PhabricatorCaches::getImmutableCache(); $cache_key_logo = 'ui.custom-header.logo-phid.v3.'.$custom_header; $logo_uri = $cache->getKey($cache_key_logo); if (!$logo_uri) { // NOTE: If the file policy has been changed to be restrictive, we'll // miss here and just show the default logo. The cache will fill later // when someone who can see the file loads the page. This might be a // little spooky, see T11982. $files = id(new PhabricatorFileQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($custom_header)) ->execute(); $file = head($files); if ($file) { $logo_uri = $file->getViewURI(); $cache->setKey($cache_key_logo, $logo_uri); } } if ($logo_uri) { $logo_style[] = 'background-size: 40px 40px;'; $logo_style[] = 'background-position: 0 0;'; $logo_style[] = 'background-image: url('.$logo_uri.')'; } } $logo_node = phutil_tag( 'span', array( 'class' => 'phabricator-main-menu-project-logo', 'style' => implode(' ', $logo_style), )); $wordmark_text = PhabricatorCustomLogoConfigType::getLogoWordmark(); - if (!strlen($wordmark_text)) { + if (!phutil_nonempty_string($wordmark_text)) { $wordmark_text = PlatformSymbols::getPlatformServerName(); } $wordmark_node = phutil_tag( 'span', array( 'class' => 'phabricator-wordmark', ), $wordmark_text); return phutil_tag( 'a', array( 'class' => 'phabricator-main-menu-brand', 'href' => '/', ), array( javelin_tag( 'span', array( 'aural' => true, ), pht('Home')), $logo_node, $wordmark_node, )); } private function renderNotificationMenu() { $viewer = $this->getViewer(); require_celerity_resource('phabricator-notification-css'); require_celerity_resource('phabricator-notification-menu-css'); $container_classes = array('alert-notifications'); $aural = array(); $dropdown_query = id(new AphlictDropdownDataQuery()) ->setViewer($viewer); $dropdown_data = $dropdown_query->execute(); $message_tag = ''; $message_notification_dropdown = ''; $conpherence_app = 'PhabricatorConpherenceApplication'; $conpherence_data = $dropdown_data[$conpherence_app]; if ($conpherence_data['isInstalled']) { $message_id = celerity_generate_unique_node_id(); $message_count_id = celerity_generate_unique_node_id(); $message_dropdown_id = celerity_generate_unique_node_id(); $message_count_number = $conpherence_data['rawCount']; if ($message_count_number) { $aural[] = phutil_tag( 'a', array( 'href' => '/conpherence/', ), pht( '%s unread messages.', new PhutilNumber($message_count_number))); } else { $aural[] = pht('No messages.'); } $message_count_tag = phutil_tag( 'span', array( 'id' => $message_count_id, 'class' => 'phabricator-main-menu-message-count', ), $conpherence_data['count']); $message_icon_tag = javelin_tag( 'span', array( 'class' => 'phabricator-main-menu-message-icon phui-icon-view '. 'phui-font-fa fa-comments', 'sigil' => 'menu-icon', ), ''); if ($message_count_number) { $container_classes[] = 'message-unread'; } $message_tag = phutil_tag( 'a', array( 'href' => '/conpherence/', 'class' => implode(' ', $container_classes), 'id' => $message_id, ), array( $message_icon_tag, $message_count_tag, )); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $message_id, 'countID' => $message_count_id, 'dropdownID' => $message_dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/conpherence/panel/', 'countType' => $conpherence_data['countType'], 'countNumber' => $message_count_number, 'unreadClass' => 'message-unread', ) + self::getFavicons()); $message_notification_dropdown = javelin_tag( 'div', array( 'id' => $message_dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', ), ''); } $bubble_tag = ''; $notification_dropdown = ''; $notification_app = 'PhabricatorNotificationsApplication'; $notification_data = $dropdown_data[$notification_app]; if ($notification_data['isInstalled']) { $count_id = celerity_generate_unique_node_id(); $dropdown_id = celerity_generate_unique_node_id(); $bubble_id = celerity_generate_unique_node_id(); $count_number = $notification_data['rawCount']; if ($count_number) { $aural[] = phutil_tag( 'a', array( 'href' => '/notification/', ), pht( '%s unread notifications.', new PhutilNumber($count_number))); } else { $aural[] = pht('No notifications.'); } $count_tag = phutil_tag( 'span', array( 'id' => $count_id, 'class' => 'phabricator-main-menu-alert-count', ), $notification_data['count']); $icon_tag = javelin_tag( 'span', array( 'class' => 'phabricator-main-menu-alert-icon phui-icon-view '. 'phui-font-fa fa-bell', 'sigil' => 'menu-icon', ), ''); if ($count_number) { $container_classes[] = 'alert-unread'; } $bubble_tag = phutil_tag( 'a', array( 'href' => '/notification/', 'class' => implode(' ', $container_classes), 'id' => $bubble_id, ), array($icon_tag, $count_tag)); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $bubble_id, 'countID' => $count_id, 'dropdownID' => $dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/notification/panel/', 'countType' => $notification_data['countType'], 'countNumber' => $count_number, 'unreadClass' => 'alert-unread', ) + self::getFavicons()); $notification_dropdown = javelin_tag( 'div', array( 'id' => $dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', ), ''); } // Admin Level Urgent Notification Channel $setup_tag = ''; $setup_notification_dropdown = ''; if ($viewer && $viewer->getIsAdmin()) { $open = PhabricatorSetupCheck::getOpenSetupIssueKeys(); if ($open) { $setup_id = celerity_generate_unique_node_id(); $setup_count_id = celerity_generate_unique_node_id(); $setup_dropdown_id = celerity_generate_unique_node_id(); $setup_count_number = count($open); if ($setup_count_number) { $aural[] = phutil_tag( 'a', array( 'href' => '/config/issue/', ), pht( '%s unresolved issues.', new PhutilNumber($setup_count_number))); } else { $aural[] = pht('No issues.'); } $setup_count_tag = phutil_tag( 'span', array( 'id' => $setup_count_id, 'class' => 'phabricator-main-menu-setup-count', ), $setup_count_number); $setup_icon_tag = javelin_tag( 'span', array( 'class' => 'phabricator-main-menu-setup-icon phui-icon-view '. 'phui-font-fa fa-exclamation-circle', 'sigil' => 'menu-icon', ), ''); if ($setup_count_number) { $container_classes[] = 'setup-unread'; } $setup_tag = phutil_tag( 'a', array( 'href' => '/config/issue/', 'class' => implode(' ', $container_classes), 'id' => $setup_id, ), array( $setup_icon_tag, $setup_count_tag, )); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $setup_id, 'countID' => $setup_count_id, 'dropdownID' => $setup_dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/config/issue/panel/', 'countType' => null, 'countNumber' => null, 'unreadClass' => 'setup-unread', ) + self::getFavicons()); $setup_notification_dropdown = javelin_tag( 'div', array( 'id' => $setup_dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', ), ''); } } $user_dropdown = null; $user_tag = null; if ($viewer->isLoggedIn()) { if (!$viewer->getIsEmailVerified()) { $bubble_id = celerity_generate_unique_node_id(); $count_id = celerity_generate_unique_node_id(); $dropdown_id = celerity_generate_unique_node_id(); $settings_uri = id(new PhabricatorEmailAddressesSettingsPanel()) ->setViewer($viewer) ->setUser($viewer) ->getPanelURI(); $user_icon = javelin_tag( 'span', array( 'class' => 'phabricator-main-menu-setup-icon phui-icon-view '. 'phui-font-fa fa-user', 'sigil' => 'menu-icon', )); $user_count = javelin_tag( 'span', array( 'class' => 'phabricator-main-menu-setup-count', 'id' => $count_id, ), 1); $user_tag = phutil_tag( 'a', array( 'href' => $settings_uri, 'class' => 'setup-unread', 'id' => $bubble_id, ), array( $user_icon, $user_count, )); Javelin::initBehavior( 'aphlict-dropdown', array( 'bubbleID' => $bubble_id, 'countID' => $count_id, 'dropdownID' => $dropdown_id, 'loadingText' => pht('Loading...'), 'uri' => '/settings/issue/', 'unreadClass' => 'setup-unread', )); $user_dropdown = javelin_tag( 'div', array( 'id' => $dropdown_id, 'class' => 'phabricator-notification-menu', 'sigil' => 'phabricator-notification-menu', 'style' => 'display: none;', )); } } $dropdowns = array( $notification_dropdown, $message_notification_dropdown, $setup_notification_dropdown, $user_dropdown, ); return array( array( $bubble_tag, $message_tag, $setup_tag, $user_tag, ), $dropdowns, $aural, ); } private function isFullSession(PhabricatorUser $viewer) { if (!$viewer->isLoggedIn()) { return false; } if (!$viewer->isUserActivated()) { return false; } if (!$viewer->hasSession()) { return false; } $session = $viewer->getSession(); if ($session->getIsPartial()) { return false; } if (!$session->getSignedLegalpadDocuments()) { return false; } $mfa_key = 'security.require-multi-factor-auth'; $need_mfa = PhabricatorEnv::getEnvConfig($mfa_key); if ($need_mfa) { $have_mfa = $viewer->getIsEnrolledInMultiFactor(); if (!$have_mfa) { return false; } } return true; } } diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php index fbc3904586..629feb9139 100644 --- a/src/view/phui/PHUIObjectItemListView.php +++ b/src/view/phui/PHUIObjectItemListView.php @@ -1,184 +1,184 @@ <?php final class PHUIObjectItemListView extends AphrontTagView { private $header; private $items; private $pager; private $noDataString; private $flush; private $simple; private $big; private $drag; private $allowEmptyList; private $itemClass = 'phui-oi-standard'; private $tail = array(); public function setAllowEmptyList($allow_empty_list) { $this->allowEmptyList = $allow_empty_list; return $this; } public function getAllowEmptyList() { return $this->allowEmptyList; } public function setFlush($flush) { $this->flush = $flush; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setPager($pager) { $this->pager = $pager; return $this; } public function setSimple($simple) { $this->simple = $simple; return $this; } public function setBig($big) { $this->big = $big; return $this; } public function setDrag($drag) { $this->drag = $drag; $this->setItemClass('phui-oi-drag'); return $this; } public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; } public function addItem(PHUIObjectItemView $item) { $this->items[] = $item; return $this; } public function setItemClass($item_class) { $this->itemClass = $item_class; return $this; } protected function getTagName() { return 'ul'; } public function newTailButton() { $button = id(new PHUIButtonView()) ->setTag('a') ->setColor(PHUIButtonView::GREY) ->setIcon('fa-chevron-down') ->setText(pht('View All Results')); $this->tail[] = $button; return $button; } protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-oi-list-view'; if ($this->flush) { $classes[] = 'phui-oi-list-flush'; require_celerity_resource('phui-oi-flush-ui-css'); } if ($this->simple) { $classes[] = 'phui-oi-list-simple'; require_celerity_resource('phui-oi-simple-ui-css'); } if ($this->big) { $classes[] = 'phui-oi-list-big'; require_celerity_resource('phui-oi-big-ui-css'); } if ($this->drag) { $classes[] = 'phui-oi-list-drag'; require_celerity_resource('phui-oi-drag-ui-css'); } return array( 'class' => $classes, ); } protected function getTagContent() { $viewer = $this->getUser(); require_celerity_resource('phui-oi-list-view-css'); require_celerity_resource('phui-oi-color-css'); $header = null; - if (strlen($this->header)) { + if (phutil_nonempty_string($this->header)) { $header = phutil_tag( 'h1', array( 'class' => 'phui-oi-list-header', ), $this->header); } if ($this->items) { if ($viewer) { foreach ($this->items as $item) { $item->setUser($viewer); } } foreach ($this->items as $item) { $item->addClass($this->itemClass); } $items = $this->items; } else if ($this->allowEmptyList) { $items = null; } else { $string = nonempty($this->noDataString, pht('No data.')); $string = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NODATA) ->appendChild($string); $items = phutil_tag( 'li', array( 'class' => 'phui-oi-empty', ), $string); } $pager = null; if ($this->pager) { $pager = $this->pager; } $tail = array(); foreach ($this->tail as $tail_item) { $tail[] = phutil_tag( 'li', array( 'class' => 'phui-oi-tail', ), $tail_item); } return array( $header, $items, $tail, $pager, $this->renderChildren(), ); } } diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 4bb9c3ea6a..6563101f98 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -1,901 +1,902 @@ <?php final class PHUIObjectItemView extends AphrontTagView { private $objectName; private $header; private $subhead; private $href; private $attributes = array(); private $icons = array(); private $barColor; private $object; private $effect; private $statusIcon; private $handleIcons = array(); private $bylines = array(); private $grippable; private $actions = array(); private $headIcons = array(); private $disabled; private $imageURI; private $imageHref; private $imageIcon; private $titleText; private $badge; private $countdownNum; private $countdownNoun; private $sideColumn; private $coverImage; private $description; private $clickable; private $mapViews = array(); private $menu; private $selectableName; private $selectableValue; private $isSelected; private $isForbidden; public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } public function addHeadIcon($icon) { $this->headIcons[] = $icon; return $this; } public function setObjectName($name) { $this->objectName = $name; return $this; } public function setGrippable($grippable) { $this->grippable = $grippable; return $this; } public function getGrippable() { return $this->grippable; } public function setEffect($effect) { $this->effect = $effect; return $this; } public function getEffect() { return $this->effect; } public function setObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->object; } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setHeader($header) { $this->header = $header; return $this; } public function setSubHead($subhead) { $this->subhead = $subhead; return $this; } public function setBadge(PHUIBadgeMiniView $badge) { $this->badge = $badge; return $this; } public function setCountdown($num, $noun) { $this->countdownNum = $num; $this->countdownNoun = $noun; return $this; } public function setTitleText($title_text) { $this->titleText = $title_text; return $this; } public function getTitleText() { return $this->titleText; } public function getHeader() { return $this->header; } public function addByline($byline) { $this->bylines[] = $byline; return $this; } public function setImageURI($image_uri) { $this->imageURI = $image_uri; return $this; } public function setImageHref($image_href) { $this->imageHref = $image_href; return $this; } public function getImageURI() { return $this->imageURI; } public function setImageIcon($image_icon) { if (!$image_icon instanceof PHUIIconView) { $image_icon = id(new PHUIIconView()) ->setIcon($image_icon); } $this->imageIcon = $image_icon; return $this; } public function getImageIcon() { return $this->imageIcon; } public function setCoverImage($image) { $this->coverImage = $image; return $this; } public function setDescription($description) { $this->description = $description; return $this; } public function setSelectable( $name, $value, $is_selected, $is_forbidden = false) { $this->selectableName = $name; $this->selectableValue = $value; $this->isSelected = $is_selected; $this->isForbidden = $is_forbidden; return $this; } public function setClickable($clickable) { $this->clickable = $clickable; return $this; } public function getClickable() { return $this->clickable; } public function setEpoch($epoch) { $date = phabricator_dual_datetime($epoch, $this->getUser()); $this->addIcon('none', $date); return $this; } public function addAction(PHUIListItemView $action) { if (count($this->actions) >= 3) { throw new Exception(pht('Limit 3 actions per item.')); } $this->actions[] = $action; return $this; } public function addIcon($icon, $label = null, $attributes = array()) { $this->icons[] = array( 'icon' => $icon, 'label' => $label, 'attributes' => $attributes, ); return $this; } public function newMenuItem() { if (!$this->menu) { $this->menu = new FuelMenuView(); } return $this->menu->newItem(); } public function newMapView() { $list = id(new FuelMapView()) ->addClass('fuel-map-property-list'); $this->mapViews[] = $list; return $list; } /** * This method has been deprecated, use @{method:setImageIcon} instead. * * @deprecated */ public function setIcon($icon) { phlog( pht('Deprecated call to setIcon(), use setImageIcon() instead.')); return $this->setImageIcon($icon); } public function setStatusIcon($icon, $label = null) { $this->statusIcon = array( 'icon' => $icon, 'label' => $label, ); return $this; } public function addHandleIcon( PhabricatorObjectHandle $handle, $label = null) { $this->handleIcons[] = array( 'icon' => $handle, 'label' => $label, ); return $this; } public function setBarColor($bar_color) { $this->barColor = $bar_color; return $this; } public function getBarColor() { return $this->barColor; } public function addAttribute($attribute) { if (!empty($attribute)) { $this->attributes[] = $attribute; } return $this; } public function setSideColumn($column) { $this->sideColumn = $column; return $this; } protected function getTagName() { return 'li'; } protected function getTagAttributes() { $sigils = array(); $item_classes = array(); $item_classes[] = 'phui-oi'; if ($this->icons) { $item_classes[] = 'phui-oi-with-icons'; } if ($this->attributes) { $item_classes[] = 'phui-oi-with-attrs'; } if ($this->handleIcons) { $item_classes[] = 'phui-oi-with-handle-icons'; } if ($this->barColor) { $item_classes[] = 'phui-oi-bar-color-'.$this->barColor; } else { $item_classes[] = 'phui-oi-no-bar'; } if ($this->actions) { $n = count($this->actions); $item_classes[] = 'phui-oi-with-actions'; $item_classes[] = 'phui-oi-with-'.$n.'-actions'; } if ($this->disabled) { $item_classes[] = 'phui-oi-disabled'; } else { $item_classes[] = 'phui-oi-enabled'; } switch ($this->effect) { case 'highlighted': $item_classes[] = 'phui-oi-highlighted'; break; case 'selected': $item_classes[] = 'phui-oi-selected'; break; case 'visited': $item_classes[] = 'phui-oi-visited'; break; case null: break; default: throw new Exception(pht('Invalid effect!')); } if ($this->isForbidden) { $item_classes[] = 'phui-oi-forbidden'; } else if ($this->isSelected) { $item_classes[] = 'phui-oi-selected'; } if ($this->selectableName !== null && !$this->isForbidden) { $item_classes[] = 'phui-oi-selectable'; $sigils[] = 'phui-oi-selectable'; Javelin::initBehavior('phui-selectable-list'); } $is_grippable = $this->getGrippable(); if ($is_grippable !== null) { $item_classes[] = 'phui-oi-has-grip'; if ($is_grippable) { $item_classes[] = 'phui-oi-grippable'; } else { $item_classes[] = 'phui-oi-ungrippable'; } } if ($this->getImageURI()) { $item_classes[] = 'phui-oi-with-image'; } if ($this->getImageIcon()) { $item_classes[] = 'phui-oi-with-image-icon'; } if ($this->getClickable()) { Javelin::initBehavior('linked-container'); $item_classes[] = 'phui-oi-linked-container'; $sigils[] = 'linked-container'; } return array( 'class' => $item_classes, 'sigil' => $sigils, ); } protected function getTagContent() { $viewer = $this->getUser(); $content_classes = array(); $content_classes[] = 'phui-oi-content'; $header_name = array(); if ($viewer) { $header_name[] = id(new PHUISpacesNamespaceContextView()) ->setUser($viewer) ->setObject($this->object); } if ($this->objectName) { $header_name[] = array( javelin_tag( 'span', array( 'class' => 'phui-oi-objname', 'sigil' => 'ungrabbable', ), $this->objectName), ' ', ); } $title_text = null; if ($this->titleText) { $title_text = $this->titleText; } else if ($this->href) { $title_text = $this->header; } $header_link = phutil_tag( $this->href ? 'a' : 'div', array( 'href' => $this->href, 'class' => 'phui-oi-link', 'title' => $title_text, ), $this->header); $description_tag = null; if ($this->description) { $decription_id = celerity_generate_unique_node_id(); $description_tag = id(new PHUITagView()) ->setIcon('fa-ellipsis-h') ->addClass('phui-oi-description-tag') ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_GREY) ->addSigil('jx-toggle-class') ->setSlimShady(true) ->setMetaData(array( 'map' => array( $decription_id => 'phui-oi-description-reveal', ), )); } $header = phutil_tag( 'div', array( 'class' => 'phui-oi-name', ), array( $this->headIcons, $header_name, $header_link, $description_tag, )); $icons = array(); if ($this->icons) { $icon_list = array(); foreach ($this->icons as $spec) { $icon = $spec['icon']; $icon = id(new PHUIIconView()) ->setIcon($icon) ->addClass('phui-oi-icon-image'); if (isset($spec['attributes']['tip'])) { $sigil = 'has-tooltip'; $meta = array( 'tip' => $spec['attributes']['tip'], 'align' => 'W', ); $icon->addSigil($sigil); $icon->setMetadata($meta); } $label = phutil_tag( 'span', array( 'class' => 'phui-oi-icon-label', ), $spec['label']); $classes = array(); $classes[] = 'phui-oi-icon'; if (isset($spec['attributes']['class'])) { $classes[] = $spec['attributes']['class']; } $icon_list[] = javelin_tag( 'li', array( 'class' => implode(' ', $classes), ), array( $icon, $label, )); } $icons[] = phutil_tag( 'ul', array( 'class' => 'phui-oi-icons', ), $icon_list); } $handle_bar = null; if ($this->handleIcons) { $handle_bar = array(); foreach ($this->handleIcons as $handleicon) { $handle_bar[] = $this->renderHandleIcon($handleicon['icon'], $handleicon['label']); } $handle_bar = phutil_tag( 'li', array( 'class' => 'phui-oi-handle-icons', ), $handle_bar); } $bylines = array(); if ($this->bylines) { foreach ($this->bylines as $byline) { $bylines[] = phutil_tag( 'div', array( 'class' => 'phui-oi-byline', ), $byline); } $bylines = phutil_tag( 'div', array( 'class' => 'phui-oi-bylines', ), $bylines); } $subhead = null; if ($this->subhead) { $subhead = phutil_tag( 'div', array( 'class' => 'phui-oi-subhead', ), $this->subhead); } if ($this->description) { $subhead = phutil_tag( 'div', array( 'class' => 'phui-oi-subhead phui-oi-description', 'id' => $decription_id, ), $this->description); } if ($icons) { $icons = phutil_tag( 'div', array( 'class' => 'phui-object-icon-pane', ), $icons); } $attrs = null; if ($this->attributes || $handle_bar) { $attrs = array(); $spacer = phutil_tag( 'span', array( 'class' => 'phui-oi-attribute-spacer', ), "\xC2\xB7"); $first = true; foreach ($this->attributes as $attribute) { $attrs[] = phutil_tag( 'li', array( 'class' => 'phui-oi-attribute', ), array( ($first ? null : $spacer), $attribute, )); $first = false; } $attrs = phutil_tag( 'ul', array( 'class' => 'phui-oi-attributes', ), array( $handle_bar, $attrs, )); } $status = null; if ($this->statusIcon) { $icon = $this->statusIcon; $status = $this->renderStatusIcon($icon['icon'], $icon['label']); } $grippable = null; if ($this->getGrippable() !== null) { $grippable = phutil_tag( 'div', array( 'class' => 'phui-oi-grip', ), ''); } $map_views = null; if ($this->mapViews) { $grid = id(new FuelGridView()) ->addClass('fuel-grid-property-list'); $row = $grid->newRow(); foreach ($this->mapViews as $map_view) { $row->newCell() ->setContent($map_view); } $map_views = $grid; } $content = phutil_tag( 'div', array( 'class' => implode(' ', $content_classes), ), array( $subhead, $attrs, $map_views, $this->renderChildren(), )); $image = null; if ($this->getImageURI()) { $image = phutil_tag( 'div', array( 'class' => 'phui-oi-image', 'style' => 'background-image: url('.$this->getImageURI().')', ), ''); } else if ($this->getImageIcon()) { $image = phutil_tag( 'div', array( 'class' => 'phui-oi-image-icon', ), $this->getImageIcon()); } - if ($image && (strlen($this->href) || strlen($this->imageHref))) { + if ($image && (phutil_nonempty_string($this->href) || + phutil_nonempty_string($this->imageHref))) { $image_href = ($this->imageHref) ? $this->imageHref : $this->href; $image = phutil_tag( 'a', array( 'href' => $image_href, ), $image); } /* Build a fake table */ $column0 = null; if ($status) { $column0 = phutil_tag( 'div', array( 'class' => 'phui-oi-col0', ), $status); } if ($this->badge) { $column0 = phutil_tag( 'div', array( 'class' => 'phui-oi-col0 phui-oi-badge', ), $this->badge); } if ($this->countdownNum) { $countdown = phutil_tag( 'div', array( 'class' => 'phui-oi-countdown-number', ), array( phutil_tag_div('', $this->countdownNum), phutil_tag_div('', $this->countdownNoun), )); $column0 = phutil_tag( 'div', array( 'class' => 'phui-oi-col0 phui-oi-countdown', ), $countdown); } if ($this->selectableName !== null) { if (!$this->isForbidden) { $checkbox = phutil_tag( 'input', array( 'type' => 'checkbox', 'name' => $this->selectableName, 'value' => $this->selectableValue, 'checked' => ($this->isSelected ? 'checked' : null), )); } else { $checkbox = null; } $column0 = phutil_tag( 'div', array( 'class' => 'phui-oi-col0 phui-oi-checkbox', ), $checkbox); } $column1 = phutil_tag( 'div', array( 'class' => 'phui-oi-col1', ), array( $header, $content, )); $column2 = null; if ($icons || $bylines) { $column2 = phutil_tag( 'div', array( 'class' => 'phui-oi-col2', ), array( $icons, $bylines, )); } /* Fixed width, right column container. */ $column3 = null; if ($this->sideColumn) { $column3 = phutil_tag( 'div', array( 'class' => 'phui-oi-col2 phui-oi-side-column', ), array( $this->sideColumn, )); } $table = phutil_tag( 'div', array( 'class' => 'phui-oi-table', ), phutil_tag_div( 'phui-oi-table-row', array( $column0, $column1, $column2, $column3, ))); $box = phutil_tag( 'div', array( 'class' => 'phui-oi-content-box', ), array( $grippable, $table, )); $actions = array(); if ($this->actions) { Javelin::initBehavior('phabricator-tooltips'); foreach (array_reverse($this->actions) as $action) { $action->setRenderNameAsTooltip(true); $actions[] = $action; } $actions = phutil_tag( 'ul', array( 'class' => 'phui-oi-actions', ), $actions); } $frame_content = phutil_tag( 'div', array( 'class' => 'phui-oi-frame-content', ), array( $actions, $image, $box, )); if ($this->menu) { $grid_view = id(new FuelGridView()) ->addClass('fuel-grid-tablet'); $grid_row = $grid_view->newRow(); $grid_row->newCell() ->setContent($frame_content); $menu = $this->menu; $grid_row->newCell() ->addClass('phui-oi-menu') ->setContent($menu); $frame_content = $grid_view; } $frame_cover = null; if ($this->coverImage) { $cover_image = phutil_tag( 'img', array( 'src' => $this->coverImage, 'class' => 'phui-oi-cover-image', )); $frame_cover = phutil_tag( 'div', array( 'class' => 'phui-oi-frame-cover', ), $cover_image); } $frame = phutil_tag( 'div', array( 'class' => 'phui-oi-frame', ), array( $frame_cover, $frame_content, )); return $frame; } private function renderStatusIcon($icon, $label) { Javelin::initBehavior('phabricator-tooltips'); $icon = id(new PHUIIconView()) ->setIcon($icon); $options = array( 'class' => 'phui-oi-status-icon', ); if (strlen($label)) { $options['sigil'] = 'has-tooltip'; $options['meta'] = array('tip' => $label, 'size' => 300); } return javelin_tag('div', $options, $icon); } private function renderHandleIcon(PhabricatorObjectHandle $handle, $label) { Javelin::initBehavior('phabricator-tooltips'); $options = array( 'class' => 'phui-oi-handle-icon', 'style' => 'background-image: url('.$handle->getImageURI().')', ); if (strlen($label)) { $options['sigil'] = 'has-tooltip'; $options['meta'] = array('tip' => $label, 'align' => 'E'); } return javelin_tag('span', $options, ''); } }