Skip to content

Commit cf16096

Browse files
fox34fox34
fox34
authored and
fox34
committed
Initial commit.
0 parents  commit cf16096

15 files changed

+606
-0
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
# Work- and project files
3+
files/*
4+
.idea/

.htaccess

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
deny from all

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# copynpaste
2+
Read-Once Copy and Paste tool for sharing (more or less) confidential data
3+
4+
# Usage
5+
Upload files, set public webserver path to /public and set includes/config.php accordingly.
6+
7+
# Security
8+
Content can be encrypted client-side via Stanford Javascript Crypto Library. JavaScript must then be enabled to read/write encrypted content.

includes/config.php

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
return [
4+
// Public subdir, must end with a slash. Must be empty for root dir.
5+
'publicSubdir' => '',
6+
7+
8+
// Public path (normally you don't need to change this line)
9+
'publicPath' => 'http' . (($_SERVER['SERVER_PORT'] == 443) ? 's' : '') . '://' . $_SERVER['HTTP_HOST'] . '/',
10+
11+
12+
// Target folder for created files. Should not be accessible from web.
13+
'targetFolder' => BASEDIR . 'files/'
14+
];

includes/functions.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
namespace noecho\copynpaste;
5+
6+
7+
// https://github.com/briancray/PHP-URL-Shortener
8+
// edited to exclude ambiguous characters: 0, 1, o, O, i, j, l, I, J
9+
function encodeShortURL(int $number) : string
10+
{
11+
$codeset = '23456789abcdefghkmnpqrstuvwxyzABCDEFGHKLMNPQRSTUVWXYZ';
12+
$base = strlen($codeset);
13+
14+
$out = '';
15+
while ($number > $base - 1) {
16+
$out = $codeset[(int)fmod($number, $base)] . $out;
17+
$number = (int)floor($number / $base);
18+
}
19+
return $codeset[$number] . $out;
20+
}
21+
22+
function showErrorPage(string $errorMsg)
23+
{
24+
$error = $errorMsg;
25+
require BASEDIR . 'templates/error.php';
26+
exit;
27+
}
28+
29+
function getNextID(): string
30+
{
31+
// counter file can be stored in target folder, since valid ids never begin with an underscore
32+
$counterFile = $GLOBALS['configuration']['targetFolder'] . '_counter.txt';
33+
34+
if (!file_exists($counterFile)) {
35+
$currentID = 0;
36+
} else {
37+
$currentID = (int)file_get_contents($counterFile);
38+
}
39+
40+
file_put_contents($counterFile, (string)++$currentID, LOCK_EX);
41+
42+
// generate some randomness to prevent guessing
43+
return
44+
encodeShortURL($currentID) .
45+
encodeShortURL(random_int((int)1e3, (int)1e4)) .
46+
encodeShortURL(random_int((int)1e2, (int)1e3));
47+
}

public/.htaccess

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
allow from all
2+
3+
RewriteEngine On
4+
5+
RewriteCond %{REQUEST_FILENAME} !-d
6+
RewriteCond %{REQUEST_FILENAME} !-f
7+
RewriteRule ^ index.php [L]

public/index.php

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
namespace noecho\copynpaste;
5+
6+
// Basic init
7+
header('Content-Type: text/html; charset=UTF-8');
8+
if (extension_loaded('zlib')) {
9+
ini_set('zlib.output_compression', 'On');
10+
}
11+
ini_set('html_errors', '0');
12+
error_reporting(E_ALL);
13+
14+
15+
define('BASEDIR', dirname(__DIR__) . '/');
16+
17+
// Load configuration
18+
$configuration = require BASEDIR . 'includes/config.php';
19+
require_once BASEDIR . 'includes/functions.php';
20+
21+
22+
// Check configuration
23+
if (!is_dir($configuration['targetFolder']) || !is_writable($configuration['targetFolder'])) {
24+
die('Target folder does not exist or is not writeable.');
25+
}
26+
27+
28+
// Parse target
29+
$request = parse_url($_SERVER['REQUEST_URI']);
30+
if (!empty($request['query'])) {
31+
parse_str($request['query'], $query);
32+
} else {
33+
$query = [];
34+
}
35+
36+
// Append / overwrite fileID from last part of path (fileID may not consist of non-alphanumerical characters)
37+
$query['fileID'] = basename($request['path']);
38+
$query['action'] = $query['action'] ?? '';
39+
40+
switch ($query['action']) {
41+
42+
43+
44+
case 'create':
45+
46+
$fields = [
47+
'year', 'month', 'week', 'dayOfWeek', 'dayOfMonth', 'hour', 'minute',
48+
'value', 'encrypted'
49+
];
50+
$boolFields = [
51+
'once', 'editable'
52+
];
53+
$entry = (object)[];
54+
foreach ($fields as $fieldName) {
55+
$entry->{$fieldName} = $_POST[$fieldName] ?? '';
56+
}
57+
foreach ($boolFields as $fieldName) {
58+
$entry->{$fieldName} = isset($_POST[$fieldName]);
59+
}
60+
61+
if (empty($entry->value) && empty($entry->encrypted)) {
62+
showErrorPage('Text may not be empty.');
63+
}
64+
65+
$id = getNextID();
66+
file_put_contents($configuration['targetFolder'] . 'entry-' . $id . '.json', json_encode($entry));
67+
68+
$justCreated = true;
69+
70+
// show finished entry
71+
require BASEDIR . 'templates/show.php';
72+
73+
break;
74+
75+
76+
77+
case 'edit':
78+
die("edit existing. id may not be empty.");
79+
80+
break;
81+
82+
83+
84+
default:
85+
86+
// show index
87+
if (empty($query['fileID'])) {
88+
require BASEDIR . 'templates/create.php';
89+
90+
// show entry if possible
91+
} else {
92+
93+
$fileName = $configuration['targetFolder'] . 'entry-' . basename($query['fileID']) . '.json';
94+
95+
// File not found
96+
if (!file_exists($fileName)) {
97+
http_response_code(404);
98+
showErrorPage('Requested entry does not exist or has already been deleted.');
99+
}
100+
101+
$entry = json_decode(file_get_contents($fileName));
102+
103+
// Entry data is invalid json
104+
if ($entry === null) {
105+
http_response_code(500);
106+
showErrorPage('Requested entry contains invalid data. Could not load data.');
107+
}
108+
109+
110+
// Check access time.
111+
$isValidAccessTime = true;
112+
$timeFields = ['year', 'month', 'week', 'dayOfWeek', 'dayOfMonth', 'hour', 'minute'];
113+
$timeCheck = '';
114+
115+
foreach ($timeFields as $field) {
116+
117+
// not restricted
118+
if ($entry->{$field} === '') {
119+
continue;
120+
}
121+
122+
// time range
123+
if (($colonPos = strpos($entry->{$field}, ':')) !== false) {
124+
$from = (int)substr($entry->{$field}, 0, $colonPos);
125+
$to = (int)substr($entry->{$field}, $colonPos + 1);
126+
127+
// single time
128+
} else {
129+
$from = $to = (int)$entry->{$field};
130+
}
131+
132+
switch ($field) {
133+
134+
case 'year':
135+
$currentValue = date('Y');
136+
break;
137+
138+
case 'month':
139+
$currentValue = date('m');
140+
break;
141+
142+
case 'week':
143+
$currentValue = date('W');
144+
break;
145+
146+
case 'dayOfWeek':
147+
$currentValue = date('N');
148+
break;
149+
150+
case 'dayOfMonth':
151+
$currentValue = date('d');
152+
break;
153+
154+
case 'hour':
155+
$currentValue = date('H');
156+
break;
157+
158+
case 'minute':
159+
$currentValue = date('i');
160+
break;
161+
162+
default:
163+
$timeCheck .= "! Unknown field, disabling access for security reasons.\n";
164+
$isValidAccessTime = false;
165+
continue 2;
166+
}
167+
168+
$currentValue = (int)$currentValue;
169+
170+
// time is invalid
171+
if ($currentValue < $from || $currentValue > $to) {
172+
$isValidAccessTime = false;
173+
$timeCheck .=
174+
$field . ' is valid from ' . $from . ' to ' . $to .
175+
', but current value is ' . $currentValue . "\n";
176+
}
177+
178+
}
179+
180+
if (!$isValidAccessTime) {
181+
showErrorPage("Access time is invalid:\n" . $timeCheck);
182+
}
183+
184+
185+
// Entry may only be shown once and user has not confirmed request
186+
if ($entry->once && !isset($query['showOnce'])) {
187+
require BASEDIR . 'templates/onceWarning.php';
188+
exit;
189+
}
190+
191+
// Force delete source file
192+
if ($entry->once && unlink($fileName) === false) {
193+
http_response_code(500);
194+
showErrorPage('Could not delete entry, quitting for security reasons.');
195+
}
196+
197+
// Finally, show the entry
198+
require BASEDIR . 'templates/show.php';
199+
}
200+
201+
}

0 commit comments

Comments
 (0)