mirror of
https://github.com/standardebooks/web.git
synced 2025-07-06 14:50:39 -04:00
Add cover art database
Co-authored-by: Job Curtis <job.curtis@gmail.com> Co-authored-by: Alex Cabal <alex@standardebooks.org>
This commit is contained in:
parent
74f747df76
commit
6a5c05511a
92 changed files with 3174 additions and 146 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,5 +12,6 @@ vendor/
|
||||||
*.log
|
*.log
|
||||||
www/manual/*
|
www/manual/*
|
||||||
!www/manual/index.php
|
!www/manual/index.php
|
||||||
|
config/apache/htpasswd-standardebooks.org
|
||||||
config/php/fpm/standardebooks.org-secrets.ini
|
config/php/fpm/standardebooks.org-secrets.ini
|
||||||
www/bulk-downloads/*/
|
www/bulk-downloads/*/
|
||||||
|
|
|
@ -11,11 +11,12 @@
|
||||||
"php": "8.1.2"
|
"php": "8.1.2"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"thecodingmachine/safe": "^1.3.3",
|
"thecodingmachine/safe": "^2.5.0",
|
||||||
"phpmailer/phpmailer": "^6.6.0",
|
"phpmailer/phpmailer": "^6.6.0",
|
||||||
"ramsey/uuid": "4.2.3",
|
"ramsey/uuid": "4.2.3",
|
||||||
"gregwar/captcha": "^1.2.0",
|
"gregwar/captcha": "^1.2.0",
|
||||||
"php-webdriver/webdriver": "^1.12.1",
|
"php-webdriver/webdriver": "^1.12.1",
|
||||||
"pear/http2": "^2.0.0"
|
"pear/http2": "^2.0.0",
|
||||||
|
"erusev/parsedown": "^1.7.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
239
composer.lock
generated
239
composer.lock
generated
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "218ea78082c83c5386eacaf3dc607eed",
|
"content-hash": "4ea5d2eba58ce5fbb4162f1bdd585d73",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
@ -67,17 +67,67 @@
|
||||||
"time": "2021-08-15T20:50:18+00:00"
|
"time": "2021-08-15T20:50:18+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "gregwar/captcha",
|
"name": "erusev/parsedown",
|
||||||
"version": "v1.2.0",
|
"version": "1.7.4",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Gregwar/Captcha.git",
|
"url": "https://github.com/erusev/parsedown.git",
|
||||||
"reference": "6e5b61b66ac89885b505153f4ef9a74ffa5b3074"
|
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/6e5b61b66ac89885b505153f4ef9a74ffa5b3074",
|
"url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||||
"reference": "6e5b61b66ac89885b505153f4ef9a74ffa5b3074",
|
"reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"php": ">=5.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^4.8.35"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Parsedown": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Emanuil Rusev",
|
||||||
|
"email": "hello@erusev.com",
|
||||||
|
"homepage": "http://erusev.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Parser for Markdown.",
|
||||||
|
"homepage": "http://parsedown.org",
|
||||||
|
"keywords": [
|
||||||
|
"markdown",
|
||||||
|
"parser"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/erusev/parsedown/issues",
|
||||||
|
"source": "https://github.com/erusev/parsedown/tree/1.7.x"
|
||||||
|
},
|
||||||
|
"time": "2019-12-30T22:54:17+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gregwar/captcha",
|
||||||
|
"version": "v1.2.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Gregwar/Captcha.git",
|
||||||
|
"reference": "229d3cdfe33d6f1349e0aec94a26e9205a6db08e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/229d3cdfe33d6f1349e0aec94a26e9205a6db08e",
|
||||||
|
"reference": "229d3cdfe33d6f1349e0aec94a26e9205a6db08e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -119,9 +169,9 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/Gregwar/Captcha/issues",
|
"issues": "https://github.com/Gregwar/Captcha/issues",
|
||||||
"source": "https://github.com/Gregwar/Captcha/tree/v1.2.0"
|
"source": "https://github.com/Gregwar/Captcha/tree/v1.2.1"
|
||||||
},
|
},
|
||||||
"time": "2023-03-24T22:12:41+00:00"
|
"time": "2023-09-26T13:45:37+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pear/http2",
|
"name": "pear/http2",
|
||||||
|
@ -172,16 +222,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "php-webdriver/webdriver",
|
"name": "php-webdriver/webdriver",
|
||||||
"version": "1.14.0",
|
"version": "1.15.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/php-webdriver/php-webdriver.git",
|
"url": "https://github.com/php-webdriver/php-webdriver.git",
|
||||||
"reference": "3ea4f924afb43056bf9c630509e657d951608563"
|
"reference": "cd52d9342c5aa738c2e75a67e47a1b6df97154e8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/3ea4f924afb43056bf9c630509e657d951608563",
|
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/cd52d9342c5aa738c2e75a67e47a1b6df97154e8",
|
||||||
"reference": "3ea4f924afb43056bf9c630509e657d951608563",
|
"reference": "cd52d9342c5aa738c2e75a67e47a1b6df97154e8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -190,7 +240,7 @@
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"php": "^7.3 || ^8.0",
|
"php": "^7.3 || ^8.0",
|
||||||
"symfony/polyfill-mbstring": "^1.12",
|
"symfony/polyfill-mbstring": "^1.12",
|
||||||
"symfony/process": "^5.0 || ^6.0"
|
"symfony/process": "^5.0 || ^6.0 || ^7.0"
|
||||||
},
|
},
|
||||||
"replace": {
|
"replace": {
|
||||||
"facebook/webdriver": "*"
|
"facebook/webdriver": "*"
|
||||||
|
@ -232,22 +282,22 @@
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
|
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
|
||||||
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.14.0"
|
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.1"
|
||||||
},
|
},
|
||||||
"time": "2023-02-09T12:12:19+00:00"
|
"time": "2023-10-20T12:21:20+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpmailer/phpmailer",
|
"name": "phpmailer/phpmailer",
|
||||||
"version": "v6.8.0",
|
"version": "v6.9.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
"url": "https://github.com/PHPMailer/PHPMailer.git",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1"
|
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1",
|
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18",
|
||||||
"reference": "df16b615e371d81fb79e506277faea67a1be18f1",
|
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -257,16 +307,17 @@
|
||||||
"php": ">=5.5.0"
|
"php": ">=5.5.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||||
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||||
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||||
"phpcompatibility/php-compatibility": "^9.3.5",
|
"phpcompatibility/php-compatibility": "^9.3.5",
|
||||||
"roave/security-advisories": "dev-latest",
|
"roave/security-advisories": "dev-latest",
|
||||||
"squizlabs/php_codesniffer": "^3.7.1",
|
"squizlabs/php_codesniffer": "^3.7.2",
|
||||||
"yoast/phpunit-polyfills": "^1.0.4"
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
|
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||||
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||||
|
@ -306,7 +357,7 @@
|
||||||
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||||
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0"
|
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -314,7 +365,7 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-03-06T14:43:22+00:00"
|
"time": "2023-11-25T22:23:28+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ramsey/collection",
|
"name": "ramsey/collection",
|
||||||
|
@ -506,23 +557,23 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/finder",
|
"name": "symfony/finder",
|
||||||
"version": "v6.3.0",
|
"version": "v6.4.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/finder.git",
|
"url": "https://github.com/symfony/finder.git",
|
||||||
"reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2"
|
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/finder/zipball/d9b01ba073c44cef617c7907ce2419f8d00d75e2",
|
"url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce",
|
||||||
"reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2",
|
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1"
|
"php": ">=8.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/filesystem": "^6.0"
|
"symfony/filesystem": "^6.0|^7.0"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -550,7 +601,7 @@
|
||||||
"description": "Finds files and directories via an intuitive fluent interface",
|
"description": "Finds files and directories via an intuitive fluent interface",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/finder/tree/v6.3.0"
|
"source": "https://github.com/symfony/finder/tree/v6.4.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -566,20 +617,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-04-02T01:25:41+00:00"
|
"time": "2023-10-31T17:30:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.27.0",
|
"version": "v1.28.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-ctype.git",
|
"url": "https://github.com/symfony/polyfill-ctype.git",
|
||||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
|
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
|
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||||
"reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
|
"reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -594,7 +645,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.27-dev"
|
"dev-main": "1.28-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -632,7 +683,7 @@
|
||||||
"portable"
|
"portable"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
|
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -648,20 +699,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
"time": "2023-01-26T09:26:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-mbstring",
|
"name": "symfony/polyfill-mbstring",
|
||||||
"version": "v1.27.0",
|
"version": "v1.28.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
"url": "https://github.com/symfony/polyfill-mbstring.git",
|
||||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
|
"reference": "42292d99c55abe617799667f454222c54c60e229"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229",
|
||||||
"reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
|
"reference": "42292d99c55abe617799667f454222c54c60e229",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -676,7 +727,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.27-dev"
|
"dev-main": "1.28-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -715,7 +766,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
|
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -731,20 +782,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
"time": "2023-07-28T09:04:16+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php80",
|
"name": "symfony/polyfill-php80",
|
||||||
"version": "v1.27.0",
|
"version": "v1.28.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
|
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||||
"reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
|
"reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -753,7 +804,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.27-dev"
|
"dev-main": "1.28-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -798,7 +849,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
|
"source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -814,20 +865,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
"time": "2023-01-26T09:26:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-php81",
|
"name": "symfony/polyfill-php81",
|
||||||
"version": "v1.27.0",
|
"version": "v1.28.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||||
"reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a"
|
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a",
|
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b",
|
||||||
"reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a",
|
"reference": "7581cd600fa9fd681b797d00b02f068e2f13263b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -836,7 +887,7 @@
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "1.27-dev"
|
"dev-main": "1.28-dev"
|
||||||
},
|
},
|
||||||
"thanks": {
|
"thanks": {
|
||||||
"name": "symfony/polyfill",
|
"name": "symfony/polyfill",
|
||||||
|
@ -877,7 +928,7 @@
|
||||||
"shim"
|
"shim"
|
||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0"
|
"source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -893,20 +944,20 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2022-11-03T14:55:06+00:00"
|
"time": "2023-01-26T09:26:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/process",
|
"name": "symfony/process",
|
||||||
"version": "v6.3.0",
|
"version": "v6.4.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/symfony/process.git",
|
"url": "https://github.com/symfony/process.git",
|
||||||
"reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628"
|
"reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628",
|
"url": "https://api.github.com/repos/symfony/process/zipball/c4b1ef0bc80533d87a2e969806172f1c2a980241",
|
||||||
"reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628",
|
"reference": "c4b1ef0bc80533d87a2e969806172f1c2a980241",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -938,7 +989,7 @@
|
||||||
"description": "Executes commands in sub-processes",
|
"description": "Executes commands in sub-processes",
|
||||||
"homepage": "https://symfony.com",
|
"homepage": "https://symfony.com",
|
||||||
"support": {
|
"support": {
|
||||||
"source": "https://github.com/symfony/process/tree/v6.3.0"
|
"source": "https://github.com/symfony/process/tree/v6.4.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -954,43 +1005,50 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-05-19T08:06:44+00:00"
|
"time": "2023-12-22T16:42:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "thecodingmachine/safe",
|
"name": "thecodingmachine/safe",
|
||||||
"version": "v1.3.3",
|
"version": "v2.5.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/thecodingmachine/safe.git",
|
"url": "https://github.com/thecodingmachine/safe.git",
|
||||||
"reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc"
|
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
|
"url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
|
||||||
"reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
|
"reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.2"
|
"php": "^8.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^0.12",
|
"phpstan/phpstan": "^1.5",
|
||||||
|
"phpunit/phpunit": "^9.5",
|
||||||
"squizlabs/php_codesniffer": "^3.2",
|
"squizlabs/php_codesniffer": "^3.2",
|
||||||
"thecodingmachine/phpstan-strict-rules": "^0.12"
|
"thecodingmachine/phpstan-strict-rules": "^1.0"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-master": "0.1-dev"
|
"dev-master": "2.2.x-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"files": [
|
"files": [
|
||||||
"deprecated/apc.php",
|
"deprecated/apc.php",
|
||||||
|
"deprecated/array.php",
|
||||||
|
"deprecated/datetime.php",
|
||||||
"deprecated/libevent.php",
|
"deprecated/libevent.php",
|
||||||
|
"deprecated/misc.php",
|
||||||
|
"deprecated/password.php",
|
||||||
"deprecated/mssql.php",
|
"deprecated/mssql.php",
|
||||||
"deprecated/stats.php",
|
"deprecated/stats.php",
|
||||||
|
"deprecated/strings.php",
|
||||||
"lib/special_cases.php",
|
"lib/special_cases.php",
|
||||||
|
"deprecated/mysqli.php",
|
||||||
"generated/apache.php",
|
"generated/apache.php",
|
||||||
"generated/apcu.php",
|
"generated/apcu.php",
|
||||||
"generated/array.php",
|
"generated/array.php",
|
||||||
|
@ -1011,6 +1069,7 @@
|
||||||
"generated/fpm.php",
|
"generated/fpm.php",
|
||||||
"generated/ftp.php",
|
"generated/ftp.php",
|
||||||
"generated/funchand.php",
|
"generated/funchand.php",
|
||||||
|
"generated/gettext.php",
|
||||||
"generated/gmp.php",
|
"generated/gmp.php",
|
||||||
"generated/gnupg.php",
|
"generated/gnupg.php",
|
||||||
"generated/hash.php",
|
"generated/hash.php",
|
||||||
|
@ -1020,7 +1079,6 @@
|
||||||
"generated/image.php",
|
"generated/image.php",
|
||||||
"generated/imap.php",
|
"generated/imap.php",
|
||||||
"generated/info.php",
|
"generated/info.php",
|
||||||
"generated/ingres-ii.php",
|
|
||||||
"generated/inotify.php",
|
"generated/inotify.php",
|
||||||
"generated/json.php",
|
"generated/json.php",
|
||||||
"generated/ldap.php",
|
"generated/ldap.php",
|
||||||
|
@ -1029,20 +1087,14 @@
|
||||||
"generated/mailparse.php",
|
"generated/mailparse.php",
|
||||||
"generated/mbstring.php",
|
"generated/mbstring.php",
|
||||||
"generated/misc.php",
|
"generated/misc.php",
|
||||||
"generated/msql.php",
|
|
||||||
"generated/mysql.php",
|
"generated/mysql.php",
|
||||||
"generated/mysqli.php",
|
|
||||||
"generated/mysqlndMs.php",
|
|
||||||
"generated/mysqlndQc.php",
|
|
||||||
"generated/network.php",
|
"generated/network.php",
|
||||||
"generated/oci8.php",
|
"generated/oci8.php",
|
||||||
"generated/opcache.php",
|
"generated/opcache.php",
|
||||||
"generated/openssl.php",
|
"generated/openssl.php",
|
||||||
"generated/outcontrol.php",
|
"generated/outcontrol.php",
|
||||||
"generated/password.php",
|
|
||||||
"generated/pcntl.php",
|
"generated/pcntl.php",
|
||||||
"generated/pcre.php",
|
"generated/pcre.php",
|
||||||
"generated/pdf.php",
|
|
||||||
"generated/pgsql.php",
|
"generated/pgsql.php",
|
||||||
"generated/posix.php",
|
"generated/posix.php",
|
||||||
"generated/ps.php",
|
"generated/ps.php",
|
||||||
|
@ -1053,7 +1105,6 @@
|
||||||
"generated/sem.php",
|
"generated/sem.php",
|
||||||
"generated/session.php",
|
"generated/session.php",
|
||||||
"generated/shmop.php",
|
"generated/shmop.php",
|
||||||
"generated/simplexml.php",
|
|
||||||
"generated/sockets.php",
|
"generated/sockets.php",
|
||||||
"generated/sodium.php",
|
"generated/sodium.php",
|
||||||
"generated/solr.php",
|
"generated/solr.php",
|
||||||
|
@ -1076,13 +1127,13 @@
|
||||||
"generated/zip.php",
|
"generated/zip.php",
|
||||||
"generated/zlib.php"
|
"generated/zlib.php"
|
||||||
],
|
],
|
||||||
"psr-4": {
|
"classmap": [
|
||||||
"Safe\\": [
|
"lib/DateTime.php",
|
||||||
"lib/",
|
"lib/DateTimeImmutable.php",
|
||||||
"deprecated/",
|
"lib/Exceptions/",
|
||||||
"generated/"
|
"deprecated/Exceptions/",
|
||||||
|
"generated/Exceptions/"
|
||||||
]
|
]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
"license": [
|
"license": [
|
||||||
|
@ -1091,24 +1142,24 @@
|
||||||
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
"description": "PHP core functions that throw exceptions instead of returning FALSE on error",
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/thecodingmachine/safe/issues",
|
"issues": "https://github.com/thecodingmachine/safe/issues",
|
||||||
"source": "https://github.com/thecodingmachine/safe/tree/v1.3.3"
|
"source": "https://github.com/thecodingmachine/safe/tree/v2.5.0"
|
||||||
},
|
},
|
||||||
"time": "2020-10-28T17:51:34+00:00"
|
"time": "2023-04-05T11:54:14+00:00"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "1.10.21",
|
"version": "1.10.50",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpstan/phpstan.git",
|
"url": "https://github.com/phpstan/phpstan.git",
|
||||||
"reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5"
|
"reference": "06a98513ac72c03e8366b5a0cb00750b487032e4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/06a98513ac72c03e8366b5a0cb00750b487032e4",
|
||||||
"reference": "b2a30186be2e4d97dce754ae4e65eb0ec2f04eb5",
|
"reference": "06a98513ac72c03e8366b5a0cb00750b487032e4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -1157,7 +1208,7 @@
|
||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-06-21T20:07:58+00:00"
|
"time": "2023-12-13T10:59:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "thecodingmachine/phpstan-safe-rule",
|
"name": "thecodingmachine/phpstan-safe-rule",
|
||||||
|
|
|
@ -295,6 +295,15 @@ Define webroot /standardebooks.org/web
|
||||||
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
||||||
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?class=$1
|
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?class=$1
|
||||||
|
|
||||||
|
# Rewrite rules for cover art
|
||||||
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
|
||||||
|
RewriteRule ^/admin/artworks/([^/\.]+)$ /admin/artworks/get.php?artworkid=$1 [L]
|
||||||
|
|
||||||
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
|
||||||
|
RewriteRule ^/admin/artworks/([^/\.]+)$ /admin/artworks/post.php?artworkid=$1 [L]
|
||||||
|
|
||||||
|
RewriteRule ^/artworks/([^/\.]+)/([^/\.]+)$ /artworks/get.php?artist=$1&artwork=$2 [L]
|
||||||
|
|
||||||
# Specific config for /bulk-downloads
|
# Specific config for /bulk-downloads
|
||||||
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
||||||
# Both directives are required
|
# Both directives are required
|
||||||
|
|
|
@ -277,6 +277,15 @@ Define webroot /standardebooks.org/web
|
||||||
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
RewriteRule ^/bulk-downloads/(.+\.zip)$ /bulk-downloads/download.php?path=$1
|
||||||
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?class=$1
|
RewriteRule ^/bulk-downloads/([^/\.]+)$ /bulk-downloads/collection.php?class=$1
|
||||||
|
|
||||||
|
# Rewrite rules for cover art
|
||||||
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^get$/"
|
||||||
|
RewriteRule ^/admin/artworks/([^/\.]+)$ /admin/artworks/get.php?artworkid=$1 [L]
|
||||||
|
|
||||||
|
RewriteCond expr "tolower(%{REQUEST_METHOD}) =~ /^post$/"
|
||||||
|
RewriteRule ^/admin/artworks/([^/\.]+)$ /admin/artworks/post.php?artworkid=$1 [L]
|
||||||
|
|
||||||
|
RewriteRule ^/artworks/([^/\.]+)/([^/\.]+)$ /artworks/get.php?artist=$1&artwork=$2 [L]
|
||||||
|
|
||||||
# Specific config for /bulk-downloads
|
# Specific config for /bulk-downloads
|
||||||
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
<DirectoryMatch "${webroot}/www/bulk-downloads">
|
||||||
# Both directives are required
|
# Both directives are required
|
||||||
|
|
|
@ -10,6 +10,9 @@ allow_url_fopen = false
|
||||||
allow_url_include = false
|
allow_url_include = false
|
||||||
expose_php = Off
|
expose_php = Off
|
||||||
|
|
||||||
|
post_max_size = 64M
|
||||||
|
upload_max_filesize = 32M
|
||||||
|
|
||||||
[Date]
|
[Date]
|
||||||
date.timezone = Etc/UTC
|
date.timezone = Etc/UTC
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,9 @@ allow_url_fopen = false
|
||||||
allow_url_include = false
|
allow_url_include = false
|
||||||
expose_php = Off
|
expose_php = Off
|
||||||
|
|
||||||
|
post_max_size = 64M
|
||||||
|
upload_max_filesize = 32M
|
||||||
|
|
||||||
[Date]
|
[Date]
|
||||||
date.timezone = Etc/UTC
|
date.timezone = Etc/UTC
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,6 @@ parameters:
|
||||||
|
|
||||||
# Ignore errors caused by type hints that should be union types. Union types are not yet supported in PHP.
|
# 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 type 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:
|
level:
|
||||||
8
|
8
|
||||||
paths:
|
paths:
|
||||||
|
@ -28,3 +25,8 @@ parameters:
|
||||||
- DONATION_ALERT_ON
|
- DONATION_ALERT_ON
|
||||||
- DONATION_DRIVE_ON
|
- DONATION_DRIVE_ON
|
||||||
- DONATION_DRIVE_COUNTER_ON
|
- DONATION_DRIVE_COUNTER_ON
|
||||||
|
earlyTerminatingMethodCalls:
|
||||||
|
Template:
|
||||||
|
- Emit404
|
||||||
|
- Emit403
|
||||||
|
- RedirectToLogin
|
||||||
|
|
5
config/sql/se/ArtistAlternateSpellings.sql
Normal file
5
config/sql/se/ArtistAlternateSpellings.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE `ArtistAlternateSpellings` (
|
||||||
|
`ArtistId` int(10) unsigned NOT NULL,
|
||||||
|
`AlternateSpelling` varchar(255) NOT NULL,
|
||||||
|
UNIQUE KEY `idxUnique` (`ArtistId`,`AlternateSpelling`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
10
config/sql/se/Artists.sql
Normal file
10
config/sql/se/Artists.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE `Artists` (
|
||||||
|
`ArtistId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`Name` varchar(191) NOT NULL,
|
||||||
|
`UrlName` varchar(255) NOT NULL,
|
||||||
|
`DeathYear` smallint unsigned NULL,
|
||||||
|
`Created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`Updated` timestamp NOT NULL,
|
||||||
|
PRIMARY KEY (`ArtistId`),
|
||||||
|
UNIQUE KEY `idxUnique` (`UrlName`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
5
config/sql/se/ArtworkTags.sql
Normal file
5
config/sql/se/ArtworkTags.sql
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
CREATE TABLE `ArtworkTags` (
|
||||||
|
`ArtworkId` int(10) unsigned NOT NULL,
|
||||||
|
`TagId` int(10) unsigned NOT NULL,
|
||||||
|
UNIQUE KEY `idxUnique` (`ArtworkId`,`TagId`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
24
config/sql/se/Artworks.sql
Normal file
24
config/sql/se/Artworks.sql
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
CREATE TABLE `Artworks` (
|
||||||
|
`ArtworkId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`ArtistId` int(10) unsigned NOT NULL,
|
||||||
|
`Name` varchar(255) NOT NULL,
|
||||||
|
`UrlName` varchar(255) NOT NULL,
|
||||||
|
`CompletedYear` smallint unsigned NULL,
|
||||||
|
`CompletedYearIsCirca` boolean NOT NULL DEFAULT FALSE,
|
||||||
|
`Created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`Updated` timestamp NOT NULL,
|
||||||
|
`Status` enum('unverified', 'approved', 'declined', 'in_use') DEFAULT 'unverified',
|
||||||
|
`ReviewerUserId` int(10) unsigned NULL,
|
||||||
|
`MuseumUrl` varchar(255) NULL,
|
||||||
|
`PublicationYear` smallint unsigned NULL,
|
||||||
|
`PublicationYearPageUrl` varchar(255) NULL,
|
||||||
|
`CopyrightPageUrl` varchar(255) NULL,
|
||||||
|
`ArtworkPageUrl` varchar(255) NULL,
|
||||||
|
`IsPublishedInUs` tinyint(1) NOT NULL DEFAULT FALSE,
|
||||||
|
`EbookWwwFilesystemPath` varchar(255) NULL,
|
||||||
|
`MimeType` varchar(64) NOT NULL,
|
||||||
|
`Exception` TEXT NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`ArtworkId`),
|
||||||
|
KEY `index1` (`Status`),
|
||||||
|
KEY `index2` (`UrlName`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
@ -3,6 +3,8 @@ CREATE TABLE `Benefits` (
|
||||||
`CanAccessFeeds` tinyint(1) unsigned NOT NULL,
|
`CanAccessFeeds` tinyint(1) unsigned NOT NULL,
|
||||||
`CanVote` tinyint(1) unsigned NOT NULL,
|
`CanVote` tinyint(1) unsigned NOT NULL,
|
||||||
`CanBulkDownload` tinyint(1) unsigned NOT NULL,
|
`CanBulkDownload` tinyint(1) unsigned NOT NULL,
|
||||||
|
`CanAddArtwork` tinyint(1) unsigned NOT NULL,
|
||||||
|
`CanReviewArtwork` tinyint(1) unsigned NOT NULL,
|
||||||
PRIMARY KEY (`UserId`),
|
PRIMARY KEY (`UserId`),
|
||||||
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
|
KEY `idxBenefits` (`CanAccessFeeds`,`CanVote`,`CanBulkDownload`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE TABLE `FeedUserAgents` (
|
CREATE TABLE `FeedUserAgents` (
|
||||||
`UserAgentId` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`UserAgentId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`UserAgent` text NOT NULL,
|
`UserAgent` text NOT NULL,
|
||||||
`Created` datetime NOT NULL,
|
`Created` datetime NOT NULL,
|
||||||
PRIMARY KEY (`UserAgentId`)
|
PRIMARY KEY (`UserAgentId`)
|
||||||
|
|
6
config/sql/se/Museums.sql
Normal file
6
config/sql/se/Museums.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE `Museums` (
|
||||||
|
`MuseumId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`Name` varchar(255) NOT NULL,
|
||||||
|
`Domain` varchar(255) NOT NULL,
|
||||||
|
PRIMARY KEY (`MuseumId`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
@ -1,6 +1,6 @@
|
||||||
CREATE TABLE `PollVotes` (
|
CREATE TABLE `PollVotes` (
|
||||||
`UserId` int(11) unsigned NOT NULL,
|
`UserId` int(10) unsigned NOT NULL,
|
||||||
`PollItemId` int(11) unsigned NOT NULL,
|
`PollItemId` int(10) unsigned NOT NULL,
|
||||||
`Created` datetime NOT NULL,
|
`Created` datetime NOT NULL,
|
||||||
UNIQUE KEY `idxUnique` (`PollItemId`,`UserId`)
|
UNIQUE KEY `idxUnique` (`PollItemId`,`UserId`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
CREATE TABLE `Polls` (
|
CREATE TABLE `Polls` (
|
||||||
`PollId` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`PollId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`Created` datetime NOT NULL,
|
`Created` datetime NOT NULL,
|
||||||
`Name` varchar(255) NOT NULL,
|
`Name` varchar(255) NOT NULL,
|
||||||
`UrlName` varchar(255) NOT NULL,
|
`UrlName` varchar(255) NOT NULL,
|
||||||
|
|
6
config/sql/se/Tags.sql
Normal file
6
config/sql/se/Tags.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE `Tags` (
|
||||||
|
`TagId` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`Name` varchar(255) NOT NULL,
|
||||||
|
PRIMARY KEY (`TagId`),
|
||||||
|
UNIQUE KEY `idxUnique` (`Name`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
153
lib/Artist.php
Normal file
153
lib/Artist.php
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
<?
|
||||||
|
use Safe\DateTime;
|
||||||
|
use function Safe\date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $UrlName
|
||||||
|
* @property array<string> $AlternateSpellings
|
||||||
|
*/
|
||||||
|
class Artist extends PropertiesBase{
|
||||||
|
public $ArtistId;
|
||||||
|
public $Name;
|
||||||
|
public $DeathYear;
|
||||||
|
public $Created;
|
||||||
|
public $Updated;
|
||||||
|
protected $_UrlName;
|
||||||
|
protected $_AlternateSpellings;
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// GETTERS
|
||||||
|
// *******
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function GetUrlName(): string{
|
||||||
|
if($this->Name === null || $this->Name == ''){
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->_UrlName === null){
|
||||||
|
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_UrlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
protected function GetAlternateSpellings(): array{
|
||||||
|
if($this->_AlternateSpellings === null){
|
||||||
|
$this->_AlternateSpellings = [];
|
||||||
|
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from ArtistAlternateSpellings
|
||||||
|
where ArtistId = ?
|
||||||
|
', [$this->ArtistId], 'stdClass');
|
||||||
|
|
||||||
|
foreach($result as $row){
|
||||||
|
$this->_AlternateSpellings[] = $row->AlternateSpelling;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_AlternateSpellings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// METHODS
|
||||||
|
// *******
|
||||||
|
|
||||||
|
public function Validate(): void{
|
||||||
|
$error = new Exceptions\ValidationException();
|
||||||
|
|
||||||
|
if($this->Name === null || $this->Name == ''){
|
||||||
|
$error->Add(new Exceptions\ArtistNameRequiredException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Name !== null && strlen($this->Name) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Artist Name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->DeathYear !== null && ($this->DeathYear <= 0 || $this->DeathYear > intval(date('Y')))){
|
||||||
|
$error->Add(new Exceptions\InvalidDeathYearException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($error->HasExceptions){
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ***********
|
||||||
|
// ORM METHODS
|
||||||
|
// ***********
|
||||||
|
|
||||||
|
public static function Get(?int $artistId): Artist{
|
||||||
|
if($artistId === null){
|
||||||
|
throw new Exceptions\InvalidArtistException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Artists
|
||||||
|
where ArtistId = ?
|
||||||
|
', [$artistId], 'Artist');
|
||||||
|
|
||||||
|
if(sizeof($result) == 0){
|
||||||
|
throw new Exceptions\InvalidArtistException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Create(): void{
|
||||||
|
$this->Validate();
|
||||||
|
Db::Query('
|
||||||
|
INSERT into Artists (Name, UrlName, DeathYear)
|
||||||
|
values (?,
|
||||||
|
?,
|
||||||
|
?)
|
||||||
|
', [$this->Name, $this->UrlName, $this->DeathYear]);
|
||||||
|
|
||||||
|
$this->ArtistId = Db::GetLastInsertedId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\ValidationException
|
||||||
|
*/
|
||||||
|
public static function GetOrCreate(Artist $artist): Artist{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Artists
|
||||||
|
where UrlName = ?
|
||||||
|
', [$artist->UrlName], 'Artist');
|
||||||
|
|
||||||
|
if(isset($result[0])){
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$artist->Create();
|
||||||
|
return $artist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function FindMatch(string $artistName): ?Artist{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT a.*
|
||||||
|
from Artists a left join ArtistAlternateSpellings alt using (ArtistId)
|
||||||
|
where a.Name = ? or alt.AlternateSpelling = ?
|
||||||
|
order by a.DeathYear desc
|
||||||
|
limit 1;
|
||||||
|
', [$artistName, $artistName], 'Artist');
|
||||||
|
|
||||||
|
return $result[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Delete(): void{
|
||||||
|
Db::Query('
|
||||||
|
DELETE
|
||||||
|
from Artists
|
||||||
|
where ArtistId = ?
|
||||||
|
', [$this->ArtistId]);
|
||||||
|
}
|
||||||
|
}
|
594
lib/Artwork.php
Normal file
594
lib/Artwork.php
Normal file
|
@ -0,0 +1,594 @@
|
||||||
|
<?
|
||||||
|
use Safe\DateTime;
|
||||||
|
use function Safe\copy;
|
||||||
|
use function Safe\date;
|
||||||
|
use function Safe\exec;
|
||||||
|
use function Safe\filesize;
|
||||||
|
use function Safe\getimagesize;
|
||||||
|
use function Safe\ini_get;
|
||||||
|
use function Safe\preg_replace;
|
||||||
|
use function Safe\sprintf;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $UrlName
|
||||||
|
* @property string $Url
|
||||||
|
* @property array<ArtworkTag> $Tags
|
||||||
|
* @property Artist $Artist
|
||||||
|
* @property string $ImageUrl
|
||||||
|
* @property string $ThumbUrl
|
||||||
|
* @property string $Thumb2xUrl
|
||||||
|
* @property string $ImageSize
|
||||||
|
* @property Ebook $Ebook
|
||||||
|
* @property Museum $Museum
|
||||||
|
* @property ?ImageMimeType $MimeType
|
||||||
|
*/
|
||||||
|
class Artwork extends PropertiesBase{
|
||||||
|
public $Name;
|
||||||
|
public $ArtworkId;
|
||||||
|
public $ArtistId;
|
||||||
|
public $CompletedYear;
|
||||||
|
public $CompletedYearIsCirca;
|
||||||
|
public $Created;
|
||||||
|
public $Updated;
|
||||||
|
public $Status;
|
||||||
|
public $EbookWwwFilesystemPath;
|
||||||
|
public $ReviewerUserId;
|
||||||
|
public $MuseumUrl;
|
||||||
|
public $PublicationYear;
|
||||||
|
public $PublicationYearPageUrl;
|
||||||
|
public $CopyrightPageUrl;
|
||||||
|
public $ArtworkPageUrl;
|
||||||
|
public $IsPublishedInUs;
|
||||||
|
public $Exception;
|
||||||
|
protected $_UrlName;
|
||||||
|
protected $_Url;
|
||||||
|
protected $_AdminUrl;
|
||||||
|
protected $_Tags = null;
|
||||||
|
protected $_Artist = null;
|
||||||
|
protected $_ImageUrl = null;
|
||||||
|
protected $_ThumbUrl = null;
|
||||||
|
protected $_Thumb2xUrl = null;
|
||||||
|
protected $_ImageSize = null;
|
||||||
|
protected $_Ebook = null;
|
||||||
|
protected $_Museum = null;
|
||||||
|
protected ?ImageMimeType $_MimeType = null;
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// GETTERS
|
||||||
|
// *******
|
||||||
|
|
||||||
|
protected function GetUrlName(): string{
|
||||||
|
if($this->Name === null || $this->Name == ''){
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->_UrlName === null){
|
||||||
|
$this->_UrlName = Formatter::MakeUrlSafe($this->Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_UrlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function GetUrl(): string{
|
||||||
|
if($this->_Url === null){
|
||||||
|
$this->_Url = '/artworks/' . $this->Artist->UrlName . '/' . $this->UrlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function GetAdminUrl(): string{
|
||||||
|
if($this->_AdminUrl === null){
|
||||||
|
$this->_AdminUrl = '/admin/artworks/' . $this->ArtworkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_AdminUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<ArtworkTag>
|
||||||
|
*/
|
||||||
|
protected function GetTags(): array{
|
||||||
|
if($this->_Tags === null){
|
||||||
|
$this->_Tags = Db::Query('
|
||||||
|
SELECT t.*
|
||||||
|
from Tags t
|
||||||
|
inner join ArtworkTags at using (TagId)
|
||||||
|
where ArtworkId = ?
|
||||||
|
', [$this->ArtworkId], 'ArtworkTag');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetMuseum(): ?Museum{
|
||||||
|
if($this->_Museum === null){
|
||||||
|
$this->_Museum = Museum::GetByUrl($this->MuseumUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Museum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ImplodeTags(): string{
|
||||||
|
$tags = $this->Tags ?? [];
|
||||||
|
$tags = array_column($tags, 'Name');
|
||||||
|
return trim(implode(', ', $tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\InvalidArtworkException
|
||||||
|
*/
|
||||||
|
protected function GetImageUrl(): string{
|
||||||
|
if($this->_ImageUrl === null){
|
||||||
|
if($this->ArtworkId === null || $this->MimeType === null){
|
||||||
|
throw new Exceptions\InvalidArtworkException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_ImageUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . $this->MimeType->GetFileExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_ImageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\ArtworkNotFoundException
|
||||||
|
*/
|
||||||
|
protected function GetThumbUrl(): string{
|
||||||
|
if($this->_ThumbUrl === null){
|
||||||
|
if($this->ArtworkId === null){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_ThumbUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_ThumbUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function GetThumb2xUrl(): string{
|
||||||
|
if($this->_Thumb2xUrl === null){
|
||||||
|
if($this->ArtworkId === null){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_Thumb2xUrl = COVER_ART_UPLOAD_PATH . $this->ArtworkId . '-thumb@2x.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Thumb2xUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function GetImageSize(): string{
|
||||||
|
try{
|
||||||
|
$bytes = @filesize(WEB_ROOT . $this->ImageUrl);
|
||||||
|
$sizes = 'BKMGTP';
|
||||||
|
$factor = intval(floor((strlen((string)$bytes) - 1) / 3));
|
||||||
|
$sizeNumber = sprintf('%.1f', $bytes / pow(1024, $factor));
|
||||||
|
$sizeUnit = $sizes[$factor] ?? '';
|
||||||
|
$this->_ImageSize = $sizeNumber . $sizeUnit;
|
||||||
|
|
||||||
|
list($imageWidth, $imageHeight) = getimagesize(WEB_ROOT . $this->ImageUrl);
|
||||||
|
if($imageWidth && $imageHeight){
|
||||||
|
$this->_ImageSize .= ' (' . $imageWidth . ' × ' . $imageHeight . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception){
|
||||||
|
// Image doesn't exist
|
||||||
|
$this->_ImageSize = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_ImageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function GetEbook(): ?Ebook{
|
||||||
|
if($this->_Ebook === null){
|
||||||
|
$this->_Ebook = Library::GetEbook($this->EbookWwwFilesystemPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Ebook;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function SetMimeType(null|string|ImageMimeType $mimeType): void{
|
||||||
|
if(is_string($mimeType)){
|
||||||
|
$this->_MimeType = ImageMimeType::tryFrom($mimeType);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$this->_MimeType = $mimeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// METHODS
|
||||||
|
// *******
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $uploadedFile
|
||||||
|
* @throws \Exceptions\ValidationException
|
||||||
|
*/
|
||||||
|
protected function Validate(array &$uploadedFile = []): void{
|
||||||
|
$error = new Exceptions\ValidationException();
|
||||||
|
|
||||||
|
if($this->Artist === null){
|
||||||
|
$error->Add(new Exceptions\InvalidArtworkException());
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
$this->Artist->Validate();
|
||||||
|
}
|
||||||
|
catch(Exceptions\ValidationException $ex){
|
||||||
|
$error->Add($ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trim($this->Exception) == ''){
|
||||||
|
$this->Exception = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Name === null || $this->Name == ''){
|
||||||
|
$error->Add(new Exceptions\ArtworkNameRequiredException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Name !== null && strlen($this->Name) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Artwork Name'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->CompletedYear !== null && ($this->CompletedYear <= 0 || $this->CompletedYear > intval(date('Y')))){
|
||||||
|
$error->Add(new Exceptions\InvalidCompletedYearException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->CompletedYear === null && $this->CompletedYearIsCirca){
|
||||||
|
$this->CompletedYearIsCirca = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->PublicationYear !== null && ($this->PublicationYear <= 0 || $this->PublicationYear > intval(date('Y')))){
|
||||||
|
$error->Add(new Exceptions\InvalidPublicationYearException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Status !== null && !in_array($this->Status, [COVER_ARTWORK_STATUS_UNVERIFIED, COVER_ARTWORK_STATUS_APPROVED, COVER_ARTWORK_STATUS_DECLINED, COVER_ARTWORK_STATUS_IN_USE])){
|
||||||
|
$error->Add(new Exceptions\InvalidArtworkException('Invalid status.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Status === COVER_ARTWORK_STATUS_IN_USE && $this->EbookWwwFilesystemPath === null){
|
||||||
|
$error->Add(new Exceptions\MissingEbookException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Tags === null || count($this->_Tags) == 0){
|
||||||
|
// In-use artwork doesn't have user-provided tags.
|
||||||
|
if($this->Status !== COVER_ARTWORK_STATUS_IN_USE){
|
||||||
|
$error->Add(new Exceptions\TagsRequiredException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Tags !== null && count($this->_Tags) > COVER_ARTWORK_MAX_TAGS){
|
||||||
|
$error->Add(new Exceptions\TooManyTagsException());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($this->Tags as $tag){
|
||||||
|
if(strlen($tag->Name) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Artwork Tag: '. $tag->Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->MuseumUrl !== null && strlen($this->MuseumUrl) > 0 && filter_var($this->MuseumUrl, FILTER_VALIDATE_URL) === false){
|
||||||
|
$error->Add(new Exceptions\InvalidMuseumUrlException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->MuseumUrl !== null && strlen($this->MuseumUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Link to an approved museum page'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->PublicationYearPageUrl !== null && strlen($this->PublicationYearPageUrl) > 0 && filter_var($this->PublicationYearPageUrl, FILTER_VALIDATE_URL) === false){
|
||||||
|
$error->Add(new Exceptions\InvalidPublicationYearPageUrlException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->PublicationYearPageUrl !== null && strlen($this->PublicationYearPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Link to page with year of publication'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->CopyrightPageUrl !== null && strlen($this->CopyrightPageUrl) > 0 && filter_var($this->CopyrightPageUrl, FILTER_VALIDATE_URL) === false){
|
||||||
|
$error->Add(new Exceptions\InvalidCopyrightPageUrlException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->CopyrightPageUrl !== null && strlen($this->CopyrightPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Link to page with copyright details'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->ArtworkPageUrl !== null && strlen($this->ArtworkPageUrl) > 0 && filter_var($this->ArtworkPageUrl, FILTER_VALIDATE_URL) === false){
|
||||||
|
$error->Add(new Exceptions\InvalidArtworkPageUrlException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->ArtworkPageUrl !== null && strlen($this->ArtworkPageUrl) > COVER_ARTWORK_MAX_STRING_LENGTH){
|
||||||
|
$error->Add(new Exceptions\StringTooLongException('Link to page with artwork'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasMuseumProof = $this->MuseumUrl !== null && $this->MuseumUrl != '';
|
||||||
|
$hasBookProof = $this->PublicationYear !== null
|
||||||
|
&& ($this->PublicationYearPageUrl !== null && $this->PublicationYearPageUrl != '')
|
||||||
|
&& ($this->ArtworkPageUrl !== null && $this->ArtworkPageUrl != '');
|
||||||
|
|
||||||
|
if(!$hasMuseumProof && !$hasBookProof && $this->Exception === null){
|
||||||
|
// In-use artwork has its public domain status tracked elsewhere, e.g., on the mailing list.
|
||||||
|
if($this->Status !== COVER_ARTWORK_STATUS_IN_USE){
|
||||||
|
$error->Add(new Exceptions\MissingPdProofException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->MimeType === null){
|
||||||
|
$error->Add(new Exceptions\InvalidMimeTypeException());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing Artwork objects with the same URL but different Artwork IDs.
|
||||||
|
try{
|
||||||
|
$existingArtwork = Artwork::GetByUrl($this->Artist->UrlName, $this->UrlName);
|
||||||
|
if($existingArtwork->ArtworkId != $this->ArtworkId){
|
||||||
|
// Duplicate found, alert the user
|
||||||
|
$error->Add(new Exceptions\ArtworkAlreadyExistsException());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exceptions\ArtworkNotFoundException){
|
||||||
|
// No duplicates found, continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_writable(WEB_ROOT . COVER_ART_UPLOAD_PATH)){
|
||||||
|
$error->Add(new Exceptions\InvalidImageUploadException('Upload path not writable.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($uploadedFile) && $this->MimeType !== null){
|
||||||
|
$uploadError = $uploadedFile['error'];
|
||||||
|
if($uploadError > UPLOAD_ERR_OK){
|
||||||
|
// see https://www.php.net/manual/en/features.file-upload.errors.php
|
||||||
|
$message = match($uploadError){
|
||||||
|
UPLOAD_ERR_INI_SIZE => 'Image upload too large (maximum ' . ini_get('upload_max_filesize') . ').',
|
||||||
|
default => 'Image failed to upload (error code ' . $uploadError . ').',
|
||||||
|
};
|
||||||
|
$error->Add(new Exceptions\InvalidImageUploadException($message));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!is_uploaded_file($uploadedFile['tmp_name'])){
|
||||||
|
throw new Exceptions\InvalidImageUploadException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($error->HasExceptions){
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<ArtworkTag> */
|
||||||
|
public static function ParseTags(?string $tags): array{
|
||||||
|
if(!$tags) return [];
|
||||||
|
|
||||||
|
$tags = array_map('trim', explode(',', $tags));
|
||||||
|
$tags = array_values(array_filter($tags));
|
||||||
|
$tags = array_unique($tags);
|
||||||
|
|
||||||
|
return array_map(function ($str){
|
||||||
|
$tag = new ArtworkTag();
|
||||||
|
$tag->Name = $str;
|
||||||
|
return $tag;
|
||||||
|
}, $tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $uploadedFile
|
||||||
|
* @throws \Exceptions\ValidationException
|
||||||
|
* @throws \Exceptions\InvalidImageUploadException
|
||||||
|
*/
|
||||||
|
public function Create(array $uploadedFile): void{
|
||||||
|
$this->Validate($uploadedFile);
|
||||||
|
$this->Created = new DateTime();
|
||||||
|
|
||||||
|
// Can't assign directly to $this->Tags because it's hidden behind a getter
|
||||||
|
$tags = [];
|
||||||
|
foreach($this->Tags as $artworkTag){
|
||||||
|
$tags[] = ArtworkTag::GetOrCreate($artworkTag);
|
||||||
|
}
|
||||||
|
$this->Tags = $tags;
|
||||||
|
|
||||||
|
$this->Artist = Artist::GetOrCreate($this->Artist);
|
||||||
|
|
||||||
|
Db::Query('
|
||||||
|
INSERT into
|
||||||
|
Artworks (ArtistId, Name, UrlName, CompletedYear, CompletedYearIsCirca, Created, Status, ReviewerUserId, MuseumUrl,
|
||||||
|
PublicationYear, PublicationYearPageUrl, CopyrightPageUrl, ArtworkPageUrl, IsPublishedInUs,
|
||||||
|
EbookWwwFilesystemPath, MimeType, Exception)
|
||||||
|
values (?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?)
|
||||||
|
', [$this->Artist->ArtistId, $this->Name, $this->UrlName, $this->CompletedYear, $this->CompletedYearIsCirca,
|
||||||
|
$this->Created, $this->Status, $this->ReviewerUserId, $this->MuseumUrl, $this->PublicationYear, $this->PublicationYearPageUrl,
|
||||||
|
$this->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookWwwFilesystemPath, $this->MimeType->value ?? null, $this->Exception]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->ArtworkId = Db::GetLastInsertedId();
|
||||||
|
|
||||||
|
foreach($this->Tags as $tag){
|
||||||
|
Db::Query('
|
||||||
|
INSERT into ArtworkTags (ArtworkId, TagId)
|
||||||
|
values (?,
|
||||||
|
?)
|
||||||
|
', [$this->ArtworkId, $tag->TagId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the source image and clean up metadata
|
||||||
|
$imageUploadPath = $uploadedFile['tmp_name'];
|
||||||
|
exec('exiftool -quiet -overwrite-_original -all= ' . escapeshellarg($imageUploadPath));
|
||||||
|
copy($imageUploadPath, WEB_ROOT . $this->ImageUrl);
|
||||||
|
|
||||||
|
// Generate the thumbnails
|
||||||
|
try{
|
||||||
|
$image = new Image($imageUploadPath);
|
||||||
|
$image->Resize(WEB_ROOT . $this->ThumbUrl, COVER_THUMBNAIL_WIDTH, COVER_THUMBNAIL_HEIGHT);
|
||||||
|
$image->Resize(WEB_ROOT . $this->Thumb2xUrl, COVER_THUMBNAIL_WIDTH * 2, COVER_THUMBNAIL_HEIGHT * 2);
|
||||||
|
}
|
||||||
|
catch(\Safe\Exceptions\FilesystemException | \Safe\Exceptions\ImageException){
|
||||||
|
throw new Exceptions\InvalidImageUploadException('Failed to generate thumbnail.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\ValidationException
|
||||||
|
*/
|
||||||
|
public function Save(): void{
|
||||||
|
$this->Validate();
|
||||||
|
|
||||||
|
Db::Query('
|
||||||
|
UPDATE Artworks
|
||||||
|
set
|
||||||
|
ArtistId = ?,
|
||||||
|
Name = ?,
|
||||||
|
UrlName = ?,
|
||||||
|
CompletedYear = ?,
|
||||||
|
CompletedYearIsCirca = ?,
|
||||||
|
Created = ?,
|
||||||
|
Status = ?,
|
||||||
|
ReviewerUserId = ?,
|
||||||
|
MuseumUrl = ?,
|
||||||
|
PublicationYear = ?,
|
||||||
|
PublicationYearPageUrl = ?,
|
||||||
|
CopyrightPageUrl = ?,
|
||||||
|
ArtworkPageUrl = ?,
|
||||||
|
IsPublishedInUs = ?,
|
||||||
|
EbookWwwFilesystemPath = ?,
|
||||||
|
MimeType = ?,
|
||||||
|
Exception = ?
|
||||||
|
where
|
||||||
|
ArtworkId = ?
|
||||||
|
', [$this->Artist->ArtistId, $this->Name, $this->UrlName, $this->CompletedYear, $this->CompletedYearIsCirca,
|
||||||
|
$this->Created, $this->Status, $this->ReviewerUserId, $this->MuseumUrl, $this->PublicationYear, $this->PublicationYearPageUrl,
|
||||||
|
$this->CopyrightPageUrl, $this->ArtworkPageUrl, $this->IsPublishedInUs, $this->EbookWwwFilesystemPath, $this->MimeType->value ?? null, $this->Exception,
|
||||||
|
$this->ArtworkId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function MarkInUse(string $ebookWwwFilesystemPath): void{
|
||||||
|
$this->EbookWwwFilesystemPath = $ebookWwwFilesystemPath;
|
||||||
|
$this->Status = COVER_ARTWORK_STATUS_IN_USE;
|
||||||
|
$this->Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Delete(): void{
|
||||||
|
Db::Query('
|
||||||
|
DELETE
|
||||||
|
from ArtworkTags
|
||||||
|
where ArtworkId = ?
|
||||||
|
', [$this->ArtworkId]);
|
||||||
|
|
||||||
|
Db::Query('
|
||||||
|
DELETE
|
||||||
|
from Artworks
|
||||||
|
where ArtworkId = ?
|
||||||
|
', [$this->ArtworkId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Contains(string $query): bool{
|
||||||
|
$searchString = $this->Name;
|
||||||
|
|
||||||
|
$searchString .= ' ' . $this->Artist->Name;
|
||||||
|
$searchString .= ' ' . implode(' ', $this->Artist->AlternateSpellings);
|
||||||
|
|
||||||
|
foreach($this->Tags as $tag){
|
||||||
|
$searchString .= ' ' . $tag->Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)));
|
||||||
|
|
||||||
|
if($query == ''){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mb_stripos($searchString, $query) !== false){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function HasMatchingMuseum(): bool{
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ***********
|
||||||
|
// ORM METHODS
|
||||||
|
// ***********
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\ArtworkNotFoundException
|
||||||
|
*/
|
||||||
|
public static function Get(?int $artworkId): Artwork{
|
||||||
|
if($artworkId === null){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Artworks
|
||||||
|
where ArtworkId = ?
|
||||||
|
', [$artworkId], 'Artwork');
|
||||||
|
|
||||||
|
if(sizeof($result) == 0){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up an existing artwork regardless of status (unlike GetByUrl()) in order to
|
||||||
|
* enforce that the Artist UrlName + Artwork UrlName combo is globally unique.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\InvalidArtworkException
|
||||||
|
*/
|
||||||
|
public static function GetByUrl(string $artistUrlName, string $artworkUrlName): Artwork{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT Artworks.*
|
||||||
|
from Artworks
|
||||||
|
inner join Artists using (ArtistId)
|
||||||
|
where Artists.UrlName = ? and Artworks.UrlName = ?
|
||||||
|
', [$artistUrlName, $artworkUrlName], 'Artwork');
|
||||||
|
|
||||||
|
if(sizeof($result) == 0){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a publically available Artwork, i.e., with approved or in_use status.
|
||||||
|
* Artwork with status unverifed and declined aren't available by URL.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\InvalidArtworkException
|
||||||
|
*/
|
||||||
|
public static function GetByUrlAndIsApproved(string $artistUrlName, string $artworkUrlName): Artwork{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT Artworks.*
|
||||||
|
from Artworks
|
||||||
|
inner join Artists using (ArtistId)
|
||||||
|
where Status in ("approved", "in_use") and
|
||||||
|
Artists.UrlName = ? and Artworks.UrlName = ?
|
||||||
|
', [$artistUrlName, $artworkUrlName], 'Artwork');
|
||||||
|
|
||||||
|
if(sizeof($result) == 0){
|
||||||
|
throw new Exceptions\ArtworkNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
}
|
66
lib/ArtworkTag.php
Normal file
66
lib/ArtworkTag.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
class ArtworkTag extends Tag{
|
||||||
|
// *******
|
||||||
|
// GETTERS
|
||||||
|
// *******
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function GetUrl(): string{
|
||||||
|
if($this->_Url === null){
|
||||||
|
$this->_Url = '/artworks?query=' . Formatter::MakeUrlSafe($this->Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// METHODS
|
||||||
|
// *******
|
||||||
|
protected function Validate(): void{
|
||||||
|
$error = new Exceptions\ValidationException();
|
||||||
|
|
||||||
|
if($this->Name === null || strlen($this->Name) === 0){
|
||||||
|
$error->Add(new Exceptions\InvalidArtworkTagException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($this->Url === null || strlen($this->Url) === 0){
|
||||||
|
$error->Add(new Exceptions\InvalidArtworkTagException());
|
||||||
|
}
|
||||||
|
|
||||||
|
if($error->HasExceptions){
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Create(): void{
|
||||||
|
$this->Validate();
|
||||||
|
|
||||||
|
Db::Query('
|
||||||
|
INSERT into Tags (Name)
|
||||||
|
values (?)
|
||||||
|
', [$this->Name]);
|
||||||
|
$this->TagId = Db::GetLastInsertedId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Exceptions\ValidationException
|
||||||
|
*/
|
||||||
|
public static function GetOrCreate(ArtworkTag $artworkTag): ArtworkTag{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Tags
|
||||||
|
where Name = ?
|
||||||
|
', [$artworkTag->Name], 'ArtworkTag');
|
||||||
|
|
||||||
|
if(isset($result[0])){
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$artworkTag->Create();
|
||||||
|
return $artworkTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,4 +3,6 @@ class Benefits{
|
||||||
public $CanAccessFeeds = false;
|
public $CanAccessFeeds = false;
|
||||||
public $CanVote = false;
|
public $CanVote = false;
|
||||||
public $CanBulkDownload = false;
|
public $CanBulkDownload = false;
|
||||||
|
public $CanUploadArtwork = false;
|
||||||
|
public $CanReviewArtwork = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ const REPOS_PATH = SITE_ROOT . '/ebooks';
|
||||||
const TEMPLATES_PATH = SITE_ROOT . '/web/templates';
|
const TEMPLATES_PATH = SITE_ROOT . '/web/templates';
|
||||||
const MANUAL_PATH = WEB_ROOT . '/manual';
|
const MANUAL_PATH = WEB_ROOT . '/manual';
|
||||||
const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/';
|
const EBOOKS_DIST_PATH = WEB_ROOT . '/ebooks/';
|
||||||
|
const COVER_ART_UPLOAD_PATH = '/images/cover-uploads/';
|
||||||
|
|
||||||
const DATABASE_DEFAULT_DATABASE = 'se';
|
const DATABASE_DEFAULT_DATABASE = 'se';
|
||||||
const DATABASE_DEFAULT_HOST = 'localhost';
|
const DATABASE_DEFAULT_HOST = 'localhost';
|
||||||
|
@ -34,6 +35,19 @@ const SORT_AUTHOR_ALPHA = 'author-alpha';
|
||||||
const SORT_READING_EASE = 'reading-ease';
|
const SORT_READING_EASE = 'reading-ease';
|
||||||
const SORT_LENGTH = 'length';
|
const SORT_LENGTH = 'length';
|
||||||
|
|
||||||
|
const COVER_THUMBNAIL_HEIGHT = 350;
|
||||||
|
const COVER_THUMBNAIL_WIDTH = 350;
|
||||||
|
const COVER_ARTWORK_PER_PAGE = 50;
|
||||||
|
const COVER_ARTWORK_STATUS_APPROVED = 'approved';
|
||||||
|
const COVER_ARTWORK_STATUS_DECLINED = 'declined';
|
||||||
|
const COVER_ARTWORK_STATUS_IN_USE = 'in_use';
|
||||||
|
const COVER_ARTWORK_STATUS_UNVERIFIED = 'unverified';
|
||||||
|
const COVER_ARTWORK_MAX_STRING_LENGTH = 191;
|
||||||
|
const COVER_ARTWORK_MAX_TAGS = 100;
|
||||||
|
const SORT_COVER_ARTWORK_CREATED_NEWEST = 'created-newest';
|
||||||
|
const SORT_COVER_ARTIST_ALPHA = 'artist-alpha';
|
||||||
|
const SORT_COVER_ARTWORK_COMPLETED_NEWEST = 'completed-newest';
|
||||||
|
|
||||||
const CAPTCHA_IMAGE_HEIGHT = 72;
|
const CAPTCHA_IMAGE_HEIGHT = 72;
|
||||||
const CAPTCHA_IMAGE_WIDTH = 230;
|
const CAPTCHA_IMAGE_WIDTH = 230;
|
||||||
const NO_REPLY_EMAIL_ADDRESS = 'admin@standardebooks.org';
|
const NO_REPLY_EMAIL_ADDRESS = 'admin@standardebooks.org';
|
||||||
|
@ -92,6 +106,7 @@ const GITHUB_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-github.log'; // Mu
|
||||||
const POSTMARK_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-postmark.log'; // Must be writable by `www-data` Unix user.
|
const POSTMARK_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-postmark.log'; // Must be writable by `www-data` Unix user.
|
||||||
const ZOHO_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-zoho.log'; // Must be writable by `www-data` Unix user.
|
const ZOHO_WEBHOOK_LOG_FILE_PATH = '/var/log/local/webhooks-zoho.log'; // Must be writable by `www-data` Unix user.
|
||||||
const DONATIONS_LOG_FILE_PATH = '/var/log/local/donations.log'; // Must be writable by `www-data` Unix user.
|
const DONATIONS_LOG_FILE_PATH = '/var/log/local/donations.log'; // Must be writable by `www-data` Unix user.
|
||||||
|
const ARTWORK_UPLOADS_LOG_FILE_PATH = '/var/log/local/artwork-uploads.log'; // Must be writable by `www-data` Unix user.
|
||||||
|
|
||||||
define('PD_YEAR', intval(gmdate('Y')) - 96);
|
define('PD_YEAR', intval(gmdate('Y')) - 96);
|
||||||
define('PD_STRING', 'January 1, ' . (PD_YEAR + 1));
|
define('PD_STRING', 'January 1, ' . (PD_YEAR + 1));
|
||||||
|
@ -100,7 +115,7 @@ define('DONATION_HOLIDAY_ALERT_ON', time() > strtotime('November 15, ' . gmdate(
|
||||||
define('DONATION_ALERT_ON', DONATION_HOLIDAY_ALERT_ON || rand(1, 4) == 2);
|
define('DONATION_ALERT_ON', DONATION_HOLIDAY_ALERT_ON || rand(1, 4) == 2);
|
||||||
|
|
||||||
// Controls the progress bar donation dialog
|
// Controls the progress bar donation dialog
|
||||||
const DONATION_DRIVE_ON = false;
|
const DONATION_DRIVE_ON = true;
|
||||||
const DONATION_DRIVE_START = 'December 11, 2023 00:00:00 America/New_York';
|
const DONATION_DRIVE_START = 'December 11, 2023 00:00:00 America/New_York';
|
||||||
const DONATION_DRIVE_END = 'January 7, 2024 23:59:00 America/New_York';
|
const DONATION_DRIVE_END = 'January 7, 2024 23:59:00 America/New_York';
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// These functions are broken out of Core.php to satisfy PHPStan
|
// These functions are broken out of Core.php to satisfy PHPStan
|
||||||
|
|
||||||
use function Safe\ob_end_clean;
|
use function Safe\ob_end_clean;
|
||||||
|
use function Safe\ob_start;
|
||||||
|
|
||||||
// Convenience alias of var_dump.
|
// Convenience alias of var_dump.
|
||||||
function vd($var): void{
|
function vd($var): void{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?
|
<?
|
||||||
use Safe\DateTime;
|
use Safe\DateTime;
|
||||||
use function Safe\preg_match;
|
use function Safe\preg_match;
|
||||||
|
use function Safe\posix_getpwuid;
|
||||||
|
|
||||||
class DbConnection{
|
class DbConnection{
|
||||||
private $_link = null;
|
private $_link = null;
|
||||||
|
@ -126,7 +127,7 @@ class DbConnection{
|
||||||
$done = true;
|
$done = true;
|
||||||
}
|
}
|
||||||
catch(\PDOException $ex){
|
catch(\PDOException $ex){
|
||||||
if($ex->errorInfo[1] == 1213 && $deadlockRetries < 3){ // InnoDB deadlock, this is normal and happens occasionally. All we have to do is retry the query.
|
if(isset($ex->errorInfo) && $ex->errorInfo[1] == 1213 && $deadlockRetries < 3){ // InnoDB deadlock, this is normal and happens occasionally. All we have to do is retry the query.
|
||||||
$deadlockRetries++;
|
$deadlockRetries++;
|
||||||
|
|
||||||
usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds
|
usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds
|
||||||
|
@ -188,12 +189,17 @@ class DbConnection{
|
||||||
$object = new $class();
|
$object = new $class();
|
||||||
|
|
||||||
for($i = 0; $i < $handle->columnCount(); $i++){
|
for($i = 0; $i < $handle->columnCount(); $i++){
|
||||||
|
if($metadata[$i] === false){
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if($row[$i] === null){
|
if($row[$i] === null){
|
||||||
$object->{$metadata[$i]['name']} = null;
|
$object->{$metadata[$i]['name']} = null;
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
switch($metadata[$i]['native_type']){
|
switch($metadata[$i]['native_type'] ?? null){
|
||||||
case 'DATETIME':
|
case 'DATETIME':
|
||||||
|
case 'TIMESTAMP':
|
||||||
$object->{$metadata[$i]['name']} = new DateTime($row[$i], new DateTimeZone('UTC'));
|
$object->{$metadata[$i]['name']} = new DateTime($row[$i], new DateTimeZone('UTC'));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -228,7 +234,7 @@ class DbConnection{
|
||||||
catch(\PDOException $ex){
|
catch(\PDOException $ex){
|
||||||
// HY000 is thrown when there is no result set, e.g. for an update operation.
|
// HY000 is thrown when there is no result set, e.g. for an update operation.
|
||||||
// If anything besides that is thrown, then send it up the stack
|
// If anything besides that is thrown, then send it up the stack
|
||||||
if($ex->errorInfo[0] != "HY000"){
|
if(!isset($ex->errorInfo) || $ex->errorInfo[0] != "HY000"){
|
||||||
throw $ex;
|
throw $ex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ use function Safe\glob;
|
||||||
use function Safe\preg_match;
|
use function Safe\preg_match;
|
||||||
use function Safe\preg_replace;
|
use function Safe\preg_replace;
|
||||||
use function Safe\sprintf;
|
use function Safe\sprintf;
|
||||||
|
use function Safe\shell_exec;
|
||||||
use function Safe\substr;
|
use function Safe\substr;
|
||||||
|
|
||||||
class Ebook{
|
class Ebook{
|
||||||
|
@ -77,8 +78,7 @@ class Ebook{
|
||||||
$this->RepoFilesystemPath = preg_replace('/\.git$/ius', '', $this->RepoFilesystemPath);
|
$this->RepoFilesystemPath = preg_replace('/\.git$/ius', '', $this->RepoFilesystemPath);
|
||||||
}
|
}
|
||||||
catch(Exception){
|
catch(Exception){
|
||||||
// We may get an exception from preg_replace if the passed repo wwwFilesystemPath contains invalid UTF8 characters,
|
// We may get an exception from preg_replace if the passed repo wwwFilesystemPath contains invalid UTF-8 characters, whichis a common injection attack vector
|
||||||
// which a common injection attack vector
|
|
||||||
throw new Exceptions\InvalidEbookException('Invalid repo filesystem path: ' . $this->RepoFilesystemPath);
|
throw new Exceptions\InvalidEbookException('Invalid repo filesystem path: ' . $this->RepoFilesystemPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ class Ebook{
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in the short history of this repo.
|
// Fill in the short history of this repo.
|
||||||
$historyEntries = explode("\n", shell_exec('cd ' . escapeshellarg($this->RepoFilesystemPath) . ' && git log -n5 --pretty=format:"%ct %H %s"') ?? '');
|
$historyEntries = explode("\n", shell_exec('cd ' . escapeshellarg($this->RepoFilesystemPath) . ' && git log -n5 --pretty=format:"%ct %H %s"'));
|
||||||
|
|
||||||
foreach($historyEntries as $entry){
|
foreach($historyEntries as $entry){
|
||||||
$array = explode(' ', $entry, 3);
|
$array = explode(' ', $entry, 3);
|
||||||
|
@ -221,7 +221,7 @@ class Ebook{
|
||||||
|
|
||||||
// Get SE tags
|
// Get SE tags
|
||||||
foreach($xml->xpath('/package/metadata/meta[@property="se:subject"]') ?: [] as $tag){
|
foreach($xml->xpath('/package/metadata/meta[@property="se:subject"]') ?: [] as $tag){
|
||||||
$this->Tags[] = new Tag($tag);
|
$this->Tags[] = new EbookTag($tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
$includeToc = sizeof($xml->xpath('/package/metadata/meta[@property="se:is-a-collection"]') ?: []) > 0;
|
$includeToc = sizeof($xml->xpath('/package/metadata/meta[@property="se:is-a-collection"]') ?: []) > 0;
|
||||||
|
@ -742,6 +742,10 @@ class Ebook{
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<SimpleXMLElement>|false|null $elements
|
||||||
|
*/
|
||||||
private function NullIfEmpty($elements): ?string{
|
private function NullIfEmpty($elements): ?string{
|
||||||
if($elements === false){
|
if($elements === false){
|
||||||
return null;
|
return null;
|
||||||
|
|
21
lib/EbookTag.php
Normal file
21
lib/EbookTag.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?
|
||||||
|
class EbookTag extends Tag{
|
||||||
|
public function __construct(string $name){
|
||||||
|
$this->Name = $name;
|
||||||
|
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
|
||||||
|
$this->_Url = '/subjects/' . $this->UrlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// *******
|
||||||
|
// GETTERS
|
||||||
|
// *******
|
||||||
|
|
||||||
|
protected function GetUrl(): string{
|
||||||
|
if($this->_Url === null){
|
||||||
|
$this->_Url = '/subjects/' . $this->UrlName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_Url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ class Email{
|
||||||
public $Subject = '';
|
public $Subject = '';
|
||||||
public $Body = '';
|
public $Body = '';
|
||||||
public $TextBody = '';
|
public $TextBody = '';
|
||||||
public $Attachments = array();
|
public $Attachments = [];
|
||||||
public $PostmarkStream = null;
|
public $PostmarkStream = null;
|
||||||
|
|
||||||
public function __construct(bool $isNoReplyEmail = false){
|
public function __construct(bool $isNoReplyEmail = false){
|
||||||
|
|
6
lib/Exceptions/ArtistNameRequiredException.php
Normal file
6
lib/Exceptions/ArtistNameRequiredException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtistNameRequiredException extends AppException{
|
||||||
|
protected $message = 'An artist name is required.';
|
||||||
|
}
|
6
lib/Exceptions/ArtworkAlreadyExistsException.php
Normal file
6
lib/Exceptions/ArtworkAlreadyExistsException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtworkAlreadyExistsException extends AppException{
|
||||||
|
protected $message = 'An artwork with this name already exists.';
|
||||||
|
}
|
6
lib/Exceptions/ArtworkNameRequiredException.php
Normal file
6
lib/Exceptions/ArtworkNameRequiredException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtworkNameRequiredException extends AppException{
|
||||||
|
protected $message = 'An artwork name is required.';
|
||||||
|
}
|
6
lib/Exceptions/ArtworkNotFoundException.php
Normal file
6
lib/Exceptions/ArtworkNotFoundException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class ArtworkNotFoundException extends AppException{
|
||||||
|
protected $message = 'We couldn’t locate that artwork.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidArtistException.php
Normal file
6
lib/Exceptions/InvalidArtistException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidArtistException extends AppException{
|
||||||
|
protected $message = 'We couldn’t locate that artist.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidArtworkException.php
Normal file
6
lib/Exceptions/InvalidArtworkException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidArtworkException extends AppException{
|
||||||
|
protected $message = 'Artwork is invalid.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidArtworkPageUrlException.php
Normal file
6
lib/Exceptions/InvalidArtworkPageUrlException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidArtworkPageUrlException extends AppException{
|
||||||
|
protected $message = 'Invalid link to page with artwork.';
|
||||||
|
}
|
5
lib/Exceptions/InvalidArtworkTagException.php
Normal file
5
lib/Exceptions/InvalidArtworkTagException.php
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidArtworkTagException extends AppException{
|
||||||
|
}
|
6
lib/Exceptions/InvalidCompletedYearException.php
Normal file
6
lib/Exceptions/InvalidCompletedYearException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidCompletedYearException extends AppException{
|
||||||
|
protected $message = 'Invalid year of completion.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidCopyrightPageUrlException.php
Normal file
6
lib/Exceptions/InvalidCopyrightPageUrlException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidCopyrightPageUrlException extends AppException{
|
||||||
|
protected $message = 'Invalid link to page with copyright details.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidDeathYearException.php
Normal file
6
lib/Exceptions/InvalidDeathYearException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidDeathYearException extends AppException{
|
||||||
|
protected $message = 'Invalid year of death.';
|
||||||
|
}
|
7
lib/Exceptions/InvalidImageUploadException.php
Normal file
7
lib/Exceptions/InvalidImageUploadException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidImageUploadException extends AppException{
|
||||||
|
protected $message = 'Uploaded image is invalid.';
|
||||||
|
}
|
7
lib/Exceptions/InvalidMimeTypeException.php
Normal file
7
lib/Exceptions/InvalidMimeTypeException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidMimeTypeException extends AppException{
|
||||||
|
protected $message = 'Uploaded image must be a JPG, BMP, PNG, or TIFF file.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidMuseumUrlException.php
Normal file
6
lib/Exceptions/InvalidMuseumUrlException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidMuseumUrlException extends AppException{
|
||||||
|
protected $message = 'Invalid link to an approved museum page.';
|
||||||
|
}
|
6
lib/Exceptions/InvalidPublicationYearException.php
Normal file
6
lib/Exceptions/InvalidPublicationYearException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidPublicationYearException extends AppException{
|
||||||
|
protected $message = 'Invalid year of publication.';
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class InvalidPublicationYearPageUrlException extends AppException{
|
||||||
|
protected $message = 'Invalid link to page with year of publication.';
|
||||||
|
}
|
|
@ -2,5 +2,5 @@
|
||||||
namespace Exceptions;
|
namespace Exceptions;
|
||||||
|
|
||||||
class InvalidRequestException extends AppException{
|
class InvalidRequestException extends AppException{
|
||||||
protected $message = 'Invalid request.';
|
protected $message = 'Invalid HTTP request.';
|
||||||
}
|
}
|
||||||
|
|
6
lib/Exceptions/MissingEbookException.php
Normal file
6
lib/Exceptions/MissingEbookException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class MissingEbookException extends AppException{
|
||||||
|
protected $message = 'Artwork marked as “in use”, but the ebook couldn’t be found in the filesystem.';
|
||||||
|
}
|
6
lib/Exceptions/MissingPdProofException.php
Normal file
6
lib/Exceptions/MissingPdProofException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class MissingPdProofException extends AppException{
|
||||||
|
protected $message = 'Missing proof of U.S. public domain status.';
|
||||||
|
}
|
11
lib/Exceptions/StringTooLongException.php
Normal file
11
lib/Exceptions/StringTooLongException.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class StringTooLongException extends AppException{
|
||||||
|
public $Source;
|
||||||
|
|
||||||
|
public function __construct(string $source = ''){
|
||||||
|
$this->Source = $source;
|
||||||
|
parent::__construct('String too long: ' . $source);
|
||||||
|
}
|
||||||
|
}
|
6
lib/Exceptions/TagsRequiredException.php
Normal file
6
lib/Exceptions/TagsRequiredException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class TagsRequiredException extends AppException{
|
||||||
|
protected $message = 'At least one tag is required.';
|
||||||
|
}
|
6
lib/Exceptions/TooManyTagsException.php
Normal file
6
lib/Exceptions/TooManyTagsException.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?
|
||||||
|
namespace Exceptions;
|
||||||
|
|
||||||
|
class TooManyTagsException extends AppException{
|
||||||
|
protected $message = 'Too many tags; the maximum is ' . COVER_ARTWORK_MAX_TAGS . '.';
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?
|
<?
|
||||||
use Safe\DateTime;
|
use Safe\DateTime;
|
||||||
|
use function Safe\exec;
|
||||||
use function Safe\file_get_contents;
|
use function Safe\file_get_contents;
|
||||||
use function Safe\file_put_contents;
|
use function Safe\file_put_contents;
|
||||||
use function Safe\tempnam;
|
use function Safe\tempnam;
|
||||||
|
|
|
@ -40,6 +40,12 @@ class Formatter{
|
||||||
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8');
|
return htmlspecialchars(trim($text ?? ''), ENT_QUOTES|ENT_XML1, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function EscapeMarkdown(?string $text): string{
|
||||||
|
$parsedown = new Parsedown();
|
||||||
|
$parsedown->setSafeMode(true);
|
||||||
|
return $parsedown->text($text);
|
||||||
|
}
|
||||||
|
|
||||||
public static function ToFileSize(?int $bytes): string{
|
public static function ToFileSize(?int $bytes): string{
|
||||||
// See https://stackoverflow.com/a/5501447
|
// See https://stackoverflow.com/a/5501447
|
||||||
$output = '';
|
$output = '';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?
|
<?
|
||||||
|
use function Safe\ini_get;
|
||||||
use function Safe\preg_match;
|
use function Safe\preg_match;
|
||||||
|
use function Safe\substr;
|
||||||
|
|
||||||
class HttpInput{
|
class HttpInput{
|
||||||
public static function RequestMethod(): int{
|
public static function RequestMethod(): int{
|
||||||
|
@ -21,6 +23,29 @@ class HttpInput{
|
||||||
return HTTP_GET;
|
return HTTP_GET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function GetMaxPostSize(): int{ // bytes
|
||||||
|
$post_max_size = ini_get('post_max_size');
|
||||||
|
$unit = substr($post_max_size, -1);
|
||||||
|
$size = (int) substr($post_max_size, 0, -1);
|
||||||
|
|
||||||
|
return match ($unit){
|
||||||
|
'g', 'G' => $size * 1024 * 1024 * 1024,
|
||||||
|
'm', 'M' => $size * 1024 * 1024,
|
||||||
|
'k', 'K' => $size * 1024,
|
||||||
|
default => $size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function IsRequestTooLarge(): bool{
|
||||||
|
if(empty($_POST) || empty($_FILES)){
|
||||||
|
if($_SERVER['CONTENT_LENGTH'] > self::GetMaxPostSize()){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public static function RequestType(): int{
|
public static function RequestType(): int{
|
||||||
return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT'] ?? '') ? WEB : REST;
|
return preg_match('/\btext\/html\b/ius', $_SERVER['HTTP_ACCEPT'] ?? '') ? WEB : REST;
|
||||||
}
|
}
|
||||||
|
@ -60,7 +85,7 @@ class HttpInput{
|
||||||
return self::GetHttpVar($variable, HTTP_VAR_ARRAY, GET, $default);
|
return self::GetHttpVar($variable, HTTP_VAR_ARRAY, GET, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function GetHttpVar(string $variable, int $type, string $set, $default){
|
private static function GetHttpVar(string $variable, int $type, string $set, mixed $default): mixed{
|
||||||
$vars = [];
|
$vars = [];
|
||||||
|
|
||||||
switch($set){
|
switch($set){
|
||||||
|
|
94
lib/Image.php
Normal file
94
lib/Image.php
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<?
|
||||||
|
use function Safe\exec;
|
||||||
|
use function Safe\imagecopyresampled;
|
||||||
|
use function Safe\imagecreatetruecolor;
|
||||||
|
use function Safe\imagejpeg;
|
||||||
|
use function Safe\getimagesize;
|
||||||
|
use function Safe\unlink;
|
||||||
|
|
||||||
|
class Image{
|
||||||
|
public $Path;
|
||||||
|
public $MimeType;
|
||||||
|
|
||||||
|
public function __construct(string $path){
|
||||||
|
$this->Path = $path;
|
||||||
|
$this->MimeType = ImageMimeType::FromFile($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return resource
|
||||||
|
* @throws \Safe\Exceptions\ImageException
|
||||||
|
* @throws Exceptions\InvalidImageUploadException
|
||||||
|
*/
|
||||||
|
private function GetImageHandle(){
|
||||||
|
switch($this->MimeType){
|
||||||
|
case ImageMimeType::JPG:
|
||||||
|
$handle = \Safe\imagecreatefromjpeg($this->Path);
|
||||||
|
break;
|
||||||
|
case ImageMimeType::BMP:
|
||||||
|
$handle = \Safe\imagecreatefrombmp($this->Path);
|
||||||
|
break;
|
||||||
|
case ImageMimeType::PNG:
|
||||||
|
$handle = \Safe\imagecreatefrompng($this->Path);
|
||||||
|
break;
|
||||||
|
case ImageMimeType::TIFF:
|
||||||
|
$handle = $this->GetImageHandleFromTiff();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \Exceptions\InvalidImageUploadException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return resource
|
||||||
|
* @throws \Exceptions\InvalidImageUploadException
|
||||||
|
*/
|
||||||
|
private function GetImageHandleFromTiff(){
|
||||||
|
$tempFilename = sys_get_temp_dir() . '/se-' . pathinfo($this->Path)['filename'] . '.jpg';
|
||||||
|
|
||||||
|
try{
|
||||||
|
exec('convert '. escapeshellarg($this->Path) . ' ' . escapeshellarg($tempFilename), $shellOutput, $resultCode);
|
||||||
|
|
||||||
|
if($resultCode !== 0){
|
||||||
|
throw new Exceptions\InvalidImageUploadException('Failed to convert TIFF to JPEG');
|
||||||
|
}
|
||||||
|
|
||||||
|
$handle = \Safe\imagecreatefromjpeg($tempFilename);
|
||||||
|
}
|
||||||
|
finally{
|
||||||
|
try{
|
||||||
|
unlink($tempFilename);
|
||||||
|
}
|
||||||
|
catch(Exception){
|
||||||
|
// Pass if file doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function Resize(string $destImagePath, int $width, int $height): void{
|
||||||
|
$imageDimensions = getimagesize($this->Path);
|
||||||
|
|
||||||
|
$imageWidth = $imageDimensions[0];
|
||||||
|
$imageHeight = $imageDimensions[1];
|
||||||
|
|
||||||
|
if($imageHeight > $imageWidth){
|
||||||
|
$destinationHeight = $height;
|
||||||
|
$destinationWidth = intval($destinationHeight * ($imageWidth / $imageHeight));
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$destinationWidth = $width;
|
||||||
|
$destinationHeight = intval($destinationWidth * ($imageHeight / $imageWidth));
|
||||||
|
}
|
||||||
|
|
||||||
|
$srcImageHandle = $this->GetImageHandle();
|
||||||
|
$thumbImageHandle = imagecreatetruecolor($destinationWidth, $destinationHeight);
|
||||||
|
|
||||||
|
imagecopyresampled($thumbImageHandle, $srcImageHandle, 0, 0, 0, 0, $destinationWidth, $destinationHeight, $imageWidth, $imageHeight);
|
||||||
|
|
||||||
|
imagejpeg($thumbImageHandle, $destImagePath);
|
||||||
|
}
|
||||||
|
}
|
47
lib/ImageMimeType.php
Normal file
47
lib/ImageMimeType.php
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<?
|
||||||
|
use function Safe\mime_content_type;
|
||||||
|
use function Safe\imagecreatefromjpeg;
|
||||||
|
|
||||||
|
enum ImageMimeType: string{
|
||||||
|
case JPG = 'image/jpeg';
|
||||||
|
case BMP = 'image/bmp';
|
||||||
|
case PNG = 'image/png';
|
||||||
|
case TIFF = 'image/tiff';
|
||||||
|
|
||||||
|
public function GetFileExtension(): string{
|
||||||
|
return match($this){
|
||||||
|
self::JPG => '.jpg',
|
||||||
|
self::BMP => '.bmp',
|
||||||
|
self::PNG => '.png',
|
||||||
|
self::TIFF => '.tif',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function FromFile(?string $path): ?ImageMimeType{
|
||||||
|
if($path === null || $path == ''){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mimeType = mime_content_type($path);
|
||||||
|
|
||||||
|
$mimeType = match($mimeType){
|
||||||
|
'image/x-ms-bmp', 'image/x-bmp' => 'image/bmp',
|
||||||
|
default => $mimeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!$mimeType){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageMimeType::tryFrom($mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string>
|
||||||
|
*/
|
||||||
|
public static function Values(): array{
|
||||||
|
return array_map(function(ImageMimeType $case){
|
||||||
|
return $case->value;
|
||||||
|
}, ImageMimeType::cases());
|
||||||
|
}
|
||||||
|
}
|
148
lib/Library.php
148
lib/Library.php
|
@ -1,17 +1,17 @@
|
||||||
<?
|
<?
|
||||||
use Safe\DateTime;
|
use Safe\DateTime;
|
||||||
use function Safe\apcu_fetch;
|
use function Safe\apcu_fetch;
|
||||||
|
use function Safe\exec;
|
||||||
use function Safe\filemtime;
|
use function Safe\filemtime;
|
||||||
use function Safe\filesize;
|
use function Safe\filesize;
|
||||||
use function Safe\glob;
|
use function Safe\glob;
|
||||||
use function Safe\gmdate;
|
use function Safe\gmdate;
|
||||||
use function Safe\ksort;
|
use function Safe\ksort;
|
||||||
use function Safe\natsort;
|
|
||||||
use function Safe\preg_match;
|
use function Safe\preg_match;
|
||||||
use function Safe\preg_replace;
|
use function Safe\preg_replace;
|
||||||
|
use function Safe\shell_exec;
|
||||||
use function Safe\sleep;
|
use function Safe\sleep;
|
||||||
use function Safe\sort;
|
use function Safe\sort;
|
||||||
use function Safe\rsort;
|
|
||||||
use function Safe\usort;
|
use function Safe\usort;
|
||||||
|
|
||||||
|
|
||||||
|
@ -159,6 +159,112 @@ class Library{
|
||||||
return self::GetFromApcu('tags');
|
return self::GetFromApcu('tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsable Artwork can be displayed publically, e.g., at /artworks.
|
||||||
|
* Unverified and declined Artwork shouldn't be browsable.
|
||||||
|
* @return array<Artwork>
|
||||||
|
*/
|
||||||
|
private static function GetBrowsableArtwork(): array{
|
||||||
|
return Db::Query('
|
||||||
|
SELECT *
|
||||||
|
FROM Artworks
|
||||||
|
WHERE Status IN ("approved", "in_use")', [], 'Artwork');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $query
|
||||||
|
* @param string $status
|
||||||
|
* @param string $sort
|
||||||
|
* @return array<Artwork>
|
||||||
|
*/
|
||||||
|
public static function FilterArtwork(string $query = null, string $status = null, string $sort = null): array{
|
||||||
|
$artworks = Library::GetBrowsableArtwork();
|
||||||
|
$matches = $artworks;
|
||||||
|
|
||||||
|
if($sort === null){
|
||||||
|
$sort = SORT_COVER_ARTWORK_CREATED_NEWEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(in_array($status, [COVER_ARTWORK_STATUS_APPROVED, COVER_ARTWORK_STATUS_IN_USE], true)){
|
||||||
|
$matches = [];
|
||||||
|
foreach($artworks as $artwork){
|
||||||
|
if($status === $artwork->Status){
|
||||||
|
$matches[] = $artwork;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$matches = [];
|
||||||
|
foreach($artworks as $artwork){
|
||||||
|
if(in_array($artwork->Status, [COVER_ARTWORK_STATUS_APPROVED, COVER_ARTWORK_STATUS_IN_USE], true)){
|
||||||
|
$matches[] = $artwork;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($query !== null){
|
||||||
|
$filteredMatches = [];
|
||||||
|
|
||||||
|
foreach($matches as $artwork){
|
||||||
|
if($artwork->Contains($query)){
|
||||||
|
$filteredMatches[] = $artwork;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$matches = $filteredMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch($sort){
|
||||||
|
case SORT_COVER_ARTIST_ALPHA:
|
||||||
|
$collator = Collator::create('en_US'); // Used for sorting letters with diacritics like in artist names
|
||||||
|
if($collator === null){
|
||||||
|
usort($matches, function($a, $b){
|
||||||
|
return strcmp(mb_strtolower($a->Artist->Name), mb_strtolower($b->Artist->Name));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
usort($matches, function($a, $b) use($collator){
|
||||||
|
return $collator->compare($a->Artist->Name, $b->Artist->Name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SORT_COVER_ARTWORK_CREATED_NEWEST:
|
||||||
|
usort($matches, function($a, $b){
|
||||||
|
if($a->Created > $b->Created){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
elseif($a->Created == $b->Created){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SORT_COVER_ARTWORK_COMPLETED_NEWEST:
|
||||||
|
usort($matches, function($a, $b){
|
||||||
|
if($a->CompletedYear > $b->CompletedYear){
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
elseif($a->CompletedYear == $b->CompletedYear){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $matches;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<mixed>
|
* @return array<mixed>
|
||||||
*/
|
*/
|
||||||
|
@ -187,6 +293,10 @@ class Library{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!is_array($results)){
|
||||||
|
$results = [$results];
|
||||||
|
}
|
||||||
|
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +321,8 @@ class Library{
|
||||||
*/
|
*/
|
||||||
public static function GetEbooksFromFilesystem(?string $webRoot = WEB_ROOT): array{
|
public static function GetEbooksFromFilesystem(?string $webRoot = WEB_ROOT): array{
|
||||||
$ebooks = [];
|
$ebooks = [];
|
||||||
$contentFiles = explode("\n", trim(shell_exec('find ' . escapeshellarg($webRoot . '/ebooks/') . ' -name "content.opf" | sort') ?? ''));
|
|
||||||
|
$contentFiles = explode("\n", trim(shell_exec('find ' . escapeshellarg($webRoot . '/ebooks/') . ' -name "content.opf" | sort')));
|
||||||
|
|
||||||
foreach($contentFiles as $path){
|
foreach($contentFiles as $path){
|
||||||
if($path == '')
|
if($path == '')
|
||||||
|
@ -435,6 +546,21 @@ class Library{
|
||||||
return $retval;
|
return $retval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function GetEbook(?string $ebookWwwFilesystemPath): ?Ebook{
|
||||||
|
if($ebookWwwFilesystemPath === null){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = self::GetFromApcu('ebook-' . $ebookWwwFilesystemPath);
|
||||||
|
|
||||||
|
if(sizeof($result) > 0){
|
||||||
|
return $result[0];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function RebuildCache(): void{
|
public static function RebuildCache(): void{
|
||||||
// We check a lockfile because this can be a long-running command.
|
// We check a lockfile because this can be a long-running command.
|
||||||
// We don't want to queue up a bunch of these in case someone is refreshing the index constantly.
|
// We don't want to queue up a bunch of these in case someone is refreshing the index constantly.
|
||||||
|
@ -459,7 +585,7 @@ class Library{
|
||||||
$authors = [];
|
$authors = [];
|
||||||
$tagsByName = [];
|
$tagsByName = [];
|
||||||
|
|
||||||
foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"') ?? '')) as $filename){
|
foreach(explode("\n", trim(shell_exec('find ' . EBOOKS_DIST_PATH . ' -name "content.opf"'))) as $filename){
|
||||||
try{
|
try{
|
||||||
$ebookWwwFilesystemPath = preg_replace('|/content\.opf|ius', '', $filename);
|
$ebookWwwFilesystemPath = preg_replace('|/content\.opf|ius', '', $filename);
|
||||||
|
|
||||||
|
@ -527,8 +653,8 @@ class Library{
|
||||||
foreach($ebooksByCollection as $collection => $sortItems){
|
foreach($ebooksByCollection as $collection => $sortItems){
|
||||||
// Sort the array by the ebook's ordinal in the collection. We use this custom sort function
|
// Sort the array by the ebook's ordinal in the collection. We use this custom sort function
|
||||||
// because an ebook may share the same place in a collection with another ebook; see above.
|
// because an ebook may share the same place in a collection with another ebook; see above.
|
||||||
usort($sortItems, function($a, $b) {
|
usort($sortItems, function($a, $b){
|
||||||
if ($a->Ordinal == $b->Ordinal) {
|
if($a->Ordinal == $b->Ordinal){
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return ($a->Ordinal < $b->Ordinal) ? -1 : 1;
|
return ($a->Ordinal < $b->Ordinal) ? -1 : 1;
|
||||||
|
@ -562,4 +688,14 @@ class Library{
|
||||||
|
|
||||||
apcu_delete($lockVar);
|
apcu_delete($lockVar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<Artist>
|
||||||
|
*/
|
||||||
|
public static function GetAllArtists(): array{
|
||||||
|
return Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Artists
|
||||||
|
order by Name asc', [], 'Artist');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
lib/Museum.php
Normal file
18
lib/Museum.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
class Museum extends PropertiesBase{
|
||||||
|
public $MuseumId;
|
||||||
|
public $Name;
|
||||||
|
public $Domain;
|
||||||
|
|
||||||
|
public static function GetByUrl(string $url): ?Museum{
|
||||||
|
$result = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Museums
|
||||||
|
where ? like concat("%", Domain, "%")
|
||||||
|
limit 1;
|
||||||
|
', [$url], 'Museum');
|
||||||
|
|
||||||
|
return $result[0] ?? null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,10 +3,9 @@ use function Safe\substr;
|
||||||
|
|
||||||
abstract class PropertiesBase{
|
abstract class PropertiesBase{
|
||||||
/**
|
/**
|
||||||
* @param mixed $var
|
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function __get($var){
|
public function __get(string $var){
|
||||||
$function = 'Get' . $var;
|
$function = 'Get' . $var;
|
||||||
$privateVar = '_' . $var;
|
$privateVar = '_' . $var;
|
||||||
|
|
||||||
|
|
17
lib/Tag.php
17
lib/Tag.php
|
@ -1,12 +1,11 @@
|
||||||
<?
|
<?
|
||||||
class Tag{
|
|
||||||
public $Name;
|
|
||||||
public $Url;
|
|
||||||
public $UrlName;
|
|
||||||
|
|
||||||
public function __construct(string $name){
|
/**
|
||||||
$this->Name = $name;
|
* @property string $Url
|
||||||
$this->UrlName = Formatter::MakeUrlSafe($this->Name);
|
*/
|
||||||
$this->Url = '/subjects/' . $this->UrlName;
|
class Tag extends PropertiesBase{
|
||||||
}
|
public $TagId;
|
||||||
|
public $Name;
|
||||||
|
public $UrlName;
|
||||||
|
protected $_Url;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?
|
<?
|
||||||
use function Safe\ob_end_clean;
|
use function Safe\ob_end_clean;
|
||||||
|
use function Safe\ob_start;
|
||||||
|
|
||||||
class Template{
|
class Template{
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +35,12 @@ class Template{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function Emit403(): void{
|
||||||
|
http_response_code(403);
|
||||||
|
include(WEB_ROOT . '/403.php');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
public static function Emit404(): void{
|
public static function Emit404(): void{
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
include(WEB_ROOT . '/404.php');
|
include(WEB_ROOT . '/404.php');
|
||||||
|
|
|
@ -7,7 +7,7 @@ DESCRIPTION
|
||||||
Deploy a Standard Ebook source repository to the web.
|
Deploy a Standard Ebook source repository to the web.
|
||||||
|
|
||||||
USAGE
|
USAGE
|
||||||
deploy-ebook-to-www [-v,--verbose] [-g,--group GROUP] [--webroot WEBROOT] [--weburl WEBURL] [--no-images] [--no-build] [--no-epubcheck] [--no-recompose] [--no-feeds] [-l,--last-push-hash HASH] DIRECTORY [DIRECTORY...]
|
deploy-ebook-to-www [-v,--verbose] [-g,--group GROUP] [--webroot WEBROOT] [--weburl WEBURL] [--no-images] [--no-build] [--no-epubcheck] [--no-recompose] [--no-feeds] [--no-bulk-downloads] [--upsert-to-cover-art-database] [-l,--last-push-hash HASH] DIRECTORY [DIRECTORY...]
|
||||||
DIRECTORY is a bare source repository.
|
DIRECTORY is a bare source repository.
|
||||||
GROUP is a groupname. Defaults to "se".
|
GROUP is a groupname. Defaults to "se".
|
||||||
WEBROOT is the path to your webroot. Defaults to "/standardebooks.org/web/www".
|
WEBROOT is the path to your webroot. Defaults to "/standardebooks.org/web/www".
|
||||||
|
@ -39,6 +39,8 @@ USAGE
|
||||||
|
|
||||||
With --no-bulk-downloads, don't generate bulk download files.
|
With --no-bulk-downloads, don't generate bulk download files.
|
||||||
|
|
||||||
|
With --upsert-to-cover-art-database, update/insert cover in the cover art database.
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
|
@ -57,6 +59,7 @@ epubcheck="true"
|
||||||
recompose="true"
|
recompose="true"
|
||||||
feeds="true"
|
feeds="true"
|
||||||
bulkDownloads="true"
|
bulkDownloads="true"
|
||||||
|
coverArtDatabase="false"
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
usage
|
usage
|
||||||
|
@ -114,6 +117,10 @@ while [ $# -gt 0 ]; do
|
||||||
bulkDownloads="false"
|
bulkDownloads="false"
|
||||||
shift 1
|
shift 1
|
||||||
;;
|
;;
|
||||||
|
--upsert-to-cover-art-database)
|
||||||
|
coverArtDatabase="true"
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
*) break ;;
|
*) break ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
@ -149,6 +156,10 @@ if ! [ -f "${scriptsDir}"/generate-bulk-downloads ]; then
|
||||||
die "\"${scriptsDir}\"/generate-bulk-downloads\" is not a file or could not be found."
|
die "\"${scriptsDir}\"/generate-bulk-downloads\" is not a file or could not be found."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! [ -f "${scriptsDir}"/upsert-to-cover-art-database ]; then
|
||||||
|
die "\"${scriptsDir}\"/upsert-to-cover-art-database\" is not a file or could not be found."
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${webRoot}"/images/covers/
|
mkdir -p "${webRoot}"/images/covers/
|
||||||
|
|
||||||
for dir in "$@"
|
for dir in "$@"
|
||||||
|
@ -387,6 +398,18 @@ do
|
||||||
mv "${imgWorkDir}/${urlSafeIdentifier}"*.{jpg,avif} "${webRoot}/images/covers/"
|
mv "${imgWorkDir}/${urlSafeIdentifier}"*.{jpg,avif} "${webRoot}/images/covers/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "${coverArtDatabase}" = "true" ]; then
|
||||||
|
if [ "${verbose}" = "true" ]; then
|
||||||
|
printf "Updating/inserting cover in the cover art database ... "
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${scriptsDir}"/upsert-to-cover-art-database --repoDir "${repoDir}" --workDir "${workDir}" --ebookWwwFilesystemPath "${webDir}"
|
||||||
|
|
||||||
|
if [ "${verbose}" = "true" ]; then
|
||||||
|
printf "Done.\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Delete the now-empty work dir (empty except for .git)
|
# Delete the now-empty work dir (empty except for .git)
|
||||||
rm --preserve-root --recursive --force "${workDir}" "${imgWorkDir}"
|
rm --preserve-root --recursive --force "${workDir}" "${imgWorkDir}"
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ $updatedByGroup = [];
|
||||||
function rrmdir($src){
|
function rrmdir($src){
|
||||||
// See https://www.php.net/manual/en/function.rmdir.php#117354
|
// See https://www.php.net/manual/en/function.rmdir.php#117354
|
||||||
$dir = opendir($src);
|
$dir = opendir($src);
|
||||||
while(false !== ($file = readdir($dir))) {
|
while(false !== ($file = readdir($dir))){
|
||||||
if (($file != '.') && ($file != '..')){
|
if (($file != '.') && ($file != '..')){
|
||||||
$full = $src . '/' . $file;
|
$full = $src . '/' . $file;
|
||||||
if(is_dir($full)){
|
if(is_dir($full)){
|
||||||
|
|
171
scripts/upsert-to-cover-art-database
Executable file
171
scripts/upsert-to-cover-art-database
Executable file
|
@ -0,0 +1,171 @@
|
||||||
|
#!/usr/bin/php
|
||||||
|
<?
|
||||||
|
require_once('/standardebooks.org/web/lib/Core.php');
|
||||||
|
|
||||||
|
$longopts = ['repoDir:', 'workDir:', 'ebookWwwFilesystemPath:', 'verbose'];
|
||||||
|
$options = getopt('v', $longopts);
|
||||||
|
|
||||||
|
$repoDir = $options['repoDir'] ?? false;
|
||||||
|
$workDir = $options['workDir'] ?? false;
|
||||||
|
$ebookWwwFilesystemPath = $options['ebookWwwFilesystemPath'] ?? false;
|
||||||
|
|
||||||
|
$verbose = false;
|
||||||
|
if(isset($options['v']) || isset($options['verbose'])){
|
||||||
|
$verbose = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coverts SimpleXMLElement objects with inner tags like this:
|
||||||
|
* '<abbr>Mr.</abbr> Smith'
|
||||||
|
* to:
|
||||||
|
* 'Mr. Smith'
|
||||||
|
*/
|
||||||
|
function StripInnerTags($elements): ?string{
|
||||||
|
if($elements === false){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($elements[0])){
|
||||||
|
return strip_tags($elements[0]->asXML());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$repoDir || !$workDir || !$ebookWwwFilesystemPath){
|
||||||
|
print("Expected usage: upsert-to-cover-art-database [-v] --repoDir <dir> --workDir <dir> --ebookWwwFilesystemPath <path>\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($verbose){
|
||||||
|
print("\nrepoDir: $repoDir\n");
|
||||||
|
print("workDir: $workDir\n");
|
||||||
|
print("ebookWwwFilesystemPath: $ebookWwwFilesystemPath\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
chdir($repoDir);
|
||||||
|
$contentOpf = shell_exec("git show HEAD:src/epub/content.opf");
|
||||||
|
$xml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $contentOpf));
|
||||||
|
$xml->registerXPathNamespace('dc', 'http://purl.org/dc/elements/1.1/');
|
||||||
|
$artistName = StripInnerTags($xml->xpath('/package/metadata/dc:contributor[@id="artist"]'));
|
||||||
|
if($artistName === null){
|
||||||
|
// Some ebooks have an artist-1 and artist-2. Take artist-1, which isn't ideal, but is usually correct.
|
||||||
|
$artistName = StripInnerTags($xml->xpath('/package/metadata/dc:contributor[@id="artist-1"]'));
|
||||||
|
if($artistName === null){
|
||||||
|
print($repoDir . " Error: Could not find artist name in content.opf\n");
|
||||||
|
exit($repoDir . " Error: missing artistName\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!file_exists($ebookWwwFilesystemPath . '/text/colophon.xhtml')){
|
||||||
|
exit($repoDir . ' Error: no text/colophon.xhtml at ' . $ebookWwwFilesystemPath . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawColophon = file_get_contents($ebookWwwFilesystemPath . '/text/colophon.xhtml');
|
||||||
|
if(empty($rawColophon)){
|
||||||
|
exit($repoDir . ' Error: empty colophon at ' . $ebookWwwFilesystemPath . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match('|a painting completed \w+ (\d+)|ius', $rawColophon, $matches);
|
||||||
|
$completedYear = null;
|
||||||
|
if(sizeof($matches) == 2){
|
||||||
|
$completedYear = (int)$matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$colophonXml = new SimpleXMLElement(str_replace('xmlns=', 'ns=', $rawColophon));
|
||||||
|
$artworkName = StripInnerTags($colophonXml->xpath('/html/body/main/section/p/i[@epub:type="se:name.visual-art.painting"]'));
|
||||||
|
if($artworkName === null){
|
||||||
|
print($repoDir . " Error: Could not find artwork name in colophon.xhtml\n");
|
||||||
|
exit($repoDir . " Error: missing artworkName");
|
||||||
|
}
|
||||||
|
|
||||||
|
$artistUrlName = Formatter::MakeUrlSafe($artistName);
|
||||||
|
$artworkUrlName = Formatter::MakeUrlSafe($artworkName);
|
||||||
|
$artwork = null;
|
||||||
|
|
||||||
|
if($verbose){
|
||||||
|
print("artistName: $artistName\n");
|
||||||
|
print("artistUrlName: $artistUrlName\n");
|
||||||
|
print("completedYear: $completedYear\n");
|
||||||
|
print("artworkName: $artworkName\n");
|
||||||
|
print("artworkUrlName: $artworkUrlName\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
$artwork = Artwork::GetByUrlAndIsApproved($artistUrlName, $artworkUrlName);
|
||||||
|
}
|
||||||
|
catch(Exceptions\ArtworkNotFoundException){
|
||||||
|
// $artwork is null by default, just continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if($artwork === null){
|
||||||
|
if($verbose){
|
||||||
|
printf($repoDir . " No existing artwork found at %s/%s, inserting new artwork.\n", $artistUrlName, $artworkUrlName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ebook colophon provides the artist's name, but not their death year.
|
||||||
|
// Prefer matching an existing artist to creating a new record with a null death year if possible.
|
||||||
|
$artist = Artist::FindMatch($artistName);
|
||||||
|
if($artist === null){
|
||||||
|
$artist = new Artist();
|
||||||
|
$artist->Name = $artistName;
|
||||||
|
}
|
||||||
|
|
||||||
|
$artwork = new Artwork();
|
||||||
|
$artwork->Artist = $artist;
|
||||||
|
$artwork->Name = $artworkName;
|
||||||
|
$artwork->CompletedYear = $completedYear;
|
||||||
|
$artwork->CompletedYearIsCirca = false;
|
||||||
|
$artwork->Created = new DateTime();
|
||||||
|
$artwork->Status = COVER_ARTWORK_STATUS_IN_USE;
|
||||||
|
$artwork->EbookWwwFilesystemPath = $ebookWwwFilesystemPath;
|
||||||
|
$artwork->MimeType = ImageMimeType::JPG;
|
||||||
|
|
||||||
|
$coverSourceFile = tempnam($workDir, 'cover.source.');
|
||||||
|
// Search for JPEG, PNG, and TIFF source files, in that order.
|
||||||
|
exec("git show HEAD:images/cover.source.jpg > $coverSourceFile.jpg", $shellOutput, $resultCode);
|
||||||
|
if($resultCode !== 0){
|
||||||
|
// No JPEG, try PNG.
|
||||||
|
exec("git show HEAD:images/cover.source.png > $coverSourceFile.png", $shellOutput, $resultCode);
|
||||||
|
if($resultCode == 0){
|
||||||
|
// Found PNG, convert it to JPEG.
|
||||||
|
exec("convert $coverSourceFile.png -resize '3750x>' -sampling-factor 4:2:0 -strip -quality 80 -colorspace RGB -interlace JPEG $coverSourceFile.jpg", $shellOutput, $resultCode);
|
||||||
|
if($resultCode !== 0){
|
||||||
|
exit($repoDir . " Error: Failed to convert images/cover.source.png to JPEG\n");
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// No JPEG or PNG, try TIFF.
|
||||||
|
exec("git show HEAD:images/cover.source.tif > $coverSourceFile.tif", $shellOutput, $resultCode);
|
||||||
|
if($resultCode == 0){
|
||||||
|
// Found TIFF, convert it to JPEG.
|
||||||
|
exec("convert $coverSourceFile.tif -resize '3750x>' -sampling-factor 4:2:0 -strip -quality 80 -colorspace RGB -interlace JPEG $coverSourceFile.jpg", $shellOutput, $resultCode);
|
||||||
|
if($resultCode !== 0){
|
||||||
|
exit($repoDir . " Error: Failed to convert images/cover.source.tif to JPEG\n");
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
exit($repoDir . " Error: no images/cover.source.jpg or images/cover.source.png or images/cover.source.tif\n");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadedFile = ['tmp_name' => $coverSourceFile . '.jpg', 'error' => UPLOAD_ERR_OK];
|
||||||
|
$artwork->Create($uploadedFile);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if($verbose){
|
||||||
|
printf($repoDir . " Existing artwork found at %s/%s, updating its status.\n", $artistUrlName, $artworkUrlName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($artwork->CompletedYear != $completedYear){
|
||||||
|
printf($repoDir . " Error: Existing database artwork completed year, %d, does not match ebook colophon completed year, %d. Not updating database.\n", $artwork->CompletedYear, $completedYear);
|
||||||
|
exit($repoDir . " Error: completed year\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if($artwork->Status === COVER_ARTWORK_STATUS_IN_USE){
|
||||||
|
printf($repoDir . " Error: Existing database artwork already marked as 'in_use' by ebook '%s'. Not updating database.\n", $artwork->EbookWwwFilesystemPath);
|
||||||
|
exit($repoDir . " Error: in_use\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
$artwork->MarkInUse($ebookWwwFilesystemPath);
|
||||||
|
}
|
83
templates/ArtworkDetail.php
Normal file
83
templates/ArtworkDetail.php
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?
|
||||||
|
$artwork = $artwork ?? null;
|
||||||
|
|
||||||
|
if($artwork === null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h1><?= Formatter::ToPlainText($artwork->Name) ?></h1>
|
||||||
|
|
||||||
|
<a href="<?= $artwork->ImageUrl ?>">
|
||||||
|
<picture>
|
||||||
|
<source srcset="<?= $artwork->Thumb2xUrl ?> 2x, <?= $artwork->ThumbUrl ?> 1x" type="image/jpg"/>
|
||||||
|
<img src="<?= $artwork->ThumbUrl ?>" alt="" property="schema:image"/>
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?= Template::ImageCopyrightNotice() ?>
|
||||||
|
|
||||||
|
<h2>Metadata</h2>
|
||||||
|
<table class="artwork-metadata">
|
||||||
|
<tr>
|
||||||
|
<td>Title</td>
|
||||||
|
<td><?= Formatter::ToPlainText($artwork->Name) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Artist</td>
|
||||||
|
<td>
|
||||||
|
<?= Formatter::ToPlainText($artwork->Artist->Name) ?><? if(sizeof($artwork->Artist->AlternateSpellings) > 0){ ?> (<abbr>AKA</abbr> <span class="author" typeof="schema:Person" property="schema:name"><?= implode('</span>, <span class="author" typeof="schema:Person" property="schema:name">', array_map('Formatter::ToPlainText', $artwork->Artist->AlternateSpellings)) ?></span>)<? } ?><? if($artwork->Artist->DeathYear !== null){ ?>, <abbr title="deceased">d.</abbr> <?= $artwork->Artist->DeathYear ?><? } ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Year completed</td>
|
||||||
|
<td><? if($artwork->CompletedYear === null){ ?>(unknown)<? }else{ ?><?= $artwork->CompletedYear ?><? if($artwork->CompletedYearIsCirca){ ?> (circa)<? } ?><? } ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>File size</td>
|
||||||
|
<td><?= $artwork->ImageSize ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Uploaded</td>
|
||||||
|
<td><?= $artwork->Created->format('F j, Y g:i a') ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td>
|
||||||
|
<td><?= Template::ArtworkStatus(['artwork' => $artwork]) ?></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td><ul class="tags"><? foreach($artwork->Tags as $tag){ ?><li><a href="<?= $tag->Url ?>"><?= Formatter::ToPlainText($tag->Name) ?></a></li><? } ?></ul></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>U.S. public domain proof</h2>
|
||||||
|
<? if($artwork->MuseumUrl !== null){ ?>
|
||||||
|
<h3>Museum page</h3>
|
||||||
|
<p><a href="<?= Formatter::ToPlainText($artwork->MuseumUrl) ?>"><?= Formatter::ToPlainText($artwork->MuseumUrl) ?></a></p>
|
||||||
|
<? if($artwork->Museum !== null){ ?>
|
||||||
|
<figure class="corrected full">
|
||||||
|
<p>Approved museum: <?= Formatter::ToPlainText($artwork->Museum->Name) ?> <code>(<?= Formatter::ToPlainText($artwork->Museum->Domain) ?>)</code></p>
|
||||||
|
</figure>
|
||||||
|
<? }else{ ?>
|
||||||
|
<figure class="wrong full">
|
||||||
|
<p>Not an approved museum.</p>
|
||||||
|
</figure>
|
||||||
|
<? } ?>
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
|
<? if($artwork->PublicationYear !== null){ ?>
|
||||||
|
<h3>Page scans</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Year book was published: <? if($artwork->PublicationYear !== null){ ?><?= $artwork->PublicationYear ?><? }else{ ?><i>Not provided</i><? } ?></li>
|
||||||
|
<li>Was book published in the U.S.: <? if($artwork->IsPublishedInUs){ ?>Yes<? }else{ ?>No<? } ?></li>
|
||||||
|
<li>Page scan of book publication year: <? if($artwork->PublicationYearPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->PublicationYearPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
|
||||||
|
<li>Page scan of rights statement page: <? if($artwork->CopyrightPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->CopyrightPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
|
||||||
|
<li>Page scan of artwork: <? if($artwork->ArtworkPageUrl !== null){ ?><a href="<?= Formatter::ToPlainText($artwork->ArtworkPageUrl) ?>">Link</a><? }else{ ?><i>Not provided</i><? } ?></li>
|
||||||
|
</ul>
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
|
<? if($artwork->Exception !== null){ ?>
|
||||||
|
<h3>Special public domain exception</h3>
|
||||||
|
<?= Formatter::EscapeMarkdown($artwork->Exception) ?>
|
||||||
|
<? } ?>
|
37
templates/ArtworkList.php
Normal file
37
templates/ArtworkList.php
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
$artworks = $artworks ?? [];
|
||||||
|
$useAdminUrl = $useAdminUrl ?? false;
|
||||||
|
?>
|
||||||
|
<ol class="artwork-list list">
|
||||||
|
<? foreach($artworks as $artwork){ ?>
|
||||||
|
<? if($useAdminUrl){ ?>
|
||||||
|
<? $url = $artwork->AdminUrl; ?>
|
||||||
|
<? }else{ ?>
|
||||||
|
<? $url = $artwork->Url; ?>
|
||||||
|
<? } ?>
|
||||||
|
<li class="<?= $artwork->Status ?>">
|
||||||
|
<div class="thumbnail-container">
|
||||||
|
<a href="<?= $url ?>">
|
||||||
|
<picture>
|
||||||
|
<source srcset="<?= $artwork->Thumb2xUrl ?> 2x, <?= $artwork->ThumbUrl ?> 1x" type="image/jpg"/>
|
||||||
|
<img src="<?= $artwork->ThumbUrl ?>" alt="" property="schema:image"/>
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p><a href="<?= $url ?>" property="schema:name"><?= Formatter::ToPlainText($artwork->Name) ?></a></p>
|
||||||
|
<p>
|
||||||
|
<span class="author" typeof="schema:Person" property="schema:name"><?= Formatter::ToPlainText($artwork->Artist->Name) ?></span>
|
||||||
|
<? if(sizeof($artwork->Artist->AlternateSpellings) > 0){ ?>(<abbr>AKA</abbr> <span class="author" typeof="schema:Person" property="schema:name"><?= implode('</span>, <span class="author" typeof="schema:Person" property="schema:name">', array_map('Formatter::ToPlainText', $artwork->Artist->AlternateSpellings)) ?></span>)<? } ?>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<p>Year completed: <? if($artwork->CompletedYear === null){ ?>(unknown)<? }else{ ?><?= $artwork->CompletedYear ?><? if($artwork->CompletedYearIsCirca){ ?> (circa)<? } ?><? } ?></p>
|
||||||
|
<p>Status: <?= Template::ArtworkStatus(['artwork' => $artwork]) ?></p>
|
||||||
|
<? if(count($artwork->Tags) > 0){ ?>
|
||||||
|
<p>Tags:</p>
|
||||||
|
<ul class="tags"><? foreach($artwork->Tags as $tag){ ?><li><a href="<?= $tag->Url ?>"><?= Formatter::ToPlainText($tag->Name) ?></a></li><? } ?></ul>
|
||||||
|
<? } ?>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<? } ?>
|
||||||
|
</ol>
|
33
templates/ArtworkSearchForm.php
Normal file
33
templates/ArtworkSearchForm.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<form action="/artworks" method="get" rel="search">
|
||||||
|
<label>Status
|
||||||
|
<select name="status" size="1">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="<?= COVER_ARTWORK_STATUS_APPROVED ?>"<? if($status == COVER_ARTWORK_STATUS_APPROVED){ ?> selected="selected"<? } ?>>Approved</option>
|
||||||
|
<option value="<?= COVER_ARTWORK_STATUS_IN_USE ?>"<? if($status == COVER_ARTWORK_STATUS_IN_USE){ ?> selected="selected"<? } ?>>In use</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="search">Keywords
|
||||||
|
<input type="search" name="query" value="<?= Formatter::ToPlainText($query ?? '') ?>"/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Sort</span>
|
||||||
|
<span>
|
||||||
|
<select name="sort">
|
||||||
|
<option value="<?= SORT_COVER_ARTWORK_CREATED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_CREATED_NEWEST){ ?> selected="selected"<? } ?>>Added date (new → old)</option>
|
||||||
|
<option value="<?= SORT_COVER_ARTIST_ALPHA ?>"<? if($sort == SORT_COVER_ARTIST_ALPHA){ ?> selected="selected"<? } ?>>Artist name (a → z)</option>
|
||||||
|
<option value="<?= SORT_COVER_ARTWORK_COMPLETED_NEWEST ?>"<? if($sort == SORT_COVER_ARTWORK_COMPLETED_NEWEST){ ?> selected="selected"<? } ?>>Completed date (new → old)</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Per page</span>
|
||||||
|
<span>
|
||||||
|
<select name="per-page">
|
||||||
|
<option value="50"<? if($perPage == 50){ ?> selected="selected"<? } ?>>50</option>
|
||||||
|
<option value="100"<? if($perPage == 100){ ?> selected="selected"<? } ?>>100</option>
|
||||||
|
<option value="200"<? if($perPage == 200){ ?> selected="selected"<? } ?>>200</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<button>Filter</button>
|
||||||
|
</form>
|
10
templates/ArtworkStatus.php
Normal file
10
templates/ArtworkStatus.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
$artwork = $artwork ?? null;
|
||||||
|
?>
|
||||||
|
<? if($artwork !== null){ ?>
|
||||||
|
<? if($artwork->Status === COVER_ARTWORK_STATUS_APPROVED){ ?>Approved<? } ?>
|
||||||
|
<? if($artwork->Status === COVER_ARTWORK_STATUS_DECLINED){ ?>Declined<? } ?>
|
||||||
|
<? if($artwork->Status === COVER_ARTWORK_STATUS_UNVERIFIED){ ?>Unverified<? } ?>
|
||||||
|
<? if($artwork->Status === COVER_ARTWORK_STATUS_IN_USE){ ?>In use<? if($artwork->Ebook !== null && $artwork->Ebook->Url !== null){ ?> by <a href="<?= $artwork->Ebook->Url ?>" property="schema:url"><span property="schema:name"><?= Formatter::ToPlainText($artwork->Ebook->Title) ?></span></a><? } ?><? } ?>
|
||||||
|
<? } ?>
|
|
@ -3,7 +3,7 @@
|
||||||
$start = new DateTime(DONATION_DRIVE_START);
|
$start = new DateTime(DONATION_DRIVE_START);
|
||||||
$end = new DateTime(DONATION_DRIVE_END);
|
$end = new DateTime(DONATION_DRIVE_END);
|
||||||
$totalCurrent = 0;
|
$totalCurrent = 0;
|
||||||
$baseTarget = 30;
|
$baseTarget = 50;
|
||||||
$stretchCurrent = 0;
|
$stretchCurrent = 0;
|
||||||
$stretchTarget = 20;
|
$stretchTarget = 20;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@ $title = $title ?? '';
|
||||||
$highlight = $highlight ?? '';
|
$highlight = $highlight ?? '';
|
||||||
$description = $description ?? '';
|
$description = $description ?? '';
|
||||||
$manual = $manual ?? false;
|
$manual = $manual ?? false;
|
||||||
|
$artwork = $artwork ?? false;
|
||||||
$colorScheme = $_COOKIE['color-scheme'] ?? 'auto';
|
$colorScheme = $_COOKIE['color-scheme'] ?? 'auto';
|
||||||
$isXslt = $isXslt ?? false;
|
$isXslt = $isXslt ?? false;
|
||||||
$feedUrl = $feedUrl ?? null;
|
$feedUrl = $feedUrl ?? null;
|
||||||
$feedTitle = $feedTitle ?? '';
|
$feedTitle = $feedTitle ?? '';
|
||||||
$is404 = $is404 ?? false;
|
$isErrorPage = $isErrorPage ?? false;
|
||||||
|
|
||||||
// As of Sep 2022, all versions of Safari have a bug where if the page is served as XHTML,
|
// As of Sep 2022, all versions of Safari have a bug where if the page is served as XHTML,
|
||||||
// then <picture> elements download all <source>s instead of the first supported match.
|
// then <picture> elements download all <source>s instead of the first supported match.
|
||||||
|
@ -38,7 +39,7 @@ if(!$isXslt){
|
||||||
<? if(Template::IsEreaderBrowser()){ ?>
|
<? if(Template::IsEreaderBrowser()){ ?>
|
||||||
<link rel="preload" as="font" href="/fonts/league-spartan-bold.ttf" type="font/ttf" crossorigin="anonymous"/>
|
<link rel="preload" as="font" href="/fonts/league-spartan-bold.ttf" type="font/ttf" crossorigin="anonymous"/>
|
||||||
<link href="/css/ereader.css?version=<?= filemtime(WEB_ROOT . '/css/ereader.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
|
<link href="/css/ereader.css?version=<?= filemtime(WEB_ROOT . '/css/ereader.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
|
||||||
<? } else { ?>
|
<? }else{ ?>
|
||||||
<link href="/css/core.css?version=<?= filemtime(WEB_ROOT . '/css/core.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
|
<link href="/css/core.css?version=<?= filemtime(WEB_ROOT . '/css/core.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
|
||||||
<? if($colorScheme == 'auto' || $colorScheme == 'dark'){ ?>
|
<? if($colorScheme == 'auto' || $colorScheme == 'dark'){ ?>
|
||||||
<link href="/css/dark.css?version=<?= filemtime(WEB_ROOT . '/css/dark.css') ?>" media="screen<? if($colorScheme == 'auto'){ ?> and (prefers-color-scheme: dark)<? } ?>" rel="stylesheet" type="text/css"/>
|
<link href="/css/dark.css?version=<?= filemtime(WEB_ROOT . '/css/dark.css') ?>" media="screen<? if($colorScheme == 'auto'){ ?> and (prefers-color-scheme: dark)<? } ?>" rel="stylesheet" type="text/css"/>
|
||||||
|
@ -50,6 +51,9 @@ if(!$isXslt){
|
||||||
<link href="/css/manual-dark.css?version=<?= filemtime(WEB_ROOT . '/css/manual-dark.css') ?>" media="screen<? if($colorScheme == 'auto'){ ?> and (prefers-color-scheme: dark)<? } ?>" rel="stylesheet" type="text/css"/>
|
<link href="/css/manual-dark.css?version=<?= filemtime(WEB_ROOT . '/css/manual-dark.css') ?>" media="screen<? if($colorScheme == 'auto'){ ?> and (prefers-color-scheme: dark)<? } ?>" rel="stylesheet" type="text/css"/>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
|
<? if($artwork){ ?>
|
||||||
|
<link href="/css/artwork.css?version=<?= filemtime(WEB_ROOT . '/css/artwork.css') ?>" media="screen" rel="stylesheet" type="text/css"/>
|
||||||
|
<? } ?>
|
||||||
<link href="/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120"/>
|
<link href="/apple-touch-icon-120x120.png" rel="apple-touch-icon" sizes="120x120"/>
|
||||||
<link href="/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152"/>
|
<link href="/apple-touch-icon-152x152.png" rel="apple-touch-icon" sizes="152x152"/>
|
||||||
<link href="/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"/>
|
<link href="/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"/>
|
||||||
|
@ -67,7 +71,7 @@ if(!$isXslt){
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<link rel="search" href="/ebooks" type="application/xhtml+xml; charset=utf-8"/>
|
<link rel="search" href="/ebooks" type="application/xhtml+xml; charset=utf-8"/>
|
||||||
<link rel="search" href="/ebooks/opensearch" type="application/opensearchdescription+xml; charset=utf-8"/>
|
<link rel="search" href="/ebooks/opensearch" type="application/opensearchdescription+xml; charset=utf-8"/>
|
||||||
<? if(!$is404){ ?>
|
<? if(!$isErrorPage){ ?>
|
||||||
<meta content="#394451" name="theme-color"/>
|
<meta content="#394451" name="theme-color"/>
|
||||||
<meta content="<? if($title != ''){ ?><?= Formatter::ToPlainText($title) ?><? }else{ ?>Standard Ebooks<? } ?>" property="og:title"/>
|
<meta content="<? if($title != ''){ ?><?= Formatter::ToPlainText($title) ?><? }else{ ?>Standard Ebooks<? } ?>" property="og:title"/>
|
||||||
<meta content="<?= $ogType ?? 'website' ?>" property="og:type"/>
|
<meta content="<?= $ogType ?? 'website' ?>" property="og:type"/>
|
||||||
|
|
1
templates/ImageCopyrightNotice.php
Normal file
1
templates/ImageCopyrightNotice.php
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<p>These images are thought to be free of copyright restrictions in the United States. They may still be under copyright in other countries. If you’re not located in the United States, you must check your local laws to verify that these images are free of copyright restrictions in the country you’re located in before accessing, downloading, or using them.</p>
|
17
www/403.php
Normal file
17
www/403.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?= Template::Header(['title' => 'You Don’t Have Permission to View This Page', 'highlight' => '', 'description' => 'You don’t have permission to view this page.', 'isErrorPage' => true]) ?>
|
||||||
|
<main>
|
||||||
|
<section class="narrow has-hero">
|
||||||
|
<hgroup>
|
||||||
|
<h1>You Don’t Have Permission to View This Page</h1>
|
||||||
|
<h2>This is 403 error.</h2>
|
||||||
|
</hgroup>
|
||||||
|
<picture data-caption="The Guard Room. David Teniers II, 1642">
|
||||||
|
<source srcset="/images/guard-room@2x.avif 2x, /images/guard-room.avif 1x" type="image/avif"/>
|
||||||
|
<source srcset="/images/guard-room@2x.jpg 2x, /images/guard-room.jpg 1x" type="image/jpg"/>
|
||||||
|
<img src="/images/guard-room@2x.jpg" alt="Baroque-era guards gather around a table in a room."/>
|
||||||
|
</picture>
|
||||||
|
<p>Your account doesn’t have permission to view this page.</p>
|
||||||
|
<p>If you arrived here in error, <a href="/about#editor-in-chief">contact us</a> so that we can fix it.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
|
@ -1,4 +1,4 @@
|
||||||
<?= Template::Header(['title' => 'We Couldn’t Find That Document', 'highlight' => '', 'description' => 'We couldn’t find that document.', 'is404' => true]) ?>
|
<?= Template::Header(['title' => 'We Couldn’t Find That Document', 'highlight' => '', 'description' => 'We couldn’t find that document.', 'isErrorPage' => true]) ?>
|
||||||
<main>
|
<main>
|
||||||
<section class="narrow has-hero">
|
<section class="narrow has-hero">
|
||||||
<hgroup>
|
<hgroup>
|
||||||
|
|
48
www/admin/artworks/get.php
Normal file
48
www/admin/artworks/get.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$exception = $_SESSION['exception'] ?? null;
|
||||||
|
|
||||||
|
try{
|
||||||
|
$artwork = Artwork::Get(HttpInput::Int(GET, 'artworkid'));
|
||||||
|
|
||||||
|
// The artwork is already approved or in use, so redirect to its public page
|
||||||
|
if($artwork->Status == COVER_ARTWORK_STATUS_APPROVED || $artwork->Status == COVER_ARTWORK_STATUS_IN_USE){
|
||||||
|
http_response_code(302);
|
||||||
|
header('Location: ' . $artwork->Url);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We got here because an artwork submission had errors and the user has to try again
|
||||||
|
if($exception){
|
||||||
|
http_response_code(422);
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exceptions\AppException){
|
||||||
|
Template::Emit404();
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= Template::Header(['title' => 'Review Artwork', 'artwork' => true, 'highlight' => '', 'description' => 'Unverified artwork.']) ?>
|
||||||
|
<main class="artworks">
|
||||||
|
<?= Template::Error(['exception' => $exception]) ?>
|
||||||
|
<section class="narrow">
|
||||||
|
<?= Template::ArtworkDetail(['artwork' => $artwork]) ?>
|
||||||
|
<? if($artwork->Status == COVER_ARTWORK_STATUS_DECLINED){ ?>
|
||||||
|
<h2>Status</h2>
|
||||||
|
<p>This artwork has been declined by a reviewer.</p>
|
||||||
|
<? } ?>
|
||||||
|
<? if($artwork->Status == COVER_ARTWORK_STATUS_UNVERIFIED){ ?>
|
||||||
|
<h2>Review</h2>
|
||||||
|
<p>Review the metadata and PD proof for this artwork submission. Approve to make it available for future producers.</p>
|
||||||
|
<form method="post" action="/admin/artworks/<?= $artwork->ArtworkId ?>">
|
||||||
|
<input type="hidden" name="_method" value="PATCH" />
|
||||||
|
<button name="status" value="<?= COVER_ARTWORK_STATUS_APPROVED ?>">Approve</button>
|
||||||
|
<button name="status" value="<?= COVER_ARTWORK_STATUS_DECLINED ?>" class="decline-button">Decline</button>
|
||||||
|
</form>
|
||||||
|
<? } ?>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
88
www/admin/artworks/index.php
Normal file
88
www/admin/artworks/index.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$artwork = null;
|
||||||
|
$status = HttpInput::Str(SESSION, 'status', false);
|
||||||
|
$page = HttpInput::Int(GET, 'page') ?? 1;
|
||||||
|
$perPage = 10;
|
||||||
|
$hasNextPage = false;
|
||||||
|
$unverifiedArtworks = [];
|
||||||
|
|
||||||
|
try{
|
||||||
|
if($GLOBALS['User'] === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$GLOBALS['User']->Benefits->CanReviewArtwork){
|
||||||
|
throw new Exceptions\InvalidPermissionsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($status !== null){
|
||||||
|
$artwork = Artwork::Get(HttpInput::Int(SESSION, 'artwork-id'));
|
||||||
|
|
||||||
|
http_response_code(201);
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($page <= 0){
|
||||||
|
$page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$unverifiedArtworks = Db::Query('
|
||||||
|
SELECT *
|
||||||
|
from Artworks
|
||||||
|
where Status = "unverified"
|
||||||
|
order by Created asc
|
||||||
|
limit ?, ?
|
||||||
|
', [($page - 1) * $perPage, $perPage + 1], 'Artwork');
|
||||||
|
|
||||||
|
if(sizeof($unverifiedArtworks) > $perPage){
|
||||||
|
array_pop($unverifiedArtworks);
|
||||||
|
$hasNextPage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exceptions\LoginRequiredException){
|
||||||
|
Template::RedirectToLogin();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidPermissionsException){
|
||||||
|
Template::Emit403(); // No permissions to submit artwork
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= Template::Header(['title' => 'Artwork Review Queue', 'artwork' => true, 'highlight' => '', 'description' => 'The queue of unverified artwork.']) ?>
|
||||||
|
<main class="artworks">
|
||||||
|
<section class="narrow">
|
||||||
|
<h1>Artwork Review Queue</h1>
|
||||||
|
|
||||||
|
<? if($status == COVER_ARTWORK_STATUS_APPROVED){ ?>
|
||||||
|
<p class="message success">
|
||||||
|
<? if($artwork !== null){ ?>
|
||||||
|
<i><a href="<?= $artwork->Url ?>" property="schema:name"><?= Formatter::ToPlainText($artwork->Name) ?></a></i> approved.
|
||||||
|
<? }else{ ?>
|
||||||
|
Artwork approved.
|
||||||
|
<? } ?>
|
||||||
|
</p>
|
||||||
|
<? } ?>
|
||||||
|
<? if($status == COVER_ARTWORK_STATUS_DECLINED){ ?>
|
||||||
|
<p class="message">
|
||||||
|
<? if($artwork !== null){ ?>
|
||||||
|
<i><?= Formatter::ToPlainText($artwork->Name) ?></i> declined.
|
||||||
|
<? }else{ ?>
|
||||||
|
Artwork declined.
|
||||||
|
<? } ?>
|
||||||
|
</p>
|
||||||
|
<? } ?>
|
||||||
|
<? if(sizeof($unverifiedArtworks) == 0){ ?>
|
||||||
|
<p>No artwork to review.</p>
|
||||||
|
<? }else{ ?>
|
||||||
|
<?= Template::ArtworkList(['artworks' => $unverifiedArtworks, 'useAdminUrl' => true]) ?>
|
||||||
|
<? } ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<a<? if($page > 1){ ?> href="/admin/artworks/?page=<?= $page - 1 ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
|
||||||
|
<a<? if($hasNextPage){ ?> href="/admin/artworks/?page=<?= $page + 1 ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
45
www/admin/artworks/post.php
Normal file
45
www/admin/artworks/post.php
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?
|
||||||
|
try{
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if(HttpInput::RequestMethod() != HTTP_PATCH){
|
||||||
|
throw new Exceptions\InvalidRequestException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($GLOBALS['User'] === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$GLOBALS['User']->Benefits->CanReviewArtwork){
|
||||||
|
throw new Exceptions\InvalidPermissionsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$artwork = Artwork::Get(HttpInput::Int(GET, 'artworkid'));
|
||||||
|
$artwork->Status = HttpInput::Str(POST, 'status', false);
|
||||||
|
$artwork->ReviewerUserId = $GLOBALS['User']->UserId;
|
||||||
|
$artwork->Save();
|
||||||
|
|
||||||
|
$_SESSION['artwork-id'] = $artwork->ArtworkId;
|
||||||
|
$_SESSION['status'] = $artwork->Status;
|
||||||
|
|
||||||
|
http_response_code(303);
|
||||||
|
header('Location: /admin/artworks');
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidRequestException){
|
||||||
|
http_response_code(405);
|
||||||
|
}
|
||||||
|
catch(Exceptions\LoginRequiredException){
|
||||||
|
Template::RedirectToLogin();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidPermissionsException){
|
||||||
|
Template::Emit403(); // No permissions to submit artwork
|
||||||
|
}
|
||||||
|
catch(Exceptions\ArtworkNotFoundException){
|
||||||
|
Template::Emit404();
|
||||||
|
}
|
||||||
|
catch(Exceptions\AppException $exception){
|
||||||
|
$_SESSION['exception'] = $exception;
|
||||||
|
|
||||||
|
http_response_code(303);
|
||||||
|
header('Location: /admin/artworks/' . $artwork->ArtworkId);
|
||||||
|
}
|
19
www/artworks/get.php
Normal file
19
www/artworks/get.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?
|
||||||
|
|
||||||
|
try{
|
||||||
|
$artistUrlName = HttpInput::Str(GET, 'artist') ?? '';
|
||||||
|
$artworkUrlName = HttpInput::Str(GET, 'artwork') ?? '';
|
||||||
|
|
||||||
|
$artwork = Artwork::GetByUrlAndIsApproved($artistUrlName, $artworkUrlName);
|
||||||
|
}
|
||||||
|
catch(Exceptions\ArtworkNotFoundException){
|
||||||
|
Template::Emit404();
|
||||||
|
}
|
||||||
|
|
||||||
|
?><?= Template::Header(['title' => $artwork->Name, 'artwork' => true]) ?>
|
||||||
|
<main class="artworks">
|
||||||
|
<section class="narrow">
|
||||||
|
<?= Template::ArtworkDetail(['artwork' => $artwork]) ?>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
88
www/artworks/index.php
Normal file
88
www/artworks/index.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?
|
||||||
|
use function Safe\preg_replace;
|
||||||
|
|
||||||
|
$page = HttpInput::Int(GET, 'page') ?? 1;
|
||||||
|
$perPage = HttpInput::Int(GET, 'per-page') ?? COVER_ARTWORK_PER_PAGE;
|
||||||
|
$query = HttpInput::Str(GET, 'query', false) ?? '';
|
||||||
|
$status = HttpInput::Str(GET, 'status', false) ?? null;
|
||||||
|
$sort = HttpInput::Str(GET, 'sort', false);
|
||||||
|
$pages = 0;
|
||||||
|
$totalArtworkCount = 0;
|
||||||
|
$pageDescription = '';
|
||||||
|
$pageTitle = '';
|
||||||
|
$queryString = '';
|
||||||
|
|
||||||
|
if($page <= 0){
|
||||||
|
$page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($perPage != COVER_ARTWORK_PER_PAGE && $perPage != 100 && $perPage != 200){
|
||||||
|
$perPage = COVER_ARTWORK_PER_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're passed string values that are the same as the defaults,
|
||||||
|
// set them to null so that we can have cleaner query strings in the navigation footer
|
||||||
|
if($sort !== null){
|
||||||
|
$sort = mb_strtolower($sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($sort === 'created-newest'){
|
||||||
|
$sort = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$artworks = Library::FilterArtwork($query != '' ? $query : null, $status, $sort);
|
||||||
|
$pageTitle = 'Browse Artwork';
|
||||||
|
$pages = ceil(sizeof($artworks) / $perPage);
|
||||||
|
$totalArtworkCount = sizeof($artworks);
|
||||||
|
$artworks = array_slice($artworks, ($page - 1) * $perPage, $perPage);
|
||||||
|
|
||||||
|
if($page > 1){
|
||||||
|
$pageTitle .= ', page ' . $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageDescription = 'Page ' . $page . ' of artwork';
|
||||||
|
|
||||||
|
if($query != ''){
|
||||||
|
$queryString .= '&query=' . urlencode($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($status !== null){
|
||||||
|
$queryString .= '&status=' . urlencode($status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($sort !== null){
|
||||||
|
$queryString .= '&sort=' . urlencode($sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($perPage !== COVER_ARTWORK_PER_PAGE){
|
||||||
|
$queryString .= '&per-page=' . urlencode((string)$perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$queryString = preg_replace('/^&/ius', '', $queryString);
|
||||||
|
?><?= Template::Header(['title' => $pageTitle, 'artwork' => true, 'description' => $pageDescription]) ?>
|
||||||
|
<main class="artworks">
|
||||||
|
<section class="narrow">
|
||||||
|
<h1>Browse U.S. Public Domain Artwork</h1>
|
||||||
|
|
||||||
|
<?= Template::ArtworkSearchForm(['query' => $query, 'status' => $status, 'sort' => $sort, 'perPage' => $perPage]) ?>
|
||||||
|
<?= Template::ImageCopyrightNotice() ?>
|
||||||
|
<? if($totalArtworkCount == 0){ ?>
|
||||||
|
<p class="no-results">No artwork matched your filters. You can try different filters, or <a href="/artworks">browse all artwork</a>.</p>
|
||||||
|
<? }else{ ?>
|
||||||
|
<?= Template::ArtworkList(['artworks' => $artworks, 'useAdminUrl' => false]) ?>
|
||||||
|
<? } ?>
|
||||||
|
<? if($totalArtworkCount > 0){ ?>
|
||||||
|
<nav>
|
||||||
|
<a<? if($page > 1){ ?> href="/artworks?page=<?= $page - 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
|
||||||
|
<ol>
|
||||||
|
<? for($i = 1; $i < $pages + 1; $i++){ ?>
|
||||||
|
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/artworks?page=<?= $i ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>"><?= $i ?></a></li>
|
||||||
|
<? } ?>
|
||||||
|
</ol>
|
||||||
|
<a<? if($page < ceil($totalArtworkCount / $perPage)){ ?> href="/artworks?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
||||||
|
</nav>
|
||||||
|
<? } ?>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
243
www/artworks/new.php
Normal file
243
www/artworks/new.php
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
<?
|
||||||
|
use function Safe\gmdate;
|
||||||
|
use function Safe\session_unset;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
$created = HttpInput::Bool(SESSION, 'artwork-created', false);
|
||||||
|
$exception = $_SESSION['exception'] ?? null;
|
||||||
|
/** @var Artwork $artwork */
|
||||||
|
$artwork = $_SESSION['artwork'] ?? null;
|
||||||
|
|
||||||
|
try{
|
||||||
|
if($GLOBALS['User'] === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$GLOBALS['User']->Benefits->CanUploadArtwork){
|
||||||
|
throw new Exceptions\InvalidPermissionsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We got here because an artwork was successfully submitted
|
||||||
|
if($created){
|
||||||
|
http_response_code(201);
|
||||||
|
$artwork = null;
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We got here because an artwork submission had errors and the user has to try again
|
||||||
|
if($exception){
|
||||||
|
http_response_code(422);
|
||||||
|
session_unset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if($artwork === null){
|
||||||
|
$artwork = new Artwork();
|
||||||
|
$artwork->Artist = new Artist();
|
||||||
|
|
||||||
|
if($GLOBALS['User']->Benefits->CanReviewArtwork){
|
||||||
|
$artwork->Status = COVER_ARTWORK_STATUS_APPROVED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exceptions\LoginRequiredException){
|
||||||
|
Template::RedirectToLogin();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidPermissionsException){
|
||||||
|
Template::Emit403(); // No permissions to submit artwork
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
|
<?= Template::Header(
|
||||||
|
[
|
||||||
|
'title' => 'Submit an Artwork',
|
||||||
|
'artwork' => true,
|
||||||
|
'highlight' => '',
|
||||||
|
'description' => 'Submit public domain artwork to the database for use as cover art.'
|
||||||
|
]
|
||||||
|
) ?>
|
||||||
|
<main>
|
||||||
|
<section class="narrow">
|
||||||
|
<h1>Submit an Artwork</h1>
|
||||||
|
|
||||||
|
<?= Template::Error(['exception' => $exception]) ?>
|
||||||
|
|
||||||
|
<? if($created){ ?>
|
||||||
|
<p class="message success">Artwork submitted!</p>
|
||||||
|
<? } ?>
|
||||||
|
|
||||||
|
<form method="post" action="/artworks" enctype="multipart/form-data">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Artist details</legend>
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<span>For existing artists, leave the year of death blank.</span>
|
||||||
|
<datalist id="artist-names">
|
||||||
|
<? foreach(Library::GetAllArtists() as $existingArtist){ ?>
|
||||||
|
<option value="<?= Formatter::ToPlainText($existingArtist->Name) ?>"><?= Formatter::ToPlainText($existingArtist->Name) ?>, d. <? if($existingArtist->DeathYear !== null){ ?><?= $existingArtist->DeathYear ?><? }else{ ?>(unknown)<? } ?></option>
|
||||||
|
<? } ?>
|
||||||
|
</datalist>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="artist-name"
|
||||||
|
list="artist-names"
|
||||||
|
required="required"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->Artist->Name) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Year of death</span>
|
||||||
|
<span>If circa or unknown, enter the latest possible year.</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="artist-year-of-death"
|
||||||
|
min="1"
|
||||||
|
max="<?= gmdate('Y') ?>"
|
||||||
|
value="<?= $artwork->Artist->DeathYear ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Artwork details</legend>
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input type="text" name="artwork-name" required="required"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->Name) ?>"/>
|
||||||
|
</label>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
Year of completion
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="artwork-year"
|
||||||
|
min="1"
|
||||||
|
max="<?= gmdate('Y') ?>"
|
||||||
|
value="<?= $artwork->CompletedYear ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="artwork-year-is-circa"
|
||||||
|
<? if($artwork->CompletedYearIsCirca){ ?>checked="checked"<? } ?>
|
||||||
|
/> Year is circa
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<label>
|
||||||
|
<span>Tags</span>
|
||||||
|
<span>A list of comma-separated tags.</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="artwork-tags"
|
||||||
|
required="required"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->ImplodeTags()) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Image</span>
|
||||||
|
<span>jpg, bmp, png, and tiff are accepted.</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="artwork-image"
|
||||||
|
required="required"
|
||||||
|
accept="<?= implode(',', ImageMimeType::Values()) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset id="pd-proof">
|
||||||
|
<legend>Proof of U.S. public domain status</legend>
|
||||||
|
<p>See the <a href="/manual/latest/10-art-and-images#10.3.3.7">US-PD clearance section of the SEMoS</a> for details on this section.</p>
|
||||||
|
<p>PD proof must take the form of <strong>either</strong>:</p>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
URL of the artwork at an <a href="/manual/latest/10-art-and-images#10.3.3.7.4">approved museum</a>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="artwork-museum-url"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->MuseumUrl) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<p><strong>or</strong> proof that the artwork was reproduced in a book published before <?= PD_STRING ?>, with <strong>all</strong> of the following:</p>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
Year of publication
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
name="artwork-publication-year"
|
||||||
|
min="1"
|
||||||
|
max="<?= gmdate('Y') ?>"
|
||||||
|
value="<?= $artwork->PublicationYear ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>URL of page with year of publication</span>
|
||||||
|
<span>Roman numerals are OK.</span>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="artwork-publication-year-page-url"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->PublicationYearPageUrl) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>URL of page with rights statement</span>
|
||||||
|
<span>Might be same URL as above; non-English is OK; keywords in other languages include <i>droits</i> and <i>rechte vorbehalten</i>.</span>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="artwork-copyright-page-url"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->CopyrightPageUrl) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="artwork-is-published-in-us"
|
||||||
|
value="true"
|
||||||
|
<? if($artwork->IsPublishedInUs){ ?> checked="checked"<? } ?> />
|
||||||
|
<span>This book was published in the U.S.</span>
|
||||||
|
<span>Yes, if a U.S. city appears anywhere near the publication year or rights statement.</span>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
URL of page with artwork
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="artwork-artwork-page-url"
|
||||||
|
autocomplete="off"
|
||||||
|
value="<?= Formatter::ToPlainText($artwork->ArtworkPageUrl) ?>"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<p><strong>or</strong> a special reason for an exception:</p>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<span>Exception reason</span>
|
||||||
|
<span>Markdown accepted.</span>
|
||||||
|
<textarea maxlength="1024" name="artwork-exception"><?= Formatter::ToPlainText($artwork->Exception) ?></textarea>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
</fieldset>
|
||||||
|
<? if($GLOBALS['User']->Benefits->CanReviewArtwork){ ?>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Reviewer options</legend>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="artwork-status"
|
||||||
|
value="<?= COVER_ARTWORK_STATUS_APPROVED ?>"
|
||||||
|
<? if($artwork->Status == COVER_ARTWORK_STATUS_APPROVED){ ?> checked="checked"<? } ?> />
|
||||||
|
Approve this artwork immediately
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<? } ?>
|
||||||
|
<div class="footer">
|
||||||
|
<button>Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<?= Template::Footer() ?>
|
74
www/artworks/post.php
Normal file
74
www/artworks/post.php
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?
|
||||||
|
use Exceptions\InvalidRequestException;
|
||||||
|
|
||||||
|
use function Safe\ini_get;
|
||||||
|
use function Safe\substr;
|
||||||
|
|
||||||
|
try{
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
if(HttpInput::RequestMethod() != HTTP_POST){
|
||||||
|
throw new InvalidRequestException('Only HTTP POST accepted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(HttpInput::IsRequestTooLarge()){
|
||||||
|
throw new Exceptions\InvalidRequestException('File upload too large.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if($GLOBALS['User'] === null){
|
||||||
|
throw new Exceptions\LoginRequiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!$GLOBALS['User']->Benefits->CanUploadArtwork){
|
||||||
|
throw new Exceptions\InvalidPermissionsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$artwork = new Artwork();
|
||||||
|
|
||||||
|
$artwork->Artist = new Artist();
|
||||||
|
$artwork->Artist->Name = HttpInput::Str(POST, 'artist-name', false);
|
||||||
|
$artwork->Artist->DeathYear = HttpInput::Int(POST, 'artist-year-of-death');
|
||||||
|
|
||||||
|
$artwork->Name = HttpInput::Str(POST, 'artwork-name', false);
|
||||||
|
$artwork->CompletedYear = HttpInput::Int(POST, 'artwork-year');
|
||||||
|
$artwork->CompletedYearIsCirca = HttpInput::Bool(POST, 'artwork-year-is-circa', false);
|
||||||
|
$artwork->Tags = Artwork::ParseTags(HttpInput::Str(POST, 'artwork-tags', false));
|
||||||
|
$artwork->Status = HttpInput::Str(POST, 'artwork-status', false) ?? COVER_ARTWORK_STATUS_UNVERIFIED;
|
||||||
|
$artwork->IsPublishedInUs = HttpInput::Bool(POST, 'artwork-is-published-in-us', false);
|
||||||
|
$artwork->PublicationYear = HttpInput::Int(POST, 'artwork-publication-year');
|
||||||
|
$artwork->PublicationYearPageUrl = HttpInput::Str(POST, 'artwork-publication-year-page-url', false);
|
||||||
|
$artwork->CopyrightPageUrl = HttpInput::Str(POST, 'artwork-copyright-page-url', false);
|
||||||
|
$artwork->ArtworkPageUrl = HttpInput::Str(POST, 'artwork-artwork-page-url', false);
|
||||||
|
$artwork->MuseumUrl = HttpInput::Str(POST, 'artwork-museum-url', false);
|
||||||
|
$artwork->MimeType = ImageMimeType::FromFile($_FILES['artwork-image']['tmp_name'] ?? null);
|
||||||
|
$artwork->Exception = HttpInput::Str(POST, 'artwork-exception', false);
|
||||||
|
|
||||||
|
// Only approved reviewers can set the status to anything but unverified when uploading
|
||||||
|
if($artwork->Status != COVER_ARTWORK_STATUS_UNVERIFIED && !$GLOBALS['User']->Benefits->CanReviewArtwork){
|
||||||
|
throw new Exceptions\InvalidPermissionsException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$artwork->Create($_FILES['artwork-image'] ?? []);
|
||||||
|
|
||||||
|
$_SESSION['artwork'] = $artwork;
|
||||||
|
$_SESSION['artwork-created'] = true;
|
||||||
|
|
||||||
|
http_response_code(303);
|
||||||
|
header('Location: /artworks/new');
|
||||||
|
}
|
||||||
|
catch(Exceptions\LoginRequiredException){
|
||||||
|
Template::RedirectToLogin();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidPermissionsException){
|
||||||
|
Template::Emit403();
|
||||||
|
}
|
||||||
|
catch(Exceptions\InvalidRequestException){
|
||||||
|
http_response_code(405);
|
||||||
|
}
|
||||||
|
catch(Exceptions\AppException $exception){
|
||||||
|
$_SESSION['artwork'] = $artwork;
|
||||||
|
$_SESSION['exception'] = $exception;
|
||||||
|
|
||||||
|
http_response_code(303);
|
||||||
|
header('Location: /artworks/new');
|
||||||
|
}
|
496
www/css/artwork.css
Normal file
496
www/css/artwork.css
Normal file
|
@ -0,0 +1,496 @@
|
||||||
|
@font-face{
|
||||||
|
font-family: "Fira Mono";
|
||||||
|
src: local("Fira Mono"), url("/fonts/fira-mono.woff2") format("woff2");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face{
|
||||||
|
font-family: "Fira Mono";
|
||||||
|
src: local("Fira Mono"), url("/fonts/fira-mono-bold.woff2") format("woff2");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button.decline-button{
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
form button.decline-button:hover{
|
||||||
|
background-color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a{
|
||||||
|
border: 1px solid rgba(0, 0, 0, .5);
|
||||||
|
font-style: normal;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--button);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-family: "League Spartan", Arial, sans-serif;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
|
||||||
|
box-shadow: 2px 2px 0 rgba(0, 0, 0, .5), 1px 1px 0px rgba(255,255,255, .5) inset;
|
||||||
|
position: relative;
|
||||||
|
text-transform: lowercase;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 1rem;
|
||||||
|
hyphens: none;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message{
|
||||||
|
background: rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav li.highlighted a:focus{
|
||||||
|
outline: 1px dashed var(--input-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a[href]:active{
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a:last-child::after{
|
||||||
|
font-family: "Fork Awesome";
|
||||||
|
content: "\f061";
|
||||||
|
transition: all 200ms ease;
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a:last-child[href]:hover::after{
|
||||||
|
left: .25rem;
|
||||||
|
position: relative;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a:first-child:before{
|
||||||
|
font-family: "Fork Awesome";
|
||||||
|
content: "\f060";
|
||||||
|
transition: all 200ms ease;
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a:first-child[href]:hover::before{
|
||||||
|
right: .25rem;
|
||||||
|
position: relative;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks nav > a:last-child[href]:hover::after{
|
||||||
|
left: .25rem;
|
||||||
|
position: relative;
|
||||||
|
transition: all 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li a:hover,
|
||||||
|
main.artworks nav ol li.highlighted a:hover,
|
||||||
|
.artworks nav > a:hover{
|
||||||
|
background-color: var(--button-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
button:disabled:hover,
|
||||||
|
.artworks nav > a:not([href]){
|
||||||
|
cursor: default;
|
||||||
|
color: #efefef;
|
||||||
|
box-shadow: none;
|
||||||
|
background: #bbb url("/images/stripes-dark.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list > li{
|
||||||
|
border-radius: .25rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16rem 1fr;
|
||||||
|
grid-column-gap: 2rem;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list > li.in_use{
|
||||||
|
background-color: rgba(0, 0, 0, .2);
|
||||||
|
background-image: url("/images/stripes.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list > li + li{
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list > li .thumbnail-container{
|
||||||
|
grid-row: 1 / span 3;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list.list > li p{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list > li p{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list > li img{
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list > li > p:nth-of-type(1) > a{
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol.artwork-list > li .author{
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol{
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li{
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li a{
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
font-variant-numeric: normal;
|
||||||
|
height: calc(1.4rem + 2rem + 2px);
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li.highlighted a{
|
||||||
|
background: var(--button);
|
||||||
|
border: 1px solid rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li.highlighted a,
|
||||||
|
main.artworks nav ol li a:hover{
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks form[action="/artworks"]{
|
||||||
|
align-items: end;
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 1rem;
|
||||||
|
grid-template-columns: auto auto auto auto 1fr;
|
||||||
|
margin: 0 1rem;
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
max-width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] > fieldset{
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] > fieldset + fieldset{
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artist details */
|
||||||
|
form[action="/artworks"] > fieldset:nth-of-type(1){
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Artwork details */
|
||||||
|
form[action="/artworks"] > fieldset:nth-of-type(2){
|
||||||
|
grid-template-columns: 1fr 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] > fieldset label:has(input[name="artwork-tags"]),
|
||||||
|
form[action="/artworks"] > fieldset label:has(input[name="artwork-image"]){
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] fieldset fieldset:has(input[name="artwork-publication-year"]){
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] fieldset fieldset:has(input[name="artwork-publication-year"]) label ~ label{
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] #pd-proof > fieldset{
|
||||||
|
border-left-color: var(--body-text);
|
||||||
|
border-left-width: 3px;
|
||||||
|
border-left-style: dotted;
|
||||||
|
padding-left: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] fieldset p{
|
||||||
|
font-style: italic;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] fieldset p:first-of-type{
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] legend{
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] label{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] div.footer{
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li:not(:first-child):not(:last-child):not(.highlighted){
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li.highlighted::before{
|
||||||
|
content: "⋯";
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li.highlighted::after{
|
||||||
|
content: "⋯";
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav ol li.highlighted:first-child::before,
|
||||||
|
main.artworks nav ol li.highlighted:last-child::after,
|
||||||
|
main.artworks nav ol li.highlighted:nth-child(2)::before,
|
||||||
|
main.artworks nav ol li.highlighted:nth-last-child(2)::after{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-metadata{
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-metadata td:first-child{
|
||||||
|
font-weight: bold;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-metadata td:last-child{
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-metadata td{
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks h1 + a{
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks aside.tip{
|
||||||
|
font-style: italic;
|
||||||
|
margin: 1rem auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
background: rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks aside.tip::before{
|
||||||
|
font-family: "Fork Awesome";
|
||||||
|
content: "\f0eb";
|
||||||
|
font-size: 1rem;
|
||||||
|
position: absolute;
|
||||||
|
font-style: normal;
|
||||||
|
left: .75rem;
|
||||||
|
top: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks aside.tip::after{
|
||||||
|
content: "Tip";
|
||||||
|
font-family: "League Spartan";
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: .75rem;
|
||||||
|
position: absolute;
|
||||||
|
font-style: normal;
|
||||||
|
left: 2rem;
|
||||||
|
top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks .tags{
|
||||||
|
justify-content: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 730px){
|
||||||
|
main.artwork nav ol li:not(.highlighted){
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 680px){
|
||||||
|
main.artworks nav ol li.highlighted::before,
|
||||||
|
main.artworks nav ol li.highlighted::after{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 500px){
|
||||||
|
main.artworks nav{
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav > a{
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav > a::before,
|
||||||
|
main.artworks nav > a::after{
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.artworks nav a[rel]::before,
|
||||||
|
main.artworks nav a[rel]::after{
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 400px){
|
||||||
|
/* Artist details */
|
||||||
|
form[action="/artworks"] > fieldset:nth-of-type(1){
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] > fieldset:nth-of-type(2){
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
form[action="/artworks"] > fieldset label:has(input[name="artwork-tags"]),
|
||||||
|
form[action="/artworks"] > fieldset label:has(input[name="artwork-image"]){
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width: 380px){
|
||||||
|
main.artworks nav > a{
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(prefers-reduced-motion: reduce){
|
||||||
|
.artworks nav > a:last-child::after,
|
||||||
|
.artworks nav > a:last-child[href]:hover::after,
|
||||||
|
.artworks nav > a:first-child:before,
|
||||||
|
.artworks nav > a:first-child[href]:hover::before,
|
||||||
|
.artworks nav > a:last-child[href]:hover::after{
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks figure p{
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: .25rem;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
color: #fff !important; /* in case code highlighting fails */
|
||||||
|
flex-grow: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks figure.wrong::before{
|
||||||
|
background: #762729;
|
||||||
|
font-family: "Fork Awesome";
|
||||||
|
content: "\f00d";
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: .5rem;
|
||||||
|
color: #CF4647;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks figure.corrected::before{
|
||||||
|
background: #406451;
|
||||||
|
font-family: "Fork Awesome";
|
||||||
|
content: "\f00c";
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
width: 1.5rem;
|
||||||
|
color: #79BD9A;
|
||||||
|
text-shadow: 1px 1px 0 rgba(0, 0, 0, .1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks figure.wrong::before,
|
||||||
|
.artworks figure.corrected::before{
|
||||||
|
display: flex;
|
||||||
|
margin-right: 0;
|
||||||
|
align-self: stretch;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
border-top-left-radius: .25rem;
|
||||||
|
border-bottom-left-radius: .25rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks figure.wrong,
|
||||||
|
.artworks figure.corrected{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artworks code{
|
||||||
|
font-variant-numeric: normal;
|
||||||
|
font-family: "Fira Mono", monospace;
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
|
@ -823,11 +823,16 @@ a.button:focus,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="text"]:focus,
|
input[type="text"]:focus,
|
||||||
input[type="month"]:focus,
|
input[type="month"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
input[type="url"]:focus,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
|
input[type="file"]:focus,
|
||||||
label.checkbox:focus-within,
|
label.checkbox:focus-within,
|
||||||
|
label:has(input[type="checkbox"]):focus-within,
|
||||||
select:focus,
|
select:focus,
|
||||||
button:focus,
|
button:focus,
|
||||||
nav a[rel]:focus{
|
nav a[rel]:focus,
|
||||||
|
textarea:focus{
|
||||||
outline: 1px dashed var(--input-outline);
|
outline: 1px dashed var(--input-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1703,8 +1708,11 @@ label.search{
|
||||||
}
|
}
|
||||||
|
|
||||||
select,
|
select,
|
||||||
|
textarea,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="month"],
|
input[type="month"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"]{
|
input[type="search"]{
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
@ -1736,6 +1744,42 @@ select{
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label span{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
label span + span{
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
label span + span i{
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"],
|
||||||
|
label:has(input[type="file"]),
|
||||||
|
label:has(input[type="checkbox"]){
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label + label:has(input[type="checkbox"]){
|
||||||
|
margin-top: 1px; /* So we can see the top outline on focus */
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input[type="checkbox"]):has(> span){
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input[type="checkbox"]):has(> span) input{
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
justify-self: center;
|
||||||
|
align-self: start;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
select[multiple]{
|
select[multiple]{
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -1809,6 +1853,8 @@ a.button,
|
||||||
button,
|
button,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="month"],
|
input[type="month"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
select{
|
select{
|
||||||
|
@ -1826,10 +1872,16 @@ input[type="text"]:focus,
|
||||||
input[type="text"]:hover,
|
input[type="text"]:hover,
|
||||||
input[type="month"]:focus,
|
input[type="month"]:focus,
|
||||||
input[type="month"]:hover,
|
input[type="month"]:hover,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
input[type="number"]:hover,
|
||||||
|
input[type="url"]:focus,
|
||||||
|
input[type="url"]:hover,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="email"]:hover,
|
input[type="email"]:hover,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
input[type="search"]:hover,
|
input[type="search"]:hover,
|
||||||
|
textarea:focus,
|
||||||
|
textarea:hover,
|
||||||
select:focus,
|
select:focus,
|
||||||
select:hover{
|
select:hover{
|
||||||
border-color: var(--input-outline);
|
border-color: var(--input-outline);
|
||||||
|
@ -1839,6 +1891,8 @@ select:hover{
|
||||||
|
|
||||||
input[type="text"]:user-invalid,
|
input[type="text"]:user-invalid,
|
||||||
input[type="month"]:user-invalid,
|
input[type="month"]:user-invalid,
|
||||||
|
input[type="number"]:user-invalid,
|
||||||
|
input[type="url"]:user-invalid,
|
||||||
input[type="email"]:user-invalid,
|
input[type="email"]:user-invalid,
|
||||||
input[type="search"]:user-invalid{
|
input[type="search"]:user-invalid{
|
||||||
border-color: #ff0000;
|
border-color: #ff0000;
|
||||||
|
@ -1847,6 +1901,8 @@ input[type="search"]:user-invalid{
|
||||||
|
|
||||||
input[type="text"]:-moz-ui-invalid,
|
input[type="text"]:-moz-ui-invalid,
|
||||||
input[type="month"]:-moz-ui-invalid,
|
input[type="month"]:-moz-ui-invalid,
|
||||||
|
input[type="number"]:-moz-ui-invalid,
|
||||||
|
input[type="url"]:-moz-ui-invalid,
|
||||||
input[type="email"]:-moz-ui-invalid,
|
input[type="email"]:-moz-ui-invalid,
|
||||||
input[type="search"]:-moz-ui-invalid{
|
input[type="search"]:-moz-ui-invalid{
|
||||||
border-color: #ff0000;
|
border-color: #ff0000;
|
||||||
|
@ -3535,12 +3591,15 @@ ul.feed p{
|
||||||
label.select:hover > span + span::after,
|
label.select:hover > span + span::after,
|
||||||
label.email::before,
|
label.email::before,
|
||||||
label.search::before,
|
label.search::before,
|
||||||
|
textarea:hover,
|
||||||
nav li.highlighted a,
|
nav li.highlighted a,
|
||||||
nav a[rel],
|
nav a[rel],
|
||||||
a.button,
|
a.button,
|
||||||
button,
|
button,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="month"],
|
input[type="month"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"],
|
input[type="search"],
|
||||||
select,
|
select,
|
||||||
|
@ -3552,6 +3611,10 @@ ul.feed p{
|
||||||
input[type="text"]:hover,
|
input[type="text"]:hover,
|
||||||
input[type="month"]:focus,
|
input[type="month"]:focus,
|
||||||
input[type="month"]:hover,
|
input[type="month"]:hover,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
input[type="number"]:hover,
|
||||||
|
input[type="url"]:focus,
|
||||||
|
input[type="url"]:hover,
|
||||||
input[type="email"]:focus,
|
input[type="email"]:focus,
|
||||||
input[type="email"]:hover,
|
input[type="email"]:hover,
|
||||||
input[type="search"]:focus,
|
input[type="search"]:focus,
|
||||||
|
|
|
@ -58,6 +58,8 @@ article.ebook section .donation{
|
||||||
select,
|
select,
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
input[type="month"],
|
input[type="month"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="url"],
|
||||||
input[type="email"],
|
input[type="email"],
|
||||||
input[type="search"]{
|
input[type="search"]{
|
||||||
box-shadow: 1px 1px 0 rgba(0, 0, 0, .5) inset;
|
box-shadow: 1px 1px 0 rgba(0, 0, 0, .5) inset;
|
||||||
|
|
|
@ -145,13 +145,13 @@ catch(Exceptions\InvalidCollectionException){
|
||||||
<? } ?>
|
<? } ?>
|
||||||
<? if(sizeof($ebooks) > 0 && $collection === null){ ?>
|
<? if(sizeof($ebooks) > 0 && $collection === null){ ?>
|
||||||
<nav>
|
<nav>
|
||||||
<a<? if($page > 1){ ?> href="/ebooks/?page=<?= $page - 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
|
<a<? if($page > 1){ ?> href="/ebooks?page=<?= $page - 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="prev"<? }else{ ?> aria-disabled="true"<? } ?>>Back</a>
|
||||||
<ol>
|
<ol>
|
||||||
<? for($i = 1; $i < $pages + 1; $i++){ ?>
|
<? for($i = 1; $i < $pages + 1; $i++){ ?>
|
||||||
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/ebooks/?page=<?= $i ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>"><?= $i ?></a></li>
|
<li<? if($page == $i){ ?> class="highlighted"<? } ?>><a href="/ebooks?page=<?= $i ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>"><?= $i ?></a></li>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
</ol>
|
</ol>
|
||||||
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks/?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
<a<? if($page < ceil($totalEbooks / $perPage)){ ?> href="/ebooks?page=<?= $page + 1 ?><? if($queryString != ''){ ?>&<?= $queryString ?><? } ?>" rel="next"<? }else{ ?> aria-disabled="true"<? } ?>>Next</a>
|
||||||
</nav>
|
</nav>
|
||||||
<? } ?>
|
<? } ?>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?
|
<?
|
||||||
|
use function Safe\exec;
|
||||||
|
|
||||||
$author = HttpInput::Str(GET, 'author', false);
|
$author = HttpInput::Str(GET, 'author', false);
|
||||||
$collection = HttpInput::Str(GET, 'collection', false);
|
$collection = HttpInput::Str(GET, 'collection', false);
|
||||||
$name = null;
|
$name = null;
|
||||||
|
|
8
www/images/cover-uploads/.gitignore
vendored
Normal file
8
www/images/cover-uploads/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Ignore everything
|
||||||
|
*
|
||||||
|
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
# This directory (/www/images/uploads) contains image uploads and thumbnails that
|
||||||
|
# should not be checked into git.
|
BIN
www/images/guard-room.avif
Normal file
BIN
www/images/guard-room.avif
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
BIN
www/images/guard-room.jpg
Normal file
BIN
www/images/guard-room.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
BIN
www/images/guard-room@2x.avif
Normal file
BIN
www/images/guard-room@2x.avif
Normal file
Binary file not shown.
After Width: | Height: | Size: 153 KiB |
BIN
www/images/guard-room@2x.jpg
Normal file
BIN
www/images/guard-room@2x.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 171 KiB |
|
@ -1,9 +1,11 @@
|
||||||
<?
|
<?
|
||||||
|
use function Safe\exec;
|
||||||
use function Safe\substr;
|
use function Safe\substr;
|
||||||
use function Safe\file_get_contents;
|
use function Safe\file_get_contents;
|
||||||
use function Safe\preg_replace;
|
use function Safe\preg_replace;
|
||||||
use function Safe\json_decode;
|
use function Safe\json_decode;
|
||||||
use function Safe\glob;
|
use function Safe\glob;
|
||||||
|
use function Safe\shell_exec;
|
||||||
|
|
||||||
// This script makes various calls to external scripts using exec() (and when called via Apache, as the www-data user).
|
// This script makes various calls to external scripts using exec() (and when called via Apache, as the www-data user).
|
||||||
// These scripts are allowed using the /etc/sudoers.d/www-data file. Only the specific scripts
|
// These scripts are allowed using the /etc/sudoers.d/www-data file. Only the specific scripts
|
||||||
|
@ -26,7 +28,10 @@ try{
|
||||||
$hashAlgorithm = $splitHash[0];
|
$hashAlgorithm = $splitHash[0];
|
||||||
$hash = $splitHash[1];
|
$hash = $splitHash[1];
|
||||||
|
|
||||||
if(!hash_equals($hash, hash_hmac($hashAlgorithm, $post, get_cfg_var('se.secrets.github.se_vcs_bot.secret')))){
|
/** @var string $gitHubWebhookSecret */
|
||||||
|
$gitHubWebhookSecret = get_cfg_var('se.secrets.github.se_vcs_bot.secret');
|
||||||
|
|
||||||
|
if(!hash_equals($hash, hash_hmac($hashAlgorithm, $post, $gitHubWebhookSecret))){
|
||||||
throw new Exceptions\InvalidCredentialsException();
|
throw new Exceptions\InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +82,8 @@ try{
|
||||||
$log->Write('Processing ebook `' . $repoName . '` located at `' . $dir . '`.');
|
$log->Write('Processing ebook `' . $repoName . '` located at `' . $dir . '`.');
|
||||||
|
|
||||||
// Check the local repo's last commit. If it matches this push, then don't do anything; we're already up to date.
|
// Check the local repo's last commit. If it matches this push, then don't do anything; we're already up to date.
|
||||||
$lastCommitSha1 = trim(shell_exec('git -C ' . escapeshellarg($dir) . ' rev-parse HEAD 2>&1') ?? '');
|
|
||||||
|
$lastCommitSha1 = trim(shell_exec('git -C ' . escapeshellarg($dir) . ' rev-parse HEAD 2>&1'));
|
||||||
|
|
||||||
if($lastCommitSha1 == ''){
|
if($lastCommitSha1 == ''){
|
||||||
$log->Write('Error getting last local commit. Output: ' . $lastCommitSha1);
|
$log->Write('Error getting last local commit. Output: ' . $lastCommitSha1);
|
||||||
|
|
|
@ -9,6 +9,7 @@ use function Safe\substr;
|
||||||
$log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH);
|
$log = new Log(POSTMARK_WEBHOOK_LOG_FILE_PATH);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
/** @var string $smtpUsername */
|
||||||
$smtpUsername = get_cfg_var('se.secrets.postmark.username');
|
$smtpUsername = get_cfg_var('se.secrets.postmark.username');
|
||||||
|
|
||||||
$log->Write('Received Postmark webhook.');
|
$log->Write('Received Postmark webhook.');
|
||||||
|
|
|
@ -21,7 +21,10 @@ try{
|
||||||
$post = file_get_contents('php://input');
|
$post = file_get_contents('php://input');
|
||||||
|
|
||||||
// Validate the Zoho secret.
|
// Validate the Zoho secret.
|
||||||
if(!hash_equals($_SERVER['HTTP_X_HOOK_SIGNATURE'], base64_encode(hash_hmac('sha256', $post, get_cfg_var('se.secrets.zoho.webhook_secret'), true)))){
|
/** @var string $zohoWebhookSecret */
|
||||||
|
$zohoWebhookSecret = get_cfg_var('se.secrets.zoho.webhook_secret');
|
||||||
|
|
||||||
|
if(!hash_equals($_SERVER['HTTP_X_HOOK_SIGNATURE'], base64_encode(hash_hmac('sha256', $post, $zohoWebhookSecret, true)))){
|
||||||
throw new Exceptions\InvalidCredentialsException();
|
throw new Exceptions\InvalidCredentialsException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue