Add Db::QueryBool() and some code style updates

This commit is contained in:
Alex Cabal 2024-09-15 13:50:13 -05:00
parent 854ec6b9df
commit 44901cf3e2
9 changed files with 100 additions and 77 deletions

View file

@ -54,4 +54,22 @@ class Db{
return 0; return 0;
} }
/**
* Returns a single boolean value for the first column database query result.
*
* This is useful for queries that return a boolean as a result, like `select exists()`.
*
* @param string $query
* @param array<mixed> $args
*/
public static function QueryBool(string $query, array $args = []): bool{
$result = $GLOBALS['DbConnection']->Query($query, $args);
if(sizeof($result) > 0){
return (bool)current((array)$result[0]);
}
return false;
}
} }

View file

@ -0,0 +1,15 @@
<?
namespace Exceptions;
class MultiSelectMethodNotFoundException extends AppException{
public function __construct(string $class = ''){
if($class != ''){
$this->message = 'Multi table select attempted, but class ' . $class . ' doesn\'t have a FromMultiTableRow() method.';
}
else{
$this->message = 'Multi table select attempted, but the class doesn\'t have a FromMultiTableRow() method.';
}
parent::__construct();
}
}

View file

@ -2,11 +2,10 @@
<? <?
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
// Delete unconfirmed newsletter subscribers who are more than a week old // Delete unconfirmed newsletter subscribers who are more than a week old.
Db::Query(' Db::Query('
DELETE DELETE
from NewsletterSubscriptions from NewsletterSubscriptions
where IsConfirmed = false where IsConfirmed = false
and datediff(utc_timestamp(), Created) >= 7 and datediff(utc_timestamp(), Created) >= 7
'); ');
?>

View file

