]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/File.php
69aee7fcee1962c7b302dd893390e43eab469642
[quix0rs-gnu-social.git] / classes / File.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2008, 2009, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.     If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 if (!defined('GNUSOCIAL')) { exit(1); }
21
22 /**
23  * Table Definition for file
24  */
25 class File extends Managed_DataObject
26 {
27     public $__table = 'file';                            // table name
28     public $id;                              // int(4)  primary_key not_null
29     public $url;                             // varchar(255)  unique_key
30     public $mimetype;                        // varchar(50)
31     public $size;                            // int(4)
32     public $title;                           // varchar(255)
33     public $date;                            // int(4)
34     public $protected;                       // int(4)
35     public $filename;                        // varchar(255)
36     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
37
38     public static function schemaDef()
39     {
40         return array(
41             'fields' => array(
42                 'id' => array('type' => 'serial', 'not null' => true),
43                 'url' => array('type' => 'varchar', 'length' => 255, 'description' => 'destination URL after following redirections'),
44                 'mimetype' => array('type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'),
45                 'size' => array('type' => 'int', 'description' => 'size of resource when available'),
46                 'title' => array('type' => 'varchar', 'length' => 255, 'description' => 'title of resource when available'),
47                 'date' => array('type' => 'int', 'description' => 'date of resource according to http query'),
48                 'protected' => array('type' => 'int', 'description' => 'true when URL is private (needs login)'),
49                 'filename' => array('type' => 'varchar', 'length' => 255, 'description' => 'if a local file, name of the file'),
50                 'width' => array('type' => 'int', 'description' => 'width in pixels, if it can be described as such and data is available'),
51                 'height' => array('type' => 'int', 'description' => 'height in pixels, if it can be described as such and data is available'),
52
53                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
54             ),
55             'primary key' => array('id'),
56             'unique keys' => array(
57                 'file_url_key' => array('url'),
58             ),
59         );
60     }
61
62     function isProtected($url) {
63         return 'http://www.facebook.com/login.php' === $url;
64     }
65
66     /**
67      * Save a new file record.
68      *
69      * @param array $redir_data lookup data eg from File_redirection::where()
70      * @param string $given_url
71      * @return File
72      */
73     public static function saveNew(array $redir_data, $given_url) {
74
75         // I don't know why we have to keep doing this but I'm adding this last check to avoid
76         // uniqueness bugs.
77
78         $file = File::getKV('url', $given_url);
79         
80         if (!$file instanceof File) {
81             $file = new File;
82             $file->url = $given_url;
83             if (!empty($redir_data['protected'])) $file->protected = $redir_data['protected'];
84             if (!empty($redir_data['title'])) $file->title = $redir_data['title'];
85             if (!empty($redir_data['type'])) $file->mimetype = $redir_data['type'];
86             if (!empty($redir_data['size'])) $file->size = intval($redir_data['size']);
87             if (isset($redir_data['time']) && $redir_data['time'] > 0) $file->date = intval($redir_data['time']);
88             $file_id = $file->insert();
89         }
90
91         Event::handle('EndFileSaveNew', array($file, $redir_data, $given_url));
92         return $file;
93     }
94
95     /**
96      * Go look at a URL and possibly save data about it if it's new:
97      * - follow redirect chains and store them in file_redirection
98      * - if a thumbnail is available, save it in file_thumbnail
99      * - save file record with basic info
100      * - optionally save a file_to_post record
101      * - return the File object with the full reference
102      *
103      * @fixme refactor this mess, it's gotten pretty scary.
104      * @param string $given_url the URL we're looking at
105      * @param int $notice_id (optional)
106      * @param bool $followRedirects defaults to true
107      *
108      * @return mixed File on success, -1 on some errors
109      *
110      * @throws ServerException on some errors
111      */
112     public function processNew($given_url, $notice_id=null, $followRedirects=true) {
113         if (empty($given_url)) return -1;   // error, no url to process
114         $given_url = File_redirection::_canonUrl($given_url);
115         if (empty($given_url)) return -1;   // error, no url to process
116         $file = File::getKV('url', $given_url);
117         if (empty($file)) {
118             $file_redir = File_redirection::getKV('url', $given_url);
119             if (empty($file_redir)) {
120                 // @fixme for new URLs this also looks up non-redirect data
121                 // such as target content type, size, etc, which we need
122                 // for File::saveNew(); so we call it even if not following
123                 // new redirects.
124                 $redir_data = File_redirection::where($given_url);
125                 if (is_array($redir_data)) {
126                     $redir_url = $redir_data['url'];
127                 } elseif (is_string($redir_data)) {
128                     $redir_url = $redir_data;
129                     $redir_data = array();
130                 } else {
131                     // TRANS: Server exception thrown when a URL cannot be processed.
132                     throw new ServerException(sprintf(_("Cannot process URL '%s'"), $given_url));
133                 }
134                 // TODO: max field length
135                 if ($redir_url === $given_url || strlen($redir_url) > 255 || !$followRedirects) {
136                     $x = File::saveNew($redir_data, $given_url);
137                     $file_id = $x->id;
138                 } else {
139                     // This seems kind of messed up... for now skipping this part
140                     // if we're already under a redirect, so we don't go into
141                     // horrible infinite loops if we've been given an unstable
142                     // redirect (where the final destination of the first request
143                     // doesn't match what we get when we ask for it again).
144                     //
145                     // Seen in the wild with clojure.org, which redirects through
146                     // wikispaces for auth and appends session data in the URL params.
147                     $x = File::processNew($redir_url, $notice_id, /*followRedirects*/false);
148                     $file_id = $x->id;
149                     File_redirection::saveNew($redir_data, $file_id, $given_url);
150                 }
151             } else {
152                 $file_id = $file_redir->file_id;
153             }
154         } else {
155             $file_id = $file->id;
156             $x = $file;
157         }
158
159         if (empty($x)) {
160             $x = File::getKV('id', $file_id);
161             if (empty($x)) {
162                 // @todo FIXME: This could possibly be a clearer message :)
163                 // TRANS: Server exception thrown when... Robin thinks something is impossible!
164                 throw new ServerException(_('Robin thinks something is impossible.'));
165             }
166         }
167
168         if (!empty($notice_id)) {
169             File_to_post::processNew($file_id, $notice_id);
170         }
171         return $x;
172     }
173
174     public static function respectsQuota(Profile $scoped, $fileSize) {
175         if ($fileSize > common_config('attachments', 'file_quota')) {
176             // TRANS: Message used to be inserted as %2$s in  the text "No file may
177             // TRANS: be larger than %1$d byte and the file you sent was %2$s.".
178             // TRANS: %1$d is the number of bytes of an uploaded file.
179             $fileSizeText = sprintf(_m('%1$d byte','%1$d bytes',$fileSize),$fileSize);
180
181             $fileQuota = common_config('attachments', 'file_quota');
182             // TRANS: Message given if an upload is larger than the configured maximum.
183             // TRANS: %1$d (used for plural) is the byte limit for uploads,
184             // TRANS: %2$s is the proper form of "n bytes". This is the only ways to have
185             // TRANS: gettext support multiple plurals in the same message, unfortunately...
186             throw new ClientException(
187                     sprintf(_m('No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.',
188                               'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.',
189                               $fileQuota),
190                     $fileQuota, $fileSizeText));
191         }
192
193         $file = new File;
194
195         $query = "select sum(size) as total from file join file_to_post on file_to_post.file_id = file.id join notice on file_to_post.post_id = notice.id where profile_id = {$scoped->id} and file.url like '%/notice/%/file'";
196         $file->query($query);
197         $file->fetch();
198         $total = $file->total + $fileSize;
199         if ($total > common_config('attachments', 'user_quota')) {
200             // TRANS: Message given if an upload would exceed user quota.
201             // TRANS: %d (number) is the user quota in bytes and is used for plural.
202             throw new ClientException(
203                     sprintf(_m('A file this large would exceed your user quota of %d byte.',
204                               'A file this large would exceed your user quota of %d bytes.',
205                               common_config('attachments', 'user_quota')),
206                     common_config('attachments', 'user_quota')));
207         }
208         $query .= ' AND EXTRACT(month FROM file.modified) = EXTRACT(month FROM now()) and EXTRACT(year FROM file.modified) = EXTRACT(year FROM now())';
209         $file->query($query);
210         $file->fetch();
211         $total = $file->total + $fileSize;
212         if ($total > common_config('attachments', 'monthly_quota')) {
213             // TRANS: Message given id an upload would exceed a user's monthly quota.
214             // TRANS: $d (number) is the monthly user quota in bytes and is used for plural.
215             throw new ClientException(
216                     sprintf(_m('A file this large would exceed your monthly quota of %d byte.',
217                               'A file this large would exceed your monthly quota of %d bytes.',
218                               common_config('attachments', 'monthly_quota')),
219                     common_config('attachments', 'monthly_quota')));
220         }
221         return true;
222     }
223
224     // where should the file go?
225
226     static function filename(Profile $profile, $origname, $mimetype)
227     {
228         try {
229             $ext = common_supported_mime_to_ext($mimetype);
230         } catch (Exception $e) {
231             // We don't support this mimetype, but let's guess the extension
232             $ext = substr(strrchr($mimetype, '/'), 1);
233         }
234
235         // Normalize and make the original filename more URL friendly.
236         $origname = basename($origname);
237         if (class_exists('Normalizer')) {
238             // http://php.net/manual/en/class.normalizer.php
239             // http://www.unicode.org/reports/tr15/
240             $origname = Normalizer::normalize($origname, Normalizer::FORM_KC);
241         }
242         $origname = preg_replace('/[^A-Za-z0-9\.\_]/', '_', $origname);
243
244         $nickname = $profile->nickname;
245         $datestamp = strftime('%Y%m%d', time());
246         do {
247             // generate new random strings until we don't run into a filename collision.
248             $random = strtolower(common_confirmation_code(16));
249             $filename = "$nickname-$datestamp-$origname-$random.$ext";
250         } while (file_exists(self::path($filename)));
251         return $filename;
252     }
253
254     /**
255      * Validation for as-saved base filenames
256      */
257     static function validFilename($filename)
258     {
259         return preg_match('/^[A-Za-z0-9._-]+$/', $filename);
260     }
261
262     /**
263      * @throws ClientException on invalid filename
264      */
265     static function path($filename)
266     {
267         if (!self::validFilename($filename)) {
268             // TRANS: Client exception thrown if a file upload does not have a valid name.
269             throw new ClientException(_("Invalid filename."));
270         }
271         $dir = common_config('attachments', 'dir');
272
273         if ($dir[strlen($dir)-1] != '/') {
274             $dir .= '/';
275         }
276
277         return $dir . $filename;
278     }
279
280     static function url($filename)
281     {
282         if (!self::validFilename($filename)) {
283             // TRANS: Client exception thrown if a file upload does not have a valid name.
284             throw new ClientException(_("Invalid filename."));
285         }
286
287         if (common_config('site','private')) {
288
289             return common_local_url('getfile',
290                                 array('filename' => $filename));
291
292         }
293
294         if (StatusNet::isHTTPS()) {
295
296             $sslserver = common_config('attachments', 'sslserver');
297
298             if (empty($sslserver)) {
299                 // XXX: this assumes that background dir == site dir + /file/
300                 // not true if there's another server
301                 if (is_string(common_config('site', 'sslserver')) &&
302                     mb_strlen(common_config('site', 'sslserver')) > 0) {
303                     $server = common_config('site', 'sslserver');
304                 } else if (common_config('site', 'server')) {
305                     $server = common_config('site', 'server');
306                 }
307                 $path = common_config('site', 'path') . '/file/';
308             } else {
309                 $server = $sslserver;
310                 $path   = common_config('attachments', 'sslpath');
311                 if (empty($path)) {
312                     $path = common_config('attachments', 'path');
313                 }
314             }
315
316             $protocol = 'https';
317         } else {
318             $path = common_config('attachments', 'path');
319             $server = common_config('attachments', 'server');
320
321             if (empty($server)) {
322                 $server = common_config('site', 'server');
323             }
324
325             $ssl = common_config('attachments', 'ssl');
326
327             $protocol = ($ssl) ? 'https' : 'http';
328         }
329
330         if ($path[strlen($path)-1] != '/') {
331             $path .= '/';
332         }
333
334         if ($path[0] != '/') {
335             $path = '/'.$path;
336         }
337
338         return $protocol.'://'.$server.$path.$filename;
339     }
340
341     function getEnclosure(){
342         $enclosure = (object) array();
343         $enclosure->title=$this->title;
344         $enclosure->url=$this->url;
345         $enclosure->title=$this->title;
346         $enclosure->date=$this->date;
347         $enclosure->modified=$this->modified;
348         $enclosure->size=$this->size;
349         $enclosure->mimetype=$this->mimetype;
350
351         if (!isset($this->filename)) {
352             $notEnclosureMimeTypes = array(null,'text/html','application/xhtml+xml');
353             $mimetype = $this->mimetype;
354             if($mimetype != null){
355                 $mimetype = strtolower($this->mimetype);
356             }
357             $semicolon = strpos($mimetype,';');
358             if($semicolon){
359                 $mimetype = substr($mimetype,0,$semicolon);
360             }
361             if (in_array($mimetype, $notEnclosureMimeTypes)) {
362                 Event::handle('FileEnclosureMetadata', array($file, &$enclosure));
363             }
364         }
365         return $enclosure;
366     }
367
368     // quick back-compat hack, since there's still code using this
369     function isEnclosure()
370     {
371         $enclosure = $this->getEnclosure();
372         return !empty($enclosure);
373     }
374
375     /**
376      * Get the attachment's thumbnail record, if any.
377      * Make sure you supply proper 'int' typed variables (or null).
378      *
379      * @param $width  int   Max width of thumbnail in pixels. (if null, use common_config values)
380      * @param $height int   Max height of thumbnail in pixels. (if null, square-crop to $width)
381      * @param $crop   bool  Crop to the max-values' aspect ratio
382      *
383      * @return File_thumbnail
384      */
385     public function getThumbnail($width=null, $height=null, $crop=false)
386     {
387         if (intval($this->width) < 1 || intval($this->height) < 1) {
388             // Old files may have 0 until migrated with scripts/upgrade.php
389             // For any legitimately unrepresentable ones, we could generate our
390             // own image (like a square with MIME type in text)
391             throw new UnsupportedMediaException('No image geometry available.');
392         }
393
394         if ($width === null) {
395             $width = common_config('thumbnail', 'width');
396             $height = common_config('thumbnail', 'height');
397             $crop = common_config('thumbnail', 'crop');
398         }
399
400         if ($height === null) {
401             $height = $width;
402             $crop = true;
403         }
404         
405         // Get proper aspect ratio width and height before lookup
406         list($width, $height, $x, $y, $w2, $h2) =
407                                 ImageFile::getScalingValues($this->width, $this->height, $width, $height, $crop);
408
409         // Doublecheck that parameters are sane and integers.
410         if ($width < 1 || $width > common_config('thumbnail', 'maxsize')
411                 || $height < 1 || $height > common_config('thumbnail', 'maxsize')) {
412             // Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile::getScalingValues
413             throw new ServerException('Bad thumbnail size parameters.');
414         }
415
416         $params = array('file_id'=> $this->id,
417                         'width'  => $width,
418                         'height' => $height);
419         $thumb = File_thumbnail::pkeyGet($params);
420         if ($thumb === null) {
421             // throws exception on failure to generate thumbnail
422             $thumb = $this->generateThumbnail($width, $height, $crop);
423         }
424         return $thumb;
425     }
426
427     /**
428      * Generate and store a thumbnail image for the uploaded file, if applicable.
429      * Call this only if you know what you're doing.
430      *
431      * @param $width  int    Maximum thumbnail width in pixels
432      * @param $height int    Maximum thumbnail height in pixels, if null, crop to $width
433      *
434      * @return File_thumbnail or null
435      */
436     protected function generateThumbnail($width, $height, $crop)
437     {
438         $width = intval($width);
439         if ($height === null) {
440             $height = $width;
441             $crop = true;
442         }
443
444         $image = ImageFile::fromFileObject($this);
445
446         list($width, $height, $x, $y, $w2, $h2) =
447                                 $image->scaleToFit($width, $height, $crop);
448
449         $outname = "thumb-{$width}x{$height}-" . $this->filename;
450         $outpath = self::path($outname);
451
452         $image->resizeTo($outpath, $width, $height, $x, $y, $w2, $h2);
453
454         // Avoid deleting the original
455         if ($image->getPath() != $this->getPath()) {
456             $image->unlink();
457         }
458         return File_thumbnail::saveThumbnail($this->id,
459                                       self::url($outname),
460                                       $width, $height);
461     }
462
463     public function getPath()
464     {
465         return self::path($this->filename);
466     }
467     public function getUrl()
468     {
469         return $this->url;
470     }
471
472     /**
473      * Blow the cache of notices that link to this URL
474      *
475      * @param boolean $last Whether to blow the "last" cache too
476      *
477      * @return void
478      */
479
480     function blowCache($last=false)
481     {
482         self::blow('file:notice-ids:%s', $this->url);
483         if ($last) {
484             self::blow('file:notice-ids:%s;last', $this->url);
485         }
486         self::blow('file:notice-count:%d', $this->id);
487     }
488
489     /**
490      * Stream of notices linking to this URL
491      *
492      * @param integer $offset   Offset to show; default is 0
493      * @param integer $limit    Limit of notices to show
494      * @param integer $since_id Since this notice
495      * @param integer $max_id   Before this notice
496      *
497      * @return array ids of notices that link to this file
498      */
499
500     function stream($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
501     {
502         $stream = new FileNoticeStream($this);
503         return $stream->getNotices($offset, $limit, $since_id, $max_id);
504     }
505
506     function noticeCount()
507     {
508         $cacheKey = sprintf('file:notice-count:%d', $this->id);
509         
510         $count = self::cacheGet($cacheKey);
511
512         if ($count === false) {
513
514             $f2p = new File_to_post();
515
516             $f2p->file_id = $this->id;
517
518             $count = $f2p->count();
519
520             self::cacheSet($cacheKey, $count);
521         } 
522
523         return $count;
524     }
525 }