stripe / stripe-php

PHP library for the Stripe API.
https://stripe.com
MIT License
3.75k stars 848 forks source link

Indexed arrays in POST requests aren't handled properly #227

Closed talon55 closed 8 years ago

talon55 commented 8 years ago

I've been trying to create a system for generating managed accounts for users in Europe, where money laundering laws demand a fairly complex list of information. In particular, a complete verification requires a list of additional owners, each with almost the complete set of data that the primary user provides. Unfortunately, submitting this information using this SDK causes some weird behavior, which I'll demonstrate using the following code and its output:

use Stripe\Account;
use Stripe\Stripe;
Stripe::setApiKey('<key>');

$request = array (
  'managed' => true,
  'country' => 'GB',
  'email' => 'something@example.com',
  'legal_entity' =>
  array (
    'type' => 'company',
    'business_name' => 'szrdxtfygvuhygft',
    'first_name' => 'srrdyftdr',
    'last_name' => 'fvffctfeg',
    'dob' =>
    array (
      'day' => '11',
      'month' => '12',
      'year' => '1989',
    ),
    'address' =>
    array (
      'country' => 'GB',
      'state' => 'London, City of',
      'city' => 'London',
      'line1' => 'rdtfyguvy',
      'postal_code' => 'N4',
    ),
    'personal_address' =>
    array (
      'city' => 'London',
      'country' => 'GB',
      'line1' => 'tfrdgt6',
      'postal_code' => 'N12',
      'state' => 'London, City of',
    ),
    'additional_owners' =>
    array (
      0 =>
      array (
        'address' =>
        array (
          'city' => 'London',
          'country' => 'GB',
          'line1' => 'ugyvtfrer',
          'postal_code' => 'N15',
          'state' => 'London, City of',
        ),
        'dob' =>
        array (
          'day' => '12',
          'month' => '5',
          'year' => '1970',
        ),
        'first_name' => 'xgvukvfrde',
        'last_name' => 'rtcyvubhy',
      ),
      1 =>
      array (
        'address' =>
        array (
          'city' => 'London',
          'country' => 'GB',
          'line1' => 'yt56r5df4tfy',
          'postal_code' => 'N14',
          'state' => 'London, City of',
        ),
        'dob' =>
        array (
          'day' => '8',
          'month' => '4',
          'year' => '1979',
        ),
        'first_name' => 'yutreuk',
        'last_name' => 'dfcgvhbjihmv',
      ),
      2 =>
      array (
        'address' =>
        array (
          'city' => 'London',
          'country' => 'GB',
          'line1' => 'fcty6he56fyh',
          'postal_code' => 'N13',
          'state' => 'London, City of',
        ),
        'dob' =>
        array (
          'day' => '6',
          'month' => '3',
          'year' => '1982',
        ),
        'first_name' => 'erxtcygvhmu',
        'last_name' => 'esfzrxgtfcygmv',
      ),
    ),
  ),
  'tos_acceptance' =>
  array (
    'date' => 1455838739,
    'ip' => '127.0.0.1',
  ),
);

$acct = Account::create($request);
var_export($acct->__toArray(true));

And the output:

