diff options
Diffstat (limited to 'application/controllers')
-rw-r--r-- | application/controllers/file.php | 1122 | ||||
-rw-r--r-- | application/controllers/tools.php | 45 | ||||
-rw-r--r-- | application/controllers/user.php | 516 | ||||
-rw-r--r-- | application/controllers/welcome.php | 27 |
4 files changed, 1683 insertions, 27 deletions
diff --git a/application/controllers/file.php b/application/controllers/file.php new file mode 100644 index 000000000..e1b43d314 --- /dev/null +++ b/application/controllers/file.php @@ -0,0 +1,1122 @@ +<?php +/* + * Copyright 2009-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class File extends MY_Controller { + + protected $json_enabled_functions = array( + "upload_history", + "do_upload", + "do_delete", + "do_multipaste", + ); + + function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + + if (is_cli_client()) { + $this->var->view_dir = "file_plaintext"; + } else { + $this->var->view_dir = "file"; + } + } + + function index() + { + if ($this->input->is_cli_request()) { + $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") { + $this->_non_existent(); + } else { + $this->upload_form(); + } + } + + function _download() + { + $id = $this->uri->segment(1); + $lexer = urldecode($this->uri->segment(2)); + + $is_multipaste = false; + if ($this->mmultipaste->id_exists($id)) { + $is_multipaste = true; + + if(!$this->mmultipaste->valid_id($id)) { + return $this->_non_existent(); + } + $files = $this->mmultipaste->get_files($id); + $this->data["title"] = "Multipaste"; + } elseif ($this->mfile->id_exists($id)) { + if (!$this->mfile->valid_id($id)) { + return $this->_non_existent(); + } + + $files = array($this->mfile->get_filedata($id)); + $this->data["title"] = htmlspecialchars($files[0]["filename"]); + } else { + assert(0); + } + + assert($files !== false); + assert(is_array($files)); + assert(count($files) >= 1); + + // don't allow unowned files to be downloaded + foreach ($files as $filedata) { + if ($filedata["user"] == 0) { + return $this->_non_existent(); + } + } + + $etag = ""; + foreach ($files as $filedata) { + $etag = sha1($etag.$filedata["hash"]); + } + + // handle some common "lexers" here + switch ($lexer) { + case "": + break; + + 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); + + $filedata = $files[0]; + $filepath = $this->mfile->file($filedata["hash"]); + $this->ddownload->serveFile($filepath, $filedata["filename"], "text/plain"); + exit(); + } + + $this->load->library("output_cache"); + + foreach ($files as $key => $filedata) { + $file = $this->mfile->file($filedata['hash']); + + // 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"]); + } + + // resolve aliases + // this is mainly used for compatibility + $lexer = $this->mfile->resolve_lexer_alias($lexer); + + // if there is no mimetype mapping we can't highlight it + $can_highlight = $this->mfile->can_highlight($filedata["mimetype"]); + + $filesize_too_big = filesize($file) > $this->config->item('upload_max_text_size'); + + 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; + } + } + + $this->output_cache->add_function(function() use ($filedata, $lexer, $is_multipaste) { + $this->_highlight_file($filedata, $lexer, $is_multipaste); + }); + } + + // 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); + $this->output_cache->render(); + echo $this->load->view($this->var->view_dir.'/html_footer', $this->data, true); + } + + private function _colorify($file, $lexer, $anchor_id = false) + { + $return_value = 0; + $output = ""; + $lines_to_remove = 0; + + $output .= '<div class="code content table">'."\n"; + $output .= '<div class="highlight"><pre>'."\n"; + + ob_start(); + if ($lexer == "ascii") { + passthru('ansi2html -p < '.escapeshellarg($file), $return_value); + // Last line is empty + $lines_to_remove = 1; + } else { + passthru('pygmentize -F codetagify -O encoding=guess,outencoding=utf8,stripnl=False -l '.escapeshellarg($lexer).' -f html '.escapeshellarg($file), $return_value); + // Last 2 items are "</pre></div>" and "" + $lines_to_remove = 2; + } + $buf = ob_get_contents(); + ob_end_clean(); + + + $buf = explode("\n", $buf); + $line_count = count($buf); + + for ($i = 1; $i <= $lines_to_remove; $i++) { + unset($buf[$line_count - $i]); + } + + foreach ($buf as $key => $line) { + $line_number = $key + 1; + if ($key == 0) { + $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=\"#$anchor\" class=\"linenumber table-cell\">" + ."<span class=\"anchor\" id=\"$anchor\"> </span>" + ."</a>" + ."<span class=\"line table-cell\">".$line."</span>\n"; + $output .= "</div>"; + } + + $output .= "</pre></div>"; + $output .= "</div>"; + + return array( + "return_value" => $return_value, + "output" => $output + ); + } + + private function _highlight_file($filedata, $lexer, $is_multipaste) + { + // 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); + } + }); + + 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() + { + $this->data["title"] .= " - Not Found"; + $this->output->set_status_header(404); + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/non_existent', $this->data); + $this->load->view('footer', $this->data); + } + + function _show_url($ids, $lexer) + { + $redirect = false; + + if (!$this->muser->logged_in()) { + $this->muser->require_session(); + // keep the upload but require the user to login + $this->session->set_userdata("last_upload", array( + "ids" => $ids, + "lexer" => $lexer + )); + $this->session->set_flashdata("uri", "file/claim_id"); + $this->muser->require_access("basic"); + } + + foreach ($ids as $id) { + if ($lexer) { + $this->data['urls'][] = site_url($id).'/'.$lexer; + } else { + $this->data['urls'][] = site_url($id).'/'; + + if (count($ids) == 1) { + $filedata = $this->mfile->get_filedata($id); + $file = $this->mfile->file($filedata['hash']); + $type = $filedata['mimetype']; + $lexer = $this->mfile->should_highlight($type); + + // If we detected a highlightable file redirect, + // otherwise show the URL because browsers would just show a DL dialog + if ($lexer) { + $redirect = true; + } + } + } + } + + if (static_storage("response_type") == "json") { + return send_json_reply($this->data["urls"]); + } + + if (is_cli_client()) { + $redirect = false; + } + + if ($redirect && count($ids) == 1) { + redirect($this->data['urls'][0], "location", 303); + } else { + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/show_url', $this->data); + $this->load->view('footer', $this->data); + } + } + + function client() + { + $this->data['title'] .= ' - Client'; + + if (file_exists(FCPATH.'data/client/latest')) { + $this->var->latest_client = trim(file_get_contents(FCPATH.'data/client/latest')); + $this->data['client_link'] = base_url().'data/client/fb-'.$this->var->latest_client.'.tar.gz'; + } else { + $this->data['client_link'] = false; + } + $this->data['client_link_dir'] = base_url().'data/client/'; + $this->data['client_link_deb'] = base_url().'data/client/deb/'; + $this->data['client_link_slackware'] = base_url().'data/client/slackware/'; + + if (preg_match('#^https?://(.*?)/.*$#', site_url(), $matches) === 1) { + $this->data["domain"] = $matches[1]; + } else { + $this->data["domain"] = "unknown domain"; + } + + if (!is_cli_client()) { + $this->load->view('header', $this->data); + } + $this->load->view($this->var->view_dir.'/client', $this->data); + if (!is_cli_client()) { + $this->load->view('footer', $this->data); + } + } + + function upload_form() + { + $this->data['title'] .= ' - Upload'; + $this->data['small_upload_size'] = $this->config->item('small_upload_size'); + $this->data['max_upload_size'] = $this->config->item('upload_max_size'); + $this->data['upload_max_age'] = $this->config->item('upload_max_age')/60/60/24; + + $this->data['username'] = $this->muser->get_username(); + + $repaste_id = $this->input->get("repaste"); + + if ($repaste_id) { + $filedata = $this->mfile->get_filedata($repaste_id); + + if ($filedata !== false && $this->mfile->can_highlight($filedata["mimetype"])) { + $this->data["textarea_content"] = file_get_contents($this->mfile->file($filedata["hash"])); + } + } + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/upload_form', $this->data); + if (is_cli_client()) { + $this->client(); + } + $this->load->view('footer', $this->data); + } + + // Allow CLI clients to query the server for the maxium filesize so they can + // stop the upload before wasting time and bandwith + function get_max_size() + { + echo $this->config->item('upload_max_size'); + } + + function thumbnail() + { + $id = $this->uri->segment(3); + + if (!$this->mfile->valid_id($id)) { + return $this->_non_existent(); + } + + $etag = "$id-thumb"; + handle_etag($etag); + + $thumb_size = 150; + + $filedata = $this->mfile->get_filedata($id); + if (!$filedata) { + show_error("Failed to get file data"); + } + + $cache_key = $filedata['hash'].'_thumb_'.$thumb_size; + + $thumb = cache_function($cache_key, 100, function() use ($id, $thumb_size){ + $CI =& get_instance(); + $thumb = $CI->mfile->makeThumb($id, $thumb_size, IMAGETYPE_JPEG); + + if ($thumb === false) { + show_error("Failed to generate thumbnail"); + } + + return $thumb; + }); + + $this->output->set_header("Cache-Control:max-age=31536000, public"); + $this->output->set_header("Expires: ".date("r", time() + 365 * 24 * 60 * 60)); + $this->output->set_content_type("image/jpeg"); + $this->output->set_output($thumb); + } + + function upload_history_thumbnails() + { + $this->muser->require_access(); + + $user = $this->muser->get_userid(); + + $query = $this->db->query(" + SELECT `id`, `filename`, `mimetype`, `date`, `hash`, `filesize` + FROM files + WHERE user = ? + AND mimetype IN ('image/jpeg', 'image/png', 'image/gif') + ORDER BY date DESC + ", array($user))->result_array(); + + foreach($query as $key => $item) { + if (!$this->mfile->valid_id($item["id"])) { + unset($query[$key]); + continue; + } + $query[$key]["tooltip"] = $this->_tooltip_for_image($item); + } + + $this->data["items"] = $query; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/upload_history_thumbnails', $this->data); + $this->load->view('footer', $this->data); + } + + function upload_history() + { + $this->muser->require_access("apikey"); + + $user = $this->muser->get_userid(); + + $query = array(); + $lengths = array(); + + // key: database field name; value: display name + $fields = array( + "id" => "ID", + "filename" => "Filename", + "mimetype" => "Mimetype", + "date" => "Date", + "hash" => "Hash", + "filesize" => "Size" + ); + + $this->data['title'] .= ' - Upload history'; + foreach($fields as $length_key => $value) { + $lengths[$length_key] = mb_strlen($value); + } + + $order = is_cli_client() ? "ASC" : "DESC"; + + $items = $this->db->query(" + SELECT ".implode(",", array_keys($fields))." + FROM files + WHERE user = ? + ", 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($items); + } + + 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($items[$key][$length_key]); + if ($len > $lengths[$length_key]) { + $lengths[$length_key] = $len; + } + } + } + } + + $total_size = $this->db->query(" + SELECT sum(filesize) sum + FROM ( + SELECT filesize + FROM files + WHERE user = ? + GROUP BY hash + ) sub + ", array($user))->row_array(); + + $this->data["items"] = $items; + $this->data["lengths"] = $lengths; + $this->data["fields"] = $fields; + $this->data["total_size"] = format_bytes($total_size["sum"]); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/upload_history', $this->data); + $this->load->view('footer', $this->data); + } + + function do_delete() + { + $this->muser->require_access("apikey"); + + $ids = $this->input->post("ids"); + $userid = $this->muser->get_userid(); + $errors = array(); + $deleted = array(); + $deleted_count = 0; + $total_count = 0; + + if (!$ids || !is_array($ids)) { + show_error("No IDs specified"); + } + + 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 ($next) { + continue; + } + + $errors[] = array( + "id" => $id, + "reason" => "doesn't exist", + ); + } + + if (static_storage("response_type") == "json") { + return send_json_reply(array( + "errors" => $errors, + "deleted" => $deleted, + "total_count" => $total_count, + "deleted_count" => $deleted_count, + )); + } + + $this->data["errors"] = $errors; + $this->data["deleted_count"] = $deleted_count; + $this->data["total_count"] = $total_count; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'/deleted', $this->data); + $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"); + + if (!is_cli_client()) { + show_error("Not a listed cli client, please use the history to delete uploads.\n", 403); + } + + $id = $this->uri->segment(3); + $this->data["id"] = $id; + $userid = $this->muser->get_userid(); + + 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; + } + } + + 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 + // don't force them to log in just yet + if (!stateful_client()) { + $this->muser->require_access(); + } + + $content = $this->input->post("content"); + $filesize = strlen($content); + $filename = "stdin"; + + if (!$content) { + show_error("Nothing was pasted, content is empty.", 400); + } + + if ($filesize > $this->config->item('upload_max_size')) { + show_error("Error while uploading: File too big", 413); + } + + $limits = $this->muser->get_upload_id_limits(); + $id = $this->mfile->new_id($limits[0], $limits[1]); + $hash = md5($content); + + $folder = $this->mfile->folder($hash); + file_exists($folder) || mkdir ($folder); + $file = $this->mfile->file($hash); + + file_put_contents($file, $content); + $this->mfile->add_file($hash, $id, $filename); + $this->_show_url(array($id), false); + } + + // Handles uploaded files + function do_upload() + { + // stateful clients get a cookie to claim the ID later + // don't force them to log in just yet + if (!stateful_client()) { + $this->muser->require_access("basic"); + } + + $ids = array(); + + $extension = $this->input->post('extension'); + $multipaste = $this->input->post('multipaste'); + + $files = getNormalizedFILES(); + + if (empty($files)) { + show_error("No file was uploaded or unknown error occured."); + } + + // Check for errors before doing anything + // First error wins and is displayed, these shouldn't happen that often anyway. + foreach ($files as $key => $file) { + // getNormalizedFILES() removes any file with error == 4 + if ($file['error'] !== UPLOAD_ERR_OK) { + // ERR_OK only for completeness, condition above ignores it + $errors = array( + UPLOAD_ERR_OK => "There is no error, the file uploaded with success", + UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini", + UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form", + UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded", + UPLOAD_ERR_NO_FILE => "No file was uploaded", + UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder", + UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk", + UPLOAD_ERR_EXTENSION => "A PHP extension stopped the file upload", + ); + + $msg = "Unknown error."; + + if (isset($errors[$file['error']])) { + $msg = $errors[$file['error']]; + } else { + $msg = "Unknown error code: ".$file['error'].". Please report a bug."; + } + + show_error("Error while uploading: ".$msg, 400); + } + + $filesize = filesize($file['tmp_name']); + if ($filesize > $this->config->item('upload_max_size')) { + show_error("Error while uploading: File too big", 413); + } + } + + foreach ($files as $key => $file) { + $limits = $this->muser->get_upload_id_limits(); + $id = $this->mfile->new_id($limits[0], $limits[1]); + $hash = md5_file($file['tmp_name']); + + // work around a curl bug and allow the client to send the real filename base64 encoded + // TODO: this interface currently sets the same filename for every file if you use multiupload + $filename = $this->input->post("filename"); + if ($filename !== false) { + $filename = base64_decode($filename, true); + } + + // fall back if base64_decode failed + if ($filename === false) { + $filename = $file['name']; + } + + $filename = trim($filename, "\r\n\0\t\x0B"); + + $folder = $this->mfile->folder($hash); + file_exists($folder) || mkdir ($folder); + $file_path = $this->mfile->file($hash); + + move_uploaded_file($file['tmp_name'], $file_path); + $this->mfile->add_file($hash, $id, $filename); + $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); + } + + function claim_id() + { + $this->muser->require_access(); + + $last_upload = $this->session->userdata("last_upload"); + + if ($last_upload === false) { + show_error("Failed to get last upload data"); + } + + $ids = $last_upload["ids"]; + $errors = array(); + + assert(is_array($ids)); + + foreach ($ids as $key => $id) { + $filedata = $this->mfile->get_filedata($id); + + if ($filedata["user"] != 0) { + $errors[] = $id; + } + + $this->mfile->adopt($id); + } + + if (!empty($errors)) { + show_error("Someone already owns '".implode(", ", $errors)."', can't reassign."); + } + + $this->session->unset_userdata("last_upload"); + + $this->_show_url($ids, $last_upload["lexer"]); + } + + function contact() + { + $file = FCPATH."data/local/contact-info.php"; + if (file_exists($file)) { + $this->data["contact_info"] = file_get_contents($file); + } else { + $this->data["contact_info"] = '<p>Contact info not available.</p>'; + } + + $this->load->view('header', $this->data); + $this->load->view('contact', $this->data); + $this->load->view('footer', $this->data); + } + + /* Functions below this comment can only be run via the CLI + * `php index.php file <function name>` + */ + + // Removes old files + function cron() + { + if (!$this->input->is_cli_request()) return; + + // 0 age disables age checks + if ($this->config->item('upload_max_age') == 0) return; + + $oldest_time = (time() - $this->config->item('upload_max_age')); + $oldest_session_time = (time() - $this->config->item("sess_expiration")); + + $small_upload_size = $this->config->item('small_upload_size'); + + $query = $this->db->query(' + SELECT hash, id, user + FROM files + WHERE date < ? OR (user = 0 AND date < ?)', + array($oldest_time, $oldest_session_time)); + + foreach($query->result_array() as $row) { + $file = $this->mfile->file($row['hash']); + if (!file_exists($file)) { + $this->mfile->delete_id($row["id"]); + continue; + } + + if ($row["user"] == 0 || filesize($file) > $small_upload_size) { + if (filemtime($file) < $oldest_time) { + unlink($file); + $this->mfile->delete_hash($row["hash"]); + } else { + $this->mfile->delete_id($row["id"]); + if ($this->mfile->stale_hash($row["hash"])) { + unlink($file); + } + } + } + } + } + + /* remove files without database entries */ + function clean_stale_files() + { + if (!$this->input->is_cli_request()) return; + + $upload_path = $this->config->item("upload_path"); + $outer_dh = opendir($upload_path); + + while (($dir = readdir($outer_dh)) !== false) { + if (!is_dir($upload_path."/".$dir) || $dir == ".." || $dir == ".") { + continue; + } + + $dh = opendir($upload_path."/".$dir); + + $empty = true; + + while (($file = readdir($dh)) !== false) { + if ($file == ".." || $file == ".") { + continue; + } + + $query = $this->db->query("SELECT hash FROM files WHERE hash = ? LIMIT 1", array($file))->row_array(); + + if (empty($query)) { + unlink($upload_path."/".$dir."/".$file); + } else { + $empty = false; + } + } + + closedir($dh); + + if ($empty) { + rmdir($upload_path."/".$dir); + } + } + closedir($outer_dh); + } + + function nuke_id() + { + if (!$this->input->is_cli_request()) return; + + $id = $this->uri->segment(3); + + $file_data = $this->mfile->get_filedata($id); + + if (empty($file_data)) { + echo "unknown id \"$id\"\n"; + return; + } + + $hash = $file_data["hash"]; + $this->mfile->delete_hash($hash); + echo "removed hash \"$hash\"\n"; + } + + function update_file_metadata() + { + if (!$this->input->is_cli_request()) return; + + $chunk = 500; + + $total = $this->db->count_all("files"); + + for ($limit = 0; $limit < $total; $limit += $chunk) { + $query = $this->db->query(" + SELECT hash + FROM files + GROUP BY hash + LIMIT $limit, $chunk + ")->result_array(); + + foreach ($query as $key => $item) { + $hash = $item["hash"]; + $filesize = intval(filesize($this->mfile->file($hash))); + $mimetype = $this->mfile->mimetype($this->mfile->file($hash)); + + $this->db->query(" + UPDATE files + SET filesize = ?, mimetype = ? + WHERE hash = ? + ", array($filesize, $mimetype, $hash)); + } + } + } +} + +# vim: set noet: diff --git a/application/controllers/tools.php b/application/controllers/tools.php new file mode 100644 index 000000000..b80dc5024 --- /dev/null +++ b/application/controllers/tools.php @@ -0,0 +1,45 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Tools extends MY_Controller { + + function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + if (!$this->input->is_cli_request()) { + show_error("This can only be called via CLI"); + } + } + + function index() + { + echo "php index.php <controller> <function> [arguments]\n"; + echo "\n"; + echo "Functions:\n"; + echo " file cron Cronjob\n"; + echo " file nuke_id <ID> Nukes all IDs sharing the same hash\n"; + echo " user cron Cronjob\n"; + echo " tools update_database Update/Initialise the database\n"; + echo "\n"; + echo "Functions that shouldn't have to be run:\n"; + echo " file clean_stale_files Remove files without database entries\n"; + echo " file update_file_metadata Update filesize and mimetype in database\n"; + exit; + } + + function update_database() + { + $this->load->library('migration'); + if ( ! $this->migration->current()) { + show_error($this->migration->error_string()); + } + } +} diff --git a/application/controllers/user.php b/application/controllers/user.php new file mode 100644 index 000000000..079f1665c --- /dev/null +++ b/application/controllers/user.php @@ -0,0 +1,516 @@ +<?php +/* + * Copyright 2012-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class User extends MY_Controller { + protected $json_enabled_functions = array( + "create_apikey", + "apikeys", + ); + + + function __construct() + { + parent::__construct(); + + $this->var->view_dir = "user/"; + } + + function index() + { + if ($this->input->is_cli_request()) { + $this->load->library("../controllers/tools"); + return $this->tools->index(); + } + + $this->data["username"] = $this->muser->get_username(); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'index', $this->data); + $this->load->view('footer', $this->data); + } + + function test_login() + { + $username = $this->input->post('username'); + $password = $this->input->post('password'); + + if ($this->muser->login($username, $password)) { + $this->output->set_status_header(204); + } else { + $this->output->set_status_header(401); + } + } + + function login() + { + $this->muser->require_session(); + $this->session->keep_flashdata("uri"); + + if ($this->input->post('process') !== false) { + $username = $this->input->post('username'); + $password = $this->input->post('password'); + + $result = $this->muser->login($username, $password); + + if ($result !== true) { + $this->data['login_error'] = true; + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'login', $this->data); + $this->load->view('footer', $this->data); + } else { + $uri = $this->session->flashdata("uri"); + if ($uri) { + redirect($uri); + } else { + redirect("/"); + } + } + } else { + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'login', $this->data); + $this->load->view('footer', $this->data); + } + } + + function create_apikey() + { + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + $comment = $this->input->post("comment"); + $comment = $comment === false ? "" : $comment; + $access_level = $this->input->post("access_level"); + + if ($access_level === false) { + $access_level = "apikey"; + } + + $valid_levels = $this->muser->get_access_levels(); + if (array_search($access_level, $valid_levels) === false) { + show_error("Invalid access levels requested."); + } + + if (strlen($comment) > 255) { + show_error("Comment may only be 255 chars long."); + } + + $key = random_alphanum(32); + + $this->db->query(" + INSERT INTO `apikeys` + (`key`, `user`, `comment`, `access_level`) + VALUES (?, ?, ?, ?) + ", array($key, $userid, $comment, $access_level)); + + if (static_storage("response_type") == "json") { + return send_json_reply(array("new_key" => $key)); + } + + if (is_cli_client()) { + echo "$key\n"; + } else { + redirect("user/apikeys"); + } + } + + function delete_apikey() + { + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + $key = $this->input->post("key"); + + $this->db->query(" + DELETE FROM `apikeys` + WHERE `user` = ? + AND `key` = ? + ", array($userid, $key)); + + redirect("user/apikeys"); + } + + function apikeys() + { + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + + $query = $this->db->query(" + SELECT `key`, UNIX_TIMESTAMP(`created`) `created`, `comment`, `access_level` + FROM `apikeys` + WHERE `user` = ? order by created desc + ", array($userid))->result_array(); + + if (static_storage("response_type") == "json") { + return send_json_reply($query); + } + + $this->data["query"] = $query; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'apikeys', $this->data); + $this->load->view('footer', $this->data); + } + + function create_invitation_key() + { + $this->duser->require_implemented("can_register_new_users"); + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + + $query = $this->db->query(" + SELECT count(*) count + FROM `actions` + WHERE `user` = ? + AND `action` = 'invitation' + ", array($userid))->row_array(); + + if ($query["count"] + 1 > 3) { + show_error("You can't create more invitation keys at this time."); + } + + $key = random_alphanum(12, 16); + + $this->db->query(" + INSERT INTO `actions` + (`key`, `user`, `date`, `action`) + VALUES (?, ?, ?, 'invitation') + ", array($key, $userid, time())); + + redirect("user/invite"); + } + + function invite() + { + $this->duser->require_implemented("can_register_new_users"); + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + + $query = $this->db->query(" + SELECT `key`, `date` + FROM `actions` + WHERE `user` = ? + AND `action` = 'invitation' + ", array($userid))->result_array(); + + $this->data["query"] = $query; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'invite', $this->data); + $this->load->view('footer', $this->data); + } + + function register() + { + $this->duser->require_implemented("can_register_new_users"); + $key = $this->uri->segment(3); + $process = $this->input->post("process"); + $values = array( + "username" => "", + "email" => "" + ); + $error = array(); + + $query = $this->muser->get_action("invitation", $key); + + $referrer = $query["user"]; + + if ($process !== false) { + $username = $this->input->post("username"); + $email = $this->input->post("email"); + $password = $this->input->post("password"); + $password_confirm = $this->input->post("password_confirm"); + + if (!$username || strlen($username) > 32 || !preg_match("/^[a-z0-9]+$/", $username)) { + $error[]= "Invalid username (only up to 32 chars of a-z0-9 are allowed)."; + } else { + if ($this->muser->username_exists($username)) { + $error[] = "Username already exists."; + } + } + + $this->load->helper("email"); + if (!valid_email($email)) { + $error[]= "Invalid email."; + } + + if (!$password || $password != $password_confirm) { + $error[]= "No password or passwords don't match."; + } + + if (empty($error)) { + $this->db->query(" + INSERT INTO users + (`username`, `password`, `email`, `referrer`) + VALUES(?, ?, ?, ?) + ", array( + $username, + $this->muser->hash_password($password), + $email, + $referrer + )); + $this->db->query(" + DELETE FROM actions + WHERE `key` = ? + ", array($key)); + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'registered', $this->data); + $this->load->view('footer', $this->data); + return; + } else { + $values["username"] = $username; + $values["email"] = $email; + } + } + + $this->data["key"] = $key; + $this->data["values"] = $values; + $this->data["error"] = $error; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'register', $this->data); + $this->load->view('footer', $this->data); + } + + // This routes the different steps of a password reset + function reset_password() + { + $this->duser->require_implemented("can_reset_password"); + $key = $this->uri->segment(3); + + if ($_SERVER["REQUEST_METHOD"] == "GET" && $key === false) { + return $this->_reset_password_username_form(); + } + + if ($key === false) { + return $this->_reset_password_send_mail(); + } + + if ($key !== false) { + return $this->_reset_password_form(); + } + } + + // This simply queries the username + function _reset_password_username_form() + { + $this->data['username'] = $this->muser->get_username(); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'reset_password_username_form', $this->data); + $this->load->view('footer', $this->data); + } + + // This sends a mail to the user containing the reset link + function _reset_password_send_mail() + { + $key = random_alphanum(12, 16); + $username = $this->input->post("username"); + + if (!$this->muser->username_exists($username)) { + show_error("Invalid username"); + } + + $userinfo = $this->db->query(" + SELECT id, email, username + FROM users + WHERE username = ? + ", array($username))->row_array(); + + $this->load->library("email"); + + $this->db->query(" + INSERT INTO `actions` + (`key`, `user`, `date`, `action`) + VALUES (?, ?, ?, 'passwordreset') + ", array($key, $userinfo["id"], time())); + + $admininfo = $this->db->query(" + SELECT email + FROM users + WHERE referrer is null + ORDER BY id asc + LIMIT 1 + ")->row_array(); + + $this->email->from($admininfo["email"]); + $this->email->to($userinfo["email"]); + $this->email->subject("FileBin password reset"); + $this->email->message("" + ."Someone requested a password reset for the account '${userinfo["username"]}'\n" + ."from the IP address '${_SERVER["REMOTE_ADDR"]}'.\n" + ."\n" + ."Please follow this link to reset your password:\n" + .site_url("user/reset_password/$key") + ); + $this->email->send(); + + // don't disclose full email addresses + $this->data["email_domain"] = substr($userinfo["email"], strpos($userinfo["email"], "@") + 1); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'reset_password_link_sent', $this->data); + $this->load->view('footer', $this->data); + } + + // This displays a form and handles the reset if the form has been filled out correctly + function _reset_password_form() + { + $process = $this->input->post("process"); + $key = $this->uri->segment(3); + $error = array(); + + $query = $this->muser->get_action("passwordreset", $key); + + $userid = $query["user"]; + + if ($process !== false) { + $password = $this->input->post("password"); + $password_confirm = $this->input->post("password_confirm"); + + if (!$password || $password != $password_confirm) { + $error[]= "No password or passwords don't match."; + } + + if (empty($error)) { + $this->db->query(" + UPDATE users + SET `password` = ? + WHERE `id` = ? + ", array($this->muser->hash_password($password), $userid)); + $this->db->query(" + DELETE FROM actions + WHERE `key` = ? + ", array($key)); + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'reset_password_success', $this->data); + $this->load->view('footer', $this->data); + return; + } + } + + $this->data["key"] = $key; + $this->data["error"] = $error; + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'reset_password_form', $this->data); + $this->load->view('footer', $this->data); + } + + function profile() + { + $this->muser->require_access(); + + if ($this->input->post("process") !== false) { + $this->_save_profile(); + } + + $this->data["profile_data"] = $this->muser->get_profile_data(); + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'profile', $this->data); + $this->load->view('footer', $this->data); + } + + private function _save_profile() + { + $this->muser->require_access(); + + /* + * Key = name of the form field + * Value = function that sanatizes the value and returns it + * TODO: some kind of error handling that doesn't loose correctly filled out fields + */ + $value_processor = array(); + + $value_processor["upload_id_limits"] = function($value) { + $values = explode("-", $value); + + if (!is_array($values) || count($values) != 2) { + show_error("Invalid upload id limit value"); + } + + $lower = intval($values[0]); + $upper = intval($values[1]); + + if ($lower > $upper) { + show_error("lower limit > upper limit"); + } + + if ($lower < 3 || $upper > 64) { + show_error("upper or lower limit out of bounds (3-64)"); + } + + return $lower."-".$upper; + }; + + $data = array(); + foreach (array_keys($value_processor) as $field) { + $value = $this->input->post($field); + + if ($value !== false) { + $data[$field] = $value_processor[$field]($value); + } + } + + if (!empty($data)) { + $this->muser->update_profile($data); + } + + $this->data["alerts"][] = array( + "type" => "success", + "message" => "Changes saved", + ); + + return true; + } + + function logout() + { + $this->muser->logout(); + redirect('/'); + } + + function hash_password() + { + $process = $this->input->post("process"); + $password = $this->input->post("password"); + $password_confirm = $this->input->post("password_confirm"); + $this->data["hash"] = false; + $this->data["password"] = $password; + + if ($process !== false) { + if (!$password || $password != $password_confirm) { + $error[]= "No password or passwords don't match."; + } else { + $this->data["hash"] = $this->muser->hash_password($password); + } + } + + $this->load->view('header', $this->data); + $this->load->view($this->var->view_dir.'hash_password', $this->data); + $this->load->view('footer', $this->data); + } + + function cron() + { + if (!$this->input->is_cli_request()) return; + + if ($this->config->item('actions_max_age') == 0) return; + + $oldest_time = (time() - $this->config->item('actions_max_age')); + + $this->db->query(" + DELETE FROM actions + WHERE date < ? + ", array($oldest_time)); + } +} diff --git a/application/controllers/welcome.php b/application/controllers/welcome.php deleted file mode 100644 index 21bef43d9..000000000 --- a/application/controllers/welcome.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); - -class Welcome extends CI_Controller { - - /** - * Index Page for this controller. - * - * Maps to the following URL - * http://example.com/index.php/welcome - * - or - - * http://example.com/index.php/welcome/index - * - or - - * Since this controller is set as the default controller in - * config/routes.php, it's displayed at http://example.com/ - * - * So any other public methods not prefixed with an underscore will - * map to /index.php/welcome/<method_name> - * @see http://codeigniter.com/user_guide/general/urls.html - */ - public function index() - { - $this->load->view('welcome_message'); - } -} - -/* End of file welcome.php */ -/* Location: ./application/controllers/welcome.php */
\ No newline at end of file |