diff --git a/lib/Db.php b/lib/Db.php index 57d2ea36..270f06ed 100644 --- a/lib/Db.php +++ b/lib/Db.php @@ -54,4 +54,22 @@ class Db{ 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 $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; + } } diff --git a/lib/Exceptions/MultiSelectMethodNotFoundException.php b/lib/Exceptions/MultiSelectMethodNotFoundException.php new file mode 100644 index 00000000..56252c5b --- /dev/null +++ b/lib/Exceptions/MultiSelectMethodNotFoundException.php @@ -0,0 +1,15 @@ +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(); + } +} diff --git a/scripts/delete-unconfirmed-newsletter-subscribers b/scripts/delete-unconfirmed-newsletter-subscribers index cb094643..495654ac 100755 --- a/scripts/delete-unconfirmed-newsletter-subscribers +++ b/scripts/delete-unconfirmed-newsletter-subscribers @@ -2,11 +2,10 @@ = 7 '); -?> diff --git a/scripts/generate-feeds b/scripts/generate-feeds index 079ec577..69977a65 100755 --- a/scripts/generate-feeds +++ b/scripts/generate-feeds @@ -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 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 = []; foreach($collections as $collection){ $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 . '.'; SaveFeed($collectionsFeed, $force, null, null, $now); - // Now generate each individual collection feed + // Now generate each individual collection feed. foreach($collectionNavigationEntries as $collectionNavigationEntry){ $id = basename($collectionNavigationEntry->Id); 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){ $allEbooks[] = $ebook; $newestEbooks[] = $ebook; @@ -116,7 +116,7 @@ $newestEbooks = array_slice($newestEbooks, 0, $ebooksPerNewestEbooksFeed); $now = new DateTimeImmutable(); -// Create OPDS feeds +// Create OPDS feeds. $opdsRootEntries = [ new OpdsNavigationEntry( '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); 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); -// 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); -// 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); -// 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); 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); 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); 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); 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); 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); SaveFeed($newestAtomFeed, $force, null, null, $now); -// Generate each individual subject feed +// Generate each individual subject feed. foreach($ebooksBySubject as $subject => $ebooks){ usort($ebooks, 'SortByUpdatedDesc'); @@ -210,7 +210,7 @@ foreach($ebooksBySubject as $subject => $ebooks){ 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){ usort($ebooks, 'SortByUpdatedDesc'); @@ -226,7 +226,7 @@ foreach($ebooksByCollection as $collection => $ebooks){ 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){ usort($ebooks, 'SortByUpdatedDesc'); @@ -240,8 +240,8 @@ foreach($ebooksByAuthor as $collection => $ebooks){ SaveFeed($collectionAtomFeed, $force, $authors[$collection]['name'], $authors[$collection]['sortedname'], $now); } -// Set ownership and permissions -// We don't use PHP's built in chown/chmod chmod can't accept strings +// Set ownership and permissions. +// 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. exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*.xml'); exec('sudo chown --preserve-root --recursive se:committers ' . escapeshellarg($webRoot) . '/feeds/*/*/*.xml'); diff --git a/scripts/ingest-fa-payments b/scripts/ingest-fa-payments index 8c504dba..c3bf5c9f 100755 --- a/scripts/ingest-fa-payments +++ b/scripts/ingest-fa-payments @@ -1,14 +1,15 @@ #!/usr/bin/php 5){ - // Safety valve for runaway logic + // Safety valve for runaway logic. 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); // Check if we need to log in to FA. - // Wait until the element is visible, then check the current URL + // 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 + // 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"]'))); $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"]'))); - // Fill out and submit the form + // 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 + // 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")]'))); } @@ -139,7 +140,7 @@ try{ 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){ $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{ - // Last seen transaction ID is not null, get everything from that ID - // Get a list of transaction IDs on the page + // 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]; @@ -218,4 +219,3 @@ catch(Exception $ex){ finally{ $driver?->quit(); } -?> diff --git a/scripts/pending-payments-watchdog b/scripts/pending-payments-watchdog index 600d46da..3cfe8a17 100644 --- a/scripts/pending-payments-watchdog +++ b/scripts/pending-payments-watchdog @@ -18,5 +18,3 @@ if(sizeof($pendingPayments) > 0){ $em->TextBody = Template::EmailAdminUnprocessedDonationsText(); $em->Send(); } - -?> diff --git a/scripts/process-pending-payments b/scripts/process-pending-payments index 64646dd8..ff80d999 100755 --- a/scripts/process-pending-payments +++ b/scripts/process-pending-payments @@ -1,7 +1,6 @@ #!/usr/bin/php addArguments(['-headless']); // WARNING: Only one dash! +$firefoxOptions->addArguments(['-headless']); // **Warning**: Only one dash! $capabilities = DesiredCapabilities::firefox(); $capabilities->setCapability(FirefoxOptions::CAPABILITY, $firefoxOptions); $driver = null; $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')); $faUsername = get_cfg_var('se.secrets.fractured_atlas.username'); $faPassword = get_cfg_var('se.secrets.fractured_atlas.password'); @@ -61,7 +60,7 @@ Db::Query(' Db::Query('commit'); 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(); } @@ -73,13 +72,13 @@ try{ if($pendingPayment->Processor == PaymentProcessorType::FracturedAtlas){ $log->Write('Processing donation ' . $pendingPayment->TransactionId . ' ...'); - if(Db::QueryInt(' + if(Db::QueryBool(' SELECT exists( select * from Payments where TransactionId = ? ) - ', [$pendingPayment->TransactionId]) > 0){ + ', [$pendingPayment->TransactionId])){ $log->Write('Donation already exists in database.'); continue; } @@ -87,24 +86,24 @@ try{ $driver->get('https://fundraising.fracturedatlas.org/admin/donations?query=' . $pendingPayment->TransactionId); // Check if we need to log in to FA. - // Wait until the element is visible, then check the current URL + // 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 + // 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"]'))); $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"]'))); - // Fill out and submit the form + // 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 + // 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")]'))); } @@ -116,8 +115,7 @@ try{ // 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, - // and the details row has the rest of the information + // 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. $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 . '")]')); @@ -145,13 +143,13 @@ try{ $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 == ''){ $payment->User = null; } } 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; } @@ -171,12 +169,12 @@ try{ $payment->User = null; } - // All set - create the payment + // All set - create the payment. try{ $payment->Create(); } catch(Exceptions\PaymentExistsException){ - // Payment already exists, just continue + // Payment already exists, just continue. $log->Write('Donation already in database.'); continue; } @@ -202,15 +200,15 @@ try{ // This payment is eligible for the Patrons Circle! if($payment->User !== null){ // Are we already a patron? - if(Db::QueryInt(' + if(!Db::QueryBool(' SELECT exists( select * from Patrons where UserId = ? and Ended is null ) - ', [$payment->UserId]) == 0){ - // Not a patron yet, add them to the Patrons Circle + ', [$payment->UserId])){ + // Not a patron yet, add them to the Patrons Circle. $patron = new Patron(); $patron->UserId = $payment->UserId; @@ -259,7 +257,7 @@ try{ } } 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){ $previousPaymentCount = Db::QueryInt(' SELECT count(*) @@ -268,7 +266,7 @@ try{ and IsRecurring = true ', [$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){ $log->Write('Sending thank you email to non-patron donor.'); $em = new Email(); @@ -310,4 +308,3 @@ catch(Exception $ex){ finally{ $driver->quit(); } -?> diff --git a/scripts/update-patrons-circle b/scripts/update-patrons-circle index f09e525f..0cee2a24 100755 --- a/scripts/update-patrons-circle +++ b/scripts/update-patrons-circle @@ -8,8 +8,7 @@ use function Safe\file_get_contents; use function Safe\preg_match_all; use function Safe\shell_exec; -// 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. +// 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. // We give a 15 day grace period to Patrons Circle members because sometimes FA can be delayed in charging. $now = new DateTimeImmutable(); @@ -35,8 +34,7 @@ $expiredPatrons = Db::Query(' if(sizeof($expiredPatrons) > 0){ $ebooksThisYear = 0; - // 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. + // 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. foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"'))) as $filename){ $metadata = file_get_contents($filename); @@ -66,7 +64,7 @@ if(sizeof($expiredPatrons) > 0){ where 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? $lastPayment = Db::Query(' SELECT * @@ -85,12 +83,12 @@ if(sizeof($expiredPatrons) > 0){ $em->Subject = 'Will you still help us make free, beautiful digital literature?'; if($lastPayment[0]->IsRecurring){ - // Email recurring donors who have lapsed + // Email recurring donors who have lapsed. $em->Body = Template::EmailPatronsCircleRecurringCompleted(); $em->TextBody = Template::EmailPatronsCircleRecurringCompletedText(); } 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->TextBody = Template::EmailPatronsCircleCompletedText(['ebooksThisYear' => $ebooksThisYear]); } @@ -99,5 +97,3 @@ if(sizeof($expiredPatrons) > 0){ } } } - -?> diff --git a/www/feeds/download.php b/www/feeds/download.php index 948c7dc5..d2ceb118 100644 --- a/www/feeds/download.php +++ b/www/feeds/download.php @@ -24,7 +24,7 @@ try{ // Certain user agents may bypass login entirely $isUserAgentAllowed = false; if(isset($_SERVER['HTTP_USER_AGENT'])){ - $isUserAgentAllowed = Db::QueryInt(' + $isUserAgentAllowed = Db::QueryBool(' SELECT exists( select * from FeedUserAgents