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
'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'),
);
}
- 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;
}
/**
// We don't have the file's URL since before, so let's continue.
}
+ // 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 = new File;
$file->url = $given_url;
if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
$redir = File_redirection::where($given_url);
$file = $redir->getFile();
- // If we still don't have a File object, let's create one now!
- if (empty($file->id)) {
- if ($redir->url === $given_url || !$followRedirects) {
- // Save the File object based on our lookup trace
- $file->saveFile();
- } else {
- $file->saveFile();
- $redir->file_id = $file->id;
- $redir->insert();
- }
- }
-
if (!$file instanceof File || empty($file->id)) {
// This should not happen
throw new ServerException('URL processing failed without new File object');
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");
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]);
}
/**
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;
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')) {
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).
* @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
*
* @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);
// 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()
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
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);
}