Skip to content

Commit

Permalink
Add system to retrieve and manage donations in a local database
Browse files Browse the repository at this point in the history
  • Loading branch information
acabal committed Jun 20, 2022
1 parent 79c531a commit 70a80d0
Show file tree
Hide file tree
Showing 46 changed files with 782 additions and 910 deletions.
676 changes: 1 addition & 675 deletions LICENSE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PHP 7+ is required.

```shell
# Install Apache, PHP, PHP-FPM, and various other dependencies.
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server
sudo apt install -y git composer php-fpm php-cli php-gd php-xml php-apcu php-mbstring php-intl php-curl php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server

# Create the site root and logs root and clone this repo into it.
sudo mkdir /standardebooks.org/
Expand Down
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"require-dev": {
"phpstan/phpstan": "^0.12.11",
"thecodingmachine/phpstan-safe-rule": "^1.0.0"
"phpstan/phpstan": "^1.7.14",
"thecodingmachine/phpstan-safe-rule": "^1.2.0"
},
"autoload": {
"psr-4": {"": "lib/"},
Expand All @@ -11,6 +11,7 @@
"thecodingmachine/safe": "^1.0.0",
"phpmailer/phpmailer": "6.6.0",
"ramsey/uuid": "4.2.3",
"gregwar/captcha": "1.1.9"
"gregwar/captcha": "1.1.9",
"php-webdriver/webdriver": "1.12.1"
}
}
12 changes: 7 additions & 5 deletions config/phpstan/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ parameters:
- '#Call to an undefined static method Template::[a-zA-Z0-9\\_]+\(\)\.#'

# Ignore errors caused by no type hints on class properties, as that's not available till PHP 7.4
- '#Property .+? has no typehint specified.#'
- '#Property .+? has no type specified.#'

# Ignore errors caused by missing phpdoc strings for arrays
- '#Method .+? has parameter .+? with no value type specified in iterable type array.#'

# Ignore errors caused by type hints that should be union types. Union types are not yet supported in PHP.
- '#Function vd(s|d)?\(\) has parameter \$var with no typehint specified.#'
- '#Method Ebook::NullIfEmpty\(\) has parameter \$elements with no typehint specified.#'
- '#Method HttpInput::GetHttpVar\(\) has no return typehint specified.#'
- '#Method HttpInput::GetHttpVar\(\) has parameter \$default with no typehint specified.#'
- '#Function vd(s|d)?\(\) has parameter \$var with no type specified.#'
- '#Method Ebook::NullIfEmpty\(\) has parameter \$elements with no type specified.#'
- '#Method HttpInput::GetHttpVar\(\) has no return type specified.#'
- '#Method HttpInput::GetHttpVar\(\) has parameter \$default with no type specified.#'
level:
7
paths:
- %rootDir%/../../../lib
- %rootDir%/../../../www
- %rootDir%/../../../scripts
dynamicConstantNames:
- SITE_STATUS
- DONATION_HOLIDAY_ALERT_ON
- DONATION_ALERT_ON
- DONATION_DRIVE_ON
- DONATION_DRIVE_COUNTER_ON
10 changes: 10 additions & 0 deletions config/sql/se/Patrons.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE `Patrons` (
`UserId` int(10) unsigned NOT NULL,
`IsAnonymous` tinyint(1) unsigned NOT NULL DEFAULT 0,
`AlternateName` varchar(80) DEFAULT NULL,
`IsSubscribedToEmails` tinyint(1) NOT NULL DEFAULT 1,
`Timestamp` datetime NOT NULL,
`DeactivatedTimestamp` datetime DEFAULT NULL,
PRIMARY KEY (`UserId`),
KEY `index2` (`IsAnonymous`,`DeactivatedTimestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
12 changes: 12 additions & 0 deletions config/sql/se/Payments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE `Payments` (
`PaymentId` int(10) unsigned NOT NULL AUTO_INCREMENT,
`UserId` int(10) unsigned DEFAULT NULL,
`Timestamp` datetime NOT NULL,
`ChannelId` tinyint(4) unsigned NOT NULL,
`TransactionId` varchar(80) NOT NULL,
`Amount` decimal(7,2) unsigned NOT NULL,
`Fee` decimal(7,2) unsigned NOT NULL DEFAULT 0.00,
`IsRecurring` tinyint(1) unsigned NOT NULL,
PRIMARY KEY (`PaymentId`),
KEY `index2` (`UserId`,`Amount`,`Timestamp`,`IsRecurring`)
) ENGINE=InnoDB AUTO_INCREMENT=828 DEFAULT CHARSET=utf8mb4;
6 changes: 6 additions & 0 deletions config/sql/se/PendingPayments.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE `PendingPayments` (
`Timestamp` datetime NOT NULL,
`ChannelId` tinyint(4) unsigned NOT NULL,
`TransactionId` varchar(80) NOT NULL,
`ProcessedOn` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
9 changes: 9 additions & 0 deletions config/sql/se/Users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE `Users` (
`UserId` int(10) unsigned NOT NULL AUTO_INCREMENT,
`Email` varchar(80) DEFAULT NULL,
`Name` varchar(255) DEFAULT NULL,
`Timestamp` datetime NOT NULL,
`Uuid` char(36) NOT NULL,
PRIMARY KEY (`UserId`),
UNIQUE KEY `idxEmail` (`Email`)
) ENGINE=InnoDB AUTO_INCREMENT=281 DEFAULT CHARSET=utf8mb4;
9 changes: 9 additions & 0 deletions config/sql/users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
create user 'alex'@'localhost' identified by unix_socket;
create user 'se'@'localhost' identified by unix_socket;
create user 'www-data'@'localhost' identified by unix_socket;

grant * on * to alex@localhost identified via unix_socket;
grant select, insert, update, delete, execute on se.* to se@localhost identified via unix_socket;
grant select, insert, update, delete, execute on se.* to www-data@localhost identified via unix_socket;

flush privileges;
7 changes: 7 additions & 0 deletions import.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?
require_once('/standardebooks.org/web/lib/Core.php');

//file_get_contents('/home/alex/donations.csv');

$csv = array_map( 'str_getcsv', file( '/home/alex/donations.csv') );
vdd($csv);
13 changes: 13 additions & 0 deletions lib/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
const CAPTCHA_IMAGE_WIDTH = 230;

const NO_REPLY_EMAIL_ADDRESS = '[email protected]';
const ADMIN_EMAIL_ADDRESS = '[email protected]';
const EDITOR_IN_CHIEF_EMAIL_ADDRESS = '[email protected]';
// We don't define the email username/password in this file to
// 1) avoid a filesystem read when email isn't being used, and
// 2) allow scripts run by users not in the www-data group to succeed, otherwise they will not be able to open secret files on startup and crash
Expand Down Expand Up @@ -72,6 +74,10 @@

const AVERAGE_READING_WORDS_PER_MINUTE = 275;

const PAYMENT_CHANNEL_FA = 0;

const FA_FEE_PERCENT = 0.87;

define('PD_YEAR', intval(gmdate('Y')) - 96);
define('PD_STRING', 'January 1, ' . (PD_YEAR + 1));

Expand All @@ -85,3 +91,10 @@
const GITHUB_IGNORED_REPOS = ['tools', 'manual', 'web']; // If we get GitHub push requests featuring these repos, silently ignore instead of returning an error.

const POSTMARK_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-postmark.log'; // Must be writable by `www-data` Unix user.

const ZOHO_SECRET_FILE_PATH = SITE_ROOT . '/config/secrets/[email protected]'; // Set in the GitHub organization global webhook settings.
const ZOHO_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-zoho.log'; // Must be writable by `www-data` Unix user.

const FA_SECRET_FILE_PATH = SITE_ROOT . '/config/secrets/fracturedatlas.org';
const DONATIONS_LOG_FILE_PATH = '/var/log/local/donations.log'; // Must be writable by `www-data` Unix user.

2 changes: 1 addition & 1 deletion lib/DbConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function Query(string $sql, array $params = [], string $class = 'stdClass
foreach($params as $parameter){
$name++;

if(is_a($parameter, 'DateTime')){
if(is_a($parameter, 'DateTime') || is_a($parameter, 'DateTimeImmutable')){
$parameter = $parameter->format('Y-m-d H:i:s');
}

Expand Down
12 changes: 6 additions & 6 deletions lib/Ebook.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function __construct(string $wwwFilesystemPath){
$this->RepoFilesystemPath = SITE_ROOT . '/ebooks/' . str_replace('/', '_', $this->RepoFilesystemPath) . '.git';

if(!is_dir($this->RepoFilesystemPath)){ // On dev systems we might not have the bare repos, so make an adjustment
$this->RepoFilesystemPath = preg_replace('/\.git$/ius', '', $this->RepoFilesystemPath) ?? '';
$this->RepoFilesystemPath = preg_replace('/\.git$/ius', '', $this->RepoFilesystemPath);
}

if(!is_dir($wwwFilesystemPath)){
Expand All @@ -83,7 +83,7 @@ public function __construct(string $wwwFilesystemPath){
$this->WwwFilesystemPath = $wwwFilesystemPath;
$this->Url = str_replace(WEB_ROOT, '', $this->WwwFilesystemPath);

$rawMetadata = file_get_contents($wwwFilesystemPath . '/content.opf') ?: '';
$rawMetadata = file_get_contents($wwwFilesystemPath . '/content.opf');

// Get the SE identifier.
preg_match('|<dc:identifier[^>]*?>(.+?)</dc:identifier>|ius', $rawMetadata, $matches);
Expand Down Expand Up @@ -207,7 +207,7 @@ public function __construct(string $wwwFilesystemPath){
// Fill the ToC if necessary
if($includeToc){
$this->TocEntries = [];
$tocDom = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($wwwFilesystemPath . '/toc.xhtml') ?: ''));
$tocDom = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($wwwFilesystemPath . '/toc.xhtml')));
$tocDom->registerXPathNamespace('epub', 'http://www.idpf.org/2007/ops');
foreach($tocDom->xpath('/html/body//nav[@epub:type="toc"]//a[not(contains(@epub:type, "z3998:roman")) and not(text() = "Titlepage" or text() = "Imprint" or text() = "Colophon" or text() = "Endnotes" or text() = "Uncopyright") and not(contains(@href, "halftitle"))]') ?: [] as $item){
$this->TocEntries[] = (string)$item;
Expand Down Expand Up @@ -493,8 +493,8 @@ public function Contains(string $query): bool{
}

// Remove diacritics and non-alphanumeric characters
$searchString = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($searchString)) ?? '');
$query = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($query)) ?? '');
$searchString = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($searchString)));
$query = trim(preg_replace('|[^a-zA-Z0-9 ]|ius', ' ', Formatter::RemoveDiacritics($query)));