@ -41,7 +41,7 @@ function CreateOpdsCollectionFeed(string $name, string $url, string $description
$collator = collator_create('en_US'); // Used for sorting letters with diacritics like in author names $collator = collator_create('en_US'); // Used for sorting letters with diacritics like in author names
usort($collections, function($a, $b) use($collator){ return $collator->compare($a['sortedname'], $b['sortedname']); }); usort($collections, function($a, $b) use($collator){ return $collator->compare($a['sortedname'], $b['sortedname']); });
// Create the collections navigation document // Create the collections navigation document.
$collectionNavigationEntries = []; $collectionNavigationEntries = [];
foreach($collections as $collection){ foreach($collections as $collection){
$entry = new OpdsNavigationEntry($collection['name'], str_replace('%s', $collection['name'], $description), $url . '/' . $collection['id'], $now, 'subsection', 'navigation'); $entry = new OpdsNavigationEntry($collection['name'], str_replace('%s', $collection['name'], $description), $url . '/' . $collection['id'], $now, 'subsection', 'navigation');
@ -52,7 +52,7 @@ function CreateOpdsCollectionFeed(string $name, string $url, string $description
$collectionsFeed->Subtitle = 'Browse Standard Ebooks by ' . $name . '.'; $collectionsFeed->Subtitle = 'Browse Standard Ebooks by ' . $name . '.';
SaveFeed($collectionsFeed, $force, null, null, $now); SaveFeed($collectionsFeed, $force, null, null, $now);
// Now generate each individual collection feed // Now generate each individual collection feed.
foreach($collectionNavigationEntries as $collectionNavigationEntry){ foreach($collectionNavigationEntries as $collectionNavigationEntry){
$id = basename($collectionNavigationEntry->Id); $id = basename($collectionNavigationEntry->Id);
usort($ebooks[$id], 'SortByUpdatedDesc'); usort($ebooks[$id], 'SortByUpdatedDesc');
@ -88,7 +88,7 @@ foreach($dirs as $dir){
} }
} }
// Iterate over all ebooks to build the various feeds // Iterate over all ebooks to build the various feeds.
foreach(Library::GetEbooksFromFilesystem($webRoot) as $ebook){ foreach(Library::GetEbooksFromFilesystem($webRoot) as $ebook){
$allEbooks[] = $ebook; $allEbooks[] = $ebook;
$newestEbooks[] = $ebook; $newestEbooks[] = $ebook;
@ -116,7 +116,7 @@ $newestEbooks = array_slice($newestEbooks, 0, $ebooksPerNewestEbooksFeed);
$now = new DateTimeImmutable(); $now = new DateTimeImmutable();
// Create OPDS feeds // Create OPDS feeds.
$opdsRootEntries = [ $opdsRootEntries = [
new OpdsNavigationEntry( new OpdsNavigationEntry(
'Newest Standard Ebooks', 'Newest Standard Ebooks',
@ -159,44 +159,44 @@ $opdsRootEntries = [
$opdsRoot = new OpdsNavigationFeed('Standard Ebooks', 'The Standard Ebooks catalog.', '/feeds/opds', $webRoot . '/feeds/opds/index.xml', $opdsRootEntries, null); $opdsRoot = new OpdsNavigationFeed('Standard Ebooks', 'The Standard Ebooks catalog.', '/feeds/opds', $webRoot . '/feeds/opds/index.xml', $opdsRootEntries, null);
SaveFeed($opdsRoot, $force, null, null, $now); SaveFeed($opdsRoot, $force, null, null, $now);
// Create the Subjects feeds // Create the Subjects feeds.
CreateOpdsCollectionFeed('subject', '/feeds/opds/subjects', 'Standard Ebooks in the “%s” subject, most-recently-released first.', $subjects, $ebooksBySubject, $now, $webRoot, $opdsRoot, $force); CreateOpdsCollectionFeed('subject', '/feeds/opds/subjects', 'Standard Ebooks in the “%s” subject, most-recently-released first.', $subjects, $ebooksBySubject, $now, $webRoot, $opdsRoot, $force);
// Create the Collections feeds // Create the Collections feeds.
CreateOpdsCollectionFeed('collection', '/feeds/opds/collections', 'Standard Ebooks in the “%s” collection, most-recently-released first.', $collections, $ebooksByCollection, $now, $webRoot, $opdsRoot, $force); CreateOpdsCollectionFeed('collection', '/feeds/opds/collections', 'Standard Ebooks in the “%s” collection, most-recently-released first.', $collections, $ebooksByCollection, $now, $webRoot, $opdsRoot, $force);
// Create the Author feeds // Create the Author feeds.
CreateOpdsCollectionFeed('author', '/feeds/opds/authors', 'Standard Ebooks by %s, most-recently-released first.', $authors, $ebooksByAuthor, $now, $webRoot, $opdsRoot, $force); CreateOpdsCollectionFeed('author', '/feeds/opds/authors', 'Standard Ebooks by %s, most-recently-released first.', $authors, $ebooksByAuthor, $now, $webRoot, $opdsRoot, $force);
// Create the All feed // Create the All feed.
$allFeed = new OpdsAcquisitionFeed('All Standard Ebooks', 'All Standard Ebooks, most-recently-updated first. This is a Complete Acquisition Feed as defined in OPDS 1.2 §2.5.', '/feeds/opds/all', $webRoot . '/feeds/opds/all.xml', $allEbooks, $opdsRoot, true); $allFeed = new OpdsAcquisitionFeed('All Standard Ebooks', 'All Standard Ebooks, most-recently-updated first. This is a Complete Acquisition Feed as defined in OPDS 1.2 §2.5.', '/feeds/opds/all', $webRoot . '/feeds/opds/all.xml', $allEbooks, $opdsRoot, true);
SaveFeed($allFeed, $force, null, null, $now); SaveFeed($allFeed, $force, null, null, $now);
// Create the Newest feed // Create the Newest feed.
$newestFeed = new OpdsAcquisitionFeed('Newest Standard Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/opds/new-releases', $webRoot . '/feeds/opds/new-releases.xml', $newestEbooks, $opdsRoot); $newestFeed = new OpdsAcquisitionFeed('Newest Standard Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/opds/new-releases', $webRoot . '/feeds/opds/new-releases.xml', $newestEbooks, $opdsRoot);
SaveFeed($newestFeed, $force, null, null, $now); SaveFeed($newestFeed, $force, null, null, $now);
// Create RSS/Atom feeds // Create RSS/Atom feeds.
// Create the RSS All feed // Create the RSS All feed.
$allRssFeed = new RssFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/feeds/rss/all', $webRoot . '/feeds/rss/all.xml', $allEbooks); $allRssFeed = new RssFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/feeds/rss/all', $webRoot . '/feeds/rss/all.xml', $allEbooks);
SaveFeed($allRssFeed, $force, null, null); SaveFeed($allRssFeed, $force, null, null);
// Create the RSS Newest feed // Create the RSS Newest feed.
$newestRssFeed = new RssFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/rss/new-releases', $webRoot . '/feeds/rss/new-releases.xml', $newestEbooks); $newestRssFeed = new RssFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/rss/new-releases', $webRoot . '/feeds/rss/new-releases.xml', $newestEbooks);
SaveFeed($newestRssFeed, $force, null, null); SaveFeed($newestRssFeed, $force, null, null);
// Create the Atom All feed // Create the Atom All feed.
$allAtomFeed = new AtomFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/feeds/atom/all', $webRoot . '/feeds/atom/all.xml', $allEbooks); $allAtomFeed = new AtomFeed('Standard Ebooks - All Ebooks', 'All Standard Ebooks, most-recently-released first.', '/feeds/atom/all', $webRoot . '/feeds/atom/all.xml', $allEbooks);
SaveFeed($allAtomFeed, $force, null, null, $now); SaveFeed($allAtomFeed, $force, null, null, $now);
// Create the Atom Newest feed // Create the Atom Newest feed.
$newestAtomFeed = new AtomFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/atom/new-releases', $webRoot . '/feeds/atom/new-releases.xml', $newestEbooks); $newestAtomFeed = new AtomFeed('Standard Ebooks - Newest Ebooks', 'The ' . number_format($ebooksPerNewestEbooksFeed) . ' latest Standard Ebooks, most-recently-released first.', '/feeds/atom/new-releases', $webRoot . '/feeds/atom/new-releases.xml', $newestEbooks);
SaveFeed($newestAtomFeed, $force, null, null, $now); SaveFeed($newestAtomFeed, $force, null, null, $now);
// Generate each individual subject feed // Generate each individual subject feed.
foreach($ebooksBySubject as $subject => $ebooks){ foreach($ebooksBySubject as $subject => $ebooks){
usort($ebooks, 'SortByUpdatedDesc'); usort($ebooks, 'SortByUpdatedDesc');
@ -210,7 +210,7 @@ foreach($ebooksBySubject as $subject => $ebooks){
SaveFeed($subjectAtomFeed, $force, $subjects[$subject]['name'], $subjects[$subject]['sortedname'], $now); SaveFeed($subjectAtomFeed, $force, $subjects[$subject]['name'], $subjects[$subject]['sortedname'], $now);
} }
// Generate each individual collection feed // Generate each individual collection feed.
foreach($ebooksByCollection as $collection => $ebooks){ foreach($ebooksByCollection as $collection => $ebooks){
usort($ebooks, 'SortByUpdatedDesc'); usort($ebooks, 'SortByUpdatedDesc');
@ -226,7 +226,7 @@ foreach($ebooksByCollection as $collection => $ebooks){
SaveFeed($collectionAtomFeed, $force, $collections[$collection]['name'], $collections[$collection]['sortedname'], $now); SaveFeed($collectionAtomFeed, $force, $collections[$collection]['name'], $collections[$collection]['sortedname'], $now);
} }
// Generate each individual author feed // Generate each individual author feed.
foreach($ebooksByAuthor as $collection => $ebooks){ foreach($ebooksByAuthor as $collection => $ebooks){
usort($ebooks, 'SortByUpdatedDesc'); usort($ebooks, 'SortByUpdatedDesc');
@ -240,8 +240,8 @@ foreach($ebooksByAuthor as $collection => $ebooks){
SaveFeed($collectionAtomFeed, $force, $authors[$collection]['name'], $authors[$collection]['sortedname'], $now); SaveFeed($collectionAtomFeed, $force, $authors[$collection]['name'], $authors[$collection]['sortedname'], $now);
} }
// Set ownership and permissions // Set ownership and permissions.
// We don't use PHP's built in chown/chmod chmod can't accept strings // We don't use PHP's built in chown/chmod chmod can't accept strings.
// The `chmod +X` command, with a capital X, makes only matched directories executable. // The `chmod +X` command, with a capital X, makes only matched directories executable.
exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*.xml'); exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*.xml');
exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*/*.xml'); exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*/*.xml');

View file

@ -1,14 +1,15 @@
#!/usr/bin/php #!/usr/bin/php
<? <?
// Note: This script must be run as a user with a $HOME directory, // Note: This script must be run as a user with a $HOME directory, otherwise Firefox won't be able to start with a profile.
// otherwise Firefox won't be able to start with a profile.
// FA is unreliable in the email notifications it sends. They are often missing. /**
// This script gets a list of FA transactions directly from their website. * FA is unreliable in the email notifications it sends. They are often missing.
// It tracks the last transaction it saw in a temp file and won't go past that. * This script gets a list of FA transactions directly from their website.
// If there is no temp file, it gets all transactions from today, and writes the temp file with the last transaction it saw. * It tracks the last transaction it saw in a temp file and won't go past that.
// Any transactions that the script finds and which don't already exist, are added to the database as pending payments. * If there is no temp file, it gets all transactions from today, and writes the temp file with the last transaction it saw.
// After that, the /scripts/process-pending-payments script will pick them up and do accounting/patron logic. * Any transactions that the script finds and which don't already exist, are added to the database as pending payments.
* After that, the `/scripts/process-pending-payments` script will pick them up and do accounting/patron logic.
*/
use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverExpectedCondition;
@ -25,10 +26,10 @@ use function Safe\set_time_limit;
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
// Disable script timeout because Selenium is very slow // Disable script timeout because Selenium is very slow.
set_time_limit(0); set_time_limit(0);
// Initialize the Selenium driver // Initialize the Selenium driver.
putenv('WEBDRIVER_FIREFOX_DRIVER=' . SITE_ROOT . '/config/selenium/geckodriver-0.31.0'); putenv('WEBDRIVER_FIREFOX_DRIVER=' . SITE_ROOT . '/config/selenium/geckodriver-0.31.0');
$firefoxOptions = new FirefoxOptions(); $firefoxOptions = new FirefoxOptions();
@ -53,7 +54,7 @@ $faItemsPerPage = 20; // How many items are on a full page of FA results?
// If /tmp/last-fa-donation doesn't exist, get all transactions from today and create the file. // If /tmp/last-fa-donation doesn't exist, get all transactions from today and create the file.
function InsertTransaction(string $transactionId): bool{ function InsertTransaction(string $transactionId): bool{
$exists = Db::QueryInt('SELECT exists( $exists = Db::QueryBool('SELECT exists(
select * select *
from from
( select 1 ( select 1
@ -104,7 +105,7 @@ try{
while($getMoreTransactions){ while($getMoreTransactions){
if($page > 5){ if($page > 5){
// Safety valve for runaway logic // Safety valve for runaway logic.
throw new Exception('Error: went past page 5 of Fractured Atlas results.'); throw new Exception('Error: went past page 5 of Fractured Atlas results.');
} }
@ -113,24 +114,24 @@ try{
$driver->get('https://fundraising.fracturedatlas.org/admin/general_support/donations?page=' . $page); $driver->get('https://fundraising.fracturedatlas.org/admin/general_support/donations?page=' . $page);
// Check if we need to log in to FA. // Check if we need to log in to FA.
// Wait until the <body> element is visible, then check the current URL // Wait until the <body> element is visible, then check the current URL.
$driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('/html/body'))); $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('/html/body')));
if(stripos($driver->getCurrentUrl(), 'auth0.com')){ if(stripos($driver->getCurrentUrl(), 'auth0.com')){
$log->Write('Logging in to Fractured Atlas ...'); $log->Write('Logging in to Fractured Atlas ...');
// We were redirected to the login page, so try to log in // We were redirected to the login page, so try to log in.
$emailField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="email"]'))); $emailField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="email"]')));
$passwordField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="password"]'))); $passwordField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="password"]')));
$submitButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[@type="submit"]'))); $submitButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[@type="submit"]')));
// Fill out and submit the form // Fill out and submit the form.
$emailField->sendKeys($faUsername); $emailField->sendKeys($faUsername);
$passwordField->sendKeys($faPassword); $passwordField->sendKeys($faPassword);
$submitButton->click(); $submitButton->click();
} }
// Wait until the page finishes loading. // 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 // We have to expand the row before we can select its contents, so click the 'expand' button once it's visible.
try{ try{
$toggleButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[contains(@class, "button-toggle")]'))); $toggleButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[contains(@class, "button-toggle")]')));
} }
@ -139,7 +140,7 @@ try{
continue; continue;
} }
// If the last seen transaction ID is null, get everything from today // If the last seen transaction ID is null, get everything from today.
if($lastSeenTransactionId === null){ if($lastSeenTransactionId === null){
$elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]][parent::tr[preceding-sibling::tr[./td[normalize-space(.) = "' . $today . '"]]]]')); $elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]][parent::tr[preceding-sibling::tr[./td[normalize-space(.) = "' . $today . '"]]]]'));
@ -166,8 +167,8 @@ try{
} }
} }
else{ else{
// Last seen transaction ID is not null, get everything from that ID // Last seen transaction ID is not null, get everything from that ID.
// Get a list of transaction IDs on the page // Get a list of transaction IDs on the page.
$elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]]')); $elements = $driver->findElements(WebDriverBy::xpath('//td[preceding-sibling::th[normalize-space(.) = "ID"]]'));
for($i = 0; $i < sizeof($elements); $i++){ for($i = 0; $i < sizeof($elements); $i++){
$td = $elements[$i]; $td = $elements[$i];
@ -218,4 +219,3 @@ catch(Exception $ex){
finally{ finally{
$driver?->quit(); $driver?->quit();
} }
?>

View file

@ -18,5 +18,3 @@ if(sizeof($pendingPayments) > 0){
$em->TextBody = Template::EmailAdminUnprocessedDonationsText(); $em->TextBody = Template::EmailAdminUnprocessedDonationsText();
$em->Send(); $em->Send();
} }
?>

View file

@ -1,7 +1,6 @@
#!/usr/bin/php #!/usr/bin/php
<? <?
// Note: This script must be run as a user with a $HOME directory, // Note: This script must be run as a user with a $HOME directory, otherwise Firefox won't be able to start with a profile.
// otherwise Firefox won't be able to start with a profile.
use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverExpectedCondition;
@ -17,21 +16,21 @@ use function Safe\set_time_limit;
require_once('/standardebooks.org/web/lib/Core.php'); require_once('/standardebooks.org/web/lib/Core.php');
// Disable script timeout because Selenium is very slow // Disable script timeout because Selenium is very slow.
set_time_limit(0); set_time_limit(0);
// Initialize the Selenium driver // Initialize the Selenium driver
putenv('WEBDRIVER_FIREFOX_DRIVER=' . SITE_ROOT . '/config/selenium/geckodriver-0.31.0'); putenv('WEBDRIVER_FIREFOX_DRIVER=' . SITE_ROOT . '/config/selenium/geckodriver-0.31.0');
$firefoxOptions = new FirefoxOptions(); $firefoxOptions = new FirefoxOptions();
$firefoxOptions->addArguments(['-headless']); // WARNING: Only one dash! $firefoxOptions->addArguments(['-headless']); // **Warning**: Only one dash!
$capabilities = DesiredCapabilities::firefox(); $capabilities = DesiredCapabilities::firefox();
$capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions);
$driver = null; $driver = null;
$log = new Log(DONATIONS_LOG_FILE_PATH); $log = new Log(DONATIONS_LOG_FILE_PATH);
$lastMonth = (new DateTimeImmutable())->sub(new DateInterval('P45D')); // 45 days, a 15 day grace period before Patrons Circle members are dropped off $lastMonth = (new DateTimeImmutable())->sub(new DateInterval('P45D')); // 45 days, a 15 day grace period before Patrons Circle members are dropped off.
$lastYear = (new DateTimeImmutable())->sub(new DateInterval('P1Y')); $lastYear = (new DateTimeImmutable())->sub(new DateInterval('P1Y'));
$faUsername = get_cfg_var('se.secrets.fractured_atlas.username'); $faUsername = get_cfg_var('se.secrets.fractured_atlas.username');
$faPassword = get_cfg_var('se.secrets.fractured_atlas.password'); $faPassword = get_cfg_var('se.secrets.fractured_atlas.password');
@ -61,7 +60,7 @@ Db::Query('
Db::Query('commit'); Db::Query('commit');
if(sizeof($pendingPayments) == 0){ if(sizeof($pendingPayments) == 0){
// Don't start the very slow Selenium driver if we have nothing to process // Don't start the very slow Selenium driver if we have nothing to process.
exit(); exit();
} }
@ -73,13 +72,13 @@ try{
if($pendingPayment->Processor == PaymentProcessorType::FracturedAtlas){ if($pendingPayment->Processor == PaymentProcessorType::FracturedAtlas){
$log->Write('Processing donation ' . $pendingPayment->TransactionId . ' ...'); $log->Write('Processing donation ' . $pendingPayment->TransactionId . ' ...');
if(Db::QueryInt(' if(Db::QueryBool('
SELECT exists( SELECT exists(
select * select *
from Payments from Payments
where TransactionId = ? where TransactionId = ?
) )
', [$pendingPayment->TransactionId]) > 0){ ', [$pendingPayment->TransactionId])){
$log->Write('Donation already exists in database.'); $log->Write('Donation already exists in database.');
continue; continue;
} }
@ -87,24 +86,24 @@ try{
$driver->get('https://fundraising.fracturedatlas.org/admin/donations?query=' . $pendingPayment->TransactionId); $driver->get('https://fundraising.fracturedatlas.org/admin/donations?query=' . $pendingPayment->TransactionId);
// Check if we need to log in to FA. // Check if we need to log in to FA.
// Wait until the <body> element is visible, then check the current URL // Wait until the <body> element is visible, then check the current URL.
$driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('/html/body'))); $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('/html/body')));
if(stripos($driver->getCurrentUrl(), 'auth0.com')){ if(stripos($driver->getCurrentUrl(), 'auth0.com')){
$log->Write('Logging in to Fractured Atlas ...'); $log->Write('Logging in to Fractured Atlas ...');
// We were redirected to the login page, so try to log in // We were redirected to the login page, so try to log in.
$emailField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="email"]'))); $emailField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="email"]')));
$passwordField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="password"]'))); $passwordField = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//input[@type="password"]')));
$submitButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[@type="submit"]'))); $submitButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[@type="submit"]')));
// Fill out and submit the form // Fill out and submit the form.
$emailField->sendKeys($faUsername); $emailField->sendKeys($faUsername);
$passwordField->sendKeys($faPassword); $passwordField->sendKeys($faPassword);
$submitButton->click(); $submitButton->click();
} }
// Wait until the page finishes loading. // 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 // We have to expand the row before we can select its contents, so click the 'expand' button once it's visible.
try{ try{
$toggleButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[contains(@class, "button-toggle")]'))); $toggleButton = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//button[contains(@class, "button-toggle")]')));
} }
@ -116,8 +115,7 @@ try{
// Our target row is now visible, extract the data! // Our target row is now visible, extract the data!
// In the FA donations table, there is a header row, and an expandable details row. The header row tells us if the donation is recurring, // In the FA donations table, there is a header row, and an expandable details row. The header row tells us if the donation is recurring, and the details row has the rest of the information.
// and the details row has the rest of the information
$detailsRow = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//tr[starts-with(@id, "expanded") and contains(@id, "' . $pendingPayment->TransactionId . '")]'))); $detailsRow = $driver->wait(20, 250)->until(WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::xpath('//tr[starts-with(@id, "expanded") and contains(@id, "' . $pendingPayment->TransactionId . '")]')));
$headerRow = $driver->findElement(WebDriverBy::xpath('//tr[not(starts-with(@id, "expanded")) and contains(@id, "' . $pendingPayment->TransactionId . '")]')); $headerRow = $driver->findElement(WebDriverBy::xpath('//tr[not(starts-with(@id, "expanded")) and contains(@id, "' . $pendingPayment->TransactionId . '")]'));
@ -145,13 +143,13 @@ try{
$payment->IsMatchingDonation = true; $payment->IsMatchingDonation = true;
} }
// We can get here via an AOGF donation that is anonymous // We can get here via an AOGF donation that is anonymous.
if($payment->User->Email == 'Not provided' || $payment->User->Email == ''){ if($payment->User->Email == 'Not provided' || $payment->User->Email == ''){
$payment->User = null; $payment->User = null;
} }
} }
catch(Exception){ catch(Exception){
// Anonymous donations don't have these elements present and will throw an exception // Anonymous donations don't have these elements present and will throw an exception.
$payment->User = null; $payment->User = null;
} }
@ -171,12 +169,12 @@ try{
$payment->User = null; $payment->User = null;
} }
// All set - create the payment // All set - create the payment.
try{ try{
$payment->Create(); $payment->Create();
} }
catch(Exceptions\PaymentExistsException){ catch(Exceptions\PaymentExistsException){
// Payment already exists, just continue // Payment already exists, just continue.
$log->Write('Donation already in database.'); $log->Write('Donation already in database.');
continue; continue;
} }
@ -202,15 +200,15 @@ try{
// This payment is eligible for the Patrons Circle! // This payment is eligible for the Patrons Circle!
if($payment->User !== null){ if($payment->User !== null){
// Are we already a patron? // Are we already a patron?
if(Db::QueryInt(' if(!Db::QueryBool('
SELECT exists( SELECT exists(
select * select *
from Patrons from Patrons
where UserId = ? where UserId = ?
and Ended is null and Ended is null
) )
', [$payment->UserId]) == 0){ ', [$payment->UserId])){
// Not a patron yet, add them to the Patrons Circle // Not a patron yet, add them to the Patrons Circle.
$patron = new Patron(); $patron = new Patron();
$patron->UserId = $payment->UserId; $patron->UserId = $payment->UserId;
@ -259,7 +257,7 @@ try{
} }
} }
else{ else{
// Not eligible to be a patron; send a thank you email anyway, but only if this is a non-recurring donation, or if it's their very first recurring donation // Not eligible to be a patron; send a thank you email anyway, but only if this is a non-recurring donation, or if it's their very first recurring. donation
if($payment->User !== null){ if($payment->User !== null){
$previousPaymentCount = Db::QueryInt(' $previousPaymentCount = Db::QueryInt('
SELECT count(*) SELECT count(*)
@ -268,7 +266,7 @@ try{
and IsRecurring = true and IsRecurring = true
', [$payment->UserId]); ', [$payment->UserId]);
// We just added a payment to the system, so if this is their very first recurring payment, we expect the count to be exactly 1 // We just added a payment to the system, so if this is their very first recurring payment, we expect the count to be exactly 1.
if(!$payment->IsRecurring || $previousPaymentCount == 1){ if(!$payment->IsRecurring || $previousPaymentCount == 1){
$log->Write('Sending thank you email to non-patron donor.'); $log->Write('Sending thank you email to non-patron donor.');
$em = new Email(); $em = new Email();
@ -310,4 +308,3 @@ catch(Exception $ex){
finally{ finally{
$driver->quit(); $driver->quit();
} }
?>

View file

@ -8,8 +8,7 @@ use function Safe\file_get_contents;
use function Safe\preg_match_all; use function Safe\preg_match_all;
use function Safe\shell_exec; use function Safe\shell_exec;
// Get a list of payments that are within 1 year / 45 days of today, and deactivate Patrons Circle members // Get a list of payments that are within 1 year / 45 days of today, and deactivate Patrons Circle members who aren't in that list.
// who aren't in that list.
// We give a 15 day grace period to Patrons Circle members because sometimes FA can be delayed in charging. // We give a 15 day grace period to Patrons Circle members because sometimes FA can be delayed in charging.
$now = new DateTimeImmutable(); $now = new DateTimeImmutable();
@ -35,8 +34,7 @@ $expiredPatrons = Db::Query('
if(sizeof($expiredPatrons) > 0){ if(sizeof($expiredPatrons) > 0){
$ebooksThisYear = 0; $ebooksThisYear = 0;
// We can't use the Library class to get ebooks because this script is typically run via cron or CLI, // We can't use the Library class to get ebooks because this script is typically run via cron or CLI, which doesn't have access PHP-FMP's APCu cache.
// which doesn't have access PHP-FMP's APCu cache.
foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"'))) as $filename){ foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"'))) as $filename){
$metadata = file_get_contents($filename); $metadata = file_get_contents($filename);
@ -66,7 +64,7 @@ if(sizeof($expiredPatrons) > 0){
where UserId = ? where UserId = ?
', [$patron->UserId]); ', [$patron->UserId]);
// Email the patron to notify them their term has ended // Email the patron to notify them their term has ended.
// Is the patron a recurring subscriber? // Is the patron a recurring subscriber?
$lastPayment = Db::Query(' $lastPayment = Db::Query('
SELECT * SELECT *
@ -85,12 +83,12 @@ if(sizeof($expiredPatrons) > 0){
$em->Subject = 'Will you still help us make free, beautiful digital literature?'; $em->Subject = 'Will you still help us make free, beautiful digital literature?';
if($lastPayment[0]->IsRecurring){ if($lastPayment[0]->IsRecurring){
// Email recurring donors who have lapsed // Email recurring donors who have lapsed.
$em->Body = Template::EmailPatronsCircleRecurringCompleted(); $em->Body = Template::EmailPatronsCircleRecurringCompleted();
$em->TextBody = Template::EmailPatronsCircleRecurringCompletedText(); $em->TextBody = Template::EmailPatronsCircleRecurringCompletedText();
} }
else{ else{
// Email one time donors who have expired after one year // Email one time donors who have expired after one year.
$em->Body = Template::EmailPatronsCircleCompleted(['ebooksThisYear' => $ebooksThisYear]); $em->Body = Template::EmailPatronsCircleCompleted(['ebooksThisYear' => $ebooksThisYear]);
$em->TextBody = Template::EmailPatronsCircleCompletedText(['ebooksThisYear' => $ebooksThisYear]); $em->TextBody = Template::EmailPatronsCircleCompletedText(['ebooksThisYear' => $ebooksThisYear]);
} }
@ -99,5 +97,3 @@ if(sizeof($expiredPatrons) > 0){
} }
} }
} }
?>

View file

@ -24,7 +24,7 @@ try{
// Certain user agents may bypass login entirely // Certain user agents may bypass login entirely
$isUserAgentAllowed = false; $isUserAgentAllowed = false;
if(isset($_SERVER['HTTP_USER_AGENT'])){ if(isset($_SERVER['HTTP_USER_AGENT'])){
$isUserAgentAllowed = Db::QueryInt(' $isUserAgentAllowed = Db::QueryBool('
SELECT exists( SELECT exists(
select * select *
from FeedUserAgents from FeedUserAgents