#!/usr/bin/php addArguments(['-headless']); // WARNING: Only one dash! $capabilities = DesiredCapabilities::firefox(); $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); $driver = null; $log = new Log(DONATIONS_LOG_FILE_PATH); $faUsername = get_cfg_var('se.secrets.fractured_atlas.username'); $faPassword = get_cfg_var('se.secrets.fractured_atlas.password'); $lastSeenTransactionId = null; $firstTransactionId = null; $transactionFilePath = '/tmp/last-fa-donation'; $transactionIds = []; $today = NOW->format('n/j/Y'); $faItemsPerPage = 20; // How many items are on a full page of FA results? // General plan: Read /tmp/last-fa-donation to see what the last transaction ID was that we processed. // If /tmp/last-fa-donation doesn't exist, get all transactions from today and create the file. function InsertTransaction(string $transactionId): bool{ $exists = Db::QueryBool('SELECT exists( select * from ( select 1 from Payments where TransactionId = ? union select 1 from PendingPayments where TransactionId = ? ) x )', [$transactionId, $transactionId]); if(!$exists){ Db::Query('INSERT into PendingPayments (Created, Processor, TransactionId) values (utc_timestamp(), ?, ?)', [Enums\PaymentProcessorType::FracturedAtlas, $transactionId]); return true; } return false; } try{ $log->Write('Ingesting FA donations...'); $driver = FirefoxDriver::start($capabilities); if(file_exists($transactionFilePath)){ $lastSeenTransactionId = trim(file_get_contents($transactionFilePath)); if($lastSeenTransactionId == ''){ $lastSeenTransactionId = null; } } if($lastSeenTransactionId === null){ $log->Write('No last transaction ID, checking everything from ' . NOW->format('Y-m-d')); } else{ $log->Write('Checking from last transaction ID ' . $lastSeenTransactionId); } $page = 1; $getMoreTransactions = true; while($getMoreTransactions){ if($page > 5){ // Safety valve for runaway logic. throw new Exception('Error: went past page 5 of Fractured Atlas results.'); } $log->Write('Getting page ' . $page . ' of transactions'); $driver->get('https://fundraising.fracturedatlas.org/admin/general_support/donations?page=' . $page); // Check if we need to log in to FA. // Wait until the element is visible, then check the current URL. $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('/html/body'))); if(stripos($driver->getCurrentURL(), 'auth0.com')){ $log->Write('Logging in to Fractured Atlas ...'); // We were redirected to the login page, so try to log in. /** @var WebDriverElement $emailField */ $emailField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="email"]'))); /** @var WebDriverElement $passwordField */ $passwordField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="password"]'))); /** @var WebDriverElement $submitButton */ $submitButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[@type="submit"]'))); // Fill out and submit the form. $emailField->sendKeys($faUsername); $passwordField->sendKeys($faPassword); $submitButton->click(); } // Wait until the page finishes loading. // We have to expand the row before we can select its contents, so click the 'expand' button once it's visible. try{ $toggleButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[contains(@class, "button-toggle")]'))); } catch(Exception){ $log->Write('Error: Couldn\'t load donation list.'); continue; } // If the last seen transaction ID is null, get everything from today. if($lastSeenTransactionId === null){ $elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]][parent::tr[preceding-sibling::tr[./td[normalize-space(.) = "' . $today . '"]]]]')); if(sizeof($elements) < $faItemsPerPage){ $getMoreTransactions = false; } for($i = 0; $i < sizeof($elements); $i++){ $td = $elements[$i]; /** @var string $transactionId */ $transactionId = $td->getDomProperty('textContent'); $transactionId = trim($transactionId); if($transactionId === ''){ continue; } if($i == 0 && $page == 1){ $firstTransactionId = $transactionId; } if(InsertTransaction($transactionId)){ $log->Write('Inserting transaction ' . $transactionId); } } } else{ // Last seen transaction ID is not null, get everything from that ID. // Get a list of transaction IDs on the page. $elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]]')); for($i = 0; $i < sizeof($elements); $i++){ $td = $elements[$i]; /** @var string $transactionId */ $transactionId = $td->getDomProperty('textContent'); $transactionId = trim($transactionId); if($transactionId === ''){ continue; } if($i == 0 && $page == 1){ $firstTransactionId = $transactionId; } if($transactionId == $lastSeenTransactionId){ $getMoreTransactions = false; break; } if(InsertTransaction($transactionId)){ $log->Write('Inserting transaction ' . $transactionId); } } } $page = $page + 1; } if($firstTransactionId !== null){ file_put_contents($transactionFilePath, $firstTransactionId); } $log->Write('Done.'); } catch(Exception $ex){ $exceptionString = vds($ex); $log->Write('Error: Uncaught exception: ' . $exceptionString); $em = new Email(true); $em->To = ADMIN_EMAIL_ADDRESS; $em->Subject = 'Ingesting FA donations failed'; $em->Body = Template::EmailDonationProcessingFailed(exception: preg_replace('/^/m', "\t", $exceptionString)); $em->TextBody = Template::EmailDonationProcessingFailedText(exception: preg_replace('/^/m', "\t", $exceptionString)); $em->Send(); throw $ex; } finally{ // `$driver` may be unintialized if we ctrl + c during Selenium initialization. /** @phpstan-ignore nullsafe.neverNull */ $driver?->quit(); }