I have the following code that sends my email:
/**
* #param array $to
* #param string $subject
* #param array $vars
* #param string $template
* #param array $from
*/
public function sendEmail(array $to, $subject, array $vars, $template = 'default', array $from = ['dev#example.com' => 'Online'])
{
$transport = 'default';
if (Configure::read('debug')) {
$transport = 'dev';
}
$mailer = new Email($transport);
if ($this->isCommandLineInterface()) {
$mailer->setDomain('http://local.peepznew.com');
}
$this->addRecipients($mailer, $to);
$mailer->setFrom($from);
$mailer->setSubject($subject);
$mailer->setTemplate($template);
if (isset($vars['preheaderText']) === false) {
$vars['preheaderText'] = '';
}
$vars['subject'] = $subject;
$mailer->setViewVars($vars);
$mailer->setEmailFormat('both');
$mailer->send();
}
This code is called from the web interface as well as from the command line. After struggling to get the full url to display in messages sent from the command line, I read the docs and came across this:
Which is why I'm doing the setDomain call. I run my code again, and it still doesn't have full Urls. So I created the exact same function in both the web interface and cli, that looks like this:
$this->sendEmail(
['my.email#example.com'],
'Test Email',
[
'title' => 'We need to select another peep',
'showFooterLinks' => true,
]
);
die;
The default template looks like so (it literally only has this one line in it):
echo $this->Html->link('test link', ['controller' => 'jobs', 'action' => 'select_staff', 1, '_full' => true]);
The emails from the web interface, using the code above, sends perfect. Full URLs and everything. However from the cli, it just sends /jobs/select_staff/1.
Why is this and how do I fix it?
Read the docs closely, they say that the domain set via setDomain() is being used when generating message IDs, ie it's being used in an E-Mail header.
Generating links is something completely different, and is affected by the App.fullBaseUrl configuration option, which is by default derived from env('HTTP_HOST') in your applications config/bootstrap.php, unless already configured in config/app.php.
It's also possible to configure the base URL separately for the CLI environment in your config/bootstrap_cli.php file, there should already be a commented snippet for doing so that looks like this:
// Set the fullBaseUrl to allow URLs to be generated in shell tasks.
// This is useful when sending email from shells.
//Configure::write('App.fullBaseUrl', php_uname('n'));
See also
Cookbook > Console Tools, Shells & Tasks > Routing in the Console Environment
Source > cakephp/app > config/bootstrap.php
Source > cakephp/app > config/bootstrap_cli.php
Source > cakephp/app > config/app.php
Related
I'm working on my cakephp project
and I am currently upgrading from 3.3.16 to 3.4.0
The project uses the cakephp-upload plugin to save an image.
The Upload plugin needs an existing entity to attach a file to it. A modification of the request is done to grab the avatar, before unsetting it to save the user.
I know this is not a good practice to modify a request, but the code was made this way.
With immutable objects in version 3.4.0, it is just not possible anymore. But i dont know how to do it properly.
Here is the error message given by my unit-test,
ran by vendor/bin/phpunit --filter testAdd tests/TestCase/Controller/Api/V1/UsersControllerTest.php:
There was 1 failure:
1) App\Test\TestCase\Controller\Api\V1\UsersControllerTest::testAdd
Failed asserting that file "/home/comptoir/Comptoir-srv/webroot/img/files/Users/photo/5/avatar/correctAvatarLogo.jpg" exists.
/home/comptoir/Comptoir-srv/tests/TestCase/Controller/Api/V1/UsersControllerTest.php:208
Here is the actual code:
public function add()
{
if (!empty($this->request->data)) {
$user = $this->Users->newEntity($this->request->data);
} else {
$user = $this->Users->newEntity();
}
$message = "";
// Get the avatar before unset it to save the user.
// The Upload plugin need an existing entity to attach a file to it.
if ($this->request->is('post')) {
if (isset($this->request->data['photo']) && !$user->errors()) {
$avatar = $this->request->data['photo'];
$this->request->data['photo'] = "";
}
$user = $this->Users->patchEntity($user, $this->request->data);
if ($this->Users->save($user)) {
$user = $this->Users->get($user->id, ['contain' => []]);
isset($avatar) ? $this->request->data['photo'] = $avatar : null;
$user = $this->Users->patchEntity($user, $this->request->data);
if ($this->Users->save($user)) {
$message = "Success";
$this->Flash->success(__d("Forms", "Your are registred on the Comptoir du Libre, welcome !"));
if (!$this->request->is('json')) {
$this->Auth->setUser($this->Auth->identify());
$this->redirect([
"prefix" => false,
"controller" => "Pages",
"language" => $this->request->param("language")
]);
}
} else {
$message = "Error";
}
} else {
$message = "Error";
$this->Flash->error(__d("Forms", "Your registration failed, please follow rules in red."));
}
$message == "Error" ? $this->set('errors', $user->errors()) : null;
}
$this->ValidationRules->config('tableRegistry', "Users");
$rules = $this->ValidationRules->get();
$userTypes = $this->Users->UserTypes->find('list', ['limit' => 200]);
$this->set(compact('user', 'userTypes', 'rules', 'message'));
$this->set('_serialize', ['user', 'userTypes', 'rules', 'message', 'errors']);
}
Does anyone know how to do that respecting the immutable rule ?
Your premise is wrong.
The Upload plugin needs an existing entity to attach a file to it
That's actually not correct, uploading files alongside creating new records works fine. There's no need for this stuff in your controller, it should be possible to handle this with a single basic save, ie you should investigate the problem that you're having with that, and fix it.
However looking at your test, it should fail anyways, because the file data that you're passing is invalid, it's neither an actual uploaded file for which is_uploaded_file() would return true, nor is it acceptable for user data to be able to define the temporary file path, and the error code, ie you're not properly validating the data if that test passes as is. Accepting such data is a security vulnerability, it could allow all sorts of attacks, from path traversal to arbitrary file injections!
Ideally your whole upload validation and writing functionality would support \Psr\Http\Message\UploadedFileInterface objects, that would allow for very simply testing by being able to pass instances of that class into the test data, that might be something worth suggesting for the plugin. Without such functionality, your second best bet would probably be something like modifying the table's validation rules before issuing the test request, so that is_uploaded_file() is being skipped, or you're switching to integration tests over HTTP, instead of the simulation in CakePHP.
How can I test a file upload function with a controller test case in CakePHP 3?
I keep running into the problem that PHP thinks the file was not actually uploaded. The validation rules that works for a browser test, but not for a test case:
->add('file', [
'is_uploaded_file' => [
'rule' => ['uploadedFile', ['optional' => false]],
'message' => 'File is no valid uploaded file'
],
I quickly found out that the is_uploaded_file and move_uploaded_file are impossible to fool in a unit test.
However, most topics on this are old and/or not about CakePHP specifically, so I figured I'd post a new question.
You don't necessarily need to modify validation rules, what you can do alternatively is using an object that implements \Psr\Http\Message\UploadedFileInterface. CakePHP's default uploaded file validation supports such objects.
CakePHP requires zendframework/zend-diactoros, so you can use \Zend\Diactoros\UploadedFile and do something like this in your tests:
$data = [
// ...
'file' => new \Zend\Diactoros\UploadedFile([
'/path/to/the/temporary/file/on/disk',
1234, // filesize in bytes
\UPLOAD_ERR_OK, // upload (error) status
'filename.jpg', // upload filename
'image/jpeg' // upload mime type
])
];
The uploadedFile rule will automatically treat such an object as an uploaded file.
Of course your code that handles the file upload must support that interface too, but it's not that complicated, you just need to make sure that the regular file upload arrays are being converted into UploadedFileInterface implementations so that your upload handler can make that a requirement.
It could of course be done in the upload handler itself, so that validation will use regular file upload arrays as well as UploadedFile objects. Another way would be to convert them earlier when creating entities, using the beforeMarshal handler/event, something along the lines of this:
public function beforeMarshal(\Cake\Event\Event $event, \ArrayObject $data, \ArrayObject $options)
{
$file = \Cake\Utility\Hash::get($data, 'file');
if ($file === null) {
return;
}
if (!($file instanceof \Psr\Http\Message\UploadedFileInterface)) {
if (!is_uploaded_file(\Cake\Utility\Hash::get($file, 'tmp_name'))) {
$file = new \Zend\Diactoros\UploadedFile(
null,
0,
UPLOAD_ERR_NO_FILE,
null,
null
);
} else {
$file = new \Zend\Diactoros\UploadedFile(
\Cake\Utility\Hash::get($file, 'tmp_name'),
\Cake\Utility\Hash::get($file, 'size'),
\Cake\Utility\Hash::get($file, 'error'),
\Cake\Utility\Hash::get($file, 'name'),
\Cake\Utility\Hash::get($file, 'type')
);
}
$data['file'] = $file;
}
}
This will convert the data into an UploadedFile object in case it's an actually uploaded file. This extra check is added because CakePHP's behavior of merging file data with the POST data, making it impossible (unless one can access the request object, or the $_FILES superglobal) to determine whether a user posted that data, or whether PHP generated that data for an actual file upload.
If you then use \Psr\Http\Message\UploadedFileInterface::moveTo() to move the file, it will work in SAPI (browser based) as well as non-SAPI (CLI) environments:
try {
$file->moveTo($targetPath);
} catch (\Exception $exception) {
$entity->setError(
'file', [__('The file could not be moved to its destination.')]
);
}
See also
PSR-7: HTTP message interfaces > Uploaded Files
API > \Cake\Validatiom\Validation::uploadedFile()
I actually figured it out almost immediately after I'd posted.
The solution is based on https://pierrerambaud.com/blog/php/2012-12-29-testing-upload-file-with-php
So the only way to get around the problem is overriding both the built-in functions: is_uploaded_file and move_uploaded_file.
The uploadedFile validation rule lives inside Cake\Validation, and I'm using the move function in a table event, so inside App\Model\Table.
I added the following to the top of the controller test case:
<?php
namespace Cake\Validation;
function is_uploaded_file($filename)
{
return true;
}
namespace App\Model\Table;
function move_uploaded_file($filename, $destination)
{
return copy($filename, $destination);
}
namespace App\Test\TestCase\Controller;
use App\Controller\CarsController;
use Cake\TestSuite\IntegrationTestTrait;
use Cake\TestSuite\TestCase;
use Cake\Core\Configure;
/**
* App\Controller\CarsController Test Case
*/
class CarsControllerTest extends BaseTestCase
{
use IntegrationTestTrait;
// ...
And it works!
I'm trying to play mp3 files from server-side to client-side. Where the client access the server passing some ID and the server return the file.
Right now, how this is working?
Well, using Laravel (server-side) and AngularJS (client-side) on distinct urls, i'm able to play the song.
But, if I get the request response I'm able to download the song.
So, what would be the good way to work so that information wouldn't be visible to the user?
I would write some sort of file proxy.
You have to move your files out of the publicly accessible area. F.a one level above the page root. So it is not possible to get the data directly.
Then you need a server side script, that gets the data and returns it with the headers you need.
Here is an example (plain PHP):
/**
* #param string $file_name
* #param string $mime
* #param bool $download
*/
public function fileProxyAction($file_name, $mime, $download = false) {
if(basename($file_name) != $file_name) return 'Filename not valid!';
$path = '... your path goes here';
$file = $path.$file_name;
if (!(file_exists($file) && is_readable($file))) return 'The file "'.$file_name.'" could not be found!';
ob_clean();
if($download === false) {
header('Content-type: '.$mime);
header('Content-length: '.filesize($file));
$open = # fopen($file, 'rb');
if ($open) {
fpassthru($open);
exit;
}
} else {
// download
$path_parts = pathinfo($file);
header("Content-Disposition: attachment; filename=\"".$path_parts["basename"]."\"");
header("Content-type: application/octet-stream");
header("Content-length: " . filesize($file));
header("Content-Disposition: filename=\"".$path_parts["basename"]."\"");
header("Cache-control: private"); // open files directly
readfile($file);
die;
}
}
Laravel has an excellent Built-In-Filesystem. Check it out. I'm sure you can optimize my method with it.
EDIT
If you need to check a token or something, you shouldn't call the fileProxyAction directly by the router. Instead let your router call a Method which checks the token or what ever you're using ;)
Example (pseudo code):
Route::get('/mp3/{id}/{token}', function($id, $token) {
if($token !== Session::get('token')) return App::abort(401);
$name = Mp3::findOrFail($id)->name;
$mime = Mp3::findOrFail($id)->mime;
return $this->fileProxyAction($name, $mime);
});
I'm using CakePHP's Media view to force file downloads. My code is pretty much exactly like the example provided in the cookbook, which I'll paste here for your convenience:
<?php
class ExampleController extends AppController {
public function download () {
$this->viewClass = 'Media';
// Download app/outside_webroot_dir/example.zip
$params = array(
'id' => 'example.zip',
'name' => 'example',
'download' => true,
'extension' => 'zip',
'path' => APP . 'outside_webroot_dir' . DS
);
$this->set($params);
}
}
In the database, I have a field that keeps track of how many times the file was downloaded. I'm looking for a way to make sure that this number is as accurate as possible, so if a user's download gets cancelled or times out, the number does not increment. Is there some way for CakePHP's Media view to report that the download was, indeed, successful?
Detecting when a file has finished downloading is no easy task. This is something that would be done on the client side with javascript, but browsers do not give you any hooks for that.
There is a pretty clever solution here (setting a cookie and then looking for it with javascript), but it only tells you when the download has started.
I have a site where the directories are set up like this
public_html/framework/cake
public_html/framework/app
public_html/index.php
public_html/contact.php
public_html/aboutus.php
Is there any way to get variables or model data from public_html/framework/app when a user navigates to public_html/aboutus.php?
I would recommend reading the HttpSocket documentation.
An example implementation would look similar to:
/**
* import HttpSocket class
*/
App::import('Core', 'HttpSocket');
/**
* instantiate and make a POST request to http://localhost/contact.php
* sending var1 => test
*/
$HttpSocket = new HttpSocket();
$HttpSocket->post('http://localhost/contact.php', array(
array('var1' => 'test')
));
/**
* response
*/
$response = $HttpSocket->response;