diff --git a/.gitignore b/.gitignore index a969224fa6..07a2f0da43 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store ._* /docs/ /src/.phutil_module_cache +/conf/custom/* diff --git a/conf/default.conf.php b/conf/default.conf.php new file mode 100644 index 0000000000..a5bb05d2fa --- /dev/null +++ b/conf/default.conf.php @@ -0,0 +1,46 @@ + null, + + + // + 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', + + +// -- Facebook --------------------------------------------------------------- + + // Can users use Facebook credentials to login to Phabricator? + 'facebook.auth-enabled' => false, + + // The Facebook "Application ID" to use for Facebook API access. + 'facebook.application-id' => null, + + // The Facebook "Application Secret" to use for Facebook API access. + 'facebook.application-secret' => null, + + 'recaptcha.public-key' => null, + 'recaptcha.private-key' => null, + + + +); diff --git a/src/infratructure/javelin/api/Javelin.php b/conf/development.conf.php similarity index 74% copy from src/infratructure/javelin/api/Javelin.php copy to conf/development.conf.php index b6e2a3d9cf..85673eab81 100644 --- a/src/infratructure/javelin/api/Javelin.php +++ b/conf/development.conf.php @@ -1,24 +1,22 @@ initBehavior($behavior, $config); - } -} +return array( + + +) + phabricator_read_config_file('default'); diff --git a/src/infratructure/javelin/api/Javelin.php b/conf/production.conf.php similarity index 74% copy from src/infratructure/javelin/api/Javelin.php copy to conf/production.conf.php index b6e2a3d9cf..9af5c29c9a 100644 --- a/src/infratructure/javelin/api/Javelin.php +++ b/conf/production.conf.php @@ -1,24 +1,23 @@ initBehavior($behavior, $config); - } -} +return array( + + +) + phabricator_read_config_file('default'); + diff --git a/externals/recaptcha/LICENSE b/externals/recaptcha/LICENSE new file mode 100644 index 0000000000..b612f71f01 --- /dev/null +++ b/externals/recaptcha/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2007 reCAPTCHA -- http://recaptcha.net +AUTHORS: + Mike Crawford + Ben Maurer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/externals/recaptcha/recaptchalib.php b/externals/recaptcha/recaptchalib.php new file mode 100644 index 0000000000..32c4f4d758 --- /dev/null +++ b/externals/recaptcha/recaptchalib.php @@ -0,0 +1,277 @@ + $value ) + $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; + + // Cut the last '&' + $req=substr($req,0,strlen($req)-1); + return $req; +} + + + +/** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ +function _recaptcha_http_post($host, $path, $data, $port = 80) { + + $req = _recaptcha_qsencode ($data); + + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + + $response = ''; + if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { + die ('Could not open socket'); + } + + fwrite($fs, $http_request); + + while ( !feof($fs) ) + $response .= fgets($fs, 1160); // One TCP-IP packet + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + + return $response; +} + + + +/** + * Gets the challenge HTML (javascript and non-javascript version). + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * @param string $pubkey A public key for reCAPTCHA + * @param string $error The error given by reCAPTCHA (optional, default is null) + * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) + + * @return string - The HTML to be embedded in the user's form. + */ +function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) +{ + if ($pubkey == null || $pubkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($use_ssl) { + $server = RECAPTCHA_API_SECURE_SERVER; + } else { + $server = RECAPTCHA_API_SERVER; + } + + $errorpart = ""; + if ($error) { + $errorpart = "&error=" . $error; + } + return ' + + '; +} + + + + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse { + var $is_valid; + var $error; +} + + +/** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ +function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) +{ + if ($privkey == null || $privkey == '') { + die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($remoteip == null || $remoteip == '') { + die ("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode ("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim ($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } + else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; + +} + +/** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ +function recaptcha_get_signup_url ($domain = null, $appname = null) { + return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); +} + +function _recaptcha_aes_pad($val) { + $block_size = 16; + $numpad = $block_size - (strlen ($val) % $block_size); + return str_pad($val, strlen ($val) + $numpad, chr($numpad)); +} + +/* Mailhide related code */ + +function _recaptcha_aes_encrypt($val,$ky) { + if (! function_exists ("mcrypt_encrypt")) { + die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); +} + + +function _recaptcha_mailhide_urlbase64 ($x) { + return strtr(base64_encode ($x), '+/', '-_'); +} + +/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ +function recaptcha_mailhide_url($pubkey, $privkey, $email) { + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = _recaptcha_aes_encrypt ($email, $ky); + + return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); +} + +/** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ +function _recaptcha_mailhide_email_parts ($email) { + $arr = preg_split("/@/", $email ); + + if (strlen ($arr[0]) <= 4) { + $arr[0] = substr ($arr[0], 0, 1); + } else if (strlen ($arr[0]) <= 6) { + $arr[0] = substr ($arr[0], 0, 3); + } else { + $arr[0] = substr ($arr[0], 0, 4); + } + return $arr; +} + +/** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://www.google.com/recaptcha/mailhide/apikey + */ +function recaptcha_mailhide_html($pubkey, $privkey, $email) { + $emailparts = _recaptcha_mailhide_email_parts ($email); + $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); + +} + + +?> diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7f24c77fc2..261d63ec43 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,331 +1,342 @@ array( 'Aphront400Response' => 'aphront/response/400', 'Aphront404Response' => 'aphront/response/404', 'AphrontAjaxResponse' => 'aphront/response/ajax', 'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration', 'AphrontController' => 'aphront/controller', 'AphrontDatabaseConnection' => 'storage/connection/base', 'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration', 'AphrontDefaultApplicationController' => 'aphront/default/controller', 'AphrontDialogResponse' => 'aphront/response/dialog', 'AphrontDialogView' => 'view/dialog', 'AphrontErrorView' => 'view/form/error', 'AphrontException' => 'aphront/exception/base', 'AphrontFileResponse' => 'aphront/response/file', 'AphrontFormCheckboxControl' => 'view/form/control/checkbox', 'AphrontFormControl' => 'view/form/control/base', 'AphrontFormFileControl' => 'view/form/control/file', 'AphrontFormMarkupControl' => 'view/form/control/markup', + 'AphrontFormPasswordControl' => 'view/form/control/password', + 'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha', 'AphrontFormSelectControl' => 'view/form/control/select', 'AphrontFormStaticControl' => 'view/form/control/static', 'AphrontFormSubmitControl' => 'view/form/control/submit', 'AphrontFormTextAreaControl' => 'view/form/control/textarea', 'AphrontFormTextControl' => 'view/form/control/text', 'AphrontFormTokenizerControl' => 'view/form/control/tokenizer', 'AphrontFormView' => 'view/form/base', 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql', 'AphrontNullView' => 'view/null', 'AphrontPageView' => 'view/page/base', 'AphrontPanelView' => 'view/layout/panel', 'AphrontQueryConnectionException' => 'storage/exception/connection', 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 'AphrontQueryCountException' => 'storage/exception/count', 'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey', 'AphrontQueryException' => 'storage/exception/base', 'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing', 'AphrontQueryParameterException' => 'storage/exception/parameter', 'AphrontQueryRecoverableException' => 'storage/exception/recoverable', 'AphrontRedirectException' => 'aphront/exception/redirect', 'AphrontRedirectResponse' => 'aphront/response/redirect', 'AphrontRequest' => 'aphront/request', 'AphrontRequestFailureView' => 'view/page/failure', 'AphrontResponse' => 'aphront/response/base', 'AphrontSideNavView' => 'view/layout/sidenav', 'AphrontTableView' => 'view/control/table', 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', - 'CelerityAPI' => 'infratructure/celerity/api', - 'CelerityResourceController' => 'infratructure/celerity/controller', - 'CelerityResourceMap' => 'infratructure/celerity/map', - 'CelerityStaticResourceResponse' => 'infratructure/celerity/response', + 'CelerityAPI' => 'infrastructure/celerity/api', + 'CelerityResourceController' => 'infrastructure/celerity/controller', + 'CelerityResourceMap' => 'infrastructure/celerity/map', + 'CelerityStaticResourceResponse' => 'infrastructure/celerity/response', 'ConduitAPIMethod' => 'applications/conduit/method/base', 'ConduitAPIRequest' => 'applications/conduit/protocol/request', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', 'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff', 'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find', 'ConduitException' => 'applications/conduit/protocol/exception', 'DifferentialAction' => 'applications/differential/constants/action', 'DifferentialAddCommentView' => 'applications/differential/view/addcomment', 'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome', 'DifferentialChangeType' => 'applications/differential/constants/changetype', 'DifferentialChangeset' => 'applications/differential/storage/changeset', 'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview', 'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview', 'DifferentialChangesetParser' => 'applications/differential/parser/changeset', 'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview', 'DifferentialComment' => 'applications/differential/storage/comment', 'DifferentialCommentEditor' => 'applications/differential/editor/comment', 'DifferentialCommentMail' => 'applications/differential/mail/comment', 'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave', 'DifferentialController' => 'applications/differential/controller/base', 'DifferentialDAO' => 'applications/differential/storage/base', 'DifferentialDiff' => 'applications/differential/storage/diff', 'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent', 'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents', 'DifferentialDiffViewController' => 'applications/differential/controller/diffview', 'DifferentialHunk' => 'applications/differential/storage/hunk', 'DifferentialLintStatus' => 'applications/differential/constants/lintstatus', 'DifferentialMail' => 'applications/differential/mail/base', 'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup', 'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff', 'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest', 'DifferentialRevision' => 'applications/differential/storage/revision', 'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist', 'DifferentialRevisionCommentView' => 'applications/differential/view/revisioncomment', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem', 'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail', 'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit', 'DifferentialRevisionEditor' => 'applications/differential/editor/revision', 'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist', 'DifferentialRevisionListData' => 'applications/differential/data/revisionlist', 'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory', 'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview', 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', - 'Javelin' => 'infratructure/javelin/api', + 'Javelin' => 'infrastructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', 'Phabricator404Controller' => 'applications/base/controller/404', 'PhabricatorAuthController' => 'applications/auth/controller/base', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console', 'PhabricatorConduitController' => 'applications/conduit/controller/base', 'PhabricatorConduitDAO' => 'applications/conduit/storage/base', 'PhabricatorConduitLogController' => 'applications/conduit/controller/log', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog', 'PhabricatorController' => 'applications/base/controller/base', 'PhabricatorDirectoryCategory' => 'applications/directory/storage/category', 'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete', 'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit', 'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist', 'PhabricatorDirectoryController' => 'applications/directory/controller/base', 'PhabricatorDirectoryDAO' => 'applications/directory/storage/base', 'PhabricatorDirectoryItem' => 'applications/directory/storage/item', 'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete', 'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit', 'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist', 'PhabricatorDirectoryMainController' => 'applications/directory/controller/main', - 'PhabricatorFacebookConnectController' => 'applications/auth/controller/facebookconnect', + 'PhabricatorEmailLoginController' => 'applications/auth/controller/email', + 'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken', + 'PhabricatorEnv' => 'infrastructure/env', + 'PhabricatorFacebookAuthController' => 'applications/auth/controller/facebookauth', + 'PhabricatorFacebookAuthDiagnosticsController' => 'applications/auth/controller/facebookauth/diagnostics', 'PhabricatorFile' => 'applications/files/storage/file', 'PhabricatorFileController' => 'applications/files/controller/base', 'PhabricatorFileDAO' => 'applications/files/storage/base', 'PhabricatorFileListController' => 'applications/files/controller/list', 'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob', 'PhabricatorFileURI' => 'applications/files/uri', 'PhabricatorFileUploadController' => 'applications/files/controller/upload', 'PhabricatorFileViewController' => 'applications/files/controller/view', 'PhabricatorLiskDAO' => 'applications/base/storage/lisk', 'PhabricatorLoginController' => 'applications/auth/controller/login', 'PhabricatorLogoutController' => 'applications/auth/controller/logout', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/base', 'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base', 'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list', 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail', 'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist', 'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit', 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorObjectHandle' => 'applications/phid/handle', 'PhabricatorObjectHandleData' => 'applications/phid/handle/data', 'PhabricatorPHID' => 'applications/phid/storage/phid', 'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate', 'PhabricatorPHIDController' => 'applications/phid/controller/base', 'PhabricatorPHIDDAO' => 'applications/phid/storage/base', 'PhabricatorPHIDListController' => 'applications/phid/controller/list', 'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup', 'PhabricatorPHIDType' => 'applications/phid/storage/type', 'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit', 'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist', 'PhabricatorPeopleController' => 'applications/people/controller/base', 'PhabricatorPeopleEditController' => 'applications/people/controller/edit', 'PhabricatorPeopleListController' => 'applications/people/controller/list', 'PhabricatorPeopleProfileController' => 'applications/people/controller/profile', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base', 'PhabricatorUser' => 'applications/people/storage/user', 'PhabricatorUserDAO' => 'applications/people/storage/base', ), 'function' => array( '_qsprintf_check_scalar_type' => 'storage/qsprintf', '_qsprintf_check_type' => 'storage/qsprintf', - 'celerity_generate_unique_node_id' => 'infratructure/celerity/api', - 'celerity_register_resource_map' => 'infratructure/celerity/map', - 'javelin_render_tag' => 'infratructure/javelin/markup', + 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api', + 'celerity_register_resource_map' => 'infrastructure/celerity/map', + 'javelin_render_tag' => 'infrastructure/javelin/markup', 'phabricator_format_relative_time' => 'view/utils', 'phabricator_format_timestamp' => 'view/utils', 'phabricator_format_units_generic' => 'view/utils', 'qsprintf' => 'storage/qsprintf', 'queryfx' => 'storage/queryfx', 'queryfx_all' => 'storage/queryfx', 'queryfx_one' => 'storage/queryfx', - 'require_celerity_resource' => 'infratructure/celerity/api', + 'require_celerity_resource' => 'infrastructure/celerity/api', 'vqsprintf' => 'storage/qsprintf', 'vqueryfx' => 'storage/queryfx', 'vqueryfx_all' => 'storage/queryfx', 'xsprintf_query' => 'storage/qsprintf', ), 'requires_class' => array( 'Aphront400Response' => 'AphrontResponse', 'Aphront404Response' => 'AphrontResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDefaultApplicationController' => 'AphrontController', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontErrorView' => 'AphrontView', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormMarkupControl' => 'AphrontFormControl', + 'AphrontFormPasswordControl' => 'AphrontFormControl', + 'AphrontFormRecaptchaControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontNullView' => 'AphrontView', 'AphrontPageView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', 'AphrontQueryConnectionException' => 'AphrontQueryException', 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 'AphrontQueryCountException' => 'AphrontQueryException', 'AphrontQueryDuplicateKeyException' => 'AphrontQueryException', 'AphrontQueryObjectMissingException' => 'AphrontQueryException', 'AphrontQueryParameterException' => 'AphrontQueryException', 'AphrontQueryRecoverableException' => 'AphrontQueryException', 'AphrontRedirectException' => 'AphrontException', 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontRequestFailureView' => 'AphrontView', 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_find_Method' => 'ConduitAPIMethod', 'DifferentialAddCommentView' => 'AphrontView', 'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail', 'DifferentialChangeset' => 'DifferentialDAO', 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialComment' => 'DifferentialDAO', 'DifferentialCommentMail' => 'DifferentialMail', 'DifferentialCommentSaveController' => 'DifferentialController', 'DifferentialController' => 'PhabricatorController', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDiff' => 'DifferentialDAO', 'DifferentialDiffContentMail' => 'DifferentialMail', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialHunk' => 'DifferentialDAO', 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialRevision' => 'DifferentialDAO', 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO', 'PhabricatorController' => 'AphrontController', 'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryController' => 'PhabricatorController', 'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', - 'PhabricatorFacebookConnectController' => 'PhabricatorAuthController', + 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', + 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', + 'PhabricatorFacebookAuthController' => 'PhabricatorAuthController', + 'PhabricatorFacebookAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLoginController' => 'PhabricatorAuthController', 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorPHID' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController', 'PhabricatorPHIDController' => 'PhabricatorController', 'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO', 'PhabricatorPHIDListController' => 'PhabricatorPHIDController', 'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController', 'PhabricatorPHIDType' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController', 'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorUser' => 'PhabricatorUserDAO', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', ), 'requires_interface' => array( ), )); diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index ef0619b0cc..a7dbbe58cf 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,188 +1,195 @@ array( '$' => 'RepositoryListController', 'new/$' => 'RepositoryEditController', 'edit/(?\d+)/$' => 'RepositoryEditController', 'delete/(?\d+)/$' => 'RepositoryDeleteController', ), '/' => array( '$' => 'PhabricatorDirectoryMainController', ), '/directory/' => array( 'item/$' => 'PhabricatorDirectoryItemListController', 'item/edit/(?:(?\d+)/)?$' => 'PhabricatorDirectoryItemEditController', 'item/delete/(?\d+)/' => 'PhabricatorDirectoryItemDeleteController', 'category/$' => 'PhabricatorDirectoryCategoryListController', 'category/edit/(?:(?\d+)/)?$' => 'PhabricatorDirectoryCategoryEditController', 'category/delete/(?\d+)/' => 'PhabricatorDirectoryCategoryDeleteController', ), '/file/' => array( '$' => 'PhabricatorFileListController', 'upload/$' => 'PhabricatorFileUploadController', '(?info)/(?[^/]+)/' => 'PhabricatorFileViewController', '(?view)/(?[^/]+)/' => 'PhabricatorFileViewController', '(?download)/(?[^/]+)/' => 'PhabricatorFileViewController', ), '/phid/' => array( '$' => 'PhabricatorPHIDLookupController', 'list/$' => 'PhabricatorPHIDListController', 'type/$' => 'PhabricatorPHIDTypeListController', 'type/edit/(?:(?\d+)/)?$' => 'PhabricatorPHIDTypeEditController', 'new/$' => 'PhabricatorPHIDAllocateController', ), '/people/' => array( '$' => 'PhabricatorPeopleListController', 'edit/(?:(?\w+)/)?$' => 'PhabricatorPeopleEditController', ), '/p/(?\w+)/$' => 'PhabricatorPeopleProfileController', '/conduit/' => array( '$' => 'PhabricatorConduitConsoleController', 'method/(?[^/]+)$' => 'PhabricatorConduitConsoleController', 'log/$' => 'PhabricatorConduitLogController', ), '/api/(?[^/]+)$' => 'PhabricatorConduitAPIController', '/D(?\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?\w+)/$' => 'DifferentialRevisionListController', 'diff/(?\d+)/$' => 'DifferentialDiffViewController', 'changeset/(?\d+)/$' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?\d+)/)?$' => 'DifferentialRevisionEditController', 'comment/' => array( 'preview/$' => 'DifferentialCommentPreviewController', 'save/$' => 'DifferentialCommentSaveController', 'inline/' => array( 'preview/$' => 'DifferentialInlineCommentPreviewController', 'edit/$' => 'DifferentialInlineCommentEditController', ), ), ), '/res/' => array( '(?pkg/)?(?[a-f0-9]{8})/(?.+\.(?:css|js))$' => 'CelerityResourceController', ), '/typeahead/' => array( 'common/(?\w+)/$' => 'PhabricatorTypeaheadCommonDatasourceController', ), '/mail/' => array( '$' => 'PhabricatorMetaMTAListController', 'send/$' => 'PhabricatorMetaMTASendController', 'view/(?\d+)/$' => 'PhabricatorMetaMTAViewController', 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?\d+)/)?$' => 'PhabricatorMetaMTAMailingListEditController', ), - '/login/$' => 'PhabricatorLoginController', + '/login/' => array( + '$' => 'PhabricatorLoginController', + 'email/$' => 'PhabricatorEmailLoginController', + 'etoken/(?\w+)/$' => 'PhabricatorEmailTokenController', + ), '/logout/$' => 'PhabricatorLogoutController', - '/facebook-connect/$' => 'PhabricatorFacebookConnectController', + '/facebook-auth/' => array( + '$' => 'PhabricatorFacebookAuthController', + 'diagnose/$' => 'PhabricatorFacebookAuthDiagnosticsController', + ), ); } public function buildRequest() { $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($_GET + $_POST); return $request; } public function handleException(Exception $ex) { $class = phutil_escape_html(get_class($ex)); $message = phutil_escape_html($ex->getMessage()); $content = '
'. '