array (
  'id' => 'acct_17g1SWKz6PJOp3cA',
  'object' => 'account',
  'business_logo' => NULL,
  'business_name' => 'szrdxtfygvuhygft',
  'business_url' => NULL,
  'charges_enabled' => true,
  'country' => 'GB',
  'currencies_supported' => 
  array (
    0 => 'usd',
    1 => 'aed',
    2 => 'afn',
    3 => 'all',
    4 => 'amd',
    5 => 'ang',
    6 => 'aoa',
    7 => 'ars',
    8 => 'aud',
    9 => 'awg',
    10 => 'azn',
    11 => 'bam',
    12 => 'bbd',
    13 => 'bdt',
    14 => 'bgn',
    15 => 'bif',
    16 => 'bmd',
    17 => 'bnd',
    18 => 'bob',
    19 => 'brl',
    20 => 'bsd',
    21 => 'bwp',
    22 => 'bzd',
    23 => 'cad',
    24 => 'cdf',
    25 => 'chf',
    26 => 'clp',
    27 => 'cny',
    28 => 'cop',
    29 => 'crc',
    30 => 'cve',
    31 => 'czk',
    32 => 'djf',
    33 => 'dkk',
    34 => 'dop',
    35 => 'dzd',
    36 => 'egp',
    37 => 'etb',
    38 => 'eur',
    39 => 'fjd',
    40 => 'fkp',
    41 => 'gbp',
    42 => 'gel',
    43 => 'gip',
    44 => 'gmd',
    45 => 'gnf',
    46 => 'gtq',
    47 => 'gyd',
    48 => 'hkd',
    49 => 'hnl',
    50 => 'hrk',
    51 => 'htg',
    52 => 'huf',
    53 => 'idr',
    54 => 'ils',
    55 => 'inr',
    56 => 'isk',
    57 => 'jmd',
    58 => 'jpy',
    59 => 'kes',
    60 => 'kgs',
    61 => 'khr',
    62 => 'kmf',
    63 => 'krw',
    64 => 'kyd',
    65 => 'kzt',
    66 => 'lak',
    67 => 'lbp',
    68 => 'lkr',
    69 => 'lrd',
    70 => 'lsl',
    71 => 'ltl',
    72 => 'mad',
    73 => 'mdl',
    74 => 'mga',
    75 => 'mkd',
    76 => 'mnt',
    77 => 'mop',
    78 => 'mro',
    79 => 'mur',
    80 => 'mvr',
    81 => 'mwk',
    82 => 'mxn',
    83 => 'myr',
    84 => 'mzn',
    85 => 'nad',
    86 => 'ngn',
    87 => 'nio',
    88 => 'nok',
    89 => 'npr',
    90 => 'nzd',
    91 => 'pab',
    92 => 'pen',
    93 => 'pgk',
    94 => 'php',
    95 => 'pkr',
    96 => 'pln',
    97 => 'pyg',
    98 => 'qar',
    99 => 'ron',
    100 => 'rsd',
    101 => 'rub',
    102 => 'rwf',
    103 => 'sar',
    104 => 'sbd',
    105 => 'scr',
    106 => 'sek',
    107 => 'sgd',
    108 => 'shp',
    109 => 'sll',
    110 => 'sos',
    111 => 'srd',
    112 => 'std',
    113 => 'svc',
    114 => 'szl',
    115 => 'thb',
    116 => 'tjs',
    117 => 'top',
    118 => 'try',
    119 => 'ttd',
    120 => 'twd',
    121 => 'tzs',
    122 => 'uah',
    123 => 'ugx',
    124 => 'uyu',
    125 => 'uzs',
    126 => 'vnd',
    127 => 'vuv',
    128 => 'wst',
    129 => 'xaf',
    130 => 'xcd',
    131 => 'xof',
    132 => 'xpf',
    133 => 'yer',
    134 => 'zar',
    135 => 'zmw',
  ),
  'debit_negative_balances' => false,
  'decline_charge_on' => 
  array (
    'avs_failure' => false,
    'cvc_failure' => false,
  ),
  'default_currency' => 'gbp',
  'details_submitted' => false,
  'display_name' => NULL,
  'email' => 'eric+corp2@sparehire.com',
  'external_accounts' => 
  array (
    'object' => 'list',
    'data' => 
    array (
    ),
    'has_more' => false,
    'total_count' => 0,
    'url' => '/v1/accounts/acct_17g1SWKz6PJOp3cA/external_accounts',
  ),
  'keys' => 
  array (
    'secret' => 'sk_test_3fRLrelwM0JHbfLXbHzucIHv',
    'publishable' => 'pk_test_F6DTJp0duUEphb87nHgcFXMS',
  ),
  'legal_entity' => 
  array (
    'additional_owners' => 
    array (
      0 => 
      array (
        'address' => 
        array (
          'city' => 'London',
          'country' => 'GB',
          'line1' => 'yt56r5df4tfy',
          'line2' => NULL,
          'postal_code' => 'N14',
          'state' => 'London, City of',
        ),
        'dob' => 
        array (
          'day' => 8,
          'month' => 4,
          'year' => 1979,
        ),
        'first_name' => 'xgvukvfrde',
        'last_name' => 'rtcyvubhy',
        'verification' => 
        array (
          'details' => NULL,
          'details_code' => NULL,
          'document' => NULL,
          'status' => 'unverified',
        ),
      ),
      1 => 
      array (
        'address' => 
        array (
          'city' => 'London',
          'country' => 'GB',
          'line1' => 'fcty6he56fyh',
          'line2' => NULL,
          'postal_code' => 'N13',
          'state' => 'London, City of',
        ),
        'dob' => 
        array (
          'day' => 6,
          'month' => 3,
          'year' => 1982,
        ),
        'first_name' => 'yutreuk',
        'last_name' => 'dfcgvhbjihmv',
        'verification' => 
        array (
          'details' => NULL,
          'details_code' => NULL,
          'document' => NULL,
          'status' => 'unverified',
        ),
      ),
      2 => 
      array (
        'address' => 
        array (
          'city' => NULL,
          'country' => NULL,
          'line1' => NULL,
          'line2' => NULL,
          'postal_code' => NULL,
          'state' => NULL,
        ),
        'dob' => 
        array (
          'day' => NULL,
          'month' => NULL,
          'year' => NULL,
        ),
        'first_name' => 'erxtcygvhmu',
        'last_name' => 'esfzrxgtfcygmv',
        'verification' => 
        array (
          'details' => NULL,
          'details_code' => NULL,
          'document' => NULL,
          'status' => 'unverified',
        ),
      ),
    ),
    'address' => 
    array (
      'city' => 'London',
      'country' => 'GB',
      'line1' => 'rdtfyguvy',
      'line2' => NULL,
      'postal_code' => 'N4',
      'state' => 'London, City of',
    ),
    'address_kana' => 
    array (
      'city' => NULL,
      'country' => 'GB',
      'line1' => NULL,
      'line2' => NULL,
      'postal_code' => NULL,
      'state' => NULL,
      'town' => NULL,
    ),
    'address_kanji' => 
    array (
      'city' => NULL,
      'country' => 'GB',
      'line1' => NULL,
      'line2' => NULL,
      'postal_code' => NULL,
      'state' => NULL,
      'town' => NULL,
    ),
    'business_name' => 'szrdxtfygvuhygft',
    'business_name_kana' => NULL,
    'business_name_kanji' => NULL,
    'business_tax_id_provided' => false,
    'dob' => 
    array (
      'day' => 11,
      'month' => 12,
      'year' => 1989,
    ),
    'first_name' => 'srrdyftdr',
    'first_name_kana' => NULL,
    'first_name_kanji' => NULL,
    'gender' => NULL,
    'last_name' => 'fvffctfeg',
    'last_name_kana' => NULL,
    'last_name_kanji' => NULL,
    'maiden_name' => NULL,
    'personal_address' => 
    array (
      'city' => 'London',
      'country' => 'GB',
      'line1' => 'tfrdgt6',
      'line2' => NULL,
      'postal_code' => 'N12',
      'state' => 'London, City of',
    ),
    'personal_address_kana' => 
    array (
      'city' => NULL,
      'country' => NULL,
      'line1' => NULL,
      'line2' => NULL,
      'postal_code' => NULL,
      'state' => NULL,
      'town' => NULL,
    ),
    'personal_address_kanji' => 
    array (
      'city' => NULL,
      'country' => NULL,
      'line1' => NULL,
      'line2' => NULL,
      'postal_code' => NULL,
      'state' => NULL,
      'town' => NULL,
    ),
    'personal_id_number_provided' => false,
    'phone_number' => NULL,
    'ssn_last_4_provided' => false,
    'type' => 'company',
    'verification' => 
    array (
      'details' => NULL,
      'details_code' => NULL,
      'document' => NULL,
      'status' => 'unverified',
    ),
  ),
  'managed' => true,
  'metadata' => 
  array (
  ),
  'product_description' => NULL,
  'statement_descriptor' => NULL,
  'support_phone' => NULL,
  'timezone' => 'Etc/UTC',
  'tos_acceptance' => 
  array (
    'date' => 1455838739,
    'ip' => '127.0.0.1',
    'user_agent' => NULL,
  ),
  'transfer_schedule' => 
  array (
    'delay_days' => 7,
    'interval' => 'daily',
  ),
  'transfers_enabled' => false,
  'verification' => 
  array (
    'disabled_reason' => NULL,
    'due_by' => NULL,
    'fields_needed' => 
    array (
      0 => 'external_account',
      1 => 'legal_entity.additional_owners.2.dob.day',
      2 => 'legal_entity.additional_owners.2.dob.month',
      3 => 'legal_entity.additional_owners.2.dob.year',
    ),
  ),
)

