diff options
Diffstat (limited to 'application/controllers')
-rw-r--r-- | application/controllers/file.php | 891 | ||||
-rw-r--r-- | application/controllers/user.php | 504 | ||||
-rw-r--r-- | application/controllers/welcome.php | 27 |
3 files changed, 1395 insertions, 27 deletions
diff --git a/application/controllers/file.php b/application/controllers/file.php new file mode 100644 index 000000000..98ccae884 --- /dev/null +++ b/application/controllers/file.php @@ -0,0 +1,891 @@ +<?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", + ); + + function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('muser'); + + if (is_cli_client()) { + $this->var->view_dir = "file_plaintext"; + } else { + $this->var->view_dir = "file"; + } + } + + function index() + { + if ($this->input->is_cli_request()) { + echo "php index.php file <function> [arguments]\n"; + echo "\n"; + echo "Functions:\n"; + echo " cron Cronjob\n"; + echo " nuke_id <ID> Nukes all IDs sharing the same hash\n"; + echo "\n"; + echo "Functions that shouldn't have to be run:\n"; + echo " clean_stale_files Remove files without database entries\n"; + echo " update_file_metadata Update filesize and mimetype in database\n"; + exit; + } + // Try to guess what the user would like to do. + $id = $this->uri->segment(1); + if (!empty($_FILES)) { + $this->do_upload(); + } 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)); + + $filedata = $this->mfile->get_filedata($id); + $file = $this->mfile->file($filedata['hash']); + + if (!$this->mfile->valid_id($id)) { + $this->_non_existent(); + return; + } + + // don't allow unowned files to be downloaded + if ($filedata["user"] == 0) { + $this->_non_existent(); + return; + } + + // helps to keep traffic low when reloading + $etag = $filedata["hash"]."-".$filedata["date"]; + + // 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; + + 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); + + // create the qr code for /ID/ + if ($lexer == "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(); + } + + // user wants the plain file + if ($lexer == 'plain') { + handle_etag($etag); + rangeDownload($file, $filedata["filename"], "text/plain"); + exit(); + } + + if ($lexer == 'info') { + $this->_display_info($id); + return; + } + + // 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) { + // 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: allow 'none'; img-src *; media-src *; font-src *; style-src * 'unsafe-inline'; script-src 'none'; object-src *; frame-src 'none'; "); + } + handle_etag($etag); + rangeDownload($file, $filedata["filename"], $filedata["mimetype"]); + exit(); + } + + $this->data['title'] = htmlspecialchars($filedata['filename']); + $this->data['id'] = $id; + + header("Content-Type: text/html\n"); + + $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 chache the result + $this->load->driver('cache', array('adapter' => $this->config->item("cache_backend"))); + if (! $cached = $this->cache->get($filedata['hash'].'_'.$lexer)) { + $cached = array(); + if ($lexer == "rmd") { + ob_start(); + + echo '<table class="content"><tr>'; + echo '<td class="markdownrender">'."\n"; + passthru('perl '.FCPATH.'scripts/Markdown.pl '.escapeshellarg($file), $cached["return_value"]); + + $cached["output"] = ob_get_contents(); + ob_end_clean(); + } else { + $cached = $this->_colorify($file, $lexer); + } + + if ($cached["return_value"] != 0) { + $ret = $this->_colorify($file, "text"); + $cached["output"] = $ret["output"]; + } + $this->cache->save($filedata['hash'].'_'.$lexer, $cached, 100); + } + + if ($cached["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>"; + } + + // Don't use append_output because the output class 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 $cached["output"]; + echo $this->load->view($this->var->view_dir.'/html_footer', $this->data, true); + } + + private function _colorify($file, $lexer) + { + $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); + } + + // 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>" + ."<span class=\"line table-cell\">".$line."</span>\n"; + $output .= "</div>"; + } + + $output .= "</pre></div>"; + $output .= "</div>"; + + return array( + "return_value" => $return_value, + "output" => $output + ); + } + + function _display_info($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("apikey"); + } + + 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 = $this->mfile->makeThumb($id, 150, IMAGETYPE_JPEG); + + if ($thumb === false) { + show_error("Failed to generate thumbnail"); + } + + $filedata = $this->mfile->get_filedata($id); + if (!$filedata) { + show_error("Failed to get file data"); + } + + $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; + } + + $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> + "; + } + + $this->data["query"] = $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"; + + $query = $this->db->query(" + SELECT ".implode(",", array_keys($fields))." + FROM files + WHERE user = ? + ORDER BY date $order + ", array($user))->result_array(); + + if (static_storage("response_type") == "json") { + return send_json_reply($query); + } + + foreach($query as $key => $item) { + $query[$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]); + 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["query"] = $query; + $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"); + $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++; + + if (!$this->mfile->id_exists($id)) { + $errors[] = array( + "id" => $id, + "reason" => "doesn't exist", + ); + continue; + } + + if ($this->mfile->delete_id($id)) { + $deleted[] = $id; + $deleted_count++; + } else { + $errors[] = array( + "id" => $id, + "reason" => "unknown error", + ); + } + } + + 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 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; + + if ($id && !$this->mfile->id_exists($id)) { + show_error("Unknown ID '$id'.", 404); + } + + if ($this->mfile->delete_id($id)) { + echo "$id has been deleted.\n"; + } else { + echo "Deletion failed. Do you really own that file?\n"; + } + } + + // Handle pastes + 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); + chmod($file, 0600); + $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("apikey"); + } + + $ids = array(); + + $extension = $this->input->post('extension'); + + $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); + chmod($file_path, 0600); + $this->mfile->add_file($hash, $id, $filename); + $ids[] = $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->db->query('DELETE FROM files WHERE id = ? LIMIT 1', array($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'])); + } else { + $this->db->query('DELETE FROM files WHERE id = ? LIMIT 1', array($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->db->query(" + DELETE FROM files + WHERE hash = ? + ", array($hash)); + + unlink($this->mfile->file($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/user.php b/application/controllers/user.php new file mode 100644 index 000000000..823166ea5 --- /dev/null +++ b/application/controllers/user.php @@ -0,0 +1,504 @@ +<?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->load->model("muser"); + + $this->var->view_dir = "user/"; + } + + function 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"); + + + 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`) + VALUES (?, ?, ?) + ", array($key, $userid, $comment)); + + 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` + 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(); + + // TODO: count both, invited users and key + $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 = 0 + 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 |