Unhandled Exception "'.$class.'": '.$message.'

'. ''.phutil_escape_html((string)$ex).''. '
'; $view = new PhabricatorStandardPageView(); $view->appendChild($content); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } public function willSendResponse(AphrontResponse $response) { $request = $this->getRequest(); if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->appendChild( '
'. $response->buildResponseString(). '
'); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } } else if ($response instanceof Aphront404Response) { $failure = new AphrontRequestFailureView(); $failure->setHeader('404 Not Found'); $failure->appendChild( '

The page you requested was not found.

'); $view = new PhabricatorStandardPageView(); $view->setTitle('404 Not Found'); $view->appendChild($failure); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(404); return $response; } return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } } diff --git a/src/aphront/request/AphrontRequest.php b/src/aphront/request/AphrontRequest.php index 6cc50fa2f5..a231ff30e2 100644 --- a/src/aphront/request/AphrontRequest.php +++ b/src/aphront/request/AphrontRequest.php @@ -1,127 +1,135 @@ env = $conf; + return $this; + } + + final public function getEnvConfig($key, $default = null) { + return idx($this->env, $key, $default); + } final public function __construct($host, $path) { $this->host = $host; $this->path = $path; } final public function setRequestData(array $request_data) { $this->requestData = $request_data; return $this; } final public function getPath() { return $this->path; } final public function getHost() { return $this->host; } final public function getInt($name, $default = null) { if (isset($this->requestData[$name])) { return (int)$this->requestData[$name]; } else { return $default; } } final public function getStr($name, $default = null) { if (isset($this->requestData[$name])) { return (string)$this->requestData[$name]; } else { return $default; } } final public function getArr($name, $default = array()) { if (isset($this->requestData[$name]) && is_array($this->requestData[$name])) { return $this->requestData[$name]; } else { return $default; } } final public function getExists($name) { return array_key_exists($name, $this->requestData); } final public function isHTTPPost() { return ($_SERVER['REQUEST_METHOD'] == 'POST'); } final public function isAjax() { return $this->getExists(self::TYPE_AJAX); } final public function isFormPost() { return $this->getExists(self::TYPE_FORM) && $this->isHTTPPost() && $this->getUser()->validateCSRFToken($this->getStr('__csrf__')); } final public function getCookie($name, $default = null) { return idx($_COOKIE, $name, $default); } final public function clearCookie($name) { $this->setCookie($name, '', time() - (60 * 60 * 24 * 30)); } final public function setCookie($name, $value, $expire = null) { if ($expire === null) { $expire = time() + (60 * 60 * 24 * 365 * 5); } setcookie( $name, $value, $expire, $path = '/', $domain = '', $secure = false, $http_only = true); } final public function setUser($user) { $this->user = $user; return $this; } final public function getUser() { return $this->user; } - - } diff --git a/src/aphront/response/ajax/__init__.php b/src/aphront/response/ajax/__init__.php index c0dd276bb7..fd1560d506 100644 --- a/src/aphront/response/ajax/__init__.php +++ b/src/aphront/response/ajax/__init__.php @@ -1,13 +1,13 @@ getRequest(); + + $e_email = true; + $e_captcha = true; + $errors = array(); + + if ($request->isFormPost()) { + $e_email = null; + $e_captcha = 'Again'; + + $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); + if (!$captcha_ok) { + $errors[] = "Captcha response is incorrect, try again."; + $e_captcha = 'Invalid'; + } + + $email = $request->getStr('email'); + if (!strlen($email)) { + $errors[] = "You must provide an email address."; + $e_email = 'Required'; + } + + if (!$errors) { + // NOTE: Don't validate the email unless the captcha is good; this makes + // it expensive to fish for valid email addresses while giving the user + // a better error if they goof their email. + + $target_user = id(new PhabricatorUser())->loadOneWhere( + 'email = %s', + $email); + + if (!$target_user) { + $errors[] = "There is no account associated with that email address."; + $e_email = "Invalid"; + } + + if (!$errors) { + $etoken = $target_user->generateEmailToken(); + + $mail = new PhabricatorMetaMTAMail(); + $mail->setSubject('Phabricator Email Authentication'); + $mail->addTos( + array( + $target_user->getEmail(), + )); + $mail->setBody( + "blah blah blah ". + PhabricatorEnv::getURI('/login/etoken/'.$etoken.'/').'?email='.phutil_escape_uri($target_user->getEmail())); + $mail->save(); + + $view = new AphrontRequestFailureView(); + $view->setHeader('Check Your Email'); + $view->appendChild( + '

