11 Commits

Author SHA1 Message Date
d06f24b1fa upd: utils
Add method to get file permissions
2026-02-16 14:16:37 -05:00
ed774a5a37 upd: profile twig
add saveProfile method and required support
2026-02-16 14:15:51 -05:00
6664a7c71e upd: user
add home church rss field
2026-02-16 14:15:10 -05:00
4be33834d4 upd: ajaxcontroller
add method to save profile and include home church rss feed url
2026-02-16 14:14:42 -05:00
b445295959 upd: database
add home church rss url for users
2026-02-16 14:13:56 -05:00
20ba17c684 git: ignore
ignore composer.lock file
2026-02-16 14:12:44 -05:00
323e668ac9 upd: dockerfile
install logrotate and cron to accomplish scripting to retrieve audio recording links for sermon
2026-02-16 14:12:09 -05:00
0d384a8fa3 fix: install
fix typo
2026-02-16 14:10:51 -05:00
b14a0c23f6 add: app:get-audio
Script command to crawl RSS feed and find uploaded recordings to update the notes
2026-01-16 13:54:45 -05:00
50cf4800fd upd: update for 1.2 processes 2025-12-05 13:39:01 -05:00
289bfc1a3f upd: split install functions 2025-12-05 13:38:10 -05:00
11 changed files with 621 additions and 84 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@
###< symfony/asset-mapper ###
/references/
composer.lock

View File

@@ -1,4 +1,4 @@
FROM php:8.4-apache
FROM php:8.5-apache
RUN apt update && \
apt upgrade -y && \
@@ -14,8 +14,13 @@ RUN apt update && \
sqlite3 \
curl \
git \
cron \
logrotate \
nano
RUN service start cron
RUN service enable cron
RUN docker-php-ext-configure gd --with-jpeg
RUN docker-php-ext-configure zip
@@ -46,6 +51,19 @@ RUN rm -rf /var/www/html/vendor
RUN rm -rf /var/www/html/tests
RUN rm -rf /var/www/html/translations
RUN echo "20 1 * * 6 root cd /var/www/html && /usr/local/bin/php bin/console app:get-audio > /var/log/sermon-notes.log 2>&1" > /etc/cron.d/get-audio
RUN chmod 644 /etc/cron.d/get-audio
RUN echo "/var/log/sermon-notes.log {
monthly
rotate 12
compress
delaycompress
missingok
notifempty
create 644 root root
}" > /etc/logrotate.d/sermon-notes
RUN COMPOSER_ALLOW_SUPERUSER=1 composer install --no-scripts --no-dev --optimize-autoloader
RUN mkdir /data

View File

@@ -7,21 +7,33 @@ 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
1. You need to make sure that you have a running SSH server on your host computer
2. On your host computer, `docker exec -it sermon-notes bash`
3. `cd var/`
4. `scp data.db {user}@{host computer IP}:{path}`
2. Make a directory in your `sermon-notes` folder for your database (e.g. `data`)
3. On your host computer, `docker exec -it sermon-notes bash`
4. `scp .env {user}@{IP}:{path}`
5. Authenticate with the password
6. This will copy the file over SFTP to the host computer
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.
- 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`
6. `cd var/`
7. `scp data.db {user}@{IP}:{path}/data`
8. This will copy the file to the host computer
9. After this then you run the `docker run...` command in Step 3 of the `Installation` instructions below
## Installation
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.
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.
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.
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. Download your preferred compose file (`wget -O compose.yml {link}`)
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
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
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 /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 /var/www/html/install.php`. This will run the `php composer` to populate the database with all the desired reference material.
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

View File

