From d7621c2c01074bdcabd6a8c1a8e95c95d00319bf Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Fri, 29 Aug 2014 17:41:33 +0200 Subject: add multipaste support Signed-off-by: Florian Pritz --- application/config/migration.php | 2 +- application/controllers/file.php | 506 +++++++++++++++------ application/libraries/Output_cache.php | 80 ++++ application/migrations/013_add_multipaste.php | 36 ++ application/models/mfile.php | 118 +++-- application/models/mmultipaste.php | 160 +++++++ application/views/file/file_info.php | 2 +- application/views/file/fragments/alert-wide.php | 3 + application/views/file/fragments/thumbnail.php | 9 + application/views/file/fragments/uploads_table.php | 26 ++ application/views/file/html_footer.php | 4 +- application/views/file/html_header.php | 89 +--- application/views/file/html_paste_footer.php | 2 + application/views/file/html_paste_header.php | 79 ++++ application/views/file/multipaste_info.php | 26 ++ application/views/file/upload_form.php | 1 + application/views/file/upload_history.php | 2 +- .../views/file/upload_history_thumbnails.php | 9 +- .../views/file_plaintext/upload_history.php | 4 +- application/views/footer.php | 4 +- data/css/style.css | 44 +- data/js/script.js | 34 +- 22 files changed, 939 insertions(+), 301 deletions(-) create mode 100644 application/libraries/Output_cache.php create mode 100644 application/migrations/013_add_multipaste.php create mode 100644 application/models/mmultipaste.php create mode 100644 application/views/file/fragments/alert-wide.php create mode 100644 application/views/file/fragments/thumbnail.php create mode 100644 application/views/file/fragments/uploads_table.php create mode 100644 application/views/file/html_paste_footer.php create mode 100644 application/views/file/html_paste_header.php create mode 100644 application/views/file/multipaste_info.php diff --git a/application/config/migration.php b/application/config/migration.php index d27107cd4..75e9bc19b 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -21,7 +21,7 @@ $config['migration_enabled'] = true; | be upgraded / downgraded to. | */ -$config['migration_version'] = 12; +$config['migration_version'] = 13; /* diff --git a/application/controllers/file.php b/application/controllers/file.php index 51d6cf156..cbda8bea2 100644 --- a/application/controllers/file.php +++ b/application/controllers/file.php @@ -13,6 +13,7 @@ class File extends MY_Controller { "upload_history", "do_upload", "do_delete", + "do_multipaste", ); function __construct() @@ -20,6 +21,7 @@ class File extends MY_Controller { parent::__construct(); $this->load->model('mfile'); + $this->load->model('mmultipaste'); if (is_cli_client()) { $this->var->view_dir = "file_plaintext"; @@ -34,10 +36,13 @@ class File extends MY_Controller { $this->load->library("../controllers/tools"); return $this->tools->index(); } + // Try to guess what the user would like to do. $id = $this->uri->segment(1); if (!empty($_FILES)) { $this->do_upload(); + } elseif (strpos($id, "m-") === 0 && $this->mmultipaste->id_exists($id)) { + $this->_download(); } elseif ($id != "file" && $this->mfile->id_exists($id)) { $this->_download(); } elseif ($id && $id != "file") { @@ -52,124 +57,151 @@ class File extends MY_Controller { $id = $this->uri->segment(1); $lexer = urldecode($this->uri->segment(2)); - $filedata = $this->mfile->get_filedata($id); - $file = $this->mfile->file($filedata['hash']); + $is_multipaste = false; + if ($this->mmultipaste->id_exists($id)) { + $is_multipaste = true; - if (!$this->mfile->valid_id($id)) { - $this->_non_existent(); - return; - } + if(!$this->mmultipaste->valid_id($id)) { + return $this->_non_existent(); + } + $files = $this->mmultipaste->get_files($id); + } elseif ($this->mfile->id_exists($id)) { + if (!$this->mfile->valid_id($id)) { + return $this->_non_existent(); + } - // don't allow unowned files to be downloaded - if ($filedata["user"] == 0) { - $this->_non_existent(); - return; + $files = array($this->mfile->get_filedata($id)); + } else { + assert(0); } - // helps to keep traffic low when reloading - $etag = $filedata["hash"]."-".$filedata["date"]; + assert($files !== false); + assert(is_array($files)); + assert(count($files) >= 1); - // autodetect the lexer for highlighting if the URL contains a / after the ID (/ID/) - // /ID/lexer disables autodetection - $autodetect_lexer = !$lexer && substr_count(ltrim($this->uri->uri_string(), "/"), '/') >= 1; + // don't allow unowned files to be downloaded + foreach ($files as $filedata) { + if ($filedata["user"] == 0) { + return $this->_non_existent(); + } + } - if ($autodetect_lexer) { - $lexer = $this->mfile->autodetect_lexer($filedata["mimetype"], $filedata["filename"]); + $etag = ""; + foreach ($files as $filedata) { + $etag = sha1($etag.$filedata["hash"]); } - // resolve aliases - // this is mainly used for compatibility - $lexer = $this->mfile->resolve_lexer_alias($lexer); + // handle some common "lexers" here + switch ($lexer) { + case "": + break; - // create the qr code for /ID/ - if ($lexer == "qr") { + case "qr": handle_etag($etag); header("Content-disposition: inline; filename=\"".$id."_qr.png\"\n"); header("Content-Type: image/png\n"); passthru('qrencode -s 10 -o - '.escapeshellarg(site_url($id).'/')); exit(); + + case "info": + return $this->_display_info($id); + + default: + if ($is_multipaste) { + show_error("Invalid action \"".htmlspecialchars($lexer)."\""); + } + break; } $this->load->driver("ddownload"); // user wants the plain file if ($lexer == 'plain') { + assert(count($files) == 1); handle_etag($etag); - $this->ddownload->serveFile($file, $filedata["filename"], "text/plain"); - exit(); - } - if ($lexer == 'info') { - $this->_display_info($id); - return; + $filedata = $files[0]; + $filepath = $this->mfile->file($filedata["hash"]); + $this->ddownload->serveFile($filepath, $filedata["filename"], "text/plain"); + exit(); } - // if there is no mimetype mapping we can't highlight it - $can_highlight = $this->mfile->can_highlight($filedata["mimetype"]); + $this->load->library("output_cache"); - $filesize_too_big = filesize($file) > $this->config->item('upload_max_text_size'); + foreach ($files as $key => $filedata) { + $file = $this->mfile->file($filedata['hash']); - if (!$can_highlight || $filesize_too_big || !$lexer) { - // prevent javascript from being executed and forbid frames - // this should allow us to serve user submitted HTML content without huge security risks - foreach (array("X-WebKit-CSP", "X-Content-Security-Policy", "Content-Security-Policy") as $header_name) { - header("$header_name: default-src 'none'; img-src *; media-src *; font-src *; style-src 'unsafe-inline' *; script-src 'none'; object-src *; frame-src 'none'; "); + // autodetect the lexer for highlighting if the URL contains a / after the ID (/ID/) + // /ID/lexer disables autodetection + $autodetect_lexer = !$lexer && substr_count(ltrim($this->uri->uri_string(), "/"), '/') >= 1; + $autodetect_lexer = $is_multipaste ? true : $autodetect_lexer; + if ($autodetect_lexer) { + $lexer = $this->mfile->autodetect_lexer($filedata["mimetype"], $filedata["filename"]); } - handle_etag($etag); - $this->ddownload->serveFile($file, $filedata["filename"], $filedata["mimetype"]); - exit(); - } - - $this->data['title'] = htmlspecialchars($filedata['filename']); - $this->data['id'] = $id; - header("Content-Type: text/html\n"); + // resolve aliases + // this is mainly used for compatibility + $lexer = $this->mfile->resolve_lexer_alias($lexer); - $this->data['current_highlight'] = htmlspecialchars($lexer); - $this->data['timeout'] = $this->mfile->get_timeout_string($id); - $this->data['lexers'] = $this->mfile->get_lexers(); - $this->data['filedata'] = $filedata; - - // highlight the file and cache the result - $highlit = cache_function($filedata['hash'].'_'.$lexer, 100, function() use ($file, $lexer){ - $CI =& get_instance(); - $ret = array(); - if ($lexer == "rmd") { - ob_start(); + // if there is no mimetype mapping we can't highlight it + $can_highlight = $this->mfile->can_highlight($filedata["mimetype"]); - echo '
'."\n"; - echo '
'."\n"; - echo '
'."\n"; - passthru('perl '.FCPATH.'scripts/Markdown.pl '.escapeshellarg($file), $ret["return_value"]); - echo '
'; + $filesize_too_big = filesize($file) > $this->config->item('upload_max_text_size'); - $ret["output"] = ob_get_clean(); - } else { - $ret = $CI->_colorify($file, $lexer); - } - - if ($ret["return_value"] != 0) { - $tmp = $CI->_colorify($file, "text"); - $ret["output"] = $tmp["output"]; + if (!$can_highlight || $filesize_too_big || !$lexer) { + if (!$is_multipaste) { + // prevent javascript from being executed and forbid frames + // this should allow us to serve user submitted HTML content without huge security risks + foreach (array("X-WebKit-CSP", "X-Content-Security-Policy", "Content-Security-Policy") as $header_name) { + header("$header_name: default-src 'none'; img-src *; media-src *; font-src *; style-src 'unsafe-inline' *; script-src 'none'; object-src *; frame-src 'none'; "); + } + handle_etag($etag); + $this->ddownload->serveFile($file, $filedata["filename"], $filedata["mimetype"]); + exit(); + } else { + switch ($filedata["mimetype"]) { + // TODO: handle video/audio + // TODO: handle more image formats (thumbnails needs to be improved) + case "image/jpeg": + case "image/png": + case "image/gif": + $filedata["tooltip"] = $this->_tooltip_for_image($filedata); + $this->output_cache->add_merge( + array("items" => array($filedata)), + 'file/fragments/thumbnail' + ); + + break; + + default: + $this->output_cache->add_merge( + array("items" => array($filedata)), + 'file/fragments/uploads_table' + ); + break; + } + continue; + } } - return $ret; - }); - if ($highlit["return_value"] != 0) { - $this->data["error_message"] = "

Error trying to process the file. - Either the lexer is unknown or something is broken. - Falling back to plain text.

"; + $this->output_cache->add_function(function() use ($filedata, $lexer, $is_multipaste) { + $this->_highlight_file($filedata, $lexer, $is_multipaste); + }); } - // Don't use append_output because the output class does too + // TODO: move lexers json to dedicated URL + $this->data['lexers'] = $this->mfile->get_lexers(); + + // Output everything + // Don't use the output class/append_output because it does too // much magic ({elapsed_time} and {memory_usage}). // Direct echo puts us on the safe side. echo $this->load->view($this->var->view_dir.'/html_header', $this->data, true); - echo $highlit["output"]; + $this->output_cache->render(); echo $this->load->view($this->var->view_dir.'/html_footer', $this->data, true); } - private function _colorify($file, $lexer) + private function _colorify($file, $lexer, $anchor_id = false) { $return_value = 0; $output = ""; @@ -205,10 +237,15 @@ class File extends MY_Controller { $line = str_replace("
", "", $line);
 			}
 
+			$anchor = "n$line_number";
+			if ($anchor_id !== false) {
+				$anchor = "n-$anchor_id-$line_number";
+			}
+
 			// Be careful not to add superflous whitespace here (we are in a 
)
 			$output .= "
" - ."" - ." " + ."" + ." " ."" ."".$line."\n"; $output .= "
"; @@ -223,16 +260,111 @@ class File extends MY_Controller { ); } - function _display_info($id) + private function _highlight_file($filedata, $lexer, $is_multipaste) { - $this->data["title"] .= " - Info $id"; - $this->data["filedata"] = $this->mfile->get_filedata($id); - $this->data["id"] = $id; - $this->data['timeout'] = $this->mfile->get_timeout_string($id); + // highlight the file and cache the result, fall back to plain text if $lexer fails + foreach (array($lexer, "text") as $lexer) { + $highlit = cache_function($filedata['hash'].'_'.$lexer, 100, + function() use ($filedata, $lexer, $is_multipaste) { + $file = $this->mfile->file($filedata['hash']); + if ($lexer == "rmd") { + ob_start(); + + echo '
'."\n"; + echo '
'."\n"; + echo '
'."\n"; + passthru('perl '.FCPATH.'scripts/Markdown.pl '.escapeshellarg($file), $return_value); + echo '
'; + + return array( + "output" => ob_get_clean(), + "return_value" => $return_value, + ); + } else { + return get_instance()->_colorify($file, $lexer, $is_multipaste ? $filedata["id"] : false); + } + }); - $this->load->view('header', $this->data); - $this->load->view($this->var->view_dir.'/file_info', $this->data); - $this->load->view('footer', $this->data); + if ($highlit["return_value"] == 0) { + break; + } else { + $message = "Error trying to process the file. Either the lexer is unknown or something is broken."; + if ($lexer != "text") { + $message .= " Falling back to plain text."; + } + $this->output_cache->render_now( + array("error_message" => "

$message

"), + "file/fragments/alert-wide" + ); + } + } + + $data = array_merge($this->data, array( + 'title' => htmlspecialchars($filedata['filename']), + 'id' => $filedata["id"], + 'current_highlight' => htmlspecialchars($lexer), + 'timeout' => $this->mfile->get_timeout_string($filedata["id"]), + 'filedata' => $filedata, + )); + + $this->output_cache->render_now($data, $this->var->view_dir.'/html_paste_header'); + $this->output_cache->render_now($highlit["output"]); + $this->output_cache->render_now($data, $this->var->view_dir.'/html_paste_footer'); + } + + private function _tooltip_for_image($filedata) + { + $filesize = format_bytes($filedata["filesize"]); + $dimensions = $this->mfile->image_dimension($this->mfile->file($filedata["hash"])); + $upload_date = date("r", $filedata["date"]); + + $tooltip = "${filedata["id"]} - $filesize
"; + $tooltip .= "$upload_date
"; + $tooltip .= "$dimensions - ${filedata["mimetype"]}
"; + + return $tooltip; + } + + private function _display_info($id) + { + if ($this->mmultipaste->id_exists($id)) { + $files = $this->mmultipaste->get_files($id); + + $this->data["title"] .= " - Info $id"; + + $multipaste = $this->mmultipaste->get_multipaste($id); + $total_size = 0; + $timeout = -1; + foreach($files as $filedata) { + $total_size += $filedata["filesize"]; + $file_timeout = $this->mfile->get_timeout($filedata["id"]); + if ($timeout == -1 || ($timeout > $file_timeout && $file_timeout >= 0)) { + $timeout = $file_timeout; + } + } + + $data = array_merge($this->data, array( + 'timeout_string' => $timeout >= 0 ? date("r", $timeout) : "Never", + 'upload_date' => $multipaste["date"], + 'id' => $id, + 'size' => $total_size, + 'file_count' => count($files), + )); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/multipaste_info', $data); + $this->load->view('footer', $this->data); + return; + } elseif ($this->mfile->id_exists($id)) { + $this->data["title"] .= " - Info $id"; + $this->data["filedata"] = $this->mfile->get_filedata($id); + $this->data["id"] = $id; + $this->data['timeout'] = $this->mfile->get_timeout_string($id); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/file_info', $this->data); + $this->load->view('footer', $this->data); + } } function _non_existent() @@ -416,20 +548,10 @@ class File extends MY_Controller { unset($query[$key]); continue; } - - $filesize = format_bytes($item["filesize"]); - $dimensions = $this->mfile->image_dimension($this->mfile->file($item["hash"])); - $upload_date = date("r", $item["date"]); - - $query[$key]["filesize"] = $filesize; - $query[$key]["tooltip"] = " - ${item["id"]} - $filesize
- $upload_date - $dimensions - ${item["mimetype"]}
- "; + $query[$key]["tooltip"] = $this->_tooltip_for_image($item); } - $this->data["query"] = $query; + $this->data["items"] = $query; $this->load->view('header', $this->data); $this->load->view($this->var->view_dir.'/upload_history_thumbnails', $this->data); @@ -462,23 +584,40 @@ class File extends MY_Controller { $order = is_cli_client() ? "ASC" : "DESC"; - $query = $this->db->query(" + $items = $this->db->query(" SELECT ".implode(",", array_keys($fields))." FROM files WHERE user = ? - ORDER BY date $order ", array($user))->result_array(); + $query = $this->db->query(" + SELECT m.url_id id, sum(f.filesize) filesize, m.date, '' hash, '' mimetype, concat(count(*), ' file(s)') filename + FROM multipaste m + JOIN multipaste_file_map mfm ON m.multipaste_id = mfm.multipaste_id + JOIN files f ON f.id = mfm.file_url_id + WHERE m.user_id = ? + GROUP BY m.url_id + ", array($user))->result_array(); + + $items = array_merge($items, $query); + uasort($items, function($a, $b) use ($order) { + if ($order == "ASC") { + return $a["date"] - $b["date"]; + } else { + return $b["date"] - $a["date"]; + } + }); + if (static_storage("response_type") == "json") { - return send_json_reply($query); + return send_json_reply($items); } - foreach($query as $key => $item) { - $query[$key]["filesize"] = format_bytes($item["filesize"]); + foreach($items as $key => $item) { + $items[$key]["filesize"] = format_bytes($item["filesize"]); if (is_cli_client()) { // Keep track of longest string to pad plaintext output correctly foreach($fields as $length_key => $value) { - $len = mb_strlen($query[$key][$length_key]); + $len = mb_strlen($items[$key][$length_key]); if ($len > $lengths[$length_key]) { $lengths[$length_key] = $len; } @@ -496,7 +635,7 @@ class File extends MY_Controller { ) sub ", array($user))->row_array(); - $this->data["query"] = $query; + $this->data["items"] = $items; $this->data["lengths"] = $lengths; $this->data["fields"] = $fields; $this->data["total_size"] = format_bytes($total_size["sum"]); @@ -511,6 +650,7 @@ class File extends MY_Controller { $this->muser->require_access("apikey"); $ids = $this->input->post("ids"); + $userid = $this->muser->get_userid(); $errors = array(); $deleted = array(); $deleted_count = 0; @@ -522,24 +662,38 @@ class File extends MY_Controller { foreach ($ids as $id) { $total_count++; + $next = false; + + foreach (array($this->mfile, $this->mmultipaste) as $model) { + if ($model->id_exists($id)) { + if ($model->get_owner($id) !== $userid) { + $errors[] = array( + "id" => $id, + "reason" => "wrong owner", + ); + continue; + } + if ($model->delete_id($id)) { + $deleted[] = $id; + $deleted_count++; + $next = true; + } else { + $errors[] = array( + "id" => $id, + "reason" => "unknown error", + ); + } + } + } - if (!$this->mfile->id_exists($id)) { - $errors[] = array( - "id" => $id, - "reason" => "doesn't exist", - ); + if ($next) { continue; } - if ($this->mfile->delete_id($id)) { - $deleted[] = $id; - $deleted_count++; - } else { - $errors[] = array( - "id" => $id, - "reason" => "unknown error", - ); - } + $errors[] = array( + "id" => $id, + "reason" => "doesn't exist", + ); } if (static_storage("response_type") == "json") { @@ -560,6 +714,62 @@ class File extends MY_Controller { $this->load->view('footer', $this->data); } + function do_multipaste() + { + $this->muser->require_access("apikey"); + + $ids = $this->input->post("ids"); + $errors = array(); + + if (!$ids || !is_array($ids)) { + show_error("No IDs specified"); + } + + if (count(array_unique($ids)) != count($ids)) { + show_error("Duplicate IDs are not supported"); + } + + foreach ($ids as $id) { + if (!$this->mfile->id_exists($id)) { + $errors[] = array( + "id" => $id, + "reason" => "doesn't exist", + ); + } + + $filedata = $this->mfile->get_filedata($id); + if ($filedata["user"] != $this->muser->get_userid()) { + $errors[] = array( + "id" => $id, + "reason" => "not owned by you", + ); + } + } + + if (!empty($errors)) { + $errorstring = ""; + foreach ($errors as $error) { + $errorstring .= $error["id"]." ".$error["reason"]."
\n"; + } + show_error($errorstring); + } + + $limits = $this->muser->get_upload_id_limits(); + $url_id = $this->mmultipaste->new_id($limits[0], $limits[1]); + + $multipaste_id = $this->mmultipaste->get_multipaste_id($url_id); + assert($multipaste_id !== false); + + foreach ($ids as $id) { + $this->db->insert("multipaste_file_map", array( + "file_url_id" => $id, + "multipaste_id" => $multipaste_id, + )); + } + + return $this->_show_url(array($url_id), false); + } + function delete() { $this->muser->require_access("apikey"); @@ -570,19 +780,29 @@ class File extends MY_Controller { $id = $this->uri->segment(3); $this->data["id"] = $id; + $userid = $this->muser->get_userid(); - if ($id && !$this->mfile->id_exists($id)) { - show_error("Unknown ID '$id'.", 404); + foreach (array($this->mfile, $this->mmultipaste) as $model) { + if ($model->id_exists($id)) { + if ($model->get_owner($id) !== $userid) { + echo "You don't own this file\n"; + return; + } + if ($model->delete_id($id)) { + echo "$id has been deleted.\n"; + } else { + echo "Deletion failed. Unknown error\n"; + } + return; + } } - if ($this->mfile->delete_id($id)) { - echo "$id has been deleted.\n"; - } else { - echo "Deletion failed. Do you really own that file?\n"; - } + show_error("Unknown ID '$id'.", 404); } // Handle pastes + // TODO: merge with do_upload and also merge the forms + // TODO: add support for multiple textareas (+ view) function do_paste() { // stateful clients get a cookie to claim the ID later @@ -628,6 +848,7 @@ class File extends MY_Controller { $ids = array(); $extension = $this->input->post('extension'); + $multipaste = $this->input->post('multipaste'); $files = getNormalizedFILES(); @@ -697,6 +918,21 @@ class File extends MY_Controller { $ids[] = $id; } + if ($multipaste !== false) { + $multipaste_url_id = $this->mmultipaste->new_id($limits[0], $limits[1]); + + $multipaste_id = $this->mmultipaste->get_multipaste_id($multipaste_url_id); + assert($multipaste_id !== false); + + foreach ($ids as $id) { + $this->db->insert("multipaste_file_map", array( + "file_url_id" => $id, + "multipaste_id" => $multipaste_id, + )); + } + $ids[] = $multipaste_url_id; + } + $this->_show_url($ids, $extension); } @@ -774,16 +1010,16 @@ class File extends MY_Controller { foreach($query->result_array() as $row) { $file = $this->mfile->file($row['hash']); if (!file_exists($file)) { - $this->db->query('DELETE FROM files WHERE id = ? LIMIT 1', array($row['id'])); + $this->mfile->delete_id($row["id"]); continue; } if ($row["user"] == 0 || filesize($file) > $small_upload_size) { if (filemtime($file) < $oldest_time) { unlink($file); - $this->db->query('DELETE FROM files WHERE hash = ?', array($row['hash'])); + $this->mfile->delete_hash($row["hash"]); } else { - $this->db->query('DELETE FROM files WHERE id = ? LIMIT 1', array($row['id'])); + $this->mfile->delete_id($row["id"]); if ($this->mfile->stale_hash($row["hash"])) { unlink($file); } @@ -838,7 +1074,6 @@ class File extends MY_Controller { $id = $this->uri->segment(3); - $file_data = $this->mfile->get_filedata($id); if (empty($file_data)) { @@ -847,14 +1082,7 @@ class File extends MY_Controller { } $hash = $file_data["hash"]; - - $this->db->query(" - DELETE FROM files - WHERE hash = ? - ", array($hash)); - - unlink($this->mfile->file($hash)); - + $this->mfile->delete_hash($hash); echo "removed hash \"$hash\"\n"; } diff --git a/application/libraries/Output_cache.php b/application/libraries/Output_cache.php new file mode 100644 index 000000000..224e9f95a --- /dev/null +++ b/application/libraries/Output_cache.php @@ -0,0 +1,80 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Output_cache { + private $output_cache = array(); + + /** + * Combine multiple objects for the same view into one + * @param data data to pass to the view + * @param view view path + */ + public function add_merge($data, $view) + { + assert($view !== NULL); + + // combine multiple objects for the same view into one + $count = count($this->output_cache); + if ($count > 0 && $this->output_cache[$count - 1]["view"] === $view) { + $this->output_cache[$count - 1]["data"] = array_merge_recursive($this->output_cache[$count - 1]["data"], $data); + } else { + $this->add($data, $view); + } + } + + /** + * Add some data that will be output directly if view is NULL or passed + * to the view otherweise. + * + * @param data data to pass to view or output + * @param view view path or NULL + */ + public function add($data, $view = NULL) + { + $this->output_cache[] = array( + "view" => $view, + "data" => $data, + ); + } + + /** + * Add a function that will be excuted when render() is called. + * This function is supposed to use render_now() to output data. + * + * @param data_function + */ + public function add_function($data_function) + { + $this->output_cache[] = array( + "view" => NULL, + "data_function" => $data_function, + ); + } + + public function render_now($data, $view = NULL) + { + if ($view !== NULL) { + echo get_instance()->load->view($view, $data, true); + } else { + echo $data; + } + } + + public function render() + { + while ($output = array_shift($this->output_cache)) { + if (isset($output["data_function"])) { + $output["data_function"](); + } else { + $data = $output["data"]; + $this->render_now($data, $output["view"]); + } + } + } +} diff --git a/application/migrations/013_add_multipaste.php b/application/migrations/013_add_multipaste.php new file mode 100644 index 000000000..edb4a0748 --- /dev/null +++ b/application/migrations/013_add_multipaste.php @@ -0,0 +1,36 @@ +db->query(' + CREATE TABLE `multipaste` ( + `url_id` varchar(255) NOT NULL, + `multipaste_id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `date` int(11) NOT NULL, + PRIMARY KEY (`url_id`), + UNIQUE KEY `multipaste_id` (`multipaste_id`), + KEY `user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;'); + + $this->db->query(' + CREATE TABLE `multipaste_file_map` ( + `multipaste_id` int(11) NOT NULL, + `file_url_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sort_order` int(10) unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`sort_order`), + UNIQUE KEY `multipaste_id` (`multipaste_id`,`file_url_id`), + KEY `multipaste_file_map_ibfk_2` (`file_url_id`), + CONSTRAINT `multipaste_file_map_ibfk_1` FOREIGN KEY (`multipaste_id`) REFERENCES `multipaste` (`multipaste_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `multipaste_file_map_ibfk_2` FOREIGN KEY (`file_url_id`) REFERENCES `files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;'); + } + + public function down() + { + show_error("downgrade not supported"); + } +} diff --git a/application/models/mfile.php b/application/models/mfile.php index c900b297c..427c74a18 100644 --- a/application/models/mfile.php +++ b/application/models/mfile.php @@ -71,15 +71,14 @@ class Mfile extends CI_Model { function get_filedata($id) { $sql = ' - SELECT hash, filename, mimetype, date, user, filesize + SELECT id, hash, filename, mimetype, date, user, filesize FROM `files` WHERE `id` = ? LIMIT 1'; $query = $this->db->query($sql, array($id)); - if ($query->num_rows() == 1) { - $return = $query->result_array(); - return $return[0]; + if ($query->num_rows() > 0) { + return $query->row_array(); } else { return false; } @@ -214,10 +213,15 @@ class Mfile extends CI_Model { $mimetype = $this->mimetype($this->file($hash)); $filesize = filesize($this->file($hash)); - $query = $this->db->query(' - INSERT INTO `files` (`hash`, `id`, `filename`, `user`, `date`, `mimetype`, `filesize`) - VALUES (?, ?, ?, ?, ?, ?, ?)', - array($hash, $id, $filename, $userid, time(), $mimetype, $filesize)); + $this->db->insert("files", array( + "id" => $id, + "hash" => $hash, + "filename" => $filename, + "date" => time(), + "user" => $userid, + "mimetype" => $mimetype, + "filesize" => $filesize, + )); } function adopt($id) @@ -241,9 +245,7 @@ class Mfile extends CI_Model { $file = $this->file($filedata['hash']); if (!file_exists($file)) { - if (isset($filedata["hash"])) { - $this->db->query('DELETE FROM files WHERE hash = ?', array($filedata['hash'])); - } + $this->delete_hash($filedata["hash"]); return false; } @@ -262,10 +264,9 @@ class Mfile extends CI_Model { // if the file has been uploaded multiple times the mtime is the time // of the last upload if (filemtime($file) < $remove_before) { - unlink($file); - $this->db->query('DELETE FROM files WHERE hash = ?', array($filedata['hash'])); + $this->delete_hash($filedata["hash"]); } else { - $this->db->query('DELETE FROM files WHERE id = ? LIMIT 1', array($id)); + $this->delete_id($id); } return false; } @@ -273,13 +274,28 @@ class Mfile extends CI_Model { return true; } - function get_timeout_string($id) + public function get_timeout($id) { $filedata = $this->get_filedata($id); $file = $this->file($filedata["hash"]); + if ($this->config->item("upload_max_age") == 0) { + return -1; + } + if (filesize($file) > $this->config->item("small_upload_size")) { - return date("r", $filedata["date"] + $this->config->item("upload_max_age")); + return $filedata["date"] + $this->config->item("upload_max_age"); + } else { + return -1; + } + } + + public function get_timeout_string($id) + { + $timeout = $this->get_timeout($id); + + if ($timeout >= 0) { + return date("r", $timeout); } else { return "unknown"; } @@ -301,34 +317,72 @@ class Mfile extends CI_Model { } } - function delete_id($id) + public function delete_id($id) { $filedata = $this->get_filedata($id); - $userid = $this->muser->get_userid(); - if (!$this->id_exists($id)) { - return false; - } - - $sql = ' - DELETE - FROM `files` - WHERE `id` = ? - AND user = ? - LIMIT 1'; - $this->db->query($sql, array($id, $userid)); + // Delete the file and all multipastes using it + // Note that this does not delete all relations in multipaste_file_map + // which is actually done by a SQL contraint. + // TODO: make it work properly without the constraint + $this->db->query(' + DELETE m, mfm, f + FROM files f + LEFT JOIN multipaste_file_map mfm ON f.id = mfm.file_url_id + LEFT JOIN multipaste m ON mfm.multipaste_id = m.multipaste_id + WHERE f.id = ? + ', array($id)); if ($this->id_exists($id)) { return false; } - if ($this->unused_file($filedata['hash'])) { - unlink($this->file($filedata['hash'])); - @rmdir($this->folder($filedata['hash'])); + if ($filedata !== false) { + assert(isset($filedata["hash"])); + if ($this->unused_file($filedata['hash'])) { + unlink($this->file($filedata['hash'])); + $dir = $this->folder($filedata['hash']); + if (count(scandir($dir)) == 2) { + rmdir($dir); + } + } } return true; } + public function delete_hash($hash) + { + // Delete all files with this hash and all multipastes using any of those files + // Note that this does not delete all relations in multipaste_file_map + // which is actually done by a SQL contraint. + // TODO: make it work properly without the constraint + $this->db->query(' + DELETE m, mfm, f + FROM files f + LEFT JOIN multipaste_file_map mfm ON f.id = mfm.file_url_id + LEFT JOIN multipaste m ON mfm.multipaste_id = m.multipaste_id + WHERE f.hash = ? + ', array($hash)); + + if (file_exists($this->file($hash))) { + unlink($this->file($hash)); + $dir = $this->folder($hash); + if (count(scandir($dir)) == 2) { + rmdir($dir); + } + } + return true; + } + + public function get_owner($id) + { + return $this->db->query(" + SELECT user + FROM files + WHERE id = ? + ", array($id))->row_array()["user"]; + } + public function get_lexers() { return cache_function('lexers', 1800, function() { $lexers = array(); diff --git a/application/models/mmultipaste.php b/application/models/mmultipaste.php new file mode 100644 index 000000000..723132a50 --- /dev/null +++ b/application/models/mmultipaste.php @@ -0,0 +1,160 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Mmultipaste extends CI_Model { + + function __construct() + { + parent::__construct(); + $this->load->model("muser"); + $this->load->model("mfile"); + } + + /** + * Returns an unused ID + * + * @param min minimal length of the resulting ID + * @param max maximum length of the resulting ID + */ + public function new_id($min = 3, $max = 6) + { + static $id_blacklist = NULL; + + if ($id_blacklist == NULL) { + // This prevents people from being unable to access their uploads + // because of URL rewriting + $id_blacklist = scandir(FCPATH); + $id_blacklist[] = "file"; + $id_blacklist[] = "user"; + } + + $max_tries = 100; + + for ($try = 0; $try < $max_tries; $try++) { + $id = "m-".random_alphanum($min, $max); + + // TODO: try to insert the id into file_groups instead of checking with + // id_exists (prevents race conditio) + if ($this->id_exists($id) || in_array($id, $id_blacklist)) { + continue; + } + + $this->db->insert("multipaste", array( + "url_id" => $id, + "user_id" => $this->muser->get_userid(), + "date" => time(), + )); + + return $id; + } + + show_error("Failed to find unused ID after $max_tries tries."); + } + + public function id_exists($id) + { + if (!$id) { + return false; + } + + $sql = ' + SELECT multipaste.url_id + FROM multipaste + WHERE multipaste.url_id = ? + LIMIT 1'; + $query = $this->db->query($sql, array($id)); + + if ($query->num_rows() == 1) { + return true; + } else { + return false; + } + } + + public function valid_id($id) + { + $files = $this->get_files($id); + foreach ($files as $file) { + if (!$this->mfile->valid_id($file["id"])) { + return false; + } + } + return true; + } + + public function delete_id($id) + { + $this->db->query(' + DELETE m, mfm + FROM multipaste m + LEFT JOIN multipaste_file_map mfm ON mfm.multipaste_id = m.multipaste_id + WHERE m.url_id = ? + ', array($id)); + + if ($this->id_exists($id)) { + return false; + } + + return true; + } + + public function get_owner($id) + { + return $this->db->query(" + SELECT user_id + FROM multipaste + WHERE url_id = ? + ", array($id))->row_array()["user_id"]; + } + + public function get_multipaste($id) + { + return $this->db->query(" + SELECT url_id, user_id, date + FROM multipaste + WHERE url_id = ? + ", array($id))->row_array(); + } + + public function get_files($url_id) + { + $ret = array(); + + $query = $this->db->query(" + SELECT mfm.file_url_id + FROM multipaste_file_map mfm + JOIN multipaste m ON m.multipaste_id = mfm.multipaste_id + WHERE m.url_id = ? + ORDER BY mfm.sort_order + ", array($url_id))->result_array(); + + foreach ($query as $row) { + $filedata = $this->mfile->get_filedata($row["file_url_id"]); + $ret[] = $filedata; + } + + return $ret; + } + + public function get_multipaste_id($url_id) + { + $query = $this->db->query(" + SELECT multipaste_id + FROM multipaste + WHERE url_id = ? + ", array($url_id)); + + if ($query->num_rows() > 0) { + return $query->row_array()["multipaste_id"]; + } + + return false; + } + +} diff --git a/application/views/file/file_info.php b/application/views/file/file_info.php index 6c2772a21..0620ac9bd 100644 --- a/application/views/file/file_info.php +++ b/application/views/file/file_info.php @@ -1,4 +1,4 @@ -
+
diff --git a/application/views/file/fragments/alert-wide.php b/application/views/file/fragments/alert-wide.php new file mode 100644 index 000000000..ae303e119 --- /dev/null +++ b/application/views/file/fragments/alert-wide.php @@ -0,0 +1,3 @@ +
+ +
diff --git a/application/views/file/fragments/thumbnail.php b/application/views/file/fragments/thumbnail.php new file mode 100644 index 000000000..6bd82fcb9 --- /dev/null +++ b/application/views/file/fragments/thumbnail.php @@ -0,0 +1,9 @@ + + diff --git a/application/views/file/fragments/uploads_table.php b/application/views/file/fragments/uploads_table.php new file mode 100644 index 000000000..142d19e91 --- /dev/null +++ b/application/views/file/fragments/uploads_table.php @@ -0,0 +1,26 @@ + +
+

Non-previewable file(s):

+
+ + + + + + + + + + + + + + + + + + + + +
IDFilenameMimetypeDateSize
/">">
+
diff --git a/application/views/file/html_footer.php b/application/views/file/html_footer.php index bbec7ebd1..bd07b63f9 100644 --- a/application/views/file/html_footer.php +++ b/application/views/file/html_footer.php @@ -1,6 +1,4 @@ -
-
- +
-
+
- -
- -
- - -
-
- - -
+ diff --git a/application/views/file/html_paste_footer.php b/application/views/file/html_paste_footer.php new file mode 100644 index 000000000..22bc4dabb --- /dev/null +++ b/application/views/file/html_paste_footer.php @@ -0,0 +1,2 @@ +
+ diff --git a/application/views/file/html_paste_header.php b/application/views/file/html_paste_header.php new file mode 100644 index 000000000..f4d3021ec --- /dev/null +++ b/application/views/file/html_paste_header.php @@ -0,0 +1,79 @@ +
+
+ + +
diff --git a/application/views/file/multipaste_info.php b/application/views/file/multipaste_info.php new file mode 100644 index 000000000..5baf732a2 --- /dev/null +++ b/application/views/file/multipaste_info.php @@ -0,0 +1,26 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + +
ID
Number of files
Date of upload
Date of removal
Total size (including duplicates)
+
+
diff --git a/application/views/file/upload_form.php b/application/views/file/upload_form.php index 612a1bf82..44828c53a 100644 --- a/application/views/file/upload_form.php +++ b/application/views/file/upload_form.php @@ -29,6 +29,7 @@

+
diff --git a/application/views/file/upload_history.php b/application/views/file/upload_history.php index 5015e9bf6..10afc53e9 100644 --- a/application/views/file/upload_history.php +++ b/application/views/file/upload_history.php @@ -15,7 +15,7 @@ - $item): ?> + $item): ?> ]" value="" class="delete-history"> /"> diff --git a/application/views/file/upload_history_thumbnails.php b/application/views/file/upload_history_thumbnails.php index bcafc44ca..a061d9676 100644 --- a/application/views/file/upload_history_thumbnails.php +++ b/application/views/file/upload_history_thumbnails.php @@ -6,14 +6,7 @@
- - - +
diff --git a/application/views/file_plaintext/upload_history.php b/application/views/file_plaintext/upload_history.php index f9a14af0b..53801494f 100644 --- a/application/views/file_plaintext/upload_history.php +++ b/application/views/file_plaintext/upload_history.php @@ -9,13 +9,13 @@ echo .mb_str_pad($fields["hash"], $lengths["hash"])." | " .mb_str_pad($fields["filesize"], $lengths["filesize"])."\n"; -foreach($query as $key => $item) { +foreach($items as $key => $item) { echo mb_str_pad($item["id"], $lengths["id"])." | " .mb_str_pad($item["filename"], $lengths["filename"])." | " .mb_str_pad($item["mimetype"], $lengths["mimetype"])." | " .date($dateformat, $item["date"])." | " - .$item["hash"]." | " + .mb_str_pad($item["hash"], $lengths["hash"])." | " .$item["filesize"]."\n"; } ?> diff --git a/application/views/footer.php b/application/views/footer.php index e748cd4e3..ae8d2e575 100644 --- a/application/views/footer.php +++ b/application/views/footer.php @@ -3,9 +3,9 @@ if (is_cli_client() && !isset($force_full_html)) { return; } ?> -
+
- +