An email has been sent with a link you can use to login.

'); + return $this->buildStandardPageResponse( + $view, + array( + 'title' => 'Email Sent', + )); + } + } + + } + + $email_auth = new AphrontFormView(); + $email_auth + ->setAction('/login/email/') + ->setUser($request->getUser()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Email') + ->setName('email') + ->setValue($request->getStr('email')) + ->setError($e_email)) + ->appendChild( + id(new AphrontFormRecaptchaControl()) + ->setLabel('Captcha') + ->setError($e_captcha)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Send Email')); + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Login Error'); + $error_view->setErrors($errors); + } + + + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->appendChild('

Forgot Password / Email Login

'); + $panel->appendChild($email_auth); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create New Account', + )); + } + +} diff --git a/src/applications/auth/controller/facebookconnect/__init__.php b/src/applications/auth/controller/email/__init__.php similarity index 64% copy from src/applications/auth/controller/facebookconnect/__init__.php copy to src/applications/auth/controller/email/__init__.php index 7c02260f35..72d4c63071 100644 --- a/src/applications/auth/controller/facebookconnect/__init__.php +++ b/src/applications/auth/controller/email/__init__.php @@ -1,22 +1,24 @@ token = $data['token']; + } + + public function processRequest() { + $request = $this->getRequest(); + + $token = $this->token; + $email = $request->getStr('email'); + + $target_user = id(new PhabricatorUser())->loadOneWhere( + 'email = %s', + $email); + + if (!$target_user || !$target_user->validateEmailToken($token)) { + $view = new AphrontRequestFailureView(); + $view->setHeader('Unable to Login'); + $view->appendChild( + '

The authentication information in the link you clicked is '. + 'invalid or out of date. Make sure you are copy-and-pasting the '. + 'entire link into your browser. You can try again, or request '. + 'a new email.

'); + $view->appendChild( + '
'. + 'Send Another Email'. + '
'); + return $this->buildStandardPageResponse( + $view, + array( + 'title' => 'Email Sent', + )); + } + + if ($request->getUser()->getPHID() != $target_user->getPHID()) { + $session_key = $target_user->establishSession('web'); + $request->setCookie('phusr', $target_user->getUsername()); + $request->setCookie('phsid', $session_key); + } + + $errors = array(); + + $e_pass = true; + $e_confirm = true; + + if ($request->isFormPost()) { + $e_pass = 'Error'; + $e_confirm = 'Error'; + + $pass = $request->getStr('password'); + $confirm = $request->getStr('confirm'); + + if (strlen($pass) < 3) { + $errors[] = 'That password is ridiculously short.'; + } + + if ($pass !== $confirm) { + $errors[] = "Passwords do not match."; + } + + if (!$errors) { + $target_user->setPassword($pass); + $target_user->save(); + return id(new AphrontRedirectResponse()) + ->setURI('/'); + } + } + + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Password Reset Failed'); + $error_view->setErrors($errors); + } else { + $error_view = null; + } + + $form = new AphrontFormView(); + $form + ->setUser($target_user) + ->setAction('/login/etoken/'.$token.'/') + ->addHiddenInput('email', $email) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('New Password') + ->setName('password') + ->setError($e_pass)) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel('Confirm Password') + ->setName('confirm') + ->setError($e_confirm)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Reset Password') + ->addCancelButton('/', 'Skip')); + + $panel = new AphrontPanelView(); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setHeader('Reset Password'); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create New Account', + )); + } + +} diff --git a/src/applications/auth/controller/facebookconnect/__init__.php b/src/applications/auth/controller/emailtoken/__init__.php similarity index 76% copy from src/applications/auth/controller/facebookconnect/__init__.php copy to src/applications/auth/controller/emailtoken/__init__.php index 7c02260f35..fd917cbcc1 100644 --- a/src/applications/auth/controller/facebookconnect/__init__.php +++ b/src/applications/auth/controller/emailtoken/__init__.php @@ -1,22 +1,21 @@ '. + 'Diagnose Facebook Auth Problems'. + ''; + $request = $this->getRequest(); if ($request->getStr('error')) { - die("OMG ERROR"); + $view = new AphrontRequestFailureView(); + $view->setHeader('Facebook Auth Failed'); + $view->appendChild( + '

