Compare commits

...

907 Commits

Author SHA1 Message Date
niklas.ortmann 83387de103 Update createArticle.php 2026-07-19 15:41:37 +02:00
niklas.ortmann dfb8e1dc89 Update updateArticle.php 2026-07-19 15:41:35 +02:00
niklas.ortmann bdbc38a339 Update profile.php 2026-07-19 15:40:16 +02:00
niklas.ortmann e6356525b9 Update comments.js 2026-07-19 15:34:50 +02:00
niklas.ortmann ca74690952 Update showArticle.php 2026-07-19 15:33:13 +02:00
niklas.ortmann 75f9fdbeb8 Update navbar.php 2026-07-19 15:31:02 +02:00
niklas.ortmann 81a27135d5 Update navbar.css 2026-07-19 15:28:08 +02:00
niklas.ortmann a58576637a Update navbar.css 2026-07-19 15:26:53 +02:00
niklas.ortmann 997957a5d3 Update logout-controller.php 2026-07-19 15:25:15 +02:00
niklas.ortmann ec794db706 logout-fix 2026-07-19 15:23:46 +02:00
niklas.ortmann ba749bca68 debugging 2026-07-19 15:21:16 +02:00
niklas.ortmann 12a6af55b0 debugging 2026-07-19 15:19:47 +02:00
niklas.ortmann 400f018104 debugging 2026-07-19 15:17:32 +02:00
niklas.ortmann 80f4b623f9 debugging 2026-07-19 15:16:22 +02:00
niklas.ortmann 6546d264b0 debugging 2026-07-19 15:15:30 +02:00
niklas.ortmann e7e120e5b8 debugging 2026-07-19 15:15:03 +02:00
niklas.ortmann ec66889a1f debugging 2026-07-19 15:13:44 +02:00
niklas.ortmann f1703a476e Update index.php 2026-07-19 15:09:22 +02:00
niklas.ortmann bac296b58a Update logout-controller.php 2026-07-19 15:07:09 +02:00
niklas.ortmann bfb2d2b0d8 navbar logout mit post 2026-07-19 14:59:45 +02:00
niklas.ortmann ad9a00fd49 csrf.php 2026-07-19 14:57:52 +02:00
niklas.ortmann 42faab17e8 Create csrf.php 2026-07-19 14:56:26 +02:00
niklas.ortmann 1da4842847 Update dataSources.local.xml 2026-07-19 14:56:25 +02:00
niklas.ortmann afd90086d8 Update profile.php 2026-07-19 14:20:43 +02:00
niklas.ortmann a7a54f877c Merge pull request 'Beitragseditor funktioniert mit und ohne JS (+Bilder löschen)' (#57) from BeitragseditorOhneJS into dev
Reviewed-on: #57
2026-07-19 13:45:33 +02:00
NOrtmann1 9b9f71d21c Update DatabaseInitializer.php 2026-07-19 13:44:27 +02:00
NOrtmann1 4b84f0ab09 Update updateArticle-controller.php 2026-07-19 13:30:39 +02:00
NOrtmann1 8c8b148bf4 Update updateArticle-controller.php 2026-07-19 13:15:23 +02:00
NOrtmann1 6bf335ce46 Update createArticle.php 2026-07-19 13:15:20 +02:00
NOrtmann1 144a1015f1 Create article-block-helper.php 2026-07-19 13:15:17 +02:00
NOrtmann1 c0a94b2eee Update createArticle-controller.php 2026-07-19 13:15:14 +02:00
NOrtmann1 f1a32120d6 Update editor.js 2026-07-19 12:48:41 +02:00
NOrtmann1 97fa1de999 Update updateArticle-controller.php 2026-07-19 12:48:38 +02:00
NOrtmann1 7341fe7733 Update updateArticle.php 2026-07-19 12:48:33 +02:00
NOrtmann1 5b0528a914 Update index-controller.php 2026-07-19 10:56:05 +02:00
NOrtmann1 ac64e65dd3 Update index-controller.php 2026-07-19 10:54:33 +02:00
NOrtmann1 96ffa3288e Update dataSources.local.xml 2026-07-19 10:52:21 +02:00
NOrtmann1 0c6cf13efa merge-commit 2026-07-19 10:50:22 +02:00
caroline.slt 325df51812 ReadMe 2026-07-18 19:51:23 +02:00
caroline.slt 97afd1ea06 Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:41:39 +02:00
caroline.slt e3903b4f3e Kommentare bearbeiten - Fehlerkorrektur 2026-07-18 19:27:36 +02:00
caroline.slt 031f2afd25 Kommentare bearbeiten 2026-07-18 18:49:48 +02:00
niklas.ortmann d3884d2fc1 Merge branch 'PaginatorOhneJS2' into dev 2026-07-18 18:33:04 +02:00
niklas.ortmann 1575f4fa24 Merge pull request 'Deiteiincludes ($pfad) per whitelist prüfen' (#53) from AllowistDateiincludes into dev
Reviewed-on: #53
2026-07-18 18:31:55 +02:00
niklas.ortmann 0aca76d2f4 Update index.php 2026-07-18 18:26:42 +02:00
niklas.ortmann eca6646071 showCategory.js 2026-07-18 18:23:11 +02:00
caroline.slt 7189133861 Korrektur 2026-07-18 18:09:11 +02:00
caroline.slt 8c961a56ce Korrektur 2026-07-18 17:54:30 +02:00
caroline.slt 4f95a3d423 JavaScript Korrektur 2026-07-18 17:43:45 +02:00
niklas.ortmann 35e5006063 Update navbar.php 2026-07-18 17:39:14 +02:00
niklas.ortmann fcb9289d80 Update navbar.php 2026-07-18 17:38:27 +02:00
niklas.ortmann 6595a3d78d Update index.php 2026-07-18 17:36:52 +02:00
niklas.ortmann 41d8ac6721 Update navbar.php 2026-07-18 17:35:38 +02:00
niklas.ortmann 63c213bae7 Update navbar.php 2026-07-18 17:34:24 +02:00
niklas.ortmann 61d72436c9 Update navbar.php 2026-07-18 17:33:40 +02:00
niklas.ortmann 721bc8d833 Update navbar.php 2026-07-18 17:32:37 +02:00
niklas.ortmann 2666d2d14f Update navbar.php 2026-07-18 17:32:01 +02:00
niklas.ortmann de1ba88860 Update showCategory.php 2026-07-18 17:30:17 +02:00
niklas.ortmann e0fb04794d Update resultsHelper.php 2026-07-18 17:28:53 +02:00
caroline.slt 5310083e11 JavaScript Korrektur 2026-07-18 17:23:52 +02:00
niklas.ortmann 6897a29574 Update search-results-data.php 2026-07-18 17:23:30 +02:00
caroline.slt b564306fb3 JavaScript Korrektur 2026-07-18 17:20:14 +02:00
niklas.ortmann 8836034670 Kategoriepaginator 2026-07-18 17:17:44 +02:00
niklas.ortmann 98b088afc0 Update alertMessages.php 2026-07-18 17:00:31 +02:00
niklas.ortmann 51c850d122 Update 404.php 2026-07-18 16:59:47 +02:00
niklas.ortmann d590561ade refactoring 2026-07-18 16:59:23 +02:00
niklas.ortmann ba11c216bd refactoring 2026-07-18 16:56:41 +02:00
niklas.ortmann 71a1b029cb refactoring 2026-07-18 16:47:09 +02:00
niklas.ortmann 2126d076d5 Entfernung alter js 2026-07-18 16:47:01 +02:00
caroline.slt 7b7208ad6b Kommentare löschen 2026-07-18 16:44:05 +02:00
niklas.ortmann 2ecd446dd0 search-results.js
Ohne JS: search-results.php sortiert/filtert/paginiert komplett serverseitig anhand der GET-Parameter (?sort=&category=&page=) und cached dabei den Session-Datensatz — es wird keine neue Suche ausgelöst, nur der bestehende $_SESSION["search_results"]-Datensatz neu aufbereitet. Echte <a href>-Links für die Pagination, ein <noscript>-Submit-Button für Sortierung/Filter.
Mit JS: results-app.js lädt den kompletten Datensatz einmalig per fetch() von einem neuen schlanken JSON-Endpoint nach und übernimmt Sortierung, Filterung und Pagination danach zu 100% im Browser — keine weiteren Serveranfragen mehr, kein Page-Reload.
sorter.js, filter.js, paginator.js werden durch die eine Datei results-app.js ersetzt (löst den Button-Konflikt).
2026-07-18 16:35:13 +02:00
niklas.ortmann 97302f1bb3 Update search-results-controller.php 2026-07-18 16:22:03 +02:00
niklas.ortmann 35d4425086 Update index-controller.php 2026-07-18 16:19:48 +02:00
niklas.ortmann d3435d4cde Update index.php 2026-07-18 16:14:09 +02:00
niklas.ortmann f61c548b9e Update index.php 2026-07-18 16:11:53 +02:00
niklas.ortmann 101a2a247b Update index-controller.php 2026-07-18 16:08:05 +02:00
niklas.ortmann 423941e7ed Update index-controller.php 2026-07-18 16:06:35 +02:00
niklas.ortmann a8e76c782d Update index-controller.php 2026-07-18 15:53:33 +02:00
niklas.ortmann 3f9c048493 Update index.php 2026-07-18 15:53:31 +02:00
niklas.ortmann fee5a701df Update index-controller.php 2026-07-18 15:50:07 +02:00
niklas.ortmann d341312192 Update index-controller.php 2026-07-18 15:47:25 +02:00
niklas.ortmann 4dadfb8863 Update index.php 2026-07-18 15:47:23 +02:00
caroline.slt ada97ec538 Kommentare ohne js verfassen 2026-07-18 15:26:13 +02:00
niklas.ortmann 0b4c5f988d Merge pull request 'Anzeigenamen' (#51) from AnzeigeNamen into dev
Reviewed-on: #51
2026-07-17 18:09:29 +02:00
niklas.ortmann 59eeb05e4b Update showCategory.php 2026-07-17 18:05:33 +02:00
niklas.ortmann 5d8239822a Update showCategory.php 2026-07-17 18:04:03 +02:00
niklas.ortmann 31585511d0 Update showCategory-controller.php 2026-07-17 18:01:59 +02:00
niklas.ortmann 2ec1b70c91 Update Article.php 2026-07-17 17:59:17 +02:00
niklas.ortmann 8f8b25249c Update showCategory-controller.php 2026-07-17 17:56:31 +02:00
niklas.ortmann 8be6e6d61c Update showCategory-controller.php 2026-07-17 17:55:37 +02:00
niklas.ortmann bad0c08e6f Update search-results-controller.php 2026-07-17 17:51:58 +02:00
niklas.ortmann 55695f3f82 Update showArticle-controller.php 2026-07-17 17:42:18 +02:00
niklas.ortmann 74cbc60cbe Update showArticle.php 2026-07-17 17:41:42 +02:00
niklas.ortmann c09c7ecfa3 Update showArticle-controller.php 2026-07-17 17:41:41 +02:00
niklas.ortmann 944e332b49 Merge pull request 'Kategorieseitenpaginator' (#50) from KategorieseitenPaginator into dev
Reviewed-on: #50
2026-07-17 17:36:10 +02:00
niklas.ortmann 4306785be9 Merge pull request 'Beitrag Bearbeiten Autorenprüfung' (#49) from Beitrag-bearbeiten-Autorenpruefung into dev
Reviewed-on: #49
2026-07-17 17:36:05 +02:00
niklas.ortmann 266b70a094 Update home.php 2026-07-17 17:32:25 +02:00
niklas.ortmann bd8593d212 Update 404.php 2026-07-17 17:29:50 +02:00
niklas.ortmann 0d009262b6 Update showCategory.php 2026-07-17 17:27:16 +02:00
niklas.ortmann bb2c368b1f Seitenleiste 2026-07-17 17:20:54 +02:00
niklas.ortmann a712f5a993 Update showCategory.css 2026-07-17 17:17:38 +02:00
niklas.ortmann 53bee08833 Update showCategory.php 2026-07-17 17:16:42 +02:00
niklas.ortmann 7a2a3ac1e9 Update showCategory.php 2026-07-17 17:13:41 +02:00
niklas.ortmann a5f02c9a6c Update showCategory.php 2026-07-17 17:13:07 +02:00
niklas.ortmann f77f09ec6f Update showCategory.php 2026-07-17 17:11:40 +02:00
niklas.ortmann 535df85cff neue showCategory.css 2026-07-17 17:11:38 +02:00
niklas.ortmann 82e8c8a1a3 Update showCategory.php 2026-07-17 17:08:02 +02:00
niklas.ortmann c0d8fdede6 Update showCategory.php 2026-07-17 17:04:54 +02:00
niklas.ortmann 958ee1c1f5 Update showCategory.php
css
2026-07-17 17:02:11 +02:00
niklas.ortmann 1dd128d480 Update showCategory.php 2026-07-17 16:59:36 +02:00
niklas.ortmann dca716ddc5 Update showCategory-controller.php 2026-07-17 16:59:34 +02:00
niklas.ortmann c1dd4fce2b Update showCategory.php 2026-07-17 16:41:09 +02:00
niklas.ortmann 16cfa92721 Update showCategory.php 2026-07-17 16:29:11 +02:00
niklas.ortmann 3b2ed78c1c Update showCategory.php 2026-07-17 16:16:14 +02:00
niklas.ortmann cebbe8869a Update showArticle-controller.php 2026-07-17 15:59:29 +02:00
niklas.ortmann e07051b356 Update showArticle-controller.php 2026-07-17 15:58:10 +02:00
niklas.ortmann 2d2bbeb39f Update showArticle-controller.php 2026-07-17 15:57:15 +02:00
niklas.ortmann c00443b6fa Update showArticle-controller.php 2026-07-17 15:56:34 +02:00
niklas.ortmann 1d541e285c Update showArticle-controller.php 2026-07-17 15:54:47 +02:00
niklas.ortmann b96c588697 Merge branch 'DummyData' into dev 2026-07-17 15:03:15 +02:00
niklas.ortmann fde9c59f91 Merge branch 'Datenbanken_zusammenfuehren' into dev 2026-07-17 14:57:22 +02:00
niklas.ortmann 72c7363a07 Datenschutz: Cookies 2026-07-10 08:32:07 +02:00
niklas.ortmann 0b36050558 Update dataSources.local.xml 2026-07-10 08:27:22 +02:00
NOrtmann1 00dbf83da4 Merge branch 'Datenbanken_zusammenfuehren' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into Datenbanken_zusammenfuehren 2026-07-07 14:13:19 +02:00
NOrtmann1 c3e060b963 Update DatabaseInitializer.php 2026-07-07 14:13:11 +02:00
niklas.ortmann a382d9254e Update README.md 2026-07-01 19:33:56 +02:00
niklas.ortmann 2b7f44b817 Merge remote-tracking branch 'origin/datenschutzkonformeRegistrierung' into dev 2026-07-01 19:30:44 +02:00
niklas.ortmann b5e4f44252 Merge branch 'dev' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into dev 2026-07-01 19:19:52 +02:00
niklas.ortmann 8561b02a08 Update README.md 2026-07-01 19:19:43 +02:00
niklas.ortmann 01f4ff2d28 Update dataSources.local.xml 2026-07-01 19:19:41 +02:00
niklas.ortmann b7e4387373 Merge pull request 'Database - und LocalArticleManager angepasst, sodass nun auch nach tags gesucht wird.' (#47) from SearchByTags into dev
Reviewed-on: #47
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-07-01 19:15:06 +02:00
niklas.ortmann e9a92eaec7 merge 2026-07-01 19:11:32 +02:00
niklas.ortmann 6aec353d50 Update DatabaseInitializer.php 2026-07-01 19:07:58 +02:00
rirat-0 7a4f2d738f Debugging 2026-07-01 00:00:46 +02:00
rirat-0 a0a469a9df Update der Suchfunktionen um nach tags zu suchen 2026-06-30 23:58:02 +02:00
niklas.ortmann 54209929fe Merge pull request 'Implementierung von Nutzungsbedingungen und Barrierefreiheitserklärung' (#46) from Nutzungsbedingungen&barrierefreiheit into dev
Reviewed-on: #46
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 22:41:56 +02:00
rirat-0 5321534d71 anpassung der filterJS fuer ergebnislimits 2026-06-30 21:13:02 +02:00
caroline.slt 7cf542c270 Richtig Einbinden 2026-06-30 21:09:49 +02:00
caroline.slt c45d23a6b3 Korrektur 2026-06-30 21:07:53 +02:00
caroline.slt ab094429b7 Erste Implementierung 2026-06-30 21:04:24 +02:00
caroline.slt cc5f46c00e Reviewänderungen 2026-06-30 20:25:46 +02:00
rirat-0 1b2ab1cccb rechtschreibung 2026-06-30 20:25:46 +02:00
rirat-0 adbf488e01 datenschutz und impressum wurden ersten, die readme entsprechend angepasst 2026-06-30 20:25:46 +02:00
niklas.ortmann 15ea5ddbfb Merge pull request 'datenschutz und impressum wurden erstellt, die readme entsprechend angepasst' (#43) from ImpressumUndDatenschutz into dev
Reviewed-on: #43
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-30 18:13:51 +02:00
niklas.ortmann b93ca321a2 Update DatabaseInitializer.php 2026-06-30 17:46:42 +02:00
niklas.ortmann 97847c76c4 Update DatabaseInitializer.php 2026-06-30 17:44:36 +02:00
niklas.ortmann 3dae5ee720 Update dataSources.local.xml 2026-06-30 17:42:53 +02:00
rirat-0 cbb662d159 testing 2026-06-30 16:16:14 +02:00
rirat-0 c5e7439541 rechtschreibung 2026-06-30 16:11:08 +02:00
rirat-0 a748d94305 datenschutz und impressum wurden ersten, die readme entsprechend angepasst 2026-06-30 16:08:26 +02:00
caroline.slt 6c1a893f80 Passwort vergessen Funktion 2026-06-26 00:05:45 +02:00
caroline.slt 788e803e84 Passwort vergessen Funktion 2026-06-25 23:45:17 +02:00
caroline.slt 4d0f1de01c Passwort vergessen Funktion 2026-06-25 23:40:33 +02:00
caroline.slt e71ee8300e Qualitätsanpassungen 2026-06-25 23:23:55 +02:00
caroline.slt f39885da10 CSS Anpassungen 2026-06-25 23:05:49 +02:00
caroline.slt b7ca5bcfe2 test rückgängig machen 2026-06-25 22:57:35 +02:00
caroline.slt 57b0782577 test 2026-06-25 22:53:45 +02:00
caroline.slt 4f524db221 Implementierung 2026-06-25 22:46:14 +02:00
NOrtmann1 c2b7462307 Update DatabaseInitializer.php 2026-06-24 17:38:40 +02:00
NOrtmann1 cdbf81878b DummyErstellung in DatabaseInitializer 2026-06-24 17:36:28 +02:00
NOrtmann1 68ec6dedd7 dummy-Anpassungen 2026-06-24 17:29:33 +02:00
NOrtmann1 12782463d7 Dummy-Daten aus den Managern ausgelagert 2026-06-24 17:19:21 +02:00
NOrtmann1 21a935edf3 require_once 'DatabaseInitializer.php'; 2026-06-24 16:41:20 +02:00
NOrtmann1 3d65f79d2c DatabaseInitializer 2026-06-24 16:36:11 +02:00
NOrtmann1 ed31210e3a Fremdschlüssel hinzugefügt 2026-06-24 15:59:41 +02:00
NOrtmann1 5030caa029 DatabaseCommentManager: neue Datenbank 2026-06-24 15:28:02 +02:00
NOrtmann1 4a87152531 DatabaseUserManager: neue Datenbank 2026-06-24 15:26:49 +02:00
NOrtmann1 1401c3afe7 DatabaseArticleManager: neue Datenbank 2026-06-24 15:23:39 +02:00
NOrtmann1 d5a3f4298f Update README.md 2026-06-17 22:39:43 +02:00
NOrtmann1 b4a5c9c020 Update README.md 2026-06-17 22:38:23 +02:00
NOrtmann1 6d6f1f31b0 Update updateArticle-controller.php 2026-06-17 22:28:01 +02:00
NOrtmann1 cafffba921 Update updateArticle-controller.php 2026-06-17 22:21:00 +02:00
NOrtmann1 8fa758d825 Update dataSources.local.xml 2026-06-17 22:20:55 +02:00
rirat-0 17034c928b anpassung der search results fuer die filterung nach likes 2026-06-17 22:03:51 +02:00
rirat-0 395041fd44 anpassung, damit die like sortierung client seitig ist 2026-06-17 21:59:12 +02:00
rirat-0 c117c7e641 Merge branch 'dev' into SuchergebnisseJS 2026-06-17 21:48:58 +02:00
niklas.ortmann 01fc8537cf Merge pull request 'Bewertungssystem' (#36) from Bewertungssystem into dev
Reviewed-on: #36
2026-06-17 21:29:24 +02:00
rirat-0 12ad9759cf aufraeumen 2 2026-06-17 20:42:31 +02:00
rirat-0 057639be7c anpassung der error message 2026-06-17 20:38:19 +02:00
rirat-0 87e7be1c10 Implementation des validators und korrektes einbinden 2026-06-17 20:30:45 +02:00
rirat-0 1db77346d0 Revert "Implementierung des validators"
This reverts commit 1e6ac0fcd9.
2026-06-17 20:14:59 +02:00
rirat-0 1e6ac0fcd9 Implementierung des validators 2026-06-17 20:14:07 +02:00
rirat-0 b3c74c2172 Debugging 8 2026-06-17 19:54:31 +02:00
rirat-0 0805958033 Anpassung der filter.js
es gab ein problem bei der sortierung, da nur 10 ergebnisse angezeigt werden. mit dieser anpassung koennen ergebnisse 'nachruecken'
2026-06-17 19:47:33 +02:00
rirat-0 5c0e978c93 Debugging 7 2026-06-17 19:38:00 +02:00
rirat-0 520298bae4 Debugging 6 2026-06-17 19:35:02 +02:00
rirat-0 969adc4836 Debugging 5 2026-06-17 19:33:09 +02:00
rirat-0 d9eca06197 Debugging 4 2026-06-17 19:30:11 +02:00
rirat-0 48f2e90e49 Debugging 3 2026-06-17 19:21:58 +02:00
rirat-0 358247a2a1 Filter funktion implementiert 2026-06-17 19:19:31 +02:00
rirat-0 de4d2fe5c0 sortieren nach likes nun moeglich 2026-06-17 17:58:40 +02:00
rirat-0 e1102eb7db Merge branch 'dev' into SuchergebnisseJS 2026-06-17 17:48:08 +02:00
rirat-0 a8df9590fd updater sorter.js fuer client seite 2026-06-17 15:38:59 +02:00
rirat-0 3e453e22ec verschieben der script aufrufe in die index.php 2026-06-17 15:31:05 +02:00
rirat-0 beeab0ec90 aufraeumen 2026-06-17 14:50:05 +02:00
rirat-0 9353a7eaaa Debugging 2 2026-06-17 14:48:33 +02:00
rirat-0 cac8f3046d Debugging 1 2026-06-17 14:45:01 +02:00
rirat-0 66eeac372c Anpassung der results seite und erstellung der script-datei 2026-06-17 14:41:05 +02:00
niklas.ortmann 4a6a3cf708 search-results -> Likes-Sortierung 2026-06-17 13:23:39 +02:00
niklas.ortmann cb44c3f5a8 Update search-results-controller.php 2026-06-17 13:11:03 +02:00
niklas.ortmann eb0a34e959 Create like-controller.php 2026-06-17 13:00:12 +02:00
niklas.ortmann 8d683cc1a0 showArticle -> like-Button 2026-06-17 12:53:33 +02:00
niklas.ortmann ee41dd4cfe Update LocalArticleManager.php 2026-06-17 12:35:17 +02:00
niklas.ortmann 3d50e6e3b3 ArticleManager um LIKES erweitert...
Einführung einer dynamischen Like-Funktion für Beiträge
2026-06-17 12:26:18 +02:00
niklas.ortmann 4f8d11881d Merge pull request 'home zeigt nun dynamisch die Kategorien an' (#35) from Startseite into dev
Reviewed-on: #35
2026-06-17 12:08:10 +02:00
niklas.ortmann 5c924d3277 Update profile.php 2026-06-17 12:06:49 +02:00
niklas.ortmann 7f703a8386 Update profile.php 2026-06-17 12:03:25 +02:00
niklas.ortmann f063ea4741 Update profile.php 2026-06-17 12:02:41 +02:00
niklas.ortmann ca643dd298 Update profile.php 2026-06-17 12:02:20 +02:00
niklas.ortmann e19225e49e Update profile.php 2026-06-17 12:01:38 +02:00
niklas.ortmann 9ffbca679e Update profile.php 2026-06-17 12:00:59 +02:00
niklas.ortmann d90a10e462 Update profile.php 2026-06-17 12:00:36 +02:00
niklas.ortmann b6f25d041b Anmerkungen von Maximilian zu Blatt 04
Ist der PHP-Code gut verständlich und übersichtlich strukturiert? Beinahe vollständig. ArticleManager::getInstance() erzeugt Dummy-Daten direkt beim Abruf der Instanz; etwas unübersichtlich, aber im Kontext der Dummy-Daten nicht zu gewichten (heißt es wird nicht negativ angerechnet).

Wie ist die Code-Qualität? Teilweise. articleAuthorValidator() ist mit TODO versehen und gibt immer true zurück. In home.php wird $dummy3->getId() aufgerufen, bevor geprüft wird, ob $dummy3 überhaupt existiert.

Wird redundanter Code vermieden? Teilweise. Die Kategorie-Listen stehen mehrfach separat in createArticle.php, updateArticle.php, navbar.php und article-validator.php. Fehlerausgaben für Artikel-Erstellung und Artikel-Bearbeitung sind ebenfalls fast identisch doppelt vorhanden.

Findet eine adäquate Trennung von PHP-Code und HTML-Code statt? Beinahe vollständig. In den Views werden teilweise noch Controller direkt eingebunden, z. B. profile.php bindet profile-controller.php ein und updateArticle.php nutzt showArticle-controller.php, um Daten zu laden (kann man theoretisch so machen, an dieser Stelle ist die Trennung dann aber nicht mehr wirklich sauber auf einem fundamentalen Level).

Ist eine Trennung in Model, View und Controller ersichtlich und auch korrekt durchgeführt? Teilweise. content-Dateien übernehmen teilweise Routing-/Controller-Aufgaben. Views binden Controller selbst ein.

Wurde das DAO-Pattern korrekt umgesetzt? Ok hier und da mit extends ein wenig fragwürdig aber kein Fehler.

Werden Sessions korrekt eingesetzt? Ok. session_start() wird mehrfach aufgerufen, z. B. in index.php, navbar.php, home-controller.php und search-results.php. Nach Login oder Registrierung wird keine neue Session-ID erzeugt (dies ist kein muss).

Findet in jedem Skript falls notwendig zunächst eine Authentifizierung angemeldeter Nutzer statt? Teilweise. createArticle-controller.php prüft bei direktem Aufruf nicht, ob ein Nutzer angemeldet ist. updateArticle-controller.php prüft ebenfalls nicht zu Beginn sauber die Authentifizierung. Die Authentifizierung liegt teilweise nur in der Formularseite, nicht im verarbeitenden Controller.

Ist bei „Eintrag bearbeiten/löschen“ sichergestellt, dass nur der Eigentümer den Eintrag verändern kann? Beinahe vollständig. Die eigentliche Aktualisierung prüft den Autor. Das Bearbeitungsformular updateArticle.php kann aber von jedem eingeloggten Nutzer mit fremder Artikel-ID geöffnet werden.

Werden in jedem Skript alle GET- bzw. POST-Parameter überprüft und ggf. auf Validität überprüft? Nein. pfad in index.php wird nicht per Whitelist validiert. id in showArticle-controller.php und updateArticle-controller.php wird nicht als gültige numerische ID geprüft. sort in search-results-controller.php wird nicht gegen erlaubte Werte validiert.

Werden bei Eingabefehlern durch den Nutzer die von ihm eingegebenen Daten bei der erneuten Formularanzeige übernommen? Teilweise. In createArticle.php wird old_category zwar gespeichert, aber nicht wieder im Dropdown ausgewählt (in updateArticle.php als TODO vermerkt). Beim Login wird die eingegebene E-Mail-Adresse nach einem Fehler nicht erneut angezeigt.

Wird eine adäquate Überprüfung von potentiellen Fehlern und Fehlerbehandlung durchgeführt? Teilweise. LocalArticleManager::saveArticle() prüft nicht, ob file_put_contents() erfolgreich war. getAllArticles() ignoriert fehlerhaftes JSON und gibt dann einfach ein leeres Array zurück. home-controller.php gibt im Fehlerfall direkt die Exception-Message aus.

Erfolgen bei Aktionen des Nutzers sowohl positive als auch negative Meldungen auf der nachfolgend angezeigten Seite? Teilweise. Nach erfolgreichem Login oder Registrierung gibt es keine sichtbare Erfolgsmeldung. Nach erfolgreicher Profiländerung gibt es ebenfalls keine positive Meldung. Beim Löschen des Accounts erfolgt nur eine Weiterleitung ohne Feedback.

Werden alle Eingabedaten beim Einbau in die HTML-Seite mit htmlspecialchars bzw. htmlentities HTML-kodiert? Beinahe vollständig. In home.php werden die Titel der Dummy-/Artikel-Links ohne htmlspecialchars() ausgegeben.

Werden alle Eingabedaten beim Einbau in eine URL mit urlencode URL-kodiert? Ok. Artikel-IDs werden mehrfach direkt in URLs eingesetzt, z. B. in profile.php, search-results.php und showCategory.php (Anmerkung: Rechne ich hier nicht negativ an). Auch die Formular-Action in updateArticle.php baut die ID ohne urlencode() in die URL ein (rechne ich ebenfalls nicht negativ an).
2026-06-17 11:58:49 +02:00
niklas.ortmann 4321c4bee5 home zeigt nun dynamisch die Kategorien an 2026-06-17 11:31:23 +02:00
niklas.ortmann d1b3641754 Update dataSources.local.xml 2026-06-17 11:11:19 +02:00
niklas.ortmann 5281c1bc7d Merge branch 'erweiterterEditor' into dev 2026-06-17 10:30:01 +02:00
niklas.ortmann aae78bf479 Update ArticleManager.php 2026-06-17 10:24:48 +02:00
niklas.ortmann af17764346 Merge pull request 'Forum implementiert' (#34) from Forum into dev
Reviewed-on: #34
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-17 10:24:24 +02:00
caroline.slt a8d321609d Reviewänderungen 2026-06-16 22:31:49 +02:00
caroline.slt 23ccbe2d02 Reviewänderungen 2026-06-16 22:09:17 +02:00
caroline.slt 47239016c5 Fehlerbehebung 2026-06-16 00:02:06 +02:00
caroline.slt f4f6960b9d Kommentare im Profil anzeigen 2026-06-15 23:55:10 +02:00
caroline.slt c4c299eed6 Fehlerbehebung 2026-06-15 23:41:28 +02:00
caroline.slt e20b59b43d Kommentarfunktion für Nutzer einschränken 2026-06-15 23:22:00 +02:00
caroline.slt 901232b683 Antwortmöglichkeit 2026-06-15 22:59:44 +02:00
caroline.slt 40c5fa69f5 Antwortmöglichkeit 2026-06-15 22:58:22 +02:00
caroline.slt 7694396454 Antwortmöglichkeit 2026-06-15 22:56:00 +02:00
caroline.slt dafc149bba Antwortmöglichkeit 2026-06-15 22:50:55 +02:00
caroline.slt f23ec27247 Antwortmöglichkeit 2026-06-15 22:47:50 +02:00
caroline.slt efb179dec2 Antwortmöglichkeit 2026-06-15 22:41:05 +02:00
caroline.slt 86222aa92e Antwortmöglichkeit 2026-06-15 22:37:50 +02:00
caroline.slt a5fb5cd360 Antwortmöglichkeit 2026-06-15 22:35:42 +02:00
caroline.slt 6f6e53a483 Antwortmöglichkeit 2026-06-15 22:32:00 +02:00
caroline.slt f13a2c6f1e css design 2026-06-15 22:17:44 +02:00
caroline.slt 59012cacfb css design 2026-06-15 22:14:51 +02:00
caroline.slt 21c5471d73 fehlerkorrektur 2026-06-15 22:08:46 +02:00
caroline.slt e1ee70b46f ajax implementiert 2026-06-15 21:54:06 +02:00
caroline.slt eb56b8d6a0 Implementierung 2026-06-15 21:45:17 +02:00
niklas.ortmann d597046f41 Merge pull request 'Paginator in der search-results implementiert mit js' (#32) from Search_Paginator into dev
Reviewed-on: #32
2026-06-14 23:42:29 +02:00
niklas.ortmann ae1c873536 Merge pull request 'Beitrag erstellen oder bearbeiten: Bei einer Fehlermeldung wird die Kategorie nicht mehr zurückgesetzt' (#31) from bugfix_KategorieNichtZuruecksetzen into dev
Reviewed-on: #31
2026-06-14 23:42:20 +02:00
niklas.ortmann 1f7a2a77f6 dummy-Einträge 2026-06-14 23:37:42 +02:00
niklas.ortmann 9269465f6b Update README.md 2026-06-14 23:37:30 +02:00
niklas.ortmann 883adfd242 Update updateArticle-controller.php 2026-06-14 23:32:33 +02:00
niklas.ortmann 1e890f9a28 Update updateArticle-controller.php 2026-06-14 23:31:42 +02:00
niklas.ortmann 2bb13d2e8c revert f3f042b3b9
revert debugging
2026-06-14 23:30:32 +02:00
niklas.ortmann cb02e05d55 Update createArticle-controller.php 2026-06-14 23:27:30 +02:00
niklas.ortmann 05777f6109 revert 7d9aae2a59
revert Update createArticle-controller.php
2026-06-14 23:25:55 +02:00
niklas.ortmann f3f042b3b9 debugging 2026-06-14 23:16:26 +02:00
niklas.ortmann dfb13b1f96 Update editor.js 2026-06-14 23:13:46 +02:00
niklas.ortmann 6fa5038589 Update editor.js 2026-06-14 23:09:49 +02:00
niklas.ortmann 55f53e3346 Update editor.js 2026-06-14 23:06:00 +02:00
niklas.ortmann f36037d36d debugging 2026-06-14 23:02:53 +02:00
niklas.ortmann 500bca80c1 Update editor.js 2026-06-14 23:00:21 +02:00
niklas.ortmann fc0361ed3a debugging 2026-06-14 23:00:19 +02:00
niklas.ortmann df1485ce49 Update updateArticle-controller.php 2026-06-14 22:53:49 +02:00
niklas.ortmann b8bceb31c6 debugging 2026-06-14 22:51:05 +02:00
niklas.ortmann f0308c3505 Update updateArticle.php 2026-06-14 22:49:48 +02:00
niklas.ortmann e7ece1ed13 Update updateArticle-controller.php 2026-06-14 22:47:05 +02:00
niklas.ortmann 8ed7f7ec8f gelöschte Bilder entfernen 2026-06-14 22:41:16 +02:00
niklas.ortmann feaef4a624 gelöschte Bilder aus /uploads löschen 2026-06-14 22:23:06 +02:00
niklas.ortmann 7d9aae2a59 Update createArticle-controller.php 2026-06-14 22:18:08 +02:00
niklas.ortmann a88f8ca638 Update updateArticle-controller.php 2026-06-14 22:12:12 +02:00
niklas.ortmann be6a5f9add Update article-validator.php 2026-06-14 22:12:10 +02:00
niklas.ortmann ed4498578d Update alertMessages.php 2026-06-14 22:10:13 +02:00
niklas.ortmann 118be50b3c Update alertMessages.php 2026-06-14 22:01:15 +02:00
niklas.ortmann 1e2184ae20 Update showArticle.php 2026-06-14 22:01:14 +02:00
niklas.ortmann 8c1e1a64bf Update article-validator.php 2026-06-14 21:59:19 +02:00
niklas.ortmann b9c28968fe Create .htaccess 2026-06-14 21:54:35 +02:00
niklas.ortmann 703d8d7519 Update editor.js 2026-06-14 21:50:28 +02:00
niklas.ortmann 8e52d99ba4 Update updateArticle-controller.php 2026-06-14 21:42:58 +02:00
niklas.ortmann d4020671c1 updateArticle 2026-06-14 21:39:09 +02:00
niklas.ortmann 5c432e5801 showArticle-Anpassungen 2026-06-14 13:09:38 +02:00
niklas.ortmann aac13297de testing 2026-06-14 13:05:55 +02:00
niklas.ortmann f60a4a4f60 Update article-validator.php 2026-06-14 13:05:20 +02:00
niklas.ortmann 0ed4de74b0 Update createArticle-controller.php 2026-06-14 13:03:14 +02:00
niklas.ortmann 48bd26342a debugging 2026-06-14 12:59:58 +02:00
niklas.ortmann b8e884a5aa Update createArticle-controller.php 2026-06-14 12:53:46 +02:00
niklas.ortmann 1dbef5ca4b Update createArticle-controller.php 2026-06-14 12:42:29 +02:00
niklas.ortmann e43ae7f96a Update editor.js 2026-06-14 11:01:13 +02:00
niklas.ortmann eab256d17f Update editor.js 2026-06-14 10:59:10 +02:00
niklas.ortmann bbdcbbf0ce Update editor.js 2026-06-14 10:55:33 +02:00
niklas.ortmann ec9bc3fe1f css 2026-06-14 10:53:21 +02:00
niklas.ortmann 80f92a384e erweiterter Beitragseditor
content ist nun im json-Format
Bilder können hochgeladen werden
Textblöcke können im Editor angehangen werden
2026-06-14 10:44:17 +02:00
niklas.ortmann 95251d23ad Update search-results-controller.php 2026-06-13 12:26:49 +02:00
niklas.ortmann c16bf574ce Update search-results.php 2026-06-13 12:21:38 +02:00
niklas.ortmann 0e44d92baa paginator.js 2026-06-13 12:19:37 +02:00
niklas.ortmann f9c1c67a38 Update ArticleManager.php 2026-06-13 11:58:25 +02:00
niklas.ortmann 286e2b6237 Update ArticleManager.php 2026-06-13 11:54:57 +02:00
niklas.ortmann fb78794261 Update UserManager.php 2026-06-13 11:47:14 +02:00
niklas.ortmann 371a65d361 Update search-results.php 2026-06-13 11:47:12 +02:00
niklas.ortmann 7bd3c8dfd1 Update README.md 2026-06-13 11:47:06 +02:00
niklas.ortmann e5aa6ea25d Update navbar.css 2026-06-13 11:38:10 +02:00
niklas.ortmann de9afd3275 Update search.php 2026-06-13 11:33:19 +02:00
niklas.ortmann 79e9f17f70 Update navbar.css 2026-06-13 11:33:16 +02:00
niklas.ortmann 12cc3c5d5b Update navbar.css 2026-06-13 11:31:19 +02:00
niklas.ortmann 404cea74ff Update navbar.css 2026-06-13 11:30:02 +02:00
niklas.ortmann 16e94947f2 Update search.php 2026-06-13 11:27:01 +02:00
niklas.ortmann aff0b0560f Update navbar.css 2026-06-13 11:26:01 +02:00
niklas.ortmann de19a6fc65 Update navbar.css 2026-06-13 11:24:46 +02:00
niklas.ortmann 1f669aadfc Update README.md 2026-06-13 11:24:42 +02:00
niklas.ortmann 33f428559c Update navbar.css 2026-06-13 11:21:36 +02:00
niklas.ortmann 0c3b9eb250 Update navbar.css 2026-06-13 11:20:41 +02:00
niklas.ortmann c654fab94a hover-effekt in Navbar 2026-06-13 11:19:22 +02:00
NOrtmann1 f2ef3c08a7 Update createArticle.php 2026-06-11 15:36:45 +02:00
NOrtmann1 8a66e8af3f Update updateArticle.php 2026-06-11 12:25:18 +02:00
NOrtmann1 41d1cc6013 Update updateArticle.php 2026-06-11 12:12:40 +02:00
NOrtmann1 c9c6a4ae81 Update dataSources.local.xml 2026-06-11 12:12:35 +02:00
niklas.ortmann a5131e33dc search-Methode angepasst 2026-06-10 20:42:32 +02:00
niklas.ortmann a19d1db990 search-Methode in DatabaseArticleManager 2026-06-10 20:39:27 +02:00
niklas.ortmann b6e7da6808 Update README.md 2026-06-10 20:36:15 +02:00
niklas.ortmann f21fab937f new_article Alert 2026-06-10 20:33:05 +02:00
niklas.ortmann 30fccfe69b Update dataSources.local.xml 2026-06-10 20:32:51 +02:00
NOrtmann1 b692d76f7d session_start wird nun vorher geprüft, ob diese schon läuft 2026-06-10 18:39:39 +02:00
NOrtmann1 56533d4a16 Update deleteAccount-controller.php 2026-06-10 18:34:49 +02:00
NOrtmann1 8a22b475f9 Update deleteAccount-controller.php 2026-06-10 18:33:44 +02:00
NOrtmann1 ae3a1be5e1 Update deleteAccount-controller.php 2026-06-10 18:32:12 +02:00
NOrtmann1 8199afc88f Update navbar.css 2026-06-10 18:27:55 +02:00
NOrtmann1 2d7ef8fc1d Update navbar.css 2026-06-10 18:27:09 +02:00
NOrtmann1 d75d78ccb3 css 2026-06-10 18:26:39 +02:00
NOrtmann1 42489f50b6 Update deleteAccount-controller.php 2026-06-10 18:24:06 +02:00
NOrtmann1 52fde19380 Update deleteAccount-controller.php 2026-06-10 18:22:36 +02:00
NOrtmann1 b1fb3e5baa Update navbar.php 2026-06-10 18:14:13 +02:00
NOrtmann1 5f3dae64c1 Update deleteAccount-controller.php 2026-06-10 18:12:02 +02:00
NOrtmann1 b31ce66ef9 Abmelden-Button aus Profile entfernt 2026-06-10 18:10:48 +02:00
NOrtmann1 93bd561ff9 refactoring 2026-06-10 18:08:23 +02:00
NOrtmann1 d0d93be96b Update profile.php 2026-06-10 18:06:52 +02:00
NOrtmann1 327a05cf63 Profil -> Beitrag Link 2026-06-10 18:01:51 +02:00
NOrtmann1 1394467abe refactoring 2026-06-10 17:55:02 +02:00
niklas.ortmann ace73f15ef Merge pull request 'Beiträge_loeschen' (#29) from Beitrag_loeschen into dev
Reviewed-on: #29
2026-06-10 17:51:34 +02:00
NOrtmann1 a5e1fb4c38 css 2026-06-10 17:49:33 +02:00
NOrtmann1 ac2451fe64 Update deleteArticle-controller.php 2026-06-10 17:34:39 +02:00
NOrtmann1 5f9789a884 alerts 2026-06-10 17:28:40 +02:00
NOrtmann1 e0fbd220f0 debugging 2026-06-10 17:23:36 +02:00
NOrtmann1 a0932fed04 debugging 2026-06-10 17:21:28 +02:00
NOrtmann1 42aa908f1c Update deleteArticle-controller.php 2026-06-10 17:20:22 +02:00
NOrtmann1 da9dc0e8fb Update deleteArticle-controller.php 2026-06-10 17:15:29 +02:00
NOrtmann1 36f6396148 Update deleteArticle-controller.php 2026-06-10 17:13:49 +02:00
viratex 12facddcd5 PDF vom Datenschema 2026-06-10 17:09:24 +02:00
NOrtmann1 429ecb4e0e Update profile.php 2026-06-10 17:09:13 +02:00
NOrtmann1 8019d9c99a deleteArticle() 2026-06-10 17:06:41 +02:00
NOrtmann1 9f9c92385d Update ArticleManager.php 2026-06-10 16:18:01 +02:00
NOrtmann1 9ecfc3b6f8 Pfadanpassungen 2026-06-10 16:16:40 +02:00
niklas.ortmann d00f2d9d53 Merge pull request 'DatabaseUserManager' (#25) from DatabaseUserManager into dev
Reviewed-on: #25
Reviewed-by: niklas.ortmann <ortmann.niklas@yahoo.de>
2026-06-10 16:15:38 +02:00
NOrtmann1 37a032aa9f Merge branch 'dev' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into dev 2026-06-10 16:15:11 +02:00
NOrtmann1 fd8e13eaa9 Update LocalArticleManager.php 2026-06-10 16:14:59 +02:00
niklas.ortmann b5d1e08d22 Merge pull request 'bugfix_updateArticle_missingID & updateArticle Leerzeichen im content' (#28) from bugfix_updateArticle_missingID into dev
Reviewed-on: #28
2026-06-10 16:08:08 +02:00
NOrtmann1 23321f90c2 Update createArticle.php 2026-06-10 16:05:07 +02:00
NOrtmann1 90c6f67c24 Update updateArticle.php 2026-06-10 16:04:06 +02:00
NOrtmann1 7e2b29bfb0 Update updateArticle.php 2026-06-10 16:00:01 +02:00
NOrtmann1 cd6aa80f59 Update updateArticle.php 2026-06-10 15:54:12 +02:00
NOrtmann1 d5fdc9383f Update updateArticle.php 2026-06-10 15:52:12 +02:00
NOrtmann1 53865efc78 redo 2026-06-09 19:06:30 +02:00
NOrtmann1 8cbd442371 unset der Eingaben verschoben 2026-06-09 19:05:04 +02:00
NOrtmann1 f54cf58dc2 Update updateArticle-controller.php 2026-06-09 18:57:26 +02:00
NOrtmann1 ede8e1cb21 Fehlermeldungsweiterleitung setzt nun die id 2026-06-09 18:55:09 +02:00
NOrtmann1 aaff15ee8c Update updateArticle-controller.php 2026-06-09 18:49:14 +02:00
NOrtmann1 bb7c85d770 .idea 2026-06-09 18:49:07 +02:00
NOrtmann1 b7160f4df6 .idea 2026-06-09 15:32:06 +02:00
NOrtmann1 a6b57bc352 Merge branch 'DatabaseArticleManager' into dev 2026-06-09 15:31:59 +02:00
NOrtmann1 70f7e05fe1 Update DatabaseArticleManager.php 2026-06-09 15:30:05 +02:00
NOrtmann1 15f17b7af4 .idea 2026-06-09 15:29:53 +02:00
caroline.slt a5650f2720 Reviewanpassungen 2026-06-08 20:20:06 +02:00
caroline.slt 4758366063 Refactoring 2026-06-08 20:18:08 +02:00
caroline.slt e7a5c2b352 Kommentare ergänzt 2026-06-08 20:10:11 +02:00
niklas.ortmann 3c14df129d Create data_sources_history.xml 2026-06-08 12:04:56 +02:00
niklas.ortmann 5660f8d59d .idea 2026-06-08 11:15:42 +02:00
caroline.slt 36ec2a539c LocalUserManager überall entfernt 2026-06-05 23:47:24 +02:00
caroline.slt 339e51cca7 Passwortabänderung 2026-06-05 23:42:06 +02:00
caroline.slt 0726ba214c Fehlerbehebung Passwort speichern 2026-06-05 23:25:49 +02:00
caroline.slt 0e032ae029 Fehlerbehebung Passwort speichern 2026-06-05 23:22:16 +02:00
caroline.slt b9dead1c8b Fehlerbehebung Passwort speichern 2026-06-05 23:18:35 +02:00
caroline.slt 9010b68a39 Richtig Einbinden 2026-06-05 23:10:36 +02:00
caroline.slt 252c14a377 Test 2026-06-05 22:56:28 +02:00
caroline.slt 4abfe801ec Test 2026-06-05 22:54:18 +02:00
caroline.slt c23a10eb6e Implement database-based user management 2026-06-05 22:34:12 +02:00
niklas.ortmann 11b55008ea Delete articles 2026-06-05 11:35:49 +02:00
niklas.ortmann 46dcc16c59 Update ArticleManager.php 2026-06-05 11:34:29 +02:00
niklas.ortmann d42dff1165 deleteArticle -> Autorisierungsprüfung 2026-06-05 11:27:43 +02:00
niklas.ortmann 11da418f60 Update DatabaseArticleManager.php 2026-06-05 11:22:38 +02:00
niklas.ortmann 981e78ff7e Update DatabaseArticleManager.php 2026-06-05 11:18:46 +02:00
niklas.ortmann c798bbd230 Update DatabaseArticleManager.php 2026-06-05 11:16:21 +02:00
niklas.ortmann e22d97e15a Update ArticleManager.php 2026-06-05 11:09:36 +02:00
niklas.ortmann c662369259 Create .htaccess 2026-06-05 10:58:39 +02:00
niklas.ortmann 11b7be4ca0 Umstellung auf DatabaseArticleManager 2026-06-05 10:47:03 +02:00
niklas.ortmann a029314bc9 Update ArticleManager.php 2026-06-05 10:46:21 +02:00
niklas.ortmann 8dd2d4f4ea Update ArticleManager.php 2026-06-05 10:45:50 +02:00
niklas.ortmann a4efbd4f0b Update ArticleManager.php 2026-06-05 10:45:25 +02:00
niklas.ortmann f63b8c6506 DatabaseUserManager 2026-06-05 10:43:07 +02:00
niklas.ortmann 198fefbdf5 Update LocalUserManager.php 2026-06-05 10:35:33 +02:00
niklas.ortmann 135bd7b9cd Update updateArticle.php 2026-06-05 10:33:21 +02:00
niklas.ortmann 4cd0383322 deleteArticle -> Autorisierungsprüfung 2026-06-05 10:31:09 +02:00
niklas.ortmann caa9a9ff8f Update DatabaseArticleManager.php 2026-06-05 10:22:30 +02:00
niklas.ortmann 524dc2a399 DataBaseArticleManager + Methoden
search-Methode muss noch implementiert werden...
2026-06-05 09:57:34 +02:00
niklas.ortmann 793d9632b2 refactoring 2026-06-04 19:02:15 +02:00
niklas.ortmann 5892a875c8 refactoring 2026-06-04 18:32:01 +02:00
niklas.ortmann b130bf9e19 Merge pull request 'Profilbearbeitung: Reviewnacharbeitung' (#22) from Profilbearbeitung into dev
Reviewed-on: #22
2026-06-03 22:25:42 +02:00
caroline.slt 1314362df9 Buttonfix 2026-06-03 21:32:14 +02:00
caroline.slt 5a1c032f1b Reviewnacharbeitung 2026-06-03 21:27:08 +02:00
caroline.slt fae4bc86ad Reviewnacharbeitung 2026-06-03 21:16:31 +02:00
caroline.slt 664092d52f Reviewnacharbeitung 2026-06-03 21:14:01 +02:00
caroline.slt 22b1dd827d Reviewnacharbeitung 2026-06-03 21:09:34 +02:00
caroline.slt 50df844e23 Reviewnacharbeitung 2026-06-03 21:05:32 +02:00
caroline.slt 45b6b41d60 Reviewnacharbeitung 2026-06-03 21:01:28 +02:00
caroline.slt a0a79e0191 Reviewnacharbeitung 2026-06-03 20:58:26 +02:00
niklas.ortmann 46def40a14 statische Beiträge löschen 2026-06-03 20:23:10 +02:00
niklas.ortmann a76113dcaf Merge branch 'dev' of https://git.gigaworld.ddns.net/niklas.ortmann/webprogrammierung into dev 2026-06-03 20:21:46 +02:00
niklas.ortmann f56a04c3ab css 2026-06-03 20:21:28 +02:00
niklas.ortmann 7f891b6ebf Merge pull request 'Kategorien nicht statisch sondern dynamisch' (#20) from dynamicCategories into dev
Reviewed-on: #20
2026-06-03 20:15:16 +02:00
niklas.ortmann 1343496a12 Merge pull request 'Vor und Nachname separat speichern sowie Profilbearbeitung' (#18) from Profilbearbeitung into dev
Reviewed-on: #18
2026-06-03 20:13:03 +02:00
niklas.ortmann 1735dd28a2 Update README.md 2026-06-03 20:12:14 +02:00
caroline.slt bfc9ee4d01 Reviewnacharbeitung 2026-06-03 20:07:57 +02:00
niklas.ortmann 9321d09489 Update home.php 2026-06-03 20:00:33 +02:00
niklas.ortmann 7269ea03db Update showCategory-controller.php 2026-06-03 19:58:09 +02:00
niklas.ortmann 2a3a15c92d Update showCategory.php 2026-06-03 19:58:05 +02:00
niklas.ortmann a7f1729e6e Update ArticleManager.php 2026-06-03 19:56:12 +02:00
niklas.ortmann cdccb0bfe8 debugging 2026-06-03 19:51:58 +02:00
niklas.ortmann 48501a9069 Update ArticleManager.php 2026-06-03 19:51:06 +02:00
niklas.ortmann a175765e33 Update main.css 2026-06-03 19:49:45 +02:00
niklas.ortmann 3d57c2e482 Update 404.php 2026-06-03 19:46:25 +02:00
niklas.ortmann bac4334fb8 Update showCategory-controller.php 2026-06-03 19:44:47 +02:00
niklas.ortmann aa3e7c2f36 Update showCategory.php 2026-06-03 19:43:41 +02:00
niklas.ortmann 46cd524f70 Update showCategory-controller.php 2026-06-03 19:43:40 +02:00
niklas.ortmann 86fb37abc4 Update showCategory-controller.php 2026-06-03 19:39:48 +02:00
niklas.ortmann 9349b91926 Update showCategory.php 2026-06-03 19:39:46 +02:00
niklas.ortmann b9c86e2074 Update showCategory-controller.php 2026-06-03 19:37:36 +02:00
niklas.ortmann 68672474e4 Update showCategory-controller.php 2026-06-03 19:36:49 +02:00
niklas.ortmann a08197cc10 Update showCategory-controller.php 2026-06-03 19:36:22 +02:00
niklas.ortmann e28620d9fb debugging 2026-06-03 19:35:13 +02:00
niklas.ortmann ded98ed757 Update LocalArticleManager.php 2026-06-03 19:34:51 +02:00
niklas.ortmann 18fe69cbcd Update showCategory-controller.php 2026-06-03 19:34:48 +02:00
niklas.ortmann e35d07ebc3 Update LocalArticleManager.php 2026-06-03 19:33:02 +02:00
niklas.ortmann e772a8b57f Kategorien nicht statisch sondern dynamisch 2026-06-03 19:29:29 +02:00
niklas.ortmann 9c3ebc9877 Merge pull request 'WIP Suche implementiert, sowie Sortieren der Ergebnisse' (#19) from ImplementedSearch into dev
Reviewed-on: #19
2026-06-03 16:07:41 +02:00
rirat-0 3d13c3b8c8 Design Anpassung der search in navbar 2026-06-03 07:59:39 +02:00
rirat-0 6c3e788f13 Limitation der eingabefelder auf 50 zeichen 2026-06-03 03:03:37 +02:00
rirat-0 aceac0bf8b aufraeumen 7 2026-06-03 02:52:22 +02:00
rirat-0 78d04f1715 aufraeumen 6 2026-06-03 02:48:11 +02:00
rirat-0 1d773ea4e6 Funktionalitaet der sortierung 2026-06-03 02:44:59 +02:00
rirat-0 a595567db1 funktionale suche der sidebox 2026-06-03 02:39:03 +02:00
rirat-0 945d3e1c1f aufraeumen 5 2026-06-03 02:28:45 +02:00
rirat-0 d58ea953af aufraeumen 4 2026-06-03 02:02:54 +02:00
rirat-0 b9475a045f aufraeumen 3 2026-06-03 01:58:02 +02:00
rirat-0 32c2a1a3d8 aufraeumen 2 2026-06-03 01:56:57 +02:00
rirat-0 fc93f2b3f0 aufraeumen 2026-06-03 01:55:33 +02:00
rirat-0 f33778f97a fehlersuche 12 2026-06-03 01:51:26 +02:00
rirat-0 1c72a03f26 fehlersuche 11 2026-06-03 01:48:00 +02:00
rirat-0 3dcbdbf23c fehlersuche 10 2026-06-03 01:45:04 +02:00
rirat-0 8143d501fa fehlersuche 9 2026-06-03 01:42:09 +02:00
rirat-0 3471eedf4a fehlersuche 8 2026-06-03 01:17:38 +02:00
rirat-0 b6a3dad2ab fehlersuche 7 2026-06-03 01:08:14 +02:00
rirat-0 375e6d9625 fehlersuche 6 2026-06-03 00:45:57 +02:00
rirat-0 4d144ec704 fehlersuche 5 2026-06-03 00:44:57 +02:00
caroline.slt 92574a0f76 Profildaten bearbeiten 2026-06-02 22:42:47 +02:00
caroline.slt ade73f3105 Vor und Nachname separat speichern 2026-06-02 22:10:15 +02:00
caroline.slt ee75620cd8 Vor und Nachname separat speichern 2026-06-02 22:01:38 +02:00
caroline.slt 94486e2b0a Vor und Nachname separat speichern 2026-06-02 21:56:52 +02:00
NOrtmann1 a26e985edf Update README.md 2026-06-02 18:32:57 +02:00
NOrtmann1 cf9ac795eb Readme + Kommentare 2026-06-02 18:20:35 +02:00
rirat-0 370c047c16 fehlersuche 4 2026-06-02 18:06:45 +02:00
rirat-0 a562a67923 fehlersuche 3 2026-06-02 18:02:40 +02:00
rirat-0 078dbd8901 fehlersuche 2 2026-06-02 18:01:14 +02:00
rirat-0 64d624a6df fehlersuche 1 2026-06-02 18:00:01 +02:00
rirat-0 90ab57e4cb Mehrere anpassungen, damit ergebnisse angezeigt werden 2026-06-02 17:53:04 +02:00
rirat-0 a00ddf18e6 update der pfade 2026-06-02 17:19:03 +02:00
rirat-0 5048dc2466 weitere anpassungen der such funktion 2026-06-02 17:16:57 +02:00
rirat-0 7209fe3e20 article.php wird nun genutzt im search results controller 2026-06-02 17:14:45 +02:00
rirat-0 4e35e67ef1 anpassung von der search function 2026-06-02 17:06:41 +02:00
rirat-0 830b57d245 anpassung der such funktion 2026-06-02 17:03:06 +02:00
rirat-0 06930644a6 Implementierung von neuem such algorithmus und entsprechenden anpassungen in anderen dateien 2026-06-02 16:58:12 +02:00
niklas.ortmann a519387773 Update updateArticle-controller.php 2026-06-02 15:45:34 +02:00
niklas.ortmann ed40596389 Update createArticle.css 2026-06-02 15:43:37 +02:00
niklas.ortmann 4c855c05ed css 2026-06-02 15:42:07 +02:00
niklas.ortmann e7189245d5 css 2026-06-02 15:37:35 +02:00
niklas.ortmann 709a4ba83c commenting 2026-06-02 15:34:19 +02:00
niklas.ortmann fb54ba2462 updateArticle-Exceptions 2026-06-02 15:31:02 +02:00
niklas.ortmann c6b55030cf updateArticle - Exeptions 2026-06-02 15:22:18 +02:00
niklas.ortmann af04557f58 Update updateArticle-controller.php 2026-06-02 15:04:52 +02:00
niklas.ortmann 2c100bddf2 Update updateArticle-controller.php 2026-06-02 15:04:05 +02:00
niklas.ortmann 8e3642c668 Merge branch 'dev' into ImplementedSearch 2026-06-02 14:48:37 +02:00
rirat-0 87d78b749b Finale Aktualisierung 2026-06-02 14:09:06 +02:00
rirat-0 604dbeca93 Aktualisierung 2026-06-02 14:07:33 +02:00
rirat-0 8e9e7d65d0 noch mehr debugging 2026-06-02 13:59:21 +02:00
rirat-0 e2c317e507 mehr debugging 2026-06-02 13:57:30 +02:00
rirat-0 5a7fa35465 test mit absoluten pfaden 2026-06-02 13:55:58 +02:00
rirat-0 2601a1c1ca naechster debugging versuch 2026-06-02 13:54:04 +02:00
rirat-0 0e26679f24 anpassung beider article manager 2026-06-02 13:48:51 +02:00
rirat-0 d5ab659217 anpassung von article managerdao 2026-06-02 13:44:37 +02:00
rirat-0 2d5919d572 alte aenderung reverted 2026-06-02 13:41:51 +02:00
rirat-0 f56a074a3c search results controller wird ueber die results seite geladen 2026-06-02 13:38:19 +02:00
rirat-0 24dc432f01 entfernung mancher aenderungen 2026-06-02 13:26:11 +02:00
rirat-0 016193de07 weitere anpassung von index.php 2026-06-02 13:24:17 +02:00
rirat-0 f04d8fecd3 Anpassung der index.php damit der richtige controller geladen wird 2026-06-02 13:20:54 +02:00
rirat-0 36276c7c88 debug versuch nr.5 2026-06-02 13:16:53 +02:00
rirat-0 4a2cf0b55b 4. versuch 2026-06-02 13:14:04 +02:00
rirat-0 400436a73f 3. versuch die homepage zu reparieren 2026-06-02 13:13:02 +02:00
rirat-0 4c378dfba6 2. versuch das problem der homepage zu loesen 2026-06-02 13:08:17 +02:00
rirat-0 c3a9de71f4 1. versuch, das problem der homepage zu beheben 2026-06-02 13:06:29 +02:00
rirat-0 ecf2e9ef57 Update von ArticleManagerDAO, damit die search Methode auch genutzt werden kann 2026-06-02 13:00:21 +02:00
rirat-0 45ac70eba2 Weitere Anpassung der search.php 2026-06-02 12:54:15 +02:00
rirat-0 3b66c6aa3a aenderung des such formulars um korrekte action zu nutzen 2026-06-02 12:49:24 +02:00
rirat-0 e37b882481 anpassung von controllern und results
search function implementiert, search controller entsprechend angepasst und die results seite angepasst
2026-06-02 12:43:59 +02:00
niklas.ortmann 5c7a47e002 Update updateArticle.php 2026-06-02 00:21:17 +02:00
niklas.ortmann bbaa73dc1a Update createArticle.php 2026-06-02 00:20:31 +02:00
niklas.ortmann 9f5806f019 Update profile-controller.php 2026-06-02 00:20:02 +02:00
niklas.ortmann 6de86c3143 $_SESSION["user_email"] 2026-06-02 00:16:53 +02:00
niklas.ortmann c9c0de6c3c Update createArticle-controller.php 2026-06-02 00:14:25 +02:00
niklas.ortmann 4f827ed3a8 Update createArticle-controller.php 2026-06-02 00:13:37 +02:00
niklas.ortmann aedac79743 Update createArticle.php 2026-06-02 00:12:53 +02:00
niklas.ortmann 84a39bf0d4 Update createArticle-controller.php 2026-06-02 00:12:51 +02:00
niklas.ortmann 75c6d099f5 debugging 2026-06-02 00:09:33 +02:00
niklas.ortmann 319957c01e Update createArticle.php 2026-06-02 00:08:47 +02:00
niklas.ortmann 26dc33ea2f Update createArticle-controller.php 2026-06-02 00:05:50 +02:00
niklas.ortmann 7597eddd7c Update createArticle.php 2026-06-02 00:05:19 +02:00
niklas.ortmann 0441b4e56f Update createArticle-controller.php 2026-06-02 00:04:32 +02:00
niklas.ortmann cbff21136c Update createArticle.php 2026-06-02 00:03:28 +02:00
niklas.ortmann 03f0d19b01 Update createArticle-controller.php 2026-06-02 00:02:14 +02:00
niklas.ortmann ca65c6c6e1 Update createArticle-controller.php 2026-06-02 00:01:20 +02:00
niklas.ortmann 2b095f16bf Update createArticle.php 2026-06-01 23:58:35 +02:00
niklas.ortmann 930bf99fa1 Update index.php 2026-06-01 23:56:44 +02:00
niklas.ortmann 366527e8e9 Update index.php 2026-06-01 23:54:38 +02:00
niklas.ortmann 4e06240307 Update index.php 2026-06-01 23:54:05 +02:00
niklas.ortmann 38608a5278 Update index.php 2026-06-01 23:51:52 +02:00
niklas.ortmann b19f30cca4 debugging 2026-06-01 23:51:13 +02:00
niklas.ortmann b206e809ac debugging 2026-06-01 23:44:36 +02:00
niklas.ortmann e88f741fa7 debugging 2026-06-01 23:42:56 +02:00
niklas.ortmann 6958e71bfb Update createArticle.php 2026-06-01 23:36:13 +02:00
niklas.ortmann c6ad0b2a59 debugging 2026-06-01 23:35:19 +02:00
niklas.ortmann da9ea4f413 Update createArticle-controller.php 2026-06-01 23:33:20 +02:00
niklas.ortmann 5703c0a6dd Update createArticle-controller.php 2026-06-01 23:31:47 +02:00
niklas.ortmann b70943dd35 Update profile-controller.php 2026-06-01 23:30:37 +02:00
niklas.ortmann 0f7099be81 debgging 2026-06-01 23:29:01 +02:00
niklas.ortmann 9cbc1637b4 merging 2026-06-01 23:26:31 +02:00
niklas.ortmann d01be7d84c Update profile-controller.php 2026-06-01 23:18:52 +02:00
niklas.ortmann 28511e0cc4 debugging 2026-06-01 23:15:48 +02:00
niklas.ortmann 3774a2caee merging 2026-06-01 23:14:25 +02:00
niklas.ortmann 70277d3712 Merge branch 'Beitrag-bearbeiten' into dev 2026-06-01 23:14:14 +02:00
niklas.ortmann 18cb092d06 Navbar: neue responisve Größen durch mehr Buttons 2026-06-01 23:03:48 +02:00
niklas.ortmann 417f34e35f Merge branch 'Registrierung' into dev 2026-06-01 23:00:11 +02:00
niklas.ortmann c62ff5b2c0 refactoring 2026-06-01 22:53:28 +02:00
niklas.ortmann ad422a134d refactoring 2026-06-01 22:52:42 +02:00
niklas.ortmann 9fce9ec8dd refactoring 2026-06-01 22:51:46 +02:00
niklas.ortmann a329137de2 refactoring 2026-06-01 22:48:39 +02:00
rirat-0 b328c2613c search.php form erstellt 2026-06-01 21:04:37 +02:00
niklas.ortmann 62a7b6773c Merge pull request 'Eingabevalidierung' (#16) from Beitrag-Eingabevalidierung into Beitrag-bearbeiten
Reviewed-on: #16
2026-06-01 14:05:28 +02:00
niklas.ortmann 625dd35b9e Update createArticle-controller.php 2026-06-01 14:02:31 +02:00
niklas.ortmann f99b7bc8ab Update updateArticle-controller.php 2026-06-01 14:02:11 +02:00
niklas.ortmann e779162ec2 Update createArticle-controller.php 2026-06-01 13:57:53 +02:00
niklas.ortmann 17345345b5 Update updateArticle-controller.php 2026-06-01 13:54:04 +02:00
niklas.ortmann 3df526d5b5 Update article-validator.php 2026-06-01 13:54:02 +02:00
niklas.ortmann d5f30176c4 article-validator.php 2026-06-01 13:41:22 +02:00
niklas.ortmann 308e5af6ff tags-> bis 50 Zeichen 2026-06-01 12:51:17 +02:00
niklas.ortmann 268fe6a6b9 Update updateArticle.php 2026-06-01 12:49:45 +02:00
niklas.ortmann a24914126f Update updateArticle-controller.php 2026-06-01 12:46:51 +02:00
niklas.ortmann 1ca27bc072 Update updateArticle.php 2026-06-01 12:43:11 +02:00
niklas.ortmann c96d93c2ce Update updateArticle-controller.php 2026-06-01 12:42:00 +02:00
niklas.ortmann cd38af22db Update updateArticle.php 2026-06-01 12:33:13 +02:00
niklas.ortmann c440c25e41 Update updateArticle.php 2026-06-01 12:30:50 +02:00
niklas.ortmann e105ab61b7 Update showArticle-controller.php 2026-06-01 12:24:59 +02:00
niklas.ortmann 10b058afa0 Update path.php 2026-06-01 12:22:14 +02:00
niklas.ortmann d18f33a87a Update showArticle-controller.php 2026-06-01 12:17:11 +02:00
niklas.ortmann 62190fa821 Update showArticle.php 2026-06-01 12:17:10 +02:00
niklas.ortmann a6210c9d6e Update showArticle-controller.php 2026-06-01 12:13:03 +02:00
niklas.ortmann fdece531af Update showArticle-controller.php 2026-06-01 12:09:44 +02:00
niklas.ortmann 8073235466 Update showArticle-controller.php 2026-06-01 12:07:26 +02:00
niklas.ortmann 7dc3c3b988 Update showArticle-controller.php 2026-06-01 12:06:32 +02:00
niklas.ortmann 21793dffd7 Update showArticle-controller.php 2026-06-01 12:03:40 +02:00
niklas.ortmann ca337c4fac debugging 2026-06-01 12:03:22 +02:00
niklas.ortmann 76beb8d62e Update showArticle-controller.php 2026-06-01 12:02:40 +02:00
niklas.ortmann c5bfd02f09 Update showArticle-controller.php 2026-06-01 12:02:21 +02:00
niklas.ortmann 9f4ca058bd Update showArticle-controller.php 2026-06-01 11:52:16 +02:00
niklas.ortmann 603c208ae4 Update showArticle-controller.php 2026-06-01 11:50:59 +02:00
niklas.ortmann 754777d69e Update showArticle-controller.php 2026-06-01 11:49:36 +02:00
niklas.ortmann 829a0e49d8 Content: zwischen 10 und 7000 Zeichen 2026-06-01 11:42:51 +02:00
niklas.ortmann f12babf2b1 Update createArticle.php 2026-06-01 11:35:25 +02:00
niklas.ortmann d6249169c6 Update createArticle.php 2026-06-01 11:31:25 +02:00
niklas.ortmann 6aa1cd9a63 Update createArticle-controller.php 2026-06-01 11:30:07 +02:00
niklas.ortmann 09795c30dd debugging 2026-06-01 11:19:01 +02:00
niklas.ortmann 59268dc102 Eingaben gehen bei Fehlermeldungen nicht verloren 2026-06-01 11:14:59 +02:00
niklas.ortmann 979378d142 Update createArticle-controller.php 2026-06-01 11:06:33 +02:00
niklas.ortmann 6ce7f0ef22 createArticle-Eingabevalidierung 2026-06-01 11:02:20 +02:00
niklas.ortmann 71a8dd5f17 updateArticle funktioniert 2026-06-01 09:41:34 +02:00
niklas.ortmann e86fef1766 Update updateArticle-controller.php 2026-06-01 09:36:31 +02:00
niklas.ortmann 3cb63f791a Update updateArticle.php 2026-06-01 09:36:28 +02:00
niklas.ortmann f96415bfe6 Update showArticle.php 2026-06-01 09:36:23 +02:00
niklas.ortmann 365f70af98 Update updateArticle.php 2026-06-01 09:29:06 +02:00
niklas.ortmann c1ddbfdf48 Update updateArticle.php 2026-06-01 09:28:29 +02:00
niklas.ortmann e575e3dfa8 Update updateArticle-controller.php 2026-06-01 09:25:22 +02:00
niklas.ortmann 560f53cfb6 Update updateArticle-controller.php 2026-06-01 09:24:51 +02:00
niklas.ortmann d8c350c20f Update showArticle-controller.php 2026-06-01 09:19:28 +02:00
niklas.ortmann 5b718f24ed Update updateArticle-controller.php 2026-06-01 09:17:00 +02:00
niklas.ortmann 6634bee77b Update home.php 2026-06-01 09:16:58 +02:00
niklas.ortmann 2a3e73d409 updateArticle 2026-06-01 09:11:11 +02:00
caroline.slt 21bdb0d66d Weitere Reviewänderungen 2026-05-31 22:53:58 +02:00
caroline.slt 5db4ad3e63 Weitere Reviewänderungen 2026-05-31 22:49:13 +02:00
caroline.slt 2ce13cef72 Weitere Reviewänderungen 2026-05-31 22:42:16 +02:00
caroline.slt 02551049e0 Weitere Reviewänderungen 2026-05-31 22:38:29 +02:00
caroline.slt 55e9fa6a20 Weitere Reviewänderungen 2026-05-31 22:34:11 +02:00
caroline.slt c82378baa8 Weitere Reviewänderungen 2026-05-31 22:26:39 +02:00
caroline.slt 541d5b4c5e Weitere Reviewänderungen 2026-05-31 22:21:50 +02:00
caroline.slt f9ab3f8db2 session start entfernt 2026-05-31 21:53:30 +02:00
caroline.slt 6c5047be30 Review Changes 2026-05-31 21:42:46 +02:00
caroline.slt 25f6d2bf4a Review Changes 2026-05-31 21:35:54 +02:00
NOrtmann1 2cd68873de Update updateArticle.php 2026-05-29 17:50:11 +01:00
NOrtmann1 d987ca271f Update updateArticle.php 2026-05-29 17:48:05 +01:00
NOrtmann1 0a37dd4b8b Update showArticle-controller.php 2026-05-29 17:45:04 +01:00
NOrtmann1 b4ca25db3d Update showArticle-controller.php 2026-05-29 17:44:30 +01:00
NOrtmann1 738b36c406 Update updateArticle-controller.php 2026-05-29 17:43:05 +01:00
NOrtmann1 c785363384 missing_id 2026-05-29 17:40:56 +01:00
NOrtmann1 32cb8a9d9e updateArticle Anpassungen 2026-05-29 17:36:29 +01:00
NOrtmann1 29e23a8d3b Update updateArticle.php 2026-05-29 17:32:57 +01:00
NOrtmann1 1a6403963a Update updateArticle.php 2026-05-29 17:29:26 +01:00
NOrtmann1 e096774233 Update updateArticle-controller.php 2026-05-29 17:29:24 +01:00
NOrtmann1 222759a1bc Update updateArticle.php 2026-05-29 18:21:46 +02:00
NOrtmann1 7a3043f644 Update profile.php 2026-05-29 18:20:27 +02:00
NOrtmann1 f1986bc3f7 Update LocalArticleManager.php 2026-05-29 18:18:31 +02:00
NOrtmann1 75d2f17a3f Update updateArticle-controller.php 2026-05-29 18:18:26 +02:00
NOrtmann1 317f4c0ce9 ArticleManager fehlende Implementationen 2026-05-29 18:13:06 +02:00
NOrtmann1 4a7bfa9567 Update profileArticles-controller.php 2026-05-29 17:37:48 +02:00
NOrtmann1 fec3cbb550 Update profile.php 2026-05-29 17:36:46 +02:00
NOrtmann1 497ce5bdb4 Update profile.php 2026-05-29 17:33:45 +02:00
NOrtmann1 53b593b380 Update profile.php 2026-05-29 17:32:51 +02:00
NOrtmann1 2d03b1f051 Update profile.php 2026-05-29 17:31:00 +02:00
NOrtmann1 d3c190096a Update profile.php 2026-05-29 17:30:09 +02:00
NOrtmann1 2998a7bf22 testing 2026-05-29 17:28:38 +02:00
NOrtmann1 43bd452a30 Update profile.php 2026-05-29 17:26:46 +02:00
NOrtmann1 dfdcc99111 Update profile.php 2026-05-29 17:23:30 +02:00
NOrtmann1 ea409dae07 Update LocalArticleManager.php 2026-05-29 17:20:07 +02:00
NOrtmann1 2810bfdce2 Update profile.php 2026-05-29 17:15:00 +02:00
NOrtmann1 ff23e6c9a2 profileArticle-controller.php Implementierung 2026-05-29 17:00:32 +02:00
niklas.ortmann 88c369de32 profile.php dynamisch Beiträge anzeigen (unvollständig 2026-05-29 16:00:31 +02:00
niklas.ortmann 52a34cd9b4 Update profile.css 2026-05-29 15:45:13 +02:00
niklas.ortmann 46eb99b534 Update profile.php 2026-05-29 15:44:14 +02:00
niklas.ortmann 0eba5f875a Update profile.php 2026-05-29 15:43:31 +02:00
niklas.ortmann b93692c160 css 2026-05-29 15:42:55 +02:00
niklas.ortmann 00f9f9cd91 Update main.css 2026-05-29 15:39:32 +02:00
niklas.ortmann 2c0d52a3d5 Update main.css 2026-05-29 15:38:57 +02:00
niklas.ortmann 4a67932002 debugging 2026-05-29 15:37:01 +02:00
niklas.ortmann 339e3025b8 debugging 2026-05-29 15:36:38 +02:00
niklas.ortmann 4d88605192 Update profile.css 2026-05-29 15:35:50 +02:00
niklas.ortmann bd3d81e80c css 2026-05-29 15:35:01 +02:00
niklas.ortmann 98683197e0 Update profile.css 2026-05-29 15:33:00 +02:00
niklas.ortmann 2ded6300ab Update profile.css 2026-05-29 15:31:47 +02:00
niklas.ortmann 79efee0979 css 2026-05-29 15:29:38 +02:00
niklas.ortmann 9db31d4b8c testing 2026-05-29 15:25:51 +02:00
niklas.ortmann d7c7ef1041 Update main.css 2026-05-29 15:24:34 +02:00
niklas.ortmann 483dd4182e css 2026-05-29 15:23:09 +02:00
niklas.ortmann f63a2be01a Update main.css 2026-05-29 15:21:54 +02:00
niklas.ortmann 838b13e39f css 2026-05-29 15:19:51 +02:00
niklas.ortmann 7b4abe32a7 Update profile.css 2026-05-29 15:18:13 +02:00
niklas.ortmann 26889a8b59 Update profile.css 2026-05-29 15:17:14 +02:00
niklas.ortmann 60302e02b6 Update profile.css 2026-05-29 15:15:15 +02:00
niklas.ortmann 72023a6634 testing 2026-05-29 15:14:28 +02:00
niklas.ortmann dd1194a1c7 Update index.php 2026-05-29 15:13:29 +02:00
niklas.ortmann d5fbfc950f Update profile.css 2026-05-29 15:12:47 +02:00
niklas.ortmann d4fa588c8f refactoring 2026-05-29 15:11:40 +02:00
niklas.ortmann 2a5ff14cb5 profile.php mit Nutzerbeiträgen + css 2026-05-29 15:09:45 +02:00
niklas.ortmann df17478d72 Update navbar.css 2026-05-29 13:33:19 +02:00
niklas.ortmann 38fba55798 Update navbar.css 2026-05-29 13:32:38 +02:00
niklas.ortmann 5b3e7bf426 Update createArticle.css 2026-05-29 13:31:10 +02:00
niklas.ortmann cb07a4c80f Update createArticle.php 2026-05-29 13:29:08 +02:00
niklas.ortmann 85512e61a2 Merge branch 'suchseite' into dev
# Conflicts:
#	index.php
2026-05-29 13:25:03 +02:00
niklas.ortmann 380736ce26 Merge pull request 'Beitrag erstellen' (#13) from Beitrag-erstellen into dev
Reviewed-on: #13
2026-05-29 13:22:11 +02:00
niklas.ortmann 9eb6feb589 Update createArticle-controller.php 2026-05-29 13:19:49 +02:00
niklas.ortmann 23eee02208 debugging 2026-05-29 13:19:00 +02:00
niklas.ortmann ca4dfd03e4 debugging 2026-05-29 13:16:41 +02:00
niklas.ortmann 5c613a82c0 debugging 2026-05-29 13:15:05 +02:00
niklas.ortmann 34ffad38a6 testing 2026-05-29 13:11:58 +02:00
niklas.ortmann b131523bf3 refactoring 2026-05-29 13:10:05 +02:00
niklas.ortmann db2bd7e195 testing 2026-05-29 13:09:20 +02:00
niklas.ortmann 51ccad038d Update ArticleManagerDAO.php 2026-05-29 13:06:55 +02:00
niklas.ortmann 18810dcfe2 refactoring 2026-05-29 13:05:28 +02:00
niklas.ortmann ed1815b78e Update LocalArticleManager.php 2026-05-29 13:04:59 +02:00
niklas.ortmann 246a2a856c Update ArticleManagerDAO.php 2026-05-29 13:03:19 +02:00
niklas.ortmann 77b3916985 Update ArticleManagerDAO.php 2026-05-29 13:02:09 +02:00
niklas.ortmann 298a9b406a Update showArticle.php 2026-05-29 12:14:15 +02:00
niklas.ortmann e5d6291aac Update showArticle.php 2026-05-29 12:13:50 +02:00
niklas.ortmann 667d009cf8 Update showArticle.php 2026-05-29 12:13:27 +02:00
niklas.ortmann 3edc689e5f home-controller.php 2026-05-29 12:09:04 +02:00
niklas.ortmann 938cca55fe showArticle.css 2026-05-29 12:02:21 +02:00
niklas.ortmann d5e177654f refactoring 2026-05-29 11:58:12 +02:00
niklas.ortmann 59bbbc6bf4 Update home.php 2026-05-29 11:52:31 +02:00
niklas.ortmann ec814c9026 Update ArticleManager.php 2026-05-29 11:51:09 +02:00
niklas.ortmann b390437790 Update ArticleManager.php 2026-05-29 11:50:05 +02:00
niklas.ortmann d08e56bb5a Update ArticleManager.php 2026-05-29 11:49:45 +02:00
niklas.ortmann ccff6ab4cb Update ArticleManager.php 2026-05-29 11:49:25 +02:00
niklas.ortmann 04c1cb00c5 Dummy-Beitrage werden in ArticleManager beim Instanziieren gesetzt. 2026-05-29 11:43:53 +02:00
niklas.ortmann 1f2be8b8ee Update showArticle-controller.php 2026-05-29 11:41:46 +02:00
niklas.ortmann f2c0b4e4b5 Update showArticle-controller.php 2026-05-29 11:39:25 +02:00
niklas.ortmann 77fd9af112 Update showArticle-controller.php 2026-05-29 11:39:04 +02:00
niklas.ortmann 7ea3ce66ce Update showArticle.php 2026-05-29 11:37:27 +02:00
niklas.ortmann a91869bf47 Update showArticle.php 2026-05-29 11:37:12 +02:00
niklas.ortmann 9ae1cd4c34 debugging 2026-05-29 11:36:39 +02:00
niklas.ortmann 475fb00001 Update showArticle-controller.php 2026-05-29 11:36:12 +02:00
niklas.ortmann 168cf68a70 debugging 2026-05-29 11:34:43 +02:00
niklas.ortmann 36d4ac0681 Update index.php 2026-05-29 11:34:11 +02:00
niklas.ortmann 1afc7b480a Update home.php 2026-05-29 11:33:26 +02:00
niklas.ortmann b88046e228 Update home.php 2026-05-29 11:32:01 +02:00
niklas.ortmann 4f00859d1b debugging 2026-05-29 11:31:21 +02:00
niklas.ortmann 1b68f037ae showArticle.php + showArticle-controller.php Implementation 2026-05-29 11:29:52 +02:00
niklas.ortmann 7067303758 Update index.php 2026-05-29 11:09:04 +02:00
niklas.ortmann e499ffba22 "Beitrag erstellt"-Nachricht von index-php nach home.php 2026-05-29 11:08:00 +02:00
niklas.ortmann c4e7ca3ac2 Update LocalArticleManager.php 2026-05-29 11:05:57 +02:00
niklas.ortmann 55367963fb Update home.php 2026-05-29 11:05:27 +02:00
niklas.ortmann 5522f4fc04 Update LocalArticleManager.php 2026-05-29 11:03:38 +02:00
niklas.ortmann 332507d433 debugging 2026-05-29 11:01:55 +02:00
niklas.ortmann fe703d4e89 Update home.php 2026-05-29 10:58:06 +02:00
niklas.ortmann ef6d6d9a91 debugging 2026-05-29 10:54:21 +02:00
niklas.ortmann 128b191af5 Update LocalArticleManager.php 2026-05-29 10:53:07 +02:00
niklas.ortmann 5612bfe0b7 debugging 2026-05-29 10:46:12 +02:00
niklas.ortmann d0f9a97394 Update home.php 2026-05-29 10:35:06 +02:00
niklas.ortmann 67ecf6e870 Update home.php 2026-05-29 10:33:26 +02:00
niklas.ortmann fd27577407 Update home.php 2026-05-29 10:33:08 +02:00
niklas.ortmann 83ccf84da3 Update Article.php 2026-05-29 10:32:43 +02:00
niklas.ortmann 968534d28e Update home.php 2026-05-29 10:30:36 +02:00
niklas.ortmann cd60423f1a debugging 2026-05-29 10:30:09 +02:00
niklas.ortmann 9bb2587c49 debugging 2026-05-29 10:29:48 +02:00
niklas.ortmann ef94a5873c debugging 2026-05-29 10:29:26 +02:00
niklas.ortmann 8dbbe9eaf9 debugging 2026-05-29 10:29:00 +02:00
niklas.ortmann 3afac85222 Update home.php 2026-05-29 10:28:05 +02:00
niklas.ortmann 9eae4985d3 Update LocalArticleManager.php 2026-05-29 10:26:21 +02:00
niklas.ortmann 63c7bb8848 Update home.php 2026-05-29 10:19:25 +02:00
niklas.ortmann 28ef21aa52 Update home.php 2026-05-29 10:18:21 +02:00
niklas.ortmann 6a6ab16e09 Update LocalArticleManager.php 2026-05-29 10:11:22 +02:00
niklas.ortmann 5130a638b9 debugging 2026-05-29 10:09:59 +02:00
niklas.ortmann bc9b5effb2 debugging 2026-05-29 10:07:53 +02:00
niklas.ortmann 5ee412b050 debugging 2026-05-29 10:06:51 +02:00
niklas.ortmann 65412e6df9 debugging 2026-05-29 10:06:18 +02:00
niklas.ortmann 22ca680f3c debugging 2026-05-29 10:04:42 +02:00
niklas.ortmann 83268d3a67 dummy-Beiträge auf Home werden aus ArticleManager bezogen 2026-05-29 10:02:50 +02:00
niklas.ortmann d24bd0aa8b Update LocalArticleManager.php 2026-05-29 09:58:17 +02:00
niklas.ortmann c52834aa0d Article.php + getArticle Anpassungen
Article-Klasse ist nun syntaktisch korrekt.
getArticle gibt nun als Rückgabe einen Typ Article zurück
2026-05-29 09:57:50 +02:00
niklas.ortmann 306b9e8152 Dummy-Beiträge
Beim Instanziieren des ArticleManagers werden drei Dummy-Beiträge erstellt.
2026-05-29 09:43:39 +02:00
niklas.ortmann 30bb0d4657 getArticle by id 2026-05-29 09:32:56 +02:00
niklas.ortmann fdd7408f53 showArticle.php + showArticle-controller.php 2026-05-29 09:31:39 +02:00
NOrtmann1 7f77297100 Update search-results.php 2026-05-29 00:30:28 +02:00
NOrtmann1 e8693f282f Update search-results.php 2026-05-29 00:28:32 +02:00
NOrtmann1 8477462e07 Update search-results.php 2026-05-29 00:18:16 +02:00
NOrtmann1 c28268faf3 search-results-controller.php 2026-05-29 00:09:47 +02:00
NOrtmann1 24a759c61a search-results-controller.php 2026-05-28 23:54:35 +02:00
NOrtmann1 0d0a7dba90 Update search-results.php 2026-05-28 23:50:49 +02:00
NOrtmann1 6de87c9e6c Update search-results.php 2026-05-28 23:23:11 +02:00
NOrtmann1 104f265e9b Update search-results.php 2026-05-28 23:21:58 +02:00
NOrtmann1 f21d819c77 Update search-results.css 2026-05-28 22:49:30 +02:00
NOrtmann1 ac1828b5f9 Update search-results.css 2026-05-28 22:48:58 +02:00
NOrtmann1 22bb2de043 Update search-results.css 2026-05-28 22:47:01 +02:00
NOrtmann1 664ad37181 Update search-results.css 2026-05-28 22:38:10 +02:00
NOrtmann1 c2c98d12bc footer in Suchergebnisleiste 2026-05-28 22:28:10 +02:00
NOrtmann1 cdc8b85f3c css 2026-05-28 22:19:31 +02:00
NOrtmann1 c0d9949037 Seitenleiste mit Filteroptionen 2026-05-28 22:16:55 +02:00
NOrtmann1 5dbe7a99b9 search-results.php + css 2026-05-28 21:55:55 +02:00
NOrtmann1 086596f68c messages-styling 2026-05-28 17:21:29 +02:00
NOrtmann1 7a206674ab Update index.php 2026-05-28 17:16:25 +02:00
NOrtmann1 9698512ccf Update index.php 2026-05-28 17:15:40 +02:00
NOrtmann1 f4d144980d Update index.php 2026-05-28 17:13:21 +02:00
NOrtmann1 1513cbda7f User-Feedback 2026-05-28 17:11:59 +02:00
NOrtmann1 1becb82b1b Update php.xml 2026-05-28 15:50:28 +02:00
NOrtmann1 91880b163d erste funktionale Version 2026-05-27 19:38:38 +02:00
NOrtmann1 81dd452f81 Update createArticle-controller.php 2026-05-27 19:37:37 +02:00
NOrtmann1 24642ad83e testing 2026-05-27 19:36:52 +02:00
NOrtmann1 9b8f4036a9 testing 2026-05-27 19:35:11 +02:00
NOrtmann1 86395f0727 testing 2026-05-27 19:32:22 +02:00
NOrtmann1 e5b617dcba testing 2026-05-27 19:31:38 +02:00
NOrtmann1 581c012a72 testing 2026-05-27 19:28:35 +02:00
caroline.slt 4d8f338649 DAO Pattern angepasst & Passwörter gehashed 2026-05-27 19:25:58 +02:00
NOrtmann1 e5eaaf857e testing 2026-05-27 19:24:57 +02:00
NOrtmann1 e0835b70df Update LocalArticleManager.php 2026-05-27 19:23:15 +02:00
NOrtmann1 ed471cdf94 Update LocalArticleManager.php 2026-05-27 19:22:20 +02:00
NOrtmann1 f825537b52 testing 2026-05-27 19:20:14 +02:00
NOrtmann1 485e08e6c6 testing 2026-05-27 19:18:59 +02:00
NOrtmann1 7ff02a6a94 testing 2026-05-27 19:14:45 +02:00
NOrtmann1 ec0148bb86 Update LocalArticleManager.php 2026-05-27 19:09:37 +02:00
NOrtmann1 9aa0793ab4 testing 2026-05-27 19:03:02 +02:00
NOrtmann1 54220682cf article.json 2026-05-27 18:20:56 +02:00
NOrtmann1 b13cff14dc testing 2026-05-27 18:16:22 +02:00
NOrtmann1 f86bdfd2ea testing 2026-05-27 18:02:30 +02:00
NOrtmann1 3933b9b55c Update createArticle-controller.php 2026-05-27 17:58:58 +02:00
NOrtmann1 7a784c8243 Update createArticle-controller.php 2026-05-27 17:49:11 +02:00
NOrtmann1 10ddf8bf7f Update createArticle-controller.php 2026-05-27 17:48:36 +02:00
NOrtmann1 0779a2c7d4 Update createArticle-controller.php 2026-05-27 17:47:11 +02:00
NOrtmann1 053ae0b52f Update createArticle-controller.php 2026-05-27 17:46:52 +02:00
NOrtmann1 be8b6372db testing 2026-05-27 17:46:03 +02:00
NOrtmann1 6068417fc5 Update createArticle-controller.php 2026-05-27 17:45:23 +02:00
NOrtmann1 14d0b23d06 testing 2026-05-27 17:44:33 +02:00
NOrtmann1 1f6ddcd1fd testing 2026-05-27 17:44:06 +02:00
NOrtmann1 701fda5ab5 testing 2026-05-27 17:42:25 +02:00
NOrtmann1 47731a8d46 testing 2026-05-27 17:41:35 +02:00
NOrtmann1 0ade795415 testing 2026-05-27 17:41:17 +02:00
NOrtmann1 2a9a60ae84 testing 2026-05-27 17:40:52 +02:00
NOrtmann1 efdc9b4175 testing 2026-05-27 17:40:29 +02:00
NOrtmann1 6076903b42 testing 2026-05-27 17:39:58 +02:00
NOrtmann1 d43218fdb2 Update createArticle-controller.php 2026-05-27 17:39:06 +02:00
NOrtmann1 554dbd677a testing 2026-05-27 17:38:41 +02:00
NOrtmann1 6a7e961cf6 testing 2026-05-27 17:34:56 +02:00
NOrtmann1 e49e614958 Update createArticle-controller.php 2026-05-27 17:32:58 +02:00
NOrtmann1 70fe6ff807 testing 2026-05-27 17:31:42 +02:00
NOrtmann1 f6932afcb8 testing 2026-05-27 17:31:19 +02:00
NOrtmann1 e3901265d8 path-refactoring 2026-05-27 17:29:45 +02:00
NOrtmann1 368a4948ec testing 2026-05-27 17:22:05 +02:00
NOrtmann1 6c07502866 testing 2026-05-27 17:19:36 +02:00
NOrtmann1 c1c8bc47d7 testing 2026-05-27 17:19:11 +02:00
NOrtmann1 8466a857c3 path-refactoring + getter & setter 2026-05-27 17:18:32 +02:00
NOrtmann1 68d9224ea3 testing 2026-05-27 17:03:40 +02:00
NOrtmann1 aac26b0e50 testing 2026-05-27 17:03:10 +02:00
NOrtmann1 a1a9fa9d3e testing 2026-05-27 17:02:21 +02:00
NOrtmann1 5625326f38 testing 2026-05-27 17:01:48 +02:00
NOrtmann1 8841cffb25 testing 2026-05-27 17:01:07 +02:00
NOrtmann1 d72bf8c3e2 testing 2026-05-27 17:00:24 +02:00
NOrtmann1 04195f5d92 Pfadanpassung (test) 2026-05-27 16:59:04 +02:00
NOrtmann1 24a3ad0639 Update createArticle.php 2026-05-27 16:56:27 +02:00
NOrtmann1 72097f891d Update createArticle.php 2026-05-27 16:55:51 +02:00
NOrtmann1 0686e98384 first test 2026-05-27 16:52:06 +02:00
NOrtmann1 7d9bef932f Update createArticle-controller.php 2026-05-27 16:16:21 +02:00
NOrtmann1 e69cb50a9c Delete UserDAO.php 2026-05-27 16:04:08 +02:00
NOrtmann1 890187cff2 statt txt -> json 2026-05-27 16:03:56 +02:00
caroline.slt 46a307a5b0 First Implementation 2026-05-26 23:14:12 +02:00
caroline.slt 7706c7dfbf First Implementation 2026-05-26 23:06:22 +02:00
caroline.slt a946cee891 First Implementation 2026-05-26 22:59:48 +02:00
NOrtmann1 f2a9d8d1b3 refactoring 2026-05-26 19:04:46 +02:00
NOrtmann1 e128a31a34 refactoring 2026-05-26 18:55:42 +02:00
NOrtmann1 d7acca36fd Update createArticle.php 2026-05-26 18:46:42 +02:00
NOrtmann1 d28560b77e Update createArticle-controller.php 2026-05-26 18:45:12 +02:00
NOrtmann1 ee6dd0ddae Update index-controller.php 2026-05-26 18:45:10 +02:00
NOrtmann1 feb64eb769 Update index-controller.php 2026-05-26 18:42:16 +02:00
NOrtmann1 ecc76e453e Update createArticle.php 2026-05-26 18:39:23 +02:00
NOrtmann1 949ca542ab $abs_path 2026-05-26 18:37:56 +02:00
NOrtmann1 b090195d9f Update article.css 2026-05-26 18:30:27 +02:00
NOrtmann1 1f1e7dc814 Update createArticle.php 2026-05-26 18:29:41 +02:00
NOrtmann1 3606a750be refactoring 2026-05-26 18:15:28 +02:00
NOrtmann1 8762bc1b07 refactoring 2026-05-26 18:06:44 +02:00
NOrtmann1 c7f279b7e9 Update createArticle-controller.php 2026-05-26 17:53:51 +02:00
NOrtmann1 92f1f077d2 refactoring 2026-05-26 17:48:50 +02:00
NOrtmann1 50120f974d Update index.php 2026-05-26 17:45:30 +02:00
NOrtmann1 713a258d48 Update FileDatabase.php 2026-05-26 17:45:06 +02:00
NOrtmann1 0a7e6df734 Update index-controller.php 2026-05-26 17:44:14 +02:00
NOrtmann1 8c769ba280 create-Article-controller Implementation 2026-05-26 17:43:07 +02:00
NOrtmann1 5bcb64f7f8 Database-(Brücken)-Klasse 2026-05-26 17:03:50 +02:00
NOrtmann1 944f40d89f Artikel-Klasse + FileDatabase - Klasse 2026-05-26 15:41:05 +02:00
NOrtmann1 b15421efee refactoring 2026-05-26 15:01:17 +02:00
NOrtmann1 3ddd66dcbb Update navbar.php 2026-05-26 12:43:21 +02:00
NOrtmann1 8af0367edf Update navbar.css 2026-05-26 12:43:00 +02:00
NOrtmann1 b95fc1adbe Update navbar.php 2026-05-26 12:42:40 +02:00
NOrtmann1 63f400995b Update navbar.css 2026-05-26 12:41:49 +02:00
NOrtmann1 8aa657ed99 doppelte ID entfernt 2026-05-26 12:41:05 +02:00
NOrtmann1 627c72bb7f Update navbar.php 2026-05-26 12:39:05 +02:00
niklas.ortmann a8a75d90ac Merge pull request 'FooterAnsEnde' (#9) from footerAnsEnde into dev
Reviewed-on: #9
2026-05-26 12:30:37 +02:00
niklas.ortmann 0b1b704115 Merge pull request 'MobileNavSchließen' (#10) from MobileNavSchließen into dev
Reviewed-on: #10
2026-05-26 12:30:27 +02:00
niklas.ortmann 0babc42359 Merge pull request 'Update main.css' (#11) from responsiveHomepage into dev
Reviewed-on: #11
2026-05-26 12:30:14 +02:00
rirat-0 4a80d42dda Update main.css
Anpassung der flexbox um inhalte besser zu skalieren
2026-05-26 11:41:11 +02:00
NOrtmann1 a2daded6b3 Footer-Link-Anpassungen (dynamisch wie navbar) 2026-05-26 11:25:47 +02:00
NOrtmann1 d3169eba18 Update navbar.css 2026-05-26 11:19:57 +02:00
NOrtmann1 2d967c200c x-Button in der Sidebar 2026-05-26 11:18:58 +02:00
NOrtmann1 2044853f94 Update navbar.php 2026-05-26 11:15:20 +02:00
NOrtmann1 9a3e4cc7dc Update navbar.php 2026-05-26 11:08:43 +02:00
NOrtmann1 f508a222c6 mobile-nav- "Beitrag erstellen" Button 2026-05-26 11:07:34 +02:00
NOrtmann1 193e526e67 Update main.css 2026-05-26 11:00:25 +02:00
NOrtmann1 f77643af44 body-css-anpassungen 2026-05-26 10:59:07 +02:00
NOrtmann1 9e900a3436 Update footer.css 2026-05-26 10:54:40 +02:00
NOrtmann1 75988ffe71 Update footer.css 2026-05-26 10:54:02 +02:00
NOrtmann1 cb1f3f0d07 Update footer.css 2026-05-26 10:52:57 +02:00
NOrtmann1 4d8017aa2b Update footer.css 2026-05-26 10:51:32 +02:00
NOrtmann1 365f37dcea responsive Design für Smartphones 2026-05-26 10:47:03 +02:00
NOrtmann1 2472143146 Update createArticle.php 2026-05-26 10:42:32 +02:00
NOrtmann1 a1184299b1 erste article-Implementation
article-Interface & -Klasse
+ createArticle.php mit article.css (Editor)
+ neuer "Beitrag erstellen"-Button in Navbar
2026-05-26 10:36:56 +02:00
caroline.slt 4d4b8aeb99 First Implementation 2026-05-26 00:20:34 +02:00
NOrtmann1 c41302d046 Klassen article und articleDAO 2026-05-22 17:09:20 +02:00
niklas.ortmann aaefd2917e Create article.php 2026-05-22 14:26:35 +02:00
NOrtmann1 ee7986bef3 Update index.php 2026-05-21 21:03:38 +02:00
NOrtmann1 8de1b1bdb5 Update index.php 2026-05-21 21:02:46 +02:00
NOrtmann1 4a663391ef Update navbar.css 2026-05-21 21:00:20 +02:00
niklas.ortmann 4cd877ea4c Merge pull request 'PHP: Dynamischer Content' (#8) from PHP_DynamischerContent into dev
Reviewed-on: #8
2026-05-21 20:57:26 +02:00
NOrtmann1 0f573d30ae linkanpassungen 2026-05-21 17:36:18 +02:00
NOrtmann1 fdd56c10b8 Update home.php 2026-05-21 17:33:42 +02:00
NOrtmann1 46daf72a14 Update index.php 2026-05-21 17:32:43 +02:00
NOrtmann1 896e6abcd8 login & register -> /content/ 2026-05-21 17:31:45 +02:00
NOrtmann1 95e5cf0908 navbar linkanpassungen 2026-05-21 17:30:07 +02:00
NOrtmann1 be35a00cef Dateien in richtige Ordner + statische HTML vom content entfernt 2026-05-21 17:19:40 +02:00
NOrtmann1 b616251511 dynamic content test
profil wird dynamisch in die index.php integriert (Test)
2026-05-21 17:13:52 +02:00
NOrtmann1 3f5313dd00 Update 404.php 2026-05-19 14:49:06 +02:00
NOrtmann1 f909d0453b Update 404.php 2026-05-13 22:55:47 +02:00
NOrtmann1 d5f4f2b8a5 Update main.css 2026-05-13 22:53:32 +02:00
niklas.ortmann 87a8e9b7d2 Merge pull request 'designcss2 zu dev' (#7) from designcss2 into dev
Reviewed-on: #7
2026-05-13 22:50:24 +02:00
NOrtmann1 729b0f08a1 Update main.css 2026-05-13 22:49:57 +02:00
NOrtmann1 2907bcdd67 Update home.php 2026-05-13 22:44:23 +02:00
NOrtmann1 65935c644b Update profile.php 2026-05-13 22:09:59 +02:00
NOrtmann1 8645900035 Update profile.php 2026-05-13 22:09:25 +02:00
NOrtmann1 49a8d49296 logo aus login und register entfernt 2026-05-13 21:53:59 +02:00
NOrtmann1 bc1051e4a2 Update register.php 2026-05-13 21:50:06 +02:00
NOrtmann1 d8f8fbbdd4 Update login.php 2026-05-13 21:48:35 +02:00
NOrtmann1 baf3f88e1e Update login.php 2026-05-13 21:47:51 +02:00
NOrtmann1 480b6cd4ae Update 404.php 2026-05-13 18:43:35 +02:00
NOrtmann1 29b588babb testing... 2026-05-13 18:41:29 +02:00
NOrtmann1 a29e48efad Update main.css 2026-05-13 18:11:03 +02:00
NOrtmann1 84a9f7063a Merge branch 'navbar' into dev 2026-05-13 18:08:42 +02:00
niklas.ortmann 58f7be9d52 Merge pull request 'Last Implementation' (#6) from designcss into dev
Reviewed-on: #6
2026-05-13 18:00:19 +02:00
caroschulte02-alt d497ea4fc3 First Implementation 2026-05-13 16:29:31 +02:00
caroschulte02-alt 80316e080a First Implementation 2026-05-13 16:18:12 +02:00
viratex bf06df461d Merge pull request 'Home, sowie Login und Register Website haben nun CSS implementiert' (#5) from responsiveCSS into dev
Reviewed-on: #5
2026-05-13 15:14:37 +02:00
viratex 503df76d6b Merge branch 'dev' into responsiveCSS 2026-05-13 15:13:22 +02:00
rirat-0 829d532518 font weight anpassung 2026-05-13 14:31:14 +02:00
rirat-0 33b0283758 update der register seite 2026-05-13 13:27:23 +02:00
rirat-0 6abc244be7 anpassung der login seite und main css fuer accessibility 2026-05-13 12:48:35 +02:00
rirat-0 012e3b8b9b update css fuer login seite 2026-05-13 12:39:11 +02:00
rirat-0 9375ab90b0 update der login seite 2026-05-13 12:04:55 +02:00
niklas.ortmann 6198d7b9ae Merge pull request 'Fehlende CRUD-Operationen' (#3) from Fehlende-CRUD-Operationen into dev
Reviewed-on: #3
2026-05-13 12:04:38 +02:00
NOrtmann1 52bede8277 Update profile.php 2026-05-13 11:27:46 +02:00
rirat-0 b81216cef1 kleine fehler und farbe behoben 2026-05-12 18:03:37 +02:00
rirat-0 43f0c78b3d Napassung von home.php und der main.css fuer responsive design 2026-05-12 14:19:57 +02:00
104 changed files with 9925 additions and 854 deletions
View File
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="IU-261.25134.95">
<data-source name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<secret-storage>master_key</secret-storage>
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/articles</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
+42
View File
@@ -0,0 +1,42 @@
<DataSourcesHistory>
<DataSourceFromHistory isRemovedFromProject="true">
<data-source source="LOCAL" name="articles" uuid="a0abcd0a-1d6f-40e4-88be-f442bcb431ba">
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/articles</jdbc-url>
<secret-storage>master_key</secret-storage>
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</DataSourceFromHistory>
<DataSourceFromHistory isRemovedFromProject="false">
<data-source source="LOCAL" name="articles" uuid="315cb5c9-2b0f-435b-b602-59823b160908">
<database-info product="SQLite" version="3.51.1" jdbc-version="4.2" driver-name="SQLite JDBC" driver-version="3.51.1.0" dbms="SQLITE" exact-version="3.51.1" exact-driver-version="3.51">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/db/articles</jdbc-url>
<secret-storage>master_key</secret-storage>
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</DataSourceFromHistory>
</DataSourcesHistory>
Generated
+1
View File
@@ -10,6 +10,7 @@
<option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" />
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.5" />
<component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" />
</component>
-41
View File
@@ -1,41 +0,0 @@
<!--
Seite: 404 Seite nicht gefunden
Funktion: Wenn eine Seite nicht gefunden wurde oder noch fehlt, dann wird hierauf verwiesen.
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<main>
<?php
include_once 'includes/navbar.php';
?>
<h1>404 - Seite nicht vorhanden</h1>
<p>
Später im Projekt sollen über index.php?pfad= ... der Inhalt der index.php dynamisch gesetzt werden.
Alle Content-Links der Navbar führen zunächst hier her. Exemplarisch wurden für die Fächer Informatik, Physik, Mathematik der Content als Seite erstellt und kann über die Navbar ausgekundschaftet werden.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+43 -11
View File
@@ -1,13 +1,45 @@
Team Tag-Buchstabe: DI-14-C-Schulte-Eilers-Ortmann
Namen der Studierenden:
- Name 1 Caroline Schulte
- Name 2 Jacques Eilers
- Name 3 Niklas Ortmann
Abgabe zu Aufgabenblatt 1
# Webprogrammierung: Gruppe DI-14-C
Hinweise:
index.html wurde zu einer index.php umbenannt.
webhook.php bitte ignorieren. Diese dient nur als zukünftige Automatisierung des Repo-Pulls (s. https://docs.gitea.com/usage/webhooks)
## Beteiligte
- Caroline Schulte
- Jacques Eilers
- Niklas Ortmann
Beim Verwenden der Navbar-Links bitte Folgendes beachten: nur die Kategorien Physik, Mathe, Informatik führen exemplarisch auf eine Beispiel-Seite.
Die anderen Links sind erstmal Platzhalter, dessen dynamischer Inhalt später mit PHP in die index.php eingefügt wird. Alle anderen Links führen auf eine 404-Seite.
## Login-Informationen für Dummy-User
`Anmeldename, Passwort, Mailadresse`:
- `max.mustermann, test12345, max.mustermann@web.de`
## Weitere Voraussetzungen zur Nutzung
- Per Klick auf das Logo gelangt man auf die Home-Seite.
## Ausgelassene Teilaufgaben
- Das Anerkennen der Datenschutzerklärung und das Einhalten der
Nutzungsbedingungen (bspw. was Rechte an eingestellten Texten und
hochgeladenen Fotos angeht) muss ein Nutzer bei der Registrierung via einer
Checkbox und dem Opt-In-Verfahren fehlt noch.
## Bekannte Fehler und Mängel
- Bitte auf die gesetzten TODO's achten. Wenn Inhalte fehlen, sind sie i.d.R. als TODO kommentiert.
- Die Kategorieseite listet momentan alle passenden Beiträge untereinander. Später sollen mit einem Paginator die neusten
Beiträge nacheinander aufgelistet werden (ähnlich wie bei der Suche, wenn nach Fach gefiltert wird).
- Wenn ein Bild aus einem Beitrag entfernt wird, dann wird noch nicht die Datei im Pfad /uploads gelöscht.
- id in showArticle-controller.php und updateArticle-controller.php wird nicht als gültige numerische ID geprüft.
- Bilder im Beitragseditor sollen zukünftig eine Bildunterschrift bekommen und größenverstellbar sein.
- Die Elemente eines Contents im Beitrag werden momentan stumpf untereinander aufgelistet. Soll später
sich responisve auch nebeneinander orientieren usw.
- Die Datenbank ist noch in die verschiedenen Manager unterteilt. Zukünftig werden alle Daten in einer Datenbank gespeichert.
## Besonderheiten des Projektes
- Es wurde AJAX verwendet, um asynchrone Erstellung von Kommentaren zu implementieren. Es ermöglicht dem Nutzer, einen
Kommentar abzusenden, ohne dass die gesamte Webseite neu geladen werden muss.
- Mit JavaScript werden auch clientseitig die Kommentare visuell hinzugefügt und die Kommentarbäume aufgebaut.
- JavaScript wird verwendet, um im erweitertem Beitragseditor clientseitig einzelne Content-Boxen erstellen und löschen
zu können.
- JavaScript wird ebenfalls verwendet, um in die Suchergebnisse clientseitig zu sortieren.
- Die Kommentarfunktion ist zusätzlich auch ohne JavaScript nutzbar. Kommentare und Antworten können auch ohne JavaScript erstellt werden.
- Eigene Kommentare und Antworten können bearbeitet und gelöscht werden. Die Bearbeitung und das Löschen funktionieren ebenfalls ohne JavaScript über normale Formulare und serverseitige Verarbeitung.
## Sonstiges
- Das Datenschema befindet sich unter /planung/Datenschema.pdf
- Im Impressum, sowie der Datenschutzerklaerung wurden Dummy-Daten verwendet
+15
View File
@@ -0,0 +1,15 @@
<!--
Seite: 404 Seite nicht gefunden
Funktion: Wenn eine Seite nicht gefunden wurde oder noch fehlt, dann wird hierauf verwiesen.
-->
<main>
<?php include_once "includes/alertMessages.php"?>
<?php
unset($_SESSION["message"]);
?>
<h1>404 - Seite nicht vorhanden</h1>
<p>
Die Seite, nach der du suchst, existiert hier leider nicht. Entweder haben wir sie verlegt, ein hungriger Bug hat sie gefressen, oder du hast dich beim Tippen vertippt. (Wir schieben die Schuld einfach mal ganz unauffällig auf dich.)
</p>
</main>
+66
View File
@@ -0,0 +1,66 @@
<!--
Seite: Barrierefreiheitserklärung
Inhalt: Erklärung zur Barrierefreiheit der Plattform
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="../images/logos/logo_icon.ico">
<link rel="stylesheet" href="../css/main.css">
<link rel="stylesheet" href="../css/navbar.css">
<link rel="stylesheet" href="../css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Barrierefreiheitserklärung</h1>
<p>
Wir bemühen uns, die Plattform EduForge möglichst barrierefrei zugänglich zu gestalten.
Die Website wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
<h2>Stand der Vereinbarkeit</h2>
<p>
Diese Website ist teilweise barrierefrei. Einzelne Funktionen oder Inhalte können
noch nicht vollständig barrierefrei nutzbar sein.
</p>
<h2>Nicht barrierefreie Inhalte</h2>
<p>
Einzelne Bilder verfügen möglicherweise noch nicht über vollständige Alternativtexte.
Außerdem kann die Bedienung mit Tastatur oder Screenreader in einzelnen Bereichen
eingeschränkt sein.
</p>
<h2>Feedback und Kontakt</h2>
<p>
Sollten Ihnen Barrieren auf unserer Website auffallen, kontaktieren Sie uns bitte
über die im Impressum angegebenen Kontaktdaten.
</p>
<h2>Erstellungsdatum</h2>
<p>
Diese Erklärung wurde im Rahmen eines Hochschulprojekts erstellt.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Passwort zurücksetzen";
$message = $message ?? "Der Bestätigungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Passwortbestätigung
Funktion: Zeigt das Ergebnis der Passwortänderung an.
-->
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+29
View File
@@ -0,0 +1,29 @@
<?php
/** @var string $title */
/** @var string $message */
/** @var string $link */
/** @var bool $isSuccess */
$title = $title ?? "Registrierung";
$message = $message ?? "Der Registrierungslink konnte nicht verarbeitet werden.";
$link = $link ?? "";
$isSuccess = $isSuccess ?? false;
?>
<!--
Ansicht: Registrierungsbestätigung
Funktion: Zeigt das Ergebnis der Registrierungsbestätigung an.
-->
<main class="login-page">
<div class="login-container">
<h1><?php echo htmlspecialchars($title); ?></h1>
<p class="alert-message <?php echo $isSuccess ? 'is-success' : 'is-error'; ?> confirm-message">
<?php echo htmlspecialchars($message); ?>
</p>
<?php echo $link; ?>
</div>
</main>
+311
View File
@@ -0,0 +1,311 @@
<?php
$user = $user ?? null;
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
// -------------------------------------------------------------------------
// Aktuelle Blockliste ermitteln: Session-Fallback (nach Validierungsfehler
// oder Zwischen-Submit "add_block"/"delete_block") oder leeres Array (Start).
// -------------------------------------------------------------------------
$rawContent = '[]';
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
$rawContent = $_SESSION['old_content'];
unset($_SESSION['old_content']);
}
$blocks = json_decode($rawContent, true);
if (!is_array($blocks)) {
$blocks = [];
}
$blocks = array_values($blocks); // sequentielle Indizes 0..n-1 sicherstellen
?>
<!--
Seite: Beitrag erstellen
Inhalt: Formular für die Erstellung eines neuen Beitrags
-->
<form method="post" action="php/controller/createArticle-controller.php" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
<?php csrf_field(); ?>
<main class="editor-main">
<?php include_once "includes/alertMessages.php"?>
<input type="text" id="title" name="title"
value="<?php echo htmlspecialchars($_SESSION['old_title'] ?? ''); unset($_SESSION['old_title']); ?>"
placeholder="Titel hier eingeben" required>
<!--
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Dadurch funktioniert das
Hinzufügen/Entfernen von Blöcken und der Bild-Upload auch ganz ohne JavaScript über
einen normalen Formular-Submit. Ist JavaScript aktiv, fängt js/editor.js diese
Submits ab und erledigt dieselbe Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<?php if ($blockType === 'text'): ?>
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<?php else: /* image */ ?>
<?php if (!empty($block['value'])): ?>
<img src="<?php echo htmlspecialchars($block['value']); ?>"
class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div>
<div id="add-block-control" class="article-editor-scope add-block-control">
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup">
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
</div>
</div>
</main>
<!-- Seitenleiste -->
<aside class="editor-sidebar">
<div class="sidebar-block">
<button type="submit" id="editor-button" class="button">Veröffentlichen</button>
</div>
<div class="sidebar-block">
<label for="category">Kategorie <span class="required">*</span></label>
<select id="category" name="category" required>
<option disabled <?php
if ((!isset($_SESSION['old_category']) || empty($_SESSION['old_category'])) && (!isset($category) || empty($category))) {
echo 'selected';
}
?>>Kategorie wählen...</option>
<optgroup label="Sprachen">
<option value="deutsch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'deutsch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'deutsch') { echo 'selected'; }
}
?>>Deutsch</option>
<option value="englisch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'englisch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'englisch') { echo 'selected'; }
}
?>>Englisch</option>
<option value="franzoesisch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'franzoesisch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'franzoesisch') { echo 'selected'; }
}
?>>Französisch</option>
<option value="latein" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'latein') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'latein') { echo 'selected'; }
}
?>>Latein</option>
<option value="literatur" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'literatur') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'literatur') { echo 'selected'; }
}
?>>Literatur</option>
</optgroup>
<optgroup label="MINT">
<option value="mathe" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'mathe') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'mathe') { echo 'selected'; }
}
?>>Mathematik</option>
<option value="biologie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'biologie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'biologie') { echo 'selected'; }
}
?>>Biologie</option>
<option value="chemie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'chemie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'chemie') { echo 'selected'; }
}
?>>Chemie</option>
<option value="physik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'physik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'physik') { echo 'selected'; }
}
?>>Physik</option>
<option value="informatik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'informatik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'informatik') { echo 'selected'; }
}
?>>Informatik</option>
<option value="astronomie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'astronomie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'astronomie') { echo 'selected'; }
}
?>>Astronomie</option>
</optgroup>
<optgroup label="Gesellschaft & Werte">
<option value="geschichte" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'geschichte') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'geschichte') { echo 'selected'; }
}
?>>Geschichte</option>
<option value="erdkunde" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'erdkunde') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'erdkunde') { echo 'selected'; }
}
?>>Erdkunde</option>
<option value="sozialkunde" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'sozialkunde') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'sozialkunde') { echo 'selected'; }
}
?>>Sozialkunde</option>
<option value="wirtschaft" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'wirtschaft') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'wirtschaft') { echo 'selected'; }
}
?>>Wirtschaftskunde</option>
<option value="religion" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'religion') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'religion') { echo 'selected'; }
}
?>>Religion</option>
<option value="ethik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'ethik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'ethik') { echo 'selected'; }
}
?>>Ethikunterricht</option>
<option value="philosophie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'philosophie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'philosophie') { echo 'selected'; }
}
?>>Philosophie</option>
<option value="psychologie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'psychologie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'psychologie') { echo 'selected'; }
}
?>>Psychologie</option>
<option value="kunst" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'kunst') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'kunst') { echo 'selected'; }
}
?>>Kunst</option>
<option value="musik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'musik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'musik') { echo 'selected'; }
}
?>>Musik</option>
<option value="theater" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'theater') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'theater') { echo 'selected'; }
}
?>>Theater</option>
</optgroup>
<optgroup label="Technik & Praxis">
<option value="technik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'technik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'technik') { echo 'selected'; }
}
?>>Technik</option>
<option value="werken" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'werken') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'werken') { echo 'selected'; }
}
?>>Werken</option>
<option value="hauswirtschaft" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'hauswirtschaft') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'hauswirtschaft') { echo 'selected'; }
}
?>>Hauswirtschaft</option>
<option value="sport" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'sport') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'sport') { echo 'selected'; }
}
?>>Sport</option>
</optgroup>
<?php
if (isset($_SESSION['old_category'])) {
unset($_SESSION['old_category']);
}
?>
</select>
</div>
<div class="sidebar-block">
<label for="tags">Schlagwörter</label>
<input type="text" id="tags" name="tags"
value="<?php echo htmlspecialchars($_SESSION['old_tags'] ?? ''); unset($_SESSION['old_tags']); ?>"
placeholder="z.B. Technik, IT (mit Komma trennen)">
</div>
</aside>
</form>
+189
View File
@@ -0,0 +1,189 @@
<!--
Seite: Datenschutzerklärung
Inhalt: Informationen zur Datenverarbeitung gemäß DSGVO
-->
<main>
<h1>Datenschutz&shy;erkl&auml;rung</h1>
<h2>1. Datenschutz auf einen Blick</h2>
<h3>Allgemeine Hinweise</h3> <p>Die folgenden Hinweise geben einen einfachen &Uuml;berblick dar&uuml;ber,
was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind
alle Daten, mit denen Sie pers&ouml;nlich identifiziert werden k&ouml;nnen. Ausf&uuml;hrliche Informationen zum
Thema Datenschutz entnehmen Sie unserer unter diesem Text aufgef&uuml;hrten
Datenschutzerkl&auml;rung.</p>
<h3>Datenerfassung auf dieser Website</h3> <h4>Wer ist verantwortlich f&uuml;r die Datenerfassung auf dieser
Website?</h4> <p>Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber. Dessen
Kontaktdaten k&ouml;nnen Sie dem Abschnitt &bdquo;Hinweis zur verantwortlichen Stelle&ldquo; in dieser
Datenschutzerkl&auml;rung entnehmen.</p> <h4>Wie erfassen wir Ihre Daten?</h4> <p>Ihre Daten werden zum
einen dadurch erhoben, dass Sie uns diese mitteilen. Hierbei kann es sich z.&nbsp;B. um Daten handeln, die Sie in ein
Kontaktformular eingeben.</p> <p>Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch
der Website durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z.&nbsp;B. Internetbrowser,
Betriebssystem oder Uhrzeit des Seitenaufrufs). Die Erfassung dieser Daten erfolgt automatisch, sobald Sie diese
Website betreten.</p> <h4>Wof&uuml;r nutzen wir Ihre Daten?</h4> <p>Ein Teil der Daten wird erhoben, um eine
fehlerfreie Bereitstellung der Website zu gew&auml;hrleisten. Andere Daten k&ouml;nnen zur Analyse Ihres
Nutzerverhaltens verwendet werden. Sofern &uuml;ber die Website Vertr&auml;ge geschlossen oder angebahnt
werden k&ouml;nnen, werden die &uuml;bermittelten Daten auch f&uuml;r Vertragsangebote, Bestellungen oder
sonstige Auftragsanfragen verarbeitet.</p> <h4>Welche Rechte haben Sie bez&uuml;glich Ihrer Daten?</h4>
<p>Sie haben jederzeit das Recht, unentgeltlich Auskunft &uuml;ber Herkunft, Empf&auml;nger und Zweck Ihrer
gespeicherten personenbezogenen Daten zu erhalten. Sie haben au&szlig;erdem ein Recht, die Berichtigung oder
L&ouml;schung dieser Daten zu verlangen. Wenn Sie eine Einwilligung zur Datenverarbeitung erteilt haben,
k&ouml;nnen Sie diese Einwilligung jederzeit f&uuml;r die Zukunft widerrufen. Au&szlig;erdem haben Sie das Recht,
unter bestimmten Umst&auml;nden die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu
verlangen. Des Weiteren steht Ihnen ein Beschwerderecht bei der zust&auml;ndigen Aufsichtsbeh&ouml;rde
zu.</p> <p>Hierzu sowie zu weiteren Fragen zum Thema Datenschutz k&ouml;nnen Sie sich jederzeit an uns
wenden.</p>
<h2>2. Hosting</h2>
<p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:</p>
<h3>Externes Hosting</h3> <p>Diese Website wird extern gehostet. Die personenbezogenen Daten, die auf dieser
Website erfasst werden, werden auf den Servern des Hosters / der Hoster gespeichert. Hierbei kann es sich
v.&nbsp;a. um IP-Adressen, Kontaktanfragen, Meta- und Kommunikationsdaten, Vertragsdaten, Kontaktdaten,
Namen, Websitezugriffe und sonstige Daten, die &uuml;ber eine Website generiert werden, handeln.</p> <p>Das
externe Hosting erfolgt zum Zwecke der Vertragserf&uuml;llung gegen&uuml;ber unseren potenziellen und
bestehenden Kunden (Art. 6 Abs. 1 lit. b DSGVO) und im Interesse einer sicheren, schnellen und effizienten
Bereitstellung unseres Online-Angebots durch einen professionellen Anbieter (Art. 6 Abs. 1 lit. f DSGVO). Sofern
eine entsprechende Einwilligung abgefragt wurde, erfolgt die Verarbeitung ausschlie&szlig;lich auf Grundlage von
Art. 6 Abs. 1 lit. a DSGVO und &sect; 25 Abs. 1 TDDDG, soweit die Einwilligung die Speicherung von Cookies oder
den Zugriff auf Informationen im Endger&auml;t des Nutzers (z.&nbsp;B. Device-Fingerprinting) im Sinne des
TDDDG umfasst. Die Einwilligung ist jederzeit widerrufbar.</p> <p>Unser(e) Hoster wird bzw. werden Ihre Daten
nur insoweit verarbeiten, wie dies zur Erf&uuml;llung seiner Leistungspflichten erforderlich ist und unsere
Weisungen in Bezug auf diese Daten befolgen.</p> <p>Wir setzen folgende(n) Hoster ein:</p>
<p>-Platzhalter-</p>
<h4>Auftragsverarbeitung</h4> <p>Wir haben einen Vertrag &uuml;ber Auftragsverarbeitung (AVV) zur Nutzung
des oben genannten Dienstes geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen
Vertrag, der gew&auml;hrleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach
unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.</p>
<h2>3. Allgemeine Hinweise und Pflicht&shy;informationen</h2>
<h3>Datenschutz</h3> <p>Die Betreiber dieser Seiten nehmen den Schutz Ihrer pers&ouml;nlichen Daten sehr
ernst. Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den gesetzlichen
Datenschutzvorschriften sowie dieser Datenschutzerkl&auml;rung.</p> <p>Wenn Sie diese Website benutzen,
werden verschiedene personenbezogene Daten erhoben. Personenbezogene Daten sind Daten, mit denen Sie
pers&ouml;nlich identifiziert werden k&ouml;nnen. Die vorliegende Datenschutzerkl&auml;rung erl&auml;utert,
welche Daten wir erheben und wof&uuml;r wir sie nutzen. Sie erl&auml;utert auch, wie und zu welchem Zweck das
geschieht.</p> <p>Wir weisen darauf hin, dass die Daten&uuml;bertragung im Internet (z.&nbsp;B. bei der
Kommunikation per E-Mail) Sicherheitsl&uuml;cken aufweisen kann. Ein l&uuml;ckenloser Schutz der Daten vor
dem Zugriff durch Dritte ist nicht m&ouml;glich.</p>
<h3>Hinweis zur verantwortlichen Stelle</h3> <p>Die verantwortliche Stelle f&uuml;r die Datenverarbeitung auf
dieser Website ist:</p> <p>Ammerlaender Heerstrasse, 114-118</p>
<p>Telefon: 0123456789<br />
E-Mail: GruppeC@uol.de</p>
<p>Verantwortliche Stelle ist die nat&uuml;rliche oder juristische Person, die allein oder gemeinsam mit anderen
&uuml;ber die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten (z.&nbsp;B. Namen, E-Mail-
Adressen o. &Auml;.) entscheidet.</p>
<h3>Speicherdauer</h3> <p>Soweit innerhalb dieser Datenschutzerkl&auml;rung keine speziellere Speicherdauer
genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck f&uuml;r die Datenverarbeitung
entf&auml;llt. Wenn Sie ein berechtigtes L&ouml;schersuchen geltend machen oder eine Einwilligung zur
Datenverarbeitung widerrufen, werden Ihre Daten gel&ouml;scht, sofern wir keine anderen rechtlich
zul&auml;ssigen Gr&uuml;nde f&uuml;r die Speicherung Ihrer personenbezogenen Daten haben (z.&nbsp;B. steuer-
oder handelsrechtliche Aufbewahrungsfristen); im letztgenannten Fall erfolgt die L&ouml;schung nach Fortfall
dieser Gr&uuml;nde.</p>
<h3>Allgemeine Hinweise zu den Rechtsgrundlagen der Datenverarbeitung auf dieser Website</h3> <p>Sofern Sie
in die Datenverarbeitung eingewilligt haben, verarbeiten wir Ihre personenbezogenen Daten auf Grundlage von Art.
6 Abs. 1 lit. a DSGVO bzw. Art. 9 Abs. 2 lit. a DSGVO, sofern besondere Datenkategorien nach Art. 9 Abs. 1 DSGVO
verarbeitet werden. Im Falle einer ausdr&uuml;cklichen Einwilligung in die &Uuml;bertragung personenbezogener
Daten in Drittstaaten erfolgt die Datenverarbeitung au&szlig;erdem auf Grundlage von Art. 49 Abs. 1 lit. a DSGVO.
Sofern Sie in die Speicherung von Cookies oder in den Zugriff auf Informationen in Ihr Endger&auml;t (z.&nbsp;B. via
Device-Fingerprinting) eingewilligt haben, erfolgt die Datenverarbeitung zus&auml;tzlich auf Grundlage von &sect;
25 Abs. 1 TDDDG. Die Einwilligung ist jederzeit widerrufbar. Sind Ihre Daten zur Vertragserf&uuml;llung oder zur
Durchf&uuml;hrung vorvertraglicher Ma&szlig;nahmen erforderlich, verarbeiten wir Ihre Daten auf Grundlage des
Art. 6 Abs. 1 lit. b DSGVO. Des Weiteren verarbeiten wir Ihre Daten, sofern diese zur Erf&uuml;llung einer
rechtlichen Verpflichtung erforderlich sind, auf Grundlage von Art. 6 Abs. 1 lit. c DSGVO. Die Datenverarbeitung
kann ferner auf Grundlage unseres berechtigten Interesses nach Art. 6 Abs. 1 lit. f DSGVO erfolgen. &Uuml;ber die
jeweils im Einzelfall einschl&auml;gigen Rechtsgrundlagen wird in den folgenden Abs&auml;tzen dieser
Datenschutzerkl&auml;rung informiert.</p>
<h3>Empf&auml;nger von personenbezogenen Daten</h3> <p>Im Rahmen unserer Gesch&auml;ftst&auml;tigkeit
arbeiten wir mit verschiedenen externen Stellen zusammen. Dabei ist teilweise auch eine &Uuml;bermittlung von
personenbezogenen Daten an diese externen Stellen erforderlich. Wir geben personenbezogene Daten nur dann an
externe Stellen weiter, wenn dies im Rahmen einer Vertragserf&uuml;llung erforderlich ist, wenn wir gesetzlich
hierzu verpflichtet sind (z.&nbsp;B. Weitergabe von Daten an Steuerbeh&ouml;rden), wenn wir ein berechtigtes
Interesse nach Art. 6 Abs. 1 lit. f DSGVO an der Weitergabe haben oder wenn eine sonstige Rechtsgrundlage die
Datenweitergabe erlaubt. Beim Einsatz von Auftragsverarbeitern geben wir personenbezogene Daten unserer
Kunden nur auf Grundlage eines g&uuml;ltigen Vertrags &uuml;ber Auftragsverarbeitung weiter. Im Falle einer
gemeinsamen Verarbeitung wird ein Vertrag &uuml;ber gemeinsame Verarbeitung geschlossen.</p>
<h3>Widerruf Ihrer Einwilligung zur Datenverarbeitung</h3> <p>Viele Datenverarbeitungsvorg&auml;nge sind
nur mit Ihrer ausdr&uuml;cklichen Einwilligung m&ouml;glich. Sie k&ouml;nnen eine bereits erteilte Einwilligung
jederzeit widerrufen. Die Rechtm&auml;&szlig;igkeit der bis zum Widerruf erfolgten Datenverarbeitung bleibt vom
Widerruf unber&uuml;hrt.</p>
<h3>Widerspruchsrecht gegen die Datenerhebung in besonderen F&auml;llen sowie gegen Direktwerbung (Art. 21
DSGVO)</h3> <p>WENN DIE DATENVERARBEITUNG AUF GRUNDLAGE VON ART. 6 ABS. 1 LIT. E ODER F
DSGVO ERFOLGT, HABEN SIE JEDERZEIT DAS RECHT, AUS GR&Uuml;NDEN, DIE SICH AUS IHRER
BESONDEREN SITUATION ERGEBEN, GEGEN DIE VERARBEITUNG IHRER PERSONENBEZOGENEN DATEN
WIDERSPRUCH EINZULEGEN; DIES GILT AUCH F&Uuml;R EIN AUF DIESE BESTIMMUNGEN GEST&Uuml;TZTES
PROFILING. DIE JEWEILIGE RECHTSGRUNDLAGE, AUF DENEN EINE VERARBEITUNG BERUHT, ENTNEHMEN
SIE DIESER DATENSCHUTZERKL&Auml;RUNG. WENN SIE WIDERSPRUCH EINLEGEN, WERDEN WIR IHRE
BETROFFENEN PERSONENBEZOGENEN DATEN NICHT MEHR VERARBEITEN, ES SEI DENN, WIR
K&Ouml;NNEN ZWINGENDE SCHUTZW&Uuml;RDIGE GR&Uuml;NDE F&Uuml;R DIE VERARBEITUNG
NACHWEISEN, DIE IHRE INTERESSEN, RECHTE UND FREIHEITEN &Uuml;BERWIEGEN ODER DIE
VERARBEITUNG DIENT DER GELTENDMACHUNG, AUS&Uuml;BUNG ODER VERTEIDIGUNG VON
RECHTSANSPR&Uuml;CHEN (WIDERSPRUCH NACH ART. 21 ABS. 1 DSGVO).</p> <p>WERDEN IHRE
PERSONENBEZOGENEN DATEN VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, SO HABEN SIE DAS
RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG SIE BETREFFENDER PERSONENBEZOGENER
DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN; DIES GILT AUCH F&Uuml;R DAS PROFILING,
SOWEIT ES MIT SOLCHER DIREKTWERBUNG IN VERBINDUNG STEHT. WENN SIE WIDERSPRECHEN,
WERDEN IHRE PERSONENBEZOGENEN DATEN ANSCHLIESSEND NICHT MEHR ZUM ZWECKE DER
DIREKTWERBUNG VERWENDET (WIDERSPRUCH NACH ART. 21 ABS. 2 DSGVO).</p>
<h3>Beschwerde&shy;recht bei der zust&auml;ndigen Aufsichts&shy;beh&ouml;rde</h3> <p>Im Falle von
Verst&ouml;&szlig;en gegen die DSGVO steht den Betroffenen ein Beschwerderecht bei einer
Aufsichtsbeh&ouml;rde, insbesondere in dem Mitgliedstaat ihres gew&ouml;hnlichen Aufenthalts, ihres
Arbeitsplatzes oder des Orts des mutma&szlig;lichen Versto&szlig;es zu. Das Beschwerderecht besteht
unbeschadet anderweitiger verwaltungsrechtlicher oder gerichtlicher Rechtsbehelfe.</p>
<h3>Recht auf Daten&shy;&uuml;bertrag&shy;barkeit</h3> <p>Sie haben das Recht, Daten, die wir auf Grundlage
Ihrer Einwilligung oder in Erf&uuml;llung eines Vertrags automatisiert verarbeiten, an sich oder an einen Dritten in
einem g&auml;ngigen, maschinenlesbaren Format aush&auml;ndigen zu lassen. Sofern Sie die direkte
&Uuml;bertragung der Daten an einen anderen Verantwortlichen verlangen, erfolgt dies nur, soweit es technisch
machbar ist.</p>
<h3>Auskunft, Berichtigung und L&ouml;schung</h3> <p>Sie haben im Rahmen der geltenden gesetzlichen
Bestimmungen jederzeit das Recht auf unentgeltliche Auskunft &uuml;ber Ihre gespeicherten personenbezogenen
Daten, deren Herkunft und Empf&auml;nger und den Zweck der Datenverarbeitung und ggf. ein Recht auf
Berichtigung oder L&ouml;schung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene
Daten k&ouml;nnen Sie sich jederzeit an uns wenden.</p>
<h3>Recht auf Einschr&auml;nkung der Verarbeitung</h3> <p>Sie haben das Recht, die Einschr&auml;nkung der
Verarbeitung Ihrer personenbezogenen Daten zu verlangen. Hierzu k&ouml;nnen Sie sich jederzeit an uns wenden.
Das Recht auf Einschr&auml;nkung der Verarbeitung besteht in folgenden F&auml;llen:</p> <ul> <li>Wenn Sie die
Richtigkeit Ihrer bei uns gespeicherten personenbezogenen Daten bestreiten, ben&ouml;tigen wir in der Regel Zeit,
um dies zu &uuml;berpr&uuml;fen. F&uuml;r die Dauer der Pr&uuml;fung haben Sie das Recht, die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn die
Verarbeitung Ihrer personenbezogenen Daten unrechtm&auml;&szlig;ig geschah/geschieht, k&ouml;nnen Sie statt
der L&ouml;schung die Einschr&auml;nkung der Datenverarbeitung verlangen.</li> <li>Wenn wir Ihre
personenbezogenen Daten nicht mehr ben&ouml;tigen, Sie sie jedoch zur Aus&uuml;bung, Verteidigung oder
Geltendmachung von Rechtsanspr&uuml;chen ben&ouml;tigen, haben Sie das Recht, statt der L&ouml;schung die
Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> <li>Wenn Sie einen
Widerspruch nach Art. 21 Abs. 1 DSGVO eingelegt haben, muss eine Abw&auml;gung zwischen Ihren und unseren
Interessen vorgenommen werden. Solange noch nicht feststeht, wessen Interessen &uuml;berwiegen, haben Sie das
Recht, die Einschr&auml;nkung der Verarbeitung Ihrer personenbezogenen Daten zu verlangen.</li> </ul>
<p>Wenn Sie die Verarbeitung Ihrer personenbezogenen Daten eingeschr&auml;nkt haben, d&uuml;rfen diese
Daten &ndash; von ihrer Speicherung abgesehen &ndash; nur mit Ihrer Einwilligung oder zur Geltendmachung,
Aus&uuml;bung oder Verteidigung von Rechtsanspr&uuml;chen oder zum Schutz der Rechte einer anderen
nat&uuml;rlichen oder juristischen Person oder aus Gr&uuml;nden eines wichtigen &ouml;ffentlichen Interesses der
Europ&auml;ischen Union oder eines Mitgliedstaats verarbeitet werden.</p>
<h4>Cookies</h4>
<p>
Unsere Internetseiten verwenden so genannte „Cookies“. Cookies sind kleine Datenpakete und richten auf
Ihrem Endgerät keinen Schaden an. Sie werden entweder vorübergehend für die Dauer einer Sitzung
(Session-Cookies) oder dauerhaft (permanente Cookies) auf Ihrem Endgerät gespeichert. Session-Cookies
werden nach Ende Ihres Besuchs automatisch gelöscht. Permanente Cookies bleiben auf Ihrem Endgerät
gespeichert, bis Sie diese selbst löschen oder eine automatische Löschung durch Ihren Webbrowser erfolgt.
Cookies haben verschiedene Funktionen. Zahlreiche Cookies sind technisch notwendig, da bestimmte
Webseitenfunktionen ohne diese nicht funktionieren würden (z. B. die Anmeldung). Andere Cookies können zur
Auswertung des Nutzerverhaltens oder zu Werbezwecken verwendet werden.
Cookies, die zur Durchführung des elektronischen Kommunikationsvorgangs, zur Bereitstellung
bestimmter, von Ihnen erwünschter Funktionen oder zur Optimierung der
Website (z. B. Cookies zur Messung des Webpublikums) erforderlich sind (notwendige Cookies), werden auf
Grundlage von Art. 6 Abs. 1 lit. f DSGVO gespeichert, sofern keine andere Rechtsgrundlage angegeben wird.
Der Websitebetreiber hat ein berechtigtes Interesse an der Speicherung von notwendigen Cookies zur
technisch fehlerfreien und optimierten Bereitstellung seiner Dienste. Sofern eine Einwilligung zur
Speicherung von Cookies und vergleichbaren Wiedererkennungstechnologien abgefragt wurde, erfolgt die
Verarbeitung ausschließlich auf Grundlage dieser Einwilligung (Art. 6 Abs. 1 lit. a DSGVO und § 25 Abs. 1
TDDDG); die Einwilligung ist jederzeit widerrufbar.
Sie können Ihren Browser so einstellen, dass Sie über das Setzen von Cookies informiert werden und
Cookies nur im Einzelfall erlauben, die Annahme von Cookies für bestimmte Fälle oder generell ausschließen
sowie das automatische Löschen der Cookies beim Schließen des Browsers aktivieren. Bei der
Deaktivierung von Cookies kann die Funktionalität dieser Website eingeschränkt sein.
Sofern weitere Cookies und Dienste auf dieser Website eingesetzt werden, können Sie dies dieser
Datenschutzerklärung entnehmen.
Auf dieser Webseite werden jedoch nur technisch notwendige Cookies verwendet.
</p>
<p>Quelle: <a href="https://www.e-recht24.de">https://www.e-recht24.de</a></p>
</main>
+38 -23
View File
@@ -1,34 +1,49 @@
<?php
include_once 'php/controller/home-controller.php';
?>
<!--
Seite: Home
Inhalt: Beinhaltet den Inhalt der Startseite
-->
<main>
<?php include_once "includes/alertMessages.php"?>
<h1>Home</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
Schön, dass du da bist. EduForge ist deine digitale Werkstatt für Lernerfolg. Wir unterstützen Schüler und
Studierende dabei, die passenden Lernmaterialien zu finden, Wissenslücken zu schließen und Gelerntes gezielt
zu vertiefen.
</p>
<div>
<a href = "informatik.php" style = "padding: 5px; text-align: center; text-decoration: none; display: inline-block;"><b>Informatik</b></a> <br>
<div>
<a href="datenschutzVSdatensicherheit.php">Datenschutz vs. Datensicherheit</a>
</div>
</div>
<div>
<a href = "mathe.php" style = "padding: 5px; text-align: center; text-decoration: none; display: inline-block;"><b>Mathe</b></a> <br>
<div>
<a href="pythagoras.php">Satz des Pythagoras</a>
</div>
</div>
<div>
<a href = "physik.php" style = "padding: 5px; text-align: center; text-decoration: none; display: inline-block;"><b>Physik</b></a>
<div>
<a href="tunneleffekt.php">Der Tunneleffekt</a>
</div>
<!-- Flexbox für aktive Kategorien -->
<div class="flexbox">
<?php
if (!empty($categoriesWithArticles) && is_array($categoriesWithArticles)):
foreach ($categoriesWithArticles as $category):
?>
<div class="container">
<a href="index.php?pfad=showCategory&category=<?php echo htmlspecialchars($category['slug']); ?>" class="category-link">
<?php echo htmlspecialchars($category['name']); ?>
</a>
<!-- die 5 neuesten Beiträge der Kategorie -->
<div class="article-links-wrapper">
<?php foreach ($category['articles'] as $article): ?>
<div class="article-link">
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($article->getId()); ?>">
<?php echo htmlspecialchars($article->getTitle()); ?>
</a>
</div>
<?php endforeach; ?>
</div>
</div>
<?php
endforeach;
else:
?>
<p>Aktuell sind keine Beiträge in den Kategorien vorhanden.</p>
<?php endif; ?>
</div>
</main>
+17
View File
@@ -0,0 +1,17 @@
<!--
Seite: Impressum
Inhalt: wer wie was auf der Webseite
-->
<main>
<h1>Impressum</h1>
<p>Gruppe C<br />
Ammerlaender Heerstrasse, 114-118<br />
26129 Oldenburg</p>
<h2>Kontakt</h2>
<p>Telefon: 0123456789<br />
E-Mail: GruppeC@uni-oldenburg.de</p>
<p>Quelle: <a href="https://www.e-recht24.de/impressum-generator.html">https://www.e-recht24.de/impressum-generator.html</a></p>
</main>
+61
View File
@@ -0,0 +1,61 @@
<?php
$error = $error ?? null;
?>
<!--
Form: Login-Bereich
Funktion: Benutzerauthentifizierung und Zugang zum eigenen Profil, Erstellen von Beiträgen, etc.
-->
<main class="login-page">
<div class="login-container">
<h1>Bitte anmelden</h1>
<?php include_once "includes/alertMessages.php"; ?>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=login">
<p class="input-label">Benutzername/E-Mail-Adresse:</p>
<input type="email"
name="email"
class="login-input"
placeholder="E-Mail-Adresse"
required
autofocus>
<p class="input-label">Passwort:</p>
<input type="password"
name="password"
class="login-input"
placeholder="Passwort"
required>
<button type="submit"
value="anmelden"
name="loginSubmit"
class="button">
anmelden
</button>
<div class="register-link">
<a href="index.php?pfad=password-forgotten">
Passwort vergessen?
</a>
</div>
<div class="register-link">
<a href="index.php?pfad=register">
Noch keinen Account? Jetzt hier registrieren!
</a>
</div>
</form>
</div>
</main>
+85
View File
@@ -0,0 +1,85 @@
<!--
Seite: Nutzungsbedingungen
Inhalt: Regelungen für die Nutzung der Plattform (AGBs und so...)
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="../images/logos/logo_icon.ico">
<link rel="stylesheet" href="../css/main.css">
<link rel="stylesheet" href="../css/navbar.css">
<link rel="stylesheet" href="../css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<main>
<h1>Nutzungsbedingungen</h1>
<p>
Diese Nutzungsbedingungen regeln die Nutzung der Plattform EduForge.
Mit der Registrierung und Nutzung der Website erkennen Nutzerinnen und Nutzer
die nachfolgenden Bedingungen an.
</p>
<h2>1. Nutzung der Plattform</h2>
<p>
EduForge dient dem Austausch von Lerninhalten, Beiträgen und Kommentaren.
Die Nutzung der Plattform darf ausschließlich im Rahmen der geltenden Gesetze erfolgen.
</p>
<h2>2. Verhalten der Nutzerinnen und Nutzer</h2>
<p>
Nutzerinnen und Nutzer verpflichten sich zu einem respektvollen Umgang miteinander.
Das Veröffentlichen von beleidigenden, diskriminierenden, rechtswidrigen oder
anderweitig unangemessenen Inhalten ist untersagt.
</p>
<h2>3. Veröffentlichung von Inhalten</h2>
<p>
Nutzerinnen und Nutzer sind für die von ihnen veröffentlichten Inhalte selbst verantwortlich.
Dies gilt insbesondere für Beiträge, Kommentare, Bilder und sonstige hochgeladene Dateien.
</p>
<h2>4. Urheberrechte</h2>
<p>
Es dürfen ausschließlich Inhalte veröffentlicht werden, an denen die Nutzerinnen und Nutzer
die erforderlichen Rechte besitzen. Durch das Einstellen von Inhalten versichern die
Nutzerinnen und Nutzer, keine Rechte Dritter zu verletzen.
</p>
<h2>5. Entfernung von Inhalten</h2>
<p>
Inhalte, die gegen diese Nutzungsbedingungen oder geltendes Recht verstoßen,
können vom Betreiber der Plattform entfernt werden.
</p>
<h2>6. Änderungen der Nutzungsbedingungen</h2>
<p>
Der Betreiber behält sich vor, diese Nutzungsbedingungen anzupassen, sofern dies
für den Betrieb oder die Weiterentwicklung der Plattform erforderlich ist.
</p>
</main>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
<?php
$error = $error ?? null;
$success = $success ?? null;
?>
<main class="login-page">
<div class="login-container">
<h1>Passwort vergessen</h1>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=password-forgotten">
<p class="input-label">E-Mail-Adresse:</p>
<input type="email"
name="email"
class="login-input"
placeholder="E-Mail-Adresse"
required>
<p class="input-label">Neues Passwort:</p>
<input type="password"
name="password"
class="login-input"
placeholder="Neues Passwort"
required>
<button type="submit"
name="passwordForgottenSubmit"
class="button">
Passwort zurücksetzen
</button>
<div class="register-link">
<a href="index.php?pfad=login">
Zurück zum Login
</a>
</div>
</form>
</div>
</main>
+236
View File
@@ -0,0 +1,236 @@
<?php
include_once 'php/controller/profile-controller.php';
$user = $user ?? null;
$isEditMode = (isset($_GET["edit"]) && $_GET["edit"] === "1") || !empty($error);
?>
<main class="form-page">
<div class="flexbox">
<div class="container">
<?php include_once "includes/alertMessages.php" ?>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=profile">
<?php csrf_field(); ?>
<label class="input-label">Vorname</label>
<input type="text"
name="vorname"
class="login-input"
value="<?php echo htmlspecialchars($_POST["vorname"] ?? $user["vorname"] ?? ""); ?>"
<?php echo $isEditMode ? "" : "readonly"; ?>
required>
<label class="input-label">Nachname</label>
<input type="text"
name="nachname"
class="login-input"
value="<?php echo htmlspecialchars($_POST["nachname"] ?? $user["nachname"] ?? ""); ?>"
<?php echo $isEditMode ? "" : "readonly"; ?>
required>
<label class="input-label">Email-Adresse</label>
<input type="email"
name="email"
class="login-input"
value="<?php echo htmlspecialchars($_POST["email"] ?? $user["email"] ?? ""); ?>"
<?php echo $isEditMode ? "" : "readonly"; ?>
required>
<label class="input-label">
<?php echo $isEditMode ? "Neues Passwort" : "Passwort"; ?>
</label>
<?php if ($isEditMode): ?>
<input type="password"
name="password"
class="login-input"
placeholder="Leer lassen, wenn es gleich bleiben soll">
<?php else: ?>
<input type="password"
name="password"
class="login-input"
value="********"
readonly>
<?php endif; ?>
<br><br>
<?php if ($isEditMode): ?>
<button type="submit"
name="saveProfile"
class="button">
Speichern
</button>
<a href="index.php?pfad=profile" class="button">
Abbrechen
</a>
<?php else: ?>
<a href="index.php?pfad=profile&edit=1" class="button">
Bearbeiten
</a>
<?php endif; ?>
</form>
<br>
<form action="php/controller/deleteAccount-controller.php" method="POST">
<?php csrf_field(); ?>
<button type="submit"
class="button"
onclick="return confirm('Möchtest du deinen Account und alle deine Beiträge wirklich unwiderruflich löschen?');">
Account löschen
</button>
</form>
</div>
<div class="container">
<h2 class="section-title">Meine Beiträge</h2>
<div class="articles-list">
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
<p class="alert-message is-error">
Es ist ein interner Fehler aufgetreten. Bitte versuche es erneut.
</p>
<?php elseif (isset($userArticles) && count($userArticles) > 0): ?>
<?php foreach ($userArticles as $userArticle): ?>
<div class="article-item">
<div class="article-meta">
<span class="article-date">
<?php echo htmlspecialchars($userArticle->getCreationDate()); ?>
</span>
<span class="article-category">
<?php echo htmlspecialchars($userArticle->getCategory()); ?>
</span>
</div>
<h3 class="article-title">
<a href="index.php?pfad=showArticle&id=<?php echo $userArticle->getID(); ?>">
<?php echo htmlspecialchars($userArticle->getTitle()); ?>
</a>
</h3>
<?php
$tags = $userArticle->getTags();
if (isset($tags) && !empty($tags)):
?>
<div class="article-view-bottom-section">
<div class="article-view-tags-label">Tags:</div>
<div class="article-view-tags-list">
<?php
$tagArray = is_array($tags) ? $tags : explode(',', $tags);
foreach ($tagArray as $tag):
$trimmedTag = trim($tag);
if (!empty($trimmedTag)):
?>
<span class="article-view-tag-item">
<?php echo htmlspecialchars($trimmedTag); ?>
</span>
<?php
endif;
endforeach;
?>
</div>
</div>
<?php endif; ?>
<a href="index.php?pfad=updateArticle&id=<?php echo $userArticle->getID(); ?>"
class="edit-link-button">
Bearbeiten
</a>
<form action="php/controller/deleteArticle-controller.php" method="POST">
<?php csrf_field(); ?>
<input type="hidden"
name="id"
value="<?php echo htmlspecialchars($userArticle->getID()); ?>">
<button type="submit"
class="button"
onclick="return confirm('Möchtest du diesen Artikel wirklich löschen?');">
Löschen
</button>
</form>
</div>
<?php endforeach; ?>
<?php else: ?>
<p>Du hast noch keine Beiträge erstellt.</p>
<button type="button"
class="button"
onclick="window.location.href='index.php?pfad=createArticle';">
Beitrag erstellen!
</button>
<?php endif; ?>
</div>
</div>
<div class="container">
<div class="comments-section">
<h2 class="section-title">Meine Kommentare</h2>
<!-- Liste aller Kommentare des Nutzers -->
<div class="comments-list">
<?php if (isset($userComments) && count($userComments) > 0): ?>
<?php foreach ($userComments as $comment): ?>
<div class="article-item">
<!-- Erstellungsdatum des Kommentars -->
<div class="article-meta">
<span class="article-date">
<?php echo htmlspecialchars($comment->getCreated()); ?>
</span>
</div>
<!-- Inhalt des Kommentars -->
<p>
<?php echo nl2br(htmlspecialchars($comment->getContent())); ?>
</p>
<!-- Link zum Beitrag, unter dem der Kommentar geschrieben wurde -->
<a href="index.php?pfad=showArticle&id=<?php echo htmlspecialchars($comment->getArticleId()); ?>"
class="edit-link-button">
Zum Beitrag
</a>
</div>
<?php endforeach; ?>
<?php else: ?>
<p>Du hast noch keine Kommentare geschrieben.</p>
<?php endif; ?>
</div>
</div>
</div>
<?php unset($_SESSION["message"]); ?>
</div>
</main>
+70
View File
@@ -0,0 +1,70 @@
<?php
$error = $error ?? null;
$success = $success ?? null;
?>
<!--
Form: Registrierung
Funktion: Erstellung neuer Benutzerkonten
-->
<main class="login-page">
<div class="login-container">
<h1>Jetzt Registrieren!</h1>
<?php if (!empty($error)): ?>
<p class="alert-message is-error">
<?php echo htmlspecialchars($error); ?>
</p>
<?php endif; ?>
<?php if (!empty($success)): ?>
<p class="alert-message is-success">
<?php echo $success; ?>
</p>
<?php endif; ?>
<form method="post" action="index.php?pfad=register">
<p class="input-label">Email:</p>
<input type="email"
name="email"
class="login-input"
placeholder="mustermann@web.de"
value="<?php echo htmlspecialchars($_POST["email"] ?? ""); ?>"
required>
<p class="input-label">Vorname:</p>
<input type="text"
name="vorname"
class="login-input"
placeholder="Max"
value="<?php echo htmlspecialchars($_POST["vorname"] ?? ""); ?>"
required>
<p class="input-label">Nachname:</p>
<input type="text"
name="nachname"
class="login-input"
placeholder="Mustermann"
value="<?php echo htmlspecialchars($_POST["nachname"] ?? ""); ?>"
required>
<p class="input-label">Passwort:</p>
<input type="password"
name="password"
class="login-input"
placeholder="Passwort"
required>
<button type="submit"
value="register"
name="registerSubmit"
class="button">
kostenlos registrieren
</button>
</form>
</div>
</main>
+212
View File
@@ -0,0 +1,212 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION["search_results"] ?? [];
$query = $_SESSION["search_query"] ?? "";
// Sortierung / Kategorie / Limit: GET-Parameter haben Vorrang, sonst
// Rückfall auf die zuletzt in der Session gemerkten Werte. So bleibt
// z.B. ein reiner Pagination-Link (?page=2) bei der aktuellen
// Sortierung/Filterung.
$currentSort = $_GET['sort'] ?? ($_SESSION['search_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['search_sort'] = $currentSort;
$currentCategory = strtolower($_GET['category'] ?? ($_SESSION['search_category'] ?? 'all'));
$_SESSION['search_category'] = $currentCategory;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['search_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10;
}
$_SESSION['search_limit'] = $limit;
// Serverseitig sortieren + filtern
$filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterSearchResultsByCategory($filteredResults, $currentCategory);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) {
$currentPage = 1;
} elseif ($currentPage > $totalPages) {
$currentPage = $totalPages;
}
$offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/results-app.js zusätzlich den kompletten
// Datensatz nach (ein einziger Request) und übernimmt Sortierung, Filter
// und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results);
// Kategorien für das Auswahlfeld (Wert => Label)
$categories = [
'Deutsch' => 'Deutsch', 'Englisch' => 'Englisch', 'Franzoesisch' => 'Französisch',
'Latein' => 'Latein', 'Literatur' => 'Literatur', 'Mathematik' => 'Mathematik',
'Biologie' => 'Biologie', 'Informatik' => 'Informatik', 'Chemie' => 'Chemie',
'Physik' => 'Physik', 'Astronomie' => 'Astronomie', 'Geschichte' => 'Geschichte',
'Erdkunde' => 'Erdkunde', 'Sozialkunde' => 'Sozialkunde', 'Wirtschaftskunde' => 'Wirtschaftskunde',
'Religion' => 'Religion', 'Ethikunterricht' => 'Ethikunterricht', 'Philosophie' => 'Philosophie',
'Psychologie' => 'Psychologie', 'Kunst' => 'Kunst', 'Musik' => 'Musik', 'Theater' => 'Theater',
'Technik' => 'Technik', 'Werken' => 'Werken', 'Hauswirtschaft' => 'Hauswirtschaft', 'Sport' => 'Sport',
];
?>
<!--
Seite: Suchergebnisse
Inhalt: Zeigt die Ergebnisse einer Suche an.
Funktioniert ohne JavaScript (serverseitige Sortierung/Filter/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/search-results.js vollständig client-seitig übernommen.
-->
<div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Filter und Suche -->
<aside class="s-res-sidebar">
<!-- Formular 1: startet eine NEUE Suche (löst immer einen Server-Request aus) -->
<form action="php/controller/search-results-controller.php" method="GET" id="search-form-id" class="s-res-sidebar-form">
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Suche anpassen</h3>
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button>
</div>
</form>
<!-- Formular 2: sortiert/filtert die BEREITS gefundenen Ergebnisse.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Suche.
Mit JS: search-results.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="s-res-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="search-results">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($query); ?>">
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3>
<div class="s-res-filter-group">
<label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<span>Alphabetisch</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
<span>Beliebtheit (Likes)</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
<span>Neueste Beiträge</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
<span>Älteste Beiträge</span>
</label>
</div>
</div>
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Kategorie filtern</h3>
<select id="category-filter" name="category" class="s-res-limit-select" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #cbd5e1;">
<option value="all" <?php echo $currentCategory === 'all' ? 'selected' : ''; ?>>Alle Kategorien</option>
<?php foreach ($categories as $value => $label): ?>
<option value="<?php echo htmlspecialchars($value); ?>" <?php echo $currentCategory === strtolower($value) ? 'selected' : ''; ?>><?php echo htmlspecialchars($label); ?></option>
<?php endforeach; ?>
</select>
</div>
<noscript>
<button type="submit" class="nav__search-button">Filter anwenden</button>
</noscript>
</form>
</aside>
<main class="s-res-main-content">
<div class="s-res-header">
<h1 class="s-res-main-title">Suchergebnisse</h1>
<p class="s-res-meta"><span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer für Ihre Suchanfrage "<?php echo htmlspecialchars($query); ?>"</p>
</div>
<!-- Ergebnisliste -->
<div class="s-res-list">
<?php if (!empty($results)): ?>
<?php foreach ($results as $item):
$likesCount = getLikeCount($item);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content">
<h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</h2>
<div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div>
</div>
<div class="s-res-arrow">&rarr;</div>
</div>
<?php endforeach; ?>
<?php elseif (isset($_SESSION["message"]) && $_SESSION["message"] === "invalid_search_query"): ?>
<p>Unzulässige Suchanfrage</p>
<?php elseif ($query !== "" && $totalResultsCount === 0 && count($rawResults) > 0): ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php elseif ($query !== "" && $totalResultsCount === 0): ?>
<p>Keine Beiträge zu diesem Suchbegriff gefunden.</p>
<?php endif; ?>
<?php unset($_SESSION["message"]); ?>
</div>
<div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite. Gehört per form="..." logisch
zum Filterformular oben -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="s-res-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select>
<noscript><button type="submit" form="s-res-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
<!-- No-JS-Fallback: -->
<noscript>
<?php echo renderNoJsPagination($currentPage, $totalPages, $query, $currentSort, $currentCategory, $limit); ?>
</noscript>
<!-- JS-Version: wird per search-results.js befüllt/eingeblendet: -->
<div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
</div>
</nav>
</div>
</main>
</div>
+24
View File
@@ -0,0 +1,24 @@
<?php
/*
* Zeigt den Inhalt einer simulierten E-Mail an.
* Die Datei wird über einen zufällig erzeugten Token geladen.
*/
$token = basename($_GET["token"] ?? "");
$file = __DIR__ . "/../data/mails/" . $token . ".html";
if (!file_exists($file)) {
echo "<p>Datei nicht gefunden.</p>";
exit();
}
?>
<main class="login-page">
<div class="login-container">
<h1>Simulierte E-Mail</h1>
<?php include $file; ?>
</div>
</main>
+483
View File
@@ -0,0 +1,483 @@
<?php
$comments = [];
$mainComments = [];
$repliesByParent = [];
$articleObj = null;
include_once 'php/controller/showArticle-controller.php';
/*
* Ermittelt, ob ohne JavaScript auf einen Kommentar
* geantwortet werden soll.
*/
$replyTo = filter_input(
INPUT_GET,
"reply_to",
FILTER_VALIDATE_INT
);
$replyAuthor = null;
if ($replyTo !== false && $replyTo !== null) {
foreach ($mainComments as $mainComment) {
if ($mainComment->getId() === $replyTo) {
$replyAuthor = $mainComment->getAuthor();
break;
}
}
}
/*
* Eine Antwort darf nur auf einen existierenden
* Hauptkommentar geschrieben werden.
*/
if ($replyAuthor === null) {
$replyTo = null;
}
?>
<!--
Seite: Anzeige für Beiträge
Funktion: Stellt einen übergebenen Beitrag dar.
-->
<!-- Hauptcontainer für die Beitragsansicht (Ausschließlich der Content-Bereich) -->
<main class="article-view-container">
<?php include_once "includes/alertMessages.php"?>
<!-- Metadaten & Titel -->
<div class="article-view-top-section">
<div class="category-and-likes-row">
<?php if (isset($category) && !empty($category)): ?>
<span class="article-view-category"><?php echo htmlspecialchars($category); ?></span>
<?php endif; ?>
<!-- Like-Anzeige und dynamischer Like-Button -->
<?php if (isset($articleObj) && $articleObj !== null): ?>
<div class="article-view-likes">
<span>❤️ <span class="like-count"><?php echo $articleObj->getLikeCount(); ?></span></span>
<?php if (isset($_SESSION["user_email"])): ?>
<form method="post"
action="php/controller/like-controller.php?id=<?php echo $articleObj->getId(); ?>"
class="like-toggle-form">
<?php csrf_field(); ?>
<button type="submit" class="like-toggle-btn">
<?php echo $articleObj->hasLiked($_SESSION["user_email"]) ? '👎 Gefällt mir nicht mehr' : '👍 Gefällt mir'; ?>
</button>
</form>
<?php else: ?>
<span class="login-hint">(Anmelden zum Liken)</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<h1 class="article-view-title">
<?php if (isset($title)) { echo htmlspecialchars($title); } ?>
</h1>
<div class="article-view-meta">
<?php if (isset($name) && !empty($name)): ?>
<span class="article-view-author">Von: <strong><?php echo htmlspecialchars($name); ?></strong></span>
<?php endif; ?>
</div>
</div>
<!-- Beitrags-Inhalt -->
<div class="article-view-content">
<?php if (isset($content)): ?>
<?php
// Versuchen, den Inhalt von JSON in ein PHP-Array umzuwandeln
$blocks = json_decode($content, true);
// Wenn das JSON valide ist und Blöcke enthält
if (json_last_error() === JSON_ERROR_NONE && is_array($blocks)):
foreach ($blocks as $block):
if (isset($block['type']) && isset($block['value'])):
if ($block['type'] === 'text'): ?>
<!-- Textblock mit XSS-Schutz und Erhalt von Zeilenumbrüchen -->
<div class="article-view-body block-text">
<?php echo nl2br(htmlspecialchars($block['value'])); ?>
</div>
<?php elseif ($block['type'] === 'image'): ?>
<!-- Bildblock, der auf den relativen Pfad im uploads-Ordner verweist -->
<div class="article-view-body block-image">
<img src="<?php echo htmlspecialchars($block['value']); ?>" alt="Beitragsbild">
</div>
<?php endif;
endif;
endforeach;
else: ?>
<!-- Fallback: Wenn der Beitrag alten Reintext aus der DB enthält -->
<div class="article-view-body block-text">
<?php echo nl2br(htmlspecialchars($content)); ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- Beitrags-Endbereich (Tags) -->
<?php if (isset($tags) && !empty($tags)): ?>
<div class="article-view-bottom-section">
<div class="article-view-tags-label">Tags:</div>
<div class="article-view-tags-list">
<?php
// Falls $tags ein String ist (z.B. "Web, CSS"), in ein Array umwandeln
$tagArray = is_array($tags) ? $tags : explode(',', $tags);
foreach ($tagArray as $tag):
$trimmedTag = trim($tag);
if (!empty($trimmedTag)):
?>
<span class="article-view-tag-item"><?php echo htmlspecialchars($trimmedTag); ?></span>
<?php
endif;
endforeach;
?>
</div>
</div>
<?php endif; ?>
<section class="article-comments-section" id="comments">
<h2>Kommentare</h2>
<?php if (isset($_SESSION["comment_message"])): ?>
<div class="alert-message <?php
echo ($_SESSION["comment_message_type"] ?? "") === "success"
? "is-success"
: "is-error";
?>">
<?php echo htmlspecialchars($_SESSION["comment_message"]); ?>
</div>
<?php
unset($_SESSION["comment_message"]);
unset($_SESSION["comment_message_type"]);
?>
<?php endif; ?>
<div id="comments-list">
<?php if (!empty($mainComments)): ?>
<?php foreach ($mainComments as $comment): ?>
<div class="comment-item"
data-comment-id="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<?php
$isDeleted = $comment->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($comment->getAuthor()); ?>
</strong>
<span>
<?php echo htmlspecialchars($comment->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($comment->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $comment->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<label for="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>">
Kommentar bearbeiten
</label>
<textarea
id="edit-comment-<?php
echo htmlspecialchars((string) $comment->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$comment->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $comment->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php if (isset($_SESSION["user_email"])): ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) $comment->getArticleId());
?>&reply_to=<?php
echo urlencode((string) $comment->getId());
?>#comment-form"
class="reply-button"
data-comment-id="<?php echo htmlspecialchars(
(string) $comment->getId()
); ?>"
data-author="<?php echo htmlspecialchars(
$comment->getAuthor()
); ?>">
Antworten
</a>
<?php endif; ?>
<?php endif; ?>
<div class="comment-replies">
<?php if (isset($repliesByParent[$comment->getId()])): ?>
<?php foreach ($repliesByParent[$comment->getId()] as $reply): ?>
<div class="comment-item comment-reply">
<?php
$isReplyDeleted = $reply->getContent()
=== "Dieser Kommentar wurde gelöscht.";
?>
<?php if ($isReplyDeleted): ?>
<p class="deleted-comment">
Dieser Kommentar wurde gelöscht.
</p>
<?php else: ?>
<p>
<strong>
<?php echo htmlspecialchars($reply->getAuthor()); ?>
</strong>
<span>
<?php echo htmlspecialchars($reply->getCreated()); ?>
</span>
</p>
<p>
<?php
echo nl2br(
htmlspecialchars($reply->getContent())
);
?>
</p>
<?php if (
isset($_SESSION["user_email"])
&& $_SESSION["user_email"] === $reply->getAuthor()
): ?>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<label for="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>">
Antwort bearbeiten
</label>
<textarea
id="edit-reply-<?php
echo htmlspecialchars((string) $reply->getId());
?>"
name="content"
required><?php echo htmlspecialchars(
$reply->getContent()
); ?></textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form">
<?php csrf_field(); ?>
<input type="hidden"
name="comment_id"
value="<?php echo htmlspecialchars(
(string) $reply->getId()
); ?>">
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) $reply->getArticleId()
); ?>">
<button type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');">
Kommentar löschen
</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="no-comments-message">
Noch keine Kommentare vorhanden.
</p>
<?php endif; ?>
</div>
<?php if (isset($_SESSION["user_email"])): ?>
<form id="comment-form"
method="post"
action="php/ajax/add-comment.php">
<?php csrf_field(); ?>
<input type="hidden"
name="article_id"
value="<?php echo htmlspecialchars(
(string) ($_GET["id"] ?? "")
); ?>">
<input type="hidden"
name="parent_comment_id"
id="parent-comment-id"
value="<?php echo $replyTo !== null
? htmlspecialchars((string) $replyTo)
: "";
?>">
<p id="reply-info"
class="reply-info"
<?php if ($replyAuthor === null): ?>
style="display: none;"
<?php endif; ?>>
<?php if ($replyAuthor !== null): ?>
Antwort auf <?php echo htmlspecialchars($replyAuthor); ?>
<a href="index.php?pfad=<?php
echo urlencode($_GET["pfad"] ?? "showArticle");
?>&id=<?php
echo urlencode((string) ($_GET["id"] ?? ""));
?>#comment-form">
Abbrechen
</a>
<?php endif; ?>
</p>
<label for="comment-content">
Kommentar
</label>
<textarea name="content"
id="comment-content"
placeholder="Schreibe einen Kommentar..."
required></textarea>
<button type="submit" class="button">
Kommentar senden
</button>
</form>
<?php else: ?>
<div class="comment-login-hint">
<p>Melde dich an, um einen Kommentar zu schreiben.</p>
<a href="index.php?pfad=login" class="button">Jetzt anmelden</a>
</div>
<?php endif; ?>
</section>
</main>
+184
View File
@@ -0,0 +1,184 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
include_once "php/controller/showCategory-controller.php";
require_once __DIR__ . '/../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? [];
$category = $_SESSION['category_name'] ?? ($_GET['category'] ?? '');
// Sortierung / Suche / Limit: GET-Parameter haben Vorrang, sonst Rückfall auf
// die zuletzt in der Session gemerkten Werte (analog zu search-results.php).
$currentSort = $_GET['sort'] ?? ($_SESSION['cat_sort'] ?? 'alphabet');
if (!in_array($currentSort, ['alphabet', 'likes', 'newest', 'oldest'])) {
$currentSort = 'alphabet';
}
$_SESSION['cat_sort'] = $currentSort;
$query = trim($_GET['q'] ?? ($_SESSION['cat_query'] ?? ''));
$_SESSION['cat_query'] = $query;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : ($_SESSION['cat_limit'] ?? 10);
if (!in_array($limit, [10, 20, 50, 100])) {
$limit = 10;
}
$_SESSION['cat_limit'] = $limit;
// Serverseitig sortieren + nach Suchbegriff filtern (Kategorie steht bereits fest)
$filteredResults = sortSearchResults($rawResults, $currentSort);
$filteredResults = filterResultsByQuery($filteredResults, $query);
$totalResultsCount = count($filteredResults);
$totalPages = max(1, (int)ceil($totalResultsCount / $limit));
$currentPage = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($currentPage < 1) {
$currentPage = 1;
} elseif ($currentPage > $totalPages) {
$currentPage = $totalPages;
}
$offset = ($currentPage - 1) * $limit;
// Dies ist die No-JS-Basisversion: nur die aktuelle Seite wird gerendert.
// Bei aktiviertem JS lädt js/showCategory.js zusätzlich den kompletten
// Kategorie-Datensatz nach (ein einziger Request) und übernimmt Sortierung,
// Suche und Pagination danach komplett im Browser, ohne weitere Serveranfragen.
$results = array_slice($filteredResults, $offset, $limit);
$resultCount = count($results);
?>
<!--
Seite: Kategorie-Ansicht
Inhalt: Zeigt alle Beiträge einer Kategorie an, sortierbar und innerhalb
der Kategorie durchsuchbar.
Funktioniert ohne JavaScript (serverseitige Sortierung/Suche/Pagination
über echte Links + Formulare) und wird bei aktiviertem JS von
js/showCategory.js vollständig client-seitig übernommen.
-->
<div class="s-res-layout-grid">
<?php include_once "includes/alertMessages.php" ?>
<!-- Links: Seitenleiste für Suche (innerhalb der Kategorie) und Sortierung -->
<aside class="s-res-sidebar">
<!-- Sortiert/durchsucht die BEREITS geladenen Kategorie-Beiträge.
Ohne JS: echter Reload dieser Seite, aber ohne erneute Datenbank-Abfrage.
Mit JS: js/showCategory.js übernimmt das live im Browser, der Submit-Button
wird dafür nicht benötigt und über <noscript> versteckt. -->
<form action="index.php" method="GET" id="cat-filter-form" class="s-res-sidebar-form">
<input type="hidden" name="pfad" value="showCategory">
<input type="hidden" name="category" value="<?php echo htmlspecialchars($category); ?>">
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">In dieser Kategorie suchen</h3>
<input type="search" id="cat-search-input" name="q" placeholder="Suchen..." class="nav__search" value="<?php echo htmlspecialchars($query); ?>" maxlength="50">
</div>
<div class="s-res-sidebar-box">
<h3 class="s-res-sidebar-title">Sortierung</h3>
<div class="s-res-filter-group">
<label class="s-res-filter-option">
<input type="radio" name="sort" value="alphabet" class="sort-radio" <?php echo $currentSort === 'alphabet' ? 'checked' : ''; ?>>
<span>Alphabetisch</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="likes" class="sort-radio" <?php echo $currentSort === 'likes' ? 'checked' : ''; ?>>
<span>Beliebtheit (Likes)</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="newest" class="sort-radio" <?php echo $currentSort === 'newest' ? 'checked' : ''; ?>>
<span>Neueste Beiträge</span>
</label>
<label class="s-res-filter-option">
<input type="radio" name="sort" value="oldest" class="sort-radio" <?php echo $currentSort === 'oldest' ? 'checked' : ''; ?>>
<span>Älteste Beiträge</span>
</label>
</div>
</div>
<noscript>
<button type="submit" class="nav__search-button">Anwenden</button>
</noscript>
</form>
</aside>
<main class="s-res-main-content">
<div class="s-res-header">
<h1 class="s-res-main-title">Kategorie: <?php echo htmlspecialchars($category); ?></h1>
<p class="s-res-meta">
<span id="s-res-result-count"><?php echo $totalResultsCount; ?></span> Treffer in dieser Kategorie<?php echo $query !== '' ? ' für "' . htmlspecialchars($query) . '"' : ''; ?>
</p>
</div>
<!-- Ergebnisliste -->
<div class="s-res-list">
<?php if (!empty($results)): ?>
<?php foreach ($results as $item):
$likesCount = getLikeCount($item);
?>
<div class="s-res-item" data-likes="<?php echo $likesCount; ?>" data-category="<?php echo strtolower($item['category'] ?? ''); ?>">
<div class="s-res-content">
<h2 class="s-res-item-title">
<a href="index.php?pfad=showArticle&id=<?php echo $item['id']; ?>" class="s-res-link">
<?php echo htmlspecialchars($item['title']); ?>
</a>
</h2>
<div class="s-res-meta-row">
<p class="s-res-author">Von: <span class="s-res-author-name"><?php echo htmlspecialchars($item['author']); ?></span></p>
<span class="s-res-likes">
❤️ <?php echo $likesCount; ?>
</span>
</div>
</div>
<div class="s-res-arrow">&rarr;</div>
</div>
<?php endforeach; ?>
<?php elseif ($query !== ''): ?>
<p>Keine Beiträge in dieser Kategorie zu diesem Suchbegriff gefunden.</p>
<?php else: ?>
<p>Keine Beiträge in dieser Kategorie gefunden.</p>
<?php endif; ?>
<?php unset($_SESSION["message"]); ?>
</div>
<div class="s-res-pagination-footer">
<!-- Auswahl der Ergebnisse pro Seite -->
<div class="s-res-limit-selector">
<label for="s-res-per-page" class="s-res-limit-label">Ergebnisse pro Seite:</label>
<select id="s-res-per-page" name="limit" form="cat-filter-form" class="s-res-limit-select">
<option value="10" <?php echo $limit === 10 ? 'selected' : ''; ?>>10</option>
<option value="20" <?php echo $limit === 20 ? 'selected' : ''; ?>>20</option>
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100</option>
</select>
<noscript><button type="submit" form="cat-filter-form" class="nav__search-button">Übernehmen</button></noscript>
</div>
<nav class="s-res-page-navigation" aria-label="Seitennavigation">
<!-- No-JS-Fallback: -->
<noscript>
<?php echo renderNoJsPaginationCategory($currentPage, $totalPages, $category, $currentSort, $query, $limit); ?>
</noscript>
<!-- JS-Version: wird per js/showCategory.js befüllt/eingeblendet: -->
<div id="js-page-navigation" style="display:none;">
<button type="button" class="s-res-page-btn" id="prev-page-btn">«</button>
<span id="dynamic-page-numbers"></span>
<button type="button" class="s-res-page-btn" id="next-page-btn">»</button>
</div>
</nav>
</div>
</main>
</div>
+326
View File
@@ -0,0 +1,326 @@
<?php
$user = $user ?? null;
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
include_once 'php/controller/showArticle-controller.php';
// Aktuelle Blockliste ermitteln:
$rawContent = '[]';
if (isset($_SESSION['old_content']) && !empty($_SESSION['old_content'])) {
$rawContent = $_SESSION['old_content'];
unset($_SESSION['old_content']);
} elseif (isset($content) && !empty($content)) {
$rawContent = $content;
}
$blocks = json_decode($rawContent, true);
if (!is_array($blocks)) {
$blocks = [];
}
$blocks = array_values($blocks);
?>
<!--
Seite: Beitrag bearbeiten
Inhalt: Formular für die Bearbeitung eines Beitrags
-->
<form method="post" action="php/controller/updateArticle-controller.php?id=<?php if(isset($id) && !empty($id)){echo htmlspecialchars($id);}else{$_SESSION["message"] = "missing_id";} ?>" id="editor-form" enctype="multipart/form-data" class="article-editor-scope.editor-container article-editor-scope editor-container">
<?php csrf_field(); ?>
<main class="editor-main">
<?php include_once "includes/alertMessages.php"?>
<input type="text" id="title" name="title"
value="<?php
if (isset($_SESSION['old_title']) && !empty($_SESSION['old_title'])){
echo htmlspecialchars($_SESSION['old_title']);
unset($_SESSION['old_title']);
}elseif (isset($title) && !empty($title)){
echo htmlspecialchars($title);
}
?>"
placeholder="Titel hier eingeben" required>
<!--
Content-Blöcke: werden serverseitig als echte, benannte Formularfelder gerendert
(blocks[i][type]...). Dadurch funktioniert das Hinzufügen/Entfernen von Blöcken und
der Bild-Upload auch ganz ohne JavaScript über einen normalen Formular-Submit.
Ist JavaScript aktiv, fängt js/editor.js diese Submits ab und erledigt dieselbe
Änderung lokal im DOM, ohne den Server zu belasten.
-->
<div id="block-container">
<?php foreach ($blocks as $i => $block): ?>
<?php
$blockType = $block['type'] ?? '';
if ($blockType !== 'text' && $blockType !== 'image') {
continue; // unbekannter/kaputter Block wird übersprungen
}
?>
<div class="editor-block article-editor-scope" data-index="<?php echo (int)$i; ?>">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][type]" value="<?php echo htmlspecialchars($blockType); ?>">
<?php if ($blockType === 'text'): ?>
<textarea name="blocks[<?php echo (int)$i; ?>][text]"
placeholder="Schreibe deinen Textblock..."><?php echo htmlspecialchars($block['value'] ?? ''); ?></textarea>
<?php else: /* image */ ?>
<?php if (!empty($block['value'])): ?>
<img src="<?php echo htmlspecialchars($block['value']); ?>"
class="block-image-preview"
style="max-width:200px;display:block;margin-top:10px;">
<input type="hidden" name="blocks[<?php echo (int)$i; ?>][existing_image]" value="<?php echo htmlspecialchars($block['value']); ?>">
<?php endif; ?>
<input type="file" name="blocks[<?php echo (int)$i; ?>][image]" accept="image/*">
<?php endif; ?>
<button type="submit" name="editor_action" value="delete_block:<?php echo (int)$i; ?>" class="delete-block-btn">✕</button>
</div>
<?php endforeach; ?>
</div>
<div id="add-block-control" class="article-editor-scope add-block-control">
<button type="button" id="plus-button" class="article-editor-scope plus-button">+</button>
<div id="block-popup" class="article-editor-scope block-popup">
<button type="submit" name="editor_action" value="add_text" data-type="text">Textblock</button>
<button type="submit" name="editor_action" value="add_image" data-type="image">Bild einfügen</button>
</div>
</div>
</main>
<!-- Seitenleiste -->
<aside class="editor-sidebar">
<div class="sidebar-block">
<button type="submit" id="editor-button" class="button">Änderungen speichern</button>
</div>
<div class="sidebar-block">
<label for="category">Kategorie <span class="required">*</span></label>
<select id="category" name="category" required>
<option disabled <?php
if ((!isset($_SESSION['old_category']) || empty($_SESSION['old_category'])) && (!isset($category) || empty($category))) {
echo 'selected';
}
?>>Kategorie wählen...</option>
<optgroup label="Sprachen">
<option value="deutsch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'deutsch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'deutsch') { echo 'selected'; }
}
?>>Deutsch</option>
<option value="englisch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'englisch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'englisch') { echo 'selected'; }
}
?>>Englisch</option>
<option value="franzoesisch" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'franzoesisch') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'franzoesisch') { echo 'selected'; }
}
?>>Französisch</option>
<option value="latein" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'latein') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'latein') { echo 'selected'; }
}
?>>Latein</option>
<option value="literatur" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'literatur') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'literatur') { echo 'selected'; }
}
?>>Literatur</option>
</optgroup>
<optgroup label="MINT">
<option value="mathe" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'mathe') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'mathe') { echo 'selected'; }
}
?>>Mathematik</option>
<option value="biologie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'biologie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'biologie') { echo 'selected'; }
}
?>>Biologie</option>
<option value="chemie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'chemie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'chemie') { echo 'selected'; }
}
?>>Chemie</option>
<option value="physik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'physik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'physik') { echo 'selected'; }
}
?>>Physik</option>
<option value="informatik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'informatik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'informatik') { echo 'selected'; }
}
?>>Informatik</option>
<option value="astronomie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'astronomie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'astronomie') { echo 'selected'; }
}
?>>Astronomie</option>
</optgroup>
<optgroup label="Gesellschaft & Werte">
<option value="geschichte" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'geschichte') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'geschichte') { echo 'selected'; }
}
?>>Geschichte</option>
<option value="erdkunde" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'erdkunde') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'erdkunde') { echo 'selected'; }
}
?>>Erdkunde</option>
<option value="sozialkunde" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'sozialkunde') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'sozialkunde') { echo 'selected'; }
}
?>>Sozialkunde</option>
<option value="wirtschaft" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'wirtschaft') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'wirtschaft') { echo 'selected'; }
}
?>>Wirtschaftskunde</option>
<option value="religion" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'religion') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'religion') { echo 'selected'; }
}
?>>Religion</option>
<option value="ethik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'ethik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'ethik') { echo 'selected'; }
}
?>>Ethikunterricht</option>
<option value="philosophie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'philosophie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'philosophie') { echo 'selected'; }
}
?>>Philosophie</option>
<option value="psychologie" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'psychologie') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'psychologie') { echo 'selected'; }
}
?>>Psychologie</option>
<option value="kunst" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'kunst') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'kunst') { echo 'selected'; }
}
?>>Kunst</option>
<option value="musik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'musik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'musik') { echo 'selected'; }
}
?>>Musik</option>
<option value="theater" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'theater') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'theater') { echo 'selected'; }
}
?>>Theater</option>
</optgroup>
<optgroup label="Technik & Praxis">
<option value="technik" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'technik') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'technik') { echo 'selected'; }
}
?>>Technik</option>
<option value="werken" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'werken') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'werken') { echo 'selected'; }
}
?>>Werken</option>
<option value="hauswirtschaft" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'hauswirtschaft') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'hauswirtschaft') { echo 'selected'; }
}
?>>Hauswirtschaft</option>
<option value="sport" <?php
if (isset($_SESSION['old_category']) && !empty($_SESSION['old_category'])) {
if ($_SESSION['old_category'] === 'sport') { echo 'selected'; }
} elseif (isset($category) && !empty($category)) {
if ($category === 'sport') { echo 'selected'; }
}
?>>Sport</option>
</optgroup>
<?php
if (isset($_SESSION['old_category'])) {
unset($_SESSION['old_category']);
}
?>
</select>
</div>
<div class="sidebar-block">
<label for="tags">Schlagwörter</label>
<input type="text" id="tags" name="tags"
value="<?php
if (isset($_SESSION['old_tags']) && !empty($_SESSION['old_tags'])){
echo htmlspecialchars($_SESSION['old_tags']);
unset($_SESSION['old_tags']);
} elseif (isset($tags) && !empty($tags)){
echo htmlspecialchars($tags);
}
?>"
placeholder="z.B. Technik, IT (mit Komma trennen)">
</div>
</aside>
</form>
+249
View File
@@ -0,0 +1,249 @@
/* editor in Flexbox */
.article-editor-scope.editor-container {
display: flex;
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
color: #1e1e1e;
background-color: #f0f2f5;
box-sizing: border-box;
max-width: 1200px;
margin: 0 auto; /* Zentriert den gesamten Container horizontal */
width: 100%;
}
.article-editor-scope * {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Hauptbereich */
.article-editor-scope .editor-main {
flex: 1;
padding: 40px;
display: flex;
flex-direction: column;
gap: 20px;
background: #ffffff;
}
.article-editor-scope #title {
font-size: 2.5rem;
font-weight: 700;
border: none;
outline: none;
width: 100%;
background: transparent;
}
.article-editor-scope #content {
flex: 1;
font-size: 1.1rem;
line-height: 1.6;
border: none;
outline: none;
resize: none;
width: 100%;
background: transparent;
}
/* Seitenleiste */
.article-editor-scope .editor-sidebar {
width: 300px;
background-color: #ffffff;
border-left: 1px solid #e0e0e0;
padding: 20px;
display: flex;
flex-direction: column;
gap: 24px;
}
.article-editor-scope .sidebar-block {
display: flex;
flex-direction: column;
gap: 8px;
}
.article-editor-scope .sidebar-block label {
font-weight: 600;
font-size: 0.9rem;
}
.article-editor-scope .required {
color: #d94f4f;
}
/* Formularelemente innerhalb der Editor-Sidebar */
.article-editor-scope .editor-sidebar select,
.article-editor-scope .editor-sidebar input[type="text"] {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 0.9rem;
background: #ffffff;
}
/* Button-Zeugs */
.button#editor-button {
padding: 18px 12px;
}
/* Container für die dynamisch per JS eingefügten Blöcke */
.article-editor-scope #block-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Styling für jeden einzelnen dynamisch generierten Block */
.article-editor-scope .editor-block {
position: relative; /* Wichtig für die absolute Positionierung des Lösch-Buttons */
width: 100%;
padding: 15px;
background-color: #fafafa;
border: 1px dashed #cccccc;
border-radius: 6px;
}
/* Textarea innerhalb eines dynamischen Textblocks */
.article-editor-scope .editor-block textarea {
width: 100%;
min-height: 120px;
padding: 10px;
border: 1px solid #dddddd;
border-radius: 4px;
font-family: inherit;
font-size: 1.1rem;
line-height: 1.6;
resize: vertical;
outline: none;
background: #ffffff;
}
/* Komfort-Löschbutton oben rechts an jedem Block */
.article-editor-scope .delete-block-btn {
position: absolute;
top: -10px;
right: -10px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #e74c3c;
color: #ffffff;
border: none;
font-size: 12px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transition: background-color 0.2s ease;
}
.article-editor-scope .delete-block-btn:hover {
background-color: #c0392b;
}
/* Steuerungselement für den Plus-Button und das Pop-up */
.article-editor-scope .add-block-control {
position: relative; /* Dient als Anker für das absolut positionierte Pop-up */
margin-top: 10px;
display: inline-block;
align-self: flex-start; /* Verhindert, dass der Button die volle Breite spannt */
}
/* Der runde Plus-Button */
.article-editor-scope .plus-button {
width: 45px;
height: 45px;
border-radius: 50%;
background-color: #3498db;
color: #ffffff;
border: none;
font-size: 26px;
font-weight: 300;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
transition: background-color 0.2s ease, transform 0.2s ease;
}
.article-editor-scope .plus-button:hover {
background-color: #2980b9;
transform: scale(1.05);
}
/* Das Pop-up Menü */
.article-editor-scope .block-popup {
position: absolute;
left: 60px; /* Platziert das Menü rechts neben dem Plus-Button */
top: 50%;
transform: translateY(-50%); /* Zentriert das Menü vertikal zum Button */
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 8px;
display: flex;
gap: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
z-index: 999;
white-space: nowrap;
}
/* Die entscheidende Klasse zum Ausblenden */
.article-editor-scope .block-popup.hidden {
display: none !important;
}
/* Buttons im Pop-up (Textblock / Bild einfügen) */
.article-editor-scope .block-popup button {
background-color: #f8f9fa;
border: 1px solid #dcdde1;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #2f3640;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.article-editor-scope .block-popup button:hover {
background-color: #f1f2f6;
border-color: #b2bec3;
}
/* Responsive Anpassungen unter 760px (für z.B. Smartphones) */
@media (max-width: 760px) {
.article-editor-scope.editor-container {
/* Sidebar nach unten */
flex-direction: column;
}
.article-editor-scope .editor-main {
/* weniger Innenabstand */
padding: 20px;
}
.article-editor-scope #content {
/* Verhindert, dass das Textfeld auf kleinen Displays kollabiert */
min-height: 300px;
}
.article-editor-scope .editor-sidebar {
width: 100%;
/* Entfernt den linken Rand und setzt ihn nach oben als Trenner */
border-left: none;
border-top: 1px solid #e0e0e0;
}
.article-editor-scope .editor-block textarea {
min-height: 150px;
}
}
-2
View File
@@ -1,7 +1,5 @@
/* FooterGrundlayout */
footer {
position: fixed;
bottom: 0;
width: 100%;
background: #333d43;
text-align: center;
+225 -4
View File
@@ -1,7 +1,228 @@
body {
background-color: coral;
main {
max-width:1200px;
margin: 0 auto;
padding: 20px;
width: 100%;
box-sizing: border-box;
}
a, button, input, select, textarea, label{
font-family: Arial, Helvetica, sans-serif;
body {
background-color: #eef2f7;
color: #1f2937;
margin: 0;
min-height: 100vh;
display: grid;
grid-template-rows: auto 1fr auto; /* Header(auto), Inhalt (bekommt Rest), Footer(auto) */
}
a, button, input, select, textarea, label, main{
font-family: Arial, Helvetica, sans-serif;
}
h1 {
text-transform: uppercase;
}
.flexbox {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
margin-top: 30px;
}
.container {
background-color: white;
border-radius: 8px;
padding: 15px;
border: 1px solid #dbe3ec;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
transition: transform 0.2s, box-shadow 0.2s;
flex: 1 1 300px;
}
.container:hover {
transform: translateY(-3px);
box-shadow: 0 6px 18px rgba(0,0,0,0.12);
}
.category-link {
display: block;
text-align: left;
text-decoration: none;
font-weight: bold;
padding: 12px;
background-color: #2563eb;
color: white;
border-radius: 8px;
margin-bottom: 10px;
transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s;
}
.category-link:hover {
background-color: #1d4ed8;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.article-link a {
color: #1f2937;
text-decoration: none;
font-weight: bold;
transition: color 0.2s;
}
.article-link a:hover {
color: #2563eb;
text-decoration: underline;
}
@media (min-width: 1024px) {
.container {
padding: 25px;
}
}
/* Seite wird horizontal und vertikal zentriert */
.login-page {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
}
.login-container {
width: 90%;
max-width: 450px;
padding: 30px;
border: 1px solid #dbe3ec;
border-radius: 10px;
background-color: white;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.logo-wrapper {
text-align: center;
margin-bottom: 20px;
}
.login-logo {
width: 120px;
height: auto;
}
.login-container h1 {
text-align: center;
margin-bottom: 25px;
font-size: 1.7rem;
color: #1e293b;
}
.input-label {
margin-bottom: 5px;
font-weight: bold;
width: 100%;
color: #1f2937;
}
.login-input {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 1px solid #d1d5db;
border-radius: 6px;
box-sizing: border-box;
font-size: 1rem;
background-color: white;
}
.login-input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.checkbox-wrapper {
margin-bottom: 20px;
color: #1f2937;
}
.button {
width: 100%;
padding: 12px;
background-color: #2563eb;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s, transform 0.2s, box-shadow 0.2s;
}
.button:hover {
background-color: #1d4ed8;
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.register-link {
margin-top: 20px;
text-align: center;
font-size: 0.9rem;
color: #1f2937;
}
.register-link a {
color: #2563eb;
text-decoration: none;
font-weight: bold;
transition: color 0.2s;
}
.register-link a:hover {
color: #1d4ed8;
text-decoration: underline;
}
/* Versteckt das doppelte Label fuer Screenreader/Accessibility */
.screenreader-only {
display: none;
}
/* Allgemeine Formularseiten */
.form-page {
display: flex;
justify-content: center;
padding: 40px 20px;
}
.form-container {
width: 90%;
max-width: 600px;
flex: 1 1 450px;
padding: 30px;
background-color: white;
border: 1px solid #dbe3ec;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
box-sizing: border-box;
}
/* Darstellung der Registrierungsbestätigung */
.confirm-message {
text-align: center;
margin: 20px 0;
}
/* Anpassung des Login-Buttons auf der Bestätigungsseite */
.confirm-button {
display: block;
width: 100%;
text-align: center;
text-decoration: none;
box-sizing: border-box;
}
+29
View File
@@ -0,0 +1,29 @@
.alert-message {
padding: 12px 16px;
margin: 12px 0;
border-radius: 6px;
border: 1px solid transparent;
font-family: sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
display: flex;
align-items: center;
gap: 10px;
}
/* Rote Fehlermeldung */
.alert-message.is-error {
color: #ba1a1a;
background-color: #ffeede;
border-color: #ffb4ab;
}
/* Grüne Erfolgsmeldung */
.alert-message.is-success {
color: #006e2c;
background-color: #e8f5e9;
border-color: #b2dfdb;
}
+84 -20
View File
@@ -57,6 +57,10 @@ CSS für die navbar
font-weight: 600;
}
.nav__dropdown-menu a:hover {
color: #1d4ed8;
}
.nav__link {
display: inline-block;
font-weight: 600;
@@ -79,6 +83,8 @@ CSS für die navbar
text-decoration: none;
font-weight: 600;
margin-left: 0.5rem;
white-space: nowrap;
}
.nav__search {
@@ -87,6 +93,12 @@ CSS für die navbar
border-radius: 4px; /* Abgerundete Ecken */
background: #fff;
margin: 0 0.5rem;
width: 100%;
}
.nav__search input {
flex: 1;
width: 100%;
}
.nav__search-button {
@@ -113,8 +125,33 @@ CSS für die navbar
display: block;
}
/* Responsive Anpassung unter 1210px */
@media (max-width: 1210px) {
.nav__search-form {
display: flex;
width: 100%;
margin: 0;
}
.nav__search {
flex: 1;
width: 100%;
}
.nav__search-button {
display: inline-block;
background: #fff;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
margin-left: 4px;
flex-shrink: 0;
white-space: nowrap;
}
/* Responsive Anpassung unter 1240px */
@media (max-width: 1240px) {
.nav {
flex-wrap: wrap; /* zweite Nav-Zeile*/
padding: 0.5rem 1rem;
@@ -149,8 +186,8 @@ CSS für die navbar
display: none;
}
/* Responsive Anpassungen unter 660px (für z.B. Smartphones) */
@media (max-width: 660px) {
/* Responsive Anpassungen unter 900px (für z.B. Smartphones) */
@media (max-width: 900px) {
.nav {
flex-direction: row;
flex-wrap: wrap;
@@ -187,22 +224,8 @@ CSS für die navbar
cursor: pointer;
}
.nav__search {
display: flex;
width: 100%;
margin: 0;
}
.nav__search-button {
display: inline-block;
background: #fff;
border: none;
border-radius: 4px;
padding: 0.4rem 0.6rem;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
margin-left: 4px;
.nav__search-button:hover {
background-color: #f8f9fa;
}
/* restliche desktop-navbar aus */
@@ -227,6 +250,9 @@ CSS für die navbar
padding: 2rem 1rem;
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
overflow-y: auto;
/* Genug Abstand oben rechts, damit Links nicht hinter dem X liegen */
padding: 4rem 1.5rem 2rem 1.5rem;
}
.nav__checkbox:checked ~ .nav__mobile-menu {
@@ -237,6 +263,18 @@ CSS für die navbar
left: 0;
}
.nav__close-btn {
position: absolute;
top: 1rem;
right: 1.5rem;
font-size: 2rem;
color: #fff;
cursor: pointer;
line-height: 1;
user-select: none;
transition: color 0.2s ease, transform 0.2s ease;
}
.nav__mobile-menu a {
color: #fff;
text-decoration: none;
@@ -247,6 +285,32 @@ CSS für die navbar
border-bottom: 1px solid #333d43;
}
.nav__logout-form {
display: contents;
margin: 0;
}
.nav__logout-form .nav__button {
width: 100%;
height: 100%;
}
.nav__mobile-logout-button {
color: #fff;
text-decoration: none;
font-size: 1.2rem;
font-weight: 600;
display: block;
width: 100%;
text-align: left;
padding: 0.5rem 1rem;
border: none;
border-bottom: 1px solid #333d43;
background: none;
cursor: pointer;
font-family: inherit;
}
.nav__mobile-submenu {
display: block;
list-style: none;
+89
View File
@@ -0,0 +1,89 @@
/*
CSS für die Profilseite
*/
/* Roter Button für Account löschen */
.delete-account-button {
background-color: #dc2626;
margin-bottom: 0;
}
.delete-account-button:hover {
background-color: #b91c1c;
}
/* Beitragsliste Styling */
.section-title {
margin-top: 0;
margin-bottom: 25px;
font-size: 1.5rem;
color: #1f2937;
border-bottom: 2px solid #eef2f7;
padding-bottom: 10px;
}
.articles-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.article-item {
padding: 15px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background-color: #f9fafb;
position: relative;
}
.article-meta {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: #6b7280;
margin-bottom: 8px;
}
.article-category {
font-weight: bold;
color: #2563eb;
}
.article-title {
margin: 0 0 10px 0;
font-size: 1.2rem;
color: #1f2937;
}
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 15px;
}
.tag {
font-size: 0.75rem;
background-color: #e5e7eb;
color: #374151;
padding: 3px 8px;
border-radius: 4px;
}
/* Bearbeiten-Button als Link deklariert */
.edit-link-button {
display: inline-block;
text-decoration: none;
font-size: 0.9rem;
font-weight: bold;
color: #2563eb;
border: 1px solid #2563eb;
padding: 6px 12px;
border-radius: 6px;
transition: background-color 0.2s, color 0.2s;
}
.edit-link-button:hover {
background-color: #2563eb;
color: white;
}
+274
View File
@@ -0,0 +1,274 @@
/*
CSS für die Suchergebnis-Seite
*/
/* Container und Layout-Schutz vor globalen Styles */
.s-res-layout-grid {
box-sizing: border-box;
max-width: 95%; /* Nutzt jetzt 95% der Bildschirmbreite statt starrer Pixel */
width: 95%; /* Erhöht das Limit für extrem große Monitore von 1200px auf 1600px */
margin: 2rem auto;
padding: 0 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
line-height: 1.5;
/* Grid für die Zweispaltigkeit */
display: grid;
grid-template-columns: 320px 1fr; /* Seitenleiste leicht verbreitert, Inhalt nimmt den gesamten Rest ein */
gap: 2.5rem; /* Etwas mehr Abstand zwischen Seitenleiste und Inhalt für das breitere Layout */
}
.s-res-layout-grid * {
box-sizing: border-box;
}
/* --- SEITENLEISTE (FILTER & SUCHE) --- */
.s-res-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.s-res-sidebar-box {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
}
.s-res-sidebar-title {
font-size: 1rem;
font-weight: 700;
color: #1a202c;
margin: 0 0 1rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Formular & deine Suchleisten-Klassen */
.s-res-search-form {
display: flex;
align-items: center;
}
/* Filter Radio-Buttons */
.s-res-filter-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.s-res-filter-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
color: #4a5568;
}
.s-res-filter-option input[type="radio"] {
margin: 0;
accent-color: #3182ce;
}
/* --- HAUPTINHALT (ERGEBNISSE) --- */
.s-res-main-content {
min-width: 0;
}
.s-res-header {
margin-bottom: 2rem;
border-bottom: 2px solid #dee2e6;
padding-bottom: 1rem;
}
.s-res-main-title {
font-size: 2rem;
color: #1a202c;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.s-res-meta {
color: #6c757d;
font-size: 0.95rem;
margin: 0;
}
.s-res-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Einzelner Beitrag */
.s-res-item {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.s-res-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-color: #cbd5e1;
}
.s-res-content {
flex: 1;
}
.s-res-item-title {
font-size: 1.25rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
}
.s-res-link {
color: #3182ce;
text-decoration: none;
transition: color 0.2s ease;
}
.s-res-link:hover {
text-decoration: underline;
color: #2b6cb0;
}
.s-res-author {
font-size: 0.875rem;
color: #4a5568;
margin: 0;
}
.s-res-author-name {
font-weight: 600;
color: #2d3748;
}
.s-res-arrow {
font-size: 1.5rem;
color: #a0aec0;
padding-left: 1rem;
transition: color 0.2s ease, transform 0.2s ease;
}
.s-res-item:hover .s-res-arrow {
color: #3182ce;
transform: translateX(3px);
}
/* ERGEBNISSE PRO SEITE & PAGINIERUNG */
.s-res-pagination-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}
/* Dropdown-Auswahl */
.s-res-limit-selector {
display: flex;
align-items: center;
gap: 0.5rem;
}
.s-res-limit-label {
font-size: 0.9rem;
color: #4a5568;
}
.s-res-limit-select {
padding: 0.35rem 1.5rem 0.35rem 0.5rem;
font-size: 0.9rem;
color: #2d3748;
background-color: #ffffff;
border: 1px solid #cbd5e1;
border-radius: 4px;
cursor: pointer;
outline: none;
transition: border-color 0.2s;
/* Diskreter nativer Pfeil-Style */
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://w3.org' viewBox='0 0 24 24' fill='none' stroke='%234a5568' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.4rem center;
background-size: 1em;
}
.s-res-limit-select:focus {
border-color: #3182ce;
}
/* Seitennummern Navigation */
.s-res-page-navigation {
display: flex;
gap: 0.25rem;
}
.s-res-page-btn {
background-color: #ffffff;
border: 1px solid #cbd5e1;
color: #4a5568;
padding: 0.35rem 0.75rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.s-res-page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #cbd5e1;
color: #2d3748;
}
.s-res-page-btn-active {
background-color: #3182ce;
border-color: #3182ce;
color: #ffffff;
cursor: default;
}
.s-res-page-btn:disabled {
background-color: #f1f5f9;
border-color: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
.s-res-meta-row {
display: flex;
gap: 15px;
align-items: center;
}
.s-res-likes {
font-size: 0.9em;
color: #475569;
}
/* Responsive Anpassungen unter 760px (für z.B. Smartphones) */
@media (max-width: 768px) {
.s-res-layout-grid {
grid-template-columns: 1fr; /* Stapelt Seitenleiste und Inhalt untereinander */
gap: 1.5rem;
margin: 1rem auto;
}
.s-res-pagination-footer {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
+286
View File
@@ -0,0 +1,286 @@
/* Container für den gesamten articleikel */
.article-view-container {
box-sizing: border-box;
max-width: 900px; /* Angenehme Lesebreite für längere Texte */
margin: 3rem auto;
padding: 0 2rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
line-height: 1.6;
}
.article-view-container * {
box-sizing: border-box;
}
/* --- KOPFDATEN-BEREICH --- */
.article-view-top-section {
margin-bottom: 2.5rem;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 1.5rem;
}
/* Kategorie-Badge */
.article-view-category {
display: inline-block;
background-color: #ebf8ff;
color: #2b6cb0;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.25rem 0.75rem;
border-radius: 4px;
margin-bottom: 0.75rem;
}
/* Hauptüberschrift */
.article-view-title {
font-size: 2.5rem;
color: #1a202c;
line-height: 1.2;
margin: 0 0 0.75rem 0;
font-weight: 800;
}
/* Autoren-Zeile */
.article-view-meta {
font-size: 0.95rem;
color: #4a5568;
}
.article-view-author strong {
color: #2d3748;
}
/* --- INHALT --- */
.article-view-content {
margin-bottom: 3rem;
}
.article-view-body {
font-size: 1.125rem;
color: #2d3748;
white-space: pre-line;
}
/* --- TAG-BEREICH --- */
.article-view-bottom-section {
border-top: 1px solid #e2e8f0;
padding-top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.article-view-tags-label {
font-size: 0.9rem;
font-weight: 600;
color: #718096;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.article-view-tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Einzelner Tag */
.article-view-tag-item {
background-color: #f1f5f9;
color: #475569;
font-size: 0.85rem;
font-weight: 500;
padding: 0.35rem 0.75rem;
border-radius: 6px;
border: 1px solid #e2e8f0;
transition: background-color 0.2s, color 0.2s;
}
.article-view-tag-item:hover {
background-color: #e2e8f0;
color: #1e293b;
cursor: default;
}
.article-view-content {
margin-bottom: 3rem;
display: flex;
flex-direction: column;
gap: 1.5rem; /* Erzeugt einen sauberen Abstand zwischen den einzelnen Blöcken */
width: 100%;
}
.article-view-body {
font-size: 1.125rem;
color: #2d3748;
width: 100%;
}
.article-view-body.block-text {
white-space: pre-line;
word-break: break-word;
}
.article-view-body.block-image {
display: flex;
justify-content: center; /* Zentriert das Bild horizontal */
}
.article-view-body.block-image img {
max-width: 100%; /* Verhindert das Ausbrechen aus der Lesebreite */
height: auto; /* Behält das originale Seitenverhältnis bei */
border-radius: 6px; /* Optionale leichte Rundung für ein moderneres Layout */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.03); /* Minimaler, eleganter Schatten */
}
/* Responsive Anpassungen unter 760px (für z.B. Smarticlephones) */
@media (max-width: 760px) {
.article-view-container {
margin: 1.5rem auto;
padding: 0 1rem;
}
.article-view-title {
font-size: 1.85rem;
}
}
/* --- KOMMENTARE --- */
.article-comments-section {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid #e2e8f0;
}
.article-comments-section h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
}
#comments-list {
margin-bottom: 2rem;
}
.comment-item {
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
#comment-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
#comment-content {
width: 100%;
min-height: 130px;
padding: 1rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
}
#comment-form .button {
width: 100%;
}
.reply-button {
display: inline-block;
margin-top: 0.75rem;
background: none;
border: none;
color: #2563eb;
font-weight: 700;
cursor: pointer;
padding: 0;
font-size: 0.95rem;
}
.reply-button:hover {
text-decoration: underline;
}
.comment-replies {
margin-top: 1rem;
margin-left: 2rem;
padding-left: 1rem;
border-left: 3px solid #cbd5e1;
}
.comment-reply {
background-color: #eef6ff;
margin-top: 1rem;
}
.reply-info {
margin: 0.5rem 0;
color: #475569;
font-weight: 600;
}
.comment-login-hint {
margin-top: 2rem;
padding: 1.5rem;
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 10px;
text-align: center;
}
.comment-login-hint p {
margin-bottom: 1rem;
}
/*
Like-Button etc.
*/
.category-and-likes-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.article-view-likes {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.95em;
}
.article-view-likes .like-count {
font-weight: bold;
}
.article-view-likes .login-hint {
font-size: 0.8em;
color: #777;
}
/* Interaktiver Like/Unlike-Button */
.like-toggle-btn {
text-decoration: none;
padding: 4px 10px;
border: 1px solid #bbb;
border-radius: 4px;
background-color: #f5f5f5;
color: #333;
font-weight: 500;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.like-toggle-btn:hover {
background-color: #eaeaea;
border-color: #999;
}
+227
View File
@@ -0,0 +1,227 @@
/* --- ZWEISPALTIIGES GRID-LAYOUT --- */
.cat-view-container {
box-sizing: border-box;
max-width: 95%; /* 95% Monitorbreite wie das Original */
width: 95%;
margin: 2rem auto;
padding: 0 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: #212529;
line-height: 1.5;
/* Grid für die Zweispaltigkeit */
display: grid;
grid-template-columns: 320px 1fr; /* 320px Sidebar, Rest für Content */
gap: 2.5rem;
}
.cat-view-container * {
box-sizing: border-box;
}
/* --- SEITENLEISTE (SORTIERUNG) --- */
.cat-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.cat-sidebar-box {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
}
.cat-sidebar-title {
font-size: 1rem;
font-weight: 700;
color: #1a202c;
margin: 0 0 1rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.cat-filter-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.cat-filter-option {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
font-size: 0.95rem;
color: #4a5568;
}
.cat-filter-option input[type="radio"] {
margin: 0;
accent-color: #3182ce;
}
/* --- HAUPTINHALT --- */
.cat-view-main {
min-width: 0;
}
.cat-view-header {
margin-bottom: 2rem;
border-bottom: 2px solid #dee2e6;
padding-bottom: 1rem;
}
.cat-view-title {
font-size: 2rem;
color: #1a202c;
margin: 0 0 0.5rem 0;
font-weight: 700;
}
.cat-view-meta-text {
color: #6c757d;
font-size: 0.95rem;
margin: 0;
}
/* --- BEITRÄGE LISTE --- */
.cat-view-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.cat-view-item {
background-color: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.cat-view-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
border-color: #cbd5e1;
}
.cat-view-content {
flex: 1;
}
.cat-view-item-title {
font-size: 1.25rem;
margin: 0 0 0.25rem 0;
font-weight: 600;
}
.cat-view-link {
color: #3182ce;
text-decoration: none;
}
.cat-view-link:hover {
text-decoration: underline;
color: #2b6cb0;
}
.cat-view-meta-row {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 0.5rem;
}
.cat-view-author {
font-size: 0.875rem;
color: #4a5568;
margin: 0;
}
.cat-view-author-name {
font-weight: 600;
color: #2d3748;
}
.cat-view-likes {
font-size: 0.9em;
color: #475569;
}
.cat-view-arrow {
font-size: 1.5rem;
color: #a0aec0;
padding-left: 1rem;
transition: color 0.2s ease, transform 0.2s ease;
}
.cat-view-item:hover .cat-view-arrow {
color: #3182ce;
transform: translateX(3px);
}
/* --- FOOTER PAGINATION --- */
.cat-view-pagination-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid #e2e8f0;
}
.cat-view-pagination-footer .s-res-page-navigation {
display: flex;
gap: 0.25rem;
}
.cat-view-pagination-footer .s-res-page-btn {
background-color: #ffffff;
border: 1px solid #cbd5e1;
color: #4a5568;
padding: 0.35rem 0.75rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.cat-view-pagination-footer .s-res-page-btn:hover:not(:disabled) {
background-color: #f8f9fa;
border-color: #cbd5e1;
color: #2d3748;
}
.cat-view-pagination-footer .cat-active-btn {
background-color: #3182ce;
border-color: #3182ce;
color: #ffffff;
cursor: default;
}
.cat-view-pagination-footer .s-res-page-btn:disabled:not(.cat-active-btn) {
background-color: #f1f5f9;
border-color: #e2e8f0;
color: #94a3b8;
cursor: not-allowed;
}
/* RESPONSIVE ANPASSUNG (Unter 768px) */
@media (max-width: 768px) {
.cat-view-container {
grid-template-columns: 1fr; /* Stapelt Sidebar und Inhalt untereinander */
gap: 1.5rem;
margin: 1rem auto;
}
.cat-view-pagination-footer {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
}
View File
+1
View File
@@ -0,0 +1 @@
[]
-47
View File
@@ -1,47 +0,0 @@
<!--
Seite: Datenschutzerklärung
Inhalt: Informationen zur Datenverarbeitung gemäß DSGVO
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Datenschutz</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
<!--
Artikel: Datenschutz vs Datensicherheit
Funktion: Inhalt zum Fach Informatik
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Datenschutz vs Datensicherheit</h1>
<p>
Datenschutz ist in unserer digital vernetzten Welt allgegenwärtig ob in sozialen Netzwerken, beim
Online-Shopping oder dem Einsatz von Bonuskarten, bei der Auswertung von Konsumverhalten und selbstverständlich
auch im schulischen Kontext.
</p>
<h2>
Was ist Datenschutz?
</h2>
<p>
Datenschutz bedeutet, dass persönliche Daten nur gesammelt, gespeichert und verwendet werden dürfen, wenn
es wirklich nötig ist und nur für klar festgelegte Zwecke. Es geht darum, die Privatsphäre von Menschen
zu schützen.
</p>
<h2>
Was ist Datenschutz?
</h2>
<p>
Datensicherheit umfasst alle technischen und organisatorischen Maßnahmen, die verhindern sollen, dass Daten
verloren gehen, beschädigt oder unbemerkt verändert werden. Sie schützt Informationen vor Missbrauch.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
<FilesMatch "\.db">
deny from all
</FilesMatch>
-47
View File
@@ -1,47 +0,0 @@
<!--
Seite: Impressum
Inhalt: wer wie was auf der Webseite
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Impressum</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "internal_error"): ?>
<p class="alert-message is-error">
Es ist ein interner Fehler beim Speichern aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "missing_parameters"): ?>
<p class="alert-message is-error">
Jeder Beitrag muss einen Titel, Kategorie und Inhalt besitzen.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_title"): ?>
<p class="alert-message is-error">
Der Titel enthält ungültige Zeichen oder erfüllt die Länge von 5-120 Zeichen nicht.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_content"): ?>
<p class="alert-message is-error">
Ein Beitrag muss Inhalt besitzen. Text- und Bildelemente dürfen nicht leer sein!
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_tags"): ?>
<p class="alert-message is-error">
Ungültige Schlagworte gefunden. Erlaubt sind nur Buchstaben, Zahlen, Leerzeichen und Bindestriche (2-50 Zeichen).
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "validation_missing"): ?>
<p class="alert-message is-error">
Bei der Validierung deiner Daten ist ein Fehler aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_category"): ?>
<p class="alert-message is-error">
Diese Kategorie ist nicht gültig.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "missing_id"): ?>
<p class="alert-message is-error">
Die Artikel-ID fehlt oder ist ungültig.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "not_found_article"): ?>
<p class="alert-message is-error">
Der angeforderte Beitrag konnte nicht gefunden werden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "unauthorized_access"): ?>
<p class="alert-message is-error">
Du hast keine Berechtigung, diesen Beitrag zu bearbeiten oder zu löschen.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "article_deleted"): ?>
<p class="alert-message is-success">
Der Beitrag wurde erfolgreich gelöscht.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "new_article"): ?>
<p class="alert-message is-success">
Dein Beitrag wurde erfolgreich veröffentlicht!
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "article_updated"): ?>
<p class="alert-message is-success">
Der Beitrag wurde erfolgreich bearbeitet und gespeichert.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "profile_updated"): ?>
<p class="alert-message is-success">
Das Profil wurde erfolgreich bearbeitet.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "image_upload_error"): ?>
<p class="alert-message is-error">
Das Bild konnte nicht hochgeladen werden. Bitte versuche es erneut oder verwende ein anderes Bildformat.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "registration_confirmed"): ?>
<p class="alert-message is-success">
Ihre Registrierung wurde erfolgreich abgeschlossen. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "password_changed"): ?>
<p class="alert-message is-success">
Ihr Passwort wurde erfolgreich geändert. Sie können sich jetzt anmelden.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "database_error"): ?>
<p class="alert-message is-error">
Es ist ein Datenbankfehler aufgetreten. Bitte versuche es erneut.
</p>
<?php endif; ?>
<?php if (isset($_SESSION["message"]) && $_SESSION["message"] == "invalid_csrf_token"): ?>
<p class="alert-message is-error">
Deine Sitzung ist abgelaufen oder die Anfrage konnte nicht überprüft werden.
Bitte lade die Seite neu und versuche es erneut.
</p>
<?php endif; ?>
<?php
unset($_SESSION["message"]);
?>
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* Baut die Blockliste aus den POST-Daten (blocks[i][type], blocks[i][text],
* blocks[i][existing_image]) und ggf. hochgeladenen Dateien (blocks[i][image])
* zusammen. Läuft bei JEDEM Submit (Zwischen-Schritt "Block hinzufügen/löschen"
* UND finales Speichern/Veröffentlichen), damit neu ausgewählte Bilder in jedem
* Fall persistiert werden, bevor PHP die temporäre Upload-Datei nach
* Request-Ende verwirft.
*
* Wird sowohl vom createArticle- als auch vom updateArticle-Controller genutzt.
*
* @param array $postBlocks $_POST['blocks'] ?? []
* @param array $fileBlocks $_FILES['blocks'] ?? []
* @param string $uploadDir absoluter Pfad zum uploads-Verzeichnis (mit trailing slash)
* @return array Liste von ['type' => 'text'|'image', 'value' => string]
*/
function rebuildBlocksFromPost(array $postBlocks, array $fileBlocks, string $uploadDir): array {
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$keys = array_keys($postBlocks);
if (isset($fileBlocks['name']) && is_array($fileBlocks['name'])) {
$keys = array_unique(array_merge($keys, array_keys($fileBlocks['name'])));
}
sort($keys, SORT_NUMERIC);
$blocks = [];
foreach ($keys as $key) {
$type = $postBlocks[$key]['type'] ?? null;
if ($type === 'text') {
$blocks[] = [
'type' => 'text',
'value' => $postBlocks[$key]['text'] ?? '',
];
} elseif ($type === 'image') {
// Vorbelegung: bereits vorhandenes Server-Bild (falls Datei nicht ersetzt wird)
$value = $postBlocks[$key]['existing_image'] ?? '';
$hasUpload = isset($fileBlocks['error'][$key]['image'])
&& $fileBlocks['error'][$key]['image'] === UPLOAD_ERR_OK;
if ($hasUpload) {
$tmpName = $fileBlocks['tmp_name'][$key]['image'];
$originalName = $fileBlocks['name'][$key]['image'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
$extension = 'jpg';
}
$fileName = 'img_' . uniqid() . '.' . $extension;
$destination = $uploadDir . $fileName;
if (move_uploaded_file($tmpName, $destination)) {
$value = 'uploads/' . $fileName;
}
// Bei Fehler: alter Wert (falls vorhanden) bleibt erhalten, Block wird nicht verworfen
}
$blocks[] = [
'type' => 'image',
'value' => $value,
];
}
// unbekannter/fehlender type -> Block wird ignoriert
}
return $blocks;
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* CSRF-Schutz nach dem Synchronizer-Token-Pattern.
*
* Pro Session wird ein einziges, zufälliges Token erzeugt,
* das in jedem Formular als verstecktes Feld mitgeschickt und bei jeder
* zustandsändernden Anfrage serverseitig mit dem Session-Token verglichen
* wird.
*
* @author Niklas Ortmann
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Gibt das aktuelle CSRF-Token der Session zurück.
*
* Existiert noch kein Token, wird eines erzeugt und in der Session
* gespeichert.
*
* @return string Das CSRF-Token
*/
function csrf_token(): string
{
if (empty($_SESSION["csrf_token"]) || !is_string($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
return $_SESSION["csrf_token"];
}
/**
* Gibt ein verstecktes Formularfeld mit dem aktuellen CSRF-Token aus.
*
* Wird in jedem Formular benötigt, das eine zustandsändernde
* Aktion auslöst.
*
* @return void
*/
function csrf_field(): void
{
echo '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars(csrf_token())
. '">';
}
/**
* Prüft, ob das per POST gesendete CSRF-Token zum Session-Token passt.
*
* Der Vergleich erfolgt zeitkonstant über hash_equals(), um
* Timing-Angriffe auf den Vergleich selbst auszuschließen.
*
* @return bool true, wenn das Token gültig ist
*/
function csrf_verify(): bool
{
$sentToken = $_POST["csrf_token"] ?? "";
$sessionToken = $_SESSION["csrf_token"] ?? "";
if (!is_string($sentToken) || $sentToken === "" || $sessionToken === "") {
return false;
}
return hash_equals($sessionToken, $sentToken);
}
/**
* Bricht die Anfrage ab und leitet mit einer Fehlermeldung um,
* wenn das mitgesendete CSRF-Token ungültig oder nicht vorhanden ist.
*
* Muss am Anfang jeder zustandsändernden POST-Aktion aufgerufen werden,
* bevor irgendeine Änderung an Daten vorgenommen wird.
*
* @param string $redirectTo Ziel-URL, zu der bei ungültigem Token
* weitergeleitet wird
* @return void
*/
function csrf_require_valid(string $redirectTo = "index.php"): void
{
if (!csrf_verify()) {
http_response_code(403);
$_SESSION["message"] = "invalid_csrf_token";
header("Location: " . $redirectTo);
exit();
}
}
+4 -3
View File
@@ -1,5 +1,6 @@
<footer>
<a href="impressum.php">Impressum</a>
<a href="datenschutz.php">Datenschutz</a>
<a href="nutzungsbedingungen.php">Nutzungsbedingungen</a>
<a href="index.php?pfad=impressum">Impressum</a>
<a href="index.php?pfad=datenschutz">Datenschutz</a>
<a href="index.php?pfad=nutzungsbedingungen">Nutzungsbedingungen</a>
<a href="index.php?pfad=accessibility">Barrierefreiheit</a>
</footer>
+117 -67
View File
@@ -1,70 +1,90 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
?>
<!--
Globales Menü, wird via PHP später in alle Seiten eingebunden
-->
<nav class="nav">
<div class="nav__left">
<a href="index.php" class="nav__logo">
<img src="images/logos/logo_full.png" alt="Logo">
</a>
<img src="images/logos/logo_full.png" alt="Logo">
</a>
</div>
<!-- Mobile navbar mit Burger-Symbol -->
<input type="checkbox" id="nav-toggle" class="nav__checkbox">
<label for="nav-toggle" id="mobile-nav">
<span></span> <!-- Burger Icon Symbol -->
<span>☰</span>
</label>
<ul id="mobile-nav" class="nav__mobile-menu">
<li><a href="profile.php">Profil</a></li>
<li><a href="login.php">Anmelden</a></li>
<li><a href="register.php">Registrieren</a></li>
<li>
<label for="nav-toggle" class="nav__close-btn">&times;</label>
</li>
<?php if (isset($_SESSION['user'])): ?>
<li><a href="index.php?pfad=profile">Profil</a></li>
<li><a href="index.php?pfad=logout">Abmelden</a></li>
<li><a href="index.php?pfad=createArticle">Beitrag erstellen</a></li>
<?php else: ?>
<li><a href="index.php?pfad=login">Anmelden</a></li>
<li><a href="index.php?pfad=register">Registrieren</a></li>
<?php endif; ?>
<li>
<label class="nav__mobile-label">Sprachen</label>
<ul class="nav__mobile-submenu">
<li><a href="404.php">Deutsch</a></li>
<li><a href="404.php">Englisch</a></li>
<li><a href="404.php">Französisch</a></li>
<li><a href="404.php">Latein</a></li>
<li><a href="404.php">Literatur</a></li>
<li><a href="index.php?pfad=showCategory&category=deutsch">Deutsch</a></li>
<li><a href="index.php?pfad=showCategory&category=englisch">Englisch</a></li>
<li><a href="index.php?pfad=showCategory&category=franzoesisch">Französisch</a></li>
<li><a href="index.php?pfad=showCategory&category=latein">Latein</a></li>
<li><a href="index.php?pfad=showCategory&category=literatur">Literatur</a></li>
</ul>
</li>
<li>
<label class="nav__mobile-label">MINT</label>
<ul class="nav__mobile-submenu">
<li><a href="mathe.php">Mathematik</a></li>
<li><a href="404.php">Biologie</a></li>
<li><a href="404.php">Chemie</a></li>
<li><a href="physik.php">Physik</a></li>
<li><a href="informatik.php">Informatik</a></li>
<li><a href="404.php">Astronomie</a></li>
<li><a href="index.php?pfad=showCategory&category=mathe">Mathematik</a></li>
<li><a href="index.php?pfad=showCategory&category=biologie">Biologie</a></li>
<li><a href="index.php?pfad=showCategory&category=chemie">Chemie</a></li>
<li><a href="index.php?pfad=showCategory&category=physik">Physik</a></li>
<li><a href="index.php?pfad=showCategory&category=informatik">Informatik</a></li>
<li><a href="index.php?pfad=showCategory&category=astronomie">Astronomie</a></li>
</ul>
</li>
<li>
<label class="nav__mobile-label">Gesellschaft & Werte</label>
<ul class="nav__mobile-submenu">
<li><a href="404.php">Geschichte</a></li>
<li><a href="404.php">Erdkunde</a></li>
<li><a href="404.php">Sozialkunde</a></li>
<li><a href="404.php">Wirtschaftskunde</a></li>
<li><a href="404.php">Religion</a></li>
<li><a href="404.php">Ethikunterricht</a></li>
<li><a href="404.php">Philosophie</a></li>
<li><a href="404.php">Psychologie</a></li>
<li><a href="404.php">Kunst</a></li>
<li><a href="404.php">Musik</a></li>
<li><a href="404.php">Theater</a></li>
<li><a href="index.php?pfad=showCategory&category=geschichte">Geschichte</a></li>
<li><a href="index.php?pfad=showCategory&category=erdkunde">Erdkunde</a></li>
<li><a href="index.php?pfad=showCategory&category=sozialkunde">Sozialkunde</a></li>
<li><a href="index.php?pfad=showCategory&category=wirtschaft">Wirtschaftskunde</a></li>
<li><a href="index.php?pfad=showCategory&category=religion">Religion</a></li>
<li><a href="index.php?pfad=showCategory&category=ethik">Ethikunterricht</a></li>
<li><a href="index.php?pfad=showCategory&category=philosophie">Philosophie</a></li>
<li><a href="index.php?pfad=showCategory&category=psychologie">Psychologie</a></li>
<li><a href="index.php?pfad=showCategory&category=kunst">Kunst</a></li>
<li><a href="index.php?pfad=showCategory&category=musik">Musik</a></li>
<li><a href="index.php?pfad=showCategory&category=theater">Theater</a></li>
</ul>
</li>
<li>
<label class="nav__mobile-label">Technik & Praxis</label>
<ul class="nav__mobile-submenu">
<li><a href="404.php">Technik</a></li>
<li><a href="404.php">Werken</a></li>
<li><a href="404.php">Hauswirtschaft</a></li>
<li><a href="404.php">Sport</a></li>
<li><a href="index.php?pfad=showCategory&category=technik">Technik</a></li>
<li><a href="index.php?pfad=showCategory&category=werken">Werken</a></li>
<li><a href="index.php?pfad=showCategory&category=hauswirtschaft">Hauswirtschaft</a></li>
<li><a href="index.php?pfad=showCategory&category=sport">Sport</a></li>
</ul>
</li>
</ul>
@@ -74,59 +94,89 @@ Globales Menü, wird via PHP später in alle Seiten eingebunden
<div class="nav__item nav__dropdown">
<button class="nav__dropdown-toggle">Sprachen</button>
<div class="nav__dropdown-menu">
<a href="404.php">Deutsch</a>
<a href="404.php">Englisch</a>
<a href="404.php">Französisch</a>
<a href="404.php">Latein</a>
<a href="404.php">Literatur</a>
<a href="index.php?pfad=showCategory&category=deutsch">Deutsch</a>
<a href="index.php?pfad=showCategory&category=englisch">Englisch</a>
<a href="index.php?pfad=showCategory&category=franzoesisch">Französisch</a>
<a href="index.php?pfad=showCategory&category=latein">Latein</a>
<a href="index.php?pfad=showCategory&category=literatur">Literatur</a>
</div>
</div>
<div class="nav__item nav__dropdown">
<button class="nav__dropdown-toggle">MINT</button>
<div class="nav__dropdown-menu">
<a href="mathe.php">Mathematik</a>
<a href="404.php">Biologie</a>
<a href="404.php">Chemie</a>
<a href="physik.php">Physik</a>
<a href="informatik.php">Informatik</a>
<a href="404.php">Astronomie</a>
<a href="index.php?pfad=showCategory&category=mathe">Mathematik</a>
<a href="index.php?pfad=showCategory&category=biologie">Biologie</a>
<a href="index.php?pfad=showCategory&category=chemie">Chemie</a>
<a href="index.php?pfad=showCategory&category=physik">Physik</a>
<a href="index.php?pfad=showCategory&category=informatik">Informatik</a>
<a href="index.php?pfad=showCategory&category=astronomie">Astronomie</a>
</div>
</div>
<div class="nav__item nav__dropdown">
<button class="nav__dropdown-toggle">Gesellschaft & Werte</button>
<div class="nav__dropdown-menu">
<a href="404.php">Geschichte</a>
<a href="404.php">Erdkunde</a>
<a href="404.php">Sozialkunde</a>
<a href="404.php">Wirtschaftskunde</a>
<a href="404.php">Religion</a>
<a href="404.php">Ethikunterricht</a>
<a href="404.php">Philosophie</a>
<a href="404.php">Psychologie</a>
<a href="404.php">Kunst</a>
<a href="404.php">Musik</a>
<a href="404.php">Theater</a>
<a href="index.php?pfad=showCategory&category=geschichte">Geschichte</a>
<a href="index.php?pfad=showCategory&category=erdkunde">Erdkunde</a>
<a href="index.php?pfad=showCategory&category=sozialkunde">Sozialkunde</a>
<a href="index.php?pfad=showCategory&category=wirtschaft">Wirtschaftskunde</a>
<a href="index.php?pfad=showCategory&category=religion">Religion</a>
<a href="index.php?pfad=showCategory&category=ethik">Ethikunterricht</a>
<a href="index.php?pfad=showCategory&category=philosophie">Philosophie</a>
<a href="index.php?pfad=showCategory&category=psychologie">Psychologie</a>
<a href="index.php?pfad=showCategory&category=kunst">Kunst</a>
<a href="index.php?pfad=showCategory&category=musik">Musik</a>
<a href="index.php?pfad=showCategory&category=theater">Theater</a>
</div>
</div>
<div class="nav__item nav__dropdown">
<button class="nav__dropdown-toggle">Technik & Praxis</button>
<div class="nav__dropdown-menu">
<a href="404.php">Technik</a>
<a href="404.php">Werken</a>
<a href="404.php">Hauswirtschaft</a>
<a href="404.php">Sport</a>
<a href="index.php?pfad=showCategory&category=technik">Technik</a>
<a href="index.php?pfad=showCategory&category=werken">Werken</a>
<a href="index.php?pfad=showCategory&category=hauswirtschaft">Hauswirtschaft</a>
<a href="index.php?pfad=showCategory&category=sport">Sport</a>
</div>
</div>
</div>
<div class="nav__right">
<a href = "profile.php" class="nav_item nav__link">Profil</a>
<div class="nav__search">
<?php
include_once 'search.php';
?>
</div>
<a href = "login.php" class="nav__item nav__button">Anmelden</a>
<a href = "register.php" class="nav__item nav__button">Registrieren</a>
<?php if (isset($_SESSION['user'])): ?>
<a href="index.php?pfad=profile" class="nav__item nav__link">
Profil
</a>
<div class="nav__search">
<?php include_once 'search.php'; ?>
</div>
<a href="index.php?pfad=createArticle" class="nav__item nav__button">
Beitrag erstellen
</a>
<a href="index.php?pfad=logout" class="nav__item nav__button">
Abmelden
</a>
<?php else: ?>
<div class="nav__search">
<?php include_once 'search.php'; ?>
</div>
<a href="index.php?pfad=login" class="nav__item nav__button">
Anmelden
</a>
<a href="index.php?pfad=register" class="nav__item nav__button">
Registrieren
</a>
<?php endif; ?>
</div>
</nav>
</nav>
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* Hilfsfunktionen rund um die Suchergebnisse.
*
* Werden sowohl von content/search-results.php (No-JS-Rendering)
* als auch von php/controller/search-results-data.php (JSON für
* die JS-Hydration) genutzt, damit Sortier-/Filterlogik nur an
* einer Stelle gepflegt werden muss
*/
function getLikeCount($item): int
{
if (isset($item['likes']) && is_array($item['likes'])) {
return count($item['likes']);
}
return (int)($item['likes'] ?? 0);
}
/**
* Sortiert die Ergebnisse
*/
function sortSearchResults(array $results, string $sortStyle): array
{
switch ($sortStyle) {
case 'likes':
usort($results, function ($a, $b) {
return getLikeCount($b) <=> getLikeCount($a);
});
break;
case 'newest':
usort($results, function ($a, $b) {
return strcmp($b['creationDate'] ?? '', $a['creationDate'] ?? '');
});
break;
case 'oldest':
usort($results, function ($a, $b) {
return strcmp($a['creationDate'] ?? '', $b['creationDate'] ?? '');
});
break;
case 'alphabet':
default:
usort($results, function ($a, $b) {
return strcasecmp($a['title'] ?? '', $b['title'] ?? '');
});
break;
}
return $results;
}
/**
* Filtert die Ergebnisse ('all' = kein Filter).
*/
function filterSearchResultsByCategory(array $results, string $category): array
{
$category = strtolower(trim($category));
if ($category === '' || $category === 'all') {
return $results;
}
return array_values(array_filter($results, function ($item) use ($category) {
return strtolower($item['category'] ?? '') === $category;
}));
}
/**
* Baut eine Such-Ergebnis-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
*/
function buildSearchResultsUrl(int $page, string $query, string $sort, string $category, int $limit): string
{
return "index.php?pfad=search-results"
. "&q=" . urlencode($query)
. "&sort=" . urlencode($sort)
. "&category=" . urlencode($category)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für den No-JS-Fall:
*/
function renderNoJsPagination(int $currentPage, int $totalPages, string $query, string $sort, string $category, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage - 1, $query, $sort, $category, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($i, $query, $sort, $category, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildSearchResultsUrl($currentPage + 1, $query, $sort, $category, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
/**
* Filtert Ergebnisse anhand eines Suchbegriffs
*
* Wird von content/showCategory.php genutzt, um innerhalb einer bereits
* geladenen Kategorie zu suchen.
*/
function filterResultsByQuery(array $results, string $query): array
{
$query = trim($query);
if ($query === '') {
return $results;
}
$needle = mb_strtolower($query);
return array_values(array_filter($results, function ($item) use ($needle) {
$haystack = mb_strtolower(($item['title'] ?? '') . ' ' . ($item['content'] ?? ''));
return mb_strpos($haystack, $needle) !== false;
}));
}
/**
* Baut eine Kategorie-URL für einen bestimmten Seitenwechsel (No-JS-Pagination).
* Analog zu buildSearchResultsUrl, aber für pfad=showCategory (feste Kategorie,
* dafür mit Freitext-Suche innerhalb der Kategorie statt Kategorie-Filter).
*/
function buildCategoryUrl(int $page, string $category, string $sort, string $query, int $limit): string
{
return "index.php?pfad=showCategory"
. "&category=" . urlencode($category)
. "&sort=" . urlencode($sort)
. "&q=" . urlencode($query)
. "&limit=" . $limit
. "&page=" . $page;
}
/**
* Rendert eine rein serverseitige Pagination für showCategory (No-JS-Fall).
* Analog zu renderNoJsPagination.
*/
function renderNoJsPaginationCategory(int $currentPage, int $totalPages, string $category, string $sort, string $query, int $limit): string
{
$html = '';
if ($currentPage > 1) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage - 1, $category, $sort, $query, $limit)) . '">&laquo;</a> ';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&laquo;</span> ';
}
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
$html .= '<span class="s-res-page-btn s-res-page-btn-active">' . $i . '</span> ';
} else {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($i, $category, $sort, $query, $limit)) . '">' . $i . '</a> ';
}
}
if ($currentPage < $totalPages) {
$html .= '<a class="s-res-page-btn" href="' . htmlspecialchars(buildCategoryUrl($currentPage + 1, $category, $sort, $query, $limit)) . '">&raquo;</a>';
} else {
$html .= '<span class="s-res-page-btn" aria-disabled="true">&raquo;</span>';
}
return $html;
}
+7 -2
View File
@@ -2,5 +2,10 @@
Suchleiste. Wird via PHP später in alle Seiten eingebunden
-->
<!--<label for="site-search">Suche</label>-->
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search">
<button type="submit" class="nav__search-button">Suchen</button>
<form action="php/controller/search-results-controller.php" method="GET" class="nav__search-form">
<input type="hidden" name="pfad" value="search-results">
<input type="search" id="site-search" name="q" placeholder="Suchen..." class="nav__search" maxlength="50" required>
<button type="submit" class="nav__search-button">Suchen</button>
</form>
+35 -17
View File
@@ -1,34 +1,52 @@
<!--
Seite: Hompage der Lernplattform
Funktion: Webseitengerüst, Anzeigen von Beiträgen, Suchfunktion etc.
-->
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
include_once "includes/csrf.php";
include_once "php/controller/index-controller.php";
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<link rel="stylesheet" href="css/search-results.css">
<link rel="stylesheet" href="css/createArticle.css">
<link rel="stylesheet" href="css/profile.css">
<link rel="stylesheet" href="css/showArticle.css">
<link rel="stylesheet" href="css/message.css">
<link rel="stylesheet" href="css/showCategory.css">
<script src="js/comments.js" defer></script>
<script src="js/editor.js" async></script>
<script src="js/search-results.js" async></script>
<script src="js/showCategory.js" async></script>
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
//Dynamischer Inhalt (später mit php...):
include_once 'content/home.php';
?>
<?php
include_once 'includes/navbar.php';
include_once "includes/alertMessages.php";
// Dynamischer Inhalt
if (isset($pfad) && $pfad !== "404" && file_exists('content/' . $pfad . '.php')) {
include_once 'content/' . $pfad . '.php';
} else {
include_once 'content/404.php';
}
include_once 'includes/footer.php';
?>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
</html>
-48
View File
@@ -1,48 +0,0 @@
<!--
Seite: Informatik
Inhalt: Eine Übersicht über alle Beiträge zum Fach
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Informatik</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<h2>Artikel:</h2>
<div>
<a href="datenschutzVSdatensicherheit.php">Datenschutz vs. Datensicherheit</a>
</div>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+310
View File
@@ -0,0 +1,310 @@
/**
* Initialisiert die Kommentarfunktion.
*
* Kommentare werden per AJAX gespeichert,
* ohne dass die Seite neu geladen werden muss.
*/
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("comment-form");
const commentsList = document.getElementById("comments-list");
const commentContent = document.getElementById("comment-content");
const parentCommentInput = document.getElementById("parent-comment-id");
const replyInfo = document.getElementById("reply-info");
if (!form || !commentsList || !commentContent || !parentCommentInput) {
return;
}
/**
* Aktiviert einen einzelnen Antworten-Link.
*
* @param {HTMLAnchorElement} replyLink Antworten-Link
*/
function registerReplyButton(replyLink) {
replyLink.addEventListener("click", function (event) {
/*
* Mit JavaScript wird die Seite nicht neu geladen.
* Ohne JavaScript funktioniert der normale Link.
*/
event.preventDefault();
parentCommentInput.value = replyLink.dataset.commentId;
if (replyInfo) {
replyInfo.textContent =
"Antwort auf " + replyLink.dataset.author;
replyInfo.style.display = "block";
}
commentContent.focus();
});
}
/**
* Registriert alle bereits vorhandenen Antwort-Buttons.
*/
document.querySelectorAll(".reply-button").forEach(function (button) {
registerReplyButton(button);
});
/**
* Sendet Kommentare per AJAX an den Server.
*/
form.addEventListener("submit", function (event) {
event.preventDefault();
const formData = new FormData(form);
const parentCommentId = parentCommentInput.value;
fetch(form.action, {
method: "POST",
body: formData,
headers: {
"X-Requested-With": "XMLHttpRequest"
}
})
.then(function (response) {
if (!response.ok) {
throw new Error("Fehlerhafte Serverantwort.");
}
return response.json();
})
.then(function (data) {
if (!data.success) {
alert(data.message);
return;
}
const emptyMessage = commentsList.querySelector(
".no-comments-message"
);
if (emptyMessage) {
emptyMessage.remove();
}
const commentElement = document.createElement("div");
commentElement.classList.add("comment-item");
commentElement.dataset.commentId = data.commentId;
if (parentCommentId !== "") {
commentElement.classList.add("comment-reply");
commentElement.innerHTML = `
<p>
<strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span>
</p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Antwort bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
`;
const parentReplies = commentsList.querySelector(
`.comment-item[data-comment-id="${parentCommentId}"] .comment-replies`
);
if (parentReplies) {
parentReplies.appendChild(commentElement);
} else {
commentsList.prepend(commentElement);
}
} else {
commentElement.innerHTML = `
<p>
<strong>${escapeHtml(data.author)}</strong>
<span>${escapeHtml(data.created)}</span>
</p>
<p>${escapeHtml(data.content).replace(/\n/g, "<br>")}</p>
<details class="edit-comment-details">
<summary class="edit-comment-button">
Kommentar bearbeiten
</summary>
<form
method="post"
action="index.php?pfad=updateComment"
class="edit-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<textarea
name="content"
required
>${escapeHtml(data.content)}</textarea>
<button type="submit" class="button">
Änderungen speichern
</button>
</form>
</details>
<form
method="post"
action="index.php?pfad=deleteComment"
class="delete-comment-form"
>
<input
type="hidden"
name="csrf_token"
value="${escapeHtml(formData.get("csrf_token"))}"
>
<input
type="hidden"
name="comment_id"
value="${escapeHtml(data.commentId)}"
>
<input
type="hidden"
name="article_id"
value="${escapeHtml(formData.get("article_id"))}"
>
<button
type="submit"
class="delete-comment-button"
onclick="return confirm('Möchtest du diesen Kommentar wirklich löschen?');"
>
Kommentar löschen
</button>
</form>
<a
href="#comment-form"
class="reply-button"
data-comment-id="${escapeHtml(data.commentId)}"
data-author="${escapeHtml(data.author)}"
>
Antworten
</a>
<div class="comment-replies"></div>
`;
commentsList.prepend(commentElement);
const newReplyButton =
commentElement.querySelector(".reply-button");
if (newReplyButton) {
registerReplyButton(newReplyButton);
}
}
commentContent.value = "";
parentCommentInput.value = "";
if (replyInfo) {
replyInfo.textContent = "";
replyInfo.style.display = "none";
}
})
.catch(function (error) {
console.error(error);
alert("Kommentar konnte nicht gesendet werden.");
});
});
/**
* Entfernt HTML-Sonderzeichen aus Nutzereingaben.
*
* @param {*} text Zu bereinigender Text
* @returns {string} Sicherer Text
*/
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = String(text ?? "");
return div.innerHTML;
}
});
+140
View File
@@ -0,0 +1,140 @@
//console.log("editor.js wurde erfolgreich geladen!");
function initEditor() {
const form = document.getElementById("editor-form");
if (!form) {
console.error("editor.js abgebrochen: Formular nicht gefunden!");
return;
} else {
console.log("Formular gefunden und editor.js initialisiert:", form);
}
const container = document.getElementById("block-container");
const plusButton = document.getElementById("plus-button");
const popup = document.getElementById("block-popup");
// Fortlaufender Zähler für eindeutige Block-Indizes. Wird nie wiederverwendet
// (auch nicht nach dem Löschen eines Blocks), damit sich neue und übrig
// gebliebene Blöcke beim finalen Submit nie einen Namen teilen.
let blockIndex = container.querySelectorAll(".editor-block").length;
// Ohne JS sind Textblock-/Bild-Button im Pop-up immer sichtbar und ganz normale
// Submit-Buttons. Erst mit JS blenden wir das Pop-up standardmäßig aus und
// steuern die Sichtbarkeit über den Plus-Button.
popup.classList.add("hidden");
plusButton.addEventListener("click", () => {
popup.classList.toggle("hidden");
});
// Klick auf "Textblock" / "Bild einfügen": lokal im DOM anlegen statt zum
// Server zu submitten (entlastet den Server, kein Page-Reload nötig).
popup.querySelectorAll('[name="editor_action"]').forEach(btn => {
btn.addEventListener("click", function (e) {
e.preventDefault();
const type = this.getAttribute("data-type");
addBlockElement(type, "");
popup.classList.add("hidden");
});
});
// Bereits vom Server gerenderte Blöcke (z.B. beim Bearbeiten eines bestehenden
// Artikels oder nach einem Validierungsfehler) ebenfalls mit JS-Verhalten ausstatten.
container.querySelectorAll(".editor-block").forEach(blockDiv => {
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
});
// Erstellt einen neuen Block inkl. echter, benannter Formularfelder
// (blocks[i][type], blocks[i][text] bzw. blocks[i][image]). Diese Felder werden
// beim finalen Submit ganz normal vom Browser als multipart/form-data verschickt
// es ist kein manuelles Zusammenbauen von JSON beim Absenden mehr nötig.
function addBlockElement(type, value = "") {
const index = blockIndex++;
const blockDiv = document.createElement("div");
blockDiv.classList.add("editor-block", "article-editor-scope");
blockDiv.setAttribute("data-index", String(index));
const typeInput = document.createElement("input");
typeInput.type = "hidden";
typeInput.name = `blocks[${index}][type]`;
typeInput.value = type;
blockDiv.appendChild(typeInput);
if (type === "text") {
const textarea = document.createElement("textarea");
textarea.name = `blocks[${index}][text]`;
textarea.placeholder = "Schreibe deinen Textblock...";
textarea.value = value;
blockDiv.appendChild(textarea);
} else if (type === "image") {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.name = `blocks[${index}][image]`;
fileInput.accept = "image/*";
blockDiv.appendChild(fileInput);
}
const deleteBtn = document.createElement("button");
deleteBtn.type = "submit";
deleteBtn.name = "editor_action";
deleteBtn.value = `delete_block:${index}`;
deleteBtn.classList.add("delete-block-btn");
deleteBtn.innerHTML = "✕";
blockDiv.appendChild(deleteBtn);
container.appendChild(blockDiv);
bindDeleteButton(blockDiv);
bindImageInput(blockDiv);
}
// Löschen-Button eines Blocks lokal abfangen: entfernt den Block nur aus dem DOM,
// statt das Formular zum Server zu senden.
function bindDeleteButton(blockDiv) {
const btn = blockDiv.querySelector(".delete-block-btn");
if (!btn) return;
btn.addEventListener("click", (e) => {
e.preventDefault();
blockDiv.remove();
});
}
// Zeigt bei Auswahl einer Bilddatei sofort eine Vorschau an. Rein optisch
// der eigentliche Datei-Upload läuft nativ über das <input type="file">.
function bindImageInput(blockDiv) {
const fileInput = blockDiv.querySelector('input[type="file"]');
if (!fileInput) return;
let imgPreview = blockDiv.querySelector(".block-image-preview");
if (!imgPreview) {
imgPreview = document.createElement("img");
imgPreview.classList.add("block-image-preview");
imgPreview.style.maxWidth = "200px";
imgPreview.style.display = "none";
imgPreview.style.marginTop = "10px";
blockDiv.insertBefore(imgPreview, fileInput.nextSibling);
}
fileInput.addEventListener("change", function () {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = (e) => {
imgPreview.src = e.target.result;
imgPreview.style.display = "block";
};
reader.readAsDataURL(this.files[0]);
}
});
}
}
// SICHERER START: Prüft, ob das HTML bereits bereit ist
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initEditor);
} else {
// Falls das DOM schon fertig geladen ist, führen wir es direkt aus
initEditor();
}
+238
View File
@@ -0,0 +1,238 @@
/**
* Übernimmt Sortierung, Kategorie-Filter und Pagination der Suchergebnisse
* vollständig clientseitig
*
* Ablauf:
* 1. Kompletten Ergebnis-Datensatz einmalig per fetch() nachladen
* (php/controller/search-results-data.php, liest nur die Session-Daten,
* keine erneute Datenbank-Suche).
* 2. Sortieren + filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ersetzt sorter.js, filter.js und paginator.js, deren Klick-Handler sich
* gegenseitig ins Gehege kamen.
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig.
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
category: 'all',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, categorySelect, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
categorySelect = document.getElementById('category-filter');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.category = categorySelect ? categorySelect.value : 'all';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Suchergebnisse konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/search-results-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
if (state.category !== 'all') {
const wanted = state.category.toLowerCase();
items = items.filter(function (item) {
return (item.category || '').toLowerCase() === wanted;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (categorySelect) {
categorySelect.addEventListener('change', function () {
state.category = this.value;
state.currentPage = 1;
render();
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Filter live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('s-res-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+242
View File
@@ -0,0 +1,242 @@
/**
* Übernimmt Sortierung, Suche (innerhalb der Kategorie) und Pagination der
* Kategorie-Ansicht vollständig clientseitig.
*
* Ablauf:
* 1. Kompletten Kategorie-Datensatz einmalig per fetch() nachladen
* 2. Sortieren + nach Suchbegriff filtern (JS-Array).
* 3. Ergebnisliste + Pagination-UI daraus neu rendern.
*
* Ohne JS bleibt die serverseitig gerenderte Seite (echte Links/Formulare)
* voll funktionsfähig
*/
(function () {
const state = {
allItems: [],
sort: 'alphabet',
query: '',
itemsPerPage: 10,
currentPage: 1,
};
let listContainer, sortRadios, searchInput, limitSelect;
let prevBtn, nextBtn, numbersContainer, jsNav, resultCountEl;
let searchDebounceTimer;
function init() {
listContainer = document.querySelector('.s-res-list');
if (!listContainer) return;
sortRadios = document.querySelectorAll('.sort-radio');
searchInput = document.getElementById('cat-search-input');
limitSelect = document.getElementById('s-res-per-page');
prevBtn = document.getElementById('prev-page-btn');
nextBtn = document.getElementById('next-page-btn');
numbersContainer = document.getElementById('dynamic-page-numbers');
jsNav = document.getElementById('js-page-navigation');
resultCountEl = document.getElementById('s-res-result-count');
const checkedRadio = document.querySelector('.sort-radio:checked');
state.sort = checkedRadio ? checkedRadio.value : 'alphabet';
state.query = searchInput ? searchInput.value : '';
state.itemsPerPage = limitSelect ? (parseInt(limitSelect.value, 10) || 10) : 10;
fetchFullDataset()
.then(function (data) {
state.allItems = data.results || [];
attachEvents();
render();
if (jsNav) jsNav.style.display = 'flex';
})
.catch(function (err) {
// JSON-Endpoint nicht erreichbar: die serverseitig gerenderte
// (No-JS-)Ansicht bleibt sichtbar und funktioniert weiter.
console.error('Kategorie-Beiträge konnten nicht nachgeladen werden:', err);
});
}
function fetchFullDataset() {
return fetch('php/controller/showCategory-data.php', { credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}
function getFilteredSortedItems() {
let items = state.allItems;
const needle = state.query.trim().toLowerCase();
if (needle !== '') {
items = items.filter(function (item) {
const haystack = ((item.title || '') + ' ' + (item.content || '')).toLowerCase();
return haystack.indexOf(needle) !== -1;
});
}
items = items.slice().sort(function (a, b) {
if (state.sort === 'likes') {
return (b.likes || 0) - (a.likes || 0);
}
if (state.sort === 'newest' || state.sort === 'oldest') {
return state.sort === 'newest' ? (b.id - a.id) : (a.id - b.id);
}
// alphabet (Standard)
return (a.title || '').toLowerCase().localeCompare((b.title || '').toLowerCase());
});
return items;
}
function render() {
const items = getFilteredSortedItems();
if (resultCountEl) resultCountEl.textContent = items.length;
const totalPages = Math.max(1, Math.ceil(items.length / state.itemsPerPage));
if (state.currentPage > totalPages) state.currentPage = totalPages;
if (state.currentPage < 1) state.currentPage = 1;
const start = (state.currentPage - 1) * state.itemsPerPage;
const pageItems = items.slice(start, start + state.itemsPerPage);
listContainer.innerHTML = '';
if (pageItems.length === 0) {
const empty = document.createElement('p');
empty.textContent = 'Keine Beiträge gefunden.';
listContainer.appendChild(empty);
} else {
pageItems.forEach(function (item) {
listContainer.appendChild(buildCard(item));
});
}
updatePaginatorUI(state.currentPage, totalPages);
}
function buildCard(item) {
const card = document.createElement('div');
card.className = 's-res-item';
card.setAttribute('data-likes', item.likes);
card.setAttribute('data-category', (item.category || '').toLowerCase());
const link = document.createElement('a');
link.className = 's-res-link';
link.href = 'index.php?pfad=showArticle&id=' + encodeURIComponent(item.id);
link.textContent = item.title;
const title = document.createElement('h2');
title.className = 's-res-item-title';
title.appendChild(link);
const authorName = document.createElement('span');
authorName.className = 's-res-author-name';
authorName.textContent = item.author;
const author = document.createElement('p');
author.className = 's-res-author';
author.append('Von: ', authorName);
const likes = document.createElement('span');
likes.className = 's-res-likes';
likes.textContent = '❤️ ' + item.likes;
const metaRow = document.createElement('div');
metaRow.className = 's-res-meta-row';
metaRow.append(author, likes);
const content = document.createElement('div');
content.className = 's-res-content';
content.append(title, metaRow);
const arrow = document.createElement('div');
arrow.className = 's-res-arrow';
arrow.textContent = '→';
card.append(content, arrow);
return card;
}
function updatePaginatorUI(currentPage, totalPages) {
if (!prevBtn || !nextBtn || !numbersContainer) return;
prevBtn.disabled = currentPage <= 1;
nextBtn.disabled = currentPage >= totalPages;
numbersContainer.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 's-res-page-btn' + (i === currentPage ? ' s-res-page-btn-active' : '');
btn.textContent = i;
btn.addEventListener('click', function () {
state.currentPage = i;
render();
});
numbersContainer.appendChild(btn);
}
}
function attachEvents() {
sortRadios.forEach(function (radio) {
radio.addEventListener('change', function () {
state.sort = this.value;
state.currentPage = 1;
render();
});
});
if (searchInput) {
// Live-Filterung während des Tippens (leicht entprellt), da der
// Datensatz bereits vollständig im Browser liegt.
searchInput.addEventListener('input', function () {
clearTimeout(searchDebounceTimer);
const value = this.value;
searchDebounceTimer = setTimeout(function () {
state.query = value;
state.currentPage = 1;
render();
}, 150);
});
}
if (limitSelect) {
limitSelect.addEventListener('change', function () {
state.itemsPerPage = parseInt(this.value, 10) || 10;
state.currentPage = 1;
render();
});
}
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (state.currentPage > 1) {
state.currentPage -= 1;
render();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
state.currentPage += 1;
render();
});
}
// Mit aktivem JS übernimmt render() Sortierung/Suche live
// ein echtes Absenden des Formulars (No-JS-Fallback) ist dann nicht mehr nötig.
const filterForm = document.getElementById('cat-filter-form');
if (filterForm) {
filterForm.addEventListener('submit', function (e) {
e.preventDefault();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
-51
View File
@@ -1,51 +0,0 @@
<!--
Form: Login-Bereich
Funktion: Benutzerauthentifizierung und Zugang zum eigenen Profil, Erstellen von Beiträgen, etc.
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<div style="width: 50%">
<h1 style="width: 60%; text-align: center">Bitte anmelden</h1><br>
<label id="inputEmail" >E-Mail-Adresse / </label>
<p style="width: 80%;">Benutzername/E-Mail-Adresse:</p>
<input type="email" name="email" style="width: 80%;" placeholder="E-Mail-Adresse" required autofocus>
<p style="width: 80%;">Passwort:</p>
<input type="password" name="password" style="width: 80%;" placeholder="Passwort" required>
<div style="width: 80%;">
<label>
<input type="checkbox" name="remember-me" value="1"> angemeldet bleiben
</label>
</div>
<button type="submit" value="anmelden" name="loginSubmit" style="width: 80%;">anmelden</button><br>
<div style="width: 70%; text-align: center">
<a href="register.php" style="text-align: center;"> Noch keinen Account? Jetzt hier registrieren! </a>
</div>
</div>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
-50
View File
@@ -1,50 +0,0 @@
<!--
Seite: Mathe
Inhalt: Eine Übersicht über alle Beiträge zum Fach
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Mathe</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<h2>Artikel:</h2>
<div>
<a href="pythagoras.php">Satz des Pythagoras</a>
</div>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
-46
View File
@@ -1,46 +0,0 @@
<!--
Seite: Nutzungsbedingungen
Inhalt: Regelungen für die Nutzung der Plattform (AGBs und so...)
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Nutzungsbedingungen</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
+184
View File
@@ -0,0 +1,184 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once "../model/CommentManager.php";
/**
* Prüft, ob die Anfrage durch JavaScript per AJAX gesendet wurde.
*/
$isAjaxRequest = isset($_SERVER["HTTP_X_REQUESTED_WITH"])
&& strtolower($_SERVER["HTTP_X_REQUESTED_WITH"]) === "xmlhttprequest";
/**
* Gibt das Ergebnis entweder als JSON zurück oder leitet
* bei einem normalen Formularaufruf wieder zum Beitrag zurück.
*
* @param bool $success War das Speichern erfolgreich?
* @param string $message Rückmeldung für den Benutzer
* @param int|null $articleId ID des Beitrags
* @param array $additionalData Zusätzliche Daten für AJAX
*/
function sendCommentResponse(
$success,
$message,
$articleId,
$additionalData = []
) {
global $isAjaxRequest;
if ($isAjaxRequest) {
header("Content-Type: application/json; charset=utf-8");
echo json_encode(
array_merge(
[
"success" => $success,
"message" => $message
],
$additionalData
)
);
exit();
}
/*
* Bei deaktiviertem JavaScript wird die Rückmeldung
* in der Session gespeichert und die Beitragsseite neu geladen.
*/
$_SESSION["comment_message"] = $message;
$_SESSION["comment_message_type"] = $success ? "success" : "error";
if ($articleId !== null) {
header(
"Location: ../../index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: ../../index.php");
}
exit();
}
/*
* Nur POST-Anfragen dürfen Kommentare erstellen.
*/
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
sendCommentResponse(
false,
"Ungültige Anfrage.",
null
);
}
/*
* Die Beitrags-ID wird zuerst eingelesen,
* damit bei Fehlern wieder zum Beitrag zurückgeleitet werden kann.
*/
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Ein Benutzer muss angemeldet sein.
*/
if (!isset($_SESSION["user_email"])) {
sendCommentResponse(
false,
"Du musst angemeldet sein, um zu kommentieren.",
$articleId !== false ? $articleId : null
);
}
/*
* Weitere Formulardaten einlesen.
*/
$content = trim($_POST["content"] ?? "");
$parentCommentId = filter_input(
INPUT_POST,
"parent_comment_id",
FILTER_VALIDATE_INT
);
/*
* Ein leerer Wert bedeutet, dass es sich um einen
* normalen Hauptkommentar handelt.
*/
if (
!isset($_POST["parent_comment_id"])
|| $_POST["parent_comment_id"] === ""
|| $_POST["parent_comment_id"] === "0"
) {
$parentCommentId = null;
}
if ($articleId === false || $articleId === null) {
sendCommentResponse(
false,
"Der zugehörige Beitrag ist ungültig.",
null
);
}
if ($content === "") {
sendCommentResponse(
false,
"Der Kommentar darf nicht leer sein.",
$articleId
);
}
/*
* Eine ungültige Eltern-ID darf nicht gespeichert werden.
*/
if (
isset($_POST["parent_comment_id"])
&& $_POST["parent_comment_id"] !== ""
&& $_POST["parent_comment_id"] !== "0"
&& $parentCommentId === false
) {
sendCommentResponse(
false,
"Der ausgewählte Kommentar ist ungültig.",
$articleId
);
}
try {
$commentManager = CommentManager::getInstance();
$commentId = $commentManager->addComment(
$articleId,
$_SESSION["user_email"],
$content,
$parentCommentId
);
sendCommentResponse(
true,
"Der Kommentar wurde erfolgreich gespeichert.",
$articleId,
[
"commentId" => $commentId,
"author" => $_SESSION["user_email"],
"content" => $content,
"created" => date("Y-m-d H:i:s"),
"parentCommentId" => $parentCommentId
]
);
} catch (Throwable $e) {
sendCommentResponse(
false,
"Der Kommentar konnte nicht gespeichert werden.",
$articleId
);
}
@@ -0,0 +1,65 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Passwortbestätigung
Funktion: Liest die zwischengespeicherten Passwortdaten aus,
aktualisiert das Passwort des Benutzers und gibt das Ergebnis
für die View zurück.
*/
$title = "Passwort zurücksetzen";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending-password/" . $token . ".json";
if (empty($token) || !file_exists($file)) {
$message = "Der Bestätigungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if ($data === null || empty($data["email"]) || empty($data["password"])) {
$message = "Die Daten zur Passwortänderung konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
$user = $dao->findUser($data["email"]);
if ($user !== null) {
$dao->updateUser(
$user["email"],
$user["email"],
$user["vorname"],
$user["nachname"],
$data["password"]
);
$_SESSION["message"] = "password_changed";
header("Location: index.php?pfad=login");
exit();
} else {
$message = "Der Benutzer konnte nicht gefunden werden.";
}
unlink($file);
} catch (Exception $e) {
$message = "Das Passwort konnte nicht geändert werden.";
}
}
}
@@ -0,0 +1,64 @@
<?php
require_once "php/model/UserManager.php";
/*
Verarbeitung der Registrierungsbestätigung
Funktion: Liest die zwischengespeicherten Registrierungsdaten aus,
legt den Benutzer an und gibt das Ergebnis für die View zurück.
*/
$title = "Registrierung";
$message = "";
$link = "";
$isSuccess = false;
$token = basename($_GET["token"] ?? "");
$file = "data/pending/" . $token . ".json";
if (empty($token) || !file_exists($file)) {
$message = "Der Registrierungslink ist ungültig oder bereits abgelaufen.";
} else {
$data = json_decode(file_get_contents($file), true);
if (
$data === null ||
empty($data["email"]) ||
empty($data["vorname"]) ||
empty($data["nachname"]) ||
empty($data["password"])
) {
$message = "Die Registrierungsdaten konnten nicht gelesen werden.";
} else {
try {
$dao = UserManager::getInstance();
if ($dao->findUser($data["email"]) === null) {
$dao->addUser(
$data["email"],
$data["vorname"],
$data["nachname"],
$data["password"]
);
}
unlink($file);
$_SESSION["message"] = "registration_confirmed";
header("Location: index.php?pfad=login");
exit();
} catch (Exception $e) {
$message = "Die Registrierung konnte nicht abgeschlossen werden.";
}
}
}
+124
View File
@@ -0,0 +1,124 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php';
require_once '../validator/article-validator.php';
require_once '../../includes/article-block-helper.php';
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ---------------------------------------------------------------------
// Zwischenspeichern
// ---------------------------------------------------------------------
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
$action = $_POST['editor_action'];
if ($action === 'add_text') {
$blocks[] = ['type' => 'text', 'value' => ''];
} elseif ($action === 'add_image') {
$blocks[] = ['type' => 'image', 'value' => ''];
} elseif (str_starts_with($action, 'delete_block:')) {
$deleteIndex = (int) substr($action, strlen('delete_block:'));
unset($blocks[$deleteIndex]);
$blocks = array_values($blocks);
}
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
header("location: ../../index.php?pfad=createArticle");
exit();
}
// ---------------------------------------------------------------------
// Echtes Veröffentlichen
// ---------------------------------------------------------------------
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
$_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=createArticle");
exit();
} else {
$title = $_POST["title"];
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
$author = $_SESSION["user_email"];
$category = $_POST["category"];
$tags = $_POST['tags'] ?? '';
// -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) {
$_SESSION["message"] = "invalid_title";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleContentValidator($content)) {
$_SESSION["message"] = "invalid_content";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleCategoryValidator($category)) {
$_SESSION["message"] = "invalid_category";
header("location: ../../index.php?pfad=createArticle");
exit();
}
if (!articleTagValidator($tags)) {
$_SESSION["message"] = "invalid_tags";
header("location: ../../index.php?pfad=createArticle");
exit();
} else {
$cleanedTags = [];
$rawTags = explode(',', $tags);
foreach ($rawTags as $rawTag) {
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tag = trim($rawTag);
$cleanedTags[] = $tag;
}
// Duplikate entfernen:
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try {
$articleManager = ArticleManager::getInstance();
// $content enthält bereits die finalen "uploads/..."-Pfade (kein Base64 mehr),
// da rebuildBlocksFromPost() Datei-Uploads sofort verarbeitet.
$articleManager->addArticle($title, $content, $author, $category, $cleanedTags);
// Formulardaten nach erfolgreichem Erstellen aus der Session löschen
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
} catch (\Throwable $e) {
$_SESSION["message"] = "internal_error";
header("location: ../../index.php?pfad=createArticle");
exit();
}
$_SESSION["message"] = "new_article";
// Weiterleitung zur Homepage
header("location: ../../index.php");
exit();
}
}
?>
@@ -0,0 +1,48 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/UserManager.php";
require_once __DIR__ . "/../model/ArticleManager.php";
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
/*
Deregistrierung
Funktion: Entfernt User aus der Datenbank und beendet die Session
*/
try {
$dao = UserManager::getInstance();
$articleManager = ArticleManager::getInstance();
if (isset($_SESSION["user_email"])) {
$dao->deleteUser($_SESSION["user_email"]);
$articles = $articleManager->getArticlesByAuthor($_SESSION["user_email"]);
foreach ($articles as $article) {
$articleManager->deleteArticle($article->getId(), $_SESSION["user_email"]);
}
} else {
throw new NotFoundException("missing_user_email");
}
$_SESSION = [];
session_destroy();
header("Location: ../../index.php");
exit();
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
header("Location: ../../index.php?pfad=profile");
exit();
}
@@ -0,0 +1,47 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/ArticleManager.php";
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (isset($_SESSION["user_email"])) {
$user = $_SESSION["user_email"];
} else {
$_SESSION = [];
session_destroy();
header("Location: ../../index.php");
exit();
}
if (isset($_POST["id"]) && !empty($_POST["id"])) {
$id = $_POST["id"];
} else {
$_SESSION["message"] = "missing_id";
header("location: ../../index.php?pfad=profile");
exit();
}
try {
$articleManager = ArticleManager::getInstance();
$articleManager->deleteArticle($id, $user);
} catch (\Exception $e) {
$_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=profile");
exit();
}
$_SESSION["message"] = "article_deleted";
header("location: ../../index.php?pfad=profile");
exit();
}
+105
View File
@@ -0,0 +1,105 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . "/../model/CommentManager.php";
/*
* Kommentare dürfen nur über ein POST-Formular gelöscht werden.
*/
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
/*
* Nur angemeldete Nutzer dürfen Kommentare löschen.
*/
if (!isset($_SESSION["user_email"])) {
$_SESSION["comment_message"] = "Du musst angemeldet sein.";
$_SESSION["comment_message_type"] = "error";
if ($articleId !== false && $articleId !== null) {
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
} else {
header("Location: index.php");
}
exit();
}
/*
* Kommentar-ID und Beitrags-ID müssen gültige Zahlen sein.
*/
if (
$commentId === false
|| $commentId === null
|| $articleId === false
|| $articleId === null
) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
header("Location: index.php");
exit();
}
try {
$commentManager = CommentManager::getInstance();
/*
* Die E-Mail-Adresse aus der Session wird mitgegeben.
* Dadurch kann der Nutzer nur eigene Kommentare löschen.
*/
$deleted = $commentManager->deleteComment(
$commentId,
$_SESSION["user_email"]
);
if ($deleted) {
$_SESSION["comment_message"] =
"Der Kommentar wurde gelöscht.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar wurde nicht gefunden oder gehört nicht dir.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht gelöscht werden.";
$_SESSION["comment_message_type"] = "error";
}
/*
* Anschließend wird wieder zum Beitrag und zu den Kommentaren geleitet.
*/
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+49
View File
@@ -0,0 +1,49 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php';
require_once 'php/model/LocalArticleManager.php';
$categoriesWithArticles = [];
$allowedCategories = [
'deutsch', 'englisch', 'franzoesisch', 'latein', 'literatur',
'mathe', 'biologie', 'chemie', 'physik', 'informatik', 'astronomie',
'geschichte', 'erdkunde', 'sozialkunde', 'wirtschaft', 'religion',
'ethik', 'philosophie', 'psychologie', 'kunst', 'musik', 'theater',
'technik', 'werken', 'hauswirtschaft', 'sport'
];
try {
$articleManager = ArticleManager::getInstance();
foreach ($allowedCategories as $categorySlug) {
$allCategoryArticles = $articleManager->getArticlesByCategory($categorySlug);
// nur Kategorien, die Beiträge enthält:
if (!empty($allCategoryArticles) && is_array($allCategoryArticles)) {
// Beiträge nach Erstellungsdatum sortieren:
usort($allCategoryArticles, function($a, $b) {
$dateA = strtotime($a->getCreationDate());
$dateB = strtotime($b->getCreationDate());
return $dateB <=> $dateA; // Absteigende Sortierung
});
// auf die 5 zuletzt hinzugefügten Beiträge begrenzen:
$limitedArticles = array_slice($allCategoryArticles, 0, 5);
$categoriesWithArticles[] = [
'slug' => $categorySlug,
'name' => ucfirst($categorySlug), // Erster Buchstabe groß für die Ansicht
'articles' => $limitedArticles
];
}
}
} catch (Exception $e){
$_SESSION["message"] = "internal_error";
}
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
// Standardpfad
$pfad = $_GET["pfad"] ?? "home";
if ($pfad === "logout") {
include_once "php/controller/logout-controller.php";
exit();
} elseif ($pfad === "deleteAccount") {
include_once "php/controller/deleteAccount-controller.php";
exit();
}
if ($pfad === "login") {
include_once "php/controller/login-controller.php";
} elseif ($pfad === "register") {
include_once "php/controller/register-controller.php";
} elseif ($pfad === "password-forgotten") {
include_once "php/controller/password-forgotten-controller.php";
} elseif ($pfad === "confirm-register") {
include_once "php/controller/confirm-register-controller.php";
} elseif ($pfad === "confirm-password") {
include_once "php/controller/confirm-password-controller.php";
} elseif ($pfad === "updateComment") {
include_once "php/controller/updateComment-controller.php";
} elseif($pfad === "deleteComment") {
include_once "php/controller/deleteComment-controller.php";
}
// Whitelist
$erlaubte_content_seiten = [
"accessibility", "confirm-password", "confirm-register", "createArticle",
"datenschutz", "home", "impressum", "login", "nutzungsbedingungen",
"password-forgotten", "profile", "register", "search-results",
"show-mail", "showArticle", "showCategory", "updateArticle"
];
if (!in_array($pfad, $erlaubte_content_seiten)) {
$pfad = "404";
}
+42
View File
@@ -0,0 +1,42 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../model/Article.php';
require_once __DIR__ . '/../model/ArticleManager.php';
// 2. Prüfen, ob eine gültige Artikel-ID übergeben wurde
if (isset($_GET["id"]) && !empty($_GET["id"])) {
$articleId = intval($_GET["id"]);
$userEmail = $_SESSION["user_email"];
if (!isset($_SESSION["user_email"]) || empty($_SESSION["user_email"])) {
$_SESSION["message"] = "unauthorized_access";
header("Location: ../../index.php?pfad=showArticle&id=" . $articleId);
exit();
}
try {
$articleManager = ArticleManager::getInstance();
$articleManager->toggleLike($articleId, $userEmail);
header("Location: ../../index.php?pfad=showArticle&id=" . $articleId);
exit();
} catch (NotFoundException $e) {
$_SESSION["message"] = "missing_id";
header("Location: ../../index.php");
exit();
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
header("Location: ../../index.php");
exit();
}
} else {
$_SESSION["message"] = "missing_id";
header("Location: ../../index.php");
exit();
}
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
require_once "php/model/UserManager.php";
$error = null;
/*
Verarbeitung des Login-Formulars
Funktion: Prüft Benutzerdaten und erstellt Session für eingeloggten Nutzer
*/
if ($_SERVER["REQUEST_METHOD"] === "POST") {
try {
$dao = UserManager::getInstance();
$email = $_POST["email"] ?? "";
$password = $_POST["password"] ?? "";
$user = $dao->findUser($email);
if ($user && password_verify($password, $user["password"])) {
$_SESSION["user"] = $user["vorname"] . " " . $user["nachname"];
$_SESSION["user_email"] = $user["email"];
header("Location: index.php");
exit();
} else {
$error = "Login fehlgeschlagen. Bitte überprüfe deine Eingaben.";
}
} catch (Exception $e) {
$error = "Es ist ein interner Fehler aufgetreten. Bitte versuche es erneut.";
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION = [];
session_destroy();
header("Location: ../../index.php");
exit();
@@ -0,0 +1,76 @@
<?php
require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php";
$error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$email = trim($_POST["email"] ?? "");
$plainPassword = $_POST["password"] ?? "";
if (!userEmailValidator($email)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
} elseif (!userPasswordValidator($plainPassword)) {
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
} else {
try {
$dao = UserManager::getInstance();
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
if (!is_dir("data/pending-password") && !mkdir("data/pending-password", 0777, true)) {
throw new RuntimeException("Ordner data/pending-password konnte nicht erstellt werden.");
}
if (!is_writable("data/mails") || !is_writable("data/pending-password")) {
throw new RuntimeException("Ordner sind nicht beschreibbar.");
}
if ($existingUser !== null) {
$pendingData = [
"email" => $email,
"password" => $plainPassword
];
file_put_contents(
"data/pending-password/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
}
if ($existingUser !== null) {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>
<a href='index.php?pfad=confirm-password&token=$token'>
Passwortänderung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Passwort zurücksetzen</h2>
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.</p>
<p>Für diese E-Mail-Adresse wurde kein Konto gefunden.</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (Exception $e) {
$error = "Die Passwortänderung konnte nicht verarbeitet werden.";
}
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
require_once "php/model/UserManager.php";
require_once "php/model/Article.php";
require_once "php/model/ArticleManager.php";
require_once "php/model/CommentManager.php";
require_once "php/validator/user-validator.php";
$error = null;
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
try {
$dao = UserManager::getInstance();
$user = $dao->findUser($_SESSION["user_email"] ?? "");
if (!$user) {
$_SESSION = [];
session_destroy();
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["saveProfile"])) {
$oldEmail = $_SESSION["user_email"];
$newEmail = trim($_POST["email"] ?? "");
$vorname = trim($_POST["vorname"] ?? "");
$nachname = trim($_POST["nachname"] ?? "");
$password = $_POST["password"] ?? "";
if (!userEmailValidator($newEmail)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
$_GET["edit"] = "1";
} elseif (!userNameValidator($vorname)) {
$error = "Der Vorname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Leerzeichen und Bindestriche enthalten.";
$_GET["edit"] = "1";
} elseif (!userNameValidator($nachname)) {
$error = "Der Nachname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Leerzeichen und Bindestriche enthalten.";
$_GET["edit"] = "1";
} elseif (!userOptionalPasswordValidator($password)) {
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
$_GET["edit"] = "1";
} else {
$updated = $dao->updateUser(
$oldEmail,
$newEmail,
$vorname,
$nachname,
$password
);
if ($updated) {
$_SESSION["user"] = $vorname . " " . $nachname;
$_SESSION["user_email"] = $newEmail;
$_SESSION["message"] = "profile_updated";
header("Location: index.php?pfad=profile");
exit();
} else {
$error = "Die Daten konnten nicht gespeichert werden.";
$_GET["edit"] = "1";
}
}
}
$user = $dao->findUser($_SESSION["user_email"] ?? "");
$articleManager = ArticleManager::getInstance();
$userArticles = $articleManager->getArticlesByAuthor($_SESSION["user_email"]);
$commentManager = CommentManager::getInstance();
$userComments = $commentManager->getCommentsByAuthor($_SESSION["user_email"]);
if (!isset($userArticles)) {
$_SESSION["message"] = "user_has_no_articles";
}
} catch (InvalidArgumentException $e) {
$error = $e->getMessage();
$_GET["edit"] = "1";
} catch (Exception $e) {
$error = "Es ist ein interner Fehler aufgetreten. Bitte versuche es erneut.";
$_SESSION["message"] = "internal_error";
$_GET["edit"] = "1";
}
@@ -0,0 +1,24 @@
<?php
/*
* Controller für die Liste der eigenen Beiträge eines Nutzers auf der eigenen Profilseite
*/
require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php';
require_once "php/model/UserManager.php";
try {
$dao = UserManager::getInstance();
$user = $dao->findUser($_SESSION["user_email"] ?? "");
$author = $user["email"];
$articleManager = ArticleManager::getInstance();
$userArticles = $articleManager->getArticlesByAuthor($author);
if(!isset($userArticles)) {
$_SESSION["message"] = "user_has_no_articles";
}
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
}
?>
+95
View File
@@ -0,0 +1,95 @@
<?php
require_once "php/model/UserManager.php";
require_once "php/validator/user-validator.php";
$error = null;
$success = null;
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$email = trim($_POST["email"] ?? "");
$vorname = trim($_POST["vorname"] ?? "");
$nachname = trim($_POST["nachname"] ?? "");
$plainPassword = $_POST["password"] ?? "";
if (!userEmailValidator($email)) {
$error = "Bitte gib eine gültige E-Mail-Adresse ein.";
} elseif (!userNameValidator($vorname)) {
$error = "Der Vorname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Umlaute, Leerzeichen und Bindestriche enthalten.";
} elseif (!userNameValidator($nachname)) {
$error = "Der Nachname muss 2 bis 20 Zeichen lang sein und darf nur Buchstaben, Umlaute, Leerzeichen und Bindestriche enthalten.";
} elseif (!userPasswordValidator($plainPassword)) {
$error = "Das Passwort muss 5 bis 12 Zeichen lang sein.";
} else {
try {
$dao = UserManager::getInstance();
// Token für die simulierte E-Mail und die spätere Bestätigung erzeugen.
$token = bin2hex(random_bytes(16));
$existingUser = $dao->findUser($email);
if (!is_dir("data/mails") && !mkdir("data/mails", 0777, true)) {
throw new RuntimeException("Ordner data/mails konnte nicht erstellt werden.");
}
if (!is_dir("data/pending") && !mkdir("data/pending", 0777, true)) {
throw new RuntimeException("Ordner data/pending konnte nicht erstellt werden.");
}
if (!is_writable("data/mails")) {
throw new RuntimeException("Ordner data/mails ist nicht beschreibbar.");
}
if (!is_writable("data/pending")) {
throw new RuntimeException("Ordner data/pending ist nicht beschreibbar.");
}
if ($existingUser === null) {
$password = password_hash($plainPassword, PASSWORD_DEFAULT);
$pendingData = [
"email" => $email,
"vorname" => $vorname,
"nachname" => $nachname,
"password" => $password
];
file_put_contents(
"data/pending/" . $token . ".json",
json_encode($pendingData, JSON_PRETTY_PRINT)
);
$mailContent = "
<h2>Registrierung bestätigen</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>
<a href='index.php?pfad=confirm-register&token=$token'>
Registrierung bestätigen
</a>
</p>
";
} else {
$mailContent = "
<h2>Registrierung</h2>
<p>Bitte ignorieren Sie diese Nachricht, wenn Sie sich nicht registrieren wollten.</p>
<p>Sie sind bereits registriert.</p>
<p>
<a href='index.php?pfad=password-forgotten'>
Passwort vergessen
</a>
</p>
";
}
file_put_contents("data/mails/" . $token . ".html", $mailContent);
// Neutrale Meldung, damit nicht sichtbar wird, ob die E-Mail bereits registriert ist.
$success = 'Weitere Infos finden Sie in der Datei
<a href="index.php?pfad=show-mail&token=' . htmlspecialchars($token) . '" target="_blank">xy</a>.';
} catch (Exception $e) {
$error = "Die Registrierung konnte nicht verarbeitet werden.";
}
}
}
@@ -0,0 +1,65 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once '../model/ArticleManager.php';
require_once '../model/UserManager.php';
require_once '../model/Article.php';
require_once '../validator/search-validator.php';
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["q"])) {
$search = trim($_GET["q"]);
if (!searchQueryValidator($search)) {
$_SESSION["search_results"] = [];
$_SESSION["search_query"] = "";
$_SESSION["message"] = "invalid_search_query";
} else {
try {
$articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance();
$results = $articleManager->search($search);
$safeArrayResults = [];
foreach ($results as $obj) {
$safeArrayResults[] = [
"id" => $obj->getId(),
"title" => $obj->getTitle(),
"content" => $obj->getContent(),
"author" => $userManager->findUser($obj->getAuthor())["vorname"] . " " . $userManager->findUser($obj->getAuthor())["nachname"],
"category" => $obj->getCategory(),
"tags" => $obj->getTags(),
"creationDate" => $obj->getCreationDate(),
"likes" => $obj->getLikes(),
];
}
$_SESSION["search_results"] = $safeArrayResults;
$_SESSION["search_query"] = $search;
$_SESSION["message"] = "new_search_results";
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
}
}
$sort = $_GET['sort'] ?? 'alphabet';
$_SESSION['search_sort'] = $sort;
$category = $_GET['category'] ?? 'all';
$_SESSION['search_category'] = $category;
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
if (!searchLimitValidator($limit)) {
$limit = 10;
}
$_SESSION['search_limit'] = $limit;
// Neue Suche -> immer wieder bei Seite 1 beginnen
header("Location: ../../index.php?pfad=search-results&q=" . urlencode($search) . "&sort=" . urlencode($sort) . "&category=" . urlencode($category) . "&limit=" . $limit . "&page=1");
exit();
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* Liefert den vollständigen, zuletzt gefundenen Ergebnis-Datensatz als JSON.
*
* Wird von js/search-results.js EINMALIG per fetch() geladen, damit Sortierung,
* Filterung und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Suche auszulösen.
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['search_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'query' => $_SESSION['search_query'] ?? '',
'results' => array_values($payload),
]);
+60
View File
@@ -0,0 +1,60 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php';
require_once 'php/model/CommentManager.php';
if (isset($_GET["id"]) && !empty($_GET["id"])){
try {
$id = $_GET["id"];
$articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id);
if($article != null){
$title = $article->getTitle();
$content = $article->getContent();
$category = $article->getCategory();
$author = $article->getAuthor();
$tags = $article->getTags();
$articleObj = $article; // Objekt für die Like-Abfagen sichern
$userManager = UserManager::getInstance();
$name = $userManager->findUser($author)["vorname"]." ".$userManager->findUser($author)["nachname"];
}else{
//header("location: index.php?pfad=404");
include_once "content/404.php";
exit();
}
if($_GET["pfad"] == "updateArticle"){
if($article->getAuthor() != $_SESSION["user_email"]){
$_SESSION["message"] = "unauthorized_access";
header("location: index.php");
exit();
}
}
$commentManager = CommentManager::getInstance();
$comments = $commentManager->getCommentsByArticle($_GET["id"]);
foreach ($comments as $comment) {
if ($comment->isReply()) {
$parentId = $comment->getParentCommentId();
$repliesByParent[$parentId][] = $comment;
} else {
$mainComments[] = $comment;
}
}
} catch (Throwable $e) {
$_SESSION["message"] = "internal_error";
header("Location: index.php");
exit();
}
}else{
$_SESSION["message"] = "missing_id";
}
?>
@@ -0,0 +1,52 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once 'php/model/Article.php';
require_once 'php/model/ArticleManager.php';
require_once 'php/model/UserManager.php';
require_once 'php/validator/article-validator.php';
require_once __DIR__ . '/../../includes/resultsHelper.php';
if (isset($_GET["category"]) && !empty($_GET["category"]) && articleCategoryValidator($_GET["category"])) {
$category = $_GET["category"];
try {
$articleManager = ArticleManager::getInstance();
$userManager = UserManager::getInstance();
$allArticles = $articleManager->getArticlesByCategory($category);
$safeArrayResults = [];
foreach ($allArticles as $article) {
$authorName = $userManager->findUser($article->getAuthor())["vorname"] . " " . $userManager->findUser($article->getAuthor())["nachname"];
$safeArrayResults[] = [
"id" => $article->getID(),
"title" => $article->getTitle(),
"content" => $article->getContent(),
"author" => $authorName,
"category" => $article->getCategory(),
"tags" => $article->getTags(),
"creationDate" => $article->getCreationDate(),
"likes" => $article->getLikes(),
];
}
// Session dient hier (wie bei search-results) als Zwischenspeicher, damit
// js/showCategory.js den kompletten Datensatz per showCategory-data.php
// nachladen kann, ohne die Kategorie-Abfrage ein zweites Mal auszuführen.
$_SESSION['category_results'] = $safeArrayResults;
$_SESSION['category_name'] = $category;
} catch (Exception $e) {
$_SESSION["message"] = "internal_error";
include_once "content/404.php";
exit();
}
} else {
$_SESSION["message"] = "invalid_category";
include_once "content/404.php";
exit();
}
+35
View File
@@ -0,0 +1,35 @@
<?php
/**
* Liefert den vollständigen, zuletzt geladenen Kategorie-Datensatz als JSON.
*
* Wird von js/showCategory.js EINMALIG per fetch() geladen, damit Sortierung,
* Suche und Pagination danach komplett im Browser laufen können, ohne
* weitere Serveranfragen oder eine erneute Datenbank-Abfrage auszulösen
*
* Analog zu php/controller/search-results-data.php
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../includes/resultsHelper.php';
$rawResults = $_SESSION['category_results'] ?? [];
$payload = array_map(function ($item) {
return [
'id' => $item['id'],
'title' => $item['title'],
'content' => $item['content'] ?? '',
'author' => $item['author'],
'category' => $item['category'] ?? '',
'likes' => getLikeCount($item),
];
}, $rawResults);
echo json_encode([
'category' => $_SESSION['category_name'] ?? '',
'results' => array_values($payload),
]);
+188
View File
@@ -0,0 +1,188 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once '../model/LocalArticleManager.php';
require_once '../model/ArticleManager.php';
require_once '../model/Article.php';
require_once '../validator/article-validator.php';
require_once '../../includes/article-block-helper.php';
if (!isset($_SESSION["user"])) {
header("Location: index.php?pfad=login");
exit();
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
if (isset($_GET["id"]) && !empty($_GET["id"])) {
$id = $_GET["id"];
} else {
$_SESSION["message"] = "missing_id";
header("location: ../../index.php?pfad=updateArticle");
exit();
}
try {
$articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id);
if ($article->getAuthor() != $_SESSION["user_email"]) {
$_SESSION["message"] = "unauthorized_access";
header("location: ../../index.php");
exit();
}
} catch (Exception $e) {
$_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
$uploadDir = __DIR__ . '/../../uploads/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Formularzustand (Titel/Tags/Kategorie/Blöcke) immer sichern, damit er nach einem
// Redirect (PRG-Pattern oder Validierungsfehler) wieder angezeigt werden kann.
$_SESSION["old_title"] = $_POST["title"] ?? '';
$_SESSION["old_tags"] = $_POST["tags"] ?? '';
$_SESSION["old_category"] = $_POST["category"] ?? '';
$blocks = rebuildBlocksFromPost($_POST['blocks'] ?? [], $_FILES['blocks'] ?? [], $uploadDir);
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
// ---------------------------------------------------------------------
// Zwischen-Schritt: Block hinzufügen oder entfernen (kein echtes Speichern).
// Wird bei aktivem JavaScript per preventDefault() abgefangen und lokal im
// DOM erledigt (js/editor.js) ohne JS läuft dieser Server-Roundtrip.
// ---------------------------------------------------------------------
if (isset($_POST['editor_action']) && $_POST['editor_action'] !== '') {
$action = $_POST['editor_action'];
if ($action === 'add_text') {
$blocks[] = ['type' => 'text', 'value' => ''];
} elseif ($action === 'add_image') {
$blocks[] = ['type' => 'image', 'value' => ''];
} elseif (str_starts_with($action, 'delete_block:')) {
$deleteIndex = (int) substr($action, strlen('delete_block:'));
unset($blocks[$deleteIndex]);
$blocks = array_values($blocks);
}
$_SESSION["old_content"] = json_encode($blocks, JSON_UNESCAPED_UNICODE);
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
// ---------------------------------------------------------------------
// Echtes Speichern
// ---------------------------------------------------------------------
if (!isset($_POST["title"]) || !isset($_POST["category"])) {
$_SESSION["message"] = "missing_parameters";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
} else {
$title = $_POST["title"];
$content = json_encode($blocks, JSON_UNESCAPED_UNICODE);
$author = $_SESSION["user_email"];
$category = $_POST["category"];
$tags = $_POST['tags'] ?? '';
// -------------------------------- Validierung der Daten: -------------------------
if (!articleTitleValidator($title)) {
$_SESSION["message"] = "invalid_title";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
if (!articleContentValidator($content)) {
$_SESSION["message"] = "invalid_content";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
if (!articleCategoryValidator($category)) {
$_SESSION["message"] = "invalid_category";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
if (!articleTagValidator($tags)) {
$_SESSION["message"] = "invalid_tags";
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
} else {
$cleanedTags = [];
$rawTags = explode(',', $tags);
foreach ($rawTags as $rawTag) {
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tag = trim($rawTag);
$cleanedTags[] = $tag;
}
// Duplikate entfernen:
$cleanedTags = array_unique($cleanedTags);
$cleanedTags = implode(',', $cleanedTags);
}
// ----------------- Verwaiste Bilder aufräumen -----------------
// Bilder, die im alten (gespeicherten) Content vorkamen, im neuen aber nicht
// mehr referenziert werden, wurden vom Nutzer entfernt oder ersetzt -> löschen.
// Hinweis/TODO: Bilder, die innerhalb derselben Bearbeitungs-Sitzung neu
// hochgeladen und noch vor dem finalen Speichern wieder entfernt wurden,
// werden hierüber nicht erfasst (sie tauchten nie im alten Content auf) und
// bleiben als Datei liegen. Für eine vollständige Bereinigung würde sich ein
// regelmäßiger Cleanup-Job anbieten, der verwaiste Dateien im uploads/-Ordner
// mit den in der Datenbank referenzierten Pfaden abgleicht.
$oldBlocks = json_decode($article->getContent(), true);
$oldImagePaths = [];
if (is_array($oldBlocks)) {
foreach ($oldBlocks as $oldBlock) {
if (($oldBlock['type'] ?? '') === 'image'
&& !empty($oldBlock['value'])
&& is_string($oldBlock['value'])
&& str_starts_with($oldBlock['value'], 'uploads/')) {
$oldImagePaths[] = $oldBlock['value'];
}
}
}
$newImagePaths = [];
foreach ($blocks as $block) {
if (($block['type'] ?? '') === 'image' && !empty($block['value'])) {
$newImagePaths[] = $block['value'];
}
}
$orphanedImages = array_diff($oldImagePaths, $newImagePaths);
foreach ($orphanedImages as $orphanedImage) {
$absolutePath = __DIR__ . '/../../' . $orphanedImage;
if (is_file($absolutePath)) {
@unlink($absolutePath);
}
}
// ----------------- Übertragung der validierten Daten in ArticleManager: ---------------------------
try {
$articleManager = ArticleManager::getInstance();
$article = $articleManager->getArticle($id);
$article->setTitle($title);
$article->setContent($content);
$article->setCategory($category);
$article->setTags($cleanedTags);
$articleManager->updateArticle($id, $article, $author);
unset($_SESSION["old_title"], $_SESSION["old_content"], $_SESSION["old_category"], $_SESSION["old_tags"]);
} catch (\Throwable $e) {
$_SESSION["message"] = $e->getMessage();
header("location: ../../index.php?pfad=updateArticle&id=$id");
exit();
}
$_SESSION["message"] = "article_updated";
// Weiterleitung zur Homepage
header("location: ../../index.php?pfad=showArticle&id=$id");
}
}
?>
@@ -0,0 +1,92 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once "php/model/CommentManager.php";
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: index.php");
exit();
}
if (!isset($_SESSION["user_email"])) {
header("Location: index.php?pfad=login");
exit();
}
$commentId = filter_input(
INPUT_POST,
"comment_id",
FILTER_VALIDATE_INT
);
$articleId = filter_input(
INPUT_POST,
"article_id",
FILTER_VALIDATE_INT
);
$content = trim($_POST["content"] ?? "");
/*
* Ohne gültige Beitrags-ID kann nicht sicher
* zum ursprünglichen Beitrag zurückgeleitet werden.
*/
if (!$articleId) {
header("Location: index.php");
exit();
}
/*
* Kommentar-ID und Inhalt werden geprüft.
*/
if (!$commentId || $content === "") {
$_SESSION["comment_message"] =
"Der Kommentar darf nicht leer sein.";
$_SESSION["comment_message_type"] = "error";
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
}
try {
$commentManager = CommentManager::getInstance();
$updated = $commentManager->updateComment(
$commentId,
$_SESSION["user_email"],
$content
);
if ($updated) {
$_SESSION["comment_message"] =
"Der Kommentar wurde erfolgreich bearbeitet.";
$_SESSION["comment_message_type"] = "success";
} else {
$_SESSION["comment_message"] =
"Der Kommentar konnte nicht bearbeitet werden.";
$_SESSION["comment_message_type"] = "error";
}
} catch (Throwable $e) {
$_SESSION["comment_message"] =
"Beim Bearbeiten des Kommentars ist ein Fehler aufgetreten.";
$_SESSION["comment_message_type"] = "error";
}
header(
"Location: index.php?pfad=showArticle&id="
. urlencode((string) $articleId)
. "#comments"
);
exit();
+186
View File
@@ -0,0 +1,186 @@
<?php
/**
* Klasse: Artikel
* Diese Klasse stellt alle Daten eines Beitrags (Beitrag) bereit
*
* @author Niklas Ortmann
*/
class Article
{
private $id;
private $title;
private $content;
private $author;
private $creationDate;
private $category;
private $tags;
private $likes;
/**
* Konstruktor
*
* @param $id integer ID des Beitrages
* @param $title string Titel des Beitrags
* @param $content string Inhalt des Beitrags
* @param $author string der Autor des des Beitrages NID
* @param $category string Kategorie des Beitrages
* @param $tags string optionale Schlagworte für eine bessere Suche
* @param $creationDate string Datum der Beitragserstellung
*/
public function __construct(int $id, string $title, string $content, string $author, string $category, string $tags, string $creationDate, array $likes = [])
{
$this->id = $id;
$this->title = $title;
$this->content = $content;
$this->author = $author;
$this->creationDate = $creationDate;
$this->category = $category;
$this->tags = $tags;
$this->likes = $likes;
}
/**
* Gibt die ID eines Beitrags zurück.
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* Gibt den Titel eines Beitrags zurück.
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* Setzt den Titel eines Beitrags
* @param $title
* @return void
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Gibt den Content eines Beitrags zurück.
*
* @return string
*/
public function getContent(): string
{
return $this->content;
}
/**
* Setzt den Content eines Beitrags.
*
* @param $content
* @return void
*/
public function setContent($content)
{
$this->content = $content;
}
/**
* Gibt den Autor eines Beitrags zurück.
* @return string
*/
public function getAuthor(): string
{
return $this->author;
}
/**
* Setzt einen neuen Autor eines Beitrages.
* @param string $author
* @return void
*/
public function setAuthor(string $author)
{
$this->author = $author;
}
/**
* Gibt das Veröffentlichungsdatum des Beitrags zurück.
* @return string
*/
public function getCreationDate(): string
{
return $this->creationDate;
}
/**
* Gibt die Kategorie eines Beitrags zurück.
* @return string
*/
public function getCategory(): string
{
return $this->category;
}
/**
* Setzt die Kategorie eines Beitrags.
* @param string $category
* @return void
*/
public function setCategory(string $category)
{
$this->category = $category;
}
/**
* Gibt die Schlagworte eines Beitrags zurück.
* @return string
*/
public function getTags(): string
{
return $this->tags;
}
/**
* Setzt die Schlagworte eines Beitrags.
* @param string $tags
*/
public function setTags(string $tags)
{
$this->tags = $tags;
}
/**
* Gibt alle User-IDs zurück, die diesen Beitrag geliked haben.
* @return array
*/
public function getLikes(): array
{
return $this->likes;
}
/**
* Gibt die Gesamtzahl der Likes zurück.
* @return int
*/
public function getLikeCount(): int
{
return count($this->likes);
}
/**
* Prüft, ob ein bestimmter Nutzer den Beitrag bereits geliked hat.
*
* @param string $userId
* @return bool
*/
public function hasLiked(string $userId): bool
{
return in_array($userId, $this->likes);
}
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
require_once 'LocalArticleManager.php';
require_once 'DatabaseArticleManager.php';
require_once 'Article.php';
/**
* Die Klasse beinhaltet alle Methoden für die Operation mit den Beitrags-Daten.
*
* @author Niklas Ortmann
*/
class ArticleManager
{
/**
* Diese Methode erstellt, falls noch keine existiert, eine Instanz einer ArticleManager-Implementierung und
* erstellt Dummy-Beiträge.
*
* @throws InternalServerErrorException
*/
public static function getInstance()
{
return DatabaseArticleManager::getInstance(); // Hier kann zwischen dem lokalen und datenbankbasiertem ArticleManager gewechselt werden.
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
require_once "Article.php";
class NotFoundException extends Exception {}
class UnauthorizedAccessException extends Exception {}
class InternalServerErrorException extends Exception {}
/**
* Die Klasse beinhaltet alle Methoden für die Operation mit den Artikel-Daten.
*
* @author Niklas Ortmann
*/
interface ArticleManagerDAO
{
/**
* Ein angemeldeter Nutzer erstellt einen neuen Beitrag.
* @param $title string Titel des Beitrags
* @param $content string Inhalt des Beitrags
* @param $author string der Autor des des Beitrages NID
* @param $category string Kategorie des Beitrages
* @param $tags string optionale Schlagworte für eine bessere Suche
*
* @throws InternalServerErrorException
*/
public function addArticle($title, $content, $author, $category, $tags);
/**
* Ändert den gespeicherten Beitrag eines übergebenen Beitrags und eines Autors.
* Es wird geprüft, ob der zu änderne Beitrag existiert und ob der übergebene Autor der Autor des originalen
* Beitrages ist.
* @param $id
* @param $article
* @param $author
* @return void
*
* @throws InternalServerErrorException
* @throws NotFoundException
* @throws UnauthorizedAccessException
*/
public function updateArticle($id, $article, $author);
/**
* Löscht einen Beitrag aus übergebener ID und dem Nutzer, der die Löschung ausführt.
* @param $id
* @param $author
* @return void
* @throws InternalServerErrorException
* @throws NotFoundException
* @throws UnauthorizedAccessException
*/
public function deleteArticle($id, $author);
/**
* Beitrag aufrufen.
* $id ID des Beitrags
*
* @return Article
* @throws InternalServerErrorException
*/
public function getArticle($id);
/**
* Alle Beiträge aufrufen.
*
* @throws InternalServerErrorException
*/
public function getAllArticles();
/**
* Gibt alle Beiträge eines Nutzer mit einer gegebenen ID aus.
* @param $author
* @return Article[]
* @throws InternalServerErrorException
*/
public function getArticlesByAuthor($author);
/**
* Durchsucht die vorhandenen Beiträge nach einem bestimmten Suchbegriff.
* Die Suche prüft, ob das übergebene Keyword im Titel oder im Inhalt eines Beitrags vorkommt.
* (Unabhängig von Groß-und Kleinschreibung)
* @param string $keyword Der eingegebene Suchbegriff.
* @return array Ein Array von Artikeln ,die dem Suchkriterium entsprechen. Wenn nichts gefunden wird, ein leeres Array.
* @throws InternalServerErrorException
*/
public function search(string $keyword): array;
/**
* Gibt alle Beiträge einer gegebenen Kategorie aus.
* @param $category
* @return mixed
* @throws InternalServerErrorException
*/
public function getArticlesByCategory($category);
/**
* Registriert oder entfernt ein Like eines Nutzers für einen Beitrag.
* Wenn schon geliked -> Unlike
* Wenn noch nicht geliked -> Like
*
* @param int $articleId Die ID des Beitrags
* @param string $userId Die ID des Nutzers
* @return bool True wenn geliked, False wenn unliked
* @throws InternalServerErrorException
* @throws NotFoundException
*/
public function toggleLike(int $articleId, string $userId): bool;
}
?>
+115
View File
@@ -0,0 +1,115 @@
<?php
/**
* Repräsentiert einen Kommentar unter einem Beitrag.
*
* Ein Kommentar kann entweder ein Hauptkommentar sein
* oder eine Antwort auf einen anderen Kommentar.
*
* @author Caroline Schulte
*/
class Comment
{
private int $id;
private int $articleId;
private ?int $parentCommentId;
private ?string $author;
private string $content;
private string $created;
/**
* Erstellt einen neuen Kommentar.
*
* @param int $id Eindeutige ID des Kommentars
* @param int $articleId ID des zugehörigen Beitrags
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null
* @param string|null $author Autor des Kommentars oder null bei gelöschten Kommentaren
* @param string $content Inhalt des Kommentars
* @param string $created Erstellungsdatum des Kommentars
*/
public function __construct(
int $id,
int $articleId,
?int $parentCommentId,
?string $author,
string $content,
string $created
) {
$this->id = $id;
$this->articleId = $articleId;
$this->parentCommentId = $parentCommentId;
$this->author = $author;
$this->content = $content;
$this->created = $created;
}
/**
* Gibt die ID des Kommentars zurück.
*
* @return int Kommentar-ID
*/
public function getId(): int
{
return $this->id;
}
/**
* Gibt die ID des zugehörigen Beitrags zurück.
*
* @return int Beitrags-ID
*/
public function getArticleId(): int
{
return $this->articleId;
}
/**
* Gibt die ID des Eltern-Kommentars zurück.
*
* @return int|null ID des Eltern-Kommentars oder null
*/
public function getParentCommentId(): ?int
{
return $this->parentCommentId;
}
/**
* Gibt zurück, ob der Kommentar eine Antwort ist.
*
* @return bool true wenn der Kommentar eine Antwort ist, sonst false
*/
public function isReply(): bool
{
return $this->parentCommentId !== null && $this->parentCommentId !== 0;
}
/**
* Gibt den Autor des Kommentars zurück.
*
* @return string|null Autor oder null bei gelöschten Kommentaren
*/
public function getAuthor(): ?string
{
return $this->author;
}
/**
* Gibt den Inhalt des Kommentars zurück.
*
* @return string Kommentarinhalt
*/
public function getContent(): string
{
return $this->content;
}
/**
* Gibt das Erstellungsdatum des Kommentars zurück.
*
* @return string Erstellungsdatum
*/
public function getCreated(): string
{
return $this->created;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
require_once "DatabaseCommentManager.php";
/**
* Zentrale Zugriffsschicht für Kommentare.
*
* Die Anwendung arbeitet ausschließlich
* mit dem CommentManager und kennt die
* konkrete Speicherimplementierung nicht.
*
* @author Caroline Schulte
*/
class CommentManager
{
/**
* Gibt die aktive Kommentarverwaltung zurück.
*
* @return CommentManagerDAO
*/
public static function getInstance()
{
return DatabaseCommentManager::getInstance();
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
require_once "Comment.php";
/**
* Schnittstelle für die Verwaltung von Kommentaren.
*
* Definiert die grundlegenden Methoden zum
* Speichern und Laden von Kommentaren.
*
* @author Caroline Schulte
*/
interface CommentManagerDAO
{
/**
* Speichert einen neuen Kommentar zu einem Beitrag.
*
* Optional kann eine parentCommentId übergeben werden,
* wenn der Kommentar eine Antwort auf einen anderen Kommentar ist.
*
* @param int $articleId ID des Beitrags
* @param string $author Autor des Kommentars
* @param string $content Inhalt des Kommentars
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null
*
* @return int ID des neu gespeicherten Kommentars
*/
public function addComment(
$articleId,
$author,
$content,
$parentCommentId = null
);
/**
* Gibt alle Kommentare eines Beitrags zurück.
*
* @param int $articleId ID des Beitrags
*
* @return Comment[] Liste der Kommentare
*/
public function getCommentsByArticle(
$articleId
);
/**
* Gibt alle Kommentare eines Autors zurück.
*
* @param string $author E-Mail-Adresse des Autors
*
* @return Comment[] Liste der Kommentare
*/
public function getCommentsByAuthor($author);
/**
* Löscht einen einzelnen Kommentar des angemeldeten Nutzers.
*
* Kommentare ohne Antworten werden vollständig entfernt.
* Kommentare mit Antworten bleiben als anonymer Platzhalter erhalten.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gefunden und gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool;
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Diese Methode wird bei der Löschung eines Benutzerkontos verwendet.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void;
/**
* Bearbeitet einen Kommentar des angemeldeten Nutzers.
*
* Nur der Autor des Kommentars darf den Inhalt ändern.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool;
}
+426
View File
@@ -0,0 +1,426 @@
<?php
require_once 'DatabaseInitializer.php';
require_once 'ArticleManagerDAO.php';
require_once 'Article.php';
/**
* Klasse: Eine SQLLite3 Lösung des ArticleManagerDAO.
*
* @author Niklas Ortmann
*/
class DatabaseArticleManager implements ArticleManagerDAO {
private static $instance = null;
private $dbPath;
/**
* Konstruktor
*
* Erstellt die Datenbankverbindung und die Tabelle articles.
* @throws RuntimeException
*/
public function __construct()
{
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
DatabaseInitializer::initialize($this->dbPath);
}
/**
* Baut die Verbindung zur Datenbank auf.
*
* @return PDO Datenbankverbindung
* @throws RuntimeException
*/
private function getConnection()
{
try {
$db = new PDO('sqlite:' . $this->dbPath, null, null);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Gibt die DatabaseArticleManager-Instanz zurück.
* @return DatabaseArticleManager
*/
public static function getInstance()
{
if (self::$instance == null) {
self::$instance = new DatabaseArticleManager();
}
return self::$instance;
}
public function addArticle($title, $content, $author, $category, $tags)
{
try {
$db = $this->getConnection();
$sql = "INSERT INTO articles (title, content, author, category, tags)
VALUES (:title, :content, :author, :category, :tags);";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
// Verknüpft die übergebenen Parameter exakt mit den SQL-Platzhaltern
$success = $command->execute([
":title" => $title,
":content" => $content,
":author" => $author,
":category" => $category,
":tags" => $tags
]);
if (!$success) {
throw new InternalServerErrorException("internal_error");
}
return intval($db->lastInsertId());
} catch (PDOException $e) {
throw new InternalServerErrorException($e->getMessage());
}
}
public function updateArticle($id, $article, $author)
{
if (empty($article)) {
throw new InternalServerErrorException("internal_error");
}
// Berechtigungsprüfung analog zur lokalen Implementierung:
if ($article->getAuthor() !== $author) {
throw new UnauthorizedAccessException("unauthorized_access");
}
try {
$db = $this->getConnection();
$sql = "UPDATE articles
SET title = :title, content = :content, author = :author, category = :category, tags = :tags
WHERE id = :id;";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$success = $command->execute([
":id" => $id,
":title" => $article->getTitle(),
":content" => $article->getContent(),
":author" => $author,
":category" => $article->getCategory(),
":tags" => $article->getTags()
]);
// rowCount() prüft, ob eine Zeile mit dieser ID existierte und geändert werden konnte
if (!$success || $command->rowCount() === 0) {
// Falls die ID nicht existiert, prüfen wir, ob sie überhaupt da ist
if (!$this->getArticle($id)) {
throw new NotFoundException("missing_id");
}
}
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
public function deleteArticle($id, $author)
{
$article = $this->getArticle($id);
if (empty($article)) {
throw new NotFoundException("not_found_article");
}
// Berechtigungsprüfung:
if ($article->getAuthor() !== $author) {
throw new UnauthorizedAccessException("unauthorized_access");
}
try {
$db = $this->getConnection();
$sql = "DELETE FROM articles WHERE id = :id;";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
if (!$command->execute([":id" => $id])) {
throw new InternalServerErrorException("internal_error");
}
} catch (PDOException $exc) {
throw new InternalServerErrorException("internal_error");
}
}
public function getArticle($id)
{
try {
$db = $this->getConnection();
$sql = "SELECT * FROM articles WHERE id = :id;";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":id" => $id]);
$row = $command->fetch(PDO::FETCH_ASSOC);
if ($row) {
$likes = $this->getLikesForArticle(intval($row['id']));
return new Article(
intval($row['id']),
$row['title'],
$row['content'],
$row['author'],
$row['category'],
$row['tags'],
$row['created'],
$likes
);
}
return null;
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
public function getAllArticles()
{
try {
$db = $this->getConnection();
$sql = "SELECT * FROM articles;";
$command = $db->query($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$rows = $command->fetchAll(PDO::FETCH_ASSOC);
$articles = [];
foreach ($rows as $row) {
$articles[] = new Article(
intval($row['id']),
$row['title'],
$row['content'],
$row['author'],
$row['category'],
$row['tags'],
$row['created']
);
}
return $articles;
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
public function getArticlesByAuthor($author)
{
try {
$db = $this->getConnection();
$sql = "SELECT * FROM articles WHERE author = :author;";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":author" => $author]);
$rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = [];
foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article(
intval($row['id']),
$row['title'],
$row['content'],
$row['author'],
$row['category'],
$row['tags'],
$row['created'],
$likes
);
}
return $filteredArticles;
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
public function getArticlesByCategory($category)
{
try {
$db = $this->getConnection();
$sql = "SELECT * FROM articles WHERE category = :category;";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
$command->execute([":category" => $category]);
$rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = [];
foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article(
intval($row['id']),
$row['title'],
$row['content'],
$row['author'],
$row['category'],
$row['tags'],
$row['created'],
$likes
);
}
return $filteredArticles;
} catch (PDOException $exc) {
throw new InternalServerErrorException("internal_error");
}
}
public function search(string $keyword): array
{
$cleankeyword = trim($keyword);
// Leeren Suchbegriff sofort abfangen
if ($cleankeyword === '') {
return [];
}
try {
$db = $this->getConnection();
$sql = "SELECT id, title, content, author, category, tags, created
FROM articles
WHERE title LIKE :keyword
OR content LIKE :keyword
OR tags LIKE :keyword";
$command = $db->prepare($sql);
if (!$command) {
throw new InternalServerErrorException("internal_error");
}
// Wildcards für die Suche hinzufügen
$searchParam = '%' . $cleankeyword . '%';
$success = $command->execute([
":keyword" => $searchParam
]);
if (!$success) {
throw new InternalServerErrorException("internal_error");
}
$rows = $command->fetchAll(PDO::FETCH_ASSOC);
$filteredArticles = [];
foreach ($rows as $row) {
$likes = $this->getLikesForArticle(intval($row['id']));
$filteredArticles[] = new Article(
intval($row['id']),
$row['title'] ?? '',
$row['content'] ?? '',
$row['author'] ?? '',
$row['category'] ?? '',
$row['tags'] ?? '',
$row['created'] ?? '',
$likes
);
}
return $filteredArticles;
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
/**
* Holt alle User-IDs, die einen bestimmten Beitrag geliked haben.
*
* @return String[] UserIDs
* @throws InternalServerErrorException
*/
private function getLikesForArticle(int $articleId): array
{
try {
$db = $this->getConnection();
$sql = "SELECT user_id FROM likes WHERE article_id = :article_id;";
$command = $db->prepare($sql);
$command->execute([':article_id' => $articleId]);
return $command->fetchAll(PDO::FETCH_COLUMN) ?: [];
} catch (PDOException $e) {
return [];
}
}
public function toggleLike(int $articleId, string $userId): bool
{
// prüfen, ob der Artikel überhaupt existiert
$article = $this->getArticle($articleId);
if (!$article) {
throw new NotFoundException("missing_id");
}
try {
$db = $this->getConnection();
// prüfen, ob das Like bereits existiert
$checkSql = "SELECT COUNT(*) FROM likes WHERE article_id = :article_id AND user_id = :user_id;";
$checkCommand = $db->prepare($checkSql);
$checkCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
$hasLiked = (int)$checkCommand->fetchColumn() > 0;
if ($hasLiked) {
// wenn bereits geliked -> Unlike
$deleteSql = "DELETE FROM likes WHERE article_id = :article_id AND user_id = :user_id;";
$deleteCommand = $db->prepare($deleteSql);
$deleteCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
return false; // gibt false zurück, da der Beitrag jetzt nicht mehr geliked ist
} else {
// wenn noch nicht geliked -> Like
$insertSql = "INSERT INTO likes (article_id, user_id) VALUES (:article_id, :user_id);";
$insertCommand = $db->prepare($insertSql);
$insertCommand->execute([
':article_id' => $articleId,
':user_id' => $userId
]);
return true; // gibt true zurück, da der Beitrag jetzt geliked ist
}
} catch (PDOException $e) {
throw new InternalServerErrorException("internal_error");
}
}
}
+416
View File
@@ -0,0 +1,416 @@
<?php
require_once 'DatabaseInitializer.php';
require_once "CommentManagerDAO.php";
require_once "Comment.php";
/**
* Verwaltet die Speicherung und das Laden von Kommentaren
* über eine SQLite-Datenbank.
*
* @author Caroline Schulte
*/
class DatabaseCommentManager implements CommentManagerDAO
{
private static $instance = null;
private $dbPath;
/**
* Erstellt die Kommentartabelle, falls diese noch nicht existiert.
*/
private function __construct()
{
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
DatabaseInitializer::initialize($this->dbPath);
}
/**
* Baut die Verbindung zur SQLite-Datenbank auf.
*
* @return PDO Datenbankverbindung
*/
private function getConnection()
{
try {
$db = new PDO('sqlite:' . $this->dbPath, null, null);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Gibt die Singleton-Instanz zurück.
*
* @return DatabaseCommentManager
*/
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new DatabaseCommentManager();
}
return self::$instance;
}
/**
* Speichert einen neuen Kommentar oder eine Antwort.
*
* @param int $articleId ID des Beitrags
* @param string $author Autor des Kommentars
* @param string $content Inhalt des Kommentars
* @param int|null $parentCommentId ID des Eltern-Kommentars oder null
*
* @return int ID des neu gespeicherten Kommentars
*/
public function addComment(
$articleId,
$author,
$content,
$parentCommentId = null
) {
try {
$db = $this->getConnection();
if ($parentCommentId === "" || $parentCommentId === 0 || $parentCommentId === "0") {
$parentCommentId = null;
}
$sql = "
INSERT INTO comments (
article_id,
parent_comment_id,
author,
content
)
VALUES (
:articleId,
:parentCommentId,
:author,
:content
)
";
$command = $db->prepare($sql);
$command->execute([
":articleId" => $articleId,
":parentCommentId" => $parentCommentId,
":author" => $author,
":content" => $content
]);
return intval($db->lastInsertId());
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Lädt alle Kommentare eines Beitrags.
*
* @param int $articleId ID des Beitrags
*
* @return Comment[]
*/
public function getCommentsByArticle($articleId)
{
try {
$db = $this->getConnection();
$sql = "
SELECT
id,
article_id,
CASE
WHEN parent_comment_id IS NULL THEN NULL
WHEN parent_comment_id = '' THEN NULL
WHEN parent_comment_id = 0 THEN NULL
ELSE parent_comment_id
END AS parent_comment_id,
author,
content,
created
FROM comments
WHERE article_id = :articleId
ORDER BY created ASC
";
$command = $db->prepare($sql);
$command->execute([":articleId" => $articleId]);
return $this->mapRowsToComments($command);
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Lädt alle Kommentare eines Autors.
*
* @param string $author E-Mail-Adresse des Autors
*
* @return Comment[]
*/
public function getCommentsByAuthor($author)
{
try {
$db = $this->getConnection();
$sql = "
SELECT
id,
article_id,
CASE
WHEN parent_comment_id IS NULL THEN NULL
WHEN parent_comment_id = '' THEN NULL
WHEN parent_comment_id = 0 THEN NULL
ELSE parent_comment_id
END AS parent_comment_id,
author,
content,
created
FROM comments
WHERE author = :author
ORDER BY created DESC
";
$command = $db->prepare($sql);
$command->execute([":author" => $author]);
return $this->mapRowsToComments($command);
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Wandelt Datenbankzeilen in Comment-Objekte um.
*
* @param PDOStatement $command Ausgeführtes Statement
*
* @return Comment[]
*/
private function mapRowsToComments($command)
{
$comments = [];
while ($row = $command->fetch(PDO::FETCH_ASSOC)) {
$parentCommentId = null;
if (
isset($row["parent_comment_id"])
&& $row["parent_comment_id"] !== null
&& $row["parent_comment_id"] !== ""
&& intval($row["parent_comment_id"]) !== 0
) {
$parentCommentId = intval($row["parent_comment_id"]);
}
$comments[] = new Comment(
intval($row["id"]),
intval($row["article_id"]),
$parentCommentId,
$row["author"],
$row["content"],
$row["created"]
);
}
return $comments;
}
/**
* Bearbeitet einen eigenen Kommentar.
*
* Der Kommentar wird nur geändert, wenn er dem
* angemeldeten Nutzer gehört.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @param string $content Neuer Kommentarinhalt
* @return bool true, wenn der Kommentar bearbeitet wurde
*/
public function updateComment(
int $commentId,
string $author,
string $content
): bool {
try {
$db = $this->getConnection();
$command = $db->prepare("
UPDATE comments
SET content = :content
WHERE id = :commentId
AND author = :author
");
$command->execute([
":content" => $content,
":commentId" => $commentId,
":author" => $author
]);
return $command->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht einen eigenen Kommentar.
*
* Hat der Kommentar Antworten, wird er anonymisiert.
* Hat er keine Antworten, wird er vollständig gelöscht.
*
* @param int $commentId ID des Kommentars
* @param string $author E-Mail-Adresse des Autors
* @return bool true, wenn der Kommentar gelöscht wurde
*/
public function deleteComment(int $commentId, string $author): bool
{
try {
$db = $this->getConnection();
/*
* Zuerst wird geprüft, ob der Kommentar existiert
* und wirklich dem angemeldeten Nutzer gehört.
*/
$checkCommand = $db->prepare("
SELECT id
FROM comments
WHERE id = :commentId
AND author = :author
");
$checkCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
if ($checkCommand->fetch() === false) {
return false;
}
/*
* Danach wird geprüft, ob Antworten auf den Kommentar existieren.
*/
$replyCommand = $db->prepare("
SELECT COUNT(*)
FROM comments
WHERE parent_comment_id = :commentId
");
$replyCommand->execute([
":commentId" => $commentId
]);
$hasReplies = (int) $replyCommand->fetchColumn() > 0;
if ($hasReplies) {
/*
* Der Kommentar wird für den Kommentarbaum benötigt.
* Deshalb bleibt er als anonymer Platzhalter erhalten.
*/
$deleteCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE id = :commentId
AND author = :author
");
} else {
/*
* Ohne Antworten kann der Kommentar vollständig
* aus der Datenbank entfernt werden.
*/
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE id = :commentId
AND author = :author
");
}
$deleteCommand->execute([
":commentId" => $commentId,
":author" => $author
]);
return $deleteCommand->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Löscht beziehungsweise anonymisiert alle Kommentare eines Nutzers.
*
* Kommentare ohne Antworten werden vollständig gelöscht.
* Kommentare mit Antworten bleiben als anonyme Platzhalter erhalten.
*
* @param string $author E-Mail-Adresse des Nutzers
* @return void
*/
public function deleteCommentsByAuthor(string $author): void
{
$db = $this->getConnection();
try {
$db->beginTransaction();
/*
* Zuerst werden alle Kommentare ohne Antworten gelöscht.
*
* Die Schleife ist wichtig, weil durch das Löschen einer Antwort
* eventuell auch der darüberliegende Kommentar keine Antworten
* mehr besitzt und anschließend ebenfalls gelöscht werden kann.
*/
do {
$deleteCommand = $db->prepare("
DELETE FROM comments
WHERE author = :author
AND NOT EXISTS (
SELECT 1
FROM comments AS replies
WHERE replies.parent_comment_id = comments.id
)
");
$deleteCommand->execute([
":author" => $author
]);
$deletedRows = $deleteCommand->rowCount();
} while ($deletedRows > 0);
/*
* Kommentare, auf die noch Antworten anderer Nutzer folgen,
* müssen für den Kommentarbaum erhalten bleiben.
*/
$placeholderCommand = $db->prepare("
UPDATE comments
SET author = NULL,
content = 'Dieser Kommentar wurde gelöscht.'
WHERE author = :author
");
$placeholderCommand->execute([
":author" => $author
]);
$db->commit();
} catch (PDOException $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw new RuntimeException("internal_error");
}
}
}
+251
View File
@@ -0,0 +1,251 @@
<?php
/**
* Diese Klasse legt einmalig beim allerersten Start in der exakt korrekten Reihenfolge die Tabellen an
*
* @author Niklas Ortmann
*/
class DatabaseInitializer {
public static function initialize($dbPath) {
if (!file_exists($dbPath)) {
try {
$db = new PDO('sqlite:' . $dbPath);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
$db->exec("
CREATE TABLE IF NOT EXISTS users (
email TEXT PRIMARY KEY,
vorname TEXT NOT NULL,
nachname TEXT NOT NULL,
password TEXT NOT NULL
);
");
$db->exec("
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
author TEXT,
category TEXT,
tags TEXT,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL ON UPDATE CASCADE
);
");
$db->exec("
CREATE TABLE IF NOT EXISTS likes (
article_id INTEGER,
user_id TEXT,
PRIMARY KEY (article_id, user_id),
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(email) ON DELETE CASCADE
);
");
$db->exec("
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id INTEGER NOT NULL,
parent_comment_id INTEGER NULL,
author TEXT NULL,
content TEXT NOT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
FOREIGN KEY (author) REFERENCES users(email) ON DELETE SET NULL,
FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE
);
");
$initializer = new self();
$availableEmails = $initializer->seedDummyUsers($db);
$initializer->seedDummyArticles($db, $availableEmails);
unset($db);
} catch (PDOException $e) {
throw new RuntimeException("internal-error");
}
}
}
/**
* Erstellt die Dummy-Benutzer in der Datenbank.
*
* @param PDO $db Die aktive Datenbankverbindung
* @return array Liste der erfolgreich angelegten E-Mail-Adressen
*/
private function seedDummyUsers(PDO $db): array {
$dummyUsers = [
['email' => 'max.mustermann@web.de', 'vorname' => 'Max', 'nachname' => 'Mustermann'],
['email' => 'erika.mustermann@web.de', 'vorname' => 'Erika', 'nachname' => 'Mustermann'],
['email' => 'john.doe@gmail.com', 'vorname' => 'John', 'nachname' => 'Doe'],
['email' => 'jane.doe@gmail.com', 'vorname' => 'Jane', 'nachname' => 'Doe'],
['email' => 'anna.schmidt@gmx.de', 'vorname' => 'Anna', 'nachname' => 'Schmidt'],
['email' => 'thomas.mueller@gmx.de', 'vorname' => 'Thomas', 'nachname' => 'Müller'],
['email' => 'sabine.fischer@outlook.com', 'vorname' => 'Sabine', 'nachname' => 'Fischer'],
['email' => 'michael.weber@outlook.com', 'vorname' => 'Michael', 'nachname' => 'Weber'],
['email' => 'julia.wagner@t-online.de', 'vorname' => 'Julia', 'nachname' => 'Wagner'],
['email' => 'stefan.becker@t-online.de', 'vorname' => 'Stefan', 'nachname' => 'Becker']
];
$passwordHash = password_hash("test12345", PASSWORD_DEFAULT);
$userInsertStmt = $db->prepare("INSERT INTO users (email, vorname, nachname, password) VALUES (:email, :vorname, :nachname, :password);");
$availableEmails = [];
foreach ($dummyUsers as $user) {
$userInsertStmt->execute([
':email' => $user['email'],
':vorname' => $user['vorname'],
':nachname' => $user['nachname'],
':password' => $passwordHash
]);
$availableEmails[] = $user['email'];
}
return $availableEmails;
}
/**
* Erstellt die 100 Fachbeiträge in der Datenbank.
*
* @param PDO $db Die aktive Datenbankverbindung
* @param array $availableEmails Pool für die Foreign-Key-Zuweisung des Autors
* @return void
*/
private function seedDummyArticles(PDO $db, array $availableEmails): void
{
$dummyArticles = [
// --- INFORMATIK & MATHE (1-20) ---
1 => ["Satz des Pythagoras", "Der Satz des Pythagoras beschreibt das Verhältnis der Seitenlängen in einem rechtwinkligen Dreieck.", "mathe", "Dreiecke, Geometrie"],
2 => ["Tunneleffekt", "Der Tunneleffekt beschreibt das quantenmechanische Phänomen, dass Teilchen Barrieren überwinden können.", "physik", "Quantenphysik, Energie"],
3 => ["Datenschutz vs Datensicherheit", "Datenschutz schützt Personen, während Datensicherheit Systeme vor unbefugten Zugriffen schützt.", "informatik", "Daten, DSGVO"],
4 => ["Einführung in Algorithmen", "Ein Algorithmus ist eine präzise Handlungsanweisung zur Lösung eines vordefinierten Problems.", "informatik", "Code, Logik"],
5 => ["Primzahlen im Detail", "Primzahlen sind natürliche Zahlen, die nur durch eins und sich selbst ohne Rest teilbar sind.", "mathe", "Zahlentheorie"],
6 => ["Lineare Algebra", "Vektoren und Matrizen bilden das Fundament für moderne Computergrafik und 3D-Engines.", "mathe", "Matrizen, Vektoren"],
7 => ["Objektorientierte Programmierung", "Die OOP nutzt Klassen und Objekte, um reale Strukturen im Quellcode abzubilden.", "informatik", "OOP, Klassen"],
8 => ["Grundlagen von HTML und CSS", "HTML strukturiert den Inhalt einer Webseite, während CSS für das optische Design zuständig ist.", "informatik", "Web, Frontend"],
9 => ["Die Relativitätstheorie", "Einsteins Theorie revolutionierte unser Verständnis von Raum, Zeit und Gravitation massiv.", "physik", "Einstein, Gravitation"],
10 => ["Datenbanken und SQL", "Strukturierte Abfragesprachen erlauben das effiziente Speichern und Abrufen großer Datenmengen.", "informatik", "SQL, Datenbank"],
11 => ["Wahrscheinlichkeitsrechnung", "Die Stochastik befasst sich mit der mathematischen Analyse von Zufallsexperimenten.", "mathe", "Zufall, Stochastik"],
12 => ["Quantencomputing", "Künftige Quantencomputer nutzen Qubits, um komplexe Berechnungen in Rekordzeit zu lösen.", "informatik", "Hardware, Zukunft"],
13 => ["Die Fibonacci-Folge", "Diese Zahlenreihe beschreibt mathematische Wachstumsmuster, die oft in der Natur vorkommen.", "mathe", "Zahlen, Natur"],
14 => ["Thermodynamik", "Die Hauptsätze der Thermodynamik regeln den Energieaustausch und die Entropie in Systemen.", "physik", "Energie, Wärme"],
15 => ["Cybersecurity Grundlagen", "Die Absicherung von Netzwerken erfordert Firewalls, Verschlüsselung und regelmäßige Audits.", "informatik", "Sicherheit, Hacker"],
16 => ["Kryptographie", "Asymmetrische Verschlüsselungsverfahren sichern heutzutage den gesamten Datenverkehr im Web.", "informatik", "Krypto, Security"],
17 => ["Analysis und Ableitungen", "Die Differentialrechnung untersucht die lokalen Änderungsraten von mathematischen Funktionen.", "mathe", "Analysis, Funktionen"],
18 => ["Schwarze Löcher", "Diese Regionen im Raum besitzen eine so starke Gravitation, dass selbst Licht nicht entkommt.", "physik", "Astronomie, Kosmos"],
19 => ["Git Versionsverwaltung", "Git erlaubt Entwicklern das parallele Arbeiten an Code-Projekten ohne Datenverlust.", "informatik", "Git, DevOps"],
20 => ["Künstliche Intelligenz", "Neuronale Netze versuchen das menschliche Gehirn für maschinelles Lernen nachzubilden.", "informatik", "KI, Software"],
// --- CHEMIE & BIOLOGIE (21-40) ---
21 => ["Das Periodensystem", "Die Elemente sind nach ihrer Ordnungszahl und chemischen Eigenschaften geordnet.", "chemie", "Elemente, Moleküle"],
22 => ["Photosynthese", "Pflanzen wandeln mithilfe von Sonnenlicht Kohlendioxid und Wasser in Glukose und Sauerstoff um.", "biologie", "Pflanzen, Energie"],
23 => ["Aufbau einer Zelle", "Die Zelle ist die kleinste lebende Einheit aller Organismen mit spezialisierten Organellen.", "biologie", "Zellen, Biologie"],
24 => ["Säuren und Basen", "Der pH-Wert misst die Konzentration von Wasserstoff-Ionen in einer wässrigen Lösung.", "chemie", "Labor, pH-Wert"],
25 => ["Die DNA-Struktur", "Die Doppelhelix enthält den genetischen Bauplan für die Entwicklung aller Lebewesen.", "biologie", "Genetik, Erbgut"],
26 => ["Chemische Bindungen", "Kovalente Bindungen entstehen durch das Teilen von Elektronenpaaren zwischen Atomen.", "chemie", "Atome, Bindung"],
27 => ["Das Immunsystem", "Weiße Blutkörperchen und Antikörper schützen den menschlichen Körper vor Krankheitserregern.", "biologie", "Gesundheit, Abwehr"],
28 => ["Katalysatoren", "Katalysatoren beschleunigen chemische Reaktionen, ohne dabei selbst verbraucht zu werden.", "chemie", "Reaktion, Chemie"],
29 => ["Ökosystem Wald", "Das Zusammenspiel von Flora, Fauna und Klima bildet ein hochsensibles ökologisches System.", "biologie", "Natur, Wald"],
30 => ["Die Mendelschen Regeln", "Diese Grundgesetze der Vererbung beschreiben, wie Merkmale an Nachkommen weitergegeben werden.", "biologie", "Genetik, Erbung"],
31 => ["Zustandsformen der Materie", "Fest, flüssig und gasförmig sind die klassischen Aggregatzustände von Stoffen.", "chemie", "Physik, Materie"],
32 => ["Evolutionstheorie", "Charles Darwin begründete die Theorie der natürlichen Auslese und Anpassung von Arten.", "biologie", "Darwin, Evolution"],
33 => ["Organische Chemie", "Die Chemie der Kohlenstoffverbindungen bildet die Basis für alles bekannte Leben.", "chemie", "Kohlenstoff, Chemie"],
34 => ["Das menschliche Gehirn", "Milliarden von Neuronen kommunizieren über Synapsen, um Reize und Gedanken zu verarbeiten.", "biologie", "Neurologie, Nerven"],
35 => ["Der Wasserkreislauf", "Verdunstung, Kondensation und Niederschlag halten das Wasser auf der Erde in Bewegung.", "geographie", "Wasser, Klima"],
36 => ["Aggregatzustände von Wasser", "Wasser zeigt ungewöhnliche Eigenschaften wie die Dichteanomalie beim Gefrieren.", "chemie", "Wasser, Eis"],
37 => ["Blutkreislauf des Menschen", "Das Herz pumpt sauerstoffreiches Blut durch Arterien in alle Organe des Körpers.", "biologie", "Herz, Medizin"],
38 => ["Das Ohmsche Gesetz", "Es beschreibt den direkten Zusammenhang zwischen Spannung, Stromstärke und Widerstand.", "physik", "Strom, Elektronik"],
39 => ["Plattentektonik", "Die Bewegung der Kontinentalplatten führt zu Erdbeben, Vulkanismus und Gebirgsbildung.", "geographie", "Erde, Geologie"],
40 => ["Proteine und Enzyme", "Enzyme wirken als Biokatalysatoren und steuern fast alle Stoffwechselprozesse.", "biologie", "Biochemie, Enzyme"],
// --- GESCHICHTE & WIRTSCHAFT (41-60) ---
41 => ["Das Römische Reich", "Vom Stadtstaat zum Weltreich prägte Rom die Rechts- und Kulturgeschichte Europas.", "geschichte", "Antike, Rom"],
42 => ["Die Französische Revolution", "Freiheit, Gleichheit, Brüderlichkeit beendeten 1789 die absolute Monarchie in Frankreich.", "geschichte", "Europa, Politik"],
43 => ["Inflation erklärt", "Inflation bezeichnet die kontinuierliche Geldentwertung und den Kaufkraftverlust.", "wirtschaft", "Geld, Finanzen"],
44 => ["Die Industrielle Revolution", "Die Erfindung der Dampfmaschine veränderte die Produktion und die Gesellschaft tiefgreifend.", "geschichte", "Industrie, Arbeit"],
45 => ["Angebot und Nachfrage", "Dieses fundamentale Marktgesetz bestimmt den Preis von Gütern in einer freien Wirtschaft.", "wirtschaft", "Markt, Preise"],
46 => ["Der Buchdruck", "Johannes Gutenbergs Erfindung revolutionierte die Verbreitung von Wissen im Mittelalter.", "geschichte", "Medien, Wissen"],
47 => ["Die Entdeckung Amerikas", "Kolumbus' Seereise im Jahr 1492 leitete das Zeitalter der Kolonialisierung ein.", "geschichte", "Entdeckung, Seefahrt"],
48 => ["Kryptowährungen", "Bitcoin nutzt Blockchain-Technologie, um dezentralen digitalen Werttransfer zu erlauben.", "wirtschaft", "Blockchain, Finanzen"],
49 => ["Das antike Griechenland", "Die Wiege der Demokratie und Philosophie brachte Denker wie Sokrates und Platon hervor.", "geschichte", "Antike, Philosophie"],
50 => ["Globalisierung", "Die weltweite Verflechtung in Wirtschaft, Kultur und Politik bringt Chancen und Risiken.", "wirtschaft", "Weltwirtschaft, Handel"],
51 => ["Der Dreißigjährige Krieg", "Ein religiöser und politischer Konflikt verwüstete zwischen 1618 und 1648 Mitteleuropa.", "geschichte", "Krieg, Europa"],
52 => ["Die Weimarer Republik", "Die erste deutsche Demokratie scheiterte an wirtschaftlichen und politischen Krisen.", "geschichte", "Deutschland, Weimar"],
53 => ["Aktien und Börse", "Unternehmen beschaffen sich Kapital durch die Ausgabe von Anteilen an Investoren.", "wirtschaft", "Aktien, Investieren"],
54 => ["Das alte Ägypten", "Pharaonen, Pyramiden und Hieroglyphen zeugen von einer hochentwickelten Hochkultur am Nil.", "geschichte", "Ägypten, Antike"],
55 => ["Der Kalte Krieg", "Das Wettrüsten zwischen USA und UdSSR prägte die globale Politik der Nachkriegszeit.", "geschichte", "Ost-West, Politik"],
56 => ["Zentralbanken und Leitzins", "Durch Zinsänderungen steuern Notenbanken die Geldmenge und bekämpfen Inflation.", "wirtschaft", "Zinsen, Geldpolitik"],
57 => ["Die Seidenstraße", "Das historische Netzwerk von Handelsrouten verband über Jahrhunderte Asien und Europa.", "geschichte", "Handel, Asien"],
58 => ["Das Mittelalter", "Ritter, Burgen und das Feudalsystem prägten diese tausendjährige Epoche Europas.", "geschichte", "Mittelalter, Feudalismus"],
59 => ["Planwirtschaft vs Marktwirtschaft", "Zentrale staatliche Steuerung steht dem freien Spiel der Marktkräfte gegenüber.", "wirtschaft", "Systeme, Wirtschaft"],
60 => ["Die Berliner Mauer", "Ihr Bau 1961 zementierte die Teilung Deutschlands, ihr Fall 1989 beendete sie.", "geschichte", "DDR, Wiedervereinigung"],
// --- ANWENDUNGEN & WEITERE THEMEN (61-100) ---
61 => ["Cloud Computing", "Das Auslagern von Rechenleistung in das Internet spart lokale IT-Infrastruktur ein.", "informatik", "Cloud, Web"],
62 => ["Responsive Webdesign", "Moderne Webseiten passen ihr Layout dynamisch an Smartphones und Desktops an.", "informatik", "Design, CSS"],
63 => ["Der Treibhauseffekt", "Gase in der Atmosphäre verhindern das Entweichen von Wärme ins Weltall.", "physik", "Klima, Umwelt"],
64 => ["Mechanik und Kräfte", "Die Newtonschen Axiome beschreiben, wie Kräfte auf Körper wirken und sie bewegen.", "physik", "Newton, Kraft"],
65 => ["Die Mendelschen Gesetze", "Die Vererbung von Genen folgt klaren statistischen Wahrscheinlichkeiten.", "biologie", "Genetik, Erbsen"],
66 => ["Lichtgeschwindigkeit", "Im Vakuum bewegt sich Licht mit knapp 300.000 Kilometern pro Sekunde.", "physik", "Licht, Relativität"],
67 => ["Die Funktion von APIs", "Programmierschnittstellen erlauben den Datenaustausch zwischen verschiedenen Systemen.", "informatik", "API, Schnittstelle"],
68 => ["Der Goldstandard", "Ein historischen Währungssystem, bei dem Geld durch echtes Gold gedeckt war.", "wirtschaft", "Gold, Währung"],
69 => ["Der Wiener Kongress", "1815 ordneten die europäischen Mächte die Landkarte nach den Napoleonischen Kriegen neu.", "geschichte", "Europa, Diplomatie"],
70 => ["Integrierte Schaltkreise", "Mikrochips enthalten Millionen Transistoren auf kleinstem Raum für Logikschaltungen.", "informatik", "Hardware, Chips"],
71 => ["Die Magellan-Expedition", "Die erste erfolgreiche Weltumsegelung bewies praktisch die Kugelgestalt der Erde.", "geschichte", "Seefahrt, Erde"],
72 => ["Das Internet der Dinge", "Alltagsgegenstände werden vernetzt, um smarte Automatisierungen zu ermöglichen.", "informatik", "IoT, SmartHome"],
73 => ["Halbwertszeit", "Die Zeitspanne, in der sich die Hälfte der instabilen Atome radioaktiv abbaut.", "physik", "Atomphysik, Strahlung"],
74 => ["Elektromagnetismus", "Die Verknüpfung von elektrischen Strömen und magnetischen Feldern treibt Motoren an.", "physik", "Strom, Magnet"],
75 => ["Das Ökosystem Meer", "Ozeane regulieren das Weltklima und bieten Lebensraum für unzählige Arten.", "biologie", "Meer, Ökologie"],
76 => ["Einführung in Docker", "Containerisierung isoliert Anwendungen samt Abhängigkeiten für stabilen Betrieb.", "informatik", "Docker, DevOps"],
77 => ["Das Römische Recht", "Viele moderne europäische Gesetzbücher basieren auf antiken römischen Rechtsprinzipien.", "geschichte", "Recht, Gesetz"],
78 => ["Verhaltensbiologie", "Untersuchung von angeborenen und erlernten Verhaltensweisen bei Mensch und Tier.", "biologie", "Verhalten, Tiere"],
79 => ["Verschlüsselung im Alltag", "HTTPS schützt Passwörter und Zahlungsdaten beim Surfen vor dem Mitlesen.", "informatik", "Web, HTTPS"],
80 => ["Die Magna Carta", "1215 schränkte dieses Dokument die absolute Macht des englischen Königs ein.", "geschichte", "England, Verfassung"],
81 => ["Marktversagen", "Wenn der freie Markt Ressourcen unvollständig verteilt, muss der Staat eingreifen.", "wirtschaft", "Markt, Staat"],
82 => ["Optische Linsen", "Konvexe und konkave Linsen brechen Licht für Brillen, Mikroskope und Kameras.", "physik", "Optik, Licht"],
83 => ["Die Entstehung der Erde", "Vor rund 4,5 Milliarden Jahren ballte sich kosmischer Staub zu unserem Planeten.", "geographie", "Erde, Kosmos"],
84 => ["Grundlagen von JavaScript", "Diese Skriptsprache macht statische Webseiten interaktiv und dynamisch nutzbar.", "informatik", "JS, Webentwicklung"],
85 => ["Die industrielle Landwirtschaft", "Moderne Techniken sichern Welternährung, belasten jedoch oft die Umwelt.", "biologie", "Landwirtschaft, Umwelt"],
86 => ["Das Schwarze Jahr 1929", "Der New Yorker Börsencrash löste die verheerende Weltwirtschaftskrise aus.", "geschichte", "Krise, Finanzen"],
87 => ["Die Evolution des Menschen", "Der Stammbaum des Homo Sapiens entwickelte sich über Millionen Jahre in Afrika.", "biologie", "Mensch, Evolution"],
88 => ["Einführung in Linux", "Das Open-Source-Betriebssystem bildet das Rückgrat moderner Server-Infrastrukturen.", "informatik", "Linux, OS"],
89 => ["Der Urknall", "Die Urknalltheorie beschreibt den Beginn des Universums aus einer Singularität.", "physik", "Astronomie, Urknall"],
90 => ["Das Periodensystem der Elemente", "Dmitri Mendelejew ordnete Elemente logisch nach ihren Atommassen.", "chemie", "Periodensystem, Chemie"],
91 => ["Die Hanse", "Ein mächtiger mittelalterlicher Bund von Kaufleuten dominierte den Nordseehandel.", "geschichte", "Handel, Mittelalter"],
92 => ["Wirtschaftswachstum", "Die Steigerung des Bruttoinlandsprodukts gilt oft als Indikator für Wohlstand.", "wirtschaft", "BIP, Finanzen"],
93 => ["Neuronale Netze", "Diese Strukturen lernen durch mathematische Gewichtung aus riesigen Datenmengen.", "informatik", "KI, Mathematik"],
94 => ["Der Erste Weltkrieg", "Der globale Konflikt von 1914 bis 1918 zerstörte das alte europäische Machtgefüge.", "geschichte", "Europa, Krieg"],
95 => ["Das Gehirn und Hormone", "Botenstoffe steuern Gefühle, Schlafzyklen und Reaktionen des Körpers.", "biologie", "Medizin, Hormone"],
96 => ["SQL Joins erklärt", "Joins verknüpfen Daten aus mehreren Tabellen über gemeinsame Schlüssel.", "informatik", "SQL, Datenbanken"],
97 => ["Wellen-Teilchen-Dualismus", "Quantenobjekte zeigen je nach Messaufbau Eigenschaften von Wellen oder Teilchen.", "physik", "Quanten, Licht"],
98 => ["Der Absolutismus", "Der Sonnenkönig Ludwig XIV. verkörperte die unbeschränkte Herrschaft des Monarchen.", "geschichte", "Frankreich, Monarchie"],
99 => ["Die Funktion von Routern", "Netzwerkgeräte leiten Datenpakete über IP-Adressen an den richtigen Empfänger.", "informatik", "Netzwerk, Internet"],
100 => ["Die Entstehung des Geldes", "Vom Tauschhandel über Naturalgeld bis hin zu modernen digitalen Fiat-Währungen.", "wirtschaft", "Geld, Geschichte"]
];
$articleInsertStmt = $db->prepare("
INSERT INTO articles (title, content, author, category, tags)
VALUES (:title, :content, :author, :category, :tags);");
foreach ($dummyArticles as $article) {
// Bestimmt per Zufall einen Autor aus dem Pool der gültigen E-Mails
$randomAuthor = $availableEmails[array_rand($availableEmails)];
// Text in (blocks[i][type]/[text]/[image])-Format umwandeln:
$content = json_encode([
['type' => 'text', 'value' => $article[1]]
], JSON_UNESCAPED_UNICODE);
$articleInsertStmt->execute([
':title' => $article[0],
':content' => $content,
':author' => $randomAuthor,
':category' => $article[2],
':tags' => $article[3]
]);
}
}
}
+244
View File
@@ -0,0 +1,244 @@
<?php
require_once 'DatabaseInitializer.php';
require_once "UserManagerDAO.php";
/**
* Klasse für den Zugriff auf Benutzerdaten über eine SQLite-Datenbank.
*
* Diese Klasse verwendet PDO, Prepared Statements und speichert
* Benutzerdaten in der Datei db/users.db.
*/
class DatabaseUserManager implements UserManagerDAO {
private static $instance = null;
private $dbPath;
/**
* Konstruktor.
*
* Erstellt die Benutzerdatenbank und die Tabelle users,
* falls diese noch nicht existieren.
*
* @throws RuntimeException wenn die Datenbank nicht erstellt werden kann
*/
public function __construct()
{
$this->dbPath = __DIR__ . '/../../db/eduforgeDB.db';
DatabaseInitializer::initialize($this->dbPath);
}
/**
* Baut eine Verbindung zur SQLite-Datenbank auf.
*
* @return PDO Datenbankverbindung
*
* @throws RuntimeException wenn keine Verbindung hergestellt werden kann
*/
private function getConnection()
{
try {
$db = new PDO('sqlite:' . $this->dbPath, null, null);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
} catch (PDOException $e) {
throw new RuntimeException("internal_error");
}
}
/**
* Gibt die Singleton-Instanz des DatabaseUserManagers zurück.
*
* @return DatabaseUserManager Instanz des DatabaseUserManagers
*/
public static function getInstance()
{
if (self::$instance == null) {
self::$instance = new DatabaseUserManager();
}
return self::$instance;
}
/**
* Sucht einen Benutzer anhand seiner E-Mail-Adresse.
*
* @param string $email E-Mail-Adresse des Benutzers
*
* @return array|null Benutzerdaten als Array oder null,
* wenn kein Benutzer gefunden wurde
*
* @throws RuntimeException wenn der Benutzer nicht geladen werden kann
*/
public function findUser($email)
{
try {
$db = $this->getConnection();
$sql = "SELECT * FROM users WHERE email = :email";
$command = $db->prepare($sql);
$command->execute([
":email" => $email
]);
$user = $command->fetch(PDO::FETCH_ASSOC);
return $user ?: null;
} catch (PDOException $e) {
throw new RuntimeException("Benutzer konnte nicht geladen werden.");
}
}
/**
* Fügt einen neuen Benutzer in die Datenbank ein.
*
* Die Speicherung erfolgt innerhalb einer Transaktion.
* Doppelte E-Mail-Adressen werden durch den Primary Key verhindert.
*
* @param string $email E-Mail-Adresse des Benutzers
* @param string $vorname Vorname des Benutzers
* @param string $nachname Nachname des Benutzers
* @param string $password Passwort-Hash des Benutzers
*
* @return void
*
* @throws InvalidArgumentException wenn die E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn der Benutzer nicht gespeichert werden kann
*/
public function addUser($email, $vorname, $nachname, $password)
{
try {
$db = $this->getConnection();
$db->beginTransaction();
$sql = "
INSERT INTO users (email, vorname, nachname, password)
VALUES (:email, :vorname, :nachname, :password)
";
$command = $db->prepare($sql);
$command->execute([
":email" => $email,
":vorname" => $vorname,
":nachname" => $nachname,
":password" => $password
]);
$db->commit();
} catch (PDOException $e) {
if (isset($db) && $db->inTransaction()) {
$db->rollBack();
}
if ($e->getCode() === "23000") {
throw new InvalidArgumentException("Diese E-Mail-Adresse wird bereits verwendet.");
}
throw new RuntimeException("Benutzer konnte nicht gespeichert werden.");
}
}
/**
* Aktualisiert die Daten eines bestehenden Benutzers.
*
* Optional kann zusätzlich das Passwort geändert werden.
* Wenn kein neues Passwort übergeben wird, bleibt das alte Passwort erhalten.
*
* @param string $oldEmail Aktuelle E-Mail-Adresse des Benutzers
* @param string $newEmail Neue E-Mail-Adresse des Benutzers
* @param string $vorname Neuer Vorname des Benutzers
* @param string $nachname Neuer Nachname des Benutzers
* @param string|null $password Neues Passwort oder null
*
* @return bool true, wenn der Benutzer aktualisiert wurde,
* sonst false
*
* @throws InvalidArgumentException wenn die neue E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn der Benutzer nicht aktualisiert werden kann
*/
public function updateUser($oldEmail, $newEmail, $vorname, $nachname, $password = null)
{
try {
$db = $this->getConnection();
if (!empty($password)) {
$sql = "
UPDATE users
SET email = :newEmail,
vorname = :vorname,
nachname = :nachname,
password = :password
WHERE email = :oldEmail
";
$params = [
":newEmail" => $newEmail,
":vorname" => $vorname,
":nachname" => $nachname,
":password" => password_hash($password, PASSWORD_DEFAULT),
":oldEmail" => $oldEmail
];
} else {
$sql = "
UPDATE users
SET email = :newEmail,
vorname = :vorname,
nachname = :nachname
WHERE email = :oldEmail
";
$params = [
":newEmail" => $newEmail,
":vorname" => $vorname,
":nachname" => $nachname,
":oldEmail" => $oldEmail
];
}
$command = $db->prepare($sql);
$command->execute($params);
return $command->rowCount() > 0;
} catch (PDOException $e) {
if ($e->getCode() === "23000") {
throw new InvalidArgumentException("Diese E-Mail-Adresse wird bereits verwendet.");
}
throw new RuntimeException("Benutzer konnte nicht aktualisiert werden.");
}
}
/**
* Löscht einen Benutzer anhand seiner E-Mail-Adresse.
*
* @param string $email E-Mail-Adresse des zu löschenden Benutzers
*
* @return bool true, wenn der Benutzer gelöscht wurde,
* sonst false
*
* @throws RuntimeException wenn der Benutzer nicht gelöscht werden kann
*/
public function deleteUser($email)
{
try {
$db = $this->getConnection();
$sql = "DELETE FROM users WHERE email = :email";
$command = $db->prepare($sql);
$command->execute([
":email" => $email
]);
return $command->rowCount() > 0;
} catch (PDOException $e) {
throw new RuntimeException("Benutzer konnte nicht gelöscht werden.");
}
}
}
+301
View File
@@ -0,0 +1,301 @@
<?php
require_once 'ArticleManagerDAO.php';
require_once 'Article.php';
/**
* Klasse: Eine lokale Lösung des ArticleManagerDAO.
* Schreibt zunächst Daten lokal in das Verzeichnis und kann diese wieder auslesen.
*
* @author Niklas Ortmann
*/
class LocalArticleManager implements ArticleManagerDAO {
private $file;
private static $instance = null;
/**
* Konstruktor
*/
public function __construct()
{
$this->file = __DIR__ . '/../../data/articles.json';
}
/**
* Gibt die LocalArticleManager-Instanz zurück.
* @return LocalArticleManager
*/
public static function getInstance()
{
if (self::$instance == null) {
self::$instance = new LocalArticleManager();
}
return self::$instance;
}
/**
* Speichert alle Artikel/Beiträge in der Datei.
* @param $articles
* @return void
* @throws InternalServerErrorException
*/
private function saveArticle($articles)
{
try{
file_put_contents(
$this->file,
json_encode($articles, JSON_PRETTY_PRINT)
);
}catch (Exception $e){
throw new InternalServerErrorException($e->getMessage());
}
}
public function addArticle($title, $content, $author, $category, $tags)
{
$articles = $this->getAllArticles();
$articles[] = [
"id" => count($articles)+1,
"title" => $title,
"content" => $content,
"author" => $author,
"category" => $category,
"tags" => $tags,
"creationDate" => date("Y-m-d H:i:s"),
"likes" => []
];
$this->saveArticle($articles);
}
public function updateArticle($id, $article, $author)
{
if (empty($article)) {
throw new NotFoundException("not_found_article");
}
// Berechtigungsprüfung:
if ($article->getAuthor() !== $author) {
throw new UnauthorizedAccessException("unauthorized_access");
}
// Beitrag aktualisieren:
$articles = $this->getAllArticles();
$updated = false;
foreach ($articles as $index => $storedArticle) {
if (isset($storedArticle['id']) && $storedArticle['id'] == $id) {
$articles[$index] = [
"id" => $id,
"title" => $article->getTitle(),
"content" => $article->getContent(),
"author" => $author,
"category" => $article->getCategory(),
"tags" => $article->getTags(),
"creationDate" => $article->getCreationDate(),
"likes" => $storedArticle['likes'] ?? []
];
$updated = true;
break;
}
}
// Nur speichern, wenn Beitrag geändert wurde:
if ($updated) {
$this->saveArticle($articles);
} else {
throw new NotFoundException("missing_id");
}
}
public function deleteArticle($id, $author)
{
$article = getArticle($id);
if (empty($article)) {
throw new NotFoundException("not_found_article");
}
// Berechtigungsprüfung:
if ($article->getAuthor() !== $author) {
throw new UnauthorizedAccessException("unauthorized_access");
}
$articles = $this->getAllArticles();
$articleFound = false;
foreach ($articles as $index => $article) {
if (isset($article['id']) && $article['id'] == $id) {
unset($articles[$index]);
$articleFound = true;
break; // Schleife abbrechen, da die ID eindeutig ist
}
}
if ($articleFound) {
// array_values stellt sicher, dass die Array-Keys wieder fortlaufend bei 0 beginnen
$this->saveArticle(array_values($articles));
}
}
public function getArticle($id)
{
$articles = $this->getAllArticles();
foreach ($articles as $article) {
if (isset($article['id']) && $article['id'] == $id) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
return new Article(
intval($article['id']),
$article['title'],
$article['content'],
$article['author'],
$article['category'],
$article['tags'],
$article['creationDate'],
$likes
);
}
}
return null;
}
public function getAllArticles(): array
{
if (!file_exists($this->file)) {
return [];
}
$json = file_get_contents($this->file);
$articles = json_decode($json, true);
return is_array($articles) ? $articles : [];
}
public function getArticlesByAuthor($author)
{
$articles = $this->getAllArticles();
$filteredArticles = [];
foreach ($articles as $article) {
if (isset($article['author']) && $article['author'] == $author) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article(
intval($article['id']),
$article['title'],
$article['content'],
$article['author'],
$article['category'],
$article['tags'],
$article['creationDate'],
$likes
);
}
}
return $filteredArticles;
}
public function search(string $keyword): array
{
$articles = $this->getAllArticles();
$filteredArticles = [];
if (!is_array($articles)) {
return [];
}
$cleanKeyword = strtolower(trim($keyword));
foreach ($articles as $article) {
$title = isset($article['title']) ? strtolower((string)$article['title']) : '';
$content = isset($article['content']) ? strtolower((string)$article['content']) : '';
$tags = isset($article['tags']) ? strtolower((string)$article['tags']) : '';
if (($cleanKeyword !== '' && strpos($title, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($content, $cleanKeyword) !== false) ||
($cleanKeyword !== '' && strpos($tags, $cleanKeyword) !== false)) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article(
intval($article['id'] ?? 0),
$article['title'] ?? '',
$article['content'] ?? '',
$article['author'] ?? '',
$article['category'] ?? '',
$article['tags'] ?? '',
$article['creationDate'] ?? '',
$likes
);
}
}
return $filteredArticles;
}
public function getArticlesByCategory($category)
{
$articles = $this->getAllArticles();
$filteredArticles = [];
foreach ($articles as $article) {
if (isset($article['category']) && $article['category'] == $category) {
$likes = isset($article['likes']) && is_array($article['likes']) ? $article['likes'] : [];
$filteredArticles[] = new Article(
intval($article['id']),
$article['title'],
$article['content'],
$article['author'],
$article['category'],
$article['tags'],
$article['creationDate'],
$likes
);
}
}
return $filteredArticles;
}
public function toggleLike(int $articleId, string $userId): bool
{
$articles = $this->getAllArticles();
$articleFound = false;
$isLikedNow = false;
foreach ($articles as $index => $article) {
if (isset($article['id']) && $article['id'] == $articleId) {
$articleFound = true;
// Likes-Array initialisieren, falls nicht vorhanden
if (!isset($articles[$index]['likes']) || !is_array($articles[$index]['likes'])) {
$articles[$index]['likes'] = [];
}
$likeIndex = array_search($userId, $articles[$index]['likes']);
if ($likeIndex !== false) {
// Bereits geliked -> Unlike
unset($articles[$index]['likes'][$likeIndex]);
// Array-Keys neu indizieren, damit JSON sauber bleibt
$articles[$index]['likes'] = array_values($articles[$index]['likes']);
$isLikedNow = false;
} else {
// Noch nicht geliked -> Like (User-ID hinzufügen)
$articles[$index]['likes'][] = $userId;
$isLikedNow = true;
}
break;
}
}
if (!$articleFound) {
throw new NotFoundException("missing_id");
}
$this->saveArticle($articles);
return $isLikedNow;
}
}
?>
+185
View File
@@ -0,0 +1,185 @@
<?php
require_once "UserManagerDAO.php";
class LocalUserManager implements UserManagerDAO {
private string $file = "data/users.json";
public static function getInstance(){
// TODO: implement the getIsntance method.
}
/**
* Lädt alle Benutzer aus der JSON-Datei.
*
* @return array Liste aller Benutzer
* @throws RuntimeException wenn die Datei nicht gelesen werden kann
* oder fehlerhafte JSON-Daten enthält
*/
private function loadUsers() {
if (!file_exists($this->file)) {
return [];
}
$json = file_get_contents($this->file);
if ($json === false) {
throw new RuntimeException("Benutzerdaten konnten nicht gelesen werden.");
}
$users = json_decode($json, true);
if ($users === null && json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("Benutzerdaten sind fehlerhaft.");
}
return is_array($users) ? $users : [];
}
/**
* Speichert alle Benutzer in die JSON-Datei.
*
* @param array $users Liste aller Benutzer
* @return void
* @throws RuntimeException wenn die Daten nicht gespeichert werden können
*/
private function saveUsers($users) {
$json = json_encode($users, JSON_PRETTY_PRINT);
if ($json === false) {
throw new RuntimeException("Benutzerdaten konnten nicht umgewandelt werden.");
}
$result = file_put_contents($this->file, $json);
if ($result === false) {
throw new RuntimeException("Benutzerdaten konnten nicht gespeichert werden.");
}
}
/**
* Sucht einen Benutzer anhand seiner E-Mail-Adresse.
*
* @param string $email E-Mail-Adresse des gesuchten Benutzers
* @return array|null Benutzerdaten oder null, wenn kein Benutzer gefunden wurde
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen werden können
*/
public function findUser($email) {
$users = $this->loadUsers();
foreach ($users as $user) {
if (isset($user["email"]) && $user["email"] === $email) {
if (!isset($user["vorname"]) && isset($user["username"])) {
$nameParts = explode(" ", $user["username"], 2);
$user["vorname"] = $nameParts[0] ?? "";
$user["nachname"] = $nameParts[1] ?? "";
}
return $user;
}
}
return null;
}
/**
* Fügt einen neuen Benutzer hinzu.
*
* @param string $email E-Mail-Adresse
* @param string $vorname Vorname
* @param string $nachname Nachname
* @param string $password Passwort-Hash
* @return void
* @throws InvalidArgumentException wenn die E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function addUser($email, $vorname, $nachname, $password) {
$users = $this->loadUsers();
foreach ($users as $user) {
if (isset($user["email"]) && $user["email"] === $email) {
throw new InvalidArgumentException("Diese E-Mail-Adresse wird bereits verwendet.");
}
}
$users[] = [
"email" => $email,
"vorname" => $vorname,
"nachname" => $nachname,
"password" => $password
];
$this->saveUsers($users);
}
/**
* Aktualisiert einen bestehenden Benutzer.
*
* @param string $oldEmail Alte E-Mail-Adresse
* @param string $newEmail Neue E-Mail-Adresse
* @param string $vorname Neuer Vorname
* @param string $nachname Neuer Nachname
* @param string|null $password Neues Passwort oder null
* @return bool true, wenn der Benutzer aktualisiert wurde, sonst false
* @throws InvalidArgumentException wenn die neue E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function updateUser($oldEmail, $newEmail, $vorname, $nachname, $password = null) {
$users = $this->loadUsers();
foreach ($users as $user) {
if (
isset($user["email"])
&& $user["email"] === $newEmail
&& $newEmail !== $oldEmail
) {
throw new InvalidArgumentException("Diese E-Mail-Adresse wird bereits verwendet.");
}
}
foreach ($users as $i => $user) {
if (isset($user["email"]) && $user["email"] === $oldEmail) {
$users[$i]["email"] = $newEmail;
$users[$i]["vorname"] = $vorname;
$users[$i]["nachname"] = $nachname;
unset($users[$i]["username"]);
if (!empty($password)) {
$users[$i]["password"] = password_hash($password, PASSWORD_DEFAULT);
}
$this->saveUsers($users);
return true;
}
}
return false;
}
/**
* Löscht einen Benutzer anhand seiner E-Mail-Adresse.
*
* TODO: wenn ein Benutzer gelöscht wird, sollten dann auch seine Beiträge gelöscht werden?
*
* @param string $email E-Mail-Adresse des zu löschenden Benutzers
* @return bool true, wenn der Benutzer gelöscht wurde, sonst false
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function deleteUser($email) {
$users = $this->loadUsers();
foreach ($users as $i => $user) {
if (isset($user["email"]) && $user["email"] === $email) {
unset($users[$i]);
$users = array_values($users);
$this->saveUsers($users);
return true;
}
}
return false;
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once "LocalUserManager.php";
require_once "DatabaseUserManager.php";
/**
* Zentrale Klasse für den Zugriff auf Benutzerdaten.
*
* Hier kann zwischen lokaler Speicherung und Datenbankspeicherung
* gewechselt werden.
*/
class UserManager {
public static function getInstance() {
return DatabaseUserManager::getInstance();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* Interface für den Zugriff auf Benutzerdaten.
*
* Definiert die Methoden, die jede UserDAO-Implementierung
* bereitstellen muss.
*/
interface UserManagerDAO {
/**
* Sucht einen Benutzer anhand seiner E-Mail-Adresse.
*
* Funktion:
* Liefert die gespeicherten Benutzerdaten zu einer E-Mail-Adresse.
*
* Eingabe:
* @param string $email E-Mail-Adresse des gesuchten Benutzers
*
* Ausgabe:
* @return array|null Benutzerdaten als Array oder null
*
* Mögliche Fehler:
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen werden können
*/
public function findUser($email);
/**
* Fügt einen neuen Benutzer hinzu.
*
* Funktion:
* Erstellt einen neuen Benutzereintrag und speichert ihn
* in der jeweiligen Datenquelle.
*
* Eingabe:
* @param string $email E-Mail-Adresse des Benutzers
* @param string $vorname Vorname des Benutzers
* @param string $nachname Nachname des Benutzers
* @param string $password Passwort des Benutzers
*
* Ausgabe:
* @return void
*
* Mögliche Fehler:
* @throws InvalidArgumentException wenn die E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function addUser($email, $vorname, $nachname, $password);
/**
* Aktualisiert einen bestehenden Benutzer.
*
* Funktion:
* Ändert die E-Mail-Adresse, den Vornamen, den Nachnamen
* und optional das Passwort eines bestehenden Benutzers.
*
* Eingabe:
* @param string $oldEmail Alte E-Mail-Adresse
* @param string $newEmail Neue E-Mail-Adresse
* @param string $vorname Neuer Vorname
* @param string $nachname Neuer Nachname
* @param string|null $password Neues Passwort oder null
*
* Ausgabe:
* @return bool true, wenn der Benutzer aktualisiert wurde, sonst false
*
* Mögliche Fehler:
* @throws InvalidArgumentException wenn die neue E-Mail-Adresse bereits verwendet wird
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function updateUser($oldEmail, $newEmail, $vorname, $nachname, $password = null);
/**
* Löscht einen Benutzer anhand seiner E-Mail-Adresse.
*
* Funktion:
* Entfernt einen vorhandenen Benutzer aus der Datenquelle.
*
* Eingabe:
* @param string $email E-Mail-Adresse des zu löschenden Benutzers
*
* Ausgabe:
* @return bool true, wenn der Benutzer gelöscht wurde, sonst false
*
* Mögliche Fehler:
* @throws RuntimeException wenn die Benutzerdaten nicht gelesen oder gespeichert werden können
*/
public function deleteUser($email);
}
+151
View File
@@ -0,0 +1,151 @@
<?php
/**
* Prüft, ob der Titel die folgenden Bedingungen erfüllt:
* Buchstaben von a-z; A-Z
* Zahlen von 0-9
* Umlaute äöüÄÖÜß
* Satzeichen .,!?:;()"„“«»_+-
* 5-120 Zeichen
* @param $title
* @return bool
*/
function articleTitleValidator($title)
{
$title = trim($title);
$titlePattern = '/^[a-zA-Z0-9äöüÄÖÜß\s.,!?:;()\'"„“«»_+-]{5,120}$/u';
if (preg_match($titlePattern, $title)) {
return true;
} else {
return false;
}
}
/**
* Prüft, ob der Content valides JSON ist UND ob alle enthaltenen Blöcke
* die inhaltlichen Kriterien erfüllen:
* Textblöcke sind nicht leer
* Bilder sind in Bildblöcken vorhanden
*
* @param mixed $content Der zu prüfende JSON-String
* @return bool
*/
function articleContentValidator($content)
{
// 1. Grundlegende Typprüfung
if (!is_string($content)) {
return false;
}
// 2. Formale JSON-Prüfung (Kompatibel mit PHP 8.2)
$blocks = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return false;
}
// 3. Inhaltliche Validierung der einzelnen Blöcke
// Falls das JSON zwar valide, aber kein Array ist (z.B. nur ein String/Zahl)
if (!is_array($blocks)) {
return false;
}
// Mindestens ein Block sollte vorhanden sein (optional, verhindert leere Beiträge)
if (empty($blocks)) {
return false;
}
foreach ($blocks as $block) {
// Jeder Block muss die Keys 'type' und 'value' besitzen
if (!isset($block['type']) || !isset($block['value'])) {
return false;
}
$type = $block['type'];
$value = $block['value'];
if ($type === 'text') {
// Validierung für Text: Darf nach dem Trimmen nicht leer sein
if (trim($value) === '') {
return false;
}
} elseif ($type === 'image') {
// Validierung für Bild: Muss entweder mit uploads/ starten (Bestand)
// oder mit data:image/ beginnen (neues Base64-Bild aus dem Editor)
if (!is_string($value)) {
return false;
}
$isValidPath = str_starts_with($value, 'uploads/');
$isValidBase64 = str_starts_with($value, 'data:image/');
if (!$isValidPath && !$isValidBase64) {
return false;
}
} else {
// Unbekannter Blocktyp wird zur Sicherheit abgewiesen
return false;
}
}
// Wenn alle Prüfungen bestanden wurden
return true;
}
/**
* Prüft, ob die Kategorie eine erlaubt Kategorie ist.
* @param $category
* @return bool
*/
function articleCategoryValidator($category)
{
$allowedCategories = [
'deutsch', 'englisch', 'franzoesisch', 'latein', 'literatur',
'mathe', 'biologie', 'chemie', 'physik', 'informatik', 'astronomie',
'geschichte', 'erdkunde', 'sozialkunde', 'wirtschaft', 'religion',
'ethik', 'philosophie', 'psychologie', 'kunst', 'musik', 'theater',
'technik', 'werken', 'hauswirtschaft', 'sport'
];
if (in_array($category, $allowedCategories, true)) {
return true;
} else {
return false;
}
}
/**
* Prüft, ob die Tags die folgenden Bedingungen erfüllen:
* Buchstaben von a-z; A-Z
* Zahlen von 0-9
* Umlaute äöüÄÖÜß
* Satzeichen -
* 2-50 Zeichen
* @param $tags
* @return bool
*/
function articleTagValidator($tags)
{
if (!isset($tags)) {
$tags = '';
}
$rawTags = explode(',', $tags);
foreach ($rawTags as $rawTag) {
// Leerzeichen am Anfang/Ende des einzelnen Tags entfernen:
$tag = trim($rawTag);
// leere Elemente überspringen:
if ($tag === '') {
continue;
}
// Tag mit Regex prüfen:
$tagPattern = '/^[a-zA-Z0-9äöüÄÖÜß\s-]{2,50}$/u';
if (!preg_match($tagPattern, $tag)) {
return false;
}
}
return true;
}
?>
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* Prüft, ob eine Suchanfrage gültig ist.
*
* Erlaubt werden Buchstaben, Zahlen, Umlaute, typische Satzzeichen und Leerzeichen.
* Die Länge muss zwischen 1 und 50 Zeichen liegen.
*
* @param string $query Zu prüfender Suchbegriff
*
* @return bool true wenn die Suche gültig ist, sonst false
*/
function searchQueryValidator($query)
{
$query = trim($query);
// Mindestens 1 Zeichen, maximal 50 Zeichen
$length = mb_strlen($query);
if ($length < 1 || $length > 50) {
return false;
}
// Erlaubt Buchstaben (inkl. Umlaut/ß), Zahlen, Leerzeichen sowie ?, !, ., -, _
$searchPattern = '/^[a-zA-Z0-9äöüÄÖÜß\s?!.,\-_]+$/u';
return preg_match($searchPattern, $query) === 1;
}
/**
* Prüft, ob das gewählte Treffer-Limit erlaubt ist.
*
* @param int|string $limit Das zu prüfende Limit
*
* @return bool true wenn das Limit 10, 20, 50 oder 100 ist, sonst false
*/
function searchLimitValidator($limit)
{
$allowedLimits = [10, 20, 50, 100];
return in_array((int)$limit, $allowedLimits, true);
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* Prüft, ob ein Vor- oder Nachname gültig ist.
*
* Erlaubt werden Buchstaben, Umlaute, Leerzeichen und Bindestriche.
* Die Länge muss zwischen 2 und 20 Zeichen liegen.
*
* @param string $name Zu prüfender Name
*
* @return bool true wenn der Name gültig ist, sonst false
*/
function userNameValidator($name)
{
$name = trim($name);
$namePattern = '/^[a-zA-ZäöüÄÖÜß\s-]{2,20}$/u';
return preg_match($namePattern, $name) === 1;
}
/**
* Prüft, ob eine E-Mail-Adresse gültig ist.
*
* Die E-Mail-Adresse muss dem Standardformat entsprechen
* und darf höchstens 100 Zeichen lang sein.
*
* @param string $email Zu prüfende E-Mail-Adresse
*
* @return bool true wenn die E-Mail-Adresse gültig ist, sonst false
*/
function userEmailValidator($email)
{
$email = trim($email);
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false
&& mb_strlen($email) <= 100;
}
/**
* Prüft, ob ein Passwort die Längenanforderungen erfüllt.
*
* Das Passwort muss zwischen 5 und 12 Zeichen lang sein.
*
* @param string $password Zu prüfendes Passwort
*
* @return bool true wenn das Passwort gültig ist, sonst false
*/
function userPasswordValidator($password)
{
$zeichenAnzahl = mb_strlen($password);
return $zeichenAnzahl >= 5 && $zeichenAnzahl <= 12;
}
/**
* Prüft ein optionales Passwort.
*
* Ein leeres Passwort ist erlaubt und bedeutet,
* dass das bestehende Passwort unverändert bleibt.
* Falls ein Passwort angegeben wurde, wird es
* mit den normalen Passwortregeln geprüft.
*
* @param string|null $password Zu prüfendes Passwort
*
* @return bool true wenn das Passwort gültig oder leer ist,
* sonst false
*/
function userOptionalPasswordValidator($password)
{
if (!isset($password) || $password === '') {
return true;
}
return userPasswordValidator($password);
}
-51
View File
@@ -1,51 +0,0 @@
<!--
Seite: Physik
Inhalt: Eine Übersicht über alle Beiträge zum Fach
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Physik</h1>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat,
sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no
sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<h2>Artikel:</h2>
<div>
<a href="tunneleffekt.php">Der Tunneleffekt</a>
</div>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
Binary file not shown.
-63
View File
@@ -1,63 +0,0 @@
<!--
Seite: Profil
Inhalt: Das eigene Profil, wenn man angemeldet ist. Dort hat man die Möglichkeit seine Angaben zu ändern.
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<hr>
<div>
<div>
<h1>Benutzerdaten ändern</h1>
</div>
<div>
<div>
<label id="Vorname">Vorname</label>
<input type="text" name="Vorname" required placeholder="Vorname" value="<?php //DB-Daten ?>">
</div>
<div>
<label id="Nachname">Nachname</label>
<input type="text" name="Nachname" required placeholder="Nachname" value="<?php //DB-Daten ?>">
</div>
<div>
<label id="Email">Email-Adresse</label>
<input type="email" name="Email" required placeholder="mustermann@web.de" value="<?php //DB-Daten ?>">
</div>
<div>
<label id="Passwort">Passwort</label>
<input type="password" name="Passwort" required placeholder="Passwort">
</div>
<div>
<br>
<button type="submit" name="submit"><i></i> Speichern</button>
</div>
</div>
</div>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>
-52
View File
@@ -1,52 +0,0 @@
<!--
Artikel: Satz des Pythagoras
Funktion: Inhalt zum Fach Mathe
-->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="description" content="EduForge">
<meta name="author" content="Niklas Ortmann">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/x-icon" href="images/logos/logo_icon.ico">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/navbar.css">
<link rel="stylesheet" href="css/footer.css">
<title>EduForge</title>
</head>
<body>
<?php
include_once 'includes/navbar.php';
?>
<main>
<h1>Satz des Pythagoras</h1>
<p>
Der Satz des Pythagoras wurde von dem griechischen Philosophen Pythagoras von Samos formuliert und im
dritten Jahrhundert vor Christus veröffentlicht.
</p>
<figure>
<img src="https://cdn8.picryl.com/photo/2016/05/14/pythagoras-e9560b-1024.jpg" alt="Trulli" style="width:10%">
<figcaption><a href="https://cdn8.picryl.com/photo/2016/05/14/pythagoras-e9560b-1024.jpg">Quelle</a></figcaption>
</figure>
<p>
In der beigefügten Abbildung sehen wir ein rechtwinkliges Dreieck, dessen drei Seiten die Längen a, b und c
besitzen. Auf jeder Seite ist ein Quadrat konstruiert. Das Quadrat auf der Seite a hat die Fläche a2, das
Quadrat auf der Seite b hat die Fläche b2 und das Quadrat auf der Seite c hat die Fläche c2. Der Satz des
Pythagoras besagt, dass die Summe der Flächen der beiden kleineren Quadrate gleich der Fläche des größten
Quadrats ist. Das bedeutet, dass a^2+b^2=c^2 (Satz des Pythagoras).
</p>
<a href="docs/Uebungsaufgaben_Pythagoras.pdf">Lernzettel-Download</a>
</main>
<?php
include_once 'includes/footer.php';
?>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More