Compare commits

...

67 commits

Author SHA1 Message Date
Simon Vieille 4fd78d1303
add builder parser for rss
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2024-05-29 11:35:22 +02:00
Simon Vieille 7e6d230e17
add builder as post editor
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2024-05-17 22:11:13 +02:00
Simon Vieille b86f3096e1
fix assets upgrade 2024-05-16 20:50:36 +02:00
Simon Vieille ecb4ca177e
change markdown editor
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
fix editorjs
2024-05-13 21:01:11 +02:00
Simon Vieille c8d99da2c2
change h1 font
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2024-04-18 22:48:19 +02:00
Simon Vieille 22ec3d036e
add border color on md editor
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2024-03-31 19:39:44 +02:00
Simon Vieille 676e9dfe67
update steps 2024-03-03 19:34:27 +01:00
Simon Vieille 508a816642 change aspect of header h1
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2024-01-27 19:47:49 +01:00
Simon Vieille 476dece659
remove novops
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-01-10 10:08:20 +01:00
Simon Vieille 1a7ea2e5a2
fix deploy step
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
ci/woodpecker/manual/build Pipeline failed
2023-12-06 21:44:18 +01:00
Simon Vieille 6fa3aafed0
fix deploy step
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-12-06 21:37:08 +01:00
Simon Vieille 1041eba4cb
fix deploy step
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-12-06 21:30:32 +01:00
Simon Vieille 698c356c0e
fix deploy step
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-12-06 21:08:19 +01:00
Simon Vieille 86558dc76d Merge branch 'feature/vault' into develop
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-12-06 20:24:14 +01:00
Simon Vieille 2ee645bdab
update ci config
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-12-06 20:20:10 +01:00
Simon Vieille 83f7946d02
update ci config
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-12-06 20:18:21 +01:00
Simon Vieille 0e0f2688c7
update novops conf
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-12-06 19:41:35 +01:00
Simon Vieille 60c60016a1
update novops conf
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-12-06 19:40:30 +01:00
Simon Vieille 8eb6ba303c
add novops conf
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-12-06 19:38:45 +01:00
Simon Vieille d83adf0473
update ci
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/manual/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2023-11-10 11:30:05 +01:00
Simon Vieille b9f5785fa6
replace stop action in messenger
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
ci/woodpecker/manual/build Pipeline was successful
2023-10-18 22:48:00 +02:00
Simon Vieille 0d1f1e29b5
replace stop action in messenger
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-10-18 22:38:22 +02:00
Simon Vieille 28f8e5c583
replace stop action in messenger 2023-10-18 22:33:33 +02:00
Simon Vieille 1f9161feef
update build step
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-10-18 22:17:06 +02:00
Simon Vieille 7800a01499
update build step
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-10-18 22:16:10 +02:00
Simon Vieille bbb1804f31
update deploy step
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-10-18 22:07:55 +02:00
Simon Vieille a273a9aac1
update deploy step 2023-10-18 22:07:20 +02:00
Simon Vieille 6e005690da
fix project template
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-10-13 00:05:15 +02:00
Simon Vieille b324bfcd97
fix ci syntax 2023-10-07 16:43:47 +02:00
Simon Vieille 850ebff7c0
fix ci syntax
Some checks are pending
ci/woodpecker/push/build Pipeline is pending
2023-09-29 16:02:54 +02:00
Simon Vieille 5b4b2b59be
refactoring 2023-09-28 21:29:51 +02:00
Simon Vieille 1a4bcf8755
upgrade search engine
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2023-09-24 19:35:27 +02:00
Simon Vieille b11da225fb
upgrade search engine
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-09-24 19:28:27 +02:00
Simon Vieille 138b4f24ee
upgrade search engine
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-09-24 18:59:33 +02:00
Simon Vieille 45bd1b408d
update ci
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2023-09-23 23:09:01 +02:00
Simon Vieille dfecbb0ea6
update ci
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-09-23 22:54:21 +02:00
Simon Vieille ab692fe082
add margin to graph on dashboard
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-09-23 12:36:49 +02:00
Simon Vieille c93754ecff
update dashboard including grafana
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2023-09-23 12:10:01 +02:00
Simon Vieille 869974c49b
fix missing variable in messenger wrapper 2023-09-23 12:09:46 +02:00
Simon Vieille 89cc5623d3
update mage conf
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-09-23 00:18:41 +02:00
Simon Vieille b8cdec4d71
update messenger script
Some checks failed
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline failed
2023-09-23 00:08:32 +02:00
Simon Vieille d51366a5c6
update deploy steps (messenger)
Some checks failed
ci/woodpecker/push/build Pipeline failed
2023-09-23 00:05:24 +02:00
Simon Vieille bfe3599054
use messenger to push influxdb stats 2023-09-23 00:03:48 +02:00
Simon Vieille 4a84b2db98 Merge branch 'feature/redis-requests' into develop
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/deployment/deploy Pipeline was successful
2023-09-22 22:07:26 +02:00
Simon Vieille a9fe3c488f
add influxdb and stat of page view 2023-09-22 22:07:22 +02:00
Simon Vieille f81e2a99a7
update ci 2023-09-22 22:07:17 +02:00
Simon Vieille 5612ea2acd
add influxdb and stat of page view 2023-09-22 22:06:56 +02:00
Simon Vieille f4169d20e9
add influxdb and stat of page view 2023-09-22 22:06:44 +02:00
Simon Vieille d39759f88e Merge branch 'feature/build' into develop
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-09-13 23:04:42 +02:00
Simon Vieille 86701a5578
remove cache-clean step 2023-09-13 23:04:35 +02:00
Simon Vieille eec59acc29
[wip] deploy task
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline failed
2023-09-13 21:25:20 +02:00
Simon Vieille b60a1acd57
[wip] deploy task
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline failed
2023-09-13 21:18:22 +02:00
Simon Vieille dbbe5d1db5
[wip] tasks
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline failed
2023-09-13 21:12:13 +02:00
Simon Vieille 35fb777eaa
[wip] events
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-09-13 21:10:50 +02:00
Simon Vieille e70764e63d
[wip] events 2023-09-13 21:10:18 +02:00
Simon Vieille ed8319f727
[wip] tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline failed
2023-09-13 20:57:58 +02:00
Simon Vieille 40670b6bfc
[wip] tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-09-13 20:55:40 +02:00
Simon Vieille d8f3979861
[wip] tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-13 20:53:15 +02:00
Simon Vieille 4bb2cc557f
[wip] tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-13 20:51:41 +02:00
Simon Vieille 9e951916a6
[wip] add volume for the build
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2023-09-13 20:50:20 +02:00
Simon Vieille 8ecb025df8
update view of forms
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
ci/woodpecker/cron/woodpecker Pipeline failed
2023-09-13 20:20:49 +02:00
Simon Vieille 3d6acca60a
add deprecated post feature
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
2023-09-10 22:49:31 +02:00
Simon Vieille bad87d913e
fix author block render
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
2023-08-19 18:23:20 +02:00
Simon Vieille dedf105f06
update footer setting type
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
2023-08-15 11:02:27 +02:00
Simon Vieille fccf54ed5a
fix liip
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
add constraints
2023-07-30 16:58:31 +02:00
Simon Vieille 53aa16951e
fix liip
add constraints
2023-07-30 16:58:26 +02:00
Simon Vieille 70cef70bf4
update murph
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/manual/woodpecker Pipeline was successful
ci/woodpecker/deployment/woodpecker Pipeline was successful
2023-07-27 18:32:55 +02:00
48 changed files with 6331 additions and 33550 deletions