@@ -2,88 +2,22 @@
<?php
if (!file_exists('/var/www/html/.env')) {
die;
}
$cmd = getopt("", ["sqlite", "mysql", "mariadb", "pgsql"]);
$key = `openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '`;
$key = substr($key, 0, 32);
$database_url = null;
$getCreds = true;
$creds = null;
if (isset($cmd['sqlite'])) {
$database_url = "DATABASE_URL=\"sqlite:////data/data.db\"";
$getCreds = false;
} elseif (isset($cmd['mysql'])) {
$database_url = "DATABASE_URL=\"mysql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?charset=utf8&use_unicode=1\"";
} elseif (isset($cmd['mariadb'])) {
$database_url = "DATABASE_URL=\"mysql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?charset=utf8mb4\"";
} elseif (isset($cmd['pgsql'])) {
$database_url = "DATABASE_URL=\"postgresql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?sslmode=require\"";
}
if (is_null($database_url)) {
$getCreds = false;
die("When calling this make sure that you enter a database type");
}
if ($getCreds) {
$db_host = readline("DB Host: ");
$db_port = readline("DB Port: ");
$db_name = readline("DB Schema: ");
$db_user = readline("DB User: ");
print "DB Password: ";
// Disable echoing of input characters
system('stty -echo');
// Read the password from standard input
$db_password = trim(fgets(STDIN));
// Re-enable echoing of input characters
system('stty echo');
$creds = <<<CREDS
DB_HOST=$db_host
DB_PORT=$db_port
DB_NAME=$db_name
DB_USER=$db_user
DB_PASS=$db_password
CREDS;
}
$output = <<<EOF
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=$key
MESSAGENER_TRANSPORT_DSN=doctrine://default?auto_setup=0
$creds$database_url
EOF;
file_put_contents('/var/www/html/.env', $output);
print "Updating packages and compiling assets".PHP_EOL;
`COMPOSE_ALLOW_SUPERUSER=1 composer update`;
`symfony console asset-map:compile`;
if ($getCreds) {
print "Creating database schema".PHP_EOL;
`symfony console doctrine:database:create`;
}
print "Creating database schema".PHP_EOL;
`symfony console doctrine:database:create`;
print "Updating migrations and setting permissions for data folder".PHP_EOL;
`symfony console doctrine:migrations:migrate --no-interaction`;
if (isset($cmd['sqlite'])) {
`chown -R www-data:www-data /data`;
}
`chown -R www-data:www-data /data`;
// import reference material
print "Importing Bible and Eccumenical Creeds".PHP_EOL;
`symfony console app:ingest-bible /var/www/html/reference/esv-bible`;
`symfony console app:ingest-bible /var/www/html/references/esv-bible`;
`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/Athanasian 'Athanasian Creed' creed ath`;
`symfony console app:import-ref /var/www/html/references/creeds/Chalcedon 'Definition of Chalcedon' creed dc`;
@@ -116,13 +50,13 @@ if ($westminsterStandards) {
`symfony console app:import-wlc /var/www/html/references/wlc 'Westminster Larger' wlc WLC{\$ndx}`;
}
$helviticConfessions = (
$helveticConfessions = (
strtolower(
readline("Do you want to import the Helvetic Confessions (1st & 2nd) (y/n)? ")
) == 'y'
);
if ($helviticConfessions) {
if ($helveticConfessions) {
print "Importing Helvitic standards".PHP_EOL;
`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/shc 'Second Helvetic Confession' 2hc 2HC{\$ndx}`;

View File

@@ -0,0 +1,39 @@
<?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 Version20260114224910 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 home_church_rss 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__user AS SELECT id, email, roles, password, name, meta_data 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, meta_data CLOB DEFAULT NULL --(DC2Type:json)
, PRIMARY KEY(id))');
$this->addSql('INSERT INTO user (id, email, roles, password, name, meta_data) SELECT id, email, roles, password, name, meta_data FROM __temp__user');
$this->addSql('DROP TABLE __temp__user');
$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON user (email)');
}
}

