summaryrefslogtreecommitdiffstats
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/config/.gitignore3
-rw-r--r--application/config/autoload.php4
-rw-r--r--application/config/config.php144
-rw-r--r--application/config/constants.php2
-rw-r--r--application/config/example/.gitignore1
-rw-r--r--application/config/example/config-local.php19
-rw-r--r--application/config/example/database.php (renamed from application/config/database.php)18
-rw-r--r--application/config/example/index.html10
-rw-r--r--application/config/example/memcached.php17
-rw-r--r--application/config/migration.php7
-rw-r--r--application/config/routes.php8
-rw-r--r--application/controllers/Api.php93
-rw-r--r--application/controllers/Main.php942
-rw-r--r--application/controllers/Tools.php119
-rw-r--r--application/controllers/User.php717
-rw-r--r--application/controllers/Welcome.php25
-rw-r--r--application/controllers/api/api_controller.php18
-rw-r--r--application/controllers/api/v2/api_info.php16
-rw-r--r--application/controllers/api/v2/file.php96
-rw-r--r--application/controllers/api/v2/user.php75
-rw-r--r--application/controllers/file/Multipaste.php113
-rw-r--r--application/core/MY_Controller.php128
-rw-r--r--application/core/MY_Input.php40
-rw-r--r--application/core/MY_Output.php16
-rw-r--r--application/exceptions/ApiException.php35
-rw-r--r--application/exceptions/FileUploadVerifyException.php23
-rw-r--r--application/exceptions/InsufficientPermissionsException.php14
-rw-r--r--application/exceptions/NotAuthenticatedException.php14
-rw-r--r--application/exceptions/NotFoundException.php14
-rw-r--r--application/exceptions/PublicApiException.php14
-rw-r--r--application/exceptions/RequestTooBigException.php14
-rw-r--r--application/exceptions/UserInputException.php14
-rw-r--r--application/exceptions/VerifyException.php23
-rw-r--r--application/helpers/filebin_helper.php325
-rw-r--r--application/libraries/Customautoloader.php42
-rw-r--r--application/libraries/Ddownload/Ddownload.php34
-rw-r--r--application/libraries/Ddownload/drivers/Ddownload_lighttpd.php26
-rw-r--r--application/libraries/Ddownload/drivers/Ddownload_nginx.php29
-rw-r--r--application/libraries/Ddownload/drivers/Ddownload_php.php111
-rw-r--r--application/libraries/Duser/Duser.php124
-rw-r--r--application/libraries/Duser/drivers/Duser_db.php79
-rw-r--r--application/libraries/Duser/drivers/Duser_fluxbb.php53
-rw-r--r--application/libraries/Duser/drivers/Duser_ldap.php79
-rw-r--r--application/libraries/ExceptionHandler.php154
-rw-r--r--application/libraries/Exif.php37
-rw-r--r--application/libraries/Image.php236
-rw-r--r--application/libraries/Image/Drivers/GD.php217
-rw-r--r--application/libraries/Image/Drivers/imagemagick.php120
-rw-r--r--application/libraries/MY_Session.php38
-rw-r--r--application/libraries/Output_cache.php82
-rw-r--r--application/libraries/ProcRunner.php129
-rw-r--r--application/libraries/Pygments.php252
-rw-r--r--application/libraries/Tempfile.php31
-rw-r--r--application/migrations/001_add_files.php45
-rw-r--r--application/migrations/002_add_users.php79
-rw-r--r--application/migrations/003_add_referrers.php61
-rw-r--r--application/migrations/004_add_filesize.php37
-rw-r--r--application/migrations/005_drop_file_password.php33
-rw-r--r--application/migrations/006_add_username_index.php35
-rw-r--r--application/migrations/007_repurpose_invitations.php68
-rw-r--r--application/migrations/008_add_profiles.php61
-rw-r--r--application/migrations/009_add_apikeys.php41
-rw-r--r--application/migrations/010_files_innodb.php20
-rw-r--r--application/migrations/011_apikeys_add_access_level.php35
-rw-r--r--application/migrations/012_add_constraints.php36
-rw-r--r--application/migrations/013_add_multipaste.php62
-rw-r--r--application/migrations/014_deduplicate_file_storage.php137
-rw-r--r--application/migrations/015_actually_deduplicate_file_storage.php56
-rw-r--r--application/migrations/016_allow_ipv6_storage.php27
-rw-r--r--application/migrations/017_increase_password_length.php27
-rw-r--r--application/migrations/018_allow_null_values_userinfo.php31
-rw-r--r--application/migrations/019_change_filesize_type.php51
-rw-r--r--application/migrations/020_update_session_table.php45
-rw-r--r--application/migrations/021_change_charset.php28
-rw-r--r--application/models/Mfile.php275
-rw-r--r--application/models/Mmultipaste.php184
-rw-r--r--application/models/Muser.php406
-rw-r--r--application/service/files.php553
-rw-r--r--application/service/multipaste_queue.php96
-rw-r--r--application/service/renderer.php187
-rw-r--r--application/service/storage.php160
-rw-r--r--application/service/user.php128
-rw-r--r--application/test/Test.php186
-rw-r--r--application/test/tests/api_v2/common.php60
-rw-r--r--application/test/tests/api_v2/test_api_permissions.php108
-rw-r--r--application/test/tests/api_v2/test_create_apikey.php66
-rw-r--r--application/test/tests/api_v2/test_file_create_multipaste.php125
-rw-r--r--application/test/tests/api_v2/test_file_delete.php82
-rw-r--r--application/test/tests/api_v2/test_file_upload.php71
-rw-r--r--application/test/tests/api_v2/test_history.php137
-rw-r--r--application/test/tests/api_v2/test_misc.php48
-rw-r--r--application/test/tests/api_v2/test_user_delete_apikey.php49
-rw-r--r--application/test/tests/test_database_schema.php38
-rw-r--r--application/test/tests/test_filebin_helper.php108
-rw-r--r--application/test/tests/test_libraries_exif.php48
-rw-r--r--application/test/tests/test_libraries_image.php110
-rw-r--r--application/test/tests/test_libraries_output_cache.php82
-rw-r--r--application/test/tests/test_libraries_procrunner.php118
-rw-r--r--application/test/tests/test_libraries_pygments.php115
-rw-r--r--application/test/tests/test_libraries_tempfile.php46
-rw-r--r--application/test/tests/test_models_muser.php113
-rw-r--r--application/test/tests/test_service_files.php121
-rw-r--r--application/test/tests/test_service_files_valid_id.php115
-rw-r--r--application/test/tests/test_service_multipaste_queue.php93
-rw-r--r--application/test/tests/test_service_storage.php172
-rw-r--r--application/test/tests/test_service_user.php65
m---------application/third_party/QrCode0
m---------application/third_party/mockery0
m---------application/third_party/parsedown0
-rwxr-xr-xapplication/third_party/test-more-php/Test-More-OO.php455
-rwxr-xr-xapplication/third_party/test-more-php/Test-More.php41
-rwxr-xr-xapplication/third_party/test-more-php/Test-Simple-OO.php237
-rwxr-xr-xapplication/third_party/test-more-php/Test-Simple.php18
-rwxr-xr-xapplication/third_party/test-more-php/t/PHProvable.pl35
-rwxr-xr-xapplication/third_party/test-more-php/t/badlib.php5
-rwxr-xr-xapplication/third_party/test-more-php/t/borklib.php5
-rwxr-xr-xapplication/third_party/test-more-php/t/goodlib.php5
-rwxr-xr-xapplication/third_party/test-more-php/t/goodpage.php5
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_bail_badplan_negative.php9
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_bail_badplan_noninteger.php9
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_bundle.php42
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_deprecated_comparisons.php27
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_deprecated_comparisons.pl25
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_exit_0.php8
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_exit_fail_260.php14
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_exit_fail_5.php14
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.php24
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.php_Test-More.out24
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.php_Test-Simple.out16
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.pl24
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.pl_Test-More.out24
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_ok.pl_Test-Simple.out24
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_skip.php11
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_func_skip.pl9
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_include_ok_badlib.php21
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_include_ok_fatal.php17
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_interp.php10
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_interp_env.php19
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_interp_set.php18
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_is_deeply.php31
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_require_ok_badlib.php12
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_require_ok_borklib.php12
-rwxr-xr-xapplication/third_party/test-more-php/t/testertests_require_ok_missing.php12
-rwxr-xr-xapplication/third_party/test-more-php/t/try.php7
-rw-r--r--application/views/contact.php1
-rw-r--r--application/views/errors/html/error_404.php65
-rw-r--r--application/views/errors/html/error_db.php65
-rw-r--r--application/views/errors/html/error_general.php44
-rw-r--r--application/views/file/deleted.php11
-rw-r--r--application/views/file/file_info.php36
-rw-r--r--application/views/file/fragments/alert-wide.php3
-rw-r--r--application/views/file/fragments/asciinema-player.php8
-rw-r--r--application/views/file/fragments/audio-player.php8
-rw-r--r--application/views/file/fragments/thumbnail.php35
-rw-r--r--application/views/file/fragments/uploads_table.php25
-rw-r--r--application/views/file/fragments/video-player.php12
-rw-r--r--application/views/file/html_footer.php3
-rw-r--r--application/views/file/html_header.php13
-rw-r--r--application/views/file/html_paste_footer.php2
-rw-r--r--application/views/file/html_paste_header.php87
-rw-r--r--application/views/file/multipaste/queue.php32
-rw-r--r--application/views/file/multipaste_info.php26
-rw-r--r--application/views/file/nav_history.php18
-rw-r--r--application/views/file/non_existent.php3
-rw-r--r--application/views/file/show_url.php8
-rw-r--r--application/views/file/upload_form.php168
-rw-r--r--application/views/file/upload_history.php39
-rw-r--r--application/views/file/upload_history_thumbnails.php26
-rw-r--r--application/views/footer.php23
-rw-r--r--application/views/header.php121
-rw-r--r--application/views/tests/echo-fragment.php5
-rw-r--r--application/views/user/apikeys.php60
-rw-r--r--application/views/user/delete_account_form.php27
-rw-r--r--application/views/user/delete_account_success.php8
-rw-r--r--application/views/user/hash_password.php38
-rw-r--r--application/views/user/index.php3
-rw-r--r--application/views/user/invite.php45
-rw-r--r--application/views/user/login.php26
-rw-r--r--application/views/user/nav.php11
-rw-r--r--application/views/user/profile.php48
-rw-r--r--application/views/user/register.php52
-rw-r--r--application/views/user/registered.php3
-rw-r--r--application/views/user/reset_password_form.php33
-rw-r--r--application/views/user/reset_password_link_sent.php3
-rw-r--r--application/views/user/reset_password_success.php3
-rw-r--r--application/views/user/reset_password_username_form.php19
-rw-r--r--application/views/welcome_message.php89
187 files changed, 12638 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..793c88b89
--- /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 data: *; media-src *; font-src data: *; 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..250c9d95c
--- /dev/null
+++ b/application/core/MY_Controller.php
@@ -0,0 +1,128 @@
+<?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);
+
+ if ($this->uri->uri_string() == "file/multipaste/ajax_submit") {
+ $this->config->set_item('csrf_regenerate', false);
+ }
+
+ $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..074e8a6c1
--- /dev/null
+++ b/application/libraries/Pygments.php
@@ -0,0 +1,252 @@
+<?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) {
+ if (empty($lexer['names'])) {
+ continue;
+ }
+ $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">&times;</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..122729eb0
--- /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>/&lt;ID&gt;/</dt><dd>automatically highlight the paste</dd>
+ <dt>/&lt;ID&gt;</dt><dd>set the detected MIME type and let the browser do the rest</dd>
+ <dt>/&lt;ID&gt;/plain</dt><dd>force the MIME type to be text/plain</dd>
+ <dt>/&lt;ID&gt;/&lt;file extension&gt;</dt><dd>override auto detection and use the supplied file extension or language name for highlighting</dd>
+ <dt>/&lt;ID&gt;/qr</dt><dd>display a qr code containing a link to <span class="example">/&lt;ID&gt;/</span></dd>
+ <dt>/&lt;ID&gt;/rmd</dt><dd>convert markdown to HTML</dd>
+ <dt>/&lt;ID&gt;/ascii</dt><dd>convert text with ANSI (shell) escape codes to HTML</dd>
+ <dt>/&lt;ID&gt;/info</dt><dd>display some information about the ID</dd>
+ <dt>/file/thumbnail/&lt;ID&gt;</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>/&lt;ID&gt;/qr</b>,
+ <b>/&lt;ID&gt;/plain</b>, <b>/&lt;ID&gt;/info</b> and
+ <b>/file/thumbnail/&lt;ID&gt;</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>/&lt;ID&gt;/</dt><dd>automatically display everything in a sensible way</dd>
+ <dt>/&lt;ID&gt;/qr</dt><dd>display a qr code containing a link to <span class="example">/&lt;ID&gt;/</span></dd>
+ <dt>/&lt;ID&gt;/info</dt><dd>display some information about the multipaste</dd>
+ <dt>/&lt;ID&gt;/tar</dt><dd>download a tarball of all files in the multipaste (files may be renamed to avoid conflicts)</dd>
+ <dt>/&lt;ID&gt;/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/foss/holgersson-overlay/src/branch/master/README.rst">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">&middot;</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">&times;</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