| Total Complexity | 105 | 
| Total Lines | 609 | 
| Duplicated Lines | 0 % | 
| Changes | 6 | ||
| Bugs | 0 | Features | 0 | 
Complex classes like MetaLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use MetaLoader, and based on these observations, apply Extract Interface, too.
| 1 | <?php | ||
| 14 | class MetaLoader | ||
| 15 | { | ||
| 16 | /** @var int|null */ | ||
| 17 | private $expire; | ||
| 18 | |||
| 19 | /** @var array */ | ||
| 20 | private $metadata = []; | ||
| 21 | |||
| 22 | /** @var object|null */ | ||
| 23 | private $oldMetadataSrc; | ||
| 24 | |||
| 25 | /** @var string|null */ | ||
| 26 | private $stateFile = null; | ||
| 27 | |||
| 28 | /** @var bool*/ | ||
| 29 | private $changed = false; | ||
| 30 | |||
| 31 | /** @var array */ | ||
| 32 | private $state = []; | ||
| 33 | |||
| 34 | /** @var array */ | ||
| 35 | private $types = [ | ||
| 36 | 'saml20-idp-remote', | ||
| 37 | 'saml20-sp-remote', | ||
| 38 | 'attributeauthority-remote' | ||
| 39 | ]; | ||
| 40 | |||
| 41 | |||
| 42 | /** | ||
| 43 | * Constructor | ||
| 44 | * | ||
| 45 | * @param int|null $expire | ||
| 46 | * @param string|null $stateFile | ||
| 47 | * @param object|null $oldMetadataSrc | ||
| 48 | */ | ||
| 49 | public function __construct(int $expire = null, string $stateFile = null, object $oldMetadataSrc = null) | ||
| 50 |     { | ||
| 51 | $this->expire = $expire; | ||
| 52 | $this->oldMetadataSrc = $oldMetadataSrc; | ||
| 53 | $this->stateFile = $stateFile; | ||
| 54 | |||
| 55 | // Read file containing $state from disk | ||
| 56 | /** @psalm-var array|null */ | ||
| 57 | $state = null; | ||
| 58 |         if (!is_null($stateFile) && is_readable($stateFile)) { | ||
| 59 | include($stateFile); | ||
| 60 | } | ||
| 61 | |||
| 62 |         if (!empty($state)) { | ||
|  | |||
| 63 | $this->state = $state; | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | |||
| 68 | /** | ||
| 69 | * Get the types of entities that will be loaded. | ||
| 70 | * | ||
| 71 | * @return array The entity types allowed. | ||
| 72 | */ | ||
| 73 | public function getTypes(): array | ||
| 76 | } | ||
| 77 | |||
| 78 | |||
| 79 | /** | ||
| 80 | * Set the types of entities that will be loaded. | ||
| 81 | * | ||
| 82 | * @param string|array $types Either a string with the name of one single type allowed, or an array with a list of | ||
| 83 | * types. Pass an empty array to reset to all types of entities. | ||
| 84 | * @return void | ||
| 85 | */ | ||
| 86 | public function setTypes($types): void | ||
| 92 | } | ||
| 93 | |||
| 94 | |||
| 95 | /** | ||
| 96 | * This function processes a SAML metadata file. | ||
| 97 | * | ||
| 98 | * @param $source array | ||
| 99 | * @return void | ||
| 100 | */ | ||
| 101 | public function loadSource(array $source): void | ||
| 102 |     { | ||
| 103 |         if (preg_match('@^https?://@i', $source['src'])) { | ||
| 104 | // Build new HTTP context | ||
| 105 | $context = $this->createContext($source); | ||
| 106 | |||
| 107 | // GET! | ||
| 108 |             try { | ||
| 109 | list($data, $responseHeaders) = \SimpleSAML\Utils\HTTP::fetch($source['src'], $context, true); | ||
| 110 |             } catch (\Exception $e) { | ||
| 111 |                 Logger::warning('metarefresh: ' . $e->getMessage()); | ||
| 112 | } | ||
| 113 | |||
| 114 | // We have response headers, so the request succeeded | ||
| 115 |             if (!isset($responseHeaders)) { | ||
| 116 | // No response headers, this means the request failed in some way, so re-use old data | ||
| 117 |                 Logger::debug('No response from ' . $source['src'] . ' - attempting to re-use cached metadata'); | ||
| 118 | $this->addCachedMetadata($source); | ||
| 119 | return; | ||
| 120 |             } elseif (preg_match('@^HTTP/1\.[01]\s304\s@', $responseHeaders[0])) { | ||
| 121 | // 304 response | ||
| 122 |                 Logger::debug('Received HTTP 304 (Not Modified) - attempting to re-use cached metadata'); | ||
| 123 | $this->addCachedMetadata($source); | ||
| 124 | return; | ||
| 125 |             } elseif (!preg_match('@^HTTP/1\.[01]\s200\s@', $responseHeaders[0])) { | ||
| 126 | // Other error | ||
| 127 |                 Logger::debug('Error from ' . $source['src'] . ' - attempting to re-use cached metadata'); | ||
| 128 | $this->addCachedMetadata($source); | ||
| 129 | return; | ||
| 130 | } | ||
| 131 |         } else { | ||
| 132 | // Local file. | ||
| 133 | $data = file_get_contents($source['src']); | ||
| 134 | $responseHeaders = null; | ||
| 135 | } | ||
| 136 | |||
| 137 | // Everything OK. Proceed. | ||
| 138 |         if (isset($source['conditionalGET']) && $source['conditionalGET']) { | ||
| 139 | // Stale or no metadata, so a fresh copy | ||
| 140 |             Logger::debug('Downloaded fresh copy'); | ||
| 141 | } | ||
| 142 | |||
| 143 |         try { | ||
| 144 | $entities = $this->loadXML($data, $source); | ||
| 145 |         } catch (\Exception $e) { | ||
| 146 |             Logger::debug('XML parser error when parsing ' . $source['src'] . ' - attempting to re-use cached metadata'); | ||
| 147 |             Logger::debug('XML parser returned: ' . $e->getMessage()); | ||
| 148 | $this->addCachedMetadata($source); | ||
| 149 | return; | ||
| 150 | } | ||
| 151 | |||
| 152 |         foreach ($entities as $entity) { | ||
| 153 |             if (isset($source['blacklist'])) { | ||
| 154 |                 if (!empty($source['blacklist']) && in_array($entity->getEntityId(), $source['blacklist'], true)) { | ||
| 155 |                     Logger::info('Skipping "' . $entity->getEntityId() . '" - blacklisted.' . "\n"); | ||
| 156 | continue; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 |             if (isset($source['whitelist'])) { | ||
| 161 |                 if (!empty($source['whitelist']) && !in_array($entity->getEntityId(), $source['whitelist'], true)) { | ||
| 162 |                     Logger::info('Skipping "' . $entity->getEntityId() . '" - not in the whitelist.' . "\n"); | ||
| 163 | continue; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | /* Do we have an attribute whitelist? */ | ||
| 168 |             if (isset($source['attributewhitelist']) && !empty($source['attributewhitelist'])) { | ||
| 169 | $idpMetadata = $entity->getMetadata20IdP(); | ||
| 170 |                 if (!isset($idpMetadata)) { | ||
| 171 | /* Skip non-IdPs */ | ||
| 172 | continue; | ||
| 173 | } | ||
| 174 | |||
| 175 | /* Do a recursive comparison for each whitelist of the attributewhitelist with the idpMetadata for this | ||
| 176 | * IdP. At least one of these whitelists should match */ | ||
| 177 | $match = false; | ||
| 178 |                 foreach ($source['attributewhitelist'] as $whitelist) { | ||
| 179 |                     if ($this->containsArray($whitelist, $idpMetadata)) { | ||
| 180 | $match = true; | ||
| 181 | break; | ||
| 182 | } | ||
| 183 | } | ||
| 184 |                 if (!$match) { | ||
| 185 | /* No match found -> next IdP */ | ||
| 186 | continue; | ||
| 187 | } | ||
| 188 |                 Logger::debug('Whitelisted entityID: '. $entity->getEntityID()); | ||
| 189 | } | ||
| 190 | |||
| 191 |             if (array_key_exists('certificates', $source) && ($source['certificates'] !== null)) { | ||
| 192 |                 if (!$entity->validateSignature($source['certificates'])) { | ||
| 193 | Logger::info( | ||
| 194 | 'Skipping "' . $entity->getEntityId() . '" - could not verify signature using certificate.' . "\n" | ||
| 195 | ); | ||
| 196 | continue; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | $template = null; | ||
| 201 |             if (array_key_exists('template', $source)) { | ||
| 202 | $template = $source['template']; | ||
| 203 | } | ||
| 204 | |||
| 205 |             if (in_array('saml20-sp-remote', $this->types, true)) { | ||
| 206 | $this->addMetadata($source['src'], $entity->getMetadata20SP(), 'saml20-sp-remote', $template); | ||
| 207 | } | ||
| 208 |             if (in_array('saml20-idp-remote', $this->types, true)) { | ||
| 209 | $this->addMetadata($source['src'], $entity->getMetadata20IdP(), 'saml20-idp-remote', $template); | ||
| 210 | } | ||
| 211 |             if (in_array('attributeauthority-remote', $this->types, true)) { | ||
| 212 | $attributeAuthorities = $entity->getAttributeAuthorities(); | ||
| 213 |                 if (!empty($attributeAuthorities)) { | ||
| 214 | $this->addMetadata( | ||
| 215 | $source['src'], | ||
| 216 | $attributeAuthorities, | ||
| 217 | 'attributeauthority-remote', | ||
| 218 | $template | ||
| 219 | ); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | } | ||
| 223 | |||
| 224 | $this->saveState($source, $responseHeaders); | ||
| 225 | } | ||
| 226 | |||
| 227 | |||
| 228 | /* | ||
| 229 | * Recursively checks whether array $dst contains array $src. If $src | ||
| 230 | * is not an array, a literal comparison is being performed. | ||
| 231 | */ | ||
| 232 | private function containsArray($src, $dst): bool | ||
| 233 |     { | ||
| 234 |         if (is_array($src)) { | ||
| 235 |             if (!is_array($dst)) { | ||
| 236 | return false; | ||
| 237 | } | ||
| 238 | $dstKeys = array_keys($dst); | ||
| 239 | |||
| 240 | /* Loop over all src keys */ | ||
| 241 |             foreach ($src as $srcKey => $srcval) { | ||
| 242 |                 if (is_int($srcKey)) { | ||
| 243 | /* key is number, check that the key appears as one | ||
| 244 | * of the destination keys: if not, then src has | ||
| 245 | * more keys than dst */ | ||
| 246 |                     if (!array_key_exists($srcKey, $dst)) { | ||
| 247 | return false; | ||
| 248 | } | ||
| 249 | |||
| 250 | /* loop over dest keys, to find value: we don't know | ||
| 251 | * whether they are in the same order */ | ||
| 252 | $submatch = false; | ||
| 253 |                     foreach ($dstKeys as $dstKey) { | ||
| 254 |                         if ($this->containsArray($srcval, $dst[$dstKey])) { | ||
| 255 | $submatch = true; | ||
| 256 | break; | ||
| 257 | } | ||
| 258 | } | ||
| 259 |                     if (!$submatch) { | ||
| 260 | return false; | ||
| 261 | } | ||
| 262 |                 } else { | ||
| 263 | /* key is regexp: find matching keys */ | ||
| 264 | $matchingDstKeys = preg_grep($srcKey, $dstKeys); | ||
| 265 |                     if (!is_array($matchingDstKeys)) { | ||
| 266 | return false; | ||
| 267 | } | ||
| 268 | |||
| 269 | $match = false; | ||
| 270 |                     foreach ($matchingDstKeys as $dstKey) { | ||
| 271 |                         if ($this->containsArray($srcval, $dst[$dstKey])) { | ||
| 272 | /* Found a match */ | ||
| 273 | $match = true; | ||
| 274 | break; | ||
| 275 | } | ||
| 276 | } | ||
| 277 |                     if (!$match) { | ||
| 278 | /* none of the keys has a matching value */ | ||
| 279 | return false; | ||
| 280 | } | ||
| 281 | } | ||
| 282 | } | ||
| 283 | /* each src key/value matches */ | ||
| 284 | return true; | ||
| 285 |         } else { | ||
| 286 | /* src is not an array, do a regexp match against dst */ | ||
| 287 | return (preg_match($src, $dst) === 1); | ||
| 288 | } | ||
| 289 | } | ||
| 290 | |||
| 291 | /** | ||
| 292 | * Create HTTP context, with any available caches taken into account | ||
| 293 | * | ||
| 294 | * @param array $source | ||
| 295 | * @return array | ||
| 296 | */ | ||
| 297 | private function createContext(array $source): array | ||
| 298 |     { | ||
| 299 | $config = Configuration::getInstance(); | ||
| 300 |         $name = $config->getString('technicalcontact_name', null); | ||
| 301 |         $mail = $config->getString('technicalcontact_email', null); | ||
| 302 | |||
| 303 | $rawheader = "User-Agent: SimpleSAMLphp metarefresh, run by $name <$mail>\r\n"; | ||
| 304 | |||
| 305 |         if (isset($source['conditionalGET']) && $source['conditionalGET']) { | ||
| 306 |             if (array_key_exists($source['src'], $this->state)) { | ||
| 307 | $sourceState = $this->state[$source['src']]; | ||
| 308 | |||
| 309 |                 if (isset($sourceState['last-modified'])) { | ||
| 310 | $rawheader .= 'If-Modified-Since: ' . $sourceState['last-modified'] . "\r\n"; | ||
| 311 | } | ||
| 312 | |||
| 313 |                 if (isset($sourceState['etag'])) { | ||
| 314 | $rawheader .= 'If-None-Match: ' . $sourceState['etag'] . "\r\n"; | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | return ['http' => ['header' => $rawheader]]; | ||
| 320 | } | ||
| 321 | |||
| 322 | |||
| 323 | /** | ||
| 324 | * @param array $source | ||
| 325 | * @return void | ||
| 326 | */ | ||
| 327 | private function addCachedMetadata(array $source): void | ||
| 328 |     { | ||
| 329 |         if (isset($this->oldMetadataSrc)) { | ||
| 330 |             foreach ($this->types as $type) { | ||
| 331 |                 foreach ($this->oldMetadataSrc->getMetadataSet($type) as $entity) { | ||
| 332 |                     if (array_key_exists('metarefresh:src', $entity)) { | ||
| 333 |                         if ($entity['metarefresh:src'] == $source['src']) { | ||
| 334 | $this->addMetadata($source['src'], $entity, $type); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | } | ||
| 338 | } | ||
| 339 | } | ||
| 340 | } | ||
| 341 | |||
| 342 | |||
| 343 | /** | ||
| 344 | * Store caching state data for a source | ||
| 345 | * | ||
| 346 | * @param array $source | ||
| 347 | * @param array|null $responseHeaders | ||
| 348 | * @return void | ||
| 349 | */ | ||
| 350 | private function saveState(array $source, ?array $responseHeaders): void | ||
| 368 | } | ||
| 369 | } | ||
| 370 | } | ||
| 371 | |||
| 372 | |||
| 373 | /** | ||
| 374 | * Parse XML metadata and return entities | ||
| 375 | * | ||
| 376 | * @param string $data | ||
| 377 | * @param array $source | ||
| 378 | * @return \SimpleSAML\Metadata\SAMLParser[] | ||
| 379 | * @throws \Exception | ||
| 380 | */ | ||
| 381 | private function loadXML(string $data, array $source): array | ||
| 382 |     { | ||
| 383 |         try { | ||
| 384 | $doc = \SAML2\DOMDocumentFactory::fromString($data); | ||
| 385 |         } catch (\Exception $e) { | ||
| 386 |             throw new \Exception('Failed to read XML from ' . $source['src']); | ||
| 387 | } | ||
| 388 | return \SimpleSAML\Metadata\SAMLParser::parseDescriptorsElement($doc->documentElement); | ||
| 389 | } | ||
| 390 | |||
| 391 | |||
| 392 | /** | ||
| 393 | * This function writes the state array back to disk | ||
| 394 | * | ||
| 395 | * @return void | ||
| 396 | */ | ||
| 397 | public function writeState(): void | ||
| 398 |     { | ||
| 399 |         if ($this->changed && !is_null($this->stateFile)) { | ||
| 400 |             Logger::debug('Writing: ' . $this->stateFile); | ||
| 401 | \SimpleSAML\Utils\System::writeFile( | ||
| 402 | $this->stateFile, | ||
| 403 | "<?php\n/* This file was generated by the metarefresh module at " . $this->getTime() . ".\n" . | ||
| 404 | " Do not update it manually as it will get overwritten. */\n" . | ||
| 405 | '$state = ' . var_export($this->state, true) . ";\n?>\n", | ||
| 406 | 0644 | ||
| 407 | ); | ||
| 408 | } | ||
| 409 | } | ||
| 410 | |||
| 411 | |||
| 412 | /** | ||
| 413 | * This function writes the metadata to stdout. | ||
| 414 | * | ||
| 415 | * @return void | ||
| 416 | */ | ||
| 417 | public function dumpMetadataStdOut(): void | ||
| 418 |     { | ||
| 419 |         foreach ($this->metadata as $category => $elements) { | ||
| 420 | echo '/* The following data should be added to metadata/' . $category . '.php. */' . "\n"; | ||
| 421 | |||
| 422 |             foreach ($elements as $m) { | ||
| 423 | $filename = $m['filename']; | ||
| 424 | $entityID = $m['metadata']['entityid']; | ||
| 425 | |||
| 426 | echo "\n"; | ||
| 427 | echo '/* The following metadata was generated from ' . $filename . ' on ' . $this->getTime() . '. */' . "\n"; | ||
| 428 | echo '$metadata[\'' . addslashes($entityID) . '\'] = ' . var_export($m['metadata'], true) . ';' . "\n"; | ||
| 429 | } | ||
| 430 | |||
| 431 | echo "\n"; | ||
| 432 | echo '/* End of data which should be added to metadata/' . $category . '.php. */' . "\n"; | ||
| 433 | echo "\n"; | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | |||
| 438 | /** | ||
| 439 | * This function adds metadata from the specified file to the list of metadata. | ||
| 440 | * This function will return without making any changes if $metadata is NULL. | ||
| 441 | * | ||
| 442 | * @param string $filename The filename the metadata comes from. | ||
| 443 | * @param \SAML2\XML\md\AttributeAuthorityDescriptor[]|null $metadata The metadata. | ||
| 444 | * @param string $type The metadata type. | ||
| 445 | * @param array|null $template The template. | ||
| 446 | * @return void | ||
| 447 | */ | ||
| 448 | private function addMetadata(string $filename, ?array $metadata, string $type, array $template = null): void | ||
| 449 |     { | ||
| 450 |         if ($metadata === null) { | ||
| 451 | return; | ||
| 452 | } | ||
| 453 | |||
| 454 |         if (isset($template)) { | ||
| 455 | $metadata = array_merge($metadata, $template); | ||
| 456 | } | ||
| 457 | |||
| 458 | $metadata['metarefresh:src'] = $filename; | ||
| 459 |         if (!array_key_exists($type, $this->metadata)) { | ||
| 460 | $this->metadata[$type] = []; | ||
| 461 | } | ||
| 462 | |||
| 463 | // If expire is defined in constructor... | ||
| 464 |         if (!empty($this->expire)) { | ||
| 465 | // If expire is already in metadata | ||
| 466 |             if (array_key_exists('expire', $metadata)) { | ||
| 467 | // Override metadata expire with more restrictive global config | ||
| 468 |                 if ($this->expire < $metadata['expire']) { | ||
| 469 | $metadata['expire'] = $this->expire; | ||
| 470 | } | ||
| 471 | |||
| 472 | // If expire is not already in metadata use global config | ||
| 473 |             } else { | ||
| 474 | $metadata['expire'] = $this->expire; | ||
| 475 | } | ||
| 476 | } | ||
| 477 | $this->metadata[$type][] = ['filename' => $filename, 'metadata' => $metadata]; | ||
| 478 | } | ||
| 479 | |||
| 480 | |||
| 481 | /** | ||
| 482 | * This function writes the metadata to an ARP file | ||
| 483 | * | ||
| 484 | * @param \SimpleSAML\Configuration $config | ||
| 485 | * @return void | ||
| 486 | */ | ||
| 487 | public function writeARPfile(Configuration $config): void | ||
| 488 |     { | ||
| 489 |         $arpfile = $config->getValue('arpfile'); | ||
| 490 | $types = ['saml20-sp-remote']; | ||
| 491 | |||
| 492 | $md = []; | ||
| 493 |         foreach ($this->metadata as $category => $elements) { | ||
| 494 |             if (!in_array($category, $types, true)) { | ||
| 495 | continue; | ||
| 496 | } | ||
| 497 | $md = array_merge($md, $elements); | ||
| 498 | } | ||
| 499 | |||
| 500 | // $metadata, $attributemap, $prefix, $suffix | ||
| 501 | $arp = new \SimpleSAML\Module\metarefresh\ARP( | ||
| 502 | $md, | ||
| 503 |             $config->getValue('attributemap', ''), | ||
| 504 |             $config->getValue('prefix', ''), | ||
| 505 |             $config->getValue('suffix', '') | ||
| 506 | ); | ||
| 507 | |||
| 508 | |||
| 509 | $arpxml = $arp->getXML(); | ||
| 510 | |||
| 511 |         Logger::info('Writing ARP file: ' . $arpfile . "\n"); | ||
| 512 | file_put_contents($arpfile, $arpxml); | ||
| 513 | } | ||
| 514 | |||
| 515 | |||
| 516 | /** | ||
| 517 | * This function writes the metadata to to separate files in the output directory. | ||
| 518 | * | ||
| 519 | * @param string $outputDir | ||
| 520 | * @return void | ||
| 521 | */ | ||
| 522 | public function writeMetadataFiles(string $outputDir): void | ||
| 560 | } | ||
| 561 | } | ||
| 562 | } | ||
| 563 | } | ||
| 564 | |||
| 565 | |||
| 566 | /** | ||
| 567 | * Save metadata for loading with the 'serialize' metadata loader. | ||
| 568 | * | ||
| 569 | * @param string $outputDir The directory we should save the metadata to. | ||
| 570 | * @return void | ||
| 571 | */ | ||
| 572 | public function writeMetadataSerialize(string $outputDir): void | ||
| 610 | } | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | |||
| 615 | /** | ||
| 616 | * @return string | ||
| 617 | */ | ||
| 618 | private function getTime(): string | ||
| 625 |