PhabricatorConduitAPIController.php 21.5 KB
Newer Older
epriestley's avatar
epriestley committed
1 2
<?php

3
final class PhabricatorConduitAPIController
epriestley's avatar
epriestley committed
4 5
  extends PhabricatorConduitController {

epriestley's avatar
epriestley committed
6 7 8 9
  public function shouldRequireLogin() {
    return false;
  }

Chad Little's avatar
Chad Little committed
10 11
  public function handleRequest(AphrontRequest $request) {
    $method = $request->getURIData('method');
epriestley's avatar
epriestley committed
12 13 14
    $time_start = microtime(true);

    $api_request = null;
15
    $method_implementation = null;
epriestley's avatar
epriestley committed
16 17 18

    $log = new PhabricatorConduitMethodCallLog();
    $log->setMethod($method);
19
    $metadata = array();
epriestley's avatar
epriestley committed
20

21 22 23 24 25
    $multimeter = MultimeterControl::getInstance();
    if ($multimeter) {
      $multimeter->setEventContext('api.'.$method);
    }

epriestley's avatar
epriestley committed
26 27
    try {

28 29 30
      list($metadata, $params, $strictly_typed) = $this->decodeConduitParams(
        $request,
        $method);
epriestley's avatar
epriestley committed
31

32
      $call = new ConduitCall($method, $params, $strictly_typed);
33
      $method_implementation = $call->getMethodImplementation();
34

35 36
      $result = null;

epriestley's avatar
epriestley committed
37 38 39 40 41 42
      // TODO: The relationship between ConduitAPIRequest and ConduitCall is a
      // little odd here and could probably be improved. Specifically, the
      // APIRequest is a sub-object of the Call, which does not parallel the
      // role of AphrontRequest (which is an indepenent object).
      // In particular, the setUser() and getUser() existing independently on
      // the Call and APIRequest is very awkward.
43

epriestley's avatar
epriestley committed
44
      $api_request = $call->getAPIRequest();
epriestley's avatar
epriestley committed
45

46
      $allow_unguarded_writes = false;
47
      $auth_error = null;
48
      $conduit_username = '-';
49
      if ($call->shouldRequireAuthentication()) {
Chad Little's avatar
Chad Little committed
50
        $auth_error = $this->authenticateUser($api_request, $metadata, $method);
51 52 53
        // If we've explicitly authenticated the user here and either done
        // CSRF validation or are using a non-web authentication mechanism.
        $allow_unguarded_writes = true;
Emil Hesslow's avatar
Emil Hesslow committed
54

55 56 57 58 59
        if ($auth_error === null) {
          $conduit_user = $api_request->getUser();
          if ($conduit_user && $conduit_user->getPHID()) {
            $conduit_username = $conduit_user->getUsername();
          }
60
          $call->setUser($api_request->getUser());
61
        }
62 63 64 65
      }

      $access_log = PhabricatorAccessLog::getLog();
      if ($access_log) {
66 67 68 69 70 71 72
        $access_log->setData(
          array(
            'u' => $conduit_username,
            'm' => $method,
          ));
      }

73
      if ($call->shouldAllowUnguardedWrites()) {
74
        $allow_unguarded_writes = true;
75 76
      }

77
      if ($auth_error === null) {
78 79 80
        if ($allow_unguarded_writes) {
          $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
        }
Emil Hesslow's avatar
Emil Hesslow committed
81

82
        try {
83
          $result = $call->execute();
84 85 86 87 88
          $error_code = null;
          $error_info = null;
        } catch (ConduitException $ex) {
          $result = null;
          $error_code = $ex->getMessage();
89 90 91
          if ($ex->getErrorDescription()) {
            $error_info = $ex->getErrorDescription();
          } else {
92
            $error_info = $call->getErrorDescription($error_code);
93
          }
94
        }
95 96 97
        if ($allow_unguarded_writes) {
          unset($unguarded);
        }
98 99
      } else {
        list($error_code, $error_info) = $auth_error;
epriestley's avatar
epriestley committed
100 101 102
      }
    } catch (Exception $ex) {
      $result = null;
103 104 105
      $error_code = ($ex instanceof ConduitException
        ? 'ERR-CONDUIT-CALL'
        : 'ERR-CONDUIT-CORE');
epriestley's avatar
epriestley committed
106 107 108
      $error_info = $ex->getMessage();
    }

epriestley's avatar
epriestley committed
109 110 111 112 113 114
    $log
      ->setCallerPHID(
        isset($conduit_user)
          ? $conduit_user->getPHID()
          : null)
      ->setError((string)$error_code)
115
      ->setDuration(phutil_microseconds_since($time_start));
epriestley's avatar
epriestley committed
116

117 118 119 120 121
    if (!PhabricatorEnv::isReadOnly()) {
      $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
      $log->save();
      unset($unguarded);
    }
epriestley's avatar
epriestley committed
122

123 124 125 126
    $response = id(new ConduitAPIResponse())
      ->setResult($result)
      ->setErrorCode($error_code)
      ->setErrorInfo($error_info);
epriestley's avatar
epriestley committed
127 128 129 130 131 132

    switch ($request->getStr('output')) {
      case 'human':
        return $this->buildHumanReadableResponse(
          $method,
          $api_request,
133 134
          $response->toDictionary(),
          $method_implementation);
epriestley's avatar
epriestley committed
135 136
      case 'json':
      default:
137
        return id(new AphrontJSONResponse())
138
          ->setAddJSONShield(false)
139
          ->setContent($response->toDictionary());
epriestley's avatar
epriestley committed
140 141 142
    }
  }

143 144 145 146 147 148 149 150 151 152
  /**
   * Authenticate the client making the request to a Phabricator user account.
   *
   * @param   ConduitAPIRequest Request being executed.
   * @param   dict              Request metadata.
   * @return  null|pair         Null to indicate successful authentication, or
   *                            an error code and error message pair.
   */
  private function authenticateUser(
    ConduitAPIRequest $api_request,
Chad Little's avatar
Chad Little committed
153 154
    array $metadata,
    $method) {
155 156 157 158

    $request = $this->getRequest();

    if ($request->getUser()->getPHID()) {
159
      $request->validateCSRF();
160 161 162
      return $this->validateAuthenticatedUser(
        $api_request,
        $request->getUser());
163 164
    }

165 166 167 168 169 170 171
    $auth_type = idx($metadata, 'auth.type');
    if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) {
      $host = idx($metadata, 'auth.host');
      if (!$host) {
        return array(
          'ERR-INVALID-AUTH',
          pht(
Joshua Spence's avatar
Joshua Spence committed
172 173
            'Request is missing required "%s" parameter.',
            'auth.host'),
174 175 176 177 178 179 180
        );
      }

      // TODO: Validate that we are the host!

      $raw_key = idx($metadata, 'auth.key');
      $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key);
181
      $ssl_public_key = $public_key->toPKCS8();
182 183 184 185 186

      // First, verify the signature.
      try {
        $protocol_data = $metadata;
        ConduitClient::verifySignature(
Chad Little's avatar
Chad Little committed
187
          $method,
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
          $api_request->getAllParameters(),
          $protocol_data,
          $ssl_public_key);
      } catch (Exception $ex) {
        return array(
          'ERR-INVALID-AUTH',
          pht(
            'Signature verification failure. %s',
            $ex->getMessage()),
        );
      }

      // If the signature is valid, find the user or device which is
      // associated with this public key.

      $stored_key = id(new PhabricatorAuthSSHKeyQuery())
        ->setViewer(PhabricatorUser::getOmnipotentUser())
        ->withKeys(array($public_key))
206
        ->withIsActive(true)
207 208
        ->executeOne();
      if (!$stored_key) {
209 210 211
        $key_summary = id(new PhutilUTF8StringTruncator())
          ->setMaximumBytes(64)
          ->truncateString($raw_key);
212 213
        return array(
          'ERR-INVALID-AUTH',
214 215 216
          pht(
            'No user or device is associated with the public key "%s".',
            $key_summary),
217 218 219 220 221 222 223 224
        );
      }

      $object = $stored_key->getObject();

      if ($object instanceof PhabricatorUser) {
        $user = $object;
      } else {
225 226 227 228 229 230 231 232 233
        if (!$stored_key->getIsTrusted()) {
          return array(
            'ERR-INVALID-AUTH',
            pht(
              'The key which signed this request is not trusted. Only '.
              'trusted keys can be used to sign API calls.'),
          );
        }

234 235 236 237 238 239
        if (!PhabricatorEnv::isClusterRemoteAddress()) {
          return array(
            'ERR-INVALID-AUTH',
            pht(
              'This request originates from outside of the Phabricator '.
              'cluster address range. Requests signed with trusted '.
Joshua Spence's avatar
Joshua Spence committed
240 241
              'device keys must originate from within the cluster.'),
          );
242 243 244
        }

        $user = PhabricatorUser::getOmnipotentUser();
epriestley's avatar
epriestley committed
245 246 247

        // Flag this as an intracluster request.
        $api_request->setIsClusterRequest(true);
248 249 250 251 252 253 254 255 256 257 258 259
      }

      return $this->validateAuthenticatedUser(
        $api_request,
        $user);
    } else if ($auth_type === null) {
      // No specified authentication type, continue with other authentication
      // methods below.
    } else {
      return array(
        'ERR-INVALID-AUTH',
        pht(
Joshua Spence's avatar
Joshua Spence committed
260 261
          'Provided "%s" ("%s") is not recognized.',
          'auth.type',
262 263 264 265
          $auth_type),
      );
    }

