From 6b7183a76232cba02490585cdba1c3acbff56ca3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Jun 2015 14:04:48 -0700 Subject: [PATCH] Modularize Aphront "sites" Summary: Fixes T5702. The path here is long and windy: - I want to move `blog.phacility.com` to the new `secure` host. - That host has `security.require-https` set, which I want to keep set (before, this was handled in a sort of hacky way at the nginx/preamble level, but I've cleaned up everything else now). - Currently, that setting forces blogs to HTTPS too, which won't work. - To let blogs be individually configurable, we need to either modularize site config or make things hackier. - Modularize rather than increasing hackiness. - Also add a little "modules" panel in Config. See T6859. This feels like a reasonable middle ground between putting this stuff in Applications and burying it in `bin/somewhere`. Test Plan: - Visited normal site. - Visited phame on-domain site. - Visited phame off-domain site. - Viewed static resources. {F561897} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T5702 Differential Revision: https://secure.phabricator.com/D13474 --- src/__phutil_library_map__.php | 16 +++ .../AphrontApplicationConfiguration.php | 108 +++++------------- src/aphront/site/AphrontSite.php | 52 +++++++++ src/aphront/site/PhabricatorPlatformSite.php | 33 ++++++ src/aphront/site/PhabricatorResourceSite.php | 41 +++++++ src/aphront/site/PhabricatorSite.php | 9 ++ .../PhabricatorConfigApplication.php | 3 + .../PhabricatorConfigController.php | 2 + .../PhabricatorConfigSiteModuleController.php | 56 +++++++++ .../phame/site/PhameBlogResourceSite.php | 30 +++++ src/applications/phame/site/PhameBlogSite.php | 63 ++++++++++ src/applications/phame/site/PhameSite.php | 22 ++++ 12 files changed, 358 insertions(+), 77 deletions(-) create mode 100644 src/aphront/site/AphrontSite.php create mode 100644 src/aphront/site/PhabricatorPlatformSite.php create mode 100644 src/aphront/site/PhabricatorResourceSite.php create mode 100644 src/aphront/site/PhabricatorSite.php create mode 100644 src/applications/config/controller/PhabricatorConfigSiteModuleController.php create mode 100644 src/applications/phame/site/PhameBlogResourceSite.php create mode 100644 src/applications/phame/site/PhameBlogSite.php create mode 100644 src/applications/phame/site/PhameSite.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bafa05b6e4..05716a9e93 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -160,6 +160,7 @@ phutil_register_library_map(array( 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', + 'AphrontSite' => 'aphront/site/AphrontSite.php', 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', 'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', @@ -1656,6 +1657,7 @@ phutil_register_library_map(array( 'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php', 'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php', 'PhabricatorConfigServerSchema' => 'applications/config/schema/PhabricatorConfigServerSchema.php', + 'PhabricatorConfigSiteModuleController' => 'applications/config/controller/PhabricatorConfigSiteModuleController.php', 'PhabricatorConfigSiteSource' => 'infrastructure/env/PhabricatorConfigSiteSource.php', 'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php', 'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php', @@ -2325,6 +2327,7 @@ phutil_register_library_map(array( 'PhabricatorPhrequentConfigOptions' => 'applications/phrequent/config/PhabricatorPhrequentConfigOptions.php', 'PhabricatorPhrictionApplication' => 'applications/phriction/application/PhabricatorPhrictionApplication.php', 'PhabricatorPhrictionConfigOptions' => 'applications/phriction/config/PhabricatorPhrictionConfigOptions.php', + 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', 'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php', @@ -2522,6 +2525,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php', + 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSMS' => 'infrastructure/sms/storage/PhabricatorSMS.php', @@ -2611,6 +2615,7 @@ phutil_register_library_map(array( 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', 'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php', 'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php', + 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', 'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php', 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php', 'PhabricatorSlowvoteCloseController' => 'applications/slowvote/controller/PhabricatorSlowvoteCloseController.php', @@ -2889,6 +2894,8 @@ phutil_register_library_map(array( 'PhameBlogListController' => 'applications/phame/controller/blog/PhameBlogListController.php', 'PhameBlogLiveController' => 'applications/phame/controller/blog/PhameBlogLiveController.php', 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', + 'PhameBlogResourceSite' => 'applications/phame/site/PhameBlogResourceSite.php', + 'PhameBlogSite' => 'applications/phame/site/PhameBlogSite.php', 'PhameBlogSkin' => 'applications/phame/skins/PhameBlogSkin.php', 'PhameBlogTransaction' => 'applications/phame/storage/PhameBlogTransaction.php', 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', @@ -2917,6 +2924,7 @@ phutil_register_library_map(array( 'PhameQueryPostsConduitAPIMethod' => 'applications/phame/conduit/PhameQueryPostsConduitAPIMethod.php', 'PhameResourceController' => 'applications/phame/controller/PhameResourceController.php', 'PhameSchemaSpec' => 'applications/phame/storage/PhameSchemaSpec.php', + 'PhameSite' => 'applications/phame/site/PhameSite.php', 'PhameSkinSpecification' => 'applications/phame/skins/PhameSkinSpecification.php', 'PhluxController' => 'applications/phlux/controller/PhluxController.php', 'PhluxDAO' => 'applications/phlux/storage/PhluxDAO.php', @@ -3523,6 +3531,7 @@ phutil_register_library_map(array( 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontResponse' => 'Phobject', 'AphrontSideNavFilterView' => 'AphrontView', + 'AphrontSite' => 'Phobject', 'AphrontStackTraceView' => 'AphrontView', 'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse', 'AphrontTableView' => 'AphrontView', @@ -5263,6 +5272,7 @@ phutil_register_library_map(array( 'PhabricatorConfigSchemaQuery' => 'Phobject', 'PhabricatorConfigSchemaSpec' => 'Phobject', 'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema', + 'PhabricatorConfigSiteModuleController' => 'PhabricatorConfigController', 'PhabricatorConfigSiteSource' => 'PhabricatorConfigProxySource', 'PhabricatorConfigSource' => 'Phobject', 'PhabricatorConfigStackSource' => 'PhabricatorConfigSource', @@ -6022,6 +6032,7 @@ phutil_register_library_map(array( 'PhabricatorPhrequentConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPhrictionApplication' => 'PhabricatorApplication', 'PhabricatorPhrictionConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorPlatformSite' => 'PhabricatorSite', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', @@ -6290,6 +6301,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryVersion' => 'Phobject', + 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSMS' => 'PhabricatorSMSDAO', @@ -6381,6 +6393,7 @@ phutil_register_library_map(array( 'PhabricatorSetupIssue' => 'Phobject', 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', + 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvoteCloseController' => 'PhabricatorSlowvoteController', @@ -6705,6 +6718,8 @@ phutil_register_library_map(array( 'PhameBlogListController' => 'PhameController', 'PhameBlogLiveController' => 'PhameController', 'PhameBlogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhameBlogResourceSite' => 'PhameSite', + 'PhameBlogSite' => 'PhameSite', 'PhameBlogSkin' => 'PhabricatorController', 'PhameBlogTransaction' => 'PhabricatorApplicationTransaction', 'PhameBlogViewController' => 'PhameController', @@ -6739,6 +6754,7 @@ phutil_register_library_map(array( 'PhameQueryPostsConduitAPIMethod' => 'PhameConduitAPIMethod', 'PhameResourceController' => 'CelerityResourceController', 'PhameSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhameSite' => 'PhabricatorSite', 'PhameSkinSpecification' => 'Phobject', 'PhluxController' => 'PhabricatorController', 'PhluxDAO' => 'PhabricatorLiskDAO', diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 4ede4161a5..d215d2f67a 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -350,7 +350,9 @@ abstract class AphrontApplicationConfiguration extends Phobject { } } - if (PhabricatorEnv::getEnvConfig('security.require-https')) { + $site = $this->buildSiteForRequest($request); + + if ($site->shouldRequireHTTPS()) { if (!$request->isHTTPS()) { $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); @@ -362,82 +364,9 @@ abstract class AphrontApplicationConfiguration extends Phobject { } } - $path = $request->getPath(); - $host = $request->getHost(); - $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); - $prod_uri = PhabricatorEnv::getEnvConfig('phabricator.production-uri'); - $file_uri = PhabricatorEnv::getEnvConfig( - 'security.alternate-file-domain'); - $allowed_uris = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); - - $uris = array_merge( - array( - $base_uri, - $prod_uri, - ), - $allowed_uris); - - $cdn_routes = array( - '/res/', - '/file/data/', - '/file/xform/', - '/phame/r/', - ); - - $host_match = false; - foreach ($uris as $uri) { - if ($host === id(new PhutilURI($uri))->getDomain()) { - $host_match = true; - break; - } - } - - if (!$host_match) { - if ($host === id(new PhutilURI($file_uri))->getDomain()) { - foreach ($cdn_routes as $route) { - if (strncmp($path, $route, strlen($route)) == 0) { - $host_match = true; - break; - } - } - } - } - - // NOTE: If the base URI isn't defined yet, don't activate alternate - // domains. - if ($base_uri && !$host_match) { - - try { - $blog = id(new PhameBlogQuery()) - ->setViewer(new PhabricatorUser()) - ->withDomain($host) - ->executeOne(); - } catch (PhabricatorPolicyException $ex) { - throw new Exception( - pht( - 'This blog is not visible to logged out users, so it can not be '. - 'visited from a custom domain.')); - } - - if (!$blog) { - if ($prod_uri && $prod_uri != $base_uri) { - $prod_str = pht('%s or %s', $base_uri, $prod_uri); - } else { - $prod_str = $base_uri; - } - throw new Exception( - pht( - 'Specified domain %s is not configured for Phabricator '. - 'requests. Please use %s to visit this instance.', - $host, - $prod_str)); - } - - // TODO: Make this more flexible and modular so any application can - // do crazy stuff here if it wants. - - $path = '/phame/live/'.$blog->getID().'/'.$path; - } + // TODO: Really, the Site should get more control here and be able to + // do its own routing logic if it wants, but we don't need that for now. + $path = $site->getPathForRouting($request); list($controller, $uri_data) = $this->buildControllerForPath($path); if (!$controller) { @@ -509,4 +438,29 @@ abstract class AphrontApplicationConfiguration extends Phobject { return array($controller, $uri_data); } + private function buildSiteForRequest(AphrontRequest $request) { + $sites = PhabricatorSite::getAllSites(); + + $site = null; + foreach ($sites as $candidate) { + $site = $candidate->newSiteForRequest($request); + if ($site) { + break; + } + } + + if (!$site) { + $path = $request->getPath(); + $host = $request->getHost(); + throw new Exception( + pht( + 'This request asked for "%s" on host "%s", but no site is '. + 'configured which can serve this request.', + $path, + $host)); + } + + return $site; + } + } diff --git a/src/aphront/site/AphrontSite.php b/src/aphront/site/AphrontSite.php new file mode 100644 index 0000000000..8c2373565b --- /dev/null +++ b/src/aphront/site/AphrontSite.php @@ -0,0 +1,52 @@ +getPath(); + } + + protected function isHostMatch($host, array $uris) { + foreach ($uris as $uri) { + if (!strlen($uri)) { + continue; + } + + $domain = id(new PhutilURI($uri))->getDomain(); + + if ($domain === $host) { + return true; + } + } + + return false; + } + + protected function isPathPrefixMatch($path, array $paths) { + foreach ($paths as $candidate) { + if (strncmp($path, $candidate, strlen($candidate)) === 0) { + return true; + } + } + + return false; + } + + final public static function getAllSites() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setSortMethod('getPriority') + ->execute(); + } + +} diff --git a/src/aphront/site/PhabricatorPlatformSite.php b/src/aphront/site/PhabricatorPlatformSite.php new file mode 100644 index 0000000000..21e2cd2529 --- /dev/null +++ b/src/aphront/site/PhabricatorPlatformSite.php @@ -0,0 +1,33 @@ +getHost(); + if ($this->isHostMatch($host, $uris)) { + return new PhabricatorPlatformSite(); + } + + return null; + } + +} diff --git a/src/aphront/site/PhabricatorResourceSite.php b/src/aphront/site/PhabricatorResourceSite.php new file mode 100644 index 0000000000..006fcf99d7 --- /dev/null +++ b/src/aphront/site/PhabricatorResourceSite.php @@ -0,0 +1,41 @@ +getHost(); + + $uri = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); + if (!strlen($uri)) { + return null; + } + + if ($this->isHostMatch($host, array($uri))) { + return new PhabricatorResourceSite(); + } + + // These are CDN routes, so we let them through even if the "Host" header + // doesn't match anything we recognize. The + $whitelist = array( + '/res/', + '/file/data/', + '/file/xform/', + ); + + $path = $request->getPath(); + if ($this->isPathPrefixMatch($path, $whitelist)) { + return new PhabricatorResourceSite(); + } + + return null; + } + +} diff --git a/src/aphront/site/PhabricatorSite.php b/src/aphront/site/PhabricatorSite.php new file mode 100644 index 0000000000..866ca178ca --- /dev/null +++ b/src/aphront/site/PhabricatorSite.php @@ -0,0 +1,9 @@ +[^/]+)/' => 'PhabricatorConfigIssueViewController', ), 'cache/' => 'PhabricatorConfigCacheController', + 'module/' => array( + 'sites/' => 'PhabricatorConfigSiteModuleController', + ), ), ); } diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index 8dda5f8c31..1265c37a61 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -24,6 +24,8 @@ abstract class PhabricatorConfigController extends PhabricatorController { $nav->addFilter('cache/', pht('Cache Status')); $nav->addLabel(pht('Welcome')); $nav->addFilter('welcome/', pht('Welcome Screen')); + $nav->addLabel(pht('Modules')); + $nav->addFilter('module/sites/', pht('Sites')); return $nav; } diff --git a/src/applications/config/controller/PhabricatorConfigSiteModuleController.php b/src/applications/config/controller/PhabricatorConfigSiteModuleController.php new file mode 100644 index 0000000000..f2b79b2675 --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigSiteModuleController.php @@ -0,0 +1,56 @@ +getViewer(); + + $sites = AphrontSite::getAllSites(); + + $rows = array(); + foreach ($sites as $key => $site) { + $rows[] = array( + $site->getPriority(), + $key, + $site->getDescription(), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Priority'), + pht('Class'), + pht('Description'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + )); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Site Modules')) + ->appendChild($table); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Site Modules')); + + $nav = $this->buildSideNavView(); + $nav->selectFilter('module/sites/'); + $nav->appendChild( + array( + $crumbs, + $box, + )); + + return $this->buildApplicationPage( + $nav, + array( + 'title' => array(pht('Site Modules')), + )); + } + +} diff --git a/src/applications/phame/site/PhameBlogResourceSite.php b/src/applications/phame/site/PhameBlogResourceSite.php new file mode 100644 index 0000000000..8bf6094a77 --- /dev/null +++ b/src/applications/phame/site/PhameBlogResourceSite.php @@ -0,0 +1,30 @@ +isPhameActive()) { + return null; + } + + $whitelist = array( + '/phame/r/', + ); + + $path = $request->getPath(); + if (!$this->isPathPrefixMatch($path, $whitelist)) { + return null; + } + + return new PhameBlogResourceSite(); + } + +} diff --git a/src/applications/phame/site/PhameBlogSite.php b/src/applications/phame/site/PhameBlogSite.php new file mode 100644 index 0000000000..f4e0a62c20 --- /dev/null +++ b/src/applications/phame/site/PhameBlogSite.php @@ -0,0 +1,63 @@ +blog = $blog; + return $this; + } + + public function getBlog() { + return $this->blog; + } + + public function getDescription() { + return pht('Serves blogs with custom domains.'); + } + + public function shouldRequireHTTPS() { + // TODO: We should probably provide options here eventually, but for now + // just never require HTTPS for external-domain blogs. + return false; + } + + public function getPriority() { + return 4000; + } + + public function newSiteForRequest(AphrontRequest $request) { + if (!$this->isPhameActive()) { + return null; + } + + $host = $request->getHost(); + + try { + $blog = id(new PhameBlogQuery()) + ->setViewer(new PhabricatorUser()) + ->withDomain($host) + ->executeOne(); + } catch (PhabricatorPolicyException $ex) { + throw new Exception( + pht( + 'This blog is not visible to logged out users, so it can not be '. + 'visited from a custom domain.')); + } + + if (!$blog) { + return null; + } + + return id(new PhameBlogSite())->setBlog($blog); + } + + public function getPathForRouting(AphrontRequest $request) { + $path = $request->getPath(); + $id = $this->getBlog()->getID(); + + return "/phame/live/{$id}/{$path}"; + } + +} diff --git a/src/applications/phame/site/PhameSite.php b/src/applications/phame/site/PhameSite.php new file mode 100644 index 0000000000..57ac543f30 --- /dev/null +++ b/src/applications/phame/site/PhameSite.php @@ -0,0 +1,22 @@ +