AssEdbeta: the semi-automatic assistant editor
$Html... time elapsed: $Time seconds
END; exit; } /******************************************************************************************************************************************** * Function: SaveReload() * Saves variables and reloads the page to move on to the next stage */ function SaveReload() { $_SESSION['Stage'] = $this->Stage; $_SESSION['PostData'] = $this->PostData; $_SESSION['QueryKey'] = $this->QueryKey; if (isset($this->WebEnv)) $_SESSION['WebEnv'] = $this->WebEnv; if (isset($this->AuthorList)) $_SESSION['AuthorList'] = $this->AuthorList; if (isset($this->AllResults)) $_SESSION['AllResults'] = $this->AllResults; if (isset($this->CurResults)) $_SESSION['CurResults'] = $this->CurResults; if ($this->Stage[0] == "searches") { foreach ($this->PostData['Searches'] as $k => $Search) { if ($this->Stage[1] > $k) $this->Html .= "\n" . $Search['SearchString'] . ": 100% (" . $Search['Count'] . " / " . $Search['Count'] . ")
\n"; elseif (($this->Stage[1] == $k) and !empty($Search['Count'])) { $width = ($this->Stage[2] / $Search['Count']) * 600; $pc = round(($width / 6), 2); $width = round($width); $width2 = 600 - $width; $this->Html .= "\n" . $Search['SearchString'] . ": " . $pc . "% (" . $this->Stage[2] . " / " . $Search['Count'] . ")
\n"; } else $this->Html .= "\n" . $Search['SearchString'] . ": 0%
\n"; } $this->Html .= "\nPost-search filters: 0%
\n"; } else { if (empty($_SESSION['Filtered']) and isset($_SESSION['ToFilter'])) $_SESSION['Filtered'] = $_SESSION['ToFilter'] - count($this->AllResults[1]); foreach ($this->PostData['Searches'] as $k => $Search) { $this->Html .= "\n" . $Search['SearchString'] . ": 100% (" . $Search['Count'] . " / " . $Search['Count'] . ")
\n"; } if (isset($_SESSION['Filtered']) and isset($_SESSION['ToFilter'])) { $width = ($_SESSION['Filtered'] / $_SESSION['ToFilter']) * 600; $pc = round(($width / 6), 2); $width = round($width); $width2 = 600 - $width; $this->Html .= "\nApplying limits and filters: " . $pc . "%
\n"; } else $this->Html .= "\nPost-search filters: 0%
\n"; } $this->Title = "AssEd: Processing searches..."; $this->OutputHtml(true); } /******************************************************************************************************************************************** * Function: PrintResults() * Prints the search results */ function PrintResults() { $_SESSION['Stage'] = $this->Stage; // Save these two variables into the session to enable re-ordering if (isset($this->AuthorList)) $_SESSION['AuthorList'] = $this->AuthorList; $this->Title = "AssEd: Search complete"; if (empty($this->AuthorList)) $this->Html .= "Sorry, it would appear that nobody has that particularly combination of skills.
\n"; else { $this->Html .= "| Author | \n"; // Print the column headers $i = 0; foreach ($this->PostData['Searches'] as $Search) { $i += 1; $this->Html .= "" . $Search['SearchString'] . "↓ | \n"; } // Check the limits if ($this->PostData['Active-All']) $this->Html .= "All pubs ↓ | \n"; if ($this->PostData['Active-5yr']) $this->Html .= "5yr ↓ | \n"; if ($this->PostData['Active']) $this->Html .= "1yr ↓ | \n"; if ($this->PostData['Active-Senior']) $this->Html .= "Last Au ↓ | \n"; $this->Html .= "
|---|---|---|---|---|---|
| " . $Author . " | \n"; // Loop through the search terms for ($i = 1; $i <= $j; $i++) { $this->Html .= "PostData['Searches'][$i]['SearchString']) . ")\">" . $Items[$i] . " | \n"; } // Check the limits if ($this->PostData['Active-All']) $this->Html .= "" . $Items['All'] . " | \n"; if ($this->PostData['Active-5yr']) { $t = array(date('Y'), date('m'), date('d')); $pdat2 = implode("/", $t); $t[0] -= 5; $pdat1 = implode("/", $t); $this->Html .= "" . $Items['5yr'] . " | \n"; } if ($this->PostData['Active']) { $t = array(date('Y'), date('m'), date('d')); $pdat2 = implode("/", $t); $t[0] -= 1; $pdat1 = implode("/", $t); $this->Html .= "" . $Items['1yr'] . " | \n"; } if ($this->PostData['Active-Senior']) $this->Html .= "" . $Items['Senior'] . " | \n"; $this->Html .= "
Warning: caught an exception when trying to retreive the XML data from PubMed. I am going to reload the page and hope that the problem goes away. The error given was: " . $e->getMessage() . "
\n"; $this->SaveReload(); } $this->Xml = $this->Connection->GetResponseBody(); // Convert the response into a string, saved in the global 'Xml' $f = fopen($filename, 'w'); // Cache the new file fwrite($f, $this->Xml); fclose($f); if (!file_exists($filename)) $this->Html .= "Warning: could not save cache file (" . $filename . ")
"; return true; } /*************************************************************************************************************************************** * Function: PubmedIds ( ) * - Gets a list of PubMed IDs and saves an Entrez session variable for later retrieval of the full items */ function PubmedIds($tidy = false, $mindate = false, $maxdate = false) { Global $ncbi_Tool, $ncbi_Email; // Die on error -- This should NEVER occur, and can probably be deleted. if (empty($this->SearchTerm)) die( "Error: missing search term for new search in this->PubmedIds.
" ); // Construct the Query Data $QueryData = array( "db" => "pubmed", "retmode" => "xml", "usehistory" => "y", "term" => $this->SearchTerm, ); if ($mindate and $maxdate) { $QueryData['mindate'] = $mindate; $QueryData['maxdate'] = $maxdate; } elseif (!empty($this->PostData['Searches'][$this->Stage[1]]['Since'])) { $y = date('Y') - $this->PostData['Searches'][$this->Stage[1]]['Since']; $QueryData['mindate'] = $y . "/" . date('m') . "/01"; $QueryData['maxdate'] = date('Y/m/01'); } $this->GetXmlFile( "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi", $QueryData ); // Get the XML file $this->Array = $this->XmlParser( $this->Xml ); // Parse it unset($this->Xml); // clean up! $this->QueryKey = $this->Array['eSearchResult']['QueryKey']; // Save entrez's "session" variables $this->WebEnv = $this->Array['eSearchResult']['WebEnv']; if (empty($this->Array['eSearchResult']['Count'])) { // Check that pubmed actually has some entries matching the query $this->Html .= "Sorry, PubMed has no items matching your search for " . $QueryData['term'] . ". Check your spelling, or try an older date cutoff.
\n"; $this->OutputHtml(); exit; } $this->PostData['Searches'][$this->Stage[1]]['Count'] = $this->Array['eSearchResult']['Count']; // Update the counts (GetCounts() does not support data ranges) if (!empty($tidy)) { // If we're being tidy, remove all the old variables and return true unset($this->Array); return true; } return $this->Array; // Otherwise, return the array } /**************************************************************************************************************************************** * Function: PubmedSearch() * I broke this out from AssEd() because I was using the same bit of code three times. This checks whether we've done the initial * PubmedIds() search, and if not, runs it. Then it's just the ExtractAuthors. * AssEd is actually much more efficiently arranged now, so it does not actually need to be separate, but, hey, it's neater this way. * The cache updater section of this function was originally a separate function, but I kept making it more code efficient, and eventually merged. */ function PubmedSearch() { Global $ncbi_CachePath; $this->SearchTerm = trim($this->PostData['Searches'][$this->Stage[1]]['SearchString']); $this->MinDate = trim($this->PostData['Searches'][$this->Stage[1]]['Since']); /***/ // Check if we have a cached author list $c = $ncbi_CachePath . "author_lists/" . trim($this->MinDate, ".") . "," . str_replace("/", "", trim($this->SearchTerm, ".")) . ".txt"; if (file_exists($c)) { $x = file_get_contents($c); $x = explode("[Results]", $x); $y = explode("\n", $x[0]); foreach ($y as $v) { $v = explode("=", $v); if ($v[0] == "Updated") $u = trim($v[1]); if ($v[0] == "Count") $this->PostData['Searches'][$this->Stage[1]]['Count'] = trim($v[1]); } if (empty($u)) $u = filectime($c); // check whether we need to update the file $updated = array( 'Y' => date('Y', $u), 'm' => date('m', $u), 'd' => "01" ); $now = array( 'Y' => date('Y'), 'm' => date('m'), 'd' => "01" ); if (($updated['Y'] == date('Y')) and ($updated['m'] == date('m'))) return true; // If we don't, we can just exit this search else { /*** CACHE UPDATER ***/ $a = explode("\n", $x[1]); // But if we do, we will first need to extract foreach ($a as $k => $r) { // the cached author list $r = explode("=", $r); $this->CurResults[$r[0]] = $r[1]; } $this->PubmedIDs(true, implode("/", $updated), implode("/", $now)); // Get the new items $this->PubmedExtractAuthors(true, false, true); $updated[0] -= $this->MinDate; $now[0] -= $this->MinDate; $this->PubmedIDs(true, implode("/", $updated), implode("/", $now)); // And remove the old ones $this->PubmedExtractAuthors(true, true, true); $this->SaveAuthors(); $this->PostData['Searches'][$this->Stage[1]]['Count'] = count($this->CurResults); return true; } } /***/ // Otherwise, run the search // if limits haven't been set, do that now. if (($this->Stage[2] == 1) OR (empty($this->WebEnv))) $this->PubmedIds(true); if ($this->PubmedExtractAuthors() === false) exit; /***/ // We will only ever get this far if the search has completed // - so, we can now cache the author list $this->SaveAuthors(); unset($this->WebEnv, $_SESSION['WebEnv'], $this->CurResults, $_SESSION['CurResults']); return true; } /**************************************************************************************************************************************** * Function PubmedExtractAuthors ( single, reverse, noisy ) * - Takes the search and finds all authors * I'm not entirely sure what the most efficient way of doing this is. I have tried using the XML>Array function, but this is * insanely memory intensive. The simple loop and text-search works, but I'm not convinced that there aren't better ways (a * preg_match, perhaps?) to do it. * Input vars: * - Single: gets all results in a single file rather than 1000 items at a time (used when updating caches at the start of the month) * - Reverse: subtracts matching authors from the list, instead of increasing their counts * - Noisy: debug mode -- prints the list of changed authors */ function PubmedExtractAuthors($single = false, $reverse = false, $noisy = false) { Global $ncbi_CachePath; // Run the search $this->QueryData = array( "db" => "pubmed", "retmode" => "xml", "usehistory" => "y", "WebEnv" => $this->WebEnv, "query_key" => $this->QueryKey, "retstart" => $this->Stage[2], "retmax" => "1000" ); if ($single) $this->QueryData['retmax'] = $this->PostData['Searches'][$this->Stage[1]]['Count']; // If we're retrieving this as a single file, amend the "retmax" to get all of the results $this->GetXmlFile( "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi", $this->QueryData ); // Get the XML file if ($this->Stage[1] > 0) if (!empty($_SESSION['AllResults'])) $this->CurResults = $_SESSION['AllResults'][$this->Stage[1]]; // Load the author list, or create empty author list array $lines = explode("\n", $this->Xml); // Extract all of the authors from the XML string if ($lines[3] == "Debug: updated cached results file: reduced count for $NewAuthor to " . $this->CurResults[$NewAuthor]; if (empty($this->CurResults[$NewAuthor])) unset($this->CurResults[$NewAuthor]); // And clean up if the author no longer has any pubs } else { if (!empty($this->CurResults[$NewAuthor])) $this->CurResults[$NewAuthor] += 1; else $this->CurResults[$NewAuthor] = 1; if ($noisy) $this->Html .= "
Debug: updated cached results file: increased count for $NewAuthor to " . $this->CurResults[$NewAuthor]; } } } if ($single) return true; // If we are running in single-search mode, it is time to return $this->Stage[2] += 1000; // Otherwise, move on to the next 100 searches if ($this->Stage[2] < $this->PostData['Searches'][$this->Stage[1]]['Count']) { $a = ($this->Stage[2]-1) / 50000; // Check whether we should save a cache and empty the author list variable in order to reduce $b = round($a); // time taken to process results. (when we get > 50000 results worth of authors in the if ($a == $b) { // CurResults array, things really slow down. $this->SaveAuthors(true); unset($this->CurResults, $_SESSION['CurResults'], $_SESSION['AllResults'][$this->Stage[1]]); } $this->SaveReload(); } if ($this->Stage[2] > 50000) { // If we make it this far, the search is over. Before we can move on though, we need to check $i = 50001; // whether we have some temporary files cached that we need to read back into the current while (true) { // authors array $c = $ncbi_CachePath . "tmp/" . trim($this->MinDate, ".") . "," . str_replace("/", "", trim($this->SearchTerm, ".")) . "," . $i . ".txt"; if (file_exists($c)) { $x = file_get_contents($c); $x = str_replace("\r\n", "\n", $x); // This is a fix for processing *nix generated author lists on windows servers $a = explode("[Results]\n", $x); $a = explode("\n", $a[1]); foreach ($a as $k => $r) { $r = explode("=", $r); if (empty($r[1])) $this->Html .= "
Debug: Invalid entry at line " . $k; if (!empty($this->CurResults[$r[0]])) $this->CurResults[$r[0]] += $r[1]; else $this->CurResults[$r[0]] = $r[1]; } unset($x, $a, $c); $i += 50000; continue; } else break; } } return true; // Move on } /******************************************************************************************************************************************** * Function: RunFilters() * Filters the author lists */ function RunFilters() { Global $ncbi_CachePath; // Get results from the files if (!empty($_SESSION['AllResults'])) $this->AllResults = $_SESSION['AllResults']; else { foreach ($this->PostData['Searches'] as $i => $Search) { $c = $ncbi_CachePath . "author_lists/" . trim($Search['Since'], ".") . "," . str_replace("/", "", trim($Search['SearchString'], ".")) . ".txt"; if (!file_exists($c)) die( "Cache error" ); $x = file_get_contents($c); $x = str_replace("\r\n", "\n", $x); // This is a fix for processing *nix generated author lists on windows servers $a = explode("[Results]\n", $x); $a = explode("\n", $a[1]); foreach ($a as $k => $r) { $r = explode("=", $r); if (empty($r[1])) $this->Html .= "
Debug: Invalid entry at line " . $k; if (($r[1] < $Search['AtLeast']) OR ($r[1] > $Search['AtMost'])) continue; $this->AllResults[$i][$r[0]] = $r[1]; } unset($x, $a, $c); } arsort($this->AllResults[1]); } if (empty($_SESSION['ToFilter'])) $_SESSION['ToFilter'] = count($this->AllResults[1]); // Filter $j = count($this->PostData['Searches']); $k = 0; $l = 0; foreach ($this->AllResults[1] as $key => $value) { $r = true; // Check the author shows up in all lists foreach ($this->AllResults as $AuthorList) { if (empty($AuthorList[$key])) $r = false; } // Now check the author has enough pubs if ($r) { if (!$AuStats = $this->PubmedAuLimits($key)) $r = false; $k += 1; } // Exclude based on the ignore list if ($r AND isset($this->PostData['Ignore']) and isset($this->PostData['IgnoreCoAus'])) { if ($this->PubmedIgnore($key)) $r = false; $k += 1; } if ($r != false) { $this->AuthorList[$key][1] = $value; for ($i = 2; $i <= $j; $i++) { $this->AuthorList[$key][$i] = $this->AllResults[$i][$key]; unset($this->AllResults[$i][$key]); } if (isset($AuStats['All'])) $this->AuthorList[$key]['All'] = $AuStats['All']; if (isset($AuStats['5yr'])) $this->AuthorList[$key]['5yr'] = $AuStats['5yr']; if (isset($AuStats['1yr'])) $this->AuthorList[$key]['1yr'] = $AuStats['1yr']; if (isset($AuStats['Senior'])) $this->AuthorList[$key]['Senior'] = $AuStats['Senior']; } unset($this->AllResults[1][$key]); $l += 1; if ($k > 200) { if (!empty($_SESSION['Filtered'])) $_SESSION['Filtered'] += $l; else $_SESSION['Filtered'] = $l; $this->Stage = array("filters", 0, 0); $this->SaveReload(); exit; } continue; } return true; } /**************************************************************************************************************************************** * Function: PubmedAuLimits ( Author ) * - Checks whether the author meets the seniority requirements */ function PubmedAuLimits($Au) { $Author = "\"" . $Au . "\""; $Url = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?"; $QueryData = array( "db" => "pubmed", "retmode" => "xml", "rettype" => "count", "usehistory" => "y", "term" => $Author, ); /* 1: Total number of publications */ if ($this->PostData['Active-All']) { $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); $k = $this->Array['eSearchResult']['Count']; if (($k < $this->PostData['AtLeast']) OR ($k > $this->PostData['AtMost'])) return false; $r['All'] = $k; } /* 2: Between A and B pubs in past five years */ if ($this->PostData['Active-5yr']) { $QueryData['reldate'] = "1825"; $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); $c = $this->Array['eSearchResult']['Count']; if (($c < $this->PostData['AtLeast-5yr']) OR ($c > $this->PostData['AtMost-5yr'])) return false; $r['5yr'] = $c; } /* 3: At least X pubs in past year */ if ($this->PostData['Active']) { $QueryData['reldate'] = "365"; $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); $y = $this->Array['eSearchResult']['Count']; if (($y < $this->PostData['AtLeast-1yr']) OR ($y > $this->PostData['AtMost-1yr'])) return false; $r['1yr'] = $y; } if (isset($QueryData['reldate'])) unset($QueryData['reldate']); /* 4: At least X pubs as senior author */ $QueryData['term'] .= "[lastau]"; // Amend the query term to last author if ($this->PostData['Active-Senior']) { $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); $s = $this->Array['eSearchResult']['Count']; if (($s < $this->PostData['AtLeastSe']) OR ($s > $this->PostData['AtMostSe'])) return false; $r['Senior'] = $s; } /* 5. Years began publishing */ $QueryData['term'] = $Author; if ($this->PostData['Active-Old']) { $mindate = array((date('Y')-$this->PostData['LimOld']), date('m'), "01"); $maxdate = array((date('Y')-$this->PostData['LimYoung']), date('m'), "01"); $QueryData['mindate'] = "1900/01/01"; $QueryData['maxdate'] = implode("/", $mindate); // See if there is anything older than the old cutoff $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); if (!empty($this->Array['eSearchResult']['Count'])) return false; $QueryData['mindate'] = implode("/", $mindate); // And make sure that there is something older than the yound cutoff $QueryData['maxdate'] = implode("/", $maxdate); $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); if (empty($this->Array['eSearchResult']['Count'])) return false; } /* 6: Years began publishing as senior author */ $QueryData['term'] .= "[lastau]"; if ($this->PostData['Active-Old-Senior']) { $mindate = array((date('Y')- $this->PostData['SeLimOld']), date('m'), "01"); $maxdate = array((date('Y')- $this->PostData['SeLimYoung']), date('m'), "01"); $QueryData['mindate'] = "1900/01/01"; $QueryData['maxdate'] = implode("/", $mindate); // See if there is anything older than the old cutoff $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); if (!empty($this->Array['eSearchResult']['Count'])) return false; $QueryData['mindate'] = implode("/", $mindate); // And make sure that there is something older than the yound cutoff $QueryData['maxdate'] = implode("/", $maxdate); $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); if (empty($this->Array['eSearchResult']['Count'])) return false; } if (isset($r)) return $r; else return true; } /**************************************************************************************************************************************** * Function: PubmedIgnore ( Author ) * - Checks whether the author matches the entries in the ignore list */ function PubmedIgnore($key) { $s = $key . " AND (" . $this->PostData['Ignore'] . ")"; $Url = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?"; $QueryData = array( "db" => "pubmed", "retmode" => "xml", "rettype" => "count", "usehistory" => "y", "mindate" => $this->PostData['IgnoreCoAus'], "maxdate" => date('Y'), "term" => $s, ); $this->GetXmlFile($Url, $QueryData); $this->Array = $this->XmlParser($this->Xml); if (!empty($this->Array['eSearchResult']['Count'])) return true; return false; } /**************************************************************************************************************************************** * Function: SaveAuthors * - I wanted to use an XML layout for the saved authors list, but that considerably raises the amount of memory required (e.g. to get * the array back out of the xml file). Therefore, we have to put up with a cheap and dirty solution * - The TempSave option was added in v0.4 to coopt this function for use in saving temporary results mid-way through the results */ function SaveAuthors($TempSave = false) { Global $ncbi_CachePath; $cutoff = 1; // If there are too many results, we can ignore all who have only one item $total = count($this->CurResults); // for the sake of performance if (!$TempSave and ($total > 250000)) $cutoff = 2; if ($TempSave) { $c = $ncbi_CachePath . "tmp/" . trim($this->MinDate, ".") . "," . str_replace("/", "", trim($this->SearchTerm, ".")) . "," . $this->Stage[2] . ".txt"; $_SESSION['Cleanup'][] = $c; } else $c = $ncbi_CachePath . "author_lists/" . trim($this->MinDate, ".") . "," . str_replace("/", "", trim($this->SearchTerm, ".")) . ".txt"; if (file_exists($c)) unlink($c); $f = fopen($c, 'w+'); $t = "Updated=" . time() . "\nCount=" . count($this->CurResults) . "\n[Results]\n"; foreach ($this->CurResults as $author => $count) { if ($count >= $cutoff) $a[] = $author . "=" . $count; } $t .= implode("\n", $a); fwrite($f, $t); fclose($f); unset($a, $t); return true; } /**************************************************************************************************************************************** * Function: XmlParser * - A generic XML to Array function */ function XmlParser($input = false) { if (empty($input)) $input = $this->XML; // First parse into struct $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0); xml_parse_into_struct($parser, $input, $vals, $index); xml_parser_free($parser); unset($input, $index); // From http://mysrc.blogspot.com/2007/02/php-xml-to-array-and-backwards.html $mnary=array(); $ary=&$mnary; foreach ($vals as $r) { $t=$r['tag']; if ($r['type']=='open') { if (isset($ary[$t])) { if (isset($ary[$t][0])) $ary[$t][]=array(); else $ary[$t]=array($ary[$t], array()); $cv=&$ary[$t][count($ary[$t])-1]; } else $cv=&$ary[$t]; if (isset($r['attributes'])) {foreach ($r['attributes'] as $k=>$v) $cv['_a'][$k]=$v;} $cv['_c']=array(); $cv['_c']['_p']=&$ary; $ary=&$cv['_c']; } elseif ($r['type']=='complete') { if (isset($ary[$t])) { // same as open if (isset($ary[$t][0])) $ary[$t][]=array(); else $ary[$t]=array($ary[$t], array()); $cv=&$ary[$t][count($ary[$t])-1]; } else $cv=&$ary[$t]; if (isset($r['attributes'])) {foreach ($r['attributes'] as $k=>$v) $cv['_a'][$k]=$v;} $cv['_v']=(isset($r['value']) ? $r['value'] : ''); } elseif ($r['type']=='close') { $ary=&$ary['_p']; } } unset($vals); $this->_del_p($mnary); return $mnary; } // _Internal: Remove recursion in result array function _del_p(&$ary) { foreach ($ary as $k => &$v) { if ($k === "_p") unset($ary[$k]); elseif (is_array($ary[$k])) $this->_del_p($ary[$k]); // Added JAD 24/5/08: Removes the _c/_v/_a when there's only one entry -- // makes the array easier to handle if (is_array($v) and (count($v) == 1) and isset($v['_v'])) $v = $v['_v']; elseif (is_array($v) and (count($v) == 1) and isset($v['_c'])) $v = $v['_c']; } } } ?>