Attachments

By David Persson aka "davidpersson_"
Even though there are already good solutions out there this project tries -as usual- to make things even a bit better and to provide a documented, easy to setup and use, extensible, clean implementation of the functionality described in detail below. Plus: It should work and integrate well into the framework everyone of us loves so much.

Parts of the code is based upon or inspired by `Improved Upload Behavior` by Tane Piper, `ImageHelper` by Jon Bennet, `Attach This!` by Alex McFadyen and `Generic Upload Behavior` by Andy Dawson.

Features


  • Association of any number of files with any record of any model
  • Clean and documented code
  • Easy to install and setup
  • Transfer of files via HTTP, local file or from a HTML file form
  • Access attachments and versions via url
  • Creation and caching of file version (e.g. thumbnails, etc.) on the fly
  • Extendable to support other file types


Download


Get latest Attachments package


Install


The package basically contains an Attachmet behavior, an Attachment component, an Attachment element and "vendor"-libraries.

  1. Copy the files from the package to the appropriate folders of your cake app
  2. Run cake bake schema run schema run create attachments from the working directory of your app or init the table with the provided sql


Setup


Cache


Edit your config/core.php and setup an additional cache config:

Download code Cache::config('binary', array('engine' => 'File', 
                                'prefix' => 'binary_', 
                                'serialize' => false)
                         );


You may choose a different cache engine but be warned that this cache has got to chew big chunks of binary data (depends on the size of the attached files you are requesting).



Behavior


Add this to the $actsAs parameter of the model you'd like to attach stuff to:

Model Class:

Download code <?php 
var $actsAs = array('Attachment');
?>


No configuration options are required. See below on how to adjust the behavior.



Component


Open up the controller file corresponding to your model and add the Attachment component:

Controller Class:

Download code <?php 
var $components = array('...','...','Attachment');
?>


Element


Edit the add and edit views of the controller make sure the form type is set to file.

View Template:

Download code <div class="examples form">
    <?php echo $form->create('Example',array('type' => 'file'));?>
        ...



Still in the view add the statement for rendering the attachment element


View Template:

Download code         ...
         <?php echo $this->element('attachment');?>
    <?php echo $form->end('Submit');?>
</div>


The element supplies you with a basic listing of attached files and fields to add more files to the record.



Usage in views


Assuming you attached an image file named freekevin.jpg to the record of the Example model with the id 23.

NOTE: You've got to completely turn off debugging in order to make this work.


To render the image you could simply do:

View Template:

Download code echo $html->image('/examples/23/attachments/freekevin.jpg');


To render a resized version of the image within constraints of 400 width and 300 height:


View Template:

Download code echo $html->image('/examples/23/attachments/freekevin.jpg/400x300');


Instead of specifying the exact dimensions for images you may use these shortcuts:


  • tiny: 16x16
  • thumb: 100x100
  • medium: 300x300
  • large: 800x800
  • port: 1000x550


View Template:

Download code echo $html->image('/examples/23/attachments/freekevin.jpg/thumb');


Currently you can only generate version of file types that are supported by the GD extension.
For extending this feature you may have a look into the source code of vendors/XFile.php and vendors/XFile/XFileImageGd.php.



Adjusting the Behavior


You can customize how the file is going to be named and where it's stored by using special markers in the options.

The markers {DS},{APP},{WWW_ROOT} and {UNIQUE_ID} are valid for base, dirname and basename.
Additionally {BASENAME},{FILENAME} and {EXTENSION} as well as any other field that is submitted with your attachment (e.g. {GROUP}) can be used within basename.

  • base: Absolute path to base directory without trailing slash
  • dirname: Relative path without trailing slash
  • basename: Basename of the destination file


Checks are enforced onto a file being attached. All of these options are pretty self explanatory.
See the source of the behavior for the correct syntax and defaults.


  • allowMimetype
  • denyMimetype
  • allowExtension
  • denyExtension
  • allowPaths
  • maxSize


At least there are three more general options.


  • infoLevel: Controls the verbosity of the output on find
  • checksumAlgo
  • consistency: Whether file and record of an attachment should be deleted if an inconsistency is detected

NOTE: You may also add additional columns to the attachments table.



Find&Save Operations


Assuming you already attached files to records, a find() issued on the Example Mode would result in (depends on verbosity set for behavior and file type):

Download code Array
(
    [Example] => Array
        (
            [id] => 1
            [title] => Let Me Show You
            [created] => 2008-01-21 16:28:33
            [modified] => 2008-01-21 16:28:33
        )

    [ExampleAttachment] => Array
        (
            [0] => Array
                (
                    [id] => 1
                    [model] => Example
                    [foreign_key] => 1
                    [base] => /home/davidpersson/Workspace/project/webroot/
                    [dirname] => files/examples
                    [basename] => freekevin.jpg
                    [filename] => freekevin
                    [extension] => jpg
                    [checksum] => 9e496bcf9f601a7501b3efaf2b19da15
                     => 49160
                    [mimetype] => image/jpeg
                    [mediatype] => image
                    [width] => 640
                    [height] => 480
                    [ratio] => 4:3
                    [megapixel] => 0
                    [quality] => 0
                    [group] => demo
                    [created] => 2008-01-21 16:28:33
                    [modified] => 2008-01-21 16:28:33
                )
            ...

        )

)


If you'd like to attach a file directly to an existing record you would build:


Download code Array
(
    [Example] => Array
        (
            [id] => 1
        )

    [ExampleAttachment] => Array
        (
            [0] => Array
                (
                    [file] => /var/log/kern.log
                )
        )

)  


...then...


Controller Class:

Download code <?php $this->Example->save($data);?>


Of course the save operation above is going to fail because the file is not within allowed paths.
By default all files below the app's temp, webroot and the systems temp directory are considered to have valid locations.


NOTE: Supplying an id for the attachment would cause the attached file to be substituted by the new file.


NOTE: Supplying an delete which is set to true causes the record and file to be deleted permanently.



You could even attach a remote file to a record by setting the file field to
e.g. http://www.cakephp.org/img/cake-logo.png.
This would cause the remote file to be downloaded, saved to your local filesystem and then attached to the record.

Comments 608

CakePHP team comments Author comments

Comment

1 Great work

Great work, bud! I was about to use the upload behavior, but I'll try yours first. Looks quite powerful.
Thanks!

edit: couldn't make it work yet :(. I used the examples you provide, but nothing happened. I've been peaking in the code all afternoon, and found some things, but not sure how to handle them. Basically, I found the code never entered the conditional in line 365 in the the behavior file. Changed the "$attachment['file']" to "$attachment['name']['file']" in that line, and entered, but couldn't save yet... At least now I got a nice error (File was not accepted for transfer. Not going to save this record. Source file argument is invalid or file is of unknown type)...
I'll keep trying/seeing this and let you know. :)
posted Wed, Feb 20th 2008, 09:53 by Ramiro Araujo

Comment

2 Very good work

This is a great piece of code. Got so much features already built in. Hats off to the developers and all the contributors.

But there is a few things I would like to mention here on how to get it working. It took me a whole day just to figure it out.

The first part is the database. The script included in the download is missing a the auto increment extra. So you should change the file in config/sql/attachments.php so that the line with:

'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),


is changed to:

'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary', 'extra' => 'auto_increment'),


Note the auto increment part at the end. Once that is done you can create it with bake. The command is just (running from your current cake root directory):
cake/console/cake schema run create attachments

That should set up your database if all your configuration is right.

And then I find that I need to change the file models/behaviors/attachment.php. In there under the function setup I have to add:
        
        if(!is_array($config)){
            $config=$this->defaultSettings;
        }

Right at the beginning of the function so that there would be a default if there wasn't one passed by the user (And actually I don't have a clue how to pass one actually).

And then in the same file under the beforeSave function I have to add the following code:

foreach($attachment as $key=>$data){
    if(is_array($data)){
        foreach($data as $skey=>$sdata){
            $uh[$skey][$key]=$sdata;
        }
    }
}        
$attachment=$uh;


before line 365 just before the test:

if(isset($attachment['file'])) {


because it seems the arrangments of the array is wrong for the Transfer object. But apart from that it works pretty well. Thank you for this great work.
posted Thu, Feb 21st 2008, 02:19 by Abdullah Zainul Abidin

Comment

3 Still trying

heh, even with Abdullah fixes, im still fighting with this, but learning a lot while doing it.
Im stuck in a mime type detection problem... since the files are being saved in the temp folder as "randomname.tmp", the detection is not made thru the extension, and cant get it working. I'll try in a production lamp server asap, since im testing in windows. :)

posted Thu, Feb 21st 2008, 08:13 by Ramiro Araujo

Comment

4 Did you try turning mime type detection off

Can you print what error you actually got? Maybe we can work it out. I'm still new to CakePHP so I value the practice.

But one thing that I can think off the top of my head is that if you don't require mime type detection then why don't you just switch it off?

And about the error you got before

File was not accepted for transfer. Not going to save this record. Source file argument is invalid or file is of unknown type

With my fix you might still get it too. Because after the remapping of the array, some of the arrays would be empty and the program would try to copy an empty file. No biggie though because if you just turn off debugging and suppress the message it should still reroute you back to your index page.
And more thing I forgot to mention, but you'll only realize this after you got the upload working and editing it again to add another file. In the tutorial it doesn't mention that the element actually require the Number helper. So in your controller you should have the line:

var $helpers=array('Number');

posted Thu, Feb 21st 2008, 11:45 by Abdullah Zainul Abidin

Comment

5 Change with behavior in 1.2.r6469

This is just warning for those risking their lives with the latest commits from SVN (1.2.r6469).

Behaviors have changed. They are no longer accessed as arrays in the model, but as BehaviorsCollection Object.

So you need to change components/attachment.php, line 76 to:
    if(!isset($controller->{$controller->modelClass}->Behaviors->Attachment)) 


Hope this helps someone.
posted Fri, Feb 22nd 2008, 11:43 by Olivier B. Deland

Comment

6 Thank you for your comments

First of all thank you for your comments! I really appreciate it that you dived into the code to find the bugs/fixes.

I fixed the schema bug, the no config bug and also included the needed change suggested by Oliver B. Deland in 0.41. You can download the new version from cakeforge if you like. The Mimetype detection should work better in the new version as well because it won't rely exclusively onto the extension.

@Abdullah Zainul Abidin:
You can find a description on how to configure the behavior in the updated README or take a look on this message http://groups.google.com/group/cake-php/msg/dada642fda1ae7bf
I could not find any wrong arrangements of the array in the beforeSave(). If you encounter wrong arrangements on html form file uploads try to check out the latest cake version from SVN.

The File was not accepted for transfer. Not going to save this record. Source file argument is invalid or file is of unknown type Error is not that serious and I'm maybe going to remove the message in the next release.

The behavior will iterate over the submitted data and checks if it finds a file to upload if it doesn't it show this message.

So e.g. if you edit a record with files already attached to it and you don't change anything and then submit it you will see the above message. This only means that there is nothing to upload/update.

P.s.: I'm currently not able to make edits to this article. Hope it's going to be possible again soon...
posted Sat, Feb 23rd 2008, 18:57 by David Persson

Comment

7 can not get it to work

Attachments look very useful, but can not get it to work.
I get this error:


Attachment behavior is not loaded for Example model [APP/controllers/components/attachment.php, line 77]


I a working with the examples provided in the source and the latest beta: 1.2.0.6311

Any ideas?
posted Tue, Mar 18th 2008, 06:20 by mike stivaktakis

Comment

8 RE can not get it to work



Attachment behavior is not loaded for Example model [APP/controllers/components/attachment.php, line 77]


This is a warning thrown by the component if the model corresponding to the controller doesn't have the Attachment behavior in it's $actsAs array.

So let's say you have an Examples Controller with the Attachment Component loaded. Then you should also have an Example Model with the Attachment Behavior in it's $actsAs array.

hope that helps


posted Tue, Mar 18th 2008, 06:41 by David Persson

Comment

9 0.42

I just spotted that there was a single ticket describing the same problem as yours in the projects tracker queue.

Please download Attachments 0.42.

As long as cake 1.2 is not final the project is going to rely onto the latest cake version from svn.
posted Tue, Mar 18th 2008, 08:32 by David Persson

Comment

10 no luck

I downloaded attachments 0.42 and the latest cake from svn but still get the same problem.
posted Tue, Mar 18th 2008, 10:58 by mike stivaktakis

Comment

11 can not get it to work even with all the tips

Hi all, this compoment looks great, but I can't get it working. -:(
I encounter similar problems that others reported. First, the code never enter this line:

if(isset($attachment['file'])) {

because of the errors that Abdullah Zainul Abidin reported.
Debugging attachment show this:

[ExmapleAttachment] => Array
(
[47e2d69465f77] => Array
(
[name] => Array
(
[file] => xxx.jpg
)

[type] => Array
(
[file] => image/jpeg
)

[tmp_name] => Array
(
[file] => \xampplite\tmp\php237.tmp
)

[error] => Array
(
[file] => 0
)

=> Array
(
[file] => 22460
)

)

)

After implementing Abdullah fix (e.g then isset($attachment['file']) would eval to true), the code enters the loop, but then it complains about mimetype (e.g because it's \xampplite\tmpphp237.tmp) during setTemporary.
Even after I disable mimetype check, I got stuck during setDestination because of extension checking (e.g. c:\xxx.jpg) failed because it didn't started with 'file://'???

I am running on Windows, Firefox.
Using this line of code:

<?php echo $form->input($assocAlias.'.'.$id.'.file',array('label' => false,'type' => 'file'));?>

generating this line on the view:
<input id="ExampleAttachment47e2d69465f77File" type="file" value="" name="data[ExampleAttachment][47e2d69465f77][file]"/>

Anything suspicious? Does anyone get this componenet working? Please share your knowledge. Thanks.
posted Thu, Mar 20th 2008, 16:35 by Peter

Comment

12 RE can not get it to work even with all the tips


because of the errors that Abdullah Zainul Abidin reported.
Debugging attachment show this:

[ExmapleAttachment] => Array
(
[47e2d69465f77] => Array
(
[name] => Array
(
[file] => xxx.jpg
)

[type] => Array
(
[file] => image/jpeg
)

[tmp_name] => Array
(
[file] => \xampplite\tmp\php237.tmp
)

[error] => Array
(
[file] => 0
)

=> Array
(
[file] => 22460
)

)

)


This looks like the data send to the controller isn't parsed right. This was fixed (don't know the exact revision) in cake 1.2 svn. In 1.2 beta the problem was existent. So I would again suggest to update to latest cake version from svn.
After doing that the array should look like this:



[ExampleAttachment] => Array
        (
            [47e2d69465f77] => Array
                (
                    [name] => xxx.jpg
                    [type] => image/jpeg
                    [tmp_name] => \\xampplite\\tmp\\php237.tmp
                    [error] => 0
                     => 22460
                )

        )


By the way I dont't if it's just a typo you made in this text but I think it must be "ExampleAttachment" instead of "ExmapleAttachment"


After implementing Abdullah fix (e.g then isset($attachment['file']) would eval to true), the code enters the loop, but then it complains about mimetype (e.g because it's \xampplite\tmpphp237.tmp) during setTemporary.

Please also update to Attachments 0.42 this problem should already been fixed.


Even after I disable mimetype check, I got stuck during setDestination because of extension checking (e.g. c:\xxx.jpg) failed because it didn't started with 'file://'???


No your path is OK. You don't need to prepend "file://???" the support for this optional syntax may be removed in Attachments 0.50


I am running on Windows, Firefox.

The code isn't tested on windows ... yet ...


<?php echo $form->input($assocAlias.'.'.$id.'.file',array('label' => false,'type' => 'file'));?>
generating this line on the view:
<input id="ExampleAttachment47e2d69465f77File" type="file" value="" name="data[ExampleAttachment][47e2d69465f77][file]"/>

This is also related to your cake version.

posted Thu, Mar 20th 2008, 19:34 by David Persson

Bug

13 Some issues I have noticed so far

Some issues I've noticed so far trying out v0.42 on cake 1.2.6598 1.2.0.6613

I had to comment out lines 76-80 in the component as it was causing the app to timeout (possibly because modelClass isn't there, I believe it's modelNames (array))

I also had to change the php opening tag from the short tag format in the element on line 50 as I purposely don't enable short tag support to maximize code portability.

I had better luck debugging with die(debug($foo)) because of timeouts and file permissions that are probably due to my particular setup - but if anyone else is having timeouts and issues of this behaviour not working, try replacing trigger_errors with dies/debugs and adding dies/debugs before function return values where you think you might be having problems. I'm sure there's more elegant ways, but I did get this working finally for my app using .42 and 6613 on php 5.2.5 with apache 2.2.8, thanks David for all your efforts.
posted Wed, Mar 26th 2008, 07:31 by Tim

Comment

14 Attachment behavior is not loaded for Ad model

This is a warning thrown by the component if the model corresponding to the controller doesn't have the Attachment behavior in it's $actsAs array.

So let's say you have an Examples Controller with the Attachment Component loaded. Then you should also have an Example Model with the Attachment Behavior in it's $actsAs array.

hope that helps


I'm getting this error as well while I have 'Attachment' defined in both my components and and actsAs arrays.

posted Wed, Apr 2nd 2008, 14:11 by Matt Crowe

Comment

15 RE Attachment behavior is not loaded


I'm getting this error as well while I have 'Attachment' defined in both my components and and actsAs arrays.


The error will disappear when you download the nightly build and the 0.42 attachments. At least this worked for me.

Later, I got another error about PATHINFO_FILENAME but fixed it with upgrading php from 5.1.x to 5.2.x ( http://www.php.net/pathinfo )

I hope tihs will help you.

However, I still get an error about the temporary file 'has incorrect mimetype'.

As far as I can see, the error occurs in the

private function setTemporary($data,$error = null)

when making the call:

$this->check('tmp',array('extension')


Any help will be appreciated.
posted Wed, Apr 2nd 2008, 15:22 by mike stivaktakis

Bug

16 Loading times

When loading thumbs from cache, I get massive page loads. to fix this i changed the following in the component


} elseif($cached = Cache::read($cacheKey,$this->cacheConfig)) {
$content = $cached;



to


} elseif($cached = Cache::read($cacheKey,$this->cacheConfig)) {
echo $content = $cached;
exit;
posted Sat, Apr 26th 2008, 03:34 by Eelco Wiersma

Bug

17 Working on windows

Thank you for this great piece of code!

I'm new to cakephp (actually, this is my first comment on the bakery and all, so please be kind ^_^)
I had an hard time while getting this behaviour to work on windows;
i'll share my experience hoping it can help someone else in the same situation.

The test were made using Win XP and Wamp2.0 -> http://www.wampserver.com/en/
with Apache 2.2.8 - MySQL 5.0.51b - PHP 5.2.6

attm 0.42, cake core to the latest svn (6816), app beta 6311


I spent some time tracking down my fault (other than using Windows). And the fault was.. I was using Windows.
If you don't want to run every time into the "Could not determine temporary file type" error, you'll have to take into consideration some path issues.

First, in {APP}/models/behaviours/attachment.php:
edit the default settings property:

private $defaultSettings = array( [..]

adding the right path to the allowed paths key, i.e:

'allowPaths' => array(TMP,'/tmp/',WWW_ROOT,'C:\\wamp\\tmp\\', 'C:/wamp/tmp/' )


This will save you an headhache ^_^

check your php.ini for the actual value of tmp - the Transfer class (in {APP}/Vendors) will then able to get the right temp uploaded file.

Second - After that, you'll notice, there is another problem.
The isLocalFile method (and therefore the isUploadedLocalFile method too) always evaluates to false.
The problem lies -i think- in the
parse_url($file)
function used in conjunction with the windows filesystem.
$url['scheme'] shoul be set to "file" (or null) for a local file.

In my case (try pr($url['scheme'])..) it was set to "C", because the tmp file is something like "C:wamptmpphpB11.tmp" !!!
(the drive letter before the colon is interpreted as the scheme)

I didn't dig deeply into the issue; just came up with the following workaround.
$url['scheme'] may be set to any drive (A: trough Z:), so just accept any single char string ^_^:

(in yourAppName/vendors/transfer.php):

    private function isLocalFile($file)
    {
        if(!is_string($file)) {
            //pr("filename is not a string". $file);
            return false;
        }
        
        $url = parse_url($file);
        //pr("url " . $url . "<br>". $file);
        
        if(isset($url['scheme']) && $url['scheme'] == 'file') {
            $file = $file = substr($file,7);
            //pr("scheme = file (substr 7)" . $file);

        // CHANGE THIS /////////////////
        //} elseif(!empty($url['scheme'])) {
        
        // TO:        //////////////
        } elseif(!empty($url['scheme']) && strlen($url['scheme']) != 1) {
            
            //pr("url scheme " . $url['scheme'] );
            //pr("scheme not = file " . $file);
            return false;
        }
         
        if(is_file($file) && is_executable($file)) {
            $this->errors[] = 'File is a local file but executable bit is set.';
            //pr('is executable'. $this->errors);
            return false;
        }
        
        return true;
    }


You're almost done. The temp files now pass the checks on windows too :)
But there is anoter problem

Third - make sure the PECL file_info extension is properly installed.
AND bug free..
see:
http://pecl.php.net/bugs/bug.php?id=7555


It's possible to set the second parameter of the finfo_open function, i.e.

finfo_open(FILEINFO_MIME,"C:/wamp/php/extras/magic"

for every occurrence in {APP}/vendors/mimetype.php,
but you don't want to have a class that would'nt work when you commit to the (probabably linux) production server, don't you?

THE solution that worked for me is there:
http://www.php.net/manual/en/fileinfo.installation.php


Note:
it seems that the php.ini setting

mime_magic.magicfile = "C:wampbinphpextrasmagic.mime"

does not work on windows!

You HAVE TO set a (system) ENVIRONEMENT VARIABLE called MAGIC -
set it to
C:wampbinphpextrasmagic
if the main magic database file is
C:wampbinphpextrasmagic.mime

Restart the system (yes, we are on windows)
And you're done!


---
Other


You may also need/want to bypass the mimetype check, by setting the second parameter ($skip) to
array([..], 'mimetype')
on every call of the $Transfer->check method.
I.e.:

if(!$this->check('tmp',array('extension','mimetype'))) {
                return false;
            } 



I had another problem with the security component; if included it gave me errors while updating a record with more than one attached file. (maybe this is not a windows only issue)


Finally, I think it's a good practice to set the configuration for the behaviour (see the examples provided with the behaviout)


'Attachment' => array(
            'base' => '{APP}webroot',
            'dirname' => 'files/organizations',
            'filename' => '{BASENAME}',
            'overwriteExisting' => true,
            'maxSize' => '8M',
        ),


Thanks again David,
and to windows users, happy baking (with attachments)!
posted Tue, May 13th 2008, 09:28 by Stefano Manfredini

Login to Submit a Comment