PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/library/tera-wurfl/admin/tera_wurfl_parser.php

https://github.com/Macarse/trifiori
PHP | 494 lines | 417 code | 11 blank | 66 comment | 77 complexity | 361adbe2d117b7c0f796427be4b08890 MD5 | raw file
  1. <?php
  2. /*
  3. * Tera_WURFL - PHP MySQL driven WURFL
  4. *
  5. * Tera-WURFL was written by Steve Kamerman, Tera Technologies and is based on the
  6. * WURFL PHP Tools from http://wurfl.sourceforge.net/. This version uses a MySQL database
  7. * to store the entire WURFL file to provide extreme performance increases.
  8. *
  9. * @package tera_wurfl
  10. * @author Steve Kamerman, Tera Technologies (kamermans AT teratechnologies DOT net)
  11. * @version Stable 1.5.1 $Date: 2007/05/09 20:09:13 $
  12. * @license http://www.mozilla.org/MPL/ MPL Vesion 1.1
  13. * $Id: tera_wurfl_parser.php,v 1.1.2.3.2.16 2007/05/09 20:09:13 kamermans Exp $
  14. * $RCSfile: tera_wurfl_parser.php,v $
  15. *
  16. * Based On: WURFL PHP Tools by Andrea Trasatti ( atrasatti AT users DOT sourceforge DOT net )
  17. *
  18. */
  19. if ( !defined('WURFL_CONFIG') )
  20. @require_once('../tera_wurfl_config.php');
  21. if ( !defined('WURFL_CONFIG') )
  22. die("NO CONFIGURATION");
  23. // temp storage for the parsed WURFL
  24. $wurfl = array();
  25. // temp storage for the parsed PATCH
  26. $wurfl_patch = array();
  27. $patch_params = array();
  28. // this function checks WURFL patch integrity/validity
  29. function checkpatch($name, $attr) {
  30. global $wurfl, $wurfl_patch, $patch_params, $checkpatch_result, $wurfl_type;
  31. if($wurfl_type == "main"){
  32. $thiswurfl = &$wurfl;
  33. }elseif($wurfl_type == "patch"){
  34. $thiswurfl = &$wurfl_patch;
  35. }else{
  36. die("Invalid wurfl_type.");
  37. }
  38. if ( $name == 'wurfl_patch' ) {
  39. $checkpatch_result['wurfl_patch'] = true;
  40. return true;
  41. } else if ( !$checkpatch_result['wurfl_patch'] ) {
  42. $checkpatch_result['wurfl_patch'] = false;
  43. toLog('checkpatch', "no wurfl_patch tag! Patch file ignored.");
  44. return false;
  45. }
  46. if ( $name == 'devices' ) {
  47. $checkpatch_result['devices'] = true;
  48. return true;
  49. } else if ( !$checkpatch_result['devices'] ) {
  50. $checkpatch_result['devices'] = false;
  51. toLog('checkpatch', "no devices tag! Patch file ignored.");
  52. return false;
  53. }
  54. if ( $name == 'device' ) {
  55. if ( isset($thiswurfl['devices'][$attr['id']]) ) {
  56. if ( $thiswurfl['devices'][$attr['id']]['user_agent'] != $attr['user_agent'] ) {
  57. $checkpatch_result['device']['id'][$attr["id"]]['patch'] = false;
  58. $checkpatch_result['device']['id'][$attr["id"]]['reason'] = 'user agent mismatch, orig='.$thiswurfl['devices'][$attr['id']]['user_agent'].', new='.$attr['user_agent'].', id='.$attr['id'].', fall_back='.$attr['fall_back'];
  59. }
  60. }
  61. /*
  62. * checking if the fall_back is disabled. I might define a device's fall_back which will be defined later in the patch file.
  63. * fall_backs checking could be done after merging.
  64. if ( $attr['id'] == 'generic' && $attr['user_agent'] == '' && $attr['fall_back'] == 'root' ) {
  65. // generic device, everything's ok.
  66. } else if ( !isset($thiswurfl['devices'][$attr['fall_back']]) ) {
  67. $checkpatch_result['device']['id'][$attr["id"]]['patch'] = false;
  68. $checkpatch_result['device']['id'][$attr["id"]]['reason'] .= 'wrong fall_back, id='.$attr['id'].', fall_back='.$attr['fall_back'];
  69. }
  70. */
  71. if ( isset($checkpatch_result['device']['id'][$attr["id"]]['patch'])
  72. && !$checkpatch_result['device']['id'][$attr["id"]]['patch'] ) {
  73. toLog('checkpatch', $checkpatch_result['device']['id'][$attr["id"]]['reason'],LOG_ERR);
  74. return false;
  75. }
  76. }
  77. return true;
  78. }
  79. function startElement($parser, $name, $attr) {
  80. global $wurfl, $wurfl_patch, $curr_event, $curr_device, $curr_group, $fp_cache, $check_patch_params, $checkpatch_result, $wurfl_type;
  81. if($wurfl_type == "main"){
  82. $thiswurfl = &$wurfl;
  83. }elseif($wurfl_type == "patch"){
  84. $thiswurfl = &$wurfl_patch;
  85. }else{
  86. die("Invalid wurfl_type.");
  87. }
  88. if ( $check_patch_params ) {
  89. // if the patch file checks fail I don't merge info retrived
  90. if ( !checkpatch($name, $attr) ) {
  91. toLog('startElement', "error on $name, ".$attr['id'],LOG_ERROR);
  92. $curr_device = 'dump_anything';
  93. return;
  94. } else if ( $curr_device == 'dump_anything' && $name != 'device' ) {
  95. // this capability is referred to a device that was erroneously defined for some reason, skip it
  96. toLog('startElement', $name." cannot be merged, the device was skipped because of an error",LOG_WARNING);
  97. return;
  98. }
  99. }
  100. switch($name) {
  101. case "ver":
  102. case "last_updated":
  103. case "official_url":
  104. case "statement":
  105. //cdata will take care of these, I'm just defining the array
  106. $thiswurfl[$name]="";
  107. //$curr_event=$thiswurfl[$name];
  108. break;
  109. case "maintainers":
  110. case "maintainer":
  111. case "authors":
  112. case "author":
  113. case "contributors":
  114. case "contributor":
  115. // for the MySQL version I will ignore these (for now)
  116. // TODO: Add support for non-device WURFL tags
  117. if ( sizeof($attr) > 0 ) {
  118. // dirty trick: author is child of authors, contributor is child of contributors
  119. while ($t = each($attr)) {
  120. // example: $thiswurfl["authors"]["author"]["name"]="Andrea Trasatti";
  121. $thiswurfl[$name."s"][$name][$attr["name"]][$t[0]]=$t[1];
  122. }
  123. }
  124. break;
  125. case "device":
  126. if ( ($attr["user_agent"] == "" || ! $attr["user_agent"]) && $attr["id"]!="generic" ) {
  127. die("No user agent and I am not generic!! id=".$attr["id"]." HELP");
  128. }
  129. if ( sizeof($attr) > 0 ) {
  130. while ($t = each($attr)) {
  131. // example: $thiswurfl["devices"]["ericsson_generic"]["fall_back"]="generic";
  132. $thiswurfl["devices"][$attr["id"]][$t[0]]=$t[1];
  133. }
  134. }
  135. $curr_device=$attr["id"];
  136. break;
  137. case "group":
  138. // this HAS NOT to be executed or we will define the id as string and then reuse it as array: ERROR
  139. //$thiswurfl["devices"][$curr_device][$attr["id"]]=$attr["id"];
  140. $curr_group=$attr["id"];
  141. break;
  142. case "capability":
  143. if ( $attr["value"] == 'true' ) {
  144. $value = true;
  145. } else if ( $attr["value"] == 'false' ) {
  146. $value = false;
  147. } else {
  148. $value = $attr["value"];
  149. $intval = intval($value);
  150. if ( strcmp($value, $intval) == 0 ) {
  151. $value = $intval;
  152. }
  153. }
  154. $thiswurfl["devices"][$curr_device][$curr_group][$attr["name"]]=$value;
  155. break;
  156. case "devices":
  157. // This might look useless but it's good when you want to parse only the devices and skip the rest
  158. if ( !isset($thiswurfl["devices"]) )
  159. $thiswurfl["devices"]=array();
  160. break;
  161. case "wurfl_patch":
  162. // opening tag of the patch file
  163. case "wurfl":
  164. // opening tag of the WURFL, nothing to do
  165. break;
  166. case "default":
  167. // unknown events are not welcome
  168. die($name." is an unknown event<br>");
  169. break;
  170. }
  171. }
  172. function endElement($parser, $name) {
  173. global $wurfl, $wurfl_patch, $curr_event, $curr_device, $curr_group, $wurfl_type;
  174. if($wurfl_type == "main"){
  175. $thiswurfl = &$wurfl;
  176. }elseif($wurfl_type == "patch"){
  177. $thiswurfl = &$wurfl_patch;
  178. }else{
  179. die("Invalid wurfl_type.");
  180. }
  181. switch ($name) {
  182. case "group":
  183. break;
  184. case "device":
  185. break;
  186. case "ver":
  187. case "last_updated":
  188. case "official_url":
  189. case "statement":
  190. $thiswurfl[$name]=$curr_event;
  191. // referring to $GLOBALS to unset curr_event because unset will not destroy
  192. // a global variable unless called in this way
  193. unset($GLOBALS['curr_event']);
  194. break;
  195. default:
  196. break;
  197. }
  198. }
  199. function characterData($parser, $data) {
  200. global $curr_event;
  201. if (trim($data) != "" ) {
  202. $curr_event.=$data;
  203. //echo "data=".$data."<br>\n";
  204. }
  205. }
  206. function emptyWurflDevTable($tablename){
  207. $droptable = "DROP TABLE IF EXISTS ".$tablename;
  208. $createtable = "CREATE TABLE `".$tablename."` (
  209. `deviceID` varchar(128) binary NOT NULL default '',
  210. `user_agent` varchar(255) default NULL,
  211. `fall_back` varchar(128) default NULL,
  212. `actual_device_root` tinyint(1) default '0',
  213. `capabilities` mediumtext,
  214. PRIMARY KEY (`deviceID`),
  215. KEY `fallback` (`fall_back`),
  216. KEY `useragent` (`user_agent`)
  217. ) TYPE=".DB_TYPE;
  218. $emptytable = "DELETE FROM ".$tablename;
  219. if(DB_EMPTY_METHOD == "DROP_CREATE"){
  220. mysql_query($droptable) or die(mysql_error());
  221. mysql_query($createtable) or die(mysql_error());
  222. }else{
  223. mysql_query($emptytable) or die(mysql_error());
  224. }
  225. return(true);
  226. }
  227. function load_wurfl($filetype="main",$source="local") {
  228. global $wurfl, $wurfl_patch, $curr_event, $curr_device, $curr_group, $fp_cache, $check_patch_params, $checkpatch_result, $wurfl_type;
  229. $wurfl_type = $filetype;
  230. if($wurfl_type == "main"){
  231. $devtable = DB_DEVICE_TABLE.DB_TEMP_EXT;
  232. $prodtable = DB_DEVICE_TABLE;
  233. $wurflfile = WURFL_FILE;
  234. $thiswurfl = &$wurfl;
  235. }elseif($wurfl_type == "patch"){
  236. $devtable = DB_PATCH_TABLE.DB_TEMP_EXT;
  237. $prodtable = DB_PATCH_TABLE;
  238. $wurflfile = WURFL_PATCH_FILE;
  239. $thiswurfl = &$wurfl_patch;
  240. }
  241. if(($source == "remote" || $source == "remote_cvs") && $wurfl_type == "main"){
  242. if($source == "remote"){
  243. $dl_url = WURFL_DL_URL;
  244. }elseif($source == "remote_cvs"){
  245. $dl_url = WURFL_CVS_URL;
  246. }
  247. $newfile = DATADIR."dl_wurfl.xml";
  248. echo "Downloading WURFL from $dl_url ...\n<br/>";
  249. flush();
  250. if(!is_writable(DATADIR)){
  251. toLog('update',"no write permissions for data directory",LOG_ERR);
  252. die("Fatal Error: The data directory is not writable. (".DATADIR.")<br/><br/><strong>Please make the data directory writable by the user that runs the webserver process, in Linux this command would do the trick if you're not too concered about security: <pre>chmod -R 777 ".DATADIR."</pre></strong>");
  253. }
  254. @ini_set('user_agent', "PHP/Tera-WURFL_$version");
  255. $dl_wurfl = file_get_contents($dl_url);
  256. file_put_contents($newfile,$dl_wurfl);
  257. $size = filesize($newfile);
  258. echo "done ($size bytes)<br />";
  259. flush();
  260. // ignore this error - I know I'm redefining a constant :P
  261. @define("WURFL_FILE",$newfile);
  262. $wurflfile = $newfile;
  263. }
  264. $thiswurfl = array();
  265. $xml_parser = xml_parser_create();
  266. xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, false);
  267. xml_set_element_handler($xml_parser, "startElement", "endElement");
  268. xml_set_character_data_handler($xml_parser, "characterData");
  269. if ( !file_exists($wurflfile) ) {
  270. toLog('parse', $wurflfile." does not exist",LOG_ERR);
  271. die($wurflfile." does not exist");
  272. }
  273. if (!($fp = fopen($wurflfile, "r"))) {
  274. toLog('parse', "$wurflfile could not be opened for XML input",LOG_ERR);
  275. die("$wurflfile could not opened XML input");
  276. }
  277. while ($data = fread($fp, 4096)) {
  278. if (!xml_parse($xml_parser, $data, feof($fp))) {
  279. $errmsg = sprintf("XML error: %s at line %d",xml_error_string(xml_get_error_code($xml_parser)),xml_get_current_line_number($xml_parser));
  280. toLog('parse',$wurflfile." ".$errmsg);
  281. die($wurflfile." ".$errmsg);
  282. }
  283. }
  284. fclose($fp);
  285. xml_parser_free($xml_parser);
  286. $devices = $thiswurfl["devices"];
  287. emptyWurflDevTable($devtable);
  288. $processedrows = count($devices);
  289. $queries = 0;
  290. $insertedrows = 0;
  291. $maxquerysize = 0;
  292. $insert_errors = array();
  293. $insertcache = array();
  294. $used_ids = array();
  295. foreach($devices as $dev_id => $dev_data) {
  296. /*
  297. * This will detect duplicate device_ids in the WURFL
  298. * if they are different cases. This is important for
  299. * databases like MySQL where keys are sorted without regard
  300. * for case.
  301. *
  302. * EDIT: I have disabled this by using 'binary' keys in MySQL
  303. * which are case sensitive.
  304. *
  305. if(in_array(strtolower($dev_id),$used_ids)){
  306. $insert_errors[] = "Duplicate ID omitted: \"$dev_id\"";
  307. continue;
  308. }else{
  309. $used_ids[] = $dev_id;
  310. }
  311. */
  312. // $wurfl_agents[$one['user_agent']] = $one['id'];
  313. // convert device root to tinyint format (0|1) for db
  314. $devroot = (isset($dev_data['actual_device_root']) && $dev_data['actual_device_root'])? 1: 0;
  315. if(strlen($dev_data['user_agent']) > 255){
  316. $insert_errors[] = "Warning: user agent too long: \"".$dev['user_agent'].'"';
  317. }
  318. if(DB_MULTI_INSERT){
  319. $insertcache[] = sprintf("(%s,%s,%s,%s,%s)",
  320. sqlPrep($dev_id),
  321. sqlPrep($dev_data['user_agent']),
  322. sqlPrep($dev_data['fall_back']),
  323. sqlPrep($devroot),
  324. sqlPrep(serialize($dev_data))
  325. );
  326. if(count($insertcache) >= DB_MAX_INSERTS){
  327. $query = "INSERT INTO ".$devtable." (deviceID, user_agent, fall_back, actual_device_root, capabilities) VALUES ".implode(",",$insertcache);
  328. mysql_query($query) or $insert_errors[] = "DB server reported error on id \"$dev_id\": ".mysql_error();
  329. $insertedrows += mysql_affected_rows();
  330. $insertcache = array();
  331. $queries++;
  332. $maxquerysize = (strlen($query)>$maxquerysize)? strlen($query): $maxquerysize;
  333. }
  334. }else{
  335. $query = sprintf("INSERT INTO ".$devtable." (deviceID, user_agent, fall_back, actual_device_root, capabilities) VALUES (%s,%s,%s,%s,%s)",
  336. sqlPrep($dev_id),
  337. sqlPrep($dev_data['user_agent']),
  338. sqlPrep($dev_data['fall_back']),
  339. sqlPrep($devroot),
  340. sqlPrep(serialize($dev_data))
  341. );
  342. mysql_query($query) or $insert_errors[]=mysql_error();
  343. $insertedrows += mysql_affected_rows();
  344. $queries++;
  345. $maxquerysize = (strlen($query)>$maxquerysize)? strlen($query): $maxquerysize;
  346. }
  347. }
  348. // some records are probably left in the insertcache
  349. if(DB_MULTI_INSERT && count($insertcache) > 0){
  350. $query = "INSERT INTO ".$devtable." (deviceID, user_agent, fall_back, actual_device_root, capabilities) VALUES ".implode(",",$insertcache);
  351. mysql_query($query) or $insert_errors[]=mysql_error();
  352. $insertedrows += mysql_affected_rows();
  353. $queries++;
  354. $maxquerysize = (strlen($query)>$maxquerysize)? strlen($query): $maxquerysize;
  355. }
  356. // perform sanity checks
  357. if(count($insert_errors) > 0){
  358. // problem with update - changes will not be applied
  359. echo "There were errors while updating the WURFL. No changes have been made to your database.<br /><br />";
  360. foreach($insert_errors as $error){
  361. toLog("load_wurfl","error inserting device: ".$error,LOG_ERR);
  362. echo "Error inserting device: ".$error."<br />";
  363. }
  364. }else{
  365. // everything seems to be fine - go ahead and overwrite the production device table (or patch table)
  366. replace_table($prodtable, $devtable);
  367. // now our cache is assumed to be tainted since the underlying data may have changed
  368. // I guess it makes sense to clear it even if it's disabled - why not?
  369. clear_cache();
  370. toLog("load_wurfl","the $wurfl_type database was successfully updated from: $source",LOG_WARNING);
  371. }
  372. return(array("total" => $processedrows, "inserted" => $insertedrows, "errors" => $insert_errors, "queries" => $queries, "maxquerysize" => $maxquerysize));
  373. }
  374. function replace_table($to_be_replaced, $replacement){
  375. @mysql_query("SELECT COUNT(deviceID) AS num FROM ".$replacement) or die("ERROR: table not found (".$replacement."): ".mysql_error());
  376. mysql_query("DROP TABLE IF EXISTS ".$to_be_replaced);
  377. mysql_query("RENAME TABLE `".$replacement."` TO `".$to_be_replaced."`") or die("ERROR: could not rename table - make sure the database user has ALTER permissions! Error message: ".mysql_error());
  378. return(true);
  379. }
  380. function clear_cache(){
  381. mysql_query("DROP TABLE IF EXISTS ".DB_CACHE_TABLE);
  382. $createtable = "CREATE TABLE `".DB_CACHE_TABLE."` (
  383. `user_agent` varchar(255) binary NOT NULL default '',
  384. `cache_data` mediumtext NOT NULL,
  385. PRIMARY KEY (`user_agent`)
  386. ) TYPE=".DB_TYPE;
  387. mysql_query($createtable) or die("ERROR: could not create cache table (".DB_CACHE_TABLE."): ".mysql_error());
  388. return(true);
  389. }
  390. function apply_patch(){
  391. emptyWurflDevTable(DB_HYBRID_TABLE);
  392. $queries = 1;
  393. $merge_errors = array();
  394. // find total number of patch records
  395. $res = mysql_query("SELECT COUNT(deviceID) AS num FROM ".DB_PATCH_TABLE);
  396. $processedrows = mysql_result($res,0,'num');
  397. $queries++;
  398. // fill the hybrid table with the stock WURFL first
  399. $fillhybrid = "INSERT INTO ".DB_HYBRID_TABLE." SELECT * FROM ".DB_DEVICE_TABLE;
  400. mysql_query($fillhybrid);
  401. $queries++;
  402. // insert all the patch devices that DON'T already exist in the WURFL into the hybrid table
  403. mysql_query("INSERT INTO ".DB_HYBRID_TABLE." SELECT p.* FROM ".DB_PATCH_TABLE." AS p LEFT JOIN ".DB_HYBRID_TABLE." AS d ON p.deviceID = d.deviceID WHERE d.deviceID IS NULL");
  404. $queries++;
  405. $newdevs = mysql_affected_rows();
  406. // get all the devices that DO exist in the main WURFL so we can merge them in the hybrid table
  407. $patchres = mysql_query("SELECT p.* FROM ".DB_PATCH_TABLE." AS p LEFT JOIN ".DB_DEVICE_TABLE." AS d ON p.deviceID = d.deviceID WHERE d.deviceID IS NOT NULL");
  408. $queries++;
  409. $mergeddevs = mysql_num_rows($patchres);
  410. while($new = mysql_fetch_assoc($patchres)){
  411. // grab the original record to merge with the new one
  412. $origres = mysql_query("SELECT * FROM ".DB_HYBRID_TABLE." WHERE deviceID=".sqlPrep($new['deviceID']));
  413. $queries++;
  414. $orig = mysql_fetch_assoc($origres);
  415. $origcap = unserialize($orig['capabilities']);
  416. $newcap = unserialize($new['capabilities']);
  417. $merged = $new;
  418. $mergedcap = $origcap;
  419. foreach($newcap as $key => $val) {
  420. if ( is_array($val) ) {
  421. // TODO: Make sure this works correctly, I'm suspicious because of LOG_NOTICE errors
  422. // $mergedcap[$key] = @array_merge($mergedcap[$key], $val);
  423. // Thanks to Mait Vilbiks for noticing that PHP5 requires 2 arrays for array_merge()
  424. // changed by kamermans 19Feb2007 (tentative release 1.5)
  425. $mergedcap[$key] = @array_merge((array)$mergedcap[$key], $val);
  426. } else {
  427. $mergedcap[$key] = $val;
  428. }
  429. }
  430. $merged['capabilities'] = serialize($mergedcap);
  431. // now we should have a merged record - update it
  432. $setstringarr = array();
  433. foreach($merged as $key => $val){
  434. $setstringarr[] = $key."=".sqlPrep($val);
  435. }
  436. $setstring = implode(", ",$setstringarr);
  437. mysql_query("UPDATE ".DB_HYBRID_TABLE." SET ".$setstring." WHERE deviceID=".sqlPrep($merged['deviceID']));
  438. $queries++;
  439. }
  440. return(array("total" => $processedrows, "new" => $newdevs, "merged" => $mergeddevs, "errors" => $merge_errors, "queries" => $queries));
  441. }
  442. function sqlPrep($value){
  443. if (get_magic_quotes_gpc()) $value = stripslashes($value);
  444. if($value == '') $value = 'NULL';
  445. else if (!is_numeric($value) || $value[0] == '0') $value = "'" . mysql_real_escape_string($value) . "'"; //Quote if not integer
  446. return $value;
  447. }
  448. // PHP4 does not have file_put_contents() so I emulate it if it's not defined
  449. if(!function_exists("file_put_contents")){
  450. function file_put_contents($n, $d, $flag = false) {
  451. $mode = @($flag == FILE_APPEND || strtoupper($flag) == 'FILE_APPEND') ? 'a' : 'w';
  452. $f = @fopen($n, $mode);
  453. if ($f === false) {
  454. return 0;
  455. } else {
  456. if (is_array($d)) $d = implode($d);
  457. $bytes_written = fwrite($f, $d);
  458. fclose($f);
  459. return $bytes_written;
  460. }
  461. }
  462. }
  463. function toLog($func, $text, $requestedLogLevel=LOG_NOTICE){
  464. if($requestedLogLevel == LOG_ERR) $this->errors[] = $text;
  465. if ( !defined('LOG_LEVEL') || LOG_LEVEL == 0 || ($requestedLogLevel-1) >= LOG_LEVEL ) {
  466. return;
  467. }
  468. if ( $requestedLogLevel == LOG_ERR ) {
  469. $warn_banner = 'ERROR: ';
  470. } else if ( $requestedLogLevel == LOG_WARNING ) {
  471. $warn_banner = 'WARNING: ';
  472. } else {
  473. $warn_banner = '';
  474. }
  475. // Thanks laacz
  476. $_textToLog = date('r')." [".php_uname('n')." ".getmypid()."]"."[$func] ".$warn_banner . $text;
  477. $_logFP = fopen(WURFL_LOG_FILE, "a+");
  478. fputs($_logFP, $_textToLog."\n");
  479. fclose($_logFP);
  480. return(true);
  481. }
  482. ?>