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