Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
100.00% |
1 / 1 |
|
100.00% |
18 / 18 |
CRAP | |
100.00% |
229 / 229 |
| MentionClient | |
100.00% |
1 / 1 |
|
100.00% |
24 / 24 |
95 | |
100.00% |
229 / 229 |
| setProxy | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
| discoverPingbackEndpoint | |
100.00% |
1 / 1 |
9 | |
100.00% |
36 / 36 |
|||
| sendPingbackToEndpoint | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
| sendPingback | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
| _parseBody | |
100.00% |
1 / 1 |
3 | |
100.00% |
6 / 6 |
|||
| _findWebmentionEndpointInHTML | |
100.00% |
1 / 1 |
6 | |
100.00% |
10 / 10 |
|||
| _findWebmentionEndpointInHeader | |
100.00% |
1 / 1 |
5 | |
100.00% |
8 / 8 |
|||
| discoverWebmentionEndpoint | |
100.00% |
1 / 1 |
15 | |
100.00% |
47 / 47 |
|||
| sendWebmentionToEndpoint | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
| sendWebmention | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
| findOutgoingLinks | |
100.00% |
1 / 1 |
7 | |
100.00% |
17 / 17 |
|||
| findLinksInText | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| findLinksInJSON | |
100.00% |
1 / 1 |
4 | |
100.00% |
6 / 6 |
|||
| sendMentions | |
100.00% |
1 / 1 |
4 | |
100.00% |
16 / 16 |
|||
| sendFirstSupportedMention | |
100.00% |
1 / 1 |
8 | |
100.00% |
16 / 16 |
|||
| enableDebug | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
| _debug | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
| _head | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
| _get | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
| _post | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
| _parse_headers | |
100.00% |
1 / 1 |
5 | |
100.00% |
6 / 6 |
|||
| anonymous function | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| xmlrpc_encode_request | |
100.00% |
1 / 1 |
2 | |
100.00% |
9 / 9 |
|||
| c | |
100.00% |
1 / 1 |
3 | |
100.00% |
8 / 8 |
|||
| <?php | |
| namespace IndieWeb; | |
| /** | |
| * Class MentionClient supports webmention, pingback and endpoint discovery. | |
| * @package IndieWeb | |
| */ | |
| class MentionClient { | |
| private static $_debugEnabled = false; | |
| private $_sourceBody; | |
| /** | |
| * @var array set of links to be checked for mentions. | |
| */ | |
| private $_links = array(); | |
| private $_headers = array(); | |
| private $_body = array(); | |
| private $_rels = array(); | |
| private $_supportsPingback = array(); | |
| private $_supportsWebmention = array(); | |
| private $_pingbackServer = array(); | |
| private $_webmentionServer = array(); | |
| private static $_proxy = false; | |
| public $usemf2 = true; // for testing, can set this to false to avoid using the Mf2 parser | |
| /** | |
| * @param string $proxy_string | |
| * @codeCoverageIgnore | |
| */ | |
| public function setProxy($proxy_string) { | |
| self::$_proxy = $proxy_string; | |
| } | |
| /** | |
| * Looks for pingback URL target. sets attributes on $this->c . | |
| * @param string $target URL | |
| * @return mixed setting $this->c('pingbackServer', $target); | |
| */ | |
| public function discoverPingbackEndpoint($target) { | |
| if($this->c('supportsPingback', $target) === null) { | |
| $this->c('supportsPingback', $target, false); | |
| // First try a HEAD request and look for X-Pingback header | |
| if(!$this->c('headers', $target)) { | |
| $head = static::_head($target); | |
| $target = $head['url']; | |
| $this->c('headers', $target, $head['headers']); | |
| } | |
| $headers = $this->c('headers', $target); | |
| if(array_key_exists('X-Pingback', $headers)) { | |
| self::_debug("discoverPingbackEndpoint: Found pingback server in header"); | |
| $this->c('pingbackServer', $target, $headers['X-Pingback']); | |
| $this->c('supportsPingback', $target, true); | |
| } else { | |
| self::_debug("discoverPingbackEndpoint: No pingback server found in header, looking in the body now"); | |
| if(!$this->c('body', $target)) { | |
| $body = static::_get($target); | |
| $target = $body['url']; | |
| $this->c('body', $target, $body['body']); | |
| $this->_parseBody($target, $body['body']); | |
| } | |
| if($rels=$this->c('rels', $target)) { | |
| // If the mf2 parser is present, then rels will have been set, and use that instead | |
| if(count($rels)) { | |
| if(array_key_exists('pingback', $rels)) { | |
| $this->c('pingbackServer', $target, $rels['pingback'][0]); | |
| $this->c('supportsPingback', $target, true); | |
| } | |
| } | |
| } else { | |
| $body = $this->c('body', $target); | |
| if(preg_match("/<link rel=\"pingback\" href=\"([^\"]+)\" ?\/?>/i", $body, $match)) { | |
| $this->c('pingbackServer', $target, $match[1]); | |
| $this->c('supportsPingback', $target, true); | |
| } | |
| } | |
| } | |
| self::_debug("discoverPingbackEndpoint: pingback server: " . $this->c('pingbackServer', $target)); | |
| } | |
| return $this->c('pingbackServer', $target); | |
| } | |
| /** | |
| * Sends pingback to endpoints | |
| * @param $endpoint string URL for pingback listener | |
| * @param $source string originating post URL | |
| * @param $target string URL like permalink of target post | |
| * @return bool Successful response MUST contain a single string | |
| */ | |
| public static function sendPingbackToEndpoint($endpoint, $source, $target) { | |
| self::_debug("sendPingbackToEndpoint: Sending pingback now!"); | |
| $payload = static::xmlrpc_encode_request('pingback.ping', array($source, $target)); | |
| $response = static::_post($endpoint, $payload, array( | |
| 'Content-type: application/xml' | |
| )); | |
| if($response['code'] != 200 || empty($response['body'])) | |
| return false; | |
| // collapse whitespace just to be safe | |
| $body = strtolower(preg_replace('/\s+/', '', $response['body'])); | |
| // successful response MUST contain a single string | |
| return $body && strpos($body, '<fault>') === false && strpos($body, '<string>') !== false; | |
| } | |
| /** | |
| * Public function to send pingbacks to $targetURL | |
| * @param $sourceURL string URL for source of pingback | |
| * @param $targetURL string URL for destination of pingback | |
| * @return bool runs sendPingbackToEndpoint(). | |
| * @see MentionClient::sendPingbackToEndpoint() | |
| */ | |
| public function sendPingback($sourceURL, $targetURL) { | |
| // If we haven't discovered the pingback endpoint yet, do it now | |
| if($this->c('supportsPingback', $targetURL) === null) { | |
| $this->discoverPingbackEndpoint($targetURL); | |
| } | |
| $pingbackServer = $this->c('pingbackServer', $targetURL); | |
| if($pingbackServer) { | |
| self::_debug("sendPingback: Sending to pingback server: " . $pingbackServer); | |
| return self::sendPingbackToEndpoint($pingbackServer, $sourceURL, $targetURL); | |
| } else { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Parses body of html. Protected method. | |
| * @param $target string the URL of the target page | |
| * @param $html string the HTML of page | |
| */ | |
| protected function _parseBody($target, $html) { | |
| if(class_exists('\Mf2\Parser') && $this->usemf2) { | |
| $parser = new \Mf2\Parser($html, $target); | |
| list($rels, $alternates) = $parser->parseRelsAndAlternates(); | |
| $this->c('rels', $target, $rels); | |
| } | |
| } | |
| /** | |
| * finds webmention endpoints in the body. protected function | |
| * @param $body | |
| * @param string $targetURL | |
| * @return bool | |
| */ | |
| protected function _findWebmentionEndpointInHTML($body, $targetURL = false) { | |
| $endpoint = false; | |
| $body = preg_replace('/<!--(.*)-->/Us', '', $body); | |
| if(preg_match('/<(?:link|a)[ ]+href="([^"]*)"[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]*\/?>/i', $body, $match) | |
| || preg_match('/<(?:link|a)[ ]+rel="[^" ]* ?webmention ?[^" ]*"[ ]+href="([^"]*)"[ ]*\/?>/i', $body, $match)) { | |
| $endpoint = $match[1]; | |
| } | |
| if($endpoint !== false && $targetURL && function_exists('\Mf2\resolveUrl')) { | |
| // Resolve the URL if it's relative | |
| $endpoint = \Mf2\resolveUrl($targetURL, $endpoint); | |
| } | |
| return $endpoint; | |
| } | |
| /** | |
| * @param $link_header | |
| * @param string $targetURL | |
| * @return bool | |
| */ | |
| protected function _findWebmentionEndpointInHeader($link_header, $targetURL = false) { | |
| $endpoint = false; | |
| if(preg_match('~<((?:https?://)?[^>]+)>; rel="?(?:https?://webmention.org/?|webmention)"?~', $link_header, $match)) { | |
| $endpoint = $match[1]; | |
| } | |
| if($endpoint && $targetURL && function_exists('\Mf2\resolveUrl')) { | |
| // Resolve the URL if it's relative | |
| $endpoint = \Mf2\resolveUrl($targetURL, $endpoint); | |
| } | |
| return $endpoint; | |
| } | |
| /** | |
| * Finds webmention endpoints at URL. Examines header request. | |
| * Also modifies $this->c to indicate if $target accepts webmention | |
| * @param $target string the URL to examine for endpoints. | |
| * @return mixed | |
| */ | |
| public function discoverWebmentionEndpoint($target) { | |
| if($this->c('supportsWebmention', $target) === null) { | |
| $this->c('supportsWebmention', $target, false); | |
| // First try a HEAD request and look for Link header | |
| if(!$this->c('headers', $target)) { | |
| $head = static::_head($target); | |
| $target = $head['url']; | |
| $this->c('headers', $target, $head['headers']); | |
| } | |
| $headers = $this->c('headers', $target); | |
| $link_header = false; | |
| if(array_key_exists('Link', $headers)) { | |
| if(is_array($headers['Link'])) { | |
| $link_header = implode($headers['Link'], ", "); | |
| } else { | |
| $link_header = $headers['Link']; | |
| } | |
| } | |
| if($link_header && ($endpoint=$this->_findWebmentionEndpointInHeader($link_header, $target))) { | |
| self::_debug("discoverWebmentionEndpoint: Found webmention server in header"); | |
| $this->c('webmentionServer', $target, $endpoint); | |
| $this->c('supportsWebmention', $target, true); | |
| } else { | |
| self::_debug("discoverWebmentionEndpoint: No webmention server found in header, looking in body now"); | |
| if(!$this->c('body', $target)) { | |
| $body = static::_get($target); | |
| $target = $body['url']; | |
| $this->c('body', $target, $body['body']); | |
| $this->_parseBody($target, $body['body']); | |
| } | |
| if($rels=$this->c('rels', $target)) { | |
| // If the mf2 parser is present, then rels will have been set, so use that instead | |
| if(count($rels)) { | |
| if(array_key_exists('webmention', $rels)) { | |
| $endpoint = $rels['webmention'][0]; | |
| $this->c('webmentionServer', $target, $endpoint); | |
| $this->c('supportsWebmention', $target, true); | |
| } elseif(array_key_exists('http://webmention.org/', $rels) || array_key_exists('http://webmention.org', $rels)) { | |
| $endpoint = $rels[array_key_exists('http://webmention.org/', $rels) ? 'http://webmention.org/' : 'http://webmention.org'][0]; | |
| $this->c('webmentionServer', $target, $endpoint); | |
| $this->c('supportsWebmention', $target, true); | |
| } | |
| } | |
| } else { | |
| if($endpoint=$this->_findWebmentionEndpointInHTML($this->c('body', $target), $target)) { | |
| $this->c('webmentionServer', $target, $endpoint); | |
| $this->c('supportsWebmention', $target, true); | |
| } | |
| } | |
| } | |
| self::_debug("discoverWebmentionEndpoint: webmention server: " . $this->c('webmentionServer', $target)); | |
| } | |
| return $this->c('webmentionServer', $target); | |
| } | |
| /** | |
| * Static function can send a webmention to an endpoint via static::_post | |
| * @param $endpoint string URL of endpoint detected | |
| * @param $source string URL of originating post (other server will check probably) | |
| * @param $target string URL of target post | |
| * @param array $additional extra optional stuff that will be included in payload. | |
| * @return array | |
| */ | |
| public static function sendWebmentionToEndpoint($endpoint, $source, $target, $additional = array()) { | |
| self::_debug("sendWebmentionToEndpoint: Sending webmention now!"); | |
| $payload = http_build_query(array_merge(array( | |
| 'source' => $source, | |
| 'target' => $target | |
| ), $additional)); | |
| return static::_post($endpoint, $payload, array( | |
| 'Content-type: application/x-www-form-urlencoded', | |
| 'Accept: application/json' | |
| )); | |
| } | |
| /** | |
| * Sends webmention to a target url. may use | |
| * @param $sourceURL | |
| * @param $targetURL | |
| * @param array $additional | |
| * @return array|bool | |
| * @see MentionClient::sendWebmentionToEndpoint() | |
| */ | |
| public function sendWebmention($sourceURL, $targetURL, $additional = array()) { | |
| // If we haven't discovered the webmention endpoint yet, do it now | |
| if($this->c('supportsWebmention', $targetURL) === null) { | |
| $this->discoverWebmentionEndpoint($targetURL); | |
| } | |
| $webmentionServer = $this->c('webmentionServer', $targetURL); | |
| if($webmentionServer) { | |
| self::_debug("sendWebmention: Sending to webmention server: " . $webmentionServer); | |
| return self::sendWebmentionToEndpoint($webmentionServer, $sourceURL, $targetURL, $additional); | |
| } else { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Scans outgoing links in block of text $input. | |
| * @param $input string html block. | |
| * @return array array of unique links or empty. | |
| */ | |
| public static function findOutgoingLinks($input) { | |
| // Find all outgoing links in the source | |
| if(is_string($input)) { | |
| preg_match_all("/<a[^>]+href=.(https?:\/\/[^'\"]+)/i", $input, $matches); | |
| return array_unique($matches[1]); | |
| } elseif (is_array($input) && array_key_exists('items', $input) && array_key_exists(0, $input['items'])) { | |
| $links = array(); | |
| // Find links in the content HTML | |
| $item = $input['items'][0]; | |
| if (array_key_exists('content', $item['properties'])) { | |
| if (is_array($item['properties']['content'][0])) { | |
| $html = $item['properties']['content'][0]['html']; | |
| $links = array_merge($links, self::findOutgoingLinks($html)); | |
| } else { | |
| $text = $item['properties']['content'][0]; | |
| $links = array_merge($links, self::findLinksInText($text)); | |
| } | |
| } | |
| // Look at all properties of the item and collect all the ones that look like URLs | |
| $links = array_merge($links, self::findLinksInJSON($item)); | |
| return array_unique($links); | |
| } else { | |
| return array(); | |
| } | |
| } | |
| /** | |
| * find all links in text. | |
| * @param $input string text block | |
| * @return mixed array of links in text block. | |
| */ | |
| public static function findLinksInText($input) { | |
| preg_match_all('/https?:\/\/[^ ]+/', $input, $matches); | |
| return array_unique($matches[0]); | |
| } | |
| /** | |
| * find links in JSON input string. | |
| * @param $input string JSON object. | |
| * @return array of links in JSON object. | |
| */ | |
| public static function findLinksInJSON($input) { | |
| $links = array(); | |
| // This recursively iterates over the whole input array and searches for | |
| // everything that looks like a URL regardless of its depth or property name | |
| foreach(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($input)) as $key => $value) { | |
| if(substr($value, 0, 7) == 'http://' || substr($value, 0, 8) == 'https://') | |
| $links[] = $value; | |
| } | |
| return $links; | |
| } | |
| /** | |
| * Tries to send webmention and pingbacks to each link on $sourceURL. Depends on Microformats2 | |
| * @param $sourceURL string URL to examine to send mentions to | |
| * @param bool $sourceBody if true will search for outgoing links with this (string). | |
| * @return int | |
| * @see \Mf2\parse | |
| */ | |
| public function sendMentions($sourceURL, $sourceBody = false) { | |
| if($sourceBody) { | |
| $this->_sourceBody = $sourceBody; | |
| $this->_links = self::findOutgoingLinks($sourceBody); | |
| } else { | |
| $body = static::_get($sourceURL); | |
| $this->_sourceBody = $body['body']; | |
| $parsed = \Mf2\parse($this->_sourceBody, $sourceURL); | |
| $this->_links = self::findOutgoingLinks($parsed); | |
| } | |
| $totalAccepted = 0; | |
| foreach($this->_links as $target) { | |
| self::_debug("sendMentions: Checking $target for webmention and pingback endpoints"); | |
| if($this->sendFirstSupportedMention($sourceURL, $target)) { | |
| $totalAccepted++; | |
| } | |
| } | |
| return $totalAccepted; | |
| } | |
| /** | |
| * @param $source | |
| * @param $target | |
| * @return bool|string | |
| */ | |
| public function sendFirstSupportedMention($source, $target) { | |
| $accepted = false; | |
| // Look for a webmention endpoint first | |
| if($this->discoverWebmentionEndpoint($target)) { | |
| $result = $this->sendWebmention($source, $target); | |
| if($result && | |
| ($result['code'] == 200 | |
| || $result['code'] == 201 | |
| || $result['code'] == 202)) { | |
| $accepted = 'webmention'; | |
| } | |
| // Only look for a pingback server if we didn't find a webmention server | |
| } else if($this->discoverPingbackEndpoint($target)) { | |
| $result = $this->sendPingback($source, $target); | |
| if($result) { | |
| $accepted = 'pingback'; | |
| } | |
| } | |
| return $accepted; | |
| } | |
| /** | |
| * Enables debug messages to appear during activity. Not recommended for production use. | |
| * @codeCoverageIgnore | |
| */ | |
| public static function enableDebug() { | |
| self::$_debugEnabled = true; | |
| } | |
| /** | |
| * @codeCoverageIgnore | |
| */ | |
| private static function _debug($msg) { | |
| if(self::$_debugEnabled) | |
| echo "\t" . $msg . "\n"; | |
| } | |
| /** | |
| * @param $url | |
| * @return array | |
| * @codeCoverageIgnore | |
| */ | |
| protected static function _head($url) { | |
| $ch = curl_init($url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_HEADER, true); | |
| curl_setopt($ch, CURLOPT_NOBODY, true); | |
| curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
| if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); | |
| $response = curl_exec($ch); | |
| return array( | |
| 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), | |
| 'headers' => self::_parse_headers(trim($response)), | |
| 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) | |
| ); | |
| } | |
| /** | |
| * Protected static function | |
| * @param $url string URL to grab through curl. | |
| * @return array with keys 'code' 'headers' and 'body' | |
| * @codeCoverageIgnore | |
| */ | |
| protected static function _get($url) { | |
| $ch = curl_init($url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_HEADER, true); | |
| curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
| if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); | |
| $response = curl_exec($ch); | |
| $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); | |
| return array( | |
| 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), | |
| 'headers' => self::_parse_headers(trim(substr($response, 0, $header_size))), | |
| 'body' => substr($response, $header_size), | |
| 'url' => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) | |
| ); | |
| } | |
| /** | |
| * @param $url | |
| * @param $body | |
| * @param array $headers | |
| * @return array | |
| * @codeCoverageIgnore | |
| */ | |
| protected static function _post($url, $body, $headers=array()) { | |
| $ch = curl_init($url); | |
| curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
| curl_setopt($ch, CURLOPT_POST, true); | |
| curl_setopt($ch, CURLOPT_POSTFIELDS, $body); | |
| curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
| curl_setopt($ch, CURLOPT_HEADER, true); | |
| if (self::$_proxy) curl_setopt($ch, CURLOPT_PROXY, self::$_proxy); | |
| $response = curl_exec($ch); | |
| self::_debug($response); | |
| $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); | |
| return array( | |
| 'code' => curl_getinfo($ch, CURLINFO_HTTP_CODE), | |
| 'headers' => self::_parse_headers(trim(substr($response, 0, $header_size))), | |
| 'body' => substr($response, $header_size) | |
| ); | |
| } | |
| /** | |
| * Protected static function to parse headers. | |
| * @param $headers | |
| * @return array | |
| */ | |
| protected static function _parse_headers($headers) { | |
| $retVal = array(); | |
| $fields = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $headers)); | |
| foreach($fields as $field) { | |
| if (preg_match('/([^:]+): (.+)/m', $field, $match)) { | |
| $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { | |
| return strtoupper($m[0]); | |
| }, strtolower(trim($match[1]))); | |
| // If there's already a value set for the header name being returned, turn it into an array and add the new value | |
| $match[1] = preg_replace_callback('/(?<=^|[\x09\x20\x2D])./', function($m) { | |
| return strtoupper($m[0]); | |
| }, strtolower(trim($match[1]))); | |
| if (isset($retVal[$match[1]])) { | |
| if (!is_array($retVal[$match[1]])) | |
| $retVal[$match[1]] = array($retVal[$match[1]]); | |
| $retVal[$match[1]][] = $match[2]; | |
| } else { | |
| $retVal[$match[1]] = trim($match[2]); | |
| } | |
| } | |
| } | |
| return $retVal; | |
| } | |
| /** | |
| * Static function for XML-RPC encoding request. | |
| * @param $method string goes into MethodName XML tag | |
| * @param $params array set of strings that go into param/value XML tags. | |
| * @return string | |
| */ | |
| public static function xmlrpc_encode_request($method, $params) { | |
| $xml = '<?xml version="1.0"?>'; | |
| $xml .= '<methodCall>'; | |
| $xml .= '<methodName>'.htmlspecialchars($method).'</methodName>'; | |
| $xml .= '<params>'; | |
| foreach ($params as $param) { | |
| $xml .= '<param><value><string>'.htmlspecialchars($param).'</string></value></param>'; | |
| } | |
| $xml .= '</params></methodCall>'; | |
| return $xml; | |
| } | |
| /** | |
| * Caching key/value system for MentionClient | |
| * @param $type | |
| * @param $url | |
| * @param mixed $val If not null, is set to default value | |
| * @return mixed | |
| */ | |
| public function c($type, $url, $val=null) { | |
| // Create the empty record if it doesn't yet exist | |
| $key = '_'.$type; | |
| if(!array_key_exists($url, $this->{$key})) { | |
| $this->{$key}[$url] = null; | |
| } | |
| if($val !== null) { | |
| $this->{$key}[$url] = $val; | |
| } | |
| return $this->{$key}[$url]; | |
| } | |
| } |