'. + 'Description: '. + phutil_escape_html($request->getStr('error_description')). + '

'); + $view->appendChild( + '

'. + 'Error: '. + phutil_escape_html($request->getStr('error')). + '

'); + $view->appendChild( + '

'. + 'Error Reason: '. + phutil_escape_html($request->getStr('error_reason')). + '

'); + $view->appendChild( + '
'. + 'Continue'. + '
'); + + return $this->buildStandardPageResponse( + $view, + array( + 'title' => 'Facebook Auth Failed', + )); } $token = $request->getStr('token'); if (!$token) { + $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id'); + $app_secret = PhabricatorEnv::getEnvConfig('facebook.application-secret'); + $redirect_uri = PhabricatorEnv::getURI('/facebook-auth/'); + $code = $request->getStr('code'); - $auth_uri = 'https://graph.facebook.com/oauth/access_token'. - '?client_id=184510521580034'. - '&redirect_uri=http://local.aphront.com/facebook-connect/'. - '&client_secret=OMGSECRETS'. - '&code='.$code; + $auth_uri = new PhutilURI( + "https://graph.facebook.com/oauth/access_token"); + $auth_uri->setQueryParams( + array( + 'client_id' => $app_id, + 'redirect_uri' => $redirect_uri, + 'client_secret' => $app_secret, + 'code' => $code, + )); $response = @file_get_contents($auth_uri); if ($response === false) { - throw new Exception('failed to open oauth thing'); + $view = new AphrontRequestFailureView(); + $view->setHeader('Facebook Auth Failed'); + $view->appendChild( + '

Unable to authenticate with Facebook. There are several reasons '. + 'this might happen:

'. + '
    '. + '
  • Phabricator may be configured with the wrong Application '. + 'Secret; or
  • '. + '
  • the Facebook OAuth access token may have expired; or
  • '. + '
  • Facebook may have revoked authorization for the '. + 'Application; or
  • '. + '
  • Facebook may be having technical problems.
  • '. + '
'. + '

You can try again, or login using another method.

'); + $view->appendChild( + '
'. + $diagnose_auth. + 'Continue'. + '
'); + + return $this->buildStandardPageResponse( + $view, + array( + 'title' => 'Facebook Auth Failed', + )); } $data = array(); parse_str($response, $data); $token = $data['access_token']; } $user_json = @file_get_contents('https://graph.facebook.com/me?access_token='.$token); $user_data = json_decode($user_json, true); $user_id = $user_data['id']; $known_user = id(new PhabricatorUser()) ->loadOneWhere('facebookUID = %d', $user_id); if ($known_user) { $session_key = $known_user->establishSession('web'); $request->setCookie('phusr', $known_user->getUsername()); $request->setCookie('phsid', $session_key); return id(new AphrontRedirectResponse()) ->setURI('/'); } $current_user = $this->getRequest()->getUser(); if ($current_user->getPHID()) { if ($current_user->getFacebookUID() && $current_user->getFacebookUID() != $user_id) { throw new Exception( "Your account is already associated with a Facebook user ID other ". "than the one you just logged in with...?"); } if ($request->isFormPost()) { $current_user->setFacebookUID($user_id); $current_user->save(); // TODO: ship them back to the 'account' page or whatever? return id(new AphrontRedirectResponse()) ->setURI('/'); } $ph_account = $current_user->getUsername(); $fb_account = phutil_escape_html($user_data['name']); $form = new AphrontFormView(); $form ->addHiddenInput('token', $token) ->setUser($request->getUser()) - ->setAction('/facebook-connect/') + ->setAction('/facebook-auth/') ->appendChild( '

Do you want to link your '. "existing Phabricator account ({$ph_account}) ". "with your Facebook account ({$fb_account}) so ". - "you can login with Facebook Connect?") + "you can login with Facebook?") ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Link Accounts') ->addCancelButton('/login/')); $panel = new AphrontPanelView(); $panel->setHeader('Link Facebook Account'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Link Facebook Account', )); } $errors = array(); $e_username = true; $user = new PhabricatorUser(); $matches = null; if (preg_match('@/([a-zA-Z0-9]+)$@', $user_data['link'], $matches)) { $user->setUsername($matches[1]); } if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = 'Required'; $errors[] = 'Username is required.'; } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { $e_username = 'Invalid'; $errors[] = 'Username may only contain letters and numbers.'; } $user->setUsername($username); $user->setFacebookUID($user_id); $user->setEmail($user_data['email']); if (!$errors) { $image = @file_get_contents('https://graph.facebook.com/me/picture?access_token='.$token); $file = PhabricatorFile::newFromFileData( $image, array( 'name' => 'fbprofile.jpg' )); $user->setProfileImagePHID($file->getPHID()); $user->setRealName($user_data['name']); try { $user->save(); $session_key = $user->establishSession('web'); $request->setCookie('phusr', $user->getUsername()); $request->setCookie('phsid', $session_key); return id(new AphrontRedirectResponse())->setURI('/'); } catch (AphrontQueryDuplicateKeyException $exception) { $key = $exception->getDuplicateKey(); if ($key == 'userName') { $e_username = 'Duplicate'; $errors[] = 'That username is not unique.'; } else { throw $exception; } } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); - $error_view->setTitle('Facebook Connect Failed'); + $error_view->setTitle('Facebook Auth Failed'); $error_view->setErrors($errors); } $form = new AphrontFormView(); $form ->addHiddenInput('token', $token) ->setUser($request->getUser()) - ->setAction('/facebook-connect/') + ->setAction('/facebook-auth/') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username') ->setName('username') ->setValue($user->getUsername()) ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Create Account')); $panel = new AphrontPanelView(); $panel->setHeader('Create New Account'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => 'Create New Account', )); } } diff --git a/src/applications/auth/controller/facebookconnect/__init__.php b/src/applications/auth/controller/facebookauth/__init__.php similarity index 71% rename from src/applications/auth/controller/facebookconnect/__init__.php rename to src/applications/auth/controller/facebookauth/__init__.php index 7c02260f35..f5b4a8f1a9 100644 --- a/src/applications/auth/controller/facebookconnect/__init__.php +++ b/src/applications/auth/controller/facebookauth/__init__.php @@ -1,22 +1,25 @@ OK'; + $res_no = 'NO'; + $res_na = 'N/A'; + + $results = array(); + + if (!$auth_enabled) { + $results['facebook.auth-enabled'] = array( + $res_no, + 'false', + 'Facebook authentication is disabled in the configuration. Edit the '. + 'environmental configuration to enable "facebook.auth-enabled".'); + } else { + $results['facebook.auth-enabled'] = array( + $res_ok, + 'true', + 'Facebook authentication is enabled.'); + } + + if (!$app_id) { + $results['facebook.application-id'] = array( + $res_no, + null, + 'No Facebook Application ID is configured. Edit the environmental '. + 'configuration to specify an application ID in '. + '"facebook.application-id". To generate an ID, sign into Facebook, '. + 'install the "Developer" application, and use it to create a new '. + 'Facebook application.'); + } else { + $results['facebook.application-id'] = array( + $res_ok, + $app_id, + 'Application ID is set.'); + } + + if (!$app_secret) { + $results['facebook.application-secret'] = array( + $res_no, + null, + 'No Facebook Application secret is configured. Edit the environmental '. + 'configuration to specify an Application Secret, in '. + '"facebook.application-secret". You can find the application secret '. + 'in the Facebook "Developer" application on Facebook.'); + } else { + $results['facebook.application-secret'] = array( + $res_ok, + "It's a secret!", + 'Application secret is set.'); + } + + $timeout = stream_context_create( + array( + 'http' => array( + 'ignore_errors' => true, + 'timeout' => 5, + ), + )); + $timeout_strict = stream_context_create( + array( + 'http' => array( + 'timeout' => 5, + ), + )); + + $internet = @file_get_contents("http://google.com/", false, $timeout); + if ($internet === false) { + $results['internet'] = array( + $res_no, + null, + 'Unable to make an HTTP request to Google. Check your outbound '. + 'internet connection and firewall/filtering settings.'); + } else { + $results['internet'] = array( + $res_ok, + null, + 'Internet seems OK.'); + } + + $facebook = @file_get_contents("http://facebook.com/", false, $timeout); + if ($facebook === false) { + $results['facebook.com'] = array( + $res_no, + null, + 'Unable to make an HTTP request to facebook.com. Facebook may be '. + 'down or inaccessible.'); + } else { + $results['facebook.com'] = array( + $res_ok, + null, + 'Made a request to facebook.com.'); + } + + $graph = @file_get_contents( + "https://graph.facebook.com/me", + false, + $timeout); + if ($graph === false) { + $results['Facebook Graph'] = array( + $res_no, + null, + "Unable to make an HTTPS request to graph.facebook.com. ". + "The Facebook graph may be down or inaccessible."); + } else { + $results['Facebook Graph'] = array( + $res_ok, + null, + 'Made a request to graph.facebook.com.'); + } + + $test_uri = new PhutilURI('https://graph.facebook.com/oauth/access_token'); + $test_uri->setQueryParams( + array( + 'client_id' => $app_id, + 'client_secret' => $app_secret, + 'grant_type' => 'client_credentials', + )); + + $token_value = @file_get_contents($test_uri, false, $timeout); + $token_strict = @file_get_contents($test_uri, false, $timeout_strict); + if ($token_value === false) { + $results['App Login'] = array( + $res_no, + null, + "Unable to perform an application login with your Application ID and ". + "Application Secret. You may have mistyped or misconfigured them; ". + "Facebook may have revoked your authorization; or Facebook may be ". + "having technical problems."); + } else { + if ($token_strict) { + $results['App Login'] = array( + $res_ok, + $token_strict, + "Raw application login to Facebook works."); + } else { + $data = json_decode($token_value, true); + if (!is_array($data)) { + $results['App Login'] = array( + $res_no, + $token_value, + "Application Login failed but the graph server did not respond ". + "with valid JSON error information. Facebook may be experiencing ". + "technical problems."); + } else { + $results['App Login'] = array( + $res_no, + null, + "Application Login failed with error: ".$token_value); + } + } + } + + return $this->renderResults($results); + } + + private function renderResults($results) { + + $rows = array(); + foreach ($results as $key => $result) { + $rows[] = array( + phutil_escape_html($key), + $result[0], + phutil_escape_html($result[1]), + phutil_escape_html($result[2]), + ); + } + + $table_view = new AphrontTableView($rows); + $table_view->setHeaders( + array( + 'Test', + 'Result', + 'Value', + 'Details', + )); + $table_view->setColumnClasses( + array( + null, + null, + null, + 'wide', + )); + + $panel_view = new AphrontPanelView(); + $panel_view->setHeader('Facebook Auth Diagnostics'); + $panel_view->appendChild( + '

These tests may be able to '. + 'help diagnose the root cause of problems you experience with '. + 'Facebook Authentication. Reload the page to run the tests again.

'); + $panel_view->appendChild($table_view); + + return $this->buildStandardPageResponse( + $panel_view, + array( + 'title' => 'Facebook Auth Diagnostics', + )); + + } + +} diff --git a/src/applications/auth/controller/facebookauth/diagnostics/__init__.php b/src/applications/auth/controller/facebookauth/diagnostics/__init__.php new file mode 100644 index 0000000000..4603969d31 --- /dev/null +++ b/src/applications/auth/controller/facebookauth/diagnostics/__init__.php @@ -0,0 +1,18 @@ +getRequest(); $error = false; $username = $request->getCookie('phusr'); if ($request->isFormPost()) { $username = $request->getStr('username'); $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username); - $user->setPassword('asdf'); - $user->save(); - $okay = false; if ($user) { if ($user->comparePassword($request->getStr('password'))) { $session_key = $user->establishSession('web'); $request->setCookie('phusr', $user->getUsername()); $request->setCookie('phsid', $session_key); return id(new AphrontRedirectResponse()) ->setURI('/'); } } if (!$okay) { $request->clearCookie('phusr'); $request->clearCookie('phsid'); } $error = true; } $error_view = null; if ($error) { $error_view = new AphrontErrorView(); $error_view->setTitle('Bad username/password.'); } $form = new AphrontFormView(); $form ->setUser($request->getUser()) ->setAction('/login/') ->appendChild( id(new AphrontFormTextControl()) - ->setLabel('Username') + ->setLabel('Username/Email') ->setName('username') ->setValue($username)) ->appendChild( - id(new AphrontFormTextControl()) + id(new AphrontFormPasswordControl()) ->setLabel('Password') - ->setName('password')) + ->setName('password') + ->setCaption( + 'Forgot your password? / Email Login')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Login')); $panel = new AphrontPanelView(); $panel->setHeader('Phabricator Login'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); - - // TODO: Hardcoded junk - $connect_uri = "https://www.facebook.com/dialog/oauth"; - - $user = $request->getUser(); - - $facebook_connect = new AphrontFormView(); - $facebook_connect - ->setAction($connect_uri) - ->addHiddenInput('client_id', 184510521580034) - ->addHiddenInput('redirect_uri', 'http://local.aphront.com/facebook-connect/') - ->addHiddenInput('scope', 'email') - ->addHiddenInput('state', $user->getCSRFToken()) - ->setUser($request->getUser()) - ->setMethod('GET') - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue("Login with Facebook Connect \xC2\xBB")); - - $panel->appendChild('

