diff options
Diffstat (limited to 'application/service')
-rw-r--r-- | application/service/files.php | 553 | ||||
-rw-r--r-- | application/service/multipaste_queue.php | 96 | ||||
-rw-r--r-- | application/service/renderer.php | 187 | ||||
-rw-r--r-- | application/service/storage.php | 160 | ||||
-rw-r--r-- | application/service/user.php | 128 |
5 files changed, 1124 insertions, 0 deletions
diff --git a/application/service/files.php b/application/service/files.php new file mode 100644 index 000000000..a98e0873f --- /dev/null +++ b/application/service/files.php @@ -0,0 +1,553 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; + +class files { + + static public function history($user) + { + $CI =& get_instance(); + $items = array(); + + $fields = array("files.id", "filename", "mimetype", "files.date", "hash", "filesize"); + $query = $CI->db->select(implode(',', $fields)) + ->from('files') + ->join('file_storage', 'file_storage.id = files.file_storage_id') + ->where('user', $user) + ->get()->result_array(); + foreach ($query as $key => $item) { + if (\libraries\Image::type_supported($item["mimetype"])) { + $item['thumbnail'] = site_url("file/thumbnail/".$item['id']); + } + $items[$item["id"]] = $item; + } + + $total_size = $CI->db->query(" + SELECT coalesce(sum(sub.filesize), 0) sum + FROM ( + SELECT DISTINCT fs.id, filesize + FROM ".$CI->db->dbprefix."file_storage fs + JOIN ".$CI->db->dbprefix."files f ON fs.id = f.file_storage_id + WHERE f.user = ? + + ) sub + ", array($user))->row_array(); + + $ret["items"] = $items; + $ret["multipaste_items"] = self::get_multipaste_history($user); + $ret["total_size"] = $total_size["sum"]; + + return $ret; + } + + static private function get_multipaste_history($user) + { + $CI =& get_instance(); + $multipaste_items_grouped = array(); + $multipaste_items = array(); + + $query = $CI->db + ->select('m.url_id, m.date') + ->from("multipaste m") + ->where("user_id", $user) + ->get()->result_array(); + $multipaste_info = array(); + foreach ($query as $item) { + $multipaste_info[$item["url_id"]] = $item; + } + + $multipaste_items_query = $CI->db + ->select("m.url_id, f.id") + ->from("multipaste m") + ->join("multipaste_file_map mfm", "m.multipaste_id = mfm.multipaste_id") + ->join("files f", "f.id = mfm.file_url_id") + ->where("m.user_id", $user) + ->order_by("mfm.sort_order") + ->get()->result_array(); + + $counter = 0; + + foreach ($multipaste_items_query as $item) { + $multipaste_info[$item["url_id"]]["items"][$item["id"]] = array( + "id" => $item["id"], + // normalize sort_order value so we don't leak any information + "sort_order" => $counter++, + ); + } + + // No idea why, but this can/could happen so be more forgiving and clean up + foreach ($multipaste_info as $key => $m) { + if (!isset($m["items"])) { + $CI->mmultipaste->delete_id($key); + unset($multipaste_info[$key]); + } + } + + return $multipaste_info; + } + + static public function add_file_data($userid, $id, $content, $filename) + { + $f = new \libraries\Tempfile(); + $file = $f->get_file(); + file_put_contents($file, $content); + self::add_file_callback($userid, $id, $file, $filename); + } + + /** + * Ellipsize text to be at max $max_lines lines long. If the last line is + * not complete (strlen($text) < $filesize), drop it so that every line of + * the returned text is complete. If there is only one line, return that + * line as is and add the ellipses at the end. + * + * @param text Text to add ellipses to + * @param max_lines Number of lines the returned text should contain + * @param filesize size of the original file where the text comes from + * @return ellipsized text + */ + static public function ellipsize($text, $max_lines, $filesize) + { + $lines = explode("\n", $text); + $orig_len = strlen($text); + $orig_linecount = count($lines); + + if ($orig_linecount > 1) { + if ($orig_len < $filesize) { + // ensure we have a full line at the end + $lines = array_slice($lines, 0, -1); + } + + if (count($lines) > $max_lines) { + $lines = array_slice($lines, 0, $max_lines); + } + + if (count($lines) != $orig_linecount) { + // only add elipses when we drop at least one line + $lines[] = "..."; + } + } elseif ($orig_len < $filesize) { + $lines[count($lines) - 1] .= " ..."; + } + + return implode("\n", $lines); + } + + static public function add_uploaded_file($userid, $id, $file, $filename) + { + self::add_file_callback($userid, $id, $file, $filename); + } + + static private function add_file_callback($userid, $id, $new_file, $filename) + { + $CI =& get_instance(); + $hash = md5_file($new_file); + $storage_id = null; + + $CI->db->trans_start(); + $query = $CI->db->select('id, hash') + ->from('file_storage') + ->where('hash', $hash) + ->get()->result_array(); + + foreach($query as $row) { + $data_id = implode("-", array($row['hash'], $row['id'])); + $old_file = $CI->mfile->file($data_id); + + if (files_are_equal($old_file, $new_file)) { + $storage_id = $row["id"]; + break; + } + } + + $new_storage_id_created = false; + if ($storage_id === null) { + $filesize = filesize($new_file); + $mimetype = mimetype($new_file); + + $CI->db->insert("file_storage", array( + "filesize" => $filesize, + "mimetype" => $mimetype, + "hash" => $hash, + "date" => time(), + )); + $storage_id = $CI->db->insert_id(); + $new_storage_id_created = true; + assert(!file_exists($CI->mfile->file($hash."-".$storage_id))); + } + $data_id = $hash."-".$storage_id; + + $dir = $CI->mfile->folder($data_id); + file_exists($dir) || mkdir ($dir); + $new_path = $CI->mfile->file($data_id); + + // Update mtime for cronjob + touch($new_path); + + // touch may create a new file if the cronjob cleaned up in between the db check and here. + // In that case the file will be empty so move in the data + if ($new_storage_id_created || filesize($new_path) === 0) { + $dest = new \service\storage($new_path); + $tmpfile = $dest->begin(); + + // $new_file may reside on a different file system so this call + // could perform a copy operation internally. $dest->commit() will + // ensure that it performs an atomic overwrite (rename). + rename($new_file, $tmpfile); + $dest->commit(); + } + + $CI->mfile->add_file($userid, $id, $filename, $storage_id); + $CI->db->trans_complete(); + } + + static public function verify_uploaded_files($files) + { + $CI =& get_instance(); + $errors = array(); + + if (empty($files)) { + throw new \exceptions\UserInputException("file/no-file", "No file was uploaded or unknown error occured."); + } + + foreach ($files as $key => $file) { + $error_message = ""; + + // getNormalizedFILES() removes any file with error == 4 + if ($file['error'] !== UPLOAD_ERR_OK) { + // ERR_OK only for completeness, condition above ignores it + $error_msgs = array( + UPLOAD_ERR_OK => "There is no error, the file uploaded with success", + UPLOAD_ERR_INI_SIZE => "The uploaded file exceeds the upload_max_filesize directive in php.ini", + UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form", + UPLOAD_ERR_PARTIAL => "The uploaded file was only partially uploaded", + UPLOAD_ERR_NO_FILE => "No file was uploaded", + UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder", + UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk", + UPLOAD_ERR_EXTENSION => "A PHP extension stopped the file upload", + ); + + $error_message = "Unknown error."; + + if (isset($error_msgs[$file['error']])) { + $error_message = $error_msgs[$file['error']]; + } else { + $error_message = "Unknown error code: ".$file['error'].". Please report a bug."; + } + } + + $filesize = filesize($file['tmp_name']); + if ($filesize > $CI->config->item('upload_max_size')) { + $error_message = "File too big"; + } + + if ($error_message != "") { + $errors[$file["formfield"]] = array( + "filename" => $file["name"], + "formfield" => $file["formfield"], + "message" => $error_message, + ); + throw new \exceptions\FileUploadVerifyException("file/upload-verify", "Failed to verify uploaded file(s)", $errors); + } + } + } + + // TODO: streamline this interface to be somewhat atomic in regards to + // wrong owner/unknown ids (verify first and throw exception) + static public function delete($ids) + { + $CI =& get_instance(); + + $userid = $CI->muser->get_userid(); + $errors = array(); + $deleted = array(); + $deleted_count = 0; + $total_count = 0; + + if (!$ids || !is_array($ids)) { + throw new \exceptions\UserInputException("file/delete/no-ids", "No IDs specified"); + } + + foreach ($ids as $id) { + $total_count++; + $nextID = false; + + foreach (array($CI->mfile, $CI->mmultipaste) as $model) { + if ($model->id_exists($id)) { + if ($model->get_owner($id) !== $userid) { + $errors[$id] = array( + "id" => $id, + "reason" => "wrong owner", + ); + $nextID = true; + continue; + } + if ($model->delete_id($id)) { + $deleted[$id] = array( + "id" => $id, + ); + $deleted_count++; + $nextID = true; + } else { + $errors[$id] = array( + "id" => $id, + "reason" => "unknown error", + ); + } + } + } + + if ($nextID) { + continue; + } + + $errors[$id] = array( + "id" => $id, + "reason" => "doesn't exist", + ); + } + + return array( + "errors" => $errors, + "deleted" => $deleted, + "total_count" => $total_count, + "deleted_count" => $deleted_count, + ); + } + + static public function create_multipaste($ids, $userid, $limits) + { + $CI =& get_instance(); + + if (!$ids || !is_array($ids)) { + throw new \exceptions\UserInputException("file/create_multipaste/no-ids", "No IDs specified"); + } + + if (count(array_unique($ids)) != count($ids)) { + throw new \exceptions\UserInputException("file/create_multipaste/duplicate-id", "Duplicate IDs are not supported"); + } + + $errors = array(); + + foreach ($ids as $id) { + if (!$CI->mfile->id_exists($id)) { + $errors[$id] = array( + "id" => $id, + "reason" => "doesn't exist", + ); + continue; + } + + $filedata = $CI->mfile->get_filedata($id); + if ($filedata["user"] != $userid) { + $errors[$id] = array( + "id" => $id, + "reason" => "not owned by you", + ); + } + } + + if (!empty($errors)) { + throw new \exceptions\VerifyException("file/create_multipaste/verify-failed", "Failed to verify ID(s)", $errors); + } + + $url_id = $CI->mmultipaste->new_id($limits[0], $limits[1]); + + $multipaste_id = $CI->mmultipaste->get_multipaste_id($url_id); + assert($multipaste_id !== false); + + foreach ($ids as $id) { + $CI->db->insert("multipaste_file_map", array( + "file_url_id" => $id, + "multipaste_id" => $multipaste_id, + )); + } + + return array( + "url_id" => $url_id, + "url" => site_url($url_id)."/", + ); + } + + static public function valid_id(array $filedata, array $config, $model, $current_date) + { + assert(isset($filedata["data_id"])); + assert(isset($filedata["id"])); + assert(isset($filedata["user"])); + assert(isset($filedata["date"])); + assert(isset($config["upload_max_age"])); + assert(isset($config["sess_expiration"])); + assert(isset($config["small_upload_size"])); + + $file = $model->file($filedata['data_id']); + + if (!$model->file_exists($file)) { + $model->delete_data_id($filedata["data_id"]); + return false; + } + + if ($filedata["user"] == 0) { + if ($filedata["date"] < $current_date - $config["sess_expiration"]) { + $model->delete_id($filedata["id"]); + return false; + } + } + + // 0 age disables age checks + if ($config['upload_max_age'] == 0) return true; + + // small files don't expire + if ($model->filesize($file) <= $config["small_upload_size"]) { + return true; + } + + // files older than this should be removed + $remove_before = $current_date - $config["upload_max_age"]; + + if ($filedata["date"] < $remove_before) { + // if the file has been uploaded multiple times the mtime is the time + // of the last upload + $mtime = $model->filemtime($file); + if ($mtime < $remove_before) { + $model->delete_data_id($filedata["data_id"]); + } else { + $model->delete_id($filedata["id"]); + } + return false; + } + + return true; + } + + static public function tooltip(array $filedata) + { + $filesize = format_bytes($filedata["filesize"]); + $file = get_instance()->mfile->file($filedata["data_id"]); + $upload_date = date("r", $filedata["date"]); + + $height = 0; + $width = 0; + try { + list($width, $height) = getimagesize($file); + } catch (\ErrorException $e) { + // likely unsupported filetype + } + + $tooltip = "${filedata["id"]} - $filesize<br>"; + $tooltip .= "$upload_date<br>"; + + + if ($height > 0 && $width > 0) { + $tooltip .= "${width}x${height} - ${filedata["mimetype"]}<br>"; + } else { + $tooltip .= "${filedata["mimetype"]}<br>"; + } + + return $tooltip; + } + + static public function clean_multipaste_tarballs() + { + $CI =& get_instance(); + + $tarball_dir = $CI->config->item("upload_path")."/special/multipaste-tarballs"; + if (is_dir($tarball_dir)) { + $tarball_cache_time = $CI->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); + } + } + } + } + } + + static public function remove_files_missing_in_db() + { + $CI =& get_instance(); + + $upload_path = $CI->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 = $CI->db->select('hash, id') + ->from('file_storage') + ->where('hash', $hash) + ->where('id', $storage_id) + ->limit(1) + ->get()->row_array(); + + if (empty($query)) { + $CI->mfile->delete_data_id($file); + } else { + $empty = false; + } + } + + closedir($dh); + + if ($empty && file_exists($upload_path."/".$dir)) { + rmdir($upload_path."/".$dir); + } + } + closedir($outer_dh); + } + + static public function remove_files_missing_on_disk() + { + $CI =& get_instance(); + + $chunk = 500; + $total = $CI->db->count_all("file_storage"); + + for ($limit = 0; $limit < $total; $limit += $chunk) { + $query = $CI->db->select('hash, id') + ->from('file_storage') + ->limit($chunk, $limit) + ->get()->result_array(); + + foreach ($query as $key => $item) { + $data_id = $item["hash"].'-'.$item['id']; + $file = $CI->mfile->file($data_id); + + if (!$CI->mfile->file_exists($file)) { + $CI->mfile->delete_data_id($data_id); + } + } + } + } + +} diff --git a/application/service/multipaste_queue.php b/application/service/multipaste_queue.php new file mode 100644 index 000000000..553e9a6c2 --- /dev/null +++ b/application/service/multipaste_queue.php @@ -0,0 +1,96 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; + +class multipaste_queue { + + public function __construct($session = null, $mfile = null, $mmultipaste = null) { + $CI =& get_instance(); + + $this->session = $session; + $this->mfile = $mfile; + $this->mmultipaste = $mmultipaste; + + if ($this->session === null) { + $this->session = $CI->session; + } + + if ($this->mfile === null) { + $CI->load->model("mfile"); + $this->mfile = $CI->mfile; + } + + if ($this->mmultipaste === null) { + $CI->load->model("mmultipaste"); + $this->mmultipaste = $CI->mmultipaste; + } + } + + /** + * Append ids to the queue + * + * @param array ids + * @return void + */ + public function append(array $ids) { + $old_ids = $this->get(); + + # replace multipaste ids with their corresponding paste ids + $ids = array_map(function($id) {return array_values($this->resolve_multipaste($id));}, $ids); + $ids = array_reduce($ids, function($a, $b) {return array_merge($a, $b);}, []); + + $ids = array_unique(array_merge($old_ids, $ids)); + $this->set($ids); + } + + /** + * Return array of ids in a multipaste if the argument id is a multipaste. + * Otherwise return an array containing just the argument id. + * + * @param id + * @return array of ids + */ + private function resolve_multipaste($id) { + if (strpos($id, "m-") === 0) { + if ($this->mmultipaste->valid_id($id)) { + return array_map(function($filedata) {return $filedata['id'];}, $this->mmultipaste->get_files($id)); + } + } + return [$id]; + } + + /** + * Get the queue + * + * @return array of ids + */ + public function get() { + $ids = $this->session->userdata("multipaste_queue"); + if ($ids === NULL) { + $ids = []; + } + + assert(is_array($ids)); + return $ids; + } + + /** + * Set the queue to $ids + * + * @param array ids + * @return void + */ + public function set(array $ids) { + $ids = array_filter($ids, function($id) {return $this->mfile->valid_id($id);}); + + $this->session->set_userdata("multipaste_queue", $ids); + } + +} diff --git a/application/service/renderer.php b/application/service/renderer.php new file mode 100644 index 000000000..6baf62c78 --- /dev/null +++ b/application/service/renderer.php @@ -0,0 +1,187 @@ +<?php +/* + * Copyright 2017 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; +class renderer { + + + /** + * @param $output_cache output cache object + * @param $mfile mfile object + * @param $data data for the rendering of views + */ + public function __construct($output_cache, $mfile, $data) + { + $this->output_cache = $output_cache; + $this->mfile = $mfile; + $this->data = $data; + } + + private function colorify($file, $lexer, $anchor_id = false) + { + $output = ""; + $lines_to_remove = 0; + + $output .= '<div class="code content table">'."\n"; + $output .= '<div class="highlight"><code class="code-container">'."\n"; + + $content = file_get_contents($file); + + $linecount = count(explode("\n", $content)); + $content = $this->reformat_json($lexer, $linecount, $content); + + if ($lexer == "ascii") { + // TODO: use exec safe and catch exception + $ret = (new \libraries\ProcRunner(array('ansi2html', '-p', '-m'))) + ->input($content) + ->forbid_stderr() + ->exec(); + // Last line is empty + $lines_to_remove = 1; + } else { + // TODO: use exec safe and catch exception + $ret = (new \libraries\ProcRunner(array('pygmentize', '-F', 'codetagify', '-O', 'encoding=guess,outencoding=utf8,stripnl=False', '-l', $lexer, '-f', 'html'))) + ->input($content) + ->exec(); + // Last 2 items are "</pre></div>" and "" + $lines_to_remove = 2; + } + + + $buf = explode("\n", $ret["stdout"]); + $line_count = count($buf); + + for ($i = 1; $i <= $lines_to_remove; $i++) { + unset($buf[$line_count - $i]); + } + + foreach ($buf as $key => $line) { + $line_number = $key + 1; + if ($key == 0) { + $line = str_replace("<div class=\"highlight\"><pre>", "", $line); + } + + $anchor = "n$line_number"; + if ($anchor_id !== false) { + $anchor = "n-$anchor_id-$line_number"; + } + + if ($line === "") { + $line = "<br>"; + } + + // Be careful not to add superflous whitespace here (we are in a <code>) + $output .= "<div class=\"table-row\">" + ."<a href=\"#$anchor\" class=\"linenumber table-cell\">" + ."<span class=\"anchor\" id=\"$anchor\"> </span>" + ."</a>" + ."<span class=\"line table-cell\">".$line."</span><!--\n"; + $output .= "--></div>"; + } + + $output .= "</code></div>"; + $output .= "</div>"; + + return array( + "return_value" => $ret["return_code"], + "output" => $output + ); + } + + public function highlight_file($filedata, $lexer, $is_multipaste) + { + // highlight the file and cache the result, fall back to plain text if $lexer fails + foreach (array($lexer, "text") as $lexer) { + $highlit = cache_function($filedata['data_id'].'_'.$lexer, 100, + function() use ($filedata, $lexer, $is_multipaste) { + $file = $this->mfile->file($filedata['data_id']); + if ($lexer == "rmd") { + ob_start(); + + echo '<div class="code content table markdownrender">'."\n"; + echo '<div class="table-row">'."\n"; + echo '<div class="table-cell">'."\n"; + + require_once(APPPATH."/third_party/parsedown/Parsedown.php"); + $parsedown = new \Parsedown(); + echo $parsedown->text(file_get_contents($file)); + + echo '</div></div></div>'; + + return array( + "output" => ob_get_clean(), + "return_value" => 0, + ); + } else { + return $this->colorify($file, $lexer, $is_multipaste ? $filedata["id"] : false); + } + }); + + if ($highlit["return_value"] == 0) { + break; + } else { + $message = "Error trying to process the file. Either the lexer is unknown or something is broken."; + if ($lexer != "text") { + $message .= " Falling back to plain text."; + } + $this->output_cache->render_now( + array("error_message" => "<p>$message</p>"), + "file/fragments/alert-wide" + ); + } + } + + $data = array_merge($this->data, array( + 'title' => htmlspecialchars($filedata['filename']), + 'id' => $filedata["id"], + 'current_highlight' => htmlspecialchars($lexer), + 'timeout' => $this->mfile->get_timeout_string($filedata["id"]), + 'filedata' => $filedata, + )); + + $this->output_cache->render_now($data, 'file/html_paste_header'); + $this->output_cache->render_now($highlit["output"]); + $this->output_cache->render_now($data, 'file/html_paste_footer'); + } + + /** + * @param $lexer + * @param $linecount + * @param $content + * @return string + */ + private function reformat_json($lexer, $linecount, $content) + { + if ($lexer !== "json" || $linecount !== 1) { + return $content; + } + + $decoded_json = json_decode($content); + if ($decoded_json === null || $decoded_json === false) { + return $content; + } + + $pretty_json = json_encode($decoded_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($pretty_json === false) { + return $content; + } + + $this->output_cache->render_now( + array( + "error_type" => "alert-info", + "error_message" => "<p>The file below has been reformated for readability. It may differ from the original.</p>" + ), + "file/fragments/alert-wide" + ); + + return $pretty_json; + } + + +} diff --git a/application/service/storage.php b/application/service/storage.php new file mode 100644 index 000000000..b2c2abca7 --- /dev/null +++ b/application/service/storage.php @@ -0,0 +1,160 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; + +/** + * This class allows to change a temporary file and replace the original one atomically + */ +class storage { + private $path; + private $tempfile = NULL; + + public function __construct($path) + { + assert(!is_dir($path)); + + $this->path = $path; + } + + /** + * Create a temp file which can be written to. + * + * Call commit() once you are done writing. + * Call rollback() to remove the file and throw away any data written. + * + * Calling this multiple times will automatically rollback previous calls. + * + * @return temp file path + */ + public function begin() + { + if($this->tempfile !== NULL) { + $this->rollback(); + } + + $this->tempfile = $this->create_tempfile(); + + return $this->tempfile; + } + + /** + * Create a temporary file. You'll need to remove it yourself when no longer needed. + * + * @return path to the temporary file + */ + private function create_tempfile() + { + $dir = dirname($this->get_file()); + $prefix = basename($this->get_file()); + + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + assert(is_dir($dir)); + + return tempnam($dir, $prefix); + } + + /** + * Save the temporary file returned by begin() to the permanent path + * (supplied to the constructor) in an atomic fashion. + */ + public function commit() + { + $ret = rename($this->tempfile, $this->get_file()); + if ($ret) { + $this->tempfile = NULL; + } + + return $ret; + } + + public function exists() + { + return file_exists($this->get_file()); + } + + public function get_file() + { + return $this->path; + } + + /** + * Throw away any changes made to the temporary file returned by begin() + */ + public function rollback() + { + if ($this->tempfile !== NULL) { + unlink($this->tempfile); + $this->tempfile = NULL; + } + } + + public function __destruct() + { + $this->rollback(); + } + + /** + * GZIPs the temp file + * + * From http://stackoverflow.com/questions/6073397/how-do-you-create-a-gz-file-using-php + * Based on function by Kioob at: + * http://www.php.net/manual/en/function.gzwrite.php#34955 + * + * @param integer $level GZIP compression level (default: 6) + * @return boolean true if operation succeeds, false on error + */ + public function gzip_compress($level = 6){ + if ($this->tempfile === NULL) { + return; + } + + $source = $this->tempfile; + $file = new storage($source); + $dest = $file->begin(); + $mode = 'wb' . $level; + $error = false; + $chunk_size = 1024*512; + + if ($fp_out = gzopen($dest, $mode)) { + if ($fp_in = fopen($source,'rb')) { + while (!feof($fp_in)) { + gzwrite($fp_out, fread($fp_in, $chunk_size)); + } + fclose($fp_in); + } else { + $error = true; + } + gzclose($fp_out); + } else { + $error = true; + } + + if ($error) { + return false; + } else { + $file->commit(); + return true; + } + } + + /** + * Delete the file if it exists. + */ + public function unlink() + { + if ($this->exists()) { + unlink($this->get_file()); + } + } +} + +# vim: set noet: diff --git a/application/service/user.php b/application/service/user.php new file mode 100644 index 000000000..336941ca4 --- /dev/null +++ b/application/service/user.php @@ -0,0 +1,128 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; + +class user { + + /** + * Create a new api key. + * + * Refer to Muser->get_access_levels() for a list of valid access levels. + * + * @param userid ID of the user + * @param comment free text comment describing the api key/it's usage/allowing to identify the key + * @param access_level access level of the key + * @return the new key + */ + static public function create_apikey($userid, $comment, $access_level) + { + $CI =& get_instance(); + + $valid_levels = $CI->muser->get_access_levels(); + if (array_search($access_level, $valid_levels) === false) { + throw new \exceptions\UserInputException("user/validation/access_level/invalid", "Invalid access levels requested."); + } + + if (strlen($comment) > 255) { + throw new \exceptions\UserInputException("user/validation/comment/too-long", "Comment may only be 255 chars long."); + } + + $key = random_alphanum(32); + + $CI->db->set(array( + 'key' => $key, + 'user' => $userid, + 'comment' => $comment, + 'access_level' => $access_level + )) + ->insert('apikeys'); + + return $key; + } + + /** + * Get apikeys for a user + * @param userid ID of the user + * @return array with the key data + */ + static public function apikeys($userid) + { + $CI =& get_instance(); + $ret = array(); + + $query = $CI->db->select('key, created, comment, access_level') + ->from('apikeys') + ->where('user', $userid) + ->order_by('created', 'desc') + ->get()->result_array(); + + // Convert timestamp to unix timestamp + // TODO: migrate database to integer timestamp and get rid of this + foreach ($query as &$record) { + if (!empty($record['created'])) { + $record['created'] = strtotime($record['created']); + } + $ret[$record["key"]] = $record; + } + unset($record); + + return array( + "apikeys" => $ret, + ); + } + + /** + * Create an invitation key for a user + * @param userid id of the user + * @return key the created invitation key + */ + static public function create_invitation_key($userid) { + $CI =& get_instance(); + + $invitations = $CI->db->select('user') + ->from('actions') + ->where('user', $userid) + ->where('action', 'invitation') + ->count_all_results(); + + if ($invitations + 1 > $CI->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); + + $CI->db->set(array( + 'key' => $key, + 'user' => $userid, + 'date' => time(), + 'action' => 'invitation' + )) + ->insert('actions'); + + return $key; + } + + /** + * Remove an invitation key belonging to a user + * @param userid id of the user + * @param key key to remove + * @return number of removed keys + */ + static public function delete_invitation_key($userid, $key) { + $CI =& get_instance(); + + $CI->db + ->where('key', $key) + ->where('user', $userid) + ->delete('actions'); + + return $CI->db->affected_rows(); + } +} |