summaryrefslogtreecommitdiffstats
path: root/application/service
diff options
context:
space:
mode:
Diffstat (limited to 'application/service')
-rw-r--r--application/service/files.php441
-rw-r--r--application/service/multipaste_queue.php96
-rw-r--r--application/service/renderer.php181
-rw-r--r--application/service/storage.php160
-rw-r--r--application/service/user.php80
5 files changed, 958 insertions, 0 deletions
diff --git a/application/service/files.php b/application/service/files.php
new file mode 100644
index 000000000..b45ac8e6f
--- /dev/null
+++ b/application/service/files.php
@@ -0,0 +1,441 @@
+<?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;
+
+ $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;
+ }
+ }
+
+ 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();
+ }
+ $data_id = $hash."-".$storage_id;
+
+ // TODO: all this doesn't have to run if the file exists. updating the mtime would be enough
+ // that would also be better for COW filesystems
+ $dir = $CI->mfile->folder($data_id);
+ file_exists($dir) || mkdir ($dir);
+ $new_path = $CI->mfile->file($data_id);
+
+ $dest = new \service\storage($new_path);
+ $tmpfile = $dest->begin();
+ rename($new_file, $tmpfile);
+ $dest->commit();
+
+ $CI->mfile->add_file($userid, $id, $filename, $storage_id);
+ }
+
+ 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;
+ }
+
+}
diff --git a/application/service/multipaste_queue.php b/application/service/multipaste_queue.php
new file mode 100644
index 000000000..453ea3429
--- /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 === false) {
+ $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..6f57e7458
--- /dev/null
+++ b/application/service/renderer.php
@@ -0,0 +1,181 @@
+<?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) {
+ $decoded_json = json_decode($content);
+ if ($decoded_json !== null && $decoded_json !== false) {
+ $pretty_json = json_encode($decoded_json, JSON_PRETTY_PRINT);
+ if ($pretty_json !== false) {
+ $content = $pretty_json;
+ $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 $content;
+ }
+
+
+}
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..1d922a102
--- /dev/null
+++ b/application/service/user.php
@@ -0,0 +1,80 @@
+<?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,
+ );
+ }
+}