Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/files/storage/PhabricatorFile.php
12242 views
1
<?php
2
3
/**
4
* Parameters
5
* ==========
6
*
7
* When creating a new file using a method like @{method:newFromFileData}, these
8
* parameters are supported:
9
*
10
* | name | Human readable filename.
11
* | authorPHID | User PHID of uploader.
12
* | ttl.absolute | Temporary file lifetime as an epoch timestamp.
13
* | ttl.relative | Temporary file lifetime, relative to now, in seconds.
14
* | viewPolicy | File visibility policy.
15
* | isExplicitUpload | Used to show users files they explicitly uploaded.
16
* | canCDN | Allows the file to be cached and delivered over a CDN.
17
* | profile | Marks the file as a profile image.
18
* | format | Internal encoding format.
19
* | mime-type | Optional, explicit file MIME type.
20
* | builtin | Optional filename, identifies this as a builtin.
21
*
22
*/
23
final class PhabricatorFile extends PhabricatorFileDAO
24
implements
25
PhabricatorApplicationTransactionInterface,
26
PhabricatorTokenReceiverInterface,
27
PhabricatorSubscribableInterface,
28
PhabricatorFlaggableInterface,
29
PhabricatorPolicyInterface,
30
PhabricatorDestructibleInterface,
31
PhabricatorConduitResultInterface,
32
PhabricatorIndexableInterface,
33
PhabricatorNgramsInterface {
34
35
const METADATA_IMAGE_WIDTH = 'width';
36
const METADATA_IMAGE_HEIGHT = 'height';
37
const METADATA_CAN_CDN = 'canCDN';
38
const METADATA_BUILTIN = 'builtin';
39
const METADATA_PARTIAL = 'partial';
40
const METADATA_PROFILE = 'profile';
41
const METADATA_STORAGE = 'storage';
42
const METADATA_INTEGRITY = 'integrity';
43
const METADATA_CHUNK = 'chunk';
44
const METADATA_ALT_TEXT = 'alt';
45
46
const STATUS_ACTIVE = 'active';
47
const STATUS_DELETED = 'deleted';
48
49
protected $name;
50
protected $mimeType;
51
protected $byteSize;
52
protected $authorPHID;
53
protected $secretKey;
54
protected $contentHash;
55
protected $metadata = array();
56
protected $mailKey;
57
protected $builtinKey;
58
59
protected $storageEngine;
60
protected $storageFormat;
61
protected $storageHandle;
62
63
protected $ttl;
64
protected $isExplicitUpload = 1;
65
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
66
protected $isPartial = 0;
67
protected $isDeleted = 0;
68
69
private $objects = self::ATTACHABLE;
70
private $objectPHIDs = self::ATTACHABLE;
71
private $originalFile = self::ATTACHABLE;
72
private $transforms = self::ATTACHABLE;
73
74
public static function initializeNewFile() {
75
$app = id(new PhabricatorApplicationQuery())
76
->setViewer(PhabricatorUser::getOmnipotentUser())
77
->withClasses(array('PhabricatorFilesApplication'))
78
->executeOne();
79
80
$view_policy = $app->getPolicy(
81
FilesDefaultViewCapability::CAPABILITY);
82
83
return id(new PhabricatorFile())
84
->setViewPolicy($view_policy)
85
->setIsPartial(0)
86
->attachOriginalFile(null)
87
->attachObjects(array())
88
->attachObjectPHIDs(array());
89
}
90
91
protected function getConfiguration() {
92
return array(
93
self::CONFIG_AUX_PHID => true,
94
self::CONFIG_SERIALIZATION => array(
95
'metadata' => self::SERIALIZATION_JSON,
96
),
97
self::CONFIG_COLUMN_SCHEMA => array(
98
'name' => 'sort255?',
99
'mimeType' => 'text255?',
100
'byteSize' => 'uint64',
101
'storageEngine' => 'text32',
102
'storageFormat' => 'text32',
103
'storageHandle' => 'text255',
104
'authorPHID' => 'phid?',
105
'secretKey' => 'bytes20?',
106
'contentHash' => 'bytes64?',
107
'ttl' => 'epoch?',
108
'isExplicitUpload' => 'bool?',
109
'mailKey' => 'bytes20',
110
'isPartial' => 'bool',
111
'builtinKey' => 'text64?',
112
'isDeleted' => 'bool',
113
),
114
self::CONFIG_KEY_SCHEMA => array(
115
'key_phid' => null,
116
'phid' => array(
117
'columns' => array('phid'),
118
'unique' => true,
119
),
120
'authorPHID' => array(
121
'columns' => array('authorPHID'),
122
),
123
'contentHash' => array(
124
'columns' => array('contentHash'),
125
),
126
'key_ttl' => array(
127
'columns' => array('ttl'),
128
),
129
'key_dateCreated' => array(
130
'columns' => array('dateCreated'),
131
),
132
'key_partial' => array(
133
'columns' => array('authorPHID', 'isPartial'),
134
),
135
'key_builtin' => array(
136
'columns' => array('builtinKey'),
137
'unique' => true,
138
),
139
'key_engine' => array(
140
'columns' => array('storageEngine', 'storageHandle(64)'),
141
),
142
),
143
) + parent::getConfiguration();
144
}
145
146
public function generatePHID() {
147
return PhabricatorPHID::generateNewPHID(
148
PhabricatorFileFilePHIDType::TYPECONST);
149
}
150
151
public function save() {
152
if (!$this->getSecretKey()) {
153
$this->setSecretKey($this->generateSecretKey());
154
}
155
if (!$this->getMailKey()) {
156
$this->setMailKey(Filesystem::readRandomCharacters(20));
157
}
158
return parent::save();
159
}
160
161
public function saveAndIndex() {
162
$this->save();
163
164
if ($this->isIndexableFile()) {
165
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
166
}
167
168
return $this;
169
}
170
171
private function isIndexableFile() {
172
if ($this->getIsChunk()) {
173
return false;
174
}
175
176
return true;
177
}
178
179
public function getMonogram() {
180
return 'F'.$this->getID();
181
}
182
183
public function scrambleSecret() {
184
return $this->setSecretKey($this->generateSecretKey());
185
}
186
187
public static function readUploadedFileData($spec) {
188
if (!$spec) {
189
throw new Exception(pht('No file was uploaded!'));
190
}
191
192
$err = idx($spec, 'error');
193
if ($err) {
194
throw new PhabricatorFileUploadException($err);
195
}
196
197
$tmp_name = idx($spec, 'tmp_name');
198
199
// NOTE: If we parsed the request body ourselves, the files we wrote will
200
// not be registered in the `is_uploaded_file()` list. It's fine to skip
201
// this check: it just protects against sloppy code from the long ago era
202
// of "register_globals".
203
204
if (ini_get('enable_post_data_reading')) {
205
$is_valid = @is_uploaded_file($tmp_name);
206
if (!$is_valid) {
207
throw new Exception(pht('File is not an uploaded file.'));
208
}
209
}
210
211
$file_data = Filesystem::readFile($tmp_name);
212
$file_size = idx($spec, 'size');
213
214
if (strlen($file_data) != $file_size) {
215
throw new Exception(pht('File size disagrees with uploaded size.'));
216
}
217
218
return $file_data;
219
}
220
221
public static function newFromPHPUpload($spec, array $params = array()) {
222
$file_data = self::readUploadedFileData($spec);
223
224
$file_name = nonempty(
225
idx($params, 'name'),
226
idx($spec, 'name'));
227
$params = array(
228
'name' => $file_name,
229
) + $params;
230
231
return self::newFromFileData($file_data, $params);
232
}
233
234
public static function newFromXHRUpload($data, array $params = array()) {
235
return self::newFromFileData($data, $params);
236
}
237
238
239
public static function newFileFromContentHash($hash, array $params) {
240
if ($hash === null) {
241
return null;
242
}
243
244
// Check to see if a file with same hash already exists.
245
$file = id(new PhabricatorFile())->loadOneWhere(
246
'contentHash = %s LIMIT 1',
247
$hash);
248
if (!$file) {
249
return null;
250
}
251
252
$copy_of_storage_engine = $file->getStorageEngine();
253
$copy_of_storage_handle = $file->getStorageHandle();
254
$copy_of_storage_format = $file->getStorageFormat();
255
$copy_of_storage_properties = $file->getStorageProperties();
256
$copy_of_byte_size = $file->getByteSize();
257
$copy_of_mime_type = $file->getMimeType();
258
259
$new_file = self::initializeNewFile();
260
261
$new_file->setByteSize($copy_of_byte_size);
262
263
$new_file->setContentHash($hash);
264
$new_file->setStorageEngine($copy_of_storage_engine);
265
$new_file->setStorageHandle($copy_of_storage_handle);
266
$new_file->setStorageFormat($copy_of_storage_format);
267
$new_file->setStorageProperties($copy_of_storage_properties);
268
$new_file->setMimeType($copy_of_mime_type);
269
$new_file->copyDimensions($file);
270
271
$new_file->readPropertiesFromParameters($params);
272
273
$new_file->saveAndIndex();
274
275
return $new_file;
276
}
277
278
public static function newChunkedFile(
279
PhabricatorFileStorageEngine $engine,
280
$length,
281
array $params) {
282
283
$file = self::initializeNewFile();
284
285
$file->setByteSize($length);
286
287
// NOTE: Once we receive the first chunk, we'll detect its MIME type and
288
// update the parent file if a MIME type hasn't been provided. This matters
289
// for large media files like video.
290
$mime_type = idx($params, 'mime-type');
291
if ($mime_type === null || !strlen($mime_type)) {
292
$file->setMimeType('application/octet-stream');
293
}
294
295
$chunked_hash = idx($params, 'chunkedHash');
296
297
// Get rid of this parameter now; we aren't passing it any further down
298
// the stack.
299
unset($params['chunkedHash']);
300
301
if ($chunked_hash) {
302
$file->setContentHash($chunked_hash);
303
} else {
304
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
305
// discussion of this.
306
$seed = Filesystem::readRandomBytes(64);
307
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
308
$seed);
309
$file->setContentHash($hash);
310
}
311
312
$file->setStorageEngine($engine->getEngineIdentifier());
313
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
314
315
// Chunked files are always stored raw because they do not actually store
316
// data. The chunks do, and can be individually formatted.
317
$file->setStorageFormat(PhabricatorFileRawStorageFormat::FORMATKEY);
318
319
$file->setIsPartial(1);
320
321
$file->readPropertiesFromParameters($params);
322
323
return $file;
324
}
325
326
private static function buildFromFileData($data, array $params = array()) {
327
328
if (isset($params['storageEngines'])) {
329
$engines = $params['storageEngines'];
330
} else {
331
$size = strlen($data);
332
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
333
334
if (!$engines) {
335
throw new Exception(
336
pht(
337
'No configured storage engine can store this file. See '.
338
'"Configuring File Storage" in the documentation for '.
339
'information on configuring storage engines.'));
340
}
341
}
342
343
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
344
if (!$engines) {
345
throw new Exception(pht('No valid storage engines are available!'));
346
}
347
348
$file = self::initializeNewFile();
349
350
$aes_type = PhabricatorFileAES256StorageFormat::FORMATKEY;
351
$has_aes = PhabricatorKeyring::getDefaultKeyName($aes_type);
352
if ($has_aes !== null) {
353
$default_key = PhabricatorFileAES256StorageFormat::FORMATKEY;
354
} else {
355
$default_key = PhabricatorFileRawStorageFormat::FORMATKEY;
356
}
357
$key = idx($params, 'format', $default_key);
358
359
// Callers can pass in an object explicitly instead of a key. This is
360
// primarily useful for unit tests.
361
if ($key instanceof PhabricatorFileStorageFormat) {
362
$format = clone $key;
363
} else {
364
$format = clone PhabricatorFileStorageFormat::requireFormat($key);
365
}
366
367
$format->setFile($file);
368
369
$properties = $format->newStorageProperties();
370
$file->setStorageFormat($format->getStorageFormatKey());
371
$file->setStorageProperties($properties);
372
373
$data_handle = null;
374
$engine_identifier = null;
375
$integrity_hash = null;
376
$exceptions = array();
377
foreach ($engines as $engine) {
378
$engine_class = get_class($engine);
379
try {
380
$result = $file->writeToEngine(
381
$engine,
382
$data,
383
$params);
384
385
list($engine_identifier, $data_handle, $integrity_hash) = $result;
386
387
// We stored the file somewhere so stop trying to write it to other
388
// places.
389
break;
390
} catch (PhabricatorFileStorageConfigurationException $ex) {
391
// If an engine is outright misconfigured (or misimplemented), raise
392
// that immediately since it probably needs attention.
393
throw $ex;
394
} catch (Exception $ex) {
395
phlog($ex);
396
397
// If an engine doesn't work, keep trying all the other valid engines
398
// in case something else works.
399
$exceptions[$engine_class] = $ex;
400
}
401
}
402
403
if (!$data_handle) {
404
throw new PhutilAggregateException(
405
pht('All storage engines failed to write file:'),
406
$exceptions);
407
}
408
409
$file->setByteSize(strlen($data));
410
411
$hash = self::hashFileContent($data);
412
$file->setContentHash($hash);
413
414
$file->setStorageEngine($engine_identifier);
415
$file->setStorageHandle($data_handle);
416
417
$file->setIntegrityHash($integrity_hash);
418
419
$file->readPropertiesFromParameters($params);
420
421
if (!$file->getMimeType()) {
422
$tmp = new TempFile();
423
Filesystem::writeFile($tmp, $data);
424
$file->setMimeType(Filesystem::getMimeType($tmp));
425
unset($tmp);
426
}
427
428
try {
429
$file->updateDimensions(false);
430
} catch (Exception $ex) {
431
// Do nothing.
432
}
433
434
$file->saveAndIndex();
435
436
return $file;
437
}
438
439
public static function newFromFileData($data, array $params = array()) {
440
$hash = self::hashFileContent($data);
441
442
if ($hash !== null) {
443
$file = self::newFileFromContentHash($hash, $params);
444
if ($file) {
445
return $file;
446
}
447
}
448
449
return self::buildFromFileData($data, $params);
450
}
451
452
public function migrateToEngine(
453
PhabricatorFileStorageEngine $engine,
454
$make_copy) {
455
456
if (!$this->getID() || !$this->getStorageHandle()) {
457
throw new Exception(
458
pht("You can not migrate a file which hasn't yet been saved."));
459
}
460
461
$data = $this->loadFileData();
462
$params = array(
463
'name' => $this->getName(),
464
);
465
466
list($new_identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
467
$engine,
468
$data,
469
$params);
470
471
$old_engine = $this->instantiateStorageEngine();
472
$old_identifier = $this->getStorageEngine();
473
$old_handle = $this->getStorageHandle();
474
475
$this->setStorageEngine($new_identifier);
476
$this->setStorageHandle($new_handle);
477
$this->setIntegrityHash($integrity_hash);
478
$this->save();
479
480
if (!$make_copy) {
481
$this->deleteFileDataIfUnused(
482
$old_engine,
483
$old_identifier,
484
$old_handle);
485
}
486
487
return $this;
488
}
489
490
public function migrateToStorageFormat(PhabricatorFileStorageFormat $format) {
491
if (!$this->getID() || !$this->getStorageHandle()) {
492
throw new Exception(
493
pht("You can not migrate a file which hasn't yet been saved."));
494
}
495
496
$data = $this->loadFileData();
497
$params = array(
498
'name' => $this->getName(),
499
);
500
501
$engine = $this->instantiateStorageEngine();
502
$old_handle = $this->getStorageHandle();
503
504
$properties = $format->newStorageProperties();
505
$this->setStorageFormat($format->getStorageFormatKey());
506
$this->setStorageProperties($properties);
507
508
list($identifier, $new_handle, $integrity_hash) = $this->writeToEngine(
509
$engine,
510
$data,
511
$params);
512
513
$this->setStorageHandle($new_handle);
514
$this->setIntegrityHash($integrity_hash);
515
$this->save();
516
517
$this->deleteFileDataIfUnused(
518
$engine,
519
$identifier,
520
$old_handle);
521
522
return $this;
523
}
524
525
public function cycleMasterStorageKey(PhabricatorFileStorageFormat $format) {
526
if (!$this->getID() || !$this->getStorageHandle()) {
527
throw new Exception(
528
pht("You can not cycle keys for a file which hasn't yet been saved."));
529
}
530
531
$properties = $format->cycleStorageProperties();
532
$this->setStorageProperties($properties);
533
$this->save();
534
535
return $this;
536
}
537
538
private function writeToEngine(
539
PhabricatorFileStorageEngine $engine,
540
$data,
541
array $params) {
542
543
$engine_class = get_class($engine);
544
545
$format = $this->newStorageFormat();
546
547
$data_iterator = array($data);
548
$formatted_iterator = $format->newWriteIterator($data_iterator);
549
$formatted_data = $this->loadDataFromIterator($formatted_iterator);
550
551
$integrity_hash = $engine->newIntegrityHash($formatted_data, $format);
552
553
$data_handle = $engine->writeFile($formatted_data, $params);
554
555
if (!$data_handle || strlen($data_handle) > 255) {
556
// This indicates an improperly implemented storage engine.
557
throw new PhabricatorFileStorageConfigurationException(
558
pht(
559
"Storage engine '%s' executed %s but did not return a valid ".
560
"handle ('%s') to the data: it must be nonempty and no longer ".
561
"than 255 characters.",
562
$engine_class,
563
'writeFile()',
564
$data_handle));
565
}
566
567
$engine_identifier = $engine->getEngineIdentifier();
568
if (!$engine_identifier || strlen($engine_identifier) > 32) {
569
throw new PhabricatorFileStorageConfigurationException(
570
pht(
571
"Storage engine '%s' returned an improper engine identifier '{%s}': ".
572
"it must be nonempty and no longer than 32 characters.",
573
$engine_class,
574
$engine_identifier));
575
}
576
577
return array($engine_identifier, $data_handle, $integrity_hash);
578
}
579
580
581
/**
582
* Download a remote resource over HTTP and save the response body as a file.
583
*
584
* This method respects `security.outbound-blacklist`, and protects against
585
* HTTP redirection (by manually following "Location" headers and verifying
586
* each destination). It does not protect against DNS rebinding. See
587
* discussion in T6755.
588
*/
589
public static function newFromFileDownload($uri, array $params = array()) {
590
$timeout = 5;
591
592
$redirects = array();
593
$current = $uri;
594
while (true) {
595
try {
596
if (count($redirects) > 10) {
597
throw new Exception(
598
pht('Too many redirects trying to fetch remote URI.'));
599
}
600
601
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
602
$current,
603
array(
604
'http',
605
'https',
606
));
607
608
list($resolved_uri, $resolved_domain) = $resolved;
609
610
$current = new PhutilURI($current);
611
if ($current->getProtocol() == 'http') {
612
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
613
$fetch_uri = $resolved_uri;
614
$fetch_host = $resolved_domain;
615
} else {
616
// For HTTPS, we can't: cURL won't verify the SSL certificate if
617
// the domain has been replaced with an IP. But internal services
618
// presumably will not have valid certificates for rebindable
619
// domain names on attacker-controlled domains, so the DNS rebinding
620
// attack should generally not be possible anyway.
621
$fetch_uri = $current;
622
$fetch_host = null;
623
}
624
625
$future = id(new HTTPSFuture($fetch_uri))
626
->setFollowLocation(false)
627
->setTimeout($timeout);
628
629
if ($fetch_host !== null) {
630
$future->addHeader('Host', $fetch_host);
631
}
632
633
list($status, $body, $headers) = $future->resolve();
634
635
if ($status->isRedirect()) {
636
// This is an HTTP 3XX status, so look for a "Location" header.
637
$location = null;
638
foreach ($headers as $header) {
639
list($name, $value) = $header;
640
if (phutil_utf8_strtolower($name) == 'location') {
641
$location = $value;
642
break;
643
}
644
}
645
646
// HTTP 3XX status with no "Location" header, just treat this like
647
// a normal HTTP error.
648
if ($location === null) {
649
throw $status;
650
}
651
652
if (isset($redirects[$location])) {
653
throw new Exception(
654
pht('Encountered loop while following redirects.'));
655
}
656
657
$redirects[$location] = $location;
658
$current = $location;
659
// We'll fall off the bottom and go try this URI now.
660
} else if ($status->isError()) {
661
// This is something other than an HTTP 2XX or HTTP 3XX status, so
662
// just bail out.
663
throw $status;
664
} else {
665
// This is HTTP 2XX, so use the response body to save the file data.
666
// Provide a default name based on the URI, truncating it if the URI
667
// is exceptionally long.
668
669
$default_name = basename($uri);
670
$default_name = id(new PhutilUTF8StringTruncator())
671
->setMaximumBytes(64)
672
->truncateString($default_name);
673
674
$params = $params + array(
675
'name' => $default_name,
676
);
677
678
return self::newFromFileData($body, $params);
679
}
680
} catch (Exception $ex) {
681
if ($redirects) {
682
throw new PhutilProxyException(
683
pht(
684
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
685
'(%s): %s',
686
$uri,
687
phutil_count($redirects),
688
implode(' > ', array_keys($redirects)),
689
$ex->getMessage()),
690
$ex);
691
} else {
692
throw $ex;
693
}
694
}
695
}
696
}
697
698
public static function normalizeFileName($file_name) {
699
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
700
$file_name = preg_replace($pattern, '_', $file_name);
701
$file_name = preg_replace('@_+@', '_', $file_name);
702
$file_name = trim($file_name, '_');
703
704
$disallowed_filenames = array(
705
'.' => 'dot',
706
'..' => 'dotdot',
707
'' => 'file',
708
);
709
$file_name = idx($disallowed_filenames, $file_name, $file_name);
710
711
return $file_name;
712
}
713
714
public function delete() {
715
// We want to delete all the rows which mark this file as the transformation
716
// of some other file (since we're getting rid of it). We also delete all
717
// the transformations of this file, so that a user who deletes an image
718
// doesn't need to separately hunt down and delete a bunch of thumbnails and
719
// resizes of it.
720
721
$outbound_xforms = id(new PhabricatorFileQuery())
722
->setViewer(PhabricatorUser::getOmnipotentUser())
723
->withTransforms(
724
array(
725
array(
726
'originalPHID' => $this->getPHID(),
727
'transform' => true,
728
),
729
))
730
->execute();
731
732
foreach ($outbound_xforms as $outbound_xform) {
733
$outbound_xform->delete();
734
}
735
736
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
737
'transformedPHID = %s',
738
$this->getPHID());
739
740
$this->openTransaction();
741
foreach ($inbound_xforms as $inbound_xform) {
742
$inbound_xform->delete();
743
}
744
$ret = parent::delete();
745
$this->saveTransaction();
746
747
$this->deleteFileDataIfUnused(
748
$this->instantiateStorageEngine(),
749
$this->getStorageEngine(),
750
$this->getStorageHandle());
751
752
return $ret;
753
}
754
755
756
/**
757
* Destroy stored file data if there are no remaining files which reference
758
* it.
759
*/
760
public function deleteFileDataIfUnused(
761
PhabricatorFileStorageEngine $engine,
762
$engine_identifier,
763
$handle) {
764
765
// Check to see if any files are using storage.
766
$usage = id(new PhabricatorFile())->loadAllWhere(
767
'storageEngine = %s AND storageHandle = %s LIMIT 1',
768
$engine_identifier,
769
$handle);
770
771
// If there are no files using the storage, destroy the actual storage.
772
if (!$usage) {
773
try {
774
$engine->deleteFile($handle);
775
} catch (Exception $ex) {
776
// In the worst case, we're leaving some data stranded in a storage
777
// engine, which is not a big deal.
778
phlog($ex);
779
}
780
}
781
}
782
783
public static function hashFileContent($data) {
784
// NOTE: Hashing can fail if the algorithm isn't available in the current
785
// build of PHP. It's fine if we're unable to generate a content hash:
786
// it just means we'll store extra data when users upload duplicate files
787
// instead of being able to deduplicate it.
788
789
$hash = hash('sha256', $data, $raw_output = false);
790
if ($hash === false) {
791
return null;
792
}
793
794
return $hash;
795
}
796
797
public function loadFileData() {
798
$iterator = $this->getFileDataIterator();
799
return $this->loadDataFromIterator($iterator);
800
}
801
802
803
/**
804
* Return an iterable which emits file content bytes.
805
*
806
* @param int Offset for the start of data.
807
* @param int Offset for the end of data.
808
* @return Iterable Iterable object which emits requested data.
809
*/
810
public function getFileDataIterator($begin = null, $end = null) {
811
$engine = $this->instantiateStorageEngine();
812
813
$format = $this->newStorageFormat();
814
815
$iterator = $engine->getRawFileDataIterator(
816
$this,
817
$begin,
818
$end,
819
$format);
820
821
return $iterator;
822
}
823
824
public function getURI() {
825
return $this->getInfoURI();
826
}
827
828
public function getViewURI() {
829
if (!$this->getPHID()) {
830
throw new Exception(
831
pht('You must save a file before you can generate a view URI.'));
832
}
833
834
return $this->getCDNURI('data');
835
}
836
837
public function getCDNURI($request_kind) {
838
if (($request_kind !== 'data') &&
839
($request_kind !== 'download')) {
840
throw new Exception(
841
pht(
842
'Unknown file content request kind "%s".',
843
$request_kind));
844
}
845
846
$name = self::normalizeFileName($this->getName());
847
$name = phutil_escape_uri($name);
848
849
$parts = array();
850
$parts[] = 'file';
851
$parts[] = $request_kind;
852
853
// If this is an instanced install, add the instance identifier to the URI.
854
// Instanced configurations behind a CDN may not be able to control the
855
// request domain used by the CDN (as with AWS CloudFront). Embedding the
856
// instance identity in the path allows us to distinguish between requests
857
// originating from different instances but served through the same CDN.
858
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
859
if ($instance !== null && strlen($instance)) {
860
$parts[] = '@'.$instance;
861
}
862
863
$parts[] = $this->getSecretKey();
864
$parts[] = $this->getPHID();
865
$parts[] = $name;
866
867
$path = '/'.implode('/', $parts);
868
869
// If this file is only partially uploaded, we're just going to return a
870
// local URI to make sure that Ajax works, since the page is inevitably
871
// going to give us an error back.
872
if ($this->getIsPartial()) {
873
return PhabricatorEnv::getURI($path);
874
} else {
875
return PhabricatorEnv::getCDNURI($path);
876
}
877
}
878
879
880
public function getInfoURI() {
881
return '/'.$this->getMonogram();
882
}
883
884
public function getBestURI() {
885
if ($this->isViewableInBrowser()) {
886
return $this->getViewURI();
887
} else {
888
return $this->getInfoURI();
889
}
890
}
891
892
public function getDownloadURI() {
893
return $this->getCDNURI('download');
894
}
895
896
public function getURIForTransform(PhabricatorFileTransform $transform) {
897
return $this->getTransformedURI($transform->getTransformKey());
898
}
899
900
private function getTransformedURI($transform) {
901
$parts = array();
902
$parts[] = 'file';
903
$parts[] = 'xform';
904
905
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
906
if ($instance !== null && strlen($instance)) {
907
$parts[] = '@'.$instance;
908
}
909
910
$parts[] = $transform;
911
$parts[] = $this->getPHID();
912
$parts[] = $this->getSecretKey();
913
914
$path = implode('/', $parts);
915
$path = $path.'/';
916
917
return PhabricatorEnv::getCDNURI($path);
918
}
919
920
public function isViewableInBrowser() {
921
return ($this->getViewableMimeType() !== null);
922
}
923
924
public function isViewableImage() {
925
if (!$this->isViewableInBrowser()) {
926
return false;
927
}
928
929
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
930
$mime_type = $this->getMimeType();
931
return idx($mime_map, $mime_type);
932
}
933
934
public function isAudio() {
935
if (!$this->isViewableInBrowser()) {
936
return false;
937
}
938
939
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
940
$mime_type = $this->getMimeType();
941
return idx($mime_map, $mime_type);
942
}
943
944
public function isVideo() {
945
if (!$this->isViewableInBrowser()) {
946
return false;
947
}
948
949
$mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types');
950
$mime_type = $this->getMimeType();
951
return idx($mime_map, $mime_type);
952
}
953
954
public function isPDF() {
955
if (!$this->isViewableInBrowser()) {
956
return false;
957
}
958
959
$mime_map = array(
960
'application/pdf' => 'application/pdf',
961
);
962
963
$mime_type = $this->getMimeType();
964
return idx($mime_map, $mime_type);
965
}
966
967
public function isTransformableImage() {
968
// NOTE: The way the 'gd' extension works in PHP is that you can install it
969
// with support for only some file types, so it might be able to handle
970
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
971
// warns you if you don't have complete support.
972
973
$matches = null;
974
$ok = preg_match(
975
'@^image/(gif|png|jpe?g)@',
976
$this->getViewableMimeType(),
977
$matches);
978
if (!$ok) {
979
return false;
980
}
981
982
switch ($matches[1]) {
983
case 'jpg';
984
case 'jpeg':
985
return function_exists('imagejpeg');
986
break;
987
case 'png':
988
return function_exists('imagepng');
989
break;
990
case 'gif':
991
return function_exists('imagegif');
992
break;
993
default:
994
throw new Exception(pht('Unknown type matched as image MIME type.'));
995
}
996
}
997
998
public static function getTransformableImageFormats() {
999
$supported = array();
1000
1001
if (function_exists('imagejpeg')) {
1002
$supported[] = 'jpg';
1003
}
1004
1005
if (function_exists('imagepng')) {
1006
$supported[] = 'png';
1007
}
1008
1009
if (function_exists('imagegif')) {
1010
$supported[] = 'gif';
1011
}
1012
1013
return $supported;
1014
}
1015
1016
public function getDragAndDropDictionary() {
1017
return array(
1018
'id' => $this->getID(),
1019
'phid' => $this->getPHID(),
1020
'uri' => $this->getBestURI(),
1021
);
1022
}
1023
1024
public function instantiateStorageEngine() {
1025
return self::buildEngine($this->getStorageEngine());
1026
}
1027
1028
public static function buildEngine($engine_identifier) {
1029
$engines = self::buildAllEngines();
1030
foreach ($engines as $engine) {
1031
if ($engine->getEngineIdentifier() == $engine_identifier) {
1032
return $engine;
1033
}
1034
}
1035
1036
throw new Exception(
1037
pht(
1038
"Storage engine '%s' could not be located!",
1039
$engine_identifier));
1040
}
1041
1042
public static function buildAllEngines() {
1043
return id(new PhutilClassMapQuery())
1044
->setAncestorClass('PhabricatorFileStorageEngine')
1045
->execute();
1046
}
1047
1048
public function getViewableMimeType() {
1049
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
1050
1051
$mime_type = $this->getMimeType();
1052
$mime_parts = explode(';', $mime_type);
1053
$mime_type = trim(reset($mime_parts));
1054
1055
return idx($mime_map, $mime_type);
1056
}
1057
1058
public function getDisplayIconForMimeType() {
1059
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
1060
$mime_type = $this->getMimeType();
1061
return idx($mime_map, $mime_type, 'fa-file-o');
1062
}
1063
1064
public function validateSecretKey($key) {
1065
return ($key == $this->getSecretKey());
1066
}
1067
1068
public function generateSecretKey() {
1069
return Filesystem::readRandomCharacters(20);
1070
}
1071
1072
public function setStorageProperties(array $properties) {
1073
$this->metadata[self::METADATA_STORAGE] = $properties;
1074
return $this;
1075
}
1076
1077
public function getStorageProperties() {
1078
return idx($this->metadata, self::METADATA_STORAGE, array());
1079
}
1080
1081
public function getStorageProperty($key, $default = null) {
1082
$properties = $this->getStorageProperties();
1083
return idx($properties, $key, $default);
1084
}
1085
1086
public function loadDataFromIterator($iterator) {
1087
$result = '';
1088
1089
foreach ($iterator as $chunk) {
1090
$result .= $chunk;
1091
}
1092
1093
return $result;
1094
}
1095
1096
public function updateDimensions($save = true) {
1097
if (!$this->isViewableImage()) {
1098
throw new Exception(pht('This file is not a viewable image.'));
1099
}
1100
1101
if (!function_exists('imagecreatefromstring')) {
1102
throw new Exception(pht('Cannot retrieve image information.'));
1103
}
1104
1105
if ($this->getIsChunk()) {
1106
throw new Exception(
1107
pht('Refusing to assess image dimensions of file chunk.'));
1108
}
1109
1110
$engine = $this->instantiateStorageEngine();
1111
if ($engine->isChunkEngine()) {
1112
throw new Exception(
1113
pht('Refusing to assess image dimensions of chunked file.'));
1114
}
1115
1116
$data = $this->loadFileData();
1117
1118
$img = @imagecreatefromstring($data);
1119
if ($img === false) {
1120
throw new Exception(pht('Error when decoding image.'));
1121
}
1122
1123
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
1124
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
1125
1126
if ($save) {
1127
$this->save();
1128
}
1129
1130
return $this;
1131
}
1132
1133
public function copyDimensions(PhabricatorFile $file) {
1134
$metadata = $file->getMetadata();
1135
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
1136
if ($width) {
1137
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
1138
}
1139
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
1140
if ($height) {
1141
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
1142
}
1143
1144
return $this;
1145
}
1146
1147
1148
/**
1149
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
1150
* resources. The builtin mechanism allows files shipped with Phabricator
1151
* to be treated like normal files so that APIs do not need to special case
1152
* things like default images or deleted files.
1153
*
1154
* Builtins are located in `resources/builtin/` and identified by their
1155
* name.
1156
*
1157
* @param PhabricatorUser Viewing user.
1158
* @param list<PhabricatorFilesBuiltinFile> List of builtin file specs.
1159
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
1160
*/
1161
public static function loadBuiltins(PhabricatorUser $user, array $builtins) {
1162
$builtins = mpull($builtins, null, 'getBuiltinFileKey');
1163
1164
// NOTE: Anyone is allowed to access builtin files.
1165
1166
$files = id(new PhabricatorFileQuery())
1167
->setViewer(PhabricatorUser::getOmnipotentUser())
1168
->withBuiltinKeys(array_keys($builtins))
1169
->execute();
1170
1171
$results = array();
1172
foreach ($files as $file) {
1173
$builtin_key = $file->getBuiltinName();
1174
if ($builtin_key !== null) {
1175
$results[$builtin_key] = $file;
1176
}
1177
}
1178
1179
$build = array();
1180
foreach ($builtins as $key => $builtin) {
1181
if (isset($results[$key])) {
1182
continue;
1183
}
1184
1185
$data = $builtin->loadBuiltinFileData();
1186
1187
$params = array(
1188
'name' => $builtin->getBuiltinDisplayName(),
1189
'canCDN' => true,
1190
'builtin' => $key,
1191
);
1192
1193
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
1194
try {
1195
$file = self::newFromFileData($data, $params);
1196
} catch (AphrontDuplicateKeyQueryException $ex) {
1197
$file = id(new PhabricatorFileQuery())
1198
->setViewer(PhabricatorUser::getOmnipotentUser())
1199
->withBuiltinKeys(array($key))
1200
->executeOne();
1201
if (!$file) {
1202
throw new Exception(
1203
pht(
1204
'Collided mid-air when generating builtin file "%s", but '.
1205
'then failed to load the object we collided with.',
1206
$key));
1207
}
1208
}
1209
unset($unguarded);
1210
1211
$file->attachObjectPHIDs(array());
1212
$file->attachObjects(array());
1213
1214
$results[$key] = $file;
1215
}
1216
1217
return $results;
1218
}
1219
1220
1221
/**
1222
* Convenience wrapper for @{method:loadBuiltins}.
1223
*
1224
* @param PhabricatorUser Viewing user.
1225
* @param string Single builtin name to load.
1226
* @return PhabricatorFile Corresponding builtin file.
1227
*/
1228
public static function loadBuiltin(PhabricatorUser $user, $name) {
1229
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
1230
->setName($name);
1231
1232
$key = $builtin->getBuiltinFileKey();
1233
1234
return idx(self::loadBuiltins($user, array($builtin)), $key);
1235
}
1236
1237
public function getObjects() {
1238
return $this->assertAttached($this->objects);
1239
}
1240
1241
public function attachObjects(array $objects) {
1242
$this->objects = $objects;
1243
return $this;
1244
}
1245
1246
public function getObjectPHIDs() {
1247
return $this->assertAttached($this->objectPHIDs);
1248
}
1249
1250
public function attachObjectPHIDs(array $object_phids) {
1251
$this->objectPHIDs = $object_phids;
1252
return $this;
1253
}
1254
1255
public function getOriginalFile() {
1256
return $this->assertAttached($this->originalFile);
1257
}
1258
1259
public function attachOriginalFile(PhabricatorFile $file = null) {
1260
$this->originalFile = $file;
1261
return $this;
1262
}
1263
1264
public function getImageHeight() {
1265
if (!$this->isViewableImage()) {
1266
return null;
1267
}
1268
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
1269
}
1270
1271
public function getImageWidth() {
1272
if (!$this->isViewableImage()) {
1273
return null;
1274
}
1275
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
1276
}
1277
1278
public function getAltText() {
1279
$alt = $this->getCustomAltText();
1280
1281
if ($alt !== null && strlen($alt)) {
1282
return $alt;
1283
}
1284
1285
return $this->getDefaultAltText();
1286
}
1287
1288
public function getCustomAltText() {
1289
return idx($this->metadata, self::METADATA_ALT_TEXT);
1290
}
1291
1292
public function setCustomAltText($value) {
1293
$value = phutil_string_cast($value);
1294
1295
if (!strlen($value)) {
1296
$value = null;
1297
}
1298
1299
if ($value === null) {
1300
unset($this->metadata[self::METADATA_ALT_TEXT]);
1301
} else {
1302
$this->metadata[self::METADATA_ALT_TEXT] = $value;
1303
}
1304
1305
return $this;
1306
}
1307
1308
public function getDefaultAltText() {
1309
$parts = array();
1310
1311
$name = $this->getName();
1312
if ($name !== null && strlen($name)) {
1313
$parts[] = $name;
1314
}
1315
1316
$stats = array();
1317
1318
$image_x = $this->getImageHeight();
1319
$image_y = $this->getImageWidth();
1320
1321
if ($image_x && $image_y) {
1322
$stats[] = pht(
1323
"%d\xC3\x97%d px",
1324
new PhutilNumber($image_x),
1325
new PhutilNumber($image_y));
1326
}
1327
1328
$bytes = $this->getByteSize();
1329
if ($bytes) {
1330
$stats[] = phutil_format_bytes($bytes);
1331
}
1332
1333
if ($stats) {
1334
$parts[] = pht('(%s)', implode(', ', $stats));
1335
}
1336
1337
if (!$parts) {
1338
return null;
1339
}
1340
1341
return implode(' ', $parts);
1342
}
1343
1344
public function getCanCDN() {
1345
if (!$this->isViewableImage()) {
1346
return false;
1347
}
1348
1349
return idx($this->metadata, self::METADATA_CAN_CDN);
1350
}
1351
1352
public function setCanCDN($can_cdn) {
1353
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
1354
return $this;
1355
}
1356
1357
public function isBuiltin() {
1358
return ($this->getBuiltinName() !== null);
1359
}
1360
1361
public function getBuiltinName() {
1362
return idx($this->metadata, self::METADATA_BUILTIN);
1363
}
1364
1365
public function setBuiltinName($name) {
1366
$this->metadata[self::METADATA_BUILTIN] = $name;
1367
return $this;
1368
}
1369
1370
public function getIsProfileImage() {
1371
return idx($this->metadata, self::METADATA_PROFILE);
1372
}
1373
1374
public function setIsProfileImage($value) {
1375
$this->metadata[self::METADATA_PROFILE] = $value;
1376
return $this;
1377
}
1378
1379
public function getIsChunk() {
1380
return idx($this->metadata, self::METADATA_CHUNK);
1381
}
1382
1383
public function setIsChunk($value) {
1384
$this->metadata[self::METADATA_CHUNK] = $value;
1385
return $this;
1386
}
1387
1388
public function setIntegrityHash($integrity_hash) {
1389
$this->metadata[self::METADATA_INTEGRITY] = $integrity_hash;
1390
return $this;
1391
}
1392
1393
public function getIntegrityHash() {
1394
return idx($this->metadata, self::METADATA_INTEGRITY);
1395
}
1396
1397
public function newIntegrityHash() {
1398
$engine = $this->instantiateStorageEngine();
1399
1400
if ($engine->isChunkEngine()) {
1401
return null;
1402
}
1403
1404
$format = $this->newStorageFormat();
1405
1406
$storage_handle = $this->getStorageHandle();
1407
$data = $engine->readFile($storage_handle);
1408
1409
return $engine->newIntegrityHash($data, $format);
1410
}
1411
1412
/**
1413
* Write the policy edge between this file and some object.
1414
*
1415
* @param phid Object PHID to attach to.
1416
* @return this
1417
*/
1418
public function attachToObject($phid) {
1419
$attachment_table = new PhabricatorFileAttachment();
1420
$attachment_conn = $attachment_table->establishConnection('w');
1421
1422
queryfx(
1423
$attachment_conn,
1424
'INSERT INTO %R (objectPHID, filePHID, attachmentMode,
1425
attacherPHID, dateCreated, dateModified)
1426
VALUES (%s, %s, %s, %ns, %d, %d)
1427
ON DUPLICATE KEY UPDATE
1428
attachmentMode = VALUES(attachmentMode),
1429
attacherPHID = VALUES(attacherPHID),
1430
dateModified = VALUES(dateModified)',
1431
$attachment_table,
1432
$phid,
1433
$this->getPHID(),
1434
PhabricatorFileAttachment::MODE_ATTACH,
1435
null,
1436
PhabricatorTime::getNow(),
1437
PhabricatorTime::getNow());
1438
1439
return $this;
1440
}
1441
1442
1443
/**
1444
* Configure a newly created file object according to specified parameters.
1445
*
1446
* This method is called both when creating a file from fresh data, and
1447
* when creating a new file which reuses existing storage.
1448
*
1449
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
1450
* for documentation.
1451
* @return this
1452
*/
1453
private function readPropertiesFromParameters(array $params) {
1454
PhutilTypeSpec::checkMap(
1455
$params,
1456
array(
1457
'name' => 'optional string',
1458
'authorPHID' => 'optional string',
1459
'ttl.relative' => 'optional int',
1460
'ttl.absolute' => 'optional int',
1461
'viewPolicy' => 'optional string',
1462
'isExplicitUpload' => 'optional bool',
1463
'canCDN' => 'optional bool',
1464
'profile' => 'optional bool',
1465
'format' => 'optional string|PhabricatorFileStorageFormat',
1466
'mime-type' => 'optional string',
1467
'builtin' => 'optional string',
1468
'storageEngines' => 'optional list<PhabricatorFileStorageEngine>',
1469
'chunk' => 'optional bool',
1470
));
1471
1472
$file_name = idx($params, 'name');
1473
$this->setName($file_name);
1474
1475
$author_phid = idx($params, 'authorPHID');
1476
$this->setAuthorPHID($author_phid);
1477
1478
$absolute_ttl = idx($params, 'ttl.absolute');
1479
$relative_ttl = idx($params, 'ttl.relative');
1480
if ($absolute_ttl !== null && $relative_ttl !== null) {
1481
throw new Exception(
1482
pht(
1483
'Specify an absolute TTL or a relative TTL, but not both.'));
1484
} else if ($absolute_ttl !== null) {
1485
if ($absolute_ttl < PhabricatorTime::getNow()) {
1486
throw new Exception(
1487
pht(
1488
'Absolute TTL must be in the present or future, but TTL "%s" '.
1489
'is in the past.',
1490
$absolute_ttl));
1491
}
1492
1493
$this->setTtl($absolute_ttl);
1494
} else if ($relative_ttl !== null) {
1495
if ($relative_ttl < 0) {
1496
throw new Exception(
1497
pht(
1498
'Relative TTL must be zero or more seconds, but "%s" is '.
1499
'negative.',
1500
$relative_ttl));
1501
}
1502
1503
$max_relative = phutil_units('365 days in seconds');
1504
if ($relative_ttl > $max_relative) {
1505
throw new Exception(
1506
pht(
1507
'Relative TTL must not be more than "%s" seconds, but TTL '.
1508
'"%s" was specified.',
1509
$max_relative,
1510
$relative_ttl));
1511
}
1512
1513
$absolute_ttl = PhabricatorTime::getNow() + $relative_ttl;
1514
1515
$this->setTtl($absolute_ttl);
1516
}
1517
1518
$view_policy = idx($params, 'viewPolicy');
1519
if ($view_policy) {
1520
$this->setViewPolicy($params['viewPolicy']);
1521
}
1522
1523
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
1524
$this->setIsExplicitUpload($is_explicit);
1525
1526
$can_cdn = idx($params, 'canCDN');
1527
if ($can_cdn) {
1528
$this->setCanCDN(true);
1529
}
1530
1531
$builtin = idx($params, 'builtin');
1532
if ($builtin) {
1533
$this->setBuiltinName($builtin);
1534
$this->setBuiltinKey($builtin);
1535
}
1536
1537
$profile = idx($params, 'profile');
1538
if ($profile) {
1539
$this->setIsProfileImage(true);
1540
}
1541
1542
$mime_type = idx($params, 'mime-type');
1543
if ($mime_type) {
1544
$this->setMimeType($mime_type);
1545
}
1546
1547
$is_chunk = idx($params, 'chunk');
1548
if ($is_chunk) {
1549
$this->setIsChunk(true);
1550
}
1551
1552
return $this;
1553
}
1554
1555
public function getRedirectResponse() {
1556
$uri = $this->getBestURI();
1557
1558
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
1559
// (if the file is a viewable image) and sometimes a local URI (if not).
1560
// For now, just detect which one we got and configure the response
1561
// appropriately. In the long run, if this endpoint is served from a CDN
1562
// domain, we can't issue a local redirect to an info URI (which is not
1563
// present on the CDN domain). We probably never actually issue local
1564
// redirects here anyway, since we only ever transform viewable images
1565
// right now.
1566
1567
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
1568
1569
return id(new AphrontRedirectResponse())
1570
->setIsExternal($is_external)
1571
->setURI($uri);
1572
}
1573
1574
public function newDownloadResponse() {
1575
// We're cheating a little bit here and relying on the fact that
1576
// getDownloadURI() always returns a fully qualified URI with a complete
1577
// domain.
1578
return id(new AphrontRedirectResponse())
1579
->setIsExternal(true)
1580
->setCloseDialogBeforeRedirect(true)
1581
->setURI($this->getDownloadURI());
1582
}
1583
1584
public function attachTransforms(array $map) {
1585
$this->transforms = $map;
1586
return $this;
1587
}
1588
1589
public function getTransform($key) {
1590
return $this->assertAttachedKey($this->transforms, $key);
1591
}
1592
1593
public function newStorageFormat() {
1594
$key = $this->getStorageFormat();
1595
$template = PhabricatorFileStorageFormat::requireFormat($key);
1596
1597
$format = id(clone $template)
1598
->setFile($this);
1599
1600
return $format;
1601
}
1602
1603
1604
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
1605
1606
1607
public function getApplicationTransactionEditor() {
1608
return new PhabricatorFileEditor();
1609
}
1610
1611
public function getApplicationTransactionTemplate() {
1612
return new PhabricatorFileTransaction();
1613
}
1614
1615
1616
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
1617
1618
1619
public function getCapabilities() {
1620
return array(
1621
PhabricatorPolicyCapability::CAN_VIEW,
1622
PhabricatorPolicyCapability::CAN_EDIT,
1623
);
1624
}
1625
1626
public function getPolicy($capability) {
1627
switch ($capability) {
1628
case PhabricatorPolicyCapability::CAN_VIEW:
1629
if ($this->isBuiltin()) {
1630
return PhabricatorPolicies::getMostOpenPolicy();
1631
}
1632
if ($this->getIsProfileImage()) {
1633
return PhabricatorPolicies::getMostOpenPolicy();
1634
}
1635
return $this->getViewPolicy();
1636
case PhabricatorPolicyCapability::CAN_EDIT:
1637
return PhabricatorPolicies::POLICY_NOONE;
1638
}
1639
}
1640
1641
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1642
$viewer_phid = $viewer->getPHID();
1643
if ($viewer_phid) {
1644
if ($this->getAuthorPHID() == $viewer_phid) {
1645
return true;
1646
}
1647
}
1648
1649
switch ($capability) {
1650
case PhabricatorPolicyCapability::CAN_VIEW:
1651
// If you can see the file this file is a transform of, you can see
1652
// this file.
1653
if ($this->getOriginalFile()) {
1654
return true;
1655
}
1656
1657
// If you can see any object this file is attached to, you can see
1658
// the file.
1659
return (count($this->getObjects()) > 0);
1660
}
1661
1662
return false;
1663
}
1664
1665
public function describeAutomaticCapability($capability) {
1666
$out = array();
1667
$out[] = pht('The user who uploaded a file can always view and edit it.');
1668
switch ($capability) {
1669
case PhabricatorPolicyCapability::CAN_VIEW:
1670
$out[] = pht(
1671
'Files attached to objects are visible to users who can view '.
1672
'those objects.');
1673
$out[] = pht(
1674
'Thumbnails are visible only to users who can view the original '.
1675
'file.');
1676
break;
1677
}
1678
1679
return $out;
1680
}
1681
1682
1683
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
1684
1685
1686
public function isAutomaticallySubscribed($phid) {
1687
return ($this->authorPHID == $phid);
1688
}
1689
1690
1691
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
1692
1693
1694
public function getUsersToNotifyOfTokenGiven() {
1695
return array(
1696
$this->getAuthorPHID(),
1697
);
1698
}
1699
1700
1701
/* -( PhabricatorDestructibleInterface )----------------------------------- */
1702
1703
1704
public function destroyObjectPermanently(
1705
PhabricatorDestructionEngine $engine) {
1706
1707
$this->openTransaction();
1708
$this->delete();
1709
$this->saveTransaction();
1710
}
1711
1712
1713
/* -( PhabricatorConduitResultInterface )---------------------------------- */
1714
1715
1716
public function getFieldSpecificationsForConduit() {
1717
return array(
1718
id(new PhabricatorConduitSearchFieldSpecification())
1719
->setKey('name')
1720
->setType('string')
1721
->setDescription(pht('The name of the file.')),
1722
id(new PhabricatorConduitSearchFieldSpecification())
1723
->setKey('uri')
1724
->setType('uri')
1725
->setDescription(pht('View URI for the file.')),
1726
id(new PhabricatorConduitSearchFieldSpecification())
1727
->setKey('dataURI')
1728
->setType('uri')
1729
->setDescription(pht('Download URI for the file data.')),
1730
id(new PhabricatorConduitSearchFieldSpecification())
1731
->setKey('size')
1732
->setType('int')
1733
->setDescription(pht('File size, in bytes.')),
1734
);
1735
}
1736
1737
public function getFieldValuesForConduit() {
1738
return array(
1739
'name' => $this->getName(),
1740
'uri' => PhabricatorEnv::getURI($this->getURI()),
1741
'dataURI' => $this->getCDNURI('data'),
1742
'size' => (int)$this->getByteSize(),
1743
'alt' => array(
1744
'custom' => $this->getCustomAltText(),
1745
'default' => $this->getDefaultAltText(),
1746
),
1747
);
1748
}
1749
1750
public function getConduitSearchAttachments() {
1751
return array();
1752
}
1753
1754
/* -( PhabricatorNgramInterface )------------------------------------------ */
1755
1756
1757
public function newNgrams() {
1758
return array(
1759
id(new PhabricatorFileNameNgrams())
1760
->setValue($this->getName()),
1761
);
1762
}
1763
1764
}
1765
1766