commit ad70c4d8354c76c3634e840a63a85e915edcfb24 Author: Vincent Rozenberg Date: Sat Sep 21 12:28:27 2024 +0200 Add files via upload diff --git a/index.php b/index.php new file mode 100644 index 0000000..e70c2a2 --- /dev/null +++ b/index.php @@ -0,0 +1,275 @@ + 'success', + 'message' => 'File ' . ($_POST['action'] === 'create' ? 'created' : 'updated') . ' successfully.' + ]; + } elseif ($_POST['action'] === 'delete') { + $filename = basename($_POST['filename']); + $filepath = $directory . '/' . $filename; + + if (strpos(realpath(dirname($filepath)), $directory) !== 0) { + die("Invalid file path"); + } + + if (is_file($filepath) && pathinfo($filepath, PATHINFO_EXTENSION) === 'md') { + unlink($filepath); + $_SESSION['alert'] = [ + 'type' => 'warning', + 'message' => 'File deleted successfully.' + ]; + } + } + } + header('Location: ' . $_SERVER['PHP_SELF']); + exit; +} + +$editFile = isset($_GET['edit']) ? basename($_GET['edit']) : null; +if ($editFile !== null && strpos($editFile, '..') !== false) { + $editFile = null; +} + +if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} + +// Display alert from session if it exists +if (isset($_SESSION['alert'])) { + $alertType = $_SESSION['alert']['type']; + $alertMessage = $_SESSION['alert']['message']; + $alert = ""; + unset($_SESSION['alert']); +} + +function renderFileListItem($file) { + $basename = basename($file); + return " +
  • +
    + " . htmlspecialchars($basename) . " + Last updated: " . date("F d, Y H:i", filemtime($file)) . " +
    +
    + + + + + + + + + +
    +
  • "; +} +?> + + + + + + + Vincent's MarkDown + + + + + + + + + +
    +
    + +
    +
    +

    +
    +
    +
    + + +
    + + +
    +
    + +
    + +
    +
    +
    + +
    +
    +

    Markdown Files

    +
    +
    +
      + +
    + + +
    +
    +

    + +

    +
    +
    +
      + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/license.md b/license.md new file mode 100644 index 0000000..79b5b54 --- /dev/null +++ b/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Vincent Rozenberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3115e03 --- /dev/null +++ b/readme.md @@ -0,0 +1,80 @@ +# Vincent's MarkDown + +This is a quick and dirty PHP-based web application for managing and viewing Markdown files. I designed it as a straightforward solution for personal use or small-scale deployments, prioritising simplicity over advanced features. + +## Disclaimer + +I want to be clear: this application is a basic, no-frills solution. I didn't design it for large-scale or high-security environments. Use it at your own risk and consider implementing additional security measures for sensitive deployments. + +## Features + +Here's what my app can do: +- Create and edit Markdown files with an easy-to-use interface +- Show a list of all Markdown files, sorted by last modified date +- Render Markdown files as HTML for easy viewing +- Delete unwanted files +- Copy shareable URLs for each file +- View files as raw HTML +- Responsive design for both desktop and mobile devices + +## Requirements + +To run this, you'll need: +- PHP 7.0 or higher +- Web server (e.g., Apache, Nginx) +- Modern web browser + +## Installation + +1. Clone this repository to your web server's document root or a subdirectory. +2. Make sure the `files` directory is writable by your web server. +3. Configure your web server to serve PHP files. +4. Access the application through your web browser. + +## Usage + +Here's how to use it: +- Create a new file: Enter a filename and content in the form at the top of the page, then click "Create File". +- Edit a file: Click the edit icon next to the file name in the list. +- View a file: Click on the file name or the eye icon. +- Delete a file: Click the trash icon and confirm the deletion. +- Copy a shareable URL: Click the copy icon next to the file. +- View a file as raw HTML: Click the code icon. + +## Security + +I've implemented some basic security measures: +- CSRF protection for form submissions +- Input sanitisation to prevent XSS attacks +- Directory traversal prevention +- Restriction on allowed file extensions + +But remember, this is a quick and dirty solution. It may not be suitable for handling sensitive information or use in high-security environments. Use at your own discretion and implement additional security measures as needed. + +## Customisation + +Feel free to customise the app! You can modify the appearance by editing the `styles.css` file. If you want to extend or modify the JavaScript functionality, check out the `scripts.js` file. + +## External Libraries + +I've used several external libraries to enhance the functionality. I'm using the latest versions available via CDN for each: + +- [Bootstrap](https://getbootstrap.com/) - For responsive design and UI components +- [EasyMDE](https://easymde.tk/) - For the Markdown editor +- [Font Awesome](https://fontawesome.com/) - For icons +- [Marked](https://marked.js.org/) - For Markdown parsing +- [Google Fonts](https://fonts.google.com/) (Roboto) - For typography + +I'm really grateful to the maintainers and contributors of these libraries for their excellent work. + +## License + +I've licensed this project under the MIT License. Check out the [LICENSE](LICENSE) file for details. + +## About Me + +[https://vincentrozenberg.com](https://vincentrozenberg.com). + +## Contributing + +Even though this is a simple, quick and dirty solution, I'm open to contributions! If you have improvements or bug fixes to suggest, please feel free to submit a Pull Request. diff --git a/scripts.js b/scripts.js new file mode 100644 index 0000000..c3174cc --- /dev/null +++ b/scripts.js @@ -0,0 +1,68 @@ +document.addEventListener('DOMContentLoaded', function() { + const easyMDE = new EasyMDE({ + element: document.getElementById('editor'), + spellChecker: false, + autosave: { + enabled: true, + uniqueId: "mdEditor", + delay: 1000, + }, + }); + + document.querySelectorAll('.copy-url').forEach(button => { + button.addEventListener('click', function(e) { + e.stopPropagation(); + const url = this.getAttribute('data-url'); + navigator.clipboard.writeText(url).then(() => { + const copyUrlModal = new bootstrap.Modal(document.getElementById('copyUrlModal')); + copyUrlModal.show(); + }); + }); + }); + + document.querySelectorAll('.file-info, .view-md').forEach(element => { + element.addEventListener('click', function() { + const filename = this.getAttribute('data-file'); + const timestamp = new Date().getTime(); + fetch(`files/${encodeURIComponent(filename)}?t=${timestamp}`) + .then(response => response.text()) + .then(markdown => { + const html = marked(markdown); + document.getElementById('viewModalBody').innerHTML = html; + document.getElementById('viewModalLabel').textContent = `Viewing: ${filename}`; + document.getElementById('modalEditBtn').href = `?edit=${encodeURIComponent(filename)}`; + document.getElementById('modalCopyUrlBtn').setAttribute('data-url', `${window.location.origin}${window.location.pathname}files/${encodeURIComponent(filename)}`); + document.getElementById('modalViewHtmlBtn').href = `view_html.php?file=${encodeURIComponent(filename)}`; + document.querySelector('#viewModal .delete-file').setAttribute('data-file', filename); + }); + }); + }); + + document.getElementById('modalCopyUrlBtn').addEventListener('click', function() { + const url = this.getAttribute('data-url'); + navigator.clipboard.writeText(url).then(() => { + const copyUrlModal = new bootstrap.Modal(document.getElementById('copyUrlModal')); + copyUrlModal.show(); + }); + }); + + document.querySelectorAll('.delete-file').forEach(button => { + button.addEventListener('click', function(e) { + const filename = this.getAttribute('data-file'); + document.getElementById('deleteFilename').value = filename; + document.getElementById('deleteModalLabel').textContent = `Confirm Deletion: ${filename}`; + }); + }); + + // Accordion functionality + const accordion = document.getElementById('olderFilesAccordion'); + if (accordion) { + accordion.addEventListener('show.bs.collapse', function () { + this.querySelector('.accordion-button').classList.remove('collapsed'); + }); + + accordion.addEventListener('hide.bs.collapse', function () { + this.querySelector('.accordion-button').classList.add('collapsed'); + }); + } +}); \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..6ec8b9b --- /dev/null +++ b/styles.css @@ -0,0 +1,219 @@ +body { + font-family: 'Roboto', sans-serif; + background-color: #ffffff; + color: #333333; +} + +.sticky-header { + position: sticky; + top: 0; + z-index: 1000; + background-color: #ffffff; + border-bottom: 1px solid #e0e0e0; +} + +.nav-brand { + font-size: 1.5rem; + text-decoration: none; + color: #333333; +} + +.nav-brand strong { + font-weight: 700; +} + +.nav-brand span { + font-weight: 300; +} + +.btn-icon { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1.5; + color: #333333; + border: 1px solid #e0e0e0; + background-color: #ffffff; +} + +.btn-icon:hover { + background-color: #f5f5f5; +} + +.card { + border: 1px solid #e0e0e0; + border-radius: 0; +} + +.card-header { + background-color: #f5f5f5; + border-bottom: 1px solid #e0e0e0; +} + +.list-group-item { + border: none; + border-bottom: 1px solid #e0e0e0; + padding: 1rem 0; +} + +.list-group-item:last-child { + border-bottom: none; +} + + + +.file-info { + display: flex; + flex-direction: column; + cursor: pointer; +} + +.file-name { + font-weight: 500; + color: #333333; +} + +.file-date { + font-size: 0.75rem; + color: #757575; +} + +.modal-content { + border-radius: 0; +} + +.modal-header { + border-bottom: 1px solid #e0e0e0; +} + +.btn-primary { + background-color: #4b4b4b; + border-color: #333333; +} + +.btn-primary:hover { + background-color: #1a1a1a; + border-color: #1a1a1a; +} + +/* Styles for view_html.php */ +.file-title { + font-size: 1rem; + color: #000000; + text-decoration: none; + margin-right: 0.5rem; +} + +.file-title:hover { + text-decoration: underline; +} + +.rendered-markdown { + line-height: 1.6; +} + +.rendered-markdown h1, +.rendered-markdown h2, +.rendered-markdown h3 { + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +.rendered-markdown ul, +.rendered-markdown ol { + padding-left: 2rem; + margin-bottom: 1rem; +} + +.rendered-markdown li { + margin-bottom: 0.5rem; +} + +.rendered-markdown p { + margin-bottom: 1rem; +} + +.rendered-markdown code { + background-color: #f8f9fa; + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; +} + +/* Styles for links in rendered Markdown and modal content */ +.rendered-markdown a, +#viewModalBody a { + color: #757575; + text-decoration: none; +} + +.rendered-markdown a:hover, +#viewModalBody a:hover { + border-bottom: 1px dotted #757575; +} + +/* Accordion styles */ +.accordion-button { + background-color: #f8f9fa; + color: #333333; + font-weight: 500; +} + +.accordion-button:not(.collapsed) { + background-color: #e9ecef; + color: #333333; +} + +.accordion-button:focus { + box-shadow: none; + border-color: rgba(0,0,0,.125); +} + +.accordion-body { + padding: 0; +} + +/* Responsive styles */ +@media (max-width: 767px) { + .sticky-header .container { + padding-left: 15px; + padding-right: 15px; + } + + .btn-icon { + padding: 0.2rem 0.4rem; + } +} + +@media (max-width: 575px) { + .file-info { + max-width: 60%; + } + + .list-group-item { + flex-wrap: wrap; + } + + .list-group-item > div:last-child { + margin-top: 0.5rem; + width: 100%; + display: flex; + justify-content: flex-end; + } + +} + +.accordion-body { + padding: 0.5em !important; +} + +.footer-text a {text-decoration: none!important; + color: #757575!important;} + +* { +border-radius: 0 !important; +} + +a { +color: #333333 !important; +text-decoration: none !important; +} + diff --git a/view_html.php b/view_html.php new file mode 100644 index 0000000..9d21d04 --- /dev/null +++ b/view_html.php @@ -0,0 +1,150 @@ +" . trim($matches[2]) . ""; + } + // List items + elseif (preg_match('/^(\s*[-*+])\s+(.+)$/', $line, $matches)) { + if (!$inList) { + $inList = true; + $listBuffer[] = ""; + $parsed = array_merge($parsed, $listBuffer); + $listBuffer = []; + $parsed[] = ""; // Add an empty line after the list + } + // Paragraphs + elseif (trim($line) !== '') { + $parsed[] = "

    " . $line . "

    "; + } + // Empty lines + else { + $parsed[] = ""; + } + } + + // Close any open list + if ($inList) { + $listBuffer[] = ""; + $parsed = array_merge($parsed, $listBuffer); + } + + $text = implode("\n", $parsed); + + // Inline formatting + $text = preg_replace('/\*\*(.+?)\*\*/', '$1', $text); + $text = preg_replace('/\*(.+?)\*/', '$1', $text); + $text = preg_replace('/`(.+?)`/', '$1', $text); + $text = preg_replace('/\[(.+?)\]\((.+?)\)/', '$1', $text); + + // Remove empty paragraphs + $text = preg_replace('/

    \s*<\/p>/', '', $text); + + return $text; +} + +$parsedContent = parseMarkdown($content); + +$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"; +$directUrl = $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; + +?> + + + + + + <?php echo htmlspecialchars($file); ?> - HTML View + + + + + + +

    +
    +
    +
    + +
    +
    +
    + + + + + + + + + \ No newline at end of file