Today I had the problem of many people (believing forum posts left unanswered for years) :
How can I replace the code that runs behind a template operator, to avoid override all my templates?
How can I trigger this operator substitution directly from another operator, live in the template? (like an on / off switch...)
How to have absolute URLs in all templates calling "ezurl" without modifying the template code?!?
I finally have the answer! A technical solution that does not change anything in the kernel, which can be done in a small independent extension and which does not break ANYTHING until you have decided to activate the operator substitution! It's clean, it's fast, a little "on" at the beginning of the template, a little "off" at the end to clean, and all templates that will be called between them will use your own operator code!
The principle is simple (at first): simply declare (registerOperators) an operator that has the same name as the one that wants to replace ... Easy ... but as a simple solution does not always work (depending on the order in which extensions are loaded, and therefore WHO has the last word on the declaration of the operator ...), it's about making a register live in the template (switch to "on": to force dynamic declaration). It is also possible to reverse the process to return to the native operator. This gives the following code:
BEFORE : {'/user/login'|ezurl()} {* relative url... *}
WITH SUBSTITUTION :
{sethacks()}... {* switch on substitution *}
{'/user/login'|ezurl()} {* absolute url, or custom result *}
{unsethacks()}... {* switch off substitution *}
AFTER : {'/user/login'|ezurl()} {* relative url again... *}Just between sethacks() and unsethacks(), I call any full view of my content, and the template operator (ezurl in my example) is the one I have recoded in my extension!
Well now, how this works?
Suppose you can do an extension with template operators (if you cannot, lean on known examples, as ezwebin ...).
In your eztemplateautoload.php file, you will get this :
$eZTemplateOperatorArray = array(); $eZTemplateOperatorArray[] = array( 'script' => 'extension/my_extension/autoloads/hackedoperators.php', 'class' => 'HackedOperators', 'operator_names' => array( 'sethacks' // NOTHING MORE HERE !!! ) );
You'll notice here, I do declare only a single operator, which allows the switch to initiate the process...
in your template operators file (hackedoperators.php for example), you will get this :
class HackedOperators {
function HackedOperators() {
$this->Operators = array(
'sethacks', 'unsethacks',
'ezurl' // HERE, FULL DECLARATION !
);
$this->NamedOperators = array(
'sethacks'=> array( ),
'unsethacks'=> array( ),
'ezurl'=> array(
'quote' => array( 'type'=>'string',
'required'=>false,'default'=>"double"),
'type' => array( 'type'=>'string',
'required'=>false,'default'=>"full"),
)
);
}
function modify($tpl, $operatorName, $operatorParameters,
$rootNamespace, $currentNamespace, &$operatorValue,
$namedParameters) {
switch ( $operatorName ) {
case 'sethacks':
$tpl->registerOperators($this); // force set for all ops
break;
case 'unsethacks':
$tpl->registerOperators(new eZURLOperator()); // reset ops
// complete here with all reset instructions
break;
case 'ezurl':
$this->ezurl($operatorValue, $namedParameters);
break;
default :
$tpl->warning( $operatorName, "Unknown operator" );
break;
}
}
function ezurl(&$value, $namedParameters) {
// HERE, PUT YOUR NEW EZURL CODE.
}
function operatorList() {
return $this->Operators;
}
function namedParameterPerOperator() {
return true;
}
function namedParameterList() {
return $this->NamedOperators;
}
}That's it!
Enjoy.
Each time a common module is called (like "content" or other simple modules, like AJAX ones), and so every time a content page, or an AJAX module in an extension is called our dear kernel (/index.php) updates a session variable called: LastAccessesURI.
This variable is used when leaving edit mode, to return to the previous page, or when a log is finished, to return to the URL that was requested before having to log ...
This is annoying because chronologically, when a person displays a page in the front office containing an AJAX call to an eZpublish extension, these steps are followed:
- URL of the desired page requested by the user -> URI set in LastAccessesURI
- URL of an AJAX module called by the browser -> AJAX URI set in LastAccessesURI
So, because the last request to eZpublish is an AJAX one, to an AJAX module, the wrong URI will be used when the user exits from edit mode, or is redirected after a successful /user/login, (or another module using LastAccessesURI variable), and then, the user is redirected to the AJAX module in full view, which is not the last page the user saw.
Fortunately, eZpublish is great (when you read directly the kernel code): we can declare our module in a particular way to prevent the LastAccessesURI variable to be updated. In the kernel, in index.php, a condition protects an update to the LastAccessesURI variable:
if ( ... && !in_array( $module->uiContextName(),
array('edit','administration','browse','authentication')
)) {
$http->setSessionVariable("LastAccessesURI",
$currentURI);
}So now, you can redeclare your AJAX module like an admin module, and that's all!
In your extension/.../modules/.../module.php, use the following code:
$ViewList['get'] = array(
'script' => 'get.php',
'ui_context' => 'administration'
); Regarding to a recent post I put here about Kerberos and Apache, there is a way to replace Kerberos when the Active Directory cannot be properly configured to accept Kerberos connections. You can simulate the NTLM auth process with the browser by 6 steps in PHP :
function get_login() {
/*
step: | type:
-------|----------------|------------------------------------
1 | C --> S | GET ...
-------|----------------|------------------------------------
2 | C <-- S | 401 Unauthorized
| | WWW-Authenticate: NTLM
-------|----------------|------------------------------------
3 | C --> S | GET ...
| | Authorization: NTLM
| | <base64-encoded type-1-message>
-------|----------------|------------------------------------
4 | C <-- S | 401 Unauthorized
| | WWW-Authenticate: NTLM
| | <base64-encoded type-2-message>
-------|----------------|------------------------------------
5 | C --> S | GET ...
| | Authorization: NTLM
<base64-encoded type-3-message>
-------|----------------|------------------------------------
6 | C <-- S | 200 Ok
-------|----------------|------------------------------------
*/
$headers = apache_request_headers();
if($headers['Authorization'] == NULL) { // step 1
header( "HTTP/1.1 401 Unauthorized" ); // step 2
header( "WWW-Authenticate: NTLM" );
exit;
};
if(isset($headers['Authorization'])
&& substr($headers['Authorization'],0,5) == 'NTLM ') {
// step 3 to 6
$chaine=$headers['Authorization'];
$chaine=substr($chaine, 5); // type1 message
$chained64=base64_decode($chaine);
if(ord($chained64{8}) == 1) { // step 3
// check NTLM flag "0xb2",
// offset 13 in type-1-message :
if (ord($chained64[13]) != 178) {
echo "Please use NTLM compatible browser";
return null;
}
$retAuth = "NTLMSSP";
$retAuth .= chr(0).chr(2).chr(0).chr(0);
$retAuth .= chr(0).chr(0).chr(0).chr(0);
$retAuth .= chr(0).chr(40).chr(0).chr(0);
$retAuth .= chr(0).chr(1).chr(130).chr(0);
$retAuth .= chr(0).chr(0).chr(2).chr(2);
$retAuth .= chr(2).chr(0).chr(0).chr(0);
$retAuth .= chr(0).chr(0).chr(0).chr(0);
$retAuth .= chr(0).chr(0).chr(0).chr(0).chr(0);
$retAuth64 =base64_encode($retAuth);
$retAuth64 = trim($retAuth64);
header( "HTTP/1.1 401 Unauthorized" ); // step 4
header( "WWW-Authenticate: NTLM $retAuth64" );
exit;
}
else if(ord($chained64{8}) == 3) { // step 5
$lenght_domain = (ord($chained64[31])*256 + ord($chained64[30]));
$offset_domain = (ord($chained64[33])*256 + ord($chained64[32]));
$domain = substr($chained64, $offset_domain, $lenght_domain);
$lenght_login = (ord($chained64[39])*256 + ord($chained64[38]));
$offset_login = (ord($chained64[41])*256 + ord($chained64[40]));
$login = substr($chained64, $offset_login, $lenght_login);
$lenght_host = (ord($chained64[47])*256 + ord($chained64[46]));
$offset_host = (ord($chained64[49])*256 + ord($chained64[48]));
$host = substr($chained64, $offset_host, $lenght_host);
}
}
$login = preg_replace("/(.)(.)/","$1",$login);
$domain = preg_replace("/(.)(.)/","$1",$domain);
$login = strtolower($login);
$domain = strtoupper($domain);
return array($login,$domain); // step 6 : accept
}
Warning : this code must be exectuted not only for the auth process when you want to login the user, but for each HTTP request to your application (so on each page).
It is very important to put HTTP 1.1 protocole in HTTP headers because HTTP 1.0 does not support Keep-alive connection and so NTLM auth. For some unknown reasons, even if some Apache versions change automatically the protocole version to 1.1 in your headers, other versions don't (for example 2.2.3 can do it and 2.2.9 can't).
Finally, keep in mind that this code is not as secure as Kerberos Apache module, because this code will never check Active Directory permission to accept the user. This function is just to retrieve login and domain, you have to make the account check yourself.
Good luck !
This week I had to make an eZpublish web site with an SSO authentication under eZpublish (NTLM). An SSO login handler is required. This login handler is executed by PHP and so is preceded by Apache authentication, with Kerberos Apache module.
The first problem comes when you want to let Kerberos authenticate the user (to give user data to PHP) or bypass authentication module if Kerberos cannot identify the user, to let eZpublish authenticate the user itself, with a classical form or another login handler : this parameter is not possible under Apache Auth modules, with a classical configuration like this:
<Directory /var/www/ezpublish>
AuthType Kerberos
KrbAuthRealms WASCOU.ORG
KrbServiceName HTTP
Krb5Keytab /root/wascou.keytab
KrbMethodNegotiate on
KrbMethodK5Passwd off
Require valid-user
Options All
</Directory>
The "Require valid-user" line will disallow site access until the user is not authenticated under Apache Kerberos module. This is the problem for users that need to be logged in with the classical form on eZpublish. Unfortunately there is no instruction to tell Kerberos to let a bypass in failure case (like with Basic or Digest modules, the same): Apache will give a HTTP 401 error, which is quite logical.
I suppose you know that eZpublish can call the user/login module from any URL that brings to a protected content: according to the user rights, a login form could be shown, and before this, a SSO login can be called. So there no way to indicate clearly to apache when eZpublish needs to login the user (to activate Kerberos authentication): telling "/user/login" URL is the only login URL is a mistake.
So, the solution is to play with well built Apache and PHP redirections, that the user cannot see, to call Kerberos module only when eZpublish needs it. Firstly, we will replace the above configuration with the following one, contained in a Location section, much more appropriated in our case :
<Location /ntlm/auth>
AuthType Kerberos
KrbAuthRealms WASCOU.ORG
KrbServiceName HTTP
Krb5Keytab /root/wascou.keytab
KrbMethodNegotiate on
KrbMethodK5Passwd off
Require valid-user
Options All
ErrorDocument 401 /user/login
</Location>
Notice that the "/ntlm/auth" URL could bring us to an eZpublish module: this module must exist (you have to create it), but the PHP script behind this will never been executed and could remain empty (Apache and eZpublish will make redirections before this execution, see next step). Also notice the "ErrorDocument 401 /user/login" line, that will redirect the user if Kerberos cannot authenticate the user (and only for the "/ntlm/auth" URL!).
The big tip is here: if Kerberos cannot authenticate the user, it must redirect to an eZpublish page. The "/user/login" is an arbitrary choice, because the SSO login handler will make redirections before the execution of the user/login script (see next step).
Now, you have to make your SSO login handler, playing with all needed redirections, to manage correctly all possible bounds.
The following SSO login handler is a complete example:
function handleSSOLogin() {
$ip = $_SERVER["REMOTE_ADDR"];
$net = $ini->variable( 'NTLMSettings', 'net' );
$mask = $ini->variable( 'NTLMSettings', 'mask' );
// tip: (net & mask) == (ip & mask) : ok!
if ((ip2long($net)&ip2long($mask))==(ip2long($ip)&ip2long($mask))) {
// 2nd case : /ntlm/auth redirected to first URL, to auth under PHP.
if ($_SESSION['ntlm_success']=="success") {
if ( array_key_exists( 'REMOTE_USER', $_SESSION )
&& array_key_exists( 'AUTH_TYPE', $_SESSION ) ) {
$remoteUser = $_SESSION['REMOTE_USER'];
$authType = $_SESSION['AUTH_TYPE'];
eZDebug::writeDebug('#25# user:'.$remoteUser,'');
$loginParts = explode( '@', $remoteUser );
$loginName = $loginParts[0];
// main call of YOUR User handler in NTLM mode
$user = LOGINCLASS::loginUser($loginName);
if ( is_object( $user ) ) {
return $user;
} else {
eZDebug::writeDebug('#36# Unable to fetch user','');
unset($_SESSION['REMOTE_USER']);
unset($_SESSION['AUTH_TYPE']);
}
} else {
eZDebug::writeDebug('#39# No sso auth performed','');
unset($_SESSION['REMOTE_USER']);
unset($_SESSION['AUTH_TYPE']);
}
$_SESSION['ntlm_success'] = "failed";
return false;
}
// first case : sso_handler redirection to /ntlm/auth.
if ($_SERVER['SCRIPT_URL'] == '/ntlm/auth') {
eZDebug::writeDebug('#47# IP on domain, Kerberos OK.','');
if (!$_SESSION['ntlm_url']) {
echo 'Cookies or/and Sessions are not activated.<br/>';
eZExecution::cleanExit();
}
$ntlm_url = $_SESSION['ntlm_url'];
unset($_SESSION['ntlm_url']);
$_SESSION['ntlm_success'] = "success";
$_SESSION['REMOTE_USER'] = $_SERVER['REMOTE_USER'];
$_SESSION['AUTH_TYPE'] = $_SERVER['AUTH_TYPE'];
eZHTTPTool::redirect($ntlm_url);
eZExecution::cleanExit();
} else if ($_SESSION['ntlm_success'] != "failed") {
eZDebug::writeDebug('#59# IP on domain, checking NTLM.','');
$_SESSION['ntlm_url']=$_SERVER['SCRIPT_URL'];
eZHTTPTool::redirect('/ntlm/auth');
eZExecution::cleanExit();
} else {
eZDebug::writeDebug('#64# IP on domain, Kerberos failed.','');
}
} else eZDebug::writeDebug('#67# IP not on domain, Stop.','');
return false;
}
This script will process like this:
- First, a test is executed to ensure we are on the right domain (by mask and IP)
- Then a redirection is done to /ntlm/auth ; we store the original URL typed in the SESSION
- When /ntlm/auth is called, apache Kerberos module will try to authenticate user
- if failed, an Apache redirection (by "ErrorDocument 401" param) is done to /user/login
- in this case, our SSO login handler will redirect to the original URL typed by the user, with a failed state for authentication (stored in the session)
- the next login handler will try an authentication with a form...
- if succeeded, our SSO login handler will store user login (given by Kerberos under $_SESSION['REMOTE_USER']) and will redirect to the original URL typed by the user,
- on this new URL, our SSO login handler will authenticate the given user in the database (you have to implement "LOGINCLASS::loginUser($loginName);" line by yourself)
- in case of success, login process is terminated, the $user is returned.
- in case of failure, false is returned to let next login handler try an authentication with a form...
If you have any question, comment this post!
Good luck!
Hi, today, I want to share a good tip to have access to an object attribute of an unknown class from eZpublish templates. This case is rare, but when you have an object with unknown attribute, or from an unknown PHP class, and you want to access an attribute by its name, if the object does not extends a datatype, the classic "object.attribute" operator in template language cannot work. The problem is that this operator call methods like attributes(), hasAttribute($attr) or attribute($attr) to explore the attributes. In your case, the object has no such methods, you cannot modify the class to add them (for some reasons, for example if the object comes from PHP SOAP), and then you need a wrapper to simulate a datatype.
I propose a DummyDatatype wrapper class, used by a template operator, like this :
class DummyDataType {
var $Object = false;
function DummyDataType($object = false) {
$this->Operators = array( 'ddt' );
$this->NamedOperators = array(
'ddt'=> array( 'attribut' => array(
'type' => 'Object',
'required' => true,
'default' => null))
);
$this->Object = $object;
}
function modify( $tpl, $operatorName, $operatorParameters,
$rootNamespace, $currentNamespace,
&$operatorValue, $namedParameters) {
switch ( $operatorName ) {
case 'ddt':
$this->Object = ($operatorValue != null)?
$operatorValue : $namedParameters['attribut'];
$operatorValue = $this;
break;
default :
$tpl->warning($operatorName,
"Unknown operator '$operatorName'");
break;
}
}
function operatorList() {
return $this->Operators;
}
function namedParameterPerOperator() {
return true;
}
function namedParameterList() {
return $this->NamedOperators;
}
function attributes() {
$obj = $this->Object;
$result = array();
foreach($obj as $key => $value) {
$result[] = $key;
}
return $result;
//TODO: test this in all cases (string,integer,...)
}
function hasAttribute( $attr ) {
if ($this->Object == false) return false;
return isset( $this->Object->$attr );
}
function attribute( $attr ) {
if (isset( $this->Object )) {
if ( isset( $this->Object->$attr ) )
return $this->Object->$attr;
}
eZDebug::writeError("Attribute '$attr' does not exist",
'DummyDataType::attribute');
$attributeData = null;
return $attributeData;
}
}
With this class, used like a template operator in an extension, you can explore an object, whatever his class,
by calling this:
my_object|ddt('my_attribute').0 {* an array *}
my_object|ddt('sub_object')|ddt('a_datatype_object').content...
{* object containing datatype object, with content attribute *}This class could be useful for some rare cases, but is a smart solution, so keep a bookmark on it ;)

View comments (1)