mirror of
https://github.com/standardebooks/web.git
synced 2025-07-06 06:40:33 -04:00
Create cookie-based login and authentication system
This commit is contained in:
parent
45221365b5
commit
0bc3dc3830
46 changed files with 528 additions and 195 deletions
|
@ -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 php-zip apache2 apache2-utils libfcgi0ldbl task-spooler ipv6calc mariadb-server libaprutil1-dbd-mysql attr
|
||||
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 libaprutil1-dbd-mysql attr libapache2-mod-xsendfile
|
||||
|
||||
# Create the site root and logs root and clone this repo into it.
|
||||
sudo mkdir /standardebooks.org/
|
||||
|
@ -26,7 +26,7 @@ echo -e "127.0.0.1\tstandardebooks.test" | sudo tee -a /etc/hosts
|
|||
openssl req -x509 -nodes -days 99999 -newkey rsa:4096 -subj "/CN=standardebooks.test" -keyout /standardebooks.org/web/config/ssl/standardebooks.test.key -sha256 -out /standardebooks.org/web/config/ssl/standardebooks.test.crt
|
||||
|
||||
# Enable the necessary Apache modules.
|
||||
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi authn_dbd
|
||||
sudo a2enmod headers expires ssl rewrite proxy proxy_fcgi authn_dbd xsendfile
|
||||
|
||||
# Link and enable the SE Apache configuration file.
|
||||
sudo ln -s /standardebooks.org/web/config/apache/standardebooks.test.conf /etc/apache2/sites-available/
|
||||
|
|
|
@ -245,7 +245,7 @@ Define webroot /standardebooks.org/web
|
|||
RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4
|
||||
|
||||
RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA]
|
||||
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
|
||||
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
|
||||
RewriteRule ^/subjects/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA]
|
||||
RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA]
|
||||
RewriteRule ^/collections/([^/]+?)/downloads$ /bulk-downloads/get.php?collection=$1
|
||||
|
@ -279,14 +279,14 @@ Define webroot /standardebooks.org/web
|
|||
RewriteCond %{QUERY_STRING} \bquery=
|
||||
RewriteRule ^/feeds/(opds|atom|rss)/all.xml$ /feeds/$1/search.php [QSA]
|
||||
|
||||
# Rewrite rules for bulk downloads
|
||||
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
||||
|
||||
# Enable mod_authn_dbd
|
||||
DBDriver mysql
|
||||
DBDParams "dbname=se user=www-data"
|
||||
# HTTP Basic Auth configuration for:
|
||||
# /bulk-downloads
|
||||
# /feeds
|
||||
# /polls/votes (we will allow access to view results at /polls/votes/index.php further down)
|
||||
<DirectoryMatch "^${webroot}/www/(polls/votes|bulk-downloads|feeds/(opds|rss|atom))">
|
||||
# HTTP Basic Auth configuration for /feeds
|
||||
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
|
||||
AuthType Basic
|
||||
AuthName "Enter your Patrons Circle email address and leave the password empty."
|
||||
Require valid-user
|
||||
|
@ -300,34 +300,14 @@ Define webroot /standardebooks.org/web
|
|||
# The hash is simply the hash of a blank password. We're only interested in the username/API key.
|
||||
# We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
|
||||
# function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
|
||||
AuthDBDUserPWQuery "\
|
||||
select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from \
|
||||
( \
|
||||
select Email, Uuid from Patrons p inner join Users u using (UserId) where p.Ended is null \
|
||||
union \
|
||||
select Email, Uuid from ApiKeys fu inner join Users u using (UserId) where fu.Ended is null \
|
||||
) x where %s in (Email, Uuid) limit 1 \
|
||||
"
|
||||
AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /bulk-downloads
|
||||
<DirectoryMatch "^${webroot}/www/bulk-downloads">
|
||||
<FilesMatch "\.php$">
|
||||
# Disable HTTP Basic auth for the index and 401 pages
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.zip$">
|
||||
ErrorDocument 401 /bulk-downloads
|
||||
</FilesMatch>
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /polls/votes
|
||||
<DirectoryMatch "^${webroot}/www/polls/votes">
|
||||
<FilesMatch "index.php$">
|
||||
# Disable HTTP Basic auth for the index page
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
||||
# Both directives are required
|
||||
XSendFile on
|
||||
XSendFilePath /standardebooks.org/web/www/bulk-downloads
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /feeds
|
||||
|
|
|
@ -227,7 +227,7 @@ Define webroot /standardebooks.org/web
|
|||
RewriteRule ^/images/covers/(.+?)\-[a-z0-9]{8}\-(cover|hero)(@2x)?\.(jpg|avif)$ /images/covers/$1-$2$3.$4
|
||||
|
||||
RewriteRule ^/ebooks/([^\./]+?)$ /ebooks/author.php?url-path=$1 [QSA]
|
||||
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
|
||||
RewriteRule ^/ebooks/([^\./]+?)/downloads$ /bulk-downloads/get.php?author=$1 [QSA]
|
||||
RewriteRule ^/subjects/([^\./]+?)$ /ebooks/index.php?tags[]=$1 [QSA]
|
||||
RewriteRule ^/collections/([^\./]+?)$ /ebooks/index.php?collection=$1 [QSA]
|
||||
RewriteRule ^/collections/([^/]+?)/downloads$ /bulk-downloads/get.php?collection=$1
|
||||
|
@ -261,14 +261,14 @@ Define webroot /standardebooks.org/web
|
|||
RewriteCond %{QUERY_STRING} \bquery=
|
||||
RewriteRule ^/feeds/(opds|atom|rss)/all.xml$ /feeds/$1/search.php [QSA]
|
||||
|
||||
# Rewrite rules for bulk downloads
|
||||
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
||||
|
||||
# Enable mod_authn_dbd
|
||||
DBDriver mysql
|
||||
DBDParams "dbname=se user=www-data"
|
||||
# HTTP Basic Auth configuration for:
|
||||
# /bulk-downloads
|
||||
# /feeds
|
||||
# /polls/votes (we will allow access to view results at /polls/votes/index.php further down)
|
||||
<DirectoryMatch "^${webroot}/www/(polls/votes|bulk-downloads|feeds/(opds|rss|atom))">
|
||||
# HTTP Basic Auth configuration for /feeds
|
||||
<DirectoryMatch "^${webroot}/www/feeds/(opds|rss|atom)">
|
||||
AuthType Basic
|
||||
AuthName "Enter your Patrons Circle email address and leave the password empty."
|
||||
Require valid-user
|
||||
|
@ -282,34 +282,14 @@ Define webroot /standardebooks.org/web
|
|||
# The hash is simply the hash of a blank password. We're only interested in the username/API key.
|
||||
# We have to do this tortured query instead of a cleaner one, because the AuthDBDUserPWQuery
|
||||
# function will only replace %s EXACTLY ONCE. We cannot have more than one %s in the query string.
|
||||
AuthDBDUserPWQuery "\
|
||||
select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from \
|
||||
( \
|
||||
select Email, Uuid from Patrons p inner join Users u using (UserId) where p.Ended is null \
|
||||
union \
|
||||
select Email, Uuid from ApiKeys fu inner join Users u using (UserId) where fu.Ended is null \
|
||||
) x where %s in (Email, Uuid) limit 1 \
|
||||
"
|
||||
AuthDBDUserPWQuery "select '$apr1$13q1pnGf$vQnIj94BXP1EPdL/4ISba.' from Users u inner join Benefits b using (UserId) where %s in (u.Email, u.Uuid) and b.CanAccessFeeds = true limit 1"
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /bulk-downloads
|
||||
<DirectoryMatch "^${webroot}/www/bulk-downloads">
|
||||
<FilesMatch "\.php$">
|
||||
# Disable HTTP Basic auth for the index and 401 pages
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
<FilesMatch "\.zip$">
|
||||
ErrorDocument 401 /bulk-downloads
|
||||
</FilesMatch>
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /polls/votes
|
||||
<DirectoryMatch "^${webroot}/www/polls/votes">
|
||||
<FilesMatch "index.php$">
|
||||
# Disable HTTP Basic auth for the index page
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
||||
# Both directives are required
|
||||
XSendFile on
|
||||
XSendFilePath /standardebooks.org/web/www/bulk-downloads
|
||||
</DirectoryMatch>
|
||||
|
||||
# Specific config for /feeds
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
CREATE TABLE `ApiKeys` (
|
||||
`UserId` int(10) unsigned NOT NULL,
|
||||
`Created` datetime NOT NULL,
|
||||
`Ended` datetime DEFAULT NULL,
|
||||
`Notes` text DEFAULT NULL,
|
||||
KEY `idxUserId` (`UserId`,`Ended`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
8
config/sql/se/Benefits.sql
Normal file
8
config/sql/se/Benefits.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE `Benefits` (
|
||||
`UserId` int(10) unsigned NOT NULL,
|
||||
`CanAccessFeeds` tinyint(1) unsigned NOT NULL,
|
||||
`CanVote` tinyint(1) unsigned NOT NULL,
|
||||
`CanBulkDownload` tinyint(1) unsigned NOT NULL,
|
||||
PRIMARY KEY (`UserId`),
|
||||
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
7
config/sql/se/Sessions.sql
Normal file
7
config/sql/se/Sessions.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE `Sessions` (
|
||||
`UserId` int(10) unsigned NOT NULL,
|
||||
`Created` datetime NOT NULL,
|
||||
`SessionId` char(36) NOT NULL,
|
||||
KEY `idxUserId` (`UserId`),
|
||||
KEY `idxSessionId` (`SessionId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
6
lib/Benefits.php
Normal file
6
lib/Benefits.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
class Benefits{
|
||||
public $CanAccessFeeds = false;
|
||||
public $CanVote = false;
|
||||
public $CanBulkDownload = false;
|
||||
}
|
|
@ -11,12 +11,13 @@ define('SITE_STATUS', get_cfg_var('se.site_status') ?: SITE_STATUS_DEV); // Set
|
|||
|
||||
// No trailing slash on any of the below constants.
|
||||
if(SITE_STATUS == SITE_STATUS_LIVE){
|
||||
define('SITE_URL', 'https://standardebooks.org');
|
||||
define('SITE_DOMAIN', 'standardebooks.org');
|
||||
}
|
||||
else{
|
||||
define('SITE_URL', 'https://standardebooks.test');
|
||||
define('SITE_DOMAIN', 'standardebooks.test');
|
||||
}
|
||||
|
||||
const SITE_URL = 'https://' . SITE_DOMAIN;
|
||||
const SITE_ROOT = '/standardebooks.org';
|
||||
const WEB_ROOT = SITE_ROOT . '/web/www';
|
||||
const REPOS_PATH = SITE_ROOT . '/ebooks';
|
||||
|
@ -49,6 +50,7 @@ const WEB = 1;
|
|||
const GET = 'GET';
|
||||
const POST = 'POST';
|
||||
const COOKIE = 'COOKIE';
|
||||
const SESSION = 'SESSION';
|
||||
|
||||
const HTTP_VAR_INT = 0;
|
||||
const HTTP_VAR_STR = 1;
|
||||
|
|
|
@ -26,3 +26,5 @@ if(SITE_STATUS == SITE_STATUS_LIVE){
|
|||
throw $ex; // Send the exception back to PHP for its usual logging routine.
|
||||
});
|
||||
}
|
||||
|
||||
$GLOBALS['User'] = Session::GetLoggedInUser();
|
||||
|
|
5
lib/Exceptions/InvalidFileException.php
Normal file
5
lib/Exceptions/InvalidFileException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidFileException extends SeException{
|
||||
}
|
6
lib/Exceptions/InvalidPermissionsException.php
Normal file
6
lib/Exceptions/InvalidPermissionsException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidPermissionsException extends SeException{
|
||||
protected $message = 'You don’t have permission to perform that action.';
|
||||
}
|
5
lib/Exceptions/InvalidSessionException.php
Normal file
5
lib/Exceptions/InvalidSessionException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidSessionException extends SeException{
|
||||
}
|
5
lib/Exceptions/LoginRequiredException.php
Normal file
5
lib/Exceptions/LoginRequiredException.php
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?
|
||||
namespace Exceptions;
|
||||
|
||||
class LoginRequiredException extends SeException{
|
||||
}
|
|
@ -2,5 +2,10 @@
|
|||
namespace Exceptions;
|
||||
|
||||
class PollVoteExistsException extends SeException{
|
||||
public $Vote = null;
|
||||
protected $message = 'You’ve already voted in this poll.';
|
||||
|
||||
public function __construct(?\PollVote $vote = null){
|
||||
$this->Vote = $vote;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,9 @@ class HttpInput{
|
|||
case COOKIE:
|
||||
$vars = $_COOKIE;
|
||||
break;
|
||||
case SESSION:
|
||||
$vars = $_SESSION;
|
||||
break;
|
||||
}
|
||||
|
||||
if(isset($vars[$variable])){
|
||||
|
|
|
@ -22,6 +22,9 @@ class Patron extends PropertiesBase{
|
|||
$this->Created = new DateTime();
|
||||
Db::Query('INSERT into Patrons (Created, UserId, IsAnonymous, AlternateName, IsSubscribedToEmails) values(?, ?, ?, ?, ?);', [$this->Created, $this->UserId, $this->IsAnonymous, $this->AlternateName, $this->IsSubscribedToEmails]);
|
||||
|
||||
|
||||
Db::Query('INSERT into Benefits (UserId, CanVote, CanAccessFeeds, CanBulkDownload) values (?, true, true, true) on duplicate key update CanVote = true, CanAccessFeeds = true, CanBulkDownload = true', [$this->UserId]);
|
||||
|
||||
// If this is a patron for the first time, send the first-time patron email.
|
||||
// Otherwise, send the returning patron email.
|
||||
$isReturning = Db::QueryInt('SELECT count(*) from Patrons where UserId = ?', [$this->UserId]) > 1;
|
||||
|
|
|
@ -35,8 +35,8 @@ class PollVote extends PropertiesBase{
|
|||
protected function Validate(): void{
|
||||
$error = new Exceptions\ValidationException();
|
||||
|
||||
if($this->UserId === null){
|
||||
$error->Add(new Exceptions\InvalidPatronException());
|
||||
if($this->UserId === null || $this->User === null){
|
||||
$error->Add(new Exceptions\InvalidUserException());
|
||||
}
|
||||
|
||||
if($this->PollItemId === null){
|
||||
|
@ -56,17 +56,17 @@ class PollVote extends PropertiesBase{
|
|||
// Basic sanity checks done, now check if we've already voted
|
||||
// in this poll
|
||||
|
||||
if($this->User === null){
|
||||
$error->Add(new Exceptions\InvalidPatronException());
|
||||
// Do we already have a vote for this poll, from this user?
|
||||
try{
|
||||
$vote = PollVote::Get($this->PollItem->Poll->UrlName, $this->UserId);
|
||||
$error->Add(new Exceptions\PollVoteExistsException($vote));
|
||||
}
|
||||
else{
|
||||
// Do we already have a vote for this poll, from this user?
|
||||
if(Db::QueryInt('
|
||||
SELECT count(*) from PollVotes pv inner join
|
||||
(select PollItemId from PollItems pi inner join Polls p using (PollId)) x
|
||||
using (PollItemId) where pv.UserId = ?', [$this->UserId]) > 0){
|
||||
$error->Add(new Exceptions\PollVoteExistsException());
|
||||
}
|
||||
catch(Exceptions\InvalidPollVoteException $ex){
|
||||
// User hasn't voted yet, carry on
|
||||
}
|
||||
|
||||
if(!$this->User->Benefits->CanVote){
|
||||
$error->Add(new Exceptions\InvalidPatronException());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,11 +78,10 @@ class PollVote extends PropertiesBase{
|
|||
public function Create(?string $email = null): void{
|
||||
if($email !== null){
|
||||
try{
|
||||
$patron = Patron::GetByEmail($email);
|
||||
$this->UserId = $patron->UserId;
|
||||
$this->User = $patron->User;
|
||||
$this->User = User::GetByEmail($email);
|
||||
$this->UserId = $this->User->UserId;
|
||||
}
|
||||
catch(Exceptions\InvalidPatronException $ex){
|
||||
catch(Exceptions\InvalidUserException $ex){
|
||||
// Can't validate patron email - do nothing for now,
|
||||
// this will be caught later when we validate the vote during creation.
|
||||
// Save the email in the User object in case we want it later,
|
||||
|
|
82
lib/Session.php
Normal file
82
lib/Session.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Safe\DateTime;
|
||||
|
||||
/**
|
||||
* @property User $User
|
||||
* @property PollItem $PollItem
|
||||
* @property string $Url
|
||||
*/
|
||||
class Session extends PropertiesBase{
|
||||
public $UserId;
|
||||
protected $_User = null;
|
||||
public $Created;
|
||||
public $SessionId;
|
||||
public $_Url;
|
||||
|
||||
|
||||
// *******
|
||||
// GETTERS
|
||||
// *******
|
||||
|
||||
protected function GetUrl(): string{
|
||||
if($this->_Url === null){
|
||||
$this->_Url = '/sessions/' . $this->SessionId;
|
||||
}
|
||||
|
||||
return $this->_Url;
|
||||
}
|
||||
|
||||
|
||||
// *******
|
||||
// METHODS
|
||||
// *******
|
||||
|
||||
public function Create(?string $email = null): void{
|
||||
$this->User = User::GetIfRegistered($email);
|
||||
$this->UserId = $this->User->UserId;
|
||||
|
||||
$existingSessions = Db::Query('SELECT SessionId, Created from Sessions where UserId = ?', [$this->UserId]);
|
||||
|
||||
if(sizeof($existingSessions) > 0){
|
||||
$this->SessionId = $existingSessions[0]->SessionId;
|
||||
$this->Created = $existingSessions[0]->Created;
|
||||
}
|
||||
else{
|
||||
$uuid = Uuid::uuid4();
|
||||
$this->SessionId = $uuid->toString();
|
||||
$this->Created = new DateTime();
|
||||
Db::Query('INSERT into Sessions (UserId, SessionId, Created) values (?, ?, ?)', [$this->UserId, $this->SessionId, $this->Created]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function GetLoggedInUser(): ?User{
|
||||
$sessionId = HttpInput::Str(COOKIE, 'sessionid');
|
||||
|
||||
if($sessionId !== null){
|
||||
$result = Db::Query('select u.* from Users u inner join Sessions s using (UserId) where s.SessionId = ?', [$sessionId], 'User');
|
||||
|
||||
if(sizeof($result) > 0){
|
||||
// Refresh the login cookie for another 2 weeks
|
||||
setcookie('sessionid', $sessionId, time() + 60 * 60 * 24 * 14 * 1, '/', SITE_DOMAIN, true, false); // Expires in two weeks
|
||||
return $result[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function Get(?string $sessionId): Session{
|
||||
if($sessionId === null){
|
||||
throw new Exceptions\InvalidSessionException();
|
||||
}
|
||||
|
||||
$result = Db::Query('SELECT * from Sessions where SessionId = ?', [$sessionId], 'Session');
|
||||
|
||||
if(sizeof($result) == 0){
|
||||
throw new Exceptions\InvalidSessionException();
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
}
|
||||
}
|
|
@ -39,4 +39,19 @@ class Template{
|
|||
include(WEB_ROOT . '/404.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
public static function RedirectToLogin(bool $redirectToDestination = true, string $destinationUrl = null): void{
|
||||
if($redirectToDestination){
|
||||
if($destinationUrl === null){
|
||||
$destinationUrl = $_SERVER['SCRIPT_URL'];
|
||||
}
|
||||
|
||||
header('Location: /sessions/new?redirect=' . urlencode($destinationUrl));
|
||||
}
|
||||
else{
|
||||
header('Location: /sessions/new');
|
||||
}
|
||||
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
|
41
lib/User.php
41
lib/User.php
|
@ -4,6 +4,7 @@ use Safe\DateTime;
|
|||
|
||||
/**
|
||||
* @property Array<Payment> $Payments
|
||||
* @property Benefits $Benefits
|
||||
*/
|
||||
class User extends PropertiesBase{
|
||||
public $UserId;
|
||||
|
@ -11,7 +12,9 @@ class User extends PropertiesBase{
|
|||
public $Email;
|
||||
public $Created;
|
||||
public $Uuid;
|
||||
protected $_IsRegistered = null;
|
||||
protected $_Payments = null;
|
||||
protected $_Benefits = null;
|
||||
|
||||
|
||||
// *******
|
||||
|
@ -29,6 +32,33 @@ class User extends PropertiesBase{
|
|||
return $this->_Payments;
|
||||
}
|
||||
|
||||
protected function GetBenefits(): Benefits{
|
||||
if($this->_Benefits === null){
|
||||
$result = Db::Query('select * from Benefits where UserId = ?', [$this->UserId], 'Benefits');
|
||||
|
||||
if(sizeof($result) == 0){
|
||||
$this->_Benefits = new Benefits();
|
||||
$this->_IsRegistered = false;
|
||||
}
|
||||
else{
|
||||
$this->_Benefits = $result[0];
|
||||
$this->_IsRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->_Benefits;
|
||||
}
|
||||
|
||||
protected function GetIsRegistered(): bool{
|
||||
if($this->_IsRegistered === null){
|
||||
// A user is "registered" if they have a benefits entry in the table.
|
||||
// This function will fill it out for us.
|
||||
$this->GetBenefits();
|
||||
}
|
||||
|
||||
return $this->_IsRegistered;
|
||||
}
|
||||
|
||||
|
||||
// *******
|
||||
// METHODS
|
||||
|
@ -84,15 +114,14 @@ class User extends PropertiesBase{
|
|||
return $result[0];
|
||||
}
|
||||
|
||||
// Get a user if by either email or uuid, ONLY IF they're either a patron or have a valid API key.
|
||||
public static function GetByPatronIdentifier(?string $identifier): User{
|
||||
if($identifier === null){
|
||||
public static function GetIfRegistered(?string $email): User{
|
||||
// We consider a user "registered" if they have a row in the Benefits table.
|
||||
// Emails without that row may only be signed up for the newsletter and thus are not "registered" users
|
||||
if($email === null){
|
||||
throw new Exceptions\InvalidUserException();
|
||||
}
|
||||
|
||||
$result = Db::Query('SELECT u.* from Patrons p inner join Users u using (UserId) where p.Ended is null and (u.Email = ? or u.Uuid = ?)
|
||||
union
|
||||
select u.* from ApiKeys fu inner join Users u using (UserId) where fu.Ended is null and (u.Email = ? or u.Uuid = ?)', [$identifier, $identifier, $identifier, $identifier], 'User');
|
||||
$result = Db::Query('SELECT u.* from Users u inner join Benefits using (UserId) where u.Email = ?', [$email], 'User');
|
||||
|
||||
if(sizeof($result) == 0){
|
||||
throw new Exceptions\InvalidUserException();
|
||||
|
|
|
@ -47,6 +47,7 @@ if(sizeof($expiredPatrons) > 0){
|
|||
|
||||
foreach($expiredPatrons as $patron){
|
||||
Db::Query('update Patrons set Ended = ? where UserId = ?', [$now, $patron->UserId]);
|
||||
Db::Query('update Benefits set CanAccessFeeds = false, CanVote = false, CanBulkDownload = false where UserId = ?', [$patron->UserId]);
|
||||
|
||||
// Email the patron to notify them their term has ended
|
||||
// Is the patron a recurring subscriber?
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<td class="number"><?= Formatter::ToPlainText($collection->UpdatedString) ?></td>
|
||||
|
||||
<? foreach($collection->ZipFiles as $item){ ?>
|
||||
<td class="download"><a href="<?= $item->Url ?>" download=""><?= $item->Type ?></a></td>
|
||||
<td class="download"><a href="<?= $item->Url ?>"><?= $item->Type ?></a></td>
|
||||
<td>(<?= Formatter::ToPlainText($item->Size) ?>)</td>
|
||||
<? } ?>
|
||||
</tr>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<section id="accessing-the-feeds">
|
||||
<h2>Accessing the feeds</h2>
|
||||
<p>Our New Releases feeds are accessible by the public. Access to our other, more detailed feeds is available to our <a href="/about#patrons-circle">Patrons Circle supporters</a> and our <a href="/about#corporate-sponsors">corporate sponsors</a>.</p>
|
||||
<p><i>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to access a feed.</i></p>
|
||||
<section id="individuals">
|
||||
<h3>Access for individuals</h3>
|
||||
<ul>
|
||||
<li><p>Join the <a href="/donate#patrons-circle">Patrons Circle</a> by making a donation to get access to all of our ebook feeds for the duration of your gift.</p></li>
|
||||
<li><p><a href="/contribute">Produce an ebook</a> for Standard Ebooks to get lifetime access to our ebook feeds. If you’ve already done that, <a href="/about#editor-in-chief">contact us</a> to enable your access.</p></li>
|
||||
</ul>
|
||||
<p>To access a feed, when prompted enter your email address and leave the password field blank.</p>
|
||||
</section>
|
||||
<section id="organizations-and-projects">
|
||||
<h3>Access for organizations and projects</h3>
|
||||
|
|
|
@ -3,12 +3,10 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\apcu_fetch;
|
||||
|
||||
$forbiddenException = null;
|
||||
$canDownload = false;
|
||||
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||
// and this page was served as the 401 page.
|
||||
$forbiddenException = new Exceptions\InvalidPatronException();
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
$authors = [];
|
||||
|
@ -25,12 +23,10 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<main>
|
||||
<section class="bulk-downloads">
|
||||
<h1>Downloads by Author</h1>
|
||||
<? if($forbiddenException !== null){ ?>
|
||||
<?= Template::Error(['exception' => $forbiddenException]) ?>
|
||||
<? if(!$canDownload){ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<? } ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p>
|
||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to download these files.</p>
|
||||
<?= Template::BulkDownloadTable(['label' => 'Author', 'collections' => $authors]); ?>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -3,12 +3,10 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\apcu_fetch;
|
||||
|
||||
$forbiddenException = null;
|
||||
$canDownload = false;
|
||||
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||
// and this page was served as the 401 page.
|
||||
$forbiddenException = new Exceptions\InvalidPatronException();
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
$collections = [];
|
||||
|
@ -25,12 +23,10 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<main>
|
||||
<section class="bulk-downloads">
|
||||
<h1>Downloads by Collection</h1>
|
||||
<? if($forbiddenException !== null){ ?>
|
||||
<?= Template::Error(['exception' => $forbiddenException]) ?>
|
||||
<? if(!$canDownload){ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<? } ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p>
|
||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to download these files.</p>
|
||||
<?= Template::BulkDownloadTable(['label' => 'Collection', 'collections' => $collections]); ?>
|
||||
</section>
|
||||
</main>
|
||||
|
|
65
www/bulk-downloads/download.php
Normal file
65
www/bulk-downloads/download.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?
|
||||
require_once('Core.php');
|
||||
|
||||
use function Safe\preg_match;
|
||||
|
||||
$path = HttpInput::Str(GET, 'path', false) ?? '';
|
||||
|
||||
try{
|
||||
$path = '/bulk-downloads/' . $path;
|
||||
|
||||
if(!is_file(WEB_ROOT . $path)){
|
||||
throw new Exceptions\InvalidFileException();
|
||||
}
|
||||
|
||||
if($GLOBALS['User'] === null){
|
||||
throw new Exceptions\LoginRequiredException();
|
||||
}
|
||||
|
||||
if(!preg_match('/\.zip$/ius', $path)){
|
||||
throw new Exceptions\InvalidPermissionsException();
|
||||
}
|
||||
|
||||
if(!$GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
throw new Exceptions\InvalidPermissionsException();
|
||||
}
|
||||
|
||||
// Everything OK, serve the file using Apache.
|
||||
// The xsendfile Apache module tells Apache to serve the file, including not-modified or etag headers.
|
||||
// Much more efficien than reading it in PHP and outputting it that way.
|
||||
header('X-Sendfile: ' . WEB_ROOT . $path);
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="' . basename($path) . '"');
|
||||
exit();
|
||||
}
|
||||
catch(Exceptions\LoginRequiredException $ex){
|
||||
if(isset($_SERVER['HTTP_REFERER'])){
|
||||
Template::RedirectToLogin(true, $_SERVER['HTTP_REFERER']);
|
||||
}
|
||||
else{
|
||||
preg_match('|(^/bulk-downloads/[^/]+?)/|ius', $path, $matches);
|
||||
if(sizeof($matches) == 2){
|
||||
// If we arrived from the bulk-downloads page,
|
||||
// Make the login form redirect to the bulk download root, instead of refreshing directly into a download
|
||||
Template::RedirectToLogin(true, $matches[1]);
|
||||
}
|
||||
else{
|
||||
Template::RedirectToLogin();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exceptions\InvalidPermissionsException $ex){
|
||||
http_response_code(403);
|
||||
}
|
||||
catch(Exceptions\InvalidFileException $ex){
|
||||
Template::Emit404();
|
||||
}
|
||||
|
||||
?><?= Template::Header(['title' => 'Download ', 'highlight' => '', 'description' => 'Download zip files containing all of the Standard Ebooks released in a given month.']) ?>
|
||||
<main>
|
||||
<section class="narrow">
|
||||
<h1>Downloading ebook collections</h1>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
</section>
|
||||
</main>
|
||||
<?= Template::Footer() ?>
|
|
@ -7,12 +7,11 @@ $collection = null;
|
|||
$collectionUrlName = HttpInput::Str(GET, 'collection', false);
|
||||
$collection = null;
|
||||
$authorUrlName = HttpInput::Str(GET, 'author', false);
|
||||
$exception = null;
|
||||
$user = null;
|
||||
$canDownload = false;
|
||||
|
||||
try{
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
$user = User::GetByPatronIdentifier($_SERVER['PHP_AUTH_USER']);
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
if($collectionUrlName !== null){
|
||||
|
@ -66,7 +65,7 @@ try{
|
|||
catch(Exceptions\InvalidUserException $ex){
|
||||
$exception = new Exceptions\InvalidPatronException();
|
||||
}
|
||||
catch(Exceptions\InvalidCollectionException $ex){
|
||||
catch(Exceptions\InvalidAuthorException $ex){
|
||||
Template::Emit404();
|
||||
}
|
||||
catch(Exceptions\InvalidCollectionException $ex){
|
||||
|
@ -77,13 +76,11 @@ catch(Exceptions\InvalidCollectionException $ex){
|
|||
<main>
|
||||
<section class="bulk-downloads">
|
||||
<h1>Download the <?= $collection->Label ?> Collection</h1>
|
||||
<?= Template::Error(['exception' => $exception]) ?>
|
||||
<? if($user === null){ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks in a collection. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to download this collection.</p>
|
||||
<? }else{ ?>
|
||||
<? if($canDownload){ ?>
|
||||
<p>Select the ebook format in which you’d like to download this collection.</p>
|
||||
<p>You can also read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which ebook format to download</a>.</p>
|
||||
<? }else{ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks in a collection. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<? } ?>
|
||||
<?= Template::BulkDownloadTable(['label' => 'Collection', 'collections' => [$collection]]); ?>
|
||||
</section>
|
||||
|
|
|
@ -3,12 +3,9 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\apcu_fetch;
|
||||
|
||||
$forbiddenException = null;
|
||||
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||
// and this page was served as the 401 page.
|
||||
$forbiddenException = new Exceptions\InvalidPatronException();
|
||||
$canDownload = false;
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
$years = [];
|
||||
|
@ -39,10 +36,7 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<source srcset="/images/the-shop-of-the-bookdealer@2x.jpg 2x, /images/the-shop-of-the-bookdealer.jpg 1x" type="image/jpg"/>
|
||||
<img src="/images/the-shop-of-the-bookdealer@2x.jpg" alt="A gentleman in regency-era dress buys books from a bookseller."/>
|
||||
</picture>
|
||||
<? if($forbiddenException !== null){ ?>
|
||||
<?= Template::Error(['exception' => $forbiddenException]) ?>
|
||||
<? } ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history.<? if(!$canDownload){ ?> You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.<? } ?></p>
|
||||
<ul>
|
||||
<li>
|
||||
<p><a href="/bulk-downloads/subjects">Downloads by subject</a></p>
|
||||
|
|
|
@ -3,12 +3,10 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\apcu_fetch;
|
||||
|
||||
$forbiddenException = null;
|
||||
$canDownload = false;
|
||||
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||
// and this page was served as the 401 page.
|
||||
$forbiddenException = new Exceptions\InvalidPatronException();
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
$years = [];
|
||||
|
@ -25,12 +23,10 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<main>
|
||||
<section class="bulk-downloads">
|
||||
<h1>Downloads by Month</h1>
|
||||
<? if($forbiddenException !== null){ ?>
|
||||
<?= Template::Error(['exception' => $forbiddenException]) ?>
|
||||
<? if(!$canDownload){ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<? } ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p>
|
||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to download these files.</p>
|
||||
<table class="download-list">
|
||||
<tbody>
|
||||
<? foreach($years as $year => $months){
|
||||
|
@ -55,7 +51,7 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<td class="number" headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-updated"><?= Formatter::ToPlainText($collection->UpdatedString) ?></td>
|
||||
|
||||
<? foreach($collection->ZipFiles as $item){ ?>
|
||||
<td headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-download" class="download"><a href="<?= $item->Url ?>" download=""><?= $item->Type ?></a></td>
|
||||
<td headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-download" class="download"><a href="<?= $item->Url ?>"><?= $item->Type ?></a></td>
|
||||
<td headers="<?= $yearHeader ?> <?= $monthHeader ?> <?= $yearHeader ?>-download">(<?= Formatter::ToPlainText($item->Size) ?>)</td>
|
||||
<? } ?>
|
||||
</tr>
|
||||
|
|
|
@ -3,12 +3,10 @@ require_once('Core.php');
|
|||
|
||||
use function Safe\apcu_fetch;
|
||||
|
||||
$forbiddenException = null;
|
||||
$canDownload = false;
|
||||
|
||||
if(isset($_SERVER['PHP_AUTH_USER'])){
|
||||
// We get here if the user entered an invalid HTTP Basic Auth username,
|
||||
// and this page was served as the 401 page.
|
||||
$forbiddenException = new Exceptions\InvalidPatronException();
|
||||
if($GLOBALS['User'] !== null && $GLOBALS['User']->Benefits->CanBulkDownload){
|
||||
$canDownload = true;
|
||||
}
|
||||
|
||||
$subjects = [];
|
||||
|
@ -25,12 +23,10 @@ catch(Safe\Exceptions\ApcuException $ex){
|
|||
<main>
|
||||
<section class="bulk-downloads">
|
||||
<h1>Downloads by Subject</h1>
|
||||
<? if($forbiddenException !== null){ ?>
|
||||
<?= Template::Error(['exception' => $forbiddenException]) ?>
|
||||
<? if(!$canDownload){ ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<? } ?>
|
||||
<p><a href="/about#patrons-circle">Patrons circle members</a> can download zip files containing all of the ebooks that were released in a given month of Standard Ebooks history. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<p>These zip files contain each ebook in every format we offer, and are updated once daily with the latest versions of each ebook. Read about <a href="/help/how-to-use-our-ebooks#which-file-to-download">which file format to download</a>.</p>
|
||||
<p>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to download these files.</p>
|
||||
<?= Template::BulkDownloadTable(['label' => 'Subject', 'collections' => $subjects]); ?>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -9,7 +9,7 @@ require_once('Core.php');
|
|||
<p>If you want to suggest a different book to produce, please carefully review <a href="/contribute/collections-policy">the kinds of work we do and don’t accept</a>.</p>
|
||||
<h2>Add a book to this list</h2>
|
||||
<p><a href="/donate#patrons-circle">Patrons Circle members</a> may submit ebooks for inclusion on this list.</p>
|
||||
<p>Patrons Circle members periodically vote on a selection from this list to pick one ebook for immediate production. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> to have a voice in the future of the Standard Ebooks catalog.</p>
|
||||
<p>Patrons Circle members <a href="/polls">periodically vote on a selection from this list</a> to pick one ebook for immediate production. You can <a href="/donate#patrons-circle">join the Patrons Circle</a> to have a voice in the future of the Standard Ebooks catalog.</p>
|
||||
<h2>For your first production</h2>
|
||||
<p>If nothing on the list below interests you, you can pitch us something else you’d like to work on.</p>
|
||||
<p>First productions should be on the shorter side (less than 100,000 words maximum) and without too many complex formatting issues like illustrations, significant endnotes, letters, poems, etc. Most short plain fiction novels fall in this category.</p>
|
||||
|
|
|
@ -1891,6 +1891,20 @@ form[action="/ebooks"] button{
|
|||
margin-top: 1.4rem;
|
||||
}
|
||||
|
||||
form.single-row{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form.single-row label{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form.single-row button{
|
||||
align-self: flex-end;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
article nav ol li:not(:first-child):not(:last-child):not(.highlighted),
|
||||
main.ebooks nav ol li:not(:first-child):not(:last-child):not(.highlighted){
|
||||
display: none;
|
||||
|
|
BIN
www/images/county-election.avif
Normal file
BIN
www/images/county-election.avif
Normal file
Binary file not shown.
BIN
www/images/county-election.jpg
Normal file
BIN
www/images/county-election.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
www/images/county-election@2x.avif
Normal file
BIN
www/images/county-election@2x.avif
Normal file
Binary file not shown.
BIN
www/images/county-election@2x.jpg
Normal file
BIN
www/images/county-election@2x.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
|
@ -39,7 +39,7 @@ if($exception){
|
|||
<label class="captcha">
|
||||
Type the letters in the <abbr class="acronym">CAPTCHA</abbr> image
|
||||
<div>
|
||||
<input type="text" name="captcha" required="required" />
|
||||
<input type="text" name="captcha" required="required" autocomplete="off" />
|
||||
<img src="/images/captcha" alt="A visual CAPTCHA." height="<?= CAPTCHA_IMAGE_HEIGHT ?>" width="<?= CAPTCHA_IMAGE_WIDTH ?>" />
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
@ -40,7 +40,7 @@ try{
|
|||
$subscription->IsSubscribedToNewsletter = HttpInput::Bool(POST, 'issubscribedtonewsletter', false);
|
||||
$subscription->IsSubscribedToSummary = HttpInput::Bool(POST, 'issubscribedtosummary', false);
|
||||
|
||||
$captcha = $_SESSION['captcha'] ?? '';
|
||||
$captcha = HttpInput::Str(SESSION, 'captcha', false) ?? '';
|
||||
|
||||
$exception = new Exceptions\ValidationException();
|
||||
|
||||
|
|
|
@ -4,9 +4,21 @@ require_once('Core.php');
|
|||
use Safe\DateTime;
|
||||
|
||||
$poll = new Poll();
|
||||
$canVote = true; // Allow non-logged-in users to see the 'vote' button
|
||||
|
||||
try{
|
||||
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
|
||||
|
||||
if(isset($GLOBALS['User'])){
|
||||
$canVote = false; // User is logged in, hide the vote button unless they haven't voted yet
|
||||
try{
|
||||
PollVote::Get($poll->UrlName, $GLOBALS['User']->UserId);
|
||||
}
|
||||
catch(Exceptions\SeException $ex){
|
||||
// User has already voted
|
||||
$canVote = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exceptions\SeException $ex){
|
||||
Template::Emit404();
|
||||
|
@ -21,9 +33,13 @@ catch(Exceptions\SeException $ex){
|
|||
<? if($poll->End !== null){ ?>
|
||||
<p class="center-notice">This poll closes on <?= $poll->End->format('F j, Y g:i A') ?>.</p>
|
||||
<? } ?>
|
||||
<p><i>If you’re a Patrons Circle member, when prompted enter your email address and leave the password field blank to vote.</i></p>
|
||||
<? if(!$canVote){ ?>
|
||||
<p class="center-notice">You’ve already voted in this poll.</p>
|
||||
<? } ?>
|
||||
<p class="button-row narrow">
|
||||
<? if($canVote){ ?>
|
||||
<a href="<?= $poll->Url ?>/votes/new" class="button">Vote now</a>
|
||||
<? } ?>
|
||||
<a href="<?= $poll->Url ?>/votes" class="button">View results</a>
|
||||
</p>
|
||||
<? }else{ ?>
|
||||
|
|
|
@ -7,8 +7,16 @@ $openPolls = Db::Query('select * from Polls where (End is null or utc_timestamp(
|
|||
|
||||
?><?= Template::Header(['title' => 'Polls', 'highlight' => '', 'description' => 'The various polls active at Standard Ebooks.']) ?>
|
||||
<main>
|
||||
<section class="narrow">
|
||||
<h1>Polls</h1>
|
||||
<section class="narrow has-hero">
|
||||
<hgroup>
|
||||
<h1>Vote in Our Polls</h1>
|
||||
<h2>and decide the direction of the Standard Ebooks catalog</h2>
|
||||
</hgroup>
|
||||
<picture>
|
||||
<source srcset="/images/county-election@2x.avif 2x, /images/county-election.avif 1x" type="image/avif"/>
|
||||
<source srcset="/images/county-election@2x.jpg 2x, /images/county-election.jpg 1x" type="image/jpg"/>
|
||||
<img src="/images/county-election@2x.jpg" alt="Voters step up to cast votes in a county poll."/>
|
||||
</picture>
|
||||
<p>Periodically members of the <a href="/about#patrons-circle">Standard Ebooks Patrons Circle</a> vote on the next ebook from the <a href="/contribute/wanted-ebooks">Wanted Ebook List</a> to enter immediate production.</p>
|
||||
<p>Anyone can <a href="/donate#patrons-circle">join the Patrons Circle</a> by making a small donation in support of our mission of producing beautiful digital literature, for free distribution.</p>
|
||||
<? if(sizeof($openPolls) > 0){ ?>
|
||||
|
|
|
@ -6,11 +6,13 @@ use function Safe\session_unset;
|
|||
session_start();
|
||||
|
||||
$vote = new PollVote();
|
||||
$created = false;
|
||||
|
||||
try{
|
||||
$vote = PollVote::Get(HttpInput::Str(GET, 'pollurlname'), HttpInput::Int(GET, 'userid'));
|
||||
|
||||
if(isset($_SESSION['vote-created']) && $_SESSION['vote-created'] == $vote->UserId){
|
||||
$created = true;
|
||||
http_response_code(201);
|
||||
session_unset();
|
||||
}
|
||||
|
@ -22,8 +24,13 @@ catch(Exceptions\SeException $ex){
|
|||
?><?= Template::Header(['title' => 'Thank you for voting!', 'highlight' => '', 'description' => 'Thank you for voting in a Standard Ebooks poll!']) ?>
|
||||
<main>
|
||||
<section class="narrow">
|
||||
<h1>Thank you for voting!</h1>
|
||||
<p class="center-notice">Your vote in the <a href="<?= $vote->PollItem->Poll->Url ?>"><?= Formatter::ToPlainText($vote->PollItem->Poll->Name) ?> poll</a> has been recorded.</p>
|
||||
<h1>Your vote has been recorded!</h1>
|
||||
<? if($created){ ?>
|
||||
<p class="center-notice">Thank you for voting in the <a href="<?= $vote->PollItem->Poll->Url ?>"><?= Formatter::ToPlainText($vote->PollItem->Poll->Name) ?> poll</a>.</p>
|
||||
<? }else{ ?>
|
||||
<p class="center-notice">Your vote in the <a href="<?= $vote->PollItem->Poll->Url ?>"><?= Formatter::ToPlainText($vote->PollItem->Poll->Name) ?> poll</a> was submitted on <?= $vote->Created->format('F j, Y g:i A') ?>.</p>
|
||||
<? } ?>
|
||||
|
||||
<p class="button-row narrow"><a class="button" href="<?= $vote->PollItem->Poll->Url ?>/votes"> view results</a></p>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -24,19 +24,20 @@ catch(Exceptions\SeException $ex){
|
|||
<? } ?>
|
||||
<table class="votes">
|
||||
<tbody>
|
||||
<? foreach($poll->PollItemsByWinner as $pollItem){ ?>
|
||||
<tr>
|
||||
<td><?= Formatter::ToPlainText($pollItem->Name) ?></td>
|
||||
<td>
|
||||
<div class="meter">
|
||||
<div aria-hidden="true">
|
||||
<p><?= number_format($pollItem->VoteCount) ?></p>
|
||||
<? foreach($poll->PollItemsByWinner as $pollItem){ ?>
|
||||
<tr>
|
||||
<td><?= Formatter::ToPlainText($pollItem->Name) ?></td>
|
||||
<td>
|
||||
<div class="meter">
|
||||
<div aria-hidden="true">
|
||||
<p><?= number_format($pollItem->VoteCount) ?></p>
|
||||
</div>
|
||||
<? /* @max must be at least 1, otherwise 0/0 will appear as 100% */ ?>
|
||||
<meter min="0" max="<?= $poll->VoteCount ?: 1 ?>" value="<?= $pollItem->VoteCount ?>"></meter>
|
||||
</div>
|
||||
<meter min="0" max="<?= $poll->VoteCount ?>" value="<?= $pollItem->VoteCount ?>"></meter>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<? } ?>
|
||||
</td>
|
||||
</tr>
|
||||
<? } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
|
|
@ -5,41 +5,60 @@ use function Safe\session_unset;
|
|||
|
||||
session_start();
|
||||
|
||||
$poll = new Poll();
|
||||
$vote = new PollVote();
|
||||
|
||||
if(isset($_SESSION['vote'])){
|
||||
$vote = $_SESSION['vote'];
|
||||
}
|
||||
else{
|
||||
$vote->User = new User();
|
||||
$vote->User->Email = $_SERVER['PHP_AUTH_USER'] ?? null;
|
||||
}
|
||||
|
||||
$exception = $_SESSION['exception'] ?? null;
|
||||
|
||||
$poll = new Poll();
|
||||
|
||||
try{
|
||||
if($GLOBALS['User'] === null){
|
||||
throw new Exceptions\LoginRequiredException();
|
||||
}
|
||||
|
||||
if(isset($_SESSION['vote'])){
|
||||
$vote = $_SESSION['vote'];
|
||||
}
|
||||
else{
|
||||
$vote->User = $GLOBALS['User'];
|
||||
}
|
||||
|
||||
|
||||
$poll = Poll::GetByUrlName(HttpInput::Str(GET, 'pollurlname', false));
|
||||
|
||||
try{
|
||||
$vote = PollVote::Get($poll->UrlName, $GLOBALS['User']->UserId);
|
||||
throw new Exceptions\PollVoteExistsException($vote);
|
||||
}
|
||||
catch(Exceptions\InvalidPollVoteException $ex){
|
||||
// User hasn't voted yet, do nothing
|
||||
}
|
||||
|
||||
if($exception){
|
||||
http_response_code(400);
|
||||
session_unset();
|
||||
}
|
||||
}
|
||||
catch(Exceptions\SeException $ex){
|
||||
catch(Exceptions\LoginRequiredException $ex){
|
||||
Template::RedirectToLogin();
|
||||
}
|
||||
catch(Exceptions\InvalidPollException $ex){
|
||||
Template::Emit404();
|
||||
}
|
||||
catch(Exceptions\PollVoteExistsException $ex){
|
||||
$redirect = $poll->Url;
|
||||
if($ex->Vote !== null){
|
||||
$redirect = $ex->Vote->Url;
|
||||
}
|
||||
|
||||
if($exception){
|
||||
http_response_code(400);
|
||||
session_unset();
|
||||
header('Location: ' . $redirect);
|
||||
exit();
|
||||
}
|
||||
|
||||
?><?= Template::Header(['title' => $poll->Name . ' - Vote Now', 'highlight' => '', 'description' => 'Vote in the ' . $poll->Name . ' poll']) ?>
|
||||
<main>
|
||||
<section class="narrow">
|
||||
<h1>Vote in the <?= Formatter::ToPlainText($poll->Name) ?> Poll</h1>
|
||||
<?= Template::Error(['exception' => $exception]) ?>
|
||||
<form method="post" action="<?= Formatter::ToPlainText($poll->Url) ?>/votes">
|
||||
<label class="email">Your email address
|
||||
<input type="email" name="email" value="<? if($vote->User !== null){ ?><?= Formatter::ToPlainText($vote->User->Email) ?><? } ?>" maxlength="80" required="required" readonly="readonly" />
|
||||
</label>
|
||||
<input type="hidden" name="email" value="<? if($vote->User !== null){ ?><?= Formatter::ToPlainText($vote->User->Email) ?><? } ?>" maxlength="80" required="required" />
|
||||
<fieldset>
|
||||
<p>Select one of these options</p>
|
||||
<ul>
|
||||
|
|
|
@ -16,8 +16,6 @@ $requestType = HttpInput::RequestType();
|
|||
$vote = new PollVote();
|
||||
|
||||
try{
|
||||
$error = new Exceptions\ValidationException();
|
||||
|
||||
$vote->PollItemId = HttpInput::Int(POST, 'pollitemid');
|
||||
|
||||
$vote->Create(HttpInput::Str(POST, 'email', false));
|
||||
|
|
40
www/sessions/new.php
Normal file
40
www/sessions/new.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?
|
||||
require_once('Core.php');
|
||||
|
||||
use function Safe\session_unset;
|
||||
|
||||
session_start();
|
||||
|
||||
if($GLOBALS['User'] !== null){
|
||||
header('Location: /');
|
||||
exit();
|
||||
}
|
||||
|
||||
$email = HttpInput::Str(SESSION, 'email', false);
|
||||
$redirect = HttpInput::Str(SESSION, 'redirect', false) ?? HttpInput::Str(GET, 'redirect', false);
|
||||
|
||||
$exception = $_SESSION['exception'] ?? null;
|
||||
|
||||
http_response_code(401);
|
||||
|
||||
if($exception){
|
||||
http_response_code(400);
|
||||
session_unset();
|
||||
}
|
||||
?><?= Template::Header(['title' => 'Log In', 'highlight' => '', 'description' => 'Log in to your Standard Ebooks Patrons Circle account.']) ?>
|
||||
<main>
|
||||
<section class="narrow">
|
||||
<h1>Log in to the Patrons Circle</h1>
|
||||
<p>Enter your email address to log in to your Patrons Circle account. Once you’re logged in, you’ll be able to <a href="/polls">vote in our occasional polls</a>, access our <a href="/bulk-downloads">bulk ebook downloads</a>, and access our <a href="/feeds">ebook feeds</a>.</p>
|
||||
<p>Anyone can <a href="/donate#patrons-circle">join the Patrons Circle</a> with a small donation in support of our continuing mission to create free, beautiful digital literature.</p>
|
||||
<?= Template::Error(['exception' => $exception]) ?>
|
||||
<form method="post" action="/sessions" class="single-row">
|
||||
<input type="hidden" name="redirect" value="<?= Formatter::ToPlainText($redirect) ?>" />
|
||||
<label class="email">Your email address
|
||||
<input type="email" name="email" value="<?= Formatter::ToPlainText($email) ?>" maxlength="80" required="required" />
|
||||
</label>
|
||||
<button>Log in</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<?= Template::Footer() ?>
|
54
www/sessions/post.php
Normal file
54
www/sessions/post.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?
|
||||
require_once('Core.php');
|
||||
|
||||
use function Safe\preg_match;
|
||||
use function Safe\session_unset;
|
||||
|
||||
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||
http_response_code(405);
|
||||
exit();
|
||||
}
|
||||
|
||||
session_start();
|
||||
|
||||
$requestType = HttpInput::RequestType();
|
||||
|
||||
$session = new Session();
|
||||
$email = HttpInput::Str(POST, 'email', false);
|
||||
$redirect = HttpInput::Str(POST, 'redirect', false);
|
||||
|
||||
try{
|
||||
if($redirect === null){
|
||||
$redirect = '/';
|
||||
}
|
||||
|
||||
$session->Create($email);
|
||||
|
||||
setcookie('sessionid', $session->SessionId, time() + 60 * 60 * 24 * 14 * 1, '/', SITE_DOMAIN, true, false); // Expires in two weeks
|
||||
|
||||
if($requestType == WEB){
|
||||
http_response_code(303);
|
||||
header('Location: ' . $redirect);
|
||||
}
|
||||
else{
|
||||
// Access via REST api; 201 CREATED with location
|
||||
http_response_code(201);
|
||||
header('Location: ' . $session->Url);
|
||||
}
|
||||
}
|
||||
catch(Exceptions\SeException $ex){
|
||||
// Login failed
|
||||
if($requestType == WEB){
|
||||
$_SESSION['email'] = $email;
|
||||
$_SESSION['redirect'] = $redirect;
|
||||
$_SESSION['exception'] = $ex;
|
||||
|
||||
// Access via form; 303 redirect to the form, which will emit a 400 BAD REQUEST
|
||||
http_response_code(303);
|
||||
header('Location: /sessions/new');
|
||||
}
|
||||
else{
|
||||
// Access via REST api; 400 BAD REQUEST
|
||||
http_response_code(400);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue