Silverstripe subsite module, how to make subsite specific members? - silverstripe

I wanted to white label silverstripe CMS, i.e. one codebase serves different domains and each domains have their own members, etc. I have asked this question Here. And I was suggested to use Subsite modules.
However this partially solved my problem (I m very new to SilverStripe and official community isn't that active).
I was able to make my custom modules Subsite specific by using following code in my ModelAdmin
<?php
class CompaniesAdmin extends ModelAdmin {
private static $url_segment = 'Companies';
private static $managed_models = "Company";
private static $menu_title = 'Companies';
private static $menu_icon = 'mysite/images/icons/company-icon.png';
public function getEditForm($id = null, $fields = null){
$form = parent::getEditForm($id, $fields);
$gridField = $form->Fields()->fieldByName($this->sanitiseClassName($this->modelClass));
if(class_exists('Subsite')){
$list = $gridField->getList()->filter(array('SubsiteID'=>Subsite::currentSubsiteID()));
$gridField->setList($list);
}
return $form;
}
}
?>
Everything works as expected, except the "Security" Module. My requirement is even membership should be subsite specific. That means members from one subsite is not visible in another subsite, they can't login to another subsite, etc.
I also visited this post by another user. However the suggested solution is not implementable in my scenario.
My Questions
Is it possible to extend subsite module so that we can have subsite
specific Members??
Can we do without modifying core files?
I was able to add hidden field for SubsiteID while adding/editing Members and was able to save member with specific SubsiteID.
How to override Security module, so that I can list/filter subsite specific Members?
How to prevent editing of other Subsite's member if someone provides member id in URL querystring?
How to prevent member of one subsite login to another subsite?
Any help is highly appreciated.
UPDATE
I tried following injector, but didn't work, got an error Fatal error: Call to a member function getList() on a non-object in...
<?php
class CustomSecurityAdmin extends SecurityAdmin {
public function getEditForm($id = null, $fields = null){
$form = parent::getEditForm($id, $fields);
$gridField = $form->Fields()->fieldByName('Member');
if(class_exists('Subsite')){
$list = $gridField->getList()->filter(array('SubsiteID'=>Subsite::currentSubsiteID()));
$gridField->setList($list);
}
return $form;
}
}
?>
And in _config.yml
Injector:
SecurityAdmin:
class: CustomSecurityAdmin
UPDATE 2
My injector for MemberAuthenticator class
<?php
class CustomMemberAuthenticator extends MemberAuthenticator {
public static function authenticate($RAW_data, Form $form = null) {
//add logic before
//get Subsite ID
$Subsite = SubsiteDomain::get()->filter('Domain', $_SERVER["HTTP_HOST"])->First();
if($Subsite){
$SubsiteID = $Subsite->SubsiteID;
}else{
$SubsiteID = 0;
}
$email = Convert::raw2sql($RAW_data['Email']);
$member = Member::get()->filter(array(
"Email" => $email,
"SubsiteID" => $SubsiteID
))->First();
if(!$member){
if($form) $form->sessionMessage("Invalid User", 'bad');
}else{
parent::authenticate($RAW_data,$form);
}
}
}
?>
And _config.yml
Injector:
MemberAuthenticator:
class: CustomMemberAuthenticator
But this didn't work the injector is not working at all

Is it possible to extend subsite module so that we can have subsite specific Members??
Yes
Can we do without modifying core files?
How to override Security module, so that I can list/filter subsite specific Members?
How to prevent member of one subsite login to another subsite?
The two classes you are probably most interested in are SecurityAdmin and MemberAuthenticator.
All silverstripe core files can be "modified" in some way... of the methods discussed on this video presentation by StripeCon - Loz Calver - Why you shouldn’t edit SilverStripe core files (and how to do it anyway) - I recommend method 3, fork the code and add this custom security to your version of silverstripe.
For SecurityAdmin the best option is to simply removing the current SecurityAdmin from the menu and adding your own custom class:
in _config.php
CMSMenu::remove_menu_item('SecurityAdmin ');
How to prevent editing of other Subsite's member if someone provides member id in URL querystring?
You could determine if the user is allowed to edit the form based on which site they belong to... either in permissions or simply use updateCMSFields to remove all fields and a validator to ensure nothing can be submitted that doesn't match your rules.
public function updateCMSFields(FieldList $fields) {
if (<not valid user to edit>) $fields = FieldList::create();
...
}
Here is another question about how to add a validator and here are the docs for that.

There is another hack for this, which flawlessly worked for me.
/mysite/extensions/CustomLeftAndMain.php
<?php
class CustomLeftAndMain extends Extension {
public function onAfterInit() {
self::handleUser();
}
public static function handleUser(){
$currentSubsiteID = Subsite::currentSubsiteID();
$member = Member::currentUser();
$memberBelongsToSubsite = $member->SubsiteID;
if($memberBelongsToSubsite>0 && $currentSubsiteID!=$memberBelongsToSubsite){
Security::logout(false);
Controller::curr()->redirect("/Security/login/?_c=1001");
}
}
}
and in /mysite/_config.php add an extension
LeftAndMain::add_extension('CustomLeftAndMain');
What above code basically does is, the system lets user login no matter which subsite they belong to. And as long as application is initiated, we'll check whether the logged in user belongs to current website or not (method handleUser does it.).
If user doesnot belong to current site, then they are logged out and then redirected to the login page.

Related

Send a custom email from sonata admin using variables

I have a list view in sonata admin. I want to add a column that will allow me to click on a link to send an email. The link action will know variables from that row in the table so that it can fill in the email. I was able to add the column and can visualize a twig template. I've added the following function to the Admin Class:
public function sendEmail( \Swift_Mailer $mailer)
{
$message = (new \Swift_Message('some email'))
->setFrom('contact#example.com')
->setTo('contact#example.com')
->setBody(
$this->renderView(
'emails/response.html.twig',
array('manufacturer' => $manuvalue, 'modelnum' => $modelnumvalue, 'email' => $email)
),
'text/html');
$mailer->send($message);
}
I'm stuck on how to connect these pieces together so that when I click on the link the email is sent and includes the params from the row. I have email working on form submit in other areas of the site, but need help figuring out the way to do this manually.
As you mentioned in the comments, what you want to do is typically a Custom Action
In order to ensure that this action can not be accessed via direct request and can only be performed by admin, you could do use a template like this for your customAction :
...
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
...
//AuthorizationCheckerInterface injected as authorizationChecker
public function customAction($id){
$object = $this->admin->getSubject();
if (!$object) {
throw new NotFoundHttpException(sprintf('unable to find the object with id: %s', $id));
}
// Check access with role of your choice
$access = $this->authorizationChecker->isGranted('ROLE_ADMIN');
if($access){
//Do your action
} else {
throw $this->createAccessDeniedException("You don't have the rights to do that!");
}
}
I ended up doing a custom route and protected it with security settings that #dirk mentioned.

SilverStripe Subsite module delete all related data on subsite deletion

I m using Silverstripe subsite module. When Subsite is deleted I want to delete all other information related to Subsite as well, like, Domains, Settings etc. I have created an extension that'll extend Subsite model.
<?php
class SubsiteExtension extends DataExtension {
public function onAfterWrite(){
parent::onAfterWrite();
//Some codes here
}
public function onBeforeDelete(){
//Check if member exist for Subsite, if so show warning.
}
public function onAfterDelete(){
$id = $this->owner->ID;
//DELETE ALL SUBDOMAINS RELATED TO DELETED SUBSITE
DB::query("DELETE FROM SubsiteDomain WHERE SubsiteID='".$id."'");
//DELETE SITE CONFIG
DB::query("DELETE FROM SiteConfig WHERE SubsiteID='".$id."'");
}
}
Problem
The code works flawlessly. Just wondering is there any other better way of deleting related records from other tables??
On method onBeforeDelete , how to show custom message saying "You can't delete this subsite unless you delete all the members" ?
Using onAfterDelete to delete related records is perfectly fine, although it really sounds a lot like you're doing unnecessary work here. Who's going to be bothered by some stray DB entries?
Regarding your onBeforeDelete approach: I would solve it otherwise. Override canDelete in your extension instead, something like this:
public function canDelete($member)
{
if( /* check if member exist for Subsite */ ){
return false;
}
// returning null here means that this extension doesn't influence
// the delete permission at this point
return null;
}
This will prevent deleting of the record in the CMS. Additionally, you could use updateCMSFields to display a notice to the user why he can't delete the record.
public function updateCMSFields(FieldList $fields)
{
if (!$this->owner->canDelete()) {
$fields->addFieldToTab(
'Root.Main',
LiteralField::create('_deleteInfo', 'Your info text')
);
}
}

Symfony2 - Sonata Admin - Edit/Create "return to list" action

I have a Sonata admin for an entity with many elements that naturally spans multiple pages in the list view.
What I'd like to do is make it so after editing, or creating a new entity, redirect the user to the appropriate page which displays the edited entity (as opposed to going back to the list's first page which is the default behavior of the sonata admin). The dafult behavior is ok when there are only 1 or 2 pages but when you have tens or even hundreds of pages, navigating back to the correct page becomes quite tedious.
So my question is what is the appropriate way to make this happen?
I'm thinking that it would involve customizing the admin controller for the entity but I'm not sure what the right extension points are. And also, how to utilize the paginator to obtain the correct page to navigate back to.
Another potential hack would be to capture the query parameters state when navigating from the list view to the edit, and then returning the user to the same URL. This won't work correctly for creating new items.
There's also the matter of the state of filters when navigating from the list view (if the user had sorted and/or filtered the list before navigating to the edit page).
I know I'm late but this can be useful for someone else...
Here is the way I've made it, by overriding AdminBundle CRUDController:
<?php
namespace MyProject\AdminBundle\Controller;
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
class CRUDController extends BaseController
{
protected function redirectTo($object, Request $request = null)
{
$response = parent::redirectTo($object, $request);
if (null !== $request->get('btn_update_and_list') || null !== $request->get('btn_create_and_list')) {
$url = $this->admin->generateUrl('list');
$last_list = $this->get('session')->get('last_list');
if(strstr($last_list['uri'], $url) && !empty($last_list['filters'])) {
$response = new RedirectResponse($this->admin->generateUrl(
'list',
array('filter' => $last_list['filters'])
));
}
}
return $response;
}
public function listAction(Request $request = null)
{
$uri_parts = explode('?', $request->getUri(), 2);
$filters = $this->admin->getFilterParameters();
$this->get('session')->set('last_list', array('uri' => $uri_parts[0], 'filters' => $filters));
$response = parent::listAction($request);
return $response;
}
}
I am having the same problem, I was thinking of passing a variable in the route to the edit page, thus giving you where the request for the edit originated from, then you could redirect to the originating page given the variable.

