From 19f0aab3221dd7760387cbec745c1eca9b215af7 Mon Sep 17 00:00:00 2001 From: Florian Pritz Date: Sat, 9 Sep 2017 16:08:00 +0200 Subject: WIP: CI3 migration Signed-off-by: Florian Pritz --- application/controllers/Api.php | 66 ++ application/controllers/Main.php | 989 ++++++++++++++++++++++++++ application/controllers/Tools.php | 130 ++++ application/controllers/User.php | 705 ++++++++++++++++++ application/controllers/api.php | 66 -- application/controllers/file/Multipaste.php | 113 +++ application/controllers/file/file_default.php | 989 -------------------------- application/controllers/file/multipaste.php | 113 --- application/controllers/tools.php | 130 ---- application/controllers/user.php | 705 ------------------ 10 files changed, 2003 insertions(+), 2003 deletions(-) create mode 100644 application/controllers/Api.php create mode 100644 application/controllers/Main.php create mode 100644 application/controllers/Tools.php create mode 100644 application/controllers/User.php delete mode 100644 application/controllers/api.php create mode 100644 application/controllers/file/Multipaste.php delete mode 100644 application/controllers/file/file_default.php delete mode 100644 application/controllers/file/multipaste.php delete mode 100644 application/controllers/tools.php delete mode 100644 application/controllers/user.php (limited to 'application/controllers') diff --git a/application/controllers/Api.php b/application/controllers/Api.php new file mode 100644 index 000000000..9540f1ff7 --- /dev/null +++ b/application/controllers/Api.php @@ -0,0 +1,66 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Api extends MY_Controller { + + public function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + } + + public function route() { + try { + $requested_version = $this->uri->segment(2); + $controller = $this->uri->segment(3); + $function = $this->uri->segment(4); + + if (!preg_match("/^v([0-9]+)(.[0-9]+){0,2}$/", $requested_version)) { + throw new \exceptions\PublicApiException("api/invalid-version", "Invalid API version requested"); + } + + $requested_version = substr($requested_version, 1); + + $major = intval(explode(".", $requested_version)[0]); + + if (!preg_match("/^[a-zA-Z-_]+$/", $controller)) { + throw new \exceptions\PublicApiException("api/invalid-endpoint", "Invalid endpoint requested"); + } + + if (!preg_match("/^[a-zA-Z-_]+$/", $function)) { + throw new \exceptions\PublicApiException("api/invalid-endpoint", "Invalid endpoint requested"); + } + + $namespace = "controllers\\api\\v".$major; + $class = $namespace."\\".$controller; + $class_info = $namespace."\\api_info"; + + if (!class_exists($class_info) || version_compare($class_info::get_version(), $requested_version, "<")) { + throw new \exceptions\PublicApiException("api/version-not-supported", "Requested API version is not supported"); + } + + if (!class_exists($class)) { + throw new \exceptions\PublicApiException("api/unknown-endpoint", "Unknown endpoint requested"); + } + + $c= new $class; + if (!method_exists($c, $function)) { + throw new \exceptions\PublicApiException("api/unknown-endpoint", "Unknown endpoint requested"); + } + return send_json_reply($c->$function()); + } catch (\exceptions\PublicApiException $e) { + return send_json_error_reply($e->get_error_id(), $e->getMessage(), $e->get_data()); + } catch (\Exception $e) { + \libraries\ExceptionHandler::log_exception($e); + return send_json_error_reply("internal-error", "An unhandled internal server error occured"); + } + } +} diff --git a/application/controllers/Main.php b/application/controllers/Main.php new file mode 100644 index 000000000..b1ec81710 --- /dev/null +++ b/application/controllers/Main.php @@ -0,0 +1,989 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Main extends MY_Controller { + + function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + } + + 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 (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(); + } + } + + /** + * Generate a page title of the format "Multipaste - $filename, $filename, … (N more)". + * This mainly helps in IRC channels to quickly determine what is in a multipaste. + * + * @param files array of filedata + * @return title to be used + */ + private function _multipaste_page_title(array $files) + { + $filecount = count($files); + $title = "Multipaste ($filecount files) - "; + $titlenames = array(); + $len = strlen($title); + $delimiter = ', '; + $maxlen = 100; + + foreach ($files as $file) { + if ($len > $maxlen) break; + + $filename = $file['filename']; + $titlenames[] = htmlspecialchars($filename); + $len += strlen($filename) + strlen($delimiter); + } + + $title .= implode($delimiter, $titlenames); + + $leftover_count = $filecount - count($titlenames); + + if ($leftover_count > 0) { + $title .= $delimiter.'… ('.$leftover_count.' more)'; + } + + return $title; + } + + 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)) { + $this->mmultipaste->delete_id($id); + return $this->_non_existent(); + } + $files = $this->mmultipaste->get_files($id); + $this->data["title"] = $this->_multipaste_page_title($files); + } 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["data_id"]); + } + + // 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"); + $qr = new \Endroid\QrCode\QrCode(); + $qr->setText(site_url($id).'/') + ->setSize(350) + ->setErrorCorrection('low') + ->render(); + exit(); + + case "info": + return $this->_display_info($id); + + case "tar": + if ($is_multipaste) { + return $this->_tarball($id); + } + + case "pls": + if ($is_multipaste) { + return $this->_generate_playlist($id); + } + + default: + if ($is_multipaste) { + throw new \exceptions\UserInputException("file/download/invalid-action", "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["data_id"]); + $this->ddownload->serveFile($filepath, $filedata["filename"], "text/plain"); + exit(); + } + + $output_cache = new \libraries\Output_cache(); + + foreach ($files as $key => $filedata) { + $file = $this->mfile->file($filedata['data_id']); + $pygments = new \libraries\Pygments($file, $filedata["mimetype"], $filedata["filename"]); + + // 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 = $pygments->autodetect_lexer(); + } + + // resolve aliases + // this is mainly used for compatibility + $lexer = $pygments->resolve_lexer_alias($lexer); + + // if there is no mimetype mapping we can't highlight it + $can_highlight = $pygments->can_highlight(); + + $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 { + $mimetype = $filedata["mimetype"]; + $base = explode("/", $filedata["mimetype"])[0]; + + if (\libraries\Image::type_supported($mimetype)) { + $filedata["tooltip"] = \service\files::tooltip($filedata); + $filedata["orientation"] = libraries\Image::get_exif_orientation($file); + $output_cache->add_merge( + array("items" => array($filedata)), + 'file/fragments/thumbnail' + ); + } else if ($base == "audio") { + $output_cache->add(array("filedata" => $filedata), "file/fragments/audio-player"); + } else if ($base == "video") { + $output_cache->add(array("filedata" => $filedata), "file/fragments/video-player"); + } else { + $output_cache->add_merge( + array("items" => array($filedata)), + 'file/fragments/uploads_table' + ); + } + continue; + } + } + + if ($lexer == "asciinema") { + $output_cache->add(array("filedata" => $filedata), "file/fragments/asciinema-player"); + } else { + $output_cache->add_function(function() use ($output_cache, $filedata, $lexer, $is_multipaste) { + $renderer = new \service\renderer($output_cache, $this->mfile, $this->data); + $renderer->highlight_file($filedata, $lexer, $is_multipaste); + }); + } + } + + // TODO: move lexers json to dedicated URL + $this->data['lexers'] = \libraries\Pygments::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('file/html_header', $this->data, true); + $output_cache->render(); + echo $this->load->view('file/html_footer', $this->data, true); + } + + 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('file/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('file/file_info', $this->data); + $this->load->view('footer', $this->data); + } + } + + private function _tarball($id) + { + if ($this->mmultipaste->id_exists($id)) { + $seen = array(); + $path = $this->mmultipaste->get_tarball_path($id); + $archive = new \service\storage($path); + + if (!$archive->exists()) { + $files = $this->mmultipaste->get_files($id); + + $total_size = 0; + foreach ($files as $filedata) { + $total_size += $filedata["filesize"]; + } + + if ($total_size > $this->config->item("tarball_max_size")) { + throw new \exceptions\PublicApiException("file/tarball/tarball-filesize-limit", "Tarball too large, refusing to create."); + } + + $tmpfile = $archive->begin(); + // create empty tar archive so PharData has something to open + file_put_contents($tmpfile, str_repeat("\0", 1024*10)); + $a = new PharData($tmpfile); + + foreach ($files as $filedata) { + $filename = $filedata["filename"]; + if (isset($seen[$filename]) && $seen[$filename]) { + $filename = $filedata["id"]."-".$filedata["filename"]; + } + assert(!isset($seen[$filename])); + $a->addFile($this->mfile->file($filedata["data_id"]), $filename); + $seen[$filename] = true; + } + $archive->gzip_compress(); + $archive->commit(); + } + + // update mtime so the cronjob will keep the file for longer + $lock = fopen($archive->get_file(), "r+"); + flock($lock, LOCK_SH); + touch($archive->get_file()); + flock($lock, LOCK_UN); + + assert(filesize($archive->get_file()) > 0); + + $this->load->driver("ddownload"); + $this->ddownload->serveFile($archive->get_file(), "$id.tar.gz", "application/x-gzip"); + } + } + + /** + * Generate a PLS v2 playlist + */ + private function _generate_playlist($id) + { + $files = $this->mmultipaste->get_files($id); + $counter = 1; + + $playlist = "[playlist]\n"; + foreach ($files as $file) { + // only add audio/video files + $base = explode("/", $file['mimetype'])[0]; + if (!($base === "audio" || $base === "video")) { + continue; + } + + $url = site_url($file["id"]); + $playlist .= sprintf("File%d=%s\n", $counter++, $url); + } + $playlist .= sprintf("NumberOfEntries=%d\n", $counter - 1); + $playlist .= "Version=2\n"; + + $this->output->set_content_type('audio/x-scpls'); + $this->output->set_output($playlist); + } + + function _non_existent() + { + $this->data["title"] .= " - Not Found"; + $this->output->set_status_header(404); + $this->load->view('header', $this->data); + $this->load->view('file/non_existent', $this->data); + $this->load->view('footer', $this->data); + } + + private function _prepare_claim($ids, $lexer) + { + if (!$this->muser->logged_in()) { + $this->muser->require_session(); + // keep the upload but require the user to login + $last_upload = $this->session->userdata("last_upload"); + if ($last_upload === false) { + $last_upload = array( + "ids" => [], + "lexer" => "", + ); + } + $last_upload = array( + "ids" => array_merge($last_upload['ids'], $ids), + "lexer" => "", + ); + $this->session->set_userdata("last_upload", $last_upload); + $this->data["redirect_uri"] = "file/claim_id"; + $this->muser->require_access("basic"); + } + + } + + function _show_url($ids, $lexer) + { + $redirect = false; + $this->_prepare_claim($ids, $lexer); + + 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['data_id']); + $pygments = new \libraries\Pygments($file, $filedata["mimetype"], $filedata["filename"]); + $lexer = $pygments->should_highlight(); + + // If we detected a highlightable file redirect, + // otherwise show the URL because browsers would just show a DL dialog + if ($lexer) { + $redirect = true; + } + } + } + } + + if ($redirect && count($ids) == 1) { + redirect($this->data['urls'][0], "location", 303); + } else { + $this->load->view('header', $this->data); + $this->load->view('file/show_url', $this->data); + $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); + + $pygments = new \libraries\Pygments($this->mfile->file($filedata["data_id"]), $filedata["mimetype"], $filedata["filename"]); + if ($filedata !== false && $pygments->can_highlight()) { + $this->data["textarea_content"] = file_get_contents($this->mfile->file($filedata["data_id"])); + } + } + + 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->load->view('header', $this->data); + $this->load->view('file/upload_form', $this->data); + $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; + $cache_timeout = 60*60*24*30; # 1 month + + $filedata = $this->mfile->get_filedata($id); + if (!$filedata) { + throw new \exceptions\ApiException("file/thumbnail/filedata-unavailable", "Failed to get file data"); + } + + $cache_key = $filedata['data_id'].'_thumb_'.$thumb_size; + + $thumb = cache_function($cache_key, $cache_timeout, function() use ($filedata, $thumb_size){ + $CI =& get_instance(); + $img = new libraries\Image($this->mfile->file($filedata["data_id"])); + $img->makeThumb($thumb_size, $thumb_size); + $thumb = $img->get(IMAGETYPE_JPEG); + 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(); + + // TODO: move to \service\files and possibly use \s\f::history() + $query = $this->db + ->select('files.id, filename, mimetype, files.date, hash, file_storage.id storage_id, filesize, user') + ->from('files') + ->join('file_storage', 'file_storage.id = files.file_storage_id') + ->where(' + (files.user = '.$this->db->escape($user).') + AND ( + mimetype LIKE \'image%\' + OR mimetype IN (\'application/pdf\') + )', null, false) + ->order_by('date', 'desc') + ->get()->result_array(); + + foreach($query as $key => $item) { + assert($item["user"] === $user); + $item["data_id"] = $item['hash']."-".$item['storage_id']; + $query[$key]["data_id"] = $item["data_id"]; + if (!$this->mfile->valid_filedata($item)) { + unset($query[$key]); + continue; + } + $query[$key]["tooltip"] = \service\files::tooltip($item); + $query[$key]["orientation"] = libraries\Image::get_exif_orientation($this->mfile->file($item["data_id"])); + } + + $this->data["items"] = $query; + + $this->load->view('header', $this->data); + $this->load->view('file/upload_history_thumbnails', $this->data); + $this->load->view('footer', $this->data); + } + + public function handle_history_submit() + { + $this->muser->require_access("apikey"); + + $process = $this->input->post("process"); + + $dispatcher = [ + "delete" => function() { + return $this->do_delete(); + }, + "multipaste" => function() { + return $this->_append_multipaste_queue(); + }, + ]; + + if (isset($dispatcher[$process])) { + $dispatcher[$process](); + } else { + throw new \exceptions\UserInputException("file/handle_history_submit/invalid-process-value", "Value in process field not found in dispatch table"); + } + } + + private function _append_multipaste_queue() + { + $ids = $this->input->post_array("ids"); + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->append($ids); + redirect("file/multipaste/queue"); + } + + function upload_history() + { + $this->muser->require_access("apikey"); + + $history = service\files::history($this->muser->get_userid()); + + // 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); + } + + foreach ($history["multipaste_items"] as $key => $item) { + $size = 0; + $filenames = array(); + $files = array(); + $max_filenames = 10; + + foreach ($item["items"] as $i) { + $size += $history["items"][$i["id"]]["filesize"]; + $files[] = array( + "filename" => $history["items"][$i["id"]]['filename'], + "sort_order" => $i["sort_order"], + ); + } + + uasort($files, function ($a, $b) { + return $a['sort_order'] - $b['sort_order']; + }); + + $filenames = array_map(function ($a) {return $a['filename'];}, $files); + + if (count($filenames) > $max_filenames) { + $filenames = array_slice($filenames, 0, $max_filenames); + $filenames[] = "..."; + } + + $history["items"][] = array( + "id" => $item["url_id"], + "filename" => count($item["items"])." file(s)", + "mimetype" => "", + "date" => $item["date"], + "hash" => "", + "filesize" => $size, + "preview_text" => implode("\n", $filenames), + ); + } + + uasort($history["items"], function($a, $b) { + return $b["date"] - $a["date"]; + }); + + foreach($history["items"] as $key => $item) { + $history["items"][$key]["filesize"] = format_bytes($item["filesize"]); + + if (isset($item['preview_text'])) { + $history["items"][$key]["preview_text"] = htmlentities($item['preview_text']); + } + } + + $this->data["items"] = $history["items"]; + $this->data["lengths"] = $lengths; + $this->data["fields"] = $fields; + $this->data["total_size"] = format_bytes($history["total_size"]); + + $this->load->view('header', $this->data); + $this->load->view('file/upload_history', $this->data); + $this->load->view('footer', $this->data); + } + + function do_delete() + { + $this->muser->require_access("apikey"); + + $ids = $this->input->post_array("ids"); + + $ret = \service\files::delete($ids); + + $this->data["errors"] = $ret["errors"]; + $this->data["deleted_count"] = $ret["deleted_count"]; + $this->data["total_count"] = $ret["total_count"]; + + $this->load->view('header', $this->data); + $this->load->view('file/deleted', $this->data); + $this->load->view('footer', $this->data); + } + + function do_multipaste() + { + $this->muser->require_access("basic"); + + $ids = $this->input->post_array("ids"); + $userid = $this->muser->get_userid(); + $limits = $this->muser->get_upload_id_limits(); + + $ret = \service\files::create_multipaste($ids, $userid, $limits); + + return $this->_show_url(array($ret["url_id"]), false); + } + + /** + * Handle submissions from the web form (files and textareas). + */ + public function do_websubmit() + { + $files = getNormalizedFILES(); + $contents = $this->input->post_array("content"); + $filenames = $this->input->post_array("filename"); + + if (!is_array($filenames) || !is_array($contents)) { + throw new \exceptions\UserInputException('file/websubmit/invalid-form', 'The submitted POST form is invalid'); + } + + $ids = array(); + $ids = array_merge($ids, $this->_handle_textarea($contents, $filenames)); + $ids = array_merge($ids, $this->_handle_files($files)); + + + if (empty($ids)) { + throw new \exceptions\UserInputException("file/websubmit/no-input", "You didn't enter any text or upload any files"); + } + + if (count($ids) > 1) { + $userid = $this->muser->get_userid(); + $limits = $this->muser->get_upload_id_limits(); + $multipaste_id = \service\files::create_multipaste($ids, $userid, $limits)["url_id"]; + + $ids[] = $multipaste_id; + $this->_prepare_claim($ids, false); + + redirect(site_url($multipaste_id)."/"); + } + + $this->_show_url($ids, false); + } + + private function _handle_files($files) + { + $ids = array(); + + if (!empty($files)) { + $limits = $this->muser->get_upload_id_limits(); + $userid = $this->muser->get_userid(); + service\files::verify_uploaded_files($files); + + foreach ($files as $key => $file) { + $id = $this->mfile->new_id($limits[0], $limits[1]); + service\files::add_uploaded_file($userid, $id, $file["tmp_name"], $file["name"]); + $ids[] = $id; + } + } + + return $ids; + } + + private function _handle_textarea($contents, $filenames) + { + $ids = array(); + + foreach ($contents as $key => $content) { + $filesize = strlen($content); + + if ($filesize == 0) { + unset($contents[$key]); + } + + if ($filesize > $this->config->item('upload_max_size')) { + throw new \exceptions\RequestTooBigException("file/websubmit/request-too-big", "Error while uploading: Paste too big"); + } + } + + $limits = $this->muser->get_upload_id_limits(); + $userid = $this->muser->get_userid(); + + foreach ($contents as $key => $content) { + $filename = "stdin"; + if (isset($filenames[$key]) && $filenames[$key] != "") { + $filename = $filenames[$key]; + } + + $id = $this->mfile->new_id($limits[0], $limits[1]); + service\files::add_file_data($userid, $id, $content, $filename); + $ids[] = $id; + } + + return $ids; + } + + function claim_id() + { + $this->muser->require_access(); + + $last_upload = $this->session->userdata("last_upload"); + + if ($last_upload === false) { + throw new \exceptions\PublicApiException("file/claim_id/last_upload-failed", "Failed to get last upload data, unable to claim uploads"); + } + + $ids = $last_upload["ids"]; + $errors = array(); + + assert(is_array($ids)); + + foreach ($ids as $key => $id) { + $affected = 0; + $affected += $this->mfile->adopt($id); + $affected += $this->mmultipaste->adopt($id); + + if ($affected == 0) { + $errors[] = $id; + } + } + + if (!empty($errors)) { + throw new \exceptions\PublicApiException("file/claim_id/failed", "Failed to claim ".implode(", ", $errors).""); + } + + $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"] = '