266 267 268 269 270 271 272 273
    $token_string = idx($metadata, 'token');
    if (strlen($token_string)) {

      if (strlen($token_string) != 32) {
        return array(
          'ERR-INVALID-AUTH',
          pht(
            'API token "%s" has the wrong length. API tokens should be '.
274 275
            '32 characters long.',
            $token_string),
276 277 278 279
        );
      }

      $type = head(explode('-', $token_string));
280 281 282 283 284 285 286 287 288 289
      $valid_types = PhabricatorConduitToken::getAllTokenTypes();
      $valid_types = array_fuse($valid_types);
      if (empty($valid_types[$type])) {
        return array(
          'ERR-INVALID-AUTH',
          pht(
            'API token "%s" has the wrong format. API tokens should be '.
            '32 characters long and begin with one of these prefixes: %s.',
            $token_string,
            implode(', ', $valid_types)),
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
          );
      }

      $token = id(new PhabricatorConduitTokenQuery())
        ->setViewer(PhabricatorUser::getOmnipotentUser())
        ->withTokens(array($token_string))
        ->withExpired(false)
        ->executeOne();
      if (!$token) {
        $token = id(new PhabricatorConduitTokenQuery())
          ->setViewer(PhabricatorUser::getOmnipotentUser())
          ->withTokens(array($token_string))
          ->withExpired(true)
          ->executeOne();
        if ($token) {
          return array(
            'ERR-INVALID-AUTH',
            pht(
              'API token "%s" was previously valid, but has expired.',
              $token_string),
          );
        } else {
          return array(
            'ERR-INVALID-AUTH',
            pht(
              'API token "%s" is not valid.',
              $token_string),
          );
        }
      }

321 322 323 324 325 326 327 328 329 330 331 332 333
      // If this is a "cli-" token, it expires shortly after it is generated
      // by default. Once it is actually used, we extend its lifetime and make
      // it permanent. This allows stray tokens to get cleaned up automatically
      // if they aren't being used.
      if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) {
        if ($token->getExpires()) {
          $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
            $token->setExpires(null);
            $token->save();
          unset($unguarded);
        }
      }

