00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00079 define( "EZ_INI_CACHE_CODE_DATE", 1043407541 );
00080 define( "EZ_INI_DEBUG_INTERNALS", false );
00081
eZINI
00083 {
rootDir = "", $useTextCodec = null, $useCache = null )
00088 {
00089 $this->Charset = "utf8";
00090 if ( $fileName == "" )
00091 $fileName = "site.ini";
00092 if ( $rootDir == "" )
00093 $rootDir = "settings";
00094 if ( $useCache === null )
00095 $useCache = eZINI::isCacheEnabled();
00096 if ( eZINI::isNoCacheAdviced() )
00097 {
00098 $useCache = false;
00099 }
00100 if ( $useTextCodec === null )
00101 $useTextCodec = eZINI::isTextCodecEnabled();
00102
00103 $this->UseTextCodec = $useTextCodec;
00104 $this->FileName = $fileName;
00105 $this->rootDir;
00106 $this->UseCache = $useCache;
00107
00108 $this->load();
00109 }
00110
filename()
00115 {
00116 return $this->FileName;
00117 }
00118
isCacheEnabled()
00125 {
00126 if ( !isset( $GLOBALS['eZINICacheEnabled'] ) )
00127 $GLOBALS['eZINICacheEnabled'] = true;
00128 return $GLOBALS['eZINICacheEnabled'];
00129 }
00130
isNoCacheAdviced()
00136 {
00137 if ( !isset( $GLOBALS['eZSiteBasics'] ) )
00138 return false;
00139 $siteBasics = $GLOBALS['eZSiteBasics'];
00140 return $siteBasics['no-cache-adviced'];
00141 }
00142
setIsCacheEnabled( $cache )
00149 {
00150 $GLOBALS['eZINICacheEnabled'] = $cache;
00151 }
00152
isDebugEnabled()
00160 {
00161 if ( !isset( $GLOBALS['eZINIDebugInternalsEnabled'] ) )
00162 $GLOBALS['eZINIDebugInternalsEnabled'] = EZ_INI_DEBUG_INTERNALS;
00163 return $GLOBALS['eZINIDebugInternalsEnabled'];
00164 }
00165
setIsDebugEnabled( $debug )
00171 {
00172 $GLOBALS['eZINIDebugInternalsEnabled'] = $debug;
00173 }
00174
isTextCodecEnabled()
00182 {
00183 if ( !isset( $GLOBALS['eZINITextCodecEnabled'] ) )
00184 $GLOBALS['eZINITextCodecEnabled'] = true;
00185 return $GLOBALS['eZINITextCodecEnabled'];
00186 }
00187
setIsTextCodecEnabled( $codec )
00193 {
00194 $GLOBALS['eZINITextCodecEnabled'] = $codec;
00195 }
00196
rootDir = "settings" )
00203 {
00204 if ( $fileName == "" )
00205 $fileName = "site.ini";
00206 if ( $rootDir == "" )
00207 $rootDir = "settings";
00208 return file_exists( $rootDir . '/' . $fileName );
00209 }
00210
reset = true )
00217 {
00218 if ( $this->UseCache )
00219 {
00220 $this->reset );
00221 }
00222 else
00223 {
00224 $this->reset );
00225 }
00226 }
00227
00228 function findInputFiles( &$inputFiles, &$iniFile )
00229 {
00230 include_once( 'lib/ezutils/classes/ezdir.php' );
00231 $inputFiles = array();
00232 $iniFile = eZDir::path( array( $this->RootDir, $this->FileName ) );
00233 if ( file_exists( $iniFile . '.php' ) )
00234 $iniFile .= '.php';
00235 if ( file_exists( $iniFile ) )
00236 $inputFiles[] = $iniFile;
00237 $overrideDirs();
00238 foreach ( $overrideDirs as $overrideDirItem )
00239 {
00240 $overrideDir = $overrideDirItem[0];
00241 $isGlobal = $overrideDirItem[1];
00242 if ( $isGlobal )
00243 $overrideFile = eZDir::path( array( $overrideDir, $this->FileName ) );
00244 else
00245 $overrideFile = eZDir::path( array( $this->RootDir, $overrideDir, $this->FileName ) );
00246 if ( file_exists( $overrideFile . '.php' ) )
00247 {
00248 $overrideFile .= '.php';
00249 $inputFiles[] = $overrideFile;
00250 }
00251 else if ( file_exists( $overrideFile ) )
00252 $inputFiles[] = $overrideFile;
00253
00254 if ( $isGlobal )
00255 $overrideFile = eZDir::path( array( $overrideDir, $this->FileName . '.append' ) );
00256 else
00257 $overrideFile = eZDir::path( array( $this->RootDir, $overrideDir, $this->FileName . '.append' ) );
00258 if ( file_exists( $overrideFile . '.php' ) )
00259 {
00260 $overrideFile .= '.php';
00261 $inputFiles[] = $overrideFile;
00262 }
00263 else if ( file_exists( $overrideFile ) )
00264 $inputFiles[] = $overrideFile;
00265 }
00266 }
00267
reset = true )
00274 {
00275 if ( $reset )
00276 $this->reset();
00277 $cachedDir = "var/cache/ini/";
00278 if ( !file_exists( $cachedDir ) )
00279 {
00280 include_once( 'lib/ezutils/classes/ezdir.php' );
00281 if ( ! eZDir::mkdir( $cachedDir, 0777, true ) )
00282 {
00283 eZDebug::writeError( "Couldn't create cache directory $cachedDir, perhaps wrong permissions", "eZINI" );
00284 }
00285 }
00286
00287 $this->findInputFiles( $inputFiles, $iniFile );
00288 if ( count( $inputFiles ) == 0 )
00289 return false;
00290
00291 $md5Files = array();
00292 foreach ( $inputFiles as $inputFile )
00293 {
00294 $md5Files[] = $inputFile;
00295 }
00296 $md5_input = implode( "\n", $md5Files );
00297 if ( $this->UseTextCodec )
00298 {
00299 include_once( "lib/ezi18n/classes/eztextcodec.php" );
00300 $md5_input .= '-' . eZTextCodec::internalCharset();
00301 }
00302 $fileName = md5( $md5_input ) . ".php";
00303 $cachedFile = $cachedDir . $fileName;
00304 $this->CacheFile = $cachedFile;
00305
00306 $inputTime = false;
00307
00308 foreach ( $inputFiles as $inputFile )
00309 {
00310 $fileTime = filemtime( $inputFile );
00311 if ( $inputTime === false or
00312 $fileTime > $inputTime )
00313 $inputTime = $fileTime;
00314 }
00315
00316 $loadCache = false;
00317 $cacheTime = false;
00318 if ( file_exists( $cachedFile ) )
00319 {
00320 $cacheTime = filemtime( $cachedFile );
00321 $loadCache = true;
00322 if ( $cacheTime < $inputTime )
00323 {
00324 $loadCache = false;
00325 }
00326 }
00327
00328 $useCache = false;
00329 if ( $loadCache )
00330 {
00331 $useCache = true;
00332 if ( eZINI::isDebugEnabled() )
00333 eZDebug::writeNotice( "Loading cache '$cachedFile' for file '" . $this->FileName . "'", "eZINI" );
00334 $charset = null;
00335 $blockValues = array();
00336 include( $cachedFile );
00337 if ( !isset( $eZIniCacheCodeDate ) or
00338 $eZIniCacheCodeDate != EZ_INI_CACHE_CODE_DATE )
00339 {
00340 if ( eZINI::isDebugEnabled() )
00341 eZDebug::writeNotice( "Old structure in cache file used, recreating '$cachedFile' to new structure", "eZINI" );
00342 $this->reset();
00343 $useCache = false;
00344 }
00345 else
00346 {
00347 $this->Charset = $charset;
00348 $this->BlockValues = $blockValues;
00349 $this->ModifiedBlockValues = array();
00350 unset( $blockValues );
00351 }
00352 }
00353 if ( !$useCache )
00354 {
00355 $this->parse( $inputFiles, $iniFile, false );
00356 $this->saveCache( $cachedFile );
00357
00358
00359 include_once( 'lib/ezutils/classes/ezlog.php' );
00360 eZLog::writeStorageLog( $fileName, $cachedDir );
00361 }
00362 }
00363
00364
saveCache( $cachedFile )
00370 {
00371
00372 $buffer = "";
00373 $i = 0;
00374 if ( is_array( $this->BlockValues ) )
00375 {
00376 $fp = @fopen( $cachedFile, "w+" );
00377 if ( $fp === false )
00378 {
00379 eZDebug::writeError( "Couldn't create cache file '$cachedFile', perhaps wrong permissions", "eZINI" );
00380 return;
00381 }
00382 fwrite( $fp, "<?php\n\$eZIniCacheCodeDate = " . EZ_INI_CACHE_CODE_DATE . ";\n" );
00383
00384
00385 fwrite( $fp, "\$charset = \"$this->Charset\";\n" );
00386 reset( $this->BlockValues );
00387 while ( list( $groupKey, $groupVal ) = each ( $this->BlockValues ) )
00388 {
00389 reset( $groupVal );
00390 while ( list( $key, $val ) = each ( $groupVal ) )
00391 {
00392 if ( is_array( $val ) )
00393 {
00394 fwrite( $fp, "\$groupArray[\"$key\"] = array();\n" );
00395 foreach ( $val as $arrayKey => $arrayValue )
00396 {
00397 if ( is_string( $arrayKey ) )
00398 $tmpArrayKey = "\"" . str_replace( "\"", "\\\"", $arrayKey ) . "\"";
00399 else
00400 $tmpArrayKey = $arrayKey;
00401 $tmpVal = str_replace( "\"", "\\\"", $arrayValue );
00402 fwrite( $fp, "\$groupArray[\"$key\"][$tmpArrayKey] = \"$tmpVal\";\n" );
00403 }
00404 }
00405 else
00406 {
00407 $tmpVal = str_replace( "\"", "\\\"", $val );
00408
00409 fwrite( $fp, "\$groupArray[\"$key\"] = \"$tmpVal\";\n" );
00410 }
00411 }
00412
00413 fwrite( $fp, "\$blockValues[\"$groupKey\"] =& \$groupArray;\n" );
00414 fwrite( $fp, "unset( \$groupArray );\n" );
00415 $i++;
00416 }
00417 fwrite( $fp, "\n?>" );
00418 fclose( $fp );
00419 if ( eZINI::isDebugEnabled() )
00420 eZDebug::writeNotice( "Wrote cache file '$cachedFile'", "eZINI" );
00421 }
00422
00423 }
00424
reset = true )
00431 {
00432 if ( $reset )
00433 $this->reset();
00434 if ( $inputFiles === false or
00435 $iniFile === false )
00436 $this->findInputFiles( $inputFiles, $iniFile );
00437
00438 foreach ( $inputFiles as $inputFile )
00439 {
00440 if ( file_exists( $inputFile ) )
00441 {
00442 $this->parseFile( $inputFile );
00443 }
00444 }
00445 }
00446
parseFile( $file )
00452 {
00453 if ( eZINI::isDebugEnabled() )
00454 eZINI' );
00455 include_once( "lib/ezutils/classes/ezfile.php" );
00456 $lines =& eZFile::splitLines( $file );
00457 if ( $lines === false )
00458 {
00459 eZDebug::writeError( "Failed opening file '$file' for reading", "eZINI" );
00460 return false;
00461 }
00462
00463 $currentBlock = "";
00464 if ( count( $lines ) > 0 )
00465 {
00466
00467 if ( preg_match( "/#\?ini(.+)\?/", $lines[0], $ini_arr ) )
00468 {
00469 $args = explode( " ", trim( $ini_arr[1] ) );
00470 foreach ( $args as $arg )
00471 {
00472 $vars = explode( '=', trim( $arg ) );
00473 if ( $vars[0] == "charset" )
00474 {
00475 $val = $vars[1];
00476 if ( $val[0] == '"' and
00477 strlen( $val ) > 0 and
00478 $val[strlen($val)-1] == '"' )
00479 $val = substr( $val, 1, strlen($val) - 2 );
00480 $this->Charset = $val;
00481 }
00482 }
00483 }
00484 }
00485
00486 if ( $this->UseTextCodec )
00487 {
00488 include_once( "lib/ezi18n/classes/eztextcodec.php" );
00489 $codec =& eZTextCodec::instance( $this->Charset );
00490 }
00491 foreach ( $lines as $line )
00492 {
00493 if ( preg_match( "/^#.*/", $line, $regs ) )
00494 continue;
00495 if ( preg_match( "/^(.+)##.*/", $line, $regs ) )
00496 $line = $regs[1];
00497 if ( trim( $line ) == '' )
00498 continue;
00499
00500 if ( preg_match("#^\[(.+)\]\s*$#", $line, $newBlockNameArray ) )
00501 {
00502 $newBlockName = trim( $newBlockNameArray[1] );
00503 $currentBlock = $newBlockName;
00504 continue;
00505 }
00506
00507
00508 if ( preg_match("#^(\w+)\\[\\]$#", $line, $valueArray ) )
00509 {
00510 $varName = trim( $valueArray[1] );
00511 $this->BlockValues[$currentBlock][$varName] = array();
00512 }
00513 else if ( preg_match("#^([a-zA-Z0-9_-]+)(\\[([a-zA-Z0-9_-]*)\\])?=(.*)$#", $line, $valueArray ) )
00514 {
00515 $varName = trim( $valueArray[1] );
00516 if ( $this->UseTextCodec )
00517 {
00518 eZDebug::accumulatorStart( 'ini_conversion', false, 'INI string conversion' );
00519 $varValue = $codec->convertString( $valueArray[4] );
00520 eZDebug::accumulatorStop( 'ini_conversion', false, 'INI string conversion' );
00521 }
00522 else
00523 {
00524 $varValue = $valueArray[4];
00525 }
00526
00527
00528 if ( $valueArray[2] )
00529 {
00530 if ( $valueArray[3] )
00531 {
00532 $keyName = $valueArray[3];
00533 if ( isset( $this->BlockValues[$currentBlock][$varName] ) and
00534 is_array( $this->BlockValues[$currentBlock][$varName] ) )
00535 $this->BlockValues[$currentBlock][$varName][$keyName] = $varValue;
00536 else
00537 $this->BlockValues[$currentBlock][$varName] = array( $keyName => $varValue );
00538 }
00539 else
00540 {
00541 if ( isset( $this->BlockValues[$currentBlock][$varName] ) and
00542 is_array( $this->BlockValues[$currentBlock][$varName] ) )
00543 $this->BlockValues[$currentBlock][$varName][] = $varValue;
00544 else
00545 $this->BlockValues[$currentBlock][$varName] = array( $varValue );
00546 }
00547 }
00548 else
00549 {
00550 $this->BlockValues[$currentBlock][$varName] = $varValue;
00551 }
00552 }
00553 }
00554
00555 return $ret;
00556 }
00557
resetCache()
00562 {
00563 if ( file_exists( $this->CacheFile ) )
00564 unlink( $this->CacheFile );
00565 }
00566
00567
save( $fileName = false, $suffix = false, $useOverride = false,
00575 $onlyModified = false )
00576 {
00577 $lineSeparator = eZSys::lineSeparator();
00578 $pathArray = array();
00579 if ( $fileName === false )
00580 $fileName = $this->FileName;
00581 $pathArray[] = $this->RootDir;
00582 if ( $useOverride )
00583 {
00584
00585 $pathArray[] = 'override';
00586 }
00587 if ( is_string( $useOverride ) and
00588 $useOverride == "append" )
00589 $fileName .= ".append";
00590 if ( $suffix !== false )
00591 $fileName .= $suffix;
00592 $originalFileName = $fileName;
00593 $backupFileName = $originalFileName . eZSys::backupFilename();
00594 $fileName .= '.tmp';
00595
00596 include_once( 'lib/ezutils/classes/ezdir.php' );
00597 $filePath = eZDir::path( array_merge( $pathArray, $fileName ) );
00598 $originalFilePath = eZDir::path( array_merge( $pathArray, $originalFileName ) );
00599 $backupFilePath = eZDir::path( array_merge( $pathArray, $backupFileName ) );
00600
00601 $fp = @fopen( $filePath, "w+");
00602 if ( !$fp )
00603 {
00604 eZDebug::writeError( "Failed opening file '$filePath' for writing", "eZINI" );
00605 return false;
00606 }
00607
00608 $writeOK = true;
00609 $written = 0;
00610 $written = fwrite( $fp, "<?php /* #?ini charset=\"" . $this->Charset . "\"?$lineSeparator$lineSeparator" );
00611 if ( $written === false )
00612 $writeOK = false;
00613 $i = 0;
00614 if ( $writeOK )
00615 {
00616 foreach( array_keys( $this->BlockValues ) as $blockName )
00617 {
00618 if ( $onlyModified )
00619 {
00620 $groupHasModified = false;
00621 if ( isset( $this->ModifiedBlockValues[$blockName] ) )
00622 {
00623 foreach ( $this->ModifiedBlockValues[$blockName] as $modifiedValue )
00624 {
00625 if ( $modifiedValue )
00626 $groupHasModified = true;
00627 }
00628 }
00629 if ( !$groupHasModified )
00630 continue;
00631 }
00632 $written = 0;
00633 if ( $i > 0 )
00634 $written = fwrite( $fp, "$lineSeparator" );
00635 if ( $written === false )
00636 {
00637 $writeOK = false;
00638 break;
00639 }
00640 $written = fwrite( $fp, "[$blockName]$lineSeparator" );
00641 if ( $written === false )
00642 {
00643 $writeOK = false;
00644 break;
00645 }
00646 foreach( array_keys( $this->BlockValues[$blockName] ) as $blockVariable )
00647 {
00648 if ( $onlyModified )
00649 {
00650 if ( !isset( $this->ModifiedBlockValues[$blockName][$blockVariable] ) or
00651 !$this->ModifiedBlockValues[$blockName][$blockVariable] )
00652 continue;
00653 }
00654 $varKey = $blockVariable;
00655 $varValue = $this->BlockValues[$blockName][$blockVariable];
00656 if ( is_array( $varValue ) )
00657 {
00658 if ( count( $varValue ) > 0 )
00659 {
00660 foreach ( $varValue as $varArrayKey => $varArrayValue )
00661 {
00662 if ( is_string( $varArrayKey ) )
00663 $written = fwrite( $fp, "$varKey" . "[$varArrayKey]=$varArrayValue$lineSeparator" );
00664 else
00665 $written = fwrite( $fp, "$varKey" . "[]=$varArrayValue$lineSeparator" );
00666 if ( $written === false )
00667 break;
00668 }
00669 }
00670 else
00671 $written = fwrite( $fp, "$varKey" . "[]$lineSeparator" );
00672 }
00673 else
00674 {
00675 $written = fwrite( $fp, "$varKey=$varValue$lineSeparator" );
00676 }
00677 if ( $written === false )
00678 {
00679 $writeOK = false;
00680 break;
00681 }
00682 }
00683 if ( !$writeOK )
00684 break;
00685 ++$i;
00686 }
00687 }
00688 if ( $writeOK )
00689 {
00690 $written = fwrite( $fp, "*/ ?>" );
00691 if ( $written === false )
00692 $writeOK = false;
00693 }
00694 @fclose( $fp );
00695 if ( !$writeOK )
00696 {
00697 unlink( $filePath );
00698 return false;
00699 }
00700
00701 if ( file_exists( $backupFileName ) )
00702 unlink( $backupFileName );
00703 if ( file_exists( $originalFilePath ) )
00704 {
00705 if ( !rename( $originalFilePath, $backupFilePath ) )
00706 return false;
00707 }
00708 if ( !rename( $filePath, $originalFilePath ) )
00709 {
00710 rename( $backupFilePath, $originalFilePath );
00711 return false;
00712 }
00713
00714 return true;
00715 }
00716
reset()
00721 {
00722 $this->BlockValues = array();
00723 $this->ModifiedBlockValues = array();
00724 }
00725
rootDir()
00732 {
00733 return $this->RootDir;
00734 }
00735
overrideDirs()
00742 {
00743 $dirs =& $GLOBALS["eZINIOverrideDirList"];
00744 if ( !isset( $dirs ) or !is_array( $dirs ) )
00745 $dirs = array( array( "override", false ) );
00746 return $dirs;
00747 }
00748
prependOverrideDir( $dir, $globalDir = false )
00753 {
00754 if ( eZINI::isDebugEnabled() )
00755 eZDebug::writeNotice( "Changing override dir to '$dir'", "eZINI" );
00756 $dirs =& $GLOBALS["eZINIOverrideDirList"];
00757 if ( !isset( $dirs ) or !is_array( $dirs ) )
00758 $dirs = array( array( 'override', false ) );
00759 $dirs = array_merge( array( array( $dir, $globalDir ) ), $dirs );
00760 $this->CacheFile = false;
00761 }
00762
appendOverrideDir( $dir, $globalDir = false )
00767 {
00768 if ( eZINI::isDebugEnabled() )
00769 eZDebug::writeNotice( "Changing override dir to '$dir'", "eZINI" );
00770 $dirs =& $GLOBALS["eZINIOverrideDirList"];
00771 if ( !isset( $dirs ) or !is_array( $dirs ) )
00772 $dirs = array( 'override' );
00773 $dirs[] = array( $dir, $globalDir );
00774 $this->CacheFile = false;
00775 }
00776
variable )
00782 {
00783 if ( $this->hasVariable( $blockName, $varName ) )
00784 $variable( $blockName, $varName );
00785 else
00786 return false;
00787 return true;
00788 }
00789
variable( $blockName, $varName )
00795 {
00796 $ret = false;
00797 if ( !isset( $this->BlockValues[$blockName] ) )
00798 eZDebug::writeError( "Undefined group: '$blockName'", "eZINI" );
00799 else if ( isset( $this->BlockValues[$blockName][$varName] ) )
00800 $ret = $this->BlockValues[$blockName][$varName];
00801 else
00802 eZDebug::writeError( "Undefined variable: '$varName' in group '$blockName'", "eZINI" );
00803
00804 return $ret;
00805 }
00806
hasVariable( $blockName, $varName )
00811 {
00812 return isSet( $this->BlockValues[$blockName][$varName] );
00813 }
00814
isVariableModified( $blockName, $varName )
00819 {
00820 return ( isset( $this->ModifiedBlockValues[$blockName][$varName] ) and
00821 $this->ModifiedBlockValues[$blockName][$varName] );
00822 }
00823
variableArray( $blockName, $varName )
00829 {
00830 $ret = $this->variable( $blockName, $varName );
00831 if ( is_array( $ret ) )
00832 {
00833 $arr = array();
00834 foreach ( $ret as $retItem )
00835 {
00836 $arr[] = explode( ";", $retItem );
00837 }
00838 $ret = $arr;
00839 }
00840 else if ( $ret !== false )
00841 $ret = explode( ";", $ret );
00842
00843 return $ret;
00844 }
00845
hasGroup( $blockName )
00850 {
00851 return isSet( $this->BlockValues[$blockName] );
00852 }
00853
group( $blockName )
00858 {
00859 if ( !isset( $this->BlockValues[$blockName] ) )
00860 {
00861 eZDebug::writeError( "Unknown group: '$origBlockName'", "eZINI" );
00862 return null;
00863 }
00864 $ret = $this->BlockValues[$blockName];
00865
00866 return $ret;
00867 }
00868
setVariable( $blockName, $varName, $varValue )
00873 {
00874 $this->BlockValues[$blockName][$varName] = $varValue;
00875 $this->ModifiedBlockValues[$blockName][$varName] = true;
00876 }
00877
getNamedArray()
00882 {
00883 return $this->BlockValues;
00884 }
00885
rootDir = "settings" )
00891 {
00892 $isLoaded =& $GLOBALS["eZINIGlobalIsLoaded-$rootDir-$fileName"];
00893 if ( !isset( $isLoaded ) )
00894 return false;
00895 return $isLoaded;
00896 }
00897
rootDir = "settings", $useTextCodec = null, $useCache = null )
00903 {
00904 $impl =& $GLOBALS["eZINIGlobalInstance-$rootDir-$fileName"];
00905 $isLoaded =& $GLOBALS["eZINIGlobalIsLoaded-$rootDir-$fileName"];
00906
00907 $class =& get_class( $impl );
00908 if ( $class != "ezini" )
00909 {
00910 $isLoaded = false;
00911
00912 $impl = new eZINI( $fileName, $rootDir, $useTextCodec, $useCache );
00913
00914 $isLoaded = true;
00915 }
00916 return $impl;
00917 }
00918
Charset;
00922
BlockValues;
00925
ModifiedBlockValues;
00928
FileName;
00931
RootDir;
00934
UseTextCodec;
00937
CacheFile;
00940 }
00941
00942 ?>