I wanted to add a tab to the user edit page ie user/%/edit/foo, and was using the twitter module as a model. After much spelunking and stepping through with a debugger, I realised that I needed to add a hook_user function in my module so that the %user_category part of the menu router path would work.
It's now functioning as expected, but I don't really have a solid idea of what I just did, and haven't found a good explanation anywhere.
Can anyone explain to me what it's about?
user_category_loads fails if given a category that does not exist, which happens at user/uid/edit/not_a_category, and it passes that category for access checks to user/uid/edit/is_a_category and thus access to those is set to false, so bam, wonky menu :'(.
When you use %user_category it means that user_category_load function is called with that argument (the uid).
The function is defined in the user module. These functions serve as a validation, if False it return FALSE, it will result in a 404, but if it return something else, like a user object, that will be passed to whatever callback function / form is run for that url.
In your case, you could probably have used %user instead which would have called user_load which is more simple, and you wouldn't have needed to do all the extra stuff to make user_category_load pass.
Summary
So user_category_load does two things.
Check that the category exist so you just can't do user/%/edit/foo.
Caches the user object.
After much trial and error, I was able to get a page working as a child of the user/%/edit path using code like this:
<?php
/**
* Implementation of hook_menu().
*/
function noc_profile_privacy_menu() {
return array(
'user/%user_category/edit/privacy' => array(
'title' => 'Portfolio privacy',
'page callback' => 'drupal_get_form',
'page arguments' => array('noc_profile_privacy_form', 1),
'access callback' => 'content_profile_page_access',
'access arguments' => array('profile', 1),
'type' => MENU_LOCAL_TASK,
'load arguments' => array('%map', '%index'),
),
);
}
/**
* Implementation of hook_user().
*/
function noc_profile_privacy_user($op, &$edit, &$account, $category = NULL) {
if ($op === 'categories') {
return array(array(
'name' => 'privacy',
'title' => t('Profile privacy'),
'weight' => 0,
));
}
}
Note that the 'name' key of what I'm returning in hook_user() is the same as what comes after user/%user/category/edit in my hook_menu() definition. I believe that is key. You will also get an error if you omit the 'load arguments' item, with exactly that value.
So I believe what the user category is is the 'privacy' in my case - the bit after edit in the path of the menu item definition.
Is this an unnecessary complication? Yes, so it seems.
Edit: I see my co-worker hefox beat me to replying to this question. I wouldn't have been able to figure all of this out without Fox's help, so mad props to the Fox.
Related
Been pulling my hair out over this for a day and exhausted my google foo. I have inherited a Silverstripe 3.4 site that we have upgraded to 4.4. But something odd has been going on with certain images after running MigrateFilesTask.
I think this is something to do with a file being attached to an unversioned objects that are accessed via ModelAdmin. But I have not been able to find a definitive solution.
Code for this object below. Problems experienced are under it.
<?php
use SilverStripe\Assets\Image;
use gorriecoe\Link\Models\Link;
use SilverStripe\Security\Member;
use SilverStripe\Control\Controller;
use SilverStripe\View\Parsers\URLSegmentFilter;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\FieldGroup;
use gorriecoe\LinkField\LinkField;
use SilverStripe\TagField\TagField;
use SilverStripe\ORM\DataObject;
use SilverStripe\SelectUpload\SelectUploadField;
class Person extends DataObject
{
private static $db = array(
'FirstName' => 'Varchar(128)',
'LastName' => 'Varchar(128)',
'Role' => 'Varchar(128)',
'DirectDialNumber' => 'Varchar(128)',
'Email' => 'Varchar(128)',
'CellphoneNumber' => 'Varchar(30)',
'DirectDial' => 'Varchar(30)',
'UrlSegment' => 'Varchar(255)',
'Blurb' => 'HTMLText',
'SortOrder' => 'Int'
);
private static $has_one = array(
'Image' => Image::class,
'Office' => 'Office',
'LinkedIn' => Link::class,
'Member' => Member::class
);
private static $many_many = array(
'Interests' => 'Section'
);
private static $belongs_many_many = array(
'ElementCollection' => 'ElementCollection'
);
static $sort_fields = array(
'FirstName' => 'First name',
'LastName' => 'Last name',
'Role' => 'Role'
);
private static $summary_fields = array(
'Name' => 'Name',
'Role' => 'Role',
'Office.Name' => 'Office'
);
private static $searchable_fields = array(
'FirstName',
'LastName',
'Role'
);
// For use with the ElementCollection
public static $templates = array(
'ElementPeople' => 'Default',
'ElementPeopleAlternative' => 'Alternative'
);
public function getCMSFields() {
$fields = parent::getCMSFields();
$fields->removeByName( ['SortOrder', 'ElementCollection', 'FirstName', 'LastName', 'Interests'] );
$firstname = TextField::create('FirstName', 'First name');
$lastname = TextField::create('LastName', 'Last name');
$fields->addFieldsToTab('Root.Main', FieldGroup::create($firstname, $lastname)->setTitle('Name')->setName('Name'), 'Role');
$image = UploadField::create('Image', 'Photo');
$image->setFolderName('Uploads/People');
$image->setCanSelectFolder(false);
$fields->addFieldToTab('Root.Main', $image);
$linkedin = LinkField::create('LinkedIn', 'LinkedIn', $this);
$fields->addFieldToTab('Root.Main', $linkedin);
$interests = TagField::create(
'Interests',
'Interests Tags',
Section::get(),
$this->Interests()
)->setShouldLazyLoad(true)
->setCanCreate(false);
$fields->addFieldToTab('Root.Main', $interests);
return $fields;
}
public function onBeforeWrite()
{
$count = 1;
$this->UrlSegment = $this->generateURLSegment();
while (!$this->validURLSegment()) {
$this->UrlSegment = preg_replace('/-[0-9]+$/', null, $this->UrlSegment) . '-' . $count;
$count++;
}
parent::onBeforeWrite();
}
}
Problem #1 is after running MigrateFileTask, ALL existing images attached to instances of this class get moved from /assets/Uploads/People to /assets/.protected/Uploads/People. The confusing part here is that there is one other class called Company that is structurally near identical, yet images for that remain in /assets/Uploads/Companies as expected.
Problem #2 is if I create a new Person object and attach an image, that image is in Draft, sitting in /assets/.protected/Uploads/People with no method of actually publishing it. Meanwhile, if I do the same with a Company object, the image is still in Draft, but I can see it in the CMS.
Can someone offer some guidance on the above? At this point I'd be happy to just be able for images to be published when the DO is and I'll manually go through every single Person record and hit save myself just to get this upgrade over the line.
You should be able to fix this issue by adding the image to your DataObejct's owns property. Basically add this:
private static $owns = [
'Image'
];
Basically owns tells a DataObject which objects to publish when it is saved:
More info in the docs: https://docs.silverstripe.org/en/4/developer_guides/model/versioning/#defining-ownership-between-related-versioned-dataobjects
The cause of issue #1 was found. Leaving this here in case it helps someone in future:
The database table File has a row for every File and Folder in the system. This table has a column called "CanViewType". It exists in both Silverstripe 3 and 4.
For the particular Folder that was causing trouble during the Migration process, I found it was the only one with that column set to "OnlyTheseUsers". The rest were set to "Inherit". This was the state of the table before the upgrade.
I'm unsure how or by what mechanism that row is ever changed, but the solution to problem #1 was to manually change that field to "Inherit" before running FileMigrationTask.
Issue #2 persists, but it looks like there are two very different issues here.
OK. So sorted problem #2 finally (see other answer for solution to #1), but it's put a massive dent in our confidence in Silverstripe and sparked a meeting here.
Code for future readers:
In your unversioned DataObject, add this. In this case, my file object is called "Image". If you had more than one file to publish on save, you would have to add one of these IF blocks for each.
public function onAfterWrite()
{
if ($this->Image()->exists() && !$this->Image()->isPublished()) {
$this->Image()->doPublish();
}
parent::onAfterWrite();
}
SIDENOTE:
This Object/File relationship really is a strange design choice. Are there actually any situations where you would want to attach a file or image to a data object and NOT publish that file at the same time as you save/publish the object/page? Developers even need to explicitly define that on Versioned objects using $owns - which I'm happy to bet that most developers have to add more times than NOT. Which should really tell a us something is around wrong way.
Adding an image to a CMS system shouldn't be hard. It should take reading basic docs at the most. Not Googling, deep API doc dives (which don't answer much) or posting on StackOverlfow (where no one really knows the answer) over three days. It's an image. A core function of the product.
I've been working with SS since v2.4 and seen all the hard lessons learned to get to v4. But this appears to be a textbook case of the simple being over-engineered.
I'm starting to work with Drupal and I'm really confused as how to create a hook_menu function that allows you to register a URL with 1 or 2 different values, that can be hidden and not displayed in the breadcrumbs.
Any help on this would be much appreciated. Even an example.
Not sure about the breadcrumbs bit, but I think you're looking for wildcard (%) and auto-loader wildcard (%mymodule_entity) components in the path.
From the hook_menu() page...
Wildcards within paths also work with integer substitution. For example, your module could register path 'my-module/%/edit'. When path 'my-module/foo/edit' is requested, integer 1 will be replaced with 'foo' and passed to the callback function. Note that wildcards may not be used as the first component.
$items['my-module/%/edit'] = array(
'page callback' => 'mymodule_abc_edit',
'page arguments' => array(1),
);
Registered paths may also contain special "auto-loader" wildcard components in the form of '%mymodule_abc', where the '%' part means that this path component is a wildcard, and the 'mymodule_abc' part defines the prefix for a load function, which here would be named mymodule_abc_load().
$items['my-module/%mymodule_abc/edit'] = array(
'page callback' => 'mymodule_abc_edit',
'page arguments' => array(1),
);
In the context of organic groups, I am writing a module which will stop users who are not members of a group from adding group posts into that group.
My module currently sets the permissions necessary and detects whether a user has the permission.
So when a user(s) are looking at a group page, I want to disable/remove the standard link to create group posts.
Try this method.
function mymodule_menu_alter(&$items) {
global $user;
// Perform code for finding out users permissions.
// lets suppose we set true or false to $restricted after all
if ($restricted && isset($items['node/add/yourtype'])) {
$items['node/add/yourtype']['access arguments'] = FALSE;
// or unset($items['node/add/yourtype']) to remove item for user
}
}
If I understood right you don't want certain users to create a content type.
So the steps are:
1) Create a menu hook.
// Here we make sure if the user goes to for creating this node type
// we can use the appropriate call back function to stop it.
function yourmodoule_menu() {
$items = array();
$items['node/add/page'] = array(
'page arguments' => array('yourmodule_additional_actions'),
'access arguments' => array('administer create content')
);
}
2) Then make a permission hook to make sure only certain users have this permission.
// drupal will only allow access to to path 'node/add/page' with people
// who have access given by you.
function yourmodule_permission() {
return array(
'add content' => array(
'title' => t('Administer create conent'),
'description' => t('Perform administration tasks and create content')
)
)
}
3) Write your code for those users who have the permission.
// Only affter they have this permisson drupal will allow them access
// to the below function.
function yourmodule_additional_actions() {
// this code will only execute if the user has the permission
// "Administer create conent"
}
Does anyone know what the best way would be solve this:
I've got a custom content type that you can view i.e via
http://site.com/node/8
The custom content type has a field with a url (Example www.google.com)
What I'm trying to do is to create a mechanism that will automatically redirect the browser to www.google.com (the field property) when I enter a url like
http://site.com/node/8/go
I tried using the Path and AutoPath modules, but couldn't get a redirect working
The shortest implementation:
<?php
function YOURMODULE_menu() {
$items = array();
$items['node/%node/go'] = array(
'page callback' => 'YOURMODULE_redirect',
'page arguments' => array(1),
'access arguments' => array('access content'),
);
return $items;
}
function YOURMODULE_redirect($node) {
if ($node->type == 'YOUR_TYPE' && isset($node->field_YOURFIELD[0]['value']) && $node->field_YOURFIELD[0]['value']) {
drupal_goto($node->field_YOURFIELD[0]['value']);
}
}
Edit: It would be advised to add some validation and probablty other stuff, but I believe this is quite obvious. And also, depends on details of your implementation.
You can use field_redirection module .
You can use Rabbit hole and Token modules. With Rabbit hole you will be able to set a Page Redirect for the content type, and with Token you can choose a field where users will be redirected when they try to access to a node.
If a user is logged in as admin, it works fine but if user is logged in, it is sending them their profile page. Why?. What files can I check?
$items['go/to/school'] = array(
'title' => 'Some page Title',
'page callback' => 'my_function',
'access callback' => 'my_access',
'type' => MENU_CALLBACK
);
function my_function() {
echo "WHATS UP"; //NEVER SHOWS UP
}
Your callback function myaccess() must return TRUE for that user, else that user has no access. This callback function can get arguments trough access arguments. When you do not provide the access callback it defaults to function user_access($access_string), in which case you still need to provide access arguments, e.g. "access content".
Also note, that after each change in the hook_menu-code you must refresh the menu-cache, since this is cached quit heavily.
If you would like this page to be visible to all users (logged-in or anonymous), the simplest way is to just return TRUE in your access callback. For example:
'access callback' => TRUE,
Otherwise, like berkes said, your access callback must return TRUE for that user to see the page. For example:
function my_access() {
global $user;
return in_array('authenticated user', $user->roles);
}
This will return TRUE if the user has a role of "authenticated user" and FALSE if they do not.