Edit via SFTP
  1. <?php namespace ferret\users;
  2. /*
  3.   PURPOSE: for managing web sessions
  4.   CONSTANTS NEEDED:
  5.   fcGlobals::GetText_SessionCookieAffix() - the name of the cookie where the session key will be stored
  6.   INTERNAL RULES:
  7.   * Get the session cookie. (If no cookie, we're not logged in.)
  8.   * Load the session record indicated by the cookie.
  9.   * Check the session record to make sure it matches the current client.
  10.   * If it does, the session's user ID is logged in; otherwise not.
  11.   * A session record is also created for anonymous users.
  12.   HISTORY:
  13.   2013-10-25 stripped Session classes out of VbzCart shop.php for use in ATC project
  14.   2013-11-09 backported improved Session classes back into user-session.php
  15.   2016-04-03 moved RandomString() to fcString::Random().
  16.   2020-12-12 some updates for v0.4
  17. */
  18. use ferret\data\cIOResult;
  19. use ferret\data\cTableResult;
  20. use ferret\data\cInsertResult;
  21. use ferret\data\cSelectResult;
  22. use ferret\data\cRowKit;
  23.  
  24. define('KS_EVENT_FERRETERIA_LOGIN_OKAY' ,'fe.login.ok');
  25. define('KS_EVENT_FERRETERIA_LOGIN_BAD' ,'fe.login.bad');
  26. define('KS_EVENT_FERRETERIA_LOGOUT' ,'fe.logout');
  27.  
  28. /*::::
  29.   PURPOSE: Handles the table of user sessions
  30. */
  31. class ctSessions extends cLogicTable implements \ferret\odata\ifLoggableTable {
  32. use \ferret\tLinkableTable; // needed for event logging
  33. use \ferret\odata\tLoggableTable; // also needed for event logging
  34.  
  35. // ++ CONFIG ++ //
  36.  
  37. protected function GetTableName() : string { return 'user_session'; } // CEMENT
  38. protected function SingleRowClass() : string { return KS_CLASS_USER_SESSION; } // CEMENT
  39. #protected function MultiRowClass() : string { return crsSessions::class; } // OVERRIDE
  40. // CEMENT
  41. public function GetActionKey() : string { return \ferret\globals\cAbstract::Me()->GetActionKey_UserSession(); }
  42. // -- collective
  43. #protected function FieldCollectiveClass() : string { return cSessionIOBank::class; } // CEMENT
  44. #protected function StorageCollectiveClass() : string { return \ferret\field\cStorageCollective::class; }
  45. #protected function DisplayCollectiveClass() : string { return \ferret\field\cDisplayCollectiveStub::class; } // CEMENT
  46.  
  47. // -- CONFIG -- //
  48. // ++ STATUS ++ //
  49.  
  50. /*----
  51.   MEANING: indicates whether a lack of session record is because the user was never logged in (FALSE)
  52.   or because they were logged in but something changed and now the client can't be trusted (TRUE).
  53.   */
  54. private $isMismatch;
  55. public function GetStatus_SessionMismatch() : bool { return $this->isMismatch; }
  56. protected function SetStatus_SessionMismatch(bool $b) { $this->isMismatch = $b; }
  57.  
  58. // -- STATUS -- //
  59. // ++ COOKIE ++ //
  60.  
  61. //++remote++//
  62.  
  63. /*----
  64.   ACTION: tosses the session cookie to the browser
  65.   RETURNS: TRUE iff successful
  66.   NOTES:
  67.   * HTTP only sets the cookie when the page is reloaded.
  68.   Because of this, and because $_COOKIE is read-only,
  69.   we have to set a local variable when we create a new
  70.   session so that subsequent requests during the same
  71.   page-load don't think it hasn't been created yet,
  72.   and end up creating multiple records for each new session.
  73.   (It was creating 3 new records and using the last one.)
  74.   HISTORY:
  75.   2018-04-24 Decided there's no point in having a cookie-domain option,
  76.   so removed commented-out code. Also, probably just moving cookie functionality
  77.   to the App class/object.
  78.   2018-04-28 Using fcGlobals for naming cookies now.
  79.   */
  80. protected function ThrowCookie(string $sSessKey) : bool {
  81. $oApp = \fcApp::Me();
  82. $ok = $oApp->SetCookieValue($oApp->Globals()->GetText_SessionCookieAffix(),$sSessKey);
  83. return $ok;
  84. }
  85.  
  86. //--remote--//
  87. //++local++//
  88.  
  89. /*----
  90.   RULES:
  91.   * If local value is set, return that.
  92.   * Otherwise, get actual cookie, set local value from it, and return that.
  93.   In other words: if local value is set, that bypasses checking for an actual cookie.
  94.   This assumes that the cookie will never get set later on during a page-load,
  95.   which seems like a reasonable assumption. (Note: the COOKIE array is effectively read-only.)
  96.   */
  97. protected function GetCookie() : \ferret\cValue {
  98. return \fcApp::Me()->GetCookie(
  99. \ferret\globals\cAbstract::Me()->GetText_SessionCookieAffix()
  100. );
  101. }
  102.  
  103. //--local--//
  104.  
  105. // -- COOKIE -- //
  106. // ++ CURRENT RECORD ++ //
  107.  
  108. private $osSess = NULL;
  109. protected function SessionStatus() : cRowKit {
  110. if (is_null($this->osSess)) {
  111. $os = new cRowKit;
  112. $this->osSess = $os;
  113.  
  114. #$os->DoPopulate();
  115. $os->Table()->SetIt($this);
  116. } else {
  117. $os = $this->osSess;
  118. }
  119. return $os;
  120. }
  121. public function LoggedInUser() : cLoginStatus {
  122. $osUser = new cLoginStatus();
  123. $osSess = $this->SessionStatus(); // cRowStatus
  124. if ($osSess->HasIt()) {
  125. $osUser->SetUserRecord(TRUE,$osSess->GetIt());
  126. } else {
  127. $osUser->GetAccountStatus()->ClearIt(); // was tried but the current user is not logged in
  128. }
  129. return $osUser;
  130. }
  131.  
  132. /*----
  133.   PURPOSE: caches requested records so we don't needlessly create multiple objects
  134.   ...because this was happening a lot, with the same Session being created 50+ times
  135.   HISTORY:
  136.   2020-01-11 written
  137.   */
  138. private $arRecs = array();
  139. public function GetRow_fromKey(int $id) : cSelectResult {
  140. if (array_key_exists($id,$this->arRecs)) {
  141. $osRec = $this->arRecs[$id];
  142. } else {
  143. $osRec = parent::GetRow_fromKey($id);
  144. $this->arRecs[$id] = $osRec;
  145. }
  146. return $osRec;
  147. }
  148.  
  149. /*----
  150.   ACTION: returns a Session object for the current connection, whether or not one already exists
  151.   * if session object has already been loaded, assume it has been validated and return it
  152.   * if not, gets session key and auth from cookie
  153.   ASSUMES: session recordset is either NULL or a valid single record (and will set it accordingly)
  154.   ...therefore if there is one loaded already, we can assume it has been validated against the current client.
  155.   HISTORY:
  156.   2012-10-13 Added caching of the Session object to avoid creating multiple copies.
  157.   2015-06-23 Fixed: Was throwing an error if there was no session key; it should just make a new session.
  158.   2016-11-14
  159.   * Moved cookie fetching/storage into GetCookieValue().
  160.   * Replacing screen output with public status methods.
  161.   * Rewriting to make more logical sense.
  162.   2019-12-10 no longer checks for multiple session-records because that is now handled by GetRow_fromKey().
  163.   2021-04-16 changed to return cInsertStatus instead of cRecordStatus (now cRecordResult)
  164.   The cInsertStatus probably needs to be refactored to be a cSpace.
  165.   */
  166. public function MakeActiveRecord() : cInsertResult {
  167. // check to see if we've already loaded a session record
  168. $osrcSess = $this->SessionStatus();
  169. if (!$osrcSess->Record()->HasIt()) {
  170. // no session record loaded yet, so let's get one
  171. $osInsSess = new cInsertResult;
  172.  
  173. $osSessKey = $this->GetCookie(); // get the session cookie
  174. if ($osSessKey->HasIt()) {
  175. $sSessKey = $osSessKey->GetIt();
  176. list($idRecd,$sToken) = explode('-',$sSessKey);
  177. if (!is_numeric($idRecd)) {
  178. throw new fcSilentException("Malformed session cookie received. Hacking attempt? Value: [$sSessKey]");
  179. }
  180.  
  181. // try to retrieve requested session record
  182. $osSelSess = $this->GetRow_fromKey($idRecd); // RowStatus
  183. $osrcSess = $osSelSess->Record();
  184. if ($osrcSess->HasIt()) {
  185. // session found
  186. $rcSess = $osrcSess->GetIt();
  187. // verify that it matches browser fingerprint
  188. $ok = $rcSess->IsValidNow($sToken); // do requested session's creds match browser's creds?
  189. $osInsSess->Record()->SetIt($rcSess);
  190. } else {
  191. // no session found with that ID; create a new one
  192. $ok = FALSE;
  193. }
  194. // if found session not valid (or not found), then there was a session mismatch:
  195. $this->SetStatus_SessionMismatch(!$ok);
  196. } else {
  197. // browser has no session
  198. $ok = FALSE;
  199. }
  200.  
  201. if ($ok) {
  202. // session accessed -- update the record
  203. // (should we also log an event? maybe only in high-debug mode)
  204. $rcSess->DoUpdate(
  205. array(
  206. 'WhenUsed' => 'NOW()'
  207. )
  208. );
  209. } else {
  210. // no session found, or session found is not valid: need a new one
  211. $rcSess = $this->SpawnRow();
  212. $rcSess->SetupAsNew();
  213. $osInsSess = $rcSess->InsertSelf();
  214. if (!$osInsSess->Action()->GetOkay()) {
  215. $sError = 'New Session record was not created successfully.';
  216. $e = new \ferret\except\cData($sError);
  217. #$e->AddDiagnostic('SESSION ID: ['.$rcSess->GetKeyValue().']');
  218. $e->AddDiagnostic('FIELD VALUES:'.$rcSess->DumpHTML());
  219. $e->AddDiagnostic($osInsSess->DumpHTML());
  220. throw $e;
  221. }
  222. #echo 'SESSION RECORD: '.$rcSess->Dump();
  223. $sSessKey = $rcSess->SessKey();
  224. $ok = $this->ThrowCookie($sSessKey);
  225. //$ok = $rcSess->CreateRecord_andThrowCookie();
  226. if (!$ok) {
  227. // If this happens, some output must have been sent before the HTTP header, preventing the cookie.
  228. $sError = "Cookie could not be sent for session key [$sSessKey].";
  229. #$e = new \ferret\except\cInternal($sError);
  230. #throw $e;
  231. echo "Internal error: $sError<br>";
  232. }
  233. $osInsSess->Record()->SetIt($rcSess);
  234. }
  235. }
  236. return $osInsSess;
  237. }
  238.  
  239. // -- CURRENT RECORD -- //
  240.  
  241. }
  242. /*::::
  243.   PURPOSE: Represents a single user session record
  244.   HISTORY:
  245.   2020-01-06 changed parent from cSingleRowSourced to cRecordDeluxe
  246. */
  247. class crcSession extends cLogicRecord {
  248. use \ferret\odata\tLoggableRecord;
  249.  
  250. // ++ SETUP ++ //
  251.  
  252. /*----
  253.   HISTORY:
  254.   2019-12-21 renaming from InitNew() to SetupAsNew() to clarify purpose
  255.   2020-03-10 $this->UnloadCurrentRow() (former first line of code) must be redundant
  256.   now that we're doing row data separately from rowsets. Removed.
  257.   Also updated function call to set the values (current first line).
  258.   */
  259. public function SetupAsNew() {
  260. $this->ClearCells();
  261. $this->SetCells(
  262. array(
  263. 'Token' => \fcString::Random(31),
  264. 'ID_Client' => NULL,
  265. 'ID_Acct' => NULL,
  266. 'WhenCreated' => NULL // hasn't been created until written to db
  267. )
  268. );
  269. $this->ClientRecord_needed(); // connect to client record (create if needed)
  270. }
  271.  
  272. // -- SETUP -- //
  273. // ++ CLASSES ++ //
  274.  
  275. protected function ClientsClass() : string { return ctClients::class; }
  276. protected function AccountsClass() : string { return ctAccts::class; }
  277.  
  278. // -- CLASSES -- //
  279. // ++ TABLES ++ //
  280.  
  281. protected function ClientTable() : cTableResult {
  282. return $this->Space()->Database()->GetIt()->MakeTable($this->ClientsClass());
  283. }
  284. protected function ClientRecord(int $id) : cRowKit {
  285. return $this->Space()->Database()->GetIt()->MakeRow($this->ClientsClass(),$id);
  286. }
  287. protected function AccountTable() : cTableResult {
  288. return $this->Space()->Database()->GetIt()->MakeTable($this->AccountsClass());
  289. }
  290. protected function AccountRecord(int $id) : cRowKit {
  291. return $this->Space()->Database()->GetIt()->MakeRow($this->AccountsClass(),$id);
  292. }
  293.  
  294. // -- TABLES -- //
  295. // ++ RECORDS ++ //
  296.  
  297. /*----
  298.   ACTION: Create a new session record from the current memory data.
  299.   HISTORY:
  300.   2016-11-14 Made PROTECTED, and renamed Create() -> CreateRecord().
  301.   2016-12-18 Needs to be PUBLIC so that the Table class can call it.
  302.   Also, no sanitization done here anymore. What isn't now handled elsewhere
  303.   is unnecessary.
  304.   2019-12-19 moved from fcrUserSession to fcrUserSessions
  305.   2019-12-21 moved it back to fcrUserSession because it's clearly a single-record operation
  306.   2020-03-10 replaced call to SetKeyValue() with call to (new) LoadSelf(); see the latter for question.
  307.   2021-07-31 renamed from CreateRecord() to InsertSelf(), for clarity
  308.   PUBLIC so Table class can call it.
  309.   */
  310. public function InsertSelf() : \ferret\data\cInsertResult {
  311. $tbl = $this->GetTable_Keyed();
  312. $osIns = $tbl->DoInsert(
  313. array(
  314. 'ID_Client' => $this->Make_ClientID(),
  315. 'ID_Acct' => $this->GetUserID_SQL(),
  316. 'Token' => $this->GetToken_SQL(),
  317. 'WhenCreated' => 'NOW()'
  318. )
  319. );
  320. if (!$osIns->Action()->GetOkay()) {
  321. echo '<b>SQL</b>: '.$osIns->GetSQL().'<br>';
  322. throw new exception('Could not create new Session record.');
  323. }
  324. #echo 'NEW ID: '.$osIns->ID()->GetIt().'<br>';
  325. $osKey = $this->KeyRef();
  326. $osKey->SetVal($osIns->ID()->GetIt());
  327. #echo 'FIELDS: '.$this->DumpHTML();
  328. $osClient = $this->ClientRecord_needed()->Record();
  329. $rcClient = $osClient->GetIt();
  330. if (!$rcClient->isNew()) {
  331. $rcClient->Stamp();
  332. }
  333. return $osIns;
  334. }
  335.  
  336. private $osClient=NULL;
  337. protected function GetClientStatus() : cRowKit {
  338. if (is_null($this->osClient)) {
  339. #$oTrack = new \ferret\cCheckPoint(__FILE__,__LINE__,$this);
  340. $this->osClient = new cRowKit; #($oTrack);
  341. }
  342. return $this->osClient;
  343. }
  344. protected function SetClientStatus(cRowKit $os) { $this->osClient = $os; }
  345. /*----
  346.   HISTORY:
  347.   2014-09-18 Creating multiple ClientRecord() methods for different circumstances:
  348.   ClientRecord_asSet() - the client record last used for the session
  349.   ClientRecord_current() - the current client record; NULL if it does not match browser fingerprint
  350.   ClientRecord_needed() - a client record that can be used; creates new one if current record
  351.   does not match browser fingerprint
  352.   2016-12-18 This now checks to make sure the session recordset actually has a row.
  353.   2019-12-10 Since this class is now a SingleRow type, checking for a current row no longer makes sense; removing that condition.
  354.   2019-12-20 Commented out because it needs refactoring.
  355.   2020-03-10 A usage-case has arisen, so uncommenting & fixing.
  356.   */
  357. protected function ClientRecord_asSet() : cRowKit {
  358. $osStuff = $this->GetClientStatus();
  359. $osRow = $osStuff->Record();
  360. if (!$osRow->HasIt()) {
  361. $osID = $this->ClientID();
  362. if ($osID->IsNonZeroInt()) {
  363. $idCli = $osID->GetValue();
  364. if (is_null($idCli)) {
  365. throw new \exception('Houston, we have a problem.');
  366. }
  367. // there's a client ID, so get the client record from that:
  368. $oStat = $this->ClientRecord($idCli);
  369. //$this->rcClient = $oStat->GetRow();
  370. $this->osClient = $oStat;
  371. }
  372. }
  373. return $this->osClient; // at this point, the status object has been updated as needed
  374. }
  375. /*----
  376.   ACTION: checks the currently-set Client record for validity
  377.   RETURNS: row status, with "found" set to FALSE if record is not valid
  378.   HISTORY:
  379.   2019-12-20 Commented out because it needs refactoring.
  380.   2020-03-10 A usage-case has arisen, so uncommenting & fixing.
  381.   Depends on ClientRecord_asSet() returning the proper type, so fixing that.
  382.   */
  383. protected function ClientRecord_current() : cRowKit {
  384. $osStuff = $this->ClientRecord_asSet();
  385. $osRow = $osStuff->Record();
  386. if ($osRow->HasIt()) {
  387. $rcCli = $osRow->GetIt();
  388. if (!$rcCli->IsValidNow()) {
  389. $osRow->ClearIt(); // doesn't match current client; need a new one
  390. }
  391. }
  392. return $osStuff;
  393. }
  394. /*----
  395.   ACTION: if the session's client record matches, then load the client record; otherwise create a new one.
  396.   HISTORY:
  397.   2019-12-20 Commented out because it needs refactoring.
  398.   2020-03-10 A usage-case has arisen, so uncommenting & fixing.
  399.   Depends on ClientRecord_current() returning the proper type, so fixing that.
  400.   */
  401. protected function ClientRecord_needed() : cRowKit {
  402. $osStuff = $this->ClientRecord_current();
  403. $osRow = $osStuff->Record();
  404. if (!$osRow->HasIt()) {
  405. // 2021-01-29 We *could* get the Table from $osStuff...
  406. $osRow = $this->ClientTable()->GetIt()->MakeRecord_forCRC();
  407. if ($osRow->HasIt()) {
  408. $rc = $osRow->GetIt();
  409. $osStuff->Record()->SetIt($rc);
  410. $osID = $rc->KeyStatus();
  411. $this->SetClientID($osID->GetIt());
  412. } else {
  413. $e = new \ferret\except\cInternal("Could not create Session record for CRC");
  414. throw $e;
  415. }
  416. }
  417. return $osStuff;
  418. }
  419.  
  420. // -- RECORDS -- //
  421. // ++ USER RECORD ++ //
  422.  
  423. private $rcUser = NULL; // logged-in user
  424. private $osrUser = NULL; // status of logged-in user
  425. /*----
  426.   PUBLIC for App object
  427.   ASSUMES: if user-state changes, $osrUser will be updated or NULLed.
  428.   */
  429. public function LoggedInUserStatus() : cAcctStatus {
  430. if (is_null($this->osrUser)) {
  431. $osrUser = new cAcctStatus();
  432. $sErrBase = 'Ferreteria internal error: trying to retrieve logged-in user record, but ';
  433. $idUser = $this->GetUserID();
  434. if (is_null($idUser)) {
  435. $osrUser->Action()->SetOkay(FALSE);
  436. $osrUser->SetMessage($sErrBase.'session has no user attached.');
  437. // Reminder: "logged in" means that the session has a user ID attached.
  438. $ok = FALSE;
  439. } else {
  440. $osdUser = $this->AccountRecord($idUser)->Record();
  441. $ok = $osdUser->HasIt();
  442. }
  443. if ($ok) {
  444. $rcUser = $osdUser->GetIt();
  445. $osrUser->Record()->SetIt($rcUser);
  446. } else {
  447. $osrUser->Record()->ClearIt();
  448. $osrUser->SetMessage($sErrBase."user record for ID=$idUser could not be loaded.");
  449. }
  450. $this->osrUser = $osrUser;
  451. }
  452. return $this->osrUser;
  453. }
  454.  
  455. // -- USER RECORD -- //
  456. // ++ FIELD VALUES ++ //
  457.  
  458. protected function GetClientID() : int { return $this->GetCell('ID_Client'); }
  459. protected function SetClientID(int $id) { $this->SetCell('ID_Client',$id); }
  460. protected function ClientID() : \ferret\cReference { return $this->CellReference('ID_Client',FALSE); }
  461.  
  462. #protected function SetClientID(int $id) { return $this->SetFieldValue('ID_Client',$id); }
  463. #protected function GetClientID() : ?int { return $this->GetFieldValue('ID_Client'); }
  464. #protected function StatusOfClientID() : \ferret\cElementResult { return $this->CellStatus('ID_Client'); }
  465. /*----
  466.   HISTORY:
  467.   2020-01-08 created as a more general way of checking the user ID (ID_Acct) status
  468.   2020-01-11 made PUBLIC for App object
  469.   */
  470. #public function UserID_status() : \ferret\cElementResult { return $this->CellStatus('ID_Acct'); }
  471. /*----
  472.   HISTORY:
  473.   2020-01-08 changing PUBLIC to PROTECTED until usage is documented
  474.   2020-01-11 there is usage
  475.   (App object calls it, apparently for the Cart object in VbzCart)
  476.   but it seems better to make UserID_status() public instead. Doing that.
  477.   */
  478. public function UserID() : \ferret\cValueReadOnly { return $this->CellStatus('ID_Acct'); }
  479. protected function GetUserID() : ?int { return $this->UserID()->GetIt(); }
  480. protected function SetUserID(int $id) { $this->SetCell('ID_Acct',$id); }
  481. protected function ClearUserID() { $this->ClearCell('ID_Acct'); }
  482.  
  483. protected function GetToken() : string { return $this->GetCell('Token'); }
  484.  
  485. //++stash++//
  486.  
  487. protected function FetchStash() : array {
  488. $oa = \fcApp::Me();
  489. $og = \fcApp::Globals();
  490. $osStash = $oa->GetCookie(
  491. $og->GetText_StashCookieAffix()
  492. );
  493.  
  494. if ($osStash->HasIt()) {
  495. $sStash = $osStash->GetIt();
  496. $arStash = unserialize($sStash);
  497. } else {
  498. $arStash = array();
  499. }
  500. return $arStash;
  501. }
  502. protected function StoreStash(array $ar) : void {
  503. $oa = \fcApp::Me();
  504. $og = \fcApp::Globals();
  505. $sStashKey = $og->GetText_StashCookieAffix();
  506. if (count($ar) > 0) {
  507. $sStashVal = serialize($ar);
  508. $oa->SetCookieValue($sStashKey,$sStashVal);
  509. } else {
  510. $oa->ClearCookie($sStashKey);
  511. }
  512. }
  513.  
  514. public function SetStashValue(string $sName,string $sValue) : void {
  515. $arStash = $this->FetchStash();
  516. $sApp = \fcApp::Me()->Globals()->GetAppKeyString();
  517. $arStash[$sApp][$sName] = $sValue;
  518. $this->StoreStash($arStash);
  519. }
  520. // NOTE 2020-11-26 NULL values are sometimes getting stashed. Is there a reason for this?
  521. public function GetStashValue(string $sName) : string {
  522. #$oa = \fcApp::Me();
  523. $og = \fcApp::Globals();
  524.  
  525. $arStash = $this->FetchStash();
  526. $sApp = $og->GetAppKeyString();
  527. $arAppStash = \ferret\csArray::Nz($arStash,$sApp,NULL);
  528. if (!is_array($arAppStash)) {
  529. if (is_null($arAppStash)) {
  530. // not sure why this happens, but treat it like there's no stash
  531. $sValue = NULL;
  532. } else {
  533. $sType = gettype($arAppStash);
  534. $s = "The app stash is supposed to be an array, but is a $sType instead.";
  535. $e = new \ferret\except\cInternal($s);
  536. $e->AddDiagnostic("app key is [$sApp]");
  537. if (is_scalar($arAppStash)) {
  538. $e->AddDiagnostic("app stash value is [$arAppStash]");
  539. }
  540. $e->AddDiagnostic('entire stash: '.\ferret\csArray::Render($arStash));
  541. throw $e;
  542. }
  543. }
  544. $sValue = \fcArray::Nz($arAppStash,$sName);
  545. return is_null($sValue)?'':$sValue;
  546. }
  547. // ACTION: retrieve the value from the stash and remove it
  548. public function PullStashValue(string $sName) : string {
  549. $sValue = $this->GetStashValue($sName);
  550. $this->ClearStashValue($sName);
  551. return $sValue;
  552. }
  553. // ACTION: delete the given value from the stash
  554. protected function ClearStashValue(string $sName) : void {
  555. #$oa = \fcApp::Me();
  556. $og = \fcApp::Globals();
  557.  
  558. $sApp = $og->GetAppKeyString();
  559. $arStash = $this->FetchStash();
  560. unset($arStash[$sApp][$sName]);
  561. $this->StoreStash($arStash);
  562. }
  563.  
  564. //--stash--//
  565.  
  566. // -- FIELD VALUES -- //
  567. // ++ FIELD CALCULATIONS ++ //
  568.  
  569. // TODO: rename to GetAcctID_SQL()
  570. protected function GetUserID_SQL() : string {
  571. if ($this->HasCell('ID_Acct')) {
  572. $idAcct = $this->CellStatus('ID_Acct')->GetIt();
  573. if (is_null($idAcct)) {
  574. return 'NULL';
  575. } else {
  576. return $idAcct;
  577. }
  578. } else {
  579. return 'NULL';
  580. }
  581. }
  582. /*----
  583.   NOTE: The token comes from the database and will never have punctuation in it,
  584.   so does not need to be sanitized before use in SQL (only needs quoting).
  585.   */
  586. protected function GetToken_SQL() : string { return '"'.$this->GetToken().'"'; }
  587. /*----
  588.   RETURNS: TRUE iff the user has been attached to the session
  589.   ...which requires that the login was authorized at some point,
  590.   possibly during an earlier execution run.
  591.   */
  592. public function UserIsLoggedIn() : bool { return !is_null($this->GetUserID()); }
  593. /*-----
  594.   INPUT: $sToken should be the token-string generated from the current browser specs
  595.   RETURNS: TRUE if the stored browser specs (GetToken()) match the given browser specs ($sToken)
  596.   Right now, this means everything has to match (cookie token, IP address, browser string)
  597.   but in the future we might allow users to reduce their individual security level
  598.   by turning off the IP address check and/or the browser check. (This may require
  599.   table modifications.)
  600.   PUBLIC so fctUserSessions can call it
  601.   HISTORY:
  602.   2015-04-26 This sometimes comes up with no record -- I'm guessing that happens when a matching
  603.   Session isn't found. (Not sure why this isn't detected elsewhere.)
  604.   2016-04-03 Removed commented-out section.
  605.   2019-12-10 The more-rigorous record-handling means that it doesn't really make sense
  606.   for a single record to not load without this failure being detected earlier. That is,
  607.   we wouldn't be trying to do stuff with the record's data if it hadn't been loaded.
  608.   Therefore: removing the IsNew() check.
  609.   2019-12-20 Reverse-engineering my own code -- why do we do anything after checking the token for a match?
  610.   Commenting that out until I understand what it's for.
  611.   */
  612. public function IsValidNow(string $sToken) : bool {
  613. $ok = ($this->GetToken() == $sToken);
  614. return $ok;
  615. }
  616. public function SessKey() : string {
  617. if ($this->IsNew()) {
  618. $sError = 'Trying to generate a session key when session record has no ID.';
  619. $e = new \ferret\except\cData($sError);
  620. $e->AddDiagnostic('FIELDS:'.$this->Dump());
  621. #$e->AddDiagnostic('HAS ROW: ['.$this->HasCurrentRow().']');
  622. throw $e;
  623. }
  624. return $this->GetKeySlug().'-'.$this->GetToken();
  625. }
  626. /*----
  627.   RETURNS: User's login name, or NULL if user not logged in
  628.   TODO: Rename this to GetLoginString() or GetAccountNameString()
  629.   */
  630. public function UserString() : ?string {
  631. $osrUser = $this->LoggedInUserStatus()->Record();
  632. if ($osrUser->HasIt()) {
  633. $rc = $osrUser->GetIt();
  634. return $rc->LoginName();
  635. } else {
  636. return NULL;
  637. }
  638. }
  639. /*----
  640.   RETURNS: User's email address, or NULL if user not logged in
  641.   */
  642. public function UserEmailAddress() : ?string {
  643. if ($this->UserIsLoggedIn()) {
  644. return $this->UserRecord()->EmailAddress();
  645. } else {
  646. return NULL;
  647. }
  648. }
  649.  
  650. // -- FIELD CALCULATIONS -- //
  651. // ++ ACTIONS ++ //
  652.  
  653. /*----
  654.   ACTION: Make sure the Client ID is set correctly for the current browser client
  655.   If not set or doesn't match, get a new one.
  656.   */
  657. protected function Make_ClientID() : int {
  658. $osCli = $this->ClientRecord_needed()->Record();
  659. $rcCli = $osCli->GetIt();
  660. $idCli = $rcCli->KeyStatus()->GetIt(); // 2021-10-13 Can we assume there will always be a Client ID?
  661. $this->SetClientID($idCli);
  662. return $idCli;
  663. }
  664. /*----
  665.   ACTION:
  666.   * use [accounts table]->AuthorizeLogin() to check credentials
  667.   * update the current Session record accordingly
  668.   * (LATER IF NEEDED: save the login status object locally)
  669.   RETURNS: login status object
  670.   TODO: Record event before starting login, then log an event_done after trying it.
  671.   DEBUGPOINT for login processes
  672.   */
  673. public function UserLogin(string $sUser,string $sPass) : \ferret\users\cLoginStatus {
  674. // prepare data for event log
  675. $arData = array(
  676. 'user' => $sUser
  677. );
  678.  
  679. // check login credentials
  680. $tUsers = $this->AccountTable()->GetIt(); // fctUserAccts
  681. $osLogin = $tUsers->AuthorizeLogin($sUser,$sPass); // fcLoginStatus
  682. #echo 'OSLOGIN: '.$osLogin->DumpHTML().'<br>';
  683. #$this->SetLoginStatus($osLogin);
  684.  
  685. $arLog = array( // $arEventExt
  686. 'user.login' => $sUser,
  687. 'action' => 'login',
  688. );
  689.  
  690. //$this->SetUserStatus($osUser);
  691. if ($osLogin->GetOkay()) {
  692. // SUCCESSFUL LOGIN
  693.  
  694. $osrcUser = $osLogin->Account()->Record();
  695.  
  696. // set user for this session
  697. $idUser = $osrcUser->GetIt()->GetAcctID();
  698. $this->SetUserID($idUser);
  699.  
  700. $arLog['success'] = 'Y';
  701. $this->CreateEvent(
  702. KS_EVENT_FERRETERIA_LOGIN_OKAY, // $sCode
  703. $arLog // $arEventExt
  704. );
  705.  
  706. $this->DoUpdate(
  707. array(
  708. 'ID_Acct' => $idUser,
  709. 'WhenUsed' => 'NOW()'
  710. )
  711. );
  712.  
  713. // TODO: check results from DoUpdate() and log results with above event
  714.  
  715. /* 2020-03-11 old event system
  716.   $tEvents->CreateBaseEvent(
  717.   KS_EVENT_FERRETERIA_LOGIN_OKAY, // $sCode
  718.   $sText, // $sText
  719.   $arData // array $arData = NULL
  720.   );
  721.   */
  722. } else {
  723. // LOGIN ATTEMPT FAILED
  724.  
  725. // clear any existing user (security precaution):
  726. $this->ClearUserID();
  727. $sErr = $osLogin->GetMessage();
  728. #$sText = $sUser.' not logged in: '.$sErr;
  729.  
  730. $arLog['success'] = 'n';
  731. $arLog['err.text'] = $sErr;
  732. $this->CreateEvent(
  733. KS_EVENT_FERRETERIA_LOGIN_BAD, // $sCode
  734. $arLog // $arEventExt
  735. );
  736.  
  737. /* 2020-03-11 old event system
  738.   $tEvents->CreateBaseEvent(
  739.   KS_EVENT_FERRETERIA_LOGIN_BAD,
  740.   $sText,
  741.   $arData
  742.   );
  743.   */
  744. }
  745. return $osLogin;
  746. }
  747. /*----
  748.   ACTION: Logs the current user out. (Clears ID_Acct in session record.)
  749.   HISTORY:
  750.   2020-12-12 Significant updates & changes.
  751.   */
  752. public function UserLogout() : void {
  753. $oApp = \fcApp::Me();
  754. $db = $this->Database()->GetIt();
  755. #$osUser = $this->GetUserStatus();
  756. #$osUser = $this->AcctData()->GetTable()->LoggedInUser();
  757. $osAcct = $this->LoggedInUserStatus();
  758. #echo 'OSACCT class: '.get_class($osAcct).'<br>';
  759. #echo $osAcct->DumpHTML();
  760. #$osAcct = $osLogin->GetLoggedInUser();
  761. $osrcAcct = $osAcct->Record();
  762. if ($osrcAcct->HasIt()) {
  763. $rcAcct = $osrcAcct->GetIt();
  764. #echo 'RCACCT class '.get_class($rcAcct).'<br>';
  765. $sLogin = $rcAcct->LoginName();
  766. $arData = array(
  767. 'user.login' => $sLogin,
  768. 'user.id' => $rcAcct->GetKeyValue(),
  769. 'what' => $sLogin.' logged out',
  770. );
  771. #$idEvent = $oApp->CreateEvent(KS_EVENT_FERRETERIA_LOGOUT,$sLogin.' logged out',$arData);
  772. $osEvent = \fcApp::Me()->LogData()->CreateEvent(KS_EVENT_FERRETERIA_LOGOUT,$arData);
  773. $isProblem = !$db->GetOkay();
  774.  
  775. #echo 'IDEVENT CLASS: '.get_class($idEvent).'<br>'; throw new exception('2020-01-07 Just checking...'); // this *should* be a status object
  776. if ($isProblem) {
  777. echo 'SQL: '.$db->sql;
  778. throw new exception('Ferreteria event-logging error recording user logout: "'.$db->ErrorString());
  779. }
  780. $this->SetCell('ID_Acct',NULL);
  781. $arUpd = array(
  782. 'ID_Acct'=>'NULL'
  783. );
  784. $this->DoUpdate($arUpd);
  785. $isProblem = !$db->GetOkay();
  786.  
  787. if ($isProblem) {
  788. // 2021-04-20 This could use some updating.
  789. echo 'SQL: '.$db->sql;
  790. throw new \exception(
  791. 'Ferreteria event-logging error recording completion of user logout: "'.
  792. $db->ErrorString()
  793. );
  794. }
  795. } else {
  796. #$oLog = $oApp->LogData(); // \ferret\odata\cLogData
  797. $osIns = $this->CreateEvent( // cInsertStatus
  798. KS_EVENT_FERRETERIA_LOGOUT,
  799. array(
  800. 'summary' => 'redundant logout' // 2020-12-12 dunno if this will work
  801. )
  802. );
  803. // WORKING HERE
  804. #$idEvent = $oApp->CreateEvent(KS_EVENT_FERRETERIA_LOGOUT,'redundant logout');
  805. }
  806. }
  807. /*----
  808.   TODO: convert this to use UpdateArray() and Save().
  809.   HISTORY:
  810.   2016-12-21 No longer needs to be public, so making it protected.
  811.   */
  812. protected function SaveUserID(int $idUser) : void {
  813. $ar = array('ID_Acct'=>$idUser);
  814. $this->DoUpdate($ar); // save account ID to database
  815. $this->SetFieldValue('ID_Acct',$idUser); // update it in RAM as well
  816. }
  817.  
  818. // -- ACTIONS -- //
  819. // ++ DEPRECATED ++ //
  820.  
  821. /*---
  822.   NOTE: As of 2016-11-03, this will return the same result as UserIsLoggedIn() because
  823.   we use UserID > 0 as a way of detecting whether the user is logged in -- but that
  824.   might change. This function will always return a boolean which answers the question
  825.   "do we know the user's ID?". That might conceivably different if, say, we want to
  826.   access some non-sensitive information about the user such as layout preferences.
  827.   Some sites will recognize users in that sort of way even when they are logged out.
  828.   I'm not sure if this is good security practice, but it's a possibility which
  829.   should be allowed for in the API even if Ferreteria doesn't currently support it.
  830.   */
  831. public function UserIsKnown() : bool {
  832. throw new exception('2020-01-08 Who calls this, and why?');
  833. return $this->GetUserID() > 0; // for now, user ID is cleared from session when user is logged out
  834. }
  835. /*----
  836.   ACTION: retrieve this session's user record or throw an error
  837.   HISTORY:
  838.   2020-01-10 created
  839.   */
  840. protected function GetLoggedInUserRecord() : crcAcct {
  841. throw new exception('2020-01-10 Call LoggedInUserStatus()->GetHasRow() instead.');
  842. if (is_null($this->rcUser)) {
  843. $sErrBase = 'Ferreteria internal error: trying to retrieve logged-in user record, but ';
  844. $idUser = $this->GetUserID();
  845. if (is_null($idUser)) {
  846. throw new exception($sErrBase.'session has no user attached.');
  847. // Reminder: "logged in" means that the session has a user ID attached.
  848. }
  849. $osrUser = $this->AcctData($idUser);
  850. if ($osrUser->GetHasRow()) {
  851. $this->rcUser = $osrUser->GetRow();
  852. } else {
  853. throw new exception($sErrBase."user record for ID=$idUser could not be loaded.");
  854. }
  855. }
  856. return $this->rcUser;
  857. }
  858.  
  859. // -- DEPRECATED -- //
  860. }
  861. class cSessionFields extends \ferret\data\cSpaceIOBank {
  862. #use \ferret\tFMapRouterForFieldset;
  863.  
  864. protected function MakeFields() : void {
  865. $oField = new \ferret\field\cNative_Num($this,'ID_Client');
  866. #$oCtrl = new ferret\field\cDisplay_HTML_DropDown($oField);
  867. #$oCtrl->SetRecords($this->ClientTable()->GetRecords_forDropDown());
  868.  
  869. $oField = new \ferret\field\cNative_Num($this,'ID_Acct');
  870. $oCtrl = new \ferret\field\cDisplay_HTML_DropDown($oField);
  871. $oCtrl->Records()->SetIt($this->GetStorage()->GetTable()->AccountTable()->GetRecords_forDropDown());
  872.  
  873. $oField = new \ferret\field\cNative_Text($this,'Stash');
  874. #$oCtrl = new ferret\field\cDisplay_HTML($oField,array('size'=>60));
  875.  
  876. $oField = new \ferret\field\cNative_Time($this,'WhenCreated');
  877. $oField->GetDisplay()->SetEditable(FALSE);
  878.  
  879. $oField = new \ferret\field\cNative_Time($this,'WhenUsed');
  880. $oField->GetDisplay()->SetEditable(FALSE);
  881.  
  882. $oField = new \ferret\field\cNative_Time($this,'WhenExpires');
  883. }
  884. }
  885.