diff options
Diffstat (limited to 'application')
187 files changed, 12630 insertions, 260 deletions
diff --git a/application/config/.gitignore b/application/config/.gitignore new file mode 100644 index 000000000..45e1c5158 --- /dev/null +++ b/application/config/.gitignore @@ -0,0 +1,3 @@ +config-local.php +database.php +memcached.php diff --git a/application/config/autoload.php b/application/config/autoload.php index 7cdc9013c..a95dc038a 100644 --- a/application/config/autoload.php +++ b/application/config/autoload.php @@ -58,7 +58,7 @@ $autoload['packages'] = array(); | | $autoload['libraries'] = array('user_agent' => 'ua'); */ -$autoload['libraries'] = array(); +$autoload['libraries'] = array('database'); /* | ------------------------------------------------------------------- @@ -89,7 +89,7 @@ $autoload['drivers'] = array(); | | $autoload['helper'] = array('url', 'file'); */ -$autoload['helper'] = array(); +$autoload['helper'] = array('url'); /* | ------------------------------------------------------------------- diff --git a/application/config/config.php b/application/config/config.php index 10315220e..e120beaf6 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -158,7 +158,11 @@ $config['composer_autoload'] = FALSE; | DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!! | */ -$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; +if (php_sapi_name() == "cli") { + $config['permitted_uri_chars'] = ''; +} else { + $config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; +} /* |-------------------------------------------------------------------------- @@ -377,10 +381,10 @@ $config['encryption_key'] = ''; | except for 'cookie_prefix' and 'cookie_httponly', which are ignored here. | */ -$config['sess_driver'] = 'files'; +$config['sess_driver'] = 'database'; $config['sess_cookie_name'] = 'ci_session'; $config['sess_expiration'] = 7200; -$config['sess_save_path'] = NULL; +$config['sess_save_path'] = "ci_sessions"; $config['sess_match_ip'] = FALSE; $config['sess_time_to_update'] = 300; $config['sess_regenerate_destroy'] = FALSE; @@ -448,7 +452,7 @@ $config['global_xss_filtering'] = FALSE; | 'csrf_regenerate' = Regenerate token on every submission | 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks */ -$config['csrf_protection'] = FALSE; +$config['csrf_protection'] = FALSE; // our controller enables this later $config['csrf_token_name'] = 'csrf_test_name'; $config['csrf_cookie_name'] = 'csrf_cookie_name'; $config['csrf_expire'] = 7200; @@ -521,3 +525,135 @@ $config['rewrite_short_tags'] = FALSE; | Array: array('10.0.1.200', '192.168.5.0/24') */ $config['proxy_ips'] = ''; + +/* +|-------------------------------------------------------------------------- +| FileBin +|-------------------------------------------------------------------------- + */ + +// This address will be used as the sender for emails (like password recovery mails). +$config['email_from'] = "webmaster@example.invalid"; + +// upload_path should NOT be readable/served by the server, but only by the script +$config['upload_path'] = FCPATH.'data/uploads'; + +// Make sure to adjust PHP's limits (post_max_size, upload_max_filesize) if necessary +$config['upload_max_size'] = 256*1024*1024; // 256MiB + +// Files smaller than this will be highlit, larger ones will simply be downloaded +// even if requested to be highlit. +$config['upload_max_text_size'] = 2*1024*1024; // 2MiB + +// Files older than this will be deleted by the cron job or when accessed. +// 0 disables deletion. +$config['upload_max_age'] = 60*60*24*5; // 5 days + +// Action keys (invitions, password resets) will be deleted after this time by +// the cron job. +$config['actions_max_age'] = 60*60*24*5; // 5 days + +// Files smaller than this won't be deleted (even if they are old enough) +$config['small_upload_size'] = 1024*10; // 10KiB + +// Maximum size for multipaste tarballs. 0 disables the feature +$config['tarball_max_size'] = 1024*1024*50; // 50MiB + +// Multipaste tarballs older than this will be deleted by the cron job +// Changing this is not recommended +$config['tarball_cache_time'] = 60*5; // 5 minutes + +// The maximum number of active invitation keys per account. +$config['max_invitation_keys'] = 3; //3 keys + + +// Possible values: +// - apc: needs the apc module and is only useful on long running php processes +// - file: you will have to clean up the cache directory yourself (./application/cache/) +// example cronjob: +// */15 * * * * find ./application/cache/ -mtime +0.5 -not \( -name .htaccess -or -name index.html \) -delete +// - memcached: config in application/config/memcached.php; you need the memcached module (with the D) +// - dummy: disables caching +// +// It is highly suggested to enable the cache. +$config['cache_backend'] = "dummy"; + + +// For possible drivers look into ./application/libraries/Duser/drivers/ +$config['authentication_driver'] = 'db'; + +// This is only used it the driver is set to ldap +if (extension_loaded("ldap")) { + $config['auth_ldap'] = array( + "host" => 'ldaps://ldap.example.com', + "port" => 636, + "basedn" => "dc=example,dc=com", + "scope" => "one", // possible values: base, one, subtree + "options" => array( + // key/values pairs for ldap_set_option + // http://php.net/manual/en/function.ldap-set-option.php + LDAP_OPT_PROTOCOL_VERSION => 3 + ), + // Please note that php-ldap converts attributes to lowercase + "userid_field" => "uidnumber", // This has to be a unique integer + "username_field" => "uid", // This is the value the user supplies on the login form + // Optional parameters + // "bind_rdn" => "uid=search-user,cn=users,dc=example,dc=com", // This is the user used to authenticate for searches + // "bind_password" => "***", // This is the password for the search user + // You can optionally filter the LDAP users who are allowed to log in using any valid LDAP filter. %s will be replaced + // by the user name. + // "filter" => "(&(uid=%s)(memberOf=cn=FileBinUsers,cn=groups,dc=example,dc=com))", + ); +} + +// This is only used if the driver is set to fluxbb +$config['auth_fluxbb'] = array( + 'database' => 'fluxbb' +); + +// This is only used if the driver is set to db. Changes to these settings will be +// applied when users sucessfully log in with their password. +// For information about these values refer to https://secure.php.net/manual/en/function.password-hash.php +$config['auth_db'] = array( + 'hashing_options' => array( + 'cost' => 10, + ), + 'hashing_algorithm' => PASSWORD_DEFAULT, +); + + +// Possible values: production, development +// "development" enables features like profiling and display of SQL queries. +$config['environment'] = "production"; + + +// This sets the download implementation. Possible values are php, nginx and lighttpd. +// The nginx and lighttpd drivers make use of the server's sendfile feature. +// +// The lighttpd driver requires the following directive to be set in your fastcgi.server configuration: +// "allow-x-send-file" => "enable" +// See http://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModFastCGI#X-Sendfile +// +// When using the nginx download driver you need to define an internal location +// from which nginx will serve your uploads: +// location ^~ /protected-uploads/ { +// internal; +// alias <upload_path>/; +// } +// See http://wiki.nginx.org/X-accel +$config['download_driver'] = 'php'; + +$config['download_nginx_location'] = '/protected-uploads'; + +if (file_exists(APPPATH.'config/config-local.php')) { + include APPPATH.'config/config-local.php'; +} + +if (getenv("ENVIRONMENT") === "testsuite" && isset($_SERVER['SERVER_PORT'])) { + $config['base_url'] = 'http://127.0.0.1:'.$_SERVER['SERVER_PORT'].'/'; +} + +if (getenv("ENVIRONMENT") === "testsuite") { + $config['upload_path'] = FCPATH.'testsuite-tmp'; + $config['auth_db']['hashing_options']['cost'] = 5; +} diff --git a/application/config/constants.php b/application/config/constants.php index 18d3b4b76..5743ee8e5 100644 --- a/application/config/constants.php +++ b/application/config/constants.php @@ -13,6 +13,8 @@ defined('BASEPATH') OR exit('No direct script access allowed'); */ defined('SHOW_DEBUG_BACKTRACE') OR define('SHOW_DEBUG_BACKTRACE', TRUE); +putenv('HOME='.FCPATH); + /* |-------------------------------------------------------------------------- | File and Directory Modes diff --git a/application/config/example/.gitignore b/application/config/example/.gitignore new file mode 100644 index 000000000..f9be8dfe0 --- /dev/null +++ b/application/config/example/.gitignore @@ -0,0 +1 @@ +!* diff --git a/application/config/example/config-local.php b/application/config/example/config-local.php new file mode 100644 index 000000000..172f0e1cc --- /dev/null +++ b/application/config/example/config-local.php @@ -0,0 +1,19 @@ +<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); + +/* + * Use this file to override any settings from config.php + * + * For descriptions of the options please refer to config.php. + */ + +# URL to the application +$config['base_url'] = ''; + +// set this to a 32char random string +$config['encryption_key'] = ''; + +$config['upload_path'] = FCPATH.'data/uploads'; + +$config['index_page'] = 'index.php'; + +$config['cache_backend'] = "dummy"; diff --git a/application/config/database.php b/application/config/example/database.php index 0088ef140..a5083d0eb 100644 --- a/application/config/database.php +++ b/application/config/example/database.php @@ -27,8 +27,6 @@ defined('BASEPATH') OR exit('No direct script access allowed'); | to the table name when using the Query Builder class | ['pconnect'] TRUE/FALSE - Whether to use a persistent connection | ['db_debug'] TRUE/FALSE - Whether database errors should be displayed. -| ['cache_on'] TRUE/FALSE - Enables/disables query caching -| ['cachedir'] The path to the folder where cache files should be stored | ['char_set'] The character set used in communicating with the database | ['dbcollat'] The character collation used in communicating with the database | NOTE: For MySQL and MySQLi databases, this setting is only used @@ -76,21 +74,25 @@ $query_builder = TRUE; $db['default'] = array( 'dsn' => '', 'hostname' => 'localhost', + 'port' => 3306, 'username' => '', 'password' => '', 'database' => '', 'dbdriver' => 'mysqli', 'dbprefix' => '', 'pconnect' => FALSE, - 'db_debug' => (ENVIRONMENT !== 'production'), - 'cache_on' => FALSE, - 'cachedir' => '', - 'char_set' => 'utf8', - 'dbcollat' => 'utf8_general_ci', + 'db_debug' => TRUE, + 'char_set' => 'utf8mb4', // if you use postgres, set this to utf8 + 'dbcollat' => 'utf8mb4_bin', // if you use postgres, set this to utf8_bin 'swap_pre' => '', 'encrypt' => FALSE, 'compress' => FALSE, - 'stricton' => FALSE, + 'stricton' => TRUE, 'failover' => array(), 'save_queries' => TRUE ); + +if (getenv("ENVIRONMENT") === "testsuite") { + $db['default']['database'] = "filebin_testsuite"; + $db['default']['dbprefix'] = "testsuite_prefix_"; +} diff --git a/application/config/example/index.html b/application/config/example/index.html new file mode 100644 index 000000000..c942a79ce --- /dev/null +++ b/application/config/example/index.html @@ -0,0 +1,10 @@ +<html> +<head> + <title>403 Forbidden</title> +</head> +<body> + +<p>Directory access is forbidden.</p> + +</body> +</html>
\ No newline at end of file diff --git a/application/config/example/memcached.php b/application/config/example/memcached.php new file mode 100644 index 000000000..29b145ec8 --- /dev/null +++ b/application/config/example/memcached.php @@ -0,0 +1,17 @@ +<?php + +$config = array( + "default" => array( + "hostname" => "127.0.0.1", + "port" => 11211, + "weight" => 1, + ), + "socket" => array( + "hostname" => FCPATH.'/memcached.sock', + "port" => 0, + "weight" => 2, + ), +); + + +?> diff --git a/application/config/migration.php b/application/config/migration.php index 4b585a65c..ffddae2ac 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -11,7 +11,7 @@ defined('BASEPATH') OR exit('No direct script access allowed'); | and disable it back when you're done. | */ -$config['migration_enabled'] = FALSE; +$config['migration_enabled'] = true; /* |-------------------------------------------------------------------------- @@ -29,7 +29,7 @@ $config['migration_enabled'] = FALSE; | defaults to 'sequential' for backward compatibility with CI2. | */ -$config['migration_type'] = 'timestamp'; +$config['migration_type'] = 'sequential'; /* |-------------------------------------------------------------------------- @@ -69,7 +69,7 @@ $config['migration_auto_latest'] = FALSE; | be upgraded / downgraded to. | */ -$config['migration_version'] = 0; +$config['migration_version'] = 21; /* |-------------------------------------------------------------------------- @@ -82,3 +82,4 @@ $config['migration_version'] = 0; | */ $config['migration_path'] = APPPATH.'migrations/'; + diff --git a/application/config/routes.php b/application/config/routes.php index 1b45740d7..4e4d8c71e 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -49,6 +49,12 @@ defined('BASEPATH') OR exit('No direct script access allowed'); | Examples: my-controller/index -> my_controller/index | my-controller/my-method -> my_controller/my_method */ -$route['default_controller'] = 'welcome'; +$route['default_controller'] = "main"; +$route['user/(.+)'] = "user/$1"; +$route['file/multipaste/(.+)'] = "file/multipaste/$1"; +$route['file/(.+)'] = "main/$1"; +$route['tools/(.+)'] = "tools/$1"; +$route['api/(.+)'] = "api/route/$1"; +$route['(.+)'] = "main/index/$1"; $route['404_override'] = ''; $route['translate_uri_dashes'] = FALSE; diff --git a/application/controllers/Api.php b/application/controllers/Api.php new file mode 100644 index 000000000..1fa49cb46 --- /dev/null +++ b/application/controllers/Api.php @@ -0,0 +1,93 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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 $this->send_json_reply($c->$function()); + } catch (\exceptions\PublicApiException $e) { + return $this->send_json_error_reply($e->get_error_id(), $e->getMessage(), $e->get_data()); + } catch (\Exception $e) { + \libraries\ExceptionHandler::log_exception($e); + return $this->send_json_error_reply("internal-error", "An unhandled internal server error occured"); + } + } + + private function send_json_reply($array, $status = "success") { + $reply = array(); + $reply["status"] = $status; + $reply["data"] = $array; + + $CI =& get_instance(); + $CI->output->set_content_type('application/json'); + $CI->output->set_output(json_encode($reply)); + } + + private function send_json_error_reply($error_id, $message, $array = null, $status_code = 400) { + $reply = array(); + $reply["status"] = "error"; + $reply["error_id"] = $error_id; + $reply["message"] = $message; + + if ($array !== null) { + $reply["data"] = $array; + } + + $CI =& get_instance(); + $CI->output->set_status_header($status_code); + $CI->output->set_content_type('application/json'); + $CI->output->set_output(json_encode($reply)); + } + +} diff --git a/application/controllers/Main.php b/application/controllers/Main.php new file mode 100644 index 000000000..b0f88753e --- /dev/null +++ b/application/controllers/Main.php @@ -0,0 +1,942 @@ +<?php +/* + * Copyright 2009-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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 (is_cli()) { + output_cli_usage(); + exit; + } + + // 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(); + } + } + + private function _handle_etag($etag) + { + $etag = strtolower($etag); + $modified = true; + + if(isset($_SERVER['HTTP_IF_NONE_MATCH'])) { + $oldtag = trim(strtolower($_SERVER['HTTP_IF_NONE_MATCH']), '"'); + if($oldtag == $etag) { + $modified = false; + } else { + $modified = true; + } + } + + header('Etag: "'.$etag.'"'); + + if (!$modified) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } + } + + /** + * 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() + { + session_write_close(); + $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": + $this->_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); + $this->_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 && preg_match('/^[^?]*\/(\?.*)?$/', $_SERVER['REQUEST_URI']); + $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 ($lexer == "asciinema") { + $output_cache->add(array("filedata" => $filedata), "file/fragments/asciinema-player"); + continue; + } + + 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'; "); + } + $this->_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; + } + } + + $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 === NULL) { + $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'); + + $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); + } + + function thumbnail() + { + session_write_close(); + $id = $this->uri->segment(3); + + if (!$this->mfile->valid_id($id)) { + return $this->_non_existent(); + } + + $etag = "$id-thumb"; + $this->_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 === null) { + $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 ($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["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 === NULL) { + 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"] = '<p>Contact info not available.</p>'; + } + + $this->load->view('header', $this->data); + $this->load->view('contact', $this->data); + $this->load->view('footer', $this->data); + } + + /* Functions below this comment can only be run via the CLI + * `php index.php file <function name>` + */ + + // Removes old files + function cron() + { + $this->_require_cli_request(); + + \service\files::clean_multipaste_tarballs(); + + $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(); + + \service\files::remove_files_missing_in_db(); + \service\files::remove_files_missing_on_disk(); + \service\files::clean_multipaste_tarballs(); + } + + 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..15f35cc9e --- /dev/null +++ b/application/controllers/Tools.php @@ -0,0 +1,119 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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() + { + output_cli_usage(); + 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"; + return; + } + + 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..c98784d50 --- /dev/null +++ b/application/controllers/User.php @@ -0,0 +1,717 @@ +<?php +/* + * Copyright 2012-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class User extends MY_Controller { + + function __construct() + { + parent::__construct(); + } + + function index() + { + if (is_cli()) { + $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') !== null) { + $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 === null ? "" : $comment; + $access_level = $this->input->post("access_level"); + + if ($access_level === null) { + $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(); + + \service\user::create_invitation_key($userid); + + redirect("user/invite"); + } + + function delete_invitation_key() + { + $this->duser->require_implemented("can_register_new_users"); + $this->muser->require_access(); + + $userid = $this->muser->get_userid(); + $key = $this->input->post("key"); + + \service\user::delete_invitation_key($userid, $key); + 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 !== null) { + $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 === null) { + return $this->_reset_password_username_form(); + } + + if ($key === null) { + return $this->_reset_password_send_mail(); + } + + if ($key !== null) { + 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 !== null) { + $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") !== null) { + $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 !== null) { + $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 !== null) { + 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"; + } + + function delete_user() + { + $this->_require_cli_request(); + $this->duser->require_implemented("can_delete_account"); + + echo "\nWARNING: Deleting a user will delete ALL their data permanently.\n\n"; + + $username = $this->_get_line_cli("Username", function($username) { + if (get_instance()->muser->username_exists($username)) { + return true; + } + return false; + }); + $this->muser->delete_user_real($username); + echo "User removed\n"; + } + +} diff --git a/application/controllers/Welcome.php b/application/controllers/Welcome.php deleted file mode 100644 index 9213c0cf5..000000000 --- a/application/controllers/Welcome.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php -defined('BASEPATH') OR exit('No direct script access allowed'); - -class Welcome extends CI_Controller { - - /** - * Index Page for this controller. - * - * Maps to the following URL - * http://example.com/index.php/welcome - * - or - - * http://example.com/index.php/welcome/index - * - or - - * Since this controller is set as the default controller in - * config/routes.php, it's displayed at http://example.com/ - * - * So any other public methods not prefixed with an underscore will - * map to /index.php/welcome/<method_name> - * @see https://codeigniter.com/user_guide/general/urls.html - */ - public function index() - { - $this->load->view('welcome_message'); - } -} diff --git a/application/controllers/api/api_controller.php b/application/controllers/api/api_controller.php new file mode 100644 index 000000000..d615d6cec --- /dev/null +++ b/application/controllers/api/api_controller.php @@ -0,0 +1,18 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace controllers\api; + +abstract class api_controller { + public function __construct() { + $this->CI =& get_instance(); + } + +} + diff --git a/application/controllers/api/v2/api_info.php b/application/controllers/api/v2/api_info.php new file mode 100644 index 000000000..8d2bdf6dc --- /dev/null +++ b/application/controllers/api/v2/api_info.php @@ -0,0 +1,16 @@ +<?php +/* + * Copyright 2014-2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class api_info extends \controllers\api\api_controller { + static public function get_version() + { + return "2.1.1"; + } +} diff --git a/application/controllers/api/v2/file.php b/application/controllers/api/v2/file.php new file mode 100644 index 000000000..3d4103f1c --- /dev/null +++ b/application/controllers/api/v2/file.php @@ -0,0 +1,96 @@ +<?php +/* + * Copyright 2014-2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class file extends \controllers\api\api_controller { + public function __construct() + { + parent::__construct(); + + $this->CI->load->model('mfile'); + $this->CI->load->model('mmultipaste'); + } + + public function upload() + { + $this->CI->muser->require_access("basic"); + + $files = getNormalizedFILES(); + + if (empty($files)) { + throw new \exceptions\PublicApiException("file/no-file", "No file was uploaded or unknown error occurred."); + } + + \service\files::verify_uploaded_files($files); + + $limits = $this->CI->muser->get_upload_id_limits(); + $userid = $this->CI->muser->get_userid(); + $urls = array(); + + foreach ($files as $file) { + $id = $this->CI->mfile->new_id($limits[0], $limits[1]); + \service\files::add_uploaded_file($userid, $id, $file["tmp_name"], $file["name"]); + $ids[] = $id; + $urls[] = site_url($id).'/'; + } + + return array( + "ids" => $ids, + "urls" => $urls, + ); + } + + public function get_config() + { + return array( + "upload_max_size" => $this->CI->config->item("upload_max_size"), + "max_files_per_request" => intval(ini_get("max_file_uploads")), + "max_input_vars" => intval(ini_get("max_input_vars")), + "request_max_size" => return_bytes(ini_get("post_max_size")), + ); + } + + public function history() + { + $this->CI->muser->require_access("apikey"); + $history = \service\files::history($this->CI->muser->get_userid()); + foreach ($history['multipaste_items'] as $key => $item) { + foreach ($item['items'] as $inner_key => $item) { + unset($history['multipaste_items'][$key]['items'][$inner_key]['sort_order']); + } + } + + $history = ensure_json_keys_contain_objects($history, array("items", "multipaste_items")); + + return $history; + } + + public function delete() + { + $this->CI->muser->require_access("apikey"); + $ids = $this->CI->input->post_array("ids"); + $ret = \service\files::delete($ids); + + $ret = ensure_json_keys_contain_objects($ret, array("errors", "deleted")); + + return $ret; + } + + public function create_multipaste() + { + $this->CI->muser->require_access("basic"); + $ids = $this->CI->input->post_array("ids"); + $userid = $this->CI->muser->get_userid(); + $limits = $this->CI->muser->get_upload_id_limits(); + + return \service\files::create_multipaste($ids, $userid, $limits); + } + +} +# vim: set noet: diff --git a/application/controllers/api/v2/user.php b/application/controllers/api/v2/user.php new file mode 100644 index 000000000..677a870c4 --- /dev/null +++ b/application/controllers/api/v2/user.php @@ -0,0 +1,75 @@ +<?php +/* + * Copyright 2014-2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace controllers\api\v2; + +class user extends \controllers\api\api_controller { + public function __construct() + { + parent::__construct(); + + $this->CI->load->model('muser'); + } + + public function apikeys() + { + $this->CI->muser->require_access("full"); + return \service\user::apikeys($this->CI->muser->get_userid()); + } + + public function create_apikey() + { + $username = $this->CI->input->post("username"); + $password = $this->CI->input->post("password"); + if ($username && $password) { + if (!$this->CI->muser->login($username, $password)) { + throw new \exceptions\NotAuthenticatedException("user/login-failed", "Login failed"); + } + } + + $this->CI->muser->require_access("full"); + + $userid = $this->CI->muser->get_userid(); + $comment = $this->CI->input->post("comment"); + $comment = $comment === null ? "" : $comment; + $access_level = $this->CI->input->post("access_level"); + + $key = \service\user::create_apikey($userid, $comment, $access_level); + + return array( + "new_key" => $key, + ); + } + + public function delete_apikey() + { + $this->CI->muser->require_access("full"); + + $userid = $this->CI->muser->get_userid(); + $key = $this->CI->input->post("delete_key"); + + $this->CI->db->where('user', $userid) + ->where('key', $key) + ->delete('apikeys'); + + $affected = $this->CI->db->affected_rows(); + + assert($affected >= 0 && $affected <= 1); + if ($affected == 1) { + return array( + "deleted_keys" => array( + $key => array ( + "key" => $key, + ), + ), + ); + } else { + throw new \exceptions\PublicApiException('user/delete_apikey/failed', 'Apikey deletion failed. Possibly wrong owner.'); + } + } +} diff --git a/application/controllers/file/Multipaste.php b/application/controllers/file/Multipaste.php new file mode 100644 index 000000000..cc8ab8819 --- /dev/null +++ b/application/controllers/file/Multipaste.php @@ -0,0 +1,113 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * 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 === null) { + $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 === null) { + $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 === null) { + $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/core/MY_Controller.php b/application/core/MY_Controller.php new file mode 100644 index 000000000..a3b6d15d7 --- /dev/null +++ b/application/core/MY_Controller.php @@ -0,0 +1,123 @@ +<?php +/* + * Copyright 2009-2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class MY_Controller extends CI_Controller { + public $data = array(); + public $var; + + function __construct() + { + parent::__construct(); + + $this->var = new StdClass(); + + $this->load->library('customautoloader'); + + // check if DB is up to date + if (!(is_cli() && $this->uri->segment(1) === "tools")) { + $this->_ensure_database_schema_up_to_date(); + } + + $old_path = getenv("PATH"); + putenv("PATH=$old_path:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin"); + + $this->load->helper(array('form', 'filebin')); + + if ($this->uri->segment(1) == "api") { + is_api_client(true); + } + + if ($this->_check_csrf_protection_required()) { + $this->_setup_csrf_protection(); + } + + if ($this->config->item("environment") == "development") { + $this->output->enable_profiler(true); + } + + $this->data['title'] = "FileBin"; + + $this->load->model("muser"); + $this->data["user_logged_in"] = $this->muser->logged_in(); + $this->data['redirect_uri'] = $this->uri->uri_string(); + if ($this->muser->has_session()) { + $this->data['show_multipaste_queue'] = !empty((new \service\multipaste_queue)->get()); + } + } + + protected function _require_cli_request() + { + if (!is_cli()) { + throw new \exceptions\PublicApiException("api/cli-only", "This function can only be accessed via the CLI interface"); + } + } + + private function _ensure_database_schema_up_to_date() + { + if (!$this->db->table_exists('migrations')){ + throw new \exceptions\PublicApiException("general/db/not-initialized", "Database not initialized. Can't find migrations table. Please run the migration script. (php index.php tools update_database)"); + } else { + $this->config->load("migration", true); + $target_version = $this->config->item("migration_version", "migration"); + + // TODO: wait 20 seconds for an update so requests don't get lost for short updates? + $row = $this->db->get('migrations')->row(); + + $current_version = $row ? $row->version : 0; + if ($current_version != $target_version) { + throw new \exceptions\PublicApiException("general/db/wrong-version", "Database version is $current_version, we want $target_version. Please run the migration script. (php index.php tools update_database)"); + } + } + } + + private function _check_csrf_protection_required() + { + if ($this->input->post("apikey") !== null || is_api_client()) { + /* This relies on the authentication code always verifying the supplied + * apikey. If the key is not verified/logged in an attacker could simply + * add an empty "apikey" field to the CSRF form to circumvent the + * protection. If we always log in if a key is supplied we can ensure + * that an attacker (and the victim since they get a cookie) can only + * access the attacker's account. + */ + // TODO: perform the apikey login here to make sure this works as expected? + return false; + } + + $uri_start = $this->uri->rsegment(1)."/".$this->uri->rsegment(2); + $csrf_whitelisted_handlers = array( + "always" => array( + /* Whitelist the upload pages because they don't cause harm and a user + * might keep the upload page open for more than csrf_expire seconds + * and we don't want to annoy them when they upload a big file and the + * CSRF check fails. + */ + "file/do_websubmit", + ), + ); + if (in_array($uri_start, $csrf_whitelisted_handlers["always"])) { + return false; + } + + if (is_cli()) { + return false; + } + + return true; + } + + private function _setup_csrf_protection() + { + // 2 functions for accessing config options, really? + $this->config->set_item('csrf_protection', true); + config_item("csrf_protection", true); + $this->security->__construct(); + $this->security->csrf_verify(); + } +} diff --git a/application/core/MY_Input.php b/application/core/MY_Input.php new file mode 100644 index 000000000..5a08ea4bb --- /dev/null +++ b/application/core/MY_Input.php @@ -0,0 +1,40 @@ +<?php +/* + * Copyright 2017 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class MY_Input extends CI_Input { + public function post($key = null, $xss_clean = false) { + $ret = parent::post($key, $xss_clean); + if (is_array($ret) || is_object($ret)) { + $data = [ + "key" => $key, + "ret" => $ret + ]; + if (preg_match("/^[a-zA-Z0-9_\.-]+$/", $key)) { + throw new \exceptions\UserInputException("input/invalid-form-field", "Invalid input in field $key", $data); + } else { + throw new \exceptions\UserInputException("input/invalid-form-field", "Invalid input", $data); + } + } + return $ret; + } + + public function post_array($key) { + $ret = parent::post($key); + if ($ret === null) { + return null; + } elseif (!is_array($ret)) { + $data = [ + "key" => $key, + "ret" => $ret + ]; + throw new \exceptions\UserInputException("input/invalid-form-field", "Invalid input", $data); + } + return $ret; + } +} diff --git a/application/core/MY_Output.php b/application/core/MY_Output.php new file mode 100644 index 000000000..402e46c80 --- /dev/null +++ b/application/core/MY_Output.php @@ -0,0 +1,16 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class MY_Output extends CI_Output { + public function __construct() + { + parent::__construct(); + $this->parse_exec_vars = false; + } +} diff --git a/application/exceptions/ApiException.php b/application/exceptions/ApiException.php new file mode 100644 index 000000000..b1ec96117 --- /dev/null +++ b/application/exceptions/ApiException.php @@ -0,0 +1,35 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class ApiException extends \Exception { + private $error_id; + private $data; + + public function __construct($error_id, $message, $data = null, $previous = null) + { + parent::__construct($message, 0, $previous); + + $this->error_id = $error_id; + $this->data = $data; + } + + public function get_error_id() + { + return $this->error_id; + } + + public function get_data() + { + return $this->data; + } + + public function get_http_error_code() + { + return 500; + } +} diff --git a/application/exceptions/FileUploadVerifyException.php b/application/exceptions/FileUploadVerifyException.php new file mode 100644 index 000000000..5125e4822 --- /dev/null +++ b/application/exceptions/FileUploadVerifyException.php @@ -0,0 +1,23 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class FileUploadVerifyException extends VerifyException { + public function __toString() + { + $ret = $this->getMessage()."\n"; + $data = $this->get_data(); + $errors = array(); + + foreach ($data as $error) { + $errors[] = sprintf("%s: %s", $error["filename"], $error["message"]); + } + + $ret .= implode("\n", $errors); + return $ret; + } +} diff --git a/application/exceptions/InsufficientPermissionsException.php b/application/exceptions/InsufficientPermissionsException.php new file mode 100644 index 000000000..a036edf9d --- /dev/null +++ b/application/exceptions/InsufficientPermissionsException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class InsufficientPermissionsException extends UserInputException { + public function get_http_error_code() + { + return 403; + } +} diff --git a/application/exceptions/NotAuthenticatedException.php b/application/exceptions/NotAuthenticatedException.php new file mode 100644 index 000000000..99ddd82fc --- /dev/null +++ b/application/exceptions/NotAuthenticatedException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class NotAuthenticatedException extends UserInputException { + public function get_http_error_code() + { + return 403; + } +} diff --git a/application/exceptions/NotFoundException.php b/application/exceptions/NotFoundException.php new file mode 100644 index 000000000..c4b9d1537 --- /dev/null +++ b/application/exceptions/NotFoundException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class NotFoundException extends UserInputException { + public function get_http_error_code() + { + return 404; + } +} diff --git a/application/exceptions/PublicApiException.php b/application/exceptions/PublicApiException.php new file mode 100644 index 000000000..d22309478 --- /dev/null +++ b/application/exceptions/PublicApiException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class PublicApiException extends ApiException { + public function __toString() + { + return $this->getMessage(); + } +} diff --git a/application/exceptions/RequestTooBigException.php b/application/exceptions/RequestTooBigException.php new file mode 100644 index 000000000..ae2ab4d22 --- /dev/null +++ b/application/exceptions/RequestTooBigException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class RequestTooBigException extends UserInputException { + public function get_http_error_code() + { + return 413; + } +} diff --git a/application/exceptions/UserInputException.php b/application/exceptions/UserInputException.php new file mode 100644 index 000000000..d4c327315 --- /dev/null +++ b/application/exceptions/UserInputException.php @@ -0,0 +1,14 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class UserInputException extends PublicApiException { + public function get_http_error_code() + { + return 400; + } +} diff --git a/application/exceptions/VerifyException.php b/application/exceptions/VerifyException.php new file mode 100644 index 000000000..0e9d8b93a --- /dev/null +++ b/application/exceptions/VerifyException.php @@ -0,0 +1,23 @@ +<?php +/* + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +namespace exceptions; + +class VerifyException extends UserInputException { + public function __toString() + { + $ret = $this->getMessage()."\n"; + $data = $this->get_data(); + $errors = array(); + + foreach ($data as $error) { + $errors[] = sprintf("%s: %s", $error["id"], $error["reason"]); + } + + $ret .= implode("\n", $errors); + return $ret; + } +} diff --git a/application/helpers/filebin_helper.php b/application/helpers/filebin_helper.php new file mode 100644 index 000000000..0fa986225 --- /dev/null +++ b/application/helpers/filebin_helper.php @@ -0,0 +1,325 @@ +<?php + +function expiration_duration($duration) +{ + $total = $duration; + $days = floor($total / 86400); + $total -= $days * 86400; + $hours = floor($total / 3600); + $total -= $hours * 3600; + $minutes = floor($total / 60); + $seconds = $total - $minutes * 60; + $times = array($days, $hours, $minutes, $seconds); + $suffixes = array(' day', ' hour', ' minute', ' second'); + $expiration = array(); + + for ($i = 0; $i < count($suffixes); $i++) { + if ($times[$i] != 0) { + $duration = $times[$i].$suffixes[$i]; + if ($times[$i] > 1) { + $duration .= "s"; + } + array_push($expiration, $duration); + } + } + + return join(", ", $expiration); +} + +function format_bytes($size) +{ + $suffixes = array('B', 'KiB', 'MiB', 'GiB', 'TiB' , 'PiB' , 'EiB', 'ZiB', 'YiB'); + $boundary = 2048.0; + + for ($suffix_pos = 0; $suffix_pos + 1 < count($suffixes); $suffix_pos++) { + if ($size <= $boundary && $size >= -$boundary) { + break; + } + $size /= 1024.0; + } + + # don't print decimals for bytes + if ($suffix_pos != 0) { + return sprintf("%.2f%s", $size, $suffixes[$suffix_pos]); + } else { + return sprintf("%.0f%s", $size, $suffixes[$suffix_pos]); + } +} + +function is_api_client($override = null) +{ + static $is_api = null; + + if ($override !== null) { + $is_api = $override; + } + + if ($is_api === null) { + $is_api = false; + } + return $is_api; +} + +function random_alphanum($min_length, $max_length = null) +{ + $random = ''; + $char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + $char_list .= "abcdefghijklmnopqrstuvwxyz"; + $char_list .= "1234567890"; + + if ($max_length === null) { + $max_length = $min_length; + } + $length = mt_rand($min_length, $max_length); + + for($i = 0; $i < $max_length; $i++) { + if (strlen($random) == $length) break; + $random .= substr($char_list, mt_rand(0, strlen($char_list) - 1), 1); + } + return $random; +} + +function link_with_mtime($file) +{ + $link = base_url($file); + + if (file_exists(FCPATH.$file)) { + $link .= "?".filemtime(FCPATH.$file); + } + + return $link; +} + +function js_cache_buster() +{ + $jsdir = FCPATH.'/data/js'; + $minified_main = $jsdir.'/main.min.js'; + if (file_exists($minified_main)) { + return filemtime($minified_main); + } + + $ret = 0; + + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($jsdir), RecursiveIteratorIterator::SELF_FIRST); + + foreach ($it as $file) { + $mtime = $file->getMTime(); + if ($file->isFile()) { + if ($mtime > $ret) { + $ret = $mtime; + } + } + } + return $ret; +} + +// Reference: http://php.net/manual/en/features.file-upload.multiple.php#109437 +// This is a little different because we don't care about the fieldname +function getNormalizedFILES() +{ + $newfiles = array(); + $ret = array(); + + foreach($_FILES as $fieldname => $fieldvalue) + foreach($fieldvalue as $paramname => $paramvalue) + foreach((array)$paramvalue as $index => $value) + $newfiles[$fieldname][$index][$paramname] = $value; + + $i = 0; + foreach ($newfiles as $fieldname => $field) { + foreach ($field as $file) { + // skip empty fields + if ($file["error"] === 4) { + continue; + } + $ret[$i] = $file; + $ret[$i]["formfield"] = $fieldname; + $i++; + } + } + + return $ret; +} + +// Allow simple checking inside views +function auth_driver_function_implemented($function) +{ + static $result = array(); + if (isset($result[$function])) { + return $result[$function]; + } + + $CI =& get_instance(); + $CI->load->driver("duser"); + $result[$function] = $CI->duser->is_implemented($function);; + + return $result[$function]; +} + +function static_storage($key, $value = null) +{ + static $storage = array(); + + if ($value !== null) { + $storage[$key] = $value; + } + + if (!isset($storage[$key])) { + $storage[$key] = null; + } + + return $storage[$key]; +} + +function stateful_client() +{ + $CI =& get_instance(); + + if ($CI->input->post("apikey")) { + return false; + } + + if (is_api_client()) { + return false; + } + + return true; +} + +function init_cache() +{ + static $done = false; + if ($done) {return;} + + $CI =& get_instance(); + $CI->load->driver('cache', array('adapter' => $CI->config->item("cache_backend"))); + $done = true; +} + +function delete_cache($key) +{ + init_cache(); + $CI =& get_instance(); + $CI->cache->delete($key); +} + +/** + * Cache the result of the function call in the cache backend. + * @param key cache key to use + * @param ttl time to live for the cache entry + * @param function function to call + * @return return value of function (will be cached) + */ +function cache_function($key, $ttl, $function) +{ + init_cache(); + $CI =& get_instance(); + if (! $content = $CI->cache->get($key)) { + $content = $function(); + $CI->cache->save($key, $content, $ttl); + } + return $content; +} + +/** + * Cache the result of a function call in the cache backend and in the memory of this process. + * @param key cache key to use + * @param ttl time to live for the cache entry + * @param function function to call + * @return return value of function (will be cached) + */ +function cache_function_full($key, $ttl, $function) { + $local_key = 'cache_function-'.$key; + if (static_storage($local_key) !== null) { + return static_storage($local_key); + } + $ret = cache_function($key, $ttl, $function); + static_storage($local_key, $ret); + return $ret; +} + +// Return mimetype of file +function mimetype($file) { + $fileinfo = new finfo(FILEINFO_MIME_TYPE); + + // XXX: Workaround for PHP#71434 https://bugs.php.net/bug.php?id=71434 + $old = error_reporting(); + error_reporting($old &~ E_NOTICE); + $mimetype = $fileinfo->file($file); + error_reporting($old); + + return $mimetype; +} + +function files_are_equal($a, $b) +{ + $chunk_size = 8*1024; + + // Check if filesize is different + if (filesize($a) !== filesize($b)) { + return false; + } + + // Check if content is different + $ah = fopen($a, 'rb'); + $bh = fopen($b, 'rb'); + + $result = true; + while (!feof($ah) && !feof($bh)) { + if (fread($ah, $chunk_size) !== fread($bh, $chunk_size)) { + $result = false; + break; + } + } + + fclose($ah); + fclose($bh); + + return $result; +} + +# Source: http://php.net/manual/en/function.ini-get.php#96996 +function return_bytes($size_str) +{ + switch (substr ($size_str, -1)) + { + case 'K': case 'k': return (int)$size_str * 1024; + case 'M': case 'm': return (int)$size_str * 1048576; + case 'G': case 'g': return (int)$size_str * 1073741824; + default: + if (strlen($size_str) === strlen(intval($size_str))) { + return (int)$size_str; + } + throw new \exceptions\ApiException('filebin-helper/invalid-input-unit', "Input has invalid unit"); + } +} + +function ensure_json_keys_contain_objects($data, $keys) { + foreach ($keys as $key) { + if (empty($data[$key])) { + $data[$key] = (object) array(); + } + } + return $data; +} + +function output_cli_usage() { + echo "php index.php <controller> <function> [arguments]\n"; + echo "\n"; + echo "Functions:\n"; + echo " file cron Cronjob\n"; + echo " file nuke_id <ID> Nukes all IDs sharing the same hash\n"; + echo " user cron Cronjob\n"; + echo " user add_user Add a user\n"; + echo " user delete_user Delete a user including all their data\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 " database entries without files and multipaste\n"; + echo " tarballs that are no longer needed\n"; + echo " file update_file_metadata Update filesize and mimetype in database\n"; +} + +# vim: set noet: diff --git a/application/libraries/Customautoloader.php b/application/libraries/Customautoloader.php new file mode 100644 index 000000000..eb14c5624 --- /dev/null +++ b/application/libraries/Customautoloader.php @@ -0,0 +1,42 @@ +<?php +/* + * Copyright 2014 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +// Original source: http://stackoverflow.com/a/9526005/953022 +class Customautoloader { + public function __construct() + { + spl_autoload_register(array($this, 'loader')); + } + + public function loader($className) + { + $namespaces = array( + 'Endroid\QrCode' => [ + ["path" => APPPATH."/third_party/QrCode/src/"], + ], + '' => [ + ["path" => APPPATH], + ["path" => APPPATH."/third_party/mockery/library/"] + ], + ); + + foreach ($namespaces as $namespace => $search_items) { + if ($namespace === '' || strpos($className, $namespace) === 0) { + foreach ($search_items as $search_item) { + $nameToLoad = str_replace($namespace, '', $className); + $path = $search_item['path'].str_replace('\\', DIRECTORY_SEPARATOR, $nameToLoad).'.php'; + if (file_exists($path)) { + require $path; + return; + } + } + } + } + } +} diff --git a/application/libraries/Ddownload/Ddownload.php b/application/libraries/Ddownload/Ddownload.php new file mode 100644 index 000000000..3a98d4154 --- /dev/null +++ b/application/libraries/Ddownload/Ddownload.php @@ -0,0 +1,34 @@ +<?php +/* + * Copyright 2013 Pierre Schmitz <pierre@archlinux.de> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +abstract class Ddownload_Driver extends CI_Driver { + + abstract public function serveFile($file, $filename, $type); +} + +class Ddownload extends CI_Driver_Library { + + protected $_adapter = null; + + protected $valid_drivers = array( + 'php', 'nginx', 'lighttpd' + ); + + function __construct() + { + $CI =& get_instance(); + + $this->_adapter = $CI->config->item('download_driver'); + } + + public function serveFile($file, $filename, $type) + { + $this->{$this->_adapter}->serveFile($file, $filename, $type); + } +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php b/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php new file mode 100644 index 000000000..fbdb04b02 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_lighttpd.php @@ -0,0 +1,26 @@ +<?php +/* + * Copyright 2013 Pierre Schmitz <pierre@archlinux.de> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_lighttpd extends Ddownload_Driver { + + public function serveFile($file, $filename, $type) + { + $CI =& get_instance(); + $upload_path = $CI->config->item('upload_path'); + + if (strpos($file, $upload_path) !== 0) { + throw new \exceptions\ApiException("libraries/ddownload/lighttpd/invalid-file-path", 'Invalid file path'); + } + + header('Content-disposition: inline; filename="'.$filename."\"\n"); + header('Content-Type: '.$type."\n"); + header('X-Sendfile: '.$file."\n"); + } + +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_nginx.php b/application/libraries/Ddownload/drivers/Ddownload_nginx.php new file mode 100644 index 000000000..58c7502a7 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_nginx.php @@ -0,0 +1,29 @@ +<?php +/* + * Copyright 2013 Pierre Schmitz <pierre@archlinux.de> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_nginx extends Ddownload_Driver { + + public function serveFile($file, $filename, $type) + { + $CI =& get_instance(); + $upload_path = $CI->config->item('upload_path'); + $download_location = $CI->config->item('download_nginx_location'); + + if (strpos($file, $upload_path) === 0) { + $file_path = substr($file, strlen($upload_path)); + } else { + throw new \exceptions\ApiException("libraries/ddownload/nginx/invalid-file-path", 'Invalid file path'); + } + + header('Content-disposition: inline; filename="'.$filename."\"\n"); + header('Content-Type: '.$type."\n"); + header('X-Accel-Redirect: '.$download_location.$file_path."\n"); + } + +} diff --git a/application/libraries/Ddownload/drivers/Ddownload_php.php b/application/libraries/Ddownload/drivers/Ddownload_php.php new file mode 100644 index 000000000..98af458c1 --- /dev/null +++ b/application/libraries/Ddownload/drivers/Ddownload_php.php @@ -0,0 +1,111 @@ +<?php +/* + * Copyright 2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Ddownload_php extends Ddownload_Driver { + + // Original source: http://www.phpfreaks.com/forums/index.php?topic=198274.msg895468#msg895468 + public function serveFile($file, $filename, $type) + { + $fp = fopen($file, 'r'); + + $size = filesize($file); // File size + $length = $size; // Content length + $start = 0; // Start byte + $end = $size - 1; // End byte + // Now that we've gotten so far without errors we send the accept range header + /* At the moment we only support single ranges. + * Multiple ranges requires some more work to ensure it works correctly + * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + * + * Multirange support annouces itself with: + * header('Accept-Ranges: bytes'); + * + * Multirange content must be sent with multipart/byteranges mediatype, + * (mediatype = mimetype) + * as well as a boundry header to indicate the various chunks of data. + */ + header("Accept-Ranges: 0-$length"); + // header('Accept-Ranges: bytes'); + // multipart/byteranges + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + if (isset($_SERVER['HTTP_RANGE'])) + { + $c_start = $start; + $c_end = $end; + // Extract the range string + list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); + // Make sure the client hasn't sent us a multibyte range + if (strpos($range, ',') !== false) + { + // (?) Shoud this be issued here, or should the first + // range be used? Or should the header be ignored and + // we output the whole content? + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + // If the range starts with an '-' we start from the beginning + // If not, we forward the file pointer + // And make sure to get the end byte if spesified + if ($range{0} == '-') + { + // The n-number of the last bytes is requested + $c_start = $size - substr($range, 1); + } + else + { + $range = explode('-', $range); + $c_start = $range[0]; + $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; + } + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $c_end = ($c_end > $end) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) + { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; // Calculate new content length + fseek($fp, $start); + header('HTTP/1.1 206 Partial Content'); + // Notify the client the byte range we'll be outputting + header("Content-Range: bytes $start-$end/$size"); + } + header("Content-Length: $length"); + header("Content-disposition: inline; filename=\"".$filename."\"\n"); + header("Content-Type: ".$type."\n"); + + // Start buffered download + $buffer = 1024 * 8; + while(!feof($fp) && ($p = ftell($fp)) <= $end) + { + if ($p + $buffer > $end) + { + // In case we're only outputtin a chunk, make sure we don't + // read past the length + $buffer = $end - $p + 1; + } + set_time_limit(0); // Reset time limit for big files + echo fread($fp, $buffer); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } + + fclose($fp); + } + +} diff --git a/application/libraries/Duser/Duser.php b/application/libraries/Duser/Duser.php new file mode 100644 index 000000000..0007fabd8 --- /dev/null +++ b/application/libraries/Duser/Duser.php @@ -0,0 +1,124 @@ +<?php +/* + * Copyright 2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +abstract class Duser_Driver extends CI_Driver { + + // List of optional functions that are implemented + // + // Possible values are: + // - can_register_new_users (only supported with the DB driver!) + // - can_reset_password (only supported with the DB driver!) + // - can_change_email (only supported with the DB driver!) + public $optional_functions = array(); + + /* + * The returned array should contain the following keys: + * - username string + * - userid INT > 0 + * + * @param username + * @param password + * @return mixed array on success, false on failure + */ + abstract public function login($username, $password); + + /* + * @param username + * @return boolean true is username exists, false otherwise + */ + public function username_exists($username) { + return null; + } + + /* + * @param userid + * @return string email address of the user + */ + public function get_email($userid) { + return null; + } +} + +class Duser extends CI_Driver_Library { + + protected $_adapter = null; + + protected $valid_drivers = array( + 'db', 'ldap', 'fluxbb' + ); + + function __construct() + { + $CI =& get_instance(); + + $this->_adapter = $CI->config->item("authentication_driver"); + } + + // require an optional function to be implemented + public function require_implemented($function) { + if (!$this->is_implemented($function)) { + throw new \exceptions\PublicApiException("libraries/duser/optional-function-not-implemented", "" + ."Optional function '".$function."' not implemented in user adapter '".$this->_adapter."'. " + ."Requested functionally unavailable."); + } + } + + // check if an optional function is implemented + public function is_implemented($function) { + if (in_array($function, $this->{$this->_adapter}->optional_functions)) { + return true; + } + + return false; + } + + public function login($username, $password) + { + $login_info = $this->{$this->_adapter}->login($username, $password); + if ($login_info === false) { + return false; + } + + $CI =& get_instance(); + + $CI->session->set_userdata(array( + 'logged_in' => true, + 'username' => $login_info["username"], + 'userid' => $login_info["userid"], + 'access_level' => 'full', + )); + + return true; + } + + public function username_exists($username) + { + if ($username === false) { + return false; + } + + return $this->{$this->_adapter}->username_exists($username); + } + + public function get_email($userid) + { + return $this->{$this->_adapter}->get_email($userid); + } + + public function test_login_credentials($username, $password) + { + $login_info = $this->{$this->_adapter}->login($username, $password); + + if (isset($login_info['username']) && $login_info['username'] === $username) { + return true; + } + + return false; + } +} diff --git a/application/libraries/Duser/drivers/Duser_db.php b/application/libraries/Duser/drivers/Duser_db.php new file mode 100644 index 000000000..062da9e54 --- /dev/null +++ b/application/libraries/Duser/drivers/Duser_db.php @@ -0,0 +1,79 @@ +<?php +/* + * Copyright 2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Duser_db extends Duser_Driver { + + /* FIXME: If you use this driver as a template, remove can_reset_password + * and can_register_new_users. These features require the DB driver and + * will NOT work with other drivers. + */ + public $optional_functions = array( + 'can_reset_password', + 'can_register_new_users', + 'can_change_email', + 'can_delete_account', + ); + + public function login($username, $password) + { + $CI =& get_instance(); + + $query = $CI->db->select('username, id, password') + ->from('users') + ->where('username', $username) + ->get()->row_array(); + + if (empty($query)) { + return false; + } + + if (password_verify($password, $query['password'])) { + $CI->muser->rehash_password($query['id'], $password, $query['password']); + return array( + "username" => $username, + "userid" => $query["id"] + ); + } else { + return false; + } + } + + public function username_exists($username) + { + $CI =& get_instance(); + + $query = $CI->db->select('id') + ->from('users') + ->where('username', $username) + ->get(); + + if ($query->num_rows() > 0) { + return true; + } else { + return false; + } + } + + public function get_email($userid) + { + $CI =& get_instance(); + + $query = $CI->db->select('email') + ->from('users') + ->where('id', $userid) + ->get()->row_array(); + + if (empty($query)) { + throw new \exceptions\ApiException("libraries/duser/db/get_email-failed", "Failed to get email address from db"); + } + + return $query["email"]; + } + +} diff --git a/application/libraries/Duser/drivers/Duser_fluxbb.php b/application/libraries/Duser/drivers/Duser_fluxbb.php new file mode 100644 index 000000000..1790e830b --- /dev/null +++ b/application/libraries/Duser/drivers/Duser_fluxbb.php @@ -0,0 +1,53 @@ +<?php +/* + * Copyright 2013 Pierre Schmitz <pierre@archlinux.de> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Duser_fluxbb extends Duser_Driver { + + private $CI = null; + private $config = array(); + + function __construct() + { + $this->CI =& get_instance(); + $this->config = $this->CI->config->item('auth_fluxbb'); + } + + public function login($username, $password) + { + $query = $this->CI->db->query(' + SELECT username, id + FROM '.$this->config['database'].'.users + WHERE username = ? AND password = ? + ', array($username, sha1($password)))->row_array(); + + if (!empty($query)) { + return array( + 'username' => $query['username'], + 'userid' => $query['id'] + ); + } else { + return false; + } + } + + public function username_exists($username) + { + $query = $this->CI->db->query(' + SELECT id + FROM '.$this->config['database'].'.users + WHERE username = ? + ', array($username)); + + if ($query->num_rows() > 0) { + return true; + } else { + return false; + } + } +} diff --git a/application/libraries/Duser/drivers/Duser_ldap.php b/application/libraries/Duser/drivers/Duser_ldap.php new file mode 100644 index 000000000..9481397d0 --- /dev/null +++ b/application/libraries/Duser/drivers/Duser_ldap.php @@ -0,0 +1,79 @@ +<?php +/* + * Copyright 2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * Contributions by Hannes Rist + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ +class Duser_ldap extends Duser_Driver { + // none supported + public $optional_functions = array(); + + // Original source: http://code.activestate.com/recipes/101525-ldap-authentication/ + public function login($username, $password) { + $CI =& get_instance(); + + $config = $CI->config->item("auth_ldap"); + + if ($username == "" || $password == "") { + return false; + } + + $ds = ldap_connect($config['host'],$config['port']); + if ($ds === false) { + return false; + } + + if (isset($config['bind_rdn']) && isset($config['bind_password'])) { + ldap_bind($ds, $config['bind_rdn'], $config['bind_password']); + } + + if (isset($config['filter'])) { + $filter = sprintf($config['filter'], $username); + } else { + $filter = $config["username_field"].'='.$username; + } + + + switch ($config["scope"]) { + case "base": + $r = ldap_read($ds, $config['basedn'], $filter); + break; + case "one": + $r = ldap_list($ds, $config['basedn'], $filter); + break; + case "subtree": + $r = ldap_search($ds, $config['basedn'], $filter); + break; + default: + throw new \exceptions\ApiException("libraries/duser/ldap/invalid-ldap-scope", "Invalid LDAP scope"); + } + if ($r === false) { + return false; + } + + foreach ($config["options"] as $key => $value) { + if (ldap_set_option($ds, $key, $value) === false) { + return false; + } + } + + $result = ldap_get_entries($ds, $r); + if ($result === false || !isset($result[0])) { + return false; + } + + // ignore errors from ldap_bind as it will throw an error if the password is incorrect + if (@ldap_bind($ds, $result[0]['dn'], $password)) { + ldap_unbind($ds); + return array( + "username" => $result[0][$config["username_field"]][0], + "userid" => $result[0][$config["userid_field"]][0] + ); + } + + return false; + } +} diff --git a/application/libraries/ExceptionHandler.php b/application/libraries/ExceptionHandler.php new file mode 100644 index 000000000..94a1d35c0 --- /dev/null +++ b/application/libraries/ExceptionHandler.php @@ -0,0 +1,154 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class ExceptionHandler { + static function setup() + { + set_error_handler(array("\libraries\ExceptionHandler", "error_handler")); + set_exception_handler(array("\libraries\ExceptionHandler", 'exception_handler')); + register_shutdown_function(array("\libraries\ExceptionHandler", "check_for_fatal")); + assert_options(ASSERT_ACTIVE, true); + assert_options(ASSERT_CALLBACK, array("\libraries\ExceptionHandler", '_assert_failure')); + } + + static function error_handler($errno, $errstr, $errfile, $errline) + { + if (!(error_reporting() & $errno)) { + // This error code is not included in error_reporting + return; + } + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); + } + + // Source: https://gist.github.com/abtris/1437966 + static private function getExceptionTraceAsString($exception) { + $rtn = ""; + $count = 0; + foreach ($exception->getTrace() as $frame) { + $args = ""; + if (isset($frame['args'])) { + $args = array(); + foreach ($frame['args'] as $arg) { + if (is_string($arg)) { + $args[] = "'" . $arg . "'"; + } elseif (is_array($arg)) { + $args[] = "Array"; + } elseif (is_null($arg)) { + $args[] = 'NULL'; + } elseif (is_bool($arg)) { + $args[] = ($arg) ? "true" : "false"; + } elseif (is_object($arg)) { + $args[] = get_class($arg); + } elseif (is_resource($arg)) { + $args[] = get_resource_type($arg); + } else { + $args[] = $arg; + } + } + $args = join(", ", $args); + } + $rtn .= sprintf( "#%s %s(%s): %s(%s)\n", + $count, + isset($frame['file']) ? $frame['file'] : 'unknown file', + isset($frame['line']) ? $frame['line'] : 'unknown line', + (isset($frame['class'])) ? $frame['class'].$frame['type'].$frame['function'] : $frame['function'], + $args ); + $count++; + } + return $rtn; + } + + static public function log_exception($ex) + { + $exceptions = array($ex); + while ($ex->getPrevious() !== null) { + $ex = $ex->getPrevious(); + $exceptions[] = $ex; + } + + foreach ($exceptions as $key => $e) { + $message = sprintf("Exception %d/%d '%s' with message '%s' in %s:%d\n", $key+1, count($exceptions), get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()); + if (method_exists($e, "get_error_id")) { + $message .= 'Error ID: '.$e->get_error_id()."\n"; + } + if (method_exists($e, "get_data") && $e->get_data() !== NULL) { + $message .= 'Data: '.var_export($e->get_data(), true)."\n"; + } + $message .= "Backtrace:\n".self::getExceptionTraceAsString($e)."\n"; + error_log($message); + } + } + + // The actual exception handler + static public function exception_handler($ex) + { + self::log_exception($ex); + + $display_errors = in_array(strtolower(ini_get('display_errors')), array('1', 'on', 'true', 'stdout')); + if (php_sapi_name() === 'cli' OR defined('STDIN')) { + $display_errors = true; + } + + $GLOBALS["is_error_page"] = true; + $heading = "Internal Server Error"; + $message = "<p>An unhandled error occured.</p>\n"; + + if ($display_errors) { + $exceptions = array($ex); + while ($ex->getPrevious() !== null) { + $ex = $ex->getPrevious(); + $exceptions[] = $ex; + } + + foreach ($exceptions as $key => $e) { + $backtrace = self::getExceptionTraceAsString($e); + $message .= '<div>'; + $message .= '<b>Exception '.($key+1).' of '.count($exceptions).'</b><br>'; + $message .= '<b>Fatal error</b>: Uncaught exception '.htmlspecialchars(get_class($e)).'<br>'; + $message .= '<b>Message</b>: '.htmlspecialchars($e->getMessage()).'<br>'; + if (method_exists($e, "get_error_id")) { + $message .= '<b>Error ID</b>: '.htmlspecialchars($e->get_error_id()).'<br>'; + } + if (method_exists($e, "get_data") && $e->get_data() !== NULL) { + $message .= '<b>Data</b>: <pre>'.htmlspecialchars(var_export($e->get_data(), true)).'</pre><br>'; + } + $message .= '<b>Backtrace:</b><br>'; + $message .= '<pre>'.htmlspecialchars(str_replace(FCPATH, "./", $backtrace)).'</pre>'; + $message .= 'thrown in <b>'.htmlspecialchars($e->getFile()).'</b> on line <b>'.htmlspecialchars($e->getLine()).'</b><br>'; + $message .= '</div>'; + } + } else { + $message .="<p>More information can be found in the php error log or by enabling display_errors.</p>"; + } + + $message = "$message"; + include VIEWPATH."/errors/html/error_general.php"; + } + + /** + * Checks for a fatal error, work around for set_error_handler not working on fatal errors. + */ + static public function check_for_fatal() + { + $error = error_get_last(); + if ($error["type"] == E_ERROR) { + self::exception_handler(new \ErrorException( + $error["message"], 0, $error["type"], $error["file"], $error["line"])); + } + } + + static public function assert_failure($file, $line, $expr, $message = "") + { + self::exception_handler(new Exception("assert($expr): Assertion failed in $file at line $line".($message != "" ? " with message: '$message'" : ""))); + exit(1); + } + +} diff --git a/application/libraries/Exif.php b/application/libraries/Exif.php new file mode 100644 index 000000000..27dd11a65 --- /dev/null +++ b/application/libraries/Exif.php @@ -0,0 +1,37 @@ +<?php +/* + * Copyright 2014-2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class Exif { + static public function get_exif($file) + { + // TODO: support more types (identify or exiftool? might be slow :( ) + try { + $type = getimagesize($file)[2]; + } catch (\ErrorException $e) { + return false; + } + switch ($type) { + case IMAGETYPE_JPEG: + getimagesize($file, $info); + if (isset($info["APP1"]) && strpos($info["APP1"], "http://ns.adobe.com/xap/1.0/") === 0) { + // ignore XMP data which makes exif_read_data throw a warning + // http://stackoverflow.com/a/8864064 + return false; + } + return @exif_read_data($file); + break; + default: + } + + return false; + } + +} diff --git a/application/libraries/Image.php b/application/libraries/Image.php new file mode 100644 index 000000000..18814e181 --- /dev/null +++ b/application/libraries/Image.php @@ -0,0 +1,236 @@ +<?php +/* + * Copyright 2014-2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries\Image; + +interface ImageDriver { + /** + * Replace the current image by reading in a file + * @param file file to read + */ + public function read($file); + + /** + * Return the current image rendered to a specific format. Passing null as + * the target_type returns the image in the format of the source image + * (loaded with read()). + * + * @param target_type one of IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG or null + * @return binary image data + */ + public function get($target_type); + + /** + * Resize the image. + * @param width + * @param height + */ + public function resize($width, $height); + + /** + * Crop the image to the area defined by x, y, width and height. + * + * @param x starting coordinate + * @param y starting coordinate + * @param width width of the area + * @param height height of the area + */ + public function crop($x, $y, $width, $height); + + /** + * Crop/resize the image to fit into the desired size. This also rotates + * the image if the source image had an EXIF orientation tag. + * + * @param width width of the resulting image + * @param height height of the resulting image + */ + public function makeThumb($target_width, $target_height); + + /** + * Rotate the image according to the sources EXIF orientation tag if any. + */ + public function apply_exif_orientation(); + + /** + * Mirror the image along the x axis. + */ + public function mirror(); + + /** + * Return priority for this driver. Higher number means a higher priority. + * + * @param mimetype mimetype of the file + * @return > 0 if supported, < 0 if the mimetype can't be handled + */ + public static function get_priority($mimetype); +} + +namespace libraries; + +/** + * This class deals with a single image and provides useful operations that + * operate on this image. + */ +class Image { + private $driver; + + private static function get_image_drivers() + { + return array( + "libraries\Image\Drivers\GD", + "libraries\Image\Drivers\imagemagick", + ); + } + + /** + * Create a new object and load the contents of file. + * @param file file to read + */ + public function __construct($file) + { + $this->read($file); + } + + /** + * Get the best driver supporting $mimetype. + * + * @param drivers list of driver classes + * @param mimetype mimetype the driver should support + * @return driver from $drivers or NULL if no driver supports the type + */ + private static function best_driver($drivers, $mimetype) + { + $best = 0; + $best_driver = null; + foreach ($drivers as $driver) { + $current = $driver::get_priority($mimetype); + if ($current > $best && $current > 0) { + $best_driver = $driver; + $best = $current; + } + } + + if ($best_driver === NULL) { + throw new \exceptions\PublicApiException("libraries/Image/unsupported-image-type", "Unsupported image type"); + } + + return $best_driver; + } + + /** + * Check if a mimetype is supported by the image library. + * + * @param mimetype + * @return true if supported, false otherwise + */ + public static function type_supported($mimetype) + { + static $cache = array(); + if (isset($cache[$mimetype])) { + return $cache[$mimetype]; + } + + try { + $driver = self::best_driver(self::get_image_drivers(), $mimetype); + $cache[$mimetype] = true; + } catch (\exceptions\ApiException $e) { + if ($e->get_error_id() == "libraries/Image/unsupported-image-type") { + $cache[$mimetype] = false; + } else { + throw $e; + } + } + return $cache[$mimetype]; + } + + /** + * Replace the current image by reading in a file + * @param file file to read + */ + public function read($file) + { + $mimetype = mimetype($file); + $driver = self::best_driver(self::get_image_drivers(), $mimetype); + $this->driver = new $driver($file); + } + + /** + * Return the current image rendered to a specific format. Passing null as + * the target_type returns the image in the format of the source image + * (loaded with read()). + * + * @param target_type one of IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG or null + * @return binary image data + */ + public function get($target_type = null) + { + return $this->driver->get($target_type); + } + + /** + * Resize the image. + * @param width + * @param height + */ + public function resize($width, $height) + { + return $this->driver->resize($width, $height); + } + + /** + * Crop the image to the area defined by x, y, width and height. + * + * @param x starting coordinate + * @param y starting coordinate + * @param width width of the area + * @param height height of the area + */ + public function crop($x, $y, $width, $height) + { + return $this->driver->crop($x, $y, $width, $height); + } + + /** + * Crop/resize the image to fit into the desired size. This also rotates + * the image if the source image had an EXIF orientation tag. + * + * @param width width of the resulting image + * @param height height of the resulting image + */ + public function makeThumb($target_width, $target_height) + { + return $this->driver->makeThumb($target_width, $target_height); + } + + static public function get_exif_orientation($file) + { + $exif = \libraries\Exif::get_exif($file); + if (isset($exif["Orientation"])) { + return $exif["Orientation"]; + } + return 0; + } + + /** + * Rotate the image according to the sources EXIF orientation tag if any. + */ + public function apply_exif_orientation() + { + return $this->driver->apply_exif_orientation(); + } + + /** + * Mirror the image along the x axis. + */ + public function mirror() + { + return $this->driver->mirror(); + } + +} diff --git a/application/libraries/Image/Drivers/GD.php b/application/libraries/Image/Drivers/GD.php new file mode 100644 index 000000000..e2e0a99be --- /dev/null +++ b/application/libraries/Image/Drivers/GD.php @@ -0,0 +1,217 @@ +<?php +/* + * Copyright 2014-2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries\Image\Drivers; + +class GD implements \libraries\Image\ImageDriver { + private $img; + private $source_type; + private $exif; + + public static function get_priority($mimetype) + { + switch($mimetype) { + case "image/jpeg": + case "image/png": + case "image/gif": + return 1000; + break; + default: + return -1; + break; + } + } + + /** + * Create a new object and load the contents of file. + * @param file file to read + */ + public function __construct($file) + { + $this->read($file); + } + + public function read($file) + { + $img = imagecreatefromstring(file_get_contents($file)); + if ($img === false) { + throw new \exceptions\ApiException("libraries/Image/unsupported-image-type", "Unsupported image type"); + } + $this->set_img_object($img); + $this->fix_alpha(); + + $this->source_type = getimagesize($file)[2]; + $this->exif = \libraries\Exif::get_exif($file); + } + + public function get($target_type = null) + { + if ($target_type === null) { + $target_type = $this->source_type; + } + + ob_start(); + switch ($target_type) { + case IMAGETYPE_GIF: + $ret = imagegif($this->img); + break; + case IMAGETYPE_JPEG: + $ret = imagejpeg($this->img); + break; + case IMAGETYPE_PNG: + $ret = imagepng($this->img); + break; + default: + assert(0); + } + $result = ob_get_clean(); + + if (!$ret || $result === false) { + throw new \exceptions\ApiException("libraries/Image/thumbnail-creation-failed", "Failed to create thumbnail"); + } + + return $result; + } + + public function resize($width, $height) + { + $temp_gdim = imagecreatetruecolor($width, $height); + $this->fix_alpha(); + imagecopyresampled( + $temp_gdim, + $this->img, + 0, 0, + 0, 0, + $width, $height, + imagesx($this->img), imagesy($this->img) + ); + + $this->set_img_object($temp_gdim); + } + + public function crop($x, $y, $width, $height) + { + $thumb = imagecreatetruecolor($width, $height); + $this->fix_alpha(); + imagecopy( + $thumb, + $this->img, + 0, 0, + $x, $y, + $width, $height + ); + + $this->set_img_object($thumb); + } + + // Source: http://salman-w.blogspot.co.at/2009/04/crop-to-fit-image-using-aspphp.html + public function makeThumb($target_width, $target_height) + { + $source_aspect_ratio = imagesx($this->img) / imagesy($this->img); + $desired_aspect_ratio = $target_width / $target_height; + + if ($source_aspect_ratio > $desired_aspect_ratio) { + // Triggered when source image is wider + $temp_height = $target_height; + $temp_width = round(($target_height * $source_aspect_ratio)); + } else { + // Triggered otherwise (i.e. source image is similar or taller) + $temp_width = $target_width; + $temp_height = round(($target_width / $source_aspect_ratio)); + } + + $this->resize($temp_width, $temp_height); + + $x0 = ($temp_width - $target_width) / 2; + $y0 = ($temp_height - $target_height) / 2; + $this->crop($x0, $y0, $target_width, $target_height); + + $this->apply_exif_orientation(); + } + + public function apply_exif_orientation() + { + if (isset($this->exif['Orientation'])) { + $mirror = false; + $deg = 0; + + switch ($this->exif['Orientation']) { + case 2: + $mirror = true; + break; + case 3: + $deg = 180; + break; + case 4: + $deg = 180; + $mirror = true; + break; + case 5: + $deg = 270; + $mirror = true; + break; + case 6: + $deg = 270; + break; + case 7: + $deg = 90; + $mirror = true; + break; + case 8: + $deg = 90; + break; + } + + if ($deg) { + $this->set_img_object(imagerotate($this->img, $deg, 0)); + } + + if ($mirror) { + $this->mirror(); + } + } + } + + public function mirror() + { + $width = imagesx($this->img); + $height = imagesy($this->img); + + $src_x = $width -1; + $src_y = 0; + $src_width = -$width; + $src_height = $height; + + $imgdest = imagecreatetruecolor($width, $height); + imagealphablending($imgdest,false); + imagesavealpha($imgdest,true); + + imagecopyresampled($imgdest, $this->img, 0, 0, $src_x, $src_y, $width, $height, $src_width, $src_height); + $this->set_img_object($imgdest); + } + + private function set_img_object($new) + { + assert($new !== false); + + $old = $this->img; + $this->img = $new; + + if ($old != null) { + imagedestroy($old); + } + } + + private function fix_alpha() + { + imagealphablending($this->img,false); + imagesavealpha($this->img,true); + } + +} diff --git a/application/libraries/Image/Drivers/imagemagick.php b/application/libraries/Image/Drivers/imagemagick.php new file mode 100644 index 000000000..33e62ffe4 --- /dev/null +++ b/application/libraries/Image/Drivers/imagemagick.php @@ -0,0 +1,120 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries\Image\Drivers; + +class imagemagick implements \libraries\Image\ImageDriver { + private $source_file; + private $arguments = array(); + + public static function get_priority($mimetype) + { + $mimetype = $mimetype; + $base = explode("/", $mimetype)[0]; + + if ($base == "image") { + return 100; + } + + return -1; + } + + /** + * Create a new object and load the contents of file. + * @param file file to read + */ + public function __construct($file) + { + $this->read($file); + } + + public function read($file) + { + if (!file_exists($file)) { + throw new \exceptions\ApiException("libraries/Image/drivers/imagemagick/missing-file", "Source file doesn't exist"); + } + + $this->source_file = $file; + $this->arguments = array(); + } + + public function get($target_type = null) + { + if ($target_type === null) { + return file_get_contents($this->source_file); + } + + $command = array("convert"); + $command = array_merge($command, $this->arguments); + $command[] = $this->source_file."[0]"; + + switch ($target_type) { + case IMAGETYPE_GIF: + $command[] = "gif:-"; + break; + case IMAGETYPE_JPEG: + $command[] = "jpeg:-"; + break; + case IMAGETYPE_PNG: + $command[] = "png:-"; + break; + default: + assert(0); + } + + try { + $ret = (new \libraries\ProcRunner($command))->forbid_nonzero()->exec(); + } catch (\exceptions\ApiException $e) { + throw new \exceptions\ApiException("libraries/Image/thumbnail-creation-failed", "Failed to create thumbnail", null, $e); + } + + return $ret["stdout"]; + } + + public function resize($width, $height) + { + $this->arguments[] = "-resize"; + $this->arguments[] = "${width}x${height}"; + } + + public function crop($x, $y, $width, $height) + { + $this->arguments[] = "+repage"; + $this->arguments[] = "-crop"; + $this->arguments[] = "${width}x${height}+${x}+${y}"; + $this->arguments[] = "+repage"; + } + + // Source: http://salman-w.blogspot.co.at/2009/04/crop-to-fit-image-using-aspphp.html + public function makeThumb($target_width, $target_height) + { + assert(is_int($target_width)); + assert(is_int($target_height)); + + $this->apply_exif_orientation(); + + $this->arguments[] = "-thumbnail"; + $this->arguments[] = "${target_width}x${target_height}^"; + $this->arguments[] = "-gravity"; + $this->arguments[] = "center"; + $this->arguments[] = "-extent"; + $this->arguments[] = "${target_width}x${target_height}^"; + } + + public function apply_exif_orientation() + { + $this->arguments[] = "-auto-orient"; + } + + public function mirror() + { + $this->arguments[] = "-flip"; + } + +} diff --git a/application/libraries/MY_Session.php b/application/libraries/MY_Session.php new file mode 100644 index 000000000..0443bca31 --- /dev/null +++ b/application/libraries/MY_Session.php @@ -0,0 +1,38 @@ +<?php +/* + * Copyright 2013 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class MY_Session extends CI_Session { + private $memory_only = false; + + public function __construct() { + $CI =& get_instance(); + $CI->load->helper("filebin"); + + /* Clients using API keys do not need a persistent session since API keys + * should be sent with each request. This reduces database queries and + * prevents us from sending useless cookies. + */ + if (!stateful_client()) { + $this->memory_only = true; + $CI->config->set_item("sess_use_database", false); + } + + parent::__construct(); + } + + public function _set_cookie($cookie_data = NULL) + { + if ($this->memory_only) { + return; + } + + parent::_set_cookie($cookie_data); + + } +} diff --git a/application/libraries/Output_cache.php b/application/libraries/Output_cache.php new file mode 100644 index 000000000..1d8339887 --- /dev/null +++ b/application/libraries/Output_cache.php @@ -0,0 +1,82 @@ +<?php +/* + * Copyright 2014,2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class Output_cache { + private $output_cache = array(); + + /** + * Combine multiple objects for the same view into one + * @param data data to pass to the view + * @param view view path + */ + public function add_merge($data, $view) + { + assert($view !== NULL); + + // combine multiple objects for the same view into one + $count = count($this->output_cache); + if ($count > 0 && $this->output_cache[$count - 1]["view"] === $view) { + $this->output_cache[$count - 1]["data"] = array_merge_recursive($this->output_cache[$count - 1]["data"], $data); + } else { + $this->add($data, $view); + } + } + + /** + * Add some data that will be output directly if view is NULL or passed + * to the view otherweise. + * + * @param data data to pass to view or output + * @param view view path or NULL + */ + public function add($data, $view = NULL) + { + $this->output_cache[] = array( + "view" => $view, + "data" => $data, + ); + } + + /** + * Add a function that will be excuted when render() is called. + * This function is supposed to use render_now() to output data. + * + * @param data_function + */ + public function add_function($data_function) + { + $this->output_cache[] = array( + "view" => NULL, + "data_function" => $data_function, + ); + } + + public function render_now($data, $view = NULL) + { + if ($view !== NULL) { + echo get_instance()->load->view($view, $data, true); + } else { + echo $data; + } + } + + public function render() + { + while ($output = array_shift($this->output_cache)) { + if (isset($output["data_function"])) { + $output["data_function"](); + } else { + $data = $output["data"]; + $this->render_now($data, $output["view"]); + } + } + } +} diff --git a/application/libraries/ProcRunner.php b/application/libraries/ProcRunner.php new file mode 100644 index 000000000..6aaaa1f20 --- /dev/null +++ b/application/libraries/ProcRunner.php @@ -0,0 +1,129 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class ProcRunner { + private $cmd; + private $input = NULL; + private $forbid_nonzero = false; + private $forbid_stderr = false; + + /** + * This function automatically escapes all parameters before executing the command. + * + * @param cmd array with the command and it's arguments + */ + function __construct($cmd) + { + assert(is_array($cmd)); + $this->cmd = implode(" ", array_map('escapeshellarg', $cmd)); + } + + /** + * Set stdin. You will have to set this to NULL if you call exec() a second + * time and don't want stdin to be sent again + * + * @param input string to send via stdin + */ + function input($input) + { + $this->input = $input; + return $this; + } + + function forbid_nonzero() + { + $this->forbid_nonzero = true; + return $this; + } + + + function forbid_stderr() + { + $this->forbid_stderr = true; + return $this; + } + + /** + * Run the command. + * + * @return array with keys return_code, stdout, stderr + */ + function exec() + { + $descriptorspec = array( + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w'), + ); + + if ($this->input !== NULL) { + $descriptorspec[0] = array('pipe', 'r'); + } + + $proc = proc_open($this->cmd, $descriptorspec, $pipes); + + if ($proc === false) { + throw new \exceptions\ApiException('procrunner/proc_open-failed', + 'Failed to run process', + array($this->cmd, $this->input) + ); + } + + if ($this->input !== NULL) { + fwrite($pipes[0], $this->input); + fclose($pipes[0]); + } + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + assert($stdout !== false); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + assert($stderr !== false); + + $return_code = proc_close($proc); + + $ret = array( + "return_code" => $return_code, + "stdout" => $stdout, + "stderr" => $stderr, + ); + + if ($this->forbid_nonzero && $return_code !== 0) { + throw new \exceptions\ApiException('procrunner/non-zero-exit', + 'Process exited with a non-zero status', + array($this->cmd, $this->input, $ret) + ); + } + + if ($this->forbid_stderr && $stderr !== "") { + throw new \exceptions\ApiException('procrunner/stderr', + 'Output on stderr not allowed but received', + array($this->cmd, $this->input, $ret) + ); + } + + return $ret; + } + + /** + * Run the command and enable some sanity checks such as empty stderr and + * zero exit status. Might enable more in the future. + * + * @See exec + */ + function execSafe() + { + $this->forbid_stderr(); + $this->forbid_nonzero(); + return $this->exec(); + } +} diff --git a/application/libraries/Pygments.php b/application/libraries/Pygments.php new file mode 100644 index 000000000..c8e1f8028 --- /dev/null +++ b/application/libraries/Pygments.php @@ -0,0 +1,249 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class Pygments { + private $file; + private $mimetype; + private $filename; + + public function __construct($file, $mimetype, $filename) { + $this->file = $file; + $this->mimetype = $mimetype; + $this->filename = $filename; + } + + private static function get_pygments_info() { + return cache_function_full('pygments_info-v2', 1800, function() { + $r = (new \libraries\ProcRunner(array(FCPATH."scripts/get_lexer_list.py")))->execSafe(); + + $ret = json_decode($r["stdout"], true); + if ($ret === NULL) { + throw new \exceptions\ApiException('pygments/json-failed', "Failed to decode JSON", $r); + } + + return $ret; + }); + } + + public static function get_lexers() { + return cache_function('lexers-v2', 1800, function() { + $last_desc = ""; + + foreach (self::get_pygments_info() as $lexer) { + $desc = $lexer['fullname']; + $name = $lexer['names'][0]; + if ($desc == $last_desc) { + continue; + } + $last_desc = $desc; + $lexers[$name] = $desc; + } + $lexers["text"] = "Plain text"; + return $lexers; + }); + } + + // Allow certain types to be highlight without doing it automatically + public function should_highlight() + { + $typearray = array( + 'image/svg+xml', + ); + if (in_array($this->mimetype, $typearray)) return false; + + if ($this->mime2lexer($this->mimetype)) return true; + + return false; + } + + public function can_highlight() + { + if ($this->mime2lexer($this->mimetype)) return true; + + return false; + } + + // Return the lexer that should be used for highlighting + public function autodetect_lexer() + { + if (!$this->should_highlight()) { + return false; + } + + $lexer = $this->mime2lexer($this->mimetype); + + // filename lexers overwrite mime type mappings + $filename_lexer = $this->filename2lexer(); + if ($filename_lexer) { + return $filename_lexer; + } + + return $lexer; + } + + // Map MIME types to lexers needed for highlighting + private function mime2lexer() + { + $typearray = array( + 'application/javascript' => 'javascript', + 'application/mbox' => 'text', + 'application/postscript' => 'postscript', + 'application/smil' => 'ocaml', + 'application/x-applix-spreadsheet' => 'actionscript', + 'application/x-awk' => 'awk', + 'application/x-desktop' => 'text', + 'application/x-fluid' => 'text', + 'application/x-genesis-rom' => 'text', + 'application/x-java' => 'java', + 'application/x-m4' => 'text', + 'application/xml-dtd' => "xml", + 'application/xml' => 'xml', + 'application/x-perl' => 'perl', + 'application/x-php' => 'php', + 'application/x-ruby' => 'ruby', + 'application/x-shellscript' => 'bash', + 'application/xslt+xml' => "xml", + 'application/x-x509-ca-cert' => 'text', + 'message/rfc822' => 'text', + 'text/css' => 'css', + 'text/html' => 'xml', + 'text/plain-ascii' => 'ascii', + 'text/plain' => 'text', + 'text/troff' => 'groff', + 'text/x-asm' => 'nasm', + 'text/x-awk' => 'awk', + 'text/x-c' => 'c', + 'text/x-c++' => 'cpp', + 'text/x-c++hdr' => 'c', + 'text/x-chdr' => 'c', + 'text/x-csrc' => 'c', + 'text/x-c++src' => 'cpp', + 'text/x-diff' => 'diff', + 'text/x-gawk' => 'awk', + 'text/x-haskell' => 'haskell', + 'text/x-java' => 'java', + 'text/x-lisp' => 'cl', + 'text/x-literate-haskell' => 'haskell', + 'text/x-lua' => 'lua', + 'text/x-makefile' => 'make', + 'text/x-ocaml' => 'ocaml', + 'text/x-patch' => 'diff', + 'text/x-perl' => 'perl', + 'text/x-php' => 'php', + 'text/x-python' => 'python', + 'text/x-ruby' => 'ruby', + 'text/x-scheme' => 'scheme', + 'text/x-shellscript' => 'bash', + 'text/x-subviewer' => 'bash', + 'text/x-tcl' => 'tcl', + 'text/x-tex' => 'tex', + ); + if (array_key_exists($this->mimetype, $typearray)) return $typearray[$this->mimetype]; + + // fall back to pygments own list if not found in our list + foreach (self::get_pygments_info() as $lexer) { + if (isset($lexer['mimetypes'][$this->mimetype])) { + return $lexer['names'][0]; + } + } + + if (strpos($this->mimetype, 'text/') === 0) return 'text'; + + # default + return false; + } + + // Map special filenames to lexers + private function filename2lexer() + { + $namearray = array( + 'asciinema.json' => 'asciinema', + 'PKGBUILD' => 'bash', + '.vimrc' => 'vim' + ); + if (array_key_exists($this->filename, $namearray)) return $namearray[$this->filename]; + + $longextarray = array( + '.asciinema.json' => 'asciinema', + ); + foreach ($longextarray as $key => $lexer) { + if (substr($this->filename, -strlen($key)) === $key) { + return $lexer; + } + } + + if (strpos($this->filename, ".") !== false) { + $extension = substr($this->filename, strrpos($this->filename, ".") + 1); + if ($extension === false) { + return false; + } + + $extensionarray = array( + 'awk' => 'awk', + 'cast' => 'asciinema', + 'c' => 'c', + 'coffee' => 'coffee-script', + 'cpp' => 'cpp', + 'diff' => 'diff', + 'go' => 'go', + 'haml' => 'haml', + 'h' => 'c', + 'hs' => 'haskell', + 'html' => 'xml', + 'java' => 'java', + 'js' => 'js', + 'lhs' => 'lhs', + 'lua' => 'lua', + 'mli' => 'ocaml', + 'mll' => 'ocaml', + 'ml' => 'ocaml', + 'mly' => 'ocaml', + 'mysql' => 'mysql', + 'patch' => 'diff', + 'pgsql' => 'postgresql', + 'php' => 'php', + 'pl' => 'perl', + 'plpgsql' => 'plpgsql', + 'pm' => 'perl', + 'postgresql' => 'postgresql', + 'pp' => 'puppet', + 'py' => 'python', + 'rb' => 'ruby', + 'rs' => 'rust', + 's' => 'asm', + 'sh' => 'bash', + 'sql' => 'sql', + 'tcl' => 'tcl', + 'tex' => 'tex', + 'yml' => 'yaml', + ); + if (array_key_exists($extension, $extensionarray)) return $extensionarray[$extension]; + } + + return false; + } + + // Handle lexer aliases + public function resolve_lexer_alias($alias) + { + if ($alias === false) return false; + $aliasarray = array( + 'py' => 'python', + 'sh' => 'bash', + 's' => 'asm', + 'pl' => 'perl' + ); + if (array_key_exists($alias, $aliasarray)) return $aliasarray[$alias]; + + return $alias; + } + +} diff --git a/application/libraries/Tempfile.php b/application/libraries/Tempfile.php new file mode 100644 index 000000000..f42efbfdf --- /dev/null +++ b/application/libraries/Tempfile.php @@ -0,0 +1,31 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class Tempfile { + private $file; + + public function __construct() + { + $this->file = tempnam(sys_get_temp_dir(), "tempfile"); + } + + public function __destruct() + { + if (file_exists($this->file)) { + unlink($this->file); + } + } + + public function get_file() + { + return $this->file; + } +} diff --git a/application/migrations/001_add_files.php b/application/migrations/001_add_files.php new file mode 100644 index 000000000..7b1398e1c --- /dev/null +++ b/application/migrations/001_add_files.php @@ -0,0 +1,45 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_files extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE IF NOT EXISTS "'.$prefix.'files" ( + "hash" varchar(32) NOT NULL, + "id" varchar(6) NOT NULL, + "filename" varchar(256) NOT NULL, + "password" varchar(40) DEFAULT NULL, + "date" integer NOT NULL, + "mimetype" varchar(255) NOT NULL, + PRIMARY KEY ("id") + ); + CREATE INDEX "'.$prefix.'files_date_idx" ON '.$prefix.'files ("date"); + CREATE INDEX "'.$prefix.'files_hash_id_idx" ON '.$prefix.'files ("hash", "id"); + '); + } else { + $this->db->query(' + CREATE TABLE IF NOT EXISTS `'.$prefix.'files` ( + `hash` varchar(32) CHARACTER SET ascii NOT NULL, + `id` varchar(6) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + `filename` varchar(256) COLLATE utf8_bin NOT NULL, + `password` varchar(40) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL, + `date` int(11) unsigned NOT NULL, + `mimetype` varchar(255) CHARACTER SET ascii NOT NULL, + PRIMARY KEY (`id`), + KEY `date` (`date`), + KEY `hash` (`hash`,`id`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_bin; + '); + } + } + + public function down() + { + $this->dbforge->drop_table('files'); + } +} diff --git a/application/migrations/002_add_users.php b/application/migrations/002_add_users.php new file mode 100644 index 000000000..454618e48 --- /dev/null +++ b/application/migrations/002_add_users.php @@ -0,0 +1,79 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_users extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE IF NOT EXISTS "'.$prefix.'users" ( + "id" serial PRIMARY KEY, + "username" character varying(32) NOT NULL, + "password" character varying(60) NOT NULL, + "email" character varying(255) NOT NULL + ) + '); + + $this->db->query(' + CREATE TABLE IF NOT EXISTS "'.$prefix.'ci_sessions" ( + "session_id" character varying(40) NOT NULL DEFAULT 0, + "ip_address" character varying(16) NOT NULL DEFAULT 0, + "user_agent" character varying(120) NOT NULL, + "last_activity" integer NOT NULL DEFAULT 0, + "user_data" text NOT NULL, + PRIMARY KEY ("session_id") + ); + CREATE INDEX "'.$prefix.'ci_sessions_last_activity_idx" ON "'.$prefix.'ci_sessions" ("last_activity"); + '); + + $this->db->query(' + ALTER TABLE "'.$prefix.'files" ADD "user" integer NOT NULL DEFAULT 0; + CREATE INDEX "'.$prefix.'user_idx" ON "'.$prefix.'files" ("user"); + '); + + } else { + + $this->db->query(' + CREATE TABLE IF NOT EXISTS `'.$prefix.'users` ( + `id` int(8) UNSIGNED NOT NULL AUTO_INCREMENT, + `username` varchar(32) COLLATE ascii_general_ci NOT NULL, + `password` varchar(60) COLLATE ascii_general_ci NOT NULL, + `email` varchar(255) COLLATE ascii_general_ci NOT NULL, + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + '); + + $this->db->query(' + CREATE TABLE IF NOT EXISTS `'.$prefix.'ci_sessions` ( + `session_id` varchar(40) NOT NULL DEFAULT 0, + `ip_address` varchar(16) NOT NULL DEFAULT 0, + `user_agent` varchar(120) NOT NULL, + `last_activity` int(10) unsigned NOT NULL DEFAULT 0, + `user_data` text NOT NULL, + PRIMARY KEY (`session_id`), + KEY `last_activity_idx` (`last_activity`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + '); + + $this->db->query(' + ALTER TABLE `'.$prefix.'files` + ADD `user` INT(8) UNSIGNED NOT NULL DEFAULT 0, + ADD INDEX (`user`) + '); + } + } + + public function down() + { + $this->dbforge->drop_table('users'); + $this->dbforge->drop_table('ci_sessions'); + if ($this->db->dbdriver == 'postgre') { + $this->db->query('ALTER TABLE "'.$prefix.'files" DROP "user"'); + } else { + $this->db->query('ALTER TABLE `'.$prefix.'files` DROP `user`'); + } + } +} diff --git a/application/migrations/003_add_referrers.php b/application/migrations/003_add_referrers.php new file mode 100644 index 000000000..e6da1c0dd --- /dev/null +++ b/application/migrations/003_add_referrers.php @@ -0,0 +1,61 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_referrers extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE "'.$prefix.'invitations" ( + "user" integer NOT NULL, + "key" character varying(16) NOT NULL, + "date" integer NOT NULL, + PRIMARY KEY ("key") + ); + CREATE INDEX "'.$prefix.'invitations_user_idx" ON "'.$prefix.'invitations" ("user"); + CREATE INDEX "'.$prefix.'invitations_date_idx" ON "'.$prefix.'invitations" ("date"); + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'users" + ADD "referrer" integer NOT NULL DEFAULT 0 + '); + + } else { + + $this->db->query(' + CREATE TABLE `'.$prefix.'invitations` ( + `user` int(8) unsigned NOT NULL, + `key` varchar(16) CHARACTER SET ascii NOT NULL, + `date` int(11) unsigned NOT NULL, + PRIMARY KEY (`key`), + KEY `user` (`user`), + KEY `date` (`date`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin + '); + $this->db->query(' + ALTER TABLE `'.$prefix.'users` + ADD `referrer` INT(8) UNSIGNED NOT NULL DEFAULT 0 + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'users" DROP "referrer" + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'users` DROP `referrer` + '); + } + $this->dbforge->drop_table('invitations'); + + } +} diff --git a/application/migrations/004_add_filesize.php b/application/migrations/004_add_filesize.php new file mode 100644 index 000000000..ca10e7dc3 --- /dev/null +++ b/application/migrations/004_add_filesize.php @@ -0,0 +1,37 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_filesize extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'files" + ADD "filesize" integer NOT NULL + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'files` + ADD `filesize` INT UNSIGNED NOT NULL + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'files" DROP "filesize" + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'files` DROP `filesize` + '); + } + } +} diff --git a/application/migrations/005_drop_file_password.php b/application/migrations/005_drop_file_password.php new file mode 100644 index 000000000..1b3b5fc12 --- /dev/null +++ b/application/migrations/005_drop_file_password.php @@ -0,0 +1,33 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Drop_file_password extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query('ALTER TABLE "'.$prefix.'files" DROP "password"'); + } else { + $this->db->query('ALTER TABLE `'.$prefix.'files` DROP `password`;'); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'files" + ADD "password" character varying(40) DEFAULT NULL + '); + } else { + $this->db->query(" + ALTER TABLE `'.$prefix.'files` + ADD `password` varchar(40) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL; + "); + } + } +} diff --git a/application/migrations/006_add_username_index.php b/application/migrations/006_add_username_index.php new file mode 100644 index 000000000..1633a95a9 --- /dev/null +++ b/application/migrations/006_add_username_index.php @@ -0,0 +1,35 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_username_index extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE UNIQUE INDEX "'.$prefix.'users_username_idx" ON "'.$prefix.'users" ("username") + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'users` + ADD UNIQUE `username` (`username`); + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query('DROP INDEX "'.$prefix.'users_username_idx"'); + } else { + $this->db->query(" + ALTER TABLE `'.$prefix.'users` + DROP INDEX `username`; + "); + } + } +} diff --git a/application/migrations/007_repurpose_invitations.php b/application/migrations/007_repurpose_invitations.php new file mode 100644 index 000000000..0bc39c64b --- /dev/null +++ b/application/migrations/007_repurpose_invitations.php @@ -0,0 +1,68 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Repurpose_invitations extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'invitations" + ADD "action" character varying(255) NOT NULL, + ADD "data" TEXT NULL; + CREATE INDEX "'.$prefix.'invitations_action_idx" ON '.$prefix.'invitations ("action"); + '); + + $this->db->query(' + UPDATE "'.$prefix.'invitations" SET "action" = \'invitation\' WHERE "action" = \'\' + '); + + $this->db->query(' + ALTER TABLE "'.$prefix.'invitations" RENAME TO '.$prefix.'actions; + '); + + } else { + + $this->db->query(' + ALTER TABLE `'.$prefix.'invitations` + ADD `action` VARCHAR(255) NOT NULL, + ADD `data` TEXT NULL, + ADD INDEX `action` (`action`); + '); + + $this->db->query(' + UPDATE `'.$prefix.'invitations` SET `action` = \'invitation\' WHERE `action` = \'\'; + '); + + $this->db->query(' + ALTER TABLE `'.$prefix.'invitations` RENAME `'.$prefix.'actions`; + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query('ALTER TABLE "'.$prefix.'actions" RENAME TO "'.$prefix.'invitations"'); + $this->db->query(' + ALTER TABLE "'.$prefix.'invitations" + DROP "action", + DROP "data"; + '); + + } else { + + $this->db->query('ALTER TABLE `'.$prefix.'actions` RENAME `'.$prefix.'invitations`'); + $this->db->query(' + ALTER TABLE `'.$prefix.'invitations` + DROP `action`, + DROP `data`; + '); + } + + } +} diff --git a/application/migrations/008_add_profiles.php b/application/migrations/008_add_profiles.php new file mode 100644 index 000000000..4cdd14de0 --- /dev/null +++ b/application/migrations/008_add_profiles.php @@ -0,0 +1,61 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_profiles extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE "'.$prefix.'profiles" ( + "user" integer NOT NULL, + "upload_id_limits" varchar(255) NOT NULL, + PRIMARY KEY ("user") + ) + '); + + $this->db->query(' + ALTER TABLE "'.$prefix.'files" ALTER COLUMN "id" TYPE varchar(255); + '); + + } else { + + $this->db->query(' + CREATE TABLE `'.$prefix.'profiles` ( + `user` int(8) unsigned NOT NULL, + `upload_id_limits` varchar(255) COLLATE utf8_bin NOT NULL, + PRIMARY KEY (`user`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin + '); + + $this->db->query(' + ALTER TABLE `'.$prefix.'files` CHANGE `id` `id` VARCHAR( 255 ); + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + DROP TABLE "'.$prefix.'profiles"; + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'files" ALTER COLUMN "id" TYPE varchar(6); + '); + + } else { + + $this->db->query(" + DROP TABLE `'.$prefix.'profiles`; + "); + $this->db->query(" + ALTER TABLE `'.$prefix.'files` CHANGE `id` `id` VARCHAR( 6 ); + "); + } + } +} diff --git a/application/migrations/009_add_apikeys.php b/application/migrations/009_add_apikeys.php new file mode 100644 index 000000000..e9dba4e41 --- /dev/null +++ b/application/migrations/009_add_apikeys.php @@ -0,0 +1,41 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_Add_apikeys extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE "'.$prefix.'apikeys" ( + "key" varchar(64) NOT NULL, + "user" integer NOT NULL, + "created" timestamp WITH TIME ZONE NOT NULL DEFAULT NOW(), + "comment" varchar(255) NOT NULL, + PRIMARY KEY ("key") + ); + CREATE INDEX "'.$prefix.'apikeys_user_idx" ON "'.$prefix.'apikeys" ("user"); + '); + + } else { + + $this->db->query(' + CREATE TABLE `'.$prefix.'apikeys` ( + `key` varchar(64) COLLATE utf8_bin NOT NULL, + `user` int(8) unsigned NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `comment` varchar(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL, + PRIMARY KEY (`key`), + KEY `user` (`user`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin + '); + } + } + + public function down() + { + $this->dbforge->drop_table('apikeys'); + } +} diff --git a/application/migrations/010_files_innodb.php b/application/migrations/010_files_innodb.php new file mode 100644 index 000000000..98f9dea31 --- /dev/null +++ b/application/migrations/010_files_innodb.php @@ -0,0 +1,20 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_files_innodb extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver != 'postgre') { + $this->db->query(' + ALTER TABLE `'.$prefix.'files` ENGINE = InnoDB; + '); + } + } + + public function down() + { + } +} diff --git a/application/migrations/011_apikeys_add_access_level.php b/application/migrations/011_apikeys_add_access_level.php new file mode 100644 index 000000000..14d0b03d3 --- /dev/null +++ b/application/migrations/011_apikeys_add_access_level.php @@ -0,0 +1,35 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_apikeys_add_access_level extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + alter table "'.$prefix.'apikeys" add "access_level" varchar(255) default \'apikey\' + '); + } else { + $this->db->query(' + alter table `'.$prefix.'apikeys` add `access_level` varchar(255) default \'apikey\'; + '); + } + } + + public function down() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + alter table "'.$prefix.'apikeys" drop "access_level" + '); + } else { + $this->db->query(' + alter table `'.$prefix.'apikeys` drop `access_level` + '); + } + } +} diff --git a/application/migrations/012_add_constraints.php b/application/migrations/012_add_constraints.php new file mode 100644 index 000000000..a2569d54d --- /dev/null +++ b/application/migrations/012_add_constraints.php @@ -0,0 +1,36 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_add_constraints extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query('ALTER TABLE "'.$prefix.'users" ALTER COLUMN "referrer" TYPE integer'); + $this->db->query('ALTER TABLE "'.$prefix.'users" ALTER COLUMN "referrer" DROP NOT NULL'); + $this->db->query('CREATE INDEX "'.$prefix.'users_referrer_idx" ON "'.$prefix.'users" ("referrer")'); + $this->db->query('UPDATE "'.$prefix.'users" SET "referrer" = NULL where "referrer" = 0'); + $this->db->query(' + ALTER TABLE "'.$prefix.'users" + ADD CONSTRAINT "'.$prefix.'referrer_user_fkey" FOREIGN KEY ("referrer") + REFERENCES "'.$prefix.'users"("id") ON DELETE RESTRICT ON UPDATE RESTRICT + '); + + } else { + + $this->db->query('ALTER TABLE `'.$prefix.'users` ADD INDEX(`referrer`);'); + $this->db->query('ALTER TABLE `'.$prefix.'users` CHANGE `referrer` `referrer` + INT(8) UNSIGNED NULL;'); + $this->db->query('UPDATE `'.$prefix.'users` SET `referrer` = NULL where `referrer` = 0;'); + $this->db->query('ALTER TABLE `'.$prefix.'users` ADD FOREIGN KEY (`referrer`) + REFERENCES `'.$prefix.'users`(`id`) ON DELETE RESTRICT ON UPDATE RESTRICT;'); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/013_add_multipaste.php b/application/migrations/013_add_multipaste.php new file mode 100644 index 000000000..6dd9bcb7b --- /dev/null +++ b/application/migrations/013_add_multipaste.php @@ -0,0 +1,62 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_add_multipaste extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE "'.$prefix.'multipaste" ( + "url_id" varchar(255) NOT NULL PRIMARY KEY, + "multipaste_id" serial UNIQUE, + "user_id" integer NOT NULL, + "date" integer NOT NULL + ); + CREATE INDEX "'.$prefix.'multipaste_user_idx" ON "'.$prefix.'multipaste" ("user_id"); + '); + + $this->db->query(' + CREATE TABLE "'.$prefix.'multipaste_file_map" ( + "multipaste_id" integer NOT NULL REFERENCES "'.$prefix.'multipaste" ("multipaste_id") ON DELETE CASCADE ON UPDATE CASCADE, + "file_url_id" varchar(255) NOT NULL REFERENCES "'.$prefix.'files"("id") ON DELETE CASCADE ON UPDATE CASCADE, + "sort_order" serial PRIMARY KEY, + UNIQUE ("multipaste_id", "file_url_id") + ); + CREATE INDEX "'.$prefix.'multipaste_file_map_file_idx" ON "'.$prefix.'multipaste_file_map" ("file_url_id"); + '); + + } else { + + $this->db->query(' + CREATE TABLE `'.$prefix.'multipaste` ( + `url_id` varchar(255) NOT NULL, + `multipaste_id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `date` int(11) NOT NULL, + PRIMARY KEY (`url_id`), + UNIQUE KEY `multipaste_id` (`multipaste_id`), + KEY `user_id` (`user_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;'); + + $this->db->query(' + CREATE TABLE `'.$prefix.'multipaste_file_map` ( + `multipaste_id` int(11) NOT NULL, + `file_url_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, + `sort_order` int(10) unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`sort_order`), + UNIQUE KEY `multipaste_id` (`multipaste_id`,`file_url_id`), + KEY `multipaste_file_map_ibfk_2` (`file_url_id`), + CONSTRAINT `'.$prefix.'multipaste_file_map_ibfk_1` FOREIGN KEY (`multipaste_id`) REFERENCES `'.$prefix.'multipaste` (`multipaste_id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `'.$prefix.'multipaste_file_map_ibfk_2` FOREIGN KEY (`file_url_id`) REFERENCES `'.$prefix.'files` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;'); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/014_deduplicate_file_storage.php b/application/migrations/014_deduplicate_file_storage.php new file mode 100644 index 000000000..bd96c56d8 --- /dev/null +++ b/application/migrations/014_deduplicate_file_storage.php @@ -0,0 +1,137 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_deduplicate_file_storage extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + CREATE TABLE "'.$prefix.'file_storage" ( + "id" serial NOT NULL, + "filesize" integer NOT NULL, + "mimetype" varchar(255) NOT NULL, + "hash" char(32) NOT NULL, + "date" integer NOT NULL, + PRIMARY KEY ("id"), + UNIQUE ("id", "hash") + ); + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'files" + ADD "file_storage_id" integer NULL; + CREATE INDEX "'.$prefix.'files_file_storage_id_idx" ON "'.$prefix.'files" ("file_storage_id"); + '); + + $this->db->query(' + INSERT INTO "'.$prefix.'file_storage" (filesize, mimetype, hash, date) + SELECT filesize, mimetype, hash, date FROM "'.$prefix.'files"; + '); + + $this->db->query(' + UPDATE "'.$prefix.'files" f + SET file_storage_id = fs.id + FROM "'.$prefix.'file_storage" fs + WHERE fs.hash = f.hash + '); + + // remove file_storage rows that are not referenced by files.id + // AND that are duplicates when grouped by hash + $this->db->query(' + DELETE + FROM "'.$prefix.'file_storage" fs + USING "'.$prefix.'file_storage" fs2 + WHERE fs.hash = fs2.hash + AND fs.id > fs2.id + AND fs.id NOT IN ( + SELECT file_storage_id + FROM "'.$prefix.'files" f + ); + '); + } else { + $this->db->query(' + CREATE TABLE `'.$prefix.'file_storage` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `filesize` int(11) NOT NULL, + `mimetype` varchar(255) NOT NULL, + `hash` char(32) NOT NULL, + `date` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `data_id` (`id`, `hash`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + '); + $this->db->query(' + ALTER TABLE `'.$prefix.'files` + ADD `file_storage_id` INT NOT NULL, + ADD INDEX (`file_storage_id`); + '); + + $this->db->query(' + INSERT INTO `'.$prefix.'file_storage` (id, filesize, mimetype, hash, date) + SELECT NULL, filesize, mimetype, hash, date FROM `'.$prefix.'files`; + '); + + $this->db->query(' + UPDATE `'.$prefix.'files` f + JOIN `'.$prefix.'file_storage` fs ON fs.hash = f.hash + SET f.file_storage_id = fs.id + '); + + // XXX: This query also exists in migration 15 + $this->db->query(' + DELETE fs + FROM `'.$prefix.'file_storage` fs, `'.$prefix.'file_storage` fs2 + WHERE fs.hash = fs2.hash + AND fs.id > fs2.id + AND fs.id NOT IN ( + SELECT file_storage_id + FROM `'.$prefix.'files` f + ); + '); + } + + $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) { + $old = $this->mfile->file($item["hash"]); + $data_id = $item["hash"].'-'.$item["id"]; + $new = $this->mfile->file($data_id); + if (file_exists($old)) { + rename($old, $new); + } else { + echo "Warning: no file found for $data_id. Skipping...\n"; + } + } + } + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'files" + ADD FOREIGN KEY ("file_storage_id") REFERENCES "'.$prefix.'file_storage"("id") ON DELETE CASCADE ON UPDATE CASCADE; + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'files` + ADD FOREIGN KEY (`file_storage_id`) REFERENCES `'.$prefix.'file_storage`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + '); + } + + $this->dbforge->drop_column("files", "hash"); + $this->dbforge->drop_column("files", "mimetype"); + $this->dbforge->drop_column("files", "filesize"); + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/015_actually_deduplicate_file_storage.php b/application/migrations/015_actually_deduplicate_file_storage.php new file mode 100644 index 000000000..76ff2d6b9 --- /dev/null +++ b/application/migrations/015_actually_deduplicate_file_storage.php @@ -0,0 +1,56 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_actually_deduplicate_file_storage extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + // no need for this query since 14 is not buggy + } else { + // XXX: This query also exists in migration 14 + $this->db->query(' + DELETE fs + FROM `'.$prefix.'file_storage` fs, `'.$prefix.'file_storage` fs2 + WHERE fs.hash = fs2.hash + AND fs.id > fs2.id + AND fs.id NOT IN ( + SELECT file_storage_id + FROM `'.$prefix.'files` f + ); + '); + } + + $chunk = 500; + $total = $this->db->count_all("file_storage"); + $consistent = true; + + 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"]; + $file = $this->mfile->file($data_id); + if (!file_exists($file)) { + echo "Warning: no file found for $data_id\n"; + $consistent = false; + } + } + } + + if (!$consistent) { + echo "Your database is not consistent with your file system.\n"; + echo "Please report this as it is most likely a bug.\n"; + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/016_allow_ipv6_storage.php b/application/migrations/016_allow_ipv6_storage.php new file mode 100644 index 000000000..726a18601 --- /dev/null +++ b/application/migrations/016_allow_ipv6_storage.php @@ -0,0 +1,27 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_allow_ipv6_storage extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" + ALTER COLUMN "ip_address" type varchar(39); + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'ci_sessions` + CHANGE `ip_address` `ip_address` varchar(39); + '); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/017_increase_password_length.php b/application/migrations/017_increase_password_length.php new file mode 100644 index 000000000..9d12d3f52 --- /dev/null +++ b/application/migrations/017_increase_password_length.php @@ -0,0 +1,27 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_increase_password_length extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'users" + ALTER COLUMN "password" type varchar(255); + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'users` + CHANGE `password` `password` varchar(255); + '); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/018_allow_null_values_userinfo.php b/application/migrations/018_allow_null_values_userinfo.php new file mode 100644 index 000000000..1497dd0d4 --- /dev/null +++ b/application/migrations/018_allow_null_values_userinfo.php @@ -0,0 +1,31 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_allow_null_values_userinfo extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'users" + ALTER COLUMN "username" DROP NOT NULL, + ALTER COLUMN "password" DROP NOT NULL, + ALTER COLUMN "email" DROP NOT NULL; + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'users` + CHANGE `username` `username` varchar(32) NULL, + CHANGE `password` `password` varchar(255) NULL, + CHANGE `email` `email` varchar(255) NULL; + '); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/019_change_filesize_type.php b/application/migrations/019_change_filesize_type.php new file mode 100644 index 000000000..33abf89ed --- /dev/null +++ b/application/migrations/019_change_filesize_type.php @@ -0,0 +1,51 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_change_filesize_type extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'file_storage" + ALTER "filesize" TYPE bigint; + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'file_storage` + MODIFY `filesize` bigint; + '); + } + + $chunk = 500; + + $this->db->where('filesize', 2147483647); + $total = $this->db->count_all_results("file_storage"); + + for ($limit = 0; $limit < $total; $limit += $chunk) { + $query = $this->db->select('hash, id') + ->from('file_storage') + ->where('filesize', 2147483647) + ->limit($chunk, $limit) + ->get()->result_array(); + + foreach ($query as $key => $item) { + $data_id = $item["hash"].'-'.$item['id']; + $filesize = filesize($this->mfile->file($data_id)); + + $this->db->where('id', $item['id']) + ->set(array( + 'filesize' => $filesize, + )) + ->update('file_storage'); + } + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/020_update_session_table.php b/application/migrations/020_update_session_table.php new file mode 100644 index 000000000..94a240def --- /dev/null +++ b/application/migrations/020_update_session_table.php @@ -0,0 +1,45 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_update_session_table extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" + DROP COLUMN "user_agent"; + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" + RENAME COLUMN "session_id" TO "id"; + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" + RENAME COLUMN "last_activity" TO "timestamp"; + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" + RENAME COLUMN "user_data" TO "data"; + '); + $this->db->query(' + ALTER TABLE "'.$prefix.'ci_sessions" ALTER COLUMN id SET DATA TYPE varchar(128); + '); + } else { + $this->db->query(' + ALTER TABLE `'.$prefix.'ci_sessions` + DROP `user_agent`, + CHANGE `session_id` `id` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, + CHANGE `last_activity` `timestamp` INT(10) UNSIGNED NOT NULL DEFAULT 0, + CHANGE `user_data` `data` BLOB NOT NULL; + '); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} diff --git a/application/migrations/021_change_charset.php b/application/migrations/021_change_charset.php new file mode 100644 index 000000000..475732ed5 --- /dev/null +++ b/application/migrations/021_change_charset.php @@ -0,0 +1,28 @@ +<?php +defined('BASEPATH') OR exit('No direct script access allowed'); + +class Migration_change_charset extends CI_Migration { + + public function up() + { + $prefix = $this->db->dbprefix; + + if ($this->db->dbdriver == 'postgre') { + # nothing to do + } else { + $this->db->query('SET FOREIGN_KEY_CHECKS = 0'); + foreach ([ + ['apikeys', 'comment', 'VARCHAR(255)'], + ['files', 'filename', 'VARCHAR(256)'], + ] as $col) { + $this->db->query('ALTER TABLE `'.$prefix.$col[0].'` CHANGE `'.$col[1].'` `'.$col[1].'` '.$col[2].' CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;'); + } + $this->db->query('SET FOREIGN_KEY_CHECKS = 1'); + } + } + + public function down() + { + throw new \exceptions\ApiException("migration/downgrade-not-supported", "downgrade not supported"); + } +} 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..ef260f47b --- /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\PublicApiException("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\PublicApiException("user/username-already-exists", "Username already exists"); + } + } + + if (!$this->valid_email($email)) { + throw new \exceptions\PublicApiException("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; + } + +} + 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(); + } +} diff --git a/application/test/Test.php b/application/test/Test.php new file mode 100644 index 000000000..b8052fbba --- /dev/null +++ b/application/test/Test.php @@ -0,0 +1,186 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test; + +require_once APPPATH."/third_party/test-more-php/Test-More-OO.php"; + +class TestMore extends \TestMore { + private $TestNamePrefix = ""; + + public function setTestNamePrefix($prefix) { + $this->TestNamePrefix = $prefix; + } + + public function ok ($Result = NULL, $TestName = NULL) { + return parent::ok($Result, $this->TestNamePrefix.$TestName); + } +} + +abstract class Test { + protected $t; + protected $server_url = ""; + private $testid = ""; + private $server_proc = null; + + public function __construct() + { + $this->t = new TestMore(); + $this->t->plan("no_plan"); + } + + public function __destruct() + { + if ($this->server_proc) { + proc_terminate($this->server_proc); + } + } + + public function startServer($port) + { + $url = "http://127.0.0.1:$port/index.php"; + + $pipes = []; + $descriptorspec = [ + 0 => ['file', '/dev/null', 'r'], + 1 => STDOUT, + 2 => STDOUT, + ]; + + $this->server_proc = proc_open("php -S 127.0.0.1:$port", $descriptorspec, $pipes); + + $this->wait_for_server($url); + $this->server_url = $url; + } + + private function wait_for_server($url) + { + while (!$this->url_is_reachable($url)) { + echo "Waiting for server at $url to start...\n"; + usleep(10000); + } + } + + private function url_is_reachable($url) + { + $handle = curl_init($url); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, TRUE); + curl_exec($handle); + $status = curl_getinfo($handle, CURLINFO_HTTP_CODE); + curl_close($handle); + + if ($status == 200) { + return true; + } + + return false; + } + + public function setTestID($testid) + { + $this->testid = $testid; + } + + // Method: POST, PUT, GET etc + // Data: array("param" => "value") ==> index.php?param=value + // Source: http://stackoverflow.com/a/9802854/953022 + protected function CallAPI($method, $url, $data = false, $return_json = false) + { + $result = $this->SendHTTPRequest($method, $url, $data); + + if ($return_json) { + return $result; + } + + $json = json_decode($result, true); + if ($json === NULL) { + $this->t->fail("json decode"); + $this->diagReply($result); + } + + return $json; + } + + protected function SendHTTPRequest($method, $url, $data = false) + { + $curl = curl_init(); + + switch ($method) { + case "POST": + curl_setopt($curl, CURLOPT_POST, 1); + + if ($data) + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); + break; + case "PUT": + curl_setopt($curl, CURLOPT_PUT, 1); + break; + default: + if ($data) + $url = sprintf("%s?%s", $url, http_build_query($data)); + } + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + "Accept: application/json", + "X-Testsuite-Testname: API request from ".$this->testid, + "Expect: ", + )); + + $result = curl_exec($curl); + + curl_close($curl); + return $result; + } + + protected function excpectStatus($testname, $reply, $status) + { + if (!isset($reply["status"]) || $reply["status"] != $status) { + $this->t->fail($testname); + $this->diagReply($reply); + } else { + $this->t->pass($testname); + } + return $reply; + } + + protected function expectSuccess($testname, $reply) + { + return $this->excpectStatus($testname, $reply, "success"); + } + + protected function expectError($testname, $reply) + { + return $this->excpectStatus($testname, $reply, "error"); + } + + protected function diagReply($reply) + { + $this->t->diag("Request got unexpected response:"); + $this->t->diag(var_export($reply, true)); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function done_testing() + { + $this->t->done_testing(); + } + + public function setTestNamePrefix($prefix) { + $this->t->setTestNamePrefix($prefix); + } +} diff --git a/application/test/tests/api_v2/common.php b/application/test/tests/api_v2/common.php new file mode 100644 index 000000000..103e156a8 --- /dev/null +++ b/application/test/tests/api_v2/common.php @@ -0,0 +1,60 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class common extends \test\Test { + + protected $userCounter = null; + + public function __construct() + { + parent::__construct(); + + $CI =& get_instance(); + $CI->load->model("muser"); + $CI->load->model("mfile"); + } + + protected function uploadFile($apikey, $file) + { + $ret = $this->CallAPI("POST", "$this->server_url/api/v2.0.0/file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create($file), + )); + $this->expectSuccess("upload file", $ret); + return $ret; + } + + protected function createUser($counter) + { + $CI =& get_instance(); + $CI->muser->add_user("apiv2testuser$counter", "testpass$counter", + "testuser$counter@testsuite.local", NULL); + return $CI->db->insert_id(); + } + + protected function createApikey($userid, $access_level = "apikey") + { + return \service\user::create_apikey($userid, "", $access_level); + } + + protected function createUserAndApikey($access_level = "apikey") + { + assert($this->userCounter !== null); + $this->userCounter++; + $userid = $this->createUser($this->userCounter); + return $this->createApikey($userid, $access_level); + } + + protected function callEndpoint($verb, $endpoint, $data, $return_json = false) + { + return $this->CallAPI($verb, "$this->server_url/api/v2.0.0/$endpoint", $data, $return_json); + } +} diff --git a/application/test/tests/api_v2/test_api_permissions.php b/application/test/tests/api_v2/test_api_permissions.php new file mode 100644 index 000000000..6df612911 --- /dev/null +++ b/application/test/tests/api_v2/test_api_permissions.php @@ -0,0 +1,108 @@ +<?php +/* + * Copyright 2015-2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_api_permissions extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23200); + $this->userCounter = 100; + } + + public function test_callPrivateEndpointsWithoutApikey() + { + $endpoints = array( + "file/upload", + "file/history", + "file/delete", + "file/create_multipaste", + "user/apikeys", + "user/create_apikey", + "user/delete_apikey", + ); + foreach ($endpoints as $endpoint) { + $ret = $this->CallEndpoint("POST", $endpoint, array( + )); + $this->expectError("call $endpoint without apikey", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'api/not-authenticated', + 'message' => 'Not authenticated. FileBin requires you to have an account, please go to the homepage at http://127.0.0.1:23200/ for more information.', + ), $ret, "expected error"); + } + } + + public function test_callPrivateEndpointsWithUnsupportedAuthentication() + { + $endpoints = array( + "file/upload", + "file/history", + "file/delete", + "file/create_multipaste", + "user/apikeys", + // create_apikey is the only one that supports username/pw + //"user/create_apikey", + "user/delete_apikey", + ); + foreach ($endpoints as $endpoint) { + $ret = $this->CallEndpoint("POST", $endpoint, array( + "username" => "apiv2testuser1", + "password" => "testpass1", + )); + $this->expectError("call $endpoint without apikey", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'api/not-authenticated', + 'message' => 'Not authenticated. FileBin requires you to have an account, please go to the homepage at http://127.0.0.1:23200/ for more information.', + ), $ret, "expected error"); + } + } + + public function test_callEndpointsWithoutEnoughPermissions() + { + $testconfig = array( + array( + "have_level" => "basic", + "wanted_level" => "apikey", + "apikey" => $this->createUserAndApikey('basic'), + "endpoints" => array( + "file/delete", + "file/history", + ), + ), + array( + "have_level" => "apikey", + "wanted_level" => "full", + "apikey" => $this->createUserAndApikey(), + "endpoints" => array( + "user/apikeys", + "user/create_apikey", + "user/delete_apikey", + ), + ), + ); + foreach ($testconfig as $test) { + foreach ($test['endpoints'] as $endpoint) { + $ret = $this->CallEndpoint("POST", $endpoint, array( + "apikey" => $test['apikey'], + )); + $this->expectError("call $endpoint without enough permissions", $ret); + $this->t->is_deeply(array( + 'status' => "error", + 'error_id' => "api/insufficient-permissions", + 'message' => "Access denied: Access level too low. Required: ${test['wanted_level']}; Have: ${test['have_level']}", + ), $ret, "expected permission error"); + } + } + } + +} diff --git a/application/test/tests/api_v2/test_create_apikey.php b/application/test/tests/api_v2/test_create_apikey.php new file mode 100644 index 000000000..203eb5531 --- /dev/null +++ b/application/test/tests/api_v2/test_create_apikey.php @@ -0,0 +1,66 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_create_apikey extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23202); + $this->userCounter = 2100; + } + + public function test_create_apikey_createNewKey() + { + $this->createUser(1); + $ret = $this->CallEndpoint("POST", "user/create_apikey", array( + "username" => "apiv2testuser1", + "password" => "testpass1", + "access_level" => "apikey", + "comment" => "main api key", + )); + $this->expectSuccess("create-apikey", $ret); + + $this->t->isnt($ret["data"]["new_key"], "", "apikey not empty"); + } + + public function test_authentication_invalidPassword() + { + $userid = $this->createUser(3); + $ret = $this->CallEndpoint("POST", "user/create_apikey", array( + "username" => "apiv2testuser3", + "password" => "wrongpass", + )); + $this->expectError("invalid password", $ret); + + $this->t->is_deeply(array ( + 'status' => 'error', + 'error_id' => 'user/login-failed', + 'message' => 'Login failed', + ), $ret, "expected error"); + } + + public function test_authentication_invalidUser() + { + $userid = $this->createUser(4); + $ret = $this->CallEndpoint("POST", "user/create_apikey", array( + "username" => "apiv2testuserinvalid", + "password" => "testpass4", + )); + $this->expectError("invalid username", $ret); + + $this->t->is_deeply(array ( + 'status' => 'error', + 'error_id' => 'user/login-failed', + 'message' => 'Login failed', + ), $ret, "expected error"); + } +} diff --git a/application/test/tests/api_v2/test_file_create_multipaste.php b/application/test/tests/api_v2/test_file_create_multipaste.php new file mode 100644 index 000000000..8556616d1 --- /dev/null +++ b/application/test/tests/api_v2/test_file_create_multipaste.php @@ -0,0 +1,125 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_file_create_multipaste extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23204); + $this->userCounter = 4100; + } + + public function test_create_multipaste_canCreate() + { + $apikey = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id2 = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + "ids[1]" => $id, + "ids[2]" => $id2, + )); + $this->expectSuccess("create multipaste", $ret); + + $this->t->isnt($ret["data"]["url_id"], "", "got a multipaste ID"); + $this->t->isnt($ret["data"]["url"], "", "got a multipaste URL"); + } + + public function test_create_multipaste_errorOnWrongID() + { + $apikey = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $id2 = $id."invalid"; + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + "ids[1]" => $id, + "ids[2]" => $id2, + )); + $this->expectError("create multipaste with wrong ID", $ret); + + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/create_multipaste/verify-failed', + 'message' => 'Failed to verify ID(s)', + 'data' => + array ( + $id2 => + array ( + 'id' => $id2, + 'reason' => 'doesn\'t exist', + ), + ), + ), $ret, "expected error response"); + } + + public function test_create_multipaste_errorOnWrongOwner() + { + $apikey = $this->createUserAndApikey("basic"); + $apikey2 = $this->createUserAndApikey("basic"); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey2, + "ids[1]" => $id, + )); + $this->expectError("create multipaste with wrong owner", $ret); + + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/create_multipaste/verify-failed', + 'message' => 'Failed to verify ID(s)', + 'data' => + array ( + $id => + array ( + 'id' => $id, + 'reason' => 'not owned by you', + ), + ), + ), $ret, "expected error response"); + } + + public function test_delete_canDeleteMultipaste() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + $ret = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + "ids[1]" => $id, + )); + $this->expectSuccess("create multipaste", $ret); + + $mid = $ret['data']['url_id']; + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey, + "ids[1]" => $mid, + )); + $this->expectSuccess("delete uploaded file", $ret); + + $this->t->ok(empty($ret["data"]["errors"]), "no errors"); + $this->t->is_deeply(array( + $mid => array( + "id" => $mid + ) + ), $ret["data"]["deleted"], "deleted wanted ID"); + $this->t->is($ret["data"]["total_count"], 1, "total_count correct"); + $this->t->is($ret["data"]["deleted_count"], 1, "deleted_count correct"); + } +} diff --git a/application/test/tests/api_v2/test_file_delete.php b/application/test/tests/api_v2/test_file_delete.php new file mode 100644 index 000000000..d9ffc5b2c --- /dev/null +++ b/application/test/tests/api_v2/test_file_delete.php @@ -0,0 +1,82 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_file_delete extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23203); + $this->userCounter = 3100; + } + + public function test_delete_canDeleteUploaded() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey, + "ids[1]" => $id, + )); + $this->expectSuccess("delete uploaded file", $ret); + + $this->t->ok(empty($ret["data"]["errors"]), "no errors"); + $this->t->is_deeply(array( + $id => array( + "id" => $id + ) + ), $ret["data"]["deleted"], "deleted wanted ID"); + $this->t->is($ret["data"]["total_count"], 1, "total_count correct"); + $this->t->is($ret["data"]["deleted_count"], 1, "deleted_count correct"); + } + + public function test_delete_errorIfNotOwner() + { + $apikey = $this->createUserAndApikey(); + $apikey2 = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey2, + "ids[1]" => $id, + )); + $this->expectSuccess("delete file of someone else", $ret); + + $this->t->ok(empty($ret["data"]["deleted"]), "not deleted"); + $this->t->is_deeply(array( + $id => array( + "id" => $id, + "reason" => "wrong owner" + ) + ), $ret["data"]["errors"], "error wanted ID"); + $this->t->is($ret["data"]["total_count"], 1, "total_count correct"); + $this->t->is($ret["data"]["deleted_count"], 0, "deleted_count correct"); + } + + public function test_delete_empty_json_structure() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->uploadFile($apikey, "data/tests/small-file"); + $id = $ret["data"]["ids"][0]; + + $ret = $this->CallEndpoint("POST", "file/delete", array( + "apikey" => $apikey, + "ids[1]" => $id, + ), true); + + $this->t->is($ret, '{"status":"success","data":{"errors":{},"deleted":{"'.$id.'":{"id":"'.$id.'"}},"total_count":1,"deleted_count":1}}', "empty lists should be json objects, not arrays"); + } + + +} diff --git a/application/test/tests/api_v2/test_file_upload.php b/application/test/tests/api_v2/test_file_upload.php new file mode 100644 index 000000000..e8717b074 --- /dev/null +++ b/application/test/tests/api_v2/test_file_upload.php @@ -0,0 +1,71 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_file_upload extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23205); + $this->userCounter = 5100; + } + + public function test_upload_uploadFile() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/small-file"), + )); + $this->expectSuccess("upload file", $ret); + + $this->t->ok(!empty($ret["data"]["ids"]), "got IDs"); + $this->t->ok(!empty($ret["data"]["urls"]), "got URLs"); + } + + public function test_upload_uploadFileSameMD5() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/message1.bin"), + "file[2]" => curl_file_create("data/tests/message2.bin"), + )); + $this->expectSuccess("upload file", $ret); + + $this->t->ok(!empty($ret["data"]["ids"]), "got IDs"); + $this->t->ok(!empty($ret["data"]["urls"]), "got URLs"); + + foreach ($ret["data"]["urls"] as $url) { + # remove tailing / + $url = substr($url, 0, strlen($url) - 1); + $data[] = $this->SendHTTPRequest("GET", $url, ''); + } + $this->t->ok($data[0] !== $data[1], 'Returned file contents should differ'); + $this->t->is($data[0], file_get_contents("data/tests/message1.bin"), "Returned correct data for file 1"); + $this->t->is($data[1], file_get_contents("data/tests/message2.bin"), "Returned correct data for file 2"); + } + + public function test_upload_uploadNothing() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + )); + $this->expectError("upload no file", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'file/no-file', + 'message' => 'No file was uploaded or unknown error occurred.', + ), $ret, "expected reply"); + } + +} diff --git a/application/test/tests/api_v2/test_history.php b/application/test/tests/api_v2/test_history.php new file mode 100644 index 000000000..f09aab9bb --- /dev/null +++ b/application/test/tests/api_v2/test_history.php @@ -0,0 +1,137 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_history extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23201); + $this->userCounter = 1100; + } + + public function test_history_empty() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("get history", $ret); + + $this->t->ok(empty($ret["data"]["items"]), "items key exists and empty"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "multipaste_items key exists and empty"); + $this->t->is($ret["data"]["total_size"], "0", "total_size = 0 since no uploads"); + } + + public function test_history_empty_json_structure() + { + $apikey = $this->createUserAndApikey(); + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + ), true); + + $this->t->is($ret, '{"status":"success","data":{"items":{},"multipaste_items":{},"total_size":"0"}}', "empty lists should be json objects, not arrays"); + } + + public function test_history_notEmptyAfterUploadSameMD5() + { + $apikey = $this->createUserAndApikey(); + $this->CallEndpoint("POST", "file/upload", array( + "apikey" => $apikey, + "file[1]" => curl_file_create("data/tests/message1.bin"), + "file[2]" => curl_file_create("data/tests/message2.bin"), + )); + $expected_filesize = filesize("data/tests/message1.bin") + filesize("data/tests/message2.bin"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after upload", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after upload (items)"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "didn't upload multipaste"); + $this->t->is($ret["data"]["total_size"], "$expected_filesize", "total_size == uploaded files"); + } + + public function test_history_notEmptyAfterMultipaste() + { + $apikey = $this->createUserAndApikey(); + $uploadid = $this->uploadFile($apikey, "data/tests/small-file")['data']['ids'][0]; + $multipasteid = $this->CallEndpoint("POST", "file/create_multipaste", array( + "apikey" => $apikey, + 'ids[1]' => $uploadid, + ))['data']['url_id']; + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after multipaste", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after multipaste (items)"); + $this->t->is($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]['id'], $uploadid, "multipaste contains correct id"); + $this->t->is_deeply(array( + 'url_id', 'date', 'items' + ), array_keys($ret['data']["multipaste_items"][$multipasteid]), "multipaste info only lists correct keys"); + $this->t->is_deeply(array('id'), array_keys($ret['data']["multipaste_items"][$multipasteid]['items'][$uploadid]), "multipaste item info only lists correct keys"); + } + + public function test_history_notEmptyAfterUpload() + { + $apikey = $this->createUserAndApikey(); + $uploadid = $this->uploadFile($apikey, "data/tests/small-file")['data']['ids'][0]; + $uploadid_image = $this->uploadFile($apikey, "data/tests/black_white.png")['data']['ids'][0]; + $expected_size = filesize("data/tests/small-file") + filesize("data/tests/black_white.png"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("history not empty after upload", $ret); + + $this->t->ok(!empty($ret["data"]["items"]), "history not empty after upload (items)"); + $this->t->is_deeply(array( + 'id', 'filename', 'mimetype', 'date', 'hash', 'filesize' + ), array_keys($ret['data']["items"][$uploadid]), "item info only lists correct keys"); + $this->t->is_deeply(array( + 'id', 'filename', 'mimetype', 'date', 'hash', 'filesize', 'thumbnail' + ), array_keys($ret['data']["items"][$uploadid_image]), "item info for image lists thumbnail too"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "didn't upload multipaste"); + $this->t->is($ret["data"]["total_size"], "$expected_size", "total_size == uploaded files"); + } + + public function test_history_notSharedBetweenUsers() + { + $apikey = $this->createUserAndApikey(); + $apikey2 = $this->createUserAndApikey(); + $this->uploadFile($apikey, "data/tests/small-file"); + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey2, + )); + $this->expectSuccess("get history", $ret); + + $this->t->ok(empty($ret["data"]["items"]), "items key exists and empty"); + $this->t->ok(empty($ret["data"]["multipaste_items"]), "multipaste_items key exists and empty"); + $this->t->is($ret["data"]["total_size"], "0", "total_size = 0 since no uploads"); + } + + public function test_history_specialVarsNotExpanded() + { + $apikey = $this->createUserAndApikey(); + $uploadid = $this->uploadFile($apikey, "data/tests/{elapsed_time}.txt")['data']['ids'][0]; + + $ret = $this->CallEndpoint("POST", "file/history", array( + "apikey" => $apikey, + )); + $this->expectSuccess("get history", $ret); + + $this->t->is($ret["data"]["items"][$uploadid]['filename'], '{elapsed_time}.txt', "{elapsed_time} is not expanded in history reply"); + } +} diff --git a/application/test/tests/api_v2/test_misc.php b/application/test/tests/api_v2/test_misc.php new file mode 100644 index 000000000..e7c249054 --- /dev/null +++ b/application/test/tests/api_v2/test_misc.php @@ -0,0 +1,48 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_misc extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23207); + $this->userCounter = 7100; + } + + public function test_apikeys_getApikey() + { + $userid = $this->createUser(2); + $apikey = $this->createApikey($userid); + $apikey_full = $this->createApikey($userid, "full"); + $ret = $this->CallEndpoint("POST", "user/apikeys", array( + "apikey" => $apikey_full, + )); + $this->expectSuccess("get apikeys", $ret); + + $this->t->is($ret["data"]["apikeys"][$apikey]["key"], $apikey, "expected key 1"); + $this->t->is($ret["data"]["apikeys"][$apikey]["access_level"], "apikey", "expected key 1 acces_level"); + $this->t->is($ret["data"]["apikeys"][$apikey]["comment"], "", "expected key 1 comment"); + $this->t->ok(is_int($ret["data"]["apikeys"][$apikey]["created"]) , "expected key 1 creation time is int"); + } + + public function test_get_config() + { + $ret = $this->CallEndpoint("GET", "file/get_config", array( + )); + $this->expectSuccess("get_config", $ret); + + $this->t->like($ret["data"]["upload_max_size"], '/[0-9]+/', "upload_max_size is int"); + $this->t->like($ret["data"]["max_files_per_request"], '/[0-9]+/', "max_files_per_request is int"); + } + + +} diff --git a/application/test/tests/api_v2/test_user_delete_apikey.php b/application/test/tests/api_v2/test_user_delete_apikey.php new file mode 100644 index 000000000..062b0d6c1 --- /dev/null +++ b/application/test/tests/api_v2/test_user_delete_apikey.php @@ -0,0 +1,49 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests\api_v2; + +class test_user_delete_apikey extends common { + + public function __construct() + { + parent::__construct(); + $this->startServer(23206); + $this->userCounter = 6100; + } + + public function test_delete_apikey_deleteOwnKey() + { + $apikey = $this->createUserAndApikey("full"); + $ret = $this->CallEndpoint("POST", "user/delete_apikey", array( + "apikey" => $apikey, + "delete_key" => $apikey, + )); + $this->expectSuccess("delete apikey", $ret); + + $this->t->is($ret["data"]["deleted_keys"][$apikey]["key"], $apikey, "expected key"); + } + + public function test_delete_apikey_errorDeleteOtherUserKey() + { + $apikey = $this->createUserAndApikey("full"); + $apikey2 = $this->createUserAndApikey("full"); + $ret = $this->CallEndpoint("POST", "user/delete_apikey", array( + "apikey" => $apikey, + "delete_key" => $apikey2, + )); + $this->expectError("delete apikey of other user", $ret); + $this->t->is_deeply(array( + 'status' => 'error', + 'error_id' => 'user/delete_apikey/failed', + 'message' => 'Apikey deletion failed. Possibly wrong owner.', + ), $ret, "expected error"); + } + +} diff --git a/application/test/tests/test_database_schema.php b/application/test/tests/test_database_schema.php new file mode 100644 index 000000000..02f188e1f --- /dev/null +++ b/application/test/tests/test_database_schema.php @@ -0,0 +1,38 @@ +<?php +/* + * Copyright 2017 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_database_schema extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function test_file_storage_bigint() { + $filesize = pow(2, 35) + 1; + + $CI =& get_instance(); + $CI->db->insert("file_storage", array( + "filesize" => $filesize, + "mimetype" => "text/plain", + "hash" => md5("test"), + "date" => time(), + )); + $id = $CI->db->insert_id(); + $db_value = $CI->db->select('filesize') + ->from('file_storage') + ->where('id', $id) + ->get()->result_array()[0]["filesize"]; + $this->t->is(intval($db_value), $filesize, "Large filesize is stored correctly in db"); + } + + +} diff --git a/application/test/tests/test_filebin_helper.php b/application/test/tests/test_filebin_helper.php new file mode 100644 index 000000000..a46d4bc3c --- /dev/null +++ b/application/test/tests/test_filebin_helper.php @@ -0,0 +1,108 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_filebin_helper extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_expiration_duration() + { + $this->t->is(expiration_duration(60*60*24*2), "2 days", "2 days"); + $this->t->is(expiration_duration(60*60*24), "1 day", "1 day"); + $this->t->is(expiration_duration(60*60*2), "2 hours", "2 hours"); + $this->t->is(expiration_duration(60*60), "1 hour", "1 hour"); + $this->t->is(expiration_duration(60*2), "2 minutes", "2 minutes"); + $this->t->is(expiration_duration(60), "1 minute", "1 minute"); + $this->t->is(expiration_duration(59), "59 seconds", "59 seconds"); + $this->t->is(expiration_duration(1), "1 second", "1 second"); + + $this->t->is(expiration_duration(60*60*24 + 60*60 + 60), "1 day, 1 hour, 1 minute", "1 day, 1 hour, 1 minute"); + $this->t->is(expiration_duration(60*60*24 + 60*60 + 120), "1 day, 1 hour, 2 minutes", "1 day, 1 hour, 2 minutes"); + $this->t->is(expiration_duration(60*60*24 + 60*60*2 + 60), "1 day, 2 hours, 1 minute", "1 day, 2 hours, 1 minute"); + $this->t->is(expiration_duration(60*60*24 + 60*60*2 + 120), "1 day, 2 hours, 2 minutes", "1 day, 2 hours, 2 minutes"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60 + 60), "2 days, 1 hour, 1 minute", "2 days, 1 hour, 1 minute"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60 + 120), "2 days, 1 hour, 2 minutes", "2 days, 1 hour, 2 minutes"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60*2 + 60), "2 days, 2 hours, 1 minute", "2 days, 2 hours, 1 minute"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60*2 + 120), "2 days, 2 hours, 2 minutes", "2 days, 2 hours, 2 minutes"); + + $this->t->is(expiration_duration(60*60*24 + 60*60), "1 day, 1 hour", "1 day, 1 hour"); + $this->t->is(expiration_duration(60*60*24 + 60*60*2), "1 day, 2 hours", "1 day, 2 hours"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60), "2 days, 1 hour", "2 days, 1 hour"); + $this->t->is(expiration_duration(60*60*24*2 + 60*60*2), "2 days, 2 hours", "2 days, 2 hours"); + + $this->t->is(expiration_duration(60*60*24 + 60), "1 day, 1 minute", "1 day, 1 minute"); + $this->t->is(expiration_duration(60*60*24 + 120), "1 day, 2 minutes", "1 day, 2 minutes"); + $this->t->is(expiration_duration(60*60*24*2 + 60), "2 days, 1 minute", "2 days, 1 minute"); + $this->t->is(expiration_duration(60*60*2*24 + 120), "2 days, 2 minutes", "2 days, 2 minutes"); + + $this->t->is(expiration_duration(60*60 + 60), "1 hour, 1 minute", "1 hour, 1 minute"); + $this->t->is(expiration_duration(60*60 + 120), "1 hour, 2 minutes", "1 hour, 2 minutes"); + $this->t->is(expiration_duration(60*60*2 + 60), "2 hours, 1 minute", "2 hours, 1 minute"); + $this->t->is(expiration_duration(60*60*2 + 120), "2 hours, 2 minutes", "2 hours, 2 minutes"); + + $this->t->is(expiration_duration(61), "1 minute, 1 second", "1 minute, 1 second"); + $this->t->is(expiration_duration(62), "1 minute, 2 seconds", "1 minute, 2 seconds"); + $this->t->is(expiration_duration(121), "2 minutes, 1 second", "2 minutes, 1 second"); + $this->t->is(expiration_duration(122), "2 minutes, 2 seconds", "2 minutes, 2 seconds"); + + $this->t->is(expiration_duration(60*60*24 + 60*60*23 + 60*59), "1 day, 23 hours, 59 minutes", "1 day, 23 hours, 59 minutes"); + $this->t->is(expiration_duration(60*60*23 + 60*59), "23 hours, 59 minutes", "23 hours, 59 minutes"); + $this->t->is(expiration_duration(60*60*2 + 60*59), "2 hours, 59 minutes", "2 hours, 59 minutes"); + } + + public function test_format_bytes() + { + $this->t->is(format_bytes(500), "500B", "500B"); + $this->t->is(format_bytes(1500), "1500B", "1500B"); + $this->t->is(format_bytes(1500*1024), "1500.00KiB", "1500.00KiB"); + $this->t->is(format_bytes(1500*1024*1024), "1500.00MiB", "1500.00MiB"); + $this->t->is(format_bytes(1500*1024*1024*1024), "1500.00GiB", "1500.00GiB"); + $this->t->is(format_bytes(1500*1024*1024*1024*1024), "1500.00TiB", "1500.00TiB"); + $this->t->is(format_bytes(1500*1024*1024*1024*1024*1024), "1500.00PiB", "1500.00PiB"); + } + + public function test_files_are_equal() + { + $a1 = FCPATH.'/data/tests/message1.bin'; + $a2 = FCPATH.'/data/tests/message2.bin'; + $b = FCPATH.'/data/tests/simple.pdf'; + $this->t->is(files_are_equal($a1, $a2), false, "Same hash, but different file"); + $this->t->is(files_are_equal($a1, $b), false, "Different filesize"); + $this->t->is(files_are_equal($a1, $a1), true, "Same file"); + $this->t->is(files_are_equal($a2, $a2), true, "Same file"); + } + + public function test_return_bytes() + { + $this->t->is(return_bytes("1k"), 1*1024, "1k"); + $this->t->is(return_bytes("1M"), 1*1024*1024, "1M"); + $this->t->is(return_bytes("1G"), 1*1024*1024*1024, "1G"); + + try { + return_bytes("1P"); + } catch (\exceptions\ApiException $e) { + $this->t->is($e->get_error_id(), 'filebin-helper/invalid-input-unit', "unhandled text: 1P"); + } + + $this->t->is(return_bytes("106954752"), 106954752, "value without unit is returned as int"); + } +} diff --git a/application/test/tests/test_libraries_exif.php b/application/test/tests/test_libraries_exif.php new file mode 100644 index 000000000..3ca821c03 --- /dev/null +++ b/application/test/tests/test_libraries_exif.php @@ -0,0 +1,48 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_exif extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_get_exif_jpeg() + { + $ret = \libraries\Exif::get_exif(FCPATH.'/data/tests/exif-orientation-examples/Portrait_1.jpg'); + + $this->t->is($ret['Orientation'], 1, "Get correct EXIF Orientation"); + $this->t->is($ret['FileName'], "Portrait_1.jpg", "Get correct EXIF FileName"); + } + + public function test_get_exif_invalidTypes() + { + $ret = \libraries\Exif::get_exif(FCPATH.'/data/tests/black_white.png'); + $this->t->is($ret, false, "PNG not supported"); + } + + public function test_get_exif_missingFile() + { + $ret = \libraries\Exif::get_exif(FCPATH.'/data/tests/thisFileDoesNotExist'); + $this->t->is($ret, false, "Should return false for missing file"); + } + +} + diff --git a/application/test/tests/test_libraries_image.php b/application/test/tests/test_libraries_image.php new file mode 100644 index 000000000..d6afc64df --- /dev/null +++ b/application/test/tests/test_libraries_image.php @@ -0,0 +1,110 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_image extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_type_supported_normalCase() + { + $this->t->is(\libraries\Image::type_supported('image/png'), true, 'image/png should be supported'); + $this->t->is(\libraries\Image::type_supported('image/jpeg'), true, 'image/jpeg should be supported'); + + $this->t->is(\libraries\Image::type_supported('application/pdf'), false, 'application/pdf should not be supported'); + $this->t->is(\libraries\Image::type_supported('application/octet-stream'), false, 'application/octet-stream should not be supported'); + $this->t->is(\libraries\Image::type_supported('text/plain'), false, 'text/plain should not be supported'); + } + + public function test_makeThumb_PNG() + { + $img = new \libraries\Image(FCPATH."/data/tests/black_white.png"); + $img->makeThumb(150, 150); + $thumb = $img->get(IMAGETYPE_PNG); + + $this->t->ok($thumb !== "", "Got thumbnail"); + } + + public function test_makeThumb_PDF() + { + try { + $img = new \libraries\Image(FCPATH."/data/tests/simple.pdf"); + $this->t->fail("PDF should not be supported"); + $img->makeThumb(150, 150); + $thumb = $img->get(IMAGETYPE_JPEG); + $this->t->ok($thumb !== "", "Got thumbnail"); + } catch (\exceptions\PublicApiException $e) { + $correct_error = $e->get_error_id() == "libraries/Image/unsupported-image-type"; + $this->t->ok($correct_error, "Should get exception"); + if (!$correct_error) { + // @codeCoverageIgnoreStart + throw $e; + // @codeCoverageIgnoreEnd + } + } + } + + public function test_makeThumb_binaryFile() + { + try { + $img = new \libraries\Image(FCPATH."/data/tests/message1.bin"); + } catch (\exceptions\PublicApiException $e) { + $correct_error = $e->get_error_id() == "libraries/Image/unsupported-image-type"; + $this->t->ok($correct_error, "Should get exception"); + if (!$correct_error) { + // @codeCoverageIgnoreStart + throw $e; + // @codeCoverageIgnoreEnd + } + } + } + + public function test_get_exif_orientation() + { + $ret = \libraries\Image::get_exif_orientation(FCPATH."/data/tests/black_white.png"); + $this->t->is($ret, 0, "Got correct Orientation for image without orientation information"); + + foreach ([1,2,3,4,5,6,7,8] as $orientation) { + $ret = \libraries\Image::get_exif_orientation(FCPATH."/data/tests/exif-orientation-examples/Landscape_$orientation.jpg"); + $this->t->is($ret, $orientation, "Got correct Orientation for Landscape_$orientation.jpg"); + + $ret = \libraries\Image::get_exif_orientation(FCPATH."/data/tests/exif-orientation-examples/Portrait_$orientation.jpg"); + $this->t->is($ret, $orientation, "Got correct Orientation for Portrait_$orientation.jpg"); + } + } + + public function test_makeThumb_differentOrientation() + { + foreach ([1,2,3,4,5,6,7,8] as $orientation) { + $img = new \libraries\Image(FCPATH."/data/tests/exif-orientation-examples/Landscape_$orientation.jpg"); + $img->makeThumb(100, 100); + $thumb = $img->get(); + $this->t->ok($thumb != '', "Got thumbnail for Landscape_$orientation.jpg"); + + $img = new \libraries\Image(FCPATH."/data/tests/exif-orientation-examples/Portrait_$orientation.jpg"); + $img->makeThumb(100, 100); + $thumb = $img->get(); + $this->t->ok($thumb != '', "Got thumbnail for Portrait_$orientation.jpg"); + } + } + +} + diff --git a/application/test/tests/test_libraries_output_cache.php b/application/test/tests/test_libraries_output_cache.php new file mode 100644 index 000000000..3668bc6b4 --- /dev/null +++ b/application/test/tests/test_libraries_output_cache.php @@ -0,0 +1,82 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_output_cache extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_add() + { + $oc = new \libraries\Output_cache(); + $oc->add("teststring"); + + ob_start(); + $oc->render(); + $output = ob_get_clean(); + + $this->t->is($output, "teststring", "Simple add renders correctly"); + } + + public function test_add_function() + { + $oc = new \libraries\Output_cache(); + $oc->add_function(function() {echo "teststring";}); + + ob_start(); + $oc->render(); + $output = ob_get_clean(); + + $this->t->is($output, "teststring", "Simple add_function renders correctly"); + } + + public function test_add_merge() + { + + $oc = new \libraries\Output_cache(); + $oc->add_merge(['items' => ["test1\n"]], 'tests/echo-fragment'); + $oc->add_merge(['items' => ["test2\n"]], 'tests/echo-fragment'); + + ob_start(); + $oc->render(); + $output = ob_get_clean(); + + $this->t->is($output, "listing 2 items:\ntest1\ntest2\n", "Simple add renders correctly"); + } + + public function test_add_merge_mixedViews() + { + + $oc = new \libraries\Output_cache(); + $oc->add_merge(['items' => ["test1\n"]], 'tests/echo-fragment'); + $oc->add_merge(['items' => ["test2\n"]], 'tests/echo-fragment'); + $oc->add("blub\n"); + $oc->add_merge(['items' => ["test3\n"]], 'tests/echo-fragment'); + + ob_start(); + $oc->render(); + $output = ob_get_clean(); + + $this->t->is($output, "listing 2 items:\ntest1\ntest2\nblub\nlisting 1 items:\ntest3\n", "Simple add renders correctly"); + } + +} + diff --git a/application/test/tests/test_libraries_procrunner.php b/application/test/tests/test_libraries_procrunner.php new file mode 100644 index 000000000..b077f991c --- /dev/null +++ b/application/test/tests/test_libraries_procrunner.php @@ -0,0 +1,118 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_procrunner extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_exec_true() + { + $p = new \libraries\ProcRunner(['true']); + $ret = $p->exec(); + + $this->t->is($ret['stderr'], '', 'stderr should be empty'); + $this->t->is($ret['stdout'], '', 'stdout should be empty'); + $this->t->is($ret['return_code'], 0, 'return code should be 0'); + } + + public function test_exec_false() + { + $p = new \libraries\ProcRunner(['false']); + $ret = $p->exec(); + + $this->t->is($ret['stderr'], '', 'stderr should be empty'); + $this->t->is($ret['stdout'], '', 'stdout should be empty'); + $this->t->is($ret['return_code'], 1, 'return code should be 1'); + } + + public function test_exec_nonexistent() + { + $p = new \libraries\ProcRunner(['thisCommandDoesNotExist']); + $ret = $p->exec(); + + $this->t->is($ret['stderr'], "sh: thisCommandDoesNotExist: command not found\n", 'stderr should be empty'); + $this->t->is($ret['stdout'], '', 'stdout should be empty'); + $this->t->is($ret['return_code'], 127, 'return code should be 127'); + } + + public function test_exec_tac() + { + + $line1 = "this is the first line"; + $line2 = "and this is the second one"; + $input = "$line1\n$line2\n"; + $output = "$line2\n$line1\n"; + + $p = new \libraries\ProcRunner(['tac']); + $p->input($input); + $ret = $p->exec(); + + $this->t->is($ret['stderr'], '', 'stderr should be empty'); + $this->t->is($ret['stdout'], $output, 'stdout should be reversed'); + $this->t->is($ret['return_code'], 0, 'return code should be 0'); + } + + public function test_forbid_nonzero() + { + $p = new \libraries\ProcRunner(['false']); + $p->forbid_nonzero(); + + try { + $p->exec(); + $this->t->ok(false, "this should have caused an an exception"); + } catch (\exceptions\ApiException $e) { + $this->t->is($e->get_error_id(), 'procrunner/non-zero-exit', "correct exception triggered"); + $this->t->is_deeply($e->get_data(), [ + "'false'", + null, + [ + 'return_code' => 1, + 'stdout' => '', + 'stderr' => '', + ], + ], "correct exception data"); + } + } + + public function test_forbid_stderr() + { + $p = new \libraries\ProcRunner(['bash', '-c', 'echo "This is a test error message" >&2; exit 2;']); + $p->forbid_stderr(); + + try { + $p->exec(); + $this->t->ok(false, "this should have caused an an exception"); + } catch (\exceptions\ApiException $e) { + $this->t->is($e->get_error_id(), 'procrunner/stderr', "correct exception triggered"); + $this->t->is_deeply($e->get_data(), [ + "'bash' '-c' 'echo \"This is a test error message\" >&2; exit 2;'", + null, + [ + 'return_code' => 2, + 'stdout' => '', + 'stderr' => "This is a test error message\n", + ], + ], "correct exception data"); + } + } +} + diff --git a/application/test/tests/test_libraries_pygments.php b/application/test/tests/test_libraries_pygments.php new file mode 100644 index 000000000..2e6e8447f --- /dev/null +++ b/application/test/tests/test_libraries_pygments.php @@ -0,0 +1,115 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_pygments extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_autodetect_lexer_normalCase() + { + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'stdin'); + $this->t->is($p->autodetect_lexer(), 'text', "text/plain should be text"); + + $p = new \libraries\Pygments('/invalid/filepath', 'application/x-php', 'stdin'); + $this->t->is($p->autodetect_lexer(), 'php', "application/php should be php"); + + // This is from pygments and not our hardcoded list + $p = new \libraries\Pygments('/invalid/filepath', 'text/x-pascal', 'stdin'); + $this->t->is($p->autodetect_lexer(), 'delphi', "text/x-pascal should be delphi"); + + $p = new \libraries\Pygments('/invalid/filepath', 'application/octet-stream', 'stdin'); + $this->t->is($p->autodetect_lexer(), false, "application/octet-stream should return false"); + } + + public function test_autodetect_lexer_specialFilenames() + { + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'foo.c'); + $this->t->is($p->autodetect_lexer(), 'c', "foo.c should be c"); + + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'PKGBUILD'); + $this->t->is($p->autodetect_lexer(), 'bash', "PKGBUILD should be bash"); + + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'asciinema.json'); + $this->t->is($p->autodetect_lexer(), 'asciinema', "asciinema.json should be asciinema"); + + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'test.asciinema.json'); + $this->t->is($p->autodetect_lexer(), 'asciinema', "asciinema.json should be asciinema"); + } + + public function test_autodetect_lexer_specialFilenamesBinaryShouldNotHighlight() + { + $p = new \libraries\Pygments('/invalid/filepath', 'application/octet-stream', 'foo.c'); + $this->t->is($p->autodetect_lexer(), false, "foo.c should not highlight if binary"); + + $p = new \libraries\Pygments('/invalid/filepath', 'application/octet-stream', 'PKGBUILD'); + $this->t->is($p->autodetect_lexer(), false, "PKGBUILD should not highlight if binary"); + } + + public function test_can_highlight_normalCase() + { + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'stdin'); + $this->t->is($p->can_highlight(), true, "text/plain can highlight"); + + $p = new \libraries\Pygments('/invalid/filepath', 'application/x-php', 'stdin'); + $this->t->is($p->can_highlight(), true, "application/x-php can highlight"); + + $p = new \libraries\Pygments('/invalid/filepath', 'application/octet-stream', 'stdin'); + $this->t->is($p->can_highlight(), false, "application/octet-stream can not highlight"); + } + + public function test_autodetect_lexer_canButShouldntHighlight() + { + $p = new \libraries\Pygments('/invalid/filepath', 'image/svg+xml', 'foo.svg'); + $this->t->is($p->autodetect_lexer(), false, "image/svg+xml should return false"); + } + + public function test_can_highlight_canButShouldntHighlight() + { + $p = new \libraries\Pygments('/invalid/filepath', 'image/svg+xml', 'foo.svg'); + $this->t->is($p->can_highlight(), true, "image/svg+xml can highlight"); + } + + public function test_autodetect_lexer_strangeFilenames() + { + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'foo.'); + $this->t->is($p->autodetect_lexer(), 'text', "foo. should be text"); + + } + + public function test_get_lexers() + { + $l = \libraries\Pygments::get_lexers(); + + $this->t->is($l['text'], 'Plain text', 'Plain text lexer exists'); + $this->t->is($l['c'], 'C', 'C lexer exists'); + } + + public function test_resolve_lexer_alias() + { + $p = new \libraries\Pygments('/invalid/filepath', 'text/plain', 'foo.pl'); + $this->t->is($p->resolve_lexer_alias('pl'), 'perl', "Test pl alias for perl"); + + $this->t->is($p->resolve_lexer_alias('thisIsInvalid'), 'thisIsInvalid', "Test invalid alias"); + } + +} + diff --git a/application/test/tests/test_libraries_tempfile.php b/application/test/tests/test_libraries_tempfile.php new file mode 100644 index 000000000..f4c10a22e --- /dev/null +++ b/application/test/tests/test_libraries_tempfile.php @@ -0,0 +1,46 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_libraries_tempfile extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_destructor_normalCase() + { + $t = new \libraries\Tempfile(); + $file = $t->get_file(); + $this->t->is(file_exists($file), true, "file should exist"); + unset($t); + $this->t->is(file_exists($file), false, "file should no longer exist after destruction of object"); + } + + public function test_destructor_alreadyRemoved() + { + $t = new \libraries\Tempfile(); + $file = $t->get_file(); + $this->t->is(file_exists($file), true, "file should exist"); + unlink($file); + $this->t->is(file_exists($file), false, "file deleted"); + unset($t); + $this->t->is(file_exists($file), false, "file should no longer exist after destruction of object"); + } +} diff --git a/application/test/tests/test_models_muser.php b/application/test/tests/test_models_muser.php new file mode 100644 index 000000000..af0e58834 --- /dev/null +++ b/application/test/tests/test_models_muser.php @@ -0,0 +1,113 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_models_muser extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + } + + public function cleanup() + { + } + + public function test_valid_username() + { + $CI =& get_instance(); + + $this->t->is($CI->muser->valid_username("thisisbob42"), true, "valid username"); + $this->t->is($CI->muser->valid_username("31337"), true, "valid username"); + $this->t->is($CI->muser->valid_username("thisisjoe"), true, "valid username"); + $this->t->is($CI->muser->valid_username("1234567890123456789012345678901"), true, "31 chars"); + $this->t->is($CI->muser->valid_username("12345678901234567890123456789012"), true, "32 chars"); + + $this->t->is($CI->muser->valid_username("Joe"), false, "contains uppercase"); + $this->t->is($CI->muser->valid_username("joe_bob"), false, "contains underscore"); + $this->t->is($CI->muser->valid_username("joe-bob"), false, "contains dash"); + $this->t->is($CI->muser->valid_username("123456789012345678901234567890123"), false, "33 chars"); + $this->t->is($CI->muser->valid_username("1234567890123456789012345678901234"), false, "34 chars"); + } + + public function test_valid_email() + { + $CI =& get_instance(); + + $this->t->is($CI->muser->valid_email("joe@bob.com"), true, "valid email"); + $this->t->is($CI->muser->valid_email("joe+mailbox@bob.com"), true, "valid email"); + $this->t->is($CI->muser->valid_email("bob@fancyaddress.net"), true, "valid email"); + + $this->t->is($CI->muser->valid_email("joebob.com"), false, "missing @"); + } + + public function test_delete_user() + { + $CI =& get_instance(); + $CI->muser->add_user("userdeltest1", "supersecret", "tester@localhost.lan", null); + $this->t->is($CI->muser->username_exists("userdeltest1"), true, "User should exist after creation"); + + $ret = $CI->muser->delete_user("userdeltest1", "wrongpassword"); + $this->t->is($ret, false, "Deletion should fail with incorrect password"); + + $ret = $CI->muser->delete_user("userdeltest1", ""); + $this->t->is($ret, false, "Deletion should fail with empty password"); + + $this->t->is($CI->muser->username_exists("userdeltest1"), true, "User should exist after failed deletions"); + + $ret = $CI->muser->delete_user("userdeltest1", "supersecret"); + $this->t->is($ret, true, "Deletion should succeed with correct data"); + $this->t->is($CI->muser->username_exists("userdeltest1"), false, "User should not exist after deletion"); + } + + public function test_delete_user_verifyFilesDeleted() + { + $CI =& get_instance(); + + $id = "testid1"; + $id2 = "testid2"; + $content = "test content"; + $filename = "some cool name"; + $username = "testuser1"; + $password = "testpass"; + + $CI->muser->add_user($username, $password, "tester@localhost.lan", null); + $userid = $CI->muser->get_userid_by_name($username); + + $CI->muser->add_user("joe", "joeisawesome", "tester2@localhost.lan", null); + $userid2 = $CI->muser->get_userid_by_name("joe"); + + \service\files::add_file_data($userid, $id, $content, $filename); + \service\files::add_file_data($userid2, $id2, $content, $filename); + + $mid = \service\files::create_multipaste([$id], $userid, [3,6])['url_id']; + $mid2 = \service\files::create_multipaste([$id2], $userid2, [3,6])['url_id']; + + $this->t->is($CI->mfile->id_exists($id), true, "File exists after being added"); + $this->t->is($CI->mmultipaste->id_exists($mid), true, "Multipaste exists after creation"); + $this->t->is($CI->mfile->id_exists($id2), true, "File2 exists after being added"); + $this->t->is($CI->mmultipaste->id_exists($mid2), true, "Multipaste2 exists after creation"); + + $ret = $CI->muser->delete_user($username, $password); + $this->t->is($ret, true, "Delete user"); + + $this->t->is($CI->mfile->id_exists($id), false, "File should be gone after deletion of user"); + $this->t->is($CI->mmultipaste->id_exists($mid), false, "Multipaste should be gone after deletion of user"); + $this->t->is($CI->mfile->id_exists($id2), true, "File2 owned by different user should still exist after deletion from other user"); + $this->t->is($CI->mmultipaste->id_exists($mid2), true, "Multipaste2 owned by different user should still exist after deletion from other user"); + } + + +} + diff --git a/application/test/tests/test_service_files.php b/application/test/tests/test_service_files.php new file mode 100644 index 000000000..b9d6fce66 --- /dev/null +++ b/application/test/tests/test_service_files.php @@ -0,0 +1,121 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_files extends \test\Test { + + public function __construct() + { + parent::__construct(); + + $CI =& get_instance(); + $CI->load->model("muser"); + $CI->load->model("mfile"); + + } + + public function test_verify_uploaded_files_noFiles() + { + $a = array(); + try { + \service\files::verify_uploaded_files($a); + // @codeCoverageIgnoreStart + $this->t->fail("verify should error"); + // @codeCoverageIgnoreEnd + } catch (\exceptions\UserInputException $e) { + $this->t->is($e->get_error_id(), "file/no-file", "verify should error"); + } + } + + public function test_verify_uploaded_files_normal() + { + $CI =& get_instance(); + $a = array( + array( + "name" => "foobar.txt", + "type" => "text/plain", + "tmp_name" => NULL, + "error" => UPLOAD_ERR_OK, + "size" => 1, + "formfield" => "file[1]", + ) + ); + + \service\files::verify_uploaded_files($a); + $this->t->pass("verify should work"); + } + + public function test_verify_uploaded_files_uploadError() + { + $CI =& get_instance(); + $a = array( + array( + "name" => "foobar.txt", + "type" => "text/plain", + "tmp_name" => NULL, + "error" => UPLOAD_ERR_NO_FILE, + "size" => 1, + "formfield" => "file[1]", + ) + ); + + try { + \service\files::verify_uploaded_files($a); + // @codeCoverageIgnoreStart + $this->t->fail("verify should error"); + // @codeCoverageIgnoreEnd + } catch (\exceptions\UserInputException $e) { + $data = $e->get_data(); + $this->t->is($e->get_error_id(), "file/upload-verify", "verify should error"); + $this->t->is_deeply(array( + 'file[1]' => array( + 'filename' => 'foobar.txt', + 'formfield' => 'file[1]', + 'message' => 'No file was uploaded', + ), + ), $data, "expected data in exception"); + } + } + + public function test_ellipsize() + { + $a1 = "abc"; + $a2 = "abc\nabc"; + $a3 = "abc\nabc\nabc"; + $a4 = "abc\nabc\nabc\nabc"; + + $this->t->is(\service\files::ellipsize($a1, 1, strlen($a1)), + $a1, "Trim 1 line to 1, no change"); + + $this->t->is(\service\files::ellipsize($a3, 3, strlen($a3)), + $a3, "Trim 3 lines to 3, no change"); + + $this->t->is(\service\files::ellipsize($a3, 5, strlen($a3)), + $a3, "Trim 3 lines to 5, no change"); + + $this->t->is(\service\files::ellipsize($a2, 1, strlen($a2)), + "$a1\n...", "Trim 2 lines to 1, drop one line"); + + $this->t->is(\service\files::ellipsize($a3, 2, strlen($a3)), + "$a2\n...", "Trim 3 lines to 2, drop one line"); + + $this->t->is(\service\files::ellipsize($a4, 2, strlen($a4)), + "$a2\n...", "Trim 4 lines to 2, drop 2 lines"); + + $this->t->is(\service\files::ellipsize($a3, 3, strlen($a3) + 1), + "$a2\n...", "Last line incomplete, drop one line"); + + $this->t->is(\service\files::ellipsize($a1, 5, strlen($a1) + 1), + "$a1 ...", "Single line incomplete, only add dots"); + } + + +} + diff --git a/application/test/tests/test_service_files_valid_id.php b/application/test/tests/test_service_files_valid_id.php new file mode 100644 index 000000000..24886be43 --- /dev/null +++ b/application/test/tests/test_service_files_valid_id.php @@ -0,0 +1,115 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_files_valid_id extends \test\Test { + private $model; + private $filedata; + private $config; + + public function __construct() + { + parent::__construct(); + + $CI =& get_instance(); + $CI->load->model("muser"); + $CI->load->model("mfile"); + + } + + public function init() + { + $this->model = \Mockery::mock("Mfile"); + $this->model->shouldReceive("delete_id")->never()->byDefault(); + $this->model->shouldReceive("delete_data_id")->never()->byDefault(); + $this->model->shouldReceive("file")->with("file-hash-1-1")->andReturn("/invalid/path/file-1")->byDefault(); + $this->model->shouldReceive("filemtime")->with("/invalid/path/file-1")->andReturn(500)->byDefault(); + $this->model->shouldReceive("filesize")->with("/invalid/path/file-1")->andReturn(50*1024)->byDefault(); + $this->model->shouldReceive("file_exists")->with("/invalid/path/file-1")->andReturn(true)->byDefault(); + + $this->filedata = array( + "data_id" => "file-hash-1-1", + "hash" => "file-hash-1", + "id" => "file-id-1", + "user" => 2, + "date" => 500, + ); + + $this->config = array( + "upload_max_age" => 20, + "sess_expiration" => 10, + "small_upload_size" => 10*1024, + ); + } + + public function cleanup() + { + \Mockery::close(); + } + + public function test_valid_id_keepNormalUpload() + { + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 505); + $this->t->is($ret, true, "normal case should be valid"); + } + + public function test_valid_id_keepSmallUpload() + { + $this->model->shouldReceive("filesize")->with("/invalid/path/file-1")->once()->andReturn(50); + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 550); + $this->t->is($ret, true, "file is old, but small and should be kept"); + } + + public function test_valid_id_removeOldFile() + { + $this->model->shouldReceive("delete_data_id")->with("file-hash-1-1")->once(); + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 550); + $this->t->is($ret, false, "file is old and should be removed"); + } + + public function test_valid_id_removeOldUpload() + { + $this->model->shouldReceive("delete_id")->with("file-id-1")->once(); + $this->model->shouldReceive("filemtime")->with("/invalid/path/file-1")->once()->andReturn(540); + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 550); + $this->t->is($ret, false, "upload is old and should be removed"); + } + + public function test_valid_id_keepNormalUnownedFile() + { + $this->filedata["user"] = 0; + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 505); + $this->t->is($ret, true, "upload is unowned and should be kept"); + } + + public function test_valid_id_removeOldUnownedFile() + { + $this->model->shouldReceive("delete_id")->with("file-id-1")->once(); + $this->filedata["user"] = 0; + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 515); + $this->t->is($ret, false, "upload is old, unowned and should be removed"); + } + + public function test_valid_id_removeMissingFile() + { + $this->model->shouldReceive("file_exists")->with("/invalid/path/file-1")->once()->andReturn(false); + $this->model->shouldReceive("delete_data_id")->with("file-hash-1-1")->once(); + + $ret = \service\files::valid_id($this->filedata, $this->config, $this->model, 505); + $this->t->is($ret, false, "missing file should be removed"); + } + +} + diff --git a/application/test/tests/test_service_multipaste_queue.php b/application/test/tests/test_service_multipaste_queue.php new file mode 100644 index 000000000..cab53e335 --- /dev/null +++ b/application/test/tests/test_service_multipaste_queue.php @@ -0,0 +1,93 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_multipaste_queue extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + $this->session = \Mockery::mock("Session"); + $this->session->shouldReceive("userdata")->never()->byDefault(); + $this->session->shouldReceive("set_userdata")->never()->byDefault(); + + $this->mfile = \Mockery::mock("Mfile"); + $this->mfile->shouldReceive("valid_id")->never()->byDefault(); + + $this->mmultipaste = \Mockery::mock("Mmultipaste"); + $this->mmultipaste->shouldReceive("valid_id")->never()->byDefault(); + + $this->m = new \service\multipaste_queue($this->session, $this->mfile, $this->mmultipaste); + } + + public function cleanup() + { + \Mockery::close(); + } + + public function test_get() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(null); + $this->t->is_deeply($this->m->get(), [], "Fresh queue is empty"); + } + + public function test_set() + { + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + + $this->t->is($this->m->set(['abc', '123']), null, "set() should succeed"); + } + + public function test_append() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(null); + $this->mfile->shouldReceive('valid_id')->with('abc')->times(2)->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc'])->once(); + $this->t->is($this->m->append(['abc']), null, "append([abc]) should succeed"); + + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(['abc']); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['123']), null, "append([123]) should succeed"); + } + + public function test_append_itemAlreadyInQueue() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(['abc', '123']); + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['abc']), null, "append([abc]) should succeed"); + } + + public function test_append_multipaste() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn([]); + $this->mmultipaste->shouldReceive('valid_id')->with('m-abc')->once()->andReturn(true); + $this->mmultipaste->shouldReceive('get_files')->with('m-abc')->once()->andReturn([ + ['id' => 'abc'], + ['id' => '123'], + ]); + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['m-abc']), null, "append([m-abc]) should succeed"); + } + + +} + diff --git a/application/test/tests/test_service_storage.php b/application/test/tests/test_service_storage.php new file mode 100644 index 000000000..83284ea4b --- /dev/null +++ b/application/test/tests/test_service_storage.php @@ -0,0 +1,172 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_storage extends \test\Test { + + private $tempdir; + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + $this->tempdir = trim((new \libraries\ProcRunner(['mktemp', '-d']))->execSafe()['stdout']); + } + + public function cleanup() + { + rmdir($this->tempdir); + } + + public function test_normalCase() + { + $file = $this->tempdir.'/testfile1'; + $storage = new \service\storage($file); + + $this->t->is($storage->get_file(), $file, "get_file returns correct path"); + + $a = $storage->begin(); + file_put_contents($a, "teststring1"); + $this->t->is(file_exists($file), false, "Test file doesn't exist yet"); + $this->t->is($storage->exists(), false, "Test file doesn't exist yet"); + + $storage->commit(); + $this->t->is(file_exists($file), true, "Test file has been created"); + $this->t->is(file_get_contents($file), 'teststring1', "Test file has correct content"); + + unlink($file); + } + + public function test_existingFile() + { + $file = $this->tempdir.'/testfile-existing-file'; + file_put_contents($file, "teststring-old"); + + $storage = new \service\storage($file); + + $a = $storage->begin(); + file_put_contents($a, "teststring-changed"); + $this->t->is(file_exists($file), true, "Test file already exists"); + $this->t->is(file_get_contents($file), 'teststring-old', "Test file has old content"); + + $storage->commit(); + $this->t->is(file_exists($file), true, "Test file still exists"); + $this->t->is(file_get_contents($file), 'teststring-changed', "Test file has updated content"); + + unlink($file); + } + + public function test_rollback() + { + $file = $this->tempdir.'/testfile-rollback'; + file_put_contents($file, "teststring-old"); + + $storage = new \service\storage($file); + + $a = $storage->begin(); + file_put_contents($a, "teststring-changed"); + $this->t->is(file_exists($file), true, "Test file already exists"); + $this->t->is(file_get_contents($file), 'teststring-old', "Test file has old content"); + + $storage->rollback(); + $this->t->is(file_exists($file), true, "Test file still exists"); + $this->t->is(file_get_contents($file), 'teststring-old', "Test file still has old content"); + + unlink($file); + } + + public function test_gzip_compress() + { + $file = $this->tempdir.'/testfile-gzip'; + file_put_contents($file, "teststring-old"); + + $storage = new \service\storage($file); + + $a = $storage->begin(); + $new_string = str_repeat("teststring-changed", 500); + file_put_contents($a, $new_string); + + $ret = $storage->gzip_compress(); + $this->t->is($ret, true, "Compression succeeded"); + + $this->t->is(file_exists($file), true, "Test file still exists"); + $this->t->is(file_get_contents($file), 'teststring-old', "Test file still has old content"); + + $storage->commit(); + + ob_start(); + readgzfile($file); + $file_content = ob_get_clean(); + + $this->t->is_deeply($new_string, $file_content, "File is compressed and has correct content"); + + unlink($file); + } + + public function test_unlink() + { + $file = $this->tempdir.'/testfile-unlink'; + file_put_contents($file, "teststring-old"); + + $storage = new \service\storage($file); + $this->t->is(file_exists($file), true, "Test file exists"); + $storage->unlink(); + $this->t->is(file_exists($file), false, "Test file has been removed"); + } + + public function test_unlink_missingFile() + { + $file = $this->tempdir.'/testfile-unlink'; + + $storage = new \service\storage($file); + $this->t->is(file_exists($file), false, "Test file does nto exist"); + $storage->unlink(); + $this->t->is(file_exists($file), false, "Test file still doesn't exist"); + } + + public function test_begin_calledMultipleTimes() + { + $file = $this->tempdir.'/testfile-begin-multi'; + file_put_contents($file, "teststring-old"); + + $storage = new \service\storage($file); + $a = $storage->begin(); + file_put_contents($a, "blub"); + + $b = $storage->begin(); + file_put_contents($b, "second write"); + + $storage->commit(); + $this->t->is(file_get_contents($file), "second write", "File contains second write"); + + unlink($file); + } + + public function test_begin_creationOfDir() + { + $dir = $this->tempdir.'/testdir/'; + $file = $dir.'testfile'; + $storage = new \service\storage($file); + + $this->t->is(is_dir($dir), false, "Directory does not exist"); + + $a = $storage->begin(); + + $this->t->is(is_dir($dir), true, "Directory exists"); + + $storage->rollback(); + rmdir($dir); + } + +} + diff --git a/application/test/tests/test_service_user.php b/application/test/tests/test_service_user.php new file mode 100644 index 000000000..d7e34a71b --- /dev/null +++ b/application/test/tests/test_service_user.php @@ -0,0 +1,65 @@ +<?php +/* + * Copyright 2018 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_user extends \test\Test { + + public function __construct() { + parent::__construct(); + } + + public function init() { + } + + public function cleanup() { + } + + public function test_invitation_key_delete() { + $CI =& get_instance(); + + $userid = 1; + + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([], $result, "database contains no actions"); + + $key = \service\user::create_invitation_key($userid); + + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([['user' => "".$userid, 'key' => $key, 'action' => 'invitation']], $result, "database contains new key"); + + $ret = \service\user::delete_invitation_key($userid+1, $key); + $this->t->is(0, $ret, "Should have removed no keys because incorrect user/key"); + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([['user' => "".$userid, 'key' => $key, 'action' => 'invitation']], $result, "database contains new key after incorrect deletion"); + + $ret = \service\user::delete_invitation_key($userid+1, "foobar-"); + $this->t->is(0, $ret, "Should have removed no keys because incorrect user/key"); + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([['user' => "".$userid, 'key' => $key, 'action' => 'invitation']], $result, "database contains new key after incorrect deletion"); + + $ret = \service\user::delete_invitation_key($userid+1, ""); + $this->t->is(0, $ret, "Should have removed no keys because incorrect user/key"); + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([['user' => "".$userid, 'key' => $key, 'action' => 'invitation']], $result, "database contains new key after incorrect deletion"); + + $ret = \service\user::delete_invitation_key($userid, ""); + $this->t->is(0, $ret, "Should have removed no keys because incorrect user/key"); + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([['user' => "".$userid, 'key' => $key, 'action' => 'invitation']], $result, "database contains new key"); + + $ret = \service\user::delete_invitation_key($userid, $key); + $this->t->is(1, $ret, "One key should be removed"); + $result = $CI->db->select('user, key, action')->from('actions')->get()->result_array(); + $this->t->is_deeply([], $result, "key has been deleted"); + + } + +} + diff --git a/application/third_party/QrCode b/application/third_party/QrCode new file mode 160000 +Subproject 4638f11b6944cccce997db7fa7508b5a7ad1a61 diff --git a/application/third_party/mockery b/application/third_party/mockery new file mode 160000 +Subproject d141c5b1b302d4d746e38ccc95ffe215129a855 diff --git a/application/third_party/parsedown b/application/third_party/parsedown new file mode 160000 +Subproject 490a8f35a4163f59230f53c34f1fbb22a864c01 diff --git a/application/third_party/test-more-php/Test-More-OO.php b/application/third_party/test-more-php/Test-More-OO.php new file mode 100755 index 000000000..7ce9f9421 --- /dev/null +++ b/application/third_party/test-more-php/Test-More-OO.php @@ -0,0 +1,455 @@ +<?php +/* + Test-More-OO.php: + A workalike of Perl's Test::More for PHP. + + Why Test-More? + Test-More is a great way to start testing RIGHT NOW. + + Why ok and not ok? + Test-More produces TAP compliant output. + For more on TAP, see: http://testanything.org + For the TAP spec, see: http://search.cpan.org/dist/TAP/TAP.pm + + Other testing libraries: + You can replace Test-Simple with Test-More without making any changes + to existing test code, providing access to further testing methods. + You can also replace any other PHP Test::More workalike library out there + with Test-More.php and it will work without making any changes to the code. + + Assertions: + produce TAP output + provide testing functions + exit with error code: + 0 all tests successful + 255 test died or all passed but wrong # of tests run + any other number how many failed (including missing or extras) + + Example: + require_once('Test-More-OO.php'); + $t = new TestMore(); + $t->plan(2); + $t->ok(1 + 1 = 2, 'One plus one equals two'); + $t->ok( doSomethingAndReturnTrue() , 'doSomethingAndReturnTrue() successful'); + + Procedural Example: + require_once('Test-More.php'); + plan(2); + ok(1 + 1 = 2, 'One plus one equals two'); + ok( doSomethingAndReturnTrue() , 'doSomethingAndReturnTrue() successful'); + + From a browser + If you are running Test-Simple on a web server and want slightly more web-readable + output, call the web_output() method/function. + + Updates + Updates will be posted to the Google code page: + http://code.google.com/p/test-more-php/ + + Bugs + Please file bug reports via the Issues tracker at the google code page. + + Acknowledgements + Michael G Schwern: http://search.cpan.org/~mschwern/Test-Simple/ + Chris Shiflet: http://shiflett.org/code/test-more.php + + Author + Copyright RJ Herrick <RJHerrick@beyondlogical.net> 2009, 2010 + +*/ + +require_once('Test-Simple-OO.php'); + +class TestMore extends TestSimple { + +/* Test-More extensions */ + private $interp; + + function pass ($name = NULL) { + return $this->ok(TRUE, $name); + } + + function fail ($name = NULL) { + return $this->ok(FALSE, $name); + } + + function _compare ($operator, $thing1, $thing2, $name = NULL) { + // Test.php's cmp_ok function accepts coderefs, hmmm + + $result = eval("return (\$thing1 $operator \$thing2);"); + + return $this->ok($result, $name); + } + + private function dumpvar($a) { + ob_start(); + var_dump($a); + $ret = ob_get_clean(); + $ret = preg_replace("/^[^\n]*\n/", "", $ret); + $ret = preg_replace("/\n$/", "", $ret); + # replace unprintable characters with questionmarks + $old = ini_get("mbstring.substitute_character"); + ini_set("mbstring.substitute_character", "?"); + $ret = mb_convert_encoding($ret, 'UTF-8', 'UTF-8'); + ini_set("mbstring.substitute_character", $old); + return $ret; + } + + function is ($thing1, $thing2, $name = NULL) { + $pass = $this->_compare ('===',$thing1,$thing2,$name); + if (!$pass) { + $this->diag(" got: ".$this->dumpvar($thing1)."", + " expected: ".$this->dumpvar($thing2).""); + } + return $pass; + } + + function isnt ($thing1, $thing2, $name = NULL) { + $pass = $this->_compare ('!==',$thing1,$thing2,$name); + if (!$pass) { + $this->diag(" got: '$thing1'", + " expected: '$thing2'"); + } + return $pass; + } + + function like ($string, $pattern, $name = NULL) { + $pass = preg_match($pattern, $string); + + $ok = $this->ok($pass, $name); + + if (!$ok) { + $this->diag(" '$string'"); + $this->diag(" doesn't match '$pattern'"); + } + + return $ok; + } + + function unlike ($string, $pattern, $name = NULL) { + $pass = !preg_match($pattern, $string); + + $ok = $this->ok($pass, $name); + + if (!$ok) { + $this->diag(" '$string'"); + $this->diag(" matches '$pattern'"); + } + + return $ok; + } + + function cmp_ok ($thing1, $operator, $thing2, $name = NULL) { + eval("\$pass = (\$thing1 $operator \$thing2);"); + + ob_start(); + var_dump($thing1); + $_thing1 = trim(ob_get_clean()); + + ob_start(); + var_dump($thing2); + $_thing2 = trim(ob_get_clean()); + + $ok = $this->ok($pass, $name); + + if (!$ok) { + $this->diag(" got: $_thing1"); + $this->diag(" expected: $_thing2"); + } + + return $ok; + } + + function can_ok ($object, $methods) { + $pass = TRUE; + $errors = array(); + if (!is_array($methods)) $methods = array($methods); + + foreach ($methods as $method) { + if (!method_exists($object, $method)) { + $pass = FALSE; + $errors[] = " method_exists(\$object, $method) failed"; + } + } + + $ok = $this->ok($pass, "method_exists(\$object, ...)"); + + if (!$ok) { + $this->diag($errors); + } + + return $ok; + } + + function isa_ok ($object, $expected_class, $object_name = 'The object') { + $got_class = get_class($object); + + if (version_compare(phpversion(), '5', '>=')) { + $pass = ($got_class == $expected_class); + } else { + $pass = ($got_class == strtolower($expected_class)); + } + + if ($pass) { + $ok = $this->ok(TRUE, "$object_name isa $expected_class"); + } else { + $ok = $this->ok(FALSE, "$object_name isn't a '$expected_class' it's a '$got_class'"); + } + + return $ok; + } + + function _include_fatal_error_handler ($buffer) { + + // Finish successfully? Carry on. + if ($buffer === 'included OK') return ''; + + $module = $this->LastModuleTested; + + // Inside ob_start, won't see the output + $this->ok(FALSE,"include $module"); + + $message = trim($buffer); + $unrunmsg = ''; + + if ( is_int($this->NumberOfTests) ) { + $unrun = $this->NumberOfTests - (int)$this->TestsRun; + $plural = $unrun == 1 ? '' : 's'; + $unrunmsg = "# Looks like ${unrun} planned test${plural} never ran.\n"; + } + + $gasp = $this->LastFail . "\n" + . "# Tried to include '$module'\n" + . "# $message\n" + . $unrunmsg + . "# Looks like 1 test aborted before it could finish due to a fatal error!\n" + . "Bail out! Terminating prematurely due to fatal error.\n" + ; + + return $gasp; + } + + function _include_ok ($module,$type) { + $path = null; + $full_path = null; + $retval = 999; + + // Resolve full path, nice to know although only necessary on windows + foreach (explode(PATH_SEPARATOR,get_include_path()) as $prefix) { + // Repeat existance test and find full path + $full_path = realpath($prefix.DIRECTORY_SEPARATOR.$module); + $lines = @file($full_path); + // Stop on success + if ($lines) { + $path = $full_path; + break; + } + } + // Make sure, if we would include it, it's not going to choke on syntax + $error = false; + if ($path) { + @exec('"'.$this->interp().'" -l '.$path, $bunk, $retval); + if ($retval===0) { + // Prep in case we hit error handler + $this->Backtrace = debug_backtrace(); + $this->LastModuleTested = $module; + ob_start(array($this,'_include_fatal_error_handler')); + if ($type === 'include') { + $done = (include $module); + } else if ($type === 'require') { + $done = (require $module); + } else { + $this->bail("Second argument to _include_ok() must be 'require' or 'include'"); + } + echo "included OK"; + ob_end_flush(); + if (!$done) $error = " Unable to $type '$module'"; + } else { + $error = " Syntax check for '$module' failed"; + } + } else { + $error = " Cannot find ${type}d file '$module'"; + } + + $pass = !$retval && $done; + $ok = $this->ok($pass, "$type $module" ); + if ($error) $this->diag($error); + if ($error && $path) $this->diag(" Resolved $module as $full_path"); + return $ok; + } + + function include_ok ($module) { + // Test success of including file, but continue testing if possible even if unable to include + + return $this->_include_ok($module,'include'); + } + + + function require_ok ($module) { + // As include_ok() but exit gracefully if requirement missing + + $ok = $this->_include_ok($module,'require'); + + // Stop testing if we fail a require test + // Not a bail because you asked for it + if ($ok == FALSE) { + $this->diag(" Exiting due to missing requirement."); + throw new RuntimeException("Missing requirement"); + } + + return $ok; + } + + function skip($why, $num) { + + if ($num < 0) $num = 0; + + $this->Skips += $num; + $this->SkipReason = $why; + + return TRUE; + } + + function eq_array ($thing1, $thing2) { + // Deprecated comparison function provided for compatibility + // Look only at values, order is important + $this->diag(" ! eq_array() is a deprecated comparison function provided for compatibility. Use array_diff() or similar instead."); + return !array_diff($thing1, $thing2); + } + + function eq_hash ($thing1, $thing2) { + // Deprecated comparison function provided for compatibility + // Look at keys and values, order is NOT important + $this->diag(" ! eq_hash() is a deprecated comparison function provided for compatibility. Use array_diff() or similar instead."); + return !array_diff_assoc($thing1, $thing2); + } + + function eq_set ($thing1, $thing2, $name = NULL) { + // Deprecated comparison function provided for compatibility + // Look only at values, duplicates are NOT important + $this->diag(" ! eq_set() is a deprecated comparison function provided for compatibility. Use array_diff() or similar instead."); + $a = $thing1; + sort($a); + $b = $thing2; + sort($b); + return !array_diff($a, $b); + } + + function is_deeply ($thing1, $thing2, $name = NULL) { + + $pass = $this->_compare_deeply($thing1, $thing2, $name); + + $ok = $this->ok($pass,$name); + + if (!$ok) { + foreach(array($thing1,$thing2) as $it){ + ob_start(); + var_dump($it); + $dump = ob_get_clean(); + #$stringified[] = implode("\n#",explode("\n",$dump)); + $stringified[] = str_replace("\n","\n# ",$dump); + } + $this->diag(" wanted: ".$stringified[0]); + $this->diag(" got: ".$stringified[1]); + } + + return $ok; + } + + function isnt_deeply ($thing1, $thing2, $name = NULL) { + + $pass = !$this->_compare_deeply($thing1, $thing2, $name); + + $ok = $this->ok($pass,$name); + + if (!$ok) $this->diag("Structures are identical.\n"); + + return $ok; + } + + function _compare_deeply ($thing1, $thing2) { + + if (is_array($thing1) && is_array($thing2)) { + if ((count($thing1) === count($thing2)) && !array_diff_key($thing1,$thing2)) { + foreach(array_keys($thing1) as $key){ + $pass = $this->_compare_deeply($thing1[$key],$thing2[$key]); + if(!$pass) { + return FALSE; + } + } + return TRUE; + + } else { + return FALSE; + } + + } else { + return $thing1 === $thing2; + } + } + + function todo ($why, $howmany) { + // Marks tests as expected to fail, then runs them anyway + + if ($howmany < 0) $howmany = 0; + + $this->Todo = $howmany; + $this->TodoReason = $why; + + return TRUE; + } + + function todo_skip ($why, $howmany) { + // Marks tests as expected to fail, then skips them, as they are expected to also create fatal errors + + $this->todo($why, $howmany); + $this->skip($why, $howmany); + + return TRUE; + } + + function todo_start ($why) { + // as starting a TODO block in Perl- instead of using todo() to set a number of tests, all + // tests until todo_end are expected to fail and run anyway + + $this->TodoBlock = FALSE; + $this->TodoReason = $why; + + return TRUE; + } + + function todo_end () { + // as ending a SKIP block in Perl + + $this->TodoBlock = FALSE; + unset($this->TodoReason); + + return TRUE; + } + + function interp ($new_interp_command=NULL) { + // Return the command used to invoke the PHP interpreter, such as for exec() + + if ($new_interp_command == NULL && $this->interp == '') { + // In some situations you might need to specify a php interpreter. + if ( isset($_SERVER['PHP']) ) { + $new_interp_command = escapeshellcmd($_SERVER['PHP']); + } else { + $new_interp_command = 'php'; + } + } + if ($new_interp_command != $this->interp) { + $this->interp = $new_interp_command; + + // Check that we can use the interpreter + @exec('"'.$this->interp.'" -v', $bunk, $retval); + if ($retval!==0) $this->bail("Unable to run PHP interpreter with '$this->interp'. Try setting the PHP environmant variable to the path of the interpreter."); + } + + return $this->interp; + } + + +} + +?> diff --git a/application/third_party/test-more-php/Test-More.php b/application/third_party/test-more-php/Test-More.php new file mode 100755 index 000000000..3cdf1ad37 --- /dev/null +++ b/application/third_party/test-more-php/Test-More.php @@ -0,0 +1,41 @@ +<?php +/* + Procedural interface wrapper for Test-More-OO.php. + See Test-More-OO.php for documentation. +*/ + +require_once('Test-More-OO.php'); + +global $__Test; +$__Test = new TestMore(); + +// Expose public API for TestMore methods as functions +function plan() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'plan'),$args); } +function ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'ok'),$args); } +function diag() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'diag'),$args); } +function web_output() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'web_output'),$args); } +function done_testing() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'done_testing'),$args); } +function bail() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'bail'),$args); } +function pass() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'pass'),$args); } +function fail() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'fail'),$args); } +function is() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'is'),$args); } +function isnt() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'isnt'),$args); } +function like() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'like'),$args); } +function unlike() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'unlike'),$args); } +function cmp_ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'cmp_ok'),$args); } +function can_ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'can_ok'),$args); } +function isa_ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'isa_ok'),$args); } +function include_ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'include_ok'),$args); } +function require_ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'require_ok'),$args); } +function skip() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'skip'),$args); } +function eq_array() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'eq_array'),$args); } +function eq_hash() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'eq_hash'),$args); } +function eq_set() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'eq_set'),$args); } +function is_deeply() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'is_deeply'),$args); } +function isnt_deeply() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'isnt_deeply'),$args); } +function todo() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'todo'),$args); } +function todo_skip() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'todo_skip'),$args); } +function todo_start() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'todo_start'),$args); } +function todo_end() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'todo_end'),$args); } + +?> diff --git a/application/third_party/test-more-php/Test-Simple-OO.php b/application/third_party/test-more-php/Test-Simple-OO.php new file mode 100755 index 000000000..9bbe4aada --- /dev/null +++ b/application/third_party/test-more-php/Test-Simple-OO.php @@ -0,0 +1,237 @@ +<?php +/* + Test-Simple-OO.php: + A workalike of Perl's Test::Simple for PHP. + + Why Test-Simple? + Test-Simple is a super simple way to start testing RIGHT NOW. + + Why ok and not ok? + Test-Simple produces TAP compliant output. + For more on TAP, see: http://testanything.org + For the TAP spec, see: http://search.cpan.org/dist/TAP/TAP.pm + + Why plan? + Planning is enforced because, unless you explicitly declare your + intent, the test set cannot ensure that all the required testing + was performed. An assumption could be made, but error prone + assumptions are exactly what testing is here to prevent. + + Other testing libraries: + You can replace Test-Simple with Test-More without making any changes + to existing test code, providing access to further testing methods. + You can also replace any other PHP Test::More workalike library out there + with Test-More.php and it will work without making any changes to the code. + + Assertions: + produce TAP output + provide basic testing functions (plan, ok) + exit with error code: + 0 all tests successful + 255 test died or all passed but wrong # of tests run + any other number how many failed (including missing or extras) + + Example: + require_once('Test-More-OO.php'); + $t = new TestMore(); + $t->plan(2); + $t->ok(1 + 1 = 2, 'One plus one equals two'); + $t->ok( doSomethingAndReturnTrue() , 'doSomethingAndReturnTrue() successful'); + + Procedural Example: + require_once('Test-Simple'); + plan(2); + ok(1 + 1 = 2, 'One plus one equals two'); + ok( doSomethingAndReturnTrue() , 'doSomethingAndReturnTrue() successful'); + + From a browser + If you are running Test-Simple on a web server and want slightly more web-readable + output, call the web_output() method/function. + + Updates + Updates will be posted to the Google code page: + http://code.google.com/p/test-more-php/ + + Bugs + Please file bug reports via the Issues tracker at the google code page. + + Acknowledgements + Michael G Schwern: http://search.cpan.org/~mschwern/Test-Simple/ + Chris Shiflet: http://shiflett.org/code/test-more.php + + Author + Copyright RJ Herrick <RJHerrick@beyondlogical.net> 2009, 2010 + +*/ + +class TestSimple { + + protected $Results = array('Failed'=>NULL,'Passed'=>NULL); + protected $TestName = array(); + protected $TestsRun = 0; + protected $Skips; + protected $NumberOfTests; + protected $CurrentTestNumber; + protected $Filter; + + protected $notes; + + function plan ($NumberOfTests = NULL, $SkipReason = '') { + // Get/set intended number of tests + + if ( is_int($this->NumberOfTests) && !is_null($NumberOfTests) ) $this->diag('The plan was already output.'); + + if ( $NumberOfTests === 'no_plan' ) { + // Equivalent to done_testing() at end of test script + $this->NumberOfTests = $NumberOfTests; + return; + } else if ( $NumberOfTests === 'skip_all' ) { + // Equivalent to done_testing() at end of test script + $this->NumberOfTests = $NumberOfTests; + $this->SkipAllReason = $SkipReason; + $this->diag("Skipping all tests: $SkipReason"); + return; + } + + // Return current value if no params passed (query to the plan) + if ( !func_num_args() && isset($this->NumberOfTests) ) return $this->NumberOfTests; + + // Number of tests looks acceptable + if (!is_int($NumberOfTests) || 0 > $NumberOfTests) $this->bail( "Number of tests must be a positive integer. You gave it '$NumberOfTests'" ); + + // If just reporting + $skipinfo = ''; + if ($this->NumberOfTests === 'skip_all') $skipinfo = ' # '.$this->SkipAllReason; + + echo "1..${NumberOfTests}${skipinfo}\n"; + $this->NumberOfTests = $NumberOfTests; + + return; + } + + function ok ($Result = NULL, $TestName = NULL) { + // Confirm param 1 is true (in the PHP sense) + // Unload the buffer regularly + if ($this->Filter) { + ob_flush(); + } + + $this->CurrentTestNumber++; + $this->TestsRun++; + + if ($this->Skips) { + $this->Skips--; + $this->TestsSkipped++; + echo('ok '.$this->CurrentTestNumber.' # skip '.$this->SkipReason."\n"); + return TRUE; + } + + if ($this->NumberOfTests === 'skip_all') { + $this->TestsSkipped++; + $this->diag("SKIP '$TestName'"); + echo('ok '.$this->CurrentTestNumber." # skip\n"); + return TRUE; + } + + if ( func_num_args() == 0 ) $this->bail('You must pass ok() a result to evaluate.'); + if ( func_num_args() == 2 ) $this->TestName[$this->CurrentTestNumber] = $TestName; + if ( func_num_args() > 2 ) $this->bail('Wrong number of arguments passed to ok()'); + + $verdict = $Result ? 'Passed' : 'Failed'; + + $this->Results[$verdict]++; + #$this->TestResult[$this->CurrentTestNumber] = $verdict; + + $caption = isset($this->TestName[$this->CurrentTestNumber]) ? $this->TestName[$this->CurrentTestNumber] : ''; + + $title = $this->CurrentTestNumber + . (isset($this->TestName[$this->CurrentTestNumber]) ? (' - '.$this->TestName[$this->CurrentTestNumber]) : ''); + + if ($verdict === 'Passed') { + echo "ok $title\n"; + return TRUE; + + } else { + echo $this->LastFail = "not ok $title\n"; + + $stack = isset($this->Backtrace) ? $this->Backtrace : debug_backtrace(); + foreach (array_reverse($stack) as $frame) { + if (isset($frame["object"]) && $frame["object"] == $this) { + $file = $frame["file"]; + $line = $frame["line"]; + break; + } + } + unset($this->Backtrace); + + if ($caption) { + $this->diag(" Failed test '$caption'"," at $file line $line."); + $this->LastFail .= "# Failed test '$caption'\n# at $file line $line."; + } else { + $this->diag(" Failed test at $file line $line."); + $this->LastFail .= "# Failed test at $file line $line."; + } + + return FALSE; + } + } + + function done_testing () { + // Change of plans (if there was one in the first place) + $this->plan((int)$this->TestsRun); + return; + } + + function bail ($message = '') { + // Problem running the program + TestSimple::_bail($message); + } + + static function _bail ($message = '') { + echo "Bail out! $message\n"; + throw new RuntimeException("Bail out! $message"); + } + + function diag() { + // Print a diagnostic comment + $diagnostics = func_get_args(); + $msg = ''; + foreach ($diagnostics as $line) $msg .= "# ".str_replace("\n","\n# ",$line)."\n"; + echo $msg; + if ($this->Filter) ob_flush(); + return $msg; + } + + function __destruct () { + // Parting remarks and proper exit code + + # if ($this->NumberOfTests === 'no_plan') done_testing(); + # if ($this->NumberOfTests === 'skip_all') plan(0); + + if ($this->TestsRun && !isset($this->NumberOfTests)) { + echo "# Tests were run but no plan() was declared and done_testing() was not seen.\n"; + } else { + if ($this->TestsRun !== $this->NumberOfTests) echo("# Looks like you planned ".(int)$this->NumberOfTests .' tests but ran '.(int)$this->TestsRun.".\n"); + + if ($this->Results['Failed']) echo("# Looks like you failed ". $this->Results['Failed'] .' tests of '.(int)$this->TestsRun.".\n"); + } + + // an extension to help debug + if ($this->notes) echo $this->notes; + + if ($this->Filter) ob_end_flush(); + + $retval = ($this->Results['Failed'] > 254) ? 254 : $this->Results['Failed']; + return; + } + + function web_output($callback = NULL) { + // Basic web formatting (newlines) of output via ob filter + if (isset($callback)) $this->Filter = $callback; + if (!isset($this->Filter)) $this->Filter = create_function('$string','$output = str_replace("\n","<br />\n",$string); return $output;'); + ob_start($this->Filter); + } + +} + +?> diff --git a/application/third_party/test-more-php/Test-Simple.php b/application/third_party/test-more-php/Test-Simple.php new file mode 100755 index 000000000..5005383df --- /dev/null +++ b/application/third_party/test-more-php/Test-Simple.php @@ -0,0 +1,18 @@ +<?php +/* + Procedural wrapper for Test-Simple.php + See Test-Simple-OO.php for documentation. +*/ + +require_once('Test-Simple-OO.php'); + +global $__Test; +$__Test = new TestSimple(); + +// Expose public API for TestSimple methods as functions +function plan() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'plan'),$args); } +function ok() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'ok'),$args); } +function diag() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'diag'),$args); } +function web_output() { global $__Test; $args = func_get_args(); return call_user_func_array(array($__Test,'web_output'),$args); } + +?> diff --git a/application/third_party/test-more-php/t/PHProvable.pl b/application/third_party/test-more-php/t/PHProvable.pl new file mode 100755 index 000000000..1d2295895 --- /dev/null +++ b/application/third_party/test-more-php/t/PHProvable.pl @@ -0,0 +1,35 @@ +#!/bin/env perl + +# PHProveable.pl +# +# A wrapper/dummy for +# +# This script allows you to use the prove program with PHP test scripts +# that output TAP, such as those written with Test-Simple or Test-More, +# without requiring that the php test script be writen with a UNIX style +# shebang line pointing to the processor: +# +# #!/bin/env php +# +# USAGE: +# Your PHP test script should be named like this: TESTSCRIPTNAME.t.php. +# You can either copy this file and name it TESTSCRIPTNAME.t or call it +# explicitly as the first and only argument: +# PHProvable.pl TESTSCRIPTNAME.t.php +# The first method means you end up with a stub for each PHP script, +# although on a system with symlinks you can use a symlink instead of +# copying PHProveable: +# ln -s PHPRoveable.pl TESTSCRIPTNAME.t +# The stub method allows you to just run `prove` in a directory and have +# it look for a /t directory, then find your *.t stubs and run them as +# usual. +# +# NOTES: +# Yeah, there are many ways to skin a cat. You could just leave the .php +# off your test script and add the shebang line, but then you can't just +# run the script via CGI without the shebang showing up as extra content, +# and it won't work on windows via the CLI. + +my $script = $ARGV[0] ? $ARGV[0] : "$0.php"; +my $php_interp = $ENV{'PHP'} ? $ENV{'PHP'} : 'php'; +exec("$php_interp $script"); diff --git a/application/third_party/test-more-php/t/badlib.php b/application/third_party/test-more-php/t/badlib.php new file mode 100755 index 000000000..ef7ee536a --- /dev/null +++ b/application/third_party/test-more-php/t/badlib.php @@ -0,0 +1,5 @@ +<?php + + die on inclusion + +?> diff --git a/application/third_party/test-more-php/t/borklib.php b/application/third_party/test-more-php/t/borklib.php new file mode 100755 index 000000000..da1ed4da9 --- /dev/null +++ b/application/third_party/test-more-php/t/borklib.php @@ -0,0 +1,5 @@ +<?php + + missing_func(); + +?> diff --git a/application/third_party/test-more-php/t/goodlib.php b/application/third_party/test-more-php/t/goodlib.php new file mode 100755 index 000000000..5b2140962 --- /dev/null +++ b/application/third_party/test-more-php/t/goodlib.php @@ -0,0 +1,5 @@ +<?php + + function xyzzy () { return true; } + +?> diff --git a/application/third_party/test-more-php/t/goodpage.php b/application/third_party/test-more-php/t/goodpage.php new file mode 100755 index 000000000..ce38cb818 --- /dev/null +++ b/application/third_party/test-more-php/t/goodpage.php @@ -0,0 +1,5 @@ +<?php + + $fnord++; + +?> diff --git a/application/third_party/test-more-php/t/testertests_bail_badplan_negative.php b/application/third_party/test-more-php/t/testertests_bail_badplan_negative.php new file mode 100755 index 000000000..81488367a --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_bail_badplan_negative.php @@ -0,0 +1,9 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + plan(-2); + + ok(1); +?> diff --git a/application/third_party/test-more-php/t/testertests_bail_badplan_noninteger.php b/application/third_party/test-more-php/t/testertests_bail_badplan_noninteger.php new file mode 100755 index 000000000..843b13d31 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_bail_badplan_noninteger.php @@ -0,0 +1,9 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + plan('xxx'); + + ok(1); +?> diff --git a/application/third_party/test-more-php/t/testertests_bundle.php b/application/third_party/test-more-php/t/testertests_bundle.php new file mode 100755 index 000000000..91bf79569 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_bundle.php @@ -0,0 +1,42 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-More.php'; + require_once($lib); + #plan(3); + + diag('Test of various functions not otherwise broken out.'); + + pass("pass() is ok"); + fail("fail() is not ok"); + + is('Ab3','Ab3','is() is ok'); + isnt('Ab3',123,'isnt() is ok'); + like('yackowackodot','/wacko/',"like() is ok"); + unlike('yackowackodot','/boing/',"unlike() is ok"); + + cmp_ok(12, '>', 10, 'cmp_ok() is ok'); + can_ok($__Test, 'plan' ); + isa_ok($__Test, 'TestMore', 'Default Testing object'); + include_ok('t/goodlib.php'); + require_ok('t/goodpage.php'); + + $foo = array(1,'B','third'); + $oof = array('third','B',1); + + $bar = array('q'=>23,'Y'=>42,); + $rab = array('Y'=>42,'q'=>23,); + + is_deeply($foo,$foo,'is_deeply() is ok'); + isnt_deeply($foo,$bar,'isnt_deeply() is ok'); + + /* + function skip($SkipReason, $num) { + function todo ($why, $howmany) { + function todo_skip ($why, $howmany) { + function todo_start ($why) { + function todo_end () { + */ + + diag("Should fail 1 test, testing fail()"); + done_testing(); +?> diff --git a/application/third_party/test-more-php/t/testertests_deprecated_comparisons.php b/application/third_party/test-more-php/t/testertests_deprecated_comparisons.php new file mode 100755 index 000000000..3fb42ee6d --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_deprecated_comparisons.php @@ -0,0 +1,27 @@ +<?php + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan('no_plan'); + + diag('Test of deprecated Test::More functions provided for compatibility completeness.'); + + $foo = array(0=>1,1=>'B',2=>'third'); + $oof = array(0=>'third',1=>'B',2=>1); + + + + $bar = array('q'=>23,'Y'=>42,); + $rab = array('Y'=>42,'q'=>23,); + + + + ok(eq_array($foo,$oof),'eq_array() with misordered array is ok'); + ok(eq_array($bar,$rab),'eq_array() with misordered assoc is ok'); + ok(eq_hash($foo,$oof),'eq_hash() with misordered array is ok'); + ok(eq_hash($bar,$rab),'eq_hash() with misordered assoc is ok'); + ok(eq_set($foo,$oof),'eq_set() with misordered array is ok'); + ok(eq_set($bar,$rab),'eq_set() with misordered assoc is ok'); + + done_testing(); + +?> diff --git a/application/third_party/test-more-php/t/testertests_deprecated_comparisons.pl b/application/third_party/test-more-php/t/testertests_deprecated_comparisons.pl new file mode 100755 index 000000000..2bea27ec4 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_deprecated_comparisons.pl @@ -0,0 +1,25 @@ +#!/bin/env perl + use strict; + use warnings; + use Test::More ('no_plan'); + + diag('Test of deprecated Test::More functions provided for compatibility completeness.'); + + my $foo = [1,'B','third']; + my $oof = ['third','B',1]; + my $foo_h = {0=>1,1=>'B',2=>'third'}; + my $oof_h = {0=>'third',1=>'B',2=>1}; + + my $bar = [23,42,]; + my $rab = [42,23,]; + my $bar_h = {'q'=>23,'Y'=>42,}; + my $rab_h = {'Y'=>42,'q'=>23,}; + + ok(eq_array($foo,$oof),'eq_array() with misordered array is ok'); + ok(eq_array($bar,$rab),'eq_array() with misordered assoc is ok'); + ok(eq_hash($foo_h,$oof_h),'eq_hash() with misordered array is ok'); + ok(eq_hash($bar_h,$rab_h),'eq_hash() with misordered assoc is ok'); + ok(eq_set($foo,$oof),'eq_set() with misordered array is ok'); + ok(eq_set($bar,$rab),'eq_set() with misordered assoc is ok'); + + done_testing(); diff --git a/application/third_party/test-more-php/t/testertests_exit_0.php b/application/third_party/test-more-php/t/testertests_exit_0.php new file mode 100755 index 000000000..7b407016f --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_exit_0.php @@ -0,0 +1,8 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + plan(1); + ok(1); +?> diff --git a/application/third_party/test-more-php/t/testertests_exit_fail_260.php b/application/third_party/test-more-php/t/testertests_exit_fail_260.php new file mode 100755 index 000000000..7737ba0ae --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_exit_fail_260.php @@ -0,0 +1,14 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + plan(262); + $failures = 260; + + ok(1); + for ($x=1;$x<$failures;$x++){ + ok(0); + } + ok(1); +?> diff --git a/application/third_party/test-more-php/t/testertests_exit_fail_5.php b/application/third_party/test-more-php/t/testertests_exit_fail_5.php new file mode 100755 index 000000000..4b6596746 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_exit_fail_5.php @@ -0,0 +1,14 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + plan(7); + $failures = 5; + + ok(1); + for ($x=0;$x<$failures;$x++){ + ok(0); + } + ok(1); +?> diff --git a/application/third_party/test-more-php/t/testertests_func_ok.php b/application/third_party/test-more-php/t/testertests_func_ok.php new file mode 100755 index 000000000..1561faf91 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.php @@ -0,0 +1,24 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + + print "# OK tests\n"; + print "# (No message for next test)\n"; + ok(1); + ok(1,"1 is ok"); + ok(TRUE,"TRUE is ok"); + ok('string',"'string' is ok"); + + print "# Not OK tests\n"; + print "# (No message for next test)\n"; + ok(0); + ok(0,"0 is not ok"); + ok(FALSE,"FALSE is not ok"); + ok('',"'' is not ok"); + ok( NULL,"NULL is not ok"); + + done_testing(); +?> diff --git a/application/third_party/test-more-php/t/testertests_func_ok.php_Test-More.out b/application/third_party/test-more-php/t/testertests_func_ok.php_Test-More.out new file mode 100755 index 000000000..d06ec52d4 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.php_Test-More.out @@ -0,0 +1,24 @@ +# OK +# (No message for next test) +ok 1 +ok 2 - 1 is ok +ok 3 - TRUE is ok +ok 4 - 'string' is ok +# Not OK +# (No message for next test) +not ok 5 +# Failed test at testertests_func_ok.php line 18. +not ok 6 - 0 is not ok +# Failed test '0 is not ok' +# at testertests_func_ok.php line 19. +not ok 7 - FALSE is not ok +# Failed test 'FALSE is not ok' +# at testertests_func_ok.php line 20. +not ok 8 - '' is not ok +# Failed test ''' is not ok' +# at testertests_func_ok.php line 21. +not ok 9 - NULL is not ok +# Failed test 'NULL is not ok' +# at testertests_func_ok.php line 22. +1..9 +# Looks like you failed 5 tests of 9. diff --git a/application/third_party/test-more-php/t/testertests_func_ok.php_Test-Simple.out b/application/third_party/test-more-php/t/testertests_func_ok.php_Test-Simple.out new file mode 100755 index 000000000..771678b8e --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.php_Test-Simple.out @@ -0,0 +1,16 @@ +# OK +# (No message for next test) +ok 1 +ok 2 - 1 is ok +ok 3 - TRUE is ok +ok 4 - 'string' is ok +# Not OK +# (No message for next test) +not ok 5 +not ok 6 - 0 is not ok +not ok 7 - FALSE is not ok +not ok 8 - '' is not ok +not ok 9 - NULL is not ok +1..9 + +# Looks like you failed 5 tests of 9. diff --git a/application/third_party/test-more-php/t/testertests_func_ok.pl b/application/third_party/test-more-php/t/testertests_func_ok.pl new file mode 100755 index 000000000..e87d97bb1 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.pl @@ -0,0 +1,24 @@ +#!/bin/env perl + use strict; + use warnings; + + my $lib = defined($ENV{'TESTLIB'}) ? $ENV{'TESTLIB'} : 'Test::Simple'; + eval "use $lib;"; + + print "# OK\n"; + print "# (No message for next test)\n"; + ok(1); + ok(1,"1 is ok"); + ok( !0,"TRUE is ok"); + ok('string',"'string' is ok"); + + print "# Not OK\n"; + print "# (No message for next test)\n"; + ok(0); + ok(0,"0 is not ok"); + ok( !1,"FALSE is not ok"); + ok('',"'' is not ok"); + ok(undef,"undef is not ok"); + + done_testing(); + diff --git a/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-More.out b/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-More.out new file mode 100755 index 000000000..e72f09462 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-More.out @@ -0,0 +1,24 @@ +# OK +# (No message for next test) +ok 1 +ok 2 - 1 is ok +ok 3 - TRUE is ok +ok 4 - 'string' is ok +# Not OK +# (No message for next test) +not ok 5 +# Failed test at testertests_func_ok.pl line 18. +not ok 6 - 0 is not ok +# Failed test '0 is not ok' +# at testertests_func_ok.pl line 19. +not ok 7 - FALSE is not ok +# Failed test 'FALSE is not ok' +# at testertests_func_ok.pl line 20. +not ok 8 - '' is not ok +# Failed test ''' is not ok' +# at testertests_func_ok.pl line 21. +not ok 9 - undef is not ok +# Failed test 'undef is not ok' +# at testertests_func_ok.pl line 22. +1..9 +# Looks like you failed 5 tests of 9. diff --git a/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-Simple.out b/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-Simple.out new file mode 100755 index 000000000..54c1e576a --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_ok.pl_Test-Simple.out @@ -0,0 +1,24 @@ +# OK +# (No message for next test) +ok 1 +ok 2 - 1 is ok +ok 3 - !0 is ok +ok 4 - 'string' is ok +# Not OK +# (No message for next test) +not ok 5 +# Failed test at testertests_func_ok.pl line 17. +not ok 6 - 0 is not ok +# Failed test '0 is not ok' +# at testertests_func_ok.pl line 18. +not ok 7 - !1 is not ok +# Failed test '!1 is not ok' +# at testertests_func_ok.pl line 19. +not ok 8 - '' is not ok +# Failed test ''' is not ok' +# at testertests_func_ok.pl line 20. +not ok 9 - undef is not ok +# Failed test 'undef is not ok' +# at testertests_func_ok.pl line 21. +Undefined subroutine &main::done_testing called at testertests_func_ok.pl line 23. +# Tests were run but no plan was declared and done_testing() was not seen. diff --git a/application/third_party/test-more-php/t/testertests_func_skip.php b/application/third_party/test-more-php/t/testertests_func_skip.php new file mode 100755 index 000000000..1a5f03823 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_skip.php @@ -0,0 +1,11 @@ +<?php + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-More.php'; + require_once($lib); + plan(2); + + skip("Test: Skip one",1); + fail("Gets skipped"); + pass("Gets run ok"); + +?> diff --git a/application/third_party/test-more-php/t/testertests_func_skip.pl b/application/third_party/test-more-php/t/testertests_func_skip.pl new file mode 100755 index 000000000..4f3c545b0 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_func_skip.pl @@ -0,0 +1,9 @@ +#!/bin/env perl + use strict; + use warnings; + use Test::More; + plan(tests=>2); + + skip("Test: Skip one",1); + fail("Gets skipped"); + pass("Gets run ok"); diff --git a/application/third_party/test-more-php/t/testertests_include_ok_badlib.php b/application/third_party/test-more-php/t/testertests_include_ok_badlib.php new file mode 100755 index 000000000..52764fb53 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_include_ok_badlib.php @@ -0,0 +1,21 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(5); + + diag('Should fail 3 of 5 tests'); + + ok(1, "Pass one"); + + include_ok('missing.php','Including a missing file should be not ok'); + + include_ok('badlib.php','Including a file with bad syntax should be not ok'); + + include_ok('borklib.php','Including a file with non-syntactical errors should be not ok'); + + ok(1, 'Continue testing after failed include'); + +?> diff --git a/application/third_party/test-more-php/t/testertests_include_ok_fatal.php b/application/third_party/test-more-php/t/testertests_include_ok_fatal.php new file mode 100755 index 000000000..982cd5caf --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_include_ok_fatal.php @@ -0,0 +1,17 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(3); + + diag('If PHP throws a fatal error, bail as nicely as possible.'); + + ok(1, "Pass one for good measure"); + + include_ok($lib,'Including a library again should redefine a function and bail.'); + + ok(1, 'This test will not be reached.'); + +?> diff --git a/application/third_party/test-more-php/t/testertests_interp.php b/application/third_party/test-more-php/t/testertests_interp.php new file mode 100755 index 000000000..f646f81d8 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_interp.php @@ -0,0 +1,10 @@ +<?php + + $lib = 'Test-More.php'; + require_once($lib); + $t = new TestMore(); + $t->plan(1); + + $t->is( $t->interp(),'php',"interp defaults to php"); + +?> diff --git a/application/third_party/test-more-php/t/testertests_interp_env.php b/application/third_party/test-more-php/t/testertests_interp_env.php new file mode 100755 index 000000000..8c12f0a12 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_interp_env.php @@ -0,0 +1,19 @@ +<?php + + $lib = 'Test-More.php'; + require_once($lib); + $t = new TestMore(); + $t->plan(1); + + if (strpos(strtoupper($_SERVER['OS']),'WINDOWS') !== FALSE) { + // Should also accept extension + $newinterp = 'php.exe'; + } else { + // Fair guess + $newinterp = '/usr/local/bin/php'; + } + + $_SERVER['PHP'] = $newinterp; + $t->is( $t->interp(),$newinterp,"set valid alternate interp via PHP environment variable ($newinterp)"); + +?> diff --git a/application/third_party/test-more-php/t/testertests_interp_set.php b/application/third_party/test-more-php/t/testertests_interp_set.php new file mode 100755 index 000000000..6e5fa2276 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_interp_set.php @@ -0,0 +1,18 @@ +<?php + + $lib = 'Test-More.php'; + require_once($lib); + $t = new TestMore(); + $t->plan(1); + + if (strpos(strtoupper($_SERVER['OS']),'WINDOWS') !== FALSE) { + // Should also accept extension + $newinterp = 'php.exe'; + } else { + // Fair guess + $newinterp = '/usr/local/bin/php'; + } + + $t->is( $t->interp($newinterp),$newinterp,"set valid alternate interp by passing arg: interp($newinterp)"); + +?> diff --git a/application/third_party/test-more-php/t/testertests_is_deeply.php b/application/third_party/test-more-php/t/testertests_is_deeply.php new file mode 100755 index 000000000..de30f2b82 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_is_deeply.php @@ -0,0 +1,31 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-More.php'; + require_once($lib); + plan('no_plan'); + + diag("Assertions:"); + + is_deeply(NULL, NULL); + is_deeply(TRUE, TRUE); + is_deeply(FALSE, FALSE); + is_deeply(42, 42); + is_deeply('abcdef', 'abcdef'); + is_deeply(array(), array()); + is_deeply(array(1), array(1)); + is_deeply(array(array()), array(array())); + is_deeply(array(array(123)), array(array(123))); + is_deeply(array(1,'abc'), array(0=>1,1=>'abc')); + + diag("Denials:"); + + isnt_deeply(NULL, TRUE, 'NULL !== TRUE'); + isnt_deeply(NULL, FALSE, 'NULL !== FALSE'); + isnt_deeply(NULL, 0, 'NULL !== 0'); + isnt_deeply(NULL, '', "NULL !== ''"); + isnt_deeply(0, FALSE, '0 !== FALSE'); + isnt_deeply(1, TRUE, '1 !== TRUE'); + +?> diff --git a/application/third_party/test-more-php/t/testertests_require_ok_badlib.php b/application/third_party/test-more-php/t/testertests_require_ok_badlib.php new file mode 100755 index 000000000..12d0e8b7e --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_require_ok_badlib.php @@ -0,0 +1,12 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(2); + + require_ok('badlib.php','Requiring a file with bad syntax should be not ok'); + + ok(1, 'Continue testing after failed require'); +?> diff --git a/application/third_party/test-more-php/t/testertests_require_ok_borklib.php b/application/third_party/test-more-php/t/testertests_require_ok_borklib.php new file mode 100755 index 000000000..4472ca795 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_require_ok_borklib.php @@ -0,0 +1,12 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(2); + + require_ok('borklib.php','Requiring a file with non-syntactical errors should be not ok'); + + ok(1, 'Continue testing after failed require'); +?> diff --git a/application/third_party/test-more-php/t/testertests_require_ok_missing.php b/application/third_party/test-more-php/t/testertests_require_ok_missing.php new file mode 100755 index 000000000..087d5d0c0 --- /dev/null +++ b/application/third_party/test-more-php/t/testertests_require_ok_missing.php @@ -0,0 +1,12 @@ +<?php + + + + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(2); + + require_ok('missing.php','Requiring a missing file should be not ok'); + + ok(1, 'Continue testing after failed require'); +?> diff --git a/application/third_party/test-more-php/t/try.php b/application/third_party/test-more-php/t/try.php new file mode 100755 index 000000000..1ca9c2410 --- /dev/null +++ b/application/third_party/test-more-php/t/try.php @@ -0,0 +1,7 @@ +#!/usr/bin/env php +<?php + $lib = isset($_SERVER['TESTLIB']) ? $_SERVER['TESTLIB'] : 'Test-Simple.php'; + require_once($lib); + plan(1); + ok(1); +?> diff --git a/application/views/contact.php b/application/views/contact.php new file mode 100644 index 000000000..6497ab6a7 --- /dev/null +++ b/application/views/contact.php @@ -0,0 +1 @@ +<?php echo $contact_info; ?> diff --git a/application/views/errors/html/error_404.php b/application/views/errors/html/error_404.php index 756ea9d62..b71da106d 100644 --- a/application/views/errors/html/error_404.php +++ b/application/views/errors/html/error_404.php @@ -1,64 +1,3 @@ <?php -defined('BASEPATH') OR exit('No direct script access allowed'); -?><!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="utf-8"> -<title>404 Page Not Found</title> -<style type="text/css"> - -::selection { background-color: #E13300; color: white; } -::-moz-selection { background-color: #E13300; color: white; } - -body { - background-color: #fff; - margin: 40px; - font: 13px/20px normal Helvetica, Arial, sans-serif; - color: #4F5155; -} - -a { - color: #003399; - background-color: transparent; - font-weight: normal; -} - -h1 { - color: #444; - background-color: transparent; - border-bottom: 1px solid #D0D0D0; - font-size: 19px; - font-weight: normal; - margin: 0 0 14px 0; - padding: 14px 15px 10px 15px; -} - -code { - font-family: Consolas, Monaco, Courier New, Courier, monospace; - font-size: 12px; - background-color: #f9f9f9; - border: 1px solid #D0D0D0; - color: #002166; - display: block; - margin: 14px 0 14px 0; - padding: 12px 10px 12px 10px; -} - -#container { - margin: 10px; - border: 1px solid #D0D0D0; - box-shadow: 0 0 8px #D0D0D0; -} - -p { - margin: 12px 15px 12px 15px; -} -</style> -</head> -<body> - <div id="container"> - <h1><?php echo $heading; ?></h1> - <?php echo $message; ?> - </div> -</body> -</html>
\ No newline at end of file +$title = "404 Page Not Found"; +include VIEWPATH."errors/html/error_general.php"; diff --git a/application/views/errors/html/error_db.php b/application/views/errors/html/error_db.php index f5a43f638..adff63559 100644 --- a/application/views/errors/html/error_db.php +++ b/application/views/errors/html/error_db.php @@ -1,64 +1,3 @@ <?php -defined('BASEPATH') OR exit('No direct script access allowed'); -?><!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="utf-8"> -<title>Database Error</title> -<style type="text/css"> - -::selection { background-color: #E13300; color: white; } -::-moz-selection { background-color: #E13300; color: white; } - -body { - background-color: #fff; - margin: 40px; - font: 13px/20px normal Helvetica, Arial, sans-serif; - color: #4F5155; -} - -a { - color: #003399; - background-color: transparent; - font-weight: normal; -} - -h1 { - color: #444; - background-color: transparent; - border-bottom: 1px solid #D0D0D0; - font-size: 19px; - font-weight: normal; - margin: 0 0 14px 0; - padding: 14px 15px 10px 15px; -} - -code { - font-family: Consolas, Monaco, Courier New, Courier, monospace; - font-size: 12px; - background-color: #f9f9f9; - border: 1px solid #D0D0D0; - color: #002166; - display: block; - margin: 14px 0 14px 0; - padding: 12px 10px 12px 10px; -} - -#container { - margin: 10px; - border: 1px solid #D0D0D0; - box-shadow: 0 0 8px #D0D0D0; -} - -p { - margin: 12px 15px 12px 15px; -} -</style> -</head> -<body> - <div id="container"> - <h1><?php echo $heading; ?></h1> - <?php echo $message; ?> - </div> -</body> -</html>
\ No newline at end of file +$title = "Database Error"; +include VIEWPATH."errors/html/error_general.php"; diff --git a/application/views/errors/html/error_general.php b/application/views/errors/html/error_general.php index fc3b2ebad..0f809dccd 100644 --- a/application/views/errors/html/error_general.php +++ b/application/views/errors/html/error_general.php @@ -1,5 +1,43 @@ <?php defined('BASEPATH') OR exit('No direct script access allowed'); + +// fancy error page only works if we can load helpers +if (class_exists("CI_Controller") && !isset($GLOBALS["is_error_page"]) && isset(get_instance()->load)) { + if (!isset($title)) { + $title = "Error"; + } + $GLOBALS["is_error_page"] = true; + + $CI =& get_instance(); + $CI->load->helper("filebin"); + $CI->load->helper("url"); + + if (is_cli()) { + $message = str_replace("</p>", "</p>\n", $message); + $message = strip_tags($message); + echo "$heading: $message\n"; + exit(); + } + + include APPPATH.'views/header.php'; + + ?> + <div class="error"> + <h1><?php echo $heading; ?></h1> + <?php echo $message; ?> + </div> + + <?php + include APPPATH.'views/footer.php'; +} elseif (php_sapi_name() === 'cli' OR defined('STDIN')) { + echo "# $heading\n"; + $msg = strip_tags(str_replace("<br>", "\n", $message)); + foreach (explode("\n", $msg) as $line) { + echo "# $line\n"; + } + exit(255); +} else { + // default CI error page ?><!DOCTYPE html> <html lang="en"> <head> @@ -50,7 +88,7 @@ code { box-shadow: 0 0 8px #D0D0D0; } -p { +p, div { margin: 12px 15px 12px 15px; } </style> @@ -61,4 +99,6 @@ p { <?php echo $message; ?> </div> </body> -</html>
\ No newline at end of file +</html> +<?php +} diff --git a/application/views/file/deleted.php b/application/views/file/deleted.php new file mode 100644 index 000000000..8a5818f2d --- /dev/null +++ b/application/views/file/deleted.php @@ -0,0 +1,11 @@ +<div class="center"> + <?php if (!empty($errors)) { + echo "<p>"; + foreach ($errors as $error) { + echo "${error["id"]}: ${error["reason"]}<br>\n"; + } + echo "</p>"; + } ?> + + <p><?php echo $deleted_count; ?> of <?php echo $total_count; ?> deleted.</p> +</div> diff --git a/application/views/file/file_info.php b/application/views/file/file_info.php new file mode 100644 index 000000000..f704001a2 --- /dev/null +++ b/application/views/file/file_info.php @@ -0,0 +1,36 @@ +<div class="center simple-container"> + <?php if($filedata): ?> + <div class="table-responive"> + <table class="table" style="margin: auto"> + <tr> + <td class="title">ID</td> + <td class="text"><a href="<?php echo site_url($id); ?>/"><?php echo $id; ?></a></td> + </tr> + <tr> + <td class="title">Filename</td> + <td class="text"><?php echo htmlspecialchars($filedata["filename"]); ?></td> + </tr> + <tr> + <td class="title">Date of upload</td> + <td class="text"><?php echo date("r", $filedata["date"]); ?></td> + </tr> + <tr> + <td class="title">Date of removal</td> + <td class="text"><?php echo $timeout; ?></td> + </tr> + <tr> + <td class="title">Size</td> + <td class="text"><?php echo format_bytes($filedata["filesize"]); ?></td> + </tr> + <tr> + <td class="title">Mimetype</td> + <td class="text"><?php echo $filedata["mimetype"]; ?></td> + </tr> + <tr> + <td class="title">Hash (MD5)</td> + <td class="text"><?php echo $filedata["hash"]; ?></td> + </tr> + </table> + </div> + <?php endif; ?> +</div> diff --git a/application/views/file/fragments/alert-wide.php b/application/views/file/fragments/alert-wide.php new file mode 100644 index 000000000..d17fdc4c0 --- /dev/null +++ b/application/views/file/fragments/alert-wide.php @@ -0,0 +1,3 @@ +<div class="alert <?php echo isset($error_type) ? $error_type : "alert-danger"; ?> alert-wide"> + <?php echo $error_message; ?> +</div> diff --git a/application/views/file/fragments/asciinema-player.php b/application/views/file/fragments/asciinema-player.php new file mode 100644 index 000000000..53a500831 --- /dev/null +++ b/application/views/file/fragments/asciinema-player.php @@ -0,0 +1,8 @@ +<div class="container-wide"> +<div class='panel panel-default'> + <div class='panel-heading'> + <?php echo anchor(site_url($filedata['id'])."/", htmlspecialchars($filedata["filename"])); ?> + </div> + <div id="player-container-<?php echo $filedata['id']; ?>" class="asciinema_player" data-url="<?php echo site_url($filedata['id']); ?>"></div> +</div> +</div> diff --git a/application/views/file/fragments/audio-player.php b/application/views/file/fragments/audio-player.php new file mode 100644 index 000000000..154153489 --- /dev/null +++ b/application/views/file/fragments/audio-player.php @@ -0,0 +1,8 @@ +<div class="container-wide"> +<p> + <audio controls="controls"> + <source src="<?php echo site_url($filedata["id"]); ?>"> + </audio> + <?php echo anchor(site_url($filedata['id'])."/", htmlspecialchars($filedata["filename"])); ?> +</p> +</div> diff --git a/application/views/file/fragments/thumbnail.php b/application/views/file/fragments/thumbnail.php new file mode 100644 index 000000000..82d0743fb --- /dev/null +++ b/application/views/file/fragments/thumbnail.php @@ -0,0 +1,35 @@ +<!-- Comment markers background: http://stackoverflow.com/a/14776780/953022 --> +<div class="container-wide"> +<?php +$base_url = site_url(); +if (substr($base_url, -1) !== "/") { + $base_url .= "/"; +} +$counter = 0; +?> +<div class="upload_thumbnails"><!-- + <?php foreach($items as $key => $item): + $counter++; + ?> + --><a + <?php if (strpos($item["mimetype"], "image/") === 0) {?>rel="gallery" class="colorbox"<?php } ?> + data-orientation="<?php echo $item["orientation"]; ?>" + href="<?php echo $base_url.$item["id"]."/"; ?>" + title="<?php echo htmlentities($item["filename"], ENT_QUOTES); ?>" + data-content="<?php echo htmlentities($item["tooltip"], ENT_QUOTES); ?>" + data-id="<?php echo $item["id"]; ?>"><!-- + --><?php if ($counter > 42) { + ?><img + class="thumb lazyload" + data-original="<?php echo $base_url."file/thumbnail/".$item["id"]; ?>" + ><?php + } else { + ?><img + class="thumb" + src="<?php echo $base_url."file/thumbnail/".$item["id"]; ?>"><?php + } ?><!-- + --><noscript><img class="thumb" src="<?php echo $base_url."file/thumbnail/".$item["id"]; ?>"></noscript></a><!-- + <?php endforeach; ?> + --> +</div> +</div> diff --git a/application/views/file/fragments/uploads_table.php b/application/views/file/fragments/uploads_table.php new file mode 100644 index 000000000..6673f22a3 --- /dev/null +++ b/application/views/file/fragments/uploads_table.php @@ -0,0 +1,25 @@ +<div class="table-responsive container-wide"> + <p>Non-previewable file(s):</p> + <table class="table table-striped tablesorter"> + <thead> + <tr> + <th>ID</th> + <th>Filename</th> + <th>Mimetype</th> + <th>Date</th> + <th>Size</th> + </tr> + </thead> + <tbody> + <?php foreach($items as $item): ?> + <tr> + <td><a href="<?php echo site_url("/".$item["id"]) ?>/"><?php echo $item["id"] ?></a></td> + <td class="wrap"><?php echo htmlspecialchars($item["filename"]); ?></td> + <td><?php echo $item["mimetype"] ?></td> + <td class="nowrap" data-sort-value="<?=$item["date"]; ?>"><?php echo date("r", $item["date"]); ?></td> + <td><?php echo format_bytes($item["filesize"]) ?></td> + </tr> + <?php endforeach; ?> + </tbody> + </table> +</div> diff --git a/application/views/file/fragments/video-player.php b/application/views/file/fragments/video-player.php new file mode 100644 index 000000000..6342c1692 --- /dev/null +++ b/application/views/file/fragments/video-player.php @@ -0,0 +1,12 @@ +<div class="container-wide"> +<div class='panel panel-default'> + <div class='panel-heading'> + <?php echo anchor(site_url($filedata['id'])."/", htmlspecialchars($filedata["filename"])); ?> + </div> + <div> + <video controls="controls"> + <source src="<?php echo site_url($filedata["id"]); ?>"> + </video> + </div> +</div> +</div> diff --git a/application/views/file/html_footer.php b/application/views/file/html_footer.php new file mode 100644 index 000000000..7c9cac8f7 --- /dev/null +++ b/application/views/file/html_footer.php @@ -0,0 +1,3 @@ +<div class="container"> +<?php +include(FCPATH."application/views/footer.php"); diff --git a/application/views/file/html_header.php b/application/views/file/html_header.php new file mode 100644 index 000000000..5f25b6acc --- /dev/null +++ b/application/views/file/html_header.php @@ -0,0 +1,13 @@ +<?php +include(FCPATH."application/views/header.php"); ?> + +</div><!-- .container --> +<script type="text/javascript"> + /* <![CDATA[ */ + window.appConfig.lexers = <?php echo json_encode($lexers); ?>; + /* ]]> */ +</script> + +<?php if (isset($error_message)) { + include 'framgents/alert-wide.php'; +} ?> diff --git a/application/views/file/html_paste_footer.php b/application/views/file/html_paste_footer.php new file mode 100644 index 000000000..22bc4dabb --- /dev/null +++ b/application/views/file/html_paste_footer.php @@ -0,0 +1,2 @@ +</div><!-- .container .paste-container --> + diff --git a/application/views/file/html_paste_header.php b/application/views/file/html_paste_header.php new file mode 100644 index 000000000..046b23dd8 --- /dev/null +++ b/application/views/file/html_paste_header.php @@ -0,0 +1,87 @@ +<div class="paste-container container-wide"> + <div style="border:1px solid #ccc;"> + <div class="navbar navbar-default navbar-static-top navbar-paste"> + <ul class="nav navbar-nav navbar-left dont-float"> + <li><a href="<?=site_url($id)."/"; ?>" class="navbar-brand" data-toggle="modal"><?php echo $title ?></a></li> + <li class="divider"></li> + <li class="dropdown"> + <a href="#" class="dropdown-toggle lexer-toggle" data-toggle="dropdown"> + Language: <?php echo htmlspecialchars($current_highlight); ?> + <b class="caret"></b> + </a> + <div class="dropdown-menu" style="padding: 15px;"> + <form class="lexer-form"> + <input data-base-url="<?=site_url($id); ?>" type="text" id="language-<?=$id; ?>" placeholder="Language" class="form-control"> + </form> + </div> + </li> + <li class="divider"></li> + <li> + <a href="#file-info-<?=$id; ?>" role="button" data-toggle="modal">Info</a> + </li> + <?php if (isset($user_logged_in) && $user_logged_in) { ?> + <li class="divider"></li> + <li><a href="<?php echo site_url('file/index?repaste='.$id); ?>" role="button">Repaste</a></li> + <?php } ?> + </ul> + <div class="btn-group navbar-right" style="margin: 8px;"> + <a class="btn btn-default linewrap-toggle" rel="tooltip" title="Toggle wrapping of long lines">Linewrap</a> + <div class="btn-group"> + <a class="btn btn-default dropdown-toggle tabwidth-toggle" rel="tooltip" title="Set tab width in spaces" data-toggle="dropdown" href="#">Tab width: <span class="tabwidth-value"></span> <span class="caret"></span></a> + <div class="dropdown-menu tabwidth-dropdown"> + <form class="tabwidth-form"> + <input type="number" class="form-control" min="0"> + </form> + </div> + </div> + <a href="<?php echo site_url($id."/plain") ?>" class="btn btn-default" rel="tooltip" title="View as plain text">Plain</a> + <a href="<?php echo site_url($id) ?>" class="btn btn-default" rel="tooltip" title="View as raw file (org. mime type)">Raw</a> + <?php if ($current_highlight === 'rmd') { ?> + <a href="<?php echo site_url($id)."/" ?>" class="btn btn-default" rel="tooltip" title="Render as Code">Code</a> + <?php } else { ?> + <a href="<?php echo site_url($id."/rmd") ?>" class="btn btn-default" rel="tooltip" title="Render as Markdown">Markdown</a> + <?php } ?> + </div> + </div> <!-- .navbar --> + <div id="file-info-<?=$id; ?>" class="modal fade" role="dialog" aria-labelledby="file-info-<?=$id; ?>" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h3 class="modal-title">Paste Information</h3> + </div> + <div class="modal-body"> + <table class="table"> + <tr> + <td style="border:0;">Filename:</td> + <td style="border:0;"><?php echo htmlspecialchars($filedata["filename"]) ?></td> + </tr> + <tr> + <td>Size:</td> + <td><?php echo format_bytes($filedata["filesize"]) ?></td> + </tr> + <tr> + <td>Mimetype:</td> + <td><?php echo $filedata["mimetype"] ?></td> + </tr> + <tr> + <td>Uploaded:</td> + <td><?php echo date("r", $filedata["date"]) ?></td> + </tr> + <tr> + <td>Removal:</td> + <td><?php echo $timeout ?></td> + </tr> + </table> + </div> + <div class="modal-footer"> + <?php echo form_open("file/do_delete/", array("style" => "display: inline")); ?> + <input type="hidden" name="ids[<?php echo $id; ?>]" value="<?php echo $id; ?>"> + <button class="btn btn-danger pull-left" aria-hidden="true">Delete</button> + </form> + <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button> + </div> + </div> + </div> + </div> <!-- .modal --> + </div> diff --git a/application/views/file/multipaste/queue.php b/application/views/file/multipaste/queue.php new file mode 100644 index 000000000..3f42b3cdf --- /dev/null +++ b/application/views/file/multipaste/queue.php @@ -0,0 +1,32 @@ +<div class="multipasteQueue"> + <?php echo form_open("file/multipaste/form_submit", ["data-ajax_url" => site_url("file/multipaste/ajax_submit")]); ?> + <div class="items"><!-- + <?php foreach ($items as $item) {?> + --><div data-id="<?php echo $item['id']; ?>"> + <input type="hidden" name="ids[<?php echo $item['id']; ?>]" value="<?php echo $item['id']; ?>"> + <div class='item'> + <?php if (isset($item['thumbnail'])) { ?> + <img + src="<?php echo $item['thumbnail']; ?>" + title="<?php echo $item['title']; ?>" + data-content="<?php echo $item['tooltip']; ?>"> + <?php } else { ?> + <div> + <?php echo $item['title']; ?><br> + <?php echo $item['tooltip']; ?> + </div> + <?php } ?> + </div> + <button class='multipaste_queue_delete btn-danger btn btn-xs'>Remove</button> + </div><!-- + <?php } ?> + --></div> + <button type="submit" class="btn btn-default" name="process" value="save"> + <div class="ajaxFeedback" style="display: none"> + <span class="glyphicon glyphicon-refresh spinning"></span> + </div> + Only save queue order + </button> + <button type="submit" class="btn btn-primary" name="process" value="create">Create multipaste</button> + </form> +</div> diff --git a/application/views/file/multipaste_info.php b/application/views/file/multipaste_info.php new file mode 100644 index 000000000..5baf732a2 --- /dev/null +++ b/application/views/file/multipaste_info.php @@ -0,0 +1,26 @@ +<div class="center simple-container"> + <div class="table-responive"> + <table class="table" style="margin: auto"> + <tr> + <td class="title">ID</td> + <td class="text"><a href="<?=site_url($id); ?>/"><?=$id; ?></a></td> + </tr> + <tr> + <td class="title">Number of files</td> + <td class="text"><?=$file_count; ?></td> + </tr> + <tr> + <td class="title">Date of upload</td> + <td class="text"><?=date("r", $upload_date); ?></td> + </tr> + <tr> + <td class="title">Date of removal</td> + <td class="text"><?=$timeout_string; ?></td> + </tr> + <tr> + <td class="title">Total size (including duplicates)</td> + <td class="text"><?=format_bytes($size); ?></td> + </tr> + </table> + </div> +</div> diff --git a/application/views/file/nav_history.php b/application/views/file/nav_history.php new file mode 100644 index 000000000..1a3e55c0b --- /dev/null +++ b/application/views/file/nav_history.php @@ -0,0 +1,18 @@ +<ul class="nav nav-tabs"> +<?php +$nav = array( + "List" => "file/upload_history", + "Thumbnails" => "file/upload_history_thumbnails", +); + +$CI =& get_instance(); + +foreach ($nav as $key => $item) { + ?> + <li <?php echo $CI->uri->uri_string() == $item ? 'class="active"' : ''; ?>> + <a href="<?php echo site_url($item); ?>"><?php echo $key; ?></a> + </li> + <?php +} +?> +</ul> diff --git a/application/views/file/non_existent.php b/application/views/file/non_existent.php new file mode 100644 index 000000000..13d8c6950 --- /dev/null +++ b/application/views/file/non_existent.php @@ -0,0 +1,3 @@ +<div class="center"> + <p>I'm sorry, but the requested file doesn't exist.</p> +</div> diff --git a/application/views/file/show_url.php b/application/views/file/show_url.php new file mode 100644 index 000000000..a3d965717 --- /dev/null +++ b/application/views/file/show_url.php @@ -0,0 +1,8 @@ +<div class="center"> + <p>You can get your file(s) here:</p> + <p> + <?php foreach ($urls as $key => $url) { ?> + <a href="<?php echo $url; ?>"><?php echo $url; ?></a><br /> + <?php } ?> + </p> +</div> diff --git a/application/views/file/upload_form.php b/application/views/file/upload_form.php new file mode 100644 index 000000000..29fbbf571 --- /dev/null +++ b/application/views/file/upload_form.php @@ -0,0 +1,168 @@ +<?php if (isset($user_logged_in) && $user_logged_in) { ?> +<?php echo form_open_multipart('file/do_websubmit'); ?> + <div class="row"> + <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 text-upload-form"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Text paste</h3> + </div> + <div class="panel-body" id="textboxes"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#text-upload-tab-1" data-toggle="tab">Paste 1 </a></li> + </ul> + <div class="tab-content"> + <div class="tab-pane active" id="text-upload-tab-1"> + <div class="panel panel-default"> + <div class="panel-heading"> + <input type="text" name="filename[1]" class="form-control" placeholder="Filename/title (default: stdin)"> + </div> + <textarea name="content[1]" class="form-control text-upload" placeholder="Paste content"><?php + if (isset($textarea_content)) { + echo $textarea_content; + } + ?></textarea> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="row"> + <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">File upload</h3> + </div> + <div class="panel-body"> + <div> + <input class="file-upload" type="file" name="file[]" multiple="multiple"><br> + </div> + </div> + </div> + </div> + <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> + <div class="panel panel-info"> + <div class="panel-heading"> + <h3 class="panel-title">Notice!</h3> + </div> + <div class="panel-body"> + <p> + You can upload files and paste text at the same time. Empty text or file inputs will be ignored. + </p> + <p><button type="submit" id="upload_button" class="btn btn-primary">Upload/Paste it!</button></p> + <p> + Uploads/pastes are <?php if ($upload_max_age > 0) { + echo "deleted after ".expiration_duration($upload_max_age); + if ($small_upload_size > 0) { + echo " unless they are smaller than ".format_bytes($small_upload_size); + } + } else { + echo "stored forever"; + } ?>. Maximum upload size is <?php echo format_bytes($max_upload_size); ?>. + You can upload a maximum of <?php echo ini_get("max_file_uploads"); ?> files at once. + </p> + </div> + </div> + </div> + </div> +</form> + +<script type="text/javascript"> + /* <![CDATA[ */ + window.appConfig.maxUploadSize = "<?php echo $max_upload_size; ?>"; + window.appConfig.maxFilesPerUpload = "<?php echo ini_get("max_file_uploads"); ?>"; + /* ]]> */ +</script> + +<?php } else { ?> + <?php echo form_open('user/login', array('class' => 'form-inline')); ?> + <input type="text" name="username" placeholder="Username" autofocus class="form-control inline-input"/> + <input type="password" name="password" placeholder="Password" class="form-control inline-input"/> + <input type="submit" class="btn btn-primary" value="Login" name="process" /> + <?php if(auth_driver_function_implemented("can_reset_password")) { ?> + <p class="help-block"><?php echo anchor("user/reset_password", "Forgot your password?"); ?></p> + <?php } ?> + </form> +<?php } ?> +<div class="row"> + <div class="col-lg-6"> + <div class="page-header"><h1>Features</h1></div> + <h3>How to link your pastes:</h3> + <dl class="dl-horizontal"> + <dt>/<ID>/</dt><dd>automatically highlight the paste</dd> + <dt>/<ID></dt><dd>set the detected MIME type and let the browser do the rest</dd> + <dt>/<ID>/plain</dt><dd>force the MIME type to be text/plain</dd> + <dt>/<ID>/<file extension></dt><dd>override auto detection and use the supplied file extension or language name for highlighting</dd> + <dt>/<ID>/qr</dt><dd>display a qr code containing a link to <span class="example">/<ID>/</span></dd> + <dt>/<ID>/rmd</dt><dd>convert markdown to HTML</dd> + <dt>/<ID>/ascii</dt><dd>convert text with ANSI (shell) escape codes to HTML</dd> + <dt>/<ID>/info</dt><dd>display some information about the ID</dd> + <dt>/file/thumbnail/<ID></dt><dd>return a JPEG thumbnail for the ID (only works for some file types)</dd> + </dl> + <p> + If your upload is not detected as text, only <b>/<ID>/qr</b>, + <b>/<ID>/plain</b>, <b>/<ID>/info</b> and + <b>/file/thumbnail/<ID></b> will work as above and all others will simply + return the file with the detected MIME type. + </p> + + <h3>How to link your multipastes:</h3> + <p>Multipaste IDs begin with <code>m-</code> and only support the following features.</p> + <dl class="dl-horizontal"> + <dt>/<ID>/</dt><dd>automatically display everything in a sensible way</dd> + <dt>/<ID>/qr</dt><dd>display a qr code containing a link to <span class="example">/<ID>/</span></dd> + <dt>/<ID>/info</dt><dd>display some information about the multipaste</dd> + <dt>/<ID>/tar</dt><dd>download a tarball of all files in the multipaste (files may be renamed to avoid conflicts)</dd> + <dt>/<ID>/pls</dt><dd>download a PLS playlist of all audio/video files in the multipaste</dd> + </dl> + + <h3>Special filenames:</h3> + <dl class="dl-horizontal"> + <dt>*.asciinema.json<br>or *.cast</dt><dd>treat the file as an <a href="https://asciinema.org/">asciinema screencast</a> and display a videoplayer for it</dd> + </dl> + </div> + + <div class="col-lg-6"> + <div class="page-header"><h1>Information</h1></div> + <p> + This website's primary goal is aiding developers, power users, students and + alike in solving problems, debugging software, sharing their configuration, + etc. It is not intended to distribute confidential or harmful information, + scripts or software or copyrighted content for which you do not have a + distribution license. + </p> + <?php if(auth_driver_function_implemented("can_register_new_users")) { ?> + <p> + If you want an account, ask someone who is already using this + service to <a href="<?php echo site_url("user/invite"); ?>">invite</a> you. + </p> + <p> + Invitations are used to control abuse and encourage users to "be nice". They + are not intended as a means of exclusivity. In case of abuse reports, involved + accounts may be banned and the user who invited them may also be banned. The + invitation tree will be followed upwards if necessary. + </p> + <?php } ?> + + <h3>Clients</h3> + <h4>Linux</h4> + <p> + Development: <?php echo anchor("https://git.server-speed.net/users/flo/fb/"); ?><br /> + Latest release: <?php echo $client_link ? anchor($client_link) : "unknown"; ?><br /> + GPG sigs, older versions: <?php echo anchor("https://paste.xinu.at/data/client"); ?> + </p> + + <p> + Arch Linux: <code>pacman -S fb-client</code><br /> + Gentoo: Add <a href="https://git.holgersson.xyz/holgersson-overlay/tree/README">this overlay</a> and run <code>emerge -a fb-client</code><br /> + FreeBSD: <code>pkg install fb</code><br /> + </p> + + <h4>Android</h4> + <p> + Development: <a href="https://github.com/sebastianrakel/fb-client-android">sebastianrakel/fb-client-android @ Github</a><br> + F-Droid Store: <a href="https://f-droid.org/repository/browse/?fdid=eu.devunit.fb_client">fb-client Android @ F-Droid</a><br> + </p> + </div> +</div> diff --git a/application/views/file/upload_history.php b/application/views/file/upload_history.php new file mode 100644 index 000000000..c6cbe7946 --- /dev/null +++ b/application/views/file/upload_history.php @@ -0,0 +1,39 @@ +<?php echo form_open("file/handle_history_submit") ?> + <div class="nav-history"> + <div class="container"> + <div class="pull-right"> + <button class="btn btn-danger" name="process" value="delete">Delete checked</button> + <button class="btn btn-primary" name="process" value="multipaste">Add checked to multipaste queue</button> + </div> + <?php include 'nav_history.php'; ?> + </div> + </div> + <div class="table-responsive"> + <table id="upload_history" class="table table-striped tablesorter {sortlist: [[4,1]]}"> + <thead> + <tr> + <th class="{sorter: false}"><input type="checkbox" name="all-ids" id="history-all"></th> + <th>ID</th> + <th>Filename</th> + <th>Mimetype + <th>Date</th> + <th>Size</th> + </tr> + </thead> + <tbody> + <?php foreach($items as $key => $item): ?> + <tr> + <td><input type="checkbox" name="ids[<?php echo $item["id"] ?>]" value="<?php echo $item["id"] ?>" class="delete-history"></td> + <td><a href="<?php echo site_url("/".$item["id"]) ?>/" data-content="<?php if (isset($item['preview_text'])) {echo htmlentities($item['preview_text'], ENT_QUOTES);} ?>"><?php echo $item["id"] ?></a></td> + <td class="wrap"><?php echo htmlspecialchars($item["filename"]); ?></td> + <td><?php echo $item["mimetype"] ?></td> + <td class="nowrap" data-sort-value="<?=$item["date"]; ?>"><?php echo date("r", $item["date"]); ?></td> + <td><?php echo $item["filesize"] ?></td> + </tr> + <?php endforeach; ?> + </tbody> + </table> + </div> +</form> + +<p>Total sum of your distinct uploads: <?php echo $total_size; ?>.</p> diff --git a/application/views/file/upload_history_thumbnails.php b/application/views/file/upload_history_thumbnails.php new file mode 100644 index 000000000..7d4fc6298 --- /dev/null +++ b/application/views/file/upload_history_thumbnails.php @@ -0,0 +1,26 @@ +<div class="nav-history"> + <div class="container"> + <div class="pull-right"> + <?php echo form_open("file/handle_history_submit/", array("id" => "submit_form", "style" => "display: inline")); ?> + <button type="submit" class="btn btn-danger" style="display: none" name='process' value='delete'>Delete selected</button> + <button type="submit" class="btn btn-primary" style="display: none" name='process' value='multipaste'>Add selected to multipaste queue</button> + </form> + <button class="btn btn-default" id="toggle_select_mode" style="display: inline">Select mode</button> + </div> + + <?php include 'nav_history.php'; ?> + </div> +</div> +<?php include 'fragments/thumbnail.php'; ?> + +<div class="row-fluid"> + <div class="span12 alert alert-block alert-info"> + <h4 class="alert-heading">Notice!</h4> + <p> + Currently only images and pdf files are displayed here. If you are + looking for something else, please switch to the + <a href="<?php echo site_url("file/upload_history"); ?>">list view</a> + which contains your complete history. + </p> + </div> +</div> diff --git a/application/views/footer.php b/application/views/footer.php new file mode 100644 index 000000000..e3d3bc752 --- /dev/null +++ b/application/views/footer.php @@ -0,0 +1,23 @@ + </div><!-- .container --> +<div id="push"></div> +</div> <!-- #wrap --> +<footer class="footer" id="footer"> + <div class="container muted credits"> + <p>Site code licensed under <a href="http://www.gnu.org/licenses/agpl-3.0.html" target="_blank">AGPL v3</a>.</p> + <p><a href="http://glyphicons.com">Glyphicons Free</a> licensed under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.</p> + <ul class="footer-links"> + <li><a href="http://git.server-speed.net/users/flo/filebin/">Source</a></li> + <li class="muted">·</li> + <li><a href="<?php echo site_url("file/contact"); ?>">Contact</a></li> + </ul> + </div> +</footer> + +<?php +$CI = &get_instance(); +if ($CI->config->item("environment") == "development" && property_exists($CI, "email")) { + echo $CI->email->print_debugger(); +} +?> +</body> +</html> diff --git a/application/views/header.php b/application/views/header.php new file mode 100644 index 000000000..47d9a02f6 --- /dev/null +++ b/application/views/header.php @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="utf-8"> + <title><?php echo isset($title) ? $title : 'FileBin'; ?></title> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="robots" content="noindex,nofollow" /> + <meta name="description" content=""> + <meta name="author" content=""> + + <link href="<?php echo link_with_mtime("/data/css/ui-lightness/jquery-ui.min.css"); ?>" rel="stylesheet"> + <link href="<?php echo link_with_mtime("/data/css/bootstrap.min.css"); ?>" rel="stylesheet"> + <link href="<?php echo link_with_mtime("/data/css/style.css"); ?>" rel="stylesheet"> + <link href="<?php echo link_with_mtime("/data/css/colorbox.css"); ?>" rel="stylesheet"> + <link href="<?php echo link_with_mtime("/data/css/asciinema-player.css"); ?>" rel="stylesheet"> + <?php + if (file_exists(FCPATH."data/local/style.css")) { + echo '<link href="'.link_with_mtime("/data/local/style.css").'" rel="stylesheet">'; + } + + if (file_exists(FCPATH."data/local/favicon.png")) { + echo '<link href="'.link_with_mtime("/data/local/favicon.png").'" rel="shortcut icon">'; + } + ?> + <script src="<?php echo link_with_mtime("/data/js/vendor/asciinema-player.js"); ?>"></script> + <script src="<?php echo link_with_mtime("/data/js/vendor/require.js"); ?>"></script> + <script type="text/javascript"> + /* <![CDATA[ */ + window.appConfig = {}; + require.config({ + baseUrl: '<?php echo base_url('/data/js'); ?>', + urlArgs: '<?php echo js_cache_buster(); ?>', + paths: { + 'main': ['main.min', 'main'] + } + }); + require(['main']); + /* ]]> */ + </script> +</head> + +<body> +<div id="wrap"> +<?php if (file_exists(FCPATH."data/local/header.inc.php")) { + include FCPATH."data/local/header.inc.php"; +}?> + <nav class="navbar navbar-fixed-top navbar-inverse" role="navigation"> + <div class="container"> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="<?php echo site_url(); ?>"><?php + if (file_exists(FCPATH."data/local/logo.svg")) { + echo '<img class="brand-icon" src="'.link_with_mtime("/data/local/logo.svg").'" style="height: 20px"> FileBin'; + } else { + echo "FileBin"; + } + ?> + </a> + </div> + <div class="collapse navbar-collapse navbar-ex1-collapse"> + <?php if(!isset($GLOBALS["is_error_page"])) { ?> + <ul class="nav navbar-nav navbar-right"> + <?php if (isset($user_logged_in) && $user_logged_in) { ?> + <li><a class="navbar-link" href="<?php echo site_url("/user/logout"); ?>">Logout</a></li> + <?php } else { ?> + <li class="dropdown"> + <a class="dropdown-toggle" href="#" data-toggle="dropdown">Login <b class="caret"></b></a> + <div class="dropdown-menu" style="padding: 5px;"> + <?php if(auth_driver_function_implemented("can_reset_password")) { ?> + <p><?php echo anchor("user/reset_password", "Forgot your password?"); ?></p> + <?php } ?> + <?php echo form_open("user/login?redirect_uri=".$redirect_uri, array("class" => "form-signin")); ?> + <input type="text" name="username" placeholder="Username" class="form-control"> + <input type="password" name="password" placeholder="Password" class="form-control"> + <button type="submit" name="process" class="btn btn-default btn-block">Login</button> + </form> + </div> + </li> + <?php } ?> + </ul> + <?php }; ?> + <ul class="nav navbar-nav"> + <?php if (isset($user_logged_in) && $user_logged_in) { ?> + <li><a href="<?php echo site_url("file/index") ?>"><span class="glyphicon glyphicon-pencil"></span> New</a></li> + <li><a href="<?php echo site_url("file/upload_history") ?>"><span class="glyphicon glyphicon-book"></span> History</a></li> + <li class="dropdown"> + <a href="<?php echo site_url("user/index"); ?>" class="dropdown-toggle" data-toggle="dropdown"> + <span class="glyphicon glyphicon-user"></span> Account <b class="caret"></b> + </a> + <ul class="dropdown-menu"> + <?php include "user/nav.php"; ?> + </ul> + </li> + <?php if (isset($show_multipaste_queue) && $show_multipaste_queue) {?> + <li class="btn-primary multipaste_button"><a href="<?php echo site_url("file/multipaste/queue"); ?>">Multipaste queue</a></li> + <?php } ?> + <?php } ?> + </ul> + </div> + </div> + </nav> + <div id="navbar-height"></div> + + <div class="container"> + <?php + if (isset($alerts)) { + foreach ($alerts as $alert) { ?> + <div class="alert alert-dismissable alert-<?php echo $alert["type"]; ?>" style="text-align: center"> + <button type="button" class="close" data-dismiss="alert">×</button> + <?php echo $alert["message"]; ?> + </div> + <?php + } + } + ?> diff --git a/application/views/tests/echo-fragment.php b/application/views/tests/echo-fragment.php new file mode 100644 index 000000000..f8c26661f --- /dev/null +++ b/application/views/tests/echo-fragment.php @@ -0,0 +1,5 @@ +<?php +echo "listing ".count($items)." items:\n"; +foreach ($items as $item) { + echo $item; +} diff --git a/application/views/user/apikeys.php b/application/views/user/apikeys.php new file mode 100644 index 000000000..2b6934c6d --- /dev/null +++ b/application/views/user/apikeys.php @@ -0,0 +1,60 @@ +<h2>API keys</h2> +<div class="table-responsive"> + <table class="table table-striped"> + <thead> + <tr> + <th>#</th> + <th>Key</th> + <th style="width: 30%;">Comment</th> + <th>Created on</th> + <th>Access</th> + <th></th> + </tr> + </thead> + <tbody> + <?php $i = 1; ?> + <?php foreach($query as $key => $item): ?> + <tr> + <td><?php echo $i++; ?></td> + <td><?php echo $item["key"]; ?></td> + <td><?php echo htmlentities($item["comment"]); ?></td> + <td><?php echo date("Y/m/d H:i", $item["created"]); ?></td> + <td> + <?php if ($item["access_level"] == "full"): ?> + <span class="glyphicon glyphicon-warning-sign"></span> + <?php endif; ?> + <?php echo $item["access_level"]; ?> + </td> + <td> + <?php echo form_open("user/delete_apikey", array("style" => "margin-bottom: 0")); ?> + <?php echo form_hidden("key", $item["key"]); ?> + <button class="btn btn-danger btn-xs" type="submit">Delete</input> + </form> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> +</div> + +<h3>Access levels:</h3> + +<dl class="dl-horizontal"> + <dt>basic</dt> + <dd>Allows uploading files.</dd> + <dt>apikey</dt> + <dd>Allows removing existing files and viewing the history. Includes <code>basic</code>.</dd> + <dt>full</dt> + <dd>Allows everything, including, but not limited to, creating and removing api keys, changing profile settings and creating invitation keys. Includes <code>apikey</code>.</dd> + +<p> + <?php echo form_open('user/create_apikey', array("class" => "form-inline")); ?> + <input type="text" name="comment" placeholder="Comment" class="form-control" style="width: 200px;"/> + <select name="access_level" class="form-control" style="width: 100px;"> + <option>basic</option> + <option selected="selected">apikey</option> + <option>full</option> + </select> + <input class="btn btn-primary" type="submit" value="Create a new key" name="process" /> +</form> +</p> diff --git a/application/views/user/delete_account_form.php b/application/views/user/delete_account_form.php new file mode 100644 index 000000000..dbb28531d --- /dev/null +++ b/application/views/user/delete_account_form.php @@ -0,0 +1,27 @@ +<div class="row"> + <div class="col-sm-12"> + <h1>Account deletion</h1> + <p> + Here you can permanently delete your account on this FileBin installation.<br> + <b>WARNING: All your data will be irrevocably deleted.</b> + </p> + </div> +</div> + +<?php echo form_open("user/delete_account"); ?> + <div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPassword" name="password" placeholder="Password" class="form-control"> + </div> + </div> + </div> + <div class='row'> + <div class="form-group col-lg-8 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" name="delete" class="form-control btn-danger">Delete my account (<?php echo htmlentities($username); ?>)</button> + </div> + </div> + </div> +</form> diff --git a/application/views/user/delete_account_success.php b/application/views/user/delete_account_success.php new file mode 100644 index 000000000..72d7ff12b --- /dev/null +++ b/application/views/user/delete_account_success.php @@ -0,0 +1,8 @@ +<div class="row"> + <div class="col-sm-12"> + <h1>Account deletion successful</h1> + <p> + Your account has been successfully deleted. Thank you for using FileBin! + </p> + </div> +</div> diff --git a/application/views/user/hash_password.php b/application/views/user/hash_password.php new file mode 100644 index 000000000..98bef9df5 --- /dev/null +++ b/application/views/user/hash_password.php @@ -0,0 +1,38 @@ +<?php +if (!empty($error)) { + echo "<p class='alert alert-danger'>"; + echo implode("<br />\n", $error); + echo "</p>"; +} + +if ($hash) { + echo "<p>Result (this hash uses a random salt, so it will be different each time you submit this form):<br />$hash</p>\n"; +} +?> +<?php echo form_open('user/hash_password'); ?> +<div class="row"> + <div class="form-group col-lg-10 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPassword" name="password" placeholder="Password" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-10 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Confirm password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPasswordConfirm" name="password_confirm" placeholder="Password confirmation" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-10 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" class="btn btn-primary" name="process">Hash it</button> + </div> + </div> +</form> + diff --git a/application/views/user/index.php b/application/views/user/index.php new file mode 100644 index 000000000..9e6f48116 --- /dev/null +++ b/application/views/user/index.php @@ -0,0 +1,3 @@ +<ul class="nav"> +<?php include "nav.php"; ?> +</ul> diff --git a/application/views/user/invite.php b/application/views/user/invite.php new file mode 100644 index 000000000..042ba0b61 --- /dev/null +++ b/application/views/user/invite.php @@ -0,0 +1,45 @@ +<div class="alert alert-warning"> + <p> + <b>Watch out!</b> + </p> + <p> + You are free to invite anyone you want to, but please keep in + mind that if this person violates the rules and is banned, your + account will also be disabled. + </p> +</div> + +<h2>Unused invitation keys</h2> +<div class="table-responsive"> + <table class="table table-striped"> + <thead> + <tr> + <th>#</th> + <th style="width: 70%;">Key</th> + <th>Created on</th> + </tr> + </thead> + <tbody> + <?php $i = 1; ?> + <?php foreach($query as $key => $item): ?> + <tr> + <td><?php echo $i++; ?></td> + <td><?php echo anchor("user/register/".$item["key"], $item["key"]) ?></td> + <td><?php echo date("Y/m/d H:i", $item["date"]) ?></td> + <td> + <?php echo form_open('user/delete_invitation_key'); ?> + <input class="btn btn-danger btn-xs" type="submit" value="Delete" name="delete" /> + <input type="hidden" name="key" value="<?php echo $item["key"]; ?>" /> + </form> + </td> + </tr> + <?php endforeach; ?> + </tbody> + </table> +</div> + +<p> + <?php echo form_open('user/create_invitation_key'); ?> + <input class="btn btn-primary btn-large" type="submit" value="Create a new key" name="process" /> + </form> +</p> diff --git a/application/views/user/login.php b/application/views/user/login.php new file mode 100644 index 000000000..6fb4d97dd --- /dev/null +++ b/application/views/user/login.php @@ -0,0 +1,26 @@ +<?php +if (isset($login_error)) { ?> + <div class="alert alert-danger">The entered credentials are invalid.</div> +<?php } ?> + +<?php echo form_open("user/login?redirect_uri=$redirect_uri", array("class" => "login-page")); ?> + <div class="form-group"> + <label class="control-label" for="inputUsername">Username</label> + <div class="controls"> + <input type="text" id="inputUsername" name="username" placeholder="Username" class="form-control"> + </div> + </div> + + <div class="form-group"> + <label class="control-label" for="inputPassword">Password</label> + <div class="controls"> + <input type="password" id="inputPassword" name="password" placeholder="Password" class="form-control"> + </div> + </div> + + <div class="form-group"> + <div class="controls"> + <button type="submit" class="btn btn-primary" name="process">Login</button> + </div> + </div> +</form> diff --git a/application/views/user/nav.php b/application/views/user/nav.php new file mode 100644 index 000000000..49c7aa988 --- /dev/null +++ b/application/views/user/nav.php @@ -0,0 +1,11 @@ +<?php if(auth_driver_function_implemented("can_register_new_users")) { ?> +<li><a href="<?php echo site_url("user/invite") ?>"><span class="glyphicon glyphicon-plus"></span> Invite</a></li> +<?php } ?> + +<li><a href="<?php echo site_url("user/profile") ?>"><span class="glyphicon glyphicon-user"></span> Profile</a></li> +<li><a href="<?php echo site_url("user/apikeys") ?>"><span class="glyphicon glyphicon-tags"></span> API keys</a></li> + +<?php if(auth_driver_function_implemented("can_reset_password")) { ?> +<li><a href="<?php echo site_url("user/reset_password") ?>"><span class="glyphicon glyphicon-lock"></span> Change password</a></li> +<?php } ?> + diff --git a/application/views/user/profile.php b/application/views/user/profile.php new file mode 100644 index 000000000..fbd29f474 --- /dev/null +++ b/application/views/user/profile.php @@ -0,0 +1,48 @@ +<?php echo form_open("user/profile"); ?> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputUsername">Username</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputUsername" name="username" placeholder="Username" disabled="disabled" value="<?php echo $profile_data["username"]; ?>" class="form-control"> + </div> + </div> +</div> + +<?php if($profile_data["email"] !== null) { ?> +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputEmail">Email</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputEmail" name="email" placeholder="Email" <?php if(!auth_driver_function_implemented("can_change_email")) { ?>disabled="disabled" <?php } ?>value="<?php echo $profile_data["email"]; ?>" class="form-control"> + </div> + </div> +</div> +<?php } ?> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputUploadIDLimits">Upload ID length limits</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputUploadIDLimits" name="upload_id_limits" placeholder="number-number" value="<?php echo $profile_data["upload_id_limits"]; ?>" class="form-control"> + <span class="help-block">Values have to be between 3 and 64 inclusive. Please remember that longer IDs don't protect your pastes from being found if you post the link somewhere a search engine can see it.</span> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" class="btn btn-primary" name="process">Save changes</button> + </div> + </div> +</div> +</form> + +<div class="row vertical-space-small"></div> + +<?php if(auth_driver_function_implemented("can_delete_account")) { ?> +<div class="row"> + <p>If you want to permanently delete your account, please click <a href="<?php echo site_url("user/delete_account"); ?>">here</a>.</p> +</div> +<?php } ?> diff --git a/application/views/user/register.php b/application/views/user/register.php new file mode 100644 index 000000000..0f03a2f00 --- /dev/null +++ b/application/views/user/register.php @@ -0,0 +1,52 @@ +<?php if (!empty($error)) { + echo "<p class='alert alert-danger'>"; + echo implode("<br />\n", $error); + echo "</p>"; +} ?> +<?php echo form_open('user/register/'.$key); ?> +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputUsername">Username</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputUsername" name="username" placeholder="Username" value="<?php echo $values["username"]; ?>" class="form-control"> + <span class="help-block">The username may contain up to 32 chars of a-z0-9 (only lowercase characters).</span> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputEmail">Email</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputEmail" name="email" placeholder="Email" value="<?php echo $values["email"]; ?>" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPassword" name="password" placeholder="Password" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Confirm password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPasswordConfirm" name="password_confirm" placeholder="Password confirmation" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" class="btn btn-primary" name="process">Register</button> + </div> + </div> +</div> +</form> + diff --git a/application/views/user/registered.php b/application/views/user/registered.php new file mode 100644 index 000000000..f13006aae --- /dev/null +++ b/application/views/user/registered.php @@ -0,0 +1,3 @@ +<div class="center"> + <p>Your account has been created, you may log in now.</p> +</div> diff --git a/application/views/user/reset_password_form.php b/application/views/user/reset_password_form.php new file mode 100644 index 000000000..9c8253189 --- /dev/null +++ b/application/views/user/reset_password_form.php @@ -0,0 +1,33 @@ +<?php if (!empty($error)) { + echo "<p class='alert alert-danger'>"; + echo implode("<br />\n", $error); + echo "</p>"; +} ?> +<?php echo form_open('user/reset_password/'.$key); ?> +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPassword" name="password" placeholder="Password" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputPassword">Confirm password</label> + <div class="col-lg-5 col-md-5"> + <input type="password" id="inputPasswordConfirm" name="password_confirm" placeholder="Password confirmation" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" class="btn btn-primary" name="process">Change password</button> + </div> + </div> +</div> +</form> + diff --git a/application/views/user/reset_password_link_sent.php b/application/views/user/reset_password_link_sent.php new file mode 100644 index 000000000..a5b249f89 --- /dev/null +++ b/application/views/user/reset_password_link_sent.php @@ -0,0 +1,3 @@ +<p> + A mail containing your password reset link has been sent to your email address at <?php echo htmlentities($email_domain); ?>. +</p> diff --git a/application/views/user/reset_password_success.php b/application/views/user/reset_password_success.php new file mode 100644 index 000000000..bc7448833 --- /dev/null +++ b/application/views/user/reset_password_success.php @@ -0,0 +1,3 @@ +<div class="center"> + <p>Your password has been changed successfully.</p> +</div> diff --git a/application/views/user/reset_password_username_form.php b/application/views/user/reset_password_username_form.php new file mode 100644 index 000000000..713cd4919 --- /dev/null +++ b/application/views/user/reset_password_username_form.php @@ -0,0 +1,19 @@ +<?php echo form_open('user/reset_password'); ?> +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <label class="control-label col-lg-2 col-md-2" for="inputUsername">Username</label> + <div class="col-lg-5 col-md-5"> + <input type="text" id="inputUsername" name="username" placeholder="Username" value="<?php echo isset($username) ? $username : ""; ?>" class="form-control"> + </div> + </div> +</div> + +<div class="row"> + <div class="form-group col-lg-8 col-md-10"> + <div class="col-lg-offset-2 col-lg-5 col-md-offset-2 col-md-5"> + <button type="submit" class="btn btn-primary" name="process">Send mail</button> + </div> + </div> +</div> +</form> + diff --git a/application/views/welcome_message.php b/application/views/welcome_message.php deleted file mode 100644 index f5115630b..000000000 --- a/application/views/welcome_message.php +++ /dev/null @@ -1,89 +0,0 @@ -<?php -defined('BASEPATH') OR exit('No direct script access allowed'); -?><!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <title>Welcome to CodeIgniter</title> - - <style type="text/css"> - - ::selection { background-color: #E13300; color: white; } - ::-moz-selection { background-color: #E13300; color: white; } - - body { - background-color: #fff; - margin: 40px; - font: 13px/20px normal Helvetica, Arial, sans-serif; - color: #4F5155; - } - - a { - color: #003399; - background-color: transparent; - font-weight: normal; - } - - h1 { - color: #444; - background-color: transparent; - border-bottom: 1px solid #D0D0D0; - font-size: 19px; - font-weight: normal; - margin: 0 0 14px 0; - padding: 14px 15px 10px 15px; - } - - code { - font-family: Consolas, Monaco, Courier New, Courier, monospace; - font-size: 12px; - background-color: #f9f9f9; - border: 1px solid #D0D0D0; - color: #002166; - display: block; - margin: 14px 0 14px 0; - padding: 12px 10px 12px 10px; - } - - #body { - margin: 0 15px 0 15px; - } - - p.footer { - text-align: right; - font-size: 11px; - border-top: 1px solid #D0D0D0; - line-height: 32px; - padding: 0 10px 0 10px; - margin: 20px 0 0 0; - } - - #container { - margin: 10px; - border: 1px solid #D0D0D0; - box-shadow: 0 0 8px #D0D0D0; - } - </style> -</head> -<body> - -<div id="container"> - <h1>Welcome to CodeIgniter!</h1> - - <div id="body"> - <p>The page you are looking at is being generated dynamically by CodeIgniter.</p> - - <p>If you would like to edit this page you'll find it located at:</p> - <code>application/views/welcome_message.php</code> - - <p>The corresponding controller for this page is found at:</p> - <code>application/controllers/Welcome.php</code> - - <p>If you are exploring CodeIgniter for the very first time, you should start by reading the <a href="user_guide/">User Guide</a>.</p> - </div> - - <p class="footer">Page rendered in <strong>{elapsed_time}</strong> seconds. <?php echo (ENVIRONMENT === 'development') ? 'CodeIgniter Version <strong>' . CI_VERSION . '</strong>' : '' ?></p> -</div> - -</body> -</html>
\ No newline at end of file |