diff --git a/scripts/aphront/aphrontpath.php b/scripts/aphront/aphrontpath.php index 07b9d8673..e768876ca 100755 --- a/scripts/aphront/aphrontpath.php +++ b/scripts/aphront/aphrontpath.php @@ -1,43 +1,42 @@ #!/usr/bin/env php <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; if ($argc !== 2 || $argv[1] === '--help') { echo "Usage: aphrontpath.php <url>\n"; echo "Purpose: Print controller which will process passed <url>.\n"; exit(1); } $url = parse_url($argv[1]); $path = '/'.(isset($url['path']) ? ltrim($url['path'], '/') : ''); $config_key = 'aphront.default-application-configuration-class'; -$config_class = PhabricatorEnv::getEnvConfig($config_key); -$application = newv($config_class, array()); +$application = PhabricatorEnv::newObjectFromConfig($config_key); $mapper = new AphrontURIMapper($application->getURIMap()); list($controller) = $mapper->mapPath($path); if (!$controller && $path[strlen($path) - 1] !== '/') { list($controller) = $mapper->mapPath($path.'/'); } if ($controller) { echo "$controller\n"; } diff --git a/src/applications/audit/editor/comment/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/comment/PhabricatorAuditCommentEditor.php index 9360a7657..dd127dcd4 100644 --- a/src/applications/audit/editor/comment/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/comment/PhabricatorAuditCommentEditor.php @@ -1,337 +1,336 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorAuditCommentEditor { private $commit; private $user; private $attachInlineComments; public function __construct(PhabricatorRepositoryCommit $commit) { $this->commit = $commit; return $this; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function setAttachInlineComments($attach_inline_comments) { $this->attachInlineComments = $attach_inline_comments; return $this; } public function addComment(PhabricatorAuditComment $comment) { $commit = $this->commit; $user = $this->user; $other_comments = id(new PhabricatorAuditComment())->loadAllWhere( 'targetPHID = %s', $commit->getPHID()); $inline_comments = array(); if ($this->attachInlineComments) { $inline_comments = id(new PhabricatorAuditInlineComment())->loadAllWhere( 'authorPHID = %s AND commitPHID = %s AND auditCommentID IS NULL', $user->getPHID(), $commit->getPHID()); } $comment ->setActorPHID($user->getPHID()) ->setTargetPHID($commit->getPHID()) ->save(); if ($inline_comments) { foreach ($inline_comments as $inline) { $inline->setAuditCommentID($comment->getID()); $inline->save(); } } // When a user submits an audit comment, we update all the audit requests // they have authority over to reflect the most recent status. The general // idea here is that if audit has triggered for, e.g., several packages, but // a user owns all of them, they can clear the audit requirement in one go // without auditing the commit for each trigger. $audit_phids = self::loadAuditPHIDsForUser($this->user); $audit_phids = array_fill_keys($audit_phids, true); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $action = $comment->getAction(); $status_map = PhabricatorAuditActionConstants::getStatusNameMap(); $status = idx($status_map, $action, null); // Status may be empty for updates which don't affect status, like // "comment". $have_any_requests = false; foreach ($requests as $request) { if (empty($audit_phids[$request->getAuditorPHID()])) { continue; } $have_any_requests = true; if ($status) { $request->setAuditStatus($status); $request->save(); } } if (!$have_any_requests) { // If the user has no current authority over any audit trigger, make a // new one to represent their audit state. $request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($user->getPHID()) ->setAuditStatus( $status ? $status : PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED) ->setAuditReasons(array("Voluntary Participant")) ->save(); $requests[] = $request; } $commit->updateAuditStatus($requests); $commit->save(); $this->publishFeedStory($comment, array_keys($audit_phids)); PhabricatorSearchCommitIndexer::indexCommit($commit); $this->sendMail($comment, $other_comments, $inline_comments); } /** * Load the PHIDs for all objects the user has the authority to act as an * audit for. This includes themselves, and any packages they are an owner * of. */ public static function loadAuditPHIDsForUser(PhabricatorUser $user) { $phids = array(); // The user can audit on their own behalf. $phids[$user->getPHID()] = true; // The user can audit on behalf of all packages they own. $owned_packages = id(new PhabricatorOwnersOwner())->loadAllWhere( 'userPHID = %s', $user->getPHID()); if ($owned_packages) { $packages = id(new PhabricatorOwnersPackage())->loadAllWhere( 'id IN (%Ld)', mpull($owned_packages, 'getPackageID')); foreach (mpull($packages, 'getPHID') as $phid) { $phids[$phid] = true; } } // The user can audit on behalf of all projects they are a member of. $query = new PhabricatorProjectQuery(); $query->setMembers(array($user->getPHID())); $projects = $query->execute(); foreach ($projects as $project) { $phids[$project->getPHID()] = true; } return array_keys($phids); } private function publishFeedStory( PhabricatorAuditComment $comment, array $more_phids) { $commit = $this->commit; $user = $this->user; $related_phids = array_merge( array( $user->getPHID(), $commit->getPHID(), ), $more_phids); id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) ->setStoryAuthorPHID($user->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_AUDIT) ->setStoryData( array( 'commitPHID' => $commit->getPHID(), 'action' => $comment->getAction(), 'content' => $comment->getContent(), )) ->publish(); } private function sendMail( PhabricatorAuditComment $comment, array $other_comments, array $inline_comments) { $commit = $this->commit; $data = $commit->loadCommitData(); $summary = $data->getSummary(); $commit_phid = $commit->getPHID(); $phids = array($commit_phid); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $handle = $handles[$commit_phid]; $name = $handle->getName(); $map = array( PhabricatorAuditActionConstants::CONCERN => 'Raised Concern', PhabricatorAuditActionConstants::ACCEPT => 'Accepted', ); $verb = idx($map, $comment->getAction(), 'Commented On'); $reply_handler = self::newReplyHandlerForCommit($commit); $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $subject = "{$prefix} [{$verb}] {$name}: {$summary}"; $threading = self::getMailThreading($commit->getPHID()); list($thread_id, $thread_topic) = $threading; $is_new = !count($other_comments); $body = $this->renderMailBody( $comment, "{$name}: {$summary}", $handle, $reply_handler, $inline_comments); $email_to = array(); $author_phid = $data->getCommitDetail('authorPHID'); if ($author_phid) { $email_to[] = $author_phid; } $email_cc = array(); foreach ($other_comments as $comment) { $email_cc[] = $comment->getActorPHID(); } $phids = array_merge($email_to, $email_cc); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->setFrom($comment->getActorPHID()) ->setThreadID($thread_id, $is_new) ->addHeader('Thread-Topic', $thread_topic) ->setRelatedPHID($commit->getPHID()) ->setIsBulk(true) ->setBody($body); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } } public static function getMailThreading($phid) { return array( '<diffusion-audit-'.$phid.'>', 'Diffusion Audit '.$phid, ); } public static function newReplyHandlerForCommit($commit) { - $handler_class = PhabricatorEnv::getEnvConfig( + $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.diffusion.reply-handler'); - $reply_handler = newv($handler_class, array()); $reply_handler->setMailReceiver($commit); return $reply_handler; } private function renderMailBody( PhabricatorAuditComment $comment, $cname, PhabricatorObjectHandle $handle, PhabricatorMailReplyHandler $reply_handler, array $inline_comments) { $commit = $this->commit; $user = $this->user; $name = $user->getUsername(); $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb( $comment->getAction()); $body = array(); $body[] = "{$name} {$verb} commit {$cname}."; if ($comment->getContent()) { $body[] = $comment->getContent(); } if ($inline_comments) { $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inline_comments, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inline_comments as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } $body[] = "INLINE COMMENTS\n ".implode("\n ", $block); } $body[] = "COMMIT\n ".PhabricatorEnv::getProductionURI($handle->getURI()); $reply_instructions = $reply_handler->getReplyHandlerInstructions(); if ($reply_instructions) { $body[] = "REPLY HANDLER ACTIONS\n ".$reply_instructions; } return implode("\n\n", $body)."\n"; } } diff --git a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php index 3184ee194..9f7553158 100644 --- a/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php +++ b/src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php @@ -1,342 +1,341 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorOAuthLoginController extends PhabricatorAuthController { private $provider; private $userID; private $accessToken; private $tokenExpires; private $oauthState; public function shouldRequireLogin() { return false; } public function willProcessRequest(array $data) { $this->provider = PhabricatorOAuthProvider::newProvider($data['provider']); } public function processRequest() { $current_user = $this->getRequest()->getUser(); $provider = $this->provider; if (!$provider->isProviderEnabled()) { return new Aphront400Response(); } $provider_name = $provider->getProviderName(); $provider_key = $provider->getProviderKey(); $request = $this->getRequest(); if ($request->getStr('error')) { $error_view = id(new PhabricatorOAuthFailureView()) ->setRequest($request); return $this->buildErrorResponse($error_view); } $error_response = $this->retrieveAccessToken($provider); if ($error_response) { return $error_response; } $userinfo_uri = new PhutilURI($provider->getUserInfoURI()); $userinfo_uri->setQueryParams( array( 'access_token' => $this->accessToken, )); $user_data = @file_get_contents($userinfo_uri); $provider->setUserData($user_data); $provider->setAccessToken($this->accessToken); $user_id = $provider->retrieveUserID(); $provider_key = $provider->getProviderKey(); $oauth_info = $this->retrieveOAuthInfo($provider); if ($current_user->getPHID()) { if ($oauth_info->getID()) { if ($oauth_info->getUserID() != $current_user->getID()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to Another Account'); $dialog->appendChild( hsprintf( '<p>The %s account you just authorized is already linked to '. 'another Phabricator account. Before you can associate your %s '. 'account with this Phabriactor account, you must unlink it from '. 'the Phabricator account it is currently linked to.</p>', $provider_name, $provider_name)); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } else { return id(new AphrontRedirectResponse()) ->setURI('/settings/page/'.$provider_key.'/'); } } $existing_oauth = id(new PhabricatorUserOAuthInfo())->loadOneWhere( 'userID = %d AND oauthProvider = %s', $current_user->getID(), $provider_key); if ($existing_oauth) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to an Account From This Provider'); $dialog->appendChild( hsprintf( '<p>The account you are logged in with is already linked to a %s '. 'account. Before you can link it to a different %s account, you '. 'must unlink the old account.</p>', $provider_name, $provider_name)); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Link '.$provider_name.' Account'); $dialog->appendChild( hsprintf( '<p>Link your %s account to your Phabricator account?</p>', $provider_name)); $dialog->addHiddenInput('token', $provider->getAccessToken()); $dialog->addHiddenInput('expires', $oauth_info->getTokenExpires()); $dialog->addHiddenInput('state', $this->oauthState); $dialog->addSubmitButton('Link Accounts'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } $oauth_info->setUserID($current_user->getID()); $this->saveOAuthInfo($oauth_info); return id(new AphrontRedirectResponse()) ->setURI('/settings/page/'.$provider_key.'/'); } // Login with known auth. if ($oauth_info->getID()) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $known_user = id(new PhabricatorUser())->load($oauth_info->getUserID()); $request->getApplicationConfiguration()->willAuthenticateUserWithOAuth( $known_user, $oauth_info, $provider); $session_key = $known_user->establishSession('web'); $this->saveOAuthInfo($oauth_info); $request->setCookie('phusr', $known_user->getUsername()); $request->setCookie('phsid', $session_key); $uri = new PhutilURI('/login/validate/'); $uri->setQueryParams( array( 'phusr' => $known_user->getUsername(), )); return id(new AphrontRedirectResponse())->setURI((string)$uri); } $oauth_email = $provider->retrieveUserEmail(); if ($oauth_email) { $known_email = id(new PhabricatorUser()) ->loadOneWhere('email = %s', $oauth_email); if ($known_email) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('Already Linked to Another Account'); $dialog->appendChild( hsprintf( '<p>The %s account you just authorized has an email address which '. 'is already in use by another Phabricator account. To link the '. 'accounts, log in to your Phabricator account and then go to '. 'Settings.</p>', $provider_name)); $dialog->addCancelButton('/login/'); return id(new AphrontDialogResponse())->setDialog($dialog); } } if (!$provider->isProviderRegistrationEnabled()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); $dialog->setTitle('No Account Registration With '.$provider_name); $dialog->appendChild( hsprintf( '<p>You can not register a new account using %s; you can only use '. 'your %s account to log into an existing Phabricator account which '. 'you have registered through other means.</p>', $provider_name, $provider_name)); $dialog->addCancelButton('/login/'); return id(new AphrontDialogResponse())->setDialog($dialog); } - $class = PhabricatorEnv::getEnvConfig('controller.oauth-registration'); - PhutilSymbolLoader::loadClass($class); - $controller = newv($class, array($this->getRequest())); + $key = 'controller.oauth-registration'; + $controller = PhabricatorEnv::newObjectFromConfig($key); $controller->setOAuthProvider($provider); $controller->setOAuthInfo($oauth_info); $controller->setOAuthState($this->oauthState); return $this->delegateToController($controller); } private function buildErrorResponse(PhabricatorOAuthFailureView $view) { $provider = $this->provider; $provider_name = $provider->getProviderName(); $view->setOAuthProvider($provider); return $this->buildStandardPageResponse( $view, array( 'title' => $provider_name.' Auth Failed', )); } private function retrieveAccessToken(PhabricatorOAuthProvider $provider) { $request = $this->getRequest(); $token = $request->getStr('token'); if ($token) { $this->tokenExpires = $request->getInt('expires'); $this->accessToken = $token; $this->oauthState = $request->getStr('state'); return null; } $client_id = $provider->getClientID(); $client_secret = $provider->getClientSecret(); $redirect_uri = $provider->getRedirectURI(); $auth_uri = $provider->getTokenURI(); $code = $request->getStr('code'); $query_data = array( 'client_id' => $client_id, 'client_secret' => $client_secret, 'redirect_uri' => $redirect_uri, 'code' => $code, ) + $provider->getExtraTokenParameters(); $post_data = http_build_query($query_data); $post_length = strlen($post_data); $stream_context = stream_context_create( array( 'http' => array( 'method' => 'POST', 'header' => "Content-Type: application/x-www-form-urlencoded\r\n". "Content-Length: {$post_length}\r\n", 'content' => $post_data, ), )); $stream = fopen($auth_uri, 'r', false, $stream_context); $response = false; $meta = null; if ($stream) { $meta = stream_get_meta_data($stream); $response = stream_get_contents($stream); fclose($stream); } if ($response === false) { return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); } $data = $provider->decodeTokenResponse($response); $token = idx($data, 'access_token'); if (!$token) { return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); } if (idx($data, 'expires')) { $this->tokenExpires = time() + $data['expires']; } $this->accessToken = $token; $this->oauthState = $request->getStr('state'); return null; } private function retrieveOAuthInfo(PhabricatorOAuthProvider $provider) { $oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere( 'oauthProvider = %s and oauthUID = %s', $provider->getProviderKey(), $provider->retrieveUserID()); if (!$oauth_info) { $oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info->setOAuthProvider($provider->getProviderKey()); $oauth_info->setOAuthUID($provider->retrieveUserID()); } $oauth_info->setAccountURI($provider->retrieveUserAccountURI()); $oauth_info->setAccountName($provider->retrieveUserAccountName()); $oauth_info->setToken($provider->getAccessToken()); $oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD); // If we have out-of-date expiration info, just clear it out. Then replace // it with good info if the provider gave it to us. $expires = $oauth_info->getTokenExpires(); if ($expires <= time()) { $expires = null; } if ($this->tokenExpires) { $expires = $this->tokenExpires; } $oauth_info->setTokenExpires($expires); return $oauth_info; } private function saveOAuthInfo(PhabricatorUserOAuthInfo $info) { // UNGUARDED WRITES: Logging-in users don't have their CSRF set up yet. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $info->save(); } } diff --git a/src/applications/auth/controller/oauth/__init__.php b/src/applications/auth/controller/oauth/__init__.php index f0c175921..356e74607 100644 --- a/src/applications/auth/controller/oauth/__init__.php +++ b/src/applications/auth/controller/oauth/__init__.php @@ -1,27 +1,26 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'aphront/response/400'); phutil_require_module('phabricator', 'aphront/response/dialog'); phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'aphront/writeguard'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); phutil_require_module('phabricator', 'applications/auth/view/oauthfailure'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/dialog'); phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'parser/uri'); -phutil_require_module('phutil', 'symbols'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorOAuthLoginController.php'); diff --git a/src/applications/differential/field/selector/base/DifferentialFieldSelector.php b/src/applications/differential/field/selector/base/DifferentialFieldSelector.php index 7b58e9241..320dfe3f3 100644 --- a/src/applications/differential/field/selector/base/DifferentialFieldSelector.php +++ b/src/applications/differential/field/selector/base/DifferentialFieldSelector.php @@ -1,37 +1,35 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ abstract class DifferentialFieldSelector { final public function __construct() { // <empty> } final public static function newSelector() { - $class = PhabricatorEnv::getEnvConfig('differential.field-selector'); - $selector = newv($class, array()); - return $selector; + return PhabricatorEnv::newObjectFromConfig('differential.field-selector'); } abstract public function getFieldSpecifications(); public function sortFieldsForRevisionList(array $fields) { return $fields; } } diff --git a/src/applications/differential/field/selector/base/__init__.php b/src/applications/differential/field/selector/base/__init__.php index 09f7eb4cf..6c8eeefe6 100644 --- a/src/applications/differential/field/selector/base/__init__.php +++ b/src/applications/differential/field/selector/base/__init__.php @@ -1,14 +1,12 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phutil', 'utils'); - phutil_require_source('DifferentialFieldSelector.php'); diff --git a/src/applications/files/storage/file/PhabricatorFile.php b/src/applications/files/storage/file/PhabricatorFile.php index adc35c4ed..b94daf247 100644 --- a/src/applications/files/storage/file/PhabricatorFile.php +++ b/src/applications/files/storage/file/PhabricatorFile.php @@ -1,359 +1,358 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ final class PhabricatorFile extends PhabricatorFileDAO { const STORAGE_FORMAT_RAW = 'raw'; protected $phid; protected $name; protected $mimeType; protected $byteSize; protected $authorPHID; protected $secretKey; protected $contentHash; protected $storageEngine; protected $storageFormat; protected $storageHandle; public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_FILE); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception("No file was uploaded!"); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception("File is not an uploaded file."); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception("File size disagrees with uploaded size."); } return $file_data; } public static function newFromPHPUpload($spec, array $params = array()) { $file_data = self::readUploadedFileData($spec); $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromFileData($data, array $params = array()) { - $selector_class = PhabricatorEnv::getEnvConfig('storage.engine-selector'); - $selector = newv($selector_class, array()); + $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); $engines = $selector->selectStorageEngines($data, $params); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $data_handle = null; $engine_identifier = null; foreach ($engines as $engine) { try { // Perform the actual write. $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new Exception( "Storage engine '{$engine}' executed writeFile() but did not ". "return a valid handle ('{$data_handle}') to the data: it must ". "be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new Exception( "Storage engine '{$engine}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } // We stored the file somewhere so stop trying to write it to other // places. break; } catch (Exception $ex) { // If an engine doesn't work, keep trying all the other valid engines // in case something else works. phlog($ex); } } if (!$data_handle) { throw new Exception("All storage engines failed to write file!"); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file = new PhabricatorFile(); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); $file->setContentHash(PhabricatorHash::digest($data)); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); if (isset($params['mime-type'])) { $file->setMimeType($params['mime-type']); } else { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } $file->save(); return $file; } public static function newFromFileDownload($uri, $name) { $uri = new PhutilURI($uri); $protocol = $uri->getProtocol(); switch ($protocol) { case 'http': case 'https': break; default: // Make sure we are not accessing any file:// URIs or similar. return null; } $timeout = stream_context_create( array( 'http' => array( 'timeout' => 5, ), )); $file_data = @file_get_contents($uri, false, $timeout); if ($file_data === false) { return null; } return self::newFromFileData($file_data, array('name' => $name)); } public static function normalizeFileName($file_name) { return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name); } public function delete() { $engine = $this->instantiateStorageEngine(); $ret = parent::delete(); $engine->deleteFile($this->getStorageHandle()); return $ret; } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception("Unknown storage format."); } return $data; } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( "You must save a file before you can generate a view URI."); } $name = phutil_escape_uri($this->getName()); $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name; return PhabricatorEnv::getCDNURI($path); } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getThumb60x45URI() { return '/file/xform/thumb-60x45/'.$this->getPHID().'/'; } public function getThumb160x120URI() { return '/file/xform/thumb-160x120/'.$this->getPHID().'/'; } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isViewableImage() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception('Unknown type matched as image MIME type.'); } } public static function getTransformableImageFormats() { $supported = array(); if (function_exists('imagejpeg')) { $supported[] = 'jpg'; } if (function_exists('imagepng')) { $supported[] = 'png'; } if (function_exists('imagegif')) { $supported[] = 'gif'; } return $supported; } protected function instantiateStorageEngine() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); foreach ($engines as $engine_class) { $engine = newv($engine_class['name'], array()); if ($engine->getEngineIdentifier() == $this->getStorageEngine()) { return $engine; } } throw new Exception("File's storage engine could be located!"); } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } return parent::save(); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } } diff --git a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php index daf6227a4..7db888576 100644 --- a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php @@ -1,388 +1,386 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @group maniphest */ final class ManiphestTransactionEditor { private $parentMessageID; private $auxiliaryFields = array(); public function setAuxiliaryFields(array $fields) { $this->auxiliaryFields = $fields; return $this; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function applyTransactions($task, array $transactions) { $email_cc = $task->getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); $value_is_phid_set = false; switch ($type) { case ManiphestTransactionType::TYPE_NONE: $old = null; break; case ManiphestTransactionType::TYPE_STATUS: $old = $task->getStatus(); break; case ManiphestTransactionType::TYPE_OWNER: $old = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_CCS: $old = $task->getCCPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_PRIORITY: $old = $task->getPriority(); break; case ManiphestTransactionType::TYPE_ATTACH: $old = $task->getAttached(); break; case ManiphestTransactionType::TYPE_TITLE: $old = $task->getTitle(); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $old = $task->getDescription(); break; case ManiphestTransactionType::TYPE_PROJECTS: $old = $task->getProjectPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); if (!$aux_key) { throw new Exception( "Expected 'aux:key' metadata on TYPE_AUXILIARY transaction."); } $old = $task->getAuxiliaryAttribute($aux_key); break; default: throw new Exception('Unknown action type.'); } $old_cmp = $old; $new_cmp = $new; if ($value_is_phid_set) { // Normalize the old and new values if they are PHID sets so we don't // get any no-op transactions where the values differ only by keys, // order, duplicates, etc. if (is_array($old)) { $old = array_filter($old); $old = array_unique($old); sort($old); $old = array_values($old); $old_cmp = $old; } if (is_array($new)) { $new = array_filter($new); $new = array_unique($new); $transaction->setNewValue($new); $new_cmp = $new; sort($new_cmp); $new_cmp = array_values($new_cmp); } } if (($old !== null) && ($old_cmp == $new_cmp)) { if (count($transactions) > 1 && !$transaction->hasComments()) { // If we have at least one other transaction and this one isn't // doing anything and doesn't have any comments, just throw it // away. unset($transactions[$key]); continue; } else { $transaction->setOldValue(null); $transaction->setNewValue(null); $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE); } } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: break; case ManiphestTransactionType::TYPE_STATUS: $task->setStatus($new); break; case ManiphestTransactionType::TYPE_OWNER: if ($new) { $handles = id(new PhabricatorObjectHandleData(array($new))) ->loadHandles(); $task->setOwnerOrdering($handles[$new]->getName()); } else { $task->setOwnerOrdering(null); } $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); break; case ManiphestTransactionType::TYPE_ATTACH: $task->setAttached($new); break; case ManiphestTransactionType::TYPE_TITLE: $task->setTitle($new); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $task->setDescription($new); break; case ManiphestTransactionType::TYPE_PROJECTS: $task->setProjectPHIDs($new); break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); $task->setAuxiliaryAttribute($aux_key, $new); break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } $task->save(); foreach ($transactions as $transaction) { $transaction->setTaskID($task->getID()); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); $this->publishFeedStory($task, $transactions); // TODO: Do this offline via timeline PhabricatorSearchManiphestIndexer::indexTask($task); $this->sendEmail($task, $transactions, $email_to, $email_cc); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } private function sendEmail($task, $transactions, $email_to, $email_cc) { $email_to = array_filter(array_unique($email_to)); $email_cc = array_filter(array_unique($email_cc)); $phids = array(); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } foreach ($email_to as $phid) { $phids[$phid] = true; } foreach ($email_cc as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($transactions); $view->setHandles($handles); $view->setAuxiliaryFields($this->auxiliaryFields); list($action, $body) = $view->renderForEmail($with_date = false); $is_create = $this->isCreate($transactions); $task_uri = PhabricatorEnv::getURI('/T'.$task->getID()); $reply_handler = $this->buildReplyHandler($task); if ($is_create) { $body .= "\n\n". "TASK DESCRIPTION\n". " ".$task->getDescription(); } $body .= "\n\n". "TASK DETAIL\n". " ".$task_uri."\n"; $reply_instructions = $reply_handler->getReplyHandlerInstructions(); if ($reply_instructions) { $body .= "\n". "REPLY HANDLER ACTIONS\n". " ".$reply_instructions."\n"; } $thread_id = '<maniphest-task-'.$task->getPHID().'>'; $task_id = $task->getID(); $title = $task->getTitle(); $prefix = $this->getSubjectPrefix(); $subject = trim("{$prefix} [{$action}] T{$task_id}: {$title}"); $mailtags = $this->getMailTags($transactions); $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->setFrom($transaction->getAuthorPHID()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID()) ->setThreadID($thread_id, $is_create) ->setRelatedPHID($task->getPHID()) ->setIsBulk(true) ->setMailTags($mailtags) ->setBody($body); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } } public function buildReplyHandler(ManiphestTask $task) { - $handler_class = PhabricatorEnv::getEnvConfig( + $handler_object = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); - - $handler_object = newv($handler_class, array()); $handler_object->setMailReceiver($task); return $handler_object; } private function publishFeedStory(ManiphestTask $task, array $transactions) { $actions = array(ManiphestAction::ACTION_UPDATE); $comments = null; foreach ($transactions as $transaction) { if ($transaction->hasComments()) { $comments = $transaction->getComments(); } switch ($transaction->getTransactionType()) { case ManiphestTransactionType::TYPE_OWNER: $actions[] = ManiphestAction::ACTION_ASSIGN; break; case ManiphestTransactionType::TYPE_STATUS: if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $actions[] = ManiphestAction::ACTION_CLOSE; } else if ($this->isCreate($transactions)) { $actions[] = ManiphestAction::ACTION_CREATE; } break; default: break; } } $action_type = ManiphestAction::selectStrongestAction($actions); $owner_phid = $task->getOwnerPHID(); $actor_phid = head($transactions)->getAuthorPHID(); $author_phid = $task->getAuthorPHID(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_MANIPHEST) ->setStoryData(array( 'taskPHID' => $task->getPHID(), 'transactionIDs' => mpull($transactions, 'getID'), 'ownerPHID' => $owner_phid, 'action' => $action_type, 'comments' => $comments, 'description' => $task->getDescription(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array_merge( array_filter( array( $task->getPHID(), $author_phid, $actor_phid, $owner_phid, )), $task->getProjectPHIDs())) ->publish(); } private function isCreate(array $transactions) { $is_create = false; foreach ($transactions as $transaction) { $type = $transaction->getTransactionType(); if (($type == ManiphestTransactionType::TYPE_STATUS) && ($transaction->getOldValue() === null) && ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) { $is_create = true; } } return $is_create; } private function getMailTags(array $transactions) { $tags = array(); foreach ($transactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransactionType::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case ManiphestTransactionType::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case ManiphestTransactionType::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } if ($xaction->hasComments()) { $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; } } return array_unique($tags); } } diff --git a/src/applications/maniphest/extensions/base/ManiphestTaskExtensions.php b/src/applications/maniphest/extensions/base/ManiphestTaskExtensions.php index 7fd8fb690..2b6429972 100644 --- a/src/applications/maniphest/extensions/base/ManiphestTaskExtensions.php +++ b/src/applications/maniphest/extensions/base/ManiphestTaskExtensions.php @@ -1,37 +1,36 @@ <?php /* - * Copyright 2011 Facebook, Inc. + * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @group maniphest */ abstract class ManiphestTaskExtensions { final public function __construct() { // <empty> } abstract public function getAuxiliaryFieldSpecifications(); final public static function newExtensions() { $key = 'maniphest.custom-task-extensions-class'; - $class = PhabricatorEnv::getEnvConfig($key); - return newv($class, array()); + return PhabricatorEnv::newObjectFromConfig($key); } } diff --git a/src/applications/maniphest/extensions/base/__init__.php b/src/applications/maniphest/extensions/base/__init__.php index 500e40e1a..5791da192 100644 --- a/src/applications/maniphest/extensions/base/__init__.php +++ b/src/applications/maniphest/extensions/base/__init__.php @@ -1,14 +1,12 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phutil', 'utils'); - phutil_require_source('ManiphestTaskExtensions.php'); diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php index c38eca13f..b5193031e 100644 --- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php @@ -1,637 +1,635 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * See #394445 for an explanation of why this thing even exists. */ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { const STATUS_QUEUE = 'queued'; const STATUS_SENT = 'sent'; const STATUS_FAIL = 'fail'; const STATUS_VOID = 'void'; const MAX_RETRIES = 250; const RETRY_DELAY = 5; protected $parameters; protected $status; protected $message; protected $retryCount; protected $nextRetry; protected $relatedPHID; public function __construct() { $this->status = self::STATUS_QUEUE; $this->retryCount = 0; $this->nextRetry = time(); $this->parameters = array(); parent::__construct(); } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } protected function setParam($param, $value) { $this->parameters[$param] = $value; return $this; } protected function getParam($param) { return idx($this->parameters, $param); } /** * Set tags (@{class:MetaMTANotificationType} constants) which identify the * content of this mail in a general way. These tags are used to allow users * to opt out of receiving certain types of mail, like updates when a task's * projects change. * * @param list<const> List of @{class:MetaMTANotificationType} constants. * @return this */ public function setMailTags(array $tags) { $this->setParam('mailtags', $tags); return $this; } /** * In Gmail, conversations will be broken if you reply to a thread and the * server sends back a response without referencing your Message-ID, even if * it references a Message-ID earlier in the thread. To avoid this, use the * parent email's message ID explicitly if it's available. This overwrites the * "In-Reply-To" and "References" headers we would otherwise generate. This * needs to be set whenever an action is triggered by an email message. See * T251 for more details. * * @param string The "Message-ID" of the email which precedes this one. * @return this */ public function setParentMessageID($id) { $this->setParam('parent-message-id', $id); return $this; } public function getParentMessageID() { return $this->getParam('parent-message-id'); } public function getSubject() { return $this->getParam('subject'); } public function addTos(array $phids) { $phids = array_unique($phids); $this->setParam('to', $phids); return $this; } public function addCCs(array $phids) { $phids = array_unique($phids); $this->setParam('cc', $phids); return $this; } public function addHeader($name, $value) { $this->parameters['headers'][$name] = $value; return $this; } public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->parameters['attachments'][] = $attachment; return $this; } public function getAttachments() { return $this->getParam('attachments'); } public function setAttachments(array $attachments) { $this->setParam('attachments', $attachments); return $this; } public function setFrom($from) { $this->setParam('from', $from); return $this; } public function setReplyTo($reply_to) { $this->setParam('reply-to', $reply_to); return $this; } public function setSubject($subject) { $this->setParam('subject', $subject); return $this; } public function setBody($body) { $this->setParam('body', $body); return $this; } public function getBody() { return $this->getParam('body'); } public function setIsHTML($html) { $this->setParam('is-html', $html); return $this; } public function getSimulatedFailureCount() { return nonempty($this->getParam('simulated-failures'), 0); } public function setSimulatedFailureCount($count) { $this->setParam('simulated-failures', $count); return $this; } public function getWorkerTaskID() { return $this->getParam('worker-task'); } public function setWorkerTaskID($id) { $this->setParam('worker-task', $id); return $this; } /** * Flag that this is an auto-generated bulk message and should have bulk * headers added to it if appropriate. Broadly, this means some flavor of * "Precedence: bulk" or similar, but is implementation and configuration * dependent. * * @param bool True if the mail is automated bulk mail. * @return this */ public function setIsBulk($is_bulk) { $this->setParam('is-bulk', $is_bulk); return $this; } /** * Use this method to set an ID used for message threading. MetaMTA will * set appropriate headers (Message-ID, In-Reply-To, References and * Thread-Index) based on the capabilities of the underlying mailer. * * @param string Unique identifier, appropriate for use in a Message-ID, * In-Reply-To or References headers. * @param bool If true, indicates this is the first message in the thread. * @return this */ public function setThreadID($thread_id, $is_first_message = false) { $this->setParam('thread-id', $thread_id); $this->setParam('is-first-message', $is_first_message); return $this; } /** * Save a newly created mail to the database and attempt to send it * immediately if the server is configured for immediate sends. When * applications generate new mail they should generally use this method to * deliver it. If the server doesn't use immediate sends, this has the same * effect as calling save(): the mail will eventually be delivered by the * MetaMTA daemon. * * @return this */ public function saveAndSend() { $ret = null; if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) { $ret = $this->sendNow(); } else { $ret = $this->save(); } return $ret; } protected function didWriteData() { parent::didWriteData(); if (!$this->getWorkerTaskID()) { $mailer_task = new PhabricatorWorkerTask(); $mailer_task->setTaskClass('PhabricatorMetaMTAWorker'); $mailer_task->setData($this->getID()); $mailer_task->save(); $this->setWorkerTaskID($mailer_task->getID()); $this->save(); } } public function buildDefaultMailer() { - $class_name = PhabricatorEnv::getEnvConfig('metamta.mail-adapter'); - PhutilSymbolLoader::loadClass($class_name); - return newv($class_name, array()); + return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); } /** * Attempt to deliver an email immediately, in this process. * * @param bool Try to deliver this email even if it has already been * delivered or is in backoff after a failed delivery attempt. * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, * instead of the default. * * @return void */ public function sendNow( $force_send = false, PhabricatorMailImplementationAdapter $mailer = null) { if ($mailer === null) { $mailer = $this->buildDefaultMailer(); } if (!$force_send) { if ($this->getStatus() != self::STATUS_QUEUE) { throw new Exception("Trying to send an already-sent mail!"); } if (time() < $this->getNextRetry()) { throw new Exception("Trying to send an email before next retry!"); } } try { $parameters = $this->parameters; $phids = array(); foreach ($parameters as $key => $value) { switch ($key) { case 'from': case 'to': case 'cc': if (!is_array($value)) { $value = array($value); } foreach (array_filter($value) as $phid) { $phids[] = $phid; } break; } } $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $exclude = array(); $params = $this->parameters; $default = PhabricatorEnv::getEnvConfig('metamta.default-address'); if (empty($params['from'])) { $mailer->setFrom($default); } else { $from = $params['from']; // If the user has set their preferences to not send them email about // things they do, exclude them from being on To or Cc. $from_user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $from); if ($from_user) { $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL; $exclude_self = $from_user ->loadPreferences() ->getPreference($pref_key); if ($exclude_self) { $exclude[$from] = true; } } if (!PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) { $handle = $handles[$from]; if (empty($params['reply-to'])) { $params['reply-to'] = $handle->getEmail(); $params['reply-to-name'] = $handle->getFullName(); } $mailer->setFrom( $default, $handle->getFullName()); unset($params['from']); } } $is_first = idx($params, 'is-first-message'); unset($params['is-first-message']); $is_threaded = (bool)idx($params, 'thread-id'); $reply_to_name = idx($params, 'reply-to-name', ''); unset($params['reply-to-name']); $add_cc = array(); $add_to = array(); foreach ($params as $key => $value) { switch ($key) { case 'from': $mailer->setFrom($handles[$value]->getEmail()); break; case 'reply-to': $mailer->addReplyTo($value, $reply_to_name); break; case 'to': $emails = $this->getDeliverableEmailsFromHandles( $value, $handles, $exclude); if ($emails) { $add_to = $emails; } break; case 'cc': $emails = $this->getDeliverableEmailsFromHandles( $value, $handles, $exclude); if ($emails) { $add_cc = $emails; } break; case 'headers': foreach ($value as $header_key => $header_value) { // NOTE: If we have \n in a header, SES rejects the email. $header_value = str_replace("\n", " ", $header_value); $mailer->addHeader($header_key, $header_value); } break; case 'attachments': foreach ($value as $attachment) { $mailer->addAttachment( $attachment->getData(), $attachment->getFilename(), $attachment->getMimeType() ); } break; case 'body': $mailer->setBody($value); break; case 'subject': if ($is_threaded) { $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix'); // If this message has a single recipient, respect their "Re:" // preference. Otherwise, use the global setting. $to = idx($params, 'to', array()); $cc = idx($params, 'cc', array()); if (count($to) == 1 && count($cc) == 0) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', head($to)); if ($user) { $prefs = $user->loadPreferences(); $pref_key = PhabricatorUserPreferences::PREFERENCE_RE_PREFIX; $add_re = $prefs->getPreference($pref_key, $add_re); } } if ($add_re) { $value = 'Re: '.$value; } } $mailer->setSubject($value); break; case 'is-html': if ($value) { $mailer->setIsHTML(true); } break; case 'is-bulk': if ($value) { if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) { $mailer->addHeader('Precedence', 'bulk'); } } break; case 'thread-id': if ($is_first && $mailer->supportsMessageIDHeader()) { $mailer->addHeader('Message-ID', $value); } else { $in_reply_to = $value; $references = array($value); $parent_id = $this->getParentMessageID(); if ($parent_id) { $in_reply_to = $parent_id; // By RFC 2822, the most immediate parent should appear last // in the "References" header, so this order is intentional. $references[] = $parent_id; } $references = implode(' ', $references); $mailer->addHeader('In-Reply-To', $in_reply_to); $mailer->addHeader('References', $references); } $thread_index = $this->generateThreadIndex($value, $is_first); $mailer->addHeader('Thread-Index', $thread_index); break; case 'mailtags': // Handled below. break; default: // Just discard. } } $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes'); $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA'); // If the message has mailtags, filter out any recipients who don't want // to receive this type of mail. $mailtags = $this->getParam('mailtags'); if ($mailtags && ($add_to || $add_cc)) { $tag_header = array(); foreach ($mailtags as $mailtag) { $tag_header[] = '<'.$mailtag.'>'; } $tag_header = implode(', ', $tag_header); $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header); $exclude = array(); $all_recipients = array_merge( array_keys($add_to), array_keys($add_cc)); $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID in (%Ls)', $all_recipients); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); foreach ($all_recipients as $recipient) { $prefs = idx($all_prefs, $recipient); if (!$prefs) { continue; } $user_mailtags = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_MAILTAGS, array()); // The user must have elected to receive mail for at least one // of the mailtags. $send = false; foreach ($mailtags as $tag) { if (idx($user_mailtags, $tag, true)) { $send = true; break; } } if (!$send) { $exclude[$recipient] = true; } } $add_to = array_diff_key($add_to, $exclude); $add_cc = array_diff_key($add_cc, $exclude); } if ($add_to) { $mailer->addTos($add_to); if ($add_cc) { $mailer->addCCs($add_cc); } } else if ($add_cc) { // If we have CC addresses but no "to" address, promote the CCs to // "to". $mailer->addTos($add_cc); } else { $this->setStatus(self::STATUS_VOID); $this->setMessage( "Message has no valid recipients: all To/CC are disabled or ". "configured not to receive this mail."); return $this->save(); } } catch (Exception $ex) { $this->setStatus(self::STATUS_FAIL); $this->setMessage($ex->getMessage()); return $this->save(); } if ($this->getRetryCount() < $this->getSimulatedFailureCount()) { $ok = false; $error = 'Simulated failure.'; } else { try { $ok = $mailer->send(); $error = null; } catch (Exception $ex) { $ok = false; $error = $ex->getMessage()."\n".$ex->getTraceAsString(); } } if (!$ok) { $this->setMessage($error); if ($this->getRetryCount() > self::MAX_RETRIES) { $this->setStatus(self::STATUS_FAIL); } else { $this->setRetryCount($this->getRetryCount() + 1); $next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY); $this->setNextRetry($next_retry); } } else { $this->setStatus(self::STATUS_SENT); } return $this->save(); } public static function getReadableStatus($status_code) { static $readable = array( self::STATUS_QUEUE => "Queued for Delivery", self::STATUS_FAIL => "Delivery Failed", self::STATUS_SENT => "Sent", self::STATUS_VOID => "Void", ); $status_code = coalesce($status_code, '?'); return idx($readable, $status_code, $status_code); } private function generateThreadIndex($seed, $is_first_mail) { // When threading, Outlook ignores the 'References' and 'In-Reply-To' // headers that most clients use. Instead, it uses a custom 'Thread-Index' // header. The format of this header is something like this (from // camel-exchange-folder.c in Evolution Exchange): /* A new post to a folder gets a 27-byte-long thread index. (The value * is apparently unique but meaningless.) Each reply to a post gets a * 32-byte-long thread index whose first 27 bytes are the same as the * parent's thread index. Each reply to any of those gets a * 37-byte-long thread index, etc. The Thread-Index header contains a * base64 representation of this value. */ // The specific implementation uses a 27-byte header for the first email // a recipient receives, and a random 5-byte suffix (32 bytes total) // thereafter. This means that all the replies are (incorrectly) siblings, // but it would be very difficult to keep track of the entire tree and this // gets us reasonable client behavior. $base = substr(md5($seed), 0, 27); if (!$is_first_mail) { // Not totally sure, but it seems like outlook orders replies by // thread-index rather than timestamp, so to get these to show up in the // right order we use the time as the last 4 bytes. $base .= ' '.pack('N', time()); } return base64_encode($base); } private function getDeliverableEmailsFromHandles( array $phids, array $handles, array $exclude) { $emails = array(); foreach ($phids as $phid) { if ($handles[$phid]->isDisabled()) { continue; } if (!$handles[$phid]->isComplete()) { continue; } if (isset($exclude[$phid])) { continue; } $emails[$phid] = $handles[$phid]->getEmail(); } return $emails; } } diff --git a/src/applications/metamta/storage/mail/__init__.php b/src/applications/metamta/storage/mail/__init__.php index 4407f4477..7df205fc8 100644 --- a/src/applications/metamta/storage/mail/__init__.php +++ b/src/applications/metamta/storage/mail/__init__.php @@ -1,21 +1,20 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'applications/metamta/storage/base'); phutil_require_module('phabricator', 'applications/people/storage/preferences'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/phid/handle/data'); phutil_require_module('phabricator', 'infrastructure/daemon/workers/storage/task'); phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phutil', 'symbols'); phutil_require_module('phutil', 'utils'); phutil_require_source('PhabricatorMetaMTAAttachment.php'); phutil_require_source('PhabricatorMetaMTAMail.php'); diff --git a/src/applications/search/selector/base/PhabricatorSearchEngineSelector.php b/src/applications/search/selector/base/PhabricatorSearchEngineSelector.php index ec5c2c0e1..b1aeee745 100644 --- a/src/applications/search/selector/base/PhabricatorSearchEngineSelector.php +++ b/src/applications/search/selector/base/PhabricatorSearchEngineSelector.php @@ -1,35 +1,34 @@ <?php /* - * Copyright 2011 Facebook, Inc. + * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @group search */ abstract class PhabricatorSearchEngineSelector { final public function __construct() { // <empty> } abstract public function newEngine(); final public static function newSelector() { - $class = PhabricatorEnv::getEnvConfig('search.engine-selector'); - return newv($class, array()); + return PhabricatorEnv::newObjectFromConfig('search.engine-selector'); } } diff --git a/src/applications/search/selector/base/__init__.php b/src/applications/search/selector/base/__init__.php index 6225135fb..45651ff42 100644 --- a/src/applications/search/selector/base/__init__.php +++ b/src/applications/search/selector/base/__init__.php @@ -1,14 +1,12 @@ <?php /** * This file is automatically generated. Lint this module to rebuild it. * @generated */ phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phutil', 'utils'); - phutil_require_source('PhabricatorSearchEngineSelector.php'); diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 2e3b6151c..fa85bf0bc 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,168 +1,178 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @task uri URI Validation */ final class PhabricatorEnv { private static $env; private static $requiredClasses = array( + 'metamta.mail-adapter' => 'PhabricatorMailImplementationAdapter', + 'metamta.maniphest.reply-handler' => 'PhabricatorMailReplyHandler', 'metamta.differential.reply-handler' => 'PhabricatorMailReplyHandler', + 'metamta.diffusion.reply-handler' => 'PhabricatorMailReplyHandler', + 'storage.engine-selector' => 'PhabricatorFileStorageEngineSelector', + 'search.engine-selector' => 'PhabricatorSearchEngineSelector', + 'differential.field-selector' => 'DifferentialFieldSelector', + 'maniphest.custom-task-extensions-class' => 'ManiphestTaskExtensions', + 'aphront.default-application-configuration-class' => + 'AphrontApplicationConfiguration', + 'controller.oauth-registration' => 'PhabricatorOAuthRegistrationController', ); public static function setEnvConfig(array $config) { self::$env = $config; } public static function getEnvConfig($key, $default = null) { return idx(self::$env, $key, $default); } public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); $object = newv($class, $args); $instanceof = idx(self::$requiredClasses, $key); if (!($object instanceof $instanceof)) { throw new Exception("Config setting '$key' must be an instance of ". "'$instanceof', is '".get_class($object)."'."); } return $object; } public static function envConfigExists($key) { return array_key_exists($key, self::$env); } public static function getURI($path) { return rtrim(self::getEnvConfig('phabricator.base-uri'), '/').$path; } public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getEnvConfig('phabricator.base-uri'); } return rtrim($production_domain, '/').$path; } public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getEnvConfig('phabricator.base-uri'); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } public static function getAllConfigKeys() { return self::$env; } public static function getDoclink($resource) { return 'http://phabricator.com/docs/phabricator/'.$resource; } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalWebResource} or * @{method:isValidRemoteWebResource}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * ##uri.allowed-protocols## configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidWebResource($uri) { return self::isValidLocalWebResource($uri) || self::isValidRemoteWebResource($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalWebResource($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid remote resource. * * @param string URI to test. * @return bool True if a URI idenfies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteWebResource($uri) { $uri = (string)$uri; $proto = id(new PhutilURI($uri))->getProtocol(); if (!$proto) { return false; } $allowed = self::getEnvConfig('uri.allowed-protocols'); if (empty($allowed[$proto])) { return false; } return true; } } diff --git a/webroot/index.php b/webroot/index.php index 13ab26268..36e04dc67 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -1,308 +1,306 @@ <?php /* * Copyright 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ $__start__ = microtime(true); error_reporting(E_ALL | E_STRICT); phabricator_detect_insane_memory_limit(); ini_set('memory_limit', -1); $env = getenv('PHABRICATOR_ENV'); // Apache if (!$env) { if (isset($_ENV['PHABRICATOR_ENV'])) { $env = $_ENV['PHABRICATOR_ENV']; } } if (!$env) { phabricator_fatal_config_error( "The 'PHABRICATOR_ENV' environmental variable is not defined. Modify ". "your httpd.conf to include 'SetEnv PHABRICATOR_ENV <env>', where '<env>' ". "is one of 'development', 'production', or a custom environment."); } if (!function_exists('mysql_connect')) { phabricator_fatal_config_error( "The PHP MySQL extension is not installed. This extension is required."); } if (!isset($_REQUEST['__path__'])) { phabricator_fatal_config_error( "__path__ is not set. Your rewrite rules are not configured correctly."); } if (get_magic_quotes_gpc()) { phabricator_fatal_config_error( "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ". "feature is 'highly discouraged' by PHP's developers and you must ". "disable it to run Phabricator. Consult the PHP manual for instructions."); } register_shutdown_function('phabricator_shutdown'); require_once dirname(dirname(__FILE__)).'/conf/__init_conf__.php'; try { setup_aphront_basics(); $conf = phabricator_read_config_file($env); $conf['phabricator.env'] = $env; phutil_require_module('phabricator', 'infrastructure/env'); PhabricatorEnv::setEnvConfig($conf); phutil_require_module('phabricator', 'aphront/console/plugin/xhprof/api'); DarkConsoleXHProfPluginAPI::hookProfiler(); phutil_require_module('phabricator', 'aphront/console/plugin/errorlog/api'); PhutilErrorHandler::initialize(); } catch (Exception $ex) { phabricator_fatal("[Initialization Exception] ".$ex->getMessage()); } $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); if ($tz) { date_default_timezone_set($tz); } phutil_require_module('phabricator', 'aphront/console/plugin/errorlog/api'); phutil_require_module('phutil', 'error'); PhutilErrorHandler::setErrorListener( array('DarkConsoleErrorLogPluginAPI', 'handleErrors')); foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } if (PhabricatorEnv::getEnvConfig('phabricator.setup')) { PhabricatorSetup::runSetup(); return; } phabricator_detect_bad_base_uri(); $host = $_SERVER['HTTP_HOST']; $path = $_REQUEST['__path__']; switch ($host) { default: $config_key = 'aphront.default-application-configuration-class'; - $config_class = PhabricatorEnv::getEnvConfig($config_key); - PhutilSymbolLoader::loadClass($config_class); - $application = newv($config_class, array()); + $application = PhabricatorEnv::newObjectFromConfig($config_key); break; } $application->setHost($host); $application->setPath($path); $application->willBuildRequest(); $request = $application->buildRequest(); $write_guard = new AphrontWriteGuard($request); PhabricatorEventEngine::initialize(); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); try { $response = $controller->willBeginExecution(); if (!$response) { $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $response = $application->handleException($ex); } try { $response = $application->willSendResponse($response); $response->setRequest($request); $response_string = $response->buildResponseString(); } catch (Exception $ex) { $write_guard->dispose(); phabricator_fatal('[Rendering Exception] '.$ex->getMessage()); } $write_guard->dispose(); // TODO: Share the $sink->writeResponse() pathway here? $sink = new AphrontPHPHTTPSink(); $sink->writeHTTPStatus($response->getHTTPResponseCode()); $headers = $response->getCacheHeaders(); $headers = array_merge($headers, $response->getHeaders()); $sink->writeHeaders($headers); // TODO: This shouldn't be possible in a production-configured environment. if (isset($_REQUEST['__profile__']) && ($_REQUEST['__profile__'] == 'all')) { $profile = DarkConsoleXHProfPluginAPI::stopProfiler(); $profile = '<div style="text-align: center; background: #ff00ff; padding: 1em; font-size: 24px; font-weight: bold;">'. '<a href="/xhprof/profile/'.$profile.'/">'. '>>> View Profile <<<'. '</a>'. '</div>'; if (strpos($response_string, '<body>') !== false) { $response_string = str_replace( '<body>', '<body>'.$profile, $response_string); } else { $sink->writeData($profile); } } $sink->writeData($response_string); /** * @group aphront */ function setup_aphront_basics() { $aphront_root = dirname(dirname(__FILE__)); $libraries_root = dirname($aphront_root); $root = null; if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) { $root = $_SERVER['PHUTIL_LIBRARY_ROOT']; } ini_set('include_path', $libraries_root.':'.ini_get('include_path')); @include_once $root.'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { echo "ERROR: Unable to load libphutil. Put libphutil/ next to ". "phabricator/, or update your PHP 'include_path' to include ". "the parent directory of libphutil/.\n"; exit(1); } // Load Phabricator itself using the absolute path, so we never end up doing // anything surprising (loading index.php and libraries from different // directories). phutil_load_library($aphront_root.'/src'); phutil_load_library('arcanist/src'); } function phabricator_fatal_config_error($msg) { phabricator_fatal("CONFIG ERROR: ".$msg."\n"); } function phabricator_detect_bad_base_uri() { $conf = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $uri = new PhutilURI($conf); switch ($uri->getProtocol()) { case 'http': case 'https': break; default: return phabricator_fatal_config_error( "'phabricator.base-uri' is set to '{$conf}', which is invalid. ". "The URI must start with 'http://' or 'https://'."); } if (strpos($uri->getDomain(), '.') === false) { phabricator_fatal_config_error( "'phabricator.base-uri' is set to '{$conf}', which is invalid. The URI ". "must contain a dot ('.'), like 'http://example.com/', not just ". "'http://example/'. Some web browsers will not set cookies on domains ". "with no TLD, and Phabricator requires cookies for login. ". "If you are using localhost, create an entry in the hosts file like ". "'127.0.0.1 example.com', and access the localhost with ". "'http://example.com/'."); } } function phabricator_detect_insane_memory_limit() { $memory_limit = ini_get('memory_limit'); $char_limit = 12; if (strlen($memory_limit) <= $char_limit) { return; } // colmdoyle ran into an issue on an Ubuntu box with Suhosin where his // 'memory_limit' was set to: // // 3232323232323232323232323232323232323232323232323232323232323232M // // Not a typo. A wizard did it. // // Anyway, with this 'memory_limit', the machine would immediately fatal // when executing the ini_set() later. I wasn't able to reproduce this on my // EC2 Ubuntu + Suhosin box, but verified that it caused the problem on his // machine and that setting it to a more sensible value fixed it. Since I // have no idea how to actually trigger the issue, we look for a coarse // approximation of it (a memory_limit setting more than 12 characters in // length). phabricator_fatal_config_error( "Your PHP 'memory_limit' is set to something ridiculous ". "(\"{$memory_limit}\"). Set it to a more reasonable value (it must be no ". "more than {$char_limit} characters long)."); } function phabricator_shutdown() { $event = error_get_last(); if (!$event) { return; } if ($event['type'] != E_ERROR && $event['type'] != E_PARSE) { return; } $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n"; if ($event) { // Even though we should be emitting this as text-plain, escape things just // to be sure since we can't really be sure what the program state is when // we get here. $msg .= phutil_escape_html($event['message'])."\n\n"; $msg .= phutil_escape_html($event['file'].':'.$event['line']); } // flip dem tables $msg .= "\n\n\n"; $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf". "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20". "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb"; phabricator_fatal($msg); } function phabricator_fatal($msg) { header( 'Content-Type: text/plain; charset=utf-8', $replace = true, $http_error = 500); error_log($msg); echo $msg; exit(1); }