Login with Facebook

'); - $panel->appendChild($facebook_connect); + $fbauth_enabled = PhabricatorEnv::getEnvConfig('facebook.auth-enabled'); + if ($fbauth_enabled) { + $auth_uri = new PhutilURI("https://www.facebook.com/dialog/oauth"); + + $user = $request->getUser(); + + $redirect_uri = PhabricatorEnv::getURI('/facebook-auth/'); + $app_id = PhabricatorEnv::getEnvConfig('facebook.application-id'); + + // TODO: In theory we should use 'state' to prevent CSRF, but the total + // effect of the CSRF attack is that an attacker can cause a user to login + // to Phabricator if they're already logged into Facebook. This does not + // seem like the most severe threat in the world, and generating CSRF for + // logged-out users is vaugely tricky. + + $facebook_auth = new AphrontFormView(); + $facebook_auth + ->setAction($auth_uri) + ->addHiddenInput('client_id', $app_id) + ->addHiddenInput('redirect_uri', $redirect_uri) + ->addHiddenInput('scope', 'email') + ->setUser($request->getUser()) + ->setMethod('GET') + ->appendChild( + '

Login or register for '. + 'Phabricator using your Facebook account.

') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue("Login with Facebook \xC2\xBB")); + + $panel->appendChild('