Sonata Admin - how to set the menu.label attribute?

According to the Sonata source code, the last node in the breadcrumb is rendered this way:
# standard_layout.html.twig #
<li class="active"><span>{{ menu.label }}</span></li>
In my setup, when opening a given Admin subclass, the last node simply becomes a raw string according to the entity handled by the Admin:
Dashboard / Entity List / Acme\SomeBundle\Entity\Stuff:000000001d74ac0a00007ff2930a326f
How can I set the value of menu.label to get something more appropriate? I have tried, in my Admin subclass, to override the following:
protected function configureTabMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null) {
$this->configureSideMenu($menu, $action, $childAdmin);
}
protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null) {
$menu->setLabel("Some nice label");
$menu->setName("Some nice name");
}
However, this does not change anything, even though I have verified that the methods above are called during runtime.
Finally found a good (and somewhat obvious) solution to this.
The Sonata Admin class uses an internal toString($object) method in order to get a label string for the entity it is handling. Thus, the key is to implement the __toString() method of the entity in question:
public function __toString() {
return "test";
}
The best way is to configure the $classnameLabel variable in the Admin Class :
class fooAdmin extends Admin
{
protected $classnameLabel = 'Custom Label';
}
But it have the same issue (weird name with entity path) doing it, even if it is working fine on all the others pages.
Apparently, the Sonata way of solving this is show here:
Quote:
While it’s very friendly of the SonataAdminBundle to notify the admin of a successful creation, the classname and some sort of hash aren’t really nice to read. This is the default string representation of an object in the SonataAdminBundle. You can change it by defining a toString() (note: no underscore prefix) method in the Admin class. This receives the object to transform to a string as the first parameter:
Source: https://sonata-project.org/bundles/admin/master/doc/getting_started/the_form_view.html#creating-a-blog-post