7
.env
View file

@ -35,3 +35,10 @@ MAILER_SENDER=example@localhost
ASSET_BASE_URL=null
UMAMI_URL=null
MESSENGER_TRANSPORT_DSN=doctrine://default
INFLUXDB_URL=
INFLUXDB_TOKEN=
INFLUXDB_BUCKET=
INFLUXDB_ORG=
INFLUXDB_DEBUG=1

View file

@ -18,6 +18,7 @@ magephp:
- "/var/cache/*"
- "/var/log/*"
- "/public/media"
- "/.secrets"
hosts:
- ssh_host
on-deploy:
@ -25,3 +26,4 @@ magephp:
- exec: { cmd: 'make doctrine-migration', desc: 'migration' }
- exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup' }
- exec: { cmd: 'php8.1 ./bin/console cache:warmup', desc: 'warmup2' }
- exec: { cmd: './bin/messenger -a restart', desc: 'messenger' }

39
.novops.yml Normal file
View file

@ -0,0 +1,39 @@
environments:
build:
variables:
- name: MYSQLDUMP
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: mysqldump
deploy:
variables:
- name: SSH_USER
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_user
- name: SSH_HOST
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_host
- name: SSH_PRIV_KEY
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: ssh_priv_key
- name: APP_DIRECTORY
value:
hvault_kv2:
mount: kv
path: deblan/deblan.io-murph
key: app_directory

View file

