| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
// allow-no-test-found: exercised by RedirectsSingleTableLiveResolveTest |
| 8 |
|
| 9 |
/** |
| 10 |
* Live resolver for the denormalized derived columns on the visible page of the |
| 11 |
* admin redirects/captured table (Denorm Step 3b, i460). |
| 12 |
* |
| 13 |
* The single-table read serves rows straight off wp_abj404_redirects, where |
| 14 |
* dest_for_view / published_status / logshits / last_used are real columns that |
| 15 |
* may be stale or empty (right after upgrade, or after a destination rename / new |
| 16 |
* 404 hit). This class resolves the derived/display values LIVE for the ~50 rows |
| 17 |
* on the current page on every read, renders from those live values, and writes |
| 18 |
* the four persisted columns back so subsequent filter/sort reads see fresh data. |
| 19 |
* That is the mechanism behind both the always-fresh display and the |
| 20 |
* instant+complete first load (correct even when the columns are still NULL). |
| 21 |
* |
| 22 |
* Resolution mirrors the staged view_done pipeline exactly (stages S4-S9) so the |
| 23 |
* output is byte-identical to the pre-refactor staged path: dest_for_view / |
| 24 |
* published_status / wp_post_id / wp_post_type per redirect type (POST, CAT/TAG, |
| 25 |
* HOME, EXTERNAL, 404-displayed, else empty/broken); logshits / logsid / |
| 26 |
* last_used rolled up from wp_abj404_logs_hits by the canonical URL key. |
| 27 |
* |
| 28 |
* wp_post_id / wp_post_type / logsid are display-only (not columns on the table). |
| 29 |
* The four persisted columns are written back idempotently (only changed rows), |
| 30 |
* and the persist DEGRADES GRACEFULLY on a read-only / disk-full host: values are |
| 31 |
* still resolved and rendered, the write is skipped, nothing throws (defensive |
| 32 |
* philosophy #2/#8). queryAndGetResults() is the centralized error handler. |
| 33 |
*/ |
| 34 |
class ABJ_404_Solution_RedirectsViewLiveResolver { |
| 35 |
|
| 36 |
/** @var ABJ_404_Solution_DatabaseCore */ |
| 37 |
private $dbCore; |
| 38 |
|
| 39 |
/** @var string|null Memoized blogname for HOME-typed rows (per request). */ |
| 40 |
private $blognameCache = null; |
| 41 |
|
| 42 |
/** @var ABJ_404_Solution_RedirectsDenormSchemaReadiness Live introspection of |
| 43 |
* the denorm columns / sort-key indexes, composed and shared so the resolver, |
| 44 |
* the read coordinator, and the header UI see the same per-request memoized |
| 45 |
* probes. */ |
| 46 |
private $schemaReadiness; |
| 47 |
|
| 48 |
/** @var ABJ_404_Solution_RedirectsHitsRollupReader Rolls up wp_abj404_logs_hits |
| 49 |
* for the visible page (S9-equivalent). */ |
| 50 |
private $hitsRollupReader; |
| 51 |
|
| 52 |
/** |
| 53 |
* Error logging is intentionally delegated to queryAndGetResults (the |
| 54 |
* centralized DAO error handler), so no logger dependency is held here. |
| 55 |
* |
| 56 |
* @param ABJ_404_Solution_DatabaseCore $dbCore |
| 57 |
* @param ABJ_404_Solution_Functions|null $f UTF-8 sanitizer source; falls |
| 58 |
* back to the container's functions service when not injected. |
| 59 |
*/ |
| 60 |
public function __construct(ABJ_404_Solution_DatabaseCore $dbCore, $f = null) { |
| 61 |
$this->dbCore = $dbCore; |
| 62 |
$f = $f !== null ? $f : abj_service('functions'); |
| 63 |
$this->schemaReadiness = new ABJ_404_Solution_RedirectsDenormSchemaReadiness($dbCore); |
| 64 |
$this->hitsRollupReader = new ABJ_404_Solution_RedirectsHitsRollupReader($dbCore, $f); |
| 65 |
} |
| 66 |
|
| 67 |
/** |
| 68 |
* The shared live schema/index readiness introspector for the redirects |
| 69 |
* table. Returned (not mirrored by per-method delegators) so the read |
| 70 |
* coordinator and the header UI consult the SAME per-request memoized probes |
| 71 |
* this resolver uses, keeping the query path and the header from drifting on |
| 72 |
* which sorts are index-ready. |
| 73 |
* |
| 74 |
* @return ABJ_404_Solution_RedirectsDenormSchemaReadiness |
| 75 |
*/ |
| 76 |
public function schemaReadiness(): ABJ_404_Solution_RedirectsDenormSchemaReadiness { |
| 77 |
return $this->schemaReadiness; |
| 78 |
} |
| 79 |
|
| 80 |
/** |
| 81 |
* Safe string read of a query-result field; absent/non-scalar becomes ''. |
| 82 |
* @param array<array-key, mixed> $row @param string $key @return string |
| 83 |
*/ |
| 84 |
private function strField(array $row, string $key): string { |
| 85 |
return isset($row[$key]) && is_scalar($row[$key]) ? (string)$row[$key] : ''; |
| 86 |
} |
| 87 |
|
| 88 |
/** |
| 89 |
* Safe int read; NULL/absent/non-numeric stays null, numeric becomes int. |
| 90 |
* @param array<array-key, mixed> $row @param string $key @return int|null |
| 91 |
*/ |
| 92 |
private function intFieldOrNull(array $row, string $key): ?int { |
| 93 |
return isset($row[$key]) && is_numeric($row[$key]) ? (int)$row[$key] : null; |
| 94 |
} |
| 95 |
|
| 96 |
/** |
| 97 |
* Safe string read that preserves the NULL/absent distinction (stays null), |
| 98 |
* which the write-back change detection needs (stored NULL != resolved ''). |
| 99 |
* @param array<array-key, mixed> $row @param string $key @return string|null |
| 100 |
*/ |
| 101 |
private function strFieldOrNull(array $row, string $key): ?string { |
| 102 |
return isset($row[$key]) && is_scalar($row[$key]) ? (string)$row[$key] : null; |
| 103 |
} |
| 104 |
|
| 105 |
/** |
| 106 |
* Resolve the derived/display columns for the visible rows LIVE, overwrite |
| 107 |
* the rendered values on each row, and persist the four denorm columns back. |
| 108 |
* |
| 109 |
* @param array<int, array<string, mixed>> $rows Rows read off wp_abj404_redirects. |
| 110 |
* @param bool $persist Whether the four denorm columns exist and may be |
| 111 |
* written back. False on a schema-drifted table that lacks the columns: |
| 112 |
* the values are still resolved for display, just not persisted. |
| 113 |
* @return array<int, array<string, mixed>> The same rows with fresh derived values. |
| 114 |
*/ |
| 115 |
public function resolveAndPersistVisibleRows(array $rows, bool $persist = true): array { |
| 116 |
if (empty($rows)) { |
| 117 |
return $rows; |
| 118 |
} |
| 119 |
|
| 120 |
$postsMap = $this->resolvePostsMap($rows); |
| 121 |
$termsMap = $this->resolveTermsMap($rows); |
| 122 |
$hitsMap = $this->hitsRollupReader->resolveHitsMap($rows); |
| 123 |
|
| 124 |
$resolved = array(); |
| 125 |
$writeBacks = array(); |
| 126 |
foreach ($rows as $row) { |
| 127 |
if (!is_array($row)) { |
| 128 |
$resolved[] = $row; |
| 129 |
continue; |
| 130 |
} |
| 131 |
$out = $this->applyResolution($row, $postsMap, $termsMap, $hitsMap); |
| 132 |
$resolved[] = $out; |
| 133 |
if (!$persist) { |
| 134 |
continue; |
| 135 |
} |
| 136 |
$persistValues = $this->persistValuesIfChanged($row, $out); |
| 137 |
if ($persistValues !== null) { |
| 138 |
$writeBacks[] = $persistValues; |
| 139 |
} |
| 140 |
} |
| 141 |
|
| 142 |
if (!empty($writeBacks)) { |
| 143 |
$this->persistResolvedColumns($writeBacks); |
| 144 |
} |
| 145 |
|
| 146 |
return $resolved; |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Look up wp_posts for every POST-typed row's numeric final_dest (S4). |
| 151 |
* @param array<int, array<string, mixed>> $rows |
| 152 |
* @return array<int, array<string, mixed>> postId => {ID, post_title, post_status, post_type} |
| 153 |
*/ |
| 154 |
private function resolvePostsMap(array $rows): array { |
| 155 |
$ids = $this->collectFinalDestIds($rows, ABJ404_TYPE_POST); |
| 156 |
if (empty($ids)) { |
| 157 |
return array(); |
| 158 |
} |
| 159 |
// allow-unbounded-select: caller-supplied ID keyset (where ID IN the current page's rows); page-bounded |
| 160 |
$query = "SELECT ID, post_title, post_status, post_type FROM {wp_posts} WHERE ID IN (" |
| 161 |
. implode(',', $ids) . ")"; |
| 162 |
return $this->indexRowsBy($this->dbCore->queryAndGetResults($query), 'ID'); |
| 163 |
} |
| 164 |
|
| 165 |
/** |
| 166 |
* Look up wp_terms for every CAT/TAG-typed row's numeric final_dest (S5). |
| 167 |
* @param array<int, array<string, mixed>> $rows |
| 168 |
* @return array<int, array<string, mixed>> termId => {term_id, name} |
| 169 |
*/ |
| 170 |
private function resolveTermsMap(array $rows): array { |
| 171 |
$ids = array_merge( |
| 172 |
$this->collectFinalDestIds($rows, ABJ404_TYPE_CAT), |
| 173 |
$this->collectFinalDestIds($rows, ABJ404_TYPE_TAG) |
| 174 |
); |
| 175 |
$ids = array_values(array_unique($ids)); |
| 176 |
if (empty($ids)) { |
| 177 |
return array(); |
| 178 |
} |
| 179 |
$query = "SELECT term_id, name FROM {wp_terms} WHERE term_id IN (" . implode(',', $ids) . ")"; |
| 180 |
return $this->indexRowsBy($this->dbCore->queryAndGetResults($query), 'term_id'); |
| 181 |
} |
| 182 |
|
| 183 |
/** |
| 184 |
* Overwrite the derived/display fields on a single row from the resolved |
| 185 |
* maps. Per-type destination resolution mirrors staged stages S4-S8 plus the |
| 186 |
* catch-all; hits come from the S9-equivalent rollup. |
| 187 |
* |
| 188 |
* @param array<string, mixed> $row |
| 189 |
* @param array<int, array<string, mixed>> $postsMap |
| 190 |
* @param array<int, array<string, mixed>> $termsMap |
| 191 |
* @param array<string, array{logshits:int, logsid:int|null, last_used:int|null}> $hitsMap |
| 192 |
* @return array<string, mixed> |
| 193 |
*/ |
| 194 |
private function applyResolution(array $row, array $postsMap, array $termsMap, array $hitsMap): array { |
| 195 |
$out = $row; |
| 196 |
|
| 197 |
$destination = $this->resolveDestinationFields($row, $postsMap, $termsMap); |
| 198 |
$out['dest_for_view'] = $destination['dest_for_view']; |
| 199 |
$out['published_status'] = $destination['published_status']; |
| 200 |
$out['wp_post_id'] = $destination['wp_post_id']; |
| 201 |
$out['wp_post_type'] = $destination['wp_post_type']; |
| 202 |
|
| 203 |
$hit = $hitsMap[$this->canonicalUrl($this->strField($row, 'url'))] ?? null; |
| 204 |
$out['logshits'] = $hit !== null ? $hit['logshits'] : null; |
| 205 |
$out['logsid'] = $hit !== null ? $hit['logsid'] : null; |
| 206 |
$out['last_used'] = $hit !== null ? $hit['last_used'] : null; |
| 207 |
|
| 208 |
return $out; |
| 209 |
} |
| 210 |
|
| 211 |
/** |
| 212 |
* Resolve dest_for_view / published_status / wp_post_id / wp_post_type for a |
| 213 |
* single row per redirect type. Mirrors staged stages S4-S8 plus the |
| 214 |
* catch-all (any other type renders empty/broken). |
| 215 |
* |
| 216 |
* @param array<string, mixed> $row |
| 217 |
* @param array<int, array<string, mixed>> $postsMap |
| 218 |
* @param array<int, array<string, mixed>> $termsMap |
| 219 |
* @return array{dest_for_view:string, published_status:int, wp_post_id:string|null, wp_post_type:string|null} |
| 220 |
*/ |
| 221 |
private function resolveDestinationFields(array $row, array $postsMap, array $termsMap): array { |
| 222 |
$type = $this->intFieldOrNull($row, 'type'); |
| 223 |
$finalDest = $this->strField($row, 'final_dest'); |
| 224 |
|
| 225 |
if ($type === ABJ404_TYPE_POST) { |
| 226 |
$post = $this->lookupById($postsMap, $finalDest); |
| 227 |
if ($post === null) { |
| 228 |
return $this->destinationFields('', 0); |
| 229 |
} |
| 230 |
return $this->destinationFields( |
| 231 |
$this->strField($post, 'post_title'), |
| 232 |
strtolower($this->strField($post, 'post_status')) === 'publish' ? 1 : 0, |
| 233 |
isset($post['ID']) ? $this->strField($post, 'ID') : null, |
| 234 |
isset($post['post_type']) ? $this->strField($post, 'post_type') : null |
| 235 |
); |
| 236 |
} |
| 237 |
if ($type === ABJ404_TYPE_CAT || $type === ABJ404_TYPE_TAG) { |
| 238 |
$term = $this->lookupById($termsMap, $finalDest); |
| 239 |
return $term === null |
| 240 |
? $this->destinationFields('', 0) |
| 241 |
: $this->destinationFields($this->strField($term, 'name'), 1); |
| 242 |
} |
| 243 |
if ($type === ABJ404_TYPE_HOME) { |
| 244 |
return $this->destinationFields($this->blogname(), 1); |
| 245 |
} |
| 246 |
if ($type === ABJ404_TYPE_EXTERNAL) { |
| 247 |
return $this->destinationFields($finalDest, 1); |
| 248 |
} |
| 249 |
if ($type === ABJ404_TYPE_404_DISPLAYED) { |
| 250 |
return $this->destinationFields('', 1); |
| 251 |
} |
| 252 |
return $this->destinationFields('', 0); |
| 253 |
} |
| 254 |
|
| 255 |
/** |
| 256 |
* @return array{dest_for_view:string, published_status:int, wp_post_id:string|null, wp_post_type:string|null} |
| 257 |
*/ |
| 258 |
private function destinationFields(string $dest, int $published, ?string $wpPostId = null, ?string $wpPostType = null): array { |
| 259 |
return array( |
| 260 |
'dest_for_view' => $dest, |
| 261 |
'published_status' => $published, |
| 262 |
'wp_post_id' => $wpPostId, |
| 263 |
'wp_post_type' => $wpPostType, |
| 264 |
); |
| 265 |
} |
| 266 |
|
| 267 |
/** |
| 268 |
* Compute the to-be-persisted values for the four denorm columns, returning |
| 269 |
* them only when they differ from what the row already stored (idempotent |
| 270 |
* write-back; converged rows produce no write). logshits is NOT NULL on the |
| 271 |
* table, so an unresolved (no-hits) row persists 0; comparing the persist |
| 272 |
* value (not the rendered NULL) against the stored 0 avoids rewriting forever. |
| 273 |
* |
| 274 |
* @param array<string, mixed> $original The row as read off the table. |
| 275 |
* @param array<string, mixed> $resolved The row after live resolution. |
| 276 |
* @return array{id:int, dest_for_view:string, dest_sort_key:string, published_status:int, logshits:int, last_used:int|null}|null |
| 277 |
*/ |
| 278 |
private function persistValuesIfChanged(array $original, array $resolved): ?array { |
| 279 |
$id = $this->intFieldOrNull($resolved, 'id'); |
| 280 |
if ($id === null) { |
| 281 |
return null; |
| 282 |
} |
| 283 |
$dest = $this->strField($resolved, 'dest_for_view'); |
| 284 |
$published = $this->intFieldOrNull($resolved, 'published_status') ?? 0; |
| 285 |
$logshits = $this->intFieldOrNull($resolved, 'logshits') ?? 0; |
| 286 |
$lastUsed = $this->intFieldOrNull($resolved, 'last_used'); |
| 287 |
|
| 288 |
$storedDest = $this->strFieldOrNull($original, 'dest_for_view'); |
| 289 |
$storedPublished = $this->intFieldOrNull($original, 'published_status'); |
| 290 |
$storedLogshits = $this->intFieldOrNull($original, 'logshits'); |
| 291 |
$storedLastUsed = $this->intFieldOrNull($original, 'last_used'); |
| 292 |
|
| 293 |
$unchanged = $storedDest === $dest |
| 294 |
&& $storedPublished === $published |
| 295 |
&& $storedLogshits === $logshits |
| 296 |
&& $storedLastUsed === $lastUsed; |
| 297 |
if ($unchanged) { |
| 298 |
return null; |
| 299 |
} |
| 300 |
|
| 301 |
// dest_sort_key is a pure function of dest_for_view (the indexable narrow |
| 302 |
// copy LEFT(dest_for_view, 191)); it rides along on the dest_for_view |
| 303 |
// change rather than being its own change trigger. The bulk backfill is |
| 304 |
// the populator for the pre-backfill NULL window. mb_substr counts |
| 305 |
// characters, matching the SQL LEFT(...,191). |
| 306 |
$destSortKey = function_exists('mb_substr') |
| 307 |
? (string) mb_substr($dest, 0, 191) |
| 308 |
: (string) substr($dest, 0, 191); |
| 309 |
|
| 310 |
return array( |
| 311 |
'id' => $id, |
| 312 |
'dest_for_view' => $dest, |
| 313 |
'dest_sort_key' => $destSortKey, |
| 314 |
'published_status' => $published, |
| 315 |
'logshits' => $logshits, |
| 316 |
'last_used' => $lastUsed, |
| 317 |
); |
| 318 |
} |
| 319 |
|
| 320 |
/** |
| 321 |
* Persist the four denorm columns for the changed rows in one batched UPDATE. |
| 322 |
* Skipped entirely when a write block (read-only replica / disk full) is |
| 323 |
* active so a degraded host still renders without an errored write. |
| 324 |
* |
| 325 |
* @param array<int, array{id:int, dest_for_view:string, dest_sort_key:string, published_status:int, logshits:int, last_used:int|null}> $writeBacks |
| 326 |
* @return void |
| 327 |
*/ |
| 328 |
private function persistResolvedColumns(array $writeBacks): void { |
| 329 |
if ($this->dbCore->noticeState()->isWriteBlockActive()) { |
| 330 |
return; |
| 331 |
} |
| 332 |
|
| 333 |
// dest_sort_key is written only when the column exists (added after the |
| 334 |
// Step 3a four); on an install still missing it, skip that one assignment |
| 335 |
// so the write-back of the other columns still succeeds (schema drift). |
| 336 |
$writeDestSortKey = $this->schemaReadiness->destSortKeyColumnPresent(); |
| 337 |
|
| 338 |
$ids = array(); |
| 339 |
$destCases = ''; |
| 340 |
$destSortCases = ''; |
| 341 |
$publishedCases = ''; |
| 342 |
$logshitsCases = ''; |
| 343 |
$lastUsedCases = ''; |
| 344 |
foreach ($writeBacks as $wb) { |
| 345 |
$id = (int)$wb['id']; |
| 346 |
$ids[] = $id; |
| 347 |
$destCases .= ' WHEN ' . $id . " THEN '" . esc_sql($wb['dest_for_view']) . "'"; |
| 348 |
$destSortCases .= ' WHEN ' . $id . " THEN '" . esc_sql($wb['dest_sort_key']) . "'"; |
| 349 |
$publishedCases .= ' WHEN ' . $id . ' THEN ' . (int)$wb['published_status']; |
| 350 |
$logshitsCases .= ' WHEN ' . $id . ' THEN ' . (int)$wb['logshits']; |
| 351 |
$lastUsedCases .= ' WHEN ' . $id . ' THEN ' |
| 352 |
. ($wb['last_used'] === null ? 'NULL' : (int)$wb['last_used']); |
| 353 |
} |
| 354 |
|
| 355 |
$idList = implode(',', $ids); |
| 356 |
$query = "UPDATE {wp_abj404_redirects} SET" |
| 357 |
. " dest_for_view = CASE id" . $destCases . " END," |
| 358 |
. ($writeDestSortKey ? " dest_sort_key = CASE id" . $destSortCases . " END," : "") |
| 359 |
. " published_status = CASE id" . $publishedCases . " END," |
| 360 |
. " logshits = CASE id" . $logshitsCases . " END," |
| 361 |
. " last_used = CASE id" . $lastUsedCases . " END" |
| 362 |
. " WHERE id IN (" . $idList . ")"; |
| 363 |
// queryAndGetResults is the centralized error handler: a write failure on |
| 364 |
// a read-only/disk-full host is logged there as a warning and never |
| 365 |
// surfaced. The resolved values were already rendered, so a skipped |
| 366 |
// persist only costs a re-resolve on the next read. |
| 367 |
$this->dbCore->queryAndGetResults($query); |
| 368 |
} |
| 369 |
|
| 370 |
/** |
| 371 |
* Collect numeric final_dest values for rows of one type; non-numeric |
| 372 |
* final_dest is dropped (matches the staged fd_int REGEXP guard). |
| 373 |
* @param array<int, array<string, mixed>> $rows @param int $type |
| 374 |
* @return array<int, int> |
| 375 |
*/ |
| 376 |
private function collectFinalDestIds(array $rows, int $type): array { |
| 377 |
$ids = array(); |
| 378 |
foreach ($rows as $row) { |
| 379 |
if (!is_array($row)) { |
| 380 |
continue; |
| 381 |
} |
| 382 |
$rowType = $this->intFieldOrNull($row, 'type'); |
| 383 |
if ($rowType !== $type) { |
| 384 |
continue; |
| 385 |
} |
| 386 |
$finalDest = $this->strField($row, 'final_dest'); |
| 387 |
if (preg_match('/^\d+$/', $finalDest)) { |
| 388 |
$ids[(int)$finalDest] = (int)$finalDest; |
| 389 |
} |
| 390 |
} |
| 391 |
return array_values($ids); |
| 392 |
} |
| 393 |
|
| 394 |
/** |
| 395 |
* @param array<string, mixed> $result queryAndGetResults() return shape. |
| 396 |
* @return array<int, array<string, mixed>> |
| 397 |
*/ |
| 398 |
private function indexRowsBy(array $result, string $keyColumn): array { |
| 399 |
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : array(); |
| 400 |
$map = array(); |
| 401 |
foreach ($rows as $row) { |
| 402 |
if (is_array($row) && isset($row[$keyColumn]) && is_numeric($row[$keyColumn])) { |
| 403 |
$map[(int)$row[$keyColumn]] = $row; |
| 404 |
} |
| 405 |
} |
| 406 |
return $map; |
| 407 |
} |
| 408 |
|
| 409 |
/** |
| 410 |
* @param array<int, array<string, mixed>> $map @param string $finalDest |
| 411 |
* @return array<string, mixed>|null |
| 412 |
*/ |
| 413 |
private function lookupById(array $map, string $finalDest): ?array { |
| 414 |
if (!preg_match('/^\d+$/', $finalDest)) { |
| 415 |
return null; |
| 416 |
} |
| 417 |
return $map[(int)$finalDest] ?? null; |
| 418 |
} |
| 419 |
|
| 420 |
/** @param string $url @return string */ |
| 421 |
private function canonicalUrl(string $url): string { |
| 422 |
return '/' . trim($url, '/'); |
| 423 |
} |
| 424 |
|
| 425 |
/** @return string */ |
| 426 |
private function blogname(): string { |
| 427 |
if ($this->blognameCache !== null) { |
| 428 |
return $this->blognameCache; |
| 429 |
} |
| 430 |
$value = ''; |
| 431 |
if (function_exists('get_option')) { |
| 432 |
$raw = get_option('blogname', ''); |
| 433 |
$value = is_scalar($raw) ? (string)$raw : ''; |
| 434 |
} |
| 435 |
if ($value === '') { |
| 436 |
$result = $this->dbCore->queryAndGetResults( |
| 437 |
"SELECT option_value FROM {wp_options} WHERE option_name = 'blogname' LIMIT 1" |
| 438 |
); |
| 439 |
$rows = is_array($result['rows'] ?? null) ? $result['rows'] : array(); |
| 440 |
if (isset($rows[0]) && is_array($rows[0])) { |
| 441 |
$value = $this->strField($rows[0], 'option_value'); |
| 442 |
} |
| 443 |
} |
| 444 |
$this->blognameCache = $value; |
| 445 |
return $value; |
| 446 |
} |
| 447 |
} |
| 448 |
|