I am writing an action in a controller where in a certain case, I want to output raw image data directly, and want to set the header content-type appropriate. However I think the header is already being set earlier by CakePHP (I am setting render to be false).
Is there a way to get around this? Thanks!
As said before, CakePHP does not send headers when render is false. Beware though, that any code doing an 'echo' will send headers (except you are using output-buffering). This includes messages from PHP (warnings etc.).
Sending the file can be done in numerous ways, but there are two basic ways:
Send the file using plain PHP
function send_file_using_plain_php($filename) {
// Avoids hard to understand error-messages
if (!file_exists($filename)) {
throw RuntimeException("File $filename not found");
}
$fileinfo = new finfo(FILEINFO_MIME);
$mime_type = $fileinfo->file($filename);
// The function above also returns the charset, if you don't want that:
$mime_type = reset(explode(";", $mime_type));
// gets last element of an array
header("Content-Type: $mime_type");
header("Content-Length: ".filesize($filename));
readfile($filename);
}
Use X-Sendfile and have the Webserver serve the file
// This was only tested with nginx
function send_file_using_x_sendfile($filename) {
// Avoids hard to understand error-messages
if (!file_exists($filename)) {
throw RuntimeException("File $filename not found");
}
$fileinfo = new finfo(FILEINFO_MIME);
$mime_type = $fileinfo->file($filename);
// The function above also returns the charset, if you don't want that:
$mime_type = reset(explode(";", $mime_type));
// gets last element of an array
header("Content-Type: $mime_type");
// The slash makes it absolute (to the document root of your server)
// For apache and lighttp use:
header("X-Sendfile: /$filename");
// or for nginx: header("X-Accel-Redirect: /$filename");
}
The first function occupies one PHP-process / thread while the data is being send and supports no Range-Requests or other advanced HTTP-features. This should therefore only be used with small files, or on very small sites.
Using X-Sendfile you get all that, but you need to know which webserver is running and maybe even a change to the configuration is needed. Especially when using lighttp or nginx this really pays off performance-wise, because these webservers are extremly good at serving static files from disk.
Both functions support files not in the document-root of the webserver. In nginx there are so called "internal locations" (http://wiki.nginx.org/HttpCoreModule#internal). These can be used with the X-Accel-Redirect-Header. Even rate-throtteling is possible, have a look at http://wiki.nginx.org/XSendfile.
If you use apache, there is mod_xsendfile, which implements the feature needed by the second function.
It's not $this->render(false), it's $this->autoRender=false; The header is not sent in the controller action unless you echo something out.
If render is false, cake will not send a header.
You can rely on plain ol' php here.
PNG:
header('Content-Type: image/gif');
readfile('path/to/myimage.gif');
JPEG:
header('Content-Type: image/jpeg');
readfile('path/to/myimage.jpg');
PNG:
header('Content-Type: image/png');
readfile('path/to/myimage.png');
Related
I am working on a project where I need to send back 302 reply. Everything seems to work, except I can't remove certain headers, i.e. From, Contact, etc. (I don't want to remove them completely, but rather substitute with my own version of it). I use KEMI with Lua to do so:
KSR.hdr.remove("From")
As I mentioned, this does not work (while other functions from hdr work fine in the same context, namely KSR.hdr.append_to_reply(...).
I decided to look at the Kamailio source code and found following lines of code in kemi.c file:
int sr_kemi_hdr_remove(sip_msg_t *msg, str *hname)
{
...
anchor=del_lump(msg, hf->name.s - msg->buf, hf->len, 0);
if (anchor==0) {
LM_ERR("cannot remove hdr %.*s\n", hname->len, hname->s);
return -1;
}
}
return 1;
}
Looking at the last parameter that del_lump takes, it is of type _hdr_types_t which describes an enum of different header types. Now, in particular to me, there were three headers I was working with:
From (type 4)
Contact (type 7)
Other (type 0)
So my question is, why does that function is hardcoded to take only OTHER headers, but not other ones, i.e. From and Contact? Is that to safeguard from breaking the SIP request (inadvertently removing required headers)?
And as a follow up question, is it even possible to remove From and Contact from reply messages?
I assume the 302 is generated by Kamailio, then several headers are copied from the incoming request, like From, To, Call-Id, CSeq. Therefore if you want a different From in the generated reply, change it in the request and then do msg_apply_changes().
Contact headers for redirect (3xx) replies are generated from the destination set of the request (modified R-URI and branches that can be created by append_branch(), lookup("location") etc.).
More headers can be added to the generated replies using append_to_reply().
Note that I gave the name of the functions for the native kamailio.cfg, but you can find them exported to Kemi as well (by core or textops, textopsx modules).
When uploading to GCS (Google Cloud Storage) using the BlobStore's createUploadURL function, I can provide a callback together with header data that will be POSTed to the callback URL.
There doesn't seem to be a way to do that with GCS's signed URL's
I know there is Object Change Notification but that won't allow the user to provide upload specific information in the header of a POST, the way it is possible with createUploadURL's callback.
My feeling is, if createUploadURL can do it, there must be a way to do it with signed URL's, but I can't find any documentation on it. I was wondering if anyone may know how createUploadURL achieves that callback calling behavior.
PS: I'm trying to move away from createUploadURL because of the __BlobInfo__ entities it creates, which for my specific use case I do not need, and somehow seem to be indelible and are wasting storage space.
Update: It worked! Here is how:
Short Answer: It cannot be done with PUT, but can be done with POST
Long Answer:
If you look at the signed-URL page, in front of HTTP_Verb, under Description, there is a subtle note that this page is only relevant to GET, HEAD, PUT, and DELETE, but POST is a completely different game. I had missed this, but it turned out to be very important.
There is a whole page of HTTP Headers that does not list an important header that can be used with POST; that header is success_action_redirect, as voscausa correctly answered.
In the POST page Google "strongly recommends" using PUT, unless dealing with form data. However, POST has a few nice features that PUT does not have. They may worry that POST gives us too many strings to hang ourselves with.
But I'd say it is totally worth dropping createUploadURL, and writing your own code to redirect to a callback. Here is how:
Code:
If you are working in Python voscausa's code is very helpful.
I'm using apejs to write javascript in a Java app, so my code looks like this:
var exp = new Date()
exp.setTime(exp.getTime() + 1000 * 60 * 100); //100 minutes
json['GoogleAccessId'] = String(appIdentity.getServiceAccountName())
json['key'] = keyGenerator()
json['bucket'] = bucket
json['Expires'] = exp.toISOString();
json['success_action_redirect'] = "https://" + request.getServerName() + "/test2/";
json['uri'] = 'https://' + bucket + '.storage.googleapis.com/';
var policy = {'expiration': json.Expires
, 'conditions': [
["starts-with", "$key", json.key],
{'Expires': json.Expires},
{'bucket': json.bucket},
{"success_action_redirect": json.success_action_redirect}
]
};
var plain = StringToBytes(JSON.stringify(policy))
json['policy'] = String(Base64.encodeBase64String(plain))
var result = appIdentity.signForApp(Base64.encodeBase64(plain, false));
json['signature'] = String(Base64.encodeBase64String(result.getSignature()))
The code above first provides the relevant fields.
Then creates a policy object. Then it stringify's the object and converts it into a byte array (you can use .getBytes in Java. I had to write a function for javascript).
A base64 encoded version of this array, populates the policy field.
Then it is signed using the appidentity package. Finally the signature is base64 encoded, and we are done.
On the client side, all members of the json object will be added to the Form, except the uri which is the form's address.
var formData = new FormData(document.forms.namedItem('upload'));
var blob = new Blob([thedata], {type: 'application/json'})
var keys = ['GoogleAccessId', 'key', 'bucket', 'Expires', 'success_action_redirect', 'policy', 'signature']
for(field in keys)
formData.append(keys[field], url[keys[field]])
formData.append('file', blob)
var rest = new XMLHttpRequest();
rest.open('POST', url.uri)
rest.onload = callback_function
rest.send(formData)
If you do not provide a redirect, the response status will be 204 for success. But if you do redirect, the status will be 200. If you got 403 or 400 something about the signature or policy maybe wrong. Look at the responseText. If is often helpful.
A few things to note:
Both POST and PUT have a signature field, but these mean slightly different things. In case of POST, this is a signature of the policy.
PUT has a baseurl which contains the key (object name), but the URL used for POST may only include bucket name
PUT requires expiration as seconds from UNIX epoch, but POST wants it as an ISO string.
A PUT signature should be URL encoded (Java: by wrapping it with a URLEncoder.encode call). But for POST, Base64 encoding suffices.
By extension, for POST do Base64.encodeBase64String(result.getSignature()), and do not use the Base64.encodeBase64URLSafeString function
You cannot pass extra headers with the POST; only those listed in the POST page are allowed.
If you provide a URL for success_action_redirect, it will receive a GET with the key, bucket and eTag.
The other benefit of using POST is you can provide size limits. With PUT however, if a file breached your size restriction, you can only delete it after it was fully uploaded, even if it is multiple-tera-bytes.
What is wrong with createUploadURL?
The method above is a manual createUploadURL.
But:
You don't get those __BlobInfo__ objects which create many indexes and are indelible. This irritates me as it wastes a lot of space (which reminds me of a separate issue: issue 4231. Please go give it a star)
You can provide your own object name, which helps create folders in your bucket.
You can provide different expiration dates for each link.
For the very very few javascript app-engineers:
function StringToBytes(sz) {
map = function(x) {return x.charCodeAt(0)}
return sz.split('').map(map)
}
You can include succes_action_redirect in a policy document when you use GCS post object.
Docs here: Docs: https://cloud.google.com/storage/docs/xml-api/post-object
Python example here: https://github.com/voscausa/appengine-gcs-upload
Example callback result:
def ok(self):
""" GCS upload success callback """
logging.debug('GCS upload result : %s' % self.request.query_string)
bucket = self.request.get('bucket', default_value='')
key = self.request.get('key', default_value='')
key_parts = key.rsplit('/', 1)
folder = key_parts[0] if len(key_parts) > 1 else None
A solution I am using is to turn on Object Changed Notifications. Any time an object is added, a Post is sent to a URL - in my case - a servlet in my project.
In the doPost() I get all info of objected added to GCS and from there, I can do whatever.
This worked great in my App Engine project.
Found this:
http://stackoverflow.com/questions/7198124/setting-the-header-to-be-content-image-in-cakephp
but either not understanding, or its not working for me.
Basically want to record 'opens' for emails. Currently it "works" but in gmail it shows that an image is not being displayed--so I want to return an actual image header. I've tried doing:
$this->layout=false;
$this->response->type('jpg');
In the Controller for Opens, but that is not working.
Web Sniffer (http://web-sniffer.net/), is showing a jpeg response, but still have a blank no file found image. How can I fix?
[edit]
Thinking this:
http://stackoverflow.com/questions/900207/return-a-php-page-as-an-image
and this:
http://book.cakephp.org/2.0/en/controllers/request-response.html
might be solution
Serve a real image
If you only send the headers for an image, but don't send the image content - it will be considered a broken image. To send a file refer to the documentation for whichever version of CakePHP you are using. For example in 2.3+:
public function opened() {
...
$this->response->file('/path/to/1x1.gif');
return $this->response;
}
Pretty sure this worked:
$name = './img/open.jpg';
$fp = fopen($name, 'rb');
$this->response->header("Content-Type: image/jpg");
$this->response->header("Content-Length: " . filesize($name));
fpassthru($fp);
Where open.jpg is a 1x1 real pixel image in cakephp's /img directory.
Would love if someone else could confirm?
Getting:
����JFIF``��C $.' ",#(7),01444'9=82<.342��C 2!!22222222222222222222222222222222222222222222222222��"����������?����
Going to manually (so I'm guessing thats a "real" image file?). No longer getting gmails no image file icon.
nvermind--websniff says this is a html request. will update when I figure out.
[edit]
think this might be correct way:
$this->response->type('jpg');
$this->response->file('./img/open.jpg');
just tested, and definitely getting a 1x1 pixel download. No jibberish like above.
Gmails caching proxy
Normally serving a real image as suggest by AD7Six would be the way to go, however with Gmails caching proxy in place you may run into problems when just serving an image.
http://www.emailmarketingtipps.de/2013/12/07/gmails-image-caching-affects-email-marketing-heal-opens-tracking/
The problem is/was that the image has been cached, and the proxy wouldn't request it a second time, making tracking of opens unreliable.
Content length to the rescue
Until recently the workaround for this has been to respond with a content length of 0 (and private cache control which seems to be necessary for some other webmail providers):
// ...
$this->response->header(array
(
'Cache-Control' => 'private',
'Content-Length' => 0
));
$this->response->type('gif');
return $this->response;
This would return a response with no content body, which is treated as broken, however not all clients did actually show a broken image symbol, still it was recommended to hide the image using styles on the img tag.
Return of cache control
However it has been reported that Gmail made some changes recently so that sending a no-cache header is now being respected again.
http://blog.movableink.com/real-time-content-and-re-open-tracking-return-to-gmail/
So in addition to AD7Six example an appropriate Cache-Control header might now do the trick
// ...
$this->response->header(array
(
'Cache-Control' => 'no-cache, max-age=0'
));
$this->response->file('/path/to/1x1.gif');
return $this->response;
I'm on a shared host and ini_set function is disabled for security reasons. I'm trying to deploy CakePHP 2.4.1 on this host. Fresh cake installation results in a blank page, with no errors shown, instead if I comment these lines:
\lib\Cake\Model\Datasource\CakeSession.php
if (empty($_SESSION)) {
if (!empty($sessionConfig['ini']) && is_array($sessionConfig['ini'])) {
foreach ($sessionConfig['ini'] as $setting => $value) {
if (ini_set($setting, $value) === false) {
throw new CakeSessionException(__d('cake_dev', 'Unable to configure the session, setting %s failed.', $setting));
}
}
}
}
Everything seems to works fine. Now, I'm asking what is the downside of keeping that snippets commented (in other word, what is that code responsible for)?
As the exception message, the method name and the rest of the code indicates, it configures the session settings, session name, cookie lifetime, save handler, etc...
Your code may run fine, and you should be able to use the PHP session_*() functions instead to configure the settings (the best place for that would probably your bootstrap.php). Also writing a dummy value into $_SESSION seems to prevent the CakeSession::_configureSession() to use ini_set(), so you don't have to modify it.
So this might work, but it shouldn't be necessary to jump through such hoops. There's no need to disable ini_set() in a properly set up shared hosting environment, and personally I'd change the hoster in case they are unable to change this behaviour.
Joomla has a feature where it loads the a minified javascript file and the uncompressed version when the site is in debug mode.
I have named both my files correctly and am include it as follows:
JHtml::_('script', JUri::root() . 'path_to_file/jquery-sortable.js');
When I put the site in debug mode, it does not load the uncompressed file.
However, If I use the following instead, it works fine:
JHtml::_('script', 'path_to_file/jquery-sortable.js');
Now I'm not sure whether this is a bug in Joomla or not, but I cannot find any information online regarding this. I would like to use JURI::root() in the path.
Does anyone have any information on this?
Indeed, if the script URL begins with http, the code that is responsible for including the uncompressed version (i.e, remove the min. segment if such exists or add -uncompressed otherwise) is ignored.
The source for this behavior:
JHtml::includeRelativeFiles() in libraries/cms/html/html.php:298
protected static function includeRelativeFiles($folder, $file, $relative, $detect_browser, $detect_debug)
{
// If http is present in filename
if (strpos($file, 'http') === 0)
{
$includes = array($file);
}
else
//process the script sourch.
}
...
}
Most of the script files, including frameworks, are included as relative paths. I guess that this behavior is meant to prevent remote resources from getting 404ed.