Contact info not available.

'; + } + + $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 ` + */ + + // Removes old files + function cron() + { + $this->_require_cli_request(); + + $tarball_dir = $this->config->item("upload_path")."/special/multipaste-tarballs"; + if (is_dir($tarball_dir)) { + $tarball_cache_time = $this->config->item("tarball_cache_time"); + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($tarball_dir), RecursiveIteratorIterator::SELF_FIRST); + + foreach ($it as $file) { + if ($file->isFile()) { + if ($file->getMTime() < time() - $tarball_cache_time) { + $lock = fopen($file, "r+"); + flock($lock, LOCK_EX); + unlink($file); + flock($lock, LOCK_UN); + } + } + } + } + + $oldest_time = (time() - $this->config->item('upload_max_age')); + $oldest_session_time = (time() - $this->config->item("sess_expiration")); + $config = array( + "upload_max_age" => $this->config->item("upload_max_age"), + "small_upload_size" => $this->config->item("small_upload_size"), + "sess_expiration" => $this->config->item("sess_expiration"), + ); + + $query = $this->db->select('file_storage_id storage_id, files.id, user, files.date, hash') + ->from('files') + ->join('file_storage', "file_storage.id = files.file_storage_id") + ->where("user", 0) + ->where("files.date <", $oldest_session_time) + ->get()->result_array(); + + foreach($query as $row) { + $row['data_id'] = $row['hash'].'-'.$row['storage_id']; + \service\files::valid_id($row, $config, $this->mfile, time()); + } + + // 0 age disables age checks + if ($this->config->item('upload_max_age') == 0) return; + + $query = $this->db->select('hash, files.id, user, files.date, file_storage.id storage_id') + ->from('files') + ->join('file_storage', "file_storage.id = files.file_storage_id") + ->where('files.date <', $oldest_time) + ->get()->result_array(); + + foreach($query as $row) { + $row['data_id'] = $row['hash'].'-'.$row['storage_id']; + \service\files::valid_id($row, $config, $this->mfile, time()); + } + } + + /* remove files without database entries */ + function clean_stale_files() + { + $this->_require_cli_request(); + + $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 == "." || $dir == "special") { + continue; + } + + $dh = opendir($upload_path."/".$dir); + + $empty = true; + + while (($file = readdir($dh)) !== false) { + if ($file == ".." || $file == ".") { + continue; + } + + try { + list($hash, $storage_id) = explode("-", $file); + } catch (\ErrorException $e) { + unlink($upload_path."/".$dir."/".$file); + continue; + } + + $query = $this->db->select('hash, id') + ->from('file_storage') + ->where('hash', $hash) + ->where('id', $storage_id) + ->limit(1) + ->get()->row_array(); + + if (empty($query)) { + $this->mfile->delete_data_id($file); + } else { + $empty = false; + } + } + + closedir($dh); + + if ($empty && file_exists($upload_path."/".$dir)) { + rmdir($upload_path."/".$dir); + } + } + closedir($outer_dh); + + // TODO: clean up special/multipaste-tarballs? cron() already expires + // after a rather short time, do we really need this here then? + } + + function nuke_id() + { + $this->_require_cli_request(); + + $id = $this->uri->segment(3); + + $file_data = $this->mfile->get_filedata($id); + + if (empty($file_data)) { + echo "unknown id \"$id\"\n"; + return; + } + + $data_id = $file_data["data_id"]; + $this->mfile->delete_data_id($data_id); + echo "removed data_id \"$data_id\"\n"; + } + + function update_file_metadata() + { + $this->_require_cli_request(); + + $chunk = 500; + + $total = $this->db->count_all("file_storage"); + + for ($limit = 0; $limit < $total; $limit += $chunk) { + $query = $this->db->select('hash, id') + ->from('file_storage') + ->limit($chunk, $limit) + ->get()->result_array(); + + foreach ($query as $key => $item) { + $data_id = $item["hash"].'-'.$item['id']; + $filepath = $this->mfile->file($data_id); + $mimetype = mimetype($filepath); + $filesize = filesize($filepath); + + $this->db->where('id', $item['id']) + ->set(array( + 'mimetype' => $mimetype, + 'filesize' => $filesize, + )) + ->update('file_storage'); + } + } + } +} + +# vim: set noet: diff --git a/application/controllers/Tools.php b/application/controllers/Tools.php new file mode 100644 index 000000000..9e0ddfb5f --- /dev/null +++ b/application/controllers/Tools.php @@ -0,0 +1,130 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Tools extends MY_Controller { + + function __construct() + { + parent::__construct(); + + $this->load->model('mfile'); + $this->_require_cli_request(); + } + + function index() + { + echo "php index.php [arguments]\n"; + echo "\n"; + echo "Functions:\n"; + echo " file cron Cronjob\n"; + echo " file nuke_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'); + $upgraded = $this->migration->current(); + if ( ! $upgraded) { + throw new \exceptions\ApiException("tools/update_database/migration-error", $this->migration->error_string()); + } + + if ($upgraded === true) { + echo "Already at latest database version. No upgrade performed\n"; + } + + if (is_int($upgraded)) { + echo "Database upgraded sucessfully to version: $upgraded\n"; + } + } + + function drop_all_tables() + { + $tables = $this->db->list_tables(); + $prefix = $this->db->dbprefix; + $tables_to_drop = array(); + + foreach ($tables as $table) { + if ($prefix === "" || strpos($table, $prefix) === 0) { + $tables_to_drop[] = $this->db->protect_identifiers($table); + } + } + + if (empty($tables_to_drop)) { + return; + } + + + if ($this->db->dbdriver !== 'postgre') { + $this->db->query('SET FOREIGN_KEY_CHECKS = 0'); + } + $this->db->query('DROP TABLE '.implode(", ", $tables_to_drop)); + if ($this->db->dbdriver !== 'postgre') { + $this->db->query('SET FOREIGN_KEY_CHECKS = 1'); + } + } + + function test() + { + global $argv; + $testcase = $argv[3]; + + $testcase = str_replace("application/", "", $testcase); + $testcase = str_replace("/", "\\", $testcase); + $testcase = str_replace(".php", "", $testcase); + + $test = new $testcase(); + + $exitcode = 0; + + $refl = new ReflectionClass($test); + foreach ($refl->getMethods() as $method) { + if (strpos($method->name, "test_") === 0) { + try { + $test->setTestNamePrefix($method->name." - "); + $test->init(); + $test->setTestID("{$testcase}->{$method->name}"); + $test->{$method->name}(); + $test->cleanup(); + } catch (\Exception $e) { + echo "not ok - uncaught exception in {$testcase}->{$method->name}\n"; + \libraries\ExceptionHandler::exception_handler($e); + $exitcode = 255; + } + } + } + + if ($exitcode == 0) { + $test->done_testing(); + } else { + exit($exitcode); + } + } + + function generate_coverage_report() + { + include APPPATH."../vendor/autoload.php"; + $coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage(); + foreach (glob(FCPATH."/test-coverage-data/*") as $file) { + $coverage->merge(unserialize(file_get_contents($file))); + } + + $writer = new \SebastianBergmann\CodeCoverage\Report\Clover(); + $writer->process($coverage, 'code-coverage-report.xml'); + $writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade(); + $writer->process($coverage, 'code-coverage-report'); + print "Report saved to ./code-coverage-report/index.html\n"; + } +} diff --git a/application/controllers/User.php b/application/controllers/User.php new file mode 100644 index 000000000..d87b544c7 --- /dev/null +++ b/application/controllers/User.php @@ -0,0 +1,705 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class User extends MY_Controller { + + function __construct() + { + parent::__construct(); + } + + 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('user/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(403); + } + } + + function login() + { + $redirect_uri = $this->input->get("redirect_uri"); + $this->muser->require_session(); + + if (!preg_match('/^[0-9a-zA-Z\/_-]*$/', $redirect_uri)) { + $redirect_uri = '/'; + } + + if ($this->muser->logged_in()) { + redirect($redirect_uri); + } + + $this->data['redirect_uri'] = $redirect_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('user/login', $this->data); + $this->load->view('footer', $this->data); + } else { + redirect($redirect_uri); + } + } else { + $this->load->view('header', $this->data); + $this->load->view('user/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"; + } + + $key = \service\user::create_apikey($userid, $comment, $access_level); + + redirect("user/apikeys"); + } + + function delete_apikey() + { + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + $key = $this->input->post("key"); + + $this->db->where('user', $userid) + ->where('key', $key) + ->delete('apikeys'); + + redirect("user/apikeys"); + } + + function apikeys() + { + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + $apikeys = \service\user::apikeys($userid); + $this->data["query"] = $apikeys["apikeys"]; + + $this->load->view('header', $this->data); + $this->load->view('user/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(); + + $invitations = $this->db->select('user') + ->from('actions') + ->where('user', $userid) + ->where('action', 'invitation') + ->count_all_results(); + + if ($invitations + 1 > $this->config->item('max_invitation_keys')) { + throw new \exceptions\PublicApiException("user/invitation-limit", "You can't create more invitation keys at this time."); + } + + $key = random_alphanum(12, 16); + + $this->db->set(array( + 'key' => $key, + 'user' => $userid, + 'date' => time(), + 'action' => 'invitation' + )) + ->insert('actions'); + + 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->select('key, date') + ->from('actions') + ->where('user', $userid) + ->where('action', 'invitation') + ->get()->result_array(); + + $this->data["query"] = $query; + + $this->load->view('header', $this->data); + $this->load->view('user/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"]; + + $this->data['redirect_uri'] = "/"; + + 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 (!$this->muser->valid_username($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."; + } + } + + if (!$this->muser->valid_email($email)) { + $error[]= "Invalid email."; + } + + if (!$password || $password !== $password_confirm) { + $error[]= "No password or passwords don't match."; + } + + if (empty($error)) { + $this->muser->add_user($username, $password, $email, $referrer); + + $this->db->where('key', $key) + ->delete('actions'); + + $this->load->view('header', $this->data); + $this->load->view('user/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('user/register', $this->data); + $this->load->view('footer', $this->data); + } + + public function delete_account() + { + $this->muser->require_access(); + $this->duser->require_implemented("can_delete_account"); + + if ($_SERVER["REQUEST_METHOD"] == "GET") { + return $this->_delete_account_form(); + } elseif ($_SERVER["REQUEST_METHOD"] == "POST") { + return $this->_delete_account_process(); + } + } + + public function _delete_account_form() + { + $this->data['username'] = $this->muser->get_username(); + + $this->load->view('header', $this->data); + $this->load->view('user/delete_account_form', $this->data); + $this->load->view('footer', $this->data); + } + + public function _delete_account_process() + { + $username = $this->muser->get_username(); + $password = $this->input->post("password"); + + $useremail = $this->muser->get_email($this->muser->get_userid()); + + if ($this->muser->delete_user($username, $password)) { + $this->muser->logout(); + + $this->load->library("email"); + $this->email->from($this->config->item("email_from")); + $this->email->to($useremail); + $this->email->subject("FileBin account deleted"); + $this->email->message("" + ."Your FileBin account '${username}' at ".site_url()."\n" + ."has been permemently deleted.\n" + ."\n" + ."The request has been sent from the IP address '${_SERVER["REMOTE_ADDR"]}'\n" + ."and was confirmed with your password.\n" + ."\n" + ."Thank you for using FileBin!\n" + ); + $this->email->send(); + unset($this->data['username']); + unset($this->data['user_logged_in']); + + $this->load->view('header', $this->data); + $this->load->view('user/delete_account_success', $this->data); + $this->load->view('footer', $this->data); + return; + } else { + $this->data['alerts'][] = array( + "type" => "danger", + "message" => "Your password was incorrect", + ); + return $this->_delete_account_form(); + } + } + + // 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('user/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)) { + throw new \exceptions\PublicApiException("user/reset_password/invalid-username", "Invalid username"); + } + + $userinfo = $this->db->select('id, email, username') + ->from('users') + ->where('username', $username) + ->get()->row_array(); + + $this->load->library("email"); + + $this->db->set(array( + 'key' => $key, + 'user' => $userinfo['id'], + 'date' => time(), + 'action' => 'passwordreset' + )) + ->insert('actions'); + + $this->email->from($this->config->item("email_from")); + $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('user/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->muser->set_password($userid, $password); + + $this->db->where('key', $key) + ->delete('actions'); + + $this->load->view('header', $this->data); + $this->load->view('user/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('user/reset_password_form', $this->data); + $this->load->view('footer', $this->data); + } + + public function change_email() + { + $this->duser->require_implemented("can_change_email"); + $key = $this->uri->segment(3); + $action = $this->uri->segment(4); + + $alerts = array(); + + $query = $this->muser->get_action("change_email", $key); + + $userid = $query["user"]; + $data = json_decode($query['data'], true); + + switch ($action) { + case 'confirm': + $this->db->where('id', $userid) + ->update('users', array( + "email" => $data['new_email'], + )); + $alerts[] = array( + "type" => "success", + "message" => "Your email address has been updated", + ); + break; + case 'reject': + $this->db->where('id', $userid) + ->update('users', array( + "email" => $data['old_email'], + )); + foreach ($data['keys'] as $k) { + $this->db->where('key', $k) + ->delete('actions'); + } + $alerts[] = array( + "type" => "success", + "message" => "Your email change request has been canceled and/or your old email address has been restored", + ); + break; + default: + assert(false); + break; + } + + $this->data["alerts"] = $alerts; + + return $this->profile(); + } + + 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('user/profile', $this->data); + $this->load->view('footer', $this->data); + } + + private function _save_profile() + { + $this->muser->require_access(); + + $old = $this->muser->get_profile_data(); + + /* + * 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(); + $alerts = array(); + + $value_processor["upload_id_limits"] = function($value) { + $values = explode("-", $value); + + if (!is_array($values) || count($values) != 2) { + throw new \exceptions\PublicApiException("user/profile/invalid-upload-id-limit", "Invalid upload id limit value"); + } + + $lower = intval($values[0]); + $upper = intval($values[1]); + + if ($lower > $upper) { + throw new \exceptions\PublicApiException("user/profile/lower-bigger-than-upper", "lower limit > upper limit"); + } + + if ($lower < 3 || $upper > 64) { + throw new \exceptions\PublicApiException("user/profile/limit-out-of-bounds", "upper or lower limit out of bounds (3-64)"); + } + + return $lower."-".$upper; + }; + + $value_processor["email"] = function($value) use ($old, &$alerts) { + if (!$this->duser->is_implemented("can_change_email")) { + return null; + } + + if ($value === $old["email"]) { + return null; + } + + if (!$this->muser->valid_email($value)) { + throw new \exceptions\PublicApiException("user/profile/invalid-email", "Invalid email"); + } + + $this->load->library("email"); + $keys = array( + "old" => random_alphanum(12,16), + "new" => random_alphanum(12,16), + ); + $emails = array( + array( + "key" => $keys['old'], + "email" => $old['email'], + "user" => $this->muser->get_userid(), + ), + array( + "key" => $keys['new'], + "email" => $value, + "user" => $this->muser->get_userid(), + ), + ); + + foreach ($emails as $email) { + $key = $email['key']; + + $this->db->set(array( + 'key' => $key, + 'user' => $this->muser->get_userid(), + 'date' => time(), + 'action' => 'change_email', + 'data' => json_encode(array( + 'old_email' => $old['email'], + 'new_email' => $value, + 'keys' => $keys, + )), + )) + ->insert('actions'); + + $this->email->from($this->config->item("email_from")); + $this->email->to($email['email']); + $this->email->subject("FileBin email change confirmation"); + $this->email->message("" + ."A request has been sent to change the email address of account '${old["username"]}'\n" + ."from ".$old['email']." to $value.\n" + ."\n" + ."Please follow this link to CONFIRM the change:\n" + .site_url("user/change_email/$key/confirm")."\n\n" + ."Please follow this link to REJECT the change:\n" + .site_url("user/change_email/$key/reject")."\n\n" + ); + $this->email->send(); + $this->email->clear(); + } + + $alerts[] = array( + "type" => "info", + "message" => "Reset and confirmation emails have been sent to your new and old address. Until your new address is confirmed the old one will be displayed and used.", + ); + + return null; + }; + + + $data = array(); + foreach (array_keys($value_processor) as $field) { + $value = $this->input->post($field); + + if ($value !== false) { + $new_value = $value_processor[$field]($value); + if ($new_value !== null) { + $data[$field] = $new_value; + } + } + } + + if (!empty($data)) { + $this->muser->update_profile($data); + } + + $alerts[] = array( + "type" => "success", + "message" => "Changes saved", + ); + $this->data["alerts"] = $alerts; + + 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('user/hash_password', $this->data); + $this->load->view('footer', $this->data); + } + + function cron() + { + $this->_require_cli_request(); + + if ($this->config->item('actions_max_age') == 0) return; + + $oldest_time = (time() - $this->config->item('actions_max_age')); + + $this->db->where('date <', $oldest_time) + ->delete('actions'); + } + + private function _get_line_cli($message, $verification_func = NULL) + { + echo "$message: "; + + while ($line = fgets(STDIN)) { + $line = trim($line); + if ($verification_func === NULL) { + return $line; + } + + if ($verification_func($line)) { + return $line; + } else { + echo "$message: "; + } + } + } + + function add_user() + { + $this->_require_cli_request(); + $this->duser->require_implemented("can_register_new_users"); + + $error = array(); + + $username = $this->_get_line_cli("Username", function($username) { + if (!$this->muser->valid_username($username)) { + echo "Invalid username (only up to 32 chars of a-z0-9 are allowed).\n"; + return false; + } else { + if (get_instance()->muser->username_exists($username)) { + echo "Username already exists.\n"; + return false; + } + } + return true; + }); + + $email = $this->_get_line_cli("Email", function($email) { + if (!$this->muser->valid_email($email)) { + echo "Invalid email.\n"; + return false; + } + return true; + }); + + $password = $this->_get_line_cli("Password", function($password) { + if (!$password || $password === "") { + echo "No password supplied.\n"; + return false; + } + return true; + }); + + $this->muser->add_user($username, $password, $email, NULL); + + echo "User added\n"; + } +} diff --git a/application/controllers/api.php b/application/controllers/api.php deleted file mode 100644 index 9540f1ff7..000000000 --- a/application/controllers/api.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * Licensed under AGPLv3 - * (see COPYING for full license text) - * - */ - -class Api extends MY_Controller { - - public function __construct() - { - parent::__construct(); - - $this->load->model('mfile'); - $this->load->model('mmultipaste'); - } - - public function route() { - try { - $requested_version = $this->uri->segment(2); - $controller = $this->uri->segment(3); - $function = $this->uri->segment(4); - - if (!preg_match("/^v([0-9]+)(.[0-9]+){0,2}$/", $requested_version)) { - throw new \exceptions\PublicApiException("api/invalid-version", "Invalid API version requested"); - } - - $requested_version = substr($requested_version, 1); - - $major = intval(explode(".", $requested_version)[0]); - - if (!preg_match("/^[a-zA-Z-_]+$/", $controller)) { - throw new \exceptions\PublicApiException("api/invalid-endpoint", "Invalid endpoint requested"); - } - - if (!preg_match("/^[a-zA-Z-_]+$/", $function)) { - throw new \exceptions\PublicApiException("api/invalid-endpoint", "Invalid endpoint requested"); - } - - $namespace = "controllers\\api\\v".$major; - $class = $namespace."\\".$controller; - $class_info = $namespace."\\api_info"; - - if (!class_exists($class_info) || version_compare($class_info::get_version(), $requested_version, "<")) { - throw new \exceptions\PublicApiException("api/version-not-supported", "Requested API version is not supported"); - } - - if (!class_exists($class)) { - throw new \exceptions\PublicApiException("api/unknown-endpoint", "Unknown endpoint requested"); - } - - $c= new $class; - if (!method_exists($c, $function)) { - throw new \exceptions\PublicApiException("api/unknown-endpoint", "Unknown endpoint requested"); - } - return send_json_reply($c->$function()); - } catch (\exceptions\PublicApiException $e) { - return send_json_error_reply($e->get_error_id(), $e->getMessage(), $e->get_data()); - } catch (\Exception $e) { - \libraries\ExceptionHandler::log_exception($e); - return send_json_error_reply("internal-error", "An unhandled internal server error occured"); - } - } -} diff --git a/application/controllers/file/Multipaste.php b/application/controllers/file/Multipaste.php new file mode 100644 index 000000000..50367697c --- /dev/null +++ b/application/controllers/file/Multipaste.php @@ -0,0 +1,113 @@ + + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Multipaste extends MY_Controller { + + function __construct() { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + } + + public function append_multipaste_queue() { + $this->muser->require_access("basic"); + + $ids = $this->input->post_array("ids"); + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->append($ids); + + redirect("file/multipaste/queue"); + } + + public function review_multipaste() { + $this->muser->require_access("basic"); + + $this->load->view('header', $this->data); + $this->load->view('file/review_multipaste', $this->data); + $this->load->view('footer', $this->data); + } + + public function queue() { + $this->muser->require_access("basic"); + + $m = new \service\multipaste_queue(); + $ids = $m->get(); + + $this->data['ids'] = $ids; + $this->data['items'] = array_map(function($id) {return $this->_get_multipaste_item($id);}, $ids); + + $this->load->view('header', $this->data); + $this->load->view('file/multipaste/queue', $this->data); + $this->load->view('footer', $this->data); + } + + public function form_submit() { + $this->muser->require_access("basic"); + + $ids = $this->input->post_array('ids'); + $process = $this->input->post('process'); + + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->set($ids); + + $dispatcher = [ + 'save' => function() use ($ids, $m) { + redirect("file/multipaste/queue"); + }, + 'create' => function() use ($ids, $m) { + $userid = $this->muser->get_userid(); + $limits = $this->muser->get_upload_id_limits(); + $ret = \service\files::create_multipaste($ids, $userid, $limits); + $m->set([]); + redirect($ret['url_id'].'/'); + }, + ]; + + if (isset($dispatcher[$process])) { + $dispatcher[$process](); + } else { + throw new \exceptions\UserInputException("file/multipaste/form_submit/invalid-process-value", "Value in process field not found in dispatch table"); + } + } + + public function ajax_submit() { + $this->muser->require_access("basic"); + $ids = $this->input->post_array('ids'); + + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->set($ids); + } + + private function _get_multipaste_item($id) { + $filedata = $this->mfile->get_filedata($id); + $item = []; + $item['id'] = $filedata['id']; + $item['tooltip'] = \service\files::tooltip($filedata); + $item['title'] = $filedata['filename']; + if (\libraries\Image::type_supported($filedata["mimetype"])) { + $item['thumbnail'] = site_url("file/thumbnail/".$filedata['id']); + } + + return $item; + } + +} diff --git a/application/controllers/file/file_default.php b/application/controllers/file/file_default.php deleted file mode 100644 index 9ef7d0880..000000000 --- a/application/controllers/file/file_default.php +++ /dev/null @@ -1,989 +0,0 @@ - - * - * Licensed under AGPLv3 - * (see COPYING for full license text) - * - */ - -class File_default extends MY_Controller { - - function __construct() - { - parent::__construct(); - - $this->load->model('mfile'); - $this->load->model('mmultipaste'); - } - - 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 (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(); - } - } - - /** - * Generate a page title of the format "Multipaste - $filename, $filename, … (N more)". - * This mainly helps in IRC channels to quickly determine what is in a multipaste. - * - * @param files array of filedata - * @return title to be used - */ - private function _multipaste_page_title(array $files) - { - $filecount = count($files); - $title = "Multipaste ($filecount files) - "; - $titlenames = array(); - $len = strlen($title); - $delimiter = ', '; - $maxlen = 100; - - foreach ($files as $file) { - if ($len > $maxlen) break; - - $filename = $file['filename']; - $titlenames[] = htmlspecialchars($filename); - $len += strlen($filename) + strlen($delimiter); - } - - $title .= implode($delimiter, $titlenames); - - $leftover_count = $filecount - count($titlenames); - - if ($leftover_count > 0) { - $title .= $delimiter.'… ('.$leftover_count.' more)'; - } - - return $title; - } - - 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)) { - $this->mmultipaste->delete_id($id); - return $this->_non_existent(); - } - $files = $this->mmultipaste->get_files($id); - $this->data["title"] = $this->_multipaste_page_title($files); - } 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["data_id"]); - } - - // 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"); - $qr = new \Endroid\QrCode\QrCode(); - $qr->setText(site_url($id).'/') - ->setSize(350) - ->setErrorCorrection('low') - ->render(); - exit(); - - case "info": - return $this->_display_info($id); - - case "tar": - if ($is_multipaste) { - return $this->_tarball($id); - } - - case "pls": - if ($is_multipaste) { - return $this->_generate_playlist($id); - } - - default: - if ($is_multipaste) { - throw new \exceptions\UserInputException("file/download/invalid-action", "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["data_id"]); - $this->ddownload->serveFile($filepath, $filedata["filename"], "text/plain"); - exit(); - } - - $output_cache = new \libraries\Output_cache(); - - foreach ($files as $key => $filedata) { - $file = $this->mfile->file($filedata['data_id']); - $pygments = new \libraries\Pygments($file, $filedata["mimetype"], $filedata["filename"]); - - // 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 = $pygments->autodetect_lexer(); - } - - // resolve aliases - // this is mainly used for compatibility - $lexer = $pygments->resolve_lexer_alias($lexer); - - // if there is no mimetype mapping we can't highlight it - $can_highlight = $pygments->can_highlight(); - - $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 { - $mimetype = $filedata["mimetype"]; - $base = explode("/", $filedata["mimetype"])[0]; - - if (\libraries\Image::type_supported($mimetype)) { - $filedata["tooltip"] = \service\files::tooltip($filedata); - $filedata["orientation"] = libraries\Image::get_exif_orientation($file); - $output_cache->add_merge( - array("items" => array($filedata)), - 'file/fragments/thumbnail' - ); - } else if ($base == "audio") { - $output_cache->add(array("filedata" => $filedata), "file/fragments/audio-player"); - } else if ($base == "video") { - $output_cache->add(array("filedata" => $filedata), "file/fragments/video-player"); - } else { - $output_cache->add_merge( - array("items" => array($filedata)), - 'file/fragments/uploads_table' - ); - } - continue; - } - } - - if ($lexer == "asciinema") { - $output_cache->add(array("filedata" => $filedata), "file/fragments/asciinema-player"); - } else { - $output_cache->add_function(function() use ($output_cache, $filedata, $lexer, $is_multipaste) { - $renderer = new \service\renderer($output_cache, $this->mfile, $this->data); - $renderer->highlight_file($filedata, $lexer, $is_multipaste); - }); - } - } - - // TODO: move lexers json to dedicated URL - $this->data['lexers'] = \libraries\Pygments::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('file/html_header', $this->data, true); - $output_cache->render(); - echo $this->load->view('file/html_footer', $this->data, true); - } - - 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('file/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('file/file_info', $this->data); - $this->load->view('footer', $this->data); - } - } - - private function _tarball($id) - { - if ($this->mmultipaste->id_exists($id)) { - $seen = array(); - $path = $this->mmultipaste->get_tarball_path($id); - $archive = new \service\storage($path); - - if (!$archive->exists()) { - $files = $this->mmultipaste->get_files($id); - - $total_size = 0; - foreach ($files as $filedata) { - $total_size += $filedata["filesize"]; - } - - if ($total_size > $this->config->item("tarball_max_size")) { - throw new \exceptions\PublicApiException("file/tarball/tarball-filesize-limit", "Tarball too large, refusing to create."); - } - - $tmpfile = $archive->begin(); - // create empty tar archive so PharData has something to open - file_put_contents($tmpfile, str_repeat("\0", 1024*10)); - $a = new PharData($tmpfile); - - foreach ($files as $filedata) { - $filename = $filedata["filename"]; - if (isset($seen[$filename]) && $seen[$filename]) { - $filename = $filedata["id"]."-".$filedata["filename"]; - } - assert(!isset($seen[$filename])); - $a->addFile($this->mfile->file($filedata["data_id"]), $filename); - $seen[$filename] = true; - } - $archive->gzip_compress(); - $archive->commit(); - } - - // update mtime so the cronjob will keep the file for longer - $lock = fopen($archive->get_file(), "r+"); - flock($lock, LOCK_SH); - touch($archive->get_file()); - flock($lock, LOCK_UN); - - assert(filesize($archive->get_file()) > 0); - - $this->load->driver("ddownload"); - $this->ddownload->serveFile($archive->get_file(), "$id.tar.gz", "application/x-gzip"); - } - } - - /** - * Generate a PLS v2 playlist - */ - private function _generate_playlist($id) - { - $files = $this->mmultipaste->get_files($id); - $counter = 1; - - $playlist = "[playlist]\n"; - foreach ($files as $file) { - // only add audio/video files - $base = explode("/", $file['mimetype'])[0]; - if (!($base === "audio" || $base === "video")) { - continue; - } - - $url = site_url($file["id"]); - $playlist .= sprintf("File%d=%s\n", $counter++, $url); - } - $playlist .= sprintf("NumberOfEntries=%d\n", $counter - 1); - $playlist .= "Version=2\n"; - - $this->output->set_content_type('audio/x-scpls'); - $this->output->set_output($playlist); - } - - function _non_existent() - { - $this->data["title"] .= " - Not Found"; - $this->output->set_status_header(404); - $this->load->view('header', $this->data); - $this->load->view('file/non_existent', $this->data); - $this->load->view('footer', $this->data); - } - - private function _prepare_claim($ids, $lexer) - { - if (!$this->muser->logged_in()) { - $this->muser->require_session(); - // keep the upload but require the user to login - $last_upload = $this->session->userdata("last_upload"); - if ($last_upload === false) { - $last_upload = array( - "ids" => [], - "lexer" => "", - ); - } - $last_upload = array( - "ids" => array_merge($last_upload['ids'], $ids), - "lexer" => "", - ); - $this->session->set_userdata("last_upload", $last_upload); - $this->data["redirect_uri"] = "file/claim_id"; - $this->muser->require_access("basic"); - } - - } - - function _show_url($ids, $lexer) - { - $redirect = false; - $this->_prepare_claim($ids, $lexer); - - 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['data_id']); - $pygments = new \libraries\Pygments($file, $filedata["mimetype"], $filedata["filename"]); - $lexer = $pygments->should_highlight(); - - // If we detected a highlightable file redirect, - // otherwise show the URL because browsers would just show a DL dialog - if ($lexer) { - $redirect = true; - } - } - } - } - - if ($redirect && count($ids) == 1) { - redirect($this->data['urls'][0], "location", 303); - } else { - $this->load->view('header', $this->data); - $this->load->view('file/show_url', $this->data); - $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); - - $pygments = new \libraries\Pygments($this->mfile->file($filedata["data_id"]), $filedata["mimetype"], $filedata["filename"]); - if ($filedata !== false && $pygments->can_highlight()) { - $this->data["textarea_content"] = file_get_contents($this->mfile->file($filedata["data_id"])); - } - } - - 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->load->view('header', $this->data); - $this->load->view('file/upload_form', $this->data); - $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; - $cache_timeout = 60*60*24*30; # 1 month - - $filedata = $this->mfile->get_filedata($id); - if (!$filedata) { - throw new \exceptions\ApiException("file/thumbnail/filedata-unavailable", "Failed to get file data"); - } - - $cache_key = $filedata['data_id'].'_thumb_'.$thumb_size; - - $thumb = cache_function($cache_key, $cache_timeout, function() use ($filedata, $thumb_size){ - $CI =& get_instance(); - $img = new libraries\Image($this->mfile->file($filedata["data_id"])); - $img->makeThumb($thumb_size, $thumb_size); - $thumb = $img->get(IMAGETYPE_JPEG); - 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(); - - // TODO: move to \service\files and possibly use \s\f::history() - $query = $this->db - ->select('files.id, filename, mimetype, files.date, hash, file_storage.id storage_id, filesize, user') - ->from('files') - ->join('file_storage', 'file_storage.id = files.file_storage_id') - ->where(' - (files.user = '.$this->db->escape($user).') - AND ( - mimetype LIKE \'image%\' - OR mimetype IN (\'application/pdf\') - )', null, false) - ->order_by('date', 'desc') - ->get()->result_array(); - - foreach($query as $key => $item) { - assert($item["user"] === $user); - $item["data_id"] = $item['hash']."-".$item['storage_id']; - $query[$key]["data_id"] = $item["data_id"]; - if (!$this->mfile->valid_filedata($item)) { - unset($query[$key]); - continue; - } - $query[$key]["tooltip"] = \service\files::tooltip($item); - $query[$key]["orientation"] = libraries\Image::get_exif_orientation($this->mfile->file($item["data_id"])); - } - - $this->data["items"] = $query; - - $this->load->view('header', $this->data); - $this->load->view('file/upload_history_thumbnails', $this->data); - $this->load->view('footer', $this->data); - } - - public function handle_history_submit() - { - $this->muser->require_access("apikey"); - - $process = $this->input->post("process"); - - $dispatcher = [ - "delete" => function() { - return $this->do_delete(); - }, - "multipaste" => function() { - return $this->_append_multipaste_queue(); - }, - ]; - - if (isset($dispatcher[$process])) { - $dispatcher[$process](); - } else { - throw new \exceptions\UserInputException("file/handle_history_submit/invalid-process-value", "Value in process field not found in dispatch table"); - } - } - - private function _append_multipaste_queue() - { - $ids = $this->input->post_array("ids"); - if ($ids === false) { - $ids = []; - } - - $m = new \service\multipaste_queue(); - $m->append($ids); - redirect("file/multipaste/queue"); - } - - function upload_history() - { - $this->muser->require_access("apikey"); - - $history = service\files::history($this->muser->get_userid()); - - // 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); - } - - foreach ($history["multipaste_items"] as $key => $item) { - $size = 0; - $filenames = array(); - $files = array(); - $max_filenames = 10; - - foreach ($item["items"] as $i) { - $size += $history["items"][$i["id"]]["filesize"]; - $files[] = array( - "filename" => $history["items"][$i["id"]]['filename'], - "sort_order" => $i["sort_order"], - ); - } - - uasort($files, function ($a, $b) { - return $a['sort_order'] - $b['sort_order']; - }); - - $filenames = array_map(function ($a) {return $a['filename'];}, $files); - - if (count($filenames) > $max_filenames) { - $filenames = array_slice($filenames, 0, $max_filenames); - $filenames[] = "..."; - } - - $history["items"][] = array( - "id" => $item["url_id"], - "filename" => count($item["items"])." file(s)", - "mimetype" => "", - "date" => $item["date"], - "hash" => "", - "filesize" => $size, - "preview_text" => implode("\n", $filenames), - ); - } - - uasort($history["items"], function($a, $b) { - return $b["date"] - $a["date"]; - }); - - foreach($history["items"] as $key => $item) { - $history["items"][$key]["filesize"] = format_bytes($item["filesize"]); - - if (isset($item['preview_text'])) { - $history["items"][$key]["preview_text"] = htmlentities($item['preview_text']); - } - } - - $this->data["items"] = $history["items"]; - $this->data["lengths"] = $lengths; - $this->data["fields"] = $fields; - $this->data["total_size"] = format_bytes($history["total_size"]); - - $this->load->view('header', $this->data); - $this->load->view('file/upload_history', $this->data); - $this->load->view('footer', $this->data); - } - - function do_delete() - { - $this->muser->require_access("apikey"); - - $ids = $this->input->post_array("ids"); - - $ret = \service\files::delete($ids); - - $this->data["errors"] = $ret["errors"]; - $this->data["deleted_count"] = $ret["deleted_count"]; - $this->data["total_count"] = $ret["total_count"]; - - $this->load->view('header', $this->data); - $this->load->view('file/deleted', $this->data); - $this->load->view('footer', $this->data); - } - - function do_multipaste() - { - $this->muser->require_access("basic"); - - $ids = $this->input->post_array("ids"); - $userid = $this->muser->get_userid(); - $limits = $this->muser->get_upload_id_limits(); - - $ret = \service\files::create_multipaste($ids, $userid, $limits); - - return $this->_show_url(array($ret["url_id"]), false); - } - - /** - * Handle submissions from the web form (files and textareas). - */ - public function do_websubmit() - { - $files = getNormalizedFILES(); - $contents = $this->input->post_array("content"); - $filenames = $this->input->post_array("filename"); - - if (!is_array($filenames) || !is_array($contents)) { - throw new \exceptions\UserInputException('file/websubmit/invalid-form', 'The submitted POST form is invalid'); - } - - $ids = array(); - $ids = array_merge($ids, $this->_handle_textarea($contents, $filenames)); - $ids = array_merge($ids, $this->_handle_files($files)); - - - if (empty($ids)) { - throw new \exceptions\UserInputException("file/websubmit/no-input", "You didn't enter any text or upload any files"); - } - - if (count($ids) > 1) { - $userid = $this->muser->get_userid(); - $limits = $this->muser->get_upload_id_limits(); - $multipaste_id = \service\files::create_multipaste($ids, $userid, $limits)["url_id"]; - - $ids[] = $multipaste_id; - $this->_prepare_claim($ids, false); - - redirect(site_url($multipaste_id)."/"); - } - - $this->_show_url($ids, false); - } - - private function _handle_files($files) - { - $ids = array(); - - if (!empty($files)) { - $limits = $this->muser->get_upload_id_limits(); - $userid = $this->muser->get_userid(); - service\files::verify_uploaded_files($files); - - foreach ($files as $key => $file) { - $id = $this->mfile->new_id($limits[0], $limits[1]); - service\files::add_uploaded_file($userid, $id, $file["tmp_name"], $file["name"]); - $ids[] = $id; - } - } - - return $ids; - } - - private function _handle_textarea($contents, $filenames) - { - $ids = array(); - - foreach ($contents as $key => $content) { - $filesize = strlen($content); - - if ($filesize == 0) { - unset($contents[$key]); - } - - if ($filesize > $this->config->item('upload_max_size')) { - throw new \exceptions\RequestTooBigException("file/websubmit/request-too-big", "Error while uploading: Paste too big"); - } - } - - $limits = $this->muser->get_upload_id_limits(); - $userid = $this->muser->get_userid(); - - foreach ($contents as $key => $content) { - $filename = "stdin"; - if (isset($filenames[$key]) && $filenames[$key] != "") { - $filename = $filenames[$key]; - } - - $id = $this->mfile->new_id($limits[0], $limits[1]); - service\files::add_file_data($userid, $id, $content, $filename); - $ids[] = $id; - } - - return $ids; - } - - function claim_id() - { - $this->muser->require_access(); - - $last_upload = $this->session->userdata("last_upload"); - - if ($last_upload === false) { - throw new \exceptions\PublicApiException("file/claim_id/last_upload-failed", "Failed to get last upload data, unable to claim uploads"); - } - - $ids = $last_upload["ids"]; - $errors = array(); - - assert(is_array($ids)); - - foreach ($ids as $key => $id) { - $affected = 0; - $affected += $this->mfile->adopt($id); - $affected += $this->mmultipaste->adopt($id); - - if ($affected == 0) { - $errors[] = $id; - } - } - - if (!empty($errors)) { - throw new \exceptions\PublicApiException("file/claim_id/failed", "Failed to claim ".implode(", ", $errors).""); - } - - $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"] = '

