eZ Publish  [trunk]
ezsslzone.php
Go to the documentation of this file.
00001 <?php
00002 /**
00003  * File containing the eZSSLZone class.
00004  *
00005  * @copyright Copyright (C) 1999-2012 eZ Systems AS. All rights reserved.
00006  * @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2
00007  * @version //autogentag//
00008  * @package kernel
00009  */
00010 
00011 /*!
00012  \class eZSSLZone ezsslzone.php
00013  \brief SSL zones handling functionality.
00014 
00015  Using functionality of this class you can mark certain parts of you site
00016  as "SSL zones". After that users will be able to access those parts only over SSL.
00017  When entering an SSL zone, user will be automatically switched to SSL.
00018  When leaving an SSL zone, user will be automatically switched to plain HTTP.
00019  Such a switch is called "access mode change" in the comments below.
00020 
00021  SSL zones may be defined on either module/view basis, or on subtree basis.
00022 
00023  For more details pleaase see doc/feautures/3.8/ssl_zones.txt
00024 */
00025 
00026 class eZSSLZone
00027 {
00028     const DEFAULT_SSL_PORT = 443;
00029 
00030     /*! \privatesection */
00031 
00032     /**
00033      * \static
00034      * Returns true if the SSL zones functionality is enabled, false otherwise.
00035      * The result is cached in memory to save time on multiple invocations.
00036      */
00037     static function enabled()
00038     {
00039         if ( isset( $GLOBALS['eZSSLZoneEnabled'] ) )
00040             return $GLOBALS['eZSSLZoneEnabled'];
00041 
00042         $enabled = false;
00043         $ini = eZINI::instance();
00044         if ( $ini->hasVariable( 'SSLZoneSettings', 'SSLZones' ) )
00045             $enabled = ( $ini->variable( 'SSLZoneSettings', 'SSLZones' ) == 'enabled' );
00046 
00047         return $GLOBALS['eZSSLZoneEnabled'] = $enabled;
00048     }
00049 
00050     /**
00051      * \static
00052      */
00053     static function cacheFileName()
00054     {
00055         return eZDir::path( array( eZSys::cacheDirectory(), 'ssl_zones_cache.php' ) );
00056     }
00057 
00058     /**
00059      * \static
00060      */
00061     static function clearCacheIfNeeded()
00062     {
00063         if ( eZSSLZone::enabled() )
00064             eZSSLZone::clearCache();
00065     }
00066 
00067     /**
00068      * \static
00069      */
00070     static function clearCache()
00071     {
00072         eZDebugSetting::writeDebug( 'kernel-ssl-zone', 'Clearing caches.' );
00073 
00074         // clear in-memory cache
00075         unset( $GLOBALS['eZSSLZonesCachedPathStrings'] );
00076 
00077         // and remove cache file
00078         $cacheFileName = eZSSLZone::cacheFileName();
00079         if ( file_exists( $cacheFileName ) )
00080             unlink( $cacheFileName );
00081     }
00082 
00083     /**
00084      * \static
00085      * Load content SSL zones definitions.
00086      * Substitute URIs with corresponding path strings
00087      * (e.g. "/news" would be subsituted with "/1/2/50").
00088      * The result is cached in memory to save time on multiple invocations.
00089      * It is also saved in a cache file that is usually updated by eZContentCacheManager along with content cache.
00090      */
00091     static function getSSLZones()
00092     {
00093         if ( !isset( $GLOBALS['eZSSLZonesCachedPathStrings'] ) ) // if in-memory cache does not exist
00094         {
00095             $cacheFileName = eZSSLZone::cacheFileName();
00096             $cacheDirName = eZSys::cacheDirectory();
00097 
00098             // if file cache does not exist then create it
00099             if ( !is_readable( $cacheFileName ) )
00100             {
00101                 $ini = eZINI::instance();
00102                 $sslSubtrees = $ini->variable( 'SSLZoneSettings', 'SSLSubtrees' );
00103 
00104                 if ( !isset( $sslSubtrees ) || !$sslSubtrees )
00105                     return array();
00106 
00107                 // if there are some content SSL zones defined in the ini settings
00108                 // then let's calculate path strings for them
00109                 $pathStringsArray = array();
00110                 foreach ( $sslSubtrees as $uri )
00111                 {
00112                     $elements = eZURLAliasML::fetchByPath( $uri );
00113                     if ( count( $elements ) == 0 )
00114                     {
00115                         eZDebug::writeError( "Cannot fetch URI '$uri'", __METHOD__ );
00116                         continue;
00117                     }
00118                     $action = $elements[0]->attribute( 'action' );
00119                     if ( !preg_match( "#^eznode:(.+)#", $action, $matches ) )
00120                     {
00121                         eZDebug::writeError( "Cannot decode action '$action' for URI '$uri'", __METHOD__ );
00122                         continue;
00123                     }
00124                     $nodeID = (int)$matches[1];
00125                     $node = eZContentObjectTreeNode::fetch( $nodeID );
00126                     if ( !$node instanceof eZContentObjectTreeNode )
00127                     {
00128                         eZDebug::writeError( "cannot fetch node by URI '$uri'", __METHOD__ );
00129                         continue;
00130                     }
00131                     $pathStringsArray[$uri] = $node->attribute( 'path_string' );
00132                     unset( $node );
00133                 }
00134 
00135                 // write calculated path strings to the file
00136                 if ( !file_exists( $cacheDirName ) )
00137                 {
00138                     eZDir::mkdir( $cacheDirName, false, true );
00139                 }
00140 
00141                 $fh = fopen( $cacheFileName, 'w' );
00142                 if ( $fh )
00143                 {
00144                     fwrite( $fh, "<?php\n\$pathStringsArray = " . var_export( $pathStringsArray, true ) . ";\n?>" );
00145                     fclose( $fh );
00146 
00147                     $perm = eZINI::instance()->variable( 'FileSettings', 'StorageFilePermissions' );
00148                     chmod( $cacheFileName, octdec( $perm ) );
00149                 }
00150 
00151                 return $GLOBALS['eZSSLZonesCachedPathStrings'] = $pathStringsArray;
00152             }
00153             else // if the cache file exists
00154             {
00155                 // let's read its contents and return them
00156                 include_once( $cacheFileName ); // stores array to $pathStringsArray
00157                 return $GLOBALS['eZSSLZonesCachedPathStrings'] = $pathStringsArray;
00158             }
00159         }
00160 
00161         // else if in-memory cache already exists then return its contents
00162         $pathStringsArray = $GLOBALS['eZSSLZonesCachedPathStrings'];
00163 
00164         return $pathStringsArray;
00165     }
00166 
00167     /**
00168      * \static
00169      * Checks if a given module/view pair is in the given list of views.
00170      * Wildcard matching on view name is done.
00171      *
00172      * \return 2        if wildcard match occurs on the given view
00173      *         1        if exact match occurs on the given view
00174      *         0        if the view is not found in the list
00175      */
00176     static function viewIsInArray( $module, $view, $moduleViews )
00177     {
00178         if ( in_array( "$module/$view", $moduleViews ) )
00179             return 2;
00180         if ( in_array( "$module/*", $moduleViews ) )
00181             return 1;
00182         return 0;
00183     }
00184 
00185     /**
00186      * \static
00187      * \return true if the view is defined as 'keep'
00188      */
00189     static function isKeepModeView( $module, $view )
00190     {
00191         $ini = eZINI::instance();
00192         $viewsModes  = $ini->variable( 'SSLZoneSettings', 'ModuleViewAccessMode' );
00193         $sslViews      = array_keys( $viewsModes, 'ssl' );
00194         $keepModeViews = array_keys( $viewsModes, 'keep' );
00195 
00196         if ( eZSSLZone::viewIsInArray( $module, $view, $keepModeViews ) <
00197              eZSSLZone::viewIsInArray( $module, $view, $sslViews ) )
00198         {
00199             eZDebugSetting::writeDebug( 'kernel-ssl-zone', 'The view cannot choose access mode itself.' );
00200             return false;
00201         }
00202 
00203         return true;
00204     }
00205 
00206     /**
00207      * \static
00208      * \param  $inSSL  The desired access mode.
00209      *
00210      * Change access mode (HTTP/HTTPS):
00211      * - If previous mode was HHTP but $inSSL is true, we switch to SSL.
00212      * - If previous mode was SSL  but $inSSL is false, we switch to HTTP.
00213      * - Otherwise no mode change is occured.
00214      *
00215      * Mode change is done by redirect to the same URL, but with changed
00216      * protocol (http/https) and TCP port.
00217      *
00218      * In case of mode change this method does not return (exit() is called).
00219      */
00220     static function switchIfNeeded( $inSSL )
00221     {
00222         // if it's undefined whether we should redirect  we do nothing
00223         if ( !isset( $inSSL ) )
00224             return;
00225 
00226         // $nowSSl is true if current access mode is HTTPS.
00227         $nowSSL = eZSys::isSSLNow();
00228 
00229         $requestURI = eZSys::requestURI();
00230         $indexDir = eZSys::indexDir( false );
00231 
00232         $sslZoneRedirectionURL = false;
00233         if ( $nowSSL && !$inSSL )
00234         {
00235             // switch to plain HTTP
00236             $ini = eZINI::instance();
00237             $host = $ini->variable( 'SiteSettings', 'SiteURL' );
00238             $sslZoneRedirectionURL = "http://" . $host . $indexDir . $requestURI;
00239         }
00240         elseif ( !$nowSSL && $inSSL )
00241         {
00242             // switch to HTTPS
00243             $host = eZSys::serverVariable( 'HTTP_HOST' );
00244             $host = preg_replace( '/:\d+$/', '', $host );
00245 
00246             $ini = eZINI::instance();
00247             $sslPort = $ini->variable( 'SiteSettings', 'SSLPort' );
00248             $sslPortString = ( $sslPort == eZSSLZone::DEFAULT_SSL_PORT ) ? '' : ":$sslPort";
00249             $sslZoneRedirectionURL = "https://" . $host  . $sslPortString . $indexDir . $requestURI;
00250         }
00251 
00252         if ( $sslZoneRedirectionURL ) // if a redirection URL is found
00253         {
00254             eZDebugSetting::writeDebug( 'kernel-ssl-zone', "redirecting to [$sslZoneRedirectionURL]" );
00255             eZHTTPTool::redirect( $sslZoneRedirectionURL, array(), false, false );
00256             eZExecution::cleanExit();
00257         }
00258     }
00259 
00260     /*! \publicsection */
00261 
00262     /**
00263      * \static
00264      * Check whether the given node should cause access mode change.
00265      * It it should, this method does not return.
00266      *
00267      * \see checkNode()
00268      */
00269     static function checkNodeID( $module, $view, $nodeID )
00270     {
00271         if ( !eZSSLZone::enabled() )
00272             return;
00273 
00274         /* If the given module/view is not in the list of 'keep mode' views,
00275          * i.e. it cannot choose access mode itself,
00276          * then do nothing.
00277          */
00278         if ( !eZSSLZone::isKeepModeView( $module, $view ) )
00279             return;
00280 
00281         // Fetch path string for the given node.
00282         $pathStrings = eZPersistentObject::fetchObjectList(
00283             eZContentObjectTreeNode::definition(), // def
00284             array( 'path_string' ),                // field_filters
00285             array( 'node_id' => $nodeID ),         // conds
00286             null,                                  // sorts
00287             null,                                  // limit
00288             false                                  // asObject
00289         );
00290 
00291         if ( !$pathStrings )
00292         {
00293             eZDebug::writeError( "Node #$nodeID not found", __METHOD__ );
00294             return;
00295         }
00296 
00297         eZSSLZone::checkNodePath( $module, $view, $pathStrings[0]['path_string'] );
00298     }
00299 
00300     /**
00301      * \static
00302      * Check whether the given node should cause access mode change.
00303      * It it should, this method does not return.
00304      */
00305     static function checkNode( $module, $view, &$node, $redirect = true )
00306     {
00307         if ( !eZSSLZone::enabled() )
00308             return;
00309 
00310         /* If the given module/view is not in the list of 'keep mode' views,
00311          * i.e. it cannot choose access mode itself,
00312          * then do nothing.
00313          */
00314         if ( !$redirect && !eZSSLZone::isKeepModeView( $module, $view ) )
00315             return;
00316 
00317         $pathString = $node->attribute( 'path_string' );
00318 
00319         return eZSSLZone::checkNodePath( $module, $view, $pathString, $redirect );
00320     }
00321 
00322     /**
00323      * \static
00324      * Check whether the given node should cause access mode change.
00325      * It it should, this method does not return.
00326      */
00327     static function checkNodePath( $module, $view, $pathString, $redirect = true )
00328     {
00329         if ( !eZSSLZone::enabled() )
00330             return;
00331 
00332         /* If the given module/view is not in the list of 'keep mode' views,
00333          * i.e. it cannot choose access mode itself,
00334          * then do nothing.
00335          */
00336         if ( !$redirect && !eZSSLZone::isKeepModeView( $module, $view ) )
00337             return;
00338 
00339         // Decide whether the node belongs to an SSL zone or not.
00340         $sslZones  = eZSSLZone::getSSLZones();
00341 
00342         $inSSLZone = false;
00343         foreach ( $sslZones as $sslZonePathString )
00344         {
00345             if ( strpos( $pathString, $sslZonePathString ) === 0 )
00346             {
00347                 $inSSLZone = true;
00348                 break;
00349             }
00350         }
00351 
00352         eZDebugSetting::writeDebug( 'kernel-ssl-zone',
00353                                     ( $inSSLZone ? 'yes' : 'no' ),
00354                                     "Does the node having path $pathString belong to an SSL zone?" );
00355 
00356         if ( $redirect )
00357             eZSSLZone::switchIfNeeded( $inSSLZone );
00358 
00359         return $inSSLZone;
00360     }
00361 
00362     /**
00363      * \static
00364      * Check whether the given object should cause access mode change.
00365      * It it should, this method does not return.
00366      */
00367     static function checkObject( $module, $view, $object )
00368     {
00369         if ( !eZSSLZone::enabled() )
00370             return;
00371 
00372         /* If the given module/view is not in the list of 'keep mode' views,
00373          * i.e. it cannot choose access mode itself,
00374          * then do nothing.
00375          */
00376         if ( !eZSSLZone::isKeepModeView( $module, $view ) )
00377             return;
00378 
00379         $pathStringList = eZPersistentObject::fetchObjectList(
00380             eZContentObjectTreeNode::definition(),                     // def
00381             array( 'path_string' ),                                    // field_filters
00382             array( 'contentobject_id' => $object->attribute( 'id' ) ), // conds
00383             null,                                                      // sorts
00384             null,                                                      // limit
00385             false                                                      // asObject
00386         );
00387 
00388         if ( is_array( $pathStringList ) && count( $pathStringList ) )
00389         {
00390             /* The object has some assigned nodes.
00391              * If at least one of those nodes belongs to an SSL zone,
00392              * we switch to SSL.
00393              */
00394 
00395             // "flatten" the array
00396             array_walk( $pathStringList, create_function( '&$a', '$a = $a[\'path_string\'];' ) );
00397         }
00398         else
00399         {
00400             /* The object has no assigned nodes.
00401              * Let's work with its parent nodes.
00402              * If at least one of the parent nodes belongs to an SSL zone,
00403              * we switch to SSL.
00404              */
00405             $pathStringList = array();
00406             $nodes = $object->parentNodes( $object->attribute( 'current' ) );
00407             if ( !is_array( $nodes ) )
00408             {
00409                 eZDebug::writeError( 'Object ' . $object->attribute( 'is' ) .
00410                                      'does not have neither assigned nor parent nodes.' );
00411             }
00412             else
00413             {
00414                 foreach( $nodes as $node )
00415                 {
00416                     $pathStringList[] = $node->attribute( 'path_string' );
00417                 }
00418             }
00419         }
00420 
00421         $inSSL = false; // does the object belong to an SSL zone?
00422         foreach ( $pathStringList as $pathString )
00423         {
00424             if ( eZSSLZone::checkNodePath( $module, $view, $pathString, false ) )
00425             {
00426                 $inSSL = true;
00427                 break;
00428             }
00429         }
00430 
00431         eZSSLZone::switchIfNeeded( $inSSL );
00432     }
00433 
00434     /**
00435      * \static
00436      * Decide whether we should change access mode for this module view or not.
00437      * Called from index.php.
00438      */
00439     static function checkModuleView( $module, $view )
00440     {
00441         if ( !eZSSLZone::enabled() )
00442             return;
00443 
00444         $ini = eZINI::instance();
00445         $viewsModes  = $ini->variable( 'SSLZoneSettings', 'ModuleViewAccessMode' );
00446 
00447         $sslViews      = array_keys( $viewsModes, 'ssl' );
00448         $keepModeViews = array_keys( $viewsModes, 'keep' );
00449 
00450         $sslPriority      = eZSSLZone::viewIsInArray( $module, $view, $sslViews      );
00451         $keepModePriority = eZSSLZone::viewIsInArray( $module, $view, $keepModeViews );
00452 
00453         if ( $sslPriority && $keepModePriority && $sslPriority == $keepModePriority )
00454         {
00455             eZDebug::writeError( "Configuration error: view $module/$view is defined both as 'ssl' and 'keep'",
00456                                  'eZSSLZone' );
00457             return;
00458         }
00459 
00460         /* If the view belongs to the list of views we should not change access mode for,
00461          * then do nothing.
00462          * (however, the view may do access mode switch itself later)
00463          */
00464         if ( $keepModePriority > $sslPriority )
00465         {
00466             eZDebugSetting::writeDebug( 'kernel-ssl-zone', 'Keeping current access mode...' );
00467             return;
00468         }
00469 
00470         /* Otherwise we look if the view is in the list of SSL views,
00471          * and if it is, we switch to SSL. Else, if it's not, we switch to plain HTTP.
00472          */
00473         $inSSL = ( $sslPriority > 0 );
00474 
00475         eZDebugSetting::writeDebug( 'kernel-ssl-zone',
00476                                     ( isset( $inSSL ) ? ( $inSSL?'yes':'no') : 'dunno' ),
00477                                     'Should we use SSL for this view?' );
00478 
00479         // Change access mode if we need to.
00480         eZSSLZone::switchIfNeeded( $inSSL );
00481     }
00482 }
00483 ?>