added logging, updated theme and included info on the landing page

This commit is contained in:
Chris
2025-05-18 10:36:29 +02:00
parent 7344e8e06a
commit 6f1719fa78
15 changed files with 436 additions and 2492 deletions

View File

@@ -1,5 +1,7 @@
# Todos # Todos
- [ ] Caching in Redis for already handled URLs so it doesn't need to go through the whole algorithm again
- [ ] View stats in redis
- [ ] Keep track of deleted (and therefore forbidden) hashes - [ ] Keep track of deleted (and therefore forbidden) hashes
- [ ] Store list in Storage Controllers as well - [ ] Store list in Storage Controllers as well
- [ ] Add a way for users to report content - [ ] Add a way for users to report content

View File

@@ -31,6 +31,7 @@ services:
- FTP_BASEDIR= - FTP_BASEDIR=
- ENCRYPTION_KEY= - ENCRYPTION_KEY=
- ALWAYS_WEBP= - ALWAYS_WEBP=
- LOG_VIEWS=false
ports: ports:
- 8080:80 - 8080:80
volumes: volumes:

View File

@@ -29,6 +29,7 @@ services:
- FTP_BASEDIR= - FTP_BASEDIR=
- ENCRYPTION_KEY= - ENCRYPTION_KEY=
- ALWAYS_WEBP= - ALWAYS_WEBP=
- LOG_VIEWS=false
ports: ports:
- 8080:80 - 8080:80
volumes: volumes:

View File

@@ -55,6 +55,7 @@ _buildConfig() {
echo "define('ALWAYS_WEBP', ${ALWAYS_WEBP:-false});" echo "define('ALWAYS_WEBP', ${ALWAYS_WEBP:-false});"
echo "define('ALLOWED_DOMAINS', '${ALLOWED_DOMAINS:-}');" echo "define('ALLOWED_DOMAINS', '${ALLOWED_DOMAINS:-}');"
echo "define('SPLIT_DATA_DIR', ${SPLIT_DATA_DIR:-false});" echo "define('SPLIT_DATA_DIR', ${SPLIT_DATA_DIR:-false});"
echo "define('LOG_VIEWS', ${LOG_VIEWS:-false});"
} }

View File

