$args); endif; $args = wp_parse_args($args, array( "path" => "", )); $this->setPath($args['path']); } /* SyndicatedPostXPathQuery::__construct() */ /** * SyndicatedPostXPathQuery::getPath * * @param array $args * @return mixed */ public function getPath ($args = array()) { $args = wp_parse_args($args, array( "parsed" => false, )); return ($args['parsed'] ? $this->parsedPath : $this->path); } /* SyndicatedPostXPathQuery::getPath () */ /** * SyndicatedPostXPathQuery::setPath * * @param string $path */ public function setPath ($path) { $this->urlHash = array(); $this->path = $path; // Allow {url} notation for namespaces. URLs will contain : and /, so... preg_match_all('/{([^}]+)}/', $path, $match, PREG_SET_ORDER); foreach ($match as $ref) : $this->urlHash[md5($ref[1])] = $ref[1]; endforeach; foreach ($this->urlHash as $hash => $url) : $path = str_replace('{'.$url.'}', '{#'.$hash.'}', $path); endforeach; $path = $this->parsePath(/*cur=*/ $path, /*orig=*/ $path); $this->parsedPath = $path; } /* SyndicatedPostXPathQuery::setPath() */ /** * SyndicatedPostXPathQuery::snipSlug * * @return string */ protected function snipSlug ($path, $start, $n) { $slug = substr($path, $start, ($n-$start)); if (strlen($slug) > 0) : if (preg_match('/{#([^}]+)}/', $slug, $ref)) : if (isset($this->urlHash[$ref[1]])) : $slug = str_replace( '{#'.$ref[1].'}', '{'.$this->urlHash[$ref[1]].'}', $slug ); endif; endif; endif; return $slug; } /* SyndicatedPostXPathQuery::snipSlug () */ /** * SyndicatedPostXPathQuery::parsePath () * * @param mixed $path * @param mixed $rootPath * @return array|object */ public function parsePath ($path, $rootPath) { if (is_array($path)) : // This looks like it's already been parsed. $pp = $path; else : $pp = array(); // Okay let's parse this thing. $n = 0; $start = 0; $state = 'slug'; while ($state != '$') : switch ($state) : case 'slash' : $slug = $this->snipSlug($path, $start, $n); if (strlen($slug) > 0) : $pp[] = $slug; endif; $n++; // don't include the slash in our next slug $start = $n; $state = (($n < strlen($path)) ? 'slug' : '$'); break; case 'brackets' : // first, snip off what we've consumed so far $slug = $this->snipSlug($path, $start, $n); if (strlen($slug) > 0) : $pp[] = $slug; endif; // now, chase the ] $depth = 1; $n++; $start = $n; // find the end of the [square-bracketed] expression while ($depth > 0 and $n != '') : $tok = ((strlen($path) > $n) ? $path[$n] : ''); switch ($tok) : case '' : // ERROR STATE: syntax error $depth = -1; $state = 'syntax-error'; break; case '[' : $depth++; break; case ']' : $depth--; break; default : // NOOP endswitch; $n++; endwhile; if ($state != 'syntax-error') : $bracketed = substr($path, $start, ($n-$start)-1); // recursive parsing $oFilter = new stdClass; $oFilter->verb = 'has'; $oFilter->query = $this->parsePath($bracketed, $rootPath); $pp[] = $oFilter; $start = $n; $state = 'slash-expected'; endif; break; case 'slash-expected' : $tok = ((strlen($path) > $n) ? $path[$n] : ''); if ($tok == '/' or $tok == '') : $state = 'slash'; else : $state = 'syntax-error'; endif; break; case 'syntax-error' : $pp = new WP_Error('xpath', __("Syntax error", "feedwordpress")); $state = '$'; break; case 'slug' : default : $tok = ((strlen($path) > $n) ? $path[$n] : ''); switch ($tok) : case '' : case '/' : $state = 'slash'; break; case '[' : $state = 'brackets'; break; default : $n++; endswitch; endswitch; endwhile; endif; return $pp; } /* SyndicatedPostXPathQuery::parsePath() */ /** * SyndicatedPostXPathQuery::match * * @param string $path * @return array */ public function match ($r = array()) { $path = $this->parsedPath; $r = wp_parse_args($r, array( "type" => SIMPLEPIE_TYPE_ATOM_10, "xmlns" => array(), "map" => array(), "context" => array(), "parent" => array(), "format" => "string", )); $this->feed_type = $r['type']; $this->xmlns = $r['xmlns']; // Start out with a get_item_tags query. $node = ''; while (strlen($node)==0 and !is_null($node)) : $node = array_shift($path); endwhile; if (is_string($node) and isset($r['map'][$node])) : $data = $r['map'][$node]; $node = array_shift($path); else : $data = $r['map']['/']; endif; $matches = $data; while ( !is_null($node)) : if (is_object($node) OR strlen($node) > 0) : list($axis, $element) = $this->xpath_name_and_axis($node); if ('self'==$axis) : if (is_object($element) and property_exists($element, 'verb')) : $subq = new self(array("path" => $element->query)); $result = $subq->match(array( "type" => $r['type'], "xmlns" => $r['xmlns'], "map" => array( "/" => $matches, ), "context" => $matches, "parent" => $r['parent'], "format" => "object", )); // when format = 'object' we should get back // a sparse array of arrays, with indices = indices // from the input array, each element = an array of // one or more matching elements if ($element->verb = 'has' and is_array($result)) : $results = array(); foreach (array_keys($result) as $a) : $results[$a] = $matches[$a]; endforeach; $matches = $results; $data = $matches; endif; elseif (is_numeric($node)) : // according to W3C, sequence starts at position 1, not 0 // so subtract 1 to line up with PHP array starting at 0 $idx = intval($element) - 1; if (isset($matches[$idx])) : $data = array($idx => $matches[$idx]); else : $data = array(); endif; $matches = array($idx => $data); endif; else : $matches = array(); foreach ($data as $idx => $datum) : if ( !is_string($datum) and isset($datum[$axis])) : foreach ($datum[$axis] as $ns => $elements) : if (isset($elements[$element])) : // Potential match. // Check namespace. if (is_string($elements[$element])) : // Attribute $addenda = array($elements[$element]); $contexts = array($datum); // Element else : $addenda = $elements[$element]; $contexts = $elements[$element]; endif; foreach ($addenda as $index => $addendum) : $context = $contexts[$index]; $namespaces = $this->xpath_possible_namespaces($node, $context); if (in_array($ns, $namespaces)) : $matches[] = $addendum; endif; endforeach; endif; endforeach; endif; endforeach; $data = $matches; endif; endif; $node = array_shift($path); endwhile; $matches = array(); foreach ($data as $idx => $datum) : if ($r['format'] == 'string') : if (is_string($datum)) : $matches[] = $datum; elseif (isset($datum['data'])) : $matches[] = $datum['data']; endif; else : $matches[$idx] = $datum; endif; endforeach; return $matches; } /* SyndicatedPostXPathQuery::match() */ public function xpath_default_namespace () { // Get the default namespace. $type = $this->feed_type; if ($type & SIMPLEPIE_TYPE_ATOM_10) : $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_10; elseif ($type & SIMPLEPIE_TYPE_ATOM_03) : $defaultNS = SIMPLEPIE_NAMESPACE_ATOM_03; elseif ($type & SIMPLEPIE_TYPE_RSS_090) : $defaultNS = SIMPLEPIE_NAMESPACE_RSS_090; elseif ($type & SIMPLEPIE_TYPE_RSS_10) : $defaultNS = SIMPLEPIE_NAMESPACE_RSS_10; elseif ($type & SIMPLEPIE_TYPE_RSS_20) : $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20; else : $defaultNS = SIMPLEPIE_NAMESPACE_RSS_20; endif; return $defaultNS; } /* SyndicatedPostXPathQuery::xpath_default_namespace() */ public function xpath_name_and_axis ($node) { // $ns = NULL; // Was unused. (gwyneth 20230920) $element = NULL; $axis = 'child'; // "In effect, `child` is the default axis." if (is_object($node) and property_exists($node, 'verb')): if ('has'==$node->verb) : $axis = 'self'; endif; elseif (strpos($node, '::') !== false) : list($axis, $node) = explode("::", $node, 2); if ($axis=='attribute') : $axis = 'attribs'; // map from W3C to SimplePie's idiosyncratic notation endif; elseif (substr($node, 0, 1)=='@') : $axis = 'attribs'; $node = substr($node, 1); elseif (is_numeric($node)) : $axis = 'self'; elseif (substr($node, 0, 1)=='/') : // FIXME: properly, we should check for // and if we have it, // treat that as short for /descendent-or-self::node()/ $axis = 'child'; $node = substr($node, 1); else : // NOOP endif; if (is_string($node) and preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) : $element = $ref[2]; elseif (is_string($node) and strpos($node, ':') !== FALSE) : list($xmlns, $element) = explode(':', $node, 2); else : $element = $node; endif; return array($axis, $element); } /* SyndicatedPostXPathQuery::xpath_name_and_axis () */ public function xpath_possible_namespaces ($node, $datum = array()) { $ns = NULL; // $element = NULL; // reverse of before: now it's the $element that's never used (gwyneth 20230920) // Is $attr really the correct thing to assign to? It's never used... (gwyneth 20230920) if (substr($node, 0, 1)=='@') : $attr = '@'; $node = substr($node, 1); else : $attr = ''; endif; if (preg_match('/^{([^}]*)}(.*)$/', $node, $ref)) : $ns = array($ref[1]); elseif (strpos($node, ':') !== FALSE) : list($xmlns, $element) = explode(':', $node, 2); if (isset($this->xmlns['reverse'][$xmlns])) : $ns = $this->xmlns['reverse'][$xmlns]; else : $ns = array($xmlns); endif; // Fucking SimplePie. For attributes in default xmlns. $defaultNS = $this->xpath_default_namespace(); if (isset($this->xmlns['forward'][$defaultNS]) and ($xmlns==$this->xmlns['forward'][$defaultNS])) : $ns[] = ''; endif; if (isset($datum['xmlns'])) : if (isset($datum['xmlns'][$xmlns])) : $ns[] = $datum['xmlns'][$xmlns]; endif; endif; else : // Often in SimplePie, the default namespace gets stored // as an empty string rather than a URL. $ns = array($this->xpath_default_namespace(), ''); endif; return array_unique($ns); } /* SyndicatedPostXPathQuery::xpath_possible_namespaces() */ } /* class SyndicatedPostXPathQuery */ // When called directly, run through and perform some tests. if (basename($_SERVER['SCRIPT_FILENAME'])==basename(__FILE__)) : # some day when I am a grown-up developer I might include # some test cases in this here section # we need to implement wp_parse_args(), __(), and class WP_Error ... #function wp_parse_args ($r, $defaults) { # return array_merge($defaults, $r); #} #function __($text, $domain) { # return $text; #} #class WP_Error { # public function __construct ( $slug, $message ) { # /*DBG*/ echo $slug; # /*DBG*/ echo ": "; # /*DBG*/ echo $message; # } #} # #header("Content-type: text/plain"); # #$spxq = new SyndicatedPostXPathQuery(array("path" => $_REQUEST['p'])); # #var_dump($spxq); endif;