I asked this question in Magento Stack Exchange but did not get any comment/answer from anyone. So I deleted the question over there and asking the same question in SO.
So, I have an external App which logs an admin user into the admin panel. Barebone structure is as follows:
<?php
use Magento\Framework\App\Bootstrap;
require __DIR__ . '/app/bootstrap.php';
class TestApp extends \Magento\Framework\App\Http implements \Magento\Framework\AppInterface {
protected $_resource = null;
protected $storeManager = null;
public function __construct(
//ActionContext $context,
\Magento\Framework\App\ResourceConnection $resource, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Customer\Model\CustomerFactory $customerFactory
// \Magento\Catalog\Model\Session $catalogSession,
// \Magento\Framework\App\State $state
) {
$this->_resource = $resource;
$this->storeManager = $storeManager;
}
public function launch() {
$currentStoreID = $this->storeManager->getStore()->getStoreId();
}
}
$bootstrap = Bootstrap::create(BP, $_SERVER);
$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
/** #var \Magento\Framework\App\Http $app */
$app = $bootstrap->createApplication('TestApp');
$bootstrap->run($app);
I have stripped off the actual login part because this is out of the context of this question.
If I call $this->storeManager->getStore()->getStoreId();, it returns the default store ID, which is not what I want. This application resides under a sub-store (not sure if that's the correct term to use) and I want the current store ID, not the default one.
Following up, I found this closed issue where the user was told to use \Magento\Store\Model\StoreResolver::getCurrentStoreId. $this->storeManager does have storeResolver inside but that's a protected property, thus inaccessible.
My question is, how do I get the current store ID and not the default one?
Related
I'm building a small website using symfony 3.2.
There is a page in which the user can change its profile data with a form. I used the structure seen in the official tutorial. Here is the declaration of my controller :
/** #Route("/profil", name="profil_show_user") */
public function userProfileAction(Request $request, UserInterface $user) {
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
throw $this->createAccessDeniedException();
}
//...
$user->getProfile();//and do stuff
//...
}
My problem is that if the user is disconnected for being inactive for too long, or if someone bookmark the page but is not connected, I have this ugly error coming :
Controller
"AppBundle\Controller\ProfilController::userProfileAction()" requires
that you provide a value for the "$user" argument. Either the argument
is nullable and no null value has been provided, no default value has
been provided or because there is a non optional argument after this
one. 500 Internal Server Error - RuntimeException
In the New in Symfony 3.2 changelog, there is something about the new User value resolver. I tried to change to UserInterface $user = null, and it make the page redirect to the path I set in failure_path of security.yml, which is the good behaviour.
But then if I'm connected and go to profil_show_user, I get that other error :
Error: Call to a member function getProfile() on null
I search thoroughly the symfony documentation but couldn't find anything.
Could someone explain to me what goes wrong, what I misunderstood and how can I make this work ?
EDIT :
I thought I might say that if I don't use te value resolver, everything works fine. This is an educationnal and curiosity question about a new feature which I don't manage to use. This code works :
/** #Route("/profil", name="profil_show_user") */
public function userProfileAction(Request $request) {
$user = $this->getUser();
if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
throw $this->createAccessDeniedException();
}
//...
$user->getProfile();//and do stuff
//...
}
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.
I taken over responsibility for a Symfony2 application, built on the Sonata Admin Bundle, and have been asked to make a small change by the users. In the xls export of a list page, the dates all appear as e.g. Wed, 01 Aug 2012 00:00:00 +0200, but the Excel format is General. The users would like the data in this column to be an Excel date type, so that it is sort-able.
I have been able to find some information about export customization, but this mostly concerns choosing the list export file types, or which fields to include, rather than how to change the format in the exported document. A similar question was asked here (I think) but there is no answer.
I think this would (or should) be very simple, but it is certainly not obvious. Any help would be much appreciated.
A small improvement for Marciano's answer.
Makes the code a bit more resilient against sonata updates.
public function getDataSourceIterator()
{
$datasourceit = parent::getDataSourceIterator();
$datasourceit->setDateTimeFormat('d/m/Y'); //change this to suit your needs
return $datasourceit;
}
In my admin class EmployeeAdmin I use getExportFields function specifies which fields we want to export:
public function getExportFields() {
return array(
$this->trans('list.label_interview_date') => 'interviewDateFormatted'
);
}
interviewDateFormatted is actually a call to the corresponding entity (Employee) method getInterviewDateFormatted which looks like this:
public function getInterviewDateFormatted() {
return ($this->interviewDate instanceof \DateTime) ? $this->interviewDate->format("Y-m-d") : "";
}
This way I can change date format or do other necessary changes to the fields I want to export.
this is my code. It's work!
use Exporter\Source\DoctrineORMQuerySourceIterator;
use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
and function:
/**
* {#inheritdoc}
*/
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
$fields=$this->getExportFields();
$query = $datagrid->getQuery();
$query->select('DISTINCT ' . $query->getRootAlias());
$query->setFirstResult(null);
$query->setMaxResults(null);
if ($query instanceof ProxyQueryInterface) {
$query->addOrderBy($query->getSortBy(), $query->getSortOrder());
$query = $query->getQuery();
}
return new DoctrineORMQuerySourceIterator($query, $fields,'d.m.Y');
}
just add this in your admin (overriding a method of the admin class you are extending). Found it reading the code. It's not in the docs.
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
$datasourceit = $this->getModelManager()->getDataSourceIterator($datagrid, $this->getExportFields());
$datasourceit->setDateTimeFormat('d/m/Y'); //change this to suit your needs
return $datasourceit;
}
Did you managed to make it work?
Date format is defined as parameter for new DoctrineORMQuerySourceIterator.php (https://github.com/sonata-project/exporter/blob/master/lib/Exporter/Source/DoctrineORMQuerySourceIterator.php)
DoctrineORMQuerySourceIterator.php is created inside getDataSourceIterator function (https://github.com/sonata-project/SonataDoctrineORMAdminBundle/blob/2705f193d6a441b9140fef0996ca392887130ec0/Model/ModelManager.php)
Inside of Admin.php there is function calling it:
public function getDataSourceIterator()
{
$datagrid = $this->getDatagrid();
$datagrid->buildPager();
return $this->getModelManager()->getDataSourceIterator($datagrid, $this->getExportFields());
}
If you write your own getDataSourceIterator() then you can change date format.
Since sonata-admin 4.0, the function getDataSourceIterator() is tagged as final, so you can't override it.
So you need to create a decorating iterator :
<?php
namespace App\Service\Admin;
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
use Sonata\AdminBundle\Exporter\DataSourceInterface;
use Sonata\DoctrineORMAdminBundle\Exporter\DataSource;
use Sonata\Exporter\Source\DoctrineORMQuerySourceIterator;
use Sonata\Exporter\Source\SourceIteratorInterface;
class DecoratingDataSource implements DataSourceInterface
{
private DataSource $dataSource;
public function __construct(DataSource $dataSource)
{
$this->dataSource = $dataSource;
}
public function createIterator(ProxyQueryInterface $query, array $fields): SourceIteratorInterface
{
/** #var DoctrineORMQuerySourceIterator $iterator */
$iterator = $this->dataSource->createIterator($query, $fields);
$iterator->setDateTimeFormat('Y-m-d H:i:s');
return $iterator;
}
}
And add it in your config/services.yaml
services:
...
App\Service\Admin\DecoratingDataSource:
decorates: 'sonata.admin.data_source.orm'
arguments: ['#App\Services\Admin\DecoratingDataSource.inner']
Found here : https://docs.sonata-project.org/projects/SonataDoctrineORMAdminBundle/en/4.x/reference/data_source/
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
How I can authentication in Symfony2 without cookies in a brouser? How can generate some like this http://some.site/hello/roman?PHPSESSID=9ebca8bd62c830d3e79272b4f585ff8f or this http://some.site/9ebca8bd62c830d3e79272b4f585ff8f/hello/roman or some other url that was always available sessionid parameter. Thank you for any help.
You have to to two things. First you must extend the session storage to get the session from the query param.
namespace Elao\BackBundle\Session;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Session\Storage\NativeFileSessionStorage;
class Storage extends NativeSessionStorage
{
public function __construct($savePath = null, array $options = array(), ContainerInterface $container)
{
$request = $container->get('request');
if ($request->query->has('sessionId')) {
$request->cookies->set(session_name(), 1); // We have to simulate this cookie, in order to bypass the "hasPreviousSession" security check
session_id($request->query->get('sessionId'));
}
return parent::__construct($savePath, $options);
}
}
Source: http://www.elao.com/blog/symfony-2/symfony-2-loading-session-from-query-param.html
The next point, should be replacing the UrlGenerator to generate every url with the session id param. A example to do this, can be found in this answer.
But as nifr in the comment said, it's not a very clean requirement.