Don't use eID Ajax dispatchers for your Extbase extension
tl;dr: Don't use eID Ajax dispatchers for your Extbase extension
I have written four articles lately explaining why my proposed typoscript_rendering approach is far superior to any other.
Not only when, but especially when having the need to call Extbase controllers via Ajax. It is very easy to use, it less likely fails and has better performance because it benefits from integrated TYPO3 caching mechanisms.
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
✓ Live Streaming✓ Interactive Chat✓ Private Shows✓ HD Quality
Anya is LIVE right now
FREE
Free to watch • No registration required • HD streaming
Ajax Examples for out of bound TypoScript Rendering
tl;dr: I published an example extension where you can see how to use the typoscript_rendering extension for Ajax requests.
In my last article I tired to theoretically describe the benefits of using the typoscript_rendering extension in favor of using eID scripts/dispatchers or page type configuration, especially when using it for Extbase Extensions.
A very basic greet action which gets the greeting result by Ajax
A paginate widget template for the News extension which replaces the paginate links to use the TypoScript Rendering, thus fetching news pages via Ajax
The latter most likely only works when using the default News template, as the delivered JavaScript makes assumptions on the HTML structure. Other than that, just install the extension, insert the example plugin or just browse the code to see how it works.
Especially look into the Fluid templates. Nothing more is necessary than using the provided view helpers (and of course some JavaScript to do and handle the Ajax calls) to ajaxify your Extbase extension.
Have fun with it and drop me a line if you have questions or like it!
tl;dr: There are several ways to get the job done. I claim to have done it the right way.
Introduction
In a previous post I cheered how nice and simple it is to implement a simple image gallery with Extbase using FAL. I presented this during the training and everybody was happy. Then a participant asked how to add an image upload functionality to the gallery... silence...
The thing is: there is no framework support for image upload in Extbase at all!
Yes, there is a fluid view helper and you get a couple of hits when searching for "Extbase file upload", even solutions which include file abstraction layer handling.
However every single example I found, implemented the functionality in the controller code. Not only that doing so clutters your controller, it is hard to re-use such code without just copy and pasting it in every controller where you need it (or extending every controller from a base class which is not much better).
My answer during the workshop was: "The framework does not have support for that and I'm not aware of a good solution. But give me this evening and will show you something tomorrow."
Solution
Step1: Using TypeConverters
After a few hours of hacking I had a proof of concept working that used a custom TypeConverter to evaluate the file upload array, move the uploaded file to a FAL storage using the FAL API and have the result persisted in the database using the Extbase persistence.
At that time I thought it would only take me a few additional hours to get it into a state that it might be merged to TYPO3 6.2 which was not released yet back then. However I did not find these few hours so TYPO3 6.2 was released without such a solution.
Meanwhile …
… Stefan Frömken did some work in this area and (without knowing about my PoC) also used a TypeConverter for the upload work. When I also had time again last week to deal with this topic he thankfully handed over his (private) codebase so I could compare it to my work. Since he had implemented more functionality in his code than I had in my PoC, but the implementation could have been improved, I was motivated to finally finish my TypeConverter to be feature complete with an additional focus on re-usability of the code and of course targeted to eventually be merged to the TYPO3 Core.
Step2: Implement error handling
Instead of throwing exceptions if something goes wrong, I used the TypeConverter API to return error messages for users. That was quite an easy one ;)
Step3: Make things configurable
To be generic, the TypeConverter needs a bit of configuration (like in which folder to upload to, or which file extensions should be allowed). Thanks to availability of PropertyMappingConfiguration, this was pretty straightforward to implement. My TypeConverter accepts four configuration options:
class UploadedFileReferenceConverter extends \TYPO3\CMS\Extbase\Property\TypeConverter\AbstractTypeConverter { /** * Folder where the file upload should go to * (including storage). */ const CONFIGURATION_UPLOAD_FOLDER = 1; /** * How to handle a upload when the name * of the uploaded file conflicts. */ const CONFIGURATION_UPLOAD_CONFLICT_MODE = 2; /** * Wheter to replace an already present resource. * Useful "for maxitems = 1" fields / propeties * with no ObjectStorage annotation. */ const CONFIGURATION_ALLOWED_FILE_EXTENSIONS = 3; /** * Wheter to replace an already attached resource. * Useful "for maxitems = 1" fields / propeties * with no ObjectStorage annotation. */ const CONFIGURATION_REPLACE_RESOURCE = 4; … }
Step4: Handle validation errors and already attached resources
That was a tough one and it took me around 2 days to finally get everything working. I could get some inspiration from TYPO3 Flow code here and in fact back ported quite a bit of it, but FAL differs a lot from Flow Resource Management, so I had to do quite a bit to get this working nicely with FAL.
The main issue here was that when editing an entity that already has an image attached to it (through a previous upload), saving the entity without re-uploading a file should keep the attached resource.
Since the knowledge if an entity already has attached resources is not (only) in the domain of the TypeConverter, I had to extend the UploadViewHelper to assign such values to a hidden input field so that the TypeConverter can do the right thing and return the correct resource instead of nothing (very much like in Flow). As a little addition I made the view helper accept child nodes so that you can render the attached resource if you like to:
The case when upload was successful, but validation of other fields of the entity failed also kept me busy quite some time because the file was there but not yet attached to the entity (as the entity is not persisted when validation fails). But a few hours later I had an implementation involving the view helper and the type converter.
Security
There are two things I considered for the TypeConverter to handle file uploads securely:
1. Deny upload of PHP files!
if (!GeneralUtility::verifyFilenameAgainstDenyPattern($uploadInfo['name'])) { throw new TypeConverterException('Uploading files with PHP file extensions is not allowed!', 1399312430); }
I cannot stress enough how important these three lines of code are. So important, that they were part of my first PoC where almost nothing else worked. These lines are independent from the configurable allowed file extensions and not optional. You do not want anyone to add PHP code to your site besides yourself!
2. Securing attached resource IDs with hmac
Transmitting the information about already attached resources is done by handing over the values via a POST argument. While this is convenient and also done in Flow that way, there is a huge difference between Flow and TYPO3 CMS that makes a difference in security. Flow uses UUIDs as identifiers for objects, TYPO3 CMS uses (numerical) UIDs. While it is (close to) impossible to guess a UUID in order to manipulate the resource attached to an entity by manipulating the POST request argument, it's pretty easy to do with numerical ids. Just increment by one and see what resource is stored with that UID :) This would allow to access any resource available in the system. To tackle that, resource IDs are appended with an hmac of the id and this hmac is checked before reconstituting the resource object.
Check it out
I made the code available on Github
The heart of it is the UploadedFileReferenceConverter, but to get it working in your extension, you also need an extended FileReference model, an extended ObjectStorageConverter and of course the UploadViewHelper. The rest of the code is more or less plain generated code from the extension builder, but you may want to look into the controller to get a glimpse how to configure the type converter and into the TCA how to properly set the match_fields so that Extbase Persistence does the right thing.
Finally
This post is pending for months and I'm happy to have it out now. I would like to thank Anja for pushing me to get this done and of course you dear reader for making it to the end of my post :)
I'm eager to get your feedback and wish Happy Forking!
Unit Testing einer TYPO3 Extbase Extension (Teil 3: Controller)
< 2. Teil: Validatoren und Repositories
Ganz nach dem Motto lieber spät als nie, hier Teil 3 der Serie Extbase Unit Testing. Darin werde ich auf das Unit Testing von Extbase Controllern eingehen.
Der Verständlichkeit halber habe ich für diesen Teil eine kleine dummy Extension erstellt. Diese Extension (jh_theanswer) besteht aus einem einzelnen AnswerController mit einer Action, der showAction.
Durch den Aufruf der showAction berechnet die Extension aufgrund eines Wertes aus einem Domainobjekt und einem magischen Wert aus dem Konstanten-File (settings.magicNumber) die Antwort nach dem Leben, dem Universum und dem ganzen Rest. Dieser Wert wird schlussendlich dem Template via assign()-Methode zur Verfügung gestellt.
Die Extension könnt ihr euch im Detail auf GitHub ansehen:
https://github.com/tschortsch/jh_theanswer
Controller initialisieren
Bevor wir die Testmethode ausführen können müssen alle Objekte, welche von Extbase verwendet werden initialisiert werden. Dies ist bei einem Controller-Test etwas komplexer. Als erstes müssen wir den Controller selbst erstellen:
class Tx_Jh_theanswer_Controller_AnswerControllerTest extends Tx_Extbase_Tests_Unit_BaseTestCase { [...] /** * Fixture * @var Tx_JhTheanswer_Controller_AnswerController */ protected $controller = NULL; public function setUp() { // create controller $this->controller = $this->objectManager->get('Tx_JhTheanswer_Controller_AnswerController'); $this->prepareController(); } }
Danach werden alle Abhängigkeiten innerhalb des Controllers initialisiert:
In der prepareController()-Methode werden zuerst Mock-Objekte eines Requests und einer View an den Controller gebunden. Zudem müssen alle im Controller verwendeten Settings aus dem $settings-Array von Hand abgebildet und ebenfalls an den Controller gebunden werden. Die dabei verwendeten Setter-Methoden (setRequest(), setView(), setSettings()) stehen standardmässig nicht in den Extbase-Controllern zur Verfügung und müssen deshalb manuell erstellt werden.
class Tx_JhTheanswer_Controller_AnswerController extends Tx_Extbase_MVC_Controller_ActionController { [...] /** * Sets the view * * This function is intended to be used for unit testing purposes only * * @param Tx_Fluid_View_TemplateView $view The new view * @return void */ public function setView(Tx_Fluid_View_TemplateView $view) { $this->view = $view; } /** * Sets the request * * This function is intended to be used for unit testing purposes only * * @param Tx_Extbase_MVC_Request $request The new request * @return void */ public function setRequest(Tx_Extbase_MVC_Request $request) { $this->request = $request; } /** * Sets the settings * * This function is intended to be used for unit testing purposes only * * @param array $settings The new settings * @return void */ public function setSettings(array $settings) { $this->settings = $settings; } }
Mit diesen Schritten sollte der Controller vollständig initialisiert sein und wir können den eigentlichen Test schreiben.
Test erstellen
In userem Test wollen wir die showAction des AnswerControllers testen:
class Tx_JhTheanswer_Controller_AnswerController extends Tx_Extbase_MVC_Controller_ActionController { [...] /** * action show * * @return void */ public function showAction() { $answerRecordUidFromSetup = $this->settings['theAnswerUid']; $answer = $this->answerRepository->findByUid($answerRecordUidFromSetup); $theAnswer = $this->calculateAnswer($answer->getValue()); $this->view->assign('theAnswer', $theAnswer); } /** * @param $value integer answer from domain object * @return integer the answer */ protected function calculateAnswer($value) { return $value + $this->settings['magicNumber']; } }
Die showAction liest ein in den Konstanten definiertes Answer-Domainobjekt aus der Datenbank und berechnet daraus mit der calculateAnswer()-Methode die gesuchte Antwort. Diese wird schlussendlich per assign()-Methode dem Template zur Verfügung gestellt.
In userem Test wollen wir nun den assign()-Aufruf überprüfen. Dadurch wird automatisch auch die calculateAnswer()-Methode getest da deren Rückgabewert im assign()-Aufruf zugewiesen wird.
class Tx_Jh_theanswer_Controller_AnswerControllerTest extends Tx_Extbase_Tests_Unit_BaseTestCase { [...] /** * @test * @author Jürg Hunziker <[email protected]> */ public function showActionAssignsAnswer() { $answer = $this->mockAnswerRepositoryFindByUid(); // the actual test $expectedAnswer = 42; // $answer->getValue() + $this->settings['magicNumber'] $this->view->expects($this->at(0))->method('assign')->with('theAnswer', $expectedAnswer); // call the controller action $this->controller->showAction(); // tear down locally defined variables unset($answer); unset($expectedAnswer); } /** * Returns dummy answer * * @return Tx_JhTheanswer_Domain_Model_Answer $answer * @author Jürg Hunziker <[email protected]> */ public function mockAnswerRepositoryFindByUid() { $answer = $this->objectManager->get('Tx_JhTheanswer_Domain_Model_Answer'); $answer->setValue(32); $this->answerRepository->expects($this->once())->method('findByUid')->will($this->returnValue($answer)); return $answer; } }
Als erstes müssen wir definieren was der findByUid()-Aufruf im answerRepository zurückgibt. Dies geschieht in der Methode mockAnswerRepositoryFindByUid(). Darin erstellen wir ein Answer-Domainobjekt und setzen dessen Value auf 32. Danach definieren wir, dass der erste Aufruf der findByUid()-Methode dieses Objekt zurückliefert. Das erstellte Answer-Objekt geben wir zudem als Rückgabewert zurück, da wir danach auf dessen Value die Berechnung durchführen.
Da wir wissen, dass die Methode calculateAnswer() im Controller lediglich den Answer-Value mit dem Wert der magicNumber-Konstante addiert können wir die $expectedAnswer auf 42 festlegen.
Nun folgt der eigentliche Test. Wir erwarten, dass beim ersten Aufruf (at(0)) der assign()-Methode der 'theAnswer'-Variable unsere $expectedAnswer zugewiesen wird:
Anya is live and ready to show you everything. Watch her strip, dance, and perform exclusive shows just for you. Interact in real-time and make your fantasies come true.
✓ Live Streaming✓ Interactive Chat✓ Private Shows✓ HD Quality
Anya is LIVE right now
FREE
Free to watch • No registration required • HD streaming
Da es in Extbase Extensions bei der Verwendung von var_dump zu einem Timeout kommt kann man sicher stattdessen folgender Zeige Codes bedienen. Die Ausgabe ist im Übrigen auch noch wesentlich besser als der Standard Befehl.