1 Commits

Author SHA1 Message Date
bb8e7f359f Merge pull request '1.2' (#23) from 1.2 into main
Reviewed-on: #23
2025-12-05 13:51:15 -05:00
43 changed files with 1962 additions and 3582 deletions

5
.gitignore vendored
View File

@@ -5,7 +5,6 @@
/public/bundles/ /public/bundles/
/var/ /var/
/vendor/ /vendor/
/migrations/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
###> phpunit/phpunit ### ###> phpunit/phpunit ###
@@ -23,6 +22,4 @@
/assets/vendor/ /assets/vendor/
###< symfony/asset-mapper ### ###< symfony/asset-mapper ###
/references/ /references/
composer.lock
.continue

View File

@@ -1,4 +1,4 @@
FROM php:8.5-apache FROM php:8.4-apache
RUN apt update && \ RUN apt update && \
apt upgrade -y && \ apt upgrade -y && \
@@ -11,14 +11,11 @@ RUN apt update && \
libjpeg-dev \ libjpeg-dev \
libicu-dev \ libicu-dev \
libpq-dev \ libpq-dev \
libsqlite3-dev \
sqlite3 \ sqlite3 \
curl \ curl \
git \ git \
cron \
nano nano
RUN docker-php-ext-configure gd --with-jpeg RUN docker-php-ext-configure gd --with-jpeg
RUN docker-php-ext-configure zip RUN docker-php-ext-configure zip
@@ -31,8 +28,7 @@ RUN docker-php-ext-install \
xml \ xml \
intl \ intl \
pdo_mysql \ pdo_mysql \
pdo_pgsql \ pdo_pgsql
pdo_sqlite
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \ php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \
@@ -41,38 +37,22 @@ RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" &&
RUN curl -sS https://get.symfony.com/cli/installer | bash && \ RUN curl -sS https://get.symfony.com/cli/installer | bash && \
mv /root/.symfony5/bin/symfony /usr/local/bin/symfony mv /root/.symfony5/bin/symfony /usr/local/bin/symfony
ARG CACHEBURST=1 COPY . /var/www/html/
ARG BRANCH=master
WORKDIR /var/www/html
RUN git clone -b ${BRANCH} --single-branch https://gitea.rkprather.com/ryan/sermon-notes.git ./
RUN git config --global --add safe.directory /var/www/html
RUN mv 000-default.conf /etc/apache2/sites-available/ RUN mv 000-default.conf /etc/apache2/sites-available/
RUN rm /var/www/html/.env*
RUN echo "20 1 * * 6 root cd /var/www/html && /usr/local/bin/php bin/console app:get-audio > /proc/1/fd/1 2>&1" > /etc/cron.d/get-audio RUN rm -rf /var/www/html/var/*
RUN chmod 644 /etc/cron.d/get-audio RUN rm -rf /var/www/html/vendor
RUN crontab /etc/cron.d/get-audio RUN rm -rf /var/www/html/tests
RUN rm -rf /var/www/html/translations
RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --no-scripts --no-dev --optimize-autoloader RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --no-scripts --no-dev --optimize-autoloader
RUN mkdir /data RUN mkdir /data
RUN mkdir -p /var/www/html/var/cache/prod RUN mkdir /var/www/html/var/cache
RUN mkdir -p /var/www/html/var/log RUN mkdir /var/www/html/var/log
RUN chown -R 33:33 /var/www/html /data RUN chown -R 33:33 /var/www/html /data
RUN find /var/www/html -type d -exec chmod 755 '{}' \; RUN chmod -R 755 /var/www/html /data
RUN find /var/www/html -type f -exec chmod 644 '{}' \;
RUN chmod 755 /data
RUN a2enmod rewrite setenvif headers
COPY prod.env /var/www/html/.env
RUN /usr/local/bin/php bin/console importmap:install
RUN /usr/local/bin/php bin/console asset-map:compile
RUN rm /var/www/html/.env
RUN chmod +x /var/www/html/bin/entrypoint.sh
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["/var/www/html/bin/entrypoint.sh"]
CMD ["apache2-foreground"]

View File

@@ -7,34 +7,21 @@ A program to take notes during a sermon. The web app was built with PHP and Sym
This was my first publicly available docker container so I did not realize what some decisions would do. If you are upgrading from v1 you first need to save your database OR you will lose all your current notes!! Follow the steps below to do that This was my first publicly available docker container so I did not realize what some decisions would do. If you are upgrading from v1 you first need to save your database OR you will lose all your current notes!! Follow the steps below to do that
1. You need to make sure that you have a running SSH server on your host computer 1. You need to make sure that you have a running SSH server on your host computer
2. Make a directory in your `sermon-notes` folder for your database (e.g. `data`) 2. On your host computer, `docker exec -it sermon-notes bash`
3. On your host computer, `docker exec -it sermon-notes bash` 3. `cd var/`
4. `scp .env {user}@{IP}:{path}` 4. `scp data.db {user}@{host computer IP}:{path}`
5. Authenticate with the password 5. Authenticate with the password
6. `cd var/` 6. This will copy the file over SFTP to the host computer
7. `scp data.db {user}@{IP}:{path}/data` 7. After this then you run the `docker run...` command in Step 1 of the `Installation` instructions below, once the container is running you need to copy the `data.db` file into the working directory of the docker container.
8. This will copy the file to the host computer - For example, if you have `~/docker/sermon-notes` as the path for the container on the host computer, you'll copy the `data.db` to `~/docker/sermon-notes/data`
9. After this then you run the `docker run...` command in Step 3 of the `Installation` instructions below
## Installation ## Installation
1. Make a directory in your desired docker storage folder (e.g. `~/docker/sermon-notes`), then `cd` into it. 1. Make a directory in your desired docker storage folder (e.g. `~/docker/sermon-notes`), then `cd` into it.
2. Create a file called `.env` in that folder, no need to add anything to it right now. 2. Create a file called `.env` in that folder, no need to add anything to it right now.
3. Download your preferred compose file (`wget -O compose.yml {link}`) 3. Run `docker run -d --name sermon-notes -p 80:80 -v $PWD/data:/data -v $PWD/.env:/var/www/html/.env gitea.rkprather.com/ryan/sermon-notes:latest`, this will download and start the container and keep it running in the background. If you already have something on port 80 change the first `80` to whatever open port you'd like.
1. [`compose.sqlite.yml`](https://gitea.rkprather.com/ryan/sermon-notes/raw/branch/main/docker/compose.sqlite.yml) - compose file for if you are planning to use SQLite 4. Run `docker exec -it sermon-notes bash install.sh` This will run an install script to create an .env file specific to your install, populate with the beginning factors, and then run a `composer` command to download the necessary package dependancies.
2. [`compose.mysql.yml`](https://gitea.rkprather.com/ryan/sermon-notes/raw/branch/main/docker/compose.mysql.yml) - compose file with an integrated MYSQL database image 5. Once complete you have a running system that you can navigate to in your browser with `http://{ip}:{port}`|`http://{hostname}:{port}`. Then you just need to register for an account. The first account that is created is made an admin so that you can access the `Reference Editor` and update any reference material if necessary.
3. [`compose.mariadb.yml`](https://gitea.rkprather.com/ryan/sermon-notes/raw/branch/main/docker/compose.mariadb.yml) - compose file with an integrate MariaDB database image
4. [`compose.pgsql.yml`](https://gitea.rkprather.com/ryan/sermon-notes/raw/branch/main/docker/compose.pgsql.yml) - compose file with an integrate Postgres database image
5. [`compose.shared-db.yml`](https://gitea.rkprather.com/ryan/sermon-notes/raw/branch/main/docker/compose.shared-db.yml) - compose file with no database image because you are planning on using an existing database container or bare metal server
4. Pull the image `docker pull gitea.rkprather.com/ryan/sermon-notes:latest`
5. **NOTE: IF UPGRADING SKIP THIS STEP!!!** - Run the setup script, this will setup your .env file so that when you start the container everything will be where it is supposed to be.
- `docker run --rm -it -v ${PWD}/.env:/var/www/html/.env gitea.rkprather.com/ryan/sermon-notes:latest php /var/www/html/setup.php --{database-type} {--shared}`
- `{database-type}` = `sqlite`, `mysql`, `mariadb`, or `pgsql`
- If you intend on this being connected to a shared database make sure that you specify `--shared`.
6. Start the container with compose `docker compose up -d`
7. **NOTE: IF UPGRADING SKIP THIS STEP!!!** Run `docker exec -it sermon-notes php /var/www/html/install.php`. This will run the `php composer` to populate the database with all the desired reference material.
- NOTE: You will see deprecation warnings, you can ignore these
8. Once complete you have a running system that you can navigate to in your browser with `http://{ip}:{port}`|`http://{hostname}:{port}`. Then you just need to register for an account. The first account that is created is made an admin so that you can access the `Reference Editor` and update any reference material if necessary.
## Operation ## Operation

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
@import '../css/fontawesome-all.min.css'; @import 'fontawesome-all.min.css';
@import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,400italic,600italic|Roboto+Slab:400,700"); @import url("https://fonts.googleapis.com/css?family=Open+Sans:400,600,400italic,600italic|Roboto+Slab:400,700");
/* /*
Editorial by HTML5 UP Editorial by HTML5 UP

View File

@@ -1,8 +0,0 @@
#!/bin/bash
# Start the cron service in the background
service cron start
chown -R www-data:www-data /var/www/html/var
# Execute the default Docker CMD (which starts Apache in the foreground)
exec "$@"

3652
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@@ -1,49 +1,29 @@
doctrine: doctrine:
dbal: dbal:
connections: url: '%env(resolve:DATABASE_URL)%'
default:
url: '%env(resolve:DATABASE_URL)%'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
transfer:
url: '%env(default::XFER_DATABASE_URL)%'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
# IMPORTANT: You MUST configure your server version, # IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file) # either here or in the DATABASE_URL env var (see .env file)
#server_version: '16' #server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm: orm:
auto_generate_proxy_classes: true auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true enable_lazy_ghost_objects: true
# report_fields_where_declared: true report_fields_where_declared: true
# validate_xml_mapping: true validate_xml_mapping: true
# auto_mapping: true naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
default_entity_manager: default auto_mapping: true
entity_managers: mappings:
default: App:
connection: default type: attribute
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware is_bundle: false
mappings: dir: '%kernel.project_dir%/src/Entity'
App: prefix: 'App\Entity'
type: attribute alias: App
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
transfer:
connection: transfer
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
mappings:
App:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver: controller_resolver:
auto_mapping: false auto_mapping: true
when@test: when@test:
doctrine: doctrine:
@@ -70,6 +50,3 @@ when@prod:
adapter: cache.app adapter: cache.app
doctrine.system_cache_pool: doctrine.system_cache_pool:
adapter: cache.system adapter: cache.system
parameters:
env(default_url): ''

View File

@@ -22,6 +22,3 @@ services:
# add more service definitions when explicit configuration is needed # add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones # please note that last definitions always *replace* previous ones
App\Service\DatabaseTransferService:
arguments:
$projectDir: '%kernel.project_dir%'

View File

@@ -1,34 +1,22 @@
services: services:
sermon-notes: sermon-notes:
image: gitea.rkprather.com/ryan/sermon-notes:1.3 image: gitea.rkprather.com/ryan/sermon-notes:latest
container_name: sermon-notes container_name: sermon-notes
hostname: sermon-notes hostname: sermon-notes
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- ${HTTP_PORT}:80 - ${HTTP_PORT}:80
volumes: volumes:
- ${PWD}/.env:/var/www/html/.env - ${PWD}/.env:/var/www/html/.env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
depends_on: depends_on:
- db - db
db: db:
image: mariadb:12.3 image: mariadb
container_name: db container_name: db
hostname: db hostname: db
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
volumes: volumes:
- ${PWD}/db_data:/var/lib/mysql - ${PWD}/db_data:/var/lib/mysql

View File

@@ -1,34 +1,22 @@
services: services:
sermon-notes: sermon-notes:
image: gitea.rkprather.com/ryan/sermon-notes:1.3 image: gitea.rkprather.com/ryan/sermon-notes:latest
container_name: sermon-notes container_name: sermon-notes
hostname: sermon-notes hostname: sermon-notes
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- ${HTTP_PORT}:80 - ${HTTP_PORT}:80
volumes: volumes:
- ${PWD}/.env:/var/www/html/.env - ${PWD}/.env:/var/www/html/.env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
depends_on: depends_on:
- db - db
db: db:
image: mysql:9.6 image: mysql
container_name: db container_name: db
hostname: db hostname: db
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
volumes: volumes:
- ${PWD}/db-data:/var/lib/mysql - ${PWD}/db-data:/var/lib/mysql

View File

@@ -1,24 +1,14 @@
services: services:
sermon-notes: sermon-notes:
container_name: sermon-notes container_name: sermon-notes
image: gitea.rkprather.com/ryan/sermon-notes:1.3 image: gitea.rkprather.com/ryan/sermon-notes:latest
hostname: sermon-notes hostname: sermon-notes
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- ${HTTP_PORT}:80 - ${HTTP_PORT}:80
volumes: volumes:
- ${PWD}/.env:/var/www/html/.env - ${PWD}/.env:/var/www/html/.env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
depends_on: depends_on:
- db - db
@@ -27,9 +17,7 @@ services:
container_name: db container_name: db
hostname: db hostname: db
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
volumes: volumes:
- ${PWD}/db-data:/var/lib/postgresql/data - ${PWD}/db-data:/var/lib/postgresql/data

View File

@@ -1,20 +1,11 @@
services: services:
sermon-notes: sermon-notes:
image: gitea.rkprather.com/ryan/sermon-notes:1.3 image: gitea.rkprather.com/ryan/sermon-notes:latest
container_name: sermon-notes container_name: sermon-notes
hostname: sermon-notes hostname: sermon-notes
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- ${HTTP_PORT}:80 - ${HTTP_PORT}:80
volumes: volumes:
- ${PWD}/.env:/var/www/html/.env - ${PWD}/.env:/var/www/html/.env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

View File

@@ -1,21 +1,12 @@
services: services:
sermon-notes: sermon-notes:
image: gitea.rkprather.com/ryan/sermon-notes:1.3 image: gitea.rkprather.com/ryan/sermon-notes:latest
container_name: sermon-notes container_name: sermon-notes
hostname: sermon-notes hostname: sermon-notes
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- ${HTTP_PORT}:80 - ${HTTP_PORT}:80
volumes: volumes:
- ${PWD}/data:/data - ${PWD}/data:/data
- ${PWD}/.env:/var/www/html/.env - ${PWD}/.env:/var/www/html/.env
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"

View File

@@ -1,28 +1,28 @@
#!/usr/bin/env php #!/usr/local/bin/php
<?php <?php
print "Updating packages and compiling assets".PHP_EOL; print "Updating packages and compiling assets".PHP_EOL;
shell_exec("COMPOSE_ALLOW_SUPERUSER=1 composer update"); `COMPOSE_ALLOW_SUPERUSER=1 composer update`;
//`symfony console asset-map:compile"); `symfony console asset-map:compile`;
print "Creating database schema".PHP_EOL; print "Creating database schema".PHP_EOL;
shell_exec("symfony console doctrine:database:create --if-not-exists"); `symfony console doctrine:database:create`;
print "Updating migrations and setting permissions for data folder".PHP_EOL; print "Updating migrations and setting permissions for data folder".PHP_EOL;
shell_exec("symfony console doctrine:schema:create"); `symfony console doctrine:migrations:migrate --no-interaction`;
shell_exec("chown -R www-data:www-data /data /var/www/html/var/cache"); `chown -R www-data:www-data /data`;
// import reference material // import reference material
print "Importing Bible and Eccumenical Creeds".PHP_EOL; print "Importing Bible and Eccumenical Creeds".PHP_EOL;
shell_exec("symfony console app:ingest-bible /var/www/html/references/esv-bible"); `symfony console app:ingest-bible /var/www/html/reference/esv-bible`;
shell_exec("symfony console app:import-ref /var/www/html/references/creeds/Apostles 'Apostles Creed' creed apc"); `symfony console app:import-ref /var/www/html/references/creeds/Apostles 'Apostles Creed' creed apc`;
shell_exec("symfony console app:import-ref /var/www/html/references/creeds/Athanasian 'Athanasian Creed' creed ath"); `symfony console app:import-ref /var/www/html/references/creeds/Athanasian 'Athanasian Creed' creed ath`;
shell_exec("symfony console app:import-ref /var/www/html/references/creeds/Chalcedon 'Definition of Chalcedon' creed dc"); `symfony console app:import-ref /var/www/html/references/creeds/Chalcedon 'Definition of Chalcedon' creed dc`;
shell_exec("symfony console app:import-ref /var/www/html/references/creeds/French 'French Confession' creed fc"); `symfony console app:import-ref /var/www/html/references/creeds/French 'French Confession' creed fc`;
shell_exec("symfony console app:import-ref /var/www/html/references/creeds/Nicene 'Nicene Creed' creed nc"); `symfony console app:import-ref /var/www/html/references/creeds/Nicene 'Nicene Creed' creed nc`;
$dutchStandards = ( $dutchStandards = (
strtolower( strtolower(
@@ -32,9 +32,9 @@ $dutchStandards = (
if ($dutchStandards) { if ($dutchStandards) {
print "Importing Dutch Standards".PHP_EOL; print "Importing Dutch Standards".PHP_EOL;
shell_exec("symfony console app:import-ref /var/www/html/references/bc Belgic belgic BC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/bc Belgic belgic BC{\$ndx}`;
shell_exec("symfony console app:import-heidelberg"); `symfony console app:import-heidelberg`;
shell_exec("symfony console app:import-ref /var/www/html/references/cd Canons cd CD"); `symfony console app:import-canons-of-dort`;
} }
$westminsterStandards = ( $westminsterStandards = (
@@ -45,21 +45,21 @@ $westminsterStandards = (
if ($westminsterStandards) { if ($westminsterStandards) {
print "Importing Westminster Standards".PHP_EOL; print "Importing Westminster Standards".PHP_EOL;
shell_exec("symfony console app:import-ref /var/www/html/references/wcf 'Westminster Confession' wcf WCF{\$ndx}"); `symfony console app:import-ref /var/www/html/references/wcf 'Westminster Confession' wcf WCF{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/wsc 'Westminster Shorter' wsc WSC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/wsc 'Westminster Shorter' wsc WSC{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/wlc 'Westminster Larger' wlc WLC{\$ndx}"); `symfony console app:import-wlc /var/www/html/references/wlc 'Westminster Larger' wlc WLC{\$ndx}`;
} }
$helveticConfessions = ( $helviticConfessions = (
strtolower( strtolower(
readline("Do you want to import the Helvetic Confessions (1st & 2nd) (y/n)? ") readline("Do you want to import the Helvetic Confessions (1st & 2nd) (y/n)? ")
) == 'y' ) == 'y'
); );
if ($helveticConfessions) { if ($helviticConfessions) {
print "Importing Helvitic standards".PHP_EOL; print "Importing Helvitic standards".PHP_EOL;
shell_exec("symfony console app:import-ref /var/www/html/references/fhc 'First Helvetic Confession' 1hc 1HC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/fhc 'First Helvetic Confession' 1hc 1HC{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/shc 'Second Helvetic Confession' 2hc 2HC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/shc 'Second Helvetic Confession' 2hc 2HC{\$ndx}`;
} }
$miscStandards = ( $miscStandards = (
@@ -70,12 +70,12 @@ $miscStandards = (
if ($miscStandards) { if ($miscStandards) {
print "Importing misc standards".PHP_EOL; print "Importing misc standards".PHP_EOL;
shell_exec("symfony console app:import-ref /var/www/html/references/39a 'Thirty-Nine Articles' 39a 39A{\$ndx}"); `symfony console app:import-ref /var/www/html/references/39a 'Thirty-Nine Articles' 39a 39A{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/ac 'Augsberg Confession' agc AGC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/ac 'Augsberg Confession' agc AGC{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/lbc 'London Baptist Confession' lbc LBC{\$ndx}"); `symfony console app:import-ref /var/www/html/references/lbc 'London Baptist Confession' lbc LBC{\$ndx}`;
shell_exec("symfony console app:import-ref /var/www/html/references/lsc 'Luther\'s Small Catechism' lsc LSC"); `symfony console app:import-ref /var/www/html/references/lsc 'Luther\'s Small Catechism' lsc LSC`;
shell_exec("symfony console app:import-ref /var/www/html/references/llc 'Luther\'s Large Catechism' llc LLC"); `symfony console app:import-ref /var/www/html/references/llc 'Luther\'s Large Catechism' llc LLC`;
shell_exec("symfony console app:import-ref /var/www/html/references/sd 'Savoy Declaration' sd SD{\$ndx}"); `symfony console app:import-ref /var/www/html/references/sd 'Savoy Declaration' sd SD{\$ndx}`;
} }
print "Sermon Notes Ready".PHP_EOL.PHP_EOL; print "Sermon Notes Ready".PHP_EOL.PHP_EOL;

0
migrations/.gitignore vendored Normal file
View File

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240505234804 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE user (id BLOB NOT NULL --(DC2Type:uuid)
, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
, password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)');
$this->addSql('CREATE TABLE messenger_messages (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, body CLOB NOT NULL, headers CLOB NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, available_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, delivered_at DATETIME DEFAULT NULL --(DC2Type:datetime_immutable)
)');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE user');
$this->addSql('DROP TABLE messenger_messages');
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240513011501 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE bible (id BLOB NOT NULL --(DC2Type:uuid)
, book VARCHAR(255) NOT NULL, chapter INTEGER NOT NULL, verse INTEGER NOT NULL, content CLOB DEFAULT NULL, book_index INTEGER NOT NULL, label VARCHAR(20) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE note (id BLOB NOT NULL --(DC2Type:uuid)
, speaker_id BLOB DEFAULT NULL --(DC2Type:uuid)
, series_id BLOB DEFAULT NULL --(DC2Type:uuid)
, user_id BLOB DEFAULT NULL --(DC2Type:uuid)
, title VARCHAR(255) NOT NULL, date DATE NOT NULL, passage VARCHAR(255) NOT NULL, refs CLOB DEFAULT NULL --(DC2Type:json)
, text CLOB DEFAULT NULL, PRIMARY KEY(id), CONSTRAINT FK_CFBDFA14D04A0F27 FOREIGN KEY (speaker_id) REFERENCES speaker (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA145278319C FOREIGN KEY (series_id) REFERENCES series (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA14A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_CFBDFA14D04A0F27 ON note (speaker_id)');
$this->addSql('CREATE INDEX IDX_CFBDFA145278319C ON note (series_id)');
$this->addSql('CREATE INDEX IDX_CFBDFA14A76ED395 ON note (user_id)');
$this->addSql('CREATE TABLE reference (id BLOB NOT NULL --(DC2Type:uuid)
, type VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, ndx INTEGER NOT NULL, content CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE series (id BLOB NOT NULL --(DC2Type:uuid)
, user_id BLOB DEFAULT NULL --(DC2Type:uuid)
, template_id BLOB DEFAULT NULL --(DC2Type:uuid)
, name VARCHAR(255) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_3A10012DA76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_3A10012D5DA0FB8 FOREIGN KEY (template_id) REFERENCES template (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_3A10012DA76ED395 ON series (user_id)');
$this->addSql('CREATE INDEX IDX_3A10012D5DA0FB8 ON series (template_id)');
$this->addSql('CREATE TABLE speaker (id BLOB NOT NULL --(DC2Type:uuid)
, user_id BLOB DEFAULT NULL --(DC2Type:uuid)
, name VARCHAR(255) NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_7B85DB61A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_7B85DB61A76ED395 ON speaker (user_id)');
$this->addSql('CREATE TABLE template (id BLOB NOT NULL --(DC2Type:uuid)
, user_id BLOB DEFAULT NULL --(DC2Type:uuid)
, name VARCHAR(255) NOT NULL, content CLOB NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_97601F83A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_97601F83A76ED395 ON template (user_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE bible');
$this->addSql('DROP TABLE note');
$this->addSql('DROP TABLE reference');
$this->addSql('DROP TABLE series');
$this->addSql('DROP TABLE speaker');
$this->addSql('DROP TABLE template');
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240513161129 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__reference AS SELECT id, type, name, label, ndx, content FROM reference');
$this->addSql('DROP TABLE reference');
$this->addSql('CREATE TABLE reference (id BLOB NOT NULL --(DC2Type:uuid)
, type VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, ndx INTEGER DEFAULT NULL, content CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO reference (id, type, name, label, ndx, content) SELECT id, type, name, label, ndx, content FROM __temp__reference');
$this->addSql('DROP TABLE __temp__reference');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__reference AS SELECT id, type, name, label, ndx, content FROM reference');
$this->addSql('DROP TABLE reference');
$this->addSql('CREATE TABLE reference (id BLOB NOT NULL --(DC2Type:uuid)
, type VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, ndx INTEGER NOT NULL, content CLOB DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO reference (id, type, name, label, ndx, content) SELECT id, type, name, label, ndx, content FROM __temp__reference');
$this->addSql('DROP TABLE __temp__reference');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240527010736 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE note ADD COLUMN recording VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__note AS SELECT id, speaker_id, series_id, user_id, title, date, passage, refs, text FROM note');
$this->addSql('DROP TABLE note');
$this->addSql('CREATE TABLE note (id BLOB NOT NULL --(DC2Type:uuid)
, speaker_id BLOB DEFAULT NULL --(DC2Type:uuid)
, series_id BLOB DEFAULT NULL --(DC2Type:uuid)
, user_id BLOB DEFAULT NULL --(DC2Type:uuid)
, title VARCHAR(255) NOT NULL, date DATE NOT NULL, passage VARCHAR(255) NOT NULL, refs CLOB DEFAULT NULL --(DC2Type:json)
, text CLOB DEFAULT NULL, PRIMARY KEY(id), CONSTRAINT FK_CFBDFA14D04A0F27 FOREIGN KEY (speaker_id) REFERENCES speaker (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA145278319C FOREIGN KEY (series_id) REFERENCES series (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CFBDFA14A76ED395 FOREIGN KEY (user_id) REFERENCES user (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO note (id, speaker_id, series_id, user_id, title, date, passage, refs, text) SELECT id, speaker_id, series_id, user_id, title, date, passage, refs, text FROM __temp__note');
$this->addSql('DROP TABLE __temp__note');
$this->addSql('CREATE INDEX IDX_CFBDFA14D04A0F27 ON note (speaker_id)');
$this->addSql('CREATE INDEX IDX_CFBDFA145278319C ON note (series_id)');
$this->addSql('CREATE INDEX IDX_CFBDFA14A76ED395 ON note (user_id)');
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240622233923 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD COLUMN meta_data CLOB DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__user AS SELECT id, email, roles, password, name FROM user');
$this->addSql('DROP TABLE user');
$this->addSql('CREATE TABLE user (id BLOB NOT NULL --(DC2Type:uuid)
, email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json)
, password VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
$this->addSql('INSERT INTO user (id, email, roles, password, name) SELECT id, email, roles, password, name FROM __temp__user');
$this->addSql('DROP TABLE __temp__user');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)');
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240717022017 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE shared_note (id BLOB NOT NULL --(DC2Type:uuid)
, note_id BLOB NOT NULL --(DC2Type:uuid)
, owner_id BLOB NOT NULL --(DC2Type:uuid)
, shared_user_id BLOB NOT NULL --(DC2Type:uuid)
, PRIMARY KEY(id), CONSTRAINT FK_754B918C26ED0855 FOREIGN KEY (note_id) REFERENCES note (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_754B918C26ED0855 ON shared_note (note_id)');
$this->addSql('CREATE TABLE shared_series (id BLOB NOT NULL --(DC2Type:uuid)
, series_id BLOB NOT NULL --(DC2Type:uuid)
, owner_id BLOB NOT NULL --(DC2Type:uuid)
, shared_user_id BLOB NOT NULL --(DC2Type:uuid)
, PRIMARY KEY(id), CONSTRAINT FK_59E803195278319C FOREIGN KEY (series_id) REFERENCES series (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('CREATE INDEX IDX_59E803195278319C ON shared_series (series_id)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE shared_note');
$this->addSql('DROP TABLE shared_series');
}
}

View File

@@ -1,9 +0,0 @@
<IfModule mod_rewrite.c>
Options -MultiViews
RewriteEngine On
SetEnvIf X-Forwarded-Proto "https" HTTPS=on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

View File

@@ -1,35 +0,0 @@
{
"name": "Sermon Notes",
"short_name": "Sermon Notes",
"description": "A personal note-taking app for sermons with reference material",
"start_url": "/",
"display": "standalone",
"orientation": "landscape",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/images/Notes-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/Notes-icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/images/Notes-icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/images/Notes-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"form_factor": "wide"
}
]
}

View File

@@ -1,56 +0,0 @@
// public/sw.js
const CACHE_NAME = 'app-cache-v1';
const ASSETS_TO_CACHE = [
'/',
'/manifest.json',
// Add paths to your main CSS and JS files here
// e.g., '/build/app.css', '/build/app.js'
];
// Install Event: Cache core static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
self.skipWaiting();
});
// Activate Event: Clean up old caches if you change the CACHE_NAME version
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cache => {
if (cache !== CACHE_NAME) {
return caches.delete(cache);
}
})
);
})
);
self.clients.claim();
});
// Fetch Event: Network First, Fallback to Cache
self.addEventListener('fetch', event => {
// Only cache GET requests
if (event.request.method !== 'GET') return;
event.respondWith(
fetch(event.request)
.then(networkResponse => {
// If the network request succeeds, clone it and put it in the cache
return caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// If the network fails (offline), try to serve from cache
return caches.match(event.request);
})
);
});

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env php #!/usr/local/bin/php
<?php <?php
@@ -8,7 +8,7 @@ if (!file_exists('/var/www/html/.env')) {
$cmd = getopt("", ["sqlite", "mysql", "mariadb", "pgsql", "shared"]); $cmd = getopt("", ["sqlite", "mysql", "mariadb", "pgsql", "shared"]);
$key = shell_exec("openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '"); $key = `openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '`;
$key = substr($key, 0, 32); $key = substr($key, 0, 32);
$database_url = null; $database_url = null;
$getCreds = true; $getCreds = true;
@@ -53,7 +53,7 @@ if ($getCreds) {
$db_port = (isset($cmd['pgsql']) ? 5432 : 3306); $db_port = (isset($cmd['pgsql']) ? 5432 : 3306);
$db_name = 'sermon_notes'; $db_name = 'sermon_notes';
$db_user = 'root'; $db_user = 'root';
$pwd = shell_exec("openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '"); $pwd = `openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '`;
$db_password = substr($pwd, 0, 32); $db_password = substr($pwd, 0, 32);
if (isset($cmd['pgsql'])) { if (isset($cmd['pgsql'])) {
@@ -85,7 +85,7 @@ $output = <<<EOF
APP_ENV=prod APP_ENV=prod
APP_DEBUG=0 APP_DEBUG=0
APP_SECRET=$key APP_SECRET=$key
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 MESSAGENER_TRANSPORT_DSN=doctrine://default?auto_setup=0
HTTP_PORT=$http_port HTTP_PORT=$http_port
$creds$database_url $creds$database_url

View File

@@ -1,282 +0,0 @@
<?php
namespace App\Command;
use App\Entity\Note;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsCommand(
name: 'app:get-audio',
description: 'Finds Notes with missing recordings and matches them to RSS feed by Date and Title.',
)]
class GetAudioCommand extends Command
{
public function __construct(
private EntityManagerInterface $entityManager,
private HttpClientInterface $httpClient
) {
parent::__construct();
}
protected function configure(): void
{
$this->addOption('dry-run', null, InputOption::VALUE_NONE, 'No DB changes.');
// No specific --debug flag needed, we will output verbose logs by default for now
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$isDryRun = $input->getOption('dry-run');
$noteRepository = $this->entityManager->getRepository(Note::class);
$io->title("Starting Audio Matcher");
// 1. Fetch Notes
$qb = $noteRepository->createQueryBuilder('n')
->leftJoin('n.user', 'u')
->addSelect('u')
->where('n.recording IS NULL OR n.recording = :empty')
->andWhere('u.homeChurchRSS IS NOT NULL')
->orderBy('n.date', 'DESC') // <--- Added Sort Here
->setParameter('empty', '');
//$query = $qb->getQuery();
//print ($query->getSql());
$notesMissingAudio = $qb->getQuery()->getResult();
$count = count($notesMissingAudio);
$io->text("Found $count notes in database missing audio.");
if ($count === 0) {
return Command::SUCCESS;
}
// 2. Group by User
$notesByUser = [];
foreach ($notesMissingAudio as $note) {
$userId = (string) $note->getUser()->getId();
$notesByUser[$userId]['user'] = $note->getUser();
$notesByUser[$userId]['notes'][] = $note;
}
// 3. Process Per User
foreach ($notesByUser as $userId => $data) {
$user = $data['user'];
$userNotes = $data['notes'];
$rssUrl = $user->getHomeChurchRSS();
$io->section("User: {$user->getEmail()} (Notes: " . count($userNotes) . ")");
$io->text("Fetching RSS: $rssUrl");
try {
// Pass $io to helper for debug output
$rssItems = $this->fetchRssItems($rssUrl, $io);
if (empty($rssItems)) {
$io->warning("RSS feed was empty or failed to parse.");
continue;
}
$matchCount = 0;
foreach ($userNotes as $note) {
if (!$note->getDate()) {
$io->text(" > Note ID {$note->getId()} skipped (No Date)");
continue;
}
$noteDateString = $note->getDate()->format('Y-m-d');
$noteTitle = $note->getTitle();
$io->text("---------------------------------------------------");
$io->text("Checking Note: [$noteDateString] '$noteTitle'");
$bestMatch = null;
$highestConfidence = 0;
foreach ($rssItems as $item) {
// DEBUG: Show Date Comparison
if ($item['date_string'] !== $noteDateString) {
// Uncomment the line below if you want to see EVERY failed date comparison (can be noisy)
// $io->text(" - REJECTED: Date mismatch (RSS: {$item['date_string']})");
continue;
}
// DEBUG: Show Score Calculation
$confidence = $this->calculateConfidence($note, $item);
$io->text(sprintf(
" - DATE MATCHED. Score: %d%%. RSS Title: '%s'",
$confidence,
$item['title']
));
if ($confidence >= 80 && $confidence > $highestConfidence) {
$highestConfidence = $confidence;
$bestMatch = $item;
}
}
if ($bestMatch) {
$matchCount++;
$io->success("Match Found! ($highestConfidence%)");
if (!$isDryRun) {
$note->setRecording($bestMatch['url']);
}
} else {
$io->text(" > No match found for this note.");
}
}
if (!$isDryRun) {
$this->entityManager->flush();
}
if ($matchCount > 0) {
$io->success("Found $matchCount matches");
}
} catch (\Exception $e) {
$io->error("Error: " . $e->getMessage());
}
}
return Command::SUCCESS;
}
/**
* Recursively fetches RSS items if pagination links are present.
*/
private function fetchRssItems(string $startUrl, SymfonyStyle $io): array
{
$items = [];
$nextUrl = $startUrl;
$pageCount = 0;
$maxPages = 20; // Safety brake to prevent infinite loops
do {
$pageCount++;
$io->text(" > Fetching Feed Page $pageCount: $nextUrl");
try {
$response = $this->httpClient->request('GET', $nextUrl);
$content = $response->getContent();
// Suppress warnings for malformed XML
$xml = @simplexml_load_string($content);
if ($xml === false) {
$io->warning("XML Parsing Failed on page $pageCount");
break;
}
} catch (\Exception $e) {
$io->warning("HTTP Request Failed on page $pageCount: " . $e->getMessage());
break;
}
// 1. Parse Items on this page
$pageItemsCount = 0;
foreach ($xml->channel->item as $item) {
$namespaces = $item->getNamespaces(true);
$speaker = '';
// Speaker Logic
if (isset($namespaces['itunes'])) {
$itunes = $item->children($namespaces['itunes']);
$speaker = (string) ($itunes->author ?? '');
}
if (empty($speaker) && isset($namespaces['dc'])) {
$dc = $item->children($namespaces['dc']);
$speaker = (string) ($dc->creator ?? '');
}
if (empty($speaker)) {
$speaker = (string) ($item->author ?? '');
}
// Date Parsing
$dateString = null;
if (isset($item->pubDate)) {
try {
$dt = new \DateTimeImmutable((string)$item->pubDate);
$dateString = $dt->format('Y-m-d');
} catch (\Exception $e) {
// ignore bad date
}
}
$items[] = [
'title' => (string) $item->title,
'speaker' => $speaker,
'url' => (string) ($item->enclosure['url'] ?? ''),
'date_string' => $dateString,
];
$pageItemsCount++;
}
$io->text(" Found $pageItemsCount items on this page.");
// 2. Look for "Next Page" link (RFC 5005 / Atom)
$nextUrl = null;
// Get namespaces on the <channel> element
$namespaces = $xml->channel->getNamespaces(true);
if (isset($namespaces['atom'])) {
$atom = $xml->channel->children($namespaces['atom']);
foreach ($atom->link as $link) {
// We are looking for <atom:link rel="next" href="..." />
$attributes = $link->attributes();
if (isset($attributes['rel']) && (string)$attributes['rel'] === 'next') {
$nextUrl = (string)$attributes['href'];
break;
}
}
}
// Fallback: Check for raw <link rel="next"> if atom ns missing (rare but happens)
if (!$nextUrl && property_exists($xml->channel, 'link')) {
foreach ($xml->channel->link as $link) {
$attributes = $link->attributes();
if (isset($attributes['rel']) && (string)$attributes['rel'] === 'next') {
$nextUrl = (string)$attributes['href'];
break;
}
}
}
} while ($nextUrl && $pageCount < $maxPages);
$io->success(sprintf("Finished fetching. Total items: %d (across %d pages)", count($items), $pageCount));
return $items;
}
private function calculateConfidence(Note $note, array $rssItem): float
{
$noteTitle = $this->normalize($note->getTitle());
$rssTitle = $this->normalize($rssItem['title']);
$noteSpeaker = $this->normalize($note->getSpeaker()->getName() ?? '');
$rssSpeaker = $this->normalize($rssItem['speaker']);
similar_text($noteTitle, $rssTitle, $titlePercent);
if (!empty($noteSpeaker) && !empty($rssSpeaker)) {
similar_text($noteSpeaker, $rssSpeaker, $speakerPercent);
return ($titlePercent + $speakerPercent) / 2;
}
return $titlePercent;
}
private function normalize(string $input): string
{
return strtolower(trim($input));
}
}

