summaryrefslogtreecommitdiffstats
path: root/application/models
diff options
context:
space:
mode:
Diffstat (limited to 'application/models')
-rw-r--r--application/models/Mfile.php275
-rw-r--r--application/models/Mmultipaste.php184
-rw-r--r--application/models/Muser.php406
3 files changed, 865 insertions, 0 deletions
diff --git a/application/models/Mfile.php b/application/models/Mfile.php
new file mode 100644
index 000000000..977240c89
--- /dev/null
+++ b/application/models/Mfile.php
@@ -0,0 +1,275 @@
+<?php
+/*
+ * Copyright 2009-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net>
+ *
+ * Licensed under AGPLv3
+ * (see COPYING for full license text)
+ *
+ */
+
+class Mfile extends CI_Model {
+
+ private $upload_path;
+
+ function __construct()
+ {
+ parent::__construct();
+ $this->load->model("muser");
+
+ $this->upload_path = $this->config->item('upload_path');
+ $this->id_validation_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"),
+ );
+ }
+
+ // Returns an unused ID
+ function new_id($min = 3, $max = 6)
+ {
+ static $id_blacklist = NULL;
+
+ if ($id_blacklist == NULL) {
+ // This prevents people from being unable to access their uploads
+ // because of URL rewriting
+ $id_blacklist = scandir(FCPATH);
+ $id_blacklist[] = "file";
+ $id_blacklist[] = "user";
+ }
+
+ $max_tries = 100;
+
+ for ($try = 0; $try < $max_tries; $try++) {
+ $id = random_alphanum($min, $max);
+
+ if ($this->id_exists($id) || in_array($id, $id_blacklist)) {
+ continue;
+ }
+
+ return $id;
+ }
+
+ throw new \exceptions\PublicApiException("file/new_id-try-limit", "Failed to find unused ID after $max_tries tries");
+ }
+
+ function id_exists($id)
+ {
+ if (!$id) {
+ return false;
+ }
+
+ $query = $this->db->select('id')
+ ->from('files')
+ ->where('id', $id)
+ ->limit(1)
+ ->get();
+
+ return $query->num_rows() == 1;
+ }
+
+ function get_filedata($id)
+ {
+ return cache_function("filedatav2-$id", 300, function() use ($id) {
+ $query = $this->db
+ ->select('files.id, hash, file_storage.id storage_id, filename, mimetype, files.date, user, filesize')
+ ->from('files')
+ ->join('file_storage', 'file_storage.id = files.file_storage_id')
+ ->where('files.id', $id)
+ ->limit(1)
+ ->get();
+
+ if ($query->num_rows() > 0) {
+ $data = $query->row_array();
+ $data["data_id"] = $data["hash"]."-".$data["storage_id"];
+ return $data;
+ } else {
+ return false;
+ }
+ });
+ }
+
+ // return the folder in which the file with $data_id is stored
+ function folder($data_id) {
+ return $this->upload_path.'/'.substr($data_id, 0, 3);
+ }
+
+ // Returns the full path to the file with $data_id
+ function file($data_id) {
+ return $this->folder($data_id).'/'.$data_id;
+ }
+
+ // Add a file to the DB
+ function add_file($userid, $id, $filename, $storage_id)
+ {
+ $this->db->insert("files", array(
+ "id" => $id,
+ "filename" => $filename,
+ "date" => time(),
+ "user" => $userid,
+ "file_storage_id" => $storage_id,
+ ));
+ }
+
+ function adopt($id)
+ {
+ $userid = $this->muser->get_userid();
+
+ $this->db->set(array('user' => $userid ))
+ ->where('id', $id)
+ ->where('user', 0)
+ ->update('files');
+ return $this->db->affected_rows();
+ }
+
+ // remove old/invalid/broken IDs
+ public function valid_id($id)
+ {
+ $filedata = $this->get_filedata($id);
+
+ if (!$filedata) {
+ return false;
+ }
+
+ return $this->valid_filedata($filedata);
+ }
+
+ public function valid_filedata($filedata)
+ {
+ return \service\files::valid_id($filedata, $this->id_validation_config, $this, time());
+ }
+
+ public function file_exists($file)
+ {
+ return file_exists($file);
+ }
+
+ public function filemtime($file)
+ {
+ return filemtime($file);
+ }
+
+ public function filesize($file)
+ {
+ return filesize($file);
+ }
+
+ public function get_timeout($id)
+ {
+ $filedata = $this->get_filedata($id);
+ $file = $this->file($filedata["data_id"]);
+
+ if ($this->config->item("upload_max_age") == 0) {
+ return -1;
+ }
+
+ if (filesize($file) > $this->config->item("small_upload_size")) {
+ return $filedata["date"] + $this->config->item("upload_max_age");
+ } else {
+ return -1;
+ }
+ }
+
+ public function get_timeout_string($id)
+ {
+ $timeout = $this->get_timeout($id);
+
+ if ($timeout >= 0) {
+ return date("r", $timeout);
+ } else {
+ return "unknown";
+ }
+ }
+
+ private function unused_file($data_id)
+ {
+ list ($hash, $storage_id) = explode("-", $data_id);
+ $query = $this->db->select('files.id')
+ ->from('files')
+ ->join('file_storage', 'file_storage.id = files.file_storage_id')
+ ->where('hash', $hash)
+ ->where('file_storage.id', $storage_id)
+ ->limit(1)
+ ->get();
+
+ return $query->num_rows() == 0;
+ }
+
+ public function delete_by_user($userid)
+ {
+ $query = $this->db->select("id")
+ ->where("user", $userid)
+ ->get("files")->result_array();
+ $ids = array_map(function ($a) {return $a['id'];}, $query);
+
+ foreach ($ids as $id) {
+ $this->delete_id($id);
+ }
+ }
+
+ public function delete_id($id)
+ {
+ $filedata = $this->get_filedata($id);
+
+ // Delete the file and all multipastes using it
+ // Note that this does not delete all relations in multipaste_file_map
+ // which is actually done by an SQL contraint.
+ // TODO: make it work properly without the constraint
+ $map = $this->db->select('url_id')
+ ->distinct()
+ ->from('multipaste_file_map')
+ ->join("multipaste", "multipaste.multipaste_id = multipaste_file_map.multipaste_id")
+ ->where('file_url_id', $id)
+ ->get()->result_array();
+
+ $this->db->where('id', $id)
+ ->delete('files');
+ delete_cache("filedata-$id");
+
+ foreach ($map as $entry) {
+ assert(!empty($entry['url_id']));
+ $this->mmultipaste->delete_id($entry["url_id"]);
+ }
+
+ if ($this->id_exists($id)) {
+ return false;
+ }
+
+ if ($filedata !== false) {
+ assert(isset($filedata["data_id"]));
+ if ($this->unused_file($filedata['data_id'])) {
+ $this->delete_data_id($filedata['data_id']);
+ }
+ }
+ return true;
+ }
+
+ public function delete_data_id($data_id)
+ {
+ list ($hash, $storage_id) = explode("-", $data_id);
+
+ $this->db->where('id', $storage_id)
+ ->delete('file_storage');
+ if (file_exists($this->file($data_id))) {
+ unlink($this->file($data_id));
+ }
+ $dir = $this->folder($data_id);
+ if (file_exists($dir)) {
+ if (count(scandir($dir)) == 2) {
+ rmdir($dir);
+ }
+ }
+ delete_cache("${data_id}_thumb_150");
+ }
+
+ public function get_owner($id)
+ {
+ return $this->db->select('user')
+ ->from('files')
+ ->where('id', $id)
+ ->get()->row_array()
+ ['user'];
+ }
+
+}
+
+# vim: set noet:
diff --git a/application/models/Mmultipaste.php b/application/models/Mmultipaste.php
new file mode 100644
index 000000000..52ea4dfb4
--- /dev/null
+++ b/application/models/Mmultipaste.php
@@ -0,0 +1,184 @@
+<?php
+/*
+ * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net>
+ *
+ * Licensed under AGPLv3
+ * (see COPYING for full license text)
+ *
+ */
+
+class Mmultipaste extends CI_Model {
+
+ function __construct()
+ {
+ parent::__construct();
+ $this->load->model("muser");
+ $this->load->model("mfile");
+ }
+
+ /**
+ * Returns an unused ID
+ *
+ * @param min minimal length of the resulting ID
+ * @param max maximum length of the resulting ID
+ */
+ public function new_id($min = 3, $max = 6)
+ {
+ static $id_blacklist = NULL;
+
+ if ($id_blacklist == NULL) {
+ // This prevents people from being unable to access their uploads
+ // because of URL rewriting
+ $id_blacklist = scandir(FCPATH);
+ $id_blacklist[] = "file";
+ $id_blacklist[] = "user";
+ }
+
+ $max_tries = 100;
+
+ for ($try = 0; $try < $max_tries; $try++) {
+ $id = "m-".random_alphanum($min, $max);
+
+ // TODO: try to insert the id into file_groups instead of checking with
+ // id_exists (prevents race conditio)
+ if ($this->id_exists($id) || in_array($id, $id_blacklist)) {
+ continue;
+ }
+
+ $this->db->insert("multipaste", array(
+ "url_id" => $id,
+ "user_id" => $this->muser->get_userid(),
+ "date" => time(),
+ ));
+
+ return $id;
+ }
+
+ throw new \exceptions\PublicApiException("file/new_id-try-limit", "Failed to find unused ID after $max_tries tries");
+ }
+
+ public function id_exists($id)
+ {
+ if (!$id) {
+ return false;
+ }
+
+ $sql = '
+ SELECT url_id
+ FROM '.$this->db->dbprefix.'multipaste
+ WHERE url_id = ?
+ LIMIT 1';
+ $query = $this->db->query($sql, array($id));
+
+ return $query->num_rows() == 1;
+ }
+
+ public function valid_id($id)
+ {
+ $files = $this->get_files($id);
+ if (count($files) === 0) {
+ return false;
+ }
+
+ foreach ($files as $file) {
+ if (!$this->mfile->valid_id($file["id"])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function adopt($id)
+ {
+ $userid = $this->muser->get_userid();
+
+ $this->db->set(array('user_id' => $userid ))
+ ->where('url_id', $id)
+ ->where('user_id', 0)
+ ->update('multipaste');
+ return $this->db->affected_rows();
+ }
+
+ public function get_tarball_path($id)
+ {
+ return $this->config->item("upload_path")."/special/multipaste-tarballs/".substr(md5($id), 0, 3)."/$id.tar.gz";
+ }
+
+ public function delete_by_user($userid)
+ {
+ $query = $this->db->select("url_id")
+ ->where("user_id", $userid)
+ ->get("multipaste")->result_array();
+ $ids = array_map(function ($a) {return $a['url_id'];}, $query);
+
+ foreach ($ids as $id) {
+ $this->delete_id($id);
+ }
+ }
+
+ public function delete_id($id)
+ {
+ $this->db->where('url_id', $id)
+ ->delete('multipaste');
+
+ $path = $this->get_tarball_path($id);
+ $f = new \service\storage($this->get_tarball_path($id));
+ $f->unlink();
+
+ return !$this->id_exists($id);
+ }
+
+ public function get_owner($id)
+ {
+ return $this->db->query("
+ SELECT user_id
+ FROM ".$this->db->dbprefix."multipaste
+ WHERE url_id = ?
+ ", array($id))->row_array()["user_id"];
+ }
+
+ public function get_multipaste($id)
+ {
+ return $this->db->query("
+ SELECT url_id, user_id, date
+ FROM ".$this->db->dbprefix."multipaste
+ WHERE url_id = ?
+ ", array($id))->row_array();
+ }
+
+ public function get_files($url_id)
+ {
+ $ret = array();
+
+ $query = $this->db->query("
+ SELECT mfm.file_url_id
+ FROM ".$this->db->dbprefix."multipaste_file_map mfm
+ JOIN ".$this->db->dbprefix."multipaste m ON m.multipaste_id = mfm.multipaste_id
+ WHERE m.url_id = ?
+ ORDER BY mfm.sort_order
+ ", array($url_id))->result_array();
+
+ foreach ($query as $row) {
+ $filedata = $this->mfile->get_filedata($row["file_url_id"]);
+ $ret[] = $filedata;
+ }
+
+ return $ret;
+ }
+
+ public function get_multipaste_id($url_id)
+ {
+ $query = $this->db->query("
+ SELECT multipaste_id
+ FROM ".$this->db->dbprefix."multipaste
+ WHERE url_id = ?
+ ", array($url_id));
+
+ if ($query->num_rows() > 0) {
+ return $query->row_array()["multipaste_id"];
+ }
+
+ return false;
+ }
+
+}
diff --git a/application/models/Muser.php b/application/models/Muser.php
new file mode 100644
index 000000000..521d1c989
--- /dev/null
+++ b/application/models/Muser.php
@@ -0,0 +1,406 @@
+<?php
+/*
+ * Copyright 2012-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net>
+ *
+ * Licensed under AGPLv3
+ * (see COPYING for full license text)
+ *
+ */
+
+class Muser extends CI_Model {
+
+ private $default_upload_id_limits = "3-6";
+
+ // last level has the most access
+ private $access_levels = array("basic", "apikey", "full");
+
+ private $hashalgo;
+ private $hashoptions = array();
+
+ function __construct()
+ {
+ parent::__construct();
+
+ $this->load->helper("filebin");
+ $this->load->driver("duser");
+ $this->hashalgo = $this->config->item('auth_db')['hashing_algorithm'];
+ $this->hashoptions = $this->config->item('auth_db')['hashing_options'];
+ }
+
+ function has_session()
+ {
+ // checking $this doesn't work
+ $CI =& get_instance();
+ if (property_exists($CI, "session")) {
+ return true;
+ }
+
+ // Only load the session class if we already have a cookie that might need to be renewed.
+ // Otherwise we just create lots of stale sessions.
+ if (isset($_COOKIE[$this->config->item("sess_cookie_name")])) {
+ $this->load->library("session");
+ return true;
+ }
+
+ return false;
+ }
+
+ function require_session()
+ {
+ if (!$this->has_session()) {
+ $this->load->library("session");
+ }
+ }
+
+ function logged_in()
+ {
+ if ($this->has_session()) {
+ return $this->session->userdata('logged_in') === true;
+ }
+
+ return false;
+ }
+
+ function login($username, $password)
+ {
+ $this->require_session();
+ return $this->duser->login($username, $password);
+ }
+
+ private function login_api_client()
+ {
+ $username = $this->input->post("username");
+ $password = $this->input->post("password");
+
+ // TODO keep for now. might be useful if adapted to apikeys instead of passwords
+ // prefer post parameters if either (username or password) is set
+ //if ($username === false && $password === false) {
+ //if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
+ //$username = $_SERVER['PHP_AUTH_USER'];
+ //$password = $_SERVER['PHP_AUTH_PW'];
+ //}
+ //}
+
+ if ($username !== false && $password !== false) {
+ if ($this->login($username, $password)) {
+ return true;
+ } else {
+ throw new \exceptions\NotAuthenticatedException("user/login-failed", "Login failed");
+ }
+ }
+
+ return null;
+ }
+
+ function apilogin($apikey)
+ {
+ $this->require_session();
+
+ // get rid of spaces and newlines
+ $apikey = trim($apikey);
+
+ $query = $this->db->select('user, access_level')
+ ->from('apikeys')
+ ->where('key', $apikey)
+ ->get()->row_array();
+
+ if (isset($query["user"])) {
+ $this->session->set_userdata(array(
+ 'logged_in' => true,
+ 'username' => '',
+ 'userid' => $query["user"],
+ 'access_level' => $query["access_level"],
+ ));
+ return true;
+ }
+
+ throw new \exceptions\NotAuthenticatedException("user/api-login-failed", "API key login failed");
+ }
+
+ function logout()
+ {
+ $this->require_session();
+ $this->session->unset_userdata('logged_in');
+ $this->session->unset_userdata('username');
+ $this->session->unset_userdata('userid');
+ $this->session->sess_destroy();
+ }
+
+ function get_username()
+ {
+ if (!$this->logged_in()) {
+ return "";
+ }
+
+ return $this->session->userdata('username');
+ }
+
+ /*
+ * Check if a given username is valid.
+ *
+ * Valid usernames contain only lowercase characters and numbers. They are
+ * also <= 32 characters in length.
+ *
+ * @return boolean
+ */
+ public function valid_username($username)
+ {
+ return strlen($username) <= 32 && preg_match("/^[a-z0-9]+$/", $username);
+ }
+
+ /**
+ * Check if a given email is valid. Only perform minimal checking since
+ * verifying emails is very very difficuly.
+ *
+ * @return boolean
+ */
+ public function valid_email($email)
+ {
+ return $email === filter_var($email, FILTER_VALIDATE_EMAIL);
+ }
+
+ public function add_user($username, $password, $email, $referrer)
+ {
+ if (!$this->valid_username($username)) {
+ throw new \exceptions\UserInputException("user/invalid-username", "Invalid username (only up to 32 chars of a-z0-9 are allowed)");
+ } else {
+ if ($this->muser->username_exists($username)) {
+ throw new \exceptions\UserInputException("user/username-already-exists", "Username already exists");
+ }
+ }
+
+ if (!$this->valid_email($email)) {
+ throw new \exceptions\UserInputException("user/invalid-email", "Invalid email");
+ }
+
+ $this->db->set(array(
+ 'username' => $username,
+ 'password' => $this->hash_password($password),
+ 'email' => $email,
+ 'referrer' => $referrer
+ ))
+ ->insert('users');
+ }
+
+ /**
+ * Delete a user.
+ *
+ * @param username
+ * @param password
+ * @return true on sucess, false otherwise
+ */
+ public function delete_user($username, $password)
+ {
+ $this->duser->require_implemented("can_delete_account");
+
+ if ($this->duser->test_login_credentials($username, $password)) {
+ $this->delete_user_real($username);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete a user
+ *
+ * @param username
+ * @return void
+ */
+ public function delete_user_real($username)
+ {
+ $this->duser->require_implemented("can_delete_account");
+ $userid = $this->get_userid_by_name($username);
+ if ($userid === null) {
+ throw new \exceptions\ApiException("user/delete", "User cannot be found", ["username" => $username]);
+ }
+
+ $this->db->delete('profiles', array('user' => $userid));
+
+ $this->load->model("mfile");
+ $this->load->model("mmultipaste");
+ $this->mfile->delete_by_user($userid);
+ $this->mmultipaste->delete_by_user($userid);
+
+ # null out user data to keep referer information traceable
+ # If referer information was relinked, one user could create many
+ # accounts, delete the account that was used to invite them and
+ # then cause trouble so that the account that invited him gets
+ # banned because the admin thinks that account invited abusers
+ $this->db->set(array(
+ 'username' => null,
+ 'password' => null,
+ 'email' => null,
+ ))
+ ->where(array('username' => $username))
+ ->update('users');
+ }
+
+ function get_userid()
+ {
+ if (!$this->logged_in()) {
+ return 0;
+ }
+
+ return $this->session->userdata("userid");
+ }
+
+ public function get_userid_by_name($username)
+ {
+ $query = $this->db->select('id')
+ ->from('users')
+ ->where('username', $username)
+ ->get()->row_array();
+ if ($query) {
+ return $query['id'];
+ }
+
+ return null;
+ }
+
+ function get_email($userid)
+ {
+ return $this->duser->get_email($userid);
+ }
+
+ public function get_access_levels()
+ {
+ return $this->access_levels;
+ }
+
+ private function check_access_level($wanted_level)
+ {
+ $session_level = $this->session->userdata("access_level");
+
+ $wanted = array_search($wanted_level, $this->get_access_levels());
+ $have = array_search($session_level, $this->get_access_levels());
+
+ if ($wanted === false || $have === false) {
+ throw new \exceptions\PublicApiException("api/invalid-accesslevel", "Failed to determine access level");
+ }
+
+ if ($have >= $wanted) {
+ return;
+ }
+
+ throw new \exceptions\InsufficientPermissionsException("api/insufficient-permissions", "Access denied: Access level too low. Required: $wanted_level; Have: $session_level");
+ }
+
+ function require_access($wanted_level = "full")
+ {
+ if ($this->input->post("apikey") !== null) {
+ $this->apilogin($this->input->post("apikey"));
+ }
+
+ //if (is_api_client()) {
+ //$this->login_api_client();
+ //}
+
+ if ($this->logged_in()) {
+ return $this->check_access_level($wanted_level);
+ }
+
+ throw new \exceptions\NotAuthenticatedException("api/not-authenticated", "Not authenticated. FileBin requires you to have an account, please go to the homepage at ".site_url()." for more information.");
+ }
+
+ function username_exists($username)
+ {
+ return $this->duser->username_exists($username);
+ }
+
+ function get_action($action, $key)
+ {
+ $query = $this->db->from('actions')
+ ->where('key', $key)
+ ->where('action', $action)
+ ->get()->row_array();
+
+ if (!isset($query["key"]) || $key !== $query["key"]) {
+ throw new \exceptions\UserInputException("user/get_action/invalid-action", "Invalid action key. Has the key been used already?");
+ }
+
+ return $query;
+ }
+
+ public function get_profile_data()
+ {
+ $userid = $this->get_userid();
+
+ $fields = array(
+ "user" => $userid,
+ "upload_id_limits" => $this->default_upload_id_limits,
+ );
+
+ $query = $this->db->select(implode(', ', array_keys($fields)))
+ ->from('profiles')
+ ->where('user', $userid)
+ ->get()->row_array();
+
+ if ($query === null) {
+ $query = [];
+ }
+
+ $extra_fields = array(
+ "username" => $this->get_username(),
+ "email" => $this->get_email($userid),
+ );
+
+ return array_merge($fields, $query, $extra_fields);
+ }
+
+ public function update_profile($data)
+ {
+ assert(is_array($data));
+
+ $data["user"] = $this->get_userid();
+
+ $exists_in_db = $this->db->get_where("profiles", array("user" => $data["user"]))->num_rows() > 0;
+
+ if ($exists_in_db) {
+ $this->db->where("user", $data["user"]);
+ $this->db->update("profiles", $data);
+ } else {
+ $this->db->insert("profiles", $data);
+ }
+ }
+
+ public function set_password($userid, $password) {
+ $this->db->where('id', $userid)
+ ->update('users', array(
+ 'password' => $this->hash_password($password)
+ ));
+ }
+
+ public function rehash_password($userid, $password, $hash) {
+ if (password_needs_rehash($hash, $this->hashalgo, $this->hashoptions)) {
+ $this->set_password($userid, $password);
+ }
+ }
+
+ public function get_upload_id_limits()
+ {
+ $userid = $this->get_userid();
+
+ $query = $this->db->select('upload_id_limits')
+ ->from('profiles')
+ ->where('user', $userid)
+ ->get()->row_array();
+
+ if (empty($query)) {
+ return explode("-", $this->default_upload_id_limits);
+ }
+
+ return explode("-", $query["upload_id_limits"]);
+ }
+
+ function hash_password($password)
+ {
+ $hash = password_hash($password, $this->hashalgo, $this->hashoptions);
+ if ($hash === false) {
+ throw new \exceptions\ApiException('user/hash_password/failed', "Failed to hash password");
+ }
+ return $hash;
+ }
+
+}
+