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