Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/base/controller/PhabricatorController.php
13401 views
1
<?php
2
3
abstract class PhabricatorController extends AphrontController {
4
5
private $handles;
6
7
public function shouldRequireLogin() {
8
return true;
9
}
10
11
public function shouldRequireAdmin() {
12
return false;
13
}
14
15
public function shouldRequireEnabledUser() {
16
return true;
17
}
18
19
public function shouldAllowPublic() {
20
return false;
21
}
22
23
public function shouldAllowPartialSessions() {
24
return false;
25
}
26
27
public function shouldRequireEmailVerification() {
28
return PhabricatorUserEmail::isEmailVerificationRequired();
29
}
30
31
public function shouldAllowRestrictedParameter($parameter_name) {
32
return false;
33
}
34
35
public function shouldRequireMultiFactorEnrollment() {
36
if (!$this->shouldRequireLogin()) {
37
return false;
38
}
39
40
if (!$this->shouldRequireEnabledUser()) {
41
return false;
42
}
43
44
if ($this->shouldAllowPartialSessions()) {
45
return false;
46
}
47
48
$user = $this->getRequest()->getUser();
49
if (!$user->getIsStandardUser()) {
50
return false;
51
}
52
53
return PhabricatorEnv::getEnvConfig('security.require-multi-factor-auth');
54
}
55
56
public function shouldAllowLegallyNonCompliantUsers() {
57
return false;
58
}
59
60
public function isGlobalDragAndDropUploadEnabled() {
61
return false;
62
}
63
64
public function willBeginExecution() {
65
$request = $this->getRequest();
66
67
if ($request->getUser()) {
68
// NOTE: Unit tests can set a user explicitly. Normal requests are not
69
// permitted to do this.
70
PhabricatorTestCase::assertExecutingUnitTests();
71
$user = $request->getUser();
72
} else {
73
$user = new PhabricatorUser();
74
$session_engine = new PhabricatorAuthSessionEngine();
75
76
$phsid = $request->getCookie(PhabricatorCookies::COOKIE_SESSION);
77
if ($phsid !== null && strlen($phsid)) {
78
$session_user = $session_engine->loadUserForSession(
79
PhabricatorAuthSession::TYPE_WEB,
80
$phsid);
81
if ($session_user) {
82
$user = $session_user;
83
}
84
} else {
85
// If the client doesn't have a session token, generate an anonymous
86
// session. This is used to provide CSRF protection to logged-out users.
87
$phsid = $session_engine->establishSession(
88
PhabricatorAuthSession::TYPE_WEB,
89
null,
90
$partial = false);
91
92
// This may be a resource request, in which case we just don't set
93
// the cookie.
94
if ($request->canSetCookies()) {
95
$request->setCookie(PhabricatorCookies::COOKIE_SESSION, $phsid);
96
}
97
}
98
99
100
if (!$user->isLoggedIn()) {
101
$csrf = PhabricatorHash::digestWithNamedKey($phsid, 'csrf.alternate');
102
$user->attachAlternateCSRFString($csrf);
103
}
104
105
$request->setUser($user);
106
}
107
108
id(new PhabricatorAuthSessionEngine())
109
->willServeRequestForUser($user);
110
111
if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) {
112
$dark_console = PhabricatorDarkConsoleSetting::SETTINGKEY;
113
if ($user->getUserSetting($dark_console) ||
114
PhabricatorEnv::getEnvConfig('darkconsole.always-on')) {
115
$console = new DarkConsoleCore();
116
$request->getApplicationConfiguration()->setConsole($console);
117
}
118
}
119
120
// NOTE: We want to set up the user first so we can render a real page
121
// here, but fire this before any real logic.
122
$restricted = array(
123
'code',
124
);
125
foreach ($restricted as $parameter) {
126
if ($request->getExists($parameter)) {
127
if (!$this->shouldAllowRestrictedParameter($parameter)) {
128
throw new Exception(
129
pht(
130
'Request includes restricted parameter "%s", but this '.
131
'controller ("%s") does not whitelist it. Refusing to '.
132
'serve this request because it might be part of a redirection '.
133
'attack.',
134
$parameter,
135
get_class($this)));
136
}
137
}
138
}
139
140
if ($this->shouldRequireEnabledUser()) {
141
if ($user->getIsDisabled()) {
142
$controller = new PhabricatorDisabledUserController();
143
return $this->delegateToController($controller);
144
}
145
}
146
147
$auth_class = 'PhabricatorAuthApplication';
148
$auth_application = PhabricatorApplication::getByClass($auth_class);
149
150
// Require partial sessions to finish login before doing anything.
151
if (!$this->shouldAllowPartialSessions()) {
152
if ($user->hasSession() &&
153
$user->getSession()->getIsPartial()) {
154
$login_controller = new PhabricatorAuthFinishController();
155
$this->setCurrentApplication($auth_application);
156
return $this->delegateToController($login_controller);
157
}
158
}
159
160
// Require users sign Legalpad documents before we check if they have
161
// MFA. If we don't do this, they can get stuck in a state where they
162
// can't add MFA until they sign, and can't sign until they add MFA.
163
// See T13024 and PHI223.
164
$result = $this->requireLegalpadSignatures();
165
if ($result !== null) {
166
return $result;
167
}
168
169
// Check if the user needs to configure MFA.
170
$need_mfa = $this->shouldRequireMultiFactorEnrollment();
171
$have_mfa = $user->getIsEnrolledInMultiFactor();
172
if ($need_mfa && !$have_mfa) {
173
// Check if the cache is just out of date. Otherwise, roadblock the user
174
// and require MFA enrollment.
175
$user->updateMultiFactorEnrollment();
176
if (!$user->getIsEnrolledInMultiFactor()) {
177
$mfa_controller = new PhabricatorAuthNeedsMultiFactorController();
178
$this->setCurrentApplication($auth_application);
179
return $this->delegateToController($mfa_controller);
180
}
181
}
182
183
if ($this->shouldRequireLogin()) {
184
// This actually means we need either:
185
// - a valid user, or a public controller; and
186
// - permission to see the application; and
187
// - permission to see at least one Space if spaces are configured.
188
189
$allow_public = $this->shouldAllowPublic() &&
190
PhabricatorEnv::getEnvConfig('policy.allow-public');
191
192
// If this controller isn't public, and the user isn't logged in, require
193
// login.
194
if (!$allow_public && !$user->isLoggedIn()) {
195
$login_controller = new PhabricatorAuthStartController();
196
$this->setCurrentApplication($auth_application);
197
return $this->delegateToController($login_controller);
198
}
199
200
if ($user->isLoggedIn()) {
201
if ($this->shouldRequireEmailVerification()) {
202
if (!$user->getIsEmailVerified()) {
203
$controller = new PhabricatorMustVerifyEmailController();
204
$this->setCurrentApplication($auth_application);
205
return $this->delegateToController($controller);
206
}
207
}
208
}
209
210
// If Spaces are configured, require that the user have access to at
211
// least one. If we don't do this, they'll get confusing error messages
212
// later on.
213
$spaces = PhabricatorSpacesNamespaceQuery::getSpacesExist();
214
if ($spaces) {
215
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
216
$user);
217
if (!$viewer_spaces) {
218
$controller = new PhabricatorSpacesNoAccessController();
219
return $this->delegateToController($controller);
220
}
221
}
222
223
// If the user doesn't have access to the application, don't let them use
224
// any of its controllers. We query the application in order to generate
225
// a policy exception if the viewer doesn't have permission.
226
$application = $this->getCurrentApplication();
227
if ($application) {
228
id(new PhabricatorApplicationQuery())
229
->setViewer($user)
230
->withPHIDs(array($application->getPHID()))
231
->executeOne();
232
}
233
234
// If users need approval, require they wait here. We do this near the
235
// end so they can take other actions (like verifying email, signing
236
// documents, and enrolling in MFA) while waiting for an admin to take a
237
// look at things. See T13024 for more discussion.
238
if ($this->shouldRequireEnabledUser()) {
239
if ($user->isLoggedIn() && !$user->getIsApproved()) {
240
$controller = new PhabricatorAuthNeedsApprovalController();
241
return $this->delegateToController($controller);
242
}
243
}
244
}
245
246
// NOTE: We do this last so that users get a login page instead of a 403
247
// if they need to login.
248
if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) {
249
return new Aphront403Response();
250
}
251
}
252
253
public function getApplicationURI($path = '') {
254
if (!$this->getCurrentApplication()) {
255
throw new Exception(pht('No application!'));
256
}
257
return $this->getCurrentApplication()->getApplicationURI($path);
258
}
259
260
public function willSendResponse(AphrontResponse $response) {
261
$request = $this->getRequest();
262
263
if ($response instanceof AphrontDialogResponse) {
264
if (!$request->isAjax() && !$request->isQuicksand()) {
265
$dialog = $response->getDialog();
266
267
$title = $dialog->getTitle();
268
$short = $dialog->getShortTitle();
269
270
$crumbs = $this->buildApplicationCrumbs();
271
$crumbs->addTextCrumb(coalesce($short, $title));
272
273
$page_content = array(
274
$crumbs,
275
$response->buildResponseString(),
276
);
277
278
$view = id(new PhabricatorStandardPageView())
279
->setRequest($request)
280
->setController($this)
281
->setDeviceReady(true)
282
->setTitle($title)
283
->appendChild($page_content);
284
285
$response = id(new AphrontWebpageResponse())
286
->setContent($view->render())
287
->setHTTPResponseCode($response->getHTTPResponseCode());
288
} else {
289
$response->getDialog()->setIsStandalone(true);
290
291
return id(new AphrontAjaxResponse())
292
->setContent(array(
293
'dialog' => $response->buildResponseString(),
294
));
295
}
296
} else if ($response instanceof AphrontRedirectResponse) {
297
if ($request->isAjax() || $request->isQuicksand()) {
298
return id(new AphrontAjaxResponse())
299
->setContent(
300
array(
301
'redirect' => $response->getURI(),
302
'close' => $response->getCloseDialogBeforeRedirect(),
303
));
304
}
305
}
306
307
return $response;
308
}
309
310
/**
311
* WARNING: Do not call this in new code.
312
*
313
* @deprecated See "Handles Technical Documentation".
314
*/
315
protected function loadViewerHandles(array $phids) {
316
return id(new PhabricatorHandleQuery())
317
->setViewer($this->getRequest()->getUser())
318
->withPHIDs($phids)
319
->execute();
320
}
321
322
public function buildApplicationMenu() {
323
return null;
324
}
325
326
protected function buildApplicationCrumbs() {
327
$crumbs = array();
328
329
$application = $this->getCurrentApplication();
330
if ($application) {
331
$icon = $application->getIcon();
332
if (!$icon) {
333
$icon = 'fa-puzzle';
334
}
335
336
$crumbs[] = id(new PHUICrumbView())
337
->setHref($this->getApplicationURI())
338
->setName($application->getName())
339
->setIcon($icon);
340
}
341
342
$view = new PHUICrumbsView();
343
foreach ($crumbs as $crumb) {
344
$view->addCrumb($crumb);
345
}
346
347
return $view;
348
}
349
350
protected function hasApplicationCapability($capability) {
351
return PhabricatorPolicyFilter::hasCapability(
352
$this->getRequest()->getUser(),
353
$this->getCurrentApplication(),
354
$capability);
355
}
356
357
protected function requireApplicationCapability($capability) {
358
PhabricatorPolicyFilter::requireCapability(
359
$this->getRequest()->getUser(),
360
$this->getCurrentApplication(),
361
$capability);
362
}
363
364
protected function explainApplicationCapability(
365
$capability,
366
$positive_message,
367
$negative_message) {
368
369
$can_act = $this->hasApplicationCapability($capability);
370
if ($can_act) {
371
$message = $positive_message;
372
$icon_name = 'fa-play-circle-o lightgreytext';
373
} else {
374
$message = $negative_message;
375
$icon_name = 'fa-lock';
376
}
377
378
$icon = id(new PHUIIconView())
379
->setIcon($icon_name);
380
381
require_celerity_resource('policy-css');
382
383
$phid = $this->getCurrentApplication()->getPHID();
384
$explain_uri = "/policy/explain/{$phid}/{$capability}/";
385
386
$message = phutil_tag(
387
'div',
388
array(
389
'class' => 'policy-capability-explanation',
390
),
391
array(
392
$icon,
393
javelin_tag(
394
'a',
395
array(
396
'href' => $explain_uri,
397
'sigil' => 'workflow',
398
),
399
$message),
400
));
401
402
return array($can_act, $message);
403
}
404
405
public function getDefaultResourceSource() {
406
return 'phabricator';
407
}
408
409
/**
410
* Create a new @{class:AphrontDialogView} with defaults filled in.
411
*
412
* @return AphrontDialogView New dialog.
413
*/
414
public function newDialog() {
415
$submit_uri = new PhutilURI($this->getRequest()->getRequestURI());
416
$submit_uri = $submit_uri->getPath();
417
418
return id(new AphrontDialogView())
419
->setUser($this->getRequest()->getUser())
420
->setSubmitURI($submit_uri);
421
}
422
423
public function newRedirect() {
424
return id(new AphrontRedirectResponse());
425
}
426
427
public function newPage() {
428
$page = id(new PhabricatorStandardPageView())
429
->setRequest($this->getRequest())
430
->setController($this)
431
->setDeviceReady(true);
432
433
$application = $this->getCurrentApplication();
434
if ($application) {
435
$page->setApplicationName($application->getName());
436
if ($application->getTitleGlyph()) {
437
$page->setGlyph($application->getTitleGlyph());
438
}
439
}
440
441
$viewer = $this->getRequest()->getUser();
442
if ($viewer) {
443
$page->setUser($viewer);
444
}
445
446
return $page;
447
}
448
449
public function newApplicationMenu() {
450
return id(new PHUIApplicationMenuView())
451
->setViewer($this->getViewer());
452
}
453
454
public function newCurtainView($object = null) {
455
$viewer = $this->getViewer();
456
457
$action_id = celerity_generate_unique_node_id();
458
459
$action_list = id(new PhabricatorActionListView())
460
->setViewer($viewer)
461
->setID($action_id);
462
463
// NOTE: Applications (objects of class PhabricatorApplication) can't
464
// currently be set here, although they don't need any of the extensions
465
// anyway. This should probably work differently than it does, though.
466
if ($object) {
467
if ($object instanceof PhabricatorLiskDAO) {
468
$action_list->setObject($object);
469
}
470
}
471
472
$curtain = id(new PHUICurtainView())
473
->setViewer($viewer)
474
->setActionList($action_list);
475
476
if ($object) {
477
$panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object);
478
foreach ($panels as $panel) {
479
$curtain->addPanel($panel);
480
}
481
}
482
483
return $curtain;
484
}
485
486
protected function buildTransactionTimeline(
487
PhabricatorApplicationTransactionInterface $object,
488
PhabricatorApplicationTransactionQuery $query = null,
489
PhabricatorMarkupEngine $engine = null,
490
$view_data = array()) {
491
492
$request = $this->getRequest();
493
$viewer = $this->getViewer();
494
$xaction = $object->getApplicationTransactionTemplate();
495
496
if (!$query) {
497
$query = PhabricatorApplicationTransactionQuery::newQueryForObject(
498
$object);
499
if (!$query) {
500
throw new Exception(
501
pht(
502
'Unable to find transaction query for object of class "%s".',
503
get_class($object)));
504
}
505
}
506
507
$pager = id(new AphrontCursorPagerView())
508
->readFromRequest($request)
509
->setURI(new PhutilURI(
510
'/transactions/showolder/'.$object->getPHID().'/'));
511
512
$xactions = $query
513
->setViewer($viewer)
514
->withObjectPHIDs(array($object->getPHID()))
515
->needComments(true)
516
->executeWithCursorPager($pager);
517
$xactions = array_reverse($xactions);
518
519
$timeline_engine = PhabricatorTimelineEngine::newForObject($object)
520
->setViewer($viewer)
521
->setTransactions($xactions)
522
->setViewData($view_data);
523
524
$view = $timeline_engine->buildTimelineView();
525
526
if ($engine) {
527
foreach ($xactions as $xaction) {
528
if ($xaction->getComment()) {
529
$engine->addObject(
530
$xaction->getComment(),
531
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
532
}
533
}
534
$engine->process();
535
$view->setMarkupEngine($engine);
536
}
537
538
$timeline = $view
539
->setPager($pager)
540
->setQuoteTargetID($this->getRequest()->getStr('quoteTargetID'))
541
->setQuoteRef($this->getRequest()->getStr('quoteRef'));
542
543
return $timeline;
544
}
545
546
547
public function buildApplicationCrumbsForEditEngine() {
548
// TODO: This is kind of gross, I'm basically just making this public so
549
// I can use it in EditEngine. We could do this without making it public
550
// by using controller delegation, or make it properly public.
551
return $this->buildApplicationCrumbs();
552
}
553
554
private function requireLegalpadSignatures() {
555
if (!$this->shouldRequireLogin()) {
556
return null;
557
}
558
559
if ($this->shouldAllowLegallyNonCompliantUsers()) {
560
return null;
561
}
562
563
$viewer = $this->getViewer();
564
565
if (!$viewer->hasSession()) {
566
return null;
567
}
568
569
$session = $viewer->getSession();
570
if ($session->getIsPartial()) {
571
// If the user hasn't made it through MFA yet, require they survive
572
// MFA first.
573
return null;
574
}
575
576
if ($session->getSignedLegalpadDocuments()) {
577
return null;
578
}
579
580
if (!$viewer->isLoggedIn()) {
581
return null;
582
}
583
584
$must_sign_docs = array();
585
$sign_docs = array();
586
587
$legalpad_class = 'PhabricatorLegalpadApplication';
588
$legalpad_installed = PhabricatorApplication::isClassInstalledForViewer(
589
$legalpad_class,
590
$viewer);
591
if ($legalpad_installed) {
592
$sign_docs = id(new LegalpadDocumentQuery())
593
->setViewer($viewer)
594
->withSignatureRequired(1)
595
->needViewerSignatures(true)
596
->setOrder('oldest')
597
->execute();
598
599
foreach ($sign_docs as $sign_doc) {
600
if (!$sign_doc->getUserSignature($viewer->getPHID())) {
601
$must_sign_docs[] = $sign_doc;
602
}
603
}
604
}
605
606
if (!$must_sign_docs) {
607
// If nothing needs to be signed (either because there are no documents
608
// which require a signature, or because the user has already signed
609
// all of them) mark the session as good and continue.
610
$engine = id(new PhabricatorAuthSessionEngine())
611
->signLegalpadDocuments($viewer, $sign_docs);
612
613
return null;
614
}
615
616
$request = $this->getRequest();
617
$request->setURIMap(
618
array(
619
'id' => head($must_sign_docs)->getID(),
620
));
621
622
$application = PhabricatorApplication::getByClass($legalpad_class);
623
$this->setCurrentApplication($application);
624
625
$controller = new LegalpadDocumentSignController();
626
$controller->setIsSessionGate(true);
627
return $this->delegateToController($controller);
628
}
629
630
631
/* -( Deprecated )--------------------------------------------------------- */
632
633
634
/**
635
* DEPRECATED. Use @{method:newPage}.
636
*/
637
public function buildStandardPageView() {
638
return $this->newPage();
639
}
640
641
642
/**
643
* DEPRECATED. Use @{method:newPage}.
644
*/
645
public function buildStandardPageResponse($view, array $data) {
646
$page = $this->buildStandardPageView();
647
$page->appendChild($view);
648
return $page->produceAphrontResponse();
649
}
650
651
}
652
653