Contact info not available.

'; - } - - $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 ` - */ - - // Removes old files - function cron() - { - $this->_require_cli_request(); - - $tarball_dir = $this->config->item("upload_path")."/special/multipaste-tarballs"; - if (is_dir($tarball_dir)) { - $tarball_cache_time = $this->config->item("tarball_cache_time"); - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($tarball_dir), RecursiveIteratorIterator::SELF_FIRST); - - foreach ($it as $file) { - if ($file->isFile()) { - if ($file->getMTime() < time() - $tarball_cache_time) { - $lock = fopen($file, "r+"); - flock($lock, LOCK_EX); - unlink($file); - flock($lock, LOCK_UN); - } - } - } - } - - $oldest_time = (time() - $this->config->item('upload_max_age')); - $oldest_session_time = (time() - $this->config->item("sess_expiration")); - $config = array( - "upload_max_age" => $this->config->item("upload_max_age"), - "small_upload_size" => $this->config->item("small_upload_size"), - "sess_expiration" => $this->config->item("sess_expiration"), - ); - - $query = $this->db->select('file_storage_id storage_id, files.id, user, files.date, hash') - ->from('files') - ->join('file_storage', "file_storage.id = files.file_storage_id") - ->where("user", 0) - ->where("files.date <", $oldest_session_time) - ->get()->result_array(); - - foreach($query as $row) { - $row['data_id'] = $row['hash'].'-'.$row['storage_id']; - \service\files::valid_id($row, $config, $this->mfile, time()); - } - - // 0 age disables age checks - if ($this->config->item('upload_max_age') == 0) return; - - $query = $this->db->select('hash, files.id, user, files.date, file_storage.id storage_id') - ->from('files') - ->join('file_storage', "file_storage.id = files.file_storage_id") - ->where('files.date <', $oldest_time) - ->get()->result_array(); - - foreach($query as $row) { - $row['data_id'] = $row['hash'].'-'.$row['storage_id']; - \service\files::valid_id($row, $config, $this->mfile, time()); - } - } - - /* remove files without database entries */ - function clean_stale_files() - { - $this->_require_cli_request(); - - $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 == "." || $dir == "special") { - continue; - } - - $dh = opendir($upload_path."/".$dir); - - $empty = true; - - while (($file = readdir($dh)) !== false) { - if ($file == ".." || $file == ".") { - continue; - } - - try { - list($hash, $storage_id) = explode("-", $file); - } catch (\ErrorException $e) { - unlink($upload_path."/".$dir."/".$file); - continue; - } - - $query = $this->db->select('hash, id') - ->from('file_storage') - ->where('hash', $hash) - ->where('id', $storage_id) - ->limit(1) - ->get()->row_array(); - - if (empty($query)) { - $this->mfile->delete_data_id($file); - } else { - $empty = false; - } - } - - closedir($dh); - - if ($empty && file_exists($upload_path."/".$dir)) { - rmdir($upload_path."/".$dir); - } - } - closedir($outer_dh); - - // TODO: clean up special/multipaste-tarballs? cron() already expires - // after a rather short time, do we really need this here then? - } - - function nuke_id() - { - $this->_require_cli_request(); - - $id = $this->uri->segment(3); - - $file_data = $this->mfile->get_filedata($id); - - if (empty($file_data)) { - echo "unknown id \"$id\"\n"; - return; - } - - $data_id = $file_data["data_id"]; - $this->mfile->delete_data_id($data_id); - echo "removed data_id \"$data_id\"\n"; - } - - function update_file_metadata() - { - $this->_require_cli_request(); - - $chunk = 500; - - $total = $this->db->count_all("file_storage"); - - for ($limit = 0; $limit < $total; $limit += $chunk) { - $query = $this->db->select('hash, id') - ->from('file_storage') - ->limit($chunk, $limit) - ->get()->result_array(); - - foreach ($query as $key => $item) { - $data_id = $item["hash"].'-'.$item['id']; - $filepath = $this->mfile->file($data_id); - $mimetype = mimetype($filepath); - $filesize = filesize($filepath); - - $this->db->where('id', $item['id']) - ->set(array( - 'mimetype' => $mimetype, - 'filesize' => $filesize, - )) - ->update('file_storage'); - } - } - } -} - -# vim: set noet: diff --git a/application/controllers/file/multipaste.php b/application/controllers/file/multipaste.php deleted file mode 100644 index 50367697c..000000000 --- a/application/controllers/file/multipaste.php +++ /dev/null @@ -1,113 +0,0 @@ - - * - * Licensed under AGPLv3 - * (see COPYING for full license text) - * - */ - -class Multipaste extends MY_Controller { - - function __construct() { - parent::__construct(); - - $this->load->model('mfile'); - $this->load->model('mmultipaste'); - } - - public function append_multipaste_queue() { - $this->muser->require_access("basic"); - - $ids = $this->input->post_array("ids"); - if ($ids === false) { - $ids = []; - } - - $m = new \service\multipaste_queue(); - $m->append($ids); - - redirect("file/multipaste/queue"); - } - - public function review_multipaste() { - $this->muser->require_access("basic"); - - $this->load->view('header', $this->data); - $this->load->view('file/review_multipaste', $this->data); - $this->load->view('footer', $this->data); - } - - public function queue() { - $this->muser->require_access("basic"); - - $m = new \service\multipaste_queue(); - $ids = $m->get(); - - $this->data['ids'] = $ids; - $this->data['items'] = array_map(function($id) {return $this->_get_multipaste_item($id);}, $ids); - - $this->load->view('header', $this->data); - $this->load->view('file/multipaste/queue', $this->data); - $this->load->view('footer', $this->data); - } - - public function form_submit() { - $this->muser->require_access("basic"); - - $ids = $this->input->post_array('ids'); - $process = $this->input->post('process'); - - if ($ids === false) { - $ids = []; - } - - $m = new \service\multipaste_queue(); - $m->set($ids); - - $dispatcher = [ - 'save' => function() use ($ids, $m) { - redirect("file/multipaste/queue"); - }, - 'create' => function() use ($ids, $m) { - $userid = $this->muser->get_userid(); - $limits = $this->muser->get_upload_id_limits(); - $ret = \service\files::create_multipaste($ids, $userid, $limits); - $m->set([]); - redirect($ret['url_id'].'/'); - }, - ]; - - if (isset($dispatcher[$process])) { - $dispatcher[$process](); - } else { - throw new \exceptions\UserInputException("file/multipaste/form_submit/invalid-process-value", "Value in process field not found in dispatch table"); - } - } - - public function ajax_submit() { - $this->muser->require_access("basic"); - $ids = $this->input->post_array('ids'); - - if ($ids === false) { - $ids = []; - } - - $m = new \service\multipaste_queue(); - $m->set($ids); - } - - private function _get_multipaste_item($id) { - $filedata = $this->mfile->get_filedata($id); - $item = []; - $item['id'] = $filedata['id']; - $item['tooltip'] = \service\files::tooltip($filedata); - $item['title'] = $filedata['filename']; - if (\libraries\Image::type_supported($filedata["mimetype"])) { - $item['thumbnail'] = site_url("file/thumbnail/".$filedata['id']); - } - - return $item; - } - -} diff --git a/application/controllers/tools.php b/application/controllers/tools.php deleted file mode 100644 index 9e0ddfb5f..000000000 --- a/application/controllers/tools.php +++ /dev/null @@ -1,130 +0,0 @@ - - * - * Licensed under AGPLv3 - * (see COPYING for full license text) - * - */ - -class Tools extends MY_Controller { - - function __construct() - { - parent::__construct(); - - $this->load->model('mfile'); - $this->_require_cli_request(); - } - - function index() - { - echo "php index.php [arguments]\n"; - echo "\n"; - echo "Functions:\n"; - echo " file cron Cronjob\n"; - echo " file nuke_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'); - $upgraded = $this->migration->current(); - if ( ! $upgraded) { - throw new \exceptions\ApiException("tools/update_database/migration-error", $this->migration->error_string()); - } - - if ($upgraded === true) { - echo "Already at latest database version. No upgrade performed\n"; - } - - if (is_int($upgraded)) { - echo "Database upgraded sucessfully to version: $upgraded\n"; - } - } - - function drop_all_tables() - { - $tables = $this->db->list_tables(); - $prefix = $this->db->dbprefix; - $tables_to_drop = array(); - - foreach ($tables as $table) { - if ($prefix === "" || strpos($table, $prefix) === 0) { - $tables_to_drop[] = $this->db->protect_identifiers($table); - } - } - - if (empty($tables_to_drop)) { - return; - } - - - if ($this->db->dbdriver !== 'postgre') { - $this->db->query('SET FOREIGN_KEY_CHECKS = 0'); - } - $this->db->query('DROP TABLE '.implode(", ", $tables_to_drop)); - if ($this->db->dbdriver !== 'postgre') { - $this->db->query('SET FOREIGN_KEY_CHECKS = 1'); - } - } - - function test() - { - global $argv; - $testcase = $argv[3]; - - $testcase = str_replace("application/", "", $testcase); - $testcase = str_replace("/", "\\", $testcase); - $testcase = str_replace(".php", "", $testcase); - - $test = new $testcase(); - - $exitcode = 0; - - $refl = new ReflectionClass($test); - foreach ($refl->getMethods() as $method) { - if (strpos($method->name, "test_") === 0) { - try { - $test->setTestNamePrefix($method->name." - "); - $test->init(); - $test->setTestID("{$testcase}->{$method->name}"); - $test->{$method->name}(); - $test->cleanup(); - } catch (\Exception $e) { - echo "not ok - uncaught exception in {$testcase}->{$method->name}\n"; - \libraries\ExceptionHandler::exception_handler($e); - $exitcode = 255; - } - } - } - - if ($exitcode == 0) { - $test->done_testing(); - } else { - exit($exitcode); - } - } - - function generate_coverage_report() - { - include APPPATH."../vendor/autoload.php"; - $coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage(); - foreach (glob(FCPATH."/test-coverage-data/*") as $file) { - $coverage->merge(unserialize(file_get_contents($file))); - } - - $writer = new \SebastianBergmann\CodeCoverage\Report\Clover(); - $writer->process($coverage, 'code-coverage-report.xml'); - $writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade(); - $writer->process($coverage, 'code-coverage-report'); - print "Report saved to ./code-coverage-report/index.html\n"; - } -} diff --git a/application/controllers/user.php b/application/controllers/user.php deleted file mode 100644 index d87b544c7..000000000 --- a/application/controllers/user.php +++ /dev/null @@ -1,705 +0,0 @@ - - * - * Licensed under AGPLv3 - * (see COPYING for full license text) - * - */ - -class User extends MY_Controller { - - function __construct() - { - parent::__construct(); - } - - 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('user/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(403); - } - } - - function login() - { - $redirect_uri = $this->input->get("redirect_uri"); - $this->muser->require_session(); - - if (!preg_match('/^[0-9a-zA-Z\/_-]*$/', $redirect_uri)) { - $redirect_uri = '/'; - } - - if ($this->muser->logged_in()) { - redirect($redirect_uri); - } - - $this->data['redirect_uri'] = $redirect_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('user/login', $this->data); - $this->load->view('footer', $this->data); - } else { - redirect($redirect_uri); - } - } else { - $this->load->view('header', $this->data); - $this->load->view('user/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"; - } - - $key = \service\user::create_apikey($userid, $comment, $access_level); - - redirect("user/apikeys"); - } - - function delete_apikey() - { - $this->muser->require_access(); - - $userid = $this->muser->get_userid(); - $key = $this->input->post("key"); - - $this->db->where('user', $userid) - ->where('key', $key) - ->delete('apikeys'); - - redirect("user/apikeys"); - } - - function apikeys() - { - $this->muser->require_access(); - - $userid = $this->muser->get_userid(); - $apikeys = \service\user::apikeys($userid); - $this->data["query"] = $apikeys["apikeys"]; - - $this->load->view('header', $this->data); - $this->load->view('user/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(); - - $invitations = $this->db->select('user') - ->from('actions') - ->where('user', $userid) - ->where('action', 'invitation') - ->count_all_results(); - - if ($invitations + 1 > $this->config->item('max_invitation_keys')) { - throw new \exceptions\PublicApiException("user/invitation-limit", "You can't create more invitation keys at this time."); - } - - $key = random_alphanum(12, 16); - - $this->db->set(array( - 'key' => $key, - 'user' => $userid, - 'date' => time(), - 'action' => 'invitation' - )) - ->insert('actions'); - - 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->select('key, date') - ->from('actions') - ->where('user', $userid) - ->where('action', 'invitation') - ->get()->result_array(); - - $this->data["query"] = $query; - - $this->load->view('header', $this->data); - $this->load->view('user/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"]; - - $this->data['redirect_uri'] = "/"; - - 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 (!$this->muser->valid_username($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."; - } - } - - if (!$this->muser->valid_email($email)) { - $error[]= "Invalid email."; - } - - if (!$password || $password !== $password_confirm) { - $error[]= "No password or passwords don't match."; - } - - if (empty($error)) { - $this->muser->add_user($username, $password, $email, $referrer); - - $this->db->where('key', $key) - ->delete('actions'); - - $this->load->view('header', $this->data); - $this->load->view('user/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('user/register', $this->data); - $this->load->view('footer', $this->data); - } - - public function delete_account() - { - $this->muser->require_access(); - $this->duser->require_implemented("can_delete_account"); - - if ($_SERVER["REQUEST_METHOD"] == "GET") { - return $this->_delete_account_form(); - } elseif ($_SERVER["REQUEST_METHOD"] == "POST") { - return $this->_delete_account_process(); - } - } - - public function _delete_account_form() - { - $this->data['username'] = $this->muser->get_username(); - - $this->load->view('header', $this->data); - $this->load->view('user/delete_account_form', $this->data); - $this->load->view('footer', $this->data); - } - - public function _delete_account_process() - { - $username = $this->muser->get_username(); - $password = $this->input->post("password"); - - $useremail = $this->muser->get_email($this->muser->get_userid()); - - if ($this->muser->delete_user($username, $password)) { - $this->muser->logout(); - - $this->load->library("email"); - $this->email->from($this->config->item("email_from")); - $this->email->to($useremail); - $this->email->subject("FileBin account deleted"); - $this->email->message("" - ."Your FileBin account '${username}' at ".site_url()."\n" - ."has been permemently deleted.\n" - ."\n" - ."The request has been sent from the IP address '${_SERVER["REMOTE_ADDR"]}'\n" - ."and was confirmed with your password.\n" - ."\n" - ."Thank you for using FileBin!\n" - ); - $this->email->send(); - unset($this->data['username']); - unset($this->data['user_logged_in']); - - $this->load->view('header', $this->data); - $this->load->view('user/delete_account_success', $this->data); - $this->load->view('footer', $this->data); - return; - } else { - $this->data['alerts'][] = array( - "type" => "danger", - "message" => "Your password was incorrect", - ); - return $this->_delete_account_form(); - } - } - - // 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('user/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)) { - throw new \exceptions\PublicApiException("user/reset_password/invalid-username", "Invalid username"); - } - - $userinfo = $this->db->select('id, email, username') - ->from('users') - ->where('username', $username) - ->get()->row_array(); - - $this->load->library("email"); - - $this->db->set(array( - 'key' => $key, - 'user' => $userinfo['id'], - 'date' => time(), - 'action' => 'passwordreset' - )) - ->insert('actions'); - - $this->email->from($this->config->item("email_from")); - $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('user/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->muser->set_password($userid, $password); - - $this->db->where('key', $key) - ->delete('actions'); - - $this->load->view('header', $this->data); - $this->load->view('user/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('user/reset_password_form', $this->data); - $this->load->view('footer', $this->data); - } - - public function change_email() - { - $this->duser->require_implemented("can_change_email"); - $key = $this->uri->segment(3); - $action = $this->uri->segment(4); - - $alerts = array(); - - $query = $this->muser->get_action("change_email", $key); - - $userid = $query["user"]; - $data = json_decode($query['data'], true); - - switch ($action) { - case 'confirm': - $this->db->where('id', $userid) - ->update('users', array( - "email" => $data['new_email'], - )); - $alerts[] = array( - "type" => "success", - "message" => "Your email address has been updated", - ); - break; - case 'reject': - $this->db->where('id', $userid) - ->update('users', array( - "email" => $data['old_email'], - )); - foreach ($data['keys'] as $k) { - $this->db->where('key', $k) - ->delete('actions'); - } - $alerts[] = array( - "type" => "success", - "message" => "Your email change request has been canceled and/or your old email address has been restored", - ); - break; - default: - assert(false); - break; - } - - $this->data["alerts"] = $alerts; - - return $this->profile(); - } - - 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('user/profile', $this->data); - $this->load->view('footer', $this->data); - } - - private function _save_profile() - { - $this->muser->require_access(); - - $old = $this->muser->get_profile_data(); - - /* - * 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(); - $alerts = array(); - - $value_processor["upload_id_limits"] = function($value) { - $values = explode("-", $value); - - if (!is_array($values) || count($values) != 2) { - throw new \exceptions\PublicApiException("user/profile/invalid-upload-id-limit", "Invalid upload id limit value"); - } - - $lower = intval($values[0]); - $upper = intval($values[1]); - - if ($lower > $upper) { - throw new \exceptions\PublicApiException("user/profile/lower-bigger-than-upper", "lower limit > upper limit"); - } - - if ($lower < 3 || $upper > 64) { - throw new \exceptions\PublicApiException("user/profile/limit-out-of-bounds", "upper or lower limit out of bounds (3-64)"); - } - - return $lower."-".$upper; - }; - - $value_processor["email"] = function($value) use ($old, &$alerts) { - if (!$this->duser->is_implemented("can_change_email")) { - return null; - } - - if ($value === $old["email"]) { - return null; - } - - if (!$this->muser->valid_email($value)) { - throw new \exceptions\PublicApiException("user/profile/invalid-email", "Invalid email"); - } - - $this->load->library("email"); - $keys = array( - "old" => random_alphanum(12,16), - "new" => random_alphanum(12,16), - ); - $emails = array( - array( - "key" => $keys['old'], - "email" => $old['email'], - "user" => $this->muser->get_userid(), - ), - array( - "key" => $keys['new'], - "email" => $value, - "user" => $this->muser->get_userid(), - ), - ); - - foreach ($emails as $email) { - $key = $email['key']; - - $this->db->set(array( - 'key' => $key, - 'user' => $this->muser->get_userid(), - 'date' => time(), - 'action' => 'change_email', - 'data' => json_encode(array( - 'old_email' => $old['email'], - 'new_email' => $value, - 'keys' => $keys, - )), - )) - ->insert('actions'); - - $this->email->from($this->config->item("email_from")); - $this->email->to($email['email']); - $this->email->subject("FileBin email change confirmation"); - $this->email->message("" - ."A request has been sent to change the email address of account '${old["username"]}'\n" - ."from ".$old['email']." to $value.\n" - ."\n" - ."Please follow this link to CONFIRM the change:\n" - .site_url("user/change_email/$key/confirm")."\n\n" - ."Please follow this link to REJECT the change:\n" - .site_url("user/change_email/$key/reject")."\n\n" - ); - $this->email->send(); - $this->email->clear(); - } - - $alerts[] = array( - "type" => "info", - "message" => "Reset and confirmation emails have been sent to your new and old address. Until your new address is confirmed the old one will be displayed and used.", - ); - - return null; - }; - - - $data = array(); - foreach (array_keys($value_processor) as $field) { - $value = $this->input->post($field); - - if ($value !== false) { - $new_value = $value_processor[$field]($value); - if ($new_value !== null) { - $data[$field] = $new_value; - } - } - } - - if (!empty($data)) { - $this->muser->update_profile($data); - } - - $alerts[] = array( - "type" => "success", - "message" => "Changes saved", - ); - $this->data["alerts"] = $alerts; - - 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('user/hash_password', $this->data); - $this->load->view('footer', $this->data); - } - - function cron() - { - $this->_require_cli_request(); - - if ($this->config->item('actions_max_age') == 0) return; - - $oldest_time = (time() - $this->config->item('actions_max_age')); - - $this->db->where('date <', $oldest_time) - ->delete('actions'); - } - - private function _get_line_cli($message, $verification_func = NULL) - { - echo "$message: "; - - while ($line = fgets(STDIN)) { - $line = trim($line); - if ($verification_func === NULL) { - return $line; - } - - if ($verification_func($line)) { - return $line; - } else { - echo "$message: "; - } - } - } - - function add_user() - { - $this->_require_cli_request(); - $this->duser->require_implemented("can_register_new_users"); - - $error = array(); - - $username = $this->_get_line_cli("Username", function($username) { - if (!$this->muser->valid_username($username)) { - echo "Invalid username (only up to 32 chars of a-z0-9 are allowed).\n"; - return false; - } else { - if (get_instance()->muser->username_exists($username)) { - echo "Username already exists.\n"; - return false; - } - } - return true; - }); - - $email = $this->_get_line_cli("Email", function($email) { - if (!$this->muser->valid_email($email)) { - echo "Invalid email.\n"; - return false; - } - return true; - }); - - $password = $this->_get_line_cli("Password", function($password) { - if (!$password || $password === "") { - echo "No password supplied.\n"; - return false; - } - return true; - }); - - $this->muser->add_user($username, $password, $email, NULL); - - echo "User added\n"; - } -} -- cgit v1.2.3-24-g4f1b