X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=classes%2FFile.php;h=b28f1373d687e92949cb3eb658e9b58e5c96e3aa;hb=3d6e25ee5f78d4fc3e00335d39724694264bbd53;hp=1a29ea5107e537dec350188c4b7b323e523b2e76;hpb=daaafd86e2dfea8e9c3e4f0c41f7cb77c926c104;p=quix0rs-gnu-social.git diff --git a/classes/File.php b/classes/File.php index 1a29ea5107..b28f1373d6 100644 --- a/classes/File.php +++ b/classes/File.php @@ -31,10 +31,10 @@ class File extends Managed_DataObject public $filehash; // varchar(64) indexed public $mimetype; // varchar(50) public $size; // int(4) - public $title; // varchar(191) not 255 because utf8mb4 takes more space + public $title; // text() public $date; // int(4) public $protected; // int(4) - public $filename; // varchar(191) not 255 because utf8mb4 takes more space + public $filename; // text() public $width; // int(4) public $height; // int(4) public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP @@ -52,10 +52,10 @@ class File extends Managed_DataObject 'filehash' => array('type' => 'varchar', 'length' => 64, 'not null' => false, 'description' => 'sha256 of the file contents, only for locally stored files of course'), 'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'), 'size' => array('type' => 'int', 'description' => 'size of resource when available'), - 'title' => array('type' => 'varchar', 'length' => 191, 'description' => 'title of resource when available'), + 'title' => array('type' => 'text', 'description' => 'title of resource when available'), 'date' => array('type' => 'int', 'description' => 'date of resource according to http query'), 'protected' => array('type' => 'int', 'description' => 'true when URL is private (needs login)'), - 'filename' => array('type' => 'varchar', 'length' => 191, 'description' => 'if a local file, name of the file'), + 'filename' => array('type' => 'text', 'description' => 'if file is stored locally (too) this is the filename'), 'width' => array('type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'), 'height' => array('type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'), @@ -71,8 +71,20 @@ class File extends Managed_DataObject ); } - function isProtected($url) { - return 'http://www.facebook.com/login.php' === $url; + public static function isProtected($url) { + + $protected_urls_exps = array( + 'https://www.facebook.com/login.php', + common_path('main/login') + ); + + foreach ($protected_urls_exps as $protected_url_exp) { + if (preg_match('!^'.preg_quote($protected_url_exp).'(.*)$!i', $url) === 1) { + return true; + } + } + + return false; } /** @@ -82,30 +94,79 @@ class File extends Managed_DataObject * @param string $given_url * @return File */ - public static function saveNew(array $redir_data, $given_url) { + public static function saveNew(array $redir_data, $given_url) + { + $file = null; + try { + // I don't know why we have to keep doing this but we run a last check to avoid + // uniqueness bugs. + $file = File::getByUrl($given_url); + return $file; + } catch (NoResultException $e) { + // We don't have the file's URL since before, so let's continue. + } - // I don't know why we have to keep doing this but I'm adding this last check to avoid - // uniqueness bugs. + // if the given url is an local attachment url and the id already exists, don't + // save a new file record. This should never happen, but let's make it foolproof + // FIXME: how about attachments servers? + $u = parse_url($given_url); + if (isset($u['host']) && $u['host'] === common_config('site', 'server')) { + $r = Router::get(); + // Skip the / in the beginning or $r->map won't match + try { + $args = $r->map(mb_substr($u['path'], 1)); + if ($args['action'] === 'attachment') { + try { + // $args['attachment'] should always be set if action===attachment, given our routing rules + $file = File::getByID($args['attachment']); + return $file; + } catch (EmptyPkeyValueException $e) { + // ...but $args['attachment'] can also be 0... + } catch (NoResultException $e) { + // apparently this link goes to us, but is _not_ an existing attachment (File) ID? + } + } + } catch (Exception $e) { + // Some other exception was thrown from $r->map, likely a + // ClientException (404) because of some malformed link to + // our own instance. It's still a valid URL however, so we + // won't abort anything... I noticed this when linking: + // https://social.umeahackerspace.se/mmn/foaf' (notice the + // apostrophe in the end, making it unrecognizable for our + // URL routing. + // That specific issue (the apostrophe being part of a link + // is something that may or may not have been fixed since, + // in lib/util.php in common_replace_urls_callback(). + } + } - $file = File::getKV('urlhash', self::hashurl($given_url)); - - if (!$file instanceof File) { - $file = new File; - $file->urlhash = self::hashurl($given_url); - $file->url = $given_url; - if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected']; - if (!empty($redir_data['title'])) $file->title = $redir_data['title']; - if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type']; - if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']); - if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']); - $file_id = $file->insert(); - } - - Event::handle('EndFileSaveNew', array($file, $redir_data, $given_url)); - assert ($file instanceof File); + $file = new File; + $file->url = $given_url; + if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected']; + if (!empty($redir_data['title'])) $file->title = $redir_data['title']; + if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type']; + if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']); + if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']); + $file->saveFile(); return $file; } + public function saveFile() { + $this->urlhash = self::hashurl($this->url); + + if (!Event::handle('StartFileSaveNew', array(&$this))) { + throw new ServerException('File not saved due to an aborted StartFileSaveNew event.'); + } + + $this->id = $this->insert(); + + if ($this->id === false) { + throw new ServerException('File/URL metadata could not be saved to the database.'); + } + + Event::handle('EndFileSaveNew', array($this)); + } + /** * Go look at a URL and possibly save data about it if it's new: * - follow redirect chains and store them in file_redirection @@ -114,7 +175,6 @@ class File extends Managed_DataObject * - optionally save a file_to_post record * - return the File object with the full reference * - * @fixme refactor this mess, it's gotten pretty scary. * @param string $given_url the URL we're looking at * @param Notice $notice (optional) * @param bool $followRedirects defaults to true @@ -133,69 +193,18 @@ class File extends Managed_DataObject throw new ServerException('No canonical URL from given URL to process'); } - $file = null; - - try { - $file = File::getByUrl($given_url); - } catch (NoResultException $e) { - // First check if we have a lookup trace for this URL already - try { - $file_redir = File_redirection::getByUrl($given_url); - $file = File::getKV('id', $file_redir->file_id); - if (!$file instanceof File) { - // File did not exist, let's clean up the File_redirection entry - $file_redir->delete(); - } - } catch (NoResultException $e) { - // We just wanted to doublecheck whether a File_thumbnail we might've had - // actually referenced an existing File object. - } - } - - // If we still don't have a File object, let's create one now! - if (!$file instanceof File) { - // @fixme for new URLs this also looks up non-redirect data - // such as target content type, size, etc, which we need - // for File::saveNew(); so we call it even if not following - // new redirects. - $redir_data = File_redirection::where($given_url); - if (is_array($redir_data)) { - $redir_url = $redir_data['url']; - } elseif (is_string($redir_data)) { - $redir_url = $redir_data; - $redir_data = array(); - } else { - // TRANS: Server exception thrown when a URL cannot be processed. - throw new ServerException(sprintf(_("Cannot process URL '%s'"), $given_url)); - } - - if ($redir_url === $given_url || !$followRedirects) { - // Save the File object based on our lookup trace - $file = File::saveNew($redir_data, $given_url); - } else { - // This seems kind of messed up... for now skipping this part - // if we're already under a redirect, so we don't go into - // horrible infinite loops if we've been given an unstable - // redirect (where the final destination of the first request - // doesn't match what we get when we ask for it again). - // - // Seen in the wild with clojure.org, which redirects through - // wikispaces for auth and appends session data in the URL params. - $file = self::processNew($redir_url, $notice, /*followRedirects*/false); - File_redirection::saveNew($redir_data, $file->id, $given_url); - } + $redir = File_redirection::where($given_url); + $file = $redir->getFile(); - if (!$file instanceof File) { - // This should only happen if File::saveNew somehow did not return a File object, - // though we have an assert for that in case the event there might've gone wrong. - // If anything else goes wrong, there should've been an exception thrown. - throw new ServerException('URL processing failed without new File object'); - } + if (!$file instanceof File || empty($file->id)) { + // This should not happen + throw new ServerException('URL processing failed without new File object'); } if ($notice instanceof Notice) { File_to_post::processNew($file, $notice); } + return $file; } @@ -251,18 +260,19 @@ class File extends Managed_DataObject public function getFilename() { - if (!self::validFilename($this->filename)) { - // TRANS: Client exception thrown if a file upload does not have a valid name. - throw new ClientException(_("Invalid filename.")); - } - return $this->filename; + return self::tryFilename($this->filename); + } + + public function getSize() + { + return intval($this->size); } // where should the file go? static function filename(Profile $profile, $origname, $mimetype) { - $ext = self::guessMimeExtension($mimetype); + $ext = self::guessMimeExtension($mimetype, $origname); // Normalize and make the original filename more URL friendly. $origname = basename($origname, ".$ext"); @@ -283,15 +293,53 @@ class File extends Managed_DataObject return $filename; } - static function guessMimeExtension($mimetype) + /** + * @param $mimetype The mimetype we've discovered for this file. + * @param $filename An optional filename which we can use on failure. + */ + static function guessMimeExtension($mimetype, $filename=null) { try { + // first see if we know the extension for our mimetype $ext = common_supported_mime_to_ext($mimetype); - } catch (Exception $e) { - // We don't support this mimetype, but let's guess the extension - $ext = substr(strrchr($mimetype, '/'), 1); + // we do, so use it! + return $ext; + } catch (Exception $e) { // FIXME: Make this exception more specific to "unknown mime=>ext relation" + // We don't know the extension for this mimetype, but let's guess. + + // If we are very liberal with uploads ($config['attachments']['supported'] === true) + // then we try to do some guessing based on the filename, if it was supplied. + if (!is_null($filename) && common_config('attachments', 'supported')===true + && preg_match('/^.+\.([A-Za-z0-9]+)$/', $filename, $matches)) { + // we matched on a file extension, so let's see if it means something. + $ext = mb_strtolower($matches[1]); + + $blacklist = common_config('attachments', 'extblacklist'); + // If we got an extension from $filename we want to check if it's in a blacklist + // so we avoid people uploading .php files etc. + if (array_key_exists($ext, $blacklist)) { + if (!is_string($blacklist[$ext])) { + // we don't have a safe replacement extension + throw new ClientException(_('Blacklisted file extension.')); + } + common_debug('Found replaced extension for filename '._ve($filename).': '._ve($ext)); + + // return a safe replacement extension ('php' => 'phps' for example) + return $blacklist[$ext]; + } + // the attachment extension based on its filename was not blacklisted so it's ok to use it + return $ext; + } + } + + // If nothing else has given us a result, try to extract it from + // the mimetype value (this turns .jpg to .jpeg for example...) + $matches = array(); + // FIXME: try to build a regexp that will get jpeg from image/jpeg as well as json from application/jrd+json + if (!preg_match('/\/([a-z0-9]+)/', mb_strtolower($mimetype), $matches)) { + throw new Exception('Malformed mimetype: '.$mimetype); } - return strtolower($ext); + return mb_strtolower($matches[1]); } /** @@ -302,19 +350,27 @@ class File extends Managed_DataObject return preg_match('/^[A-Za-z0-9._-]+$/', $filename); } + static function tryFilename($filename) + { + if (!self::validFilename($filename)) + { + throw new InvalidFilenameException($filename); + } + // if successful, return the filename for easy if-statementing + return $filename; + } + /** * @throws ClientException on invalid filename */ static function path($filename) { - if (!self::validFilename($filename)) { - // TRANS: Client exception thrown if a file upload does not have a valid name. - throw new ClientException(_("Invalid filename.")); - } + self::tryFilename($filename); + $dir = common_config('attachments', 'dir'); - if ($dir[strlen($dir)-1] != '/') { - $dir .= '/'; + if (!in_array($dir[mb_strlen($dir)-1], ['/', '\\'])) { + $dir .= DIRECTORY_SEPARATOR; } return $dir . $filename; @@ -322,10 +378,7 @@ class File extends Managed_DataObject static function url($filename) { - if (!self::validFilename($filename)) { - // TRANS: Client exception thrown if a file upload does not have a valid name. - throw new ClientException(_("Invalid filename.")); - } + self::tryFilename($filename); if (common_config('site','private')) { @@ -381,28 +434,48 @@ class File extends Managed_DataObject return $protocol.'://'.$server.$path.$filename; } + static $_enclosures = array(); + function getEnclosure(){ + if (isset(self::$_enclosures[$this->getID()])) { + return self::$_enclosures[$this->getID()]; + } + $enclosure = (object) array(); - foreach (array('title', 'url', 'date', 'modified', 'size', 'mimetype') as $key) { - $enclosure->$key = $this->$key; + foreach (array('title', 'url', 'date', 'modified', 'size', 'mimetype', 'width', 'height') as $key) { + if ($this->$key !== '') { + $enclosure->$key = $this->$key; + } } - $needMoreMetadataMimetypes = array(null, 'application/xhtml+xml'); + $needMoreMetadataMimetypes = array(null, 'application/xhtml+xml', 'text/html'); if (!isset($this->filename) && in_array(common_bare_mime($enclosure->mimetype), $needMoreMetadataMimetypes)) { // This fetches enclosure metadata for non-local links with unset/HTML mimetypes, // which may be enriched through oEmbed or similar (implemented as plugins) Event::handle('FileEnclosureMetadata', array($this, &$enclosure)); } - if (empty($enclosure->mimetype) || in_array(common_bare_mime($enclosure->mimetype), $needMoreMetadataMimetypes)) { + if (empty($enclosure->mimetype)) { // This means we either don't know what it is, so it can't // be shown as an enclosure, or it is an HTML link which // does not link to a resource with further metadata. throw new ServerException('Unknown enclosure mimetype, not enough metadata'); } + + self::$_enclosures[$this->getID()] = $enclosure; return $enclosure; } + public function hasThumbnail() + { + try { + $this->getThumbnail(); + } catch (Exception $e) { + return false; + } + return true; + } + /** * Get the attachment's thumbnail record, if any. * Make sure you supply proper 'int' typed variables (or null). @@ -410,6 +483,8 @@ class File extends Managed_DataObject * @param $width int Max width of thumbnail in pixels. (if null, use common_config values) * @param $height int Max height of thumbnail in pixels. (if null, square-crop to $width) * @param $crop bool Crop to the max-values' aspect ratio + * @param $force_still bool Don't allow fallback to showing original (such as animated GIF) + * @param $upscale mixed Whether or not to scale smaller images up to larger thumbnail sizes. (null = site default) * * @return File_thumbnail * @@ -417,7 +492,7 @@ class File extends Managed_DataObject * @throws UnsupportedMediaException if, despite trying, we can't understand how to make a thumbnail for this format * @throws ServerException on various other errors */ - public function getThumbnail($width=null, $height=null, $crop=false, $force_still=true) + public function getThumbnail($width=null, $height=null, $crop=false, $force_still=true, $upscale=null) { // Get some more information about this file through our ImageFile class $image = ImageFile::fromFileObject($this); @@ -425,11 +500,18 @@ class File extends Managed_DataObject // null means "always use file as thumbnail" // false means you get choice between frozen frame or original when calling getThumbnail if (is_null(common_config('thumbnail', 'animated')) || !$force_still) { - throw new UseFileAsThumbnailException($this->id); + try { + // remote files with animated GIFs as thumbnails will match this + return File_thumbnail::byFile($this); + } catch (NoResultException $e) { + // and if it's not a remote file, it'll be safe to use the locally stored File + throw new UseFileAsThumbnailException($this); + } } } - return $image->getFileThumbnail($width, $height, $crop); + return $image->getFileThumbnail($width, $height, $crop, + !is_null($upscale) ? $upscale : common_config('thumbnail', 'upscale')); } public function getPath() @@ -441,17 +523,16 @@ class File extends Managed_DataObject return $filepath; } - public function getUrl() + public function getAttachmentUrl() { - if (!empty($this->filename)) { + return common_local_url('attachment', array('attachment'=>$this->getID())); + } + + public function getUrl($prefer_local=true) + { + if ($prefer_local && !empty($this->filename)) { // A locally stored file, so let's generate a URL for our instance. - $url = self::url($this->filename); - if (self::hashurl($url) !== $this->urlhash) { - // For indexing purposes, in case we do a lookup on the 'url' field. - // also we're fixing possible changes from http to https, or paths - $this->updateUrl($url); - } - return $url; + return self::url($this->getFilename()); } // No local filename available, return the URL we have stored @@ -471,7 +552,7 @@ class File extends Managed_DataObject /** * @param string $hashstr String of (preferrably lower case) hexadecimal characters, same as result of 'hash_file(...)' */ - static public function getByHash($hashstr, $alg=File::FILEHASH_ALG) + static public function getByHash($hashstr) { $file = new File(); $file->filehash = strtolower($hashstr); @@ -530,7 +611,9 @@ class File extends Managed_DataObject function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) { - $stream = new FileNoticeStream($this); + // FIXME: Try to get the Profile::current() here in some other way to avoid mixing + // the current session user with possibly background/queue processing. + $stream = new FileNoticeStream($this, Profile::current()); return $stream->getNotices($offset, $limit, $since_id, $max_id); }