diff options
author | Florian Pritz <bluewind@xinu.at> | 2014-08-29 17:45:48 +0200 |
---|---|---|
committer | Florian Pritz <bluewind@xinu.at> | 2014-08-29 17:45:48 +0200 |
commit | 6065c00e637872804d057bfe6fc6b719f4652019 (patch) | |
tree | b4523c79395ab0cae3c8bd3386283476332cdd61 | |
parent | cc72236f71c60396b69631656f8e0ad00a6301c8 (diff) | |
parent | f8674b37bd7db4e47103391b28dfa5b34d58f29f (diff) |
Merge branch 'multipaste' into working
26 files changed, 1128 insertions, 460 deletions
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 '<div class="code content table markdownrender">'."\n"; - echo '<div class="table-row">'."\n"; - echo '<div class="table-cell">'."\n"; - passthru('perl '.FCPATH.'scripts/Markdown.pl '.escapeshellarg($file), $ret["return_value"]); - echo '</div></div></div>'; + $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"] = "<p>Error trying to process the file. - Either the lexer is unknown or something is broken. - Falling back to plain text.</p>"; + $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("<div class=\"highlight\"><pre>", "", $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 <pre>) $output .= "<div class=\"table-row\">" - ."<a href=\"#n$line_number\" class=\"linenumber table-cell\">" - ."<span class=\"anchor\" id=\"n$line_number\"> </span>" + ."<a href=\"#$anchor\" class=\"linenumber table-cell\">" + ."<span class=\"anchor\" id=\"$anchor\"> </span>" ."</a>" ."<span class=\"line table-cell\">".$line."</span>\n"; $output .= "</div>"; @@ -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 '<div class="code content table markdownrender">'."\n"; + echo '<div class="table-row">'."\n"; + echo '<div class="table-cell">'."\n"; + passthru('perl '.FCPATH.'scripts/Markdown.pl '.escapeshellarg($file), $return_value); + echo '</div></div></div>'; + + 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" => "<p>$message</p>"), + "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<br>"; + $tooltip .= "$upload_date<br>"; + $tooltip .= "$dimensions - ${filedata["mimetype"]}<br>"; + + 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<br> - $upload_date - $dimensions - ${item["mimetype"]}<br> - "; + $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"]."<br>\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/errors/error_404.php b/application/errors/error_404.php index 792726a67..cfe923d63 100644 --- a/application/errors/error_404.php +++ b/application/errors/error_404.php @@ -1,62 +1,3 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<title>404 Page Not Found</title> -<style type="text/css"> - -::selection{ background-color: #E13300; color: white; } -::moz-selection{ background-color: #E13300; color: white; } -::webkit-selection{ background-color: #E13300; color: white; } - -body { - background-color: #fff; - margin: 40px; - font: 13px/20px normal Helvetica, Arial, sans-serif; - color: #4F5155; -} - -a { - color: #003399; - background-color: transparent; - font-weight: normal; -} - -h1 { - color: #444; - background-color: transparent; - border-bottom: 1px solid #D0D0D0; - font-size: 19px; - font-weight: normal; - margin: 0 0 14px 0; - padding: 14px 15px 10px 15px; -} - -code { - font-family: Consolas, Monaco, Courier New, Courier, monospace; - font-size: 12px; - background-color: #f9f9f9; - border: 1px solid #D0D0D0; - color: #002166; - display: block; - margin: 14px 0 14px 0; - padding: 12px 10px 12px 10px; -} - -#container { - margin: 10px; - border: 1px solid #D0D0D0; - -webkit-box-shadow: 0 0 8px #D0D0D0; -} - -p { - margin: 12px 15px 12px 15px; -} -</style> -</head> -<body> - <div id="container"> - <h1><?php echo $heading; ?></h1> - <?php echo $message; ?> - </div> -</body> -</html>
\ No newline at end of file +<?php +$title = "404 Page Not Found"; +include "application/errors/error_general.php"; diff --git a/application/errors/error_db.php b/application/errors/error_db.php index b396cda9f..255513634 100644 --- a/application/errors/error_db.php +++ b/application/errors/error_db.php @@ -1,62 +1,3 @@ -<!DOCTYPE html> -<html lang="en"> -<head> -<title>Database Error</title> -<style type="text/css"> - -::selection{ background-color: #E13300; color: white; } -::moz-selection{ background-color: #E13300; color: white; } -::webkit-selection{ background-color: #E13300; color: white; } - -body { - background-color: #fff; - margin: 40px; - font: 13px/20px normal Helvetica, Arial, sans-serif; - color: #4F5155; -} - -a { - color: #003399; - background-color: transparent; - font-weight: normal; -} - -h1 { - color: #444; - background-color: transparent; - border-bottom: 1px solid #D0D0D0; - font-size: 19px; - font-weight: normal; - margin: 0 0 14px 0; - padding: 14px 15px 10px 15px; -} - -code { - font-family: Consolas, Monaco, Courier New, Courier, monospace; - font-size: 12px; - background-color: #f9f9f9; - border: 1px solid #D0D0D0; - color: #002166; - display: block; - margin: 14px 0 14px 0; - padding: 12px 10px 12px 10px; -} - -#container { - margin: 10px; - border: 1px solid #D0D0D0; - -webkit-box-shadow: 0 0 8px #D0D0D0; -} - -p { - margin: 12px 15px 12px 15px; -} -</style> -</head> -<body> - <div id="container"> - <h1><?php echo $heading; ?></h1> - <?php echo $message; ?> - </div> -</body> -</html>
\ No newline at end of file +<?php +$title = "Database Error"; +include "application/errors/error_general.php"; diff --git a/application/errors/error_general.php b/application/errors/error_general.php index 6c67fa33f..be495e4f6 100644 --- a/application/errors/error_general.php +++ b/application/errors/error_general.php @@ -2,14 +2,21 @@ // fancy error page only works if we can load helpers if (class_exists("CI_Controller") && !isset($GLOBALS["is_error_page"])) { - $title = "Error"; + if (!isset($title)) { + $title = "Error"; + } $GLOBALS["is_error_page"] = true; $CI =& get_instance(); $CI->load->helper("filebin"); $CI->load->helper("url"); + if ($CI->input->is_cli_request()) { + is_cli_client(true); + } + if (static_storage("response_type") == "json") { + $message = str_replace("</p>", "</p>\n", $message); $array = array( "status" => "error", "message" => strip_tags($message), @@ -20,6 +27,7 @@ if (class_exists("CI_Controller") && !isset($GLOBALS["is_error_page"])) { } if (is_cli_client()) { + $message = str_replace("</p>", "</p>\n", $message); $message = strip_tags($message); echo "$heading: $message\n"; exit(); 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 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_add_multipaste extends CI_Migration { + + public function up() + { + $this->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 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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 @@ -<div class="center"> +<div class="center simple-container"> <?php if($filedata): ?> <div class="table-responive"> <table class="table" style="margin: auto"> 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 @@ +<div class="alert alert-danger alert-wide"> + <?php echo $error_message; ?> +</div> 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 @@ +<!-- Comment markers background: http://stackoverflow.com/a/14776780/953022 --> +<div class="container container-wide"> +<div class="upload_thumbnails"><!-- + <?php foreach($items as $key => $item): ?> + --><a href="<?php echo site_url("/".$item["id"])."/"; ?>" title="<?php echo htmlentities($item["filename"]); ?>" data-content="<?php echo htmlentities($item["tooltip"]); ?>" data-id="<?php echo $item["id"]; ?>"><img class="thumb" src="<?php echo site_url("file/thumbnail/".$item["id"]); ?>"></a><!-- + <?php endforeach; ?> + --> +</div> +</div> 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 @@ +<?php register_js_include("/data/js/jquery.tablesorter.min.js"); ?> +<div class="table-responsive container-wide"> + <p>Non-previewable file(s):</p> + <table class="table table-striped tablesorter"> + <thead> + <tr> + <th>ID</th> + <th>Filename</th> + <th>Mimetype</th> + <th>Date</th> + <th>Size</th> + </tr> + </thead> + <tbody> + <?php foreach($items as $item): ?> + <tr> + <td><a href="<?php echo site_url("/".$item["id"]) ?>/"><?php echo $item["id"] ?></a></td> + <td class="wrap"><?php echo htmlspecialchars($item["filename"]); ?></td> + <td><?php echo $item["mimetype"] ?></td> + <td class="nowrap" data-sort-value="<?=$item["date"]; ?>"><?php echo date("r", $item["date"]); ?></td> + <td><?php echo format_bytes($item["filesize"]) ?></td> + </tr> + <?php endforeach; ?> + </tbody> + </table> +</div> 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 @@ - </div> - </div> - +<div class="container"> <?php $force_full_html = true; include(FCPATH."application/views/footer.php"); diff --git a/application/views/file/html_header.php b/application/views/file/html_header.php index 2c556720c..fdce101a2 100644 --- a/application/views/file/html_header.php +++ b/application/views/file/html_header.php @@ -2,95 +2,14 @@ $force_full_html = true; include(FCPATH."application/views/header.php"); ?> -</div> +</div><!-- .container --> <script type="text/javascript"> /* <![CDATA[ */ window.lexers = <?php echo json_encode($lexers); ?>; - window.paste_base = '<?php echo site_url($id) ?>'; /* ]]> */ </script> -<?php if (isset($error_message)) { ?> -<div class="alert alert-danger" style="text-align: center; border-radius: 0;"> - <?php echo $error_message; ?> -</div> -<?php } ?> - -<div class="container paste-container"> - <div style="border:1px solid #ccc;"> - <div class="navbar navbar-default navbar-static-top navbar-paste"> - <ul class="nav navbar-nav navbar-left dont-float"> - <li><a href="#file-info" class="navbar-brand" data-toggle="modal"><?php echo $title ?></a></li> - <li class="divider"></li> - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="language-toggle"> - Language: <?php echo htmlspecialchars($current_highlight); ?> - <b class="caret"></b> - </a> - <div class="dropdown-menu" style="padding: 15px;"> - <form> - <input type="text" id="language" placeholder="Language" class="form-control"> - </form> - </div> - </li> - <li class="divider"></li> - <li> - <a href="#file-info" role="button" data-toggle="modal">Info</a> - </li> - <li class="divider"></li> - <li><a href="<?php echo site_url('file/index?repaste='.$id); ?>" role="button">Repaste</a></li> - </ul> - <div class="btn-group navbar-right" style="margin: 8px;"> - <a id="linewrap" class="btn btn-default" rel="tooltip" title="Toggle wrapping of long lines">Linewrap</a> - <a href="<?php echo site_url($id."/plain") ?>" class="btn btn-default" rel="tooltip" title="View as plain text">Plain</a> - <a href="<?php echo site_url($id) ?>" class="btn btn-default" rel="tooltip" title="View as raw file (org. mime type)">Raw</a> - <?php if ($current_highlight === 'rmd') { ?> - <a href="<?php echo site_url($id)."/" ?>" class="btn btn-default" rel="tooltip" title="Render as Code">Code</a> - <?php } else { ?> - <a href="<?php echo site_url($id."/rmd") ?>" class="btn btn-default" rel="tooltip" title="Render as Markdown">Markdown</a> - <?php } ?> - </div> - </div> <!-- .navbar --> - <div id="file-info" class="modal fade" role="dialog" aria-labelledby="file-info" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal">×</button> - <h3 class="modal-title">Paste Information</h3> - </div> - <div class="modal-body"> - <table class="table"> - <tr> - <td style="border:0;">Filename:</td> - <td style="border:0;"><?php echo htmlspecialchars($filedata["filename"]) ?></td> - </tr> - <tr> - <td>Size:</td> - <td><?php echo format_bytes($filedata["filesize"]) ?></td> - </tr> - <tr> - <td>Mimetype:</td> - <td><?php echo $filedata["mimetype"] ?></td> - </tr> - <tr> - <td>Uploaded:</td> - <td><?php echo date("r", $filedata["date"]) ?></td> - </tr> - <tr> - <td>Removal:</td> - <td><?php echo $timeout ?></td> - </tr> - </table> - </div> - <div class="modal-footer"> - <?php echo form_open("file/do_delete/", array("style" => "display: inline")); ?> - <input type="hidden" name="ids[<?php echo $id; ?>]" value="<?php echo $id; ?>"> - <button class="btn btn-danger pull-left" aria-hidden="true">Delete</button> - </form> - <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button> - </div> - </div> - </div> - </div> <!-- .modal --> - <div> +<?php if (isset($error_message)) { + include 'framgents/alert-wide.php'; +} ?> 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 @@ +</div><!-- .container .paste-container --> + 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 @@ +<div class="container paste-container container-wide"> + <div style="border:1px solid #ccc;"> + <div class="navbar navbar-default navbar-static-top navbar-paste"> + <ul class="nav navbar-nav navbar-left dont-float"> + <li><a href="<?=site_url($id)."/"; ?>" class="navbar-brand" data-toggle="modal"><?php echo $title ?></a></li> + <li class="divider"></li> + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="language-toggle-<?=$id; ?>"> + Language: <?php echo htmlspecialchars($current_highlight); ?> + <b class="caret"></b> + </a> + <div class="dropdown-menu" style="padding: 15px;"> + <form> + <input data-base-url="<?=site_url($id); ?>" type="text" id="language-<?=$id; ?>" placeholder="Language" class="form-control"> + </form> + </div> + </li> + <li class="divider"></li> + <li> + <a href="#file-info-<?=$id; ?>" role="button" data-toggle="modal">Info</a> + </li> + <?php if (isset($user_logged_in) && $user_logged_in) { ?> + <li class="divider"></li> + <li><a href="<?php echo site_url('file/index?repaste='.$id); ?>" role="button">Repaste</a></li> + <?php } ?> + </ul> + <div class="btn-group navbar-right" style="margin: 8px;"> + <a id="linewrap-<?=$id; ?>" class="btn btn-default" rel="tooltip" title="Toggle wrapping of long lines">Linewrap</a> + <a href="<?php echo site_url($id."/plain") ?>" class="btn btn-default" rel="tooltip" title="View as plain text">Plain</a> + <a href="<?php echo site_url($id) ?>" class="btn btn-default" rel="tooltip" title="View as raw file (org. mime type)">Raw</a> + <?php if ($current_highlight === 'rmd') { ?> + <a href="<?php echo site_url($id)."/" ?>" class="btn btn-default" rel="tooltip" title="Render as Code">Code</a> + <?php } else { ?> + <a href="<?php echo site_url($id."/rmd") ?>" class="btn btn-default" rel="tooltip" title="Render as Markdown">Markdown</a> + <?php } ?> + </div> + </div> <!-- .navbar --> + <div id="file-info-<?=$id; ?>" class="modal fade" role="dialog" aria-labelledby="file-info-<?=$id; ?>" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h3 class="modal-title">Paste Information</h3> + </div> + <div class="modal-body"> + <table class="table"> + <tr> + <td style="border:0;">Filename:</td> + <td style="border:0;"><?php echo htmlspecialchars($filedata["filename"]) ?></td> + </tr> + <tr> + <td>Size:</td> + <td><?php echo format_bytes($filedata["filesize"]) ?></td> + </tr> + <tr> + <td>Mimetype:</td> + <td><?php echo $filedata["mimetype"] ?></td> + </tr> + <tr> + <td>Uploaded:</td> + <td><?php echo date("r", $filedata["date"]) ?></td> + </tr> + <tr> + <td>Removal:</td> + <td><?php echo $timeout ?></td> + </tr> + </table> + </div> + <div class="modal-footer"> + <?php echo form_open("file/do_delete/", array("style" => "display: inline")); ?> + <input type="hidden" name="ids[<?php echo $id; ?>]" value="<?php echo $id; ?>"> + <button class="btn btn-danger pull-left" aria-hidden="true">Delete</button> + </form> + <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button> + </div> + </div> + </div> + </div> <!-- .modal --> + </div> 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 @@ +<div class="center simple-container"> + <div class="table-responive"> + <table class="table" style="margin: auto"> + <tr> + <td class="title">ID</td> + <td class="text"><a href="<?=site_url($id); ?>/"><?=$id; ?></a></td> + </tr> + <tr> + <td class="title">Number of files</td> + <td class="text"><?=$file_count; ?></td> + </tr> + <tr> + <td class="title">Date of upload</td> + <td class="text"><?=date("r", $upload_date); ?></td> + </tr> + <tr> + <td class="title">Date of removal</td> + <td class="text"><?=$timeout_string; ?></td> + </tr> + <tr> + <td class="title">Total size (including duplicates)</td> + <td class="text"><?=format_bytes($size); ?></td> + </tr> + </table> + </div> +</div> diff --git a/application/views/file/upload_form.php b/application/views/file/upload_form.php index c5b28536c..44828c53a 100644 --- a/application/views/file/upload_form.php +++ b/application/views/file/upload_form.php @@ -26,7 +26,10 @@ <h3 class="panel-title">File upload</h3> </div> <div class="panel-body"> - <input class="file-upload" type="file" name="file[]" multiple="multiple"><br> + <div> + <input class="file-upload" type="file" name="file[]" multiple="multiple"><br> + </div> + <label><input type="checkbox" name="multipaste" value="1"> Create multipaste</label><br> <button type="submit" id="upload_button" class="btn btn-primary">Upload it!</button> </div> </div> @@ -57,6 +60,7 @@ <script type="text/javascript"> /* <![CDATA[ */ var max_upload_size = "<?php echo $max_upload_size; ?>"; + var max_files_per_upload = "<?php echo ini_get("max_file_uploads"); ?>"; /* ]]> */ </script> diff --git a/application/views/file/upload_history.php b/application/views/file/upload_history.php index 634ad5935..10afc53e9 100644 --- a/application/views/file/upload_history.php +++ b/application/views/file/upload_history.php @@ -1,11 +1,12 @@ <?php register_js_include("/data/js/jquery.tablesorter.min.js"); ?> +<?php register_js_include("/data/js/jquery.metadata.js"); ?> <?php include 'nav_history.php'; ?> <?php echo form_open("file/do_delete") ?> <div class="table-responsive"> - <table id="upload_history" class="table table-striped tablesorter"> + <table id="upload_history" class="table table-striped tablesorter {sortlist: [[4,1]]}"> <thead> <tr> - <th><input type="checkbox" name="all-ids" id="history-all"></th> + <th class="{sorter: false}"><input type="checkbox" name="all-ids" id="history-all"></th> <th>ID</th> <th>Filename</th> <th>Mimetype @@ -14,13 +15,13 @@ </tr> </thead> <tbody> - <?php foreach($query as $key => $item): ?> + <?php foreach($items as $key => $item): ?> <tr> <td><input type="checkbox" name="ids[<?php echo $item["id"] ?>]" value="<?php echo $item["id"] ?>" class="delete-history"></td> <td><a href="<?php echo site_url("/".$item["id"]) ?>/"><?php echo $item["id"] ?></a></td> <td class="wrap"><?php echo htmlspecialchars($item["filename"]); ?></td> <td><?php echo $item["mimetype"] ?></td> - <td class="nowrap"><?php echo date("r", $item["date"]); ?><span class="hidden">t=<?php echo $item["date"]; ?></span></td> + <td class="nowrap" data-sort-value="<?=$item["date"]; ?>"><?php echo date("r", $item["date"]); ?></td> <td><?php echo $item["filesize"] ?></td> </tr> <?php endforeach; ?> 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 @@ </div> <?php include 'nav_history.php'; ?> - -<!-- Comment markers background: http://stackoverflow.com/a/14776780/953022 --> -<div class="upload_history_thumbnails"><!-- - <?php foreach($query as $key => $item): ?> - --><a href="<?php echo site_url("/".$item["id"]); ?>" title="<?php echo htmlentities($item["filename"]); ?>" data-content="<?php echo htmlentities($item["tooltip"]); ?>" data-id="<?php echo $item["id"]; ?>"><img class="thumb" src="<?php echo site_url("file/thumbnail/".$item["id"]); ?>"></a><!-- - <?php endforeach; ?> - --> -</div> +<?php include 'fragments/thumbnail.php'; ?> <div class="row-fluid"> <div class="span12 alert alert-block alert-info"> 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; } ?> - </div> + </div><!-- .container --> <div id="push"></div> -</div> +</div> <!-- #wrap --> <footer class="footer" id="footer"> <div class="container muted credits"> <p>Site code licensed under <a href="http://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPL v3</a>.</p> diff --git a/data/css/style.css b/data/css/style.css index 5f1286e82..18a8e8bb1 100644 --- a/data/css/style.css +++ b/data/css/style.css @@ -62,6 +62,10 @@ width: 200px; } +.file-upload { + width: 100%; +} + .navbar-nav > li > .dropdown-menu { margin-top: 2px; } @@ -81,8 +85,8 @@ @media (min-width: 768px) { .anchor { visibility: hidden; - padding-top: 60px; - margin-top: -60px; + padding-top: 70px; + margin-top: -70px; } #navbar-height { @@ -119,6 +123,11 @@ textarea.text-upload { word-wrap: normal; } +.alert-wide { + text-align: center; + border-radius: 0; +} + code, pre, textarea { font-family: "Dejavu sans mono", Monaco, monospace; } @@ -184,13 +193,23 @@ body { padding-left: 0; } -.paste-container { - padding-top: 40px; - background: #eee; - padding: 3px; +.container-wide { + padding: 0; max-width: 100%; margin-left: 20px; margin-right: 20px; + margin-bottom: 20px; +} + +.simple-container { + margin-left: 20px; + margin-right: 20px; + margin-bottom: 20px; +} + +.paste-container { + padding: 3px; + background: #eee; } .code pre { @@ -265,9 +284,6 @@ body { .highlight_line { background: #ffffcc; } -#file-info { - display: none; -} .ui-autocomplete { z-index: 1500; @@ -276,29 +292,29 @@ body { .popover { word-break: break-word; word-wrap: normal; + max-width: 400px; } -.upload_history_thumbnails { +.upload_thumbnails { margin: 0 auto; - padding-bottom: 50px; } -.upload_history_thumbnails img.thumb, -.upload_history_thumbnails a { +.upload_thumbnails img.thumb, +.upload_thumbnails a { width: 150px; height: 150px; } -.upload_history_thumbnails a { +.upload_thumbnails a { margin: 1px; display: inline-block; } -.upload_history_thumbnails .marked { +.upload_thumbnails .marked { background: red; } -.upload_history_thumbnails .marked img { +.upload_thumbnails .marked img { opacity: 0.4; } diff --git a/data/js/jquery.metadata.js b/data/js/jquery.metadata.js new file mode 100644 index 000000000..eb98e80ad --- /dev/null +++ b/data/js/jquery.metadata.js @@ -0,0 +1,120 @@ +/* + * Metadata - jQuery plugin for parsing metadata from elements + * + * Copyright (c) 2006 John Resig, Yehuda Katz, Jörn Zaefferer, Paul McLanahan + * + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +/** + * Sets the type of metadata to use. Metadata is encoded in JSON, and each property + * in the JSON will become a property of the element itself. + * + * There are three supported types of metadata storage: + * + * attr: Inside an attribute. The name parameter indicates *which* attribute. + * + * class: Inside the class attribute, wrapped in curly braces: { } + * + * elem: Inside a child element (e.g. a script tag). The + * name parameter indicates *which* element. + * + * The metadata for an element is loaded the first time the element is accessed via jQuery. + * + * As a result, you can define the metadata type, use $(expr) to load the metadata into the elements + * matched by expr, then redefine the metadata type and run another $(expr) for other elements. + * + * @name $.metadata.setType + * + * @example <p id="one" class="some_class {item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("class") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from the class attribute + * + * @example <p id="one" class="some_class" data="{item_id: 1, item_label: 'Label'}">This is a p</p> + * @before $.metadata.setType("attr", "data") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a "data" attribute + * + * @example <p id="one" class="some_class"><script>{item_id: 1, item_label: 'Label'}</script>This is a p</p> + * @before $.metadata.setType("elem", "script") + * @after $("#one").metadata().item_id == 1; $("#one").metadata().item_label == "Label" + * @desc Reads metadata from a nested script element + * + * @param String type The encoding type + * @param String name The name of the attribute to be used to get metadata (optional) + * @cat Plugins/Metadata + * @descr Sets the type of encoding to be used when loading metadata for the first time + * @type undefined + * @see metadata() + */ + +(function($) { + +$.extend({ + metadata : { + defaults : { + type: 'class', + name: 'metadata', + cre: /({.*})/, + single: 'metadata' + }, + setType: function( type, name ){ + this.defaults.type = type; + this.defaults.name = name; + }, + get: function( elem, opts ){ + var settings = $.extend({},this.defaults,opts); + // check for empty string in single property + if ( !settings.single.length ) settings.single = 'metadata'; + + var data = $.data(elem, settings.single); + // returned cached data if it already exists + if ( data ) return data; + + data = "{}"; + + if ( settings.type == "class" ) { + var m = settings.cre.exec( elem.className ); + if ( m ) + data = m[1]; + } else if ( settings.type == "elem" ) { + if( !elem.getElementsByTagName ) + return undefined; + var e = elem.getElementsByTagName(settings.name); + if ( e.length ) + data = $.trim(e[0].innerHTML); + } else if ( elem.getAttribute != undefined ) { + var attr = elem.getAttribute( settings.name ); + if ( attr ) + data = attr; + } + + if ( data.indexOf( '{' ) <0 ) + data = "{" + data + "}"; + + data = eval("(" + data + ")"); + + $.data( elem, settings.single, data ); + return data; + } + } +}); + +/** + * Returns the metadata object for the first member of the jQuery object. + * + * @name metadata + * @descr Returns element's metadata object + * @param Object opts An object contianing settings to override the defaults + * @type jQuery + * @cat Plugins/Metadata + */ +$.fn.metadata = function( opts ){ + return $.metadata.get( this[0], opts ); +}; + +})(jQuery);
\ No newline at end of file diff --git a/data/js/script.js b/data/js/script.js index ea9bac814..77336a88e 100644 --- a/data/js/script.js +++ b/data/js/script.js @@ -10,7 +10,7 @@ function fixedEncodeURIComponent (str) { $('.highlight_line').removeClass("highlight_line"); - if (hash.match(/^#n\d+$/) === null) { + if (hash.match(/^#n(?:-.+-)?\d+$/) === null) { return; } @@ -25,22 +25,24 @@ function fixedEncodeURIComponent (str) { lexer_source.push({ label: window.lexers[key], value: key }); } - $('#language').autocomplete({ + $('[id^=language-]').autocomplete({ source: lexer_source, select: function(event, ui) { - window.location = window.paste_base + '/' + fixedEncodeURIComponent(ui.item.value); + event.preventDefault(); + window.location = $(event.target).data("base-url") + '/' + fixedEncodeURIComponent(ui.item.value); } }); - $(document).on("keyup", "#language", function(event) { + $(document).on("keyup", "[id^=language-]", function(event) { if (event.keyCode == 13) { - window.location = window.paste_base + '/' + fixedEncodeURIComponent($(this).val()); + event.preventDefault(); + window.location = $(event.target).data("base-url") + '/' + fixedEncodeURIComponent($(this).val()); } }); - $('#language-toggle').click(function() { + $('[id^=language-toggle-]').click(function(event) { setTimeout(function() { - $('#language').focus(); + $(event.target).parent().find('[id^=language-]').focus(); }, 0); }); @@ -54,7 +56,7 @@ function fixedEncodeURIComponent (str) { }); window.lines_wrapped = true; - $('#linewrap').click(function() { + $('[id^=linewrap-]').click(function() { if (window.lines_wrapped == true) { $(".highlight > pre").css("white-space", "pre"); } else { @@ -63,7 +65,7 @@ function fixedEncodeURIComponent (str) { window.lines_wrapped = !window.lines_wrapped; }); - $('.upload_history_thumbnails a').popover({ + $('.upload_thumbnails a').popover({ trigger: "hover", placement: "bottom", html: true, @@ -75,7 +77,7 @@ function fixedEncodeURIComponent (str) { window.page_mode = "normal"; $('#delete_button').hide(); $("#delete_form input[id^='delete_']").remove(); - $(".upload_history_thumbnails .marked").removeClass("marked"); + $(".upload_thumbnails .marked").removeClass("marked"); break; default: window.page_mode = "delete"; @@ -84,7 +86,7 @@ function fixedEncodeURIComponent (str) { } }); - $('.upload_history_thumbnails a').on("click", function(event) { + $('.upload_thumbnails a').on("click", function(event) { if (window.page_mode == "delete") { event.preventDefault(); var data_id = $(event.target).parent().attr("data-id"); @@ -105,8 +107,14 @@ function fixedEncodeURIComponent (str) { }); function handle_resize() { - var div = $('.upload_history_thumbnails'); - div.width(div.parent().width() - (div.parent().width() % div.find('a').outerWidth(true))); + $('.upload_thumbnails').each(function() { + var div = $(this); + + need_multiple_lines = div.parent().width() < (div.find('a').outerWidth(true) * div.find('a').size()); + + div.css('margin-left', need_multiple_lines ? "auto" : "0"); + div.width(div.parent().width() - (div.parent().width() % div.find('a').outerWidth(true))); + }); } $(window).resize(function() { @@ -118,15 +126,26 @@ function fixedEncodeURIComponent (str) { if (window.File && window.FileList) { function checkFileUpload(evt) { var sum = 0; - var files = evt.target.files; + var filenum = 0; + var files = []; + + $('.file-upload').each(function() { + for (var i = 0; i < this.files.length; i++) { + var file = this.files[i]; + files.push(file); + } + }); - // TODO: check all forms, not only the one we are called from for (var i = 0; i < files.length; i++) { - var f = evt.target.files[i]; + var f = files[i]; sum += f.size; + filenum++; } - if (sum > max_upload_size) { + if (filenum > max_files_per_upload) { + document.getElementById('upload_button').innerHTML = "Too many files"; + document.getElementById('upload_button').disabled = true; + } else if (sum > max_upload_size) { document.getElementById('upload_button').innerHTML = "File(s) too big"; document.getElementById('upload_button').disabled = true; } else { @@ -135,9 +154,25 @@ function fixedEncodeURIComponent (str) { } } - $('.file-upload').bind('change', checkFileUpload); + $(document).on('change', '.file-upload', checkFileUpload); } + $(document).on("change", '.file-upload', function() { + var need_new = true; + + $('.file-upload').each(function() { + if ($(this).prop("files").length == 0) { + need_new = false; + return; + } + }); + + if (need_new) { + $(this).parent().append('<input class="file-upload" type="file" name="file[]" multiple="multiple"><br>'); + } + + }); + if (typeof $.tablesorter !== 'undefined') { // source: https://projects.archlinux.org/archweb.git/tree/sitestatic/archweb.js $.tablesorter.addParser({ @@ -185,30 +220,19 @@ function fixedEncodeURIComponent (str) { }, type: 'numeric' }); - $.tablesorter.addParser({ - // set a unique id - id: 'mydate', - re: /t=([0-9]+)$/, - is: function(s) { - // return false so this parser is not auto detected - return false; - }, - format: function(s) { - var matches = this.re.exec(s); - if (!matches) { - return 0; + + $(".tablesorter").tablesorter({ + textExtraction: function(node) { + var attr = $(node).attr('data-sort-value'); + if (typeof attr !== 'undefined' && attr !== false) { + var intAttr = parseInt(attr); + if (!isNaN(intAttr)) { + return intAttr; + } + return attr; } - //console.log(s, matches); - return matches[1]; - }, - type: 'numeric' - }); - $("#upload_history:has(tbody tr)").tablesorter({ - headers: { - 0: {sorter: false}, - 4: {sorter: "mydate"}, - }, - sortList: [[4,1]], + return $(node).text(); + } }); } |