@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorProfileMenuItemViewList
4 extends Phobject {
5
6 private $engine;
7 private $views = array();
8 private $selectedView;
9
10 public function setProfileMenuEngine(PhabricatorProfileMenuEngine $engine) {
11 $this->engine = $engine;
12 return $this;
13 }
14
15 public function getProfileMenuEngine() {
16 return $this->engine;
17 }
18
19 public function addItemView(PhabricatorProfileMenuItemView $view) {
20 $this->views[] = $view;
21 return $this;
22 }
23
24 public function getItemViews() {
25 return $this->views;
26 }
27
28 public function setSelectedView(PhabricatorProfileMenuItemView $view) {
29 $found = false;
30 foreach ($this->getItemViews() as $item_view) {
31 if ($view === $item_view) {
32 $found = true;
33 break;
34 }
35 }
36
37 if (!$found) {
38 throw new Exception(
39 pht(
40 'Provided view is not one of the views in the list: you can only '.
41 'select a view which appears in the list.'));
42 }
43
44 $this->selectedView = $view;
45
46 return $this;
47 }
48
49 public function setSelectedViewWithItemIdentifier($identifier) {
50 $views = $this->getViewsWithItemIdentifier($identifier);
51
52 if (!$views) {
53 throw new Exception(
54 pht(
55 'No views match identifier "%s"!',
56 $identifier));
57 }
58
59 return $this->setSelectedView(head($views));
60 }
61
62 public function getViewsWithItemIdentifier($identifier) {
63 $views = $this->getItemViews();
64
65 $results = array();
66 foreach ($views as $view) {
67 $config = $view->getMenuItemConfiguration();
68
69 if (!$config->matchesIdentifier($identifier)) {
70 continue;
71 }
72
73 $results[] = $view;
74 }
75
76 return $results;
77 }
78
79 public function getDefaultViews() {
80 $engine = $this->getProfileMenuEngine();
81 $can_pin = $engine->isMenuEnginePinnable();
82
83 $views = $this->getItemViews();
84
85 // Remove all the views which were built by an item that can not be the
86 // default item.
87 foreach ($views as $key => $view) {
88 $config = $view->getMenuItemConfiguration();
89
90 if (!$config->canMakeDefault()) {
91 unset($views[$key]);
92 continue;
93 }
94 }
95
96 // Remove disabled views.
97 foreach ($views as $key => $view) {
98 if ($view->getDisabled()) {
99 unset($views[$key]);
100 }
101 }
102
103 // If this engine supports pinning items and we have candidate views from a
104 // valid pinned item, they are the default views.
105 if ($can_pin) {
106 $pinned = array();
107
108 foreach ($views as $key => $view) {
109 $config = $view->getMenuItemConfiguration();
110
111 if ($config->isDefault()) {
112 $pinned[] = $view;
113 continue;
114 }
115 }
116
117 if ($pinned) {
118 return $pinned;
119 }
120 }
121
122 // Return whatever remains that's still valid.
123 return $views;
124 }
125
126 public function newNavigationView() {
127 $engine = $this->getProfileMenuEngine();
128
129 $base_uri = $engine->getItemURI('');
130 $base_uri = new PhutilURI($base_uri);
131
132 $navigation = id(new AphrontSideNavFilterView())
133 ->setIsProfileMenu(true)
134 ->setBaseURI($base_uri);
135
136 $views = $this->getItemViews();
137 $selected_item = null;
138 $item_key = 0;
139 $items = array();
140 foreach ($views as $view) {
141 $list_item = $view->newListItemView();
142
143 // Assign unique keys to the list items. These keys are purely internal.
144 $list_item->setKey(sprintf('item(%d)', $item_key++));
145
146 if ($this->selectedView) {
147 if ($this->selectedView === $view) {
148 $selected_item = $list_item;
149 }
150 }
151
152 $navigation->addMenuItem($list_item);
153 $items[] = $list_item;
154 }
155
156 if (!$views) {
157 // If the navigation menu has no items, add an empty label item to
158 // force it to render something.
159 $empty_item = id(new PHUIListItemView())
160 ->setType(PHUIListItemView::TYPE_LABEL);
161 $navigation->addMenuItem($empty_item);
162 }
163
164 $highlight_key = $this->getHighlightedItemKey(
165 $items,
166 $selected_item);
167 $navigation->selectFilter($highlight_key);
168
169 return $navigation;
170 }
171
172 /**
173 * @param array<PHUIListItemView> $items
174 * @param ?PHUIListItemView $selected_item
175 */
176 private function getHighlightedItemKey(
177 array $items,
178 ?PHUIListItemView $selected_item = null) {
179
180 assert_instances_of($items, PHUIListItemView::class);
181
182 $default_key = null;
183 if ($selected_item) {
184 $default_key = $selected_item->getKey();
185 }
186
187 $engine = $this->getProfileMenuEngine();
188 $controller = $engine->getController();
189
190 // In some rare cases, when like building the "Favorites" menu on a
191 // 404 page, we may not have a controller. Just accept whatever default
192 // behavior we'd otherwise end up with.
193 if (!$controller) {
194 return $default_key;
195 }
196
197 $request = $controller->getRequest();
198
199 // See T12949. If one of the menu items is a link to the same URI that
200 // the page was accessed with, we want to highlight that item. For example,
201 // this allows you to add links to a menu that apply filters to a
202 // workboard.
203
204 $matches = array();
205 foreach ($items as $item) {
206 $href = $item->getHref();
207 if ($this->isMatchForRequestURI($request, $href)) {
208 $matches[] = $item;
209 }
210 }
211
212 foreach ($matches as $match) {
213 if ($match->getKey() === $default_key) {
214 return $default_key;
215 }
216 }
217
218 if ($matches) {
219 return head($matches)->getKey();
220 }
221
222 return $default_key;
223 }
224
225 private function isMatchForRequestURI(AphrontRequest $request, $item_uri) {
226 $request_uri = $request->getAbsoluteRequestURI();
227 $item_uri = new PhutilURI($item_uri);
228
229 // If the request URI and item URI don't have matching paths, they
230 // do not match.
231 if ($request_uri->getPath() !== $item_uri->getPath()) {
232 return false;
233 }
234
235 // If the request URI and item URI don't have matching parameters, they
236 // also do not match. We're specifically trying to let "?filter=X" work
237 // on Workboards, among other use cases, so this is important.
238 $request_params = $request_uri->getQueryParamsAsPairList();
239 $item_params = $item_uri->getQueryParamsAsPairList();
240 if ($request_params !== $item_params) {
241 return false;
242 }
243
244 // If the paths and parameters match, the item domain must be: empty; or
245 // match the request domain; or match the production domain.
246
247 $request_domain = $request_uri->getDomain();
248
249 $production_uri = PhabricatorEnv::getProductionURI('/');
250 $production_domain = id(new PhutilURI($production_uri))
251 ->getDomain();
252
253 $allowed_domains = array(
254 '',
255 $request_domain,
256 $production_domain,
257 );
258 $allowed_domains = array_fuse($allowed_domains);
259
260 $item_domain = $item_uri->getDomain();
261 $item_domain = (string)$item_domain;
262
263 if (isset($allowed_domains[$item_domain])) {
264 return true;
265 }
266
267 return false;
268 }
269
270}