diff --git a/resources/sql/patches/090.forceuniqueprojectnames.php b/resources/sql/patches/090.forceuniqueprojectnames.php index f8a7508d99..c3881ad011 100644 --- a/resources/sql/patches/090.forceuniqueprojectnames.php +++ b/resources/sql/patches/090.forceuniqueprojectnames.php @@ -1,107 +1,107 @@ openTransaction(); $table->beginReadLocking(); $projects = $table->loadAll(); $slug_map = array(); foreach ($projects as $project) { $project->setPhrictionSlug($project->getName()); $slug = $project->getPhrictionSlug(); if ($slug == '/') { $project_id = $project->getID(); echo "Project #{$project_id} doesn't have a meaningful name...\n"; $project->setName(trim('Unnamed Project '.$project->getName())); } $slug_map[$slug][] = $project->getID(); } foreach ($slug_map as $slug => $similar) { if (count($similar) <= 1) { continue; } echo "Too many projects are similar to '{$slug}'...\n"; foreach (array_slice($similar, 1, null, true) as $key => $project_id) { $project = $projects[$project_id]; $old_name = $project->getName(); $new_name = rename_project($project, $projects); echo "Renaming project #{$project_id} ". "from '{$old_name}' to '{$new_name}'.\n"; $project->setName($new_name); } } $update = $projects; while ($update) { $size = count($update); foreach ($update as $key => $project) { $id = $project->getID(); $name = $project->getName(); $project->setPhrictionSlug($name); $slug = $project->getPhrictionSlug(); echo "Updating project #{$id} '{$name}' ({$slug})..."; try { queryfx( $project->establishConnection('w'), 'UPDATE %T SET name = %s, phrictionSlug = %s WHERE id = %d', $project->getTableName(), $name, $slug, $project->getID()); unset($update[$key]); echo "okay.\n"; - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { echo "failed, will retry.\n"; } } if (count($update) == $size) { throw new Exception( 'Failed to make any progress while updating projects. Schema upgrade '. 'has failed. Go manually fix your project names to be unique (they are '. 'probably ridiculous?) and then try again.'); } } $table->endReadLocking(); $table->saveTransaction(); echo "Done.\n"; /** * Rename the project so that it has a unique slug, by appending (2), (3), etc. * to its name. */ function rename_project($project, $projects) { $suffix = 2; while (true) { $new_name = $project->getName().' ('.$suffix.')'; $project->setPhrictionSlug($new_name); $new_slug = $project->getPhrictionSlug(); $okay = true; foreach ($projects as $other) { if ($other->getID() == $project->getID()) { continue; } if ($other->getPhrictionSlug() == $new_slug) { $okay = false; break; } } if ($okay) { break; } else { $suffix++; } } return $new_name; } diff --git a/resources/sql/patches/20130913.maniphest.1.migratesearch.php b/resources/sql/patches/20130913.maniphest.1.migratesearch.php index 7f7be6d505..1f7e7c78f8 100644 --- a/resources/sql/patches/20130913.maniphest.1.migratesearch.php +++ b/resources/sql/patches/20130913.maniphest.1.migratesearch.php @@ -1,212 +1,212 @@ establishConnection('w'); $search_table = new PhabricatorSearchQuery(); $search_conn_w = $search_table->establishConnection('w'); // See T1812. This is an old status constant from the time of this migration. $old_open_status = 0; echo "Updating saved Maniphest queries...\n"; $rows = new LiskRawMigrationIterator($conn_w, 'maniphest_savedquery'); foreach ($rows as $row) { $id = $row['id']; echo "Updating query {$id}...\n"; $data = queryfx_one( $search_conn_w, 'SELECT parameters FROM %T WHERE queryKey = %s', $search_table->getTableName(), $row['queryKey']); if (!$data) { echo "Unable to locate query data.\n"; continue; } $data = json_decode($data['parameters'], true); if (!is_array($data)) { echo "Unable to decode query data.\n"; continue; } if (idx($data, 'view') != 'custom') { echo "Query is not a custom query.\n"; continue; } $new_data = array( 'limit' => 1000, ); if (isset($data['lowPriority']) || isset($data['highPriority'])) { $lo = idx($data, 'lowPriority'); $hi = idx($data, 'highPriority'); $priorities = array(); $all = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($all as $pri => $name) { if (($lo !== null) && ($pri < $lo)) { continue; } if (($hi !== null) && ($pri > $hi)) { continue; } $priorities[] = $pri; } if (count($priorities) != count($all)) { $new_data['priorities'] = $priorities; } } foreach ($data as $key => $value) { switch ($key) { case 'fullTextSearch': if (strlen($value)) { $new_data['fulltext'] = $value; } break; case 'userPHIDs': // This was (I think?) one-off data provied to specific hard-coded // queries. break; case 'projectPHIDs': foreach ($value as $k => $v) { if ($v === null || $v === ManiphestTaskOwner::PROJECT_NO_PROJECT) { $new_data['withNoProject'] = true; unset($value[$k]); break; } } if ($value) { $new_data['allProjectPHIDs'] = $value; } break; case 'anyProjectPHIDs': if ($value) { $new_data['anyProjectPHIDs'] = $value; } break; case 'anyUserProjectPHIDs': if ($value) { $new_data['userProjectPHIDs'] = $value; } break; case 'excludeProjectPHIDs': if ($value) { $new_data['excludeProjectPHIDs'] = $value; } break; case 'ownerPHIDs': foreach ($value as $k => $v) { if ($v === null || $v === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $new_data['withUnassigned'] = true; unset($value[$k]); break; } } if ($value) { $new_data['assignedPHIDs'] = $value; } break; case 'authorPHIDs': if ($value) { $new_data['authorPHIDs'] = $value; } break; case 'taskIDs': if ($value) { $new_data['ids'] = $value; } break; case 'status': $include_open = !empty($value['open']); $include_closed = !empty($value['closed']); if ($include_open xor $include_closed) { if ($include_open) { $new_data['statuses'] = array( $old_open_status, ); } else { $statuses = array(); foreach (ManiphestTaskStatus::getTaskStatusMap() as $status => $n) { if ($status != $old_open_status) { $statuses[] = $status; } } $new_data['statuses'] = $statuses; } } break; case 'order': $map = array( 'priority' => 'priority', 'updated' => 'updated', 'created' => 'created', 'title' => 'title', ); if (isset($map[$value])) { $new_data['order'] = $map[$value]; } else { $new_data['order'] = 'priority'; } break; case 'group': $map = array( 'priority' => 'priority', 'owner' => 'assigned', 'status' => 'status', 'project' => 'project', 'none' => 'none', ); if (isset($map[$value])) { $new_data['group'] = $map[$value]; } else { $new_data['group'] = 'priority'; } break; } } $saved = id(new PhabricatorSavedQuery()) ->setEngineClassName('ManiphestTaskSearchEngine'); foreach ($new_data as $key => $value) { $saved->setParameter($key, $value); } try { $saved->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore this, we just have duplicate saved queries. } $named = id(new PhabricatorNamedQuery()) ->setEngineClassName('ManiphestTaskSearchEngine') ->setQueryKey($saved->getQueryKey()) ->setQueryName($row['name']) ->setUserPHID($row['userPHID']); try { $named->save(); } catch (Exception $ex) { // The user already has this query under another name. This can occur if // the migration runs twice. echo "Failed to save named query.\n"; continue; } echo "OK.\n"; } echo "Done.\n"; diff --git a/src/__tests__/PhabricatorInfrastructureTestCase.php b/src/__tests__/PhabricatorInfrastructureTestCase.php index 759dbb2059..d90e004af4 100644 --- a/src/__tests__/PhabricatorInfrastructureTestCase.php +++ b/src/__tests__/PhabricatorInfrastructureTestCase.php @@ -1,123 +1,123 @@ true, ); } /** * This is more of an acceptance test case instead of a unit test. It verifies * that all symbols can be loaded correctly. It can catch problems like * missing methods in descendants of abstract base classes. */ public function testEverythingImplemented() { id(new PhutilSymbolLoader())->selectAndLoadSymbols(); $this->assertTrue(true); } /** * This is more of an acceptance test case instead of a unit test. It verifies * that all the library map is up-to-date. */ public function testLibraryMap() { $library = phutil_get_current_library_name(); $root = phutil_get_library_root($library); $new_library_map = id(new PhutilLibraryMapBuilder($root)) ->buildMap(); $bootloader = PhutilBootloader::getInstance(); $old_library_map = $bootloader->getLibraryMapWithoutExtensions($library); unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]); $this->assertEqual( $new_library_map, $old_library_map, 'The library map does not appear to be up-to-date. Try '. 'rebuilding the map with `arc liberate`.'); } public function testApplicationsInstalled() { $all = PhabricatorApplication::getAllApplications(); $installed = PhabricatorApplication::getAllInstalledApplications(); $this->assertEqual( count($all), count($installed), 'In test cases, all applications should default to installed.'); } public function testMySQLAgreesWithUsAboutBMP() { // Build a string with every BMP character in it, then insert it into MySQL // and read it back. We expect to get the same string out that we put in, // demonstrating that strings which pass our BMP checks are also valid in // MySQL and no silent data truncation will occur. $buf = ''; for ($ii = 0x01; $ii <= 0x7F; $ii++) { $buf .= chr($ii); } for ($ii = 0xC2; $ii <= 0xDF; $ii++) { for ($jj = 0x80; $jj <= 0xBF; $jj++) { $buf .= chr($ii).chr($jj); } } // NOTE: This is \xE0\xA0\xZZ. for ($ii = 0xE0; $ii <= 0xE0; $ii++) { for ($jj = 0xA0; $jj <= 0xBF; $jj++) { for ($kk = 0x80; $kk <= 0xBF; $kk++) { $buf .= chr($ii).chr($jj).chr($kk); } } } // NOTE: This is \xE1\xZZ\xZZ through \xEF\xZZ\xZZ. for ($ii = 0xE1; $ii <= 0xEF; $ii++) { for ($jj = 0x80; $jj <= 0xBF; $jj++) { for ($kk = 0x80; $kk <= 0xBF; $kk++) { $buf .= chr($ii).chr($jj).chr($kk); } } } $this->assertEqual(194431, strlen($buf)); $this->assertTrue(phutil_is_utf8_with_only_bmp_characters($buf)); $write = id(new HarbormasterScratchTable()) ->setData('all.utf8.bmp') ->setBigData($buf) ->save(); $read = id(new HarbormasterScratchTable())->load($write->getID()); $this->assertEqual($buf, $read->getBigData()); } public function testRejectMySQLBMPQueries() { $table = new HarbormasterScratchTable(); $conn_r = $table->establishConnection('w'); $snowman = "\xE2\x98\x83"; $gclef = "\xF0\x9D\x84\x9E"; qsprintf($conn_r, 'SELECT %B', $snowman); qsprintf($conn_r, 'SELECT %s', $snowman); qsprintf($conn_r, 'SELECT %B', $gclef); $caught = null; try { qsprintf($conn_r, 'SELECT %s', $gclef); - } catch (AphrontQueryCharacterSetException $ex) { + } catch (AphrontCharacterSetQueryException $ex) { $caught = $ex; } - $this->assertTrue($caught instanceof AphrontQueryCharacterSetException); + $this->assertTrue($caught instanceof AphrontCharacterSetQueryException); } } diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 95b124c5e8..5035cae8ad 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,311 +1,311 @@ getResourceURIMapRules() + array( '/~/' => array( '' => 'DarkConsoleController', 'data/(?P[^/]+)/' => 'DarkConsoleDataController', ), ); } protected function getResourceURIMapRules() { $extensions = CelerityResourceController::getSupportedResourceTypes(); $extensions = array_keys($extensions); $extensions = implode('|', $extensions); return array( '/res/' => array( '(?:(?P[0-9]+)T/)?'. '(?P[^/]+)/'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:'.$extensions.'))' => 'CelerityPhabricatorResourceController', ), ); } /** * @phutil-external-symbol class PhabricatorStartup */ public function buildRequest() { $parser = new PhutilQueryStringParser(); $data = array(); // If the request has "multipart/form-data" content, we can't use // PhutilQueryStringParser to parse it, and the raw data supposedly is not // available anyway (according to the PHP documentation, "php://input" is // not available for "multipart/form-data" requests). However, it is // available at least some of the time (see T3673), so double check that // we aren't trying to parse data we won't be able to parse correctly by // examining the Content-Type header. $content_type = idx($_SERVER, 'CONTENT_TYPE'); $is_form_data = preg_match('@^multipart/form-data@i', $content_type); $raw_input = PhabricatorStartup::getRawInput(); if (strlen($raw_input) && !$is_form_data) { $data += $parser->parseQueryString($raw_input); } else if ($_POST) { $data += $_POST; } $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); $request->setCookiePrefix($cookie_prefix); return $request; } public function handleException(Exception $ex) { $request = $this->getRequest(); // For Conduit requests, return a Conduit response. if ($request->isConduit()) { $response = new ConduitAPIResponse(); $response->setErrorCode(get_class($ex)); $response->setErrorInfo($ex->getMessage()); return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } // For non-workflow requests, return a Ajax response. if ($request->isAjax() && !$request->isJavelinWorkflow()) { // Log these; they don't get shown on the client and can be difficult // to debug. phlog($ex); $response = new AphrontAjaxResponse(); $response->setError( array( 'code' => get_class($ex), 'info' => $ex->getMessage(), )); return $response; } $user = $request->getUser(); if (!$user) { // If we hit an exception very early, we won't have a user. $user = new PhabricatorUser(); } if ($ex instanceof PhabricatorSystemActionRateLimitException) { $dialog = id(new AphrontDialogView()) ->setTitle(pht('Slow Down!')) ->setUser($user) ->setErrors(array(pht('You are being rate limited.'))) ->appendParagraph($ex->getMessage()) ->appendParagraph($ex->getRateExplanation()) ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...')); $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) { $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $ex->getFactors(), $ex->getFactorValidationResults(), $user, $request); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Entering High Security')) ->setShortTitle(pht('Security Checkpoint')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) ->setErrors( array( pht( 'You are taking an action which requires you to enter '. 'high security.'), )) ->appendParagraph( pht( 'High security mode helps protect your account from security '. 'threats, like session theft or someone messing with your stuff '. 'while you\'re grabbing a coffee. To enter high security mode, '. 'confirm your credentials.')) ->appendChild($form->buildLayoutView()) ->appendParagraph( pht( 'Your account will remain in high security mode for a short '. 'period of time. When you are finished taking sensitive '. 'actions, you should leave high security.')) ->setSubmitURI($request->getPath()) ->addCancelButton($ex->getCancelURI()) ->addSubmitButton(pht('Enter High Security')); foreach ($request->getPassthroughRequestParameters() as $key => $value) { $dialog->addHiddenInput($key, $value); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorPolicyException) { if (!$user->isLoggedIn()) { // If the user isn't logged in, just give them a login form. This is // probably a generally more useful response than a policy dialog that // they have to click through to get a login form. // // Possibly we should add a header here like "you need to login to see // the thing you are trying to look at". $login_controller = new PhabricatorAuthStartController($request); $auth_app_class = 'PhabricatorAuthApplication'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); $login_controller->setCurrentApplication($auth_app); return $login_controller->processRequest(); } $list = $ex->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = phutil_tag('li', array(), $item); } if ($list) { $list = phutil_tag('ul', array(), $list); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $ex->getRejection()), phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht('Users with the "%s" capability:', $ex->getCapabilityName())), $list, ); $dialog = new AphrontDialogView(); $dialog ->setTitle($ex->getTitle()) ->setClass('aphront-access-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', pht('Close')); } else { $dialog->addCancelButton('/', pht('OK')); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof AphrontUsageException) { $error = new AphrontErrorView(); $error->setTitle($ex->getTitle()); $error->appendChild($ex->getMessage()); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($error); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(500); return $response; } // Always log the unhandled exception. phlog($ex); $class = get_class($ex); $message = $ex->getMessage(); - if ($ex instanceof AphrontQuerySchemaException) { + if ($ex instanceof AphrontSchemaQueryException) { $message .= "\n\n". "NOTE: This usually indicates that the MySQL schema has not been ". "properly upgraded. Run 'bin/storage upgrade' to ensure your ". "schema is up to date."; } if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $trace = id(new AphrontStackTraceView()) ->setUser($user) ->setTrace($ex->getTrace()); } else { $trace = null; } $content = phutil_tag( 'div', array('class' => 'aphront-unhandled-exception'), array( phutil_tag('div', array('class' => 'exception-message'), $message), $trace, )); $dialog = new AphrontDialogView(); $dialog ->setTitle('Unhandled Exception ("'.$class.'")') ->setClass('aphront-exception-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); $response->setHTTPResponseCode(500); return $response; } public function willSendResponse(AphrontResponse $response) { return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, )); } } diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index a481a5df30..b76a27d812 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -1,587 +1,587 @@ accountKey = idx($data, 'akey'); } public function processRequest() { $request = $this->getRequest(); if ($request->getUser()->isLoggedIn()) { return $this->renderError(pht('You are already logged in.')); } $is_setup = false; if (strlen($this->accountKey)) { $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); list($account, $provider, $response) = $result; $is_default = false; } else if ($this->isFirstTimeSetup()) { list($account, $provider, $response) = $this->loadSetupAccount(); $is_default = true; $is_setup = true; } else { list($account, $provider, $response) = $this->loadDefaultAccount(); $is_default = true; } if ($response) { return $response; } if (!$provider->shouldAllowRegistration()) { // TODO: This is a routine error if you click "Login" on an external // auth source which doesn't allow registration. The error should be // more tailored. return $this->renderError( pht( 'The account you are attempting to register with uses an '. 'authentication provider ("%s") which does not allow registration. '. 'An administrator may have recently disabled registration with this '. 'provider.', $provider->getProviderName())); } $user = new PhabricatorUser(); $default_username = $account->getUsername(); $default_realname = $account->getRealName(); $default_email = $account->getEmail(); if (!PhabricatorUserEmail::isValidAddress($default_email)) { $default_email = null; } if ($default_email !== null) { // If the account source provided an email, but it's not allowed by // the configuration, roadblock the user. Previously, we let the user // pick a valid email address instead, but this does not align well with // user expectation and it's not clear the cases it enables are valuable. // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { return $this->renderError( array( pht( 'The account you are attempting to register with has an invalid '. 'email address (%s). This Phabricator install only allows '. 'registration with specific email addresses:', $default_email), phutil_tag('br'), phutil_tag('br'), PhabricatorUserEmail::describeAllowedAddresses(), )); } // If the account source provided an email, but another account already // has that email, just pretend we didn't get an email. // TODO: See T3340. // TODO: See T3472. if ($default_email !== null) { $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $default_email); if ($same_email) { $default_email = null; } } } $profile = id(new PhabricatorRegistrationProfile()) ->setDefaultUsername($default_username) ->setDefaultEmail($default_email) ->setDefaultRealName($default_realname) ->setCanEditUsername(true) ->setCanEditEmail(($default_email === null)) ->setCanEditRealName(true) ->setShouldVerifyEmail(false); $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER; $event_data = array( 'account' => $account, 'profile' => $profile, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $default_username = $profile->getDefaultUsername(); $default_email = $profile->getDefaultEmail(); $default_realname = $profile->getDefaultRealName(); $can_edit_username = $profile->getCanEditUsername(); $can_edit_email = $profile->getCanEditEmail(); $can_edit_realname = $profile->getCanEditRealName(); $must_set_password = $provider->shouldRequireRegistrationPassword(); $can_edit_anything = $profile->getCanEditAnything() || $must_set_password; $force_verify = $profile->getShouldVerifyEmail(); // Automatically verify the administrator's email address during first-time // setup. if ($is_setup) { $force_verify = true; } $value_username = $default_username; $value_realname = $default_realname; $value_email = $default_email; $value_password = null; $errors = array(); $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name'); $e_username = strlen($value_username) ? null : true; $e_realname = $require_real_name ? true : null; $e_email = strlen($value_email) ? null : true; $e_password = true; $e_captcha = true; $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; if ($request->isFormPost() || !$can_edit_anything) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); if ($must_set_password) { $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht('Captcha response is incorrect, try again.'); $e_captcha = pht('Invalid'); } } if ($can_edit_username) { $value_username = $request->getStr('username'); if (!strlen($value_username)) { $e_username = pht('Required'); $errors[] = pht('Username is required.'); } else if (!PhabricatorUser::validateUsername($value_username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } else { $e_username = null; } } if ($must_set_password) { $value_password = $request->getStr('password'); $value_confirm = $request->getStr('confirm'); if (!strlen($value_password)) { $e_password = pht('Required'); $errors[] = pht('You must choose a password.'); } else if ($value_password !== $value_confirm) { $e_password = pht('No Match'); $errors[] = pht('Password and confirmation must match.'); } else if (strlen($value_password) < $min_len) { $e_password = pht('Too Short'); $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); } else if ( PhabricatorCommonPasswords::isCommonPassword($value_password)) { $e_password = pht('Very Weak'); $errors[] = pht( 'Password is pathologically weak. This password is one of the '. 'most common passwords in use, and is extremely easy for '. 'attackers to guess. You must choose a stronger password.'); } else { $e_password = null; } } if ($can_edit_email) { $value_email = $request->getStr('email'); if (!strlen($value_email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($value_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } if ($can_edit_realname) { $value_realname = $request->getStr('realName'); if (!strlen($value_realname) && $require_real_name) { $e_realname = pht('Required'); $errors[] = pht('Real name is required.'); } else { $e_realname = null; } } if (!$errors) { $image = $this->loadProfilePicture($account); if ($image) { $user->setProfileImagePHID($image->getPHID()); } try { if ($force_verify) { $verify_email = true; } else { $verify_email = ($account->getEmailVerified()) && ($value_email === $default_email); } if ($provider->shouldTrustEmails() && $value_email === $default_email) { $verify_email = true; } $email_obj = id(new PhabricatorUserEmail()) ->setAddress($value_email) ->setIsVerified((int)$verify_email); $user->setUsername($value_username); $user->setRealname($value_realname); if ($is_setup) { $must_approve = false; } else { $must_approve = PhabricatorEnv::getEnvConfig( 'auth.require-approval'); } if ($must_approve) { $user->setIsApproved(0); } else { $user->setIsApproved(1); } $user->openTransaction(); $editor = id(new PhabricatorUserEditor()) ->setActor($user); $editor->createNewUser($user, $email_obj); if ($must_set_password) { $envelope = new PhutilOpaqueEnvelope($value_password); $editor->changePassword($user, $envelope); } if ($is_setup) { $editor->makeAdminUser($user, true); } $account->setUserPHID($user->getPHID()); $provider->willRegisterAccount($account); $account->save(); $user->saveTransaction(); if (!$email_obj->getIsVerified()) { $email_obj->sendVerificationEmail($user); } if ($must_approve) { $this->sendWaitingForApprovalEmail($user); } return $this->loginUser($user); - } catch (AphrontQueryDuplicateKeyException $exception) { + } catch (AphrontDuplicateKeyQueryException $exception) { $same_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user->getUserName()); $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $value_email); if ($same_username) { $e_username = pht('Duplicate'); $errors[] = pht('Another user already has that username.'); } if ($same_email) { // TODO: See T3340. $e_email = pht('Duplicate'); $errors[] = pht('Another user already has that email.'); } if (!$same_username && !$same_email) { throw $exception; } } } unset($unguarded); } $form = id(new AphrontFormView()) ->setUser($request->getUser()); if (!$is_default) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('External Account')) ->setValue( id(new PhabricatorAuthAccountView()) ->setUser($request->getUser()) ->setExternalAccount($account) ->setAuthProvider($provider))); } if ($can_edit_username) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Phabricator Username')) ->setValue($value_username) ->setError($e_username)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Password')) ->setName('password') ->setError($e_password) ->setCaption( $min_len ? pht('Minimum length of %d characters.', $min_len) : null)); $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Confirm Password')) ->setName('confirm') ->setError($e_password)); } if ($can_edit_email) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($value_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } if ($can_edit_realname) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realName') ->setValue($value_realname) ->setError($e_realname)); } if ($must_set_password) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); } $submit = id(new AphrontFormSubmitControl()); if ($is_setup) { $submit ->setValue(pht('Create Admin Account')); } else { $submit ->addCancelButton($this->getApplicationURI('start/')) ->setValue(pht('Register Phabricator Account')); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($is_setup) { $crumbs->addTextCrumb(pht('Setup Admin Account')); $title = pht('Welcome to Phabricator'); } else { $crumbs->addTextCrumb(pht('Register')); $crumbs->addTextCrumb($provider->getProviderName()); $title = pht('Phabricator Registration'); } $welcome_view = null; if ($is_setup) { $welcome_view = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setTitle(pht('Welcome to Phabricator')) ->appendChild( pht( 'Installation is complete. Register your administrator account '. 'below to log in. You will be able to configure options and add '. 'other authentication mechanisms (like LDAP or OAuth) later on.')); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form) ->setFormErrors($errors); return $this->buildApplicationPage( array( $crumbs, $welcome_view, $object_box, ), array( 'title' => $title, )); } private function loadDefaultAccount() { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { if (!$candidate_provider->shouldAllowRegistration()) { unset($providers[$key]); continue; } if (!$candidate_provider->isDefaultRegistrationProvider()) { unset($providers[$key]); } } if (!$providers) { $response = $this->renderError( pht( 'There are no configured default registration providers.')); return array($account, $provider, $response); } else if (count($providers) > 1) { $response = $this->renderError( pht( 'There are too many configured default registration providers.')); return array($account, $provider, $response); } $provider = head($providers); $account = $provider->getDefaultExternalAccount(); return array($account, $provider, $response); } private function loadSetupAccount() { $provider = new PhabricatorPasswordAuthProvider(); $provider->attachProviderConfig( id(new PhabricatorAuthProviderConfig()) ->setShouldAllowRegistration(1) ->setShouldAllowLogin(1) ->setIsEnabled(true)); $account = $provider->getDefaultExternalAccount(); $response = null; return array($account, $provider, $response); } private function loadProfilePicture(PhabricatorExternalAccount $account) { $phid = $account->getProfileImagePHID(); if (!$phid) { return null; } // NOTE: Use of omnipotent user is okay here because the registering user // can not control the field value, and we can't use their user object to // do meaningful policy checks anyway since they have not registered yet. // Reaching this means the user holds the account secret key and the // registration secret key, and thus has permission to view the image. $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return null; } try { $xformer = new PhabricatorImageTransformer(); return $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } catch (Exception $ex) { phlog($ex); return null; } } protected function renderError($message) { return $this->renderErrorPage( pht('Registration Failed'), array($message)); } private function sendWaitingForApprovalEmail(PhabricatorUser $user) { $title = '[Phabricator] '.pht( 'New User "%s" Awaiting Approval', $user->getUsername()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection( pht( 'Newly registered user "%s" is awaiting account approval by an '. 'administrator.', $user->getUsername())); $body->addTextSection( pht('APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/people/query/approval/')); $body->addTextSection( pht('DISABLE APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/config/edit/auth.require-approval/')); $admins = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIsAdmin(true) ->execute(); if (!$admins) { return; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(mpull($admins, 'getPHID')) ->setSubject($title) ->setBody($body->render()) ->saveAndSend(); } } diff --git a/src/applications/config/check/PhabricatorSetupCheckDatabase.php b/src/applications/config/check/PhabricatorSetupCheckDatabase.php index 9d2b9c731e..0361acdc4c 100644 --- a/src/applications/config/check/PhabricatorSetupCheckDatabase.php +++ b/src/applications/config/check/PhabricatorSetupCheckDatabase.php @@ -1,142 +1,142 @@ getUser(); $conn_pass = $conf->getPassword(); $conn_host = $conf->getHost(); $conn_port = $conf->getPort(); ini_set('mysql.connect_timeout', 2); $config = array( 'user' => $conn_user, 'pass' => $conn_pass, 'host' => $conn_host, 'port' => $conn_port, 'database' => null, ); $conn_raw = PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array($config)); try { queryfx($conn_raw, 'SELECT 1'); - } catch (AphrontQueryConnectionException $ex) { + } catch (AphrontConnectionQueryException $ex) { $message = pht( "Unable to connect to MySQL!\n\n". "%s\n\n". "Make sure Phabricator and MySQL are correctly configured.", $ex->getMessage()); $this->newIssue('mysql.connect') ->setName(pht('Can Not Connect to MySQL')) ->setMessage($message) ->setIsFatal(true) ->addRelatedPhabricatorConfig('mysql.host') ->addRelatedPhabricatorConfig('mysql.port') ->addRelatedPhabricatorConfig('mysql.user') ->addRelatedPhabricatorConfig('mysql.pass'); return; } $engines = queryfx_all($conn_raw, 'SHOW ENGINES'); $engines = ipull($engines, 'Support', 'Engine'); $innodb = idx($engines, 'InnoDB'); if ($innodb != 'YES' && $innodb != 'DEFAULT') { $message = pht( "The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ". "your MySQL configuration.". "\n\n". "(If you aleady created tables, MySQL incorrectly used some other ". "engine to create them. You need to convert them or drop and ". "reinitialize them.)"); $this->newIssue('mysql.innodb') ->setName(pht('MySQL InnoDB Engine Not Available')) ->setMessage($message) ->setIsFatal(true); return; } $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace'); $databases = queryfx_all($conn_raw, 'SHOW DATABASES'); $databases = ipull($databases, 'Database', 'Database'); if (empty($databases[$namespace.'_meta_data'])) { $message = pht( 'Run the storage upgrade script to setup Phabricator\'s database '. 'schema.'); $this->newIssue('storage.upgrade') ->setName(pht('Setup MySQL Schema')) ->setMessage($message) ->setIsFatal(true) ->addCommand(hsprintf('phabricator/ $ ./bin/storage upgrade')); } else { $config['database'] = $namespace.'_meta_data'; $conn_meta = PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array($config)); $applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status'); $applied = ipull($applied, 'patch', 'patch'); $all = PhabricatorSQLPatchList::buildAllPatches(); $diff = array_diff_key($all, $applied); if ($diff) { $this->newIssue('storage.patch') ->setName(pht('Upgrade MySQL Schema')) ->setMessage(pht( "Run the storage upgrade script to upgrade Phabricator's database ". "schema. Missing patches:
%s
", phutil_implode_html(phutil_tag('br'), array_keys($diff)))) ->addCommand( hsprintf('phabricator/ $ ./bin/storage upgrade')); } } $host = PhabricatorEnv::getEnvConfig('mysql.host'); $matches = null; if (preg_match('/^([^:]+):(\d+)$/', $host, $matches)) { $host = $matches[1]; $port = $matches[2]; $this->newIssue('storage.mysql.hostport') ->setName(pht('Deprecated mysql.host Format')) ->setSummary( pht( 'Move port information from `mysql.host` to `mysql.port` in your '. 'config.')) ->setMessage( pht( 'Your `mysql.host` configuration contains a port number, but '. 'this usage is deprecated. Instead, put the port number in '. '`mysql.port`.')) ->addPhabricatorConfig('mysql.host') ->addPhabricatorConfig('mysql.port') ->addCommand( hsprintf( 'phabricator/ $ ./bin/config set mysql.host %s', $host)) ->addCommand( hsprintf( 'phabricator/ $ ./bin/config set mysql.port %s', $port)); } } } diff --git a/src/applications/differential/storage/DifferentialDraft.php b/src/applications/differential/storage/DifferentialDraft.php index 4831d8a5f9..6ad406d3f0 100644 --- a/src/applications/differential/storage/DifferentialDraft.php +++ b/src/applications/differential/storage/DifferentialDraft.php @@ -1,51 +1,51 @@ setObjectPHID($object_phid) ->setAuthorPHID($author_phid) ->setDraftKey($draft_key) ->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { // no worries } } public static function deleteHasDraft( $author_phid, $object_phid, $draft_key) { $draft = id(new DifferentialDraft())->loadOneWhere( 'objectPHID = %s AND authorPHID = %s AND draftKey = %s', $object_phid, $author_phid, $draft_key); if ($draft) { $draft->delete(); } } public static function deleteAllDrafts( $author_phid, $object_phid) { $drafts = id(new DifferentialDraft())->loadAllWhere( 'objectPHID = %s AND authorPHID = %s', $object_phid, $author_phid); foreach ($drafts as $draft) { $draft->delete(); } } } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index 5a701e44c6..b3be8ae643 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,668 +1,668 @@ id = (int)idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $content_type_map = HeraldAdapter::getEnabledAdapterMap($user); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); if ($this->id) { $id = $this->id; $rule = id(new HeraldRuleQuery()) ->setViewer($user) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI("rule/{$id}/"); } else { $rule = new HeraldRule(); $rule->setAuthorPHID($user->getPHID()); $rule->setMustMatchAll(1); $content_type = $request->getStr('content_type'); $rule->setContentType($content_type); $rule_type = $request->getStr('rule_type'); if (!isset($rule_type_map[$rule_type])) { $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; } $rule->setRuleType($rule_type); $adapter = HeraldAdapter::getAdapterForContentType( $rule->getContentType()); if (!$adapter->supportsRuleType($rule->getRuleType())) { throw new Exception( pht( "This rule's content type does not support the selected rule ". "type.")); } if ($rule->isObjectRule()) { $rule->setTriggerObjectPHID($request->getStr('targetPHID')); $object = id(new PhabricatorObjectQuery()) ->setViewer($user) ->withPHIDs(array($rule->getTriggerObjectPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$object) { throw new Exception( pht('No valid object provided for object rule!')); } if (!$adapter->canTriggerOnObject($object)) { throw new Exception( pht('Object is of wrong type for adapter!')); } } $cancel_uri = $this->getApplicationURI(); } if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldManageGlobalRulesCapability::CAPABILITY); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( pht( 'This rule was created with a newer version of Herald. You can not '. 'view or edit it in this older version. Upgrade your Phabricator '. 'deployment.')); } // Upgrade rule version to our version, since we might add newly-defined // conditions, etc. $rule->setConfigVersion($local_version); $rule_conditions = $rule->loadConditions(); $rule_actions = $rule->loadActions(); $rule->attachConditions($rule_conditions); $rule->attachActions($rule_actions); $e_name = true; $errors = array(); if ($request->isFormPost() && $request->getStr('save')) { list($e_name, $errors) = $this->saveRule($adapter, $rule, $request); if (!$errors) { $id = $rule->getID(); $uri = $this->getApplicationURI("rule/{$id}/"); return id(new AphrontRedirectResponse())->setURI($uri); } } $must_match_selector = $this->renderMustMatchSelector($rule); $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); $handles = $this->loadHandlesForRule($rule); require_celerity_resource('herald-css'); $content_type_name = $content_type_map[$rule->getContentType()]; $rule_type_name = $rule_type_map[$rule->getRuleType()]; $form = id(new AphrontFormView()) ->setUser($user) ->setID('herald-rule-edit-form') ->addHiddenInput('content_type', $rule->getContentType()) ->addHiddenInput('rule_type', $rule->getRuleType()) ->addHiddenInput('save', 1) ->appendChild( // Build this explicitly (instead of using addHiddenInput()) // so we can add a sigil to it. javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rule', 'sigil' => 'rule', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Rule Name')) ->setName('name') ->setError($e_name) ->setValue($rule->getName())); $trigger_object_control = false; if ($rule->isObjectRule()) { $trigger_object_control = id(new AphrontFormStaticControl()) ->setValue( pht( 'This rule triggers for %s.', $handles[$rule->getTriggerObjectPHID()]->renderLink())); } $form ->appendChild( id(new AphrontFormMarkupControl()) ->setValue(pht( 'This %s rule triggers for %s.', phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $content_type_name)))) ->appendChild($trigger_object_control) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Conditions')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-condition', 'mustcapture' => true ), pht('New Condition'))) ->setDescription( pht('When %s these conditions are met:', $must_match_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-conditions', 'class' => 'herald-condition-table' ), ''))) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Action')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'create-action', 'mustcapture' => true, ), pht('New Action'))) ->setDescription(pht( 'Take these actions %s this rule matches:', $repetition_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-actions', 'class' => 'herald-action-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Rule')) ->addCancelButton($cancel_uri)); $this->setupEditorBehavior($rule, $handles, $adapter); $title = $rule->getID() ? pht('Edit Herald Rule') : pht('Create Herald Rule'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Edit Rule'), )); } private function saveRule(HeraldAdapter $adapter, $rule, $request) { $rule->setName($request->getStr('name')); $match_all = ($request->getStr('must_match') == 'all'); $rule->setMustMatchAll((int)$match_all); $repetition_policy_param = $request->getStr('repetition_policy'); $rule->setRepetitionPolicy( HeraldRepetitionPolicyConfig::toInt($repetition_policy_param)); $e_name = true; $errors = array(); if (!strlen($rule->getName())) { $e_name = pht('Required'); $errors[] = pht('Rule must have a name.'); } $data = json_decode($request->getStr('rule'), true); if (!is_array($data) || !$data['conditions'] || !$data['actions']) { throw new Exception('Failed to decode rule data.'); } $conditions = array(); foreach ($data['conditions'] as $condition) { if ($condition === null) { // We manage this as a sparse array on the client, so may receive // NULL if conditions have been removed. continue; } $obj = new HeraldCondition(); $obj->setFieldName($condition[0]); $obj->setFieldCondition($condition[1]); if (is_array($condition[2])) { $obj->setValue(array_keys($condition[2])); } else { $obj->setValue($condition[2]); } try { $adapter->willSaveCondition($obj); } catch (HeraldInvalidConditionException $ex) { $errors[] = $ex->getMessage(); } $conditions[] = $obj; } $actions = array(); foreach ($data['actions'] as $action) { if ($action === null) { // Sparse on the client; removals can give us NULLs. continue; } if (!isset($action[1])) { // Legitimate for any action which doesn't need a target, like // "Do nothing". $action[1] = null; } $obj = new HeraldAction(); $obj->setAction($action[0]); $obj->setTarget($action[1]); try { $adapter->willSaveAction($rule, $obj); } catch (HeraldInvalidActionException $ex) { $errors[] = $ex; } $actions[] = $obj; } $rule->attachConditions($conditions); $rule->attachActions($actions); if (!$errors) { try { $edit_action = $rule->getID() ? 'edit' : 'create'; $rule->openTransaction(); $rule->save(); $rule->saveConditions($conditions); $rule->saveActions($actions); $rule->logEdit($request->getUser()->getPHID(), $edit_action); $rule->saveTransaction(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_name = pht('Not Unique'); $errors[] = pht('Rule name is not unique. Choose a unique name.'); } } return array($e_name, $errors); } private function setupEditorBehavior( HeraldRule $rule, array $handles, HeraldAdapter $adapter) { $serial_conditions = array( array('default', 'default', ''), ); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); switch ($condition->getFieldName()) { case HeraldAdapter::FIELD_TASK_PRIORITY: $value_map = array(); $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($value as $priority) { $value_map[$priority] = idx($priority_map, $priority); } $value = $value_map; break; default: if (is_array($value)) { $value_map = array(); foreach ($value as $k => $fbid) { $value_map[$fbid] = $handles[$fbid]->getName(); } $value = $value_map; } break; } $serial_conditions[] = array( $condition->getFieldName(), $condition->getFieldCondition(), $value, ); } } $serial_actions = array( array('default', ''), ); if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { switch ($action->getAction()) { case HeraldAdapter::ACTION_FLAG: case HeraldAdapter::ACTION_BLOCK: $current_value = $action->getTarget(); break; default: if (is_array($action->getTarget())) { $target_map = array(); foreach ((array)$action->getTarget() as $fbid) { $target_map[$fbid] = $handles[$fbid]->getName(); } $current_value = $target_map; } else { $current_value = $action->getTarget(); } break; } $serial_actions[] = array( $action->getAction(), $current_value, ); } } $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); $all_rules = mpull($all_rules, 'getName', 'getPHID'); asort($all_rules); $all_fields = $adapter->getFieldNameMap(); $all_conditions = $adapter->getConditionNameMap(); $all_actions = $adapter->getActionNameMap($rule->getRuleType()); $fields = $adapter->getFields(); $field_map = array_select_keys($all_fields, $fields); // Populate any fields which exist in the rule but which we don't know the // names of, so that saving a rule without touching anything doesn't change // it. foreach ($rule->getConditions() as $condition) { if (empty($field_map[$condition->getFieldName()])) { $field_map[$condition->getFieldName()] = pht(''); } } $actions = $adapter->getActions($rule->getRuleType()); $action_map = array_select_keys($all_actions, $actions); $config_info = array(); $config_info['fields'] = $field_map; $config_info['conditions'] = $all_conditions; $config_info['actions'] = $action_map; foreach ($config_info['fields'] as $field => $name) { $field_conditions = $adapter->getConditionsForField($field); $config_info['conditionMap'][$field] = $field_conditions; } foreach ($config_info['fields'] as $field => $fname) { foreach ($config_info['conditionMap'][$field] as $condition) { $value_type = $adapter->getValueTypeForFieldAndCondition( $field, $condition); $config_info['values'][$field][$condition] = $value_type; } } $config_info['rule_type'] = $rule->getRuleType(); foreach ($config_info['actions'] as $action => $name) { $config_info['targets'][$action] = $adapter->getValueTypeForAction( $action, $rule->getRuleType()); } $changeflag_options = PhabricatorRepositoryPushLog::getHeraldChangeFlagConditionOptions(); Javelin::initBehavior( 'herald-rule-editor', array( 'root' => 'herald-rule-edit-form', 'conditions' => (object)$serial_conditions, 'actions' => (object)$serial_actions, 'select' => array( HeraldAdapter::VALUE_CONTENT_SOURCE => array( 'options' => PhabricatorContentSource::getSourceNameMap(), 'default' => PhabricatorContentSource::SOURCE_WEB, ), HeraldAdapter::VALUE_FLAG_COLOR => array( 'options' => PhabricatorFlagColor::getColorNameMap(), 'default' => PhabricatorFlagColor::COLOR_BLUE, ), HeraldPreCommitRefAdapter::VALUE_REF_TYPE => array( 'options' => array( PhabricatorRepositoryPushLog::REFTYPE_BRANCH => pht('branch (git/hg)'), PhabricatorRepositoryPushLog::REFTYPE_TAG => pht('tag (git)'), PhabricatorRepositoryPushLog::REFTYPE_BOOKMARK => pht('bookmark (hg)'), ), 'default' => PhabricatorRepositoryPushLog::REFTYPE_BRANCH, ), HeraldPreCommitRefAdapter::VALUE_REF_CHANGE => array( 'options' => $changeflag_options, 'default' => PhabricatorRepositoryPushLog::CHANGEFLAG_ADD, ), ), 'template' => $this->buildTokenizerTemplates($handles) + array( 'rules' => $all_rules, ), 'author' => array($rule->getAuthorPHID() => $handles[$rule->getAuthorPHID()]->getName()), 'info' => $config_info, )); } private function loadHandlesForRule($rule) { $phids = array(); foreach ($rule->getActions() as $action) { if (!is_array($action->getTarget())) { continue; } foreach ($action->getTarget() as $target) { $target = (array)$target; foreach ($target as $phid) { $phids[] = $phid; } } } foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { foreach ($value as $phid) { $phids[] = $phid; } } } $phids[] = $rule->getAuthorPHID(); if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $this->loadViewerHandles($phids); } /** * Render the selector for the "When (all of | any of) these conditions are * met:" element. */ private function renderMustMatchSelector($rule) { return AphrontFormSelectControl::renderSelectTag( $rule->getMustMatchAll() ? 'all' : 'any', array( 'all' => pht('all of'), 'any' => pht('any of'), ), array( 'name' => 'must_match', )); } /** * Render the selector for "Take these actions (every time | only the first * time) this rule matches..." element. */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { $repetition_policy = HeraldRepetitionPolicyConfig::toString( $rule->getRepetitionPolicy()); $repetition_options = $adapter->getRepetitionOptions(); $repetition_names = HeraldRepetitionPolicyConfig::getMap(); $repetition_map = array_select_keys($repetition_names, $repetition_options); if (count($repetition_map) < 2) { return head($repetition_names); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, $repetition_map, array( 'name' => 'repetition_policy', )); } } protected function buildTokenizerTemplates(array $handles) { $template = new AphrontTokenizerTemplateView(); $template = $template->render(); $sources = array( 'repository' => new DiffusionRepositoryDatasource(), 'legaldocuments' => new LegalpadDocumentDatasource(), 'taskpriority' => new ManiphestTaskPriorityDatasource(), 'buildplan' => new HarbormasterBuildPlanDatasource(), 'arcanistprojects' => new DiffusionArcanistProjectDatasource(), 'package' => new PhabricatorOwnersPackageDatasource(), 'project' => new PhabricatorProjectDatasource(), 'user' => new PhabricatorPeopleDatasource(), 'email' => new PhabricatorMetaMTAMailableDatasource(), 'userorproject' => new PhabricatorProjectOrUserDatasource(), ); foreach ($sources as $key => $source) { $sources[$key] = array( 'uri' => $source->getDatasourceURI(), 'placeholder' => $source->getPlaceholderText(), ); } return array( 'source' => $sources, 'username' => $this->getRequest()->getUser()->getUserName(), 'icons' => mpull($handles, 'getTypeIcon', 'getPHID'), 'markup' => $template, ); } /** * Load rules for the "Another Herald rule..." condition dropdown, which * allows one rule to depend upon the success or failure of another rule. */ private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); // Any rule can depend on a global rule. $all_rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL)) ->withContentTypes(array($rule->getContentType())) ->execute(); if ($rule->isObjectRule()) { // Object rules may depend on other rules for the same object. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT)) ->withContentTypes(array($rule->getContentType())) ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID())) ->execute(); } if ($rule->isPersonalRule()) { // Personal rules may depend upon your other personal rules. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL)) ->withContentTypes(array($rule->getContentType())) ->withAuthorPHIDs(array($rule->getAuthorPHID())) ->execute(); } // mark disabled rules as disabled since they are not useful as such; // don't filter though to keep edit cases sane / expected foreach ($all_rules as $current_rule) { if ($current_rule->getIsDisabled()) { $current_rule->makeEphemeral(); $current_rule->setName($rule->getName().' '.pht('(Disabled)')); } } // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); return $all_rules; } } diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php index 287d8e6560..79393c7536 100644 --- a/src/applications/macro/controller/PhabricatorMacroEditController.php +++ b/src/applications/macro/controller/PhabricatorMacroEditController.php @@ -1,264 +1,264 @@ id = idx($data, 'id'); } public function processRequest() { $this->requireApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $macro = id(new PhabricatorMacroQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->executeOne(); if (!$macro) { return new Aphront404Response(); } } else { $macro = new PhabricatorFileImageMacro(); $macro->setAuthorPHID($user->getPHID()); } $errors = array(); $e_name = true; $e_file = null; $file = null; $can_fetch = PhabricatorEnv::getEnvConfig('security.allow-outbound-http'); if ($request->isFormPost()) { $original = clone $macro; $new_name = null; if ($request->getBool('name_form') || !$macro->getID()) { $new_name = $request->getStr('name'); $macro->setName($new_name); if (!strlen($macro->getName())) { $errors[] = pht('Macro name is required.'); $e_name = pht('Required'); } else if (!preg_match('/^[a-z0-9:_-]{3,}\z/', $macro->getName())) { $errors[] = pht( 'Macro must be at least three characters long and contain only '. 'lowercase letters, digits, hyphens, colons and underscores.'); $e_name = pht('Invalid'); } else { $e_name = null; } } $file = null; if ($request->getFileExists('file')) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), 'isExplicitUpload' => true, )); } else if ($request->getStr('url')) { try { $file = PhabricatorFile::newFromFileDownload( $request->getStr('url'), array( 'name' => $request->getStr('name'), 'authorPHID' => $user->getPHID(), 'isExplicitUpload' => true, )); } catch (Exception $ex) { $errors[] = pht('Could not fetch URL: %s', $ex->getMessage()); } } else if ($request->getStr('phid')) { $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($request->getStr('phid'))) ->executeOne(); } if ($file) { if (!$file->isViewableInBrowser()) { $errors[] = pht('You must upload an image.'); $e_file = pht('Invalid'); } else { $macro->setFilePHID($file->getPHID()); $macro->attachFile($file); $e_file = null; } } if (!$macro->getID() && !$file) { $errors[] = pht('You must upload an image to create a macro.'); $e_file = pht('Required'); } if (!$errors) { try { $xactions = array(); if ($new_name !== null) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransactionType::TYPE_NAME) ->setNewValue($new_name); } if ($file) { $xactions[] = id(new PhabricatorMacroTransaction()) ->setTransactionType(PhabricatorMacroTransactionType::TYPE_FILE) ->setNewValue($file->getPHID()); } $editor = id(new PhabricatorMacroEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = $editor->applyTransactions($original, $xactions); $view_uri = $this->getApplicationURI('/view/'.$original->getID().'/'); return id(new AphrontRedirectResponse())->setURI($view_uri); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { throw $ex; $errors[] = pht('Macro name is not unique!'); $e_name = pht('Duplicate'); } } } $current_file = null; if ($macro->getFilePHID()) { $current_file = $macro->getFile(); } $form = new AphrontFormView(); $form->addHiddenInput('name_form', 1); $form->setUser($request->getUser()); $form ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($macro->getName()) ->setCaption( pht('This word or phrase will be replaced with the image.')) ->setError($e_name)); if (!$macro->getID()) { if ($current_file) { $current_file_view = id(new PhabricatorFileLinkView()) ->setFilePHID($current_file->getPHID()) ->setFileName($current_file->getName()) ->setFileViewable(true) ->setFileViewURI($current_file->getBestURI()) ->render(); $form->addHiddenInput('phid', $current_file->getPHID()); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Selected File')) ->setValue($current_file_view)); $other_label = pht('Change File'); } else { $other_label = pht('File'); } if ($can_fetch) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url')) ->setError($request->getFileExists('file') ? false : $e_file)); } $form->appendChild( id(new AphrontFormFileControl()) ->setLabel($other_label) ->setName('file') ->setError($request->getStr('url') ? false : $e_file)); } $view_uri = $this->getApplicationURI('/view/'.$macro->getID().'/'); if ($macro->getID()) { $cancel_uri = $view_uri; } else { $cancel_uri = $this->getApplicationURI(); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Image Macro')) ->addCancelButton($cancel_uri)); $crumbs = $this->buildApplicationCrumbs(); if ($macro->getID()) { $title = pht('Edit Image Macro'); $crumb = pht('Edit Macro'); $crumbs->addTextCrumb(pht('Macro "%s"', $macro->getName()), $view_uri); } else { $title = pht('Create Image Macro'); $crumb = pht('Create Macro'); } $crumbs->addTextCrumb($crumb, $request->getRequestURI()); $upload = null; if ($macro->getID()) { $upload_form = id(new AphrontFormView()) ->setEncType('multipart/form-data') ->setUser($request->getUser()); if ($can_fetch) { $upload_form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URL')) ->setName('url') ->setValue($request->getStr('url'))); } $upload_form ->appendChild( id(new AphrontFormFileControl()) ->setLabel(pht('File')) ->setName('file')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Upload File'))); $upload = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Upload New File')) ->setForm($upload_form); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, $upload, ), array( 'title' => $title, )); } } diff --git a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php index 402e16f554..b0a82e7887 100644 --- a/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php +++ b/src/applications/mailinglists/controller/PhabricatorMailingListsEditController.php @@ -1,133 +1,133 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($this->id) { $page_title = pht('Edit Mailing List'); $list = id(new PhabricatorMailingListQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->executeOne(); if (!$list) { return new Aphront404Response(); } } else { $page_title = pht('Create Mailing List'); $list = new PhabricatorMetaMTAMailingList(); } $e_email = true; $e_uri = null; $e_name = true; $errors = array(); $crumbs = $this->buildApplicationCrumbs(); if ($request->isFormPost()) { $list->setName($request->getStr('name')); $list->setEmail($request->getStr('email')); $list->setURI($request->getStr('uri')); $e_email = null; $e_name = null; if (!strlen($list->getEmail())) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } if (!strlen($list->getName())) { $e_name = pht('Required'); $errors[] = pht('Name is required.'); } else if (preg_match('/[ ,]/', $list->getName())) { $e_name = pht('Invalid'); $errors[] = pht('Name must not contain spaces or commas.'); } if ($list->getURI()) { if (!PhabricatorEnv::isValidWebResource($list->getURI())) { $e_uri = pht('Invalid'); $errors[] = pht('Mailing list URI must point to a valid web page.'); } } if (!$errors) { try { $list->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI()); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_email = pht('Duplicate'); $errors[] = pht('Another mailing list already uses that address.'); } } } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($list->getID()) { $form->setAction($this->getApplicationURI('/edit/'.$list->getID().'/')); } else { $form->setAction($this->getApplicationURI('/edit/')); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($list->getEmail()) ->setCaption(pht('Email will be delivered to this address.')) ->setError($e_email)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setError($e_name) ->setCaption(pht('Human-readable display and autocomplete name.')) ->setValue($list->getName())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('URI')) ->setName('uri') ->setError($e_uri) ->setCaption(pht('Optional link to mailing list archives or info.')) ->setValue($list->getURI())) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save')) ->addCancelButton($this->getApplicationURI())); if ($list->getID()) { $crumbs->addTextCrumb(pht('Edit Mailing List')); } else { $crumbs->addTextCrumb(pht('Create Mailing List')); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $page_title, )); } } diff --git a/src/applications/owners/controller/PhabricatorOwnersEditController.php b/src/applications/owners/controller/PhabricatorOwnersEditController.php index aface6a277..10ce29c9a5 100644 --- a/src/applications/owners/controller/PhabricatorOwnersEditController.php +++ b/src/applications/owners/controller/PhabricatorOwnersEditController.php @@ -1,270 +1,270 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $package = id(new PhabricatorOwnersPackage())->load($this->id); if (!$package) { return new Aphront404Response(); } } else { $package = new PhabricatorOwnersPackage(); $package->setPrimaryOwnerPHID($user->getPHID()); } $e_name = true; $e_primary = true; $errors = array(); if ($request->isFormPost()) { $package->setName($request->getStr('name')); $package->setDescription($request->getStr('description')); $old_auditing_enabled = $package->getAuditingEnabled(); $package->setAuditingEnabled( ($request->getStr('auditing') === 'enabled') ? 1 : 0); $primary = $request->getArr('primary'); $primary = reset($primary); $old_primary = $package->getPrimaryOwnerPHID(); $package->setPrimaryOwnerPHID($primary); $owners = $request->getArr('owners'); if ($primary) { array_unshift($owners, $primary); } $owners = array_unique($owners); $paths = $request->getArr('path'); $repos = $request->getArr('repo'); $excludes = $request->getArr('exclude'); $path_refs = array(); for ($ii = 0; $ii < count($paths); $ii++) { if (empty($paths[$ii]) || empty($repos[$ii])) { continue; } $path_refs[] = array( 'repositoryPHID' => $repos[$ii], 'path' => $paths[$ii], 'excluded' => $excludes[$ii], ); } if (!strlen($package->getName())) { $e_name = pht('Required'); $errors[] = pht('Package name is required.'); } else { $e_name = null; } if (!$package->getPrimaryOwnerPHID()) { $e_primary = pht('Required'); $errors[] = pht('Package must have a primary owner.'); } else { $e_primary = null; } if (!$path_refs) { $errors[] = pht('Package must include at least one path.'); } if (!$errors) { $package->attachUnsavedOwners($owners); $package->attachUnsavedPaths($path_refs); $package->attachOldAuditingEnabled($old_auditing_enabled); $package->attachOldPrimaryOwnerPHID($old_primary); $package->attachActorPHID($user->getPHID()); try { $package->save(); return id(new AphrontRedirectResponse()) ->setURI('/owners/package/'.$package->getID().'/'); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_name = pht('Duplicate'); $errors[] = pht('Package name must be unique.'); } } } else { $owners = $package->loadOwners(); $owners = mpull($owners, 'getUserPHID'); $paths = $package->loadPaths(); $path_refs = array(); foreach ($paths as $path) { $path_refs[] = array( 'repositoryPHID' => $path->getRepositoryPHID(), 'path' => $path->getPath(), 'excluded' => $path->getExcluded(), ); } } $handles = $this->loadViewerHandles($owners); $primary = $package->getPrimaryOwnerPHID(); if ($primary && isset($handles[$primary])) { $handle_primary_owner = array($handles[$primary]); } else { $handle_primary_owner = array(); } $handles_all_owners = array_select_keys($handles, $owners); if ($package->getID()) { $title = pht('Edit Package'); $side_nav_filter = 'edit/'.$this->id; } else { $title = pht('New Package'); $side_nav_filter = 'new'; } $this->setSideNavFilter($side_nav_filter); $repos = id(new PhabricatorRepositoryQuery()) ->setViewer($user) ->execute(); $default_paths = array(); foreach ($repos as $repo) { $default_path = $repo->getDetail('default-owners-path'); if ($default_path) { $default_paths[$repo->getPHID()] = $default_path; } } $repos = mpull($repos, 'getCallsign', 'getPHID'); $template = new AphrontTypeaheadTemplateView(); $template = $template->render(); Javelin::initBehavior( 'owners-path-editor', array( 'root' => 'path-editor', 'table' => 'paths', 'add_button' => 'addpath', 'repositories' => $repos, 'input_template' => $template, 'pathRefs' => $path_refs, 'completeURI' => '/diffusion/services/path/complete/', 'validateURI' => '/diffusion/services/path/validate/', 'repositoryDefaultPaths' => $default_paths, )); require_celerity_resource('owners-path-editor-css'); $cancel_uri = $package->getID() ? '/owners/package/'.$package->getID().'/' : '/owners/'; $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($package->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setLabel(pht('Primary Owner')) ->setName('primary') ->setLimit(1) ->setValue($handle_primary_owner) ->setError($e_primary)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setLabel(pht('Owners')) ->setName('owners') ->setValue($handles_all_owners)) ->appendChild( id(new AphrontFormSelectControl()) ->setName('auditing') ->setLabel(pht('Auditing')) ->setCaption( pht('With auditing enabled, all future commits that touch '. 'this package will be reviewed to make sure an owner '. 'of the package is involved and the commit message has '. 'a valid revision, reviewed by, and author.')) ->setOptions(array( 'disabled' => pht('Disabled'), 'enabled' => pht('Enabled'), )) ->setValue( $package->getAuditingEnabled() ? 'enabled' : 'disabled')) ->appendChild( id(new AphrontFormInsetView()) ->setTitle(pht('Paths')) ->addDivAttributes(array('id' => 'path-editor')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'addpath', 'mustcapture' => true, ), pht('Add New Path'))) ->setDescription( pht('Specify the files and directories which comprise '. 'this package.')) ->setContent(javelin_tag( 'table', array( 'class' => 'owners-path-editor-table', 'sigil' => 'paths', ), ''))) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($package->getDescription())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue(pht('Save Package'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $nav = $this->buildSideNavView(); $nav->appendChild($form_box); return $this->buildApplicationPage( array( $nav, ), array( 'title' => $title, )); } protected function getExtraPackageViews(AphrontSideNavFilterView $view) { if ($this->id) { $view->addFilter('edit/'.$this->id, pht('Edit')); } else { $view->addFilter('new', pht('New')); } } } diff --git a/src/applications/people/controller/PhabricatorPeopleNewController.php b/src/applications/people/controller/PhabricatorPeopleNewController.php index 2eef703485..a766f3fdd3 100644 --- a/src/applications/people/controller/PhabricatorPeopleNewController.php +++ b/src/applications/people/controller/PhabricatorPeopleNewController.php @@ -1,223 +1,223 @@ type = $data['type']; } public function processRequest() { $request = $this->getRequest(); $admin = $request->getUser(); switch ($this->type) { case 'standard': $is_bot = false; break; case 'bot': $is_bot = true; break; default: return new Aphront404Response(); } $user = new PhabricatorUser(); $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name'); $e_username = true; $e_realname = $require_real_name ? true : null; $e_email = true; $errors = array(); $welcome_checked = true; $new_email = null; $request = $this->getRequest(); if ($request->isFormPost()) { $welcome_checked = $request->getInt('welcome'); $user->setUsername($request->getStr('username')); $new_email = $request->getStr('email'); if (!strlen($new_email)) { $errors[] = pht('Email is required.'); $e_email = pht('Required'); } else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } $user->setRealName($request->getStr('realname')); if (!strlen($user->getUsername())) { $errors[] = pht('Username is required.'); $e_username = pht('Required'); } else if (!PhabricatorUser::validateUsername($user->getUsername())) { $errors[] = PhabricatorUser::describeValidUsername(); $e_username = pht('Invalid'); } else { $e_username = null; } if (!strlen($user->getRealName()) && $require_real_name) { $errors[] = pht('Real name is required.'); $e_realname = pht('Required'); } else { $e_realname = null; } if (!$errors) { try { $email = id(new PhabricatorUserEmail()) ->setAddress($new_email) ->setIsVerified(0); // Automatically approve the user, since an admin is creating them. $user->setIsApproved(1); // If the user is a bot, approve their email too. if ($is_bot) { $email->setIsVerified(1); } id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email); if ($is_bot) { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeSystemAgentUser($user, true); } if ($welcome_checked && !$is_bot) { $user->sendWelcomeEmail($admin); } $response = id(new AphrontRedirectResponse()) ->setURI('/p/'.$user->getUsername().'/'); return $response; - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $errors[] = pht('Username and email must be unique.'); $same_username = id(new PhabricatorUser()) ->loadOneWhere('username = %s', $user->getUsername()); $same_email = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $new_email); if ($same_username) { $e_username = pht('Duplicate'); } if ($same_email) { $e_email = pht('Duplicate'); } } } } $form = id(new AphrontFormView()) ->setUser($admin); if ($is_bot) { $form->appendRemarkupInstructions( pht( 'You are creating a new **bot/script** user account.')); } else { $form->appendRemarkupInstructions( pht( 'You are creating a new **standard** user account.')); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Username')) ->setName('username') ->setValue($user->getUsername()) ->setError($e_username)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Real Name')) ->setName('realname') ->setValue($user->getRealName()) ->setError($e_realname)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($new_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); if (!$is_bot) { $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, pht('Send "Welcome to Phabricator" email with login instructions.'), $welcome_checked)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Create User'))); if ($is_bot) { $form ->appendChild(id(new AphrontFormDividerControl())) ->appendRemarkupInstructions( pht( '**Why do bot/script accounts need an email address?**'. "\n\n". 'Although bots do not normally receive email from Phabricator, '. 'they can interact with other systems which require an email '. 'address. Examples include:'. "\n\n". " - If the account takes actions which //send// email, we need ". " an address to use in the //From// header.\n". " - If the account creates commits, Git and Mercurial require ". " an email address for authorship.\n". " - If you send email //to// Phabricator on behalf of the ". " account, the address can identify the sender.\n". " - Some internal authentication functions depend on accounts ". " having an email address.\n". "\n\n". "The address will automatically be verified, so you do not need ". "to be able to receive mail at this address, and can enter some ". "invalid or nonexistent (but correctly formatted) address like ". "`bot@yourcompany.com` if you prefer.")); } $title = pht('Create New User'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/people/controller/PhabricatorPeopleRenameController.php b/src/applications/people/controller/PhabricatorPeopleRenameController.php index 873ddeeb5f..aef56c295e 100644 --- a/src/applications/people/controller/PhabricatorPeopleRenameController.php +++ b/src/applications/people/controller/PhabricatorPeopleRenameController.php @@ -1,122 +1,122 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $admin = $request->getUser(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($admin) ->withIDs(array($this->id)) ->executeOne(); if (!$user) { return new Aphront404Response(); } $profile_uri = '/p/'.$user->getUsername().'/'; id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $admin, $request, $profile_uri); $errors = array(); $v_username = $user->getUsername(); $e_username = true; if ($request->isFormPost()) { $v_username = $request->getStr('username'); if (!strlen($v_username)) { $e_username = pht('Required'); $errors[] = pht('New username is required.'); } else if ($v_username == $user->getUsername()) { $e_username = pht('Invalid'); $errors[] = pht('New username must be different from old username.'); } else if (!PhabricatorUser::validateUsername($v_username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } if (!$errors) { try { id(new PhabricatorUserEditor()) ->setActor($admin) ->changeUsername($user, $v_username); $new_uri = '/p/'.$v_username.'/'; return id(new AphrontRedirectResponse())->setURI($new_uri); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_username = pht('Not Unique'); $errors[] = pht('Another user already has that username.'); } } } $inst1 = pht( 'Be careful when renaming users!'); $inst2 = pht( 'The old username will no longer be tied to the user, so anything '. 'which uses it (like old commit messages) will no longer associate '. 'correctly. (And, if you give a user a username which some other user '. 'used to have, username lookups will begin returning the wrong user.)'); $inst3 = pht( 'It is generally safe to rename newly created users (and test users '. 'and so on), but less safe to rename established users and unsafe to '. 'reissue a username.'); $inst4 = pht( 'Users who rely on password authentication will need to reset their '. 'password after their username is changed (their username is part of '. 'the salt in the password hash).'); $inst5 = pht( 'The user will receive an email notifying them that you changed their '. 'username, with instructions for logging in and resetting their '. 'password if necessary.'); $form = id(new AphrontFormView()) ->setUser($admin) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Old Username')) ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('New Username')) ->setValue($v_username) ->setName('username') ->setError($e_username)); if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Change Username')) ->appendChild($errors) ->appendParagraph($inst1) ->appendParagraph($inst2) ->appendParagraph($inst3) ->appendParagraph($inst4) ->appendParagraph($inst5) ->appendParagraph(null) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Rename User')) ->addCancelButton($profile_uri); } } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index aafb20a7e2..ed038ee59c 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -1,598 +1,598 @@ getID()) { throw new Exception('User has already been created!'); } if ($email->getID()) { throw new Exception('Email has already been created!'); } if (!PhabricatorUser::validateUsername($user->getUsername())) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } // Always set a new user's email address to primary. $email->setIsPrimary(1); // If the primary address is already verified, also set the verified flag // on the user themselves. if ($email->getIsVerified()) { $user->setIsEmailVerified(1); } $this->willAddEmail($email); $user->openTransaction(); try { $user->save(); $email->setUserPHID($user->getPHID()); $email->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { // We might have written the user but failed to write the email; if // so, erase the IDs we attached. $user->setID(null); $user->setPHID(null); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_CREATE); $log->setNewValue($email->getAddress()); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function updateUser( PhabricatorUser $user, PhabricatorUserEmail $email = null) { if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->save(); if ($email) { $email->save(); } $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_EDIT); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function changePassword( PhabricatorUser $user, PhutilOpaqueEnvelope $envelope) { if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->reload(); $user->setPassword($envelope); $user->save(); $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_PASSWORD); $log->save(); $user->saveTransaction(); } /** * @task edit */ public function changeUsername(PhabricatorUser $user, $username) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } $old_username = $user->getUsername(); $user->openTransaction(); $user->reload(); $user->setUsername($username); try { $user->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $user->setUsername($old_username); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_USERNAME); $log->setOldValue($old_username); $log->setNewValue($username); $log->save(); $user->saveTransaction(); $user->sendUsernameChangeEmail($actor, $old_username); } /* -( Editing Roles )------------------------------------------------------ */ /** * @task role */ public function makeAdminUser(PhabricatorUser $user, $admin) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsAdmin() == $admin) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_ADMIN); $log->setOldValue($user->getIsAdmin()); $log->setNewValue($admin); $user->setIsAdmin((int)$admin); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsSystemAgent() == $system_agent) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_SYSTEM_AGENT); $log->setOldValue($user->getIsSystemAgent()); $log->setNewValue($system_agent); $user->setIsSystemAgent((int)$system_agent); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function disableUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsDisabled() == $disable) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_DISABLE); $log->setOldValue($user->getIsDisabled()); $log->setNewValue($disable); $user->setIsDisabled((int)$disable); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function approveUser(PhabricatorUser $user, $approve) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsApproved() == $approve) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_APPROVE); $log->setOldValue($user->getIsApproved()); $log->setNewValue($approve); $user->setIsApproved($approve); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /* -( Adding, Removing and Changing Email )-------------------------------- */ /** * @task email */ public function addEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if ($email->getID()) { throw new Exception('Email has already been created!'); } // Use changePrimaryEmail() to change primary email. $email->setIsPrimary(0); $email->setUserPHID($user->getPHID()); $this->willAddEmail($email); $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); try { $email->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $user->endWriteLocking(); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_ADD); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function removeEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getIsPrimary()) { throw new Exception("Can't remove primary email!"); } if ($email->getUserPHID() != $user->getPHID()) { throw new Exception('Email not owned by user!'); } $email->delete(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_REMOVE); $log->setOldValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); $this->revokePasswordResetLinks($user); return $this; } /** * @task email */ public function changePrimaryEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception('User does not own email!'); } if ($email->getIsPrimary()) { throw new Exception('Email is already primary!'); } if (!$email->getIsVerified()) { throw new Exception('Email is not verified!'); } $old_primary = $user->loadPrimaryEmail(); if ($old_primary) { $old_primary->setIsPrimary(0); $old_primary->save(); } $email->setIsPrimary(1); $email->save(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_PRIMARY); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); if ($old_primary) { $old_primary->sendOldPrimaryEmail($user, $email); } $email->sendNewPrimaryEmail($user); $this->revokePasswordResetLinks($user); return $this; } /** * Verify a user's email address. * * This verifies an individual email address. If the address is the user's * primary address and their account was not previously verified, their * account is marked as email verified. * * @task email */ public function verifyEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception(pht('User does not own email!')); } if (!$email->getIsVerified()) { $email->setIsVerified(1); $email->save(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_VERIFY); $log->setNewValue($email->getAddress()); $log->save(); } if (!$user->getIsEmailVerified()) { // If the user just verified their primary email address, mark their // account as email verified. $user_primary = $user->loadPrimaryEmail(); if ($user_primary->getID() == $email->getID()) { $user->setIsEmailVerified(1); $user->save(); } } $user->endWriteLocking(); $user->saveTransaction(); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function willAddEmail(PhabricatorUserEmail $email) { // Hard check before write to prevent creation of disallowed email // addresses. Normally, the application does checks and raises more // user friendly errors for us, but we omit the courtesy checks on some // pathways like administrative scripts for simplicity. if (!PhabricatorUserEmail::isValidAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeValidAddresses()); } if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeAllowedAddresses()); } } private function revokePasswordResetLinks(PhabricatorUser $user) { // Revoke any outstanding password reset links. If an attacker compromises // an account, changes the email address, and sends themselves a password // reset link, it could otherwise remain live for a short period of time // and allow them to compromise the account again later. PhabricatorAuthTemporaryToken::revokeTokens( $user, array($user->getPHID()), array( PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE, PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE, )); } } diff --git a/src/applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php b/src/applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php index 410d46ae12..c3236b81f7 100644 --- a/src/applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php +++ b/src/applications/people/lipsum/PhabricatorPeopleTestDataGenerator.php @@ -1,105 +1,105 @@ generateRealname(); $username = $this->generateUsername($realname); $email = $this->generateEmail($username); $admin = PhabricatorUser::getOmnipotentUser(); $user = new PhabricatorUser(); $user->setUsername($username); $user->setRealname($realname); $email_object = id(new PhabricatorUserEmail()) ->setAddress($email) ->setIsVerified(1); id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email_object); return $user; - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { } } } protected function generateRealname() { $realname_generator = new PhutilRealnameContextFreeGrammar(); $random_real_name = $realname_generator->generate(); return $random_real_name; } protected function generateUsername($random_real_name) { $name = strtolower($random_real_name); $name = preg_replace('/[^a-z]/s' , ' ', $name); $name = preg_replace('/\s+/', ' ', $name); $words = explode(' ', $name); $random = rand(0, 4); $reduced = ''; if ($random == 0) { foreach ($words as $w) { if ($w == end($words)) { $reduced .= $w; } else { $reduced .= $w[0]; } } } else if ($random == 1) { foreach ($words as $w) { if ($w == $words[0]) { $reduced .= $w; } else { $reduced .= $w[0]; } } } else if ($random == 2) { foreach ($words as $w) { if ($w == $words[0] || $w == end($words)) { $reduced .= $w; } else { $reduced .= $w[0]; } } } else if ($random == 3) { foreach ($words as $w) { if ($w == $words[0] || $w == end($words)) { $reduced .= $w; } else { $reduced .= $w[0].'.'; } } } else if ($random == 4) { foreach ($words as $w) { if ($w == $words[0] || $w == end($words)) { $reduced .= $w; } else { $reduced .= $w[0].'_'; } } } $random1 = rand(0, 4); if ($random1 >= 1) { $reduced = ucfirst($reduced); } $username = $reduced; return $username; } protected function generateEmail($username) { $default_email_domain = 'example.com'; $email = $username.'@'.$default_email_domain; return $email; } } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index ded6f90f2d..4eb91bb098 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -1,193 +1,193 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $submit_button = pht('Save Changes'); $page_title = pht('Edit Blog'); $cancel_uri = $this->getApplicationURI('blog/view/'.$blog->getID().'/'); } else { $blog = id(new PhameBlog()) ->setCreatorPHID($user->getPHID()); $blog->setViewPolicy(PhabricatorPolicies::POLICY_USER); $blog->setEditPolicy(PhabricatorPolicies::POLICY_USER); $blog->setJoinPolicy(PhabricatorPolicies::POLICY_USER); $submit_button = pht('Create Blog'); $page_title = pht('Create Blog'); $cancel_uri = $this->getApplicationURI(); } $e_name = true; $e_custom_domain = null; $errors = array(); if ($request->isFormPost()) { $name = $request->getStr('name'); $description = $request->getStr('description'); $custom_domain = $request->getStr('custom_domain'); $skin = $request->getStr('skin'); if (empty($name)) { $errors[] = pht('You must give the blog a name.'); $e_name = pht('Required'); } else { $e_name = null; } $blog->setName($name); $blog->setDescription($description); $blog->setDomain(nonempty($custom_domain, null)); $blog->setSkin($skin); $blog->setViewPolicy($request->getStr('can_view')); $blog->setEditPolicy($request->getStr('can_edit')); $blog->setJoinPolicy($request->getStr('can_join')); if (!empty($custom_domain)) { list($error_label, $error_text) = $blog->validateCustomDomain($custom_domain); if ($error_label) { $errors[] = $error_text; $e_custom_domain = $error_label; } if ($blog->getViewPolicy() != PhabricatorPolicies::POLICY_PUBLIC) { $errors[] = pht( 'For custom domains to work, the blog must have a view policy of '. 'public.'); // Prefer earlier labels for the multiple error scenario. if (!$e_custom_domain) { $e_custom_domain = pht('Invalid Policy'); } } } // Don't let users remove their ability to edit blogs. PhabricatorPolicyFilter::mustRetainCapability( $user, $blog, PhabricatorPolicyCapability::CAN_EDIT); if (!$errors) { try { $blog->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('blog/view/'.$blog->getID().'/')); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $errors[] = pht('Domain must be unique.'); $e_custom_domain = pht('Not Unique'); } } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($blog) ->execute(); $skins = PhameSkinSpecification::loadAllSkinSpecifications(); $skins = mpull($skins, 'getName'); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($blog->getName()) ->setID('blog-name') ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('description') ->setValue($blog->getDescription()) ->setID('blog-description') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_view')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_edit')) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicyObject($blog) ->setPolicies($policies) ->setName('can_join')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Custom Domain')) ->setName('custom_domain') ->setValue($blog->getDomain()) ->setCaption( pht('Must include at least one dot (.), e.g. blog.example.com')) ->setError($e_custom_domain)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Skin')) ->setName('skin') ->setValue($blog->getSkin()) ->setOptions($skins)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($page_title, $this->getApplicationURI('blog/new')); $nav = $this->renderSideNavFilterView(); $nav->selectFilter($this->id ? null : 'blog/new'); $nav->appendChild( array( $crumbs, $form_box, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, )); } } diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 165ec3894a..6483ae6136 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -1,187 +1,187 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $post = id(new PhamePostQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$post) { return new Aphront404Response(); } $cancel_uri = $this->getApplicationURI('/post/view/'.$this->id.'/'); $submit_button = pht('Save Changes'); $page_title = pht('Edit Post'); } else { $blog = id(new PhameBlogQuery()) ->setViewer($user) ->withIDs(array($request->getInt('blog'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_JOIN, )) ->executeOne(); if (!$blog) { return new Aphront404Response(); } $post = PhamePost::initializePost($user, $blog); $cancel_uri = $this->getApplicationURI('/blog/view/'.$blog->getID().'/'); $submit_button = pht('Save Draft'); $page_title = pht('Create Post'); } $e_phame_title = null; $e_title = true; $errors = array(); if ($request->isFormPost()) { $comments = $request->getStr('comments_widget'); $data = array('comments_widget' => $comments); $phame_title = $request->getStr('phame_title'); $phame_title = PhabricatorSlug::normalize($phame_title); $title = $request->getStr('title'); $post->setTitle($title); $post->setPhameTitle($phame_title); $post->setBody($request->getStr('body')); $post->setConfigData($data); if ($phame_title == '/') { $errors[] = pht('Phame title must be nonempty.'); $e_phame_title = pht('Required'); } if (!strlen($title)) { $errors[] = pht('Title must be nonempty.'); $e_title = pht('Required'); } else { $e_title = null; } if (!$errors) { try { $post->save(); $uri = $this->getApplicationURI('/post/view/'.$post->getID().'/'); return id(new AphrontRedirectResponse())->setURI($uri); - } catch (AphrontQueryDuplicateKeyException $e) { + } catch (AphrontDuplicateKeyQueryException $e) { $e_phame_title = pht('Not Unique'); $errors[] = pht('Another post already uses this slug. '. 'Each post must have a unique slug.'); } } } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($post->getBlogPHID())) ->executeOne(); $form = id(new AphrontFormView()) ->setUser($user) ->addHiddenInput('blog', $request->getInt('blog')) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Blog')) ->setValue($handle->renderLink())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($post->getTitle()) ->setID('post-title') ->setError($e_title)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phame Title')) ->setName('phame_title') ->setValue(rtrim($post->getPhameTitle(), '/')) ->setID('post-phame-title') ->setCaption(pht('Up to 64 alphanumeric characters '. 'with underscores for spaces. '. 'Formatting is enforced.')) ->setError($e_phame_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Body')) ->setName('body') ->setValue($post->getBody()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setID('post-body') ->setUser($user) ->setDisableMacros(true)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Comments Widget')) ->setName('comments_widget') ->setvalue($post->getCommentsWidget()) ->setOptions($post->getCommentsWidgetOptionsForSelect())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $loading = phutil_tag_div( 'aphront-panel-preview-loading-text', pht('Loading preview...')); $preview_panel = phutil_tag_div('aphront-panel-preview', array( phutil_tag_div('phame-post-preview-header', pht('Post Preview')), phutil_tag('div', array('id' => 'post-preview'), $loading), )); require_celerity_resource('phame-css'); Javelin::initBehavior( 'phame-post-preview', array( 'preview' => 'post-preview', 'body' => 'post-body', 'title' => 'post-title', 'phame_title' => 'post-phame-title', 'uri' => '/phame/post/preview/', )); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $page_title, $this->getApplicationURI('/post/view/'.$this->id.'/')); $nav = $this->renderSideNavFilterView(null); $nav->appendChild( array( $crumbs, $form_box, $preview_panel, )); return $this->buildApplicationPage( $nav, array( 'title' => $page_title, )); } } diff --git a/src/applications/phlux/controller/PhluxEditController.php b/src/applications/phlux/controller/PhluxEditController.php index ba1fcd8de5..ac64ffcaa2 100644 --- a/src/applications/phlux/controller/PhluxEditController.php +++ b/src/applications/phlux/controller/PhluxEditController.php @@ -1,183 +1,183 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $is_new = ($this->key === null); if ($is_new) { $var = new PhluxVariable(); $var->setViewPolicy(PhabricatorPolicies::POLICY_USER); $var->setEditPolicy(PhabricatorPolicies::POLICY_USER); } else { $var = id(new PhluxVariableQuery()) ->setViewer($user) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withKeys(array($this->key)) ->executeOne(); if (!$var) { return new Aphront404Response(); } $view_uri = $this->getApplicationURI('/view/'.$this->key.'/'); } $e_key = ($is_new ? true : null); $e_value = true; $errors = array(); $key = $var->getVariableKey(); $display_value = null; $value = $var->getVariableValue(); if ($request->isFormPost()) { if ($is_new) { $key = $request->getStr('key'); if (!strlen($key)) { $errors[] = pht('Variable key is required.'); $e_key = pht('Required'); } else if (!preg_match('/^[a-z0-9.-]+\z/', $key)) { $errors[] = pht( 'Variable key "%s" must contain only lowercase letters, digits, '. 'period, and hyphen.', $key); $e_key = pht('Invalid'); } } $raw_value = $request->getStr('value'); $value = json_decode($raw_value, true); if ($value === null && strtolower($raw_value) !== 'null') { $e_value = pht('Invalid'); $errors[] = pht('Variable value must be valid JSON.'); $display_value = $raw_value; } if (!$errors) { $editor = id(new PhluxVariableEditor()) ->setActor($user) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request); $xactions = array(); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhluxTransaction::TYPE_EDIT_KEY) ->setNewValue($key); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhluxTransaction::TYPE_EDIT_VALUE) ->setNewValue($value); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($request->getStr('viewPolicy')); $xactions[] = id(new PhluxTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($request->getStr('editPolicy')); try { $editor->applyTransactions($var, $xactions); $view_uri = $this->getApplicationURI('/view/'.$key.'/'); return id(new AphrontRedirectResponse())->setURI($view_uri); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_key = pht('Not Unique'); $errors[] = pht('Variable key must be unique.'); } } } if ($display_value === null) { if (is_array($value) && (array_keys($value) !== array_keys(array_values($value)))) { $json = new PhutilJSON(); $display_value = $json->encodeFormatted($value); } else { $display_value = json_encode($value); } } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($var) ->execute(); $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setValue($var->getVariableKey()) ->setLabel(pht('Key')) ->setName('key') ->setError($e_key) ->setCaption(pht('Lowercase letters, digits, dot and hyphen only.')) ->setDisabled(!$is_new)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setValue($display_value) ->setLabel(pht('Value')) ->setName('value') ->setCaption(pht('Enter value as JSON.')) ->setError($e_value)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($var) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($var) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setPolicies($policies)); if ($is_new) { $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Variable'))); } else { $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Variable')) ->addCancelButton($view_uri)); } $crumbs = $this->buildApplicationCrumbs(); if ($is_new) { $title = pht('Create Variable'); $crumbs->addTextCrumb($title, $request->getRequestURI()); } else { $title = pht('Edit %s', $this->key); $crumbs->addTextCrumb($title, $request->getRequestURI()); } $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } } diff --git a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php index f8ec634e4b..b86be02e30 100644 --- a/src/applications/phragment/controller/PhragmentSnapshotCreateController.php +++ b/src/applications/phragment/controller/PhragmentSnapshotCreateController.php @@ -1,168 +1,168 @@ dblob = idx($data, 'dblob', ''); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $parents = $this->loadParentFragments($this->dblob); if ($parents === null) { return new Aphront404Response(); } $fragment = nonempty(last($parents), null); if ($fragment === null) { return new Aphront404Response(); } PhabricatorPolicyFilter::requireCapability( $viewer, $fragment, PhabricatorPolicyCapability::CAN_EDIT); $children = id(new PhragmentFragmentQuery()) ->setViewer($viewer) ->needLatestVersion(true) ->withLeadingPath($fragment->getPath().'/') ->execute(); $errors = array(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); if (strlen($v_name) === 0) { $errors[] = pht('You must specify a name.'); } if (strpos($v_name, '/') !== false) { $errors[] = pht('Snapshot names can not contain "/".'); } if (!count($errors)) { $snapshot = null; try { // Create the snapshot. $snapshot = id(new PhragmentSnapshot()) ->setPrimaryFragmentPHID($fragment->getPHID()) ->setName($v_name) ->save(); - } catch (AphrontQueryDuplicateKeyException $e) { + } catch (AphrontDuplicateKeyQueryException $e) { $errors[] = pht('A snapshot with this name already exists.'); } if (!count($errors)) { // Add the primary fragment. id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($fragment->getPHID()) ->setFragmentVersionPHID($fragment->getLatestVersionPHID()) ->save(); // Add all of the child fragments. foreach ($children as $child) { id(new PhragmentSnapshotChild()) ->setSnapshotPHID($snapshot->getPHID()) ->setFragmentPHID($child->getPHID()) ->setFragmentVersionPHID($child->getLatestVersionPHID()) ->save(); } return id(new AphrontRedirectResponse()) ->setURI('/phragment/snapshot/view/'.$snapshot->getID()); } } } $fragment_sequence = '-'; if ($fragment->getLatestVersion() !== null) { $fragment_sequence = $fragment->getLatestVersion()->getSequence(); } $rows = array(); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('th', array(), 'Fragment'), phutil_tag('th', array(), 'Version'))); $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $fragment->getPath()), phutil_tag('td', array(), $fragment_sequence))); foreach ($children as $child) { $sequence = '-'; if ($child->getLatestVersion() !== null) { $sequence = $child->getLatestVersion()->getSequence(); } $rows[] = phutil_tag( 'tr', array(), array( phutil_tag('td', array(), $child->getPath()), phutil_tag('td', array(), $sequence))); } $table = phutil_tag( 'table', array('class' => 'remarkup-table'), $rows); $container = phutil_tag( 'div', array('class' => 'phabricator-remarkup'), array( phutil_tag( 'p', array(), pht( 'The snapshot will contain the following fragments at '. 'the specified versions: ')), $table)); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Fragment Path')) ->setDisabled(true) ->setValue('/'.$fragment->getPath())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Snapshot Name')) ->setName('name')) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Create Snapshot')) ->addCancelButton( $this->getApplicationURI('browse/'.$fragment->getPath()))) ->appendChild( id(new PHUIFormDividerControl())) ->appendInstructions($container); $crumbs = $this->buildApplicationCrumbsWithPath($parents); $crumbs->addTextCrumb(pht('Create Snapshot')); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create Snapshot of %s', $fragment->getName())) ->setFormErrors($errors) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $this->renderConfigurationWarningIfRequired(), $box), array( 'title' => pht('Create Fragment'), )); } } diff --git a/src/applications/releeph/controller/product/ReleephProductCreateController.php b/src/applications/releeph/controller/product/ReleephProductCreateController.php index 1a410187d0..6168c5fa1e 100644 --- a/src/applications/releeph/controller/product/ReleephProductCreateController.php +++ b/src/applications/releeph/controller/product/ReleephProductCreateController.php @@ -1,162 +1,162 @@ getRequest(); $name = trim($request->getStr('name')); $trunk_branch = trim($request->getStr('trunkBranch')); $arc_pr_id = $request->getInt('arcPrID'); $arc_projects = $this->loadArcProjects(); $e_name = true; $e_trunk_branch = true; $errors = array(); if ($request->isFormPost()) { if (!$name) { $e_name = pht('Required'); $errors[] = pht( 'Your product should have a simple, descriptive name.'); } if (!$trunk_branch) { $e_trunk_branch = pht('Required'); $errors[] = pht( 'You must specify which branch you will be picking from.'); } $arc_project = $arc_projects[$arc_pr_id]; $pr_repository = $arc_project->loadRepository(); if (!$errors) { $releeph_product = id(new ReleephProject()) ->setName($name) ->setTrunkBranch($trunk_branch) ->setRepositoryPHID($pr_repository->getPHID()) ->setArcanistProjectID($arc_project->getID()) ->setCreatedByUserPHID($request->getUser()->getPHID()) ->setIsActive(1); try { $releeph_product->save(); return id(new AphrontRedirectResponse()) ->setURI($releeph_product->getURI()); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_name = pht('Not Unique'); $errors[] = pht('Another product already uses this name.'); } } } $arc_project_options = $this->getArcProjectSelectOptions($arc_projects); $product_name_input = id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setDisableAutocomplete(true) ->setName('name') ->setValue($name) ->setError($e_name) ->setCaption(pht('A name like "Thrift" but not "Thrift releases".')); $arc_project_input = id(new AphrontFormSelectControl()) ->setLabel(pht('Arc Project')) ->setName('arcPrID') ->setValue($arc_pr_id) ->setCaption(pht( 'If your Arc project isn\'t listed, associate it with a repository %s', phutil_tag( 'a', array( 'href' => '/repository/', 'target' => '_blank', ), 'here'))) ->setOptions($arc_project_options); $branch_name_preview = id(new ReleephBranchPreviewView()) ->setLabel(pht('Example Branch')) ->addControl('projectName', $product_name_input) ->addControl('arcProjectID', $arc_project_input) ->addStatic('template', '') ->addStatic('isSymbolic', false); $form = id(new AphrontFormView()) ->setUser($request->getUser()) ->appendChild($product_name_input) ->appendChild($arc_project_input) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Trunk')) ->setName('trunkBranch') ->setValue($trunk_branch) ->setError($e_trunk_branch) ->setCaption(pht('The development branch, '. 'from which requests will be picked.'))) ->appendChild($branch_name_preview) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/releeph/project/') ->setValue(pht('Create Release Product'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Create New Product')) ->setFormErrors($errors) ->setForm($form); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('New Product')); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => pht('Create New Product'), )); } private function loadArcProjects() { $viewer = $this->getRequest()->getUser(); $projects = id(new PhabricatorRepositoryArcanistProjectQuery()) ->setViewer($viewer) ->needRepositories(true) ->execute(); $projects = mfilter($projects, 'getRepository'); $projects = msort($projects, 'getName'); return $projects; } private function getArcProjectSelectOptions(array $arc_projects) { assert_instances_of($arc_projects, 'PhabricatorRepositoryArcanistProject'); $repos = mpull($arc_projects, 'getRepository'); $repos = mpull($repos, null, 'getID'); $groups = array(); foreach ($arc_projects as $arc_project) { $id = $arc_project->getID(); $repo_id = $arc_project->getRepository()->getID(); $groups[$repo_id][$id] = $arc_project->getName(); } $choices = array(); foreach ($groups as $repo_id => $group) { $repo_name = $repos[$repo_id]->getName(); $callsign = $repos[$repo_id]->getCallsign(); $name = "r{$callsign} ({$repo_name})"; $choices[$name] = $group; } ksort($choices); return $choices; } } diff --git a/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php b/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php index a6e8fe0d96..2d8a40c6d4 100644 --- a/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php +++ b/src/applications/repository/conduit/RepositoryCreateConduitAPIMethod.php @@ -1,144 +1,144 @@ formatStringConstants(array('git', 'hg', 'svn')); return array( 'name' => 'required string', 'vcs' => 'required '.$vcs_const, 'callsign' => 'required string', 'description' => 'optional string', 'encoding' => 'optional string', 'tracking' => 'optional bool', 'uri' => 'required string', 'credentialPHID' => 'optional string', 'svnSubpath' => 'optional string', 'branchFilter' => 'optional list', 'closeCommitsFilter' => 'optional list', 'pullFrequency' => 'optional int', 'defaultBranch' => 'optional string', 'heraldEnabled' => 'optional bool, default = true', 'autocloseEnabled' => 'optional bool, default = true', 'svnUUID' => 'optional string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR-PERMISSIONS' => 'You do not have the authority to call this method.', 'ERR-DUPLICATE' => 'Duplicate repository callsign.', 'ERR-BAD-CALLSIGN' => 'Callsign is required and must be ALL UPPERCASE LETTERS.', 'ERR-UNKNOWN-REPOSITORY-VCS' => 'Unknown repository VCS type.', ); } protected function execute(ConduitAPIRequest $request) { $application = id(new PhabricatorApplicationQuery()) ->setViewer($request->getUser()) ->withClasses(array('PhabricatorDiffusionApplication')) ->executeOne(); PhabricatorPolicyFilter::requireCapability( $request->getUser(), $application, DiffusionCreateRepositoriesCapability::CAPABILITY); // TODO: This has some duplication with (and lacks some of the validation // of) the web workflow; refactor things so they can share more code as this // stabilizes. Specifically, this should move to transactions since they // work properly now. $repository = PhabricatorRepository::initializeNewRepository( $request->getUser()); $repository->setName($request->getValue('name')); $callsign = $request->getValue('callsign'); if (!preg_match('/^[A-Z]+\z/', $callsign)) { throw new ConduitException('ERR-BAD-CALLSIGN'); } $repository->setCallsign($callsign); $local_path = PhabricatorEnv::getEnvConfig( 'repository.default-local-path'); $local_path = rtrim($local_path, '/'); $local_path = $local_path.'/'.$callsign.'/'; $vcs = $request->getValue('vcs'); $map = array( 'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT, 'hg' => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL, 'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN, ); if (empty($map[$vcs])) { throw new ConduitException('ERR-UNKNOWN-REPOSITORY-VCS'); } $repository->setVersionControlSystem($map[$vcs]); $repository->setCredentialPHID($request->getValue('credentialPHID')); $remote_uri = $request->getValue('uri'); PhabricatorRepository::assertValidRemoteURI($remote_uri); $details = array( 'encoding' => $request->getValue('encoding'), 'description' => $request->getValue('description'), 'tracking-enabled' => (bool)$request->getValue('tracking', true), 'remote-uri' => $remote_uri, 'local-path' => $local_path, 'branch-filter' => array_fill_keys( $request->getValue('branchFilter', array()), true), 'close-commits-filter' => array_fill_keys( $request->getValue('closeCommitsFilter', array()), true), 'pull-frequency' => $request->getValue('pullFrequency'), 'default-branch' => $request->getValue('defaultBranch'), 'herald-disabled' => !$request->getValue('heraldEnabled', true), 'svn-subpath' => $request->getValue('svnSubpath'), 'disable-autoclose' => !$request->getValue('autocloseEnabled', true), ); foreach ($details as $key => $value) { $repository->setDetail($key, $value); } try { $repository->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { throw new ConduitException('ERR-DUPLICATE'); } return $repository->toDictionary(); } } diff --git a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php index 18b54a3c59..15dbb0f757 100644 --- a/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php +++ b/src/applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php @@ -1,675 +1,675 @@ repairMode = $repair_mode; return $this; } public function getRepairMode() { return $this->repairMode; } /** * @task discovery */ public function discoverCommits() { $repository = $this->getRepository(); $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $refs = $this->discoverSubversionCommits(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $refs = $this->discoverMercurialCommits(); break; case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $refs = $this->discoverGitCommits(); break; default: throw new Exception("Unknown VCS '{$vcs}'!"); } // Clear the working set cache. $this->workingSet = array(); // Record discovered commits and mark them in the cache. foreach ($refs as $ref) { $this->recordCommit( $repository, $ref->getIdentifier(), $ref->getEpoch(), $ref->getCanCloseImmediately(), $ref->getParents()); $this->commitCache[$ref->getIdentifier()] = true; } return $refs; } /* -( Discovering Git Repositories )--------------------------------------- */ /** * @task git */ private function discoverGitCommits() { $repository = $this->getRepository(); if (!$repository->isHosted()) { $this->verifyGitOrigin($repository); } $branches = id(new DiffusionLowLevelGitRefQuery()) ->setRepository($repository) ->withIsOriginBranch(true) ->execute(); if (!$branches) { // This repository has no branches at all, so we don't need to do // anything. Generally, this means the repository is empty. return array(); } $branches = $this->sortBranches($branches); $branches = mpull($branches, 'getCommitIdentifier', 'getShortName'); $this->log( pht( 'Discovering commits in repository %s.', $repository->getCallsign())); $this->fillCommitCache(array_values($branches)); $refs = array(); foreach ($branches as $name => $commit) { $this->log(pht('Examining branch "%s", at "%s".', $name, $commit)); if (!$repository->shouldTrackBranch($name)) { $this->log(pht('Skipping, branch is untracked.')); continue; } if ($this->isKnownCommit($commit)) { $this->log(pht('Skipping, HEAD is known.')); continue; } $this->log(pht('Looking for new commits.')); $branch_refs = $this->discoverStreamAncestry( new PhabricatorGitGraphStream($repository, $commit), $commit, $repository->shouldAutocloseBranch($name)); $this->didDiscoverRefs($branch_refs); $refs[] = $branch_refs; } return array_mergev($refs); } /** * Verify that the "origin" remote exists, and points at the correct URI. * * This catches or corrects some types of misconfiguration, and also repairs * an issue where Git 1.7.1 does not create an "origin" for `--bare` clones. * See T4041. * * @param PhabricatorRepository Repository to verify. * @return void */ private function verifyGitOrigin(PhabricatorRepository $repository) { list($remotes) = $repository->execxLocalCommand( 'remote show -n origin'); $matches = null; if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) { throw new Exception( "Expected 'Fetch URL' in 'git remote show -n origin'."); } $remote_uri = $matches[1]; $expect_remote = $repository->getRemoteURI(); if ($remote_uri == 'origin') { // If a remote does not exist, git pretends it does and prints out a // made up remote where the URI is the same as the remote name. This is // definitely not correct. // Possibly, we should use `git remote --verbose` instead, which does not // suffer from this problem (but is a little more complicated to parse). $valid = false; $exists = false; } else { $normal_type_git = PhabricatorRepositoryURINormalizer::TYPE_GIT; $remote_normal = id(new PhabricatorRepositoryURINormalizer( $normal_type_git, $remote_uri))->getNormalizedPath(); $expect_normal = id(new PhabricatorRepositoryURINormalizer( $normal_type_git, $expect_remote))->getNormalizedPath(); $valid = ($remote_normal == $expect_normal); $exists = true; } if (!$valid) { if (!$exists) { // If there's no "origin" remote, just create it regardless of how // strongly we own the working copy. There is almost no conceivable // scenario in which this could do damage. $this->log( pht( 'Remote "origin" does not exist. Creating "origin", with '. 'URI "%s".', $expect_remote)); $repository->execxLocalCommand( 'remote add origin %P', $repository->getRemoteURIEnvelope()); // NOTE: This doesn't fetch the origin (it just creates it), so we won't // know about origin branches until the next "pull" happens. That's fine // for our purposes, but might impact things in the future. } else { if ($repository->canDestroyWorkingCopy()) { // Bad remote, but we can try to repair it. $this->log( pht( 'Remote "origin" exists, but is pointed at the wrong URI, "%s". '. 'Resetting origin URI to "%s.', $remote_uri, $expect_remote)); $repository->execxLocalCommand( 'remote set-url origin %P', $repository->getRemoteURIEnvelope()); } else { // Bad remote and we aren't comfortable repairing it. $message = pht( 'Working copy at "%s" has a mismatched origin URI, "%s". '. 'The expected origin URI is "%s". Fix your configuration, or '. 'set the remote URI correctly. To avoid breaking anything, '. 'Phabricator will not automatically fix this.', $repository->getLocalPath(), $remote_uri, $expect_remote); throw new Exception($message); } } } } /* -( Discovering Subversion Repositories )-------------------------------- */ /** * @task svn */ private function discoverSubversionCommits() { $repository = $this->getRepository(); if (!$repository->isHosted()) { $this->verifySubversionRoot($repository); } $upper_bound = null; $limit = 1; $refs = array(); do { // Find all the unknown commits on this path. Note that we permit // importing an SVN subdirectory rather than the entire repository, so // commits may be nonsequential. if ($upper_bound === null) { $at_rev = 'HEAD'; } else { $at_rev = ($upper_bound - 1); } try { list($xml, $stderr) = $repository->execxRemoteCommand( 'log --xml --quiet --limit %d %s', $limit, $repository->getSubversionBaseURI($at_rev)); } catch (CommandException $ex) { $stderr = $ex->getStdErr(); if (preg_match('/(path|File) not found/', $stderr)) { // We've gone all the way back through history and this path was not // affected by earlier commits. break; } throw $ex; } $xml = phutil_utf8ize($xml); $log = new SimpleXMLElement($xml); foreach ($log->logentry as $entry) { $identifier = (int)$entry['revision']; $epoch = (int)strtotime((string)$entry->date[0]); $refs[$identifier] = id(new PhabricatorRepositoryCommitRef()) ->setIdentifier($identifier) ->setEpoch($epoch) ->setCanCloseImmediately(true); if ($upper_bound === null) { $upper_bound = $identifier; } else { $upper_bound = min($upper_bound, $identifier); } } // Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially // import large repositories fairly quickly, while pulling only as much // data as we need in the common case (when we've already imported the // repository and are just grabbing one commit at a time). $limit = min($limit * 2, 256); } while ($upper_bound > 1 && !$this->isKnownCommit($upper_bound)); krsort($refs); while ($refs && $this->isKnownCommit(last($refs)->getIdentifier())) { array_pop($refs); } $refs = array_reverse($refs); $this->didDiscoverRefs($refs); return $refs; } private function verifySubversionRoot(PhabricatorRepository $repository) { list($xml) = $repository->execxRemoteCommand( 'info --xml %s', $repository->getSubversionPathURI()); $xml = phutil_utf8ize($xml); $xml = new SimpleXMLElement($xml); $remote_root = (string)($xml->entry[0]->repository[0]->root[0]); $expect_root = $repository->getSubversionPathURI(); $normal_type_svn = PhabricatorRepositoryURINormalizer::TYPE_SVN; $remote_normal = id(new PhabricatorRepositoryURINormalizer( $normal_type_svn, $remote_root))->getNormalizedPath(); $expect_normal = id(new PhabricatorRepositoryURINormalizer( $normal_type_svn, $expect_root))->getNormalizedPath(); if ($remote_normal != $expect_normal) { throw new Exception( pht( 'Repository "%s" does not have a correctly configured remote URI. '. 'The remote URI for a Subversion repository MUST point at the '. 'repository root. The root for this repository is "%s", but the '. 'configured URI is "%s". To resolve this error, set the remote URI '. 'to point at the repository root. If you want to import only part '. 'of a Subversion repository, use the "Import Only" option.', $repository->getCallsign(), $remote_root, $expect_root)); } } /* -( Discovering Mercurial Repositories )--------------------------------- */ /** * @task hg */ private function discoverMercurialCommits() { $repository = $this->getRepository(); $branches = id(new DiffusionLowLevelMercurialBranchesQuery()) ->setRepository($repository) ->execute(); $this->fillCommitCache(mpull($branches, 'getCommitIdentifier')); $refs = array(); foreach ($branches as $branch) { // NOTE: Mercurial branches may have multiple heads, so the names may // not be unique. $name = $branch->getShortName(); $commit = $branch->getCommitIdentifier(); $this->log(pht('Examining branch "%s" head "%s".', $name, $commit)); if (!$repository->shouldTrackBranch($name)) { $this->log(pht('Skipping, branch is untracked.')); continue; } if ($this->isKnownCommit($commit)) { $this->log(pht('Skipping, this head is a known commit.')); continue; } $this->log(pht('Looking for new commits.')); $branch_refs = $this->discoverStreamAncestry( new PhabricatorMercurialGraphStream($repository, $commit), $commit, $close_immediately = true); $this->didDiscoverRefs($branch_refs); $refs[] = $branch_refs; } return array_mergev($refs); } /* -( Internals )---------------------------------------------------------- */ private function discoverStreamAncestry( PhabricatorRepositoryGraphStream $stream, $commit, $close_immediately) { $discover = array($commit); $graph = array(); $seen = array(); // Find all the reachable, undiscovered commits. Build a graph of the // edges. while ($discover) { $target = array_pop($discover); if (empty($graph[$target])) { $graph[$target] = array(); } $parents = $stream->getParents($target); foreach ($parents as $parent) { if ($this->isKnownCommit($parent)) { continue; } $graph[$target][$parent] = true; if (empty($seen[$parent])) { $seen[$parent] = true; $discover[] = $parent; } } } // Now, sort them topographically. $commits = $this->reduceGraph($graph); $refs = array(); foreach ($commits as $commit) { $refs[] = id(new PhabricatorRepositoryCommitRef()) ->setIdentifier($commit) ->setEpoch($stream->getCommitDate($commit)) ->setCanCloseImmediately($close_immediately) ->setParents($stream->getParents($commit)); } return $refs; } private function reduceGraph(array $edges) { foreach ($edges as $commit => $parents) { $edges[$commit] = array_keys($parents); } $graph = new PhutilDirectedScalarGraph(); $graph->addNodes($edges); $commits = $graph->getTopographicallySortedNodes(); // NOTE: We want the most ancestral nodes first, so we need to reverse the // list we get out of AbstractDirectedGraph. $commits = array_reverse($commits); return $commits; } private function isKnownCommit($identifier) { if (isset($this->commitCache[$identifier])) { return true; } if (isset($this->workingSet[$identifier])) { return true; } if ($this->repairMode) { // In repair mode, rediscover the entire repository, ignoring the // database state. We can hit the local cache above, but if we miss it // stop the script from going to the database cache. return false; } $this->fillCommitCache(array($identifier)); return isset($this->commitCache[$identifier]); } private function fillCommitCache(array $identifiers) { if (!$identifiers) { return; } $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'repositoryID = %d AND commitIdentifier IN (%Ls)', $this->getRepository()->getID(), $identifiers); foreach ($commits as $commit) { $this->commitCache[$commit->getCommitIdentifier()] = true; } while (count($this->commitCache) > self::MAX_COMMIT_CACHE_SIZE) { array_shift($this->commitCache); } } /** * Sort branches so we process closeable branches first. This makes the * whole import process a little cheaper, since we can close these commits * the first time through rather than catching them in the refs step. * * @task internal * * @param list List of branch heads. * @return list Sorted list of branch heads. */ private function sortBranches(array $branches) { $repository = $this->getRepository(); $head_branches = array(); $tail_branches = array(); foreach ($branches as $branch) { $name = $branch->getShortName(); if ($repository->shouldAutocloseBranch($name)) { $head_branches[] = $branch; } else { $tail_branches[] = $branch; } } return array_merge($head_branches, $tail_branches); } private function recordCommit( PhabricatorRepository $repository, $commit_identifier, $epoch, $close_immediately, array $parents) { $commit = new PhabricatorRepositoryCommit(); $commit->setRepositoryID($repository->getID()); $commit->setCommitIdentifier($commit_identifier); $commit->setEpoch($epoch); if ($close_immediately) { $commit->setImportStatus(PhabricatorRepositoryCommit::IMPORTED_CLOSEABLE); } $data = new PhabricatorRepositoryCommitData(); $conn_w = $repository->establishConnection('w'); try { // If this commit has parents, look up their IDs. The parent commits // should always exist already. $parent_ids = array(); if ($parents) { $parent_rows = queryfx_all( $conn_w, 'SELECT id, commitIdentifier FROM %T WHERE commitIdentifier IN (%Ls) AND repositoryID = %d', $commit->getTableName(), $parents, $repository->getID()); $parent_map = ipull($parent_rows, 'id', 'commitIdentifier'); foreach ($parents as $parent) { if (empty($parent_map[$parent])) { throw new Exception( pht('Unable to identify parent "%s"!', $parent)); } $parent_ids[] = $parent_map[$parent]; } } else { // Write an explicit 0 so we can distinguish between "really no // parents" and "data not available". if (!$repository->isSVN()) { $parent_ids = array(0); } } $commit->openTransaction(); $commit->save(); $data->setCommitID($commit->getID()); $data->save(); foreach ($parent_ids as $parent_id) { queryfx( $conn_w, 'INSERT IGNORE INTO %T (childCommitID, parentCommitID) VALUES (%d, %d)', PhabricatorRepository::TABLE_PARENTS, $commit->getID(), $parent_id); } $commit->saveTransaction(); $this->insertTask($repository, $commit); queryfx( $conn_w, 'INSERT INTO %T (repositoryID, size, lastCommitID, epoch) VALUES (%d, 1, %d, %d) ON DUPLICATE KEY UPDATE size = size + 1, lastCommitID = IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID), epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)', PhabricatorRepository::TABLE_SUMMARY, $repository->getID(), $commit->getID(), $epoch); if ($this->repairMode) { // Normally, the query should throw a duplicate key exception. If we // reach this in repair mode, we've actually performed a repair. $this->log(pht('Repaired commit "%s".', $commit_identifier)); } PhutilEventEngine::dispatchEvent( new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFUSION_DIDDISCOVERCOMMIT, array( 'repository' => $repository, 'commit' => $commit, ))); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $commit->killTransaction(); // Ignore. This can happen because we discover the same new commit // more than once when looking at history, or because of races or // data inconsistency or cosmic radiation; in any case, we're still // in a good state if we ignore the failure. } } private function didDiscoverRefs(array $refs) { foreach ($refs as $ref) { $this->workingSet[$ref->getIdentifier()] = true; } } private function insertTask( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit, $data = array()) { $vcs = $repository->getVersionControlSystem(); switch ($vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: $class = 'PhabricatorRepositoryGitCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: $class = 'PhabricatorRepositorySvnCommitMessageParserWorker'; break; case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker'; break; default: throw new Exception("Unknown repository type '{$vcs}'!"); } $data['commitID'] = $commit->getID(); PhabricatorWorker::scheduleTask($class, $data); } } diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index c6e0745d26..bad363c36d 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -1,98 +1,98 @@ queryKey = idx($data, 'queryKey'); } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); if ($request->getStr('jump') != 'no') { $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; if ($viewer->loadPreferences($pref_jump, 1)) { $response = PhabricatorJumpNavHandler::getJumpResponse( $viewer, $request->getStr('query')); if ($response) { return $response; } } } $engine = new PhabricatorSearchApplicationSearchEngine(); $engine->setViewer($viewer); // NOTE: This is a little weird. If we're coming from primary search, we // load the user's first search filter and overwrite the "query" part of // it, then send them to that result page. This is sort of odd, but lets // users choose a default query like "Open Tasks" in a reasonable way, // with only this piece of somewhat-sketchy code. See discussion in T4365. if ($request->getBool('search:primary')) { $named_queries = $engine->loadEnabledNamedQueries(); if ($named_queries) { $named = head($named_queries); $query_key = $named->getQueryKey(); $saved = null; if ($engine->isBuiltinQuery($query_key)) { $saved = $engine->buildSavedQueryFromBuiltin($query_key); } else { $saved = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); } if ($saved) { $saved->setParameter('query', $request->getStr('query')); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $saved->setID(null)->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); $results_uri = $engine->getQueryResultsPageURI( $saved->getQueryKey()).'#R'; return id(new AphrontRedirectResponse())->setURI($results_uri); } } } $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->queryKey) ->setSearchEngine($engine) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView($for_app = false) { $viewer = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorSearchApplicationSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index d7327c67d7..adc0b9146e 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,882 +1,882 @@ viewer = $viewer; return $this; } protected function requireViewer() { if (!$this->viewer) { throw new Exception('Call setViewer() before using an engine!'); } return $this->viewer; } public function setContext($context) { $this->context = $context; return $this; } public function isPanelContext() { return ($this->context == self::CONTEXT_PANEL); } public function saveQuery(PhabricatorSavedQuery $query) { $query->setEngineClassName(get_class($this)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } /** * Create a saved query object from the request. * * @param AphrontRequest The search request. * @return PhabricatorSavedQuery */ abstract public function buildSavedQueryFromRequest( AphrontRequest $request); /** * Executes the saved query. * * @param PhabricatorSavedQuery The saved query to operate on. * @return The result of the query. */ abstract public function buildQueryFromSavedQuery( PhabricatorSavedQuery $saved); /** * Builds the search form using the request. * * @param AphrontFormView Form to populate. * @param PhabricatorSavedQuery The query from which to build the form. * @return void */ abstract public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $query); public function getErrors() { return $this->errors; } public function addError($error) { $this->errors[] = $error; return $this; } /** * Return an application URI corresponding to the results page of a query. * Normally, this is something like `/application/query/QUERYKEY/`. * * @param string The query key to build a URI for. * @return string URI where the query can be executed. * @task uri */ public function getQueryResultsPageURI($query_key) { return $this->getURI('query/'.$query_key.'/'); } /** * Return an application URI for query management. This is used when, e.g., * a query deletion operation is cancelled. * * @return string URI where queries can be managed. * @task uri */ public function getQueryManagementURI() { return $this->getURI('query/edit/'); } /** * Return the URI to a path within the application. Used to construct default * URIs for management and results. * * @return string URI to path. * @task uri */ abstract protected function getURI($path); /** * Return a human readable description of the type of objects this query * searches for. * * For example, "Tasks" or "Commits". * * @return string Human-readable description of what this engine is used to * find. */ abstract public function getResultTypeDescription(); public function newSavedQuery() { return id(new PhabricatorSavedQuery()) ->setEngineClassName(get_class($this)); } public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); $menu->newLabel(pht('Queries')); $named_queries = $this->loadEnabledNamedQueries(); foreach ($named_queries as $query) { $key = $query->getQueryKey(); $uri = $this->getQueryResultsPageURI($key); $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); } if ($viewer->isLoggedIn()) { $manage_uri = $this->getQueryManagementURI(); $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); } $menu->newLabel(pht('Search')); $advanced_uri = $this->getQueryResultsPageURI('advanced'); $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->withEngineClassNames(array(get_class($this))) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); $builtin = $this->getBuiltinQueries($viewer); $builtin = mpull($builtin, null, 'getQueryKey'); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin()) { if (isset($builtin[$key])) { $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); unset($builtin[$key]); } else { unset($named_queries[$key]); } } unset($builtin[$key]); } $named_queries = msort($named_queries, 'getSortKey'); return $named_queries + $builtin; } public function loadEnabledNamedQueries() { $named_queries = $this->loadAllNamedQueries(); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { unset($named_queries[$key]); } } return $named_queries; } /* -( Applications )------------------------------------------------------- */ protected function getApplicationURI($path = '') { return $this->getApplication()->getApplicationURI($path); } protected function getApplication() { if (!$this->application) { $class = $this->getApplicationClassName(); $this->application = id(new PhabricatorApplicationQuery()) ->setViewer($this->requireViewer()) ->withClasses(array($class)) ->withInstalled(true) ->executeOne(); if (!$this->application) { throw new Exception( pht( 'Application "%s" is not installed!', $class)); } } return $this->application; } protected function getApplicationClassName() { throw new PhutilMethodNotImplementedException(); } /* -( Constructing Engines )----------------------------------------------- */ /** * Load all available application search engines. * * @return list All available engines. * @task construct */ public static function getAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); return $engines; } /** * Get an engine by class name, if it exists. * * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does * not exist. * @task construct */ public static function getEngineByClassName($class_name) { return idx(self::getAllEngines(), $class_name); } /* -( Builtin Queries )---------------------------------------------------- */ /** * @task builtin */ public function getBuiltinQueries() { $names = $this->getBuiltinQueryNames(); $queries = array(); $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) ->setUserPHID($this->requireViewer()->getPHID()) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) ->setSequence((1 << 24) + $sequence++) ->setIsBuiltin(true); } return $queries; } /** * @task builtin */ public function getBuiltinQuery($query_key) { if (!$this->isBuiltinQuery($query_key)) { throw new Exception("'{$query_key}' is not a builtin!"); } return idx($this->getBuiltinQueries(), $query_key); } /** * @task builtin */ protected function getBuiltinQueryNames() { return array(); } /** * @task builtin */ public function isBuiltinQuery($query_key) { $builtins = $this->getBuiltinQueries(); return isset($builtins[$query_key]); } /** * @task builtin */ public function buildSavedQueryFromBuiltin($query_key) { throw new Exception("Builtin '{$query_key}' is not supported!"); } /* -( Reading Utilities )--------------------------------------------------- */ /** * Read a list of user PHIDs from a request in a flexible way. This method * supports either of these forms: * * users[]=alincoln&users[]=htaft * users=alincoln,htaft * * Additionally, users can be specified either by PHID or by name. * * The main goal of this flexibility is to allow external programs to generate * links to pages (like "alincoln's open revisions") without needing to make * API calls. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @param list Other permitted PHID types. * @return list List of user PHIDs. * * @task read */ protected function readUsersFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); $allow_types = array_fuse($allow_types); $user_type = PhabricatorPHIDConstants::PHID_TYPE_USER; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$type])) { $phids[] = $item; } else { $names[] = $item; } } if ($names) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireViewer()) ->withUsernames($names) ->execute(); foreach ($users as $user) { $phids[] = $user->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of generic PHIDs from a request in a flexible way. Like * @{method:readUsersFromRequest}, this method supports either array or * comma-delimited forms. Objects can be specified either by PHID or by * object name. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @param list Optional, list of permitted PHID types. * @return list List of object PHIDs. * * @task read */ protected function readPHIDsFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->requireViewer()) ->withNames($list) ->execute(); $list = mpull($objects, 'getPHID'); if (!$list) { return array(); } // If only certain PHID types are allowed, filter out all the others. if ($allow_types) { $allow_types = array_fuse($allow_types); foreach ($list as $key => $phid) { if (empty($allow_types[phid_get_type($phid)])) { unset($list[$key]); } } } return $list; } /** * Read a list of items from the request, in either array format or string * format: * * list[]=item1&list[]=item2 * list=item1,item2 * * This provides flexibility when constructing URIs, especially from external * sources. * * @param AphrontRequest Request to read strings from. * @param string Key to read in the request. * @return list List of values. */ protected function readListFromRequest( AphrontRequest $request, $key) { $list = $request->getArr($key, null); if ($list === null) { $list = $request->getStrList($key); } if (!$list) { return array(); } return $list; } protected function readDateFromRequest( AphrontRequest $request, $key) { return id(new AphrontFormDateControl()) ->setUser($this->requireViewer()) ->setName($key) ->setAllowNull(true) ->readValueFromRequest($request); } protected function readBoolFromRequest( AphrontRequest $request, $key) { if (!strlen($request->getStr($key))) { return null; } return $request->getBool($key); } protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) { $value = $query->getParameter($key); if ($value === null) { return $value; } return $value ? 'true' : 'false'; } /* -( Dates )-------------------------------------------------------------- */ /** * @task dates */ protected function parseDateTime($date_time) { if (!strlen($date_time)) { return null; } return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); } /** * @task dates */ protected function buildDateRange( AphrontFormView $form, PhabricatorSavedQuery $saved_query, $start_key, $start_name, $end_key, $end_name) { $start_str = $saved_query->getParameter($start_key); $start = null; if (strlen($start_str)) { $start = $this->parseDateTime($start_str); if (!$start) { $this->addError( pht( '"%s" date can not be parsed.', $start_name)); } } $end_str = $saved_query->getParameter($end_key); $end = null; if (strlen($end_str)) { $end = $this->parseDateTime($end_str); if (!$end) { $this->addError( pht( '"%s" date can not be parsed.', $end_name)); } } if ($start && $end && ($start >= $end)) { $this->addError( pht( '"%s" must be a date before "%s".', $start_name, $end_name)); } $form ->appendChild( id(new PHUIFormFreeformDateControl()) ->setName($start_key) ->setLabel($start_name) ->setValue($start_str)) ->appendChild( id(new AphrontFormTextControl()) ->setName($end_key) ->setLabel($end_name) ->setValue($end_str)); } /* -( Paging and Executing Queries )--------------------------------------- */ public function getPageSize(PhabricatorSavedQuery $saved) { return $saved->getParameter('limit', 100); } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new AphrontPagerView(); } else { $pager = new AphrontCursorPagerView(); } $page_size = $this->getPageSize($saved); if (is_finite($page_size)) { $pager->setPageSize($page_size); } else { // Consider an INF pagesize to mean a large finite pagesize. // TODO: It would be nice to handle this more gracefully, but math // with INF seems to vary across PHP versions, systems, and runtimes. $pager->setPageSize(0xFFFF); } return $pager; } public function executeQuery( PhabricatorPolicyAwareQuery $query, AphrontView $pager) { $query->setViewer($this->requireViewer()); if ($this->shouldUseOffsetPaging()) { $objects = $query->executeWithOffsetPager($pager); } else { $objects = $query->executeWithCursorPager($pager); } return $objects; } /* -( Rendering )---------------------------------------------------------- */ public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function renderResults( array $objects, PhabricatorSavedQuery $query) { $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->witHPHIDs($phids) ->execute(); } else { $handles = array(); } return $this->renderResultList($objects, $query, $handles); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { throw new Exception(pht('Not supported here yet!')); } /* -( Application Search )------------------------------------------------- */ /** * Retrieve an object to use to define custom fields for this search. * * To integrate with custom fields, subclasses should override this method * and return an instance of the application object which implements * @{interface:PhabricatorCustomFieldInterface}. * * @return PhabricatorCustomFieldInterface|null Object with custom fields. * @task appsearch */ public function getCustomFieldObject() { return null; } /** * Get the custom fields for this search. * * @return PhabricatorCustomFieldList|null Custom fields, if this search * supports custom fields. * @task appsearch */ public function getCustomFieldList() { if ($this->customFields === false) { $object = $this->getCustomFieldObject(); if ($object) { $fields = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->setViewer($this->requireViewer()); } else { $fields = null; } $this->customFields = $fields; } return $this->customFields; } /** * Moves data from the request into a saved query. * * @param AphrontRequest Request to read. * @param PhabricatorSavedQuery Query to write to. * @return void * @task appsearch */ protected function readCustomFieldsFromRequest( AphrontRequest $request, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->readApplicationSearchValueFromRequest( $this, $request); $saved->setParameter($key, $value); } } /** * Applies data from a saved query to an executable query. * * @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain. * @param PhabricatorSavedQuery Saved query to read. * @return void */ protected function applyCustomFieldsToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $field->applyApplicationSearchConstraintToQuery( $this, $query, $saved->getParameter($key)); } } protected function applyOrderByToQuery( PhabricatorCursorPagedPolicyAwareQuery $query, array $standard_values, $order) { if (substr($order, 0, 7) === 'custom:') { $list = $this->getCustomFieldList(); if (!$list) { $query->setOrderBy(head($standard_values)); return; } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); if ($key === $order) { $index = $field->buildOrderIndex(); if ($index === null) { $query->setOrderBy(head($standard_values)); return; } $query->withApplicationSearchOrder( $field, $index, false); break; } } } else { $order = idx($standard_values, $order); if ($order) { $query->setOrderBy($order); } else { $query->setOrderBy(head($standard_values)); } } } protected function getCustomFieldOrderOptions() { $list = $this->getCustomFieldList(); if (!$list) { return; } $custom_order = array(); foreach ($list->getFields() as $field) { if ($field->shouldAppearInApplicationSearch()) { if ($field->buildOrderIndex() !== null) { $key = $this->getKeyForCustomField($field); $custom_order[$key] = $field->getFieldName(); } } } return $custom_order; } /** * Get a unique key identifying a field. * * @param PhabricatorCustomField Field to identify. * @return string Unique identifier, suitable for use as an input name. */ public function getKeyForCustomField(PhabricatorCustomField $field) { return 'custom:'.$field->getFieldIndex(); } /** * Add inputs to an application search form so the user can query on custom * fields. * * @param AphrontFormView Form to update. * @param PhabricatorSavedQuery Values to prefill. * @return void */ protected function appendCustomFieldsToForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $list = $this->getCustomFieldList(); if (!$list) { return; } $phids = array(); foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $saved->getParameter($key); $phids[$key] = $field->getRequiredHandlePHIDsForApplicationSearch($value); } $all_phids = array_mergev($phids); $handles = array(); if ($all_phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($all_phids) ->execute(); } foreach ($list->getFields() as $field) { $key = $this->getKeyForCustomField($field); $value = $saved->getParameter($key); $field->appendToApplicationSearchForm( $this, $form, $value, array_select_keys($handles, $phids[$key])); } } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php index c4a7ecf419..d5966f8eaa 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php @@ -1,382 +1,382 @@ getUser(); $editable = PhabricatorEnv::getEnvConfig('account.editable'); $uri = $request->getRequestURI(); $uri->setQueryParams(array()); if ($editable) { $new = $request->getStr('new'); if ($new) { return $this->returnNewAddressResponse($request, $uri, $new); } $delete = $request->getInt('delete'); if ($delete) { return $this->returnDeleteAddressResponse($request, $uri, $delete); } } $verify = $request->getInt('verify'); if ($verify) { return $this->returnVerifyAddressResponse($request, $uri, $verify); } $primary = $request->getInt('primary'); if ($primary) { return $this->returnPrimaryAddressResponse($request, $uri, $primary); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); $rowc = array(); $rows = array(); foreach ($emails as $email) { $button_verify = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('verify', $email->getID()), 'sigil' => 'workflow', ), pht('Verify')); $button_make_primary = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('primary', $email->getID()), 'sigil' => 'workflow', ), pht('Make Primary')); $button_remove = javelin_tag( 'a', array( 'class' => 'button small grey', 'href' => $uri->alter('delete', $email->getID()), 'sigil' => 'workflow' ), pht('Remove')); $button_primary = phutil_tag( 'a', array( 'class' => 'button small disabled', ), pht('Primary')); if (!$email->getIsVerified()) { $action = $button_verify; } else if ($email->getIsPrimary()) { $action = $button_primary; } else { $action = $button_make_primary; } if ($email->getIsPrimary()) { $remove = $button_primary; $rowc[] = 'highlighted'; } else { $remove = $button_remove; $rowc[] = null; } $rows[] = array( $email->getAddress(), $action, $remove, ); } $table = new AphrontTableView($rows); $table->setHeaders( array( pht('Email'), pht('Status'), pht('Remove'), )); $table->setColumnClasses( array( 'wide', 'action', 'action', )); $table->setRowClasses($rowc); $table->setColumnVisibility( array( true, true, $editable, )); $view = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $header->setHeader(pht('Email Addresses')); if ($editable) { $icon = id(new PHUIIconView()) ->setIconFont('fa-plus'); $button = new PHUIButtonView(); $button->setText(pht('Add New Address')); $button->setTag('a'); $button->setHref($uri->alter('new', 'true')); $button->setIcon($icon); $button->addSigil('workflow'); $header->addActionLink($button); } $view->setHeader($header); $view->appendChild($table); return $view; } private function returnNewAddressResponse( AphrontRequest $request, PhutilURI $uri, $new) { $user = $request->getUser(); $e_email = true; $email = null; $errors = array(); if ($request->isDialogFormPost()) { $email = trim($request->getStr('email')); if ($new == 'verify') { // The user clicked "Done" from the "an email has been sent" dialog. return id(new AphrontReloadResponse())->setURI($uri); } PhabricatorSystemActionEngine::willTakeAction( array($user->getPHID()), new PhabricatorSettingsAddEmailAction(), 1); if (!strlen($email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } if (!$errors) { $object = id(new PhabricatorUserEmail()) ->setAddress($email) ->setIsVerified(0); try { id(new PhabricatorUserEditor()) ->setActor($user) ->addEmail($user, $object); $object->sendVerificationEmail($user); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('new', 'verify') ->setTitle(pht('Verification Email Sent')) ->appendChild(phutil_tag('p', array(), pht( 'A verification email has been sent. Click the link in the '. 'email to verify your address.'))) ->setSubmitURI($uri) ->addSubmitButton(pht('Done')); return id(new AphrontDialogResponse())->setDialog($dialog); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $email = pht('Duplicate'); $errors[] = pht('Another user already has this email.'); } } } if ($errors) { $errors = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('new', 'true') ->setTitle(pht('New Address')) ->appendChild($errors) ->appendChild($form) ->addSubmitButton(pht('Save')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnDeleteAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); // NOTE: You can only delete your own email addresses, and you can not // delete your primary address. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($user) ->removeEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('delete', $email_id) ->setTitle(pht("Really delete address '%s'?", $address)) ->appendParagraph( pht( 'Are you sure you want to delete this address? You will no '. 'longer be able to use it to login.')) ->appendParagraph( pht( 'Note: Removing an email address from your account will invalidate '. 'any outstanding password reset links.')) ->addSubmitButton(pht('Delete')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnVerifyAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); // NOTE: You can only send more email for your unverified addresses. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { $email->sendVerificationEmail($user); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('verify', $email_id) ->setTitle(pht('Send Another Verification Email?')) ->appendChild(phutil_tag('p', array(), pht( 'Send another copy of the verification email to %s?', $address))) ->addSubmitButton(pht('Send Email')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } private function returnPrimaryAddressResponse( AphrontRequest $request, PhutilURI $uri, $email_id) { $user = $request->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $user, $request, $this->getPanelURI()); // NOTE: You can only make your own verified addresses primary. $email = id(new PhabricatorUserEmail())->loadOneWhere( 'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0', $email_id, $user->getPHID()); if (!$email) { return new Aphront404Response(); } if ($request->isFormPost()) { id(new PhabricatorUserEditor()) ->setActor($user) ->changePrimaryEmail($user, $email); return id(new AphrontRedirectResponse())->setURI($uri); } $address = $email->getAddress(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->addHiddenInput('primary', $email_id) ->setTitle(pht('Change primary email address?')) ->appendParagraph( pht( 'If you change your primary address, Phabricator will send all '. 'email to %s.', $address)) ->appendParagraph( pht( 'Note: Changing your primary email address will invalidate any '. 'outstanding password reset links.')) ->addSubmitButton(pht('Change Primary Address')) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php index ae0af29505..c285b749b9 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php @@ -1,429 +1,429 @@ getUser(); $user = $this->getUser(); $generate = $request->getStr('generate'); if ($generate) { return $this->processGenerate($request); } $edit = $request->getStr('edit'); $delete = $request->getStr('delete'); if (!$edit && !$delete) { return $this->renderKeyListView($request); } $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); $id = nonempty($edit, $delete); if ($id && is_numeric($id)) { // NOTE: This prevents editing/deleting of keys not owned by the user. $key = id(new PhabricatorUserSSHKey())->loadOneWhere( 'userPHID = %s AND id = %d', $user->getPHID(), (int)$id); if (!$key) { return new Aphront404Response(); } } else { $key = new PhabricatorUserSSHKey(); $key->setUserPHID($user->getPHID()); } if ($delete) { return $this->processDelete($request, $key); } $e_name = true; $e_key = true; $errors = array(); $entire_key = $key->getEntireKey(); if ($request->isFormPost()) { $key->setName($request->getStr('name')); $entire_key = $request->getStr('key'); if (!strlen($entire_key)) { $errors[] = pht('You must provide an SSH Public Key.'); $e_key = pht('Required'); } else { try { list($type, $body, $comment) = self::parsePublicKey($entire_key); $key->setKeyType($type); $key->setKeyBody($body); $key->setKeyHash(md5($body)); $key->setKeyComment($comment); $e_key = null; } catch (Exception $ex) { $e_key = pht('Invalid'); $errors[] = $ex->getMessage(); } } if (!strlen($key->getName())) { $errors[] = pht('You must name this public key.'); $e_name = pht('Required'); } else { $e_name = null; } if (!$errors) { try { $key->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI()); - } catch (AphrontQueryDuplicateKeyException $ex) { + } catch (AphrontDuplicateKeyQueryException $ex) { $e_key = pht('Duplicate'); $errors[] = pht('This public key is already associated with a user '. 'account.'); } } } $is_new = !$key->getID(); if ($is_new) { $header = pht('Add New SSH Public Key'); $save = pht('Add Key'); } else { $header = pht('Edit SSH Public Key'); $save = pht('Save Changes'); } $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('edit', $is_new ? 'true' : $key->getID()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') ->setValue($key->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Public Key')) ->setName('key') ->setValue($entire_key) ->setError($e_key)) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getPanelURI()) ->setValue($save)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setFormErrors($errors) ->setForm($form); return $form_box; } private function renderKeyListView(AphrontRequest $request) { $user = $this->getUser(); $viewer = $request->getUser(); $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $user->getPHID()); $rows = array(); foreach ($keys as $key) { $rows[] = array( phutil_tag( 'a', array( 'href' => $this->getPanelURI('?edit='.$key->getID()), ), $key->getName()), $key->getKeyComment(), $key->getKeyType(), phabricator_date($key->getDateCreated(), $viewer), phabricator_time($key->getDateCreated(), $viewer), javelin_tag( 'a', array( 'href' => $this->getPanelURI('?delete='.$key->getID()), 'class' => 'small grey button', 'sigil' => 'workflow', ), pht('Delete')), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You haven't added any SSH Public Keys.")); $table->setHeaders( array( pht('Name'), pht('Comment'), pht('Type'), pht('Created'), pht('Time'), '', )); $table->setColumnClasses( array( 'wide pri', '', '', '', 'right', 'action', )); $panel = new PHUIObjectBoxView(); $header = new PHUIHeaderView(); $upload_icon = id(new PHUIIconView()) ->setIconFont('fa-upload'); $upload_button = id(new PHUIButtonView()) ->setText(pht('Upload Public Key')) ->setHref($this->getPanelURI('?edit=true')) ->setTag('a') ->setIcon($upload_icon); try { PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); $can_generate = true; } catch (Exception $ex) { $can_generate = false; } $generate_icon = id(new PHUIIconView()) ->setIconFont('fa-lock'); $generate_button = id(new PHUIButtonView()) ->setText(pht('Generate Keypair')) ->setHref($this->getPanelURI('?generate=true')) ->setTag('a') ->setWorkflow(true) ->setDisabled(!$can_generate) ->setIcon($generate_icon); $header->setHeader(pht('SSH Public Keys')); $header->addActionLink($generate_button); $header->addActionLink($upload_button); $panel->setHeader($header); $panel->appendChild($table); return $panel; } private function processDelete( AphrontRequest $request, PhabricatorUserSSHKey $key) { $viewer = $request->getUser(); $user = $this->getUser(); $name = phutil_tag('strong', array(), $key->getName()); if ($request->isDialogFormPost()) { $key->delete(); return id(new AphrontReloadResponse()) ->setURI($this->getPanelURI()); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addHiddenInput('delete', $key->getID()) ->setTitle(pht('Really delete SSH Public Key?')) ->appendChild(phutil_tag('p', array(), pht( 'The key "%s" will be permanently deleted, and you will not longer be '. 'able to use the corresponding private key to authenticate.', $name))) ->addSubmitButton(pht('Delete Public Key')) ->addCancelButton($this->getPanelURI()); return id(new AphrontDialogResponse()) ->setDialog($dialog); } private function processGenerate(AphrontRequest $request) { $user = $this->getUser(); $viewer = $request->getUser(); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, $this->getPanelURI()); $is_self = ($user->getPHID() == $viewer->getPHID()); if ($request->isFormPost()) { $keys = PhabricatorSSHKeyGenerator::generateKeypair(); list($public_key, $private_key) = $keys; $file = PhabricatorFile::buildFromFileDataOrHash( $private_key, array( 'name' => 'id_rsa_phabricator.key', 'ttl' => time() + (60 * 10), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, )); list($type, $body, $comment) = self::parsePublicKey($public_key); $key = id(new PhabricatorUserSSHKey()) ->setUserPHID($user->getPHID()) ->setName('id_rsa_phabricator') ->setKeyType($type) ->setKeyBody($body) ->setKeyHash(md5($body)) ->setKeyComment(pht('Generated')) ->save(); // NOTE: We're disabling workflow on submit so the download works. We're // disabling workflow on cancel so the page reloads, showing the new // key. if ($is_self) { $what_happened = pht( 'The public key has been associated with your Phabricator '. 'account. Use the button below to download the private key.'); } else { $what_happened = pht( 'The public key has been associated with the %s account. '. 'Use the button below to download the private key.', phutil_tag('strong', array(), $user->getUsername())); } $dialog = id(new AphrontDialogView()) ->setTitle(pht('Download Private Key')) ->setUser($viewer) ->setDisableWorkflowOnCancel(true) ->setDisableWorkflowOnSubmit(true) ->setSubmitURI($file->getDownloadURI()) ->appendParagraph( pht( 'Successfully generated a new keypair.')) ->appendParagraph($what_happened) ->appendParagraph( pht( 'After you download the private key, it will be destroyed. '. 'You will not be able to retrieve it if you lose your copy.')) ->addSubmitButton(pht('Download Private Key')) ->addCancelButton($this->getPanelURI(), pht('Done')); return id(new AphrontDialogResponse()) ->setDialog($dialog); } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->addCancelButton($this->getPanelURI()); try { PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); if ($is_self) { $explain = pht( 'This will generate an SSH keypair, associate the public key '. 'with your account, and let you download the private key.'); } else { $explain = pht( 'This will generate an SSH keypair, associate the public key with '. 'the %s account, and let you download the private key.', phutil_tag('strong', array(), $user->getUsername())); } $dialog ->addHiddenInput('generate', true) ->setTitle(pht('Generate New Keypair')) ->appendParagraph($explain) ->appendParagraph( pht( 'Phabricator will not retain a copy of the private key.')) ->addSubmitButton(pht('Generate Keypair')); } catch (Exception $ex) { $dialog ->setTitle(pht('Unable to Generate Keys')) ->appendParagraph($ex->getMessage()); } return id(new AphrontDialogResponse()) ->setDialog($dialog); } private static function parsePublicKey($entire_key) { $parts = str_replace("\n", '', trim($entire_key)); // The third field (the comment) can have spaces in it, so split this // into a maximum of three parts. $parts = preg_split('/\s+/', $parts, 3); if (preg_match('/private\s*key/i', $entire_key)) { // Try to give the user a better error message if it looks like // they uploaded a private key. throw new Exception( pht('Provide your public key, not your private key!')); } switch (count($parts)) { case 1: throw new Exception( pht('Provided public key is not properly formatted.')); case 2: // Add an empty comment part. $parts[] = ''; break; case 3: // This is the expected case. break; } list($type, $body, $comment) = $parts; $recognized_keys = array( 'ssh-dsa', 'ssh-dss', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', ); if (!in_array($type, $recognized_keys)) { $type_list = implode(', ', $recognized_keys); throw new Exception( pht( 'Public key type should be one of: %s', $type_list)); } return array($type, $body, $comment); } } diff --git a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php index 56457ceceb..ed3c8560d5 100644 --- a/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php +++ b/src/infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php @@ -1,144 +1,144 @@ false, ); } public function testIsolation() { // This will fail if the connection isn't isolated. queryfx( $this->newIsolatedConnection(), 'INSERT INVALID SYNTAX'); $this->assertTrue(true); } public function testInsertGeneratesID() { $conn = $this->newIsolatedConnection(); queryfx($conn, 'INSERT'); $id1 = $conn->getInsertID(); queryfx($conn, 'INSERT'); $id2 = $conn->getInsertID(); $this->assertTrue((bool)$id1, 'ID1 exists.'); $this->assertTrue((bool)$id2, 'ID2 exists.'); $this->assertTrue( $id1 != $id2, "IDs '{$id1}' and '{$id2}' are distinct."); } public function testDeletePermitted() { $conn = $this->newIsolatedConnection(); queryfx($conn, 'DELETE'); $this->assertTrue(true); } public function testTransactionStack() { $conn = $this->newIsolatedConnection(); $conn->openTransaction(); queryfx($conn, 'INSERT'); $conn->saveTransaction(); $this->assertEqual( array( 'START TRANSACTION', 'INSERT', 'COMMIT', ), $conn->getQueryTranscript()); $conn = $this->newIsolatedConnection(); $conn->openTransaction(); queryfx($conn, 'INSERT 1'); $conn->openTransaction(); queryfx($conn, 'INSERT 2'); $conn->killTransaction(); $conn->openTransaction(); queryfx($conn, 'INSERT 3'); $conn->openTransaction(); queryfx($conn, 'INSERT 4'); $conn->saveTransaction(); $conn->saveTransaction(); $conn->openTransaction(); queryfx($conn, 'INSERT 5'); $conn->killTransaction(); queryfx($conn, 'INSERT 6'); $conn->saveTransaction(); $this->assertEqual( array( 'START TRANSACTION', 'INSERT 1', 'SAVEPOINT Aphront_Savepoint_1', 'INSERT 2', 'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1', 'SAVEPOINT Aphront_Savepoint_1', 'INSERT 3', 'SAVEPOINT Aphront_Savepoint_2', 'INSERT 4', 'SAVEPOINT Aphront_Savepoint_1', 'INSERT 5', 'ROLLBACK TO SAVEPOINT Aphront_Savepoint_1', 'INSERT 6', 'COMMIT', ), $conn->getQueryTranscript()); } public function testTransactionRollback() { $check = array(); $phid = new HarbormasterScratchTable(); $phid->openTransaction(); for ($ii = 0; $ii < 3; $ii++) { $key = $this->generateTestData(); $obj = new HarbormasterScratchTable(); $obj->setData($key); $obj->save(); $check[] = $key; } $phid->killTransaction(); foreach ($check as $key) { $this->assertNoSuchRow($key); } } private function newIsolatedConnection() { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } private function generateTestData() { return Filesystem::readRandomCharacters(20); } private function assertNoSuchRow($data) { try { $row = id(new HarbormasterScratchTable())->loadOneWhere( 'data = %s', $data); $this->assertEqual( null, $row, 'Expect fake row to exist only in isolation.'); - } catch (AphrontQueryConnectionException $ex) { + } catch (AphrontConnectionQueryException $ex) { // If we can't connect to the database, conclude that the isolated // connection actually is isolated. Philosophically, this perhaps allows // us to claim this test does not depend on the database? } } } diff --git a/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php b/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php index d78737c2f6..ec238ecd59 100644 --- a/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php +++ b/src/infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php @@ -1,37 +1,37 @@ false, ); } public function testConnectionFailures() { $conn = id(new HarbormasterScratchTable())->establishConnection('r'); queryfx($conn, 'SELECT 1'); // We expect the connection to recover from a 2006 (lost connection) when // outside of a transaction... $conn->simulateErrorOnNextQuery(2006); queryfx($conn, 'SELECT 1'); // ...but when transactional, we expect the query to throw when the // connection is lost, because it indicates the transaction was aborted. $conn->openTransaction(); $conn->simulateErrorOnNextQuery(2006); $caught = null; try { queryfx($conn, 'SELECT 1'); - } catch (AphrontQueryConnectionLostException $ex) { + } catch (AphrontConnectionLostQueryException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof Exception); } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index b3f54d0817..52b72f228f 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,1715 +1,1715 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or `null` if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing */ abstract class LiskDAO { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_BINARY = 'binary'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $ephemeral = false; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ abstract protected function getConnectionNamespace(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. * @return AprontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * public function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_BINARY * You can optionally provide a map of columns to a flag indicating that * they store binary data. These columns will not raise an error when * handling binary writes. * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { - throw new AphrontQueryCountException( + throw new AphrontCountQueryException( 'More than 1 result from loadOneWhere()!'); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($pattern /* , $args... */) { $connection = $this->establishConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 1); $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception("Unable to reload object that hasn't been loaded!"); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { - throw new AphrontQueryObjectMissingException(); + throw new AphrontObjectMissingQueryException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via @{method:queryData}) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling * @{method:getProperties}. * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with @{method:loadAllWhere}, but sometimes that * isn't flexible enough. One case is if you need to do joins to select the * right objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than @{method:loadAllWhere}, but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorCalendarEvent(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { - throw new AphrontQueryCountException( + throw new AphrontCountQueryException( 'More than 1 result from loadOneRelative()!'); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } public function getPHID() { return $this->phid; } /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. * @return LiskDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'."); } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using @{method:loadFromArray} (unless you're * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you * should just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getPropertyValues() { $map = array(); foreach ($this->getProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getPropertyValues(); $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); $binary = $this->getBinaryColumns(); foreach ($map as $key => $value) { if (!empty($binary[$key])) { $map[$key] = qsprintf($conn, '%C = %nB', $key, $value); } else { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } } $map = implode(', ', $map); $id = $this->getID(); $conn->query( 'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this->getTableName(), $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterID($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception('Unknown CONFIG_IDs mechanism!'); } $this->willWriteData($data); $columns = array_keys($data); $binary = $this->getBinaryColumns(); foreach ($data as $key => $value) { try { if (!empty($binary[$key])) { $data[$key] = qsprintf($conn, '%nB', $value); } else { $data[$key] = qsprintf($conn, '%ns', $value); } - } catch (AphrontQueryParameterException $parameter_exception) { + } catch (AphrontParameterQueryException $parameter_exception) { throw new PhutilProxyException( pht( "Unable to insert or update object of class %s, field '%s' ". "has a nonscalar value.", get_class($this), $key), $parameter_exception); } } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( 'You are using manual IDs. You must override the '. 'shouldInsertWhenSaved() method to properly detect '. 'when to insert a new record.'); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( 'This DAO does not have a single-part primary key. The method you '. 'called requires a single-part primary key.'); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ protected function generatePHID() { throw new Exception( 'To use CONFIG_AUX_PHID, you need to overload '. 'generatePHID() to perform PHID generation.'); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also @{method:willReadData}. * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also @{method:willWriteData}. * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior * of @{method:getField} instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( 'Lisk process isolation level was reduced below 0.'); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( 'Lisk transaction isolation level was reduced below 0.'); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } public static function closeAllConnections() { self::$connections = array(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = json_encode($data[$col]); } break; default: throw new Exception("Unknown serialization format '{$format}'."); } } } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception("Bad getter call: {$method}"); } $dispatch_map[$method] = $property; } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception("Bad setter call: {$method}"); } $dispatch_map[$method] = $property; } $this->writeField($property, $args[0]); return $this; } throw new Exception("Unable to resolve method '{$method}'."); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.'); $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterID( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } private function getBinaryColumns() { return $this->getConfigOption(self::CONFIG_BINARY); } }