/jsonld.php
http://github.com/digitalbazaar/php-json-ld · PHP · 6038 lines · 3804 code · 546 blank · 1688 comment · 887 complexity · d9edef1298ff26620ddd5bd83d40cc96 MD5 · raw file
Large files are truncated click here to view the full file
- <?php
- /**
- * PHP implementation of the JSON-LD API.
- * Version: 0.4.8-dev
- *
- * @author Dave Longley
- *
- * BSD 3-Clause License
- * Copyright (c) 2011-2014 Digital Bazaar, Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- *
- * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * Neither the name of the Digital Bazaar, Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
- * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
- /**
- * Performs JSON-LD compaction.
- *
- * @param mixed $input the JSON-LD object to compact.
- * @param mixed $ctx the context to compact with.
- * @param assoc [$options] options to use:
- * [base] the base IRI to use.
- * [graph] true to always output a top-level graph (default: false).
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the compacted JSON-LD output.
- */
- function jsonld_compact($input, $ctx, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->compact($input, $ctx, $options);
- }
- /**
- * Performs JSON-LD expansion.
- *
- * @param mixed $input the JSON-LD object to expand.
- * @param assoc[$options] the options to use:
- * [base] the base IRI to use.
- * [documentLoader(url)] the document loader.
- *
- * @return array the expanded JSON-LD output.
- */
- function jsonld_expand($input, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->expand($input, $options);
- }
- /**
- * Performs JSON-LD flattening.
- *
- * @param mixed $input the JSON-LD to flatten.
- * @param mixed $ctx the context to use to compact the flattened output, or
- * null.
- * @param [options] the options to use:
- * [base] the base IRI to use.
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the flattened JSON-LD output.
- */
- function jsonld_flatten($input, $ctx, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->flatten($input, $ctx, $options);
- }
- /**
- * Performs JSON-LD framing.
- *
- * @param mixed $input the JSON-LD object to frame.
- * @param stdClass $frame the JSON-LD frame to use.
- * @param assoc [$options] the framing options.
- * [base] the base IRI to use.
- * [embed] default @embed flag (default: true).
- * [explicit] default @explicit flag (default: false).
- * [requireAll] default @requireAll flag (default: true).
- * [omitDefault] default @omitDefault flag (default: false).
- * [documentLoader(url)] the document loader.
- *
- * @return stdClass the framed JSON-LD output.
- */
- function jsonld_frame($input, $frame, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->frame($input, $frame, $options);
- }
- /**
- * **Experimental**
- *
- * Links a JSON-LD document's nodes in memory.
- *
- * @param mixed $input the JSON-LD document to link.
- * @param mixed $ctx the JSON-LD context to apply or null.
- * @param assoc [$options] the options to use:
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [documentLoader(url)] the document loader.
- *
- * @return the linked JSON-LD output.
- */
- function jsonld_link($input, $ctx, $options) {
- // API matches running frame with a wildcard frame and embed: '@link'
- // get arguments
- $frame = new stdClass();
- if($ctx) {
- $frame->{'@context'} = $ctx;
- }
- $frame->{'@embed'} = '@link';
- return jsonld_frame($input, $frame, $options);
- };
- /**
- * Performs RDF dataset normalization on the given input. The input is
- * JSON-LD unless the 'inputFormat' option is used. The output is an RDF
- * dataset unless the 'format' option is used.
- *
- * @param mixed $input the JSON-LD object to normalize.
- * @param assoc [$options] the options to use:
- * [base] the base IRI to use.
- * [intputFormat] the format if input is not JSON-LD:
- * 'application/nquads' for N-Quads.
- * [format] the format if output is a string:
- * 'application/nquads' for N-Quads.
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the normalized output.
- */
- function jsonld_normalize($input, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->normalize($input, $options);
- }
- /**
- * Converts an RDF dataset to JSON-LD.
- *
- * @param mixed $input a serialized string of RDF in a format specified
- * by the format option or an RDF dataset to convert.
- * @param assoc [$options] the options to use:
- * [format] the format if input not an array:
- * 'application/nquads' for N-Quads (default).
- * [useRdfType] true to use rdf:type, false to use @type
- * (default: false).
- * [useNativeTypes] true to convert XSD types into native types
- * (boolean, integer, double), false not to (default: false).
- *
- * @return array the JSON-LD output.
- */
- function jsonld_from_rdf($input, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->fromRDF($input, $options);
- }
- /**
- * Outputs the RDF dataset found in the given JSON-LD object.
- *
- * @param mixed $input the JSON-LD object.
- * @param assoc [$options] the options to use:
- * [base] the base IRI to use.
- * [format] the format to use to output a string:
- * 'application/nquads' for N-Quads.
- * [produceGeneralizedRdf] true to output generalized RDF, false
- * to produce only standard RDF (default: false).
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the resulting RDF dataset (or a serialization of it).
- */
- function jsonld_to_rdf($input, $options=array()) {
- $p = new JsonLdProcessor();
- return $p->toRDF($input, $options);
- }
- /**
- * JSON-encodes (with unescaped slashes) the given stdClass or array.
- *
- * @param mixed $input the native PHP stdClass or array which will be
- * converted to JSON by json_encode().
- * @param int $options the options to use.
- * [JSON_PRETTY_PRINT] pretty print.
- * @param int $depth the maximum depth to use.
- *
- * @return the encoded JSON data.
- */
- function jsonld_encode($input, $options=0, $depth=512) {
- // newer PHP has a flag to avoid escaped '/'
- if(defined('JSON_UNESCAPED_SLASHES')) {
- return json_encode($input, JSON_UNESCAPED_SLASHES | $options, $depth);
- }
- // use a simple string replacement of '\/' to '/'.
- return str_replace('\\/', '/', json_encode($input, $options, $depth));
- }
- /**
- * Decodes a serialized JSON-LD object.
- *
- * @param string $input the JSON-LD input.
- *
- * @return mixed the resolved JSON-LD object, null on error.
- */
- function jsonld_decode($input) {
- return json_decode($input);
- }
- /**
- * Parses a link header. The results will be key'd by the value of "rel".
- *
- * Link: <http://json-ld.org/contexts/person.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
- *
- * Parses as: {
- * 'http://www.w3.org/ns/json-ld#context': {
- * target: http://json-ld.org/contexts/person.jsonld,
- * type: 'application/ld+json'
- * }
- * }
- *
- * If there is more than one "rel" with the same IRI, then entries in the
- * resulting map for that "rel" will be arrays of objects, otherwise they will
- * be single objects.
- *
- * @param string $header the link header to parse.
- *
- * @return assoc the parsed result.
- */
- function jsonld_parse_link_header($header) {
- $rval = array();
- // split on unbracketed/unquoted commas
- if(!preg_match_all(
- '/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) {
- return $rval;
- }
- $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/';
- foreach($entries as $entry) {
- if(!preg_match($r_link_header, $entry[0], $match)) {
- continue;
- }
- $result = (object)array('target' => $match[1]);
- $params = $match[2];
- $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/';
- preg_match_all($r_params, $params, $matches, PREG_SET_ORDER);
- foreach($matches as $match) {
- $result->{$match[1]} = $match[2] ?: $match[3];
- }
- $rel = property_exists($result, 'rel') ? $result->rel : '';
- if(!isset($rval[$rel])) {
- $rval[$rel] = $result;
- } else if(is_array($rval[$rel])) {
- $rval[$rel][] = $result;
- } else {
- $rval[$rel] = array($rval[$rel], $result);
- }
- }
- return $rval;
- }
- /**
- * Relabels all blank nodes in the given JSON-LD input.
- *
- * @param mixed input the JSON-LD input.
- */
- function jsonld_relabel_blank_nodes($input) {
- $p = new JsonLdProcessor();
- return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input);
- }
- /** JSON-LD shared in-memory cache. */
- global $jsonld_cache;
- $jsonld_cache = new stdClass();
- /** The default active context cache. */
- $jsonld_cache->activeCtx = new ActiveContextCache();
- /** Stores the default JSON-LD document loader. */
- global $jsonld_default_load_document;
- $jsonld_default_load_document = 'jsonld_default_document_loader';
- /**
- * Sets the default JSON-LD document loader.
- *
- * @param callable load_document(url) the document loader.
- */
- function jsonld_set_document_loader($load_document) {
- global $jsonld_default_load_document;
- $jsonld_default_load_document = $load_document;
- }
- /**
- * Retrieves JSON-LD at the given URL.
- *
- * @param string $url the URL to retrieve.
- *
- * @return the JSON-LD.
- */
- function jsonld_get_url($url) {
- global $jsonld_default_load_document;
- if($jsonld_default_load_document !== null) {
- $document_loader = $jsonld_default_load_document;
- } else {
- $document_loader = 'jsonld_default_document_loader';
- }
- $remote_doc = call_user_func($document_loader, $url);
- if($remote_doc) {
- return $remote_doc->document;
- }
- return null;
- }
- /**
- * The default implementation to retrieve JSON-LD at the given URL.
- *
- * @param string $url the URL to to retrieve.
- *
- * @return stdClass the RemoteDocument object.
- */
- function jsonld_default_document_loader($url) {
- $doc = (object)array(
- 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
- $redirects = array();
- $opts = array(
- 'http' => array(
- 'method' => 'GET',
- 'header' =>
- "Accept: application/ld+json\r\n"),
- /* Note: Use jsonld_default_secure_document_loader for security. */
- 'ssl' => array(
- 'verify_peer' => false,
- 'allow_self_signed' => true)
- );
- $context = stream_context_create($opts);
- $content_type = null;
- stream_context_set_params($context, array('notification' =>
- function($notification_code, $severity, $message) use (
- &$redirects, &$content_type) {
- switch($notification_code) {
- case STREAM_NOTIFY_REDIRECTED:
- $redirects[] = $message;
- break;
- case STREAM_NOTIFY_MIME_TYPE_IS:
- $content_type = $message;
- break;
- };
- }));
- $result = @file_get_contents($url, false, $context);
- if($result === false) {
- throw new JsonLdException(
- 'Could not retrieve a JSON-LD document from the URL: ' . $url,
- 'jsonld.LoadDocumentError', 'loading document failed');
- }
- $link_header = array();
- foreach($http_response_header as $header) {
- if(strpos($header, 'link') === 0) {
- $value = explode(': ', $header);
- if(count($value) > 1) {
- $link_header[] = $value[1];
- }
- }
- }
- $link_header = jsonld_parse_link_header(join(',', $link_header));
- if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
- $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
- } else {
- $link_header = null;
- }
- if($link_header && $content_type !== 'application/ld+json') {
- // only 1 related link header permitted
- if(is_array($link_header)) {
- throw new JsonLdException(
- 'URL could not be dereferenced, it has more than one ' .
- 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
- 'multiple context link headers', array('url' => $url));
- }
- $doc->{'contextUrl'} = $link_header->target;
- }
- // update document url based on redirects
- $redirs = count($redirects);
- if($redirs > 0) {
- $url = $redirects[$redirs - 1];
- }
- $doc->document = $result;
- $doc->documentUrl = $url;
- return $doc;
- }
- /**
- * The default implementation to retrieve JSON-LD at the given secure URL.
- *
- * @param string $url the secure URL to to retrieve.
- *
- * @return stdClass the RemoteDocument object.
- */
- function jsonld_default_secure_document_loader($url) {
- if(strpos($url, 'https') !== 0) {
- throw new JsonLdException(
- "Could not GET url: '$url'; 'https' is required.",
- 'jsonld.LoadDocumentError', 'loading document failed');
- }
- $doc = (object)array(
- 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
- $redirects = array();
- // default JSON-LD https GET implementation
- $opts = array(
- 'http' => array(
- 'method' => 'GET',
- 'header' =>
- "Accept: application/ld+json\r\n"),
- 'ssl' => array(
- 'verify_peer' => true,
- 'allow_self_signed' => false,
- 'cafile' => '/etc/ssl/certs/ca-certificates.crt'));
- $context = stream_context_create($opts);
- $content_type = null;
- stream_context_set_params($context, array('notification' =>
- function($notification_code, $severity, $message) use (
- &$redirects, &$content_type) {
- switch($notification_code) {
- case STREAM_NOTIFY_REDIRECTED:
- $redirects[] = $message;
- break;
- case STREAM_NOTIFY_MIME_TYPE_IS:
- $content_type = $message;
- break;
- };
- }));
- $result = @file_get_contents($url, false, $context);
- if($result === false) {
- throw new JsonLdException(
- 'Could not retrieve a JSON-LD document from the URL: ' + $url,
- 'jsonld.LoadDocumentError', 'loading document failed');
- }
- $link_header = array();
- foreach($http_response_header as $header) {
- if(strpos($header, 'link') === 0) {
- $value = explode(': ', $header);
- if(count($value) > 1) {
- $link_header[] = $value[1];
- }
- }
- }
- $link_header = jsonld_parse_link_header(join(',', $link_header));
- if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
- $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
- } else {
- $link_header = null;
- }
- if($link_header && $content_type !== 'application/ld+json') {
- // only 1 related link header permitted
- if(is_array($link_header)) {
- throw new JsonLdException(
- 'URL could not be dereferenced, it has more than one ' .
- 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
- 'multiple context link headers', array('url' => $url));
- }
- $doc->{'contextUrl'} = $link_header->target;
- }
- // update document url based on redirects
- foreach($redirects as $redirect) {
- if(strpos($redirect, 'https') !== 0) {
- throw new JsonLdException(
- "Could not GET redirected url: '$redirect'; 'https' is required.",
- 'jsonld.LoadDocumentError', 'loading document failed');
- }
- $url = $redirect;
- }
- $doc->document = $result;
- $doc->documentUrl = $url;
- return $doc;
- }
- /** Registered global RDF dataset parsers hashed by content-type. */
- global $jsonld_rdf_parsers;
- $jsonld_rdf_parsers = new stdClass();
- /**
- * Registers a global RDF dataset parser by content-type, for use with
- * jsonld_from_rdf. Global parsers will be used by JsonLdProcessors that do
- * not register their own parsers.
- *
- * @param string $content_type the content-type for the parser.
- * @param callable $parser(input) the parser function (takes a string as
- * a parameter and returns an RDF dataset).
- */
- function jsonld_register_rdf_parser($content_type, $parser) {
- global $jsonld_rdf_parsers;
- $jsonld_rdf_parsers->{$content_type} = $parser;
- }
- /**
- * Unregisters a global RDF dataset parser by content-type.
- *
- * @param string $content_type the content-type for the parser.
- */
- function jsonld_unregister_rdf_parser($content_type) {
- global $jsonld_rdf_parsers;
- if(property_exists($jsonld_rdf_parsers, $content_type)) {
- unset($jsonld_rdf_parsers->{$content_type});
- }
- }
- /**
- * Parses a URL into its component parts.
- *
- * @param string $url the URL to parse.
- *
- * @return assoc the parsed URL.
- */
- function jsonld_parse_url($url) {
- if($url === null) {
- $url = '';
- }
- $keys = array(
- 'href', 'protocol', 'scheme', '?authority', 'authority',
- '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path',
- '?query', 'query', '?fragment', 'fragment');
- $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/";
- preg_match($regex, $url, $match);
- $rval = array();
- $flags = array();
- $len = count($keys);
- for($i = 0; $i < $len; ++$i) {
- $key = $keys[$i];
- if(strpos($key, '?') === 0) {
- $flags[substr($key, 1)] = !empty($match[$i]);
- } else if(!isset($match[$i])) {
- $rval[$key] = null;
- } else {
- $rval[$key] = $match[$i];
- }
- }
- if(!$flags['authority']) {
- $rval['authority'] = null;
- }
- if(!$flags['auth']) {
- $rval['auth'] = $rval['user'] = $rval['pass'] = null;
- }
- if(!$flags['port']) {
- $rval['port'] = null;
- }
- if(!$flags['query']) {
- $rval['query'] = null;
- }
- if(!$flags['fragment']) {
- $rval['fragment'] = null;
- }
- $rval['normalizedPath'] = jsonld_remove_dot_segments(
- $rval['path'], !!$rval['authority']);
- return $rval;
- }
- /**
- * Removes dot segments from a URL path.
- *
- * @param string $path the path to remove dot segments from.
- * @param bool $has_authority true if the URL has an authority, false if not.
- */
- function jsonld_remove_dot_segments($path, $has_authority) {
- $rval = '';
- if(strpos($path, '/') === 0) {
- $rval = '/';
- }
- // RFC 3986 5.2.4 (reworked)
- $input = explode('/', $path);
- $output = array();
- while(count($input) > 0) {
- if($input[0] === '.' || ($input[0] === '' && count($input) > 1)) {
- array_shift($input);
- continue;
- }
- if($input[0] === '..') {
- array_shift($input);
- if($has_authority ||
- (count($output) > 0 && $output[count($output) - 1] !== '..')) {
- array_pop($output);
- } else {
- // leading relative URL '..'
- $output[] = '..';
- }
- continue;
- }
- $output[] = array_shift($input);
- }
- return $rval . implode('/', $output);
- }
- /**
- * Prepends a base IRI to the given relative IRI.
- *
- * @param mixed $base a string or the parsed base IRI.
- * @param string $iri the relative IRI.
- *
- * @return string the absolute IRI.
- */
- function jsonld_prepend_base($base, $iri) {
- // skip IRI processing
- if($base === null) {
- return $iri;
- }
- // already an absolute IRI
- if(strpos($iri, ':') !== false) {
- return $iri;
- }
- // parse base if it is a string
- if(is_string($base)) {
- $base = jsonld_parse_url($base);
- }
- // parse given IRI
- $rel = jsonld_parse_url($iri);
- // per RFC3986 5.2.2
- $transform = array('protocol' => $base['protocol']);
- if($rel['authority'] !== null) {
- $transform['authority'] = $rel['authority'];
- $transform['path'] = $rel['path'];
- $transform['query'] = $rel['query'];
- } else {
- $transform['authority'] = $base['authority'];
- if($rel['path'] === '') {
- $transform['path'] = $base['path'];
- if($rel['query'] !== null) {
- $transform['query'] = $rel['query'];
- } else {
- $transform['query'] = $base['query'];
- }
- } else {
- if(strpos($rel['path'], '/') === 0) {
- // IRI represents an absolute path
- $transform['path'] = $rel['path'];
- } else {
- // merge paths
- $path = $base['path'];
- // append relative path to the end of the last directory from base
- if($rel['path'] !== '') {
- $idx = strrpos($path, '/');
- $idx = ($idx === false) ? 0 : $idx + 1;
- $path = substr($path, 0, $idx);
- if(strlen($path) > 0 && substr($path, -1) !== '/') {
- $path .= '/';
- }
- $path .= $rel['path'];
- }
- $transform['path'] = $path;
- }
- $transform['query'] = $rel['query'];
- }
- }
- // remove slashes and dots in path
- $transform['path'] = jsonld_remove_dot_segments(
- $transform['path'], !!$transform['authority']);
- // construct URL
- $rval = $transform['protocol'];
- if($transform['authority'] !== null) {
- $rval .= '//' . $transform['authority'];
- }
- $rval .= $transform['path'];
- if($transform['query'] !== null) {
- $rval .= '?' . $transform['query'];
- }
- if($rel['fragment'] !== null) {
- $rval .= '#' . $rel['fragment'];
- }
- // handle empty base
- if($rval === '') {
- $rval = './';
- }
- return $rval;
- }
- /**
- * Removes a base IRI from the given absolute IRI.
- *
- * @param mixed $base the base IRI.
- * @param string $iri the absolute IRI.
- *
- * @return string the relative IRI if relative to base, otherwise the absolute
- * IRI.
- */
- function jsonld_remove_base($base, $iri) {
- // skip IRI processing
- if($base === null) {
- return $iri;
- }
- if(is_string($base)) {
- $base = jsonld_parse_url($base);
- }
- // establish base root
- $root = '';
- if($base['href'] !== '') {
- $root .= "{$base['protocol']}//{$base['authority']}";
- } else if(strpos($iri, '//') === false) {
- // support network-path reference with empty base
- $root .= '//';
- }
- // IRI not relative to base
- if($root === '' || strpos($iri, $root) !== 0) {
- return $iri;
- }
- // remove root from IRI
- $rel = jsonld_parse_url(substr($iri, strlen($root)));
- // remove path segments that match (do not remove last segment unless there
- // is a hash or query)
- $base_segments = explode('/', $base['normalizedPath']);
- $iri_segments = explode('/', $rel['normalizedPath']);
- $last = ($rel['query'] || $rel['fragment']) ? 0 : 1;
- while(count($base_segments) > 0 && count($iri_segments) > $last) {
- if($base_segments[0] !== $iri_segments[0]) {
- break;
- }
- array_shift($base_segments);
- array_shift($iri_segments);
- }
- // use '../' for each non-matching base segment
- $rval = '';
- if(count($base_segments) > 0) {
- // don't count the last segment (if it ends with '/' last path doesn't
- // count and if it doesn't end with '/' it isn't a path)
- array_pop($base_segments);
- foreach($base_segments as $segment) {
- $rval .= '../';
- }
- }
- // prepend remaining segments
- $rval .= implode('/', $iri_segments);
- // add query and hash
- if($rel['query'] !== null) {
- $rval .= "?{$rel['query']}";
- }
- if($rel['fragment'] !== null) {
- $rval .= "#{$rel['fragment']}";
- }
- if($rval === '') {
- $rval = './';
- }
- return $rval;
- }
- /**
- * A JSON-LD processor.
- */
- class JsonLdProcessor {
- /** XSD constants */
- const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
- const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
- const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
- const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
- /** RDF constants */
- const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List';
- const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
- const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
- const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
- const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
- const RDF_LANGSTRING =
- 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
- /** Restraints */
- const MAX_CONTEXT_URLS = 10;
- /** Processor-specific RDF dataset parsers. */
- protected $rdfParsers = null;
- /**
- * Constructs a JSON-LD processor.
- */
- public function __construct() {}
- /**
- * Performs JSON-LD compaction.
- *
- * @param mixed $input the JSON-LD object to compact.
- * @param mixed $ctx the context to compact with.
- * @param assoc $options the compaction options.
- * [base] the base IRI to use.
- * [compactArrays] true to compact arrays to single values when
- * appropriate, false not to (default: true).
- * [graph] true to always output a top-level graph (default: false).
- * [skipExpansion] true to assume the input is expanded and skip
- * expansion, false not to, defaults to false.
- * [activeCtx] true to also return the active context used.
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the compacted JSON-LD output.
- */
- public function compact($input, $ctx, $options) {
- global $jsonld_default_load_document;
- if($ctx === null) {
- throw new JsonLdException(
- 'The compaction context must not be null.',
- 'jsonld.CompactError', 'invalid local context');
- }
- // nothing to compact
- if($input === null) {
- return null;
- }
- self::setdefaults($options, array(
- 'base' => is_string($input) ? $input : '',
- 'compactArrays' => true,
- 'graph' => false,
- 'skipExpansion' => false,
- 'activeCtx' => false,
- 'documentLoader' => $jsonld_default_load_document,
- 'link' => false));
- if($options['link']) {
- // force skip expansion when linking, "link" is not part of the
- // public API, it should only be called from framing
- $options['skipExpansion'] = true;
- }
- if($options['skipExpansion'] === true) {
- $expanded = $input;
- } else {
- // expand input
- try {
- $expanded = $this->expand($input, $options);
- } catch(JsonLdException $e) {
- throw new JsonLdException(
- 'Could not expand input before compaction.',
- 'jsonld.CompactError', null, null, $e);
- }
- }
- // process context
- $active_ctx = $this->_getInitialContext($options);
- try {
- $active_ctx = $this->processContext($active_ctx, $ctx, $options);
- } catch(JsonLdException $e) {
- throw new JsonLdException(
- 'Could not process context before compaction.',
- 'jsonld.CompactError', null, null, $e);
- }
- // do compaction
- $compacted = $this->_compact($active_ctx, null, $expanded, $options);
- if($options['compactArrays'] &&
- !$options['graph'] && is_array($compacted)) {
- if(count($compacted) === 1) {
- // simplify to a single item
- $compacted = $compacted[0];
- } else if(count($compacted) === 0) {
- // simplify to an empty object
- $compacted = new stdClass();
- }
- } else if($options['graph']) {
- // always use array if graph option is on
- $compacted = self::arrayify($compacted);
- }
- // follow @context key
- if(is_object($ctx) && property_exists($ctx, '@context')) {
- $ctx = $ctx->{'@context'};
- }
- // build output context
- $ctx = self::copy($ctx);
- $ctx = self::arrayify($ctx);
- // remove empty contexts
- $tmp = $ctx;
- $ctx = array();
- foreach($tmp as $v) {
- if(!is_object($v) || count(array_keys((array)$v)) > 0) {
- $ctx[] = $v;
- }
- }
- // remove array if only one context
- $ctx_length = count($ctx);
- $has_context = ($ctx_length > 0);
- if($ctx_length === 1) {
- $ctx = $ctx[0];
- }
- // add context and/or @graph
- if(is_array($compacted)) {
- // use '@graph' keyword
- $kwgraph = $this->_compactIri($active_ctx, '@graph');
- $graph = $compacted;
- $compacted = new stdClass();
- if($has_context) {
- $compacted->{'@context'} = $ctx;
- }
- $compacted->{$kwgraph} = $graph;
- } else if(is_object($compacted) && $has_context) {
- // reorder keys so @context is first
- $graph = $compacted;
- $compacted = new stdClass();
- $compacted->{'@context'} = $ctx;
- foreach($graph as $k => $v) {
- $compacted->{$k} = $v;
- }
- }
- if($options['activeCtx']) {
- return array('compacted' => $compacted, 'activeCtx' => $active_ctx);
- }
- return $compacted;
- }
- /**
- * Performs JSON-LD expansion.
- *
- * @param mixed $input the JSON-LD object to expand.
- * @param assoc $options the options to use:
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [keepFreeFloatingNodes] true to keep free-floating nodes,
- * false not to, defaults to false.
- * [documentLoader(url)] the document loader.
- *
- * @return array the expanded JSON-LD output.
- */
- public function expand($input, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'keepFreeFloatingNodes' => false,
- 'documentLoader' => $jsonld_default_load_document));
- // if input is a string, attempt to dereference remote document
- if(is_string($input)) {
- $remote_doc = call_user_func($options['documentLoader'], $input);
- } else {
- $remote_doc = (object)array(
- 'contextUrl' => null,
- 'documentUrl' => null,
- 'document' => $input);
- }
- try {
- if($remote_doc->document === null) {
- throw new JsonLdException(
- 'No remote document found at the given URL.',
- 'jsonld.NullRemoteDocument');
- }
- if(is_string($remote_doc->document)) {
- $remote_doc->document = self::_parse_json($remote_doc->document);
- }
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not retrieve a JSON-LD document from the URL.',
- 'jsonld.LoadDocumentError', 'loading document failed',
- array('remoteDoc' => $remote_doc), $e);
- }
- // set default base
- self::setdefault($options, 'base', $remote_doc->documentUrl ?: '');
- // build meta-object and retrieve all @context urls
- $input = (object)array(
- 'document' => self::copy($remote_doc->document),
- 'remoteContext' => (object)array(
- '@context' => $remote_doc->contextUrl));
- if(isset($options['expandContext'])) {
- $expand_context = self::copy($options['expandContext']);
- if(is_object($expand_context) &&
- property_exists($expand_context, '@context')) {
- $input->expandContext = $expand_context;
- } else {
- $input->expandContext = (object)array('@context' => $expand_context);
- }
- }
- // retrieve all @context URLs in the input
- try {
- $this->_retrieveContextUrls(
- $input, new stdClass(), $options['documentLoader'], $options['base']);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not perform JSON-LD expansion.',
- 'jsonld.ExpandError', null, null, $e);
- }
- $active_ctx = $this->_getInitialContext($options);
- $document = $input->document;
- $remote_context = $input->remoteContext->{'@context'};
- // process optional expandContext
- if(property_exists($input, 'expandContext')) {
- $active_ctx = self::_processContext(
- $active_ctx, $input->expandContext, $options);
- }
- // process remote context from HTTP Link Header
- if($remote_context) {
- $active_ctx = self::_processContext(
- $active_ctx, $remote_context, $options);
- }
- // do expansion
- $expanded = $this->_expand($active_ctx, null, $document, $options, false);
- // optimize away @graph with no other properties
- if(is_object($expanded) && property_exists($expanded, '@graph') &&
- count(array_keys((array)$expanded)) === 1) {
- $expanded = $expanded->{'@graph'};
- } else if($expanded === null) {
- $expanded = array();
- }
- // normalize to an array
- return self::arrayify($expanded);
- }
- /**
- * Performs JSON-LD flattening.
- *
- * @param mixed $input the JSON-LD to flatten.
- * @param ctx the context to use to compact the flattened output, or null.
- * @param assoc $options the options to use:
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [documentLoader(url)] the document loader.
- *
- * @return array the flattened output.
- */
- public function flatten($input, $ctx, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'base' => is_string($input) ? $input : '',
- 'documentLoader' => $jsonld_default_load_document));
- try {
- // expand input
- $expanded = $this->expand($input, $options);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not expand input before flattening.',
- 'jsonld.FlattenError', null, null, $e);
- }
- // do flattening
- $flattened = $this->_flatten($expanded);
- if($ctx === null) {
- return $flattened;
- }
- // compact result (force @graph option to true, skip expansion)
- $options['graph'] = true;
- $options['skipExpansion'] = true;
- try {
- $compacted = $this->compact($flattened, $ctx, $options);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not compact flattened output.',
- 'jsonld.FlattenError', null, null, $e);
- }
- return $compacted;
- }
- /**
- * Performs JSON-LD framing.
- *
- * @param mixed $input the JSON-LD object to frame.
- * @param stdClass $frame the JSON-LD frame to use.
- * @param $options the framing options.
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [embed] default @embed flag: '@last', '@always', '@never', '@link'
- * (default: '@last').
- * [explicit] default @explicit flag (default: false).
- * [requireAll] default @requireAll flag (default: true).
- * [omitDefault] default @omitDefault flag (default: false).
- * [documentLoader(url)] the document loader.
- *
- * @return stdClass the framed JSON-LD output.
- */
- public function frame($input, $frame, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'base' => is_string($input) ? $input : '',
- 'compactArrays' => true,
- 'embed' => '@last',
- 'explicit' => false,
- 'requireAll' => true,
- 'omitDefault' => false,
- 'documentLoader' => $jsonld_default_load_document));
- // if frame is a string, attempt to dereference remote document
- if(is_string($frame)) {
- $remote_frame = call_user_func($options['documentLoader'], $frame);
- } else {
- $remote_frame = (object)array(
- 'contextUrl' => null,
- 'documentUrl' => null,
- 'document' => $frame);
- }
- try {
- if($remote_frame->document === null) {
- throw new JsonLdException(
- 'No remote document found at the given URL.',
- 'jsonld.NullRemoteDocument');
- }
- if(is_string($remote_frame->document)) {
- $remote_frame->document = self::_parse_json($remote_frame->document);
- }
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not retrieve a JSON-LD document from the URL.',
- 'jsonld.LoadDocumentError', 'loading document failed',
- array('remoteDoc' => $remote_frame), $e);
- }
- // preserve frame context
- $frame = $remote_frame->document;
- if($frame !== null) {
- $ctx = (property_exists($frame, '@context') ?
- $frame->{'@context'} : new stdClass());
- if($remote_frame->contextUrl !== null) {
- if($ctx !== null) {
- $ctx = $remote_frame->contextUrl;
- } else {
- $ctx = self::arrayify($ctx);
- $ctx[] = $remote_frame->contextUrl;
- }
- $frame->{'@context'} = $ctx;
- }
- }
- try {
- // expand input
- $expanded = $this->expand($input, $options);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not expand input before framing.',
- 'jsonld.FrameError', null, null, $e);
- }
- try {
- // expand frame
- $opts = $options;
- $opts['keepFreeFloatingNodes'] = true;
- $expanded_frame = $this->expand($frame, $opts);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not expand frame before framing.',
- 'jsonld.FrameError', null, null, $e);
- }
- // do framing
- $framed = $this->_frame($expanded, $expanded_frame, $options);
- try {
- // compact result (force @graph option to true, skip expansion, check
- // for linked embeds)
- $options['graph'] = true;
- $options['skipExpansion'] = true;
- $options['link'] = new ArrayObject();
- $options['activeCtx'] = true;
- $result = $this->compact($framed, $ctx, $options);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not compact framed output.',
- 'jsonld.FrameError', null, null, $e);
- }
- $compacted = $result['compacted'];
- $active_ctx = $result['activeCtx'];
- // get graph alias
- $graph = $this->_compactIri($active_ctx, '@graph');
- // remove @preserve from results
- $options['link'] = new ArrayObject();
- $compacted->{$graph} = $this->_removePreserve(
- $active_ctx, $compacted->{$graph}, $options);
- return $compacted;
- }
- /**
- * Performs JSON-LD normalization.
- *
- * @param mixed $input the JSON-LD object to normalize.
- * @param assoc $options the options to use:
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [inputFormat] the format if input is not JSON-LD:
- * 'application/nquads' for N-Quads.
- * [format] the format if output is a string:
- * 'application/nquads' for N-Quads.
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the normalized output.
- */
- public function normalize($input, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'base' => is_string($input) ? $input : '',
- 'documentLoader' => $jsonld_default_load_document));
- if(isset($options['inputFormat'])) {
- if($options['inputFormat'] != 'application/nquads') {
- throw new JsonLdException(
- 'Unknown normalization input format.', 'jsonld.NormalizeError');
- }
- $dataset = $this->parseNQuads($input);
- } else {
- try {
- // convert to RDF dataset then do normalization
- $opts = $options;
- if(isset($opts['format'])) {
- unset($opts['format']);
- }
- $opts['produceGeneralizedRdf'] = false;
- $dataset = $this->toRDF($input, $opts);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not convert input to RDF dataset before normalization.',
- 'jsonld.NormalizeError', null, null, $e);
- }
- }
- // do normalization
- return $this->_normalize($dataset, $options);
- }
- /**
- * Converts an RDF dataset to JSON-LD.
- *
- * @param mixed $dataset a serialized string of RDF in a format specified
- * by the format option or an RDF dataset to convert.
- * @param assoc $options the options to use:
- * [format] the format if input is a string:
- * 'application/nquads' for N-Quads (default).
- * [useRdfType] true to use rdf:type, false to use @type
- * (default: false).
- * [useNativeTypes] true to convert XSD types into native types
- * (boolean, integer, double), false not to (default: false).
- *
- * @return array the JSON-LD output.
- */
- public function fromRDF($dataset, $options) {
- global $jsonld_rdf_parsers;
- self::setdefaults($options, array(
- 'useRdfType' => false,
- 'useNativeTypes' => false));
- if(!isset($options['format']) && is_string($dataset)) {
- // set default format to nquads
- $options['format'] = 'application/nquads';
- }
- // handle special format
- if(isset($options['format']) && $options['format']) {
- // supported formats (processor-specific and global)
- if(($this->rdfParsers !== null &&
- !property_exists($this->rdfParsers, $options['format'])) ||
- $this->rdfParsers === null &&
- !property_exists($jsonld_rdf_parsers, $options['format'])) {
- throw new JsonLdException(
- 'Unknown input format.',
- 'jsonld.UnknownFormat', null, array('format' => $options['format']));
- }
- if($this->rdfParsers !== null) {
- $callable = $this->rdfParsers->{$options['format']};
- } else {
- $callable = $jsonld_rdf_parsers->{$options['format']};
- }
- $dataset = call_user_func($callable, $dataset);
- }
- // convert from RDF
- return $this->_fromRDF($dataset, $options);
- }
- /**
- * Outputs the RDF dataset found in the given JSON-LD object.
- *
- * @param mixed $input the JSON-LD object.
- * @param assoc $options the options to use:
- * [base] the base IRI to use.
- * [expandContext] a context to expand with.
- * [format] the format to use to output a string:
- * 'application/nquads' for N-Quads.
- * [produceGeneralizedRdf] true to output generalized RDF, false
- * to produce only standard RDF (default: false).
- * [documentLoader(url)] the document loader.
- *
- * @return mixed the resulting RDF dataset (or a serialization of it).
- */
- public function toRDF($input, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'base' => is_string($input) ? $input : '',
- 'produceGeneralizedRdf' => false,
- 'documentLoader' => $jsonld_default_load_document));
- try {
- // expand input
- $expanded = $this->expand($input, $options);
- } catch(JsonLdException $e) {
- throw new JsonLdException(
- 'Could not expand input before serialization to RDF.',
- 'jsonld.RdfError', null, null, $e);
- }
- // create node map for default graph (and any named graphs)
- $namer = new UniqueNamer('_:b');
- $node_map = (object)array('@default' => new stdClass());
- $this->_createNodeMap($expanded, $node_map, '@default', $namer);
- // output RDF dataset
- $dataset = new stdClass();
- $graph_names = array_keys((array)$node_map);
- sort($graph_names);
- foreach($graph_names as $graph_name) {
- $graph = $node_map->{$graph_name};
- // skip relative IRIs
- if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) {
- $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options);
- }
- }
- $rval = $dataset;
- // convert to output format
- if(isset($options['format']) && $options['format']) {
- // supported formats
- if($options['format'] === 'application/nquads') {
- $rval = self::toNQuads($dataset);
- } else {
- throw new JsonLdException(
- 'Unknown output format.', 'jsonld.UnknownFormat',
- null, array('format' => $options['format']));
- }
- }
- return $rval;
- }
- /**
- * Processes a local context, resolving any URLs as necessary, and returns a
- * new active context in its callback.
- *
- * @param stdClass $active_ctx the current active context.
- * @param mixed $local_ctx the local context to process.
- * @param assoc $options the options to use:
- * [documentLoader(url)] the document loader.
- *
- * @return stdClass the new active context.
- */
- public function processContext($active_ctx, $local_ctx, $options) {
- global $jsonld_default_load_document;
- self::setdefaults($options, array(
- 'base' => '',
- 'documentLoader' => $jsonld_default_load_document));
- // return initial context early for null context
- if($local_ctx === null) {
- return $this->_getInitialContext($options);
- }
- // retrieve URLs in local_ctx
- $local_ctx = self::copy($local_ctx);
- if(is_string($local_ctx) or (
- is_object($local_ctx) && !property_exists($local_ctx, '@context'))) {
- $local_ctx = (object)array('@context' => $local_ctx);
- }
- try {
- $this->_retrieveContextUrls(
- $local_ctx, new stdClass(),
- $options['documentLoader'], $options['base']);
- } catch(Exception $e) {
- throw new JsonLdException(
- 'Could not process JSON-LD context.',
- 'jsonld.ContextError', null, null, $e);
- }
- // process context
- return $this->_processContext($active_ctx, $local_ctx, $options);
- }
- /**
- * Returns true if the given subject has the given property.
- *
- * @param stdClass $subject the subject to check.
- * @param string $property the property to look for.
- *
- * @return bool true if the subject has the given property, false if not.
- */
- public static function hasProperty($subject, $property) {
- $rval = false;
- if(property_exists($subject, $property)) {
- $value = $subject->{$property};
- $rval = (!is_array($value) || count($value) > 0);
- }
- return $rval;
- }
- /**
- * Determines if the given value is a property of the given subject.
- *
- * @param stdClass $subject the subject to check.
- * @param string $property the property to check.
- * @param mixed $value the value to check.
- *
- * @return bool true if the value exists, false if not.
- */
- public static function hasValue($subject, $property, $value) {
- $rval = false;
- if(self::hasProperty($subject, $property)) {
- $val = $subject->{$property};
- $is_list = self::_isList($val);
- if(is_array($val) || $is_list) {
- if($is_list) {
- $val = $val->{'@list'};
- }
- foreach($val as $v) {
- if(self::compareValues($value, $v)) {
- $rval = true;
- break;
- }
- }
- } else if(!is_array($value)) {
- // avoid matching the set of values with an array value parameter
- $rval = self::compareValues($value, $val);
- }
- }
- return $rval;
- }
- /**
- * Adds a value to a subject. If the value is an array, all values in the
- * array will be added.
- *
- * Note: If the value is a subject that already exists as a property of the
- * given subject, this method makes no attempt to deeply merge properties.
- * Instead, the value will not be added.
- *
- * @param stdClass $subject the subject to add the value to.
- * @param string $property the property that relates the value to the subject.
- * @param mixed $value the value to add.
- * @param assoc [$options] the options to use:
- * [propertyIsArray] true if the property is always an array, false
- * if not (default: false).
- * [allowDuplicate] true to allow duplicates, false not to (uses a
- * simple shallow comparison of subject ID or value)
- * (default: true).
- */
- public static function addValue(
- $subject, $property, $value, $options=array()) {
- self::setdefaults($options, array(
- 'allowDuplicate' => true,
- 'propertyIsArray' => false));
- if(is_array($value)) {
- if(count($value) === 0 && $options['propertyIsArray'] &&
- !property_exists($subject, $property)) {
- $subject->{$property} = array();
- }
- foreach($value as $v) {
- self::addValue($subject, $property, $v, $options);
- }
- } else if(property_exists($subject, $property)) {
- // check if subject already has value if duplicates not allowed
- $has_value = (!$options['allowDuplicate'] &&
- self::hasValue($subject, $property, $value));
- // make property an array if value not present or always an array
- if(!is_array($subject->{$property}) &&
- (!$has_value || $options['propertyIsArray'])) {
- $subject->{$property} = array($subject->{$property});
- }
- // add new value
- if(!$has_value) {
- $subject->{$property}[] = $value;
- }
- } else {
- // add new value as set or single value
- $subject->{$property} = ($options['propertyIsArray'] ?
- array($value) : $value);
- }
- }
- /**
- * Gets all of the values for a subject's property as an array.
- *
- * @param stdClass $subject the subject.
- * @param string $property the property.
- *
- * @return array all of the values for a subject's property as an array.
- */
- public static function getValues($subject, $property) {
- $rval = (property_exists($subject, $property) ?
- $subject->{$property} : array());
- return self::arrayify($rval);
- }
- /**
- * Removes a property from a subject.
- *
- * @param stdClass $subject the subject.
- * @param string $property the property.
- */
- public static function removeProperty($subject, $property) {
- unset($subject->{$property});
- }
- /**
- * Removes a value from a subject.
- *
- * @param stdClass $subject the subject.
- * @param string $property the property that relates the value to the subject.
- * @param mixed $value the value to remove.
- * @param assoc [$options] the options to use:
- * [propertyIsArray] true if the property is always an array,
- * false if not (default: false).
- */
- public static function removeValue(
- $subject, $property, $value, $options=array()) {
- self::setdefaults($options, array(
- 'propertyIsArray' => false));
- // filter out value
- $filter = function($e) use ($value) {
- return !sel…