Compare commits
10 Commits
bb8e7f359f
...
1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| d06f24b1fa | |||
| ed774a5a37 | |||
| 6664a7c71e | |||
| 4be33834d4 | |||
| b445295959 | |||
| 20ba17c684 | |||
| 323e668ac9 | |||
| 0d384a8fa3 | |||
| b14a0c23f6 | |||
| 50cf4800fd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@
|
||||
###< symfony/asset-mapper ###
|
||||
|
||||
/references/
|
||||
composer.lock
|
||||
20
Dockerfile
20
Dockerfile
@@ -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
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ print "Updating migrations and setting permissions for data folder".PHP_EOL;
|
||||
// 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`;
|
||||
@@ -50,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}`;
|
||||
|
||||
39
migrations/Version20260114224910.php
Normal file
39
migrations/Version20260114224910.php
Normal 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)');
|
||||
}
|
||||
}
|
||||
282
src/Command/GetAudioCommand.php
Normal file
282
src/Command/GetAudioCommand.php
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user