+function jirafeau_admin_clean()
+{
+ $count = 0;
+ /* Get all links files. */
+ $stack = array(VAR_LINKS);
+ while (($d = array_shift($stack)) && $d != null) {
+ $dir = scandir($d);
+
+ foreach ($dir as $node) {
+ if (strcmp($node, '.') == 0 || strcmp($node, '..') == 0 ||
+ preg_match('/\.tmp/i', "$node")) {
+ continue;
+ }
+
+ if (is_dir($d . $node)) {
+ /* Push new found directory. */
+ $stack[] = $d . $node . '/';
+ } elseif (is_file($d . $node)) {
+ /* Read link information. */
+ $l = jirafeau_get_link(basename($node));
+ if (!count($l)) {
+ continue;
+ }
+ $p = s2p($l['hash']);
+ if ($l['time'] > 0 && $l['time'] < time() || // expired
+ !file_exists(VAR_FILES . $p . $l['hash']) || // invalid
+ !file_exists(VAR_FILES . $p . $l['hash'] . '_count')) { // invalid
+ jirafeau_delete_link($node);
+ $count++;
+ }
+ }
+ }
+ }
+ return $count;
+}
+
+
+/**
+ * Clean old async transfers.
+ * @return number of cleaned files.
+ */
+function jirafeau_admin_clean_async()
+{
+ $count = 0;
+ /* Get all links files. */
+ $stack = array(VAR_ASYNC);
+ while (($d = array_shift($stack)) && $d != null) {
+ $dir = scandir($d);
+
+ foreach ($dir as $node) {
+ if (strcmp($node, '.') == 0 || strcmp($node, '..') == 0 ||
+ preg_match('/\.tmp/i', "$node")) {
+ continue;
+ }
+
+ if (is_dir($d . $node)) {
+ /* Push new found directory. */
+ $stack[] = $d . $node . '/';
+ } elseif (is_file($d . $node)) {
+ /* Read async information. */
+ $a = jirafeau_get_async_ref(basename($node));
+ if (!count($a)) {
+ continue;
+ }
+ /* Delete transfers older than 1 hour. */
+ if (time() - $a['last_edited'] > 3600) {
+ jirafeau_async_delete(basename($node));
+ $count++;
+ }
+ }
+ }
+ }
+ return $count;
+}
+
+/**
+ * Better strval function for debug purposes
+ */
+function jirafeau_strval($value)
+{
+ if (gettype($value) == "boolean") {
+ return $value ? 'true' : 'false';
+ }
+ return strval($value);
+}
+
+/**
+ * Show file/folder permissions
+ */
+function jirafeau_fileperms($path)
+{
+ $out = substr(sprintf("%o", @fileperms($path)), -4) . ", ";
+ $out .= "read " . (is_readable($path) ? "OK" : "KO") . ", ";
+ $out .= "write " . (is_writable($path) ? "OK" : "KO");
+ return $out;
+}
+
+/**
+ * Show some useful informations for bug reporting.
+ */
+function jirafeau_admin_bug_report($cfg)
+{
+ $out = "<fieldset><legend>" . t('REPORTING_AN_ISSUE') . "</legend>";
+ $out .= "If you have a problem related to Jirafeau, please <a href='https://gitlab.com/mojo42/Jirafeau/-/issues'>open an issue</a>, explain your problem in english and copy-paste the following content:<br/><br/><code>";
+
+ $out .= "# Jirafeau<br/>";
+ $out .= "- version: " . JIRAFEAU_VERSION . "<br/>";
+ $jirafeau_options = [
+ 'debug',
+ 'file_hash',
+ 'litespeed_workaround',
+ 'store_uploader_ip',
+ 'installation_done',
+ 'enable_crypt',
+ 'preview',
+ 'maximal_upload_size',
+ 'store_uploader_ip',
+ 'max_upload_chunk_size_bytes'
+ ];
+ foreach ($jirafeau_options as &$o) {
+ $v = $cfg[$o];
+ $out .= "- $o: " . jirafeau_strval($v) . " (" . gettype($v) . ")<br/>";
+ }
+ $out .= "<br/>";
+
+ $out .= "# PHP options<br/>";
+ $out .= "- php version: " . phpversion() . "<br/>";
+ $out .= "- sodium version: " . phpversion('sodium') . "<br/>";
+ $out .= "- mcrypt version: " . phpversion('mcrypt') . "<br/>";
+ $php_options = [
+ 'post_max_size',
+ 'upload_max_filesize',
+ 'safe_mode',
+ 'max_execution_time',
+ 'max_input_time'
+ ];
+ foreach ($php_options as &$o) {
+ $v = ini_get($o);
+ $out .= "- $o: " . jirafeau_strval($v) . " (" . gettype($v). ")<br/>";
+ }
+ $out .= "- can set_time_limit: " . (set_time_limit(0) ? "yes" : "no") . "<br/>";
+ $out .= "<br/>";
+
+ $out .= "# File permissions<br/>";
+ $out .= "- 'var' folder permissions: " . jirafeau_fileperms($cfg['var_root']) . "<br/>";
+ $out .= "- 'file' folder permissions: " . jirafeau_fileperms(VAR_FILES) . "<br/>";
+ $out .= "- 'links' folder permissions: " . jirafeau_fileperms(VAR_LINKS) . "<br/>";
+ $out .= "- 'async' folder permissions: " . jirafeau_fileperms(VAR_ASYNC) . "<br/>";
+ $out .= "<br/>";
+
+ $out .= "# Server details<br/>";
+ $out .= "- server software: " . $_SERVER["SERVER_SOFTWARE"] . "<br/>";
+ $out .= "<br/>";
+
+ $out .= "# OS details<br/>";
+ $out .= "- OS: " . php_uname() . "<br/>";
+ $out .= "<br/>";
+
+ $out .= "# Browser details<br/>";
+ $out .= "<script type='text/javascript' lang='Javascript'>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ document.write('- html5 support: ' + (check_html5_file_api() ? 'yes' : 'no') + '<br/>');
+ document.write('- user agent: ' + navigator.userAgent + '<br/>');
+ // @license-end
+ </script>";
+ $out .= "<br/>";
+
+ $out .= "# Memory<br/>";
+ $out .= "- memory_get_peak_usage: " . jirafeau_human_size(memory_get_peak_usage()) . "<br/>";
+
+ $out .= "</code></fieldset>";
+ return $out;
+}
+
+/**
+ * Read async transfer information
+ * @return array containing information.
+ */
+function jirafeau_get_async_ref($ref)
+{
+ $out = array();
+ $refinfos = VAR_ASYNC . s2p("$ref") . "$ref";
+
+ if (!file_exists($refinfos)) {
+ return $out;
+ }
+
+ $c = file($refinfos);
+ $out['file_name'] = trim($c[0]);
+ $out['mime_type'] = trim($c[1]);
+ $out['key'] = trim($c[2], NL);
+ $out['time'] = trim($c[3]);
+ $out['onetime'] = trim($c[4]);
+ $out['ip'] = trim($c[5]);
+ $out['last_edited'] = trim($c[6]);
+ $out['next_code'] = trim($c[7]);
+ return $out;
+}
+
+/**
+ * Delete async transfer information
+ */
+function jirafeau_async_delete($ref)
+{
+ $p = s2p("$ref");
+ if (file_exists(VAR_ASYNC . $p . $ref)) {
+ unlink(VAR_ASYNC . $p . $ref);
+ }
+ if (file_exists(VAR_ASYNC . $p . $ref . '_data')) {
+ unlink(VAR_ASYNC . $p . $ref . '_data');
+ }
+ $parse = VAR_ASYNC . $p;
+ $scan = array();
+ while (file_exists($parse)
+ && ($scan = scandir($parse))
+ && count($scan) == 2 // '.' and '..' folders => empty.
+ && basename($parse) != basename(VAR_ASYNC)) {
+ rmdir($parse);
+ $parse = substr($parse, 0, strlen($parse) - strlen(basename($parse)) - 1);
+ }
+}
+
+/**
+ * Init a new asynchronous upload.
+ * @param $filename Name of the file to send
+ * @param $one_time One time upload parameter
+ * @param $key eventual password (or blank)
+ * @param $time time limit
+ * @param $ip ip address of the client
+ * @return a string containing a temporary reference followed by a code or a string starting with 'Error'
+ */
+function jirafeau_async_init($filename, $type, $one_time, $key, $time, $ip)
+{
+ /* Create temporary folder. */
+ $ref;
+ $p;
+ $code = jirafeau_gen_random(4);
+ do {
+ $ref = jirafeau_gen_random(32);
+ $p = VAR_ASYNC . s2p($ref);
+ } while (file_exists($p));
+ @mkdir($p, 0755, true);
+ if (!file_exists($p)) {
+ return 'Error: cannot create async folder.';
+ }
+
+ /* touch empty data file */
+ $w_path = $p . $ref . '_data';
+ touch($w_path);
+
+ /* md5 password or empty */
+ $password = '';
+ if (!empty($key)) {
+ $password = md5($key);
+ }
+
+ /* Store information. */
+ $p .= $ref;
+ $handle = fopen($p, 'w');
+ fwrite(
+ $handle,
+ str_replace(NL, '', trim($filename)) . NL .
+ str_replace(NL, '', trim($type)) . NL . $password . NL .
+ $time . NL . ($one_time ? 'O' : 'R') . NL . $ip . NL .
+ time() . NL . $code . NL
+ );
+ fclose($handle);
+
+ return $ref . NL . $code ;
+}
+
+/**
+ * Append a piece of file on the asynchronous upload.
+ * @param $ref asynchronous upload reference
+ * @param $file piece of data
+ * @param $code client code for this operation
+ * @param $max_file_size maximum allowed file size
+ * @return a string containing a next code to use or a string starting with 'Error'
+ */
+function jirafeau_async_push($ref, $data, $code, $max_file_size)
+{
+ /* Get async infos. */
+ $a = jirafeau_get_async_ref($ref);
+
+ /* Check some errors. */
+ if (count($a) == 0) {
+ return "Error: cannot find transfer";
+ }
+ if ($a['next_code'] != "$code") {
+ return "Error: bad transfer code";
+ }
+ if ($data['error'] != UPLOAD_ERR_OK) {
+ // Check error code in https://www.php.net/manual/en/features.file-upload.errors.php
+ $data_details = print_r($data, true);
+ return "Error: upload error: {$data_details}";
+ }
+ if (empty($data['tmp_name'])) {
+ return "Error: missing tmp_name";
+ }
+ if (!is_uploaded_file($data['tmp_name'])) {
+ return "Error: tmp_name may not be uploaded";
+ }
+
+ $p = s2p($ref);
+
+ /* File path. */
+ $r_path = $data['tmp_name'];
+ $w_path = VAR_ASYNC . $p . $ref . '_data';
+
+ /* Check that file size is not above upload limit. */
+ if ($max_file_size > 0 &&
+ filesize($r_path) + filesize($w_path) > $max_file_size * 1024 * 1024) {
+ jirafeau_async_delete($ref);
+ return "Error: file size is above upload limit";
+ }
+
+ /* Concatenate data. */
+ $r = fopen($r_path, 'r');
+ $w = fopen($w_path, 'a');
+ while (!feof($r)) {
+ if (fwrite($w, fread($r, 1024)) === false) {
+ fclose($r);
+ fclose($w);
+ jirafeau_async_delete($ref);
+ return "Error: cannot write file";
+ }
+ }
+ fclose($r);
+ fclose($w);
+ unlink($r_path);
+
+ /* Update async file. */
+ $code = jirafeau_gen_random(4);
+ $handle = fopen(VAR_ASYNC . $p . $ref, 'w');
+ fwrite(
+ $handle,
+ $a['file_name'] . NL. $a['mime_type'] . NL. $a['key'] . NL .
+ $a['time'] . NL . $a['onetime'] . NL . $a['ip'] . NL .
+ time() . NL . $code . NL
+ );
+ fclose($handle);
+ return $code;
+}
+
+/**
+ * Finalize an asynchronous upload.
+ * @param $ref asynchronous upload reference
+ * @param $code client code for this operation
+ * @param $crypt boolean asking to crypt or not
+ * @param $link_name_length link name length
+ * @return a string containing the download reference followed by a delete code or a string starting with 'Error'
+ */
+function jirafeau_async_end($ref, $code, $crypt, $link_name_length, $file_hash_method)
+{
+ /* Get async infos. */
+ $a = jirafeau_get_async_ref($ref);
+ if (count($a) == 0
+ || $a['next_code'] != "$code") {
+ return "Error: bad code for ending transfer";
+ }
+
+ /* Generate link infos. */
+ $p = VAR_ASYNC . s2p($ref) . $ref . "_data";
+ if (!file_exists($p)) {
+ return "Error: referenced file does not exist";
+ }
+
+ $crypted = false;
+ $crypt_key = '';
+ if ($crypt == true && extension_loaded('sodium') == true) {
+ $crypt_key = jirafeau_encrypt_file($p, $p.'.crypt');
+ if (strlen($crypt_key) > 0) {
+ if (rename($p.'.crypt', $p) === true) {
+ $crypted = true;
+ }
+ }
+ }
+
+ $hash = jirafeau_hash_file($file_hash_method, $p);
+ $size = filesize($p);
+ $np = s2p($hash);
+ $delete_link_code = jirafeau_gen_random(5);
+
+ /* File already exist ? */
+ if (!file_exists(VAR_FILES . $np)) {
+ @mkdir(VAR_FILES . $np, 0755, true);
+ }
+ if (!file_exists(VAR_FILES . $np . $hash)) {
+ rename($p, VAR_FILES . $np . $hash);
+ }
+
+ /* Increment or create count file. */
+ $counter = 0;
+ if (file_exists(VAR_FILES . $np . $hash . '_count')) {
+ $content = file(VAR_FILES . $np . $hash. '_count');
+ $counter = trim($content[0]);
+ }
+ $counter++;
+ $handle = fopen(VAR_FILES . $np . $hash. '_count', 'w');
+ fwrite($handle, $counter);
+ fclose($handle);
+
+ /* Create link. */
+ $link_tmp_name = VAR_LINKS . $hash . rand(0, 10000) . '.tmp';
+ $handle = fopen($link_tmp_name, 'w');
+ fwrite(
+ $handle,
+ $a['file_name'] . NL . $a['mime_type'] . NL . $size . NL .
+ $a['key'] . NL . $a['time'] . NL . $hash . NL . $a['onetime'] . NL .
+ time() . NL . $a['ip'] . NL . $delete_link_code . NL . ($crypted ? 'C2' : 'O')
+ );
+ fclose($handle);
+ $hash_link = substr(base_16_to_64(md5_file($link_tmp_name)), 0, $link_name_length);
+ $l = s2p("$hash_link");
+ if (!@mkdir(VAR_LINKS . $l, 0755, true)) {
+ return "Error: cannot create folder in LINKS";
+ }
+ if (!rename($link_tmp_name, VAR_LINKS . $l . $hash_link)) {
+ return "Error: cannot rename file in LINKS";
+ }
+
+ /* Clean async upload. */
+ jirafeau_async_delete($ref);
+ return $hash_link . NL . $delete_link_code . NL . urlencode($crypt_key);
+}
+
+function jirafeau_crypt_create_iv($base, $size)
+{
+ $iv = '';
+ while (strlen($iv) < $size) {
+ $iv = $iv . $base;
+ }
+ $iv = substr($iv, 0, $size);
+ return $iv;
+}
+
+/**
+ * Crypt file using Sodium and returns decrypt key.
+ * @param $fp_src file path to the file to crypt.
+ * @param $fp_dst file path to the file to write crypted file (must not be the same).
+ * @return key used to encrypt the file
+ */
+function jirafeau_encrypt_file($fp_src, $fp_dst)
+{
+ $fs = filesize($fp_src);
+ if ($fs === false || $fs == 0 || extension_loaded('sodium') == false || $fp_src == $fp_dst) {
+ return '';
+ }
+
+ /* Generate key. */
+ $crypt_key = bin2hex(random_bytes(SODIUM_CRYPTO_STREAM_XCHACHA20_KEYBYTES / 2));
+ /* Init module. */
+ [$crypt_state, $crypt_header] = sodium_crypto_secretstream_xchacha20poly1305_init_push($crypt_key);
+ /* Crypt file. */
+ $r = fopen($fp_src, 'rb');
+ $w = fopen($fp_dst, 'wb');
+ fwrite($w, $crypt_header);
+
+ for ($i = 0; $i < $fs; $i += JIRAFEAU_SODIUM_CHUNKSIZE) {
+ $to_enc = fread($r, JIRAFEAU_SODIUM_CHUNKSIZE);
+ $enc = sodium_crypto_secretstream_xchacha20poly1305_push($crypt_state, $to_enc);
+
+ if (fwrite($w, $enc) === false) {
+ return '';
+ }
+ }
+
+ fclose($r);
+ fclose($w);
+
+ /* Cleanup. */
+ sodium_memzero($crypt_state);
+
+ return $crypt_key;
+}
+
+/**
+ * Decrypt file using Sodium.
+ * @param $fp_src file path to the file to decrypt.
+ * @param $fp_dst file path to the file to write decrypted file (must not be the same).
+ * @param $k decryption key
+ * @return true if decryption succeeded, false otherwise
+ */
+function jirafeau_decrypt_file($fp_src, $fp_dst, $k)