Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / search / controller / PhabricatorApplicationSearchController.php
bloba06fca7583cbbc5843956dd0f3bb56b7550b93db
1 <?php
3 final class PhabricatorApplicationSearchController
4 extends PhabricatorSearchBaseController {
6 private $searchEngine;
7 private $navigation;
8 private $queryKey;
9 private $preface;
10 private $activeQuery;
12 public function setPreface($preface) {
13 $this->preface = $preface;
14 return $this;
17 public function getPreface() {
18 return $this->preface;
21 public function setQueryKey($query_key) {
22 $this->queryKey = $query_key;
23 return $this;
26 protected function getQueryKey() {
27 return $this->queryKey;
30 public function setNavigation(AphrontSideNavFilterView $navigation) {
31 $this->navigation = $navigation;
32 return $this;
35 protected function getNavigation() {
36 return $this->navigation;
39 public function setSearchEngine(
40 PhabricatorApplicationSearchEngine $search_engine) {
41 $this->searchEngine = $search_engine;
42 return $this;
45 protected function getSearchEngine() {
46 return $this->searchEngine;
49 protected function getActiveQuery() {
50 if (!$this->activeQuery) {
51 throw new Exception(pht('There is no active query yet.'));
54 return $this->activeQuery;
57 protected function validateDelegatingController() {
58 $parent = $this->getDelegatingController();
60 if (!$parent) {
61 throw new Exception(
62 pht('You must delegate to this controller, not invoke it directly.'));
65 $engine = $this->getSearchEngine();
66 if (!$engine) {
67 throw new PhutilInvalidStateException('setEngine');
70 $engine->setViewer($this->getRequest()->getUser());
72 $parent = $this->getDelegatingController();
75 public function processRequest() {
76 $this->validateDelegatingController();
78 $query_action = $this->getRequest()->getURIData('queryAction');
79 if ($query_action == 'export') {
80 return $this->processExportRequest();
83 if ($query_action === 'customize') {
84 return $this->processCustomizeRequest();
87 $key = $this->getQueryKey();
88 if ($key == 'edit') {
89 return $this->processEditRequest();
90 } else {
91 return $this->processSearchRequest();
95 private function processSearchRequest() {
96 $parent = $this->getDelegatingController();
97 $request = $this->getRequest();
98 $user = $request->getUser();
99 $engine = $this->getSearchEngine();
100 $nav = $this->getNavigation();
101 if (!$nav) {
102 $nav = $this->buildNavigation();
105 if ($request->isFormPost()) {
106 $saved_query = $engine->buildSavedQueryFromRequest($request);
107 $engine->saveQuery($saved_query);
108 return id(new AphrontRedirectResponse())->setURI(
109 $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
112 $named_query = null;
113 $run_query = true;
114 $query_key = $this->queryKey;
115 if ($query_key == 'advanced') {
116 $run_query = false;
117 $query_key = $request->getStr('query');
118 } else if ($query_key === null || !strlen($query_key)) {
119 $found_query_data = false;
121 if ($request->isHTTPGet() || $request->isQuicksand()) {
122 // If this is a GET request and it has some query data, don't
123 // do anything unless it's only before= or after=. We'll build and
124 // execute a query from it below. This allows external tools to build
125 // URIs like "/query/?users=a,b".
126 $pt_data = $request->getPassthroughRequestData();
128 $exempt = array(
129 'before' => true,
130 'after' => true,
131 'nux' => true,
132 'overheated' => true,
135 foreach ($pt_data as $pt_key => $pt_value) {
136 if (isset($exempt[$pt_key])) {
137 continue;
140 $found_query_data = true;
141 break;
145 if (!$found_query_data) {
146 // Otherwise, there's no query data so just run the user's default
147 // query for this application.
148 $query_key = $engine->getDefaultQueryKey();
152 if ($engine->isBuiltinQuery($query_key)) {
153 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
154 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
155 } else if ($query_key) {
156 $saved_query = id(new PhabricatorSavedQueryQuery())
157 ->setViewer($user)
158 ->withQueryKeys(array($query_key))
159 ->executeOne();
161 if (!$saved_query) {
162 return new Aphront404Response();
165 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
166 } else {
167 $saved_query = $engine->buildSavedQueryFromRequest($request);
169 // Save the query to generate a query key, so "Save Custom Query..." and
170 // other features like "Bulk Edit" and "Export Data" work correctly.
171 $engine->saveQuery($saved_query);
174 $this->activeQuery = $saved_query;
176 $nav->selectFilter(
177 'query/'.$saved_query->getQueryKey(),
178 'query/advanced');
180 $form = id(new AphrontFormView())
181 ->setUser($user)
182 ->setAction($request->getPath());
184 $engine->buildSearchForm($form, $saved_query);
186 $errors = $engine->getErrors();
187 if ($errors) {
188 $run_query = false;
191 $submit = id(new AphrontFormSubmitControl())
192 ->setValue(pht('Search'));
194 if ($run_query && !$named_query && $user->isLoggedIn()) {
195 $save_button = id(new PHUIButtonView())
196 ->setTag('a')
197 ->setColor(PHUIButtonView::GREY)
198 ->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
199 ->setText(pht('Save Query'))
200 ->setIcon('fa-bookmark');
201 $submit->addButton($save_button);
204 $form->appendChild($submit);
205 $body = array();
207 if ($this->getPreface()) {
208 $body[] = $this->getPreface();
211 if ($named_query) {
212 $title = $named_query->getQueryName();
213 } else {
214 $title = pht('Advanced Search');
217 $header = id(new PHUIHeaderView())
218 ->setHeader($title)
219 ->setProfileHeader(true);
221 $box = id(new PHUIObjectBoxView())
222 ->setHeader($header)
223 ->addClass('application-search-results');
225 if ($run_query || $named_query) {
226 $box->setShowHide(
227 pht('Edit Query'),
228 pht('Hide Query'),
229 $form,
230 $this->getApplicationURI('query/advanced/?query='.$query_key),
231 (!$named_query ? true : false));
232 } else {
233 $box->setForm($form);
236 $body[] = $box;
237 $more_crumbs = null;
239 if ($run_query) {
240 $exec_errors = array();
242 $box->setAnchor(
243 id(new PhabricatorAnchorView())
244 ->setAnchorName('R'));
246 try {
247 $engine->setRequest($request);
249 $query = $engine->buildQueryFromSavedQuery($saved_query);
251 $pager = $engine->newPagerForSavedQuery($saved_query);
252 $pager->readFromRequest($request);
254 $query->setReturnPartialResultsOnOverheat(true);
256 $objects = $engine->executeQuery($query, $pager);
258 $force_nux = $request->getBool('nux');
259 if (!$objects || $force_nux) {
260 $nux_view = $this->renderNewUserView($engine, $force_nux);
261 } else {
262 $nux_view = null;
265 $is_overflowing =
266 $pager->willShowPagingControls() &&
267 $engine->getResultBucket($saved_query);
269 $force_overheated = $request->getBool('overheated');
270 $is_overheated = $query->getIsOverheated() || $force_overheated;
272 if ($nux_view) {
273 $box->appendChild($nux_view);
274 } else {
275 $list = $engine->renderResults($objects, $saved_query);
277 if (!($list instanceof PhabricatorApplicationSearchResultView)) {
278 throw new Exception(
279 pht(
280 'SearchEngines must render a "%s" object, but this engine '.
281 '(of class "%s") rendered something else ("%s").',
282 'PhabricatorApplicationSearchResultView',
283 get_class($engine),
284 phutil_describe_type($list)));
287 if ($list->getObjectList()) {
288 $box->setObjectList($list->getObjectList());
290 if ($list->getTable()) {
291 $box->setTable($list->getTable());
293 if ($list->getInfoView()) {
294 $box->setInfoView($list->getInfoView());
297 if ($is_overflowing) {
298 $box->appendChild($this->newOverflowingView());
301 if ($list->getContent()) {
302 $box->appendChild($list->getContent());
305 if ($is_overheated) {
306 $box->appendChild($this->newOverheatedView($objects));
309 $result_header = $list->getHeader();
310 if ($result_header) {
311 $box->setHeader($result_header);
312 $header = $result_header;
315 $actions = $list->getActions();
316 if ($actions) {
317 foreach ($actions as $action) {
318 $header->addActionLink($action);
322 $use_actions = $engine->newUseResultsActions($saved_query);
324 // TODO: Eventually, modularize all this stuff.
325 $builtin_use_actions = $this->newBuiltinUseActions();
326 if ($builtin_use_actions) {
327 foreach ($builtin_use_actions as $builtin_use_action) {
328 $use_actions[] = $builtin_use_action;
332 if ($use_actions) {
333 $use_dropdown = $this->newUseResultsDropdown(
334 $saved_query,
335 $use_actions);
336 $header->addActionLink($use_dropdown);
339 $more_crumbs = $list->getCrumbs();
341 if ($pager->willShowPagingControls()) {
342 $pager_box = id(new PHUIBoxView())
343 ->setColor(PHUIBoxView::GREY)
344 ->addClass('application-search-pager')
345 ->appendChild($pager);
346 $body[] = $pager_box;
349 } catch (PhabricatorTypeaheadInvalidTokenException $ex) {
350 $exec_errors[] = pht(
351 'This query specifies an invalid parameter. Review the '.
352 'query parameters and correct errors.');
353 } catch (PhutilSearchQueryCompilerSyntaxException $ex) {
354 $exec_errors[] = $ex->getMessage();
355 } catch (PhabricatorSearchConstraintException $ex) {
356 $exec_errors[] = $ex->getMessage();
357 } catch (PhabricatorInvalidQueryCursorException $ex) {
358 $exec_errors[] = $ex->getMessage();
361 // The engine may have encountered additional errors during rendering;
362 // merge them in and show everything.
363 foreach ($engine->getErrors() as $error) {
364 $exec_errors[] = $error;
367 $errors = $exec_errors;
370 if ($errors) {
371 $box->setFormErrors($errors, pht('Query Errors'));
374 $crumbs = $parent
375 ->buildApplicationCrumbs()
376 ->setBorder(true);
378 if ($more_crumbs) {
379 $query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
380 $crumbs->addTextCrumb($title, $query_uri);
382 foreach ($more_crumbs as $crumb) {
383 $crumbs->addCrumb($crumb);
385 } else {
386 $crumbs->addTextCrumb($title);
389 require_celerity_resource('application-search-view-css');
391 return $this->newPage()
392 ->setTitle(pht('Query: %s', $title))
393 ->setCrumbs($crumbs)
394 ->setNavigation($nav)
395 ->addClass('application-search-view')
396 ->appendChild($body);
399 private function processExportRequest() {
400 $viewer = $this->getViewer();
401 $engine = $this->getSearchEngine();
402 $request = $this->getRequest();
404 if (!$this->canExport()) {
405 return new Aphront404Response();
408 $query_key = $this->getQueryKey();
409 if ($engine->isBuiltinQuery($query_key)) {
410 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
411 } else if ($query_key) {
412 $saved_query = id(new PhabricatorSavedQueryQuery())
413 ->setViewer($viewer)
414 ->withQueryKeys(array($query_key))
415 ->executeOne();
416 } else {
417 $saved_query = null;
420 if (!$saved_query) {
421 return new Aphront404Response();
424 $cancel_uri = $engine->getQueryResultsPageURI($query_key);
426 $named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
428 if ($named_query) {
429 $filename = $named_query->getQueryName();
430 $sheet_title = $named_query->getQueryName();
431 } else {
432 $filename = $engine->getResultTypeDescription();
433 $sheet_title = $engine->getResultTypeDescription();
435 $filename = phutil_utf8_strtolower($filename);
436 $filename = PhabricatorFile::normalizeFileName($filename);
438 $all_formats = PhabricatorExportFormat::getAllExportFormats();
440 $available_options = array();
441 $unavailable_options = array();
442 $formats = array();
443 $unavailable_formats = array();
444 foreach ($all_formats as $key => $format) {
445 if ($format->isExportFormatEnabled()) {
446 $available_options[$key] = $format->getExportFormatName();
447 $formats[$key] = $format;
448 } else {
449 $unavailable_options[$key] = pht(
450 '%s (Not Available)',
451 $format->getExportFormatName());
452 $unavailable_formats[$key] = $format;
455 $format_options = $available_options + $unavailable_options;
457 // Try to default to the format the user used last time. If you just
458 // exported to Excel, you probably want to export to Excel again.
459 $format_key = $this->readExportFormatPreference();
460 if (!isset($formats[$format_key])) {
461 $format_key = head_key($format_options);
464 // Check if this is a large result set or not. If we're exporting a
465 // large amount of data, we'll build the actual export file in the daemons.
467 $threshold = 1000;
468 $query = $engine->buildQueryFromSavedQuery($saved_query);
469 $pager = $engine->newPagerForSavedQuery($saved_query);
470 $pager->setPageSize($threshold + 1);
471 $objects = $engine->executeQuery($query, $pager);
472 $object_count = count($objects);
473 $is_large_export = ($object_count > $threshold);
475 $errors = array();
477 $e_format = null;
478 if ($request->isFormPost()) {
479 $format_key = $request->getStr('format');
481 if (isset($unavailable_formats[$format_key])) {
482 $unavailable = $unavailable_formats[$format_key];
483 $instructions = $unavailable->getInstallInstructions();
485 $markup = id(new PHUIRemarkupView($viewer, $instructions))
486 ->setRemarkupOption(
487 PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
488 false);
490 return $this->newDialog()
491 ->setTitle(pht('Export Format Not Available'))
492 ->appendChild($markup)
493 ->addCancelButton($cancel_uri, pht('Done'));
496 $format = idx($formats, $format_key);
498 if (!$format) {
499 $e_format = pht('Invalid');
500 $errors[] = pht('Choose a valid export format.');
503 if (!$errors) {
504 $this->writeExportFormatPreference($format_key);
506 $export_engine = id(new PhabricatorExportEngine())
507 ->setViewer($viewer)
508 ->setSearchEngine($engine)
509 ->setSavedQuery($saved_query)
510 ->setTitle($sheet_title)
511 ->setFilename($filename)
512 ->setExportFormat($format);
514 if ($is_large_export) {
515 $job = $export_engine->newBulkJob($request);
517 return id(new AphrontRedirectResponse())
518 ->setURI($job->getMonitorURI());
519 } else {
520 $file = $export_engine->exportFile();
521 return $file->newDownloadResponse();
526 $export_form = id(new AphrontFormView())
527 ->setViewer($viewer)
528 ->appendControl(
529 id(new AphrontFormSelectControl())
530 ->setName('format')
531 ->setLabel(pht('Format'))
532 ->setError($e_format)
533 ->setValue($format_key)
534 ->setOptions($format_options));
536 if ($is_large_export) {
537 $submit_button = pht('Continue');
538 } else {
539 $submit_button = pht('Download Data');
542 return $this->newDialog()
543 ->setTitle(pht('Export Results'))
544 ->setErrors($errors)
545 ->appendForm($export_form)
546 ->addCancelButton($cancel_uri)
547 ->addSubmitButton($submit_button);
550 private function processEditRequest() {
551 $parent = $this->getDelegatingController();
552 $request = $this->getRequest();
553 $viewer = $request->getUser();
554 $engine = $this->getSearchEngine();
556 $nav = $this->getNavigation();
557 if (!$nav) {
558 $nav = $this->buildNavigation();
561 $named_queries = $engine->loadAllNamedQueries();
563 $can_global = $viewer->getIsAdmin();
565 $groups = array(
566 'personal' => array(
567 'name' => pht('Personal Saved Queries'),
568 'items' => array(),
569 'edit' => true,
571 'global' => array(
572 'name' => pht('Global Saved Queries'),
573 'items' => array(),
574 'edit' => $can_global,
578 foreach ($named_queries as $named_query) {
579 if ($named_query->isGlobal()) {
580 $group = 'global';
581 } else {
582 $group = 'personal';
585 $groups[$group]['items'][] = $named_query;
588 $default_key = $engine->getDefaultQueryKey();
590 $lists = array();
591 foreach ($groups as $group) {
592 $lists[] = $this->newQueryListView(
593 $group['name'],
594 $group['items'],
595 $default_key,
596 $group['edit']);
599 $crumbs = $parent
600 ->buildApplicationCrumbs()
601 ->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
602 ->setBorder(true);
604 $nav->selectFilter('query/edit');
606 $header = id(new PHUIHeaderView())
607 ->setHeader(pht('Saved Queries'))
608 ->setProfileHeader(true);
610 $view = id(new PHUITwoColumnView())
611 ->setHeader($header)
612 ->setFooter($lists);
614 return $this->newPage()
615 ->setTitle(pht('Saved Queries'))
616 ->setCrumbs($crumbs)
617 ->setNavigation($nav)
618 ->appendChild($view);
621 private function newQueryListView(
622 $list_name,
623 array $named_queries,
624 $default_key,
625 $can_edit) {
627 $engine = $this->getSearchEngine();
628 $viewer = $this->getViewer();
630 $list = id(new PHUIObjectItemListView())
631 ->setViewer($viewer);
633 if ($can_edit) {
634 $list_id = celerity_generate_unique_node_id();
635 $list->setID($list_id);
637 Javelin::initBehavior(
638 'search-reorder-queries',
639 array(
640 'listID' => $list_id,
641 'orderURI' => '/search/order/'.get_class($engine).'/',
645 foreach ($named_queries as $named_query) {
646 $class = get_class($engine);
647 $key = $named_query->getQueryKey();
649 $item = id(new PHUIObjectItemView())
650 ->setHeader($named_query->getQueryName())
651 ->setHref($engine->getQueryResultsPageURI($key));
653 if ($named_query->getIsDisabled()) {
654 if ($can_edit) {
655 $item->setDisabled(true);
656 } else {
657 // If an item is disabled and you don't have permission to edit it,
658 // just skip it.
659 continue;
663 if ($can_edit) {
664 if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
665 $icon = 'fa-plus';
666 $disable_name = pht('Enable');
667 } else {
668 $icon = 'fa-times';
669 if ($named_query->getIsBuiltin()) {
670 $disable_name = pht('Disable');
671 } else {
672 $disable_name = pht('Delete');
676 if ($named_query->getID()) {
677 $disable_href = '/search/delete/id/'.$named_query->getID().'/';
678 } else {
679 $disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
682 $item->addAction(
683 id(new PHUIListItemView())
684 ->setIcon($icon)
685 ->setHref($disable_href)
686 ->setRenderNameAsTooltip(true)
687 ->setName($disable_name)
688 ->setWorkflow(true));
691 $default_disabled = $named_query->getIsDisabled();
692 $default_icon = 'fa-thumb-tack';
694 if ($default_key === $key) {
695 $default_color = 'green';
696 } else {
697 $default_color = null;
700 $item->addAction(
701 id(new PHUIListItemView())
702 ->setIcon("{$default_icon} {$default_color}")
703 ->setHref('/search/default/'.$key.'/'.$class.'/')
704 ->setRenderNameAsTooltip(true)
705 ->setName(pht('Make Default'))
706 ->setWorkflow(true)
707 ->setDisabled($default_disabled));
709 if ($can_edit) {
710 if ($named_query->getIsBuiltin()) {
711 $edit_icon = 'fa-lock lightgreytext';
712 $edit_disabled = true;
713 $edit_name = pht('Builtin');
714 $edit_href = null;
715 } else {
716 $edit_icon = 'fa-pencil';
717 $edit_disabled = false;
718 $edit_name = pht('Edit');
719 $edit_href = '/search/edit/id/'.$named_query->getID().'/';
722 $item->addAction(
723 id(new PHUIListItemView())
724 ->setIcon($edit_icon)
725 ->setHref($edit_href)
726 ->setRenderNameAsTooltip(true)
727 ->setName($edit_name)
728 ->setDisabled($edit_disabled));
731 $item->setGrippable($can_edit);
732 $item->addSigil('named-query');
733 $item->setMetadata(
734 array(
735 'queryKey' => $named_query->getQueryKey(),
738 $list->addItem($item);
741 $list->setNoDataString(pht('No saved queries.'));
743 return id(new PHUIObjectBoxView())
744 ->setHeaderText($list_name)
745 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
746 ->setObjectList($list);
749 public function buildApplicationMenu() {
750 $menu = $this->getDelegatingController()
751 ->buildApplicationMenu();
753 if ($menu instanceof PHUIApplicationMenuView) {
754 $menu->setSearchEngine($this->getSearchEngine());
757 return $menu;
760 private function buildNavigation() {
761 $viewer = $this->getViewer();
762 $engine = $this->getSearchEngine();
764 $nav = id(new AphrontSideNavFilterView())
765 ->setUser($viewer)
766 ->setBaseURI(new PhutilURI($this->getApplicationURI()));
768 $engine->addNavigationItems($nav->getMenu());
770 return $nav;
773 private function renderNewUserView(
774 PhabricatorApplicationSearchEngine $engine,
775 $force_nux) {
777 // Don't render NUX if the user has clicked away from the default page.
778 if ($this->getQueryKey() !== null && strlen($this->getQueryKey())) {
779 return null;
782 // Don't put NUX in panels because it would be weird.
783 if ($engine->isPanelContext()) {
784 return null;
787 // Try to render the view itself first, since this should be very cheap
788 // (just returning some text).
789 $nux_view = $engine->renderNewUserView();
791 if (!$nux_view) {
792 return null;
795 $query = $engine->newQuery();
796 if (!$query) {
797 return null;
800 // Try to load any object at all. If we can, the application has seen some
801 // use so we just render the normal view.
802 if (!$force_nux) {
803 $object = $query
804 ->setViewer(PhabricatorUser::getOmnipotentUser())
805 ->setLimit(1)
806 ->setReturnPartialResultsOnOverheat(true)
807 ->execute();
808 if ($object) {
809 return null;
813 return $nux_view;
816 private function newUseResultsDropdown(
817 PhabricatorSavedQuery $query,
818 array $dropdown_items) {
820 $viewer = $this->getViewer();
822 $action_list = id(new PhabricatorActionListView())
823 ->setViewer($viewer);
824 foreach ($dropdown_items as $dropdown_item) {
825 $action_list->addAction($dropdown_item);
828 return id(new PHUIButtonView())
829 ->setTag('a')
830 ->setHref('#')
831 ->setText(pht('Use Results'))
832 ->setIcon('fa-bars')
833 ->setDropdownMenu($action_list)
834 ->addClass('dropdown');
837 private function newOverflowingView() {
838 $message = pht(
839 'The query matched more than one page of results. Results are '.
840 'paginated before bucketing, so later pages may contain additional '.
841 'results in any bucket.');
843 return id(new PHUIInfoView())
844 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
845 ->setFlush(true)
846 ->setTitle(pht('Buckets Overflowing'))
847 ->setErrors(
848 array(
849 $message,
853 public static function newOverheatedError($has_results) {
854 $overheated_link = phutil_tag(
855 'a',
856 array(
857 'href' => 'https://phurl.io/u/overheated',
858 'target' => '_blank',
860 pht('Learn More'));
862 if ($has_results) {
863 $message = pht(
864 'This query took too long, so only some results are shown. %s',
865 $overheated_link);
866 } else {
867 $message = pht(
868 'This query took too long. %s',
869 $overheated_link);
872 return $message;
875 private function newOverheatedView(array $results) {
876 $message = self::newOverheatedError((bool)$results);
878 return id(new PHUIInfoView())
879 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
880 ->setFlush(true)
881 ->setTitle(pht('Query Overheated'))
882 ->setErrors(
883 array(
884 $message,
888 private function newBuiltinUseActions() {
889 $actions = array();
890 $request = $this->getRequest();
891 $viewer = $request->getUser();
893 $is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
895 $engine = $this->getSearchEngine();
896 $engine_class = get_class($engine);
898 $query_key = $this->getActiveQuery()->getQueryKey();
900 $can_use = $engine->canUseInPanelContext();
901 $is_installed = PhabricatorApplication::isClassInstalledForViewer(
902 'PhabricatorDashboardApplication',
903 $viewer);
905 if ($can_use && $is_installed) {
906 $actions[] = id(new PhabricatorActionView())
907 ->setIcon('fa-dashboard')
908 ->setName(pht('Add to Dashboard'))
909 ->setWorkflow(true)
910 ->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
913 if ($this->canExport()) {
914 $export_uri = $engine->getExportURI($query_key);
915 $actions[] = id(new PhabricatorActionView())
916 ->setIcon('fa-download')
917 ->setName(pht('Export Data'))
918 ->setWorkflow(true)
919 ->setHref($export_uri);
922 if ($is_dev) {
923 $engine = $this->getSearchEngine();
924 $nux_uri = $engine->getQueryBaseURI();
925 $nux_uri = id(new PhutilURI($nux_uri))
926 ->replaceQueryParam('nux', true);
928 $actions[] = id(new PhabricatorActionView())
929 ->setIcon('fa-user-plus')
930 ->setName(pht('DEV: New User State'))
931 ->setHref($nux_uri);
934 if ($is_dev) {
935 $overheated_uri = $this->getRequest()->getRequestURI()
936 ->replaceQueryParam('overheated', true);
938 $actions[] = id(new PhabricatorActionView())
939 ->setIcon('fa-fire')
940 ->setName(pht('DEV: Overheated State'))
941 ->setHref($overheated_uri);
944 return $actions;
947 private function canExport() {
948 $engine = $this->getSearchEngine();
949 if (!$engine->canExport()) {
950 return false;
953 // Don't allow logged-out users to perform exports. There's no technical
954 // or policy reason they can't, but we don't normally give them access
955 // to write files or jobs. For now, just err on the side of caution.
957 $viewer = $this->getViewer();
958 if (!$viewer->getPHID()) {
959 return false;
962 return true;
965 private function readExportFormatPreference() {
966 $viewer = $this->getViewer();
967 $export_key = PhabricatorExportFormatSetting::SETTINGKEY;
968 $value = $viewer->getUserSetting($export_key);
970 if (is_string($value)) {
971 return $value;
974 return '';
977 private function writeExportFormatPreference($value) {
978 $viewer = $this->getViewer();
979 $request = $this->getRequest();
981 if (!$viewer->isLoggedIn()) {
982 return;
985 $export_key = PhabricatorExportFormatSetting::SETTINGKEY;
986 $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
988 $editor = id(new PhabricatorUserPreferencesEditor())
989 ->setActor($viewer)
990 ->setContentSourceFromRequest($request)
991 ->setContinueOnNoEffect(true)
992 ->setContinueOnMissingFields(true);
994 $xactions = array();
995 $xactions[] = $preferences->newTransaction($export_key, $value);
996 $editor->applyTransactions($preferences, $xactions);
999 private function processCustomizeRequest() {
1000 $viewer = $this->getViewer();
1001 $engine = $this->getSearchEngine();
1002 $request = $this->getRequest();
1004 $object_phid = $request->getStr('search.objectPHID');
1005 $context_phid = $request->getStr('search.contextPHID');
1007 // For now, the object can only be a dashboard panel, so just use a panel
1008 // query explicitly.
1009 $object = id(new PhabricatorDashboardPanelQuery())
1010 ->setViewer($viewer)
1011 ->withPHIDs(array($object_phid))
1012 ->requireCapabilities(
1013 array(
1014 PhabricatorPolicyCapability::CAN_VIEW,
1015 PhabricatorPolicyCapability::CAN_EDIT,
1017 ->executeOne();
1018 if (!$object) {
1019 return new Aphront404Response();
1022 $object_name = pht('%s %s', $object->getMonogram(), $object->getName());
1024 // Likewise, the context object can only be a dashboard.
1025 if ($context_phid !== null && !strlen($context_phid)) {
1026 $context = id(new PhabricatorDashboardQuery())
1027 ->setViewer($viewer)
1028 ->withPHIDs(array($context_phid))
1029 ->executeOne();
1030 if (!$context) {
1031 return new Aphront404Response();
1033 } else {
1034 $context = $object;
1037 $done_uri = $context->getURI();
1039 if ($request->isFormPost()) {
1040 $saved_query = $engine->buildSavedQueryFromRequest($request);
1041 $engine->saveQuery($saved_query);
1042 $query_key = $saved_query->getQueryKey();
1043 } else {
1044 $query_key = $this->getQueryKey();
1045 if ($engine->isBuiltinQuery($query_key)) {
1046 $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
1047 } else if ($query_key) {
1048 $saved_query = id(new PhabricatorSavedQueryQuery())
1049 ->setViewer($viewer)
1050 ->withQueryKeys(array($query_key))
1051 ->executeOne();
1052 } else {
1053 $saved_query = null;
1057 if (!$saved_query) {
1058 return new Aphront404Response();
1061 $form = id(new AphrontFormView())
1062 ->setViewer($viewer)
1063 ->addHiddenInput('search.objectPHID', $object_phid)
1064 ->addHiddenInput('search.contextPHID', $context_phid)
1065 ->setAction($request->getPath());
1067 $engine->buildSearchForm($form, $saved_query);
1069 $errors = $engine->getErrors();
1070 if ($request->isFormPost()) {
1071 if (!$errors) {
1072 $xactions = array();
1074 // Since this workflow is currently used only by dashboard panels,
1075 // we can hard-code how the edit works.
1076 $xactions[] = $object->getApplicationTransactionTemplate()
1077 ->setTransactionType(
1078 PhabricatorDashboardQueryPanelQueryTransaction::TRANSACTIONTYPE)
1079 ->setNewValue($query_key);
1081 $editor = $object->getApplicationTransactionEditor()
1082 ->setActor($viewer)
1083 ->setContentSourceFromRequest($request)
1084 ->setContinueOnNoEffect(true)
1085 ->setContinueOnMissingFields(true);
1087 $editor->applyTransactions($object, $xactions);
1089 return id(new AphrontRedirectResponse())->setURI($done_uri);
1093 return $this->newDialog()
1094 ->setTitle(pht('Customize Query: %s', $object_name))
1095 ->setErrors($errors)
1096 ->setWidth(AphrontDialogView::WIDTH_FULL)
1097 ->appendForm($form)
1098 ->addCancelButton($done_uri)
1099 ->addSubmitButton(pht('Save Changes'));