View File

@@ -144,10 +144,6 @@ class IngestReferenceCommand extends Command
$ref->setContent($md); $ref->setContent($md);
$ref->setName($this->name); $ref->setName($this->name);
$ref->setType($this->type); $ref->setType($this->type);
if ($this->type == 'cd') {
$label = substr(basename($file), 0, -3);
}
$ref->setLabel($label); $ref->setLabel($label);
$this->io->success("Ingested {$this->name} as {$this->type}:{$label}"); $this->io->success("Ingested {$this->name} as {$this->type}:{$label}");

View File

@@ -20,7 +20,6 @@ use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -454,47 +453,6 @@ class AjaxController extends AbstractController
return $res; return $res;
} }
#[Route('/save-profile', name: 'app_save_profile', methods: ['POST'])]
public function saveProfile(Request $req, EntityManagerInterface $emi): Response
{
$data = json_decode($req->getContent());
/** @var App\Entity\User $user */
$user = $this->getUser();
if (!$user) {
return new JsonResponse(['msg' => 'No User']);
}
if ($data->passChange) {
if(!$data->password) {
return new JsonResponse(['msg' => 'Blank password']);
}
// @todo check that password matches current password
if ($data->password != $user->getPassword()) {
return new JsonResponse(['msg' => 'Invalid password']);
}
if ($data->newPassword != $data->confPassword) {
return new JsonResponse(['msg' => 'Passwords don\'t match']);
}
}
$user->setName($data->name);
$user->setEmail($data->email);
$user->setHomeChurchRSS($data->homeChurch);
$emi->persist($user);
try {
$emi->flush();
} catch (Exception $e) {
return new JsonResponse();
}
return new JsonResponse(['msg' => 'Updated']);
}
#[Route('/save-settings', name: 'app_save_settings', methods: ['POST'])] #[Route('/save-settings', name: 'app_save_settings', methods: ['POST'])]
public function saveSettings(Request $req, EntityManagerInterface $emi): Response public function saveSettings(Request $req, EntityManagerInterface $emi): Response
{ {

View File

@@ -7,7 +7,6 @@ use App\Entity\User;
use App\Entity\Speaker; use App\Entity\Speaker;
use App\Entity\Series; use App\Entity\Series;
use App\Entity\SharedNote; use App\Entity\SharedNote;
use App\Service\DatabaseTransferService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -41,7 +40,6 @@ class DefaultController extends AbstractController
'last4Notes' => $last4Notes, 'last4Notes' => $last4Notes,
'reverseNoteSort' => $openNotes, 'reverseNoteSort' => $openNotes,
'isAdmin' => $this->isGranted('ROLE_ADMIN'), 'isAdmin' => $this->isGranted('ROLE_ADMIN'),
'xferDB' => DatabaseTransferService::isTransferEnabled(),
'meta' => $meta, 'meta' => $meta,
'speakers' => $speakers, 'speakers' => $speakers,
'series' => $series, 'series' => $series,
@@ -91,7 +89,7 @@ class DefaultController extends AbstractController
]); ]);
} }
#[Route('/reference-editor', 'app_reference_editor')] #[Route('/reference-editor', name: 'app_reference_editor')]
public function referenceEditor(EntityManagerInterface $emi): Response public function referenceEditor(EntityManagerInterface $emi): Response
{ {
$this->denyAccessUnlessGranted('ROLE_ADMIN'); $this->denyAccessUnlessGranted('ROLE_ADMIN');
@@ -99,64 +97,10 @@ class DefaultController extends AbstractController
return $this->render('editors/reference-editor.html.twig'); return $this->render('editors/reference-editor.html.twig');
} }
#[Route('/template-editor', 'app_template_editor')] #[Route('/template-editor', name: 'app_template_editor')]
public function templateEditor(): Response public function templateEditor(): Response
{ {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
return $this->render('editors/template-editor.html.twig'); return $this->render('editors/template-editor.html.twig');
} }
#[Route('/xfer-database', name: 'app_admin_transfer_db')]
public function transfer(DatabaseTransferService $service, Request $request): Response
{
$step = $request->query->get('step', 'init');
$session = $request->getSession();
if ($step === 'init') {
$service->createSchema();
$session->remove('transfer_logs');
return $this->redirectToRoute('app_admin_transfer_db', ['step' => 0]);
}
if ($step === 'summary') {
// Finalize
$service->finalizeEnvSwap();
return $this->render('default/transfer_summary.html.twig', [
'logs' => $session->get('transfer_logs', [])
]);
}
$classes = $service->getEntityClasses();
$totalClasses = count($classes);
if (isset($classes[$step])) {
$class = explode("\\", $classes[$step]);
$className = end($class);
$func = "transfer{$className}Table";
if (method_exists($service, $func)) {
$skippedCount = $service->{$func}();
if ($skippedCount > 0) {
$logs = $session->get('transfer_logs', []);
$logs[] = "Skipped $skippedCount orphaned records in the $className table.";
$session->set('transfer_logs', $logs);
}
$progress = round((($step+1) / $totalClasses) * 100);
$nextStep = (($step + 1) < $totalClasses) ? ($step + 1) : 'summary';
return $this->render('default/transfer_progress.html.twig', [
'current' => (isset($classes[$step+1]) ? end(explode("\\", $classes[$step+1])) : 'summary'),
'progress' => $progress,
'next_step' => $nextStep,
]);
}
}
// Step 7: Logout and redirect
return $this->redirectToRoute('app_logout');
}
} }

