private function formatRequestDataForCURL()

in src/future/http/HTTPSFuture.php [625:735]


  private function formatRequestDataForCURL() {
    // We're generating a value to hand to cURL as CURLOPT_POSTFIELDS. The way
    // cURL handles this value has some tricky caveats.

    // First, we can return either an array or a query string. If we return
    // an array, we get a "multipart/form-data" request. If we return a
    // query string, we get an "application/x-www-form-urlencoded" request.

    // Second, if we return an array we can't duplicate keys. The user might
    // want to send the same parameter multiple times.

    // Third, if we return an array and any of the values start with "@",
    // cURL includes arbitrary files off disk and sends them to an untrusted
    // remote server. For example, an array like:
    //
    //   array('name' => '@/usr/local/secret')
    //
    // ...will attempt to read that file off disk and transmit its contents with
    // the request. This behavior is pretty surprising, and it can easily
    // become a relatively severe security vulnerability which allows an
    // attacker to read any file the HTTP process has access to. Since this
    // feature is very dangerous and not particularly useful, we prevent its
    // use. Broadly, this means we must reject some requests because they
    // contain an "@" in an inconvenient place.

    // Generally, to avoid the "@" case and because most servers usually
    // expect "application/x-www-form-urlencoded" data, we try to return a
    // string unless there are files attached to this request.

    $data = $this->getData();
    $files = $this->files;

    $any_data = ($data || (is_string($data) && strlen($data)));
    $any_files = (bool)$this->files;

    if (!$any_data && !$any_files) {
      // No files or data, so just bail.
      return null;
    }

    if (!$any_files) {
      // If we don't have any files, just encode the data as a query string,
      // make sure it's not including any files, and we're good to go.
      if (is_array($data)) {
        $data = phutil_build_http_querystring($data);
      }

      $this->checkForDangerousCURLMagic($data, $is_query_string = true);

      return $data;
    }

    // If we've made it this far, we have some files, so we need to return
    // an array. First, convert the other data into an array if it isn't one
    // already.

    if (is_string($data)) {
      // NOTE: We explicitly don't want fancy array parsing here, so just
      // do a basic parse and then convert it into a dictionary ourselves.
      $parser = new PhutilQueryStringParser();
      $pairs = $parser->parseQueryStringToPairList($data);

      $map = array();
      foreach ($pairs as $pair) {
        list($key, $value) = $pair;
        if (array_key_exists($key, $map)) {
          throw new Exception(
            pht(
              'Request specifies two values for key "%s", but parameter '.
              'names must be unique if you are posting file data due to '.
              'limitations with cURL.',
              $key));
        }
        $map[$key] = $value;
      }

      $data = $map;
    }

    foreach ($data as $key => $value) {
      $this->checkForDangerousCURLMagic($value, $is_query_string = false);
    }

    foreach ($this->files as $name => $info) {
      if (array_key_exists($name, $data)) {
        throw new Exception(
          pht(
            'Request specifies a file with key "%s", but that key is also '.
            'defined by normal request data. Due to limitations with cURL, '.
            'requests that post file data must use unique keys.',
            $name));
      }

      $tmp = new TempFile($info['name']);
      Filesystem::writeFile($tmp, $info['data']);
      $this->temporaryFiles[] = $tmp;

      // In 5.5.0 and later, we can use CURLFile. Prior to that, we have to
      // use this "@" stuff.

      if (class_exists('CURLFile', false)) {
        $file_value = new CURLFile((string)$tmp, $info['mime'], $info['name']);
      } else {
        $file_value = '@'.(string)$tmp;
      }

      $data[$name] = $file_value;
    }

    return $data;
  }