@ -1,54 +1,51 @@
variables:
- &volumes
- node16_cache:/root/.npm
volumes: &volumes
- node_cache:/root/.npm
- /data/${CI_REPO}:/builds
pipeline:
db-wait:
when:
event: [push, pull_request, tag, manual]
branch: [master, master-*, develop, develop-*, feature/*]
steps:
"Wait the database":
image: gitnet.fr/deblan/timeout:latest
commands:
- /bin/timeout -t 30 -v -c 'while true; do nc -z -v db 3306 2>&1 | grep succeeded && exit 0; sleep 0.5; done'
db-create:
"Create database":
image: mariadb:10.3
secrets: [mysqldump]
commands:
- mysql -hdb -uroot -proot -e "CREATE DATABASE app"
- eval "$MYSQLDUMP" | mysql -hdb -uroot -proot app
when:
branch: [master, master-*, develop, develop-*]
app-config:
"Configure app":
image: deblan/php:8.1
commands:
- echo APP_ENV=prod >> .env.local
- echo APP_SECRET=$(openssl rand -hex 32) >> .env.local
- echo DATABASE_URL=mysql://root:root@db/app >> .env.local
when:
branch: [master, master-*, develop, develop-*]
php-composer:
"Installs PHP dependencies":
image: deblan/php:8.1
commands:
- apt-get update && apt-get -y install git
- composer install --no-scripts
db-migrate:
"Migrates database":
image: deblan/php:8.1
environment:
- PHP=php
commands:
- ./bin/doctrine-migrate
when:
branch: [master, master-*, develop, develop-*, feature/*]
app-jsroutes:
"Generates JS routes":
image: deblan/php:8.1
commands:
- php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json
when:
branch: [master, master-*, develop, develop-*, feature/*]
node-build:
"Build assets":
image: node:16-alpine
environment:
- CPU_COUNT=3
@ -63,29 +60,18 @@ pipeline:
- test -f public/js/fos_js_routes.json || echo "{}" > public/js/fos_js_routes.json
- npm run build
security-check:
"Check dependencies":
image: gitnet.fr/deblan/osv-detector:v0.9
commands:
- osv-detector composer.lock yarn.lock
failure: ignore
app-deploy:
image: deblan/php:8.1
secrets: [ssh_user, ssh_host, ssh_priv_key, app_directory]
"Build the cache":
image: deblan/mage
volumes: *volumes
commands:
- apt-get update && apt-get -y install rsync openssh-client
- mkdir "$HOME/.ssh"
- echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519"
- chmod 700 "$HOME/.ssh"
- chmod 600 "$HOME/.ssh/id_ed25519"
- composer global require andres-montanez/magallanes
- cp .mage.yml.dist .mage.yml
- sed -i "s/ssh_user/$SSH_USER/g" .mage.yml
- sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml
- sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml
- /root/.config/composer/vendor/bin/mage deploy "$CI_BUILD_DEPLOY_TARGET"
when:
event: [deployment]
- cd /builds
- rsync -az "$CI_WORKSPACE/" "$CI_COMMIT_SHA"
services:
db:
@ -94,5 +80,4 @@ services:
- MARIADB_ROOT_PASSWORD=root
volumes:
node16_cache:
temp: {}
node_cache:

25
.woodpecker/deploy.yml Normal file
View file

@ -0,0 +1,25 @@
variables:
volumes: &volumes
- /data/${CI_REPO}:/builds
when:
event: [deployment]
skip_clone: true
steps:
"Deploy":
image: deblan/mage
secrets: [ssh_priv_key, ssh_user, ssh_host, app_directory]
volumes: *volumes
commands:
- cd "/builds/$CI_COMMIT_SHA"
- mkdir "$HOME/.ssh"
- echo "$SSH_PRIV_KEY" > "$HOME/.ssh/id_ed25519"
- chmod 700 "$HOME/.ssh"
- chmod 600 "$HOME/.ssh/id_ed25519"
- cp .mage.yml.dist .mage.yml
- sed -i "s/ssh_user/$SSH_USER/g" .mage.yml
- sed -i "s/ssh_host/$SSH_HOST/g" .mage.yml
- sed -i "s#app_directory#$APP_DIRECTORY#g" .mage.yml
- mage deploy "$CI_PIPELINE_DEPLOY_TARGET"

View file

@ -1,57 +1,63 @@
@import "../../vendor/murph/murph-core/src/core/Resources/assets/css/admin.scss";
@import "~simplemde/dist/simplemde.min.css";
.CodeMirror-fullscreen, .editor-toolbar.fullscreen {
z-index: 2000;
}
@import '../../vendor/murph/murph-core/src/core/Resources/assets/css/admin.scss';
@import '@kangc/v-md-editor/lib/style/base-editor.css';
@import '@kangc/v-md-editor/lib/theme/style/vuepress.css';
.ejs-link {
margin: 10px auto;
max-width: 80%;
border: 2px solid #333;
border-radius: 5px;
margin: 10px auto;
max-width: 80%;
border: 2px solid #333;
border-radius: 5px;
&--anchor {
display: block;
padding: 30px;
&--anchor {
display: block;
padding: 30px;
}
&-content {
display: inline-block;
vertical-align: top;
&--title {
font-weight: bold;
}
&-content {
display: inline-block;
vertical-align: top;
&--title {
font-weight: bold;
}
&--description {
font-size: 15px;
}
&--link {
padding-top: 10px;
font-size: 14px;
line-height: 20px;
}
&--description {
font-size: 15px;
}
$image-size: 85px;
&--anchor--with-image &-content {
width: calc(100% - $image-size - 5px);
padding-right: 25px;
&--link {
padding-top: 10px;
font-size: 14px;
line-height: 20px;
}
}
&--image {
display: inline-block;
width: $image-size;
height: $image-size;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
$image-size: 85px;
&--anchor--with-image &-content {
width: calc(100% - $image-size - 5px);
padding-right: 25px;
}
&--image {
display: inline-block;
width: $image-size;
height: $image-size;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
}
.choices__list--dropdown {
z-index: 3;
}
.v-md-editor {
border: 1px solid $input-border-color;
box-shadow: none;
}
.v-md-editor--fullscreen {
z-index: 3000;
}

View file

@ -411,6 +411,9 @@ pre[class*="language-"] {
.h1 {
font-weight: normal;
font-size: 40px;
font-family: MainFont;
text-shadow: none;
color: hsla(0, 0%, 100%, 0.7);
}
.h3 {
@ -522,6 +525,34 @@ pre[class*="language-"] {
}
}
.post-author-wrapper {
.post-author-avatar {
vertical-align: top;
width: 100px;
padding: 10px;
display: inline-block;
}
.post-author {
display: inline-block;
width: calc(100% - 100px);
padding: 10px 10px 10px 30px;
}
@media screen and (max-width: 774px) {
.post-author-avatar {
display: block;
margin: auto;
}
.post-author {
display: block;
width: 100%;
padding-right: 20px;
}
}
}
.content hr, .content .hr {
border: 0;
border-bottom: 1px dashed $color-hr-border;
@ -1335,3 +1366,18 @@ $links: (
}
}
}
.deprecated {
color: #fff;
background: #3abff8;
padding: 1rem;
border-radius: var(--rounded-box, 1rem);
text-align: center;
svg {
display: inline-block;
height: 25px;
vertical-align: top;
margin-right: 8px;
}
}

View file

@ -1,6 +1,6 @@
import '../../vendor/murph/murph-core/src/core/Resources/assets/js/admin.js'
require('./admin_modules/simplemde')()
require('./admin_modules/md-editor')()
const $ = require('jquery')
const Sortable = require('sortablejs').Sortable

View file

@ -0,0 +1,34 @@
const Vue = require('vue').default
const VueMarkdownEditor = require('@kangc/v-md-editor')
const githubTheme = require('@kangc/v-md-editor/lib/theme/github.js')
const fr = require('@kangc/v-md-editor/lib/lang/fr-FR').default
const hljs = require('highlight.js')
VueMarkdownEditor.use(githubTheme, {Hljs: hljs})
VueMarkdownEditor.lang.use('fr-FR', fr)
Vue.use(VueMarkdownEditor)
module.exports = () => {
const components = document.querySelectorAll('.markdown-editor')
components.forEach((component) => {
return new Vue({
el: component,
template: `
<div>
<textarea :name="name" v-model="value" class="d-none"></textarea>
<v-md-editor v-model="value" mode="edit"></v-md-editor>
</div>
`,
data() {
return {
name: component.getAttribute('data-name'),
value: JSON.parse(component.getAttribute('data-value')),
}
},
components: {
VueMarkdownEditor
}
})
})
}

129
bin/messenger Executable file
View file

@ -0,0 +1,129 @@
#!/bin/sh
set -eu
usage() {
printf "Usage: %s [-l DEBUG_LEVEL] [-h] start|stop|restart\n" "$0"
}
help() {
cat << EOH
SYNOPSIS
$0 [-l DEBUG_LEVEL] [-h] -a start|stop|restart
DESCRIPTION
$0 manages symfony messenger
OPTIONS
-h Show this help
-l debug|info|notice|warning|error
Debug level
-a start|stop|restart|status
EOH
}
on_interrupt() {
log -l notice ""
log -l notice "Process aborted!"
exit 130
}
start_messenger() {
nohup php8.1 bin/console messenger:consume 2>/dev/null >/dev/null &
log -t -l notice "Started"
}
stop_messenger() {
php8.1 bin/console messenger:stop-workers 2>/dev/null >/dev/null
log -t -l notice "Stopped"
}
get_pid() {
pgrep -f messenger:consume
}
main() {
cd "$(dirname "0")"
ACTION=
while getopts "l:ha:" option; do
case "${option}" in
h) help; exit 0;;
l) LOG_VERBOSE="$OPTARG";;
a) ACTION="$OPTARG";;
?) log -l error "$(usage)"; exit 1;;
esac
done
if [ "$ACTION" = "start" ]; then
start_messenger
elif [ "$ACTION" = "stop" ]; then
stop_messenger
elif [ "$ACTION" = "restart" ]; then
stop_messenger
start_messenger
elif [ "$ACTION" = "status" ]; then
get_pid
else
log -l error "Action is required."
fi
exit 0
}
log() {
LOG_VERBOSE="${LOG_VERBOSE:-info}"
LEVEL=info
TIME=
while getopts "tl:" option; do
case "${option}" in
l) LEVEL="$OPTARG"; shift $((OPTIND-1));;
t) TIME="$(printf "[%s] " "$(date +'%Y-%m-%dT%H:%M:%S.%s')")"; shift $((OPTIND-1));;
*) exit 1;;
esac
done
if [ -t 2 ] && [ -z "${NO_COLOR-}" ]; then
case "${LEVEL}" in
debug) COLOR="$(tput setaf 3)";;
notice) COLOR="$(tput setaf 4)";;
warning) COLOR="$(tput setaf 5)";;
error) COLOR="$(tput setaf 1)";;
*) COLOR="$(tput sgr0)";;
esac
fi
case "${LEVEL}" in
debug) LEVEL=100;;
notice) LEVEL=250;;
warning) LEVEL=300;;
error) LEVEL=400;;
*) LEVEL=200;;
esac
case "${LOG_VERBOSE}" in
debug) LOG_VERBOSE_VALUE=100;;
notice) LOG_VERBOSE_VALUE=250;;
warning) LOG_VERBOSE_VALUE=300;;
error) LOG_VERBOSE_VALUE=400;;
*) LOG_VERBOSE_VALUE=200;;
esac
if [ $LEVEL -ge $LOG_VERBOSE_VALUE ]; then
printf "%s\n" "$*" | while IFS='' read -r LINE; do
printf "%s%s%s\n" "${COLOR:-}" "${TIME:-}" "$LINE" >&2
done
fi
}
trap on_interrupt INT
main "$@"

View file

@ -8,9 +8,12 @@
"beberlei/doctrineextensions": "^1.3",
"friendsofsymfony/jsrouting-bundle": "^2.7",
"gregwar/captcha-bundle": "^2.2",
"guzzlehttp/guzzle": "^7.8",
"influxdata/influxdb-client-php": "^3.4",
"knplabs/knp-markdown-bundle": "^1.9",
"knplabs/knp-menu-bundle": "^3.1",
"murph/murph-core": "dev-master",
"symfony/messenger": "5.4.*",
"twig/intl-extra": "^3.5"
},
"require-dev": {
@ -31,7 +34,8 @@
"sort-packages": true,
"allow-plugins": {
"symfony/flex": true,
"symfony/runtime": true
"symfony/runtime": true,
"php-http/discovery": true
}
},
"autoload": {

View file

@ -13,18 +13,21 @@ liip_imagine:
max: [600, 600]
crop:
size: [600, 270]
start: [0, 0]
project_preview_filter:
filters:
downscale:
max: [600, 600]
crop:
size: [600, 270]
start: [0, 0]
post_preview_filter:
filters:
downscale:
max: [600, 600]
crop:
size: [600, 300]
start: [0, 0]
site_avatar:
filters:
downscale:

View file

@ -0,0 +1,7 @@
framework:
messenger:
transports:
async: "%env(MESSENGER_TRANSPORT_DSN)%"
routing:
'App\Message\PageViewMessage': async

View file

@ -1,6 +1,6 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['@Core/form/bootstrap_4_form_theme.html.twig']
form_themes: ['form/bootstrap_4_form_theme.html.twig']
auto_reload: true
paths:
'%kernel.project_dir%/templates/core/': Core

View file

@ -4,6 +4,11 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
influxdb_url: '%env(INFLUXDB_URL)%'
influxdb_token: '%env(INFLUXDB_TOKEN)%'
influxdb_bucket: '%env(INFLUXDB_BUCKET)%'
influxdb_org: '%env(INFLUXDB_ORG)%'
influxdb_debug: '%env(INFLUXDB_DEBUG)%'
services:
# default configuration for services in *this* file
@ -47,6 +52,14 @@ services:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
App\Api\InfluxDB:
arguments:
$url: '%influxdb_url%'
$token: '%influxdb_token%'
$bucket: '%influxdb_bucket%'
$org: '%influxdb_org%'
$debug: '%influxdb_debug%'
site.route_loader:
class: App\Core\Router\SiteRouteLoader
tags: [routing.loader]
@ -69,5 +82,9 @@ services:
tags:
- {name: markdown.parser, alias: comment}
App\EventListener\StatListener:
tags:
- { name: kernel.event_listener, event: kernel.request }
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

30
nohup.out Normal file
View file

@ -0,0 +1,30 @@
[OK] Consuming messages from transport "async".
// The worker will automatically exit once it has received a stop signal via
// the messenger:stop-workers command.
// Quit the worker with CONTROL-C.
// Re-run the command with a -vv option to see logs about consumed messages.
[OK] Consuming messages from transport "async".
// The worker will automatically exit once it has received a stop signal via
// the messenger:stop-workers command.
// Quit the worker with CONTROL-C.
// Re-run the command with a -vv option to see logs about consumed messages.
[OK] Consuming messages from transport "async".
// The worker will automatically exit once it has received a stop signal via
// the messenger:stop-workers command.
// Quit the worker with CONTROL-C.
// Re-run the command with a -vv option to see logs about consumed messages.

29948
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,13 +8,15 @@
"build": "./node_modules/.bin/encore production"
},
"dependencies": {
"@kangc/v-md-editor": "^1.7.12",
"daisyui": "^2.31.0",
"editorjs-hyperlink": "^1.0.6",
"editorjs-inline-tool": "^0.4.0",
"encore": "^0.0.30-beta",
"lozad": "^1.16.0",
"murph-project": "^1",
"murph-project": "^1.9.4",
"particles.js": "^2.0.0",
"prismjs": "^1.23.0",
"simplemde": "^1.11.2",
"tingle.js": "^0.16.0",
"vanillajs-datepicker": "^1.1.4",
"vue": "^2.6.14"

46
src/Api/InfluxDB.php Normal file
View file

@ -0,0 +1,46 @@
<?php
namespace App\Api;
use InfluxDB2\Client;
use InfluxDB2\Model\WritePrecision;
/**
* class InfluxDB.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class InfluxDB
{
protected ?Client $client = null;
public function __construct(
protected ?string $url,
protected ?string $token,
protected ?string $bucket,
protected ?string $org,
protected bool $debug = false
) {
if (isset($this->url, $this->token, $this->bucket, $this->org)) {
$this->client = new Client([
'url' => $this->url,
'token' => $this->token,
'bucket' => $this->bucket,
'org' => $this->org,
'debug' => $this->debug,
'precision' => WritePrecision::S,
'timeout' => 1,
]);
}
}
public function isAvailable(): bool
{
return $this->getClient() !== null;
}
public function getClient(): ?Client
{
return $this->client;
}
}

View file

@ -2,31 +2,30 @@
namespace App\Controller\Blog;
use App\Analytic\DateRangeAnalytic;
use App\Core\Controller\Admin\Crud\CrudController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Crud\Field\DatetimeField;
use App\Core\Crud\Field\TextField;
use App\Core\Entity\EntityInterface;
use App\Core\Form\FileUploadHandler;
use App\Core\Manager\EntityManager;
use App\Core\Repository\Site\NodeRepository;
use App\Entity\Blog\Post;
use App\Entity\Blog\Post as Entity;
use App\Factory\Blog\PostFactory as EntityFactory;
use App\Form\Blog\Filter\PostFilterType;
use App\Form\Blog\PostType;
use App\Form\Blog\PostType as EntityType;
use App\Repository\Blog\PostRepositoryQuery as RepositoryQuery;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Form;
use App\Core\Entity\EntityInterface;
use App\Entity\Blog\Post;
use App\Analytic\DateRangeAnalytic;
use App\Core\Repository\Site\NodeRepository;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
#[Route(path: '/admin/blog/post')]
class PostAdminController extends CrudController
@ -68,7 +67,7 @@ class PostAdminController extends CrudController
'attr' => ['class' => 'miw-400'],
])
->setField('index', 'ID', TextField::class, [
'property_builder' => function(EntityInterface $entity) {
'property_builder' => function (EntityInterface $entity) {
return sprintf('#%d', $entity->getId());
},
'sort' => ['id', '.id'],
@ -85,7 +84,7 @@ class PostAdminController extends CrudController
'format' => 'd/m/Y H:i',
'sort' => ['publishedAt', '.publishedAt'],
'attr' => ['class' => 'miw-200'],
'inline_form' => function(FormBuilderInterface $builder) {
'inline_form' => function (FormBuilderInterface $builder) {
$builder->add(
'publishedAt',
DateTimeType::class,
@ -107,7 +106,7 @@ class PostAdminController extends CrudController
'view' => 'blog/post_admin/field/status.html.twig',
'sort' => ['status', '.status'],
'attr' => ['class' => 'miw-100'],
'inline_form' => function(FormBuilderInterface $builder) {
'inline_form' => function (FormBuilderInterface $builder) {
$builder->add(
'status',
ChoiceType::class,
@ -125,15 +124,15 @@ class PostAdminController extends CrudController
],
]
);
}
},
])
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
->setBatchAction('index', 'draft', 'Statut : publier', function(EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'draft', 'Statut : publier', function (EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Post::PUBLISHED));
})
->setBatchAction('index', 'publish', 'Statut : brouillon', function(EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'publish', 'Statut : brouillon', function (EntityInterface $entity, EntityManager $manager) {
$manager->update($entity->setStatus(Post::DRAFT));
})
;
@ -152,7 +151,7 @@ class PostAdminController extends CrudController
$factory->create($this->getUser()),
$entityManager,
$request,
function(Entity $entity, Form $form, Request $request) use ($fileUpload) {
function (Entity $entity, Form $form, Request $request) use ($fileUpload) {
$directory = 'uploads/post/'.date('Y');
$fileUpload->handleForm(
@ -173,7 +172,7 @@ class PostAdminController extends CrudController
$entity,
$entityManager,
$request,
function(Entity $entity, Form $form, Request $request) use ($fileUpload) {
function (Entity $entity, Form $form, Request $request) use ($fileUpload) {
$directory = 'uploads/post/'.date('Y');
$fileUpload->handleForm(
@ -187,7 +186,7 @@ class PostAdminController extends CrudController
);
}
#[Route(path: "/inline_edit/{entity}/{context}/{label}", name: 'admin_blog_post_inline_edit', methods: ['GET', 'POST'])]
#[Route(path: '/inline_edit/{entity}/{context}/{label}', name: 'admin_blog_post_inline_edit', methods: ['GET', 'POST'])]
public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response
{
return $this->doInlineEdit($context, $label, $entity, $entityManager, $request);
@ -268,8 +267,7 @@ class PostAdminController extends CrudController
DateRangeAnalytic $analytic,
NodeRepository $nodeRepository,
string $range = '7days'
): Response
{
): Response {
if (!in_array($range, ['7days', '30days', '90days', '1year'])) {
throw $this->createNotFoundException();
}

View file

@ -20,6 +20,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use App\Factory\Blog\PostFollowFactory;
use App\Manager\PostFollowManager;
use App\Core\Twig\Extension\EditorJsExtension;
use App\Core\Twig\Extension\BuilderExtension;
class PostController extends PageController
{
@ -165,7 +166,11 @@ class PostController extends PageController
return $query;
}
public function rss(PostParser $parser, EditorJsExtension $editorJsExtension): Response
public function rss(
PostParser $parser,
EditorJsExtension $editorJsExtension,
BuilderExtension $builderExtension
): Response
{
$entities = $this->createQuery()->paginate(1, 20);
$items = [];
@ -173,6 +178,8 @@ class PostController extends PageController
foreach ($entities as $entity) {
if ($entity->getContentFormat() === 'editorjs') {
$description = $editorJsExtension->buildHtml($entity->getContent());
} elseif ($entity->getContentFormat() === 'builder') {
$description = $builderExtension->buildHtml($entity->getContent());
} else {
$description = $parser->transformMarkdown($entity->getContent());
}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Controller;
use App\Core\Controller\User\UserAdminController as BaseUserAdminController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Factory\UserFactory as Factory;
use App\Core\Manager\EntityManager;
use App\Core\Security\TokenGenerator;
use App\Entity\User as Entity;
use App\Repository\UserRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class UserAdminController extends BaseUserAdminController
{
#[Route(path: '/admin/user/{page}', name: 'admin_user_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return parent::index($query, $request, $session, $page);
}
#[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response
{
return parent::new($factory, $entityManager, $request, $tokenGenerator);
}
#[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return parent::show($entity);
}
#[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return parent::filter($session);
}
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::edit($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/inline_edit/{entity}/{context}/{label}', name: 'admin_user_inline_edit', methods: ['GET', 'POST'])]
public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::inlineEdit($context, $label, $entity, $entityManager, $request);
}
#[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::delete($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])]
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
{
return parent::requestResetting($entity, $eventDispatcher, $request);
}
protected function getConfiguration(): CrudConfiguration
{
if ($this->configuration) {
return $this->configuration;
}
return parent::getConfiguration()
->setView('form', 'admin/user_admin/_form.html.twig')
->setView('show_entity', 'admin/user_admin/_show.html.twig')
;
}
}

View file

@ -90,11 +90,18 @@ class Post implements EntityInterface
#[ORM\Column(type: 'array')]
private $parameters = [];
#[ORM\ManyToOne(targetEntity: self::class, inversedBy: 'deprecatedPosts')]
private $recommandedPost;
#[ORM\OneToMany(mappedBy: 'recommandedPost', targetEntity: self::class)]
private $deprecatedPosts;
public function __construct()
{
$this->categories = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->postFollows = new ArrayCollection();
$this->deprecatedPosts = new ArrayCollection();
}
public function getId(): ?int
@ -475,4 +482,46 @@ class Post implements EntityInterface
}
)[0]['value'] ?? null;
}
public function getRecommandedPost(): ?self
{
return $this->recommandedPost;
}
public function setRecommandedPost(?self $recommandedPost): self
{
$this->recommandedPost = $recommandedPost;
return $this;
}
/**
* @return Collection<int, self>
*/
public function getDeprecatedPosts(): Collection
{
return $this->deprecatedPosts;
}
public function addDeprecatedPost(self $deprecatedPost): self
{
if (!$this->deprecatedPosts->contains($deprecatedPost)) {
$this->deprecatedPosts[] = $deprecatedPost;
$deprecatedPost->setRecommandedPost($this);
}
return $this;
}
public function removeDeprecatedPost(self $deprecatedPost): self
{
if ($this->deprecatedPosts->removeElement($deprecatedPost)) {
// set the owning side to null (unless already changed)
if ($deprecatedPost->getRecommandedPost() === $this) {
$deprecatedPost->setRecommandedPost(null);
}
}
return $this;
}
}

