/** * Generates a new filename in an attempt to create a unique name * * @param string $filename The filename to generate another name for * @return string The newly generated filename */ private function generateNewFilename($filename) { $filename_info = fFilesystem::getPathInfo($filename); if (preg_match('#_copy(\\d+)($|\\.)#D', $filename_info['filename'], $match)) { $i = $match[1] + 1; } else { $i = 1; } $extension = $filename_info['extension'] ? '.' . $filename_info['extension'] : ''; return preg_replace('#_copy\\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension; }
/** * Validates a $_FILES array against the upload configuration * * @param array $file_array The $_FILES array for a single file * @return string The validation error message */ private function validateField($file_array) { if (empty($file_array['name'])) { if ($this->required) { return self::compose('Please upload a file'); } return NULL; } if ($file_array['error'] == UPLOAD_ERR_FORM_SIZE || $file_array['error'] == UPLOAD_ERR_INI_SIZE) { $max_size = !empty($_POST['MAX_FILE_SIZE']) ? $_POST['MAX_FILE_SIZE'] : ini_get('upload_max_filesize'); $max_size = !is_numeric($max_size) ? fFilesystem::convertToBytes($max_size) : $max_size; return self::compose('The file uploaded is over the limit of %s', fFilesystem::formatFilesize($max_size)); } if ($this->max_size && $file_array['size'] > $this->max_size) { return self::compose('The file uploaded is over the limit of %s', fFilesystem::formatFilesize($this->max_size)); } if (empty($file_array['tmp_name']) || empty($file_array['size'])) { if ($this->required) { return self::compose('Please upload a file'); } return NULL; } if (!empty($this->mime_types) && file_exists($file_array['tmp_name'])) { $contents = file_get_contents($file_array['tmp_name'], FALSE, NULL, 0, 4096); if (!in_array(fFile::determineMimeType($file_array['name'], $contents), $this->mime_types)) { return self::compose($this->mime_type_message); } } if (!$this->allow_php) { $file_info = fFilesystem::getPathInfo($file_array['name']); if (in_array(strtolower($file_info['extension']), array('php', 'php4', 'php5'))) { return self::compose('The file uploaded is a PHP file, but those are not permitted'); } } if (!$this->allow_dot_files) { if (substr($file_array['name'], 0, 1) == '.') { return self::compose('The name of the uploaded file may not being with a .'); } } if ($this->image_dimensions && file_exists($file_array['tmp_name'])) { if (fImage::isImageCompatible($file_array['tmp_name'])) { list($width, $height, $other) = getimagesize($file_array['tmp_name']); if ($this->image_dimensions['min_width'] && $width < $this->image_dimensions['min_width']) { return self::compose('The uploaded image is narrower than the minimum width of %spx', $this->image_dimensions['min_width']); } if ($this->image_dimensions['min_height'] && $height < $this->image_dimensions['min_height']) { return self::compose('The uploaded image is shorter than the minimum height of %spx', $this->image_dimensions['min_height']); } if ($this->image_dimensions['max_width'] && $width > $this->image_dimensions['max_width']) { return self::compose('The uploaded image is wider than the maximum width of %spx', $this->image_dimensions['max_width']); } if ($this->image_dimensions['max_height'] && $height > $this->image_dimensions['max_height']) { return self::compose('The uploaded image is taller than the maximum height of %spx', $this->image_dimensions['max_height']); } } } if ($this->image_ratio && file_exists($file_array['tmp_name'])) { if (fImage::isImageCompatible($file_array['tmp_name'])) { list($width, $height, $other) = getimagesize($file_array['tmp_name']); if ($this->image_ratio['allow_excess_dimension'] == 'width' && $width / $height < $this->image_ratio['width'] / $this->image_ratio['height']) { return self::compose('The uploaded image is too narrow for its height. The required ratio is %1$sx%2$s or wider.', $this->image_ratio['width'], $this->image_ratio['height']); } if ($this->image_ratio['allow_excess_dimension'] == 'height' && $width / $height > $this->image_ratio['width'] / $this->image_ratio['height']) { return self::compose('The uploaded image is too short for its width. The required ratio is %1$sx%2$s or taller.', $this->image_ratio['width'], $this->image_ratio['height']); } } } }
/** * Renames the current file * * If the filename already exists and the overwrite flag is set to false, * a new filename will be created. * * This operation will be reverted if a filesystem transaction is in * progress and is later rolled back. * * @param string $new_filename The new full path to the file or a new filename in the current directory * @param boolean $overwrite If the new filename already exists, `TRUE` will cause the file to be overwritten, `FALSE` will cause the new filename to change * @return fFile The file object, to allow for method chaining */ public function rename($new_filename, $overwrite) { $this->tossIfDeleted(); if (!$this->getParent()->isWritable()) { throw new fEnvironmentException('The file, %s, can not be renamed because the directory containing it is not writable', $this->file); } // If the filename does not contain any folder traversal, rename the file in the current directory if (preg_match('#^[^/\\\\]+$#D', $new_filename)) { $new_filename = $this->getParent()->getPath() . $new_filename; } $info = fFilesystem::getPathInfo($new_filename); if (!file_exists($info['dirname'])) { throw new fProgrammerException('The new filename specified, %s, is inside of a directory that does not exist', $new_filename); } // Make the filename absolute $new_filename = fDirectory::makeCanonical(realpath($info['dirname'])) . $info['basename']; if ($this->file == $new_filename && $overwrite) { return $this; } if (file_exists($new_filename) && !$overwrite) { $new_filename = fFilesystem::makeUniqueName($new_filename); } if (file_exists($new_filename)) { if (!is_writable($new_filename)) { throw new fEnvironmentException('The new filename specified, %s, already exists, but is not writable', $new_filename); } if (fFilesystem::isInsideTransaction()) { fFilesystem::recordWrite(new fFile($new_filename)); } // Windows requires that the existing file be deleted before being replaced unlink($new_filename); } else { $new_dir = new fDirectory($info['dirname']); if (!$new_dir->isWritable()) { throw new fEnvironmentException('The new filename specified, %s, is inside of a directory that is not writable', $new_filename); } } rename($this->file, $new_filename); // Allow filesystem transactions if (fFilesystem::isInsideTransaction()) { fFilesystem::recordRename($this->file, $new_filename); } fFilesystem::updateFilenameMap($this->file, $new_filename); return $this; }
/** * Renames the current directory * * This operation will NOT be performed until the filesystem transaction * has been committed, if a transaction is in progress. Any non-Flourish * code (PHP or system) will still see this directory (and all contained * files/dirs) as existing with the old paths until that point. * * @param string $new_dirname The new full path to the directory or a new name in the current parent directory * @param boolean $overwrite If the new dirname already exists, TRUE will cause the file to be overwritten, FALSE will cause the new filename to change * @return void */ public function rename($new_dirname, $overwrite) { $this->tossIfDeleted(); if (!$this->getParent()->isWritable()) { throw new fEnvironmentException('The directory, %s, can not be renamed because the directory containing it is not writable', $this->directory); } // If the dirname does not contain any folder traversal, rename the dir in the current parent directory if (preg_match('#^[^/\\\\]+$#D', $new_dirname)) { $new_dirname = $this->getParent()->getPath() . $new_dirname; } $info = fFilesystem::getPathInfo($new_dirname); if (!file_exists($info['dirname'])) { throw new fProgrammerException('The new directory name specified, %s, is inside of a directory that does not exist', $new_dirname); } if (file_exists($new_dirname)) { if (!is_writable($new_dirname)) { throw new fEnvironmentException('The new directory name specified, %s, already exists, but is not writable', $new_dirname); } if (!$overwrite) { $new_dirname = fFilesystem::makeUniqueName($new_dirname); } } else { $parent_dir = new fDirectory($info['dirname']); if (!$parent_dir->isWritable()) { throw new fEnvironmentException('The new directory name specified, %s, is inside of a directory that is not writable', $new_dirname); } } rename($this->directory, $new_dirname); // Make the dirname absolute $new_dirname = fDirectory::makeCanonical(realpath($new_dirname)); // Allow filesystem transactions if (fFilesystem::isInsideTransaction()) { fFilesystem::rename($this->directory, $new_dirname); } fFilesystem::updateFilenameMapForDirectory($this->directory, $new_dirname); }
/** * Validates the uploaded file, ensuring a file was actually uploaded and that is matched the restrictions put in place * * @throws fValidationException When no file is uploaded or the uploaded file violates the options set for this object * * @param string $field The field the file was uploaded through * @param integer $index If the field was an array of file uploads, this specifies which one to validate * @return void */ public function validate($field, $index = NULL) { if (!self::check($field)) { throw new fProgrammerException('The field specified, %s, does not appear to be a file upload field', $field); } $file_array = $this->extractFileUploadArray($field, $index); // Do some validation of the file provided if (empty($file_array['name'])) { throw new fValidationException('Please upload a file'); } if ($file_array['error'] == UPLOAD_ERR_FORM_SIZE || $file_array['error'] == UPLOAD_ERR_INI_SIZE) { $max_size = !empty($_POST['MAX_FILE_SIZE']) ? $_POST['MAX_FILE_SIZE'] : ini_get('upload_max_filesize'); $max_size = !is_numeric($max_size) ? fFilesystem::convertToBytes($max_size) : $max_size; $msg = $this->max_message != "" ? $this->max_message : 'The file uploaded is over the limit of %s'; throw new fValidationException($msg, fFilesystem::formatFilesize($max_size)); } if ($this->max_file_size && $file_array['size'] > $this->max_file_size) { $msg = $this->max_message != "" ? $this->max_message : 'The file uploaded is over the limit of %s'; throw new fValidationException($msg, fFilesystem::formatFilesize($this->max_file_size)); } if (empty($file_array['tmp_name']) || empty($file_array['size'])) { throw new fValidationException('Please upload a file'); } if (!empty($this->mime_types) && file_exists($file_array['tmp_name']) && !in_array(fFile::determineMimeType($file_array['tmp_name']), $this->mime_types)) { throw new fValidationException($this->mime_type_message); } if (!$this->allow_php) { $file_info = fFilesystem::getPathInfo($file_array['name']); if (in_array(strtolower($file_info['extension']), array('php', 'php4', 'php5'))) { throw new fValidationException('The file uploaded is a PHP file, but those are not permitted'); } } return $file_array; }
/** * Adds an attachment to the email * * If a duplicate filename is detected, it will be changed to be unique. * * @param string $filename The name of the file to attach * @param string $mime_type The mime type of the file * @param string $contents The contents of the file * @return void */ public function addAttachment($filename, $mime_type, $contents) { if (!self::stringlike($filename)) { throw new fProgrammerException('The filename specified, %s, does not appear to be a valid filename', $filename); } $filename = (string) $filename; $i = 1; while (isset($this->attachments[$filename])) { $filename_info = fFilesystem::getPathInfo($filename); $extension = $filename_info['extension'] ? '.' . $filename_info['extension'] : ''; $filename = preg_replace('#_copy\\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension; $i++; } $this->attachments[$filename] = array('mime-type' => $mime_type, 'contents' => $contents); }
/** * Validates a $_FILES array against the upload configuration * * @param array $file_array The $_FILES array for a single file * @return string The validation error message */ private function validateField($file_array) { if (empty($file_array['name'])) { if ($this->required) { return self::compose('Please upload a file'); } return NULL; } if ($file_array['error'] == UPLOAD_ERR_FORM_SIZE || $file_array['error'] == UPLOAD_ERR_INI_SIZE) { $max_size = !empty($_POST['MAX_FILE_SIZE']) ? $_POST['MAX_FILE_SIZE'] : ini_get('upload_max_filesize'); $max_size = !is_numeric($max_size) ? fFilesystem::convertToBytes($max_size) : $max_size; return self::compose('The file uploaded is over the limit of %s', fFilesystem::formatFilesize($max_size)); } if ($this->max_size && $file_array['size'] > $this->max_size) { return self::compose('The file uploaded is over the limit of %s', fFilesystem::formatFilesize($this->max_size)); } if (empty($file_array['tmp_name']) || empty($file_array['size'])) { if ($this->required) { return self::compose('Please upload a file'); } return NULL; } if (!empty($this->mime_types) && file_exists($file_array['tmp_name'])) { $contents = file_get_contents($file_array['tmp_name'], FALSE, NULL, 0, 4096); if (!in_array(fFile::determineMimeType($file_array['name'], $contents), $this->mime_types)) { return self::compose($this->mime_type_message); } } if (!$this->allow_php) { $file_info = fFilesystem::getPathInfo($file_array['name']); if (in_array(strtolower($file_info['extension']), array('php', 'php4', 'php5'))) { return self::compose('The file uploaded is a PHP file, but those are not permitted'); } } if (!$this->allow_dot_files) { if (substr($file_array['name'], 0, 1) == '.') { return self::compose('The name of the uploaded file may not being with a .'); } } }
/** * Saves any changes to the image * * If the file type is different than the current one, removes the current * file once the new one is created. * * This operation will be reverted by a filesystem transaction being rolled * back. If a transaction is in progress and the new image type causes a * new file to be created, the old file will not be deleted until the * transaction is committed. * * @param string $new_image_type The new file format for the image: 'NULL` (no change), `'jpg'`, `'gif'`, `'png'` * @param integer $jpeg_quality The quality setting to use for JPEG images - this may be ommitted * @param boolean $overwrite If an existing file with the same name and extension should be overwritten * @param string :$new_image_type * @param boolean :$overwrite * @return fImage The image object, to allow for method chaining */ public function saveChanges($new_image_type = NULL, $jpeg_quality = 90, $overwrite = FALSE) { // This allows ommitting the $jpeg_quality parameter, which is very useful for non-jpegs $args = func_get_args(); if (count($args) == 2 && is_bool($args[1])) { $overwrite = $args[1]; $jpeg_quality = 90; } $this->tossIfDeleted(); self::determineProcessor(); if (self::$processor == 'none') { throw new fEnvironmentException("The changes to the image can't be saved because neither the GD extension or ImageMagick appears to be installed on the server"); } $type = self::getImageType($this->file); if ($type == 'tif' && self::$processor == 'gd') { throw new fEnvironmentException('The image specified, %s, is a TIFF file and the GD extension can not handle TIFF files. Please install ImageMagick if you wish to manipulate TIFF files.', $this->file); } $valid_image_types = array('jpg', 'gif', 'png'); if ($new_image_type !== NULL && !in_array($new_image_type, $valid_image_types)) { throw new fProgrammerException('The new image type specified, %1$s, is invalid. Must be one of: %2$s.', $new_image_type, join(', ', $valid_image_types)); } if (is_numeric($jpeg_quality)) { $jpeg_quality = (int) round($jpeg_quality); } if (!is_integer($jpeg_quality) || $jpeg_quality < 1 || $jpeg_quality > 100) { throw new fProgrammerException('The JPEG quality specified, %1$s, is either not an integer, less than %2$s or greater than %3$s.', $jpeg_quality, 1, 100); } if ($new_image_type && fFilesystem::getPathInfo($this->file, 'extension') != $new_image_type) { if ($overwrite) { $path_info = fFilesystem::getPathInfo($this->file); $output_file = $path_info['dirname'] . $path_info['filename'] . '.' . $new_image_type; } else { $output_file = fFilesystem::makeUniqueName($this->file, $new_image_type); } if (file_exists($output_file)) { if (!is_writable($output_file)) { throw new fEnvironmentException('Changes to the image can not be saved because the file, %s, is not writable', $output_file); } } else { $output_dir = dirname($output_file); if (!is_writable($output_dir)) { throw new fEnvironmentException('Changes to the image can not be saved because the directory to save the new file, %s, is not writable', $output_dir); } } } else { $output_file = $this->file; if (!is_writable($output_file)) { throw new fEnvironmentException('Changes to the image can not be saved because the file, %s, is not writable', $output_file); } } // If we don't have any changes and no name change, just exit if (!$this->pending_modifications && $output_file == $this->file) { return $this; } // Wrap changes to the image into the filesystem transaction if ($output_file == $this->file && fFilesystem::isInsideTransaction()) { fFilesystem::recordWrite($this); } if (self::$processor == 'gd') { $this->processWithGD($output_file, $jpeg_quality); } elseif (self::$processor == 'imagemagick') { $this->processWithImageMagick($output_file, $jpeg_quality); } $old_file = $this->file; fFilesystem::updateFilenameMap($this->file, $output_file); // If we created a new image, delete the old one if ($output_file != $old_file) { $old_image = new fImage($old_file); $old_image->delete(); } $this->pending_modifications = array(); return $this; }
/** * Processes the current image using ImageMagick * * @param string $output_file The file to save the image to * @param integer $jpeg_quality The JPEG quality to use * @return void */ private function processWithImageMagick($output_file, $jpeg_quality) { $type = self::getImageType($this->file); $command_line = escapeshellcmd(self::$imagemagick_dir . 'convert'); if (self::$imagemagick_temp_dir) { $command_line .= ' -set registry:temporary-path ' . escapeshellarg(self::$imagemagick_temp_dir) . ' '; } $command_line .= ' ' . escapeshellarg($this->file) . ' '; // Animated gifs need to be coalesced if ($this->isAnimatedGif()) { $command_line .= ' -coalesce '; } // TIFF files should be set to a depth of 8 if ($type == 'tif') { $command_line .= ' -depth 8 '; } foreach ($this->pending_modifications as $mod) { // Perform the resize operation if ($mod['operation'] == 'resize') { $command_line .= ' -resize ' . $mod['width'] . 'x' . $mod['height'] . ' '; // Perform the crop operation } elseif ($mod['operation'] == 'crop') { $command_line .= ' -crop ' . $mod['width'] . 'x' . $mod['height']; $command_line .= '+' . $mod['start_x'] . '+' . $mod['start_y']; $command_line .= ' -repage ' . $mod['width'] . 'x' . $mod['height'] . '+0+0 '; // Perform the desaturate operation } elseif ($mod['operation'] == 'desaturate') { $command_line .= ' -colorspace GRAY '; } } // Default to the RGB colorspace if (strpos($command_line, ' -colorspace ')) { $command_line .= ' -colorspace RGB '; } // Set up jpeg compression $path_info = fFilesystem::getPathInfo($output_file); $new_type = $path_info['extension']; $new_type = $new_type == 'jpeg' ? 'jpg' : $new_type; if (!in_array($new_type, array('gif', 'jpg', 'png'))) { $new_type = $type; } if ($new_type == 'jpg') { $command_line .= ' -compress JPEG -quality ' . $jpeg_quality . ' '; } $command_line .= ' ' . escapeshellarg($new_type . ':' . $output_file); exec($command_line); }