Login with Facebook

'); + $panel->appendChild($facebook_auth); + } return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => 'Login', )); } } diff --git a/src/applications/auth/controller/login/__init__.php b/src/applications/auth/controller/login/__init__.php index c3c5e62c3b..85ec5fcd3e 100644 --- a/src/applications/auth/controller/login/__init__.php +++ b/src/applications/auth/controller/login/__init__.php @@ -1,20 +1,22 @@ true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(self::PHID_TYPE); } public function setPassword($password) { $this->setPasswordSalt(md5(mt_rand())); $hash = $this->hashPassword($password); $this->setPasswordHash($hash); return $this; } public function comparePassword($password) { $password = $this->hashPassword($password); return ($password === $this->getPasswordHash()); } private function hashPassword($password) { $password = $this->getUsername(). $password. $this->getPHID(). $this->getPasswordSalt(); for ($ii = 0; $ii < 1000; $ii++) { $password = md5($password); } return $password; } - const CSRF_CYCLE_FREQUENCY = 3600; - - public function getCSRFToken() { - return $this->generateCSRFToken(time()); + const CSRF_CYCLE_FREQUENCY = 3600; + const CSRF_TOKEN_LENGTH = 16; + + const EMAIL_CYCLE_FREQUENCY = 86400; + const EMAIL_TOKEN_LENGTH = 24; + + public function getCSRFToken($offset = 0) { + return $this->generateToken( + time() + (self::CSRF_CYCLE_FREQUENCY * $offset), + self::CSRF_CYCLE_FREQUENCY, + PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), + self::CSRF_TOKEN_LENGTH); } public function validateCSRFToken($token) { for ($ii = -1; $ii <= 1; $ii++) { - $time = time() + (self::CSRF_CYCLE_FREQUENCY * $ii); - $valid = $this->generateCSRFToken($time); + $valid = $this->getCSRFToken($ii); if ($token == $valid) { return true; } } return false; } - private function generateCSRFToken($epoch) { - $time_block = floor($epoch / (60 * 60)); - // TODO: this should be a secret lolol - $key = '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3'; + private function generateToken($epoch, $frequency, $key, $len) { + $time_block = floor($epoch / $frequency); $vec = $this->getPHID().$this->passwordHash.$key.$time_block; - return substr(md5($vec), 0, 16); + return substr(sha1($vec), 0, $len); } public function establishSession($session_type) { $conn_w = $this->establishConnection('w'); $urandom = fopen('/dev/urandom', 'r'); if (!$urandom) { throw new Exception("Failed to open /dev/urandom!"); } + $entropy = fread($urandom, 20); if (strlen($entropy) != 20) { throw new Exception("Failed to read /dev/urandom!"); } $session_key = sha1($entropy); queryfx( $conn_w, 'INSERT INTO phabricator_session '. '(userPHID, type, sessionKey, sessionStart)'. ' VALUES '. '(%s, %s, %s, UNIX_TIMESTAMP()) '. 'ON DUPLICATE KEY UPDATE '. 'sessionKey = VALUES(sessionKey), '. 'sessionStart = VALUES(sessionStart)', $this->getPHID(), $session_type, $session_key); return $session_key; } + public function generateEmailToken($offset = 0) { + return $this->generateToken( + time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), + self::EMAIL_CYCLE_FREQUENCY, + PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(), + self::EMAIL_TOKEN_LENGTH); + } + + public function validateEmailToken($token) { + for ($ii = -1; $ii <= 1; $ii++) { + $valid = $this->generateEmailToken($ii); + if ($token == $valid) { + return true; + } + } + return false; + } + } diff --git a/src/applications/people/storage/user/__init__.php b/src/applications/people/storage/user/__init__.php index 6b89e2b125..ff97486c50 100644 --- a/src/applications/people/storage/user/__init__.php +++ b/src/applications/people/storage/user/__init__.php @@ -1,14 +1,15 @@ path = $data['path']; $this->hash = $data['hash']; $this->package = !empty($data['package']); } public function processRequest() { $path = $this->path; // Sanity checking to keep this from exposing anything sensitive. $path = preg_replace('@(//|\\.\\.)@', '', $path); $matches = null; if (!preg_match('/\.(css|js)$/', $path, $matches)) { throw new Exception("Only CSS and JS resources may be served."); } - + $type = $matches[1]; $root = dirname(phutil_get_library_root('phabricator')); if ($this->package) { $map = CelerityResourceMap::getInstance(); $paths = $map->resolvePackage($this->hash); if (!$paths) { return new Aphront404Response(); } - + try { $data = array(); foreach ($paths as $path) { $data[] = Filesystem::readFile($root.'/webroot/'.$path); } $data = implode("\n\n", $data); } catch (Exception $ex) { return new Aphront404Response(); } } else { try { $data = Filesystem::readFile($root.'/webroot/'.$path); } catch (Exception $ex) { return new Aphront404Response(); } } $response = new AphrontFileResponse(); $response->setContent($data); switch ($type) { case 'css': $response->setMimeType("text/css; charset=utf-8"); break; case 'js': $response->setMimeType("text/javascript; charset=utf-8"); break; } - + $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); return $response; } } diff --git a/src/infratructure/celerity/controller/__init__.php b/src/infrastructure/celerity/controller/__init__.php similarity index 74% rename from src/infratructure/celerity/controller/__init__.php rename to src/infrastructure/celerity/controller/__init__.php index c31e65c785..47494296c7 100644 --- a/src/infratructure/celerity/controller/__init__.php +++ b/src/infrastructure/celerity/controller/__init__.php @@ -1,16 +1,18 @@ resourceMap = $resource_map; return $this; } public function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { continue; } $this->resolveResource($map, $symbol); } return $map; } private function resolveResource(array &$map, $symbol) { if (empty($this->resourceMap[$symbol])) { throw new Exception( "Attempting to resolve unknown resource, '{$symbol}'."); } $info = $this->resourceMap[$symbol]; foreach ($info['requires'] as $requires) { if (!empty($map[$requires])) { continue; } $this->resolveResource($map, $requires); } $map[$symbol] = $info; } - + public function setPackageMap($package_map) { $this->packageMap = $package_map; } - + public function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $info) { if (isset($handled[$symbol])) { continue; } if (empty($this->packageMap['reverse'][$symbol])) { $packaged[$symbol] = $info; } else { $package = $this->packageMap['reverse'][$symbol]; $package_info = $this->packageMap['packages'][$package]; $packaged[$package_info['name']] = $package_info; foreach ($package_info['symbols'] as $symbol) { $handled[$symbol] = true; } } } return $packaged; } - + public function resolvePackage($package_hash) { $package = idx($this->packageMap['packages'], $package_hash); if (!$package) { return null; } - + $paths = array(); foreach ($package['symbols'] as $symbol) { $paths[] = $this->resourceMap[$symbol]['disk']; } - + return $paths; } } function celerity_register_resource_map(array $map, array $package_map) { $instance = CelerityResourceMap::getInstance(); $instance->setResourceMap($map); $instance->setPackageMap($package_map); } diff --git a/src/infratructure/celerity/map/__init__.php b/src/infrastructure/celerity/map/__init__.php similarity index 82% copy from src/infratructure/celerity/map/__init__.php copy to src/infrastructure/celerity/map/__init__.php index e2362e7274..d34da6d1be 100644 --- a/src/infratructure/celerity/map/__init__.php +++ b/src/infrastructure/celerity/map/__init__.php @@ -1,12 +1,13 @@ initBehavior($behavior, $config); +final class PhabricatorEnv { + private static $env; + + 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 getURI($path) { + return rtrim(self::getEnvConfig('phabricator.base-uri'), '/').$path; + } + } diff --git a/src/infratructure/celerity/map/__init__.php b/src/infrastructure/env/__init__.php similarity index 51% rename from src/infratructure/celerity/map/__init__.php rename to src/infrastructure/env/__init__.php index e2362e7274..ca8608ef16 100644 --- a/src/infratructure/celerity/map/__init__.php +++ b/src/infrastructure/env/__init__.php @@ -1,12 +1,12 @@ label = $label; return $this; } public function getLabel() { return $this->label; } public function setCaption($caption) { $this->caption = $caption; return $this; } public function getCaption() { return $this->caption; } public function setError($error) { $this->error = $error; return $this; } public function getError() { return $this->error; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setValue($value) { $this->value = $value; return $this; } public function getValue() { return $this->value; } public function setDisabled($disabled) { $this->disabled = $disabled; return $this; } public function getDisabled() { return $this->disabled; } abstract protected function renderInput(); abstract protected function getCustomControlClass(); + protected function shouldRender() { + return true; + } + final public function render() { + if (!$this->shouldRender()) { + return null; + } + $custom_class = $this->getCustomControlClass(); if (strlen($this->getLabel())) { $label = ''; } else { $label = null; $custom_class .= ' aphront-form-control-nolabel'; } $input = '
'. $this->renderInput(). '
'; if (strlen($this->getError())) { $error = $this->getError(); if ($error === true) { $error = '*'; } else { $error = "\xC2\xAB ".$error; } $error = '
'. phutil_escape_html($error). '
'; } else { $error = null; } if (strlen($this->getCaption())) { $caption = '
'. - phutil_escape_html($this->getCaption()). + $this->getCaption(). '
'; } else { $caption = null; } return '
'. $error. $label. $input. $caption. '
'. '
'; } } diff --git a/src/view/form/control/checkbox/__init__.php b/src/view/form/control/checkbox/__init__.php index 612300678f..9194f048d6 100644 --- a/src/view/form/control/checkbox/__init__.php +++ b/src/view/form/control/checkbox/__init__.php @@ -1,15 +1,15 @@ initBehavior($behavior, $config); +class AphrontFormPasswordControl extends AphrontFormControl { + + protected function getCustomControlClass() { + return 'aphront-form-control-password'; + } + + protected function renderInput() { + return phutil_render_tag( + 'input', + array( + 'type' => 'password', + 'name' => $this->getName(), + 'value' => $this->getValue(), + 'disabled' => $this->getDisabled() ? 'disabled' : null, + )); } + } diff --git a/src/view/form/control/checkbox/__init__.php b/src/view/form/control/password/__init__.php similarity index 63% copy from src/view/form/control/checkbox/__init__.php copy to src/view/form/control/password/__init__.php index 612300678f..42c6af6b7b 100644 --- a/src/view/form/control/checkbox/__init__.php +++ b/src/view/form/control/password/__init__.php @@ -1,15 +1,14 @@ getStr('recaptcha_challenge_field'); + $response = $request->getStr('recaptcha_response_field'); + $resp = recaptcha_check_answer( + PhabricatorEnv::getEnvConfig('recaptcha.private-key'), + $_SERVER['REMOTE_ADDR'], + $challenge, + $response); + + return (bool)@$resp->is_valid; + } + + protected function renderInput() { + self::requireLib(); + + return recaptcha_get_html( + PhabricatorEnv::getEnvConfig('recaptcha.public-key'), + $error = null, + $use_ssl = false); + } + +} diff --git a/src/view/form/control/checkbox/__init__.php b/src/view/form/control/recaptcha/__init__.php similarity index 50% copy from src/view/form/control/checkbox/__init__.php copy to src/view/form/control/recaptcha/__init__.php index 612300678f..3803deffe0 100644 --- a/src/view/form/control/checkbox/__init__.php +++ b/src/view/form/control/recaptcha/__init__.php @@ -1,15 +1,15 @@ ', where '' ". + "is one of 'development', 'production', or a custom environment."); +} + +$conf = phabricator_read_config_file($env); +$conf['phabricator.env'] = $env; + setup_aphront_basics(); +phutil_require_module('phabricator', 'infrastructure/env'); +PhabricatorEnv::setEnvConfig($conf); + $host = $_SERVER['HTTP_HOST']; $path = $_REQUEST['__path__']; // Based on the host and path, choose which application should serve the // request. The default is the Aphront demo, but you'll want to replace this // with whichever other applications you're running. switch ($host) { default: phutil_require_module('phutil', 'autoload'); phutil_autoload_class('AphrontDefaultApplicationConfiguration'); $application = new AphrontDefaultApplicationConfiguration(); break; } $application->setHost($host); $application->setPath($path); $request = $application->buildRequest(); $application->setRequest($request); list($controller, $uri_data) = $application->buildController(); try { $controller->willBeginExecution(); $controller->willProcessRequest($uri_data); $response = $controller->processRequest(); } catch (AphrontRedirectException $ex) { $response = id(new AphrontRedirectResponse()) ->setURI($ex->getURI()); } catch (Exception $ex) { $response = $application->handleException($ex); } $response = $application->willSendResponse($response); $response->setRequest($request); $response_string = $response->buildResponseString(); $code = $response->getHTTPResponseCode(); if ($code != 200) { header("HTTP/1.0 {$code}"); } $headers = $response->getCacheHeaders(); $headers = array_merge($headers, $response->getHeaders()); foreach ($headers as $header) { list($header, $value) = $header; header("{$header}: {$value}"); } echo $response_string; /** * @group aphront */ function setup_aphront_basics() { $aphront_root = dirname(dirname(__FILE__)); $libraries_root = dirname($aphront_root); ini_set('include_path', ini_get('include_path').':'.$libraries_root.'/'); @include_once 'libphutil/src/__phutil_library_init__.php'; if (!@constant('__LIBPHUTIL__')) { echo "ERROR: Unable to load libphutil. Update your PHP 'include_path' to ". "include the parent directory of libphutil/.\n"; exit(1); } if (!ini_get('date.timezone')) { date_default_timezone_set('America/Los_Angeles'); } phutil_load_library($libraries_root.'/arcanist/src'); phutil_load_library($aphront_root.'/src'); } function __autoload($class_name) { PhutilSymbolLoader::loadClass($class_name); } + + +function phabricator_read_config_file($config) { + $root = dirname(dirname(__FILE__)); + $conf = include $root.'/conf/'.$config.'.conf.php'; + if ($conf === false) { + throw new Exception("Failed to read config file '{$config}'."); + } + return $conf; +} + diff --git a/webroot/rsrc/css/aphront/panel-view.css b/webroot/rsrc/css/aphront/panel-view.css index 1acd2f7560..1aaed03fa4 100644 --- a/webroot/rsrc/css/aphront/panel-view.css +++ b/webroot/rsrc/css/aphront/panel-view.css @@ -1,40 +1,39 @@ /** * @provides aphront-panel-view-css */ .aphront-panel-view { background: #f3f3f3; border: 1px solid #c0c0c0; border-width: 1px 0 0; padding: 1em 2em; margin: 1em 2em; } .aphront-panel-view h1 { font-size: 14px; font-weight: bold; padding: 2px 0 8px; } .aphront-panel-view a.create-button { float: right; } .aphront-panel-view p.aphront-panel-instructions { margin: .5em 2em .75em; font-size: 13px; } .aphront-panel-width-form { width: 720px; margin-right: auto; margin-left: auto; } .aphront-panel-width-wide { width: 1080px; margin-right: auto; margin-left: auto; } - diff --git a/webroot/rsrc/css/aphront/request-failure-view.css b/webroot/rsrc/css/aphront/request-failure-view.css index de13f0a6a0..5d76af5123 100644 --- a/webroot/rsrc/css/aphront/request-failure-view.css +++ b/webroot/rsrc/css/aphront/request-failure-view.css @@ -1,27 +1,41 @@ /** * @provides aphront-request-failure-view-css */ .aphront-request-failure-view { margin: 2em auto; background: #eff2f7; width: 600px; } .aphront-request-failure-view .aphront-request-failure-head { padding: 1em 2em; border-bottom: 1px solid #afb2b7; background: #dfe2e7; } .aphront-request-failure-view .aphront-request-failure-head h1 { font-size: 24px; } .aphront-request-failure-view .aphront-request-failure-body { - padding: 1em 2em; + padding: 1em 2em 1.5em; } .aphront-request-failure-view .aphront-request-failure-body p { - margin: .5em 0 1.25em; + margin: .5em 0; +} + +.aphront-failure-continue { + margin-top: 1.5em; + text-align: right; +} + +.aphront-failure-continue a.button { + margin-left: 1em; +} + +.aphront-request-failure-view ul { + list-style: disc; + margin-left: 3em; }