allMethodNames(); foreach($methodNames as $methodName) { if(substr($methodName,0,8) == 'generate') { $this->addWrapperMethod(substr($methodName,8), 'getFormattedImage'); } } parent::defineMethods(); } /** * An image exists if it has a filename. * @return boolean */ public function exists() { if(isset($this->record["Filename"])) { return true; } } /** * Get the relative URL for this Image. * Overwrites File->URL() which returns an absolute URL. * * @todo Refactor to return absolute URL like {@link File} * @uses Director::baseURL() * @return string */ function getURL() { return Director::baseURL() . $this->Filename; } /** * Return an XHTML img tag for this Image. * @return string */ function Tag() { if(file_exists("../" . $this->Filename)) { $url = $this->URL(); $title = $this->Title; return ""; } } /** * Return an XHTML img tag for this Image. * @return string */ function forTemplate() { return $this->Tag(); } /** * Load a recently uploaded image into this image field. * @param array $tmpFile The array entry from $_FILES * @return boolean Returns true if successful */ function loadUploaded($tmpFile) { if(parent::loadUploaded($tmpFile)) { $this->deleteFormattedImages(); return true; } } function loadUploadedImage($tmpFile) { if(!is_array($tmpFile)) { user_error("Image::loadUploadedImage() Not passed an array. Most likely, the form hasn't got the right enctype", E_USER_ERROR); } if(!$tmpFile['size']) { return; } $base = dirname(dirname($_SERVER['SCRIPT_FILENAME'])); $class = $this->class; // Create a folder if(!file_exists("$base/assets")) { mkdir("$base/assets", Filesystem::$folder_create_mask); } if(!file_exists("$base/assets/$class")) { mkdir("$base/assets/$class", Filesystem::$folder_create_mask); } // Generate default filename $file = str_replace(' ', '-',$tmpFile['name']); $file = ereg_replace('[^A-Za-z0-9+.-]+','',$file); $file = ereg_replace('-+', '-',$file); if(!$file) { $file = "file.jpg"; } $file = "assets/$class/$file"; while(file_exists("$base/$file")) { $i = $i ? ($i+1) : 2; $oldFile = $file; $file = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $file); if($oldFile == $file && $i > 2) user_error("Couldn't fix $file with $i", E_USER_ERROR); } if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$file")) { // Remove the old images $this->deleteFormattedImages(); return true; } } public function SetWidth($width) { return $this->getFormattedImage('SetWidth', $width); } /** * Resize this Image by width, keeping aspect ratio. Use in templates with $SetWidth. * @return GD */ public function generateSetWidth(GD $gd, $width) { return $gd->resizeByWidth($width); } /** * Resize this Image by height, keeping aspect ratio. Use in templates with $SetHeight. * @return GD */ public function generateSetHeight(GD $gd, $height){ return $gd->resizeByHeight($height); } public function CMSThumbnail() { return $this->getFormattedImage('CMSThumbnail'); } /** * Resize this image for the CMS. Use in templates with $CMSThumbnail. * @return GD */ function generateCMSThumbnail(GD $gd) { return $gd->paddedResize($this->stat('cms_thumbnail_width'),$this->stat('cms_thumbnail_height')); } /** * Resize this image for preview in the Asset section. Use in templates with $AssetLibraryPreview. * @return GD */ function generateAssetLibraryPreview(GD $gd) { return $gd->paddedResize($this->stat('asset_preview_width'),$this->stat('asset_preview_height')); } /** * Resize this image for thumbnail in the Asset section. Use in templates with $AssetLibraryThumbnail. * @return GD */ function generateAssetLibraryThumbnail(GD $gd) { return $gd->paddedResize($this->stat('asset_thumbnail_width'),$this->stat('asset_thumbnail_height')); } /** * Resize this image for use as a thumbnail in a strip. Use in templates with $StripThumbnail. * @return GD */ function generateStripThumbnail(GD $gd) { return $gd->croppedResize($this->stat('strip_thumbnail_width'),$this->stat('strip_thumbnail_height')); } function generatePaddedImage(GD $gd, $width, $height) { return $gd->paddedResize($width, $height); } /** * Return an image object representing the image in the given format. * This image will be generated using generateFormattedImage(). * The generated image is cached, to flush the cache append ?flush=1 to your URL. * @param string $format The name of the format. * @param string $arg1 An argument to pass to the generate function. * @param string $arg2 A second argument to pass to the generate function. * @return Image_Cached */ function getFormattedImage($format, $arg1 = null, $arg2 = null) { if($this->ID && $this->Filename && Director::fileExists($this->Filename)) { $cacheFile = $this->cacheFilename($format, $arg1, $arg2); if(!file_exists("../".$cacheFile) || isset($_GET['flush'])) { $this->generateFormattedImage($format, $arg1, $arg2); } return new Image_Cached($cacheFile); } } /** * Return the filename for the cached image, given it's format name and arguments. * @param string $format The format name. * @param string $arg1 The first argument passed to the generate function. * @param string $arg2 The second argument passed to the generate function. * @return string */ function cacheFilename($format, $arg1 = null, $arg2 = null) { $folder = $this->ParentID ? $this->Parent()->Filename : "assets/"; $format = $format.$arg1.$arg2; return $folder . "_resampled/$format-" . $this->Name; } /** * Generate an image on the specified format. It will save the image * at the location specified by cacheFilename(). The image will be generated * using the specific 'generate' method for the specified format. * @param string $format Name of the format to generate. * @param string $arg1 Argument to pass to the generate method. * @param string $arg2 A second argument to pass to the generate method. */ function generateFormattedImage($format, $arg1 = null, $arg2 = null) { $cacheFile = $this->cacheFilename($format, $arg1, $arg2); $gd = new GD("../" . $this->Filename); if($gd->hasGD()){ $generateFunc = "generate$format"; if($this->hasMethod($generateFunc)){ $gd = $this->$generateFunc($gd, $arg1, $arg2); if($gd){ $gd->writeTo("../" . $cacheFile); } } else { USER_ERROR("Image::generateFormattedImage - Image $format function not found.",E_USER_WARNING); } } } /** * Generate a resized copy of this image with the given width & height. * Use in templates with $ResizedImage. */ function generateResizedImage($gd, $width, $height) { if(is_numeric($gd) || !$gd){ USER_ERROR("Image::generateFormattedImage - generateResizedImage is being called by legacy code or gd is not set.",E_USER_WARNING); }else{ return $gd->resize($width, $height); } } /** * Generate a resized copy of this image with the given width & height, cropping to maintain aspect ratio. * Use in templates with $CroppedImage */ function generateCroppedImage($gd, $width, $height) { return $gd->croppedResize($width, $height); } /** * Remove all of the formatted cached images. * Should be called by any method that updates the current image. */ public function deleteFormattedImages() { if($this->Filename) { $numDeleted = 0; $methodNames = $this->allMethodNames(); $numDeleted = 0; foreach($methodNames as $methodName) { if(substr($methodName,0,8) == 'generate') { $format = substr($methodName,8); $cacheFile = $this->cacheFilename($format); if(Director::fileExists($cacheFile)) { unlink(Director::getAbsFile($cacheFile)); $numDeleted++; } } } return $numDeleted; } } /** * Get the dimensions of this Image. * @param string $dim If this is equal to "string", return the dimensions in string form, * if it is 0 return the height, if it is 1 return the width. * @return string|int */ function getDimensions($dim = "string") { if($this->getField('Filename')) { $imagefile = Director::baseFolder() . '/' . $this->getField('Filename'); if(file_exists($imagefile)) { $size = getimagesize($imagefile); return ($dim === "string") ? "$size[0]x$size[1]" : $size[$dim]; } else { return ($dim === "string") ? "file '$imagefile' not found" : null; } } } /** * Get the width of this image. * @return int */ function getWidth() { return $this->getDimensions(0); } /** * Get the height of this image. * @return int */ function getHeight() { return $this->getDimensions(1); } // ################### // DEPRECATED // ################### /** * @deprecated */ function URL() { return $this->getURL(); } } /** * A resized / processed {@link Image} object. * When Image object are processed or resized, a suitable Image_Cached object is returned, pointing to the * cached copy of the processed image. * @package sapphire * @subpackage filesystem */ class Image_Cached extends Image { /** * Create a new cached image. * @param string $filename The filename of the image. * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods. Singletons * don't have their defaults set. */ public function __construct($filename = null, $isSingleton = false) { parent::__construct(array(), $isSingleton); $this->Filename = $filename; } public function getRelativePath() { return $this->getField('Filename'); } // Prevent this from doing anything public function requireTable() { } public function debug() { return "Image_Cached object for $this->Filename"; } } /** * A db field type designed to help save images. * @deprecated Use a has_one relationship pointing to the file table instead. * @package sapphire * @subpackage filesystem */ class Image_Saver extends DBField { function saveInto($record) { $image = $record->getComponent($this->name); if(!$image) { $image = $record->createComponent($this->name); } if($image) { $image->loadUploaded($this->value); } else { user_error("ImageSaver::saveInto() Image field '$this->name' note found", E_USER_ERROR); } } function requireField() { return null; } } /** * Uploader support for the uploading anything which is a File or subclass of File, eg Image. * @package sapphire * @subpackage filesystem */ class Image_Uploader extends Controller { /** * Ensures the css is loaded for the iframe. */ function iframe() { if(!Permission::check('ADMIN')) Security::permissionFailure($this); Requirements::css("cms/css/Image_iframe.css"); return array(); } /** * Image object attached to this class. * @var Image */ protected $imageObj; /** * Associated parent object. * @var DataObject */ protected $linkedObj; /** * Finds the associated parent object from the urlParams. * @return DataObject */ function linkedObj() { if(!$this->linkedObj) { $this->linkedObj = DataObject::get_by_id($this->urlParams['Class'], $this->urlParams['ID']); if(!$this->linkedObj) { user_error("Data object '{$this->urlParams['Class']}.{$this->urlParams['ID']}' couldn't be found", E_USER_ERROR); } } return $this->linkedObj; } /** * Returns the Image object attached to this class. * @return Image */ function Image() { if(!$this->imageObj) { $funcName = $this->urlParams['Field']; $linked = $this->linkedObj(); $this->imageObj = $linked->obj($funcName); if(!$this->imageObj) {$this->imageObj = new Image(null);} } return $this->imageObj; } /** * Returns true if the file attachment is an image. * Otherwise, it's a file. * @return boolean */ function IsImage() { $className = $this->Image()->class; return $className == "Image" || is_subclass_of($className, "Image"); } function UseSimpleForm() { if(!$this->useSimpleForm) { $this->useSimpleForm = false; } return $this->useSimpleForm; } /** * Return a link to this uploader. * @return string */ function Link($action = null) { return $this->RelativeLink($action); } /** * Return the relative link to this uploader. * @return string */ function RelativeLink($action = null) { if(!$action) { $action = "index"; } return "images/$action/{$this->urlParams['Class']}/{$this->urlParams['ID']}/{$this->urlParams['Field']}"; } /** * Form to show the current image and allow you to upload another one. * @return Form */ function EditImageForm() { $isImage = $this->IsImage(); $type = $isImage ? _t('Controller.IMAGE', "Image") : _t('Controller.FILE', "File"); if($this->Image()->ID) { $title = sprintf( _t('ImageUploader.REPLACE', "Replace %s", PR_MEDIUM, 'Replace file/image'), $type ); $fromYourPC = _t('ImageUploader.ONEFROMCOMPUTER', "With one from your computer"); $fromTheDB = _t('ImageUplaoder.ONEFROMFILESTORE', "With one from the file store"); } else { $title = sprintf( _t('ImageUploader.ATTACH', "Attach %s", PR_MEDIUM, 'Attach image/file'), $type ); $fromYourPC = _t('ImageUploader.FROMCOMPUTER', "From your computer"); $fromTheDB = _t('ImageUploader.FROMFILESTORE', "From the file store"); } return new Form( $this, 'EditImageForm', new FieldSet( new HiddenField("Class", null, $this->urlParams['Class']), new HiddenField("ID", null, $this->urlParams['ID']), new HiddenField("Field", null, $this->urlParams['Field']), new HeaderField($title), new SelectionGroup("ImageSource", array( "new//$fromYourPC" => new FieldGroup("", new FileField("Upload","") ), "existing//$fromTheDB" => new FieldGroup("", new TreeDropdownField("ExistingFile", "","File") ) )) ), new FieldSet( new FormAction("save",$title) ) ); } /** * A simple version of the upload form. * @returns string */ function EditImageSimpleForm() { $isImage = $this->IsImage(); $type = $isImage ? _t('Controller.IMAGE') : _t('Controller.FILE'); if($this->Image()->ID) { $title = sprintf( _t('ImageUploader.REPLACE'), $type ); $fromYourPC = _t('ImageUploader.ONEFROMCOMPUTER'); } else { $title = sprintf( _t('ImageUploader.ATTACH'), $type ); $fromTheDB = _t('ImageUploader.ONEFROMFILESTORE'); } return new Form($this, 'EditImageSimpleForm', new FieldSet( new HiddenField("Class", null, $this->urlParams['Class']), new HiddenField("ID", null, $this->urlParams['ID']), new HiddenField("Field", null, $this->urlParams['Field']), new FileField("Upload","") ), new FieldSet( new FormAction("save",$title) )); } /** * A form to delete this image. * @return string */ function DeleteImageForm() { if($this->Image()->ID) { $isImage = $this->IsImage(); $type = $isImage ? _t('Controller.IMAGE') : _t('Controller.FILE'); $title = sprintf( _t('ImageUploader.DELETE', 'Delete %s', PR_MEDIUM, 'Delete file/image'), $type ); $form = new Form( $this, 'DeleteImageForm', new FieldSet( new HiddenField("Class", null, $this->urlParams['Class']), new HiddenField("ID", null, $this->urlParams['ID']), new HiddenField("Field", null, $this->urlParams['Field']) ), new FieldSet( $deleteAction = new ConfirmedFormAction( "delete", $title, sprintf(_t('ImageUploader.REALLYDELETE', "Do you really want to remove this %s?"), $type) ) ) ); $deleteAction->addExtraClass('delete'); return $form; } } /** * Save the data in this form. */ function save($data, $form) { if($data['ImageSource'] != 'existing' && $data['Upload']['size'] == 0) { // No image has been uploaded Director::redirectBack(); return; } $owner = DataObject::get_by_id($data['Class'], $data['ID']); $fieldName = $data['Field'] . 'ID'; if($data['ImageSource'] == 'existing') { if(!$data['ExistingFile']) { // No image has been selected Director::redirectBack(); return; } $owner->$fieldName = $data['ExistingFile']; // Edit the class name, if applicable $existingFile = DataObject::get_by_id("File", $data['ExistingFile']); $desiredClass = $owner->has_one($data['Field']); // Unless specifically asked, we don't want the user to be able // to select a folder if(is_a($existingFile, 'Folder') && $desiredClass != 'Folder') { Director::redirectBack(); return; } if(!is_a($existingFile, $desiredClass)) { $existingFile->ClassName = $desiredClass; $existingFile->write(); } } else { // TODO We need to replace this with a way to get the type of a field $imageClass = $owner->has_one($data['Field']); // If we can't find the relationship, assume its an Image. if( !$imageClass) { if(!is_subclass_of( $imageClass, 'Image' )){ $imageClass = 'Image'; } } // Assuming its a decendant of File $image = new $imageClass(); $image->loadUploaded($data['Upload']); $owner->$fieldName = $image->ID; // store the owner id with the uploaded image $member = Member::currentUser(); $image->OwnerID = $member->ID; $image->write(); } $owner->write(); Director::redirectBack(); } /** * Delete the image referenced by this form. */ function delete($data, $form) { $owner = DataObject::get_by_id( $data[ 'Class' ], $data[ 'ID' ] ); $fieldName = $data[ 'Field' ] . 'ID'; $owner->$fieldName = 0; $owner->write(); Director::redirect($this->Link('iframe')); } /** * Flush all of the generated images. */ function flush() { if(!Permission::check('ADMIN')) Security::permissionFailure($this); $images = DataObject::get("Image",""); $numItems = 0; $num = 0; foreach($images as $image) { $numDeleted = $image->deleteFormattedImages(); if($numDeleted) { $numItems++; } $num += $numDeleted; } echo $num . ' formatted images from ' . $numItems . ' items flushed'; } /** * Transfer all the content from the Image table into the File table. * @deprecated This function is only used to migrate content from old databases. */ function transferlegacycontent() { if(!Permission::check('ADMIN')) Security::permissionFailure($this); $images = DB::query("SELECT * FROM _obsolete_Image"); echo "