334 335 336 337 338 339 340 341 342
      // If this is a "clr-" token, Phabricator must be configured in cluster
      // mode and the remote address must be a cluster node.
      if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) {
        if (!PhabricatorEnv::isClusterRemoteAddress()) {
          return array(
            'ERR-INVALID-AUTH',
            pht(
              'This request originates from outside of the Phabricator '.
              'cluster address range. Requests signed with cluster API '.
Joshua Spence's avatar
Joshua Spence committed
343 344
              'tokens must originate from within the cluster.'),
          );
345
        }
epriestley's avatar
epriestley committed
346 347 348

        // Flag this as an intracluster request.
        $api_request->setIsClusterRequest(true);
349 350
      }

351 352 353 354
      $user = $token->getObject();
      if (!($user instanceof PhabricatorUser)) {
        return array(
          'ERR-INVALID-AUTH',
Joshua Spence's avatar
Joshua Spence committed
355
          pht('API token is not associated with a valid user.'),
356 357 358 359 360 361 362 363 364
        );
      }

      return $this->validateAuthenticatedUser(
        $api_request,
        $user);
    }

    $access_token = idx($metadata, 'access_token');
365 366 367 368 369 370 371 372 373 374 375 376 377 378

    // Some OAuth2 clients only like to do an Authorization header. This
    // would be the same Authorization header which PHP strips from us.
    // Luckily apache_request_headers() seems to work.
    if (!$access_token) {
      $all_headers = apache_request_headers();
      if (isset($all_headers['Authorization'])) {
	$auth_header = explode(' ', $all_headers['Authorization']);
        if (count($auth_header) == 2 && $auth_header[0] == 'Bearer') {
          $access_token = $auth_header[1];
	}
      }
    }

379
    if ($access_token) {
380
      $token = id(new PhabricatorOAuthServerAccessToken())
Joshua Spence's avatar
Joshua Spence committed
381
        ->loadOneWhere('token = %s', $access_token);
382 383 384
      if (!$token) {
        return array(
          'ERR-INVALID-AUTH',
Joshua Spence's avatar
Joshua Spence committed
385
          pht('Access token does not exist.'),
386 387 388 389
        );
      }

      $oauth_server = new PhabricatorOAuthServer();
390 391
      $authorization = $oauth_server->authorizeToken($token);
      if (!$authorization) {
392 393
        return array(
          'ERR-INVALID-AUTH',
394
          pht('Access token is invalid or expired.'),
395
        );
396
      }
397

398 399 400 401
      $user = id(new PhabricatorPeopleQuery())
        ->setViewer(PhabricatorUser::getOmnipotentUser())
        ->withPHIDs(array($token->getUserPHID()))
        ->executeOne();
402 403 404
      if (!$user) {
        return array(
          'ERR-INVALID-AUTH',
Joshua Spence's avatar
Joshua Spence committed
405
          pht('Access token is for invalid user.'),
406 407
        );
      }
408 409 410 411 412 413 414 415 416

      $ok = $this->authorizeOAuthMethodAccess($authorization, $method);
      if (!$ok) {
        return array(
          'ERR-OAUTH-ACCESS',
          pht('You do not have authorization to call this method.'),
        );
      }

417 418
      $api_request->setOAuthToken($token);

419 420 421
      return $this->validateAuthenticatedUser(
        $api_request,
        $user);
422 423
    }

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440

    // For intracluster requests, use a public user if no authentication
    // information is provided. We could do this safely for any request,
    // but making the API fully public means there's no way to disable badly
    // behaved clients.
    if (PhabricatorEnv::isClusterRemoteAddress()) {
      if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
        $api_request->setIsClusterRequest(true);

        $user = new PhabricatorUser();
        return $this->validateAuthenticatedUser(
          $api_request,
          $user);
      }
    }


441 442 443 444
    // Handle sessionless auth.
    // TODO: This is super messy.
    // TODO: Remove this in favor of token-based auth.

epriestley's avatar
epriestley committed
445 446 447 448 449 450 451
    if (isset($metadata['authUser'])) {
      $user = id(new PhabricatorUser())->loadOneWhere(
        'userName = %s',
        $metadata['authUser']);
      if (!$user) {
        return array(
          'ERR-INVALID-AUTH',
Joshua Spence's avatar
Joshua Spence committed
452
          pht('Authentication is invalid.'),
epriestley's avatar
epriestley committed
453 454 455 456 457
        );
      }
      $token = idx($metadata, 'authToken');
      $signature = idx($metadata, 'authSignature');
      $certificate = $user->getConduitCertificate();
458 459
      $hash = sha1($token.$certificate);
      if (!phutil_hashes_are_identical($hash, $signature)) {
epriestley's avatar
epriestley committed
460 461
        return array(
          'ERR-INVALID-AUTH',
Joshua Spence's avatar
Joshua Spence committed
462
          pht('Authentication is invalid.'),
epriestley's avatar
epriestley committed
463 464
        );
      }
465 466 467
      return $this->validateAuthenticatedUser(
        $api_request,
        $user);
epriestley's avatar
epriestley committed
468 469
    }

470 471 472
    // Handle session-based auth.
    // TODO: Remove this in favor of token-based auth.

473 474 475
    $session_key = idx($metadata, 'sessionKey');
    if (!$session_key) {
      return array(
476
        'ERR-INVALID-SESSION',
Joshua Spence's avatar
Joshua Spence committed
477
        pht('Session key is not present.'),
478 479 480
      );
    }

481
    $user = id(new PhabricatorAuthSessionEngine())
482
      ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key);
483 484 485 486

    if (!$user) {
      return array(
        'ERR-INVALID-SESSION',
Joshua Spence's avatar
Joshua Spence committed
487
        pht('Session key is invalid.'),
488 489 490
      );
    }

491 492 493 494 495 496 497 498 499
    return $this->validateAuthenticatedUser(
      $api_request,
      $user);
  }

  private function validateAuthenticatedUser(
    ConduitAPIRequest $request,
    PhabricatorUser $user) {

epriestley's avatar
epriestley committed
500
    if (!$user->canEstablishAPISessions()) {
501
      return array(
epriestley's avatar
epriestley committed
502 503
        'ERR-INVALID-AUTH',
        pht('User account is not permitted to use the API.'),
504
      );
505 506 507
    }

    $request->setUser($user);
508 509 510 511

    id(new PhabricatorAuthSessionEngine())
      ->willServeRequestForUser($user);

512 513 514
    return null;
  }

epriestley's avatar
epriestley committed
515 516 517
  private function buildHumanReadableResponse(
    $method,
    ConduitAPIRequest $request = null,
518 519
    $result = null,
    ConduitAPIMethod $method_implementation = null) {
epriestley's avatar
epriestley committed
520 521

    $param_rows = array();
522
    $param_rows[] = array('Method', $this->renderAPIValue($method));
epriestley's avatar
epriestley committed
523 524 525
    if ($request) {
      foreach ($request->getAllParameters() as $key => $value) {
        $param_rows[] = array(
epriestley's avatar
epriestley committed
526
          $key,
527
          $this->renderAPIValue($value),
epriestley's avatar
epriestley committed
528 529 530 531 532 533 534 535 536 537 538 539 540 541
        );
      }
    }

    $param_table = new AphrontTableView($param_rows);
    $param_table->setColumnClasses(
      array(
        'header',
        'wide',
      ));

    $result_rows = array();
    foreach ($result as $key => $value) {
      $result_rows[] = array(
epriestley's avatar
epriestley committed
542
        $key,
543
        $this->renderAPIValue($value),
epriestley's avatar
epriestley committed
544 545 546 547 548 549 550 551 552 553
      );
    }

    $result_table = new AphrontTableView($result_rows);
    $result_table->setColumnClasses(
      array(
        'header',
        'wide',
      ));

Chad Little's avatar
Chad Little committed
554 555 556 557
    $param_panel = id(new PHUIObjectBoxView())
      ->setHeaderText(pht('Method Parameters'))
      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
      ->setTable($param_table);
epriestley's avatar
epriestley committed
558

Chad Little's avatar
Chad Little committed
559 560 561 562
    $result_panel = id(new PHUIObjectBoxView())
      ->setHeaderText(pht('Method Result'))
      ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
      ->setTable($result_table);
epriestley's avatar
epriestley committed
563

564 565
    $method_uri = $this->getApplicationURI('method/'.$method.'/');

566 567
    $crumbs = $this->buildApplicationCrumbs()
      ->addTextCrumb($method, $method_uri)
Chad Little's avatar
Chad Little committed
568 569
      ->addTextCrumb(pht('Call'))
      ->setBorder(true);
570

571 572 573 574 575 576 577 578
    $example_panel = null;
    if ($request && $method_implementation) {
      $params = $request->getAllParameters();
      $example_panel = $this->renderExampleBox(
        $method_implementation,
        $params);
    }

Chad Little's avatar
Chad Little committed
579 580 581 582 583 584 585 586
    $title = pht('Method Call Result');
    $header = id(new PHUIHeaderView())
      ->setHeader($title)
      ->setHeaderIcon('fa-exchange');

    $view = id(new PHUITwoColumnView())
      ->setHeader($header)
      ->setFooter(array(
Chad Little's avatar
Chad Little committed
587 588
        $param_panel,
        $result_panel,
589
        $example_panel,
epriestley's avatar
epriestley committed
590
      ));
Chad Little's avatar
Chad Little committed
591 592 593 594 595 596 597 598

    $title = pht('Method Call Result');

    return $this->newPage()
      ->setTitle($title)
      ->setCrumbs($crumbs)
      ->appendChild($view);

epriestley's avatar
epriestley committed
599 600
  }

601 602 603 604 605 606
  private function renderAPIValue($value) {
    $json = new PhutilJSON();
    if (is_array($value)) {
      $value = $json->encodeFormatted($value);
    }

607 608 609 610
    $value = phutil_tag(
      'pre',
      array('style' => 'white-space: pre-wrap;'),
      $value);
611 612 613 614

    return $value;
  }

615 616 617
  private function decodeConduitParams(
    AphrontRequest $request,
    $method) {
618

619 620 621 622 623 624 625 626 627
    $content_type = $request->getHTTPHeader('Content-Type');

    if ($content_type == 'application/json') {
      throw new Exception(
        pht('Use form-encoded data to submit parameters to Conduit endpoints. '.
            'Sending a JSON-encoded body and setting \'Content-Type\': '.
            '\'application/json\' is not currently supported.'));
    }

628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
    // Look for parameters from the Conduit API Console, which are encoded
    // as HTTP POST parameters in an array, e.g.:
    //
    //   params[name]=value&params[name2]=value2
    //
    // The fields are individually JSON encoded, since we require users to
    // enter JSON so that we avoid type ambiguity.

    $params = $request->getArr('params', null);
    if ($params !== null) {
      foreach ($params as $key => $value) {
        if ($value == '') {
          // Interpret empty string null (e.g., the user didn't type anything
          // into the box).
          $value = 'null';
        }
        $decoded_value = json_decode($value, true);
        if ($decoded_value === null && strtolower($value) != 'null') {
          // When json_decode() fails, it returns null. This almost certainly
          // indicates that a user was using the web UI and didn't put quotes
          // around a string value. We can either do what we think they meant
          // (treat it as a string) or fail. For now, err on the side of
          // caution and fail. In the future, if we make the Conduit API
          // actually do type checking, it might be reasonable to treat it as
          // a string if the parameter type is string.
          throw new Exception(
Joshua Spence's avatar
Joshua Spence committed
654 655 656 657 658 659 660
            pht(
              "The value for parameter '%s' is not valid JSON. All ".
              "parameters must be encoded as JSON values, including strings ".
              "(which means you need to surround them in double quotes). ".
              "Check your syntax. Value was: %s.",
              $key,
              $value));
661 662 663 664
        }
        $params[$key] = $decoded_value;
      }

665 666 667
      $metadata = idx($params, '__conduit__', array());
      unset($params['__conduit__']);

668
      return array($metadata, $params, true);
669 670 671
    }

    // Otherwise, look for a single parameter called 'params' which has the
672
    // entire param dictionary JSON encoded.
673
    $params_json = $request->getStr('params');
674
    if (strlen($params_json)) {
675 676 677 678 679 680
      $params = null;
      try {
        $params = phutil_json_decode($params_json);
      } catch (PhutilJSONParserException $ex) {
        throw new PhutilProxyException(
          pht(
Joshua Spence's avatar
Joshua Spence committed
681
            "Invalid parameter information was passed to method '%s'.",
682 683
            $method),
          $ex);
684
      }
685 686 687 688

      $metadata = idx($params, '__conduit__', array());
      unset($params['__conduit__']);

689
      return array($metadata, $params, true);
690 691 692 693 694 695 696 697 698 699 700 701 702
    }

    // If we do not have `params`, assume this is a simple HTTP request with
    // HTTP key-value pairs.
    $params = array();
    $metadata = array();
    foreach ($request->getPassthroughRequestData() as $key => $value) {
      $meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
      if ($meta_key !== null) {
        $metadata[$meta_key] = $value;
      } else {
        $params[$key] = $value;
      }
703 704
    }

705
    return array($metadata, $params, false);
706
  }
Joshua Spence's avatar
Joshua Spence committed
707

708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
  private function authorizeOAuthMethodAccess(
    PhabricatorOAuthClientAuthorization $authorization,
    $method_name) {

    $method = ConduitAPIMethod::getConduitMethod($method_name);
    if (!$method) {
      return false;
    }

    $required_scope = $method->getRequiredScope();
    switch ($required_scope) {
      case ConduitAPIMethod::SCOPE_ALWAYS:
        return true;
      case ConduitAPIMethod::SCOPE_NEVER:
        return false;
    }

    $authorization_scope = $authorization->getScope();
    if (!empty($authorization_scope[$required_scope])) {
      return true;
    }

    return false;
  }


epriestley's avatar
epriestley committed
734
}