SilverStripe 3.5.1 - Redirect to AbsoluteLink of SilverStripe page if user tries to navigate to amp.html version - silverstripe

I am working on a fork of the SilverStripe AMP module (https://github.com/thezenmonkey/silverstripe-amp) and have to prevent anyone from navigating to the amp.html version of a SilverStripe page if the amp.html link has not been generated into the head of the page.
A little additional info: We have added 2 fields that are meant to appear on the AMP versions of every page: AmpImage and AmpContent (a rich text editor). If either of these is empty, I have the code setup to NOT generate an AMP page for the SilverStripe page in question. This would seem to be enough but an additional requirement has been added, which is the redirect functionality mentioned about so no one can actually navigate to the amp.html page.
I was thinking of doing a redirect with the AmpSiteTreeExtension file but it does not appear to allow for redirects, then I thought of having a function in Page.php that would check if the url contains amp.html, then referencing it with AmpSiteTreeExtension, but every time I try, I get an error saying the function does not exist on "Page" or it's not public.
Is there a good way to handle this kind of situation? Is it best to do it with Page.php or using some other method?
Here are the files that I am working with:
AmpSiteTreeExtension
public function MetaTags(&$tags)
{
if ($this->owner->AmpContent != "" && $this->owner->AmpImageID != "") {
if ($this->owner->class != "HomePage") {
$ampLink = $this->owner->AbsoluteLink() . "amp.html";
} else {
$ampLink = $this->owner->AbsoluteLink() . "home/" . "amp.html";
}
$tags .= "<link rel='amphtml' href='$ampLink' /> \n";
}
//add a redirect here? Referencing a function from Page.php like so: $this->owner->functionName() causes the error mentioned above
}
}
<?php
AmpController
class AmpController extends Extension
{
private static $allowed_actions = array('amp');
private static $url_handlers = array(
'amp.html' => 'amp'
);
public function amp()
{
Requirements::clear();
$class = Controller::curr()->ClassName;
$page = $this->owner->renderWith(array("$class"."_amp", "Amp"));
return $this->AmplfyHTML($page);
}
public function AmplfyHTML($content)
{
if (!$content) {
return false;
}
$content = preg_replace('/style=\\"[^\\"]*\\"/', '', $content);
$content = str_replace("<img", "<amp-img", $content);
return $content;
}
}

From what I can tell, you're trying to redirect in the MetaTags() method of the SiteTree extension... and from what I can tell, that MetaTags() method it's probably used in some Silverstripe templates like this: $MetaTags
...and there's no way you can apply redirects this way.
You should do all this redirect stuff in a controller class, and from your example that controller it's probably the AmpController class which is probalby extending the functionality of the Page_Controller class.
Now I'll assume that AmpController it's an extension of Page_Controller, so I would do it like this:
class Page_Controller extends ContentController {
public function init() {
parent::init();
// you might have some other stuff here
// make sure this is the last line in this method
$this->extend('updateInit');
}
public function yourRedirectMethod() {
// do your redirect thing here
}
}
Key here is the following:
I extend the init() method in the controller - this will allow me to
extend the page controller's init() functionality using the
updateInit() method in the extension class (AmpController in this
case).
Instead of adding the method that's doing the redirect, to the Page
class, I added it to the Page_Controller class (the
yourRedirectMethod() method).
Now here comes the AmpController class, where I implement the updateInit() method:
class AmpController extends Extension {
private static $allowed_actions = array('amp');
private static $url_handlers = array(
'amp.html' => 'amp'
);
public function amp()
{
Requirements::clear();
$class = Controller::curr()->ClassName;
$page = $this->owner->renderWith(array("$class"."_amp", "Amp"));
return $this->AmplfyHTML($page);
}
public function AmplfyHTML($content)
{
if (!$content) {
return false;
}
$content = preg_replace('/style=\\"[^\\"]*\\"/', '', $content);
$content = str_replace("<img", "<amp-img", $content);
return $content;
}
public function updateInit() {
$should_redirect = true; // of course you add your own condition here to decide wether to redirect or not
if ($should_redirect) {
$this->owner->yourRedirectFunction();
}
}
}
The only thing here, is that you'll need to update the $should_redirect variable above (I've set it to true by default for this example - but here's where you decide if you should redirect or not)... and yes, you can reference here in the AmpController class stuff from the Page class I think, like this for exmaple: $this->owner->Title

Related

Symfony : how to set init data on login

