diff options
Diffstat (limited to 'application/libraries')
19 files changed, 1856 insertions, 0 deletions
diff --git a/application/libraries/Customautoloader.php b/application/libraries/Customautoloader.php new file mode 100644 index 000000000..426364ee3 --- /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..808dfe776 --- /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( + 'ddownload_php', 'ddownload_nginx', 'ddownload_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..344db53f0 --- /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..8005a00bb --- /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( + 'duser_db', 'duser_ldap', 'duser_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..b80385fe0 --- /dev/null +++ b/application/libraries/Duser/drivers/Duser_ldap.php @@ -0,0 +1,68 @@ +<?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; + } + + switch ($config["scope"]) { + case "base": + $r = ldap_read($ds, $config['basedn'], $config["username_field"].'='.$username); + break; + case "one": + $r = ldap_list($ds, $config['basedn'], $config["username_field"].'='.$username); + break; + case "subtree": + $r = ldap_search($ds, $config['basedn'], $config["username_field"].'='.$username); + 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..8295469eb --- /dev/null +++ b/application/libraries/Image/Drivers/imagemagick.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 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" + || in_array($mimetype, array("application/pdf"))) { + 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..174642141 --- /dev/null +++ b/application/libraries/Pygments.php @@ -0,0 +1,245 @@ +<?php +/* + * Copyright 2015 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace libraries; + +class Pygments { + private $file; + private $mimetype; + private $filename; + + public function __construct($file, $mimetype, $filename) { + $this->file = $file; + $this->mimetype = $mimetype; + $this->filename = $filename; + } + + private static function get_pygments_info() { + return cache_function_full('pygments_info-v2', 1800, function() { + $r = (new \libraries\ProcRunner(array(FCPATH."scripts/get_lexer_list.py")))->execSafe(); + + $ret = json_decode($r["stdout"], true); + if ($ret === NULL) { + throw new \exceptions\ApiException('pygments/json-failed', "Failed to decode JSON", $r); + } + + return $ret; + }); + } + + public static function get_lexers() { + return cache_function('lexers-v2', 1800, function() { + $last_desc = ""; + + foreach (self::get_pygments_info() as $lexer) { + $desc = $lexer['fullname']; + $name = $lexer['names'][0]; + if ($desc == $last_desc) { + continue; + } + $last_desc = $desc; + $lexers[$name] = $desc; + } + $lexers["text"] = "Plain text"; + return $lexers; + }); + } + + // Allow certain types to be highlight without doing it automatically + public function should_highlight() + { + $typearray = array( + 'image/svg+xml', + ); + if (in_array($this->mimetype, $typearray)) return false; + + if ($this->mime2lexer($this->mimetype)) return true; + + return false; + } + + public function can_highlight() + { + if ($this->mime2lexer($this->mimetype)) return true; + + return false; + } + + // Return the lexer that should be used for highlighting + public function autodetect_lexer() + { + if (!$this->should_highlight()) { + return false; + } + + $lexer = $this->mime2lexer($this->mimetype); + + // filename lexers overwrite mime type mappings + $filename_lexer = $this->filename2lexer(); + if ($filename_lexer) { + return $filename_lexer; + } + + return $lexer; + } + + // Map MIME types to lexers needed for highlighting + private function mime2lexer() + { + $typearray = array( + 'application/javascript' => 'javascript', + 'application/mbox' => 'text', + 'application/postscript' => 'postscript', + 'application/smil' => 'ocaml', + 'application/x-applix-spreadsheet' => 'actionscript', + 'application/x-awk' => 'awk', + 'application/x-desktop' => 'text', + 'application/x-fluid' => 'text', + 'application/x-genesis-rom' => 'text', + 'application/x-java' => 'java', + 'application/x-m4' => 'text', + 'application/xml-dtd' => "xml", + 'application/xml' => 'xml', + 'application/x-perl' => 'perl', + 'application/x-php' => 'php', + 'application/x-ruby' => 'ruby', + 'application/x-shellscript' => 'bash', + 'application/xslt+xml' => "xml", + 'application/x-x509-ca-cert' => 'text', + 'message/rfc822' => 'text', + 'text/css' => 'css', + 'text/html' => 'xml', + 'text/plain-ascii' => 'ascii', + 'text/plain' => 'text', + 'text/troff' => 'groff', + 'text/x-asm' => 'nasm', + 'text/x-awk' => 'awk', + 'text/x-c' => 'c', + 'text/x-c++' => 'cpp', + 'text/x-c++hdr' => 'c', + 'text/x-chdr' => 'c', + 'text/x-csrc' => 'c', + 'text/x-c++src' => 'cpp', + 'text/x-diff' => 'diff', + 'text/x-gawk' => 'awk', + 'text/x-haskell' => 'haskell', + 'text/x-java' => 'java', + 'text/x-lisp' => 'cl', + 'text/x-literate-haskell' => 'haskell', + 'text/x-lua' => 'lua', + 'text/x-makefile' => 'make', + 'text/x-ocaml' => 'ocaml', + 'text/x-patch' => 'diff', + 'text/x-perl' => 'perl', + 'text/x-php' => 'php', + 'text/x-python' => 'python', + 'text/x-ruby' => 'ruby', + 'text/x-scheme' => 'scheme', + 'text/x-shellscript' => 'bash', + 'text/x-subviewer' => 'bash', + 'text/x-tcl' => 'tcl', + 'text/x-tex' => 'tex', + ); + if (array_key_exists($this->mimetype, $typearray)) return $typearray[$this->mimetype]; + + // fall back to pygments own list if not found in our list + foreach (self::get_pygments_info() as $lexer) { + if (isset($lexer['mimetypes'][$this->mimetype])) { + return $lexer['names'][0]; + } + } + + if (strpos($this->mimetype, 'text/') === 0) return 'text'; + + # default + return false; + } + + // Map special filenames to lexers + private function filename2lexer() + { + $namearray = array( + 'asciinema.json' => 'asciinema', + 'PKGBUILD' => 'bash', + '.vimrc' => 'vim' + ); + if (array_key_exists($this->filename, $namearray)) return $namearray[$this->filename]; + + $longextarray = array( + '.asciinema.json' => 'asciinema', + ); + foreach ($longextarray as $key => $lexer) { + if (substr($this->filename, -strlen($key)) === $key) { + return $lexer; + } + } + + if (strpos($this->filename, ".") !== false) { + $extension = substr($this->filename, strrpos($this->filename, ".") + 1); + if ($extension === false) { + return false; + } + + $extensionarray = array( + 'awk' => 'awk', + 'c' => 'c', + 'coffee' => 'coffee-script', + 'cpp' => 'cpp', + 'diff' => 'diff', + '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', + 'postgresql' => 'postgresql', + 'pp' => 'puppet', + 'py' => 'python', + 'rb' => 'ruby', + '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; + } +} |