if($query == ''){
return false;
Expand Down Expand Up @@ -586,7 +586,7 @@ public function GenerateJsonLd(): string{
}
}

return json_encode($output, JSON_PRETTY_PRINT) ?: '';
return json_encode($output, JSON_PRETTY_PRINT);
}

private function GenerateContributorJsonLd(Contributor $contributor): stdClass{
Expand Down
2 changes: 1 addition & 1 deletion lib/Email.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use function Safe\define;
use function Safe\file_get_contents;

define('EMAIL_SMTP_USERNAME', trim(file_get_contents(POSTMARK_SECRET_FILE_PATH)) ?: '');
define('EMAIL_SMTP_USERNAME', trim(file_get_contents(POSTMARK_SECRET_FILE_PATH)));

class Email{
public $To = '';
Expand Down
6 changes: 6 additions & 0 deletions lib/Exceptions/PaymentExistsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?
namespace Exceptions;

class PaymentExistsException extends SeException{
protected $message = 'This transaction ID already exists.';
}
6 changes: 6 additions & 0 deletions lib/Exceptions/UserExistsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?
namespace Exceptions;

class UserExistsException extends SeException{
protected $message = 'This email already exists in the database.';
}
4 changes: 2 additions & 2 deletions lib/Formatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public static function MakeUrlSafe(string $text): string{
$text = mb_strtolower(trim($text));

// Then convert any non-digit, non-letter character to a space
$text = preg_replace('/[^0-9a-zA-Z]/ius', ' ', $text) ?: '';
$text = preg_replace('/[^0-9a-zA-Z]/ius', ' ', $text);

// Then convert any instance of one or more space to dash
$text = preg_replace('/\s+/ius', '-', $text) ?: '';
$text = preg_replace('/\s+/ius', '-', $text);

// Finally, trim dashes
$text = trim($text, '-');
Expand Down
2 changes: 1 addition & 1 deletion lib/Library.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public static function RebuildCache(): void{

foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"') ?? '')) as $filename){
try{
$ebookWwwFilesystemPath = preg_replace('|/content\.opf|ius', '', $filename) ?? '';
$ebookWwwFilesystemPath = preg_replace('|/content\.opf|ius', '', $filename);

$ebook = new Ebook($ebookWwwFilesystemPath);

Expand Down
1 change: 1 addition & 0 deletions lib/Log.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use function Safe\fclose;
use function Safe\error_log;
use function Safe\gmdate;
use function Safe\substr;

class Log{
private $RequestId = null;
Expand Down
26 changes: 7 additions & 19 deletions lib/OpdsFeed.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,14 @@ protected function Sha1Entries(string $xmlString): string{
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $xmlString));
$xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$xml->registerXPathNamespace('schema', 'http://schema.org/');
$entries = $xml->xpath('/feed/entry');

if(!$entries){
$entries = [];
// Remove any <updated> elements, we don't want to compare against those.
foreach($xml->xpath('//updated') ?: [] as $element){
unset($element[0]);
}

$output = '';
foreach($entries as $entry){
// Remove any <updated> elements, we don't want to compare against those.
// This makes it easier to for example generate a new subjects index,
// while updating it at the same time.
$elements = $xml->xpath('/feed/entry/updated');

if(!$elements){
$elements = [];
}

foreach($elements as $element){
unset($element[0]);
}

foreach($xml->xpath('/feed/entry') ?: [] as $entry){
$output .= $entry->asXml();
}

Expand All @@ -60,6 +47,7 @@ protected function SaveIfChanged(string $path, string $feed, string $updatedTime
$tempFilename = tempnam('/tmp/', 'se-opds-');
file_put_contents($tempFilename, $feed);
exec('se clean ' . escapeshellarg($tempFilename) . ' 2>&1', $output); // Capture the result in case there's an error, otherwise it prints to stdout
$feed = file_get_contents($tempFilename);

// Did we actually update the feed? If so, write to file and update the index
if(!is_file($path) || ($this->Sha1Entries($feed) != $this->Sha1Entries(file_get_contents($path)))){
Expand All @@ -70,13 +58,13 @@ protected function SaveIfChanged(string $path, string $feed, string $updatedTime
}
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', file_get_contents($parentFilepath)));

$feedEntries = $xml->xpath('/feed/entry[id="' . $this->Id . '"]/updated');
$feedEntries = $xml->xpath('/feed/entry[id="' . $this->Id . '"]');
if(!$feedEntries){
$feedEntries = [];
}

if(sizeof($feedEntries) > 0){
$feedEntries[0][0] = $updatedTimestamp;
$feedEntries[0]->{'updated'} = $updatedTimestamp;
}

$xmlString = $xml->asXml();
Expand Down
57 changes: 57 additions & 0 deletions lib/Patron.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?
use Safe\DateTime;

class Patron extends PropertiesBase{
protected $User = null;
public $UserId = null;
public $IsAnonymous;
public $AlternateName;
public $IsSubscribedToEmail;
public $Timestamp = null;
public $DeactivatedTimestamp = null;

public static function Get(int $userId): ?Patron{
$result = Db::Query('select * from Patrons where UserId = ?', [$userId], 'Patron');

return $result[0] ?? null;
}

protected function GetUser(): ?User{
if($this->User === null && $this->UserId !== null){
$this->User = User::Get($this->UserId);
}

return $this->User;
}

public function Create(bool $sendEmail = true): void{
Db::Query('insert into Patrons (Timestamp, UserId, IsAnonymous, AlternateName, IsSubscribedToEmail) values(utc_timestamp(), ?, ?, ?, ?);', [$this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmail]);

if($sendEmail){
$this->SendWelcomeEmail();
}
}

public function Reactivate(bool $sendEmail = true): void{
Db::Query('update Patrons set Timestamp = utc_timestamp(), DeactivatedTimestamp = null, IsAnonymous = ?, IsSubscribedToEmail = ?, AlternateName = ? where UserId = ?;', [$this->IsAnonymous, $this->IsSubscribedToEmail, $this->AlternateName, $this->UserId]);
$this->Timestamp = new DateTime();
$this->DeactivatedTimestamp = null;

if($sendEmail){
$this->SendWelcomeEmail();
}
}

private function SendWelcomeEmail(): void{
$this->GetUser();
if($this->User !== null){
$em = new Email();
$em->To = $this->User->Email;
$em->From = EDITOR_IN_CHIEF_EMAIL_ADDRESS;
$em->Subject = 'Thank you for supporting Standard Ebooks!';
$em->Body = Template::EmailPatronsCircleWelcome(['isAnonymous' => $this->IsAnonymous]);
$em->TextBody = Template::EmailPatronsCircleWelcomeText(['isAnonymous' => $this->IsAnonymous]);
//$em->Send();
}
}
}
Loading

0 comments on commit 70a80d0

Please sign in to comment.