foldershare-8.x-1.2/src/ManageFileSystem.php

src/ManageFileSystem.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
<?php
 
namespace Drupal\foldershare;
 
use Drupal\file\FileInterface;
use Drupal\Core\File\FileSystemInterface;
 
use Drupal\foldershare\Utilities\FileUtilities;
 
/**
 * Manages file storage in the local file system.
 *
 * @ingroup foldershare
 */
final class ManageFileSystem {
 
  /*--------------------------------------------------------------------
   *
   * Constants.
   *
   *--------------------------------------------------------------------*/
 
  /**
   * The number of base-10 digits of file ID used for directory and file names.
   *
   * Directory and file names are generated automatically based upon the
   * file entity IDs of stored files.
   *
   * The number of digits is typically 4, which supports 10,000 files
   * or subdirectories in each subdirectory.  Keeping this number of
   * items small improves performance for operations that must open and
   * read directories. But keeping it large reduces the directory
   * depth, which can slightly improve file path handling.
   *
   * Operating system file systems may be limited in the number of separate
   * files and directories that they can support. For 32-bit file systems,
   * this limit is around 2 billion total.
   *
   * The following examples illustrate URIs when varying this number from
   * 1 to 10 for a File entity 123,456 with a module file directory DIR in
   * the public file system:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 1:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 2:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 3:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 4:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 5:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 6:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 7:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 8:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 9:
   *
   * - DIGITS_PER_SERVER_DIRECTORY_NAME = 10:
   *
   * @var int
   * @see ::getFileUri()
   */
  const DIGITS_PER_SERVER_DIRECTORY_NAME = 4;
 
  /**
   * The name of the top-level folder into which the module places files.
   *
   * The folder is within the public or private file system, depending
   * upon which file system is selected by the site administrator using
   * the module's settings.
   *
   * @var string
   * @see ::getFileUri()
   */
  const FILE_DIRECTORY = 'foldersharefiles';
 
  /*---------------------------------------------------------------------
   *
   * Temporary files.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Creates an empty temporary file in the module's temporary directory.
   *
   * A new empty file is created in the module's temporary files directory.
   * The file has a randomly generated name that does not collide with
   * anything else in the directory. Permissions are set appropriately for
   * web server access.
   *
   * @return string
   *   Returns a URI for a local temp file on success, and NULL on error.
   *
   * @see ::getTempDirectoryUri()
   * @see ::createLocalTempDirectory()
   */
  public static function createLocalTempFile() {
    // Create a temporary file with a random name in the temp directory.
    // The random name starts with the given prefix. Some OSes (e.g. Windows)
    // limit this prefix to three characters.
    $uri = FileUtilities::tempnam(self::getTempDirectoryUri(), 'tmp');
    if ($uri === FALSE) {
      return NULL;
    }
 
    // Insure the file is accessible.
    if (FileUtilities::chmod($uri) === FALSE) {
      return NULL;
    }
 
    return $uri;
  }
 
  /**
   * Creates an empty temporary directory in the module's data directory tree.
   *
   * A new empty directory is created in the module's temporary files directory.
   * The directory has a randomly generated name that does not collide with
   * anything else in the directory. Permissions are set appropriate for
   * web server access.
   *
   * @return string
   *   Returns a URI for a local temp directory on success, and NULL on error.
   *
   * @see ::getTempDirectoryUri()
   * @see ::createLocalTempFile()
   */
  public static function createLocalTempDirectory() {
    // There is no "tempnam()" for directories, only files. Use it to get
    // a safe random name, then replace the file with a directory.
    $uri = self::createLocalTempFile();
    if ($uri === NULL ||
        FileUtilities::unlink($uri) === FALSE ||
        FileUtilities::mkdir($uri) === FALSE ||
        FileUtilities::chmod($uri) === FALSE) {
      return NULL;
    }
 
    return $uri;
  }
 
  /**
   * Returns the URI for a temporary directory.
   *
   * @return string
   *   The URI for the directory path.
   */
  public static function getTempDirectoryUri() {
    // Use the Drupal standard "temporary" URI scheme, which goes to the
    // site's configured temp directory.
    return 'temporary://';
  }
 
  /*---------------------------------------------------------------------
   *
   * URIs.
   *
   *---------------------------------------------------------------------*/
 
  /**
   * Returns the URI for a file managed by the module.
   *
   * The returned URI has the form "SCHEME://SUBDIR/DIRS.../FILE.EXTENSION"
   * where:
   *
   * - SCHEME is either "public" or "private", based upon the module's file
   *   storage configuration.
   *
   * - SUBDIR is the name of the module's subdirectory for its files within
   *   the public or private file system.
   *
   * - DIRS.../FILE is a chain of numeric subdirectory names and a numeric
   *   file name (see below).
   *
   * - EXTENSION is the filename extension from the user-visible file name.
   *
   * Numeric subdirectory names are based upon the File object's entity ID,
   * and not the user-chosen file name or folder names. Such names have
   * multiple possible problems:
   *
   * - User file and directory names are not guaranteed to be unique site-wide,
   *   so they cannot safely coexist with names from other users unless some
   *   safe organization mechanism is introduced.
   *
   * - User file and directory names may use the full UTF-8 character set
   *   (except for a few restricted characters, like '/', ':', and '\'), but
   *   the underlying file system may not support the same characters.
   *
   * - USer file and diretory names may be up to the maximum length supported
   *   by the module (such as 255 characters), but the underlying file system
   *   may be more restrictive. For instance, file paths (the chain of
   *   directory names from '/' down to and including the file name) may be
   *   limited as well (such as to 1024 characters), which imposes another
   *   limit on directory depth.
   *
   * - User files may be nested within folders, and those folders within
   *   folders, to an arbitrary depth, but the underlying file system may
   *   have depth limits.
   *
   * - Any number of user files may be stored within the same folder, but
   *   the underlying file system may have limits or performance issues with
   *   very large directories.
   *
   * For these reasons, user-chosen file and directory names cannot be used
   * as-is. They must be replaced with alternative names that are safe for
   * any file system and unique.
   *
   * This module creates directory and file names based upon the unique
   * integer entity ID of the File object referencing the file. These IDs
   * are stored in Drupal as 64-bit integers, which can be represented as
   * 20 zero-padded digits (0..9).  These digits are then divided into groups
   * of consecutive digits based upon a module-wide configuration constant
   * DIGITS_PER_SERVER_DIRECTORY_NAME.  A typical value for this constant
   * is 4. This causes a 20-digit number to be split into 5 groups of 4 digits
   * each. The last of these groups is used as a numeric name for the file,
   * while the preceding groups are used as numeric names for subdirectories.
   *
   * As more File objects are created for use by this module, more numeric
   * files are added to the directories. When an entity ID rolls over to
   * update the 5th digit, the file is put into the next numeric subdirectory,
   * and so on. This keeps directory sizes under control (with a maximum of
   * 10^4 = 10,000 files each).
   *
   * The numeric file name has the original file's filename extension appended
   * so that file field formatters and other Drupal tools that look at paths
   * and filenames will see a proper extension.
   *
   * Once the file path is built for the given File entity, any directories
   * needed are created, permissions are set, and .htaccess files are added.
   *
   * @param \Drupal\file\FileInterface $file
   *   The file current stored locally, or soon to be stored locally.
   *
   * @return string
   *   The URI for the directory path to the file.
   *
   * @see ::getTempDirectoryUri()
   * @see ::prepareDirectories()
   * @see \Drupal\foldershare\Settings::getFileScheme()
   */
  public static function getFileUri(FileInterface $file) {
    //
    // Use multi-byte character string functions throughout. Local file
    // systems and URIs use UTF-8.
    //
    // Create file path
    // ----------------
    // The file URI is composed of:
    // - The configured public or private scheme.
    // - The configured subdirectory for files within the site.
    // - A chain of numeric directory names for the File entity ID.
    // - A final numeric file name.
    // - The file name extension of the original file.
    //
    // Start the path with the file directory.
    $path = self::FILE_DIRECTORY;
 
    // Entity IDs in Drupal are 64-bits. The maximum number of base-10 digits
    // is therefore 20. The DIGITS_PER_SERVER_DIRECTORY_NAME constant
    // determines how many digits are used in the directory and file names.
    $digits = sprintf('%020d', (int) $file->id());
 
    // Add numeric subdirectories based on the entity ID.
    // The last name added is for the file itself.
    for ($pos = 0; $pos < 20;) {
      $path .= '/';
      for ($i = 0; $i < self::DIGITS_PER_SERVER_DIRECTORY_NAME; ++$i) {
        $path .= $digits[$pos++];
      }
    }
 
    // Add the file's extension, if any.
    $ext = ManageFilenameExtensions::getExtensionFromPath($file->getFilename());
    if (empty($ext) === FALSE) {
      $path .= '.' . $ext;
    }
 
    //
    // Create directories
    // ------------------
    // Create any numeric directories that don't already exist, set their
    // permissions, and add .htaccess files as needed.
    self::prepareFileDirectories($path);
 
    // Add "public" or "private" to create the URI.
    return Settings::getFileScheme() . '://' . $path;
  }
 
  /**
   * Creates module file directories and adds .htaccess files.
   *
   * The given URI is converted to a file system path and all missing
   * directories on the path created and their permissions set.
   *
   * If the module's file directory does not have a .htaccess file, or
   * there is a file but it may have been edited, then a new .htaccess
   * file is added that block's direct web server access.
   *
   * @param string $path
   *   The local file path for a file under management by this module.
   *
   * @return bool
   *   Returns TRUE on success and FALSE on an error. The only error case
   *   occurs when a directory cannot be created, and a message is logged.
   *
   * @see \Drupal\Core\File\FileSystemInterface::prepareDirectory()
   */
  private static function prepareFileDirectories(string $path) {
    //
    // Setup.
    // ------
    // Get the current public/private file system choice for file storage,
    // then build a path to the parent directory.
    $manager = \Drupal::service('stream_wrapper_manager');
    $fileSystem = \Drupal::service('file_system');
 
    $fileScheme = Settings::getFileScheme();
    $stream = $manager->getViaScheme($fileScheme);
 
    if ($stream === FALSE) {
      // Fail with unknown scheme.
      return FALSE;
    }
 
    $streamDirectory = $stream->getDirectoryPath();
    $parentPath = FileUtilities::dirname($streamDirectory . '/' . $path);
 
    //
    // Create parent directories, if needed.
    // -------------------------------------
    // If one or more parent directories on the file's path do not exist yet.
    // Create them, setting permissions.
    if (FileUtilities::fileExists($parentPath) === FALSE) {
      if ($fileSystem->prepareDirectory(
        $parentPath,
        (FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) === FALSE) {
        // The parent directory creation failed. There are several possible
        // reasons for this:
        // - The directory already exists.
        // - The file system is full.
        // - The file system is offline.
        // - Something horrific is going on.
        //
        // Of these, only the first case is interesting. While above we
        // checked if the directory already exists, it is possible that
        // another process has snuck in and created the directory before
        // we've tried to create it. That will cause a failure here.
        //
        // Ideally, that other process was an instance of Drupal using
        // FolderShare. In that case the file prepare would have occurred
        // correctly, with permissions set, etc. But we can't be sure.
        // So, let's try again to be sure, but only if the directory now
        // appears to exist.
        $failure = TRUE;
        if (FileUtilities::fileExists($parentPath) === TRUE) {
          if ($fileSystem->prepareDirectory(
            $parentPath,
            (FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) === TRUE) {
            // It worked this time!
            $failure = FALSE;
          }
        }
 
        if ($failure === TRUE) {
          // The parent directories could not be created.
          ManageLog::critical(
            "Local file system: '@path' could not be created.\nThe directory is needed by the @module module to store uploaded files. There may be a problem with the server's file system directories and/or permissions.",
            [
              '@path'   => $parentPath,
              '@module' => Constants::MODULE,
            ]);
          return FALSE;
        }
      }
      elseif (FileUtilities::fileExists($parentPath) === FALSE) {
        // The parent directory creation succeeded, but the directory isn't
        // there? This should not be possible.
        ManageLog::critical(
          "Local file system: '@path' could not be read.\nThe directory is needed by the @module module to store uploaded files. There may be a problem with the server's file system permissions.",
          [
            '@path'   => $parentPath,
            '@module' => Constants::MODULE,
          ]);
        return FALSE;
      }
    }
 
    if ($fileScheme !== 'private') {
      //
      // Create .htaccess file in top directory, if needed.
      // --------------------------------------------------
      // Drupal can be configured to use one or both of:
      // - a public (default) directory served directly by the web server.
      // - a private directory out of view from the web server.
      //
      // For a private directory, Drupal requires and adds a .htaccess file
      // that insures that if (somehow) a web server can see the directory,
      // everything within the directory is blocked from being directly served
      // by the web server. Instead, file accesses are redirected to Drupal,
      // which can do access control.
      //
      // For a public directory, Drupal adds a .htaccess file that allows
      // a web server to directly serve the files. File accesses then do not
      // invoke Drupal, and there is no access control.
      //
      // FolderShare always wants access control. When the module is configured
      // to use a private directory, there is nothing more required. Drupal's
      // default .htaccess file already directs accesses back to Drupal, even
      // for subdirectories and files deep below.
      //
      // When FolderShare is configured to use a public directory, however,
      // the Drupal default .htaccess at the top of the files directory tree
      // is not suitable. The module's subdirectory needs its own .htaccess
      // file that closes down direct web server access. Thereafter, accesses
      // to files anywhwere in the module's files subdirectory cause redirects
      // to Drupal where we can do access control.
      //
      // SECURITY NOTE:
      // - A .htaccess file is not required when using a private file system.
      //   We assume that the private file system is, indeed, private and that
      //   a web server therefore cannot see the files stored there, or an
      //   .htaccess file stored there.
      //
      // - On the first use of the directory, there will be no .htaccess file
      //   and one must be created.
      //
      // - On further use of the directory, it is possible that a hacker or
      //   misguided site admin has edited the .htaccess file to open access.
      //   This is not appropriate since the files in this directory are
      //   strictly under FolderShare management. To prevent a possible
      //   access control bypass, the .htaccess file is OVERWRITTEN every
      //   time this function is called.
      $filesPath = $streamDirectory . '/' . self::FILE_DIRECTORY;
 
      // file_save_access's third argument (TRUE) indicates the function
      // should overwrite any existing .htaccess file. Unfortunately, it
      // does not actually do this... if the file doesn't have write
      // permission. In order to force the issue, we need to first change
      // the file's permission to gain write permission, if the file exists.
      $htaccess = $filesPath . '/.htaccess';
      if (FileUtilities::fileExists($htaccess) === TRUE) {
        @chmod($htaccess, 0666);
      }
 
      file_save_htaccess($filesPath, TRUE, TRUE);
 
      // SECURITY NOTE:
      // - It is possible that a hacker or site admin has added a .htaccess
      //   file to intermediate directories. These should be removed, but
      //   detecting them requires a top-down directory tree traversal,
      //   which is expensive.
    }
 
    // The parent directories now all exist, though the file does not yet.
    return TRUE;
  }
 
  /**
   * Parses a URI for a module file and returns the File entity ID.
   *
   * @param string $path
   *   The local file path for a file managed by this module.
   *
   * @return int|bool
   *   Returns the integer File entity ID parsed from the path, or FALSE if
   *   the path is not for a file managed by this module.
   */
  public static function getFileEntityId(string $path) {
    //
    // Find start of numeric directory names
    // -------------------------------------
    // Look for use of the module's FILE_DIRECTORY name. If not found,
    // the path is malformed.
    $fileDirectory = self::FILE_DIRECTORY;
    $pos = mb_strpos($path, $fileDirectory);
    if ($pos === FALSE) {
      // Fail. FILE_DIRECTORY is not in the path, therefore this is not a
      // path to a module file.
      return FALSE;
    }
 
    //
    // Parse entity ID
    // ---------------
    // Strip the file directory from the front of the path, then use the
    // remainder to build the 20 digit entity ID. Return the integer form
    // of that ID.
    $remainder = mb_substr($path, ($pos + mb_strlen($fileDirectory) + 1));
 
    // Remove all of the slashes, joining together the numeric names of
    // the subdirectories.
    $digits = mb_ereg_replace('/', '', $remainder);
 
    // Remove the file extension, if any.
    $dotIndex = mb_strrpos($digits, '.');
    if ($dotIndex !== FALSE) {
      $digits = mb_substr($digits, 0, $dotIndex);
    }
 
    if (mb_strlen($digits) !== 20) {
      // Fail. Wrong number of digits. Not directory names and file name.
      return FALSE;
    }
 
    // Convert the numeric string to a file entity ID.
    if (is_numeric($digits) === FALSE) {
      // Fail. Malformed directory and file names.
      return FALSE;
    }
 
    return (int) intval($digits);
  }
 
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc