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 (!Event::handle('StartFileSaveNew', array(&$redir_data, $given_url))) {
- throw new ServerException('File not saved due to an aborted StartFileSaveNew event.');
+ // 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->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();
+ $file->saveFile();
+ return $file;
+ }
- if ($file_id === false) {
+ 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($file, $redir_data, $given_url));
- return $file;
+ Event::handle('EndFileSaveNew', array($this));
}
/**
* - 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
throw new ServerException('No canonical URL from given URL to process');
}
- $file = null;
+ $redir = File_redirection::where($given_url);
+ $file = $redir->getFile();
- 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);
- }
-
- 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;
}
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);
+ // we do, so use it!
+ return $ext;
+ } catch (UnknownMimeExtensionException $e) {
+ // We don't know the extension for this mimetype, but let's guess.
+
+ // If we can't recognize the extension from the MIME, we try
+ // to guess based on filename, if one was supplied.
+ if (!is_null($filename) && 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;
+ }
} catch (Exception $e) {
- // We don't support this mimetype, but let's guess the extension
- $ext = substr(strrchr($mimetype, '/'), 1);
+ common_log(LOG_INFO, 'Problem when figuring out extension for mimetype: '._ve($e));
}
- return strtolower($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 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()
+ {
+ return common_local_url('attachment', array('attachment'=>$this->getID()));
+ }
+
+ /**
+ * @param mixed $use_local true means require local, null means prefer local, false means use whatever is stored
+ */
+ public function getUrl($use_local=null)
{
- if (!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);
+ if ($use_local !== false) {
+ if (is_string($this->filename) || !empty($this->filename)) {
+ // A locally stored file, so let's generate a URL for our instance.
+ return self::url($this->getFilename());
+ }
+ if ($use_local) {
+ // if the file wasn't stored locally (has filename) and we require a local URL
+ throw new FileNotStoredLocallyException($this);
}
- return $url;
}
+
// No local filename available, return the URL we have stored
return $this->url;
}
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);
}