Symfony Multiple application interaction

In symfony 1.4, how to call an action of another application from the current action?
There's a blog post about that here:
http://symfony.com/blog/cross-application-links
There's a plugin for it:
https://github.com/rande/swCrossLinkApplicationPlugin
And there are some blogposts explaining it:
http://rabaix.net/en/articles/2009/05/30/cross-link-application-with-symfony
http://rabaix.net/en/articles/2009/07/13/cross-link-application-with-symfony-part-2
(Note: both are for symfony 1.2, but they should also work in 1.4)
To route to your frontend to your backend, there are three easy steps:
1. Add to your backend configuration the following two methods
These methods read the backend routing, and use it to generate routes. You'll need to supply the link to it, since php does not know how you configured your webserver for the other application.
.
// apps/backend/config/backendConfiguration.class.php
class backendConfiguration extends sfApplicationConfiguration
{
protected $frontendRouting = null;
public function generateFrontendUrl($name, $parameters = array())
{
return 'http://frontend.example.com'.$this->getFrontendRouting()->generate($name, $parameters);
}
public function getFrontendRouting()
{
if (!$this->frontendRouting)
{
$this->frontendRouting = new sfPatternRouting(new sfEventDispatcher());
$config = new sfRoutingConfigHandler();
$routes = $config->evaluate(array(sfConfig::get('sf_apps_dir').'/frontend/config/routing.yml'));
$this->frontendRouting->setRoutes($routes);
}
return $this->frontendRouting;
}
// ...
}
2. You can link to your application in such a fashion now:
$this->redirect($this->getContext()->getConfiguration()->generateFrontendUrl('hello', array('name' => 'Bar')));
3. Since it's a bit tedious to write, you can create a helper
function link_to_frontend($name, $parameters)
{
return sfProjectConfiguration::getActive()->generateFrontendUrl($name, $parameters);
}
The sfCrossLinkApplicationPlugin does this , this, but in a bit simpler fashion, you would be able to use a syntax similar to this:
<?php if($sf_user->isSuperAdmin()):?>
<?php link_to('Edit Blog Post', '#backend.edit_post?id='.$blog->getId()) ?>
<?php endif ?>
It would be something like this:
public function executeActionA(sfWebRequest $request)
{
$this->redirect("http:://host/app/url_to_action");
}
In Symfony each application is independent from the others, so if you need to call an action of another app, you need to request it directly.
Each app is represented by one main controller (frontend, backend, webapp), this controller takes care of the delivery of each request to the corresponding action (and lots of other things like filters, etc.).
I really recommend you to read this, it would be quite more explanatory: Symfony - Inside the Controller Layer

Resources