@@ -22,7 +22,8 @@ In this file you can set the following options. For a simple working example con
| UPLOAD_CODE | string | If set, all uploads require this code via GET or POST variable "uploadcode" to succeed | | UPLOAD_CODE | string | If set, all uploads require this code via GET or POST variable "uploadcode" to succeed |
| REDIS_SERVER (NOT IMPLEMENTED) | IP | If you define a REDIS server IP here, it will enable you to use the FFMPEG Worker | | REDIS_SERVER (NOT IMPLEMENTED) | IP | If you define a REDIS server IP here, it will enable you to use the FFMPEG Worker |
| UPLOAD_QUOTA (NOT IMPLEMENTED) | int | Size in MB. If set, will only allow uploads if combined size of uploads on Server is smaller than this value. Does not account for ALT_FOLDER data and resized versions of original uploads won't be added to calculation | | UPLOAD_QUOTA (NOT IMPLEMENTED) | int | Size in MB. If set, will only allow uploads if combined size of uploads on Server is smaller than this value. Does not account for ALT_FOLDER data and resized versions of original uploads won't be added to calculation |
| MAX_RESIZED_IMAGES (NOT IMPLEMENTED | string | If set, limits count of resized images/videos per file on server | | MAX_RESIZED_IMAGES (NOT IMPLEMENTED ) | string | If set, limits count of resized images/videos per file on server |
| LOG_VIEWS | bool | If set, will log all views into the ./logs/app.log. Pretty resource heavy, only use for debugging purposes |
# Content controllers # Content controllers

View File

@@ -58,7 +58,7 @@ class API
//check if we should get a file from a remote URL //check if we should get a file from a remote URL
if ($_REQUEST['url']) { if ($_REQUEST['url']) {
$url = trim($_REQUEST['url']); $url = trim(rawurldecode($_REQUEST['url']));
if (checkURLForPrivateIPRange($url)) { if (checkURLForPrivateIPRange($url)) {
addToLog(getUserIP() . " tried to get us to download a file from: " . $url . " but it is in a private IP range"); addToLog(getUserIP() . " tried to get us to download a file from: " . $url . " but it is in a private IP range");
return ['status' => 'err', 'reason' => 'Private IP range']; return ['status' => 'err', 'reason' => 'Private IP range'];

View File

@@ -41,6 +41,8 @@ function architect($u)
if(isExistingHash($el)) if(isExistingHash($el))
{ {
$hash = $el; $hash = $el;
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash (".$_SERVER['HTTP_USER_AGENT'].")\tIt was locally found", ROOT.DS.'logs/views.log');
break; break;
} }
// if we don't have a hash yet but the element looks like it could be a hash // if we don't have a hash yet but the element looks like it could be a hash
@@ -59,6 +61,9 @@ function architect($u)
if(!file_exists(ROOT.DS.'tmp'.DS.$hash)) continue; if(!file_exists(ROOT.DS.'tmp'.DS.$hash)) continue;
storeFile(ROOT.DS.'tmp'.DS.$hash,$hash,true); storeFile(ROOT.DS.'tmp'.DS.$hash,$hash,true);
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash (".$_SERVER['HTTP_USER_AGENT'].")\tIt was found in Storage Controller $contr", ROOT.DS.'logs/views.log');
break; // we break here because we already have the file. no need to check other storage controllers break; // we break here because we already have the file. no need to check other storage controllers
} }
else if($c->isEnabled()===true && defined('ENCRYPTION_KEY') && ENCRYPTION_KEY !='' && $c->hashExists($el.'.enc')) //this is an encrypted file. Let's decrypt it else if($c->isEnabled()===true && defined('ENCRYPTION_KEY') && ENCRYPTION_KEY !='' && $c->hashExists($el.'.enc')) //this is an encrypted file. Let's decrypt it
@@ -73,6 +78,9 @@ function architect($u)
storeFile(ROOT.DS.'tmp'.DS.$hash,$hash,true); storeFile(ROOT.DS.'tmp'.DS.$hash,$hash,true);
unlink(ROOT.DS.'tmp'.DS.$el.'.enc'); unlink(ROOT.DS.'tmp'.DS.$el.'.enc');
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." viewed $hash (".$_SERVER['HTTP_USER_AGENT'].")\tIt was found encrypted in Storage Controller $contr", ROOT.DS.'logs/views.log');
break; // we break here because we already have the file. no need to check other storage controllers break; // we break here because we already have the file. no need to check other storage controllers
} }
} }
@@ -85,6 +93,8 @@ function architect($u)
{ {
if((new $cc)::ctype=='dynamic' && in_array((new $cc)->getRegisteredExtensions()[0],$u) ) if((new $cc)::ctype=='dynamic' && in_array((new $cc)->getRegisteredExtensions()[0],$u) )
{ {
if(defined('LOG_VIEWS') && LOG_VIEWS===true)
addToLog(getUserIP()." (".$_SERVER['HTTP_USER_AGENT'].") requested ".implode("/",$u)."\tIt's a dynamic image handled by $cc", ROOT.DS.'logs/views.log');
$hash = true; $hash = true;
break; break;
} }
@@ -112,6 +122,7 @@ function architect($u)
//@todo: allow MASTER_DELETE_IP to be CIDR range or coma separated //@todo: allow MASTER_DELETE_IP to be CIDR range or coma separated
if(getDeleteCodeOfHash($hash)==$code || (defined('MASTER_DELETE_CODE') && MASTER_DELETE_CODE==$code ) || (defined('MASTER_DELETE_IP') && MASTER_DELETE_IP==getUserIP()) ) if(getDeleteCodeOfHash($hash)==$code || (defined('MASTER_DELETE_CODE') && MASTER_DELETE_CODE==$code ) || (defined('MASTER_DELETE_IP') && MASTER_DELETE_IP==getUserIP()) )
{ {
addToLog(getUserIP()." (".$_SERVER['HTTP_USER_AGENT'].") deleted $hash");
deleteHash($hash); deleteHash($hash);
exit($hash.' deleted successfully'); exit($hash.' deleted successfully');
} }
@@ -1045,9 +1056,9 @@ function updateMetaData($hash, $meta)
} }
} }
function addToLog($data) function addToLog($data,$logfile=ROOT.DS.'logs/app.log')
{ {
$fp = fopen(ROOT.DS.'logs/app.log','a'); $fp = fopen($logfile,'a');
fwrite($fp,date("d.m.y H:i")."\t[".getURL()."] | ".$data."\n"); fwrite($fp,date("d.m.y H:i")."\t[".getURL()."] | ".$data."\n");
fclose($fp); fclose($fp);
} }

View File