View file

@ -7,6 +7,7 @@ use App\Core\Entity\Site\Page\FileBlock;
use App\Form\Type\SimpleMdTextareaBlockType;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Form\FormBuilderInterface;
use App\Form\MarkdownBlockType;
#[ORM\Entity]
class SimplePage extends TitledPage
@ -17,7 +18,7 @@ class SimplePage extends TitledPage
$builder->add(
'content',
SimpleMdTextareaBlockType::class,
MarkdownBlockType::class,
[
'label' => 'Contenu',
'options' => [

View file

@ -0,0 +1,24 @@
<?php
namespace App\EventListener;
use App\Message\PageViewMessage;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Messenger\MessageBusInterface;
/**
* class StatListener.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class StatListener
{
public function __construct(protected MessageBusInterface $bus)
{
}
public function onKernelRequest(RequestEvent $event)
{
$this->bus->dispatch(new PageViewMessage(time()));
}
}

View file

@ -9,6 +9,7 @@ use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use App\Core\Form\FileManager\FilePickerType;
use App\Core\Form\Type\TinymceTextareaType;
/**
* class SettingEventSubscriber.
@ -36,6 +37,7 @@ class SettingEventSubscriber extends EventSubscriber
$this->manager->init('stats_umami_url', '📊 Statistiques', 'Adresse tableau de bord Umami', '');
$this->manager->init('stats_umami_tag', '📊 Statistiques', 'Script Umami', '');
$this->manager->init('stats_grafana_url', '📊 Statistiques', 'Adresse tableau de bord Grafana', '');
$this->manager->init('post_author_description', '🖊️ Article', 'Description auteur', '');
@ -68,7 +70,7 @@ class SettingEventSubscriber extends EventSubscriber
);
}
if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url'])) {
if (in_array($entity->getCode(), ['giphy_api_key', 'stats_umami_url', 'stats_grafana_url'])) {
$builder->add(
'value',
TextType::class,
@ -93,7 +95,7 @@ class SettingEventSubscriber extends EventSubscriber
);
}
if (in_array($entity->getCode(), ['blog_footer', 'post_author_description'])) {
if (in_array($entity->getCode(), ['post_author_description'])) {
$event->setOption('view', 'large');
$builder->add(
@ -107,5 +109,20 @@ class SettingEventSubscriber extends EventSubscriber
]
);
}
if (in_array($entity->getCode(), ['blog_footer'])) {
$event->setOption('view', 'large');
$builder->add(
'value',
TinymceTextareaType::class,
[
'label' => $entity->getLabel(),
'attr' => [
'rows' => 20,
],
]
);
}
}
}

View file

@ -25,6 +25,8 @@ use App\Form\Type\SimpleMdTextareaType;
use App\Core\Form\Type\EditorJsTextareaType;
use App\Core\Form\FileManager\FilePickerType;
use App\Core\Form\Type\CollectionType as MurphCollectionType;
use App\Form\MarkdownType;
use App\Core\Form\Type\BuilderType;
class PostType extends AbstractType
{
@ -51,6 +53,7 @@ class PostType extends AbstractType
'required' => true,
'choices' => [
'Markdown' => 'markdown',
'Builder' => 'builder',
'HTML' => 'html',
'Editor JS' => 'editorjs',
],
@ -61,8 +64,9 @@ class PostType extends AbstractType
);
$types = [
'markdown' => SimpleMdTextareaType::class,
'html' => SimpleMdTextareaType::class,
'markdown' => MarkdownType::class,
'builder' => BuilderType::class,
'html' => MarkdownType::class,
'editorjs' => EditorJsTextareaType::class,
];
@ -113,6 +117,28 @@ class PostType extends AbstractType
]
);
$builder->add(
'recommandedPost',
EntityType::class,
[
'label' => 'Article recommandé',
'class' => Post::class,
'choice_label' => 'title',
'required' => false,
'multiple' => false,
'attr' => [
'data-jschoice' => '',
],
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('a')
->orderBy('a.title', 'ASC')
;
},
'constraints' => [
],
]
);
$builder->add(
'status',
ChoiceType::class,
@ -172,6 +198,7 @@ class PostType extends AbstractType
'attr' => [
],
'constraints' => [
new Image(),
],
]
);
@ -300,19 +327,19 @@ class PostType extends AbstractType
]
);
$builder->add(
'parameters',
MurphCollectionType::class,
[
'label' => 'Paramètres',
'entry_type' => PostParameterType::class,
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'row_attr' => ['class' => 'mb-3'],
]
);
// $builder->add(
// 'parameters',
// MurphCollectionType::class,
// [
// 'label' => 'Paramètres',
// 'entry_type' => PostParameterType::class,
// 'by_reference' => false,
// 'allow_add' => true,
// 'allow_delete' => true,
// 'prototype' => true,
// 'row_attr' => ['class' => 'mb-3'],
// ]
// );
}
public function configureOptions(OptionsResolver $resolver)

View file

@ -0,0 +1,21 @@
<?php
namespace App\Form;
use App\Core\Form\Site\Page\TextareaBlockType;
use Symfony\Component\Form\FormBuilderInterface;
class MarkdownBlockType extends TextareaBlockType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
'value',
MarkdownType::class,
array_merge([
'required' => false,
'label' => false,
], $options['options']),
);
}
}

13
src/Form/MarkdownType.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace App\Form;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class MarkdownType extends TextareaType
{
public function getBlockPrefix()
{
return 'markdown';
}
}

View file

@ -2,7 +2,9 @@
namespace App;
use App\Core\DependencyInjection\Compiler\BuilderBlockPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
@ -35,4 +37,9 @@ class Kernel extends BaseKernel
(require $path)($routes->withPath($path), $this);
}
}
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new BuilderBlockPass());
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Message;
final class PageViewMessage
{
public function __construct(public int $time)
{
}
public function getTime(): int
{
return $this->time;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\MessageHandler;
use App\Message\PageViewMessage;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use App\Api\InfluxDB;
use InfluxDB2\WriteType;
use InfluxDB2\Point;
final class PageViewMessageHandler implements MessageHandlerInterface
{
public function __construct(protected InfluxDB $influxDB)
{
}
public function __invoke(PageViewMessage $message)
{
if (!$this->influxDB->isAvailable()) {
return;
}
$client = $this->influxDB->getClient();
$writeApi = $client->createWriteApi(['writeType' => WriteType::SYNCHRONOUS]);
$pageView = new Point('page_view');
$pageView
->addTag('request', 'view')
->addField('value', 1)
->time($message->getTime())
;
$writeApi->write($pageView);
$writeApi->close();
$client->close();
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Middleware;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
final class PageViewMiddleware implements MiddlewareInterface
{
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
// ...
return $stack->next()->handle($envelope, $stack);
}
}

View file

@ -54,15 +54,13 @@ class PostRepositoryQuery extends RepositoryQuery
;
}
protected function filterHandler(string $name, $value)
{
if ('category' === $name) {
$this->inCategory($value);
}
}
public function search(?string $keywords, ?string $tag)
{
$keywords = explode(' ', $keywords);
$filterWords = fn ($keyword) => '' !== trim($keyword) && preg_match('/[a-zA-Z]+/', $keyword);
$keywords = array_filter($keywords, $filterWords);
if ($keywords) {
$conn = $this->repository->getEm()->getConnection();
@ -70,43 +68,107 @@ class PostRepositoryQuery extends RepositoryQuery
'SELECT
post.id,
post.title,
MATCH(post.title) AGAINST(:search) AS MATCH_TITLE,
MATCH(post.content) AGAINST(:search) AS MATCH_CONTENT
post.content,
post.published_at
FROM post
WHERE
post.status = 1 AND
post.published_at < :date
ORDER BY
MATCH_TITLE DESC,
MATCH_CONTENT DESC
');
'
);
$statement = $query->execute([
':search' => $keywords,
':date' => (new \DateTime())->format('Y-m-d H:i:s'),
]);
$results = $statement->fetchAll();
$ids = [];
$matches = [];
foreach ($results as $k => $v) {
$rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT'];
$initWords = explode(' ', $v['title']);
$words = [];
if ($rate >= 7) {
$ids[] = $v['id'];
foreach ($initWords as $initWord) {
$words = array_merge($words, preg_split('/[:_\'-]+/', $initWord));
}
}
if (0 == count($ids)) {
foreach ($results as $k => $v) {
$rate = ($v['MATCH_TITLE'] * 2) + $v['MATCH_CONTENT'];
$words = array_filter($words, $filterWords);
if ($rate >= 6) {
$ids[] = $v['id'];
foreach ($keywords as $keyword) {
if (str_contains(mb_strtolower($v['content']), mb_strtolower($keyword))) {
$similarity = 99;
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
++$matches[$v['id']]['count'];
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
}
foreach ($words as $word) {
if (str_contains(mb_strtolower($word), mb_strtolower($keyword))) {
$similarity = 150;
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
++$matches[$v['id']]['count'];
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
} else {
$lev = levenshtein($word, $keyword);
$similarity = 100 - ($lev * 100 / mb_strlen($word));
if ($similarity > 70) {
if (isset($matches[$v['id']])) {
$matches[$v['id']]['similarity'] += $similarity;
} else {
$matches[$v['id']] = [
'id' => $v['id'],
'title' => $v['title'],
'published_at' => $v['published_at'],
'similarity' => $similarity,
'count' => 1,
];
}
}
}
}
}
}
$matches = array_filter($matches, function($match) use ($keywords) {
return (100 * $match['count'] / count($keywords)) > 80;
});
usort($matches, function ($a, $b) {
if ($a['similarity'] > $b['similarity']) {
return -1;
}
if ($b['similarity'] > $a['similarity']) {
return 1;
}
return ($a['published_at'] != $b['published_at']) * -1;
});
$ids = array_column($matches, 'id');
if (!$ids) {
$ids = [-1];
}
@ -127,4 +189,11 @@ class PostRepositoryQuery extends RepositoryQuery
return $this;
}
protected function filterHandler(string $name, $value)
{
if ('category' === $name) {
$this->inCategory($value);
}
}
}

View file

@ -0,0 +1 @@
{{ include('@Core/user/user_admin/_form.html.twig') }}

View file

@ -0,0 +1 @@
{{ include('@Core/user/user_admin/_show.html.twig') }}

View file

@ -40,7 +40,36 @@
{% endfor %}
</div>
</div>
<iframe src="{{ setting('stats_umami_url') }}" class="col-12 col-md-9" frameborder="0" style="height: calc(100vh - 170px)">
</iframe>
<div class="col-12 col-md-9 pt-1">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#tab-stats-umami">
{{ 'Umami'|trans }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#tab-stats-grafana">
{{ 'Grafana'|trans }}
</a>
</li>
</ul>
<div class="tab-content pt-4">
<div class="tab-pane show active" id="tab-stats-umami">
<iframe
src="{{ setting('stats_umami_url') }}"
frameborder="0"
style="width: 100%; height: calc(100vh - 220px)"
></iframe>
</div>
<div class="tab-pane" id="tab-stats-grafana">
<iframe
src="{{ setting('stats_grafana_url') }}"
frameborder="0" style="width: 100%; height: calc(100vh - 220px)"
></iframe>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,79 @@
<?php
namespace App\Controller;
use App\Core\Controller\User\UserAdminController as BaseUserAdminController;
use App\Core\Crud\CrudConfiguration;
use App\Core\Factory\UserFactory as Factory;
use App\Core\Manager\EntityManager;
use App\Core\Security\TokenGenerator;
use App\Entity\User as Entity;
use App\Repository\UserRepositoryQuery as RepositoryQuery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
class UserAdminController extends BaseUserAdminController
{
#[Route(path: '/admin/user/{page}', name: 'admin_user_index', methods: ['GET'], requirements: ['page' => '\d+'])]
public function index(RepositoryQuery $query, Request $request, Session $session, int $page = 1): Response
{
return parent::index($query, $request, $session, $page);
}
#[Route(path: '/admin/user/new', name: 'admin_user_new', methods: ['GET', 'POST'])]
public function new(Factory $factory, EntityManager $entityManager, Request $request, TokenGenerator $tokenGenerator): Response
{
return parent::new($factory, $entityManager, $request, $tokenGenerator);
}
#[Route(path: '/admin/user/show/{entity}', name: 'admin_user_show', methods: ['GET'])]
public function show(Entity $entity): Response
{
return parent::show($entity);
}
#[Route(path: '/admin/user/filter', name: 'admin_user_filter', methods: ['GET'])]
public function filter(Session $session): Response
{
return parent::filter($session);
}
#[Route(path: '/admin/user/edit/{entity}', name: 'admin_user_edit', methods: ['GET', 'POST'])]
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::edit($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/inline_edit/{entity}/{context}/{label}', name: 'admin_user_inline_edit', methods: ['GET', 'POST'])]
public function inlineEdit(string $context, string $label, Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::inlineEdit($context, $label, $entity, $entityManager, $request);
}
#[Route(path: '/admin/user/delete/{entity}', name: 'admin_user_delete', methods: ['DELETE', 'POST'])]
public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response
{
return parent::delete($entity, $entityManager, $request);
}
#[Route(path: '/admin/user/resetting_request/{entity}', name: 'admin_user_resetting_request', methods: ['POST'])]
public function requestResetting(Entity $entity, EventDispatcherInterface $eventDispatcher, Request $request): Response
{
return parent::requestResetting($entity, $eventDispatcher, $request);
}
protected function getConfiguration(): CrudConfiguration
{
if ($this->configuration) {
return $this->configuration;
}
return parent::getConfiguration()
->setView('form', 'admin/user_admin/_form.html.twig')
->setView('show_entity', 'admin/user_admin/_show.html.twig')
;
}
}

View file

@ -0,0 +1 @@
{{ include('@Core/user/user_admin/_form.html.twig') }}

View file

@ -0,0 +1 @@
{{ include('@Core/user/user_admin/_show.html.twig') }}

View file

@ -1,7 +1,7 @@
<div class="row">
<div class="col-md-3 p-3">
<div class="row">
{% for item in ['categories', 'slug'] %}
{% for item in ['categories', 'slug', 'recommandedPost'] %}
<div class="col-md-12">
{{ form_row(form[item]) }}
</div>
@ -38,7 +38,7 @@
</span>
</div>
{% for item in ['image', 'image2', 'parameters', 'status', 'contentFormat', 'publishedAt'] %}
{% for item in ['image', 'image2', 'status', 'contentFormat', 'publishedAt'] %}
<div class="col-md-12">
{{ form_row(form[item]) }}
</div>

View file

@ -3,7 +3,7 @@
{% block list_item_actions_before %}
{% if configuration.action(context, 'analytic_stats', true) %}
{% set analytics = path(
configuration.pageRoute('analytic_stats'),
configuration.pageRoute('analytic_stats'),
{entity: item.id}|merge(configuration.pageRouteParams('analytic_stats'))
) %}

View file

@ -0,0 +1,8 @@
{% extends '@Core/form/bootstrap_4_form_theme.html.twig' %}
{% block markdown_widget %}
<div {% for attr, value in row_attr %}{{ attr }}="{{ value }}" {% endfor %}>
<div class="markdown-editor" data-value="{{ value|json_encode }}" data-name="{{ full_name }}" data-id="{{ id }}">
</div>
</div>
{% endblock %}

View file

@ -5,7 +5,7 @@
<div class="wide-menu hidden md:block">
<div class="fixed-menu">
<div class="text-center">
<a href="{{- safe_path('blog_menu_posts', {_domain: _domain}) -}}">
<a href="{{- safe_path('blog_menu_posts', {_domain: _domain}) -}}" class="avatar-logo">
{%- if avatar -%}
<img src="{{- asset(avatar)|imagine_filter('site_avatar') -}}" class="rounded-full inline mb-2" alt="{{- avatar|file_attribute('title') -}}">
{%- endif -%}

View file

@ -10,46 +10,50 @@
</div>
{% if showForm %}
<div class="body">
<form class="form grid grid-flow-row-dens grid-cols-2 gap-5" method="POST" data-form-bot action="{{ safe_url('blog_tech_form_without_javascript', {page: app.request.uri, _domain: _domain}) }}">
<div class="col-span-2 md:col-span-1">
{{ form_label(form.name, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.name, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.name) }}
</div>
<div class="col-span-2 md:col-span-1">
{{ form_label(form.email, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.email, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.email) }}
</div>
<div class="col-span-2">
{{ form_label(form.subject, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.subject, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.subject) }}
</div>
<div class="col-span-2">
{{ form_label(form.message, null, {label_attr: {class: 'label'}}) }}
{{ form_errors(form.message) }}
{{ form_widget(form.message, {attr: {cols: 30, rows: 10, class: 'textarea textarea-bordered w-full'}}) }}
</div>
<div class="col-span-2">
<div class="md:flex justify-start gap-3">
{{ form_label(form.captcha, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.captcha, {attr: {class: 'input input-bordered'}}) }}
</div>
</div>
<div class="col-span-2">
<label class="label justify-start gap-3" for="rgpd">
<input type="checkbox" id="rgpd" class="checkbox" required>
<span class="label">En validant ce formulaire, vous acceptez que j'utilise votre e-mail pour vous fournir une réponse.</span>
</label>
</div>
<div class="col-span-2">
<input type="submit" class="btn btn-primary" value="Envoyer" />
</div>
<div class="reviews">
<div class="rounded-2xl shadow-md p-8 mb-8 bg-box form" id="grid">
<div class="h4">M'envoyer un message</div>
{{ form_rest(form) }}
</form>
<form class="form grid grid-flow-row-dens grid-cols-2 gap-5" method="POST" data-form-bot action="{{ safe_url('blog_tech_form_without_javascript', {page: app.request.uri, _domain: _domain}) }}">
<div class="col-span-2 md:col-span-1">
{{ form_label(form.name, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.name, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.name) }}
</div>
<div class="col-span-2 md:col-span-1">
{{ form_label(form.email, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.email, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.email) }}
</div>
<div class="col-span-2">
{{ form_label(form.subject, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.subject, {attr: {class: 'input input-bordered w-full'}}) }}
{{ form_errors(form.subject) }}
</div>
<div class="col-span-2">
{{ form_label(form.message, null, {label_attr: {class: 'label'}}) }}
{{ form_errors(form.message) }}
{{ form_widget(form.message, {attr: {cols: 30, rows: 10, class: 'textarea textarea-bordered w-full'}}) }}
</div>
<div class="col-span-2">
<div class="md:flex justify-start gap-3">
{{ form_label(form.captcha, null, {label_attr: {class: 'label'}}) }}
{{ form_widget(form.captcha, {attr: {class: 'input input-bordered'}}) }}
</div>
</div>
<div class="col-span-2">
<label class="label justify-start gap-3" for="rgpd">
<input type="checkbox" id="rgpd" class="checkbox" required>
<span class="label">En validant ce formulaire, vous acceptez que j'utilise votre e-mail pour vous fournir une réponse.</span>
</label>
</div>
<div class="col-span-2">
<input type="submit" class="btn btn-primary" value="Envoyer" />
</div>
{{ form_rest(form) }}
</form>
</div>
</div>
{% endif %}
{% endblock %}

View file

@ -38,11 +38,25 @@
</div>
{% endif %}
<div class="body-content {% if post.parameter('podcast') %}is-podcast{% endif %}">
<div class="body-content">
{%- if post.recommandedPost -%}
<div class="deprecated">
<svg stroke-width="2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#ffffff"><path d="M20.043 21H3.957c-1.538 0-2.5-1.664-1.734-2.997l8.043-13.988c.77-1.337 2.699-1.337 3.468 0l8.043 13.988C22.543 19.336 21.58 21 20.043 21zM12 9v4" stroke="#ffffff" stroke-width="2" stroke-linecap="round"></path><path d="M12 17.01l.01-.011" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<strong>Cet article est déprécié</strong><br>
Article recommandé : <a href="{{ safe_path('blog_menu_post', {
post: post.recommandedPost.id,
slug: post.recommandedPost.slug,
_domain: _domain})
}}"><strong>{{ post.recommandedPost.title }}</strong></a>
</div>
{%- endif -%}
{% if post.contentFormat == 'html' %}
{{- post.content|murph_url|file_attributes|post -}}
{% elseif post.contentFormat == 'markdown' %}
{{- post.content|murph_url|file_attributes|markdown('post')|lazy_load -}}
{% elseif post.contentFormat == 'builder' %}
{{- post.content|block_to_html|lazy_load -}}
{% elseif post.contentFormat == 'editorjs' %}
{{- post.content|murph_url|file_attributes|editorjs_to_html|raw -}}
{% endif %}
@ -55,14 +69,14 @@
{% set description = setting('post_author_description') %}
{%- if description and not post.isQuick -%}
<div class="body">
<div class="rounded-2xl shadow-md p-2 md:p-8 flex justify-start bg-box">
<div class="body post-author-wrapper">
<div class="rounded-2xl shadow-md p-2 md:p-8 bg-box">
{%- set avatar = setting('avatar_image') -%}
{%- if avatar -%}
<p class="mr-8">
<div class="post-author-avatar">
<img src="{{ asset(avatar)|imagine_filter('site_avatar') }}" alt="Simon Vieille" title="Simon Vieille" class="rounded-full">
</p>
</div>
{%- endif -%}
<div class="post-author">
@ -101,12 +115,8 @@
{% form_theme form with "form_div_layout.html.twig" %}
<div class="grid" id="form">
<div class="rounded-2xl shadow-md p-8 mb-8 bg-box form" id="grid">
<form class="form" method="POST" data-form-bot action="{{ safe_url('blog_tech_form_without_javascript', {page: app.request.uri, _domain: _domain}) }}">
{% if comments|length %}
<hr>
{% endif %}
<div class="h4">Ajouter un commentaire</div>
<div class="grid grid-flow-row-dens grid-cols-2 gap-5">

View file

@ -13,29 +13,27 @@
</div>
</div>
<div class="grid grid-flow-row-dens grid-cols-12 md:p-8 gap-5">
{% for project in projects %}
<div class="card shadow-md col-span-12 md:col-span-6 lg:col-span-4 m-3 bg-box">
<div class="card">
{% if project.image %}
<figure>
<img src="{{ asset(project.image)|imagine_filter('project_preview_filter') }}" alt="{{ project.label }}">
</figure>
{% endif %}
<div class="card-body">
<h2 class="card-title">{{ project.label }}</h2>
<div class="grid grid-flow-row-dens grid-cols-12 md:p-8 gap-5">
{% for project in projects %}
<div class="card shadow-md col-span-12 md:col-span-6 lg:col-span-4 m-3 bg-box">
{% if project.image %}
<figure>
<img src="{{ asset(project.image)|imagine_filter('project_preview_filter') }}" alt="{{ project.label }}">
</figure>
{% endif %}
<div class="card-body">
<h2 class="card-title">{{ project.label }}</h2>
{{- project.description|murph_url|markdown('post') -}}
{{- project.description|murph_url|markdown('post') -}}
<div class="card-actions mt-5">
{% for link in project.links %}
<a class="btn btn-xs" href="{{ link.url|murph_url }}" target="_blank">
{{- link.label -}}
</a>
{% endfor %}
</div>
</div>
</div>
<div class="card-actions mt-5">
{% for link in project.links %}
<a class="btn btn-xs" href="{{ link.url|murph_url }}" target="_blank">
{{- link.label -}}
</a>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>

8591
yarn.lock

File diff suppressed because it is too large Load diff