TYPO3 performs access control to elements in the frontend by a set of standard criterias - socalled "enablefields". These include the possibility for hidden, starttime, endtime and fe_group filtering.
When you want to restrict access to a content element on your website so only certain people can see it you must create frontend users and assign member groups to those users. The access to a single element is then controlled by selecting a specific user group for which the element is visible only. If a user is not logged in or not a member of the selected group he will not get to see the element!
However the limitation is that only one group can be selected! What if we want to allow access for multiple groups?
Well this is not possible directly but requires a little fiddling with the source code of the "cms" extension of TYPO3. This is what we will work on in this part of the tutorial.
Skill level
In order to complete this level you should be an advanced TYPO3 user and PHP expert. You should be a developer.
"enablefields" are fields in a table which holds the value for either hidden/visible, start time, end time or user group access. The fields are pointed to by an entry in the "ctrl" section for tables in $TCA. For instance the configuration for the table "tt_content" looks like this:
$TCA['tt_content'] = Array (
'ctrl' => Array (
'label' => 'header',
'label_alt' => 'subheader,bodytext',
'sortby' => 'sorting',
'tstamp' => 'tstamp',
'title' => 'LLL:EXT:cms/locallang_tca.php:tt_content',
'delete' => 'deleted',
'type' => 'CType',
'prependAtCopy' => 'LLL:EXT:lang/locallang_general.php:LGL.prependAtCopy',
'copyAfterDuplFields' => 'colPos,sys_language_uid',
'useColumnsForDefaultValues' => 'colPos,sys_language_uid',
'enablecolumns' => Array (
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
'fe_group' => 'fe_group',
),
. . .
"enablefields" are used only in the frontend since they are all about frontend access to elements - not backend access! So hidden records, records with start and end times or access restriction will always be visible for backend users.
The "delete" key of the "ctrl" section is however also included in the filtering for "enable fields" but that feature is valid for both the frontend and backend. In fact TYPO3 is not allowed to recognize a record with the "deleted" field set!
Filtering out "disabled records"
Whenever records from tables configured in $TCA are selected in the context of the frontend for display on webpages the function enableFields() is called (should be!) with the table name as argument. It might look like this:
. . .
} else {$query="FROM ".$table." WHERE pid IN (".$pidList.")".chr(10).$this->cObj->enableFields($table);
}
. . .
$this->cObj is normally available in plugins and is the parent object which is an instance of the class "tslib_cObj" from the file "class.tslib_content.php".
So in tslib/class.tslib_content.php we will find the function enableFields() - but just to see it act as a simple wrapper for the function $GLOBALS["TSFE"]->sys_page->enableFields()
$TSFE is an instance of the class "tslib_fe" and looking inside of "tslib/class.tslib_fe.php" we will find that $GLOBALS["TSFE"]->sys_page is an instance of the class "t3lib_pageSelect".
The class "t3lib_pageSelect" is found in "t3lib/class.t3lib_page.php". Now we should be there:
function enableFields($table,$show_hidden=-1,$ignore_array=array()) { if ($show_hidden==-1 && is_object($GLOBALS["TSFE"])) { $show_hidden = $table=="pages" ? $GLOBALS["TSFE"]->showHiddenPage : $GLOBALS["TSFE"]->showHiddenRecords; } if ($show_hidden==-1) $show_hidden=0; $ctrl = $GLOBALS["TCA"][$table]["ctrl"]; $query=""; if (is_array($ctrl)) { if ($ctrl["delete"]) { $query.=" AND NOT ".$table.".".$ctrl["delete"]; } if (is_array($ctrl["enablecolumns"])) { if ($ctrl["enablecolumns"]["disabled"] && !$show_hidden && !$ignore_array["disabled"]) { $field = $table.".".$ctrl["enablecolumns"]["disabled"]; $query.=" AND NOT ".$field; } if ($ctrl["enablecolumns"]["starttime"] && !$ignore_array["starttime"]) { $field = $table.".".$ctrl["enablecolumns"]["starttime"]; $query.=" AND (".$field."<=".$GLOBALS["SIM_EXEC_TIME"].")"; } if ($ctrl["enablecolumns"]["endtime"] && !$ignore_array["endtime"]) { $field = $table.".".$ctrl["enablecolumns"]["endtime"]; $query.=" AND (".$field."=0 OR ".$field.">".$GLOBALS["SIM_EXEC_TIME"].")"; } if ($ctrl["enablecolumns"]["fe_group"] && !$ignore_array["fe_group"]) { $field = $table.".".$ctrl["enablecolumns"]["fe_group"]; $gr_list = $GLOBALS["TSFE"]->gr_list; if (!strcmp($gr_list,"")) $gr_list=0; $query.=" AND ".$field." IN (".$gr_list.")"; } } } else {die ("NO entry in the \$TCA-array for '".$table."'");} return $query;}
This function simply looks in the "ctrl" section of $TCA for the table and will put together the part of the WHERE clause that will filter out all records that should be hidden according to the settings for the enablefields, if any.
This is the general theory.
Since we are going to implement our own access control for user groups we will apparently have to either extend or totally replace the function enableFields() from the class "t3lib_pageSelect". So it should not be wasted time to create an extension class for it.
But first, lets create a new, user defined extension by hand:
User defined extension
1: Create a directory in typo3conf/ext/ named "user_accessctrl"
2: Create a file named "ext_emconf.php" in "user_accessctrl/":
<?php$EM_CONF[$_EXTKEY] = Array ( 'title' => 'New Access Control', 'version' => '0.0.0');?>
3: Create a file named "ext_localconf.php" in "user_accessctrl/":
<?php$TYPO3_CONF_VARS["FE"]["XCLASS"]["t3lib/class.t3lib_page.php"] = t3lib_extMgm:extPath('user_accessctrl').'class.ux_t3lib_pageSelect.php';?>4: Create a file named "class.ux_t3lib_pageSelect.php" in "user_accessctrl/":
<?phpclass ux_t3lib_pageSelect extends t3lib_pageSelect { /** * Extending function for enableFields() */ function enableFields($table,$show_hidden=-1,$ignore_array=array()) { // Call parent function (the original!) $return_value = parent::enableFields($table,$show_hidden,$ignore_array); // Output the value so we can see what is produced: t3lib_div::debug(array($return_value)); // Return the value: return $return_value; }}?>Then go to the Extension Manager and install the extension:
... Oups:
Parse error: parse error in /www/htdocs/typo3/32/dummy_tut/typo3conf/temp_CACHED_pseed3_ext_localconf.php on line 121Typo3 Fatal Error: Extension key "cms" was NOT loaded!
The solution to this problem is a) not to panic and then b) open the file "temp_CACHED_pseed3_ext_localconf.php" and find the problem:
Apparently a ":" (colon) was missing! So...
a) we add the colon here so the file does not generate a parse error and
b) we add the colon in the source file located in ... user_accessctrl/ext_localconf.php!
(The weird thing is that line 121 had nothing to do with this problem...)
Anyways, after this little intermezzo our extension should be installed and if we clear the cache and hit the frontend of page "License C" we should see something like this:
This debug information was outputted by the function t3lib_div::debug() which was found in our extension function of the enableFields() function.
The WHERE clause
As you can see from the screen dump the enableFields() function was called three times for the "tt_content" table - that is reasonable since we insert page content elements three times on this page!
The WHERE clause returned by parent::enableFields() is this:
AND NOT tt_content.deleted
AND NOT tt_content.hidden
AND (tt_content.starttime<=1047535575)
AND (tt_content.endtime=0 OR tt_content.endtime>1047535575)
AND tt_content.fe_group IN (0,-1)
The first line checks for the mandatory "deleted" field, the other four checks for the hidden, starttime, endtime and fe_groups.
Removing the check for "fe_group"
Now, add this file, "ext_tables.php", to the extension:
<?phpt3lib_div::loadTCA('tt_content');unset($TCA['tt_content']['ctrl']['enablecolumns']['fe_group']);?>
Clear all cache, clear cache files and reload the frontend. The WHERE clause for tt_content elements is now reduced to:
AND NOT tt_content.deleted
AND NOT tt_content.hidden
AND (tt_content.starttime<=1047536630)
AND (tt_content.endtime=0 OR tt_content.endtime>1047536630)
As you can see the check for the "tt_content.fe_group" field is not included anymore since we removed the definition of this column from the "enablecolumns" key of the "ctrl" section for the table "tt_content".
Now we are ready to change the fe_group field - the "Access" selector box - to a multiple select field. We will do two things:
manipulate the entry for "fe_group" in the "columns" section of $TCA['tt_content']
re-configure the field from a integer to a varchar(100)
First add this to the ext_tables.php file (red lines):
<?php
t3lib_div::loadTCA('tt_content');unset($TCA['tt_content']['ctrl']['enablecolumns']['fe_group']);
unset($TCA['tt_content']['columns']['fe_group']['config']['items']);
$TCA['tt_content']['columns']['fe_group']['config']['size']=5;
$TCA['tt_content']['columns']['fe_group']['config']['maxitems']=20;
?>
Then create the file "ext_tables.sql" and add this content:
# Redefining the fe_group field:
CREATE TABLE tt_content (
fe_group varchar(100) DEFAULT '' NOT NULL
);
Now go to the Extension Manager, click the "user_accessctrl" extension and you should see this form:
Press "Make updates".
Then go and edit a page content element:
As you can see we have successfully redefined the field so that more than one group can be selected. Try and add a few groups.
If you look at the field value when more than one group is added you will see that it is a comma list of the uids of the groups, eg. "2,1". This is what we must select on if we want to have multiple group access control.
Now we are ready to create the logic for our new "enablecolumn" type. In fact we should give it a name and use the "enablecolumns" array to activate it. Thus we can use it not only for content elements but for any element in the database (except "pages" which needs more complicated manipulation).
Add this line to ext_tables.php:
<?php
t3lib_div::loadTCA('tt_content');unset($TCA['tt_content']['ctrl']['enablecolumns']['fe_group']);
unset($TCA['tt_content']['columns']['fe_group']['config']['items']);
$TCA['tt_content']['columns']['fe_group']['config']['size']=5;
$TCA['tt_content']['columns']['fe_group']['config']['maxitems']=20;
$TCA['tt_content']['ctrl']['enablecolumns']['user_accessctrl_multigroup'] = 'fe_group';
?>
All there is left now is to program the extended enableFields() function:
<?phpclass ux_t3lib_pageSelect extends t3lib_pageSelect { /** * Extending function for enableFields() */ function enableFields($table,$show_hidden=-1,$ignore_array=array()) { global $TCA; // Call parent function (the original!) $return_value = parent::enableFields($table,$show_hidden,$ignore_array); // Check for our custom enable-column, "user_accessctrl_multigroup": if (is_array($TCA[$table]) && $TCA[$table]['ctrl']['enablecolumns']['user_accessctrl_multigroup']) { $field = $table.'.'.$TCA[$table]['ctrl']['enablecolumns']['user_accessctrl_multigroup']; $orChecks=array(); $orChecks[]=$field.'=""'; // If the field is empty, then OK $orChecks[]=$field.'="0"'; // If the field is empty, then OK $memberGroups = t3lib_div::intExplode(",",$GLOBALS['TSFE']->gr_list); foreach($memberGroups as $value) { if ($value > 0) { // If user is member of a real group, not zero or negative pseudo group $orChecks[]='('.$field.' LIKE "%,'.$value.',%" OR '. $field.' LIKE "'.$value.',%" OR '. $field.' LIKE "%,'.$value.'" OR '. $field.'="'.$value.'")'; } } $return_value.=' AND ('.implode(' OR ',$orChecks).')'; }#t3lib_div::debug(array($return_value)); // Return the value: return $return_value; }}?>
This is what it does:
First, call the parent function to get the enableFields clause for the standard fields!
Then, check if a field is configured for the key "user_accessctrl_multigroup"
If so, create a big OR statement where all field values equal to "" (blank string) or 0 (zero) will always be selected plus all fields which has one of the positive group ids of the current user (if any) in the list or groups.
The result from this should be a query like this provided that the current user is member of the groups with uid "1" and "2" (value found in $TSFE->gr_list):
AND NOT tt_content.deleted
AND NOT tt_content.hidden
AND (tt_content.starttime<=1047539324)
AND (tt_content.endtime=0 OR tt_content.endtime>1047539324)
AND (
tt_content.fe_group="" OR
tt_content.fe_group="0" OR
(tt_content.fe_group LIKE "%,1,%" OR
tt_content.fe_group LIKE "1,%" OR
tt_content.fe_group LIKE "%,1" OR
tt_content.fe_group="1"
) OR
(tt_content.fe_group LIKE "%,2,%" OR
tt_content.fe_group LIKE "2,%" OR
tt_content.fe_group LIKE "%,2" OR
tt_content.fe_group="2"
)
(The extension "user_accessctrl" can be found as the file "T3X_user_accessctrl-0_0_0.t3x" in the "part3/" folder of the tutorial extension.)
You might ask if it's possible to restrict access to pages by selecting on users instead of user groups. The answer is "no" - unless you disable caching!
The problem is that caching of pages is based on a hash string which includes the id, the type and the gr_list parameters (plus a little more) and therefore you can only make cached access restrictions based on user group combinations and nothing more. The only way to do this differently is to disable caching for the pages. Probably if you want user based access you really want to make some application on a page which serves user-specific content - that is totally different and can be done by creating a plugin which inserts a non-cached cObject on a page.
This extension will work for all other tables as well, except the "pages" table. Well, if the function is used to get the enableFields for the pages table it will work correctly. But the pages table is a little special because the object $GLOBALS["TSFE"]->sys_page contains a lot of functions selecting pages for menus etc. without using the enableFields() function but rather by the internal variable $this->where_hid_del.
You will therefore have to control the $this->where_hid_del variable and do so even at a very early stage in its existence if you want your custom access field taken into consideration when the current page and it's visibility is evaluated.
The ->where_hid_del variable is set as a part of the initialization of the $GLOBALS["TSFE"]->sys_page object inside the class/method tslib_fe::fetch_the_id().
Have fun.