eZ Publish  [trunk]
ezurlaliasml.php
Go to the documentation of this file.
00001 <?php
00002 /**
00003  * File containing the eZURLAlias 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 eZURLAliasML ezurlaliasml.php
00013   \brief Handles URL aliases in eZ Publish
00014 
00015   URL aliases are different names for existing URLs in eZ Publish.
00016   Using URL aliases allows for having better looking urls on the webpage
00017   as well as having fixed URLs pointing to various locations.
00018 
00019   This class handles storing, fetching, moving and subtree updates on
00020   eZ Publish URL aliases, this performed using methods from eZPersistentObject.
00021 
00022   The table used to store path information is designed to keep each element in
00023   the path (separated by /) in one row, ie. not the entire path.
00024   Each row uses the *parent* field to say which element is the parent of the current one,
00025   a value of 0 means a top-level path element.
00026   The system also supports path elemens in multiple languages, each language
00027   is stored in separate rows but with the same path element ID, the exception is
00028   when the text of multiple languages are the same then they will simply share the
00029   same row.
00030 
00031   Instead of manipulating path elements directly it is recommended to use one
00032   the higher level methods for fetching or storing a path.
00033 
00034   For objects the methods getChildren() and getPath() can be used to fetch the child elements and path string.
00035 
00036   Typically you will not have a path element object and should use on of these static functions:
00037 
00038   - storePath() - Stores a given path with specified action, all parent are created if they don't exist.
00039   - fetchByPath() - Fetch path elements by path string, some wildcard support is also available.
00040   - translate() - Translate requested path string into the internal path.
00041 
00042   For more detailed path element handling these static methods are available:
00043 
00044   - fetchByAction() - Fetch a path element based on the action.
00045   - fetchByParentID() - Fetch path elements based on parent ID.
00046   - fetchPathByActionList() - Fetch path string based on action values, this is more optimized than getPath().
00047 
00048   - setLangMaskAlwaysAvailable() - Updates language mask for path elements based on actions.
00049 
00050   Most of these methods have some common arguments, they can be:
00051   - $maskLanguages - If true then only elements which matches the currently prioritized languaes is processed.
00052   - $onlyPrioritized - If true then only the top prioritized language of the elements is considered. Requires $maskLanguages to be set to true.
00053   - $includeRedirections - If true then elements which redirects to this is also processed.
00054 
00055 */
00056 
00057 class eZURLAliasML extends eZPersistentObject
00058 {
00059     // Return values from storePath()
00060     const LINK_ID_NOT_FOUND = 1;
00061     const LINK_ID_WRONG_ACTION = 2;
00062     const LINK_ALREADY_TAKEN = 3;
00063     const ACTION_INVALID = 51;
00064     const DB_ERROR = 101;
00065 
00066     /*!
00067      Optionally computed path string for this element, used for caching purposes.
00068      */
00069     public $Path;
00070     private static $charset = null;
00071 
00072     /*!
00073      Initializes a new URL alias from database row.
00074      \note If 'path' is set it will be cached in $Path.
00075     */
00076     function eZURLAliasML( $row )
00077     {
00078         $this->eZPersistentObject( $row );
00079         $this->Path = null;
00080         if ( isset( $row['path'] ) )
00081         {
00082             $this->Path = $row['path'];
00083         }
00084     }
00085 
00086     static public function definition()
00087     {
00088         static $definition = array( "fields" => array( "id" => array( 'name' => 'ID',
00089                                                         'datatype' => 'integer',
00090                                                         'default' => 0,
00091                                                         'required' => true ),
00092                                          "parent" => array( 'name' => 'Parent',
00093                                                             'datatype' => 'integer',
00094                                                             'default' => 0,
00095                                                             'required' => true ),
00096                                          "lang_mask" => array( 'name' => 'LangMask',
00097                                                                'datatype' => 'integer',
00098                                                                'default' => 0,
00099                                                                'required' => true ),
00100                                          "text" => array( 'name' => 'Text',
00101                                                           'datatype' => 'string',
00102                                                           'default' => '',
00103                                                           'required' => true ),
00104                                          "text_md5" => array( 'name' => 'TextMD5',
00105                                                               'datatype' => 'string',
00106                                                               'default' => '',
00107                                                               'required' => true ),
00108                                          "action" => array( 'name' => 'Action',
00109                                                             'datatype' => 'string',
00110                                                             'default' => '',
00111                                                             'required' => true ),
00112                                          "action_type" => array( 'name' => 'ActionType',
00113                                                                  'datatype' => 'string',
00114                                                                  'default' => '',
00115                                                                  'required' => true ),
00116                                          "link" => array( 'name' => 'Link',
00117                                                           'datatype' => 'integer',
00118                                                           'default' => 0,
00119                                                           'required' => true ),
00120                                          "is_alias" => array( 'name' => 'IsAlias',
00121                                                                  'datatype' => 'integer',
00122                                                                  'default' => 0,
00123                                                                  'required' => true ),
00124                                          "is_original" => array( 'name' => 'IsOriginal',
00125                                                                  'datatype' => 'integer',
00126                                                                  'default' => 0,
00127                                                                  'required' => true ),
00128                                          "alias_redirects" => array( 'name' => 'AliasRedirects',
00129                                                                      'datatype' => 'integer',
00130                                                                      'default' => 1,
00131                                                                      'required' => true ) ),
00132                       "keys" => array( "parent", "text_md5" ),
00133                       "function_attributes" => array( "children" => "getChildren",
00134                                                       "path" => "getPath" ),
00135                       "class_name" => "eZURLAliasML",
00136                       "name" => "ezurlalias_ml" );
00137         return $definition;
00138     }
00139 
00140     /*!
00141      Unicode-aware strtolower, performs the conversion by using eZCharTransform
00142      */
00143     static function strtolower( $text )
00144     {
00145         //We need to detect our internal charset
00146         if ( self::$charset === null )
00147         {
00148             self::$charset = eZTextCodec::internalCharset();
00149         }
00150 
00151         //First try to use mbstring
00152         if ( extension_loaded( 'mbstring' ) )
00153         {
00154             return mb_strtolower( $text, self::$charset );
00155         }
00156         else
00157         {
00158             // Fall back if mbstring is not available
00159             $char = eZCharTransform::instance();
00160             return $char->transformByGroup( $text, 'lowercase' );
00161         }
00162     }
00163 
00164     /*!
00165      Converts the action property into a real url which responds to the
00166      module/view on the site.
00167      */
00168     function actionURL()
00169     {
00170         return eZURLAliasML::actionToUrl( $this->Action );
00171     }
00172 
00173     /*!
00174      Creates a new path element with given arguments, MD5 sum is automatically created.
00175 
00176      \param $element The text string for the path element.
00177      \param $action  Action string.
00178      \param $parentID ID of parent path element.
00179      \param $language ID or mask of languages
00180      \param $languageName Name of language(s), comma separated
00181      */
00182     static function create( $element, $action, $parentID, $language )
00183     {
00184         $row = array( 'text'      => $element,
00185                       'text_md5'  => md5( eZURLAliasML::strtolower( $element ) ),
00186                       'parent'    => $parentID,
00187                       'lang_mask' => $language,
00188                       'action'    => $action );
00189         return new eZURLAliasML( $row );
00190     }
00191 
00192     /*!
00193      Overrides the default behaviour to automatically update TextMD5.
00194      */
00195     function setAttribute( $name, $value )
00196     {
00197         eZPersistentObject::setAttribute( $name, $value );
00198         if ( $name == 'text' )
00199         {
00200             $this->TextMD5 = md5( eZURLAliasML::strtolower( $value ) );
00201         }
00202         else if ( $name == 'action' )
00203         {
00204             $this->ActionType = null;
00205         }
00206     }
00207 
00208     /*!
00209      Generates the md5 for the alias and stores the values.
00210      \note Transaction unsafe. If you call several transaction unsafe methods you must enclose
00211      the calls within a db transaction; thus within db->begin and db->commit.
00212     */
00213     function store( $fieldFilters = null )
00214     {
00215         if ( $this->ID === null )
00216         {
00217             $this->ID = self::getNewID();
00218         }
00219         if ( $this->Link === null )
00220         {
00221             $this->Link = $this->ID;
00222         }
00223         if ( $this->TextMD5 === null )
00224         {
00225             $this->TextMD5 = md5( eZURLAliasML::strtolower( $this->Text ) );
00226         }
00227         $this->IsOriginal = ($this->ID == $this->Link) ? 1 : 0;
00228         if ( $this->IsAlias )
00229             $this->IsOriginal = true;
00230         if ( $this->Action == "nop:" ) // nop entries can always be replaced
00231             $this->IsOriginal = false;
00232         if ( strlen( $this->ActionType ) == 0 )
00233         {
00234             if ( preg_match( "#^(.+):#", $this->Action, $matches ) )
00235                 $this->ActionType = $matches[1];
00236             else
00237                 $this->ActionType = 'nop';
00238         }
00239 
00240         eZPersistentObject::store( $fieldFilters );
00241     }
00242 
00243     /*!
00244      \static
00245      Removes all path elements which matches the action name $actionName and value $actionValue.
00246      */
00247     static public function removeByAction( $actionName, $actionValue )
00248     {
00249         // If this is an original element we must get rid of all elements which points to it.
00250         $db = eZDB::instance();
00251         $actionStr = $db->escapeString( $actionName . ':' . $actionValue );
00252         $query = "DELETE FROM ezurlalias_ml WHERE action = '{$actionStr}'";
00253         $db->query( $query );
00254     }
00255 
00256     /*!
00257      \static
00258      Removes a URL-Alias which has parent $parentID, MD5 text $textMD5 and language $language.
00259      If the entry has only the specified language and there are existing children the entry will be disabled instead of removed.
00260      If the entry has other languages other than the one which was specified the language bit is removed.
00261 
00262      \param $parentID ID of the parent element
00263      \param $textMD5  MD5 of the lowercase version of the text, see eZURLAliasML::strtolower().
00264      \param $language The language entry to remove, can be a string with the locale or a language object (eZContentLanguage).
00265      */
00266     public static function removeSingleEntry( $parentID, $textMD5, $language )
00267     {
00268         $parentID = (int)$parentID;
00269         if ( !is_object( $language ) )
00270             $language = eZContentLanguage::fetchByLocale( $language );
00271         $languageID = (int)$language->attribute( 'id' );
00272         $db = eZDB::instance();
00273 
00274         $bitDel   = $db->bitAnd( 'lang_mask' ,  (~$languageID) );
00275         $bitMatch = $db->bitAnd( 'lang_mask', $languageID ) . ' > 0';
00276         $bitMask  = $db->bitAnd( 'lang_mask', ~1 );
00277 
00278 
00279         // Fetch data for the given entry
00280         $rows = $db->arrayQuery( "SELECT * FROM ezurlalias_ml WHERE parent = {$parentID} AND text_md5 = '" . $db->escapeString( $textMD5 ) . "' AND $bitMatch" );
00281         if ( count( $rows ) == 0 )
00282             return false;
00283 
00284         $id   = (int)$rows[0]['id'];
00285         $mask = (int)$rows[0]['lang_mask'];
00286         if ( ($mask & ~($languageID | 1)) == 0 )
00287         {
00288             // No more languages for this entry so we need to check for children
00289             $childRows = $db->arrayQuery( "SELECT * FROM ezurlalias_ml WHERE parent = {$id}" );
00290             if ( count( $childRows ) > 0 )
00291             {
00292                 // Turn entry into a nop: to disable it
00293                 $element = new eZURLAliasML( $rows[0] );
00294                 $element->LangMask = 1;
00295                 $element->Action = "nop:";
00296                 $element->ActionType = "nop";
00297                 $element->IsAlias = 0;
00298                 $element->store();
00299                 return;
00300             }
00301         }
00302         // Remove language bit from selected entries and remove entries which have no languages.
00303         $db->query( "UPDATE ezurlalias_ml SET lang_mask = $bitDel WHERE parent = {$parentID} AND text_md5 = '" . $db->escapeString( $textMD5 ) . "' AND $bitMatch" );
00304         $db->query( "DELETE FROM ezurlalias_ml WHERE parent = {$parentID} AND text_md5 = '" . $db->escapeString( $textMD5 ) . "' AND $bitMask = 0" );
00305     }
00306 
00307     /*!
00308      Finds all the children of the current element.
00309 
00310      For more control over the list use fetchByParentID().
00311      */
00312     function getChildren()
00313     {
00314         return eZURLAliasML::fetchByParentID( $this->ID, true, true, false );
00315     }
00316 
00317     /*!
00318      Calculates the full path for the current item and returns it.
00319 
00320      \param $locale The locale for which a path should be calculated.
00321      \param $incomingLanguageList Array of locale codes representing the prioritized site language list.
00322 
00323      \note If you know the action values of the path use fetchPathByActionList() instead, it is more optimized.
00324      \note The calculated path is cached in $Path.
00325      */
00326     function getPath( $locale = null, $incomingLanguageList = null )
00327     {
00328         if ( $this->Path !== null )
00329             return $this->Path;
00330 
00331         // Fetch path 'text' elements of correct parent path
00332         $path = array( $this->Text );
00333         $id = (int)$this->Parent;
00334         $db = eZDB::instance();
00335         while ( $id != 0 )
00336         {
00337             $query = "SELECT parent, lang_mask, text FROM ezurlalias_ml WHERE id={$id}";
00338             if ( $locale !== null && is_string( $locale ) )
00339             {
00340                 // We also want to consider the prioritized language list for the
00341                 // destination siteaccess, so that untranslated objects, are not
00342                 // disregarded from the URL.
00343                 if ( $incomingLanguageList !== null )
00344                 {
00345                     eZContentLanguage::setPrioritizedLanguages( $incomingLanguageList );
00346                 }
00347 
00348                 $langMask = trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) );
00349                 $query .= " AND ({$langMask})";
00350             }
00351             $rows = $db->arrayQuery( $query );
00352 
00353             if ( count( $rows ) == 0 )
00354             {
00355                 break;
00356             }
00357             $result = eZURLAliasML::choosePrioritizedRow( $rows );
00358 
00359             if ( $incomingLanguageList !== null )
00360             {
00361                 eZContentLanguage::clearPrioritizedLanguages();
00362             }
00363 
00364             if ( !$result )
00365             {
00366                 $result = $rows[0];
00367             }
00368             $id = (int)$result['parent'];
00369             array_unshift( $path, $result['text'] );
00370         }
00371         $this->Path = implode( '/', $path );
00372         return $this->Path;
00373     }
00374 
00375     /*!
00376      \static
00377      Stores the full path $path to point to action $action, any missing parents are created as placeholders (ie. nop:).
00378 
00379      Returns an array containing the entry 'status' which is the status code, is \c true if all went well, a number otherwise (see class constants).
00380      Will contain 'path' for succesful creation or if the path already exists.
00381 
00382      \param $path String containing full path, leading and trailing slashes are stripped.
00383      \param $action Action string for entry.
00384      \param $languageName The language to use for entry, can be a string (locale code, e.g. 'nor-NO') an eZContentLanguage object or false for the top prioritized language.
00385      \param $linkID Numeric ID for link field, if it is set to false the entry will point to itself. Use this for redirections. Use \c true if you want to create an link/alias which points to a module (ie. no entry in urlalias table).
00386      \param $alwaysAvailable If true the entry will be available in any language.
00387      \param $rootID ID of the parent element to start at, use 0/false for the very top.
00388      \param $cleanupElements If true each element in the path will be cleaned up according to the current URL transformation rules.
00389      \param $autoAdjustName If true it will adjust the name until it is unique in the path. Used together with $linkID.
00390      \param $reportErrors If true it will report found errors using eZDebug, if \c false errors are only return in 'status'.
00391      \param $aliasRedirects If true and an alias is being stored it will redirect (using HTTP 301) to it's destination.
00392      */
00393     static function storePath( $path, $action,
00394                         $languageName = false, $linkID = false, $alwaysAvailable = false, $rootID = false,
00395                         $cleanupElements = true, $autoAdjustName = false, $reportErrors = true, $aliasRedirects = true )
00396     {
00397         $path = eZURLAliasML::cleanURL( $path );
00398         if ( $languageName === false )
00399         {
00400             $languageName = eZContentLanguage::topPriorityLanguage();
00401         }
00402         if ( is_object( $languageName ) )
00403         {
00404             $languageObj  = $languageName;
00405             $languageID   = (int)$languageName->attribute( 'id' );
00406             $languageName = $languageName->attribute( 'locale' );
00407         }
00408         else
00409         {
00410             $languageObj = eZContentLanguage::fetchByLocale( $languageName );
00411             $languageID  = (int)$languageObj->attribute( 'id' );
00412         }
00413         $languageMask = $languageID;
00414         if ( $alwaysAvailable )
00415             $languageMask |= 1;
00416 
00417         $path = eZURLAliasML::cleanURL( $path );
00418         $elements = explode( '/', $path );
00419 
00420         $db = eZDB::instance();
00421         $parentID = 0;
00422 
00423         // If the root ID is specified we will start the parent search from that
00424         if ( $rootID !== false )
00425         {
00426             $parentID = $rootID;
00427         }
00428         $i = 0;
00429         // Top element is handled separately.
00430         $topElement = array_pop( $elements );
00431         // Find correct parent, and create missing ones if necessary
00432         $createdPath = array();
00433         foreach ( $elements as $element )
00434         {
00435             $actionStr = $db->escapeString( $action );
00436             if ( $cleanupElements )
00437                 $element = eZURLAliasML::convertToAlias( $element, 'noname' . (count($createdPath)+1) );
00438             $elementStr = $db->escapeString( eZURLAliasML::strtolower( $element ) );
00439 
00440             $query = "SELECT * FROM ezurlalias_ml WHERE text_md5 = " . eZURLAliasML::md5( $db, $elementStr, false ) . " AND parent = {$parentID}";
00441             $rows = $db->arrayQuery( $query );
00442             if ( count( $rows ) == 0 )
00443             {
00444                 // Create a fake element to ensure we have a parent
00445                 $elementObj = eZURLAliasML::create( $element, "nop:", $parentID, 1 );
00446                 $elementObj->store();
00447                 $parentID = (int)$elementObj->attribute( 'id' );
00448             }
00449             else
00450             {
00451                 $parentID = (int)$rows[0]['link'];
00452             }
00453             $createdPath[] = $element;
00454 
00455             ++$i;
00456         }
00457         if ( $parentID != 0 )
00458         {
00459             $sql = "SELECT text, parent FROM ezurlalias_ml WHERE id = {$parentID}";
00460             $rows = $db->arrayQuery( $sql );
00461             if ( count( $rows ) > 0 )
00462             {
00463                 // A special case. If the special entry with empty text is used as parent
00464                 // the parent must be adjust to 0 (ie. real top level).
00465                 if ( strlen( $rows[0]['text'] ) == 0 && $rows[0]['parent'] == 0 )
00466                 {
00467                     $createdPath = array();
00468                     $parentID = 0;
00469                 }
00470             }
00471         }
00472 
00473         if ( !preg_match( "#^(.+):(.+)$#", $action, $matches ) )
00474         {
00475             return array( 'status' => self::ACTION_INVALID,
00476                           'error_message' => "The action value " . var_export( $action, true ) . " is invalid",
00477                           'error_number' => self::ACTION_INVALID,
00478                           'path'    => null,
00479                           'element' => null );
00480         }
00481         $actionName  = $matches[1];
00482         $actionValue = $matches[2];
00483         $existingElementID = null;
00484         $alwaysMask = $alwaysAvailable ? 1 : 0;
00485 
00486         $actionStr = $db->escapeString( $action );
00487         $actionTypeStr = $db->escapeString( $actionName );
00488 
00489         $createdElement = null;
00490         if ( $linkID === false )
00491         {
00492             if ( $cleanupElements )
00493                 $topElement = eZURLAliasML::convertToAlias( $topElement, 'noname' . (count($createdPath)+1) );
00494 
00495             $adjustName = false;
00496             $curElementID = null;
00497             $newElementID = null;
00498             $newText = $topElement;
00499             $uniqueCounter = 0;
00500 
00501             // Loop until we a valid entry point, which means:
00502             // 1. The entry does not exist yet, so create a new one
00503             // 2. The entry exists but is re-usable (e.g. nop or same action)
00504             // 3. The entry exists and cannot be re-used, instead the name is adjusted to be unique.
00505             while ( true )
00506             {
00507                 $newText = $topElement;
00508                 if ( $uniqueCounter > 0 )
00509                     $newText .= ($uniqueCounter + 1);
00510                 $textMD5 = eZURLAliasML::md5( $db, $newText );
00511 
00512                 $query = "SELECT * FROM ezurlalias_ml WHERE parent = $parentID AND text_md5 = {$textMD5}";
00513                 $rows = $db->arrayQuery( $query );
00514                 if ( count( $rows ) == 0 )
00515                 {
00516                     // No such entry, create a new one
00517                     break;
00518                 }
00519 
00520                 $row = $rows[0];
00521                 $curID = (int)$row['id'];
00522                 $curAction = $row['action'];
00523                 if ( $curAction == 'nop:' || $curAction == $action || $row['is_original'] == 0 )
00524                 {
00525                     // We can reuse the element so record the ID
00526                     $curElementID = $curID;
00527                     $newElementID = $curID;
00528                     break;
00529                 }
00530 
00531                 if ( !$autoAdjustName )
00532                 {
00533                     if ( $reportErrors )
00534                         eZDebug::writeError( "Tried to store path '{$path}' but the path already exists (ID: {$curID}) but with action '{$curAction}', the new action was '{$action}'" );
00535                     return array( 'status' => self::LINK_ALREADY_TAKEN,
00536                                   'path'    => $path,
00537                                   'element' => null );
00538                 }
00539                 // Need to adjust name, re-iterate
00540                 ++$uniqueCounter;
00541             }
00542             $textEsc = $db->escapeString( $newText );
00543 
00544             // See if there is already a node in the same level with the same action
00545             if ( $newElementID === null )
00546             {
00547                 $query = "SELECT * FROM ezurlalias_ml " .
00548                          "WHERE parent = $parentID AND action = '{$actionStr}' AND is_original = 1 AND is_alias = 0";
00549                 $rows = $db->arrayQuery( $query );
00550                 if ( count( $rows ) > 0 )
00551                 {
00552                     $newElementID = (int)$rows[0]['id'];
00553                 }
00554             }
00555 
00556             // Create or update the element
00557             if ( $curElementID !== null )
00558             {
00559                 // Check if an already existing entry at the same level exists, with a different id
00560                 // if so the id must be updated.
00561                 $query = "SELECT * FROM ezurlalias_ml " .
00562                          "WHERE parent = $parentID AND action = '{$actionStr}' AND is_original = 1 AND is_alias = 0";
00563                 $rows = $db->arrayQuery( $query );
00564                 if ( count( $rows ) > 0 )
00565                 {
00566                     $existingEntryId = (int)$rows[0]['id'];
00567 
00568                     if ( $existingEntryId != $curElementID )
00569                     {
00570                         // move history entry to the same id
00571                         $query = "UPDATE ezurlalias_ml SET id = {$existingEntryId} " .
00572                                  "WHERE parent = $parentID AND text_md5 = {$textMD5}";
00573                         $res = $db->query( $query );
00574                         if ( !$res ) return eZURLAliasML::dbError( $db );
00575                         $curElementID = $existingEntryId;
00576                     }
00577                 }
00578 
00579                 $bitOr = $db->bitOr( $db->bitAnd( 'lang_mask', ~1 ), $languageMask );
00580                 // Note: The `text` field is updated too, this ensures case-changes are stored.
00581                 $query = "UPDATE ezurlalias_ml SET link = id, lang_mask = {$bitOr}, text = '{$textEsc}', action = '{$actionStr}', action_type = '{$actionTypeStr}', is_alias = 0, is_original = 1 " .
00582                          "WHERE parent = $parentID AND text_md5 = {$textMD5}";
00583                 $res = $db->query( $query );
00584                 if ( !$res ) return eZURLAliasML::dbError( $db );
00585                 $newElementID = $curElementID;
00586             }
00587             else
00588             {
00589                 $element = new eZURLAliasML( array( 'id'=> $newElementID,
00590                                                     'link' => null,
00591                                                     'parent' => $parentID,
00592                                                     'text' => $newText,
00593                                                     'lang_mask' => $languageID | $alwaysMask,
00594                                                     'action' => $action ) );
00595                 $element->store();
00596                 $newElementID = (int)$element->attribute( 'id' );
00597                 $createdElement = $element;
00598             }
00599             $createdPath[] = $newText;
00600 
00601             // OMS-urlalias-fix: We want to retain the lang_mask of url entries, but mark others as history elements is_original = 0
00602             // Furthermore this change is not performed on custom alias entries.
00603             $bitAnd = $db->bitAnd( 'lang_mask', $languageID );
00604 
00605             // First we look at the entries to mark as history entries, if an entry comprise more languages, it must not be set as history element.
00606             $query = "SELECT * FROM ezurlalias_ml " .
00607                      "WHERE action = '{$actionStr}' AND (${bitAnd} > 0) AND is_original = 1 AND is_alias = 0 AND (parent != $parentID OR text_md5 != {$textMD5})";
00608             $toBeUpdated = $db->arrayQuery( $query );
00609 
00610             // 0. Check if the entry to be updated represents multiple languages:
00611             // IF YES:
00612             //  1. "Downgrade" existing entry, by removing the active translation's language id from the language_mask.
00613             // IF NO:
00614             //  1. Mark entry as a history entry
00615 
00616             if ( count( $toBeUpdated ) > 0 )
00617             {
00618                 $languageMask = $toBeUpdated[0]['lang_mask'];
00619                 if ( ( $languageMask & ~( $languageID | 1 ) ) != 0 )
00620                 {
00621                     // "Composite entry", downgrade current entry
00622                     $currentEntry = new eZURLAliasML( $toBeUpdated[0] );
00623                     $currentEntry->LangMask = (int)$currentEntry->LangMask & ~$languageID;
00624                     $currentEntry->store();
00625                 }
00626                 else
00627                 {
00628                     // Mark as history element.
00629                     $query = "UPDATE ezurlalias_ml SET is_original = 0 " .
00630                              "WHERE action = '{$actionStr}' AND (${bitAnd} > 0) AND is_original = 1 AND is_alias = 0 AND (parent != $parentID OR text_md5 != {$textMD5})";
00631                     $res = $db->query( $query );
00632                     if ( !$res ) return eZURLAliasML::dbError( $db );
00633                 }
00634             }
00635 
00636             // OMS-urlalias-fix: instead entries without language we look at history elements with same action (and language)
00637             // Look for other nodes with the same action and language
00638             // if found make then link to the new entry
00639             $bitAnd = $db->bitAnd( 'lang_mask', $languageID );
00640             $query = "SELECT * FROM ezurlalias_ml " .
00641                      "WHERE action = '{$actionStr}' AND (${bitAnd} > 0) AND is_original = 0 AND (parent != $parentID OR text_md5 != {$textMD5})";
00642             $rows = $db->arrayQuery( $query );
00643             foreach ( $rows as $row )
00644             {
00645                 $idtmp = (int)$row['id'];
00646                 if ( $idtmp == $newElementID )
00647                 {
00648                     $idtmp = self::getNewID();
00649                 }
00650                 $parentIDTmp = (int)$row['parent'];
00651                 $textMD5Tmp = eZURLAliasML::md5( $db, $row['text'] );
00652 
00653                 // OMS-urlalias-fix: We do not touch the lang_mask here
00654                 $res = $db->query( "UPDATE ezurlalias_ml SET id = {$idtmp}, link = {$newElementID}, is_alias = 0, is_original = 0 " .
00655                                    "WHERE parent = {$parentIDTmp} AND text_md5 = {$textMD5Tmp}" );
00656                 if ( !$res ) return eZURLAliasML::dbError( $db );
00657             }
00658             $res = $db->query( $query );
00659             if ( !$res ) return eZURLAliasML::dbError( $db );
00660 
00661             // Look for other nodes which is a link for the current action
00662             // if found make then link to the new entry
00663             // OMS-urlalias-fix: We only want to update the links of entries within the same language.
00664             // Also, only to be applied on normal entries, not custom aliases
00665             $bitAnd = $db->bitAnd( 'lang_mask', $languageID );
00666             $query = "UPDATE ezurlalias_ml SET link = {$newElementID}, is_alias = 0, is_original = 0 " .
00667                      "WHERE action = '{$actionStr}' AND is_original = 0 AND is_alias = 0 AND (${bitAnd} > 0) AND (parent != $parentID OR text_md5 != {$textMD5})";
00668             $res = $db->query( $query );
00669             if ( !$res ) return eZURLAliasML::dbError( $db );
00670 
00671 
00672             // Move children from old node to the new node
00673             // Conflicts:
00674             // New       |       Old |  Action
00675             // -------------------------------
00676             // Element   | Link      | Delete old
00677             // Element   | Element   | Will not happen, if so delete old
00678             // Element   | Other     | Reparent with new name
00679             // Element   | nop       | Delete old
00680             // Link      | Link      | Delete old
00681             // Link      | Element   | Delete new, reparent
00682             // Link      | Other     | Delete new, reparent
00683             // Link      | nop       | Delete old
00684             // nop       | Link      | Delete new, reparent
00685             // nop       | Element   | Delete new, reparent
00686             // nop       | nop       | Delete old
00687 
00688             // TODO: Handle all conflict cases, for now only the `Delete old, reparent` action is done
00689 
00690             // OMS-urlalias-fix: We are only updating child nodes within the same language,
00691             // and only for real system-generated url aliases. Custom aliases are left alone.
00692             $bitAnd = $db->bitAnd( 'lang_mask', $languageID );
00693             $query = "SELECT id FROM ezurlalias_ml " .
00694                      "WHERE action = '{$actionStr}' AND is_alias = 0 AND (parent != $parentID OR text_md5 != {$textMD5})";
00695             $rows = $db->arrayQuery( $query );
00696             foreach ( $rows as $row )
00697             {
00698                 $oldParentID = (int)$row['id'];
00699                 $query = "UPDATE ezurlalias_ml SET parent = {$newElementID} " .
00700                          "WHERE parent = {$oldParentID} AND (${bitAnd} > 0)";
00701                 $res = $db->query( $query );
00702                 if ( !$res ) return eZURLAliasML::dbError( $db );
00703             }
00704         }
00705         else
00706         {
00707             // Check the link ID
00708             if ( $linkID !== true )
00709             {
00710                 $linkID = (int)$linkID;
00711                 // Step 1, find existing ID
00712                 $query = "SELECT * FROM ezurlalias_ml WHERE id = '{$linkID}'";
00713                 $rows = $db->arrayQuery( $query );
00714                 // Some sanity checking
00715                 if ( count( $rows ) == 0 )
00716                 {
00717                     if ( $reportErrors )
00718                         eZDebug::writeError( "The link ID $linkID does not exist, cannot create the link", __METHOD__ );
00719                     return array( 'status' => eZURLAliasML::LINK_ID_NOT_FOUND );
00720                 }
00721                 if ( $rows[0]['action'] != $action )
00722                 {
00723                     if ( $reportErrors )
00724                         eZDebug::writeError( "The link ID $linkID uses a different action ({$rows[0]['action']}) than the requested action ({$action}) for the link, cannot create the link", __METHOD__ );
00725                     return array( 'status' => eZURLAliasML::LINK_ID_WRONG_ACTION );
00726                 }
00727                 // If the element which is pointed to is a link, then grab the link id from that instead
00728                 if ( $rows[0]['link'] != $rows[0]['id'] )
00729                 {
00730                     $linkID = (int)$rows[0]['link'];
00731                 }
00732             }
00733             else
00734             {
00735                 $linkID = null;
00736             }
00737 
00738             if ( $cleanupElements )
00739                 $topElement = eZURLAliasML::convertToAlias( $topElement, 'noname' . (count($createdPath)+1) );
00740 
00741             $adjustName = false;
00742             $curElementID  = null;
00743             $newText = $topElement;
00744             $uniqueCounter = 0;
00745             $rows = null; // Will be filled in by the while loop
00746 
00747             // Loop until we a valid entry point, which means:
00748             // 1. The entry does not exist yet, so create a new one
00749             // 2. The entry exists but is re-usable (e.g. nop or same action)
00750             // 3. The entry exists and cannot be re-used, instead the name is adjusted to be unique.
00751             while ( true )
00752             {
00753                 $newText = $topElement;
00754                 if ( $uniqueCounter > 0 )
00755                     $newText .= ($uniqueCounter + 1);
00756                 $textMD5 = eZURLAliasML::md5( $db, $newText );
00757 
00758                 $query = "SELECT * FROM ezurlalias_ml WHERE parent = $parentID AND text_md5 = {$textMD5}";
00759                 $rows = $db->arrayQuery( $query );
00760                 if ( count( $rows ) == 0 )
00761                 {
00762                     // No such entry, create a new one
00763                     break;
00764                 }
00765 
00766                 $row = $rows[0];
00767                 $curID = (int)$row['id'];
00768                 $curLink = (int)$row['link'];
00769                 $curAction = $row['action'];
00770                 if ( $curAction == $action )
00771                 {
00772                     // If the current node is the same action and is not a link we
00773                     // cannot replace it with a link node.
00774                     if ( $curID != $curLink )
00775                     {
00776                         // We can reuse the element so record the ID
00777                         $curElementID = $curID;
00778                         break;
00779                     }
00780                 }
00781                 else if ( $curAction == 'nop:' || $row['is_original'] == 0 )
00782                 {
00783                     // We can reuse the element so record the ID
00784                     $curElementID = $curID;
00785                     break;
00786                 }
00787 
00788                 if ( !$autoAdjustName )
00789                 {
00790                     if ( $reportErrors )
00791                         eZDebug::writeError( "Tried to store path '{$path}' but the path already exists (ID: {$curID}) but with action '{$curAction}', the new action was '{$action}'" );
00792                     return array( 'status' => self::LINK_ALREADY_TAKEN,
00793                                   'path'    => $path,
00794                                   'element' => null );
00795                 }
00796                 // Need to adjust name, re-iterate
00797                 ++$uniqueCounter;
00798             }
00799             $textEsc = $db->escapeString( $newText );
00800 
00801             // Create or update the element
00802             if ( $curElementID !== null )
00803             {
00804                 $element = new eZURLAliasML( $rows[0] ); // $rows is from the while loop
00805                 $element->LangMask  |= $languageID | $alwaysMask;
00806                 $element->IsAlias    = 1;
00807                 $element->Action     = $action;
00808                 // Note: The `text` field is updated too, this ensures case-changes are stored.
00809                 $element->Text       = $newText;
00810                 $element->TextMD5    = null;
00811                 $element->ActionType = null;
00812                 $element->Link       = null;
00813             }
00814             else
00815             {
00816                 $element = new eZURLAliasML( array( 'id'=> null,
00817                                                     'link' => null,
00818                                                     'parent' => $parentID,
00819                                                     'text' => $newText,
00820                                                     'lang_mask' => $languageID | $alwaysMask,
00821                                                     'action' => $action,
00822                                                     'is_alias' => 1 ) );
00823             }
00824             $element->AliasRedirects = $aliasRedirects ? 1 : 0;
00825             $element->store();
00826             $createdPath[]  = $topElement;
00827             $createdElement = $element;
00828         }
00829         return array( 'status' => true,
00830                       'path'    => join( "/", $createdPath ),
00831                       'element' => $createdElement );
00832     }
00833 
00834     /*!
00835      \static
00836      \private
00837 
00838      Returns a structure with the current database error.
00839      \note This is used by storePath().
00840      */
00841     static private function dbError( $db )
00842     {
00843         return array( 'status' => self::DB_ERROR,
00844                       'error_message' => $db->errorMessage(),
00845                       'error_number'  => $db->errorNumber(),
00846                       'path' => null,
00847                       'element' => null );
00848     }
00849 
00850     /*!
00851      \static
00852      Fetches real path element(s) which matches the action name $actionName and value $actionValue.
00853 
00854      Lets say we have the following elements:
00855 
00856      \code
00857      === ==== ====== =========== ==========
00858      id  link parent text        action
00859      === ==== ====== =========== ==========
00860      1   1    0      'ham'       'eznode:4'
00861      2   6    0      'spam'      'eznode:55'
00862      3   3    0      'bicycle'   'eznode:5'
00863      4   4    0      'superman'  'nop:'
00864      5   5    3      'repairman' 'eznode:42'
00865      6   6    3      'repoman'   'eznode:55'
00866      === ==== ====== =========== ==========
00867      \endcode
00868 
00869      then we try to fetch a specific action:
00870      \code
00871      $elements = eZURLAliasML::fetchByAction( 'eznode', 5 );
00872      \endcode
00873 
00874      it would return:
00875      \code
00876      === ==== ====== =========== ==========
00877      id  link parent text        action
00878      === ==== ====== =========== ==========
00879      3   3    0      'bicycle'   'eznode:5'
00880      === ==== ====== =========== ==========
00881      \endcode
00882 
00883      Now let's try with an element which is redirecting:
00884      \code
00885      $elements = eZURLAliasML::fetchByAction( 'eznode', 10 );
00886      \endcode
00887 
00888      it would return:
00889      \code
00890      === ==== ====== =========== ==========
00891      id  link parent text        action
00892      === ==== ====== =========== ==========
00893      2   6    0      'spam'      'eznode:55'
00894      === ==== ====== =========== ==========
00895      \endcode
00896      */
00897     static public function fetchByAction( $actionName, $actionValue, $maskLanguages = false, $onlyPrioritized = false, $includeRedirections = false )
00898     {
00899         $action = $actionName . ":" . $actionValue;
00900         $db = eZDB::instance();
00901         $actionStr = $db->escapeString( $action );
00902         $langMask = '';
00903         if ( $maskLanguages === true )
00904         {
00905             $langMask = "(" . trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) ) . ") AND ";
00906         }
00907         else if ( is_string( $maskLanguages ) )
00908         {
00909             // maskByLocale can support array input, here we only want one item.
00910             $mask = eZContentLanguage::maskByLocale( $maskLanguages );
00911             $langFilter = $db->bitAnd( 'lang_mask', $mask );
00912             $langMask = "({$langFilter} > 0) AND";
00913         }
00914         $query = "SELECT * FROM ezurlalias_ml WHERE $langMask action = '$actionStr'";
00915         if ( !$includeRedirections )
00916         {
00917             $query .= " AND is_original = 1 AND is_alias = 0";
00918         }
00919 
00920         $rows = $db->arrayQuery( $query );
00921         if ( count( $rows ) == 0 )
00922             return array();
00923         $rows = eZURLAliasML::filterRows( $rows, $onlyPrioritized );
00924         $objectList = eZPersistentObject::handleRows( $rows, 'eZURLAliasML', true );
00925         return $objectList;
00926     }
00927 
00928     /*!
00929      \static
00930      Fetches path element(s) which matches the parent ID $id.
00931 
00932      Lets say we have the following elements:
00933 
00934      === ==== ====== =========== ==========
00935      id  link parent text        action
00936      === ==== ====== =========== ==========
00937      1   1    0      'ham'       'eznode:4'
00938      2   6    0      'spam'      'eznode:55'
00939      3   3    0      'bicycle'   'eznode:5'
00940      4   4    0      'superman'  'nop:'
00941      5   5    3      'repairman' 'eznode:42'
00942      6   6    3      'repoman'   'eznode:55'
00943      === ==== ====== =========== ==========
00944 
00945      then we try to fetch a specific ID:
00946      \code
00947      eZURLAliasML::fetchByParentID( 0 );
00948      \endcode
00949 
00950      it would return (ie. no redirections):
00951      \code
00952      === ==== ====== =========== ==========
00953      id  link parent text        action
00954      === ==== ====== =========== ==========
00955      1   1    0      'ham'       'eznode:4'
00956      3   3    0      'bicycle'   'eznode:5'
00957      4   4    0      'superman'  'nop:'
00958      === ==== ====== =========== ==========
00959      \endcode
00960 
00961      Now let's try with an element which is redirecting:
00962      \code
00963      $includeRedirections = true;
00964      eZURLAliasML::fetchByParentID( 0, false, false, $includeRedirections );
00965      \endcode
00966 
00967      it would return:
00968      \code
00969      === ==== ====== =========== ==========
00970      id  link parent text        action
00971      === ==== ====== =========== ==========
00972      1   1    0      'ham'       'eznode:4'
00973      2   6    0      'spam'      'eznode:55'
00974      3   3    0      'bicycle'   'eznode:5'
00975      4   4    0      'superman'  'nop:'
00976      === ==== ====== =========== ==========
00977      \endcode
00978     */
00979     static public function fetchByParentID( $id, $maskLanguages = false, $onlyPrioritized = false, $includeRedirections = true )
00980     {
00981         $db = eZDB::instance();
00982         $id = (int)$id;
00983         $langMask = trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) );
00984         $redirSQL = '';
00985         if ( !$includeRedirections )
00986         {
00987             $redirSQL = " AND is_original = 1";
00988         }
00989         $langMask = '';
00990         if ( $maskLanguages )
00991         {
00992             $langMask = "(" . trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) ) . ") AND ";
00993         }
00994         $query = "SELECT * FROM ezurlalias_ml WHERE $langMask parent = {$id} $redirSQL";
00995         $rows = $db->arrayQuery( $query );
00996         $rows = eZURLAliasML::filterRows( $rows, $onlyPrioritized );
00997         return eZPersistentObject::handleRows( $rows, 'eZURLAliasML', true );
00998     }
00999 
01000     /*!
01001      \static
01002      Fetches the path string based on the action $actionName and the values $actionValues.
01003      The first entry in $actionValues would be the top-most path element in the path
01004      the second entry the child of the first path element and so on.
01005 
01006      Lets say we have the following elements:
01007      \code
01008      === ==== ====== =========== ==========
01009      id  link parent text        action
01010      === ==== ====== =========== ==========
01011      1   1    0      'ham'       'eznode:4'
01012      2   6    0      'spam'      'eznode:55'
01013      3   3    0      'bicycle'   'eznode:5'
01014      4   4    0      'superman'  'nop:'
01015      5   5    3      'repairman' 'eznode:42'
01016      6   6    3      'repoman'   'eznode:55'
01017      === ==== ====== =========== ==========
01018      \endcode
01019 
01020      then we try to fetch a specific ID:
01021      \code
01022      $path = eZURLAliasML::fetchPathByActionList( 'eznode', array( 3, 5 ) );
01023      \endcode
01024 
01025      it would return:
01026      \code
01027      'bicycle/repairman'
01028      \endcode
01029 
01030      \note This function is faster than getPath() since it can fetch all elements in one SQL.
01031      \note If the fetched elements does not point to each other (parent/id) then null is returned.
01032      */
01033     static public function fetchPathByActionList( $actionName, $actionValues, $locale = null )
01034     {
01035         if ( !is_array( $actionValues ) || count( $actionValues ) == 0 )
01036         {
01037             eZDebug::writeError( "Action values array must not be empty", __METHOD__ );
01038             return null;
01039         }
01040         $db = eZDB::instance();
01041         $actionList = array();
01042         foreach ( $actionValues as $i => $value )
01043         {
01044             $actionList[] = "'" . $db->escapeString( $actionName . ":" . $value ) . "'";
01045         }
01046         $actionStr = join( ", ", $actionList );
01047         $filterSQL = trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) );
01048         $query = "SELECT id, parent, lang_mask, text, action FROM ezurlalias_ml WHERE ( {$filterSQL} ) AND action in ( {$actionStr} ) AND is_original = 1 AND is_alias=0";
01049         $rows = $db->arrayQuery( $query );
01050         $actionMap = array();
01051         foreach ( $rows as $row )
01052         {
01053             $action = $row['action'];
01054             if ( !isset( $actionMap[$action] ) )
01055                 $actionMap[$action] = array();
01056             $actionMap[$action][] = $row;
01057         }
01058 
01059         if ( $locale !== null && is_string( $locale ) && !empty( $locale ) )
01060         {
01061             $selectedLanguage = eZContentLanguage::fetchByLocale( $locale );
01062             $prioritizedLanguages = eZContentLanguage::prioritizedLanguages();
01063             // Add $selectedLanguage on top of $prioritizedLanguages to take it into account with the highest priority
01064             if ( $selectedLanguage instanceof eZContentLanguage )
01065                 array_unshift( $prioritizedLanguages, $selectedLanguage );
01066         }
01067         else
01068         {
01069             $prioritizedLanguages = eZContentLanguage::prioritizedLanguages();
01070         }
01071 
01072         $path = array();
01073         $lastID = false;
01074         foreach ( $actionValues as $actionValue )
01075         {
01076             $action = $actionName . ":" . $actionValue;
01077             if ( !isset( $actionMap[$action] ) )
01078             {
01079 //                eZDebug::writeError( "The action '{$action}' was not found in the database for the current language language filter, cannot calculate path." );
01080                 return null;
01081             }
01082             $actionRows = $actionMap[$action];
01083             $defaultRow = null;
01084             foreach( $prioritizedLanguages as $language )
01085             {
01086                 foreach ( $actionRows as $row )
01087                 {
01088                     $langMask   = (int)$row['lang_mask'];
01089                     $wantedMask = (int)$language->attribute( 'id' );
01090                     if ( ( $wantedMask & $langMask ) > 0 )
01091                     {
01092                         $defaultRow = $row;
01093                         break 2;
01094                     }
01095                     // If the 'always available' bit is set then choose it as the default
01096                     if ( ($langMask & 1) > 0 )
01097                     {
01098                         $defaultRow = $row;
01099                     }
01100                 }
01101             }
01102             if ( $defaultRow )
01103             {
01104                 $id = (int)$defaultRow['id'];
01105                 $paren = (int)$defaultRow['parent'];
01106 
01107                 // If the parent is 0 it means the element is at the top, ie. reset the path and lastID
01108                 if ( $paren == 0 )
01109                 {
01110                     $lastID = false;
01111                     $path = array();
01112                 }
01113 
01114                 $path[] = $defaultRow['text'];
01115 
01116                 // Check for a valid path
01117                 if ( $lastID !== false && $lastID != $paren )
01118                 {
01119                     eZDebug::writeError( "The parent ID $paren of element with ID $id does not point to the last entry which had ID $lastID, incorrect path would be calculated, aborting" );
01120                     return null;
01121                 }
01122                 $lastID = $id;
01123             }
01124             else
01125             {
01126                 // No row was found
01127                 eZDebug::writeError( "Fatal error, no row was chosen for action " . $actionName . ":" . $actionValue );
01128                 return null;
01129             }
01130         }
01131         return join( "/", $path );
01132     }
01133 
01134     /*!
01135      \static
01136      Fetches the path element(s) which has the path $uriString.
01137      If $glob is set it will use $uriString as the folder to search in and $glob as
01138      the starting text to match against.
01139 
01140      Lets say we have the following elements:
01141      \code
01142      === ==== ====== =========== ==========
01143      id  link parent text        action
01144      === ==== ====== =========== ==========
01145      1   1    0      'ham'       'eznode:4'
01146      2   6    0      'spam'      'eznode:55'
01147      3   3    0      'bicycle'   'eznode:5'
01148      4   4    0      'superman'  'nop:'
01149      5   5    3      'repairman' 'eznode:42'
01150      6   6    3      'repoman'   'eznode:55'
01151      === ==== ====== =========== ==========
01152      \endcode
01153 
01154      Then we try to fetch a specific path:
01155      \code
01156      $elements = eZURLAliasML::fetchByPath( "bicycle/repairman" );
01157      \endcode
01158 
01159      we would get:
01160      \code
01161      === ==== ====== =========== ==========
01162      id  link parent text        action
01163      === ==== ====== =========== ==========
01164      5   5    3      'repairman' 'eznode:42'
01165      === ==== ====== =========== ==========
01166      \endcode
01167 
01168      \code
01169      $elements = eZURLAliasML::fetchByPath( "bicycle", "rep" ); // bicycle/rep*
01170      \endcode
01171 
01172      we would get:
01173      \code
01174      === ==== ====== =========== ==========
01175      id  link parent text        action
01176      === ==== ====== =========== ==========
01177      5   5    3      'repairman' 'eznode:42'
01178      6   6    3      'repoman'   'eznode:55'
01179      === ==== ====== =========== ==========
01180      \endcode
01181      */
01182     static public function fetchByPath( $uriString, $glob = false )
01183     {
01184         $uriString = eZURLAliasML::cleanURL( $uriString );
01185 
01186         $db = eZDB::instance();
01187         if ( $uriString == '' && $glob !== false )
01188             $elements = array();
01189         else
01190             $elements = explode( '/', $uriString );
01191         $len      = count( $elements );
01192         $i = 0;
01193         $selects = array();
01194         $tables  = array();
01195         $conds   = array();
01196         $prevTable = false;
01197         foreach ( $elements as $element )
01198         {
01199             $table     = "e" . $i;
01200             $langMask  = trim( eZContentLanguage::languagesSQLFilter( $table, 'lang_mask' ) );
01201 
01202             if ( $glob === false && ($i == $len - 1) )
01203                 $selects[] = eZURLAliasML::generateFullSelect( $table );
01204             else
01205                 $selects[] = eZURLAliasML::generateSelect( $table, $i, $len );
01206             $tables[]  = "ezurlalias_ml " . $table;
01207             $conds[]   = eZURLAliasML::generateCond( $table, $prevTable, $i, $langMask, $element );
01208             $prevTable = $table;
01209             ++$i;
01210         }
01211         if ( $glob !== false )
01212         {
01213             ++$len;
01214             $table     = "e" . $i;
01215             $langMask  = trim( eZContentLanguage::languagesSQLFilter( $table, 'lang_mask' ) );
01216 
01217             $selects[] = eZURLAliasML::generateFullSelect( $table );
01218             $tables[]  = "ezurlalias_ml " . $table;
01219             $conds[]   = eZURLAliasML::generateGlobCond( $table, $prevTable, $i, $langMask, $glob );
01220             $prevTable = $table;
01221             ++$i;
01222         }
01223         $elementOffset = $i - 1;
01224         $query = "SELECT DISTINCT " . join( ", ", $selects ) . " FROM " . join( ", ", $tables ) . " WHERE " . join( " AND ", $conds );
01225 
01226         $pathRows = $db->arrayQuery( $query );
01227         $elements = array();
01228         if ( count( $pathRows ) > 0 )
01229         {
01230             foreach ( $pathRows as $pathRow )
01231             {
01232                 $redirectLink = false;
01233                 $table = "e" . $elementOffset;
01234                 $element = array( 'id'        => $pathRow[$table . "_id"],
01235                                   'parent'    => $pathRow[$table . "_parent"],
01236                                   'lang_mask' => $pathRow[$table . "_lang_mask"],
01237                                   'text'      => $pathRow[$table . "_text"],
01238                                   'action'    => $pathRow[$table . "_action"],
01239                                   'link'      => $pathRow[$table . "_link"] );
01240                 $path = array();
01241                 $lastID = false;
01242                 for ( $i = 0; $i < $len; ++$i )
01243                 {
01244                     $table = "e" . $i;
01245                     $id   = $pathRow[$table . "_id"];
01246                     $link = $pathRow[$table . "_link"];
01247                     $path[] = $pathRow[$table . "_text"];
01248                     if ( $link != $id )
01249                     {
01250                         // Mark the redirect link
01251                         $redirectLink = $link;
01252                         $redirectOffset = $i;
01253                     }
01254                     $lastID = $link;
01255                 }
01256                 if ( $redirectLink )
01257                 {
01258                     $newLinkID = $redirectLink;
01259                     // Resolve new links until a real element is found.
01260                     // TODO: Add max redirection count?
01261                     while ( $newLinkID )
01262                     {
01263                         $query = "SELECT id, parent, lang_mask, text, link FROM ezurlalias_ml WHERE id={$newLinkID}";
01264                         $rows = $db->arrayQuery( $query );
01265                         if ( count( $rows ) == 0 )
01266                         {
01267                             return false;
01268                         }
01269                         $newLinkID = false;
01270                         if ( $rows[0]['id'] != $rows[0]['link'] )
01271                             $newLinkID = (int)$rows[0]['link'];
01272                     }
01273                     $id = (int)$newLinkID;
01274                     $path = array();
01275 
01276                     // Fetch path 'text' elements of correct parent path
01277                     while ( $id != 0 )
01278                     {
01279                         $query = "SELECT parent, lang_mask, text FROM ezurlalias_ml WHERE id={$id}";
01280                         $rows = $db->arrayQuery( $query );
01281                         if ( count( $rows ) == 0 )
01282                         {
01283                             break;
01284                         }
01285                         $result = eZURLAliasML::choosePrioritizedRow( $rows );
01286                         if ( !$result )
01287                         {
01288                             $result = $rows[0];
01289                         }
01290                         $id = (int)$result['parent'];
01291                         array_unshift( $path, $result['text'] );
01292                     }
01293                     // Fill in end of path elements
01294                     for ( $i = $redirectOffset; $i < $len; ++$i )
01295                     {
01296                         $table = "e" . $i;
01297                         $path[] = $pathRow[$table . "_text"];
01298                     }
01299                 }
01300                 $element['path'] = implode( '/', $path );
01301                 $elements[] = $element;
01302             }
01303         }
01304         $rows = array();
01305         $ids = array();
01306         // Discard duplicates
01307         foreach ( $elements as $element )
01308         {
01309             $id = (int)$element['id'];
01310             if ( isset( $ids[$id] ) )
01311                 continue;
01312             $ids[$id] = true;
01313             $rows[] = $element;
01314         }
01315         $objectList = eZPersistentObject::handleRows( $rows, 'eZURLAliasML', true );
01316         return $objectList;
01317     }
01318 
01319     /*!
01320      \static
01321      The same as 'fetchByPath' but extracting nodeID from action.
01322      Only first entry will be processed if 'fetchByPath' returns multiple result(e.g. $glob is wildcard).
01323      \return nodeID on success or \c false otherwise.
01324      */
01325     static public function fetchNodeIDByPath( $uriString, $glob = false )
01326     {
01327         $nodeID = false;
01328 
01329         $urlAliasMLList = eZURLAliasML::fetchByPath( $uriString, $glob );
01330         if ( is_array( $urlAliasMLList ) && count( $urlAliasMLList ) > 0 )
01331             $nodeID = eZURLAliasML::nodeIDFromAction( $urlAliasMLList[0]->Action );
01332 
01333         return $nodeID;
01334     }
01335 
01336     /*!
01337      \static
01338      Transforms the URI if there exists an alias for it, the new URI is replaced in $uri.
01339      \return \c true is if successful, \c false otherwise
01340      \return The string with new url is returned if the translation was found, but the resource has moved.
01341 
01342      Lets say we have the following elements:
01343      \code
01344      === ==== ====== =========== ==========
01345      id  link parent text        action
01346      === ==== ====== =========== ==========
01347      1   1    0      'ham'       'eznode:4'
01348      2   6    0      'spam'      'eznode:55'
01349      3   3    0      'bicycle'   'eznode:5'
01350      4   4    0      'superman'  'nop:'
01351      5   5    3      'repairman' 'eznode:42'
01352      6   6    3      'repoman'   'eznode:55'
01353      === ==== ====== =========== ==========
01354      \endcode
01355 
01356      then we try to translate a path:
01357      \code
01358      $uri = "bicycle/repairman";
01359      $result = eZURLAliasML::translate( $uri );
01360      if ( $result )
01361      {
01362          echo $result, "\n";
01363          echo $uri, "\n";
01364      }
01365      \endcode
01366 
01367      we would get:
01368      \code
01369      '1'
01370      'content/view/full/42'
01371      \endcode
01372 
01373      If we then were to try:
01374      \code
01375      $uri = "spam";
01376      $result = eZURLAliasML::translate( $uri );
01377      if ( $result )
01378      {
01379          echo $result, "\n";
01380          echo $uri, "\n";
01381      }
01382      \endcode
01383 
01384      we would get:
01385      \code
01386      'bicycle/repoman'
01387      'error/301'
01388      \endcode
01389 
01390      Trying a non-existing path:
01391      \code
01392      $uri = "spam/a-lot";
01393      $result = eZURLAliasML::translate( $uri );
01394      if ( $result )
01395      {
01396          echo $result, "\n";
01397          echo $uri, "\n";
01398      }
01399      \endcode
01400 
01401      then $result would be empty:
01402 
01403      Alterntively we can also do a reverse lookup:
01404      \code
01405      $uri = "content/view/full/55";
01406      $result = eZURLAliasML::translate( $uri, true );
01407      if ( $result )
01408      {
01409          echo $result, "\n";
01410          echo $uri, "\n";
01411      }
01412      \endcode
01413 
01414      we would get:
01415      \code
01416      '1'
01417      'bicycle/repoman'
01418      \endcode
01419     */
01420     static public function translate( &$uri, $reverse = false )
01421     {
01422         if ( $uri instanceof eZURI )
01423         {
01424             $uriString = $uri->elements();
01425         }
01426         else
01427         {
01428             $uriString = $uri;
01429         }
01430         $uriString = eZURLAliasML::cleanURL( $uriString );
01431         $internalURIString = $uriString;
01432         $originalURIString = $uriString;
01433 
01434         $ini = eZINI::instance();
01435 
01436         $prefixAdded = false;
01437         $prefix = $ini->hasVariable( 'SiteAccessSettings', 'PathPrefix' ) &&
01438                       $ini->variable( 'SiteAccessSettings', 'PathPrefix' ) != '' ? eZURLAliasML::cleanURL( $ini->variable( 'SiteAccessSettings', 'PathPrefix' ) ) : false;
01439 
01440         if ( $prefix )
01441         {
01442             $escapedPrefix = preg_quote( $prefix, '#' );
01443             // Only prepend the path prefix if it's not already the first element of the url.
01444             if ( !preg_match( "#^$escapedPrefix(/.*)?$#i", $uriString )  )
01445             {
01446                 $exclude = $ini->hasVariable( 'SiteAccessSettings', 'PathPrefixExclude' )
01447                            ? $ini->variable( 'SiteAccessSettings', 'PathPrefixExclude' )
01448                            : false;
01449                 $breakInternalURI = false;
01450                 foreach ( $exclude as $item )
01451                 {
01452                     $escapedItem = preg_quote( $item, '#' );
01453                     if ( preg_match( "#^$escapedItem(/.*)?$#i", $uriString )  )
01454                     {
01455                         $breakInternalURI = true;
01456                         break;
01457                     }
01458                 }
01459 
01460                 if ( !$breakInternalURI )
01461                 {
01462                     $internalURIString = $prefix . '/' . $uriString;
01463                     $prefixAdded = true;
01464                 }
01465             }
01466         }
01467 
01468         $db = eZDB::instance();
01469         $elements = explode( '/', $internalURIString );
01470         $len      = count( $elements );
01471         if ( $reverse )
01472         {
01473             return eZURLAliasML::reverseTranslate( $uri, $uriString, $internalURIString );
01474         }
01475 
01476         $i = 0;
01477         $selects = array();
01478         $tables  = array();
01479         $conds   = array();
01480         foreach ( $elements as $element )
01481         {
01482             $table = "e" . $i;
01483 
01484             $selectString = "{$table}.id AS {$table}_id, ";
01485             $selectString .= "{$table}.link AS {$table}_link, ";
01486             $selectString .= "{$table}.text AS {$table}_text, ";
01487             $selectString .= "{$table}.text_md5 AS {$table}_text_md5, ";
01488             $selectString .= "{$table}.is_alias AS {$table}_is_alias, ";
01489 
01490             if ( $i == $len - 1 )
01491                 $selectString .= "{$table}.action AS {$table}_action, ";
01492 
01493             $selectString .= "{$table}.alias_redirects AS {$table}_alias_redirects";
01494             $selects[] = $selectString;
01495 
01496             $tables[]  = "ezurlalias_ml " . $table;
01497             $langMask = trim( eZContentLanguage::languagesSQLFilter( $table, 'lang_mask' ) );
01498             if ( $i == 0 )
01499             {
01500                 $conds[]   = "{$table}.parent = 0 AND ({$langMask}) AND {$table}.text_md5 = " . eZURLAliasML::md5( $db, $element );
01501             }
01502             else
01503             {
01504                 $conds[]   = "{$table}.parent = {$prevTable}.link AND ({$langMask}) AND {$table}.text_md5 = " . eZURLAliasML::md5( $db, $element );
01505             }
01506             $prevTable = $table;
01507             ++$i;
01508         }
01509 
01510         $query = "SELECT " . join( ", ", $selects ) . " FROM " . join( ", ", $tables ) . " WHERE " . join( " AND ", $conds );
01511         $return = false;
01512         $urlAliasArray = $db->arrayQuery( $query, array( 'limit' => 1 ) );
01513         if ( count( $urlAliasArray ) > 0 )
01514         {
01515             $pathRow = $urlAliasArray[0];
01516             $l   = count( $pathRow );
01517             $redirectLink = false;
01518             $redirectAction = false;
01519             $lastID = false;
01520             $action = false;
01521             $verifiedPath = array();
01522             $doRedirect = false;
01523 
01524             for ( $i = 0; $i < $len; ++$i )
01525             {
01526                 $table = "e" . $i;
01527                 $id   = $pathRow[$table . "_id"];
01528                 $link = $pathRow[$table . "_link"];
01529                 $text = $pathRow[$table . "_text"];
01530                 $isAlias = $pathRow[$table . '_is_alias'];
01531                 $aliasRedirects = $pathRow[$table . '_alias_redirects'];
01532                 $verifiedPath[] = $text;
01533                 if ( $i == $len - 1 )
01534                 {
01535                     $action = $pathRow[$table . "_action"];
01536                 }
01537                 if ( $link != $id )
01538                 {
01539                     $doRedirect = true;
01540                 }
01541                 else if ( $isAlias && $action !== false )
01542                 {
01543                     if ( $aliasRedirects )
01544                     {
01545                         // If the entry is an alias and we have an action we redirect to the original
01546                         // url of that action.
01547                         $redirectAction = $action;
01548                         $doRedirect = true;
01549                     }
01550                 }
01551                 $lastID = $link;
01552             }
01553 
01554             if ( !$doRedirect )
01555             {
01556                 $verifiedPathString = implode( '/', $verifiedPath );
01557                 // Check for case difference
01558                 if ( $prefixAdded )
01559                 {
01560                     if ( strcmp( $originalURIString, substr( $verifiedPathString, strlen( $prefix ) + 1 ) ) != 0 )
01561                     {
01562                         $doRedirect = true;
01563                     }
01564                 }
01565                 else if ( strcmp( $verifiedPathString, $internalURIString ) != 0 )
01566                 {
01567                     $doRedirect = true;
01568                 }
01569             }
01570 
01571             if ( preg_match( "#^module:(.+)$#", $action, $matches ) and $doRedirect )
01572             {
01573                 $uriString = 'error/301';
01574                 $return = $matches[1];
01575             }
01576             else if ( $doRedirect )
01577             {
01578                 if ( $redirectAction !== false )
01579                 {
01580                     $query = "SELECT id FROM ezurlalias_ml WHERE action = '" . $db->escapeString( $action ) . "' AND is_original = 1 AND is_alias = 0";
01581                     $rows  = $db->arrayQuery( $query );
01582                     if ( count( $rows ) > 0 )
01583                     {
01584                         $id        = (int)$rows[0]['id'];
01585                     }
01586                     else
01587                     {
01588                         $id        = false;
01589                         $uriString = 'error/301';
01590                         $return    = join( "/", $pathData );
01591                     }
01592                 }
01593                 else
01594                 {
01595                     $id = (int)$lastID;
01596                 }
01597 
01598                 if ( $id !== false )
01599                 {
01600                     $pathData = array();
01601                     // Figure out the correct path by iterating down the parents until we have all
01602                     // elements figured out.
01603 
01604                     while ( $id != 0 )
01605                     {
01606                         $query = "SELECT parent, lang_mask, text FROM ezurlalias_ml WHERE id={$id}";
01607                         $rows = $db->arrayQuery( $query );
01608                         if ( count( $rows ) == 0 )
01609                         {
01610                             break;
01611                         }
01612                         $result = eZURLAliasML::choosePrioritizedRow( $rows );
01613                         if ( !$result )
01614                         {
01615                             $result = $rows[0];
01616                         }
01617                         $id = (int)$result['parent'];
01618                         array_unshift( $pathData, $result['text'] );
01619                     }
01620                     $uriString = 'error/301';
01621                     $return = join( "/", $pathData );
01622                 }
01623 
01624                 // Remove prefix of redirect uri if needed
01625                 if ( $prefix && is_string( $return ) )
01626                 {
01627                     if ( strncasecmp( $return, $prefix . '/', strlen( $prefix ) + 1 ) == 0 )
01628                     {
01629                         $return = substr( $return, strlen( $prefix ) + 1 );
01630                     }
01631                 }
01632             }
01633             else
01634             {
01635                 // See http://issues.ez.no/19062
01636                 // If $uriString matches a nop action, we need to check if we also match a wildcard
01637                 // since we might want to translate it.
01638                 // Default action for nop actions is to display the root node "/" (see eZURLAliasML::actionToURL())
01639                 if ( strpos( $action, 'nop') !== false && eZURLWildcard::wildcardExists( $uriString ) )
01640                 {
01641                     $return = false;
01642                 }
01643                 else
01644                 {
01645                     $uriString = eZURLAliasML::actionToUrl( $action );
01646                     $return = true;
01647                 }
01648 
01649             }
01650 
01651             if ( $uri instanceof eZURI )
01652             {
01653                 $uri->setURIString( $uriString, false );
01654             }
01655             else
01656             {
01657                 $uri = $uriString;
01658             }
01659         }
01660 
01661         return $return;
01662     }
01663 
01664     /*!
01665      \private
01666      \static
01667      Perform reverse translation of uri, that is from system-url to url alias.
01668      */
01669     static public function reverseTranslate( &$uri, $uriString, $internalURIString )
01670     {
01671         $db = eZDB::instance();
01672 
01673         $action = eZURLAliasML::urlToAction( $internalURIString );
01674         if ( $action !== false )
01675         {
01676             $langMask = trim( eZContentLanguage::languagesSQLFilter( 'ezurlalias_ml', 'lang_mask' ) );
01677             $actionStr = $db->escapeString( $action );
01678             $query = "SELECT id, parent, lang_mask, text, action FROM ezurlalias_ml WHERE ($langMask) AND action='{$actionStr}' AND is_original = 1 AND is_alias = 0";
01679             $rows = $db->arrayQuery( $query );
01680             $path = array();
01681             $count = count( $rows );
01682             if ( $count != 0 )
01683             {
01684                 $row = eZURLAliasML::choosePrioritizedRow( $rows );
01685                 if ( $row === false )
01686                 {
01687                     $row = $rows[0];
01688                 }
01689                 $paren = (int)$row['parent'];
01690                 $path[] = $row['text'];
01691                 // We have the parent so now do an iterative lookup until we have the top element
01692                 while ( $paren != 0 )
01693                 {
01694                     $query = "SELECT id, parent, lang_mask, text FROM ezurlalias_ml WHERE ($langMask) AND id=$paren AND is_original = 1 AND is_alias = 0";
01695                     $rows = $db->arrayQuery( $query );
01696                     $count = count( $rows );
01697                     if ( $count != 0 )
01698                     {
01699                         $row = eZURLAliasML::choosePrioritizedRow( $rows );
01700                         if ( $row === false )
01701                         {
01702                             $row = $rows[0];
01703                         }
01704                         $paren = (int)$row['parent'];
01705                         array_unshift( $path, $row['text'] );
01706                     }
01707                     else
01708                     {
01709                         eZDebug::writeError( "Lookup of parent ID $paren failed, cannot perform reverse lookup of alias." );
01710                         return false;
01711                     }
01712                 }
01713                 $uriString = join( '/', $path );
01714                 if ( $uri instanceof eZURI )
01715                 {
01716                     $uri->setURIString( $uriString, false );
01717                 }
01718                 else
01719                 {
01720                     $uri = $uriString;
01721                 }
01722                 return true;
01723             }
01724             else
01725             {
01726                 return false;
01727             }
01728         }
01729         return false;
01730     }
01731 
01732     /*!
01733      \static
01734      Checks if url translation should be used on the current url.
01735 
01736      \param $uri The current eZUri object
01737      */
01738     static public function urlTranslationEnabledByUri( eZURI $uri )
01739     {
01740         if ( $uri->isEmpty() )
01741             return false;
01742 
01743         $ini = eZINI::instance();
01744         if ( $ini->variable( 'URLTranslator', 'Translation' ) === 'enabled' )
01745         {
01746             if ( $ini->variable( 'URLTranslator', 'TranslatableSystemUrls' ) === 'disabled' )
01747             {
01748                 $moduleName = $uri->element( 0 );
01749                 $moduleINI  = eZINI::instance( 'module.ini' );
01750                 $moduleList = $moduleINI->variable( 'ModuleSettings', 'ModuleList' );
01751                 if ( in_array( $moduleName, $moduleList, true ) )
01752                   return false;
01753             }
01754             return true;
01755         }
01756         return false;
01757     }
01758 
01759     /*!
01760      \static
01761      Checks if the text entry $text is unique on the current level in the URL path.
01762      If not the name is adjusted with a number at the end until it becomes unique.
01763      The unique text string is returned.
01764 
01765      \param $text The text element which is to be checked
01766      \param $action The action string which is to be excluded from the check. Set to empty string to disable the exclusion.
01767      \param $linkCheck If true then it will see all existing entries as taken.
01768      */
01769     static public function findUniqueText( $parentElementID, $text, $action, $linkCheck = false, $languageID = false )
01770     {
01771         $db = eZDB::instance();
01772         $uniqueNumber =  0;
01773         // If there is no parent we need to check against reserved words
01774         if ( $parentElementID == 0 )
01775         {
01776             $moduleINI = eZINI::instance( 'module.ini' );
01777             $reserved = $moduleINI->variable( 'ModuleSettings', 'ModuleList' );
01778             foreach ( $reserved as $res )
01779             {
01780                 if ( strcasecmp( $text, $res ) == 0 )
01781                 {
01782                     // The name is a reserved word so it needs to be changed
01783                     ++$uniqueNumber;
01784                     break;
01785                 }
01786             }
01787         }
01788         $suffix = '';
01789         if ( $uniqueNumber )
01790             $suffix = $uniqueNumber + 1;
01791 
01792         $actionSQL = '';
01793         if ( strlen( $action ) > 0 )
01794         {
01795             $actionEsc = $db->escapeString( $action );
01796             $actionSQL = "AND action != '$actionEsc'";
01797         }
01798         $languageSQL = "";
01799         if ( $languageID !== false )
01800         {
01801             $languageSQL = "AND " . $db->bitAnd(  'lang_mask', $languageID ) . ' > 0';
01802         }
01803         // Loop until we find a unique name
01804         while ( true )
01805         {
01806             $textEsc = eZURLAliasML::md5( $db, $text . $suffix );
01807             $query = "SELECT * FROM ezurlalias_ml WHERE parent = $parentElementID $actionSQL $languageSQL AND text_md5 = $textEsc";
01808             if ( !$linkCheck )
01809             {
01810                 $query .= " AND is_original = 1";
01811             }
01812             if ( $db->databaseName() === 'mysql' )
01813             {
01814                 $query .= ' LOCK IN SHARE MODE';
01815             }
01816             $rows = $db->arrayQuery( $query );
01817             if ( count( $rows ) == 0 )
01818             {
01819                 return $text . $suffix;
01820             }
01821 
01822             ++$uniqueNumber;
01823             $suffix = $uniqueNumber + 1;
01824         }
01825     }
01826 
01827     /*!
01828      \static
01829      Updates the lang_mask field for path elements which matches action $actionName and value $actionValue.
01830      If $langID is false then bit 0 (the *always available* bit) will be removed, otherwise it will set bit 0 for the chosen language and remove it for other languages.
01831      */
01832     static public function setLangMaskAlwaysAvailable( $langID, $actionName, $actionValue )
01833     {
01834         if ( !$actionName )
01835         {
01836             eZDebug::writeError( "ActionName value must not be empty", __METHOD__ );
01837             return null;
01838         }
01839         $db = eZDB::instance();
01840         if ( is_array( $actionName ) )
01841         {
01842             $actions = array();
01843             foreach ( $actionName as $actionItem )
01844             {
01845                 $action = $actionItem[0] . ":" . $actionItem[1];
01846                 $actions[] = "'" . $db->escapeString( $action ) . "'";
01847             }
01848             $actionSql = "action in (" . implode( ', ', $actions ) . ")";
01849         }
01850         else
01851         {
01852             $action = $actionName . ":" . $actionValue;
01853             $actionSql = "action = '" . $db->escapeString( $action ) . "'";
01854         }
01855         if ( $langID !== false )
01856         {
01857             // Set the 0 bit for chosen language
01858             $bitOp = $db->bitOr( 'lang_mask', 1 );
01859             $langWhere = ' AND ' . $db->bitAnd( 'lang_mask' , (int)$langID ) . ' > 0';
01860 
01861             $query = "UPDATE ezurlalias_ml SET lang_mask = $bitOp WHERE $actionSql $langWhere";
01862             $db->query( $query );
01863 
01864             // Clear the 0 bit for all other languages
01865             $bitOp = $db->bitAnd( 'lang_mask', ~1 );
01866             $langWhere = ' AND ' . $db->bitAnd( 'lang_mask' , (int)$langID ) . ' = 0';
01867 
01868             $query = "UPDATE ezurlalias_ml SET lang_mask = $bitOp WHERE $actionSql $langWhere";
01869             $db->query( $query );
01870         }
01871         else
01872         {
01873             $bitOp = $db->bitAnd( 'lang_mask', ~1 );
01874             $query = "UPDATE ezurlalias_ml SET lang_mask = $bitOp WHERE $actionSql";
01875             $db->query( $query );
01876         }
01877     }
01878 
01879     /**
01880      * Chooses the most prioritized row (based on language) of $rows and returns it.
01881      * @param array $rows
01882      * @return array|false The most prioritized row, or false if no match was found
01883      */
01884     static public function choosePrioritizedRow( $rows )
01885     {
01886         $result = false;
01887         $score = 0;
01888         foreach ( $rows as $row )
01889         {
01890             if ( $result )
01891             {
01892                 $newScore = eZURLAliasML::languageScore( $row['lang_mask'] );
01893                 if ( $newScore > $score )
01894                 {
01895                     $result = $row;
01896                     $score = $newScore;
01897                 }
01898             }
01899             else
01900             {
01901                 $result = $row;
01902                 $score = eZURLAliasML::languageScore( $row['lang_mask'] );
01903             }
01904         }
01905 
01906         // If score is still 0, this means that the objects languages don't
01907         // match the INI settings, and these should be fix according to the doc.
01908         if ( $score == 0 )
01909         {
01910             eZDebug::writeWarning(
01911                 "None of the available languages are prioritized in the SiteLanguageList setting. An arbitrary language will be used.",
01912                 __METHOD__ );
01913         }
01914 
01915         return $result;
01916     }
01917 
01918     /*!
01919      \static
01920      \private
01921      Filters the DB rows $rows by selecting the most prioritized row per
01922      path element and returns the new row list.
01923      \param $onlyPrioritized If false all rows are returned, if true filtering is performed.
01924      */
01925     static private function filterRows( $rows, $onlyPrioritized )
01926     {
01927         if ( !$onlyPrioritized )
01928         {
01929             return $rows;
01930         }
01931         $idMap = array();
01932         foreach ( $rows as $row )
01933         {
01934             if ( !isset( $idMap[$row['id']] ) )
01935             {
01936                 $idMap[$row['id']] = array();
01937             }
01938             $idMap[$row['id']][] = $row;
01939         }
01940 
01941         $rows = array();
01942         foreach ( $idMap as $id => $langRows )
01943         {
01944             $rows[] = eZURLAliasML::choosePrioritizedRow( $langRows );
01945         }
01946 
01947         return $rows;
01948     }
01949 
01950     /*!
01951      \static
01952      \private
01953      Calculates the score of the language mask $mask based upon the currently
01954      prioritized languages and returns it.
01955      \note The higher the value the more the language is prioritized.
01956      */
01957     static private function languageScore( $mask )
01958     {
01959         $prioritizedLanguages = eZContentLanguage::prioritizedLanguages();
01960         $scores = array();
01961         $score = 1;
01962         $mask   = (int)$mask;
01963         krsort( $prioritizedLanguages );
01964         foreach ( $prioritizedLanguages as $prioritizedLanguage )
01965         {
01966             $id = (int)$prioritizedLanguage->attribute( 'id' );
01967             if ( $id & $mask )
01968             {
01969                 $scores[] = $score;
01970             }
01971             ++$score;
01972         }
01973         if ( count( $scores ) > 0 )
01974         {
01975             return max( $scores );
01976         }
01977         else
01978         {
01979             return 0;
01980         }
01981     }
01982 
01983     /*!
01984      \static
01985      Decodes the action string $action into an internal path string and returns it.
01986 
01987      The following actions are supported:
01988      - eznode - argument is node ID, path is 'content/view/full/<nodeID>'
01989      - module - argument is module/view/args, path is the arguments
01990      - nop    - a no-op, path is '/'
01991      */
01992     static public function actionToUrl( $action )
01993     {
01994         if ( !preg_match( "#^([a-zA-Z0-9_]+):(.+)?$#", $action, $matches ) )
01995         {
01996             eZDebug::writeError( "Action is not of valid syntax '{$action}'" );
01997             return false;
01998         }
01999 
02000         $type = $matches[1];
02001         $args = '';
02002         if ( isset( $matches[2] ) )
02003             $args = $matches[2];
02004         switch ( $type )
02005         {
02006             case 'eznode':
02007                 if ( !is_numeric( $args ) )
02008                 {
02009                     eZDebug::writeError( "Arguments to eznode action must be an integer, got '{$args}'" );
02010                     return false;
02011                 }
02012                 $url = 'content/view/full/' . $args;
02013                 break;
02014 
02015             case 'module':
02016                 $url = $args;
02017                 break;
02018 
02019             case 'nop':
02020                 $url = '/';
02021                 break;
02022 
02023             default:
02024                 eZDebug::writeError( "Unknown action type '{$type}', cannot handle it" );
02025                 return false;
02026         }
02027         return $url;
02028     }
02029 
02030     /*!
02031      \static
02032      Takes the url string $url and returns the action string for it.
02033 
02034      The following path are supported:
02035      - content/view/full/<nodeID> => eznode:<nodeID>
02036 
02037      If the url points to an existing module it will return module:<url>
02038 
02039      \return false if the action could not be figured out.
02040      */
02041     static public function urlToAction( $url )
02042     {
02043         if ( preg_match( "#^content/view/full/([0-9]+)$#", $url, $matches ) )
02044         {
02045             return "eznode:" . $matches[1];
02046         }
02047         if ( preg_match( "#^([a-zA-Z0-9]+)/#", $url, $matches ) )
02048         {
02049             $name = $matches[1];
02050             $module = eZModule::exists( $name );
02051             if ( $module !== null )
02052                 return 'module:' . $url;
02053         }
02054         return false;
02055     }
02056 
02057     /*!
02058      \static
02059      Makes sure the URL \a $url does not contain leading and trailing slashes (/).
02060      \return the clean URL
02061     */
02062     static public function cleanURL( $url )
02063     {
02064         return trim( $url, '/ ' );
02065     }
02066 
02067     /*!
02068      \static
02069      Transform a semi-valid url into one that can be stored in the url-alias system.
02070      Removes leading/trailing slashes and repeated slashes.
02071 
02072      \code
02073      echo eZURLAliasML::sanitizeURL( "" ); // Result ""
02074      echo eZURLAliasML::sanitizeURL( "users//the_dude" ); // Result "users/the_dude"
02075      echo eZURLAliasML::sanitizeURL( "archive/products/" ); // Result "archive/products"
02076      \endcode
02077      \return the sanitized URL
02078     */
02079     static public function sanitizeURL( $url )
02080     {
02081         $url = preg_replace( "#//+#", "/", trim( $url, '/' ) );
02082         return $url;
02083     }
02084 
02085     /*!
02086      \private
02087      \static
02088      Generates partial SELECT part of SQL based on table $table, counter $i and total length $len.
02089      */
02090     static private function generateSelect( $table, $i, $len )
02091     {
02092         if ( $i == $len - 1 )
02093         {
02094             $select = "{$table}.id AS {$table}_id, {$table}.link AS {$table}_link, {$table}.text AS {$table}_text, {$table}.text_md5 AS {$table}_text_md5, {$table}.action AS {$table}_action";
02095         }
02096         else
02097         {
02098             $select = "{$table}.id AS {$table}_id, {$table}.link AS {$table}_link, {$table}.text AS {$table}_text, {$table}.text_md5 AS {$table}_text_md5";
02099         }
02100         return $select;
02101     }
02102 
02103     /*!
02104      \private
02105      \static
02106      Generates full SELECT part of SQL based on table $table.
02107      */
02108     static private function generateFullSelect( $table )
02109     {
02110         $select = "{$table}.id AS {$table}_id, {$table}.parent AS {$table}_parent, {$table}.lang_mask AS {$table}_lang_mask, {$table}.text AS {$table}_text, {$table}.text_md5 AS {$table}_text_md5, {$table}.action AS {$table}_action, {$table}.link AS {$table}_link";
02111         return $select;
02112     }
02113 
02114     /*!
02115      \private
02116      \static
02117      Generates WHERE part of SQL based on table $table, previous table $prevTable, counter $i, language mask $langMask and text $element.
02118      */
02119     static private function generateCond( $table, $prevTable, $i, $langMask, $element )
02120     {
02121         $db = eZDB::instance();
02122         if ( $i == 0 )
02123         {
02124             $cond = "{$table}.parent = 0 AND ({$langMask}) AND {$table}.text_md5 = " . eZURLAliasML::md5( $db, $element );
02125         }
02126         else
02127         {
02128             $cond = "{$table}.parent = {$prevTable}.link AND ({$langMask}) AND {$table}.text_md5 = " . eZURLAliasML::md5( $db, $element );
02129         }
02130         return $cond;
02131     }
02132 
02133     /*!
02134      \private
02135      \static
02136      Generates WHERE part of SQL for a wildcard match based on table $table, previous table $prevTable, counter $i, language mask $langMask and wildcard text $glob.
02137      \note $glob does not contain the wildcard character * but only the beginning of the matching text.
02138      */
02139     static private function generateGlobCond( $table, $prevTable, $i, $langMask, $glob )
02140     {
02141         $db = eZDB::instance();
02142         if ( $i == 0 )
02143         {
02144             $cond = "{$table}.parent = 0 AND ({$langMask}) AND {$table}.text LIKE '" . $db->escapeString( $glob ) . "%'";
02145         }
02146         else
02147         {
02148             $cond = "{$table}.parent = {$prevTable}.link AND ({$langMask}) AND {$table}.text LIKE '" . $db->escapeString( $glob ) . "%'";
02149         }
02150         return $cond;
02151     }
02152 
02153     /*!
02154      \static
02155      Converts the path \a $urlElement into a new alias url which only conists of valid characters
02156      in the URL.
02157      For non-Unicode setups this means character in the range a-z, numbers and _, for Unicode
02158      setups it means all characters except space, &, ;, /, :, =, ?, [, ], (, ), -
02159 
02160      Invalid characters are converted to -.
02161      \return the converted element
02162 
02163      Example with a non-Unicode setup
02164      \example
02165      'My car' => 'My-car'
02166      'What is this?' => 'What-is-this'
02167      'This & that' => 'This-that'
02168      'myfile.tpl' => 'Myfile-tpl',
02169      'øæå' => 'oeaeaa'
02170      \endexample
02171     */
02172     static public function convertToAlias( $urlElement, $defaultValue = false )
02173     {
02174         $trans = eZCharTransform::instance();
02175 
02176         $ini = eZINI::instance();
02177         $group = $ini->variable( 'URLTranslator', 'TransformationGroup' );
02178 
02179         $urlElement = $trans->transformByGroup( $urlElement, $group );
02180         if ( strlen( $urlElement ) == 0 )
02181         {
02182             if ( $defaultValue === false )
02183                 $urlElement = '_1';
02184             else
02185             {
02186                 $urlElement = $defaultValue;
02187                 $urlElement = $trans->transformByGroup( $urlElement, $group );
02188             }
02189         }
02190         return $urlElement;
02191     }
02192 
02193     /*!
02194      \static
02195      Converts the path \a $urlElement into a new alias url which only conists of valid characters
02196      in the URL.
02197      This means character in the range a-z, numbers and _.
02198 
02199      Invalid characters are converted to -.
02200      \return the converted element
02201 
02202      \example
02203      'My car' => 'My-car'
02204      'What is this?' => 'What-is-this'
02205      'This & that' => 'This-that'
02206      'myfile.tpl' => 'Myfile-tpl',
02207      'øæå' => 'oeaeaa'
02208      \endexample
02209 
02210      \note Provided for creating url alias as they were before 3.10. Also used to make path_identification_string.
02211     */
02212     static public function convertToAliasCompat( $urlElement, $defaultValue = false )
02213     {
02214         $trans = eZCharTransform::instance();
02215 
02216         $urlElement = $trans->transformByGroup( $urlElement, "urlalias_compat" );
02217         if ( strlen( $urlElement ) == 0 )
02218         {
02219             if ( $defaultValue === false )
02220                 $urlElement = '_1';
02221             else
02222             {
02223                 $urlElement = $defaultValue;
02224                 $urlElement = $trans->transformByGroup( $urlElement, "urlalias_compat" );
02225             }
02226         }
02227         return $urlElement;
02228     }
02229 
02230     /*!
02231      \static
02232      Converts the path \a $pathURL into a new alias path with limited characters.
02233      For more information on the conversion see convertToAlias().
02234      \note each element in the path (separated by / (slash) ) is converted separately.
02235      \return the converted path
02236     */
02237     static public function convertPathToAlias( $pathURL )
02238     {
02239         $result = array();
02240 
02241         $elements = explode( '/', $pathURL );
02242 
02243         foreach ( $elements as $element )
02244         {
02245             $element = eZURLAliasML::convertToAlias( $element );
02246             $result[] = $element;
02247         }
02248 
02249         return implode( '/', $result );
02250     }
02251 
02252     /*!
02253      \static
02254      Grabs nodeID from action string.
02255      \return nodeID on success, \c false otherwise.
02256      */
02257     static public function nodeIDFromAction( $action )
02258     {
02259         $nodeID = false;
02260         $pos = strpos( $action, 'eznode:' );
02261         if ( $pos === 0 ) // make sure $action starts from 'eznode:'
02262             $nodeID = substr( $action, strlen( 'eznode:' ) );
02263 
02264         return $nodeID;
02265     }
02266 
02267     /*!
02268      \static
02269      Wraps a database md5 call around the string $text and returns the new SQL for it.
02270 
02271      \param $escape If true it will lowercase the text and escape it.
02272      \note If the database is Oracle and the text is empty the MD5 is computed by PHP
02273            and returned.
02274      */
02275     static private function md5( $db, $text, $escape = true )
02276     {
02277         // Special case for Oracle since it cannot calculate MD5 for empty strings
02278         if ( strlen( $text ) == 0 && $db->databaseName() == 'oracle' )
02279             return "'" . $db->escapeString( md5( $text ) ) . "'";
02280 
02281         if ( $escape )
02282             $text = $db->escapeString( eZURLAliasML::strtolower( $text ) );
02283         return $db->md5( "'" . $text . "'" );
02284     }
02285 
02286     static function getNewID()
02287     {
02288         $db = eZDB::instance();
02289         if ( $db->supportsDefaultValuesInsertion() )
02290         {
02291             $db->query( 'INSERT INTO ezurlalias_ml_incr DEFAULT VALUES' );
02292         }
02293         else
02294         {
02295             // can not use VALUES(DEFAULT), because of http://bugs.mysql.com/bug.php?id=42270
02296             $db->query( 'INSERT INTO ezurlalias_ml_incr(id) VALUES(NULL)' );
02297         }
02298 
02299         return $db->lastSerialID( 'ezurlalias_ml_incr', 'id' );
02300     }
02301 }
02302 
02303 ?>