Odyssey
MoneyDesk3.prg
1 <?php
2 
3 # MoneyDesktop data feed using MDX 5 specifications
4 #
5 #
6 # CU : Client Code
7 #
8 # note that request will include either userkey (indicating SSO)
9 # or login/password (indicating aggregated access)
10 #
11 # login : login id, or possibly cu:member
12 # password : password for aggregated account
13 # userkey : token for SSO access (replaces both login and password)
14 # start_date : Start date, default to 30days past
15 # page : selected page of possible pagination. We always use page=1 of 1
16 #
17 
18 $serviceMinimal = true;
19 $serviceShowInfo = false;
20 $serviceLoadMenu = false;
21 $serviceShowMenu = false;
22 
23 // ** INCLUDE MAIN GLOBAL SCRIPT -- Handles security / global variable values
24 // hcuService will be returning a status object: e.g. ["homecuErrors":{[{"message":"message1"}...{"message":"messageN"}}]
25 require_once(dirname(__FILE__) . '/../library/hcuService.i');
26 require_once(dirname(__FILE__) . '/../library/hcuDispFunctions.i');
27 require_once('hcuAuthShared.i');
28 require_once('MDesk_API.i');
29 require_once('cutrusted.i');
30 require_once('LogSSO.i');
31 
32 /*
33  * Force logging
34  */
35 //$logSessionData = file_get_contents('php://input');
36 //$logheaders = apache_request_headers();
37 //$logReqPathArr = explode("/", $_SERVER['PATH_INFO']);
38 //$logCU = HCU_array_key_value(1, $logReqPathArr);
39 //$logEnd = end($logReqPathArr);
40 //
41 //$logParms = array("Cu" => $logCU, // credit union
42 // "memberId" => '', // member id
43 // "SSOVendor" => 'MDX',
44 // "userIP" => $_SERVER['REMOTE_ADDR'], // user's ip address
45 // "dbConn" => $dbh); // get the environment info passed in
46 //$logParms["token"] = ''; // the id used across all communications in session
47 //$logParms["txnId"] = time(); // the id for this transaction
48 //$logParms["logPoint"] = "MDX $logEnd"; // this action in a readable form
49 //$logParms["request"] = "HEADERS: " . json_encode($logheaders) . "\n\n"; // the request
50 //$logParms["request"] .= "PATH: {$_SERVER['PATH_INFO']}\n";
51 //$logParms["request"] .= "INPUT: $logSessionData"; // the request
52 //$logParms["reply"] = ''; // the response
53 //LogSSOActivity($logParms);
54 /*
55  * End Force logging
56  */
57 
58 // setIncludeFiles(false,true,'bootstrap',true);
59 # php 5 import variables
60 #
61 //require("dms_imp_val.i");
62 $inPost = array();
63 $varOk = array(
64  "start_date" => array('filter' => FILTER_SANITIZE_STRING),
65  "page" => array('filter' => FILTER_SANITIZE_STRING)
66 );
67 
68 HCU_ImportVars($inPost, "", $varOk);
69 
70 $encoding = 'UTF-8';
71 $SENDAS = 'XML'; #set a sane default - can be overridden in trusted detail -
72 # but need a value in case of early error.
73 # consider trusted detail settings for these ?
74 $client_source_override = 'MDX'; # unique for Mx
75 
76 function trim_item(&$item, $key) {
77  $item = trim($item);
78 }
79 
80 try {
81  array_walk_recursive($inPost, 'trim_item');
82 
83  $mxReqPathArr = explode("/", $_SERVER['PATH_INFO']);
84 
85  $CU = HCU_array_key_value(1, $mxReqPathArr);
86  # look a little closer at that CU value --
87  # ctype_alnum ensures not empty / null, and contains only letters / digits
88  if (!ctype_alnum($CU)) {
89  throw new Exception("Invalid Request ", 9401); # empty ORG
90  }
91 
92  if (!hcu_checkService($dbh, "MDESK")) {
93  $omsg = hcu_checkServiceMsg($dbh, "MDESK");
94  throw new Exception("$omsg", 9503);
95  }
96 
97  # cutrusted_read will fall back to master if detail not found
98  $mxTrusted = cutrusted_read($dbh, array('Cu' => $CU, 'trustedid' => 'MDesk3'));
99 
100  if ($mxTrusted['status']['Response'] == 'false') {
101  throw new Exception("Client not configured for remote Mx access", 9500); # missing trusted detail
102  }
103  if (HCU_array_key_exists('data', $mxTrusted)) {
104  $mxParms = $mxTrusted['data'];
105  } else {
106  throw new Exception("Client not configured for remote Mx access", 9500); # missing trusted detail parms
107  }
108  # previous hcu_checkService looks for all MDesk services down at server level
109  # now look for MDesk service down for this particular client
110  if (HCU_array_key_value('mxSvsDown', $mxParms)) {
111  $omsg = (HCU_array_key_value('mxDownMsg', $mxParms) ? $mxParms['mxDownMsg'] : 'Service temporarily unavailable. Please try again later');
112  throw new Exception("$omsg", 9503);
113  }
114 
115  # mdTokenKey used to hash Mx user key
116  $mdtokenkey = HCU_array_key_value('mdTokenKey', $mxParms);
117 
118  if (empty($mdtokenkey)) {
119  throw new Exception("Client misconfigured for remote Mx access", 9500); # missing trusted detail parms (keys)
120  }
121 
122  # revisit this code - trying to ensure ipWhiteList is array for array merge, even if it is empty
123  $ipWhiteList = ( HCU_array_key_exists('ipWhiteList', $mxParms) ? HCU_array_key_value('ipWhiteList', $mxParms) : '' );
124  # strip anything other than digits, hyphens, dots, and the commas
125  $ipWhiteList = preg_replace('/[^0-9-\.\/,]/', '', $ipWhiteList);
126 
127  if (!empty($ipWhiteList)) {
128  # split into array
129  $ipArray = explode(',', $ipWhiteList);
130  } else {
131  $ipArray = array();
132  }
133 
134  # add HomeCU addresses to array
135  $ipallow = array_merge(array('199.184.207', # allow DMS/Homecu also
136  '192.168.169',
137  '172.16.0.0/12', # 172.16.0.0 – 172.31.255.255 for Docker
138  '174.129.8.163', #(www6)
139  '174.129.23.225', #(www5)
140  '107.21.119.104', #(www3)
141  '107.20.248.233', #(www0 / devel)
142  '34.214.70.112', #(my.homecu.net / alpha.homecu.io)
143  '34.214.139.200', #(my.homecu.net / alpha.homecu.io)
144  '34.214.230.179', #(my.homecu.net / alpha.homecu.io)
145  '209.161.7.186', # (DMS DSL Line)
146  '184.73.202.7' # (monitor)
147  ), $ipArray);
148  $IPPass = false;
149  $IPCaller = $_SERVER['REMOTE_ADDR'];
150  $IPPass = whitelist($IPCaller, $ipallow);
151 
152  if ($IPPass == false) {
153  throw new Exception("Unauthorized Access {$_SERVER['REMOTE_ADDR']}", 9403); # IP whitelist fail
154  }
155 
156  # default to production mode
157  $testing = ( HCU_array_key_exists('testing', $mxParms) ? HCU_array_key_value('testing', $mxParms) : 0 );
158 
159  if ($testing) {
160  # HCU-issued sharekey used by trusted vendor for hashing CRED2/CRED3
161  # maybe not needed for MDX5?
162  $mdkey = HCU_array_key_value('mdTestKey', $mxParms);
163  $mdAPI_key = HCU_array_key_value('testAPIKey', $mxParms);
164  $mdData_URL = HCU_array_key_value('testServerURL', $mxParms);
165  $mxHMACKey = HCU_array_key_value('testHMACKey', $mxParms);
166  } else {
167  # HCU-issued sharekey used by trusted vendor for hashing CRED2/CRED3
168  # maybe not needed for MDX5?
169  $mdkey = HCU_array_key_value('mdShareKey', $mxParms);
170  $mdAPI_key = HCU_array_key_value('APIKey', $mxParms);
171  $mdData_URL = HCU_array_key_value('ServerURL', $mxParms);
172  $mxHMACKey = HCU_array_key_value('HMACKey', $mxParms);
173  }
174 
175  if (empty($mdkey)) {
176  throw new Exception("Client misconfigured for remote authentication", 9500); # missing trusted detail parms (keys)
177  }
178 
179  if (empty($mxHMACKey)) {
180  throw new Exception("Client misconfigured for remote Mx access", 9500); # missing trusted detail parms (keys)
181  }
182 
183  $mxRestOpt = end($mxReqPathArr);
184  if ($mxRestOpt == 'sessions') {
185  $mxSessionData = file_get_contents('php://input');
186  } else {
187  $mxSessionData = ''; # 'accounts' & 'transactions' are GET - no body
188  }
189  $headers = apache_request_headers();
190  $validRequest = mxValidRequest($headers, $mxHMACKey, $mxRestOpt, $mxSessionData);
191 
192  if (HCU_array_key_value('status', $validRequest)) {
193  $reason = HCU_array_key_value('message', $validRequest);
194  throw new Exception("Invalid Request", 9412);
195  }
196 
197  $mdExtendable = ( HCU_array_key_exists('mdExtendable', $mxParms) ? HCU_array_key_value('mdExtendable', $mxParms) : 0 );
198  if ($mdExtendable && ( empty($mdAPI_key) || empty($mdData_URL) )) {
199  throw new Exception("Client misconfigured for extendable token", 9500); # missing trusted detail parms (URL &/or API key)
200  }
201 
202  $SENDAS = ( HCU_array_key_exists('resultsAs', $mxParms) ? HCU_array_key_value('resultsAs', $mxParms) : 'XML' );
203  $client_key_TTL = ( HCU_array_key_exists('mdTokenTTL', $mxParms) ? HCU_array_key_value('mdTokenTTL', $mxParms) : 1800 );
204 
205  # start building a stub HB_ENV
206  $HB_ENV['Cu'] = $CU;
207  $HB_ENV['platform'] = $client_source_override;
208  $HB_ENV['client_key_TTL'] = $client_key_TTL;
209  $UPDAWARE = 0; # Mx cannot update security -
210  # set a 'No Activate' flag so we don't try to add live users w/pin
211  # this could be a trusted detail also, but in this case Mx
212  # cannot 1st-time activate, so force the setting
213  $HB_ENV['NoActivation'] = 1;
214 
215  if (!in_array($mxRestOpt, array('sessions', 'accounts', 'transactions'))) {
216  throw new Exception('Invalid Request', 9404);
217  }
218 
219  if ($mxRestOpt == 'accounts' || $mxRestOpt == 'transactions') {
220  $mxSessionKey = HCU_array_key_value('MDX-Session-Key', $headers);
221  if (empty($mxSessionKey)) {
222  throw new Exception("No Session Key", 9401);
223  }
224  $mxBundle = openMxKey($CU, $mxSessionKey);
225  if (empty($mxBundle)) {
226  throw new Exception("Invalid Session Key (empty)", 9401);
227  }
228  if (HCU_array_key_value('kt', $mxBundle) != 'A') {
229  throw new Exception("Invalid Session Key (type)", 9401);
230  }
231  if (HCU_array_key_value('ttl', $mxBundle) < time()) {
232  throw new Exception("Expired Session Key", 9401);
233  }
234 # # build UserKey
235  $login = HCU_array_key_value('u', $mxBundle);
236  if (empty($login)) {
237  throw new Exception("Invalid Session Key (user)", 9401);
238  }
239 
240  Load_HB_ENVc($dbh, $CU, $login, $HB_ENV); # user_name / member number problem USERNAME
241  # make sure we found somebody
242  if (!HCU_array_key_value('rowfound', $HB_ENV)) {
243  throw new Exception("Invalid User", 9404);
244  }
245 
246  $userrec = GetUserbyName($dbh, $CU, $login);
247  if (!HCU_array_key_value('rowfound', $userrec)) {
248  throw new Exception("Invalid Session Key (user)", 9401);
249  }
250 
251  mdesk_setLogging($dbh, $CU, $login, $mxParms);
252 
253  $mfadate = HCU_array_key_value('mfadate', $userrec);
254  if (HCU_array_key_value('cd', $mxBundle) < $mfadate) {
255  throw new Exception("Invalid Session Key (old cred)", 9401);
256  }
257  }
258 
259  if ($mxRestOpt == 'sessions') {
260  $mxSessionKey = HCU_array_key_value('MDX-Session-Key', $headers);
261  if (!empty($mxSessionKey)) {
262  $mxBundle = openMxKey($CU, $mxSessionKey);
263  if (empty($mxBundle)) {
264  throw new Exception("Invalid Session Key (empty)", 9401);
265  }
266  if (HCU_array_key_value('kt', $mxBundle) != 'S') {
267  throw new Exception("Invalid Session Key (type)", 9401);
268  }
269  if (HCU_array_key_value('ttl', $mxBundle) < time()) {
270  throw new Exception("Expired Session Key", 9401);
271  }
272  # # build UserKey
273  $login = HCU_array_key_value('u', $mxBundle);
274  if (empty($login)) {
275  throw new Exception("Invalid Session Key (user)", 9401);
276  }
277  Load_HB_ENVc($dbh, $CU, $login, $HB_ENV); # user_name / member number problem USERNAME
278  # make sure we found somebody
279  if (!HCU_array_key_value('rowfound', $HB_ENV)) {
280  throw new Exception("Invalid User", 9404);
281  }
282 
283  $userrec = GetUserbyName($dbh, $CU, $login);
284  if (!HCU_array_key_value('rowfound', $userrec)) {
285  throw new Exception("Invalid Session Key (user)", 9401);
286  }
287 
288  mdesk_setLogging($dbh, $CU, $login, $mxParms);
289  } else {
290  # initial sessions request / no session key
291  # populate $login
292  #
293  # parse body xml in mxSessionData to get parameters to test
294  $sessionXML = simplexml_load_string($mxSessionData, 'SimpleXMLElement', LIBXML_NOCDATA);
295  $sessionJSON = json_encode($sessionXML);
296  $sessionArray = json_decode($sessionJSON, true);
297 
298 
299  $session = HCU_array_key_value('session', $sessionArray);
300 
301  if (HCU_array_key_value('userkey', $session)) {
302  # got a userkey, validate it
303 
304  $ckKey = CheckV94key($CU, HCU_array_key_value('userkey', $session), $mdtokenkey, 'S');
305  # if userkey check fails
306  if (HCU_array_key_value('Code', $ckKey['Status']) > 0) {
307  $status = HCU_array_key_value('Message', $ckKey['Status']);
308  $code = HCU_array_key_value('Code', $ckKey['Status']);
309  # be careful what you add to status message -
310  # unescaped characters will cause XML parse error on error output
311 // $HB_ENV["SYSENV"]["logger"]->info(__LINE__ . " ckKey: " . json_encode($ckKey));
312  throw new Exception("Invalid Credentials (userkey) {$status}", 9401);
313  }
314  # set $login from userkey content
315  $apptokarr = HCU_array_key_value('data', $ckKey);
316  $login = HCU_array_key_value('A', $apptokarr);
317 
318  $userrec = GetUserbyName($dbh, $CU, $login);
319  if (!HCU_array_key_value('rowfound', $userrec)) {
320  throw new Exception("Invalid User", 9404);
321  }
322 
323  mdesk_setLogging($dbh, $CU, $login, $mxParms);
324 
325  $ckarr = array();
326  $cktoken = MakeV94Dkey($CU, $login, $userrec, $client_key_TTL, $mdtokenkey, 'S');
327  parse_str(urldecode($cktoken), $ckarr);
328  if (HCU_array_key_value('P', $ckarr) !== HCU_array_key_value('P', $apptokarr)) {
329  throw new Exception("Invalid Credentials (authentication required)", 9401);
330  }
331  if ($mdExtendable) {
332  # if configured to do so and the userkey is more than 15 min old
333  # extend the userkey stored at Mx
334  $ckExp = HCU_array_key_value('E', $ckarr);
335  $tkExp = HCU_array_key_value('E', $apptokarr);
336  if ($ckExp - $tkExp > 900) {
337  # if the new token would expire more than 15min later than the current one, set a new one
338  mdesk_setLogging($dbh, $CU, $login, $mxParms);
339  $mxUpdated = mdesk_sync($mdData_URL, $mdAPI_key, $CU, $HB_ENV['Uid'], $HB_ENV['Cn'], $HB_ENV['Ml'], $cktoken, $mxParms);
340  if (HCU_array_key_value('code', $mxUpdated) > 0) {
341  $status = HCU_array_key_value('status', $mxUpdated);
342  $code = HCU_array_key_value('code', $mxUpdated);
343  throw new Exception("Error ({$status} {$code}). Unable to connect to MoneyDesktop.", 9500);
344  }
345  }
346  }
347  } else {
348  # no userkey - check login and password
349  $login = HCU_array_key_value('login', $session);
350  $userpass = HCU_array_key_value('password', $session);
351 
352  if (empty($login) || preg_match("/[\\\`,\"\s;]/", $login) || empty($userpass)) { # user_name / member number problem MEMBERACCT
353  throw new Exception("Invalid Credentials", 9400); # Member Bad Characters or empty passwd
354  }
355 
356  mdesk_setLogging($dbh, $CU, $login, $mxParms);
357 
358  # strip leading zeroes, unless configured not to do so
359  if (!(preg_match("/\D/", $login)) && !HCU_array_key_value('preserveZeros', $mxParms)) {
360  $login = preg_replace("/^0*/", "", $login);
361  }
362  }
363  }
364  # parse body xml in mxSessionData to get parameters to test
365  $sessionXML = simplexml_load_string($mxSessionData, 'SimpleXMLElement', LIBXML_NOCDATA);
366  $sessionJSON = json_encode($sessionXML);
367  $sessionArray = json_decode($sessionJSON, true);
368 
369 
370  $session = HCU_array_key_value('session', $sessionArray);
371 
372  if (HCU_array_key_value('login', $session) || HCU_array_key_value('password', $session)) {
373  # if got login and password?
374  $login = HCU_array_key_value('login', $session);
375  $userpass = HCU_array_key_value('password', $session);
376 
377  if (empty($login) || preg_match("/[\\\`,\"\s;]/", $login) || empty($userpass)) { # user_name / member number problem MEMBERACCT
378  throw new Exception("Invalid Credentials", 9400); # Member Bad Characters or empty passwd
379  }
380  # strip leading zeroes, unless configured not to do so
381  if (!(preg_match("/\D/", $login)) && !HCU_array_key_value('preserveZeros', $mxParms)) {
382  $login = preg_replace("/^0*/", "", $login);
383  }
384  Load_HB_ENVc($dbh, $CU, $login, $HB_ENV); # user_name / member number problem USERNAME
385  # make sure we found somebody
386  if (!HCU_array_key_value('rowfound', $HB_ENV)) {
387  throw new Exception("Invalid User", 9404);
388  }
389  # removed this part of the test - failed because the hash doesn't start with $1$ anymore
390  # and ... ancient news that older crypt only tested first 8 char of passwd and would get
391  # false match. MD5 cured that (hence the $1$ test), and we have long since moved on
392  # && ( preg_match('/^\$1\$/', $HB_ENV['password']) || strlen($userpass) < 9)
393 
394  if (!(trim($userpass) > '' && password_verify($userpass, $HB_ENV['password']) )) {
395  LogFail($dbh, $HB_ENV, array('ORG' => $CU), GetUserFlagsValue('MEM_LOGIN_FAILED_PWD'));
396  throw new Exception("Authentication Failed (password)", 9401); # password
397  }
398  $userrec = GetUserByName($dbh, $CU, $login); # user_name / member number problem USERNAME
399  # make sure we found somebody
400  if (!HCU_array_key_value('rowfound', $userrec)) {
401  throw new Exception("Invalid User", 9404);
402  }
403 
404  # if locked, throw error 4011 Locked Account
405  if ($userrec['lockedacct']) {
406  throw new Exception('Member Account Locked', 9401); # locked
407  }
408  if (($userrec['forceupdate'] && ($UPDAWARE & $userrec['forceupdate']) < $userrec['forceupdate'] ) ||
409  empty($userrec['email'])) {
410  throw new Exception('Member Account Access Blocked', 9401); # Blocked waiting for required security updates
411  }
412 
413  # password ok, if 2-factor, send MFA challenge or fall thru to send data
414 
415  if (HCU_array_key_value('cver', $HB_ENV) == 'F') {
416  # if cu using MFA
417  # build MFA-pending key
418  $mxBundle = array(
419  'u' => $login,
420  'p' => $password, # really?
421  'kt' => 'S',
422  'ttl' => time() + 600, # session key, 10 minutes to complete
423  'cd' => time()); # show MFA change date as now
424  $SessionKey = createMxKey($CU, $mxBundle);
425  if (empty($SessionKey)) {
426  throw new Exception("Key Creation Failed", 9500);
427  }
428 
429  # send challenge
430  $data_reply = Mx5_challenge($dbh, $HB_ENV, $MC);
431  if (HCU_array_key_value('Status', $data_reply) !== 'Success') {
432  throw new Exception("Building Challenge " . $data_reply['Status'], 9500); # email
433  }
434  $reply_arr = HCU_array_key_value('data', $data_reply);
435  $reply_arr['session']['key'] = $SessionKey;
436  } else {
437  # login/password validated, and not 2-factor
438  # build UserKey with TTL lifespan to store at Mx
439 // $PCHANGE = strtotime($HB_ENV['pwchange']);
440  $userrec = GetUserbyName($dbh, $CU, $HB_ENV['Cn']);
441  if (!HCU_array_key_value('rowfound', $userrec)) {
442  throw new Exception("Invalid User", 9404);
443  }
444  $mxtoken = MakeV94Dkey($CU, $HB_ENV['Cn'], $userrec, $client_key_TTL, $mdtokenkey, 'S');
445 
446 // # got here via login/userpass no MFA, which means member is aggregating.
447 // # do not make Mx user / member records. But DO return a userkey?
448 // $mxUpdated = mdesk_sync($mdData_URL, $mdAPI_key, $CU, $HB_ENV['Uid'], $HB_ENV['Cn'], $HB_ENV['Ml'], $mxtoken, $parms);
449 // if (HCU_array_key_value('code', $mxUpdated) > 0) {
450 // $status = HCU_array_key_value('status', $mxUpdated);
451 // $code = HCU_array_key_value('code', $mxUpdated);
452 // throw new Exception("Error ({$status} {$code}). Unable to connect to MoneyDesktop", 9500);
453 // }
454  # track login
455  LogPass($dbh, $HB_ENV);
456  # and build a 10-minute session key
457  $mxBundle = array(
458  'u' => $login,
459  'kt' => 'A',
460  'ttl' => time() + 600, # session key, 10 minutes
461  'cd' => time());
462  $SessionKey = createMxKey($CU, $mxBundle);
463  if (empty($SessionKey)) {
464  throw new Exception("Session Key Creation Failed", 9500);
465  }
466 
467  $reply_arr = array('session' => array('key' => $SessionKey, 'mxToken' => $mxtoken));
468  }
469  } elseif (HCU_array_key_exists('challenges', $session) && is_array($session['challenges'])) {
470  # elseif got MFA?
471  # validate
472  # if invalid, throw error 9401
473  # build UserKey
474  # update Mx
475  # end
476  # we have challenge responses - pack them into a list for validation
477  $challresp = array();
478  foreach ($session['challenges']['challenge'] as $chalkey => $chalinfo) {
479  $challresp[$chalinfo['id']] = $chalinfo['answer'];
480  }
481  /*
482  * <mdx version='5.0'>
483  <session>
484  <key>UNIQUE_KEY_FOR_THIS_SESSION</key>
485  <challenges>
486  <challenge>
487  <id>UNIQUE_IDENTIFIER_FOR_THIS_CHALLENGE</id>
488  <answer><![CDATA[answer]]></answer>
489  </challenge>
490  <challenge>
491  <id>UNIQUE_IDENTIFIER_FOR_THIS_CHALLENGE</id>
492  <answer><![CDATA[answer]]></question>
493  </challenge>
494  <!-- additional challenge questions -->
495  </challenges>
496  </session>
497  </mdx>
498  */
499  if (!HCU_array_key_value('MFA_E', $challresp) || !isValidEmail(HCU_array_key_value('MFA_E', $challresp), $userrec)) {
500  LogFail($dbh, $HB_ENV, array('ORG' => $CU), GetUserFlagsValue('MEM_LOGIN_FAILED_EMAIL'));
501  throw new Exception("MFA Failed (EML)", 9401); # email
502  }
503  # and now check the challenge responses
504  /*
505  * passing a temp array so I can turn off the '1 random' setting.
506  * Require all challenge responses from Mx
507  */
508 
509  $tmpEnv = array();
510  $tmpEnv['mfaquest'] = $HB_ENV['mfaquest'];
511  $tmpEnv['Fset2'] = 0; # turn off '1 random'
512  list($qfail, $qreason) = MFQ_response($tmpEnv, $challresp);
513 
514  if ($qfail) {
515  LogFail($dbh, $HB_ENV, array('ORG' => $CU), $failreason, GetUserFlagsValue('MEM_LOGIN_FAILED_QST'));
516  throw new Exception("MFA Failed (Challenge)", 9401); # email
517  }
518  # challenge passed, build a UserKey to store at Mx
519 // $PCHANGE = strtotime($HB_ENV['pwchange']);
520  $userrec = GetUserbyName($dbh, $CU, $HB_ENV['Cn']);
521  if (!HCU_array_key_value('rowfound', $userrec)) {
522  throw new Exception("Invalid User", 9404);
523  }
524  $mxtoken = MakeV94Dkey($CU, $HB_ENV['Cn'], $userrec, $client_key_TTL, $mdtokenkey, 'S');
525 
526 // # got here via login/userpass/MFA, which means member is aggregating.
527 // # do not make Mx user / member records. But DO return a userkey?
528 // $mxUpdated = mdesk_sync($mdData_URL, $mdAPI_key, $CU, $HB_ENV['Uid'], $HB_ENV['Cn'], $HB_ENV['Ml'], $mxtoken, $parms);
529 // if (HCU_array_key_value('code', $mxUpdated) > 0) {
530 // $status = HCU_array_key_value('status', $mxUpdated);
531 // $code = HCU_array_key_value('code', $mxUpdated);
532 // throw new Exception("Error ({$status} {$code}). Unable to connect to MoneyDesktop", 9500);
533 // }
534  # track login
535  LogPass($dbh, $HB_ENV);
536 
537  # and build a 10-minute authenticated session key
538  $mxBundle = array(
539  'u' => $login,
540  'kt' => 'A',
541  'ttl' => time() + 600, # session key, 10 minutes
542  'cd' => $userrec['mfadate']);
543  $SessionKey = createMxKey($CU, $mxBundle);
544  if (empty($SessionKey)) {
545  throw new Exception("Session Key Creation Failed", 9500);
546  }
547 
548 // $reply_arr = array('session' => array('key' => $SessionKey, 'mxToken' => $mxtoken));
549  $reply_arr = array('session' => array('key' => $SessionKey));
550  } elseif (HCU_array_key_exists('userkey', $session)) {
551  # elseif got userkey?
552  # if invalid throw error 9401
553  # if extendable && foreground
554  # update Mx
555  # AuthMode=SSO w/Userkey
556  // see if original userkey or the session ticket
557  $userkey = HCU_array_key_value('userkey', $session);
558  $apptokarr = array();
559  parse_str(urldecode($userkey), $apptokarr);
560 
561  // see if original UserKey
562  if (HCU_array_key_exists('E', $apptokarr) && HCU_array_key_exists('H', $apptokarr)) {
563  $login = $apptokarr['A'];
564  $HMethod = (HCU_array_key_exists('P', $apptokarr) ? 'S' : 'M');
565  $keycheck = CheckV94key($CU, $userkey, $mdtokenkey, $HMethod);
566 
567  if ($keycheck['Status']['Message'] !== 'Success') {
568  throw new Exception($keycheck['Status']['Message'], 9401);
569  }
570  } else {
571  throw new Exception("Invalid Userkey Credentials Corrupt", 9401);
572  }
573  # and build a 10-minute authenticated session key
574  $mxBundle = array(
575  'u' => $login,
576  'kt' => 'A',
577  'ttl' => time() + 600, # session key, 10 minutes
578  'cd' => time()); # used $userrec['mfadate'] but userrec undefined here?
579  $SessionKey = createMxKey($CU, $mxBundle);
580  if (empty($SessionKey)) {
581  throw new Exception("Session Key Creation Failed", 9500);
582  }
583  $reply_arr = array('session' => array('key' => $SessionKey));
584  $mxSessionKey = $SessionKey; # so logging gets the right value
585  $mxParms["environment"]["reply"] = "$SessionKey";
586  } else {
587  # throw error 4102 no userkey and no credentials
588  throw new Exception("Invalid Userkey Credentials Missing ", 9401);
589  }
590  }
591  if ($mxRestOpt == 'accounts') {
592  # checking key, loading HB_ENV & userrec occurs at top
593  # return account data
594  #GET /accounts
595  $HB_ENV['HideCert'] = HCU_array_key_value('HideCert', $mxParms);
596  $data_reply = Mx5_accts($dbh, $HB_ENV);
597  # check for errors
598  if (HCU_array_key_value('Status', $data_reply) !== 'Success') {
599  throw new Exception("Retrieving Accounts " . $data_reply['Status'], 9500); # email
600  }
601  $reply_arr = HCU_array_key_value('data', $data_reply);
602  if (HCU_array_key_value('hashlist', $data_reply) && is_array($data_reply['hashlist']) ) {
603  $mxParms["environment"]["reply"] = '';
604  foreach($data_reply['hashlist'] as $hash => $acct) {
605  $mxParms["environment"]["reply"] .= "$hash : {$acct['acct']} \n";
606  }
607  }
608  }
609  if ($mxRestOpt == 'transactions') {
610  # checking key, loading HB_ENV & userrec occurs at top
611  # validate keyacctid
612  # return transaction data
613  #GET /client-id/accounts/:account_id/transactions?start_date=YYYY-MM-DD&page=n
614  # parse to get $account_id and $start date, set $end_date
615  # tie the array index to 'accounts' +1 in case the path changes?
616  # account_id should be encrypted or something to hide it
617  # and then un-hide it here before calling for history
618  $account_id = $mxReqPathArr[3];
619  $HB_ENV['HideCert'] = HCU_array_key_value('HideCert', $mxParms);
620  $hcuList = Mx5_accts($HB_ENV['dbh'], $HB_ENV);
621 // $HB_ENV["SYSENV"]["logger"]->info(" id: {$account_id} " . print_r($hcuList,true));
622 
623  if (HCU_array_key_exists('hashlist', $hcuList) &&
624  HCU_array_key_exists($account_id, $hcuList['hashlist'])) {
625  $KEYACCTID = $hcuList['hashlist'][$account_id]['acct']; # gets C|666665|12
626  $KEYACCTPOS = $hcuList['hashlist'][$account_id]['pos']; # gets numeric idx
627  $mxParms["environment"]["reply"] = "$account_id : $KEYACCTID ";
628 // $HB_ENV["SYSENV"]["logger"]->info(" id: {$KEYACCTID} pos: {$KEYACCTPOS}");
629  } else {
630  throw new Exception("Requested account not found", 9404); # email
631  }
632  $varOk = array(
633  "start_date" => array('filter' => FILTER_SANITIZE_STRING),
634  "end_date" => array('filter' => FILTER_SANITIZE_STRING),
635  "page" => array('filter' => FILTER_SANITIZE_NUMBER_INT));
636 
637  HCU_ImportVars($mxPosted, "", $varOk);
638  $start_date = HCU_array_key_value('start_date', $mxPosted);
639  $end_date = HCU_array_key_value('end_date', $mxPosted);
640  $dflt_end = date("Y-m-d", time() + (4 * 24 * 60 * 60)); # + 4 days
641  $end_date = (empty($end_date) ? $dflt_end : $end_date);
642  $page = HCU_array_key_value('page', $mxPosted);
643  $page = (empty($page) ? 1 : $page);
644 
645  $balinfo = $hcuList['data'][$KEYACCTPOS]['account'];
646 // $HB_ENV["SYSENV"]["logger"]->info(print_r($balinfo,true));
647 
648  switch (substr($KEYACCTID, 0, 1)) {
649  case 'D':
650  # test for locked / unviewable
651 // $reply_arr = Mx5_dpTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
652  $data_reply = Mx5_dpTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
653  break;
654  case 'L':
655  # test for locked / unviewable
656  # test for info from 3rd party, treat as unviewable
657 // $reply_arr = Mx5_lnTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
658  $data_reply = Mx5_lnTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
659  break;
660  case 'C':
661  # test for locked / unviewable
662  # test for info from 3rd party, treat as unviewable
663 // $reply_arr = Mx5_ccTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
664  $data_reply = Mx5_ccTrans($dbh, $HB_ENV, $KEYACCTID, $balinfo, $start_date, $end_date);
665  break;
666  default:
667  throw new Exception("Invalid account requested", 9404); # email
668  break;
669  }
670  # check for errors
671  if (HCU_array_key_value('Status', $data_reply) !== 'Success') {
672  throw new Exception("Retrieving Transactions " . $data_reply['Status'], 9500); # email
673  }
674 
675  $reply_arr = HCU_array_key_value('data', $data_reply);
676  if (HCU_array_key_exists('account',$reply_arr) && HCU_array_key_exists('transactions', $reply_arr['account'])) {
677  # transactions array has 1 entry even if there are no actual transactions
678  $trancount = (integer) count($reply_arr['account']['transactions']) -1;
679  $mxParms["environment"]["reply"] .= "\n[$start_date - $end_date $trancount transactions]";
680  }
681  }
682  /*
683  * logging here?
684  *
685  */
686  if ($mxParms["logging"] == "enabled") {
687  $logParms = $mxParms["environment"]; // get the environment info passed in
688 
689  $logSessionData = file_get_contents('php://input');
690  $logheaders = apache_request_headers();
691  $logReqPathArr = explode("/", $_SERVER['PATH_INFO']);
692  $logEnd = end($logReqPathArr);
693 
694  $logParms["token"] = $mxSessionKey; // MDX session key, or a substr of it?
695  $logParms["txnId"] = time(); // the id for this transaction
696  $logParms["logPoint"] = "MDX $logEnd"; // this action in a readable form
697  $logParms["request"] = "HEADERS: " . json_encode($logheaders) . "\n\n";
698  $logParms["request"] .= "PATH: {$_SERVER['PATH_INFO']}\n";
699  $logParms["request"] .= "INPUT: $logSessionData";
700 
701  LogSSOActivity($logParms);
702  }
703  /*
704  *
705  */
706 
707  send_response($reply_arr, 200, $SENDAS);
708 #========== END of PROCESSING ============
709 
710  /*
711  * may need to move the apache_note after the sql, and use both user_name and alias.
712  * check cuauth to see about urlencoding alias
713  */
714  apache_note('user_name', "{$CU}:{$login}"); # user_name / member number problem Which one does Odyssey get?
715 } catch (Exception $ex) {
716  $httpcode = $ex->getCode();
717  $httpcode -= 9000;
718  $reply_arr = array('error' =>
719  array('code' => $ex->getCode(), 'message' => $ex->getMessage() . " (" . $ex->getLine() . ")"));
720  send_response($reply_arr, $httpcode, $SENDAS);
721 }
722 
723 function send_response($reply_arr, $httpcode, $sendas = 'XML') {
724  if (!$httpcode) {
725  $httpcode = 200; # if code not set, default to 200 OK
726  }
727  http_response_code($httpcode);
728 
729  switch ($sendas) {
730  case 'JSON':
731  $xmlResp = HCU_JsonEncode($reply_arr);
732  header("Content-Type: application/json");
733 // header("Content-disposition: inline; filename=\"{$HB_ENV['Cu']}_txns.json\"");
734  break;
735  case 'XML':
736  default:
737  $attrs = array('version' => '5.0');
738  $xmlResp = Format_AppFeed(assocArrayToXML($reply_arr, 'mdx', $attrs));
739 // $xmlResp = assocArrayToXML($reply_arr, 'mdx', $attrs);
740  break;
741  }
742  header("Content-Type: application/vnd.moneydesktop.mdx.v5+xml");
743  header("Content-length: " . strlen($xmlResp));
744  print $xmlResp;
745 
746  /*
747  * Force logging
748  */
749 // if ($httpcode != 200) {
750 //$logSessionData = file_get_contents('php://input');
751 //$logheaders = apache_request_headers();
752 //$logReqPathArr = explode("/", $_SERVER['PATH_INFO']);
753 //$logCU = HCU_array_key_value(1, $logReqPathArr);
754 //$logEnd = end($logReqPathArr);
755 //
756 //$logParms = array("Cu" => $logCU, // credit union
757 // "memberId" => '', // member id
758 // "SSOVendor" => 'MDX',
759 // "userIP" => $_SERVER['REMOTE_ADDR'], // user's ip address
760 // "dbConn" => $GLOBALS['dbh']); // get the environment info passed in
761 //$logParms["token"] = ''; // the id used across all communications in session
762 //$logParms["txnId"] = time(); // the id for this transaction
763 //$logParms["logPoint"] = "MDX $logEnd Error"; // this action in a readable form
764 //$logParms["request"] = "HEADERS: " . json_encode($logheaders) . "\n\n"; // the request
765 //$logParms["request"] .= "PATH: {$_SERVER['PATH_INFO']}\n";
766 //$logParms["request"] .= "INPUT: $logSessionData"; // the request
767 //$logParms["reply"] = "HTTP Code: $httpcode\n"; // the response
768 //$logParms["reply"] .= "\nContent-Type: application/vnd.moneydesktop.mdx.v5+xml";
769 //$logParms["reply"] .= "\nContent-length: " . strlen($xmlResp);
770 //$logParms["reply"] .= "$xmlResp\n";
771 //LogSSOActivity($logParms);
772 // }
773  /*
774  * End Force logging
775  */
776 
777  exit;
778 }
779 
780 function sqlmdy($date) {
781 
782  if (strtolower($date) == "now" || strtolower($date) == "today") {
783  $date = date("Y-m-d");
784  }
785  # only allow 0-9 and dash(-) or slash (/)
786  # also allow dot (.) for milliseconds
787  if (preg_match("/[^0-9\-\/\.]/", $date)) {
788  return false;
789  }
790  if (preg_match("/[-\/]/", $date)) {
791  list ($yy, $mm, $dd) = preg_split("/[-\/\.]/", $date);
792  } else {
793  $yy = substr($date, 0, 4);
794  $mm = substr($date, 4, 2);
795  $dd = substr($date, 6, 2);
796  }
797  $mm = sprintf("%02d", intval($mm));
798  $dd = sprintf("%02d", intval($dd));
799  if (strlen($yy) > 0 && strlen($yy) < 4) {
800  $yy = ($yy < 70 ? 2000 + $yy : 1900 + $yy);
801  }
802  $yy = sprintf("%04d", intval($yy));
803  if (checkdate($mm, $dd, $yy)) {
804  return "${yy}${mm}${dd}";
805  } else {
806  return false;
807  }
808 }
809 
810 /**
811  *
812  * @param string $IPCaller ip address of calling client
813  * @param array $ipWhiteList array of acceptable ip addresses, possible cidr
814  * @return boolean IPCaller found acceptable or not?
815  */
816 function whitelist($IPCaller, $ipWhiteList) {
817  $IPPass = false;
818 
819  foreach ($ipWhiteList as $value) {
820  if (strpos($value, '/')) {
821  if (cidr_match($IPCaller, $value)) {
822  $IPPass = true;
823  break;
824  }
825  } elseif (substr($IPCaller, 0, strlen(trim($value))) == trim($value)) {
826  $IPPass = true;
827  break;
828  }
829  }
830  return $IPPass;
831 }
832 
833 /**
834  *
835  * @param string $ip ip address of calling client
836  * @param string $range cidr address from allowed list
837  * @return integer 1 if ip addr falls in given range, else 0
838  */
839 function cidr_match($ip, $range) {
840  list ($subnet, $bits) = explode('/', $range);
841  $ip = ip2long($ip);
842  $subnet = ip2long($subnet);
843  $mask = -1 << (32 - $bits);
844  $subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
845  $lownetwork = $subnet & $mask;
846  $highbroadcast = $subnet | (~$mask & 0xFFFFFFFF);
847 
848  $pass = ($ip > $lownetwork && $ip < $highbroadcast ? 1 : 0);
849  #return ($ip & $mask) == $subnet;
850 
851  return $pass;
852 }
853 
854 /**
855  *
856  * @param string $data XML string to be formatted
857  * @return string XML 'pretty' with indents
858  */
859 function Format_AppFeed($data) {
860 
861  $dom = new DOMDocument();
862 
863  $dom->preserveWhiteSpace = false;
864  $dom->formatOutput = true;
865 
866  $dom->loadXML($data);
867  $out = $dom->saveXML();
868 
869  $out = str_replace('<?xml version="1.0"?>', '', $out);
870 
871  return ($out);
872 }
873 
874 /**
875  *
876  * @param type $dbh
877  * @param type $HB_ENV
878  * @param type $start_date
879  * @param type $end_date
880  * @param type $KEYACCTID
881  * @return array
882  * @throws Exception
883  */
884 function Mx5_accts($dbh, $HB_ENV) {
885 
886  try {
887  $balances = Get_Balances($dbh, $HB_ENV);
888 
889  $reply_arr = array();
890  $reply_idx = 0;
891 
892  if (HCU_array_key_exists('dp', $balances) && count($balances['dp'])) {
893 
894 # for each $balances['dp'], print list
895  foreach ($balances['dp'] as $balkey => $balinfo) {
896  if (HCU_array_key_value('trust', $balinfo) == 'transfer') {
897  # cross-account transfer only, don't send to Mx
898  continue;
899  }
900  $desc = htmlspecialchars($balinfo['description'], ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
901  $displaydesc = htmlspecialchars($balinfo['displaydesc'], ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
902  $atype = ($balinfo['certnumber'] == "0" || $HB_ENV['HideCert'] ? $balinfo['accounttype'] : "{$balinfo['accounttype']}_{$balinfo['certnumber']}");
903  $atype = ($balinfo['accountnumber'] . '-' . $atype);
904  $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
905 
906  switch ($balinfo['deposittype']) {
907  case "Y":
908  $acttype = "CHECKING";
909  break;
910  case "N":
911  case "S":
912  default:
913  $acttype = "SAVINGS";
914  break;
915  }
916  $hash = sha1("{$HB_ENV['Cu']}{$balkey}");
917 
918  $itm_arr = array(
919  #'bid' => $balkey,
920  'id' => $hash,
921  'type' => $acttype,
922  'name' => $balinfo['displaydesc'],
923  'balance' => $balinfo['currentbal'],
924  'account_number' => $atype,
925  'currency_code' => 'USD');
926  if (HCU_array_key_value('Fset', $HB_ENV) & GetFlagsetValue('CU_SHOWAVAILABLE')) {
927  $itm_arr['available_balance'] = $balinfo['availablebal'];
928  } else {
929  # available_balance is required @ Mx. Populate w/currentbal if we aren't showing it
930  $itm_arr['available_balance'] = $balinfo['currentbal'];
931  }
932 //<mdx version="5.0">
933 // <accounts>
934 // <account>
935 // <id>84943189431</id>
936 // <type>CHECKING</type>
937 // <name><![CDATA[Premium Checking]]></name>
938 // <balance>1972.38</balance>
939 // <available_balance>1972.38</available_balance>
940 // <account_number>12345-678<account_number>
941 // <currency_code>USD</currency_code>
942 // <!-- additional MDX account fields as desired-->
943 // </account>
944 // <!-- additional <account> elements as needed -->
945 // </accounts>
946 //</mdx>
947  $reply_arr['accounts'][]['account'] = $itm_arr;
948  $reply_arr['hashlist'][$hash] = array('acct' => $balkey, 'pos' => $reply_idx);
949  $reply_idx++;
950  }
951  }
952 
953 
954 #
955 # If CU2_SPEC18, try to get credit card loans
956  if (HCU_array_key_value('Fset2', $HB_ENV) & GetFlagsetValue('CU2_SPEC18')) {
957 
958  if (HCU_array_key_exists('cc', $balances) && count($balances['cc'])) {
959 
960  foreach ($balances['cc'] as $balkey => $balinfo) {
961  if (HCU_array_key_value('trust', $balinfo) == 'transfer') {
962  # cross-account transfer only, don't send to Mx
963  continue;
964  }
965 
966  $nextduedate = $balinfo['nextduedate'];
967  $creditlimit = $balinfo['creditlimit'];
968 
969  $desc = $balinfo['description'];
970  $desc = htmlspecialchars("$desc", ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
971  $displaydesc = $balinfo['displaydesc'];
972  $displaydesc = htmlspecialchars("$displaydesc", ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
973  $loan = $balinfo['loan'];
974  $atype = ($balinfo['accountnumber'] . '-' . $loan);
975  $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
976  $balance = $balinfo['currentbal'];
977  $creditlimit = $balinfo['creditlimit'];
978  $available = $creditlimit - $balance;
979  $balance = sprintf("%.2f", $balance * -1);
980  $available = ($available < 0 ? "" : $available);
981  $cur_avail = (HCU_array_key_value('Fset2', $HB_ENV) & GetFlagsetValue('CU2_CALL_CCAVAIL') ? "Call" : $available);
982 
983  $hash = sha1("{$HB_ENV['Cu']}{$balkey}");
984 
985  $itm_arr = array(
986  #'id' => $balkey,
987  'id' => $hash,
988  'type' => 'CREDIT_CARD',
989  'name' => $balinfo['displaydesc'],
990  'balance' => $balinfo['currentbal'],
991  'account_number' => $atype,
992  'currency_code' => 'USD');
993 
994  if ($cur_avail > 0) {
995  $itm_arr['available_balance'] = $cur_avail;
996  }
997  $reply_arr['accounts'][]['account'] = $itm_arr;
998  $reply_arr['hashlist'][$hash] = array('acct' => $balkey, 'pos' => $reply_idx);
999  $reply_idx++;
1000  }
1001  }
1002  }
1003 
1004 # try to get loans
1005  if (count($balances['ln'])) {
1006 
1007  foreach ($balances['ln'] as $balkey => $balinfo) {
1008  if (HCU_array_key_value('trust', $balinfo) == 'transfer') {
1009  # cross-account transfer only, don't send to Mx
1010  continue;
1011  }
1012 // $HB_ENV["SYSENV"]["logger"]->info(__LINE__ . " From Get_Balances: " . json_encode(array('desc' => $balinfo['description'],'displaydesc' => $balinfo['displaydesc'])));
1013 
1014  $balance = $balinfo['currentbal'];
1015  $loan = $balinfo['loan'];
1016  $atype = ($balinfo['accountnumber'] . '-' . $loan);
1017  $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1018  $desc = htmlspecialchars($balinfo['description'], ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1019  $displaydesc = htmlspecialchars($balinfo['displaydesc'], ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1020  $payoff = HCU_array_key_value('payoff', $balinfo);
1021  $nextduedate = $balinfo['nextduedate'];
1022  $creditlimit = $balinfo['creditlimit'];
1023 
1024  $hash = sha1("{$HB_ENV['Cu']}{$balkey}");
1025 
1026  $itm_arr = array(
1027  #'id' => $balkey,
1028  'id' => $hash,
1029  'type' => 'LOAN',
1030  'name' => $balinfo['displaydesc'],
1031  'balance' => $balinfo['currentbal'],
1032  'account_number' => $atype,
1033  'currency_code' => 'USD');
1034 
1035  $reply_arr['accounts'][]['account'] = $itm_arr;
1036  $reply_arr['hashlist'][$hash] = array('acct' => $balkey, 'pos' => $reply_idx);
1037  $reply_idx++;
1038  }
1039  }
1040  # returning hashlist under data returned it to caller script as data - ick!
1041  # also caused the Format_AppFeed function to throw an error due to invalid XML content
1042  # so bump it up as a peer to data - can still use to validate w/o exposing list
1043  $result = array('Status' => 'Success', 'data' => $reply_arr['accounts'], 'hashlist' => $reply_arr['hashlist']);
1044  } catch (Exception $e) {
1045  $result = array('Status' => 'Failed ' . htmlspecialchars($e->getMessage(), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE), 'data' => $reply_arr);
1046  }
1047 
1048  return $result;
1049 }
1050 
1051 // end Mx5_accts
1052 function Mx5_dpTrans($dbh, $HB_ENV, $balkey, $balinfo, $sqlstart, $sqlend) {
1053  try {
1054 
1055  $reply_arr = array('account' => array(
1056  'id' => $balinfo['id'],
1057  'transactions' => array('@attributes' => array('start_date' => $sqlstart, '3' => 'three', 'pages' => 1, 'page' => 1))));
1058 
1059  $history = Get_History($dbh, $HB_ENV, $balkey, $sqlstart, $sqlend);
1060 
1061  if (HCU_array_key_exists($balkey, $history) && is_array($history[$balkey])) {
1062 
1063  $atype = $balinfo['account_number'];
1064 // $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES);
1065  foreach ($history[$balkey] as $tnum => $detl) {
1066 # process the list
1067  $itm_arr = array();
1068 
1069  $tranamount = $detl['amount'];
1070  $tranamount = str_replace(",", "", str_replace("$", "", $tranamount));
1071  $tranamount = sprintf("%.2f", $tranamount);
1072  $check = (HCU_array_key_exists('checkno', $detl) ? $detl['checkno'] : 0);
1073  $trandesc = $detl['description'];
1074  if ($trandesc < " " && $check != 0) {
1075  $trandesc = "CHK";
1076  }
1077  $trandesc = (preg_replace("/<BR>/", " ", $trandesc));
1078  $trandesc = (preg_replace("/&nbsp;/", " ", $trandesc));
1079 // $trandesc = htmlspecialchars($trandesc, ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1080  $trandesc = (trim($trandesc) == '' ? '.' : $trandesc);
1081 
1082  if ($tranamount < 0) {
1083  $trntype = 'DEBIT';
1084  } else {
1085  $trntype = 'CREDIT';
1086  }
1087  $itm_arr['id'] = "{$atype}-{$detl['traceno']}";
1088  $itm_arr['type'] = $trntype;
1089  $itm_arr['amount'] = $tranamount;
1090  $itm_arr['description'] = array("@cdata" => $trandesc);
1091  $itm_arr['status'] = 'POSTED';
1092  $itm_arr['transacted_on'] = date('Y-m-d', strtotime($detl['date']));
1093  $itm_arr['posted_on'] = date('Y-m-d', strtotime($detl['date']));
1094  if ($balinfo['type'] == 'CHECKING' and $check != 0) {
1095  $itm_arr['check_number'] = $check;
1096  }
1097  $reply_arr['account']['transactions'][]['transaction'] = $itm_arr;
1098  }
1099  }
1100 
1101  $result = array('Status' => 'Success', 'data' => $reply_arr);
1102  } catch (Exception $e) {
1103  $result = array('Status' => 'Failed ' . htmlspecialchars($e->getMessage(), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE), 'data' => $reply_arr);
1104  }
1105  return $result;
1106 }
1107 
1108 function Mx5_ccTrans($dbh, $HB_ENV, $balkey, $balinfo, $sqlstart, $sqlend) {
1109  try {
1110 
1111  $hisinfo = HCU_array_key_value('hisinfo', $balinfo);
1112  $incchist = (empty($hisinfo) || trim(strtoupper($hisinfo)) == 'HOMECU' ? 1 : 0);
1113 
1114  $reply_arr = array('account' => array(
1115  'id' => $balinfo['id'],
1116  #<transactions start_date="2013-06-07" pages="5" page="1">
1117  #'transactions' => 'start_date="' . $sqlstart . ' pages="1" page="1"'));
1118  'transactions' => array('@attributes' => array('start_date' => $sqlstart, 'pages' => 1, 'page' => 1))));
1119 
1120  if ($incchist) {
1121  $history = Get_History($dbh, $HB_ENV, $balkey, $sqlstart, $sqlend);
1122  if (HCU_array_key_exists($balkey, $history) && is_array($history[$balkey])) {
1123 
1124  $atype = $balinfo['account_number'];
1125 // $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES);
1126  foreach ($history[$balkey] as $tnum => $detl) {
1127 
1128  $itm_arr = array();
1129 
1130  $principle = (HCU_array_key_exists('principal', $detl) ? $detl['principal'] : 0);
1131  /*
1132  * As clarified by Jeff Heuer of MoneyDesktop 7/9/2018
1133  * A debit should always have a negative amount and a credit should always have a positive amount
1134  * in OFX data feeds. If you send it with TRNTYPE of DEBIT then the amount should be negative. If
1135  * you send it with TRNTYPE of CREDIT then the amount should be positive. That is true across all
1136  * account types (checking, savings, credit cards, loans, etc.). A DEBIT always means money out of
1137  * the user's pocket (something that subtracts from their net worth). A CREDIT adds to their net worth.
1138  *
1139  * But the data coming from CUSA core for CCU has the sign opposite of that, which Gary confirmed is
1140  * correct from the CU perspective. So I am flipping the sign to satisfy MoneyDesktop / CCU. At this
1141  * time it appears CCU is our only client who is both 1) using MoneyDesktop and 2) showing credit card
1142  * transaction history on the books of the cu and included in the data feed from the core (CUSA) to HomeCU.
1143  *
1144  * Note: checked existing clients with cc history on their books - seems consistent that they get to us
1145  * with the signs opposite what MoneyDesktop expects - so maybe not as big a deal as I feared
1146  * So toggle the sign before deciding DEBIT / CREDIT
1147  */
1148  $principle = $principle * -1;
1149  if ($principle < 0) {
1150  $trntype = "DEBIT";
1151  } else {
1152  $trntype = "CREDIT";
1153  }
1154 
1155  $totalpay = $detl['totalpay'];
1156  $trdesc = $detl['description'];
1157 // $trdesc = htmlentities($trdesc, ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1158  $trdesc = (trim($trdesc) == '' ? '.' : $trdesc);
1159 
1160  $itm_arr['id'] = "{$atype}-{$detl['traceno']}";
1161  $itm_arr['type'] = $trntype;
1162  $itm_arr['amount'] = $totalpay;
1163  $itm_arr['description'] = array("@cdata" => $trdesc);
1164  $itm_arr['status'] = 'POSTED';
1165  $itm_arr['transacted_on'] = date('Y-m-d', strtotime($detl['date']));
1166  $itm_arr['posted_on'] = date('Y-m-d', strtotime($detl['date']));
1167 
1168  $reply_arr['account']['transactions'][]['transaction'] = $itm_arr;
1169  }
1170  }
1171  }
1172  $result = array('Status' => 'Success', 'data' => $reply_arr);
1173  } catch (Exception $e) {
1174  $result = array('Status' => 'Failed ' . htmlspecialchars($e->getMessage(), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE), 'data' => $reply_arr);
1175  }
1176  return $result;
1177 }
1178 
1179 function Mx5_lnTrans($dbh, $HB_ENV, $balkey, $balinfo, $sqlstart, $sqlend) {
1180  try {
1181  $reply_arr = array('account' => array(
1182  'id' => $balinfo['id'],
1183  #<transactions start_date="2013-06-07" pages="5" page="1">
1184  #'transactions' => 'start_date="' . $sqlstart . ' pages="1" page="1"'));
1185  'transactions' => array('@attributes' => array('start_date' => $sqlstart, 'pages' => 1, 'page' => 1))));
1186 
1187  $hisinfo = HCU_array_key_value('hisinfo', $balinfo);
1188  $incchist = (empty($hisinfo) || trim(strtoupper($hisinfo)) == 'HOMECU' ? 1 : 0);
1189 
1190  if ($incchist) {
1191  $history = Get_History($dbh, $HB_ENV, $balkey, $sqlstart, $sqlend);
1192  if (HCU_array_key_exists($balkey, $history) && is_array($history[$balkey])) {
1193 
1194  $atype = $balinfo['account_number'];
1195 // $atype = htmlspecialchars(trim($atype), ENT_NOQUOTES);
1196  foreach ($history[$balkey] as $tnum => $detl) {
1197  $itm_arr = array();
1198 
1199  $principle = (HCU_array_key_exists('principal', $detl) ? $detl['principal'] : 0);
1200  $interest = (HCU_array_key_exists('interest', $detl) ? $detl['interest'] : 0);
1201  $totalpay = (HCU_array_key_exists('totalpay', $detl) ? $detl['totalpay'] : 0);
1202  $traceno = (HCU_array_key_exists('traceno', $detl) ? $detl['traceno'] : 0);
1203  $trdesc = (HCU_array_key_exists('description', $detl) ? $detl['description'] : '');
1204  $trdesc = (trim($trdesc) == '' ? '.' : $trdesc);
1205 
1206  # OFX set them this way -
1207  if ($principle < 0) {
1208  $trntype = "PAYMENT";
1209  } else {
1210  $trntype = "ADVANCE";
1211  }
1212  # for MDX - CREDIT increases member net worth
1213  if ($principle < 0) {
1214  $trntype = "CREDIT";
1215  } else {
1216  $trntype = "DEBIT";
1217  }
1218 
1219 // $trdesc = htmlentities($trdesc, ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE);
1220 # transaction data row
1221 // if (($HB_ENV['Fset'] & GetFlagsetValue('CU_SHOWLNTXNSPLIT')) == GetFlagsetValue('CU_SHOWLNTXNSPLIT')) {
1222 // $itm_arr['TRNAMT'] = $totalpay;
1223 // $itm_arr['LOANTRNAMT'] = array('PRINAMT' => $principle, 'INTAMT' => $interest);
1224 // } else {
1225 // $itm_arr['TRNAMT'] = $principle;
1226 // }
1227 
1228  $itm_arr['id'] = "{$atype}-{$detl['traceno']}";
1229  $itm_arr['type'] = $trntype;
1230  $itm_arr['amount'] = $totalpay;
1231  $itm_arr['description'] = array("@cdata" => $trdesc);
1232  $itm_arr['status'] = 'POSTED';
1233  $itm_arr['transacted_on'] = date('Y-m-d', strtotime($detl['date']));
1234  $itm_arr['posted_on'] = date('Y-m-d', strtotime($detl['date']));
1235 
1236  $reply_arr['account']['transactions'][]['transaction'] = $itm_arr;
1237  }
1238  }
1239  }
1240  $result = array('Status' => 'Success', 'data' => $reply_arr);
1241  } catch (Exception $e) {
1242  $result = array('Status' => 'Failed ' . htmlspecialchars($e->getMessage(), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE), 'data' => $reply_arr);
1243  }
1244  return $result;
1245 }
1246 
1247 function Mx5_challenge($dbh, $HB_ENV, $MC) {
1248  /*
1249  * <mdx version='5.0'>
1250  <session>
1251  <key>UNIQUE_KEY_FOR_THIS_SESSION</key> # key will be added after return
1252  <challenges>
1253  <challenge>
1254  <id>UNIQUE_IDENTIFIER_FOR_THIS_CHALLENGE</id>
1255  <question><![CDATA[What is your mother's maiden name?]]></question>
1256  </challenge>
1257  <challenge>
1258  <id>UNIQUE_IDENTIFIER_FOR_THIS_CHALLENGE</id>
1259  <question><![CDATA[What city were you born in?]]></question>
1260  </challenge>
1261  <!-- additional challenge questions -->
1262  </challenges>
1263  </session>
1264  </mdx>
1265 
1266  */
1267  try {
1268  # for backward compatibility at Mx
1269  # this MFA_send_chall sends MFA_E and all MFA_Q settings
1270  # regardless of '1 random' settings
1271  /*
1272  * Turn off the '1 random' setting - Mx doesn't support it
1273  */
1274 
1275  $HB_ENV['flagset2'] = $HB_ENV['flagset2'] & (~ GetFlagsetValue('CU2_RANDOM_CHAL')); # turn off '1 random'
1276 
1277  $MemberChallengeQuestions_ary = GetChallengeQuestions("CHALLENGE", $dbh, $HB_ENV, $MC, $HB_ENV['Cn']);
1278 
1279  $reply_arr = array('session' => array('challenges' => array()));
1280 
1281 # force 'What email' as first challenge question
1282  $itm_arr = array('challenge' => array(
1283  'id' => 'MFA_E',
1284  'question' => array("@cdata" => 'What email address is saved with this account?')));
1285 
1286  $reply_arr['session']['challenges'][] = $itm_arr;
1287 
1288 # and now add mfa questions, if any were found
1289  if (count($MemberChallengeQuestions_ary)) {
1290  foreach ((array) $MemberChallengeQuestions_ary as $chakey => $mfaitem) {
1291  $itm_arr = array('challenge' => array(
1292  'id' => "MFA_{$mfaitem['cqid']}",
1293  'question' => array("@cdata" => "{$mfaitem['display']}")));
1294 
1295  $reply_arr['session']['challenges'][] = $itm_arr;
1296  }
1297  }
1298 
1299  $result = array('Status' => 'Success', 'data' => $reply_arr);
1300  } catch (Exception $e) {
1301  $result = array('Status' => 'Failed ' . htmlspecialchars($e->getMessage(), ENT_NOQUOTES | ENT_XML1, 'UTF-8', FALSE), 'data' => $reply_arr);
1302  }
1303 
1304  return $result;
1305 }