mirror of
https://we.phorge.it/source/phorge.git
synced 2025-04-08 02:18:33 +02:00
Summary: Depends on D19155. Ref T13094. Ref T4340. We can't currently implement a strict `form-action 'self'` content security policy because some file downloads rely on a `<form />` which sometimes POSTs to the CDN domain. Broadly, stop generating these forms. We just redirect instead, and show an interstitial confirm dialog if no CDN domain is configured. This makes the UX for installs with no CDN domain a little worse and the UX for everyone else better. Then, implement the stricter Content-Security-Policy. This also removes extra confirm dialogs for downloading Harbormaster build logs and data exports. Test Plan: - Went through the plain data export, data export with bulk jobs, ssh key generation, calendar ICS download, Diffusion data, Paste data, Harbormaster log data, and normal file data download workflows with a CDN domain. - Went through all those workflows again without a CDN domain. - Grepped for affected symbols (`getCDNURI()`, `getDownloadURI()`). - Added an evil form to a page, tried to submit it, was rejected. - Went through the ReCaptcha and Stripe flows again to see if they're submitting any forms. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13094, T4340 Differential Revision: https://secure.phabricator.com/D19156
975 lines
27 KiB
PHP
975 lines
27 KiB
PHP
<?php
|
|
|
|
final class PhabricatorApplicationSearchController
|
|
extends PhabricatorSearchBaseController {
|
|
|
|
private $searchEngine;
|
|
private $navigation;
|
|
private $queryKey;
|
|
private $preface;
|
|
private $activeQuery;
|
|
|
|
public function setPreface($preface) {
|
|
$this->preface = $preface;
|
|
return $this;
|
|
}
|
|
|
|
public function getPreface() {
|
|
return $this->preface;
|
|
}
|
|
|
|
public function setQueryKey($query_key) {
|
|
$this->queryKey = $query_key;
|
|
return $this;
|
|
}
|
|
|
|
protected function getQueryKey() {
|
|
return $this->queryKey;
|
|
}
|
|
|
|
public function setNavigation(AphrontSideNavFilterView $navigation) {
|
|
$this->navigation = $navigation;
|
|
return $this;
|
|
}
|
|
|
|
protected function getNavigation() {
|
|
return $this->navigation;
|
|
}
|
|
|
|
public function setSearchEngine(
|
|
PhabricatorApplicationSearchEngine $search_engine) {
|
|
$this->searchEngine = $search_engine;
|
|
return $this;
|
|
}
|
|
|
|
protected function getSearchEngine() {
|
|
return $this->searchEngine;
|
|
}
|
|
|
|
protected function getActiveQuery() {
|
|
if (!$this->activeQuery) {
|
|
throw new Exception(pht('There is no active query yet.'));
|
|
}
|
|
|
|
return $this->activeQuery;
|
|
}
|
|
|
|
protected function validateDelegatingController() {
|
|
$parent = $this->getDelegatingController();
|
|
|
|
if (!$parent) {
|
|
throw new Exception(
|
|
pht('You must delegate to this controller, not invoke it directly.'));
|
|
}
|
|
|
|
$engine = $this->getSearchEngine();
|
|
if (!$engine) {
|
|
throw new PhutilInvalidStateException('setEngine');
|
|
}
|
|
|
|
$engine->setViewer($this->getRequest()->getUser());
|
|
|
|
$parent = $this->getDelegatingController();
|
|
}
|
|
|
|
public function processRequest() {
|
|
$this->validateDelegatingController();
|
|
|
|
$query_action = $this->getRequest()->getURIData('queryAction');
|
|
if ($query_action == 'export') {
|
|
return $this->processExportRequest();
|
|
}
|
|
|
|
$key = $this->getQueryKey();
|
|
if ($key == 'edit') {
|
|
return $this->processEditRequest();
|
|
} else {
|
|
return $this->processSearchRequest();
|
|
}
|
|
}
|
|
|
|
private function processSearchRequest() {
|
|
$parent = $this->getDelegatingController();
|
|
$request = $this->getRequest();
|
|
$user = $request->getUser();
|
|
$engine = $this->getSearchEngine();
|
|
$nav = $this->getNavigation();
|
|
if (!$nav) {
|
|
$nav = $this->buildNavigation();
|
|
}
|
|
|
|
if ($request->isFormPost()) {
|
|
$saved_query = $engine->buildSavedQueryFromRequest($request);
|
|
$engine->saveQuery($saved_query);
|
|
return id(new AphrontRedirectResponse())->setURI(
|
|
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
|
|
}
|
|
|
|
$named_query = null;
|
|
$run_query = true;
|
|
$query_key = $this->queryKey;
|
|
if ($this->queryKey == 'advanced') {
|
|
$run_query = false;
|
|
$query_key = $request->getStr('query');
|
|
} else if (!strlen($this->queryKey)) {
|
|
$found_query_data = false;
|
|
|
|
if ($request->isHTTPGet() || $request->isQuicksand()) {
|
|
// If this is a GET request and it has some query data, don't
|
|
// do anything unless it's only before= or after=. We'll build and
|
|
// execute a query from it below. This allows external tools to build
|
|
// URIs like "/query/?users=a,b".
|
|
$pt_data = $request->getPassthroughRequestData();
|
|
|
|
$exempt = array(
|
|
'before' => true,
|
|
'after' => true,
|
|
'nux' => true,
|
|
'overheated' => true,
|
|
);
|
|
|
|
foreach ($pt_data as $pt_key => $pt_value) {
|
|
if (isset($exempt[$pt_key])) {
|
|
continue;
|
|
}
|
|
|
|
$found_query_data = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$found_query_data) {
|
|
// Otherwise, there's no query data so just run the user's default
|
|
// query for this application.
|
|
$query_key = $engine->getDefaultQueryKey();
|
|
}
|
|
}
|
|
|
|
if ($engine->isBuiltinQuery($query_key)) {
|
|
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
|
|
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
|
|
} else if ($query_key) {
|
|
$saved_query = id(new PhabricatorSavedQueryQuery())
|
|
->setViewer($user)
|
|
->withQueryKeys(array($query_key))
|
|
->executeOne();
|
|
|
|
if (!$saved_query) {
|
|
return new Aphront404Response();
|
|
}
|
|
|
|
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
|
|
} else {
|
|
$saved_query = $engine->buildSavedQueryFromRequest($request);
|
|
|
|
// Save the query to generate a query key, so "Save Custom Query..." and
|
|
// other features like "Bulk Edit" and "Export Data" work correctly.
|
|
$engine->saveQuery($saved_query);
|
|
}
|
|
|
|
$this->activeQuery = $saved_query;
|
|
|
|
$nav->selectFilter(
|
|
'query/'.$saved_query->getQueryKey(),
|
|
'query/advanced');
|
|
|
|
$form = id(new AphrontFormView())
|
|
->setUser($user)
|
|
->setAction($request->getPath());
|
|
|
|
$engine->buildSearchForm($form, $saved_query);
|
|
|
|
$errors = $engine->getErrors();
|
|
if ($errors) {
|
|
$run_query = false;
|
|
}
|
|
|
|
$submit = id(new AphrontFormSubmitControl())
|
|
->setValue(pht('Search'));
|
|
|
|
if ($run_query && !$named_query && $user->isLoggedIn()) {
|
|
$save_button = id(new PHUIButtonView())
|
|
->setTag('a')
|
|
->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
|
|
->setText(pht('Save Query'))
|
|
->setIcon('fa-floppy-o');
|
|
$submit->addButton($save_button);
|
|
}
|
|
|
|
// TODO: A "Create Dashboard Panel" action goes here somewhere once
|
|
// we sort out T5307.
|
|
|
|
$form->appendChild($submit);
|
|
$body = array();
|
|
|
|
if ($this->getPreface()) {
|
|
$body[] = $this->getPreface();
|
|
}
|
|
|
|
if ($named_query) {
|
|
$title = $named_query->getQueryName();
|
|
} else {
|
|
$title = pht('Advanced Search');
|
|
}
|
|
|
|
$header = id(new PHUIHeaderView())
|
|
->setHeader($title)
|
|
->setProfileHeader(true);
|
|
|
|
$box = id(new PHUIObjectBoxView())
|
|
->setHeader($header)
|
|
->addClass('application-search-results');
|
|
|
|
if ($run_query || $named_query) {
|
|
$box->setShowHide(
|
|
pht('Edit Query'),
|
|
pht('Hide Query'),
|
|
$form,
|
|
$this->getApplicationURI('query/advanced/?query='.$query_key),
|
|
(!$named_query ? true : false));
|
|
} else {
|
|
$box->setForm($form);
|
|
}
|
|
|
|
$body[] = $box;
|
|
$more_crumbs = null;
|
|
|
|
if ($run_query) {
|
|
$exec_errors = array();
|
|
|
|
$box->setAnchor(
|
|
id(new PhabricatorAnchorView())
|
|
->setAnchorName('R'));
|
|
|
|
try {
|
|
$engine->setRequest($request);
|
|
|
|
$query = $engine->buildQueryFromSavedQuery($saved_query);
|
|
|
|
$pager = $engine->newPagerForSavedQuery($saved_query);
|
|
$pager->readFromRequest($request);
|
|
|
|
$objects = $engine->executeQuery($query, $pager);
|
|
|
|
$force_nux = $request->getBool('nux');
|
|
if (!$objects || $force_nux) {
|
|
$nux_view = $this->renderNewUserView($engine, $force_nux);
|
|
} else {
|
|
$nux_view = null;
|
|
}
|
|
|
|
$is_overflowing =
|
|
$pager->willShowPagingControls() &&
|
|
$engine->getResultBucket($saved_query);
|
|
|
|
$force_overheated = $request->getBool('overheated');
|
|
$is_overheated = $query->getIsOverheated() || $force_overheated;
|
|
|
|
if ($nux_view) {
|
|
$box->appendChild($nux_view);
|
|
} else {
|
|
$list = $engine->renderResults($objects, $saved_query);
|
|
|
|
if (!($list instanceof PhabricatorApplicationSearchResultView)) {
|
|
throw new Exception(
|
|
pht(
|
|
'SearchEngines must render a "%s" object, but this engine '.
|
|
'(of class "%s") rendered something else.',
|
|
'PhabricatorApplicationSearchResultView',
|
|
get_class($engine)));
|
|
}
|
|
|
|
if ($list->getObjectList()) {
|
|
$box->setObjectList($list->getObjectList());
|
|
}
|
|
if ($list->getTable()) {
|
|
$box->setTable($list->getTable());
|
|
}
|
|
if ($list->getInfoView()) {
|
|
$box->setInfoView($list->getInfoView());
|
|
}
|
|
|
|
if ($is_overflowing) {
|
|
$box->appendChild($this->newOverflowingView());
|
|
}
|
|
|
|
if ($list->getContent()) {
|
|
$box->appendChild($list->getContent());
|
|
}
|
|
|
|
if ($is_overheated) {
|
|
$box->appendChild($this->newOverheatedView($objects));
|
|
}
|
|
|
|
$result_header = $list->getHeader();
|
|
if ($result_header) {
|
|
$box->setHeader($result_header);
|
|
$header = $result_header;
|
|
}
|
|
|
|
$actions = $list->getActions();
|
|
if ($actions) {
|
|
foreach ($actions as $action) {
|
|
$header->addActionLink($action);
|
|
}
|
|
}
|
|
|
|
$use_actions = $engine->newUseResultsActions($saved_query);
|
|
|
|
// TODO: Eventually, modularize all this stuff.
|
|
$builtin_use_actions = $this->newBuiltinUseActions();
|
|
if ($builtin_use_actions) {
|
|
foreach ($builtin_use_actions as $builtin_use_action) {
|
|
$use_actions[] = $builtin_use_action;
|
|
}
|
|
}
|
|
|
|
if ($use_actions) {
|
|
$use_dropdown = $this->newUseResultsDropdown(
|
|
$saved_query,
|
|
$use_actions);
|
|
$header->addActionLink($use_dropdown);
|
|
}
|
|
|
|
$more_crumbs = $list->getCrumbs();
|
|
|
|
if ($pager->willShowPagingControls()) {
|
|
$pager_box = id(new PHUIBoxView())
|
|
->setColor(PHUIBoxView::GREY)
|
|
->addClass('application-search-pager')
|
|
->appendChild($pager);
|
|
$body[] = $pager_box;
|
|
}
|
|
}
|
|
} catch (PhabricatorTypeaheadInvalidTokenException $ex) {
|
|
$exec_errors[] = pht(
|
|
'This query specifies an invalid parameter. Review the '.
|
|
'query parameters and correct errors.');
|
|
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
|
|
$exec_errors[] = $ex->getMessage();
|
|
} catch (PhabricatorSearchConstraintException $ex) {
|
|
$exec_errors[] = $ex->getMessage();
|
|
}
|
|
|
|
// The engine may have encountered additional errors during rendering;
|
|
// merge them in and show everything.
|
|
foreach ($engine->getErrors() as $error) {
|
|
$exec_errors[] = $error;
|
|
}
|
|
|
|
$errors = $exec_errors;
|
|
}
|
|
|
|
if ($errors) {
|
|
$box->setFormErrors($errors, pht('Query Errors'));
|
|
}
|
|
|
|
$crumbs = $parent
|
|
->buildApplicationCrumbs()
|
|
->setBorder(true);
|
|
|
|
if ($more_crumbs) {
|
|
$query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
|
|
$crumbs->addTextCrumb($title, $query_uri);
|
|
|
|
foreach ($more_crumbs as $crumb) {
|
|
$crumbs->addCrumb($crumb);
|
|
}
|
|
} else {
|
|
$crumbs->addTextCrumb($title);
|
|
}
|
|
|
|
require_celerity_resource('application-search-view-css');
|
|
|
|
return $this->newPage()
|
|
->setApplicationMenu($this->buildApplicationMenu())
|
|
->setTitle(pht('Query: %s', $title))
|
|
->setCrumbs($crumbs)
|
|
->setNavigation($nav)
|
|
->addClass('application-search-view')
|
|
->appendChild($body);
|
|
}
|
|
|
|
private function processExportRequest() {
|
|
$viewer = $this->getViewer();
|
|
$engine = $this->getSearchEngine();
|
|
$request = $this->getRequest();
|
|
|
|
if (!$this->canExport()) {
|
|
return new Aphront404Response();
|
|
}
|
|
|
|
$query_key = $this->getQueryKey();
|
|
if ($engine->isBuiltinQuery($query_key)) {
|
|
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
|
|
} else if ($query_key) {
|
|
$saved_query = id(new PhabricatorSavedQueryQuery())
|
|
->setViewer($viewer)
|
|
->withQueryKeys(array($query_key))
|
|
->executeOne();
|
|
} else {
|
|
$saved_query = null;
|
|
}
|
|
|
|
if (!$saved_query) {
|
|
return new Aphront404Response();
|
|
}
|
|
|
|
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
|
|
|
|
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
|
|
|
|
if ($named_query) {
|
|
$filename = $named_query->getQueryName();
|
|
$sheet_title = $named_query->getQueryName();
|
|
} else {
|
|
$filename = $engine->getResultTypeDescription();
|
|
$sheet_title = $engine->getResultTypeDescription();
|
|
}
|
|
$filename = phutil_utf8_strtolower($filename);
|
|
$filename = PhabricatorFile::normalizeFileName($filename);
|
|
|
|
$all_formats = PhabricatorExportFormat::getAllExportFormats();
|
|
|
|
$available_options = array();
|
|
$unavailable_options = array();
|
|
$formats = array();
|
|
$unavailable_formats = array();
|
|
foreach ($all_formats as $key => $format) {
|
|
if ($format->isExportFormatEnabled()) {
|
|
$available_options[$key] = $format->getExportFormatName();
|
|
$formats[$key] = $format;
|
|
} else {
|
|
$unavailable_options[$key] = pht(
|
|
'%s (Not Available)',
|
|
$format->getExportFormatName());
|
|
$unavailable_formats[$key] = $format;
|
|
}
|
|
}
|
|
$format_options = $available_options + $unavailable_options;
|
|
|
|
// Try to default to the format the user used last time. If you just
|
|
// exported to Excel, you probably want to export to Excel again.
|
|
$format_key = $this->readExportFormatPreference();
|
|
if (!isset($formats[$format_key])) {
|
|
$format_key = head_key($format_options);
|
|
}
|
|
|
|
// Check if this is a large result set or not. If we're exporting a
|
|
// large amount of data, we'll build the actual export file in the daemons.
|
|
|
|
$threshold = 1000;
|
|
$query = $engine->buildQueryFromSavedQuery($saved_query);
|
|
$pager = $engine->newPagerForSavedQuery($saved_query);
|
|
$pager->setPageSize($threshold + 1);
|
|
$objects = $engine->executeQuery($query, $pager);
|
|
$object_count = count($objects);
|
|
$is_large_export = ($object_count > $threshold);
|
|
|
|
$errors = array();
|
|
|
|
$e_format = null;
|
|
if ($request->isFormPost()) {
|
|
$format_key = $request->getStr('format');
|
|
|
|
if (isset($unavailable_formats[$format_key])) {
|
|
$unavailable = $unavailable_formats[$format_key];
|
|
$instructions = $unavailable->getInstallInstructions();
|
|
|
|
$markup = id(new PHUIRemarkupView($viewer, $instructions))
|
|
->setRemarkupOption(
|
|
PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
|
|
false);
|
|
|
|
return $this->newDialog()
|
|
->setTitle(pht('Export Format Not Available'))
|
|
->appendChild($markup)
|
|
->addCancelButton($cancel_uri, pht('Done'));
|
|
}
|
|
|
|
$format = idx($formats, $format_key);
|
|
|
|
if (!$format) {
|
|
$e_format = pht('Invalid');
|
|
$errors[] = pht('Choose a valid export format.');
|
|
}
|
|
|
|
if (!$errors) {
|
|
$this->writeExportFormatPreference($format_key);
|
|
|
|
$export_engine = id(new PhabricatorExportEngine())
|
|
->setViewer($viewer)
|
|
->setSearchEngine($engine)
|
|
->setSavedQuery($saved_query)
|
|
->setTitle($sheet_title)
|
|
->setFilename($filename)
|
|
->setExportFormat($format);
|
|
|
|
if ($is_large_export) {
|
|
$job = $export_engine->newBulkJob($request);
|
|
|
|
return id(new AphrontRedirectResponse())
|
|
->setURI($job->getMonitorURI());
|
|
} else {
|
|
$file = $export_engine->exportFile();
|
|
return $file->newDownloadResponse();
|
|
}
|
|
}
|
|
}
|
|
|
|
$export_form = id(new AphrontFormView())
|
|
->setViewer($viewer)
|
|
->appendControl(
|
|
id(new AphrontFormSelectControl())
|
|
->setName('format')
|
|
->setLabel(pht('Format'))
|
|
->setError($e_format)
|
|
->setValue($format_key)
|
|
->setOptions($format_options));
|
|
|
|
if ($is_large_export) {
|
|
$submit_button = pht('Continue');
|
|
} else {
|
|
$submit_button = pht('Download Data');
|
|
}
|
|
|
|
return $this->newDialog()
|
|
->setTitle(pht('Export Results'))
|
|
->setErrors($errors)
|
|
->appendForm($export_form)
|
|
->addCancelButton($cancel_uri)
|
|
->addSubmitButton($submit_button);
|
|
}
|
|
|
|
private function processEditRequest() {
|
|
$parent = $this->getDelegatingController();
|
|
$request = $this->getRequest();
|
|
$viewer = $request->getUser();
|
|
$engine = $this->getSearchEngine();
|
|
|
|
$nav = $this->getNavigation();
|
|
if (!$nav) {
|
|
$nav = $this->buildNavigation();
|
|
}
|
|
|
|
$named_queries = $engine->loadAllNamedQueries();
|
|
|
|
$can_global = $viewer->getIsAdmin();
|
|
|
|
$groups = array(
|
|
'personal' => array(
|
|
'name' => pht('Personal Saved Queries'),
|
|
'items' => array(),
|
|
'edit' => true,
|
|
),
|
|
'global' => array(
|
|
'name' => pht('Global Saved Queries'),
|
|
'items' => array(),
|
|
'edit' => $can_global,
|
|
),
|
|
);
|
|
|
|
foreach ($named_queries as $named_query) {
|
|
if ($named_query->isGlobal()) {
|
|
$group = 'global';
|
|
} else {
|
|
$group = 'personal';
|
|
}
|
|
|
|
$groups[$group]['items'][] = $named_query;
|
|
}
|
|
|
|
$default_key = $engine->getDefaultQueryKey();
|
|
|
|
$lists = array();
|
|
foreach ($groups as $group) {
|
|
$lists[] = $this->newQueryListView(
|
|
$group['name'],
|
|
$group['items'],
|
|
$default_key,
|
|
$group['edit']);
|
|
}
|
|
|
|
$crumbs = $parent
|
|
->buildApplicationCrumbs()
|
|
->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
|
|
->setBorder(true);
|
|
|
|
$nav->selectFilter('query/edit');
|
|
|
|
$header = id(new PHUIHeaderView())
|
|
->setHeader(pht('Saved Queries'))
|
|
->setProfileHeader(true);
|
|
|
|
$view = id(new PHUITwoColumnView())
|
|
->setHeader($header)
|
|
->setFooter($lists);
|
|
|
|
return $this->newPage()
|
|
->setApplicationMenu($this->buildApplicationMenu())
|
|
->setTitle(pht('Saved Queries'))
|
|
->setCrumbs($crumbs)
|
|
->setNavigation($nav)
|
|
->appendChild($view);
|
|
}
|
|
|
|
private function newQueryListView(
|
|
$list_name,
|
|
array $named_queries,
|
|
$default_key,
|
|
$can_edit) {
|
|
|
|
$engine = $this->getSearchEngine();
|
|
$viewer = $this->getViewer();
|
|
|
|
$list = id(new PHUIObjectItemListView())
|
|
->setViewer($viewer);
|
|
|
|
if ($can_edit) {
|
|
$list_id = celerity_generate_unique_node_id();
|
|
$list->setID($list_id);
|
|
|
|
Javelin::initBehavior(
|
|
'search-reorder-queries',
|
|
array(
|
|
'listID' => $list_id,
|
|
'orderURI' => '/search/order/'.get_class($engine).'/',
|
|
));
|
|
}
|
|
|
|
foreach ($named_queries as $named_query) {
|
|
$class = get_class($engine);
|
|
$key = $named_query->getQueryKey();
|
|
|
|
$item = id(new PHUIObjectItemView())
|
|
->setHeader($named_query->getQueryName())
|
|
->setHref($engine->getQueryResultsPageURI($key));
|
|
|
|
if ($named_query->getIsDisabled()) {
|
|
if ($can_edit) {
|
|
$item->setDisabled(true);
|
|
} else {
|
|
// If an item is disabled and you don't have permission to edit it,
|
|
// just skip it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($can_edit) {
|
|
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
|
|
$icon = 'fa-plus';
|
|
$disable_name = pht('Enable');
|
|
} else {
|
|
$icon = 'fa-times';
|
|
if ($named_query->getIsBuiltin()) {
|
|
$disable_name = pht('Disable');
|
|
} else {
|
|
$disable_name = pht('Delete');
|
|
}
|
|
}
|
|
|
|
if ($named_query->getID()) {
|
|
$disable_href = '/search/delete/id/'.$named_query->getID().'/';
|
|
} else {
|
|
$disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
|
|
}
|
|
|
|
$item->addAction(
|
|
id(new PHUIListItemView())
|
|
->setIcon($icon)
|
|
->setHref($disable_href)
|
|
->setRenderNameAsTooltip(true)
|
|
->setName($disable_name)
|
|
->setWorkflow(true));
|
|
}
|
|
|
|
$default_disabled = $named_query->getIsDisabled();
|
|
$default_icon = 'fa-thumb-tack';
|
|
|
|
if ($default_key === $key) {
|
|
$default_color = 'green';
|
|
} else {
|
|
$default_color = null;
|
|
}
|
|
|
|
$item->addAction(
|
|
id(new PHUIListItemView())
|
|
->setIcon("{$default_icon} {$default_color}")
|
|
->setHref('/search/default/'.$key.'/'.$class.'/')
|
|
->setRenderNameAsTooltip(true)
|
|
->setName(pht('Make Default'))
|
|
->setWorkflow(true)
|
|
->setDisabled($default_disabled));
|
|
|
|
if ($can_edit) {
|
|
if ($named_query->getIsBuiltin()) {
|
|
$edit_icon = 'fa-lock lightgreytext';
|
|
$edit_disabled = true;
|
|
$edit_name = pht('Builtin');
|
|
$edit_href = null;
|
|
} else {
|
|
$edit_icon = 'fa-pencil';
|
|
$edit_disabled = false;
|
|
$edit_name = pht('Edit');
|
|
$edit_href = '/search/edit/id/'.$named_query->getID().'/';
|
|
}
|
|
|
|
$item->addAction(
|
|
id(new PHUIListItemView())
|
|
->setIcon($edit_icon)
|
|
->setHref($edit_href)
|
|
->setRenderNameAsTooltip(true)
|
|
->setName($edit_name)
|
|
->setDisabled($edit_disabled));
|
|
}
|
|
|
|
$item->setGrippable($can_edit);
|
|
$item->addSigil('named-query');
|
|
$item->setMetadata(
|
|
array(
|
|
'queryKey' => $named_query->getQueryKey(),
|
|
));
|
|
|
|
$list->addItem($item);
|
|
}
|
|
|
|
$list->setNoDataString(pht('No saved queries.'));
|
|
|
|
return id(new PHUIObjectBoxView())
|
|
->setHeaderText($list_name)
|
|
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
|
->setObjectList($list);
|
|
}
|
|
|
|
public function buildApplicationMenu() {
|
|
$menu = $this->getDelegatingController()
|
|
->buildApplicationMenu();
|
|
|
|
if ($menu instanceof PHUIApplicationMenuView) {
|
|
$menu->setSearchEngine($this->getSearchEngine());
|
|
}
|
|
|
|
return $menu;
|
|
}
|
|
|
|
private function buildNavigation() {
|
|
$viewer = $this->getViewer();
|
|
$engine = $this->getSearchEngine();
|
|
|
|
$nav = id(new AphrontSideNavFilterView())
|
|
->setUser($viewer)
|
|
->setBaseURI(new PhutilURI($this->getApplicationURI()));
|
|
|
|
$engine->addNavigationItems($nav->getMenu());
|
|
|
|
return $nav;
|
|
}
|
|
|
|
private function renderNewUserView(
|
|
PhabricatorApplicationSearchEngine $engine,
|
|
$force_nux) {
|
|
|
|
// Don't render NUX if the user has clicked away from the default page.
|
|
if (strlen($this->getQueryKey())) {
|
|
return null;
|
|
}
|
|
|
|
// Don't put NUX in panels because it would be weird.
|
|
if ($engine->isPanelContext()) {
|
|
return null;
|
|
}
|
|
|
|
// Try to render the view itself first, since this should be very cheap
|
|
// (just returning some text).
|
|
$nux_view = $engine->renderNewUserView();
|
|
|
|
if (!$nux_view) {
|
|
return null;
|
|
}
|
|
|
|
$query = $engine->newQuery();
|
|
if (!$query) {
|
|
return null;
|
|
}
|
|
|
|
// Try to load any object at all. If we can, the application has seen some
|
|
// use so we just render the normal view.
|
|
if (!$force_nux) {
|
|
$object = $query
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
|
->setLimit(1)
|
|
->execute();
|
|
if ($object) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return $nux_view;
|
|
}
|
|
|
|
private function newUseResultsDropdown(
|
|
PhabricatorSavedQuery $query,
|
|
array $dropdown_items) {
|
|
|
|
$viewer = $this->getViewer();
|
|
|
|
$action_list = id(new PhabricatorActionListView())
|
|
->setViewer($viewer);
|
|
foreach ($dropdown_items as $dropdown_item) {
|
|
$action_list->addAction($dropdown_item);
|
|
}
|
|
|
|
return id(new PHUIButtonView())
|
|
->setTag('a')
|
|
->setHref('#')
|
|
->setText(pht('Use Results'))
|
|
->setIcon('fa-bars')
|
|
->setDropdownMenu($action_list)
|
|
->addClass('dropdown');
|
|
}
|
|
|
|
private function newOverflowingView() {
|
|
$message = pht(
|
|
'The query matched more than one page of results. Results are '.
|
|
'paginated before bucketing, so later pages may contain additional '.
|
|
'results in any bucket.');
|
|
|
|
return id(new PHUIInfoView())
|
|
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
|
->setFlush(true)
|
|
->setTitle(pht('Buckets Overflowing'))
|
|
->setErrors(
|
|
array(
|
|
$message,
|
|
));
|
|
}
|
|
|
|
private function newOverheatedView(array $results) {
|
|
if ($results) {
|
|
$message = pht(
|
|
'Most objects matching your query are not visible to you, so '.
|
|
'filtering results is taking a long time. Only some results are '.
|
|
'shown. Refine your query to find results more quickly.');
|
|
} else {
|
|
$message = pht(
|
|
'Most objects matching your query are not visible to you, so '.
|
|
'filtering results is taking a long time. Refine your query to '.
|
|
'find results more quickly.');
|
|
}
|
|
|
|
return id(new PHUIInfoView())
|
|
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
|
->setFlush(true)
|
|
->setTitle(pht('Query Overheated'))
|
|
->setErrors(
|
|
array(
|
|
$message,
|
|
));
|
|
}
|
|
|
|
private function newBuiltinUseActions() {
|
|
$actions = array();
|
|
$request = $this->getRequest();
|
|
$viewer = $request->getUser();
|
|
|
|
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
|
|
|
|
$engine = $this->getSearchEngine();
|
|
$engine_class = get_class($engine);
|
|
|
|
$query_key = $this->getActiveQuery()->getQueryKey();
|
|
|
|
$can_use = $engine->canUseInPanelContext();
|
|
$is_installed = PhabricatorApplication::isClassInstalledForViewer(
|
|
'PhabricatorDashboardApplication',
|
|
$viewer);
|
|
|
|
if ($can_use && $is_installed) {
|
|
$actions[] = id(new PhabricatorActionView())
|
|
->setIcon('fa-dashboard')
|
|
->setName(pht('Add to Dashboard'))
|
|
->setWorkflow(true)
|
|
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
|
|
}
|
|
|
|
if ($this->canExport()) {
|
|
$export_uri = $engine->getExportURI($query_key);
|
|
$actions[] = id(new PhabricatorActionView())
|
|
->setIcon('fa-download')
|
|
->setName(pht('Export Data'))
|
|
->setWorkflow(true)
|
|
->setHref($export_uri);
|
|
}
|
|
|
|
if ($is_dev) {
|
|
$engine = $this->getSearchEngine();
|
|
$nux_uri = $engine->getQueryBaseURI();
|
|
$nux_uri = id(new PhutilURI($nux_uri))
|
|
->setQueryParam('nux', true);
|
|
|
|
$actions[] = id(new PhabricatorActionView())
|
|
->setIcon('fa-user-plus')
|
|
->setName(pht('DEV: New User State'))
|
|
->setHref($nux_uri);
|
|
}
|
|
|
|
if ($is_dev) {
|
|
$overheated_uri = $this->getRequest()->getRequestURI()
|
|
->setQueryParam('overheated', true);
|
|
|
|
$actions[] = id(new PhabricatorActionView())
|
|
->setIcon('fa-fire')
|
|
->setName(pht('DEV: Overheated State'))
|
|
->setHref($overheated_uri);
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
|
|
private function canExport() {
|
|
$engine = $this->getSearchEngine();
|
|
if (!$engine->canExport()) {
|
|
return false;
|
|
}
|
|
|
|
// Don't allow logged-out users to perform exports. There's no technical
|
|
// or policy reason they can't, but we don't normally give them access
|
|
// to write files or jobs. For now, just err on the side of caution.
|
|
|
|
$viewer = $this->getViewer();
|
|
if (!$viewer->getPHID()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function readExportFormatPreference() {
|
|
$viewer = $this->getViewer();
|
|
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
|
|
return $viewer->getUserSetting($export_key);
|
|
}
|
|
|
|
private function writeExportFormatPreference($value) {
|
|
$viewer = $this->getViewer();
|
|
$request = $this->getRequest();
|
|
|
|
if (!$viewer->isLoggedIn()) {
|
|
return;
|
|
}
|
|
|
|
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
|
|
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
|
|
|
|
$editor = id(new PhabricatorUserPreferencesEditor())
|
|
->setActor($viewer)
|
|
->setContentSourceFromRequest($request)
|
|
->setContinueOnNoEffect(true)
|
|
->setContinueOnMissingFields(true);
|
|
|
|
$xactions = array();
|
|
$xactions[] = $preferences->newTransaction($export_key, $value);
|
|
$editor->applyTransactions($preferences, $xactions);
|
|
}
|
|
|
|
}
|