Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image upload support! #34

Merged
merged 9 commits into from Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@
/data/cache/
/vendor/
src/config.ini
/src/assets/
1 change: 1 addition & 0 deletions .idea/lamb.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion docs/caddy.md
Expand Up @@ -4,11 +4,13 @@ A working Caddyfile is provided in the project root.

## PHP-FPM

The `data` directory must be writable by the user php-fpm runs under, this is usually `www-data`:
The `data` and `src/assets` directory must be writable by the user php-fpm runs under, this is usually `www-data`:

```shell
sudo chown $USER:www-data data -R
sudo chmod g+w data -R
sudo chown $USER:www-data src/assets -R
sudo chmod g+w src/assets -R
```

To allow logins, add the output of `HIDDEN=1 php make_password_hash.php hackme` (don't use hackme) as an
Expand Down
4 changes: 3 additions & 1 deletion docs/nginx.md
Expand Up @@ -7,11 +7,13 @@ Update the `lamb.test` file to point to your preferred server_name, logs and doc

## PHP-FPM

The `data` directory must be writable by the user php-fpm runs under, this is usually `www-data`:
The `data` and `src/assets` directory must be writable by the user php-fpm runs under, this is usually `www-data`:

```shell
sudo chown $USER:www-data data -R
sudo chmod g+w data -R
sudo chown $USER:www-data src/assets -R
sudo chmod g+w src/assets -R
```

To allow logins, add the output of `HIDDEN=1 php make_password_hash.php hackme` (don't use hackme) as an
Expand Down
4 changes: 4 additions & 0 deletions src/css/styles.css
Expand Up @@ -39,6 +39,10 @@ pre code {
padding: 0.5em;
}

img {
max-width: 100%;
}

input[type='submit'], button {
cursor: pointer;
}
Expand Down
2 changes: 1 addition & 1 deletion src/index.php
Expand Up @@ -88,7 +88,6 @@ function post_has_slug( string $lookup ) : string|null {

# Bootstrap
header( 'Cache-Control: max-age=300' );
header( "Content-Security-Policy: default-src 'self'; img-src https:; object-src 'none'; require-trusted-types-for 'script'" );
session_start();
R::setup( 'sqlite:../data/lamb.db' );

Expand Down Expand Up @@ -116,6 +115,7 @@ function post_has_slug( string $lookup ) : string|null {
Route\register_route( 'search', __NAMESPACE__ . '\\Response\respond_search', $lookup );
Route\register_route( 'status', __NAMESPACE__ . '\\Response\respond_status', $lookup );
Route\register_route( 'tag', __NAMESPACE__ . '\\Response\respond_tag', $lookup );
Route\register_route( 'upload', __NAMESPACE__ . '\\Response\respond_upload', $lookup );

$template = $action;
if ( post_has_slug( $action ) === $action ) {
Expand Down
18 changes: 9 additions & 9 deletions src/js/logged_in/growing-input.js
@@ -1,13 +1,13 @@
(() => {
var ta = document.querySelector('textarea')
if (!ta) return
ta.style.overflow = 'hidden'
ta.addEventListener('keyup', growing_input)
growing_input({ 'target': ta })
var ta = document.querySelector('textarea')
if (!ta) return
ta.style.overflow = 'hidden'
ta.addEventListener('input', growing_input)
growing_input({'target': ta})
})()

function growing_input (ev) {
const target = ev.target
target.style.height = 'auto'
target.style.height = (10 + target.scrollHeight) + 'px'
function growing_input(ev) {
const target = ev.target
target.style.height = 'auto'
target.style.height = (10 + target.scrollHeight) + 'px'
}
43 changes: 43 additions & 0 deletions src/js/logged_in/upload-image.js
@@ -0,0 +1,43 @@
document.addEventListener('DOMContentLoaded', function () {
const textarea = document.getElementById('contents');

textarea.addEventListener('dragover', function (event) {
event.stopPropagation();
event.preventDefault();
});

textarea.addEventListener('drop', function (event) {
event.stopPropagation();
event.preventDefault();

const files = event.dataTransfer.files;
if (files.length > 0) {
handleFiles(files, textarea);
}
});
});

/**
* Handle files dropped into the textarea.
*
* @param {FileList} files
* @param {HTMLElement} textarea
*/
function handleFiles(files, textarea) {
const formData = new FormData();
for (const file of files) {
formData.append('imageFiles[]', file);
}
const currentText = textarea.value;
const cursorPosition = textarea.selectionStart;
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
textarea.value = currentText.slice(0, cursorPosition) + data + currentText.slice(cursorPosition);
textarea.dispatchEvent(new Event('input'));
})
.catch(error => console.error(error));
}
65 changes: 63 additions & 2 deletions src/response.php
Expand Up @@ -3,14 +3,17 @@
namespace Svandragt\Lamb\Response;

use JetBrains\PhpStorm\NoReturn;
use JsonException;
use RedBeanPHP\R;
use RedBeanPHP\RedException\SQL;
use Svandragt\Lamb\Security;
use Svandragt\Lamb\Config;
use Svandragt\Lamb\Security;
use function Svandragt\Lamb\Config\parse_matter;
use function Svandragt\Lamb\Route\is_reserved_route;
use function Svandragt\Lamb\transform;
use const ROOT_DIR;

const IMAGE_FILES = 'imageFiles';
#[NoReturn]
function redirect_404( $fallback ) : void {
global $request_uri;
Expand Down Expand Up @@ -195,7 +198,7 @@ function respond_edit( array $args ) : array {

# Atom feed
#[NoReturn]
function respond_feed() : array {
function respond_feed() : void {
global $config;
global $data;

Expand Down Expand Up @@ -279,3 +282,61 @@ function respond_tag( array $args ) : array {

return $data;
}

/**
* @param array $args
*
* @return void
* @throws JsonException
*/
#[NoReturn]
function respond_upload( array $args ) : void {
if ( empty( $_FILES[ IMAGE_FILES ] ) ) {
// invalid request http status code
header( 'HTTP/1.1 400 Bad Request' );
die( 'No files uploaded!' );
}
Security\require_login();

$files = [];
foreach ( $_FILES[ IMAGE_FILES ] as $name => $items ) {
foreach ( $items as $k => $value ) {
$files[ $k ][ $name ] = $_FILES[ IMAGE_FILES ][ $name ][ $k ];
}
}

$out = '';
foreach ( $files as $f ) {
if ( $f['error'] !== UPLOAD_ERR_OK ) {
// File upload failed
echo json_encode( 'File upload error: ' . $f['error'] );
die();
}
// File upload successful
$temp_fp = $f['tmp_name'];
$ext = pathinfo( $f['name'] )['extension'];
$new_fn = sha1( $f['name'] ) . ".$ext";
$new_fp = sprintf( "%s/%s", get_upload_dir(), $new_fn );
if ( ! move_uploaded_file( $temp_fp, $new_fp ) ) {
echo json_encode( 'Move upload error: ' . $temp_fp );
die();
}
$upload_url = str_replace( ROOT_DIR, ROOT_URL, get_upload_dir() );
$out .= sprintf( "![%s](%s)", $f['name'], "$upload_url/$new_fn" );
}

echo json_encode( $out, JSON_THROW_ON_ERROR );
die();
}

function get_upload_dir() {
// get an upload directory in the current directory based on YYYY/MM/filename.ext
$upload_dir = sprintf( "%s/assets/%s", ROOT_DIR, date( "Y/m" ) );
if ( ! is_dir( $upload_dir ) ) {
if ( ! mkdir( $upload_dir, 0777, true ) && ! is_dir( $upload_dir ) ) {
throw new \RuntimeException( sprintf( 'Directory "%s" was not created', $upload_dir ) );
}
}

return $upload_dir;
}
6 changes: 5 additions & 1 deletion src/views/actions/edit.php
Expand Up @@ -6,7 +6,11 @@
<h2> Edit Status</h2>

<form method="post" action="/edit" id="editform">
<textarea placeholder="What's happening?" name="contents" required><?= strip_tags( $post->body ); ?></textarea>
<textarea placeholder="What's happening?" name="contents" required
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)"
id="contents"
><?= strip_tags( $post->body ); ?></textarea>
<input type="hidden" name="id" value="<?= strip_tags( $post->id ); ?>"/>
<input type="submit" form="editform" name="submit" value="<?= SUBMIT_EDIT; ?>">
<input type="hidden" name="<?= HIDDEN_CSRF_NAME; ?>" value="<?= csrf_token(); ?>"/>
Expand Down
7 changes: 5 additions & 2 deletions src/views/actions/home.php
@@ -1,6 +1,9 @@
<?php if ( isset( $_SESSION[ SESSION_LOGIN ] ) ): ?>
<form method="post" action="/">
<textarea placeholder="What's happening?" name="contents" required></textarea>
<form method="post" action="/" enctype="multipart/form-data">
<textarea placeholder="What's happening?" name="contents" required
ondrop="handleDrop(event)"
ondragover="handleDragOver(event)" id="contents"
></textarea>
<input type="submit" name="submit" value="<?= SUBMIT_CREATE; ?>">
<input type="hidden" name="<?= HIDDEN_CSRF_NAME; ?>" value="<?= csrf_token(); ?>"/>
</form>
Expand Down
2 changes: 1 addition & 1 deletion src/views/html.php
Expand Up @@ -119,7 +119,7 @@ function the_styles() : void {

function the_scripts() : void {
$scripts = [
'logged_in' => [ '/growing-input.js', '/confirm-delete.js', '/link-edit-buttons.js' ],
'logged_in' => [ '/growing-input.js', '/confirm-delete.js', '/link-edit-buttons.js', '/upload-image.js' ],
];
$assets = asset_loader( $scripts, 'js' );
foreach ( $assets as $id => $href ) {
Expand Down