View File

@@ -14,7 +14,6 @@ use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: 'app_user')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])] #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSerializable class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSerializable
{ {
@@ -72,9 +71,6 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?array $metaData = null; private ?array $metaData = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $homeChurchRSS = null;
public function __construct() public function __construct()
{ {
$this->series = new ArrayCollection(); $this->series = new ArrayCollection();
@@ -327,16 +323,4 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer
return $this; return $this;
} }
public function getHomeChurchRSS(): ?string
{
return $this->homeChurchRSS;
}
public function setHomeChurchRSS(?string $homeChurchRSS): static
{
$this->homeChurchRSS = $homeChurchRSS;
return $this;
}
} }

View File

@@ -1,501 +0,0 @@
<?php
namespace App\Service;
use App\Entity\Bible;
use App\Entity\Note;
use App\Entity\Reference;
use App\Entity\Series;
use App\Entity\Speaker;
use App\Entity\Template;
use App\Entity\User;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Persistence\ManagerRegistry;
use PDO;
use PDOException;
use Symfony\Component\Uid\Uuid;
class DatabaseTransferService
{
private PDO $srcDB;
private PDO $destDB;
public function __construct(
private string $projectDir,
private ManagerRegistry $doctrine
) {
// connect the source database
switch (explode(":", $_ENV['DATABASE_URL'])[0]) {
case 'pdo-sqlite';
list($driverName, $dbFile) = explode(":/", $_ENV['DATABASE_URL']);
$this->srcDB = new PDO('sqlite:'.$dbFile);
break;
case 'pdo-mysql';
case 'pdo-pgsql';
$dsn = $this->convertDBURLString($_ENV['DATABASE_URL']);
$this->srcDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
}
// connect the destination database
switch(explode(':', $_ENV['XFER_DATABASE_URL'])[0]) {
case 'pdo-sqlite':
list($driverName, $dbFile) = explode(":/", $_ENV['XFER_DATABASE']);
$this->destDB = new PDO('sqlite:'.$dbFile);
break;
case 'pdo-mysql':
case 'pdo-pgsql':
$dsn = $this->convertDBURLString($_ENV['XFER_DATABASE_URL']);
$this->destDB = new PDO($dsn['dsn'], $dsn['username'], $dsn['password']);
}
}
public function createSchema(): void
{
$destEm = $this->doctrine->getManager('transfer');
$metadata = $destEm->getMetadataFactory()->getAllMetadata();
$tool = new SchemaTool($destEm);
$tool->dropSchema($metadata);
$tool->createSchema($metadata);
}
public function getEntityClasses(): array
{
return [
User::class,
Bible::class,
Reference::class,
Template::class,
Series::class,
Speaker::class,
Note::class
];
}
public function transferUserTable(): int
{
$sql = "SELECT * FROM user";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO user (id, email, roles, password, name, meta_data, home_church_rss) ".
"VALUES ".
"(:id, :email, :roles, :password, :name, :meta_data, :home_church)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'email' => $row['email'],
'roles' => $row['roles'],
'password' => $row['password'],
'name' => $row['name'],
'meta_data' => $row['meta_data'],
'home_church' => $row['home_church_rss'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferBibleTable(): int
{
$sql = "SELECT * FROM bible";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO bible (id, book, chapter, verse, content, book_index, label) ".
"VALUES ".
"(:id, :book, :chapter, :verse, :content, :book_index, :label)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$uuid = Uuid::fromString($row['id']);
$params = [
'id' => $uuid->toBinary(),
'book' => $row['book'],
'chapter' => $row['chapter'],
'verse' => $row['verse'],
'content' => $row['content'],
'book_index' => $row['book_index'],
'label' => $row['label'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferReferenceTable(): int
{
$sql = "SELECT * FROM reference";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO reference (id, type, name, label, ndx, content) ".
"VALUES ".
"(:id, :type, :name, :label, :ndx, :content)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'type' => $row['type'],
'name' => $row['name'],
'label' => $row['label'],
'ndx' => $row['ndx'],
'content' => $row['content'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferTemplateTable(): int
{
$sql = "SELECT * FROM template";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO template (id, user_id, name, content) ".
"VALUES ".
"(:id, :user_id, :name, :content)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'user_id' => $row['user_id'],
'name' => $row['name'],
'content' => $row['content'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferSpeakerTable(): int
{
$sql = "SELECT * FROM speaker";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO speaker (id, name, user_id) ".
"VALUES ".
"(:id, :name, :user_id)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'name' => $row['name'],
'user_id' => $row['user_id'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferSeriesTable(): int
{
$sql = "SELECT * FROM series";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO series (id, name, user_id, template_id) ".
"VALUES ".
"(:id, :name, :user_id, :template_id)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'name' => $row['name'],
'user_id' => $row['user_id'],
'template_id' => $row['template_id'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function transferNoteTable(): int
{
$sql = "SELECT * FROM note";
$stmt = $this->srcDB->prepare($sql);
$stmt->execute();
$insQuery = "INSERT INTO note (id, title, date, passage, refs, text, speaker_id, series_id, user_id, recording) ".
"VALUES ".
"(:id, :title, :date, :passage, :refs, :text, :speaker_id, :series_id, :user_id, :recording)";
$destStmt = $this->destDB->prepare($insQuery);
$skippedCount = 0;
$this->destDB->beginTransaction();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$params = [
'id' => $row['id'],
'speaker_id' => $row['speaker_id'],
'series_id' => $row['series_id'],
'user_id' => $row['user_id'],
'title' => $row['title'],
'date' => $row['date'],
'passage' => $row['passage'],
'refs' => $row['refs'],
'text' => $row['text'],
'recording' => $row['recording'],
];
$this->destDB->exec("SAVEPOINT row_insert");
try {
$destStmt->execute($params);
$this->destDB->exec("RELEASE SAVEPOINT row_insert");
} catch (PDOException $e) {
$skipCodes = [
'23000',
'23503',
'23505',
];
if (in_array($e->getCode(), $skipCodes, true)) {
$this->destDB->exec("ROLLBACK TO SAVEPOINT row_insert");
$skippedCount++; // 4. Increment the counter
continue;
}
$this->destDB->rollBack();
print_r($params);
die("Critical Error: ".$e->getMessage());
}
}
$this->destDB->commit();
return $skippedCount;
}
public function finalizeEnvSwap(): void
{
$envPath = $this->projectDir . '/.env';
if (!file_exists($envPath)) {
throw new \Exception(".env file not found at $envPath");
}
$lines = file($envPath);
$newLines = [];
$xferUrlValue = null;
// 1. First pass: Find the value of XFER_DATABASE_URL
foreach ($lines as $line) {
if (preg_match('/^XFER_DATABASE_URL=(.*)/', trim($line), $matches)) {
$xferUrlValue = $matches[1];
break;
}
}
if (!$xferUrlValue) {
throw new \Exception("XFER_DATABASE_URL not found in .env file.");
}
// 2. Second pass: Perform the swap
foreach ($lines as $line) {
$trimmed = trim($line);
// Prepend #OLD_ to the existing DATABASE_URL
if (preg_match('/^DATABASE_URL=/', $trimmed)) {
$newLines[] = "# OLD_" . $line;
continue;
}
// Rename XFER_DATABASE_URL to DATABASE_URL
if (preg_match('/^XFER_DATABASE_URL=/', $trimmed)) {
$newLines[] = "DATABASE_URL=" . $xferUrlValue . PHP_EOL;
continue;
}
$newLines[] = $line;
}
file_put_contents($envPath, implode('', $newLines));
}
public static function isTransferEnabled(): bool
{
return (isset($_ENV['XFER_DATABASE_URL']) && !empty($_ENV['XFER_DATABASE_URL']));
}
private function convertDBURLString(string $DBURL): array
{
$dsn = preg_split("/[:|\/|\@]+/", $DBURL);
if (count($dsn) > 6) {
die('You cannot have special characters in your password for the database entry during this process. Change the database user password to just use alpha-numeric characters, then run again. You can change it back to more secure after this transfer is complete');
}
$res = [
'dsn' => substr($dsn[0], 4).':'.
'host='.$dsn[3].';'.
'port='.$dsn[4].';'.
'dbname='.$dsn[5],
'username' => $dsn[1],
'password' => $dsn[2]
];
return $res;
}
}

View File

@@ -32,58 +32,4 @@ class Utils
// error message or try to resend the message // error message or try to resend the message
} }
} }
public static function filePerms($file): string
{
$perms = fileperms($file);
switch ($perms & 0xF000) {
case 0xC000: // socket
$info = 's';
break;
case 0xA000: // symbolic link
$info = 'l';
break;
case 0x8000: // regular
$info = 'r';
break;
case 0x6000: // block special
$info = 'b';
break;
case 0x4000: // directory
$info = 'd';
break;
case 0x2000: // character special
$info = 'c';
break;
case 0x1000: // FIFO pipe
$info = 'p';
break;
default: // unknown
$info = 'u';
}
// Owner
$info .= (($perms & 0x0100) ? 'r' : '-');
$info .= (($perms & 0x0080) ? 'w' : '-');
$info .= (($perms & 0x0040) ?
(($perms & 0x0800) ? 's' : 'x' ) :
(($perms & 0x0800) ? 'S' : '-'));
// Group
$info .= (($perms & 0x0020) ? 'r' : '-');
$info .= (($perms & 0x0010) ? 'w' : '-');
$info .= (($perms & 0x0008) ?
(($perms & 0x0400) ? 's' : 'x' ) :
(($perms & 0x0400) ? 'S' : '-'));
// World
$info .= (($perms & 0x0004) ? 'r' : '-');
$info .= (($perms & 0x0002) ? 'w' : '-');
$info .= (($perms & 0x0001) ?
(($perms & 0x0200) ? 't' : 'x' ) :
(($perms & 0x0200) ? 'T' : '-'));
return $info;
}
} }

View File

@@ -1,13 +1,4 @@
{ {
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
}
},
"doctrine/doctrine-bundle": { "doctrine/doctrine-bundle": {
"version": "2.12", "version": "2.12",
"recipe": { "recipe": {
@@ -100,18 +91,6 @@
".env" ".env"
] ]
}, },
"symfony/form": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.2",
"ref": "7d86a6723f4a623f59e2bf966b6aad2fc461d36b"
},
"files": [
"config/packages/csrf.yaml"
]
},
"symfony/framework-bundle": { "symfony/framework-bundle": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {
@@ -203,18 +182,6 @@
"tests/bootstrap.php" "tests/bootstrap.php"
] ]
}, },
"symfony/property-info": {
"version": "8.0",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"config/packages/property_info.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.0", "version": "7.0",
"recipe": { "recipe": {

View File

@@ -8,7 +8,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<link rel='manifest' href='{{ asset('manifest.json') }}'>
<title>{% block title %}Welcome!{% endblock %}</title> <title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %} {% block stylesheets %}{% endblock %}
@@ -16,19 +15,5 @@
<body class='is-preload' onload='{% if onLoad is defined %}{{ onLoad }}{% endif %}'> <body class='is-preload' onload='{% if onLoad is defined %}{{ onLoad }}{% endif %}'>
{% block body %}{% endblock %} {% block body %}{% endblock %}
{% block javascripts %}{% endblock %} {% block javascripts %}{% endblock %}
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(error => {
console.log('ServiceWorker registration failed: ', error);
});
});
}
</script>
</body> </body>
</html> </html>

View File

@@ -181,7 +181,7 @@ let saveFailureCount = {{ meta.saveFailureCount }};
<!-- The modal body --> <!-- The modal body -->
<form id="emailForm" class="modal-body"> <form id="emailForm" class="modal-body">
<label for="shareEmail">Enter Friends Email:</label> <label for="shareEmail">Enter Friends Email:</label>
<input type="email" id="shareEmail" name="email" autocomplete=false required /> <input type="email" id="shareEmail" name="email" required />
<button type='button' id="submit" class="btn btn-primary" onclick='shareNote()'>Submit</button> <button type='button' id="submit" class="btn btn-primary" onclick='shareNote()'>Submit</button>
</form> </form>

View File

@@ -6,7 +6,7 @@
<link href="{{ asset('css/main.css') }}" rel="stylesheet" /> <link href="{{ asset('css/main.css') }}" rel="stylesheet" />
<link href="{{ asset('css/jquery-ui.theme.css') }}" rel='stylesheet' /> <link href="{{ asset('css/jquery-ui.theme.css') }}" rel='stylesheet' />
<link href="{{ asset('css/jquery-ui.structure.css') }}" rel='stylesheet' /> <link href="{{ asset('css/jquery-ui.structure.css') }}" rel='stylesheet' />
<link href="{{ asset('styles/style.css') }}" rel='stylesheet' /> <link href="{{ asset('css/style.css') }}" rel='stylesheet' />
<link href='//cdn.datatables.net/2.0.8/css/dataTables.dataTables.min.css' rel='stylesheet' /> <link href='//cdn.datatables.net/2.0.8/css/dataTables.dataTables.min.css' rel='stylesheet' />
<style> <style>
.flex-container { .flex-container {
@@ -80,49 +80,6 @@ $(function() {
}); });
function saveProfile() {
const name = $('#name');
const email = $('#email');
const homeChurch = $('#home-church');
const password = $('#password');
const newPassword = $('#new-password');
const confPassword = $('#conf-password');
let passChange = false;
if (newPassword.val().length > 0) {
if (password.val().length == 0) {
alert('If you want to change your password you need to put in the current password as well');
return;
}
if (newPassword.val() != confPassword.val()) {
alert('New password and confirm passwords do not match');
return;
}
passChange = true;
}
fetch('/save-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'name': name.val(),
'email': email.val(),
'homeChurch': homeChurch.val(),
'passChange': passChange,
'password': password.val(),
'newPassword': newPassword.val(),
'confPassword': confPassword.val()
})
})
.then(response => response.json())
.then(results => {
alert(results.msg);
});
}
function saveSettings() { function saveSettings() {
var saveInterval = $('#save-interval'); var saveInterval = $('#save-interval');
var saveReferences = $('#save-references'); var saveReferences = $('#save-references');
@@ -171,9 +128,6 @@ function rollUp(cont) {
<label for='email'>Email: </label> <label for='email'>Email: </label>
<input type='email' id='email' name='email' value='{{ app.user.email }}' /><br /> <input type='email' id='email' name='email' value='{{ app.user.email }}' /><br />
<label for='home-church'>Home Church RSS Feed: </label>
<input type='text' id='home-church' name='home-church' value='{{ app.user.homeChurchRSS }}' /><br />
<label for='password'>Password: </label> <label for='password'>Password: </label>
<input type='password' id='password' name='password' /><br/> <input type='password' id='password' name='password' /><br/>

View File

@@ -25,9 +25,6 @@
<li><a href="#" onclick="saveNote()">Save Note</a></li> <li><a href="#" onclick="saveNote()">Save Note</a></li>
{% if isAdmin is defined and isAdmin %} {% if isAdmin is defined and isAdmin %}
<li><a href='/reference-editor'>Reference Editor</a></li> <li><a href='/reference-editor'>Reference Editor</a></li>
{% if xferDB is defined and xferDB %}
<li><a href='/xfer-database'>Transfer Database</a></li>
{% endif %}
{% endif %} {% endif %}
<li><a href='#' onclick="openRef()">Open Reference</a></li> <li><a href='#' onclick="openRef()">Open Reference</a></li>
<li><a href='/template-editor'>Template Editor</a></li> <li><a href='/template-editor'>Template Editor</a></li>

View File

@@ -1,24 +0,0 @@
{% extends 'base.html.twig' %}
{% block body %}
<div class="container mt-5 text-center">
<h2>Transferring Database...</h2>
<p>Processing: <strong>{{ current }}</strong></p>
<div class="progress mb-3" style="height: 30px;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: {{ progress }}%;">
{{ progress }}%
</div>
</div>
<p>Please do not close this window.</p>
</div>
<script>
// Automatically move to the next step after a short delay
setTimeout(() => {
window.location.href = "{{ path('app_admin_transfer_db', {step: next_step}) }}";
}, 500);
</script>
{% endblock %}

View File

@@ -1,57 +0,0 @@
{% extends 'base.html.twig' %}
{% block stylesheets %}
<link href="{{ asset('css/main.css') }}" rel="stylesheet" />
<link href='{{ asset('css/jquery-ui.theme.css') }}' rel='stylesheet' />
<link href='{{ asset('css/jquery-ui.structure.css') }}' rel='stylesheet' />
<link href='{{ asset('styles/style.css') }}' rel='stylesheet' />
<link href='//cdn.datatables.net/2.0.8/css/dataTables.dataTables.min.css' rel='stylesheet' />
<style>
button.button i {
font-size: 1.5em;
}
.input-error {
border: solid 2px red !important;
}
</style>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('js/jquery.min.js') }}"></script>
<script src='{{ asset('js/jquery-ui.js') }}'></script>
<script src="{{ asset('js/browser.min.js') }}"></script>
<script src="{{ asset('js/breakpoints.min.js') }}"></script>
<script src="{{ asset('js/util.js') }}"></script>
<script src="{{ asset('js/main.js') }}"></script>
<script src='//momentjs.com/downloads/moment-with-locales.js'></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/markdown-it/13.0.2/markdown-it.min.js" integrity="sha512-ohlWmsCxOu0bph1om5eDL0jm/83eH09fvqLDhiEdiqfDeJbEvz4FSbeY0gLJSVJwQAp0laRhTXbUQG+ZUuifUQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src='//cdn.datatables.net/2.0.8/js/dataTables.min.js'></script>
{% endblock %}
{% block body %}
<div class="container mt-5 text-center">
<h2 class="text-success">Database Transfer Complete!</h2>
<p>Your data has been successfully moved to the new database, and the environment variables have been updated.</p>
{% if logs|length > 0 %}
<div class="alert alert-warning text-start mx-auto" style="max-width: 600px;">
<h5 class="alert-heading">Data Clean-up Notice</h5>
<p>During the transfer, the following orphaned records were safely ignored to maintain database integrity:</p>
<ul>
{% for log in logs %}
<li>{{ log }}</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="alert alert-success mx-auto" style="max-width: 600px;">
Perfect transfer! No orphaned records were found.
</div>
{% endif %}
<div class="mt-4">
<p class="text-muted">You must log in again to establish a connection with the new database.</p>
<a href="{{ path('app_logout') }}" class="btn btn-primary btn-lg">Acknowledge & Relogin</a>
</div>
</div>
{% endblock %}