The problem here is that each subarray within the additional_owners field (ie. 'address' and 'dob') is getting shifted up in the response. This is easiest to see for dob because it's all numeric:

In the request:

'dob' => // index 0
        array (
          'day' => '12',
          'month' => '5',
          'year' => '1970',
        ),
'dob' => // index 1
        array (
          'day' => '8',
          'month' => '4',
          'year' => '1979',
        ),
 'dob' => // index 2
        array (
          'day' => '6',
          'month' => '3',
          'year' => '1982',
        ),

And the response:

'dob' => // index 0
        array (
          'day' => 8,
          'month' => 4,
          'year' => 1979,
        ),
 'dob' => // index 1
        array (
          'day' => 6,
          'month' => 3,
          'year' => 1982,
        ),
'dob' => // index 2
        array (
          'day' => NULL,
          'month' => NULL,
          'year' => NULL,
        ),

I've done some testing, and I've traced the issue to how the SDK and API handle indexed arrays. Specifically, lines 210-214 in Stripe\HttpClient\CurlClient::encode() differentiate between indexed and associative arrays:

if ($prefix && $k && !is_int($k)) {
    $k = $prefix."[".$k."]";
} elseif ($prefix) {
    $k = $prefix."[]";
}

If I modify it to look like this:

if ($prefix && ($k || $k === 0)) {
    $k = $prefix."[".$k."]";
}

Then the API receives the data correctly (albeit with the additional_ownersarray represented as an object with numeric indices) and provides the proper response. I'm not sure if you can view data from my Stripe account, but the response logs for this test are at https://dashboard.stripe.com/test/logs/req_7vsH7fDUhIkpR3.

Please let me know if I can provide any more information to help you resolve this bug.

talon55 commented 8 years ago

I did a little bit more digging, and it turns out that the issue depends on the order of the keys in the nested subarrays. From the above example, this causes the bug:

'address' =>array (...),
'dob' => array (...),
'first_name' => 'xgvukvfrde',
'last_name' => 'rtcyvubhy',

but this works as intended:

'first_name' => 'xgvukvfrde',
'last_name' => 'rtcyvubhy',
'address' =>array (...),
'dob' => array (...),
bkrausz commented 8 years ago

Thanks for the detailed report @saber3005! Looking at the log line you sent over, it looks like this was received properly in your request, and the correct response was sent back. I suspect the issue was with the update post-creation (since we take the response we get back from the server and update the Account object with its data). I'm digging deeper now.

bkrausz commented 8 years ago

(Nevermind, I've reproduced exactly as you described this)

brandur commented 8 years ago

@bkrausz Thanks for the fix!

Released as part of 3.9.1.