94
setup.php Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/local/bin/php
<?php
if (!file_exists('/var/www/html/.env')) {
die;
}
$cmd = getopt("", ["sqlite", "mysql", "mariadb", "pgsql", "shared"]);
$key = `openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '`;
$key = substr($key, 0, 32);
$database_url = null;
$getCreds = true;
$creds = null;
$http_port = readline("What port do you want the server to listen on (80)? ");
$http_port = (empty($http_port) ? 80 : $http_port);
if (isset($cmd['sqlite'])) {
$database_url = "DATABASE_URL=\"sqlite:////data/data.db\"";
$getCreds = false;
} elseif (isset($cmd['mysql'])) {
$database_url = "DATABASE_URL=\"mysql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?charset=utf8&use_unicode=1\"";
} elseif (isset($cmd['mariadb'])) {
$database_url = "DATABASE_URL=\"mysql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?charset=utf8mb4\"";
} elseif (isset($cmd['pgsql'])) {
$database_url = "DATABASE_URL=\"postgresql://\${DB_USER}:\${DB_PASS}@\${DB_HOST}:\${DB_PORT}/\${DB_NAME}?sslmode=require\"";
}
if (is_null($database_url)) {
$getCreds = false;
die("When calling this make sure that you enter a database type");
}
if ($getCreds) {
$dbInfo = null;
if (isset($cmd['shared'])) {
$db_host = readline("DB Host: ");
$db_port = readline("DB Port: ");
$db_name = readline("DB Schema: ");
$db_user = readline("DB User: ");
print "DB Password: ";
// Disable echoing of input characters
system('stty -echo');
// Read the password from standard input
$db_password = trim(fgets(STDIN));
// Re-enable echoing of input characters
system('stty echo');
} else {
$db_host = 'db';
$db_port = (isset($cmd['pgsql']) ? 5432 : 3306);
$db_name = 'sermon_notes';
$db_user = 'root';
$pwd = `openssl rand -base64 32 | tr -d '=' | tr -d '+' | tr -d '/' | tr -d ' '`;
$db_password = substr($pwd, 0, 32);
if (isset($cmd['pgsql'])) {
$dbInfo = <<<INFO
POSTGRES_USER=$db_user
POSTGRES_PASSWORD=$db_password
INFO;
} elseif (isset($cmd['mysql']) || isset($cmd['mariadb'])) {
$dbInfo = <<<INFO
MYSQL_ROOT_PASSWORD=$db_password
INFO;
}
}
$creds = <<<CREDS
DB_HOST=$db_host
DB_PORT=$db_port
DB_NAME=$db_name
DB_USER=$db_user
DB_PASS=$db_password
$dbInfo
CREDS;
}
$output = <<<EOF
APP_ENV=prod
APP_DEBUG=0
APP_SECRET=$key
MESSAGENER_TRANSPORT_DSN=doctrine://default?auto_setup=0
HTTP_PORT=$http_port
$creds$database_url
EOF;
file_put_contents('/var/www/html/.env', $output);

View File

@@ -0,0 +1,282 @@
<?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

@@ -20,6 +20,7 @@ use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Attribute\Route;
@@ -453,6 +454,47 @@ class AjaxController extends AbstractController
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'])]
public function saveSettings(Request $req, EntityManagerInterface $emi): Response
{

View File

@@ -71,6 +71,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer
#[ORM\Column(nullable: true)]
private ?array $metaData = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $homeChurchRSS = null;
public function __construct()
{
$this->series = new ArrayCollection();
@@ -323,4 +326,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, JsonSer
return $this;
}
public function getHomeChurchRSS(): ?string
{
return $this->homeChurchRSS;
}
public function setHomeChurchRSS(?string $homeChurchRSS): static
{
$this->homeChurchRSS = $homeChurchRSS;
return $this;
}
}

View File

@@ -32,4 +32,58 @@ class Utils
// 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

@@ -6,7 +6,7 @@
<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('css/style.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>
.flex-container {
@@ -80,6 +80,49 @@ $(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() {
var saveInterval = $('#save-interval');
var saveReferences = $('#save-references');
@@ -128,6 +171,9 @@ function rollUp(cont) {
<label for='email'>Email: </label>
<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>
<input type='password' id='password' name='password' /><br/>