@@ -1,6 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--[if IEMobile 7 ]> <html class="no-js iem7"> <![endif]--> <!--[if IEMobile 7 ]> <html class="no-js iem7"> <![endif]-->
<!--[if (gt IEMobile 7)|!(IEMobile)]><!--> <html class="no-js"> <!--<![endif]--> <!--[if (gt IEMobile 7)|!(IEMobile)]><!-->
<html class="no-js"> <!--<![endif]-->
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -13,6 +15,8 @@
<!-- PictShare overwrites --> <!-- PictShare overwrites -->
<link href="/css/pictshare.css" rel="stylesheet"> <link href="/css/pictshare.css" rel="stylesheet">
<link href="/css/dropzone.css" rel="stylesheet"> <link href="/css/dropzone.css" rel="stylesheet">
<link href="/css/hljs-dracula.css" rel="stylesheet">
<!-- github-fork-ribbon-css <!-- github-fork-ribbon-css
https://simonwhitaker.github.io/github-fork-ribbon-css/ --> https://simonwhitaker.github.io/github-fork-ribbon-css/ -->
@@ -39,6 +43,7 @@
<meta name="rating" content="general"> <meta name="rating" content="general">
</HEAD> </HEAD>
<BODY> <BODY>
<a class="github-fork-ribbon left-top" href="https://github.com/HaschekSolutions/pictshare" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a> <a class="github-fork-ribbon left-top" href="https://github.com/HaschekSolutions/pictshare" data-ribbon="Fork me on GitHub" title="Fork me on GitHub">Fork me on GitHub</a>
@@ -60,13 +65,12 @@
<?php } else { ?> <?php } else { ?>
<div id="uploadinfo"></div> <div id="uploadinfo"></div>
<p> <p>
Max Upload size: <?=(int)(ini_get('upload_max_filesize'))?>MB / File<br/>
Allowed file types: <?= implode(', ', getAllContentFiletypes()) ?>
<?php <?php
echo "Max Upload size: ". (int)(ini_get('upload_max_filesize'))."MB / File<br/>"; if (defined('UPLOAD_CODE') && UPLOAD_CODE != ''):?>
echo "Allowed file types: ". implode(', ',getAllContentFiletypes()); <br>Upload Code: <input type="password" id="uploadcode" />
<?php endif; ?>
if(defined('UPLOAD_CODE') && UPLOAD_CODE!='')
echo '<br>Upload Code: <input type="password" id="uploadcode" />';
?>
</p> </p>
<form class="dropzone well" id="dropzone" method="post" action="/api/upload" enctype="multipart/form-data"> <form class="dropzone well" id="dropzone" method="post" action="/api/upload" enctype="multipart/form-data">
<div class="fallback"> <div class="fallback">
@@ -78,13 +82,188 @@
</div> </div>
</div> </div>
</div>
<div class="container">
<h2 id="api" class="section-heading">Using PictShare</h2>
<div class="row">
<div class="col-6">
<h3>Basics</h3>
<p>
When you upload an image you'll get a link like this:<pre><code class="url"><?= getURL() ?>abcef123.jpg</code></pre>
You can modify the size of the image by adding a size parameter to the URL. For example this will render the image in 800x600:
<pre><code class="url"><?= getURL() ?>800x600/abcef123.jpg</code></pre>
</p>
<p>
If you want to force the size to really be 800x600, you can use the "forcesize" parameter. It will still keep the aspect ratio but zoom in as needed to fit the dimensions
<pre><code class="url"><?= getURL() ?>800x600/forcesize/abcef123.jpg</code></pre>
</p>
<p>There are many more of these <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/MODIFIERS.md">modifiers</a> and even <a target="_blank" href="https://github.com/HaschekSolutions/pictshare/blob/master/rtfm/IMAGEFILTERS.md">filters</a> available</p>
</div>
</div>
<hr class="no-print border border-primary border-2 opacity-50">
</div>
<div class="container">
<h2 id="api" class="section-heading">Using the API</h2>
<div class="row">
<div class="col-6">
<h2>Basics</h2>
<p>
All API calls are done via GET or POST requests. The API will return JSON encoded data.
</p>
Base URL
<pre><code class="url"><?= getURL() ?>api</code></pre>
</div>
<div class="col-6">
<h2>Error handling</h2>
When the status is err there will always be a field "reason" that explains what went wrong.
<pre><code class="json">
{
"status": "err",
"reason": "File not a valid image"
}
</code></pre>
</div>
<div class="w-100">
<hr />
</div>
<div class="col-6">
<h2>Uploading an image</h2>
API call
<pre><code class="url">/upload</code></pre>
<p>You can post a file using the POST variable <span class="badge text-bg-secondary">image</span></p>
CURL example
<pre><code class="bash">curl -s -F "file=@myphoto.jpg" "<?= getURL() ?>upload"</code></pre>
Output
<pre><code class="json">
{
"status": "ok",
"hash": "7eli4d.jpg",
"filetype": "image/jpeg",
"url": "http://localhost:8080/7eli4d.jpg",
"delete_code": "jxgat3wze8lmn9sqwxy4x32p2xm7211g",
"delete_url": "http://localhost:8080/delete_jxgat3wze8lmn9sqwxy4x32p2xm7211g/7eli4d.jpg"
}</code></pre>
</div>
<div class="col-6">
<h2>Grabbing a URL</h2>
API call
<pre><code class="url">/upload?url=<span class="badge text-bg-secondary">url of image</span></code></pre>
<p>You can specify a URL in the POST or GET variable <span class="badge text-bg-secondary">url</span>
that the call will try to download and process.</p>
CURL example
<pre><code class="bash">curl -s "<?= getURL() ?>api/upload?url=https://pictshare.net/d2j1e1.png</code></pre>
Output
<pre><code class="json">
{
"status": "ok",
"hash": "ysj455.webp",
"filetype": "image/webp",
"url": "http://localhost:8080/ysj455.webp",
"delete_code": "4l0w04l4s42xddt2s5mrj1wikxz11l5z",
"delete_url": "http://localhost:8080/delete_4l0w04l4s42xddt2s5mrj1wikxz11l5z/ysj455.webp"
}
</code></pre>
</div>
<div class="w-100">
<hr />
</div>
<div class="col-6">
<h2>Uploading Base64 encoded content</h2>
<p>It's also possible to upload supported files via Base64 strings by providing the <span class="badge text-bg-secondary">base64</span> http parameter</p>
API call
<pre><code class="url">/hash/<span class="badge badge-success">sha1 hash of file</span></code></pre>
CURL example
<pre><code class="bash">(echo -n "base64="; echo -n "data:image/jpeg;base64,$(base64 -w 0 Screenshot3.jpg)") | curl --data @- <?= getURL() ?>api/upload</code></pre>
Output
<pre><code class="json">
{
"status": "ok",
"hash": "5e6alk.jpg",
"filetype": "image/jpeg",
"url": "http://localhost:8080/5e6alk.jpg",
"delete_code": "7ha2b5ccvsuvdd3qdnegzb2zqa9zxb5t",
"delete_url": "http://localhost:8080/delete_7ha2b5ccvsuvdd3qdnegzb2zqa9zxb5t/5e6alk.jpg"
}</code></pre>
</div>
<div class="col-6">
<h2>Free vs Key</h2>
<p>If no valid key is sent to the API, the free quota of 10 calls per IP and day will be used. The
daily free quota will reset 24 hours after the last API call (with a quota cost of 1) was
received.</p>
Quota costs
<table class="table table-dark">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">No key</th>
<th scope="col">With key</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Uploading a new image</th>
<td>1</td>
<td>1</td>
</tr>
<tr>
<th scope="row">Uploading an existing image</th>
<td>1</td>
<td>0</td>
</tr>
<tr>
<th scope="row">Checking a hash</th>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="container">
<footer>(c)<?php echo date("y"); ?> by<br /><a href="https://haschek.solutions" target="_blank"><img height="30" src="/css/imgs/hs_logo.png" /></a></footer> <footer>(c)<?php echo date("y"); ?> by<br /><a href="https://haschek.solutions" target="_blank"><img height="30" src="/css/imgs/hs_logo.png" /></a></footer>
</div> </div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/dropzone.js"></script> <script src="/js/dropzone.js"></script>
<script src="/js/highlight.pack.js"></script>
<script src="/js/pictshare.js"></script> <script src="/js/pictshare.js"></script>
</BODY> </BODY>
</HTML> </HTML>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

76
web/css/hljs-dracula.css Normal file
View File

@@ -0,0 +1,76 @@
/*
Dracula Theme v1.2.0
https://github.com/zenorocha/dracula-theme
Copyright 2015, All rights reserved
Code licensed under the MIT license
http://zenorocha.mit-license.org
@author Éverton Ribeiro <nuxlli@gmail.com>
@author Zeno Rocha <hi@zenorocha.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #282a36;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-section,
.hljs-link {
color: #8be9fd;
}
.hljs-function .hljs-keyword {
color: #ff79c6;
}
.hljs,
.hljs-subst {
color: #f8f8f2;
}
.hljs-string,
.hljs-title,
.hljs-name,
.hljs-type,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {
color: #f1fa8c;
}
.hljs-comment,
.hljs-quote,
.hljs-deletion,
.hljs-meta {
color: #6272a4;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-title,
.hljs-section,
.hljs-doctag,
.hljs-type,
.hljs-name,
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}

2377
web/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,40 +1,91 @@
Dropzone.autoDiscover = false; Dropzone.autoDiscover = false;
hljs.initHighlightingOnLoad();
$(function () { document.addEventListener("DOMContentLoaded", function () {
var myDropzone = new Dropzone("#dropzone"); var myDropzone = new Dropzone("#dropzone");
//console.log(myDropzone.options); if (typeof maxUploadFileSize !== "undefined")
if (maxUploadFileSize !== undefined)
myDropzone.options.maxFilesize = maxUploadFileSize; myDropzone.options.maxFilesize = maxUploadFileSize;
myDropzone.options.timeout = 0,
myDropzone.options.timeout = 0;
myDropzone.on("sending", function(file, xhr, formData) { myDropzone.on("sending", function(file, xhr, formData) {
if(document.getElementById("uploadcode")) var uploadCodeElem = document.getElementById("uploadcode");
formData.append("uploadcode", document.getElementById("uploadcode").value); if (uploadCodeElem)
formData.append("uploadcode", uploadCodeElem.value);
}); });
myDropzone.on('error', function(file, response) { myDropzone.on('error', function(file, response) {
alert("Error: "+response.reason); var uploadInfo = document.getElementById("uploadinfo");
if (response == null || response == "null") {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason is unknown :(",
"danger"
)
);
} else {
if (response.status == 'err') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response.reason,
"danger"
)
);
} else {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
"Reason: " + response,
"danger"
)
);
}
}
}); });
myDropzone.on("success", function (file, response) { myDropzone.on("success", function (file, response) {
console.log("raw response: " + response); console.log("raw response: " + response);
if (response == null || response == "null") var uploadInfo = document.getElementById("uploadinfo");
$("#uploadinfo").append("<div class='alert alert-danger' role='alert'><strong>Error uploading " + file.name + "</strong><br/>Reason is unknown :(</div>") if (response == null || response == "null") {
else { uploadInfo.insertAdjacentHTML("beforeend",
var o = response; renderMessage(
if (o.status == 'ok') "Error uploading " + file.name,
$("#uploadinfo").append("<div class='alert alert-success' role='alert'><strong>" + file.name + "</strong> uploaded as <a target='_blank' href='/" + o.hash + "'>" + o.hash + "</a><br/>URL: <a target='_blank' href='" + o.url + "'>" + o.url + "</a> <button class='btn btn-xs' onClick='navigator.clipboard.writeText(\"" + o.url + "\");'>Copy URL</button></div>") "Reason is unknown :(",
else if (o.status == 'err') "danger"
$("#uploadinfo").append("<div class='alert alert-danger' role='alert'><strong>Error uploading " + file.name + "</strong><br/>Reason: " + o.reason + "</div>") )
console.log(o) );
} else {
if (response.status == 'ok') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(file.name+" uploaded as <a target='_blank' href='/" + response.hash + "'>" + response.hash + "</a>","URL: <a target='_blank' href='" + response.url + "'>" + response.url + "</a> <button class='btn btn-primary btn-sm' onClick='navigator.clipboard.writeText(\"" + response.url + "\");'>Copy URL</button>","success")
);
} else if (response.status == 'err') {
uploadInfo.insertAdjacentHTML("beforeend",
renderMessage(
"Error uploading " + file.name,
response.reason,
"danger"
)
);
}
} }
}); });
document.onpaste = function (event) { document.onpaste = function (event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items; var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (index in items) { for (var index in items) {
var item = items[index]; var item = items[index];
if (item.kind === 'file') { if (item.kind === 'file') {
// adds the file to your dropzone instance myDropzone.addFile(item.getAsFile());
myDropzone.addFile(item.getAsFile())
} }
} }
};
});
function renderMessage(title,message,type){
if(!type)
type = "danger";
return `<div class='alert alert-${type}' role='alert'><strong>${title}</strong><br/>${message}</div>`;
} }
})