I'm facing a dilemna as well as an optimization problem :
In my Symfony 2.8 application, I have custom settings and other business logic data to load (from database tables, not from SF parameters) that a logged in user can be needed to use at different pages.
At first those data where scarcely needed, so i loaded them only when the page required it. But now as the application grows, i need them more often.
So i was thinking about loading them when the user logs in, and save them as localStorage on client side because cookies are too small.
But i'm not sure how to best do it.
I have a login success handler, that allows to redirect on the correct page when user is successfully logged.
For the moment i have this one :
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $router;
protected $authorizationChecker;
public function __construct(Router $router, AuthorizationChecker $authorizationChecker)
{
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
}
/**
* What to do when user logs in.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin'));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user'));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url);
}
return $response;
}
}
So i was thinking about adding a setInitialData() method in which i would get all the settings i need and modifying onAuthenticationSuccess :
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
//retrieve array of data to be set in the init
$toBeSaved = $this->setInitialData();
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin', ['initdata'=>$toBeSaved]));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user', ['initdata'=>$toBeSaved]));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url, ['initdata'=>$toBeSaved]);
}
return $response;
}
And then on the main template, i would retrieve that data
{% for paramName, paramValue in app.request.query %}
{% if paramName == 'initdata' %}
<div id="initdata" data-init="{{paramValue|json_encode}}"></div>
{% endif %}
{% endfor %}
and add a javascript block with something like :
<script>
if ($('#initdata').length > 0){
localStorage.removeItem('initdata');
localStorage.setItem('initdata', JSON.stringify($('#initdata').data('init')));
}
</script>
But this method doesn't seems right : i'm not sure this is the best way to do it.
And furthermore, since these are sent in a redirect, the data are shown in the query string, which is not ideal :(
This will not fly as by having multiple parameters you create multiple <div> elements with identical ID = initdata. Subsequent jQuery selector will only capture the first one (afaik).
I see that you indeed send params via query string. This takes care of multiple value, but this also exposes your user setting in user URL, doesn't it? If it does, it has security vulnerability all over the wall. Remember, such URLs are persisted in your browser's history.
Instead, I suggest you create a separate controller action /_get_user_settings which you will call via AJAX GET. Server will serve JSON response which you can save to your localStorage with little or no problem at all.
Hope this helps...

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

how to use Symfony methods Action excluding the "Action" word

I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.

How can I use something like the onAfterPublish() hook on a versioned dataobject in SilverStripe

I have a simple versioned dataobject in SilverStripe. I'm trying to hook into the publication action and send out an email whenever the dataobject is published.
I don't think the onAfterPublish() method is available on dataobjects (only pages), so I'm looking to either mimic that or get enough logic working in the onAfterWrite() function.
Here's my code at the moment:
static $has_written = false; // Hack so it only fires once on write()
public function onAfterWrite()
{
parent::onAfterWrite();
if (!self::$has_written) {
$stage = $this->getSourceQueryParam("Versioned.stage");
if ($stage === 'Live') {
$email = new Email();
...
$email->send();
}
}
self::$has_written = true;
}
The Versioned class, that is used for versioning DataObjects, does not have an onAfterPublish hook but it does have an onBeforeVersionedPublish hook that could be used to send out emails:
public function onBeforeVersionedPublish($fromStage, $toStage, $createNewVersion = false) {
$email = Email::create();
// ...
$email->send();
}

How to inject content to Twig generated content?

I would like to inject content to Twig generated content after everything else has been parsed and done.
Right now I'm using this code below:
public function onResponse(KernelEvent $event)
{
// TODO: find a better way to inject
$event->getResponse()->setContent(
$this->asseticProcessor->inject($event->getResponse()->getContent()));
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::RESPONSE => array('onResponse', -9999),
);
}
However, I feel like it may not be the optimal way to do so. First of all, at least I want to do the injection only when Twig HTML templates are actually rendered (in some cases, the controller can simply return a response without rendering anything, or they can render json and in such case I don't have to manipulate the content)
Late to the party here, but you could use a kernel event listener for this. For example:
services:
response_modifier:
class: Some\Bundle\Listener\ModifyResponseListener
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -127 }
Set the priority very low so that it fires towards the of the response chain. The listener might work like so:
class ModifyResponseListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
// We probably want to ignore sub-requests
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$content = $response->getContent();
$content = preg_replace('/foo/is', 'bar', $content);
$response->setContent($content);
}
}
The above will replace all occurrences of "foo" with "bar" in the final output.
Note that you might also have to check that the response is not a redirection, ajax, stream or whatever before modifying the content.

Resources