PageRenderTime 73ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 2ms

/jsonld.php

http://github.com/digitalbazaar/php-json-ld
PHP | 6038 lines | 3804 code | 546 blank | 1688 comment | 887 complexity | d9edef1298ff26620ddd5bd83d40cc96 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * PHP implementation of the JSON-LD API.
  4. * Version: 0.4.8-dev
  5. *
  6. * @author Dave Longley
  7. *
  8. * BSD 3-Clause License
  9. * Copyright (c) 2011-2014 Digital Bazaar, Inc.
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. *
  15. * Redistributions of source code must retain the above copyright notice,
  16. * this list of conditions and the following disclaimer.
  17. *
  18. * Redistributions in binary form must reproduce the above copyright
  19. * notice, this list of conditions and the following disclaimer in the
  20. * documentation and/or other materials provided with the distribution.
  21. *
  22. * Neither the name of the Digital Bazaar, Inc. nor the names of its
  23. * contributors may be used to endorse or promote products derived from
  24. * this software without specific prior written permission.
  25. *
  26. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
  27. * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
  28. * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
  29. * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  30. * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  31. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
  32. * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  33. * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  34. * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  35. * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  36. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  37. */
  38. /**
  39. * Performs JSON-LD compaction.
  40. *
  41. * @param mixed $input the JSON-LD object to compact.
  42. * @param mixed $ctx the context to compact with.
  43. * @param assoc [$options] options to use:
  44. * [base] the base IRI to use.
  45. * [graph] true to always output a top-level graph (default: false).
  46. * [documentLoader(url)] the document loader.
  47. *
  48. * @return mixed the compacted JSON-LD output.
  49. */
  50. function jsonld_compact($input, $ctx, $options=array()) {
  51. $p = new JsonLdProcessor();
  52. return $p->compact($input, $ctx, $options);
  53. }
  54. /**
  55. * Performs JSON-LD expansion.
  56. *
  57. * @param mixed $input the JSON-LD object to expand.
  58. * @param assoc[$options] the options to use:
  59. * [base] the base IRI to use.
  60. * [documentLoader(url)] the document loader.
  61. *
  62. * @return array the expanded JSON-LD output.
  63. */
  64. function jsonld_expand($input, $options=array()) {
  65. $p = new JsonLdProcessor();
  66. return $p->expand($input, $options);
  67. }
  68. /**
  69. * Performs JSON-LD flattening.
  70. *
  71. * @param mixed $input the JSON-LD to flatten.
  72. * @param mixed $ctx the context to use to compact the flattened output, or
  73. * null.
  74. * @param [options] the options to use:
  75. * [base] the base IRI to use.
  76. * [documentLoader(url)] the document loader.
  77. *
  78. * @return mixed the flattened JSON-LD output.
  79. */
  80. function jsonld_flatten($input, $ctx, $options=array()) {
  81. $p = new JsonLdProcessor();
  82. return $p->flatten($input, $ctx, $options);
  83. }
  84. /**
  85. * Performs JSON-LD framing.
  86. *
  87. * @param mixed $input the JSON-LD object to frame.
  88. * @param stdClass $frame the JSON-LD frame to use.
  89. * @param assoc [$options] the framing options.
  90. * [base] the base IRI to use.
  91. * [embed] default @embed flag (default: true).
  92. * [explicit] default @explicit flag (default: false).
  93. * [requireAll] default @requireAll flag (default: true).
  94. * [omitDefault] default @omitDefault flag (default: false).
  95. * [documentLoader(url)] the document loader.
  96. *
  97. * @return stdClass the framed JSON-LD output.
  98. */
  99. function jsonld_frame($input, $frame, $options=array()) {
  100. $p = new JsonLdProcessor();
  101. return $p->frame($input, $frame, $options);
  102. }
  103. /**
  104. * **Experimental**
  105. *
  106. * Links a JSON-LD document's nodes in memory.
  107. *
  108. * @param mixed $input the JSON-LD document to link.
  109. * @param mixed $ctx the JSON-LD context to apply or null.
  110. * @param assoc [$options] the options to use:
  111. * [base] the base IRI to use.
  112. * [expandContext] a context to expand with.
  113. * [documentLoader(url)] the document loader.
  114. *
  115. * @return the linked JSON-LD output.
  116. */
  117. function jsonld_link($input, $ctx, $options) {
  118. // API matches running frame with a wildcard frame and embed: '@link'
  119. // get arguments
  120. $frame = new stdClass();
  121. if($ctx) {
  122. $frame->{'@context'} = $ctx;
  123. }
  124. $frame->{'@embed'} = '@link';
  125. return jsonld_frame($input, $frame, $options);
  126. };
  127. /**
  128. * Performs RDF dataset normalization on the given input. The input is
  129. * JSON-LD unless the 'inputFormat' option is used. The output is an RDF
  130. * dataset unless the 'format' option is used.
  131. *
  132. * @param mixed $input the JSON-LD object to normalize.
  133. * @param assoc [$options] the options to use:
  134. * [base] the base IRI to use.
  135. * [intputFormat] the format if input is not JSON-LD:
  136. * 'application/nquads' for N-Quads.
  137. * [format] the format if output is a string:
  138. * 'application/nquads' for N-Quads.
  139. * [documentLoader(url)] the document loader.
  140. *
  141. * @return mixed the normalized output.
  142. */
  143. function jsonld_normalize($input, $options=array()) {
  144. $p = new JsonLdProcessor();
  145. return $p->normalize($input, $options);
  146. }
  147. /**
  148. * Converts an RDF dataset to JSON-LD.
  149. *
  150. * @param mixed $input a serialized string of RDF in a format specified
  151. * by the format option or an RDF dataset to convert.
  152. * @param assoc [$options] the options to use:
  153. * [format] the format if input not an array:
  154. * 'application/nquads' for N-Quads (default).
  155. * [useRdfType] true to use rdf:type, false to use @type
  156. * (default: false).
  157. * [useNativeTypes] true to convert XSD types into native types
  158. * (boolean, integer, double), false not to (default: false).
  159. *
  160. * @return array the JSON-LD output.
  161. */
  162. function jsonld_from_rdf($input, $options=array()) {
  163. $p = new JsonLdProcessor();
  164. return $p->fromRDF($input, $options);
  165. }
  166. /**
  167. * Outputs the RDF dataset found in the given JSON-LD object.
  168. *
  169. * @param mixed $input the JSON-LD object.
  170. * @param assoc [$options] the options to use:
  171. * [base] the base IRI to use.
  172. * [format] the format to use to output a string:
  173. * 'application/nquads' for N-Quads.
  174. * [produceGeneralizedRdf] true to output generalized RDF, false
  175. * to produce only standard RDF (default: false).
  176. * [documentLoader(url)] the document loader.
  177. *
  178. * @return mixed the resulting RDF dataset (or a serialization of it).
  179. */
  180. function jsonld_to_rdf($input, $options=array()) {
  181. $p = new JsonLdProcessor();
  182. return $p->toRDF($input, $options);
  183. }
  184. /**
  185. * JSON-encodes (with unescaped slashes) the given stdClass or array.
  186. *
  187. * @param mixed $input the native PHP stdClass or array which will be
  188. * converted to JSON by json_encode().
  189. * @param int $options the options to use.
  190. * [JSON_PRETTY_PRINT] pretty print.
  191. * @param int $depth the maximum depth to use.
  192. *
  193. * @return the encoded JSON data.
  194. */
  195. function jsonld_encode($input, $options=0, $depth=512) {
  196. // newer PHP has a flag to avoid escaped '/'
  197. if(defined('JSON_UNESCAPED_SLASHES')) {
  198. return json_encode($input, JSON_UNESCAPED_SLASHES | $options, $depth);
  199. }
  200. // use a simple string replacement of '\/' to '/'.
  201. return str_replace('\\/', '/', json_encode($input, $options, $depth));
  202. }
  203. /**
  204. * Decodes a serialized JSON-LD object.
  205. *
  206. * @param string $input the JSON-LD input.
  207. *
  208. * @return mixed the resolved JSON-LD object, null on error.
  209. */
  210. function jsonld_decode($input) {
  211. return json_decode($input);
  212. }
  213. /**
  214. * Parses a link header. The results will be key'd by the value of "rel".
  215. *
  216. * Link: <http://json-ld.org/contexts/person.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
  217. *
  218. * Parses as: {
  219. * 'http://www.w3.org/ns/json-ld#context': {
  220. * target: http://json-ld.org/contexts/person.jsonld,
  221. * type: 'application/ld+json'
  222. * }
  223. * }
  224. *
  225. * If there is more than one "rel" with the same IRI, then entries in the
  226. * resulting map for that "rel" will be arrays of objects, otherwise they will
  227. * be single objects.
  228. *
  229. * @param string $header the link header to parse.
  230. *
  231. * @return assoc the parsed result.
  232. */
  233. function jsonld_parse_link_header($header) {
  234. $rval = array();
  235. // split on unbracketed/unquoted commas
  236. if(!preg_match_all(
  237. '/(?:<[^>]*?>|"[^"]*?"|[^,])+/', $header, $entries, PREG_SET_ORDER)) {
  238. return $rval;
  239. }
  240. $r_link_header = '/\s*<([^>]*?)>\s*(?:;\s*(.*))?/';
  241. foreach($entries as $entry) {
  242. if(!preg_match($r_link_header, $entry[0], $match)) {
  243. continue;
  244. }
  245. $result = (object)array('target' => $match[1]);
  246. $params = $match[2];
  247. $r_params = '/(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/';
  248. preg_match_all($r_params, $params, $matches, PREG_SET_ORDER);
  249. foreach($matches as $match) {
  250. $result->{$match[1]} = $match[2] ?: $match[3];
  251. }
  252. $rel = property_exists($result, 'rel') ? $result->rel : '';
  253. if(!isset($rval[$rel])) {
  254. $rval[$rel] = $result;
  255. } else if(is_array($rval[$rel])) {
  256. $rval[$rel][] = $result;
  257. } else {
  258. $rval[$rel] = array($rval[$rel], $result);
  259. }
  260. }
  261. return $rval;
  262. }
  263. /**
  264. * Relabels all blank nodes in the given JSON-LD input.
  265. *
  266. * @param mixed input the JSON-LD input.
  267. */
  268. function jsonld_relabel_blank_nodes($input) {
  269. $p = new JsonLdProcessor();
  270. return $p->_labelBlankNodes(new UniqueNamer('_:b'), $input);
  271. }
  272. /** JSON-LD shared in-memory cache. */
  273. global $jsonld_cache;
  274. $jsonld_cache = new stdClass();
  275. /** The default active context cache. */
  276. $jsonld_cache->activeCtx = new ActiveContextCache();
  277. /** Stores the default JSON-LD document loader. */
  278. global $jsonld_default_load_document;
  279. $jsonld_default_load_document = 'jsonld_default_document_loader';
  280. /**
  281. * Sets the default JSON-LD document loader.
  282. *
  283. * @param callable load_document(url) the document loader.
  284. */
  285. function jsonld_set_document_loader($load_document) {
  286. global $jsonld_default_load_document;
  287. $jsonld_default_load_document = $load_document;
  288. }
  289. /**
  290. * Retrieves JSON-LD at the given URL.
  291. *
  292. * @param string $url the URL to retrieve.
  293. *
  294. * @return the JSON-LD.
  295. */
  296. function jsonld_get_url($url) {
  297. global $jsonld_default_load_document;
  298. if($jsonld_default_load_document !== null) {
  299. $document_loader = $jsonld_default_load_document;
  300. } else {
  301. $document_loader = 'jsonld_default_document_loader';
  302. }
  303. $remote_doc = call_user_func($document_loader, $url);
  304. if($remote_doc) {
  305. return $remote_doc->document;
  306. }
  307. return null;
  308. }
  309. /**
  310. * The default implementation to retrieve JSON-LD at the given URL.
  311. *
  312. * @param string $url the URL to to retrieve.
  313. *
  314. * @return stdClass the RemoteDocument object.
  315. */
  316. function jsonld_default_document_loader($url) {
  317. $doc = (object)array(
  318. 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
  319. $redirects = array();
  320. $opts = array(
  321. 'http' => array(
  322. 'method' => 'GET',
  323. 'header' =>
  324. "Accept: application/ld+json\r\n"),
  325. /* Note: Use jsonld_default_secure_document_loader for security. */
  326. 'ssl' => array(
  327. 'verify_peer' => false,
  328. 'allow_self_signed' => true)
  329. );
  330. $context = stream_context_create($opts);
  331. $content_type = null;
  332. stream_context_set_params($context, array('notification' =>
  333. function($notification_code, $severity, $message) use (
  334. &$redirects, &$content_type) {
  335. switch($notification_code) {
  336. case STREAM_NOTIFY_REDIRECTED:
  337. $redirects[] = $message;
  338. break;
  339. case STREAM_NOTIFY_MIME_TYPE_IS:
  340. $content_type = $message;
  341. break;
  342. };
  343. }));
  344. $result = @file_get_contents($url, false, $context);
  345. if($result === false) {
  346. throw new JsonLdException(
  347. 'Could not retrieve a JSON-LD document from the URL: ' . $url,
  348. 'jsonld.LoadDocumentError', 'loading document failed');
  349. }
  350. $link_header = array();
  351. foreach($http_response_header as $header) {
  352. if(strpos($header, 'link') === 0) {
  353. $value = explode(': ', $header);
  354. if(count($value) > 1) {
  355. $link_header[] = $value[1];
  356. }
  357. }
  358. }
  359. $link_header = jsonld_parse_link_header(join(',', $link_header));
  360. if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
  361. $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
  362. } else {
  363. $link_header = null;
  364. }
  365. if($link_header && $content_type !== 'application/ld+json') {
  366. // only 1 related link header permitted
  367. if(is_array($link_header)) {
  368. throw new JsonLdException(
  369. 'URL could not be dereferenced, it has more than one ' .
  370. 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
  371. 'multiple context link headers', array('url' => $url));
  372. }
  373. $doc->{'contextUrl'} = $link_header->target;
  374. }
  375. // update document url based on redirects
  376. $redirs = count($redirects);
  377. if($redirs > 0) {
  378. $url = $redirects[$redirs - 1];
  379. }
  380. $doc->document = $result;
  381. $doc->documentUrl = $url;
  382. return $doc;
  383. }
  384. /**
  385. * The default implementation to retrieve JSON-LD at the given secure URL.
  386. *
  387. * @param string $url the secure URL to to retrieve.
  388. *
  389. * @return stdClass the RemoteDocument object.
  390. */
  391. function jsonld_default_secure_document_loader($url) {
  392. if(strpos($url, 'https') !== 0) {
  393. throw new JsonLdException(
  394. "Could not GET url: '$url'; 'https' is required.",
  395. 'jsonld.LoadDocumentError', 'loading document failed');
  396. }
  397. $doc = (object)array(
  398. 'contextUrl' => null, 'document' => null, 'documentUrl' => $url);
  399. $redirects = array();
  400. // default JSON-LD https GET implementation
  401. $opts = array(
  402. 'http' => array(
  403. 'method' => 'GET',
  404. 'header' =>
  405. "Accept: application/ld+json\r\n"),
  406. 'ssl' => array(
  407. 'verify_peer' => true,
  408. 'allow_self_signed' => false,
  409. 'cafile' => '/etc/ssl/certs/ca-certificates.crt'));
  410. $context = stream_context_create($opts);
  411. $content_type = null;
  412. stream_context_set_params($context, array('notification' =>
  413. function($notification_code, $severity, $message) use (
  414. &$redirects, &$content_type) {
  415. switch($notification_code) {
  416. case STREAM_NOTIFY_REDIRECTED:
  417. $redirects[] = $message;
  418. break;
  419. case STREAM_NOTIFY_MIME_TYPE_IS:
  420. $content_type = $message;
  421. break;
  422. };
  423. }));
  424. $result = @file_get_contents($url, false, $context);
  425. if($result === false) {
  426. throw new JsonLdException(
  427. 'Could not retrieve a JSON-LD document from the URL: ' + $url,
  428. 'jsonld.LoadDocumentError', 'loading document failed');
  429. }
  430. $link_header = array();
  431. foreach($http_response_header as $header) {
  432. if(strpos($header, 'link') === 0) {
  433. $value = explode(': ', $header);
  434. if(count($value) > 1) {
  435. $link_header[] = $value[1];
  436. }
  437. }
  438. }
  439. $link_header = jsonld_parse_link_header(join(',', $link_header));
  440. if(isset($link_header['http://www.w3.org/ns/json-ld#context'])) {
  441. $link_header = $link_header['http://www.w3.org/ns/json-ld#context'];
  442. } else {
  443. $link_header = null;
  444. }
  445. if($link_header && $content_type !== 'application/ld+json') {
  446. // only 1 related link header permitted
  447. if(is_array($link_header)) {
  448. throw new JsonLdException(
  449. 'URL could not be dereferenced, it has more than one ' .
  450. 'associated HTTP Link Header.', 'jsonld.LoadDocumentError',
  451. 'multiple context link headers', array('url' => $url));
  452. }
  453. $doc->{'contextUrl'} = $link_header->target;
  454. }
  455. // update document url based on redirects
  456. foreach($redirects as $redirect) {
  457. if(strpos($redirect, 'https') !== 0) {
  458. throw new JsonLdException(
  459. "Could not GET redirected url: '$redirect'; 'https' is required.",
  460. 'jsonld.LoadDocumentError', 'loading document failed');
  461. }
  462. $url = $redirect;
  463. }
  464. $doc->document = $result;
  465. $doc->documentUrl = $url;
  466. return $doc;
  467. }
  468. /** Registered global RDF dataset parsers hashed by content-type. */
  469. global $jsonld_rdf_parsers;
  470. $jsonld_rdf_parsers = new stdClass();
  471. /**
  472. * Registers a global RDF dataset parser by content-type, for use with
  473. * jsonld_from_rdf. Global parsers will be used by JsonLdProcessors that do
  474. * not register their own parsers.
  475. *
  476. * @param string $content_type the content-type for the parser.
  477. * @param callable $parser(input) the parser function (takes a string as
  478. * a parameter and returns an RDF dataset).
  479. */
  480. function jsonld_register_rdf_parser($content_type, $parser) {
  481. global $jsonld_rdf_parsers;
  482. $jsonld_rdf_parsers->{$content_type} = $parser;
  483. }
  484. /**
  485. * Unregisters a global RDF dataset parser by content-type.
  486. *
  487. * @param string $content_type the content-type for the parser.
  488. */
  489. function jsonld_unregister_rdf_parser($content_type) {
  490. global $jsonld_rdf_parsers;
  491. if(property_exists($jsonld_rdf_parsers, $content_type)) {
  492. unset($jsonld_rdf_parsers->{$content_type});
  493. }
  494. }
  495. /**
  496. * Parses a URL into its component parts.
  497. *
  498. * @param string $url the URL to parse.
  499. *
  500. * @return assoc the parsed URL.
  501. */
  502. function jsonld_parse_url($url) {
  503. if($url === null) {
  504. $url = '';
  505. }
  506. $keys = array(
  507. 'href', 'protocol', 'scheme', '?authority', 'authority',
  508. '?auth', 'auth', 'user', 'pass', 'host', '?port', 'port', 'path',
  509. '?query', 'query', '?fragment', 'fragment');
  510. $regex = "/^(([^:\/?#]+):)?(\/\/(((([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(:(\d*))?))?([^?#]*)(\?([^#]*))?(#(.*))?/";
  511. preg_match($regex, $url, $match);
  512. $rval = array();
  513. $flags = array();
  514. $len = count($keys);
  515. for($i = 0; $i < $len; ++$i) {
  516. $key = $keys[$i];
  517. if(strpos($key, '?') === 0) {
  518. $flags[substr($key, 1)] = !empty($match[$i]);
  519. } else if(!isset($match[$i])) {
  520. $rval[$key] = null;
  521. } else {
  522. $rval[$key] = $match[$i];
  523. }
  524. }
  525. if(!$flags['authority']) {
  526. $rval['authority'] = null;
  527. }
  528. if(!$flags['auth']) {
  529. $rval['auth'] = $rval['user'] = $rval['pass'] = null;
  530. }
  531. if(!$flags['port']) {
  532. $rval['port'] = null;
  533. }
  534. if(!$flags['query']) {
  535. $rval['query'] = null;
  536. }
  537. if(!$flags['fragment']) {
  538. $rval['fragment'] = null;
  539. }
  540. $rval['normalizedPath'] = jsonld_remove_dot_segments(
  541. $rval['path'], !!$rval['authority']);
  542. return $rval;
  543. }
  544. /**
  545. * Removes dot segments from a URL path.
  546. *
  547. * @param string $path the path to remove dot segments from.
  548. * @param bool $has_authority true if the URL has an authority, false if not.
  549. */
  550. function jsonld_remove_dot_segments($path, $has_authority) {
  551. $rval = '';
  552. if(strpos($path, '/') === 0) {
  553. $rval = '/';
  554. }
  555. // RFC 3986 5.2.4 (reworked)
  556. $input = explode('/', $path);
  557. $output = array();
  558. while(count($input) > 0) {
  559. if($input[0] === '.' || ($input[0] === '' && count($input) > 1)) {
  560. array_shift($input);
  561. continue;
  562. }
  563. if($input[0] === '..') {
  564. array_shift($input);
  565. if($has_authority ||
  566. (count($output) > 0 && $output[count($output) - 1] !== '..')) {
  567. array_pop($output);
  568. } else {
  569. // leading relative URL '..'
  570. $output[] = '..';
  571. }
  572. continue;
  573. }
  574. $output[] = array_shift($input);
  575. }
  576. return $rval . implode('/', $output);
  577. }
  578. /**
  579. * Prepends a base IRI to the given relative IRI.
  580. *
  581. * @param mixed $base a string or the parsed base IRI.
  582. * @param string $iri the relative IRI.
  583. *
  584. * @return string the absolute IRI.
  585. */
  586. function jsonld_prepend_base($base, $iri) {
  587. // skip IRI processing
  588. if($base === null) {
  589. return $iri;
  590. }
  591. // already an absolute IRI
  592. if(strpos($iri, ':') !== false) {
  593. return $iri;
  594. }
  595. // parse base if it is a string
  596. if(is_string($base)) {
  597. $base = jsonld_parse_url($base);
  598. }
  599. // parse given IRI
  600. $rel = jsonld_parse_url($iri);
  601. // per RFC3986 5.2.2
  602. $transform = array('protocol' => $base['protocol']);
  603. if($rel['authority'] !== null) {
  604. $transform['authority'] = $rel['authority'];
  605. $transform['path'] = $rel['path'];
  606. $transform['query'] = $rel['query'];
  607. } else {
  608. $transform['authority'] = $base['authority'];
  609. if($rel['path'] === '') {
  610. $transform['path'] = $base['path'];
  611. if($rel['query'] !== null) {
  612. $transform['query'] = $rel['query'];
  613. } else {
  614. $transform['query'] = $base['query'];
  615. }
  616. } else {
  617. if(strpos($rel['path'], '/') === 0) {
  618. // IRI represents an absolute path
  619. $transform['path'] = $rel['path'];
  620. } else {
  621. // merge paths
  622. $path = $base['path'];
  623. // append relative path to the end of the last directory from base
  624. if($rel['path'] !== '') {
  625. $idx = strrpos($path, '/');
  626. $idx = ($idx === false) ? 0 : $idx + 1;
  627. $path = substr($path, 0, $idx);
  628. if(strlen($path) > 0 && substr($path, -1) !== '/') {
  629. $path .= '/';
  630. }
  631. $path .= $rel['path'];
  632. }
  633. $transform['path'] = $path;
  634. }
  635. $transform['query'] = $rel['query'];
  636. }
  637. }
  638. // remove slashes and dots in path
  639. $transform['path'] = jsonld_remove_dot_segments(
  640. $transform['path'], !!$transform['authority']);
  641. // construct URL
  642. $rval = $transform['protocol'];
  643. if($transform['authority'] !== null) {
  644. $rval .= '//' . $transform['authority'];
  645. }
  646. $rval .= $transform['path'];
  647. if($transform['query'] !== null) {
  648. $rval .= '?' . $transform['query'];
  649. }
  650. if($rel['fragment'] !== null) {
  651. $rval .= '#' . $rel['fragment'];
  652. }
  653. // handle empty base
  654. if($rval === '') {
  655. $rval = './';
  656. }
  657. return $rval;
  658. }
  659. /**
  660. * Removes a base IRI from the given absolute IRI.
  661. *
  662. * @param mixed $base the base IRI.
  663. * @param string $iri the absolute IRI.
  664. *
  665. * @return string the relative IRI if relative to base, otherwise the absolute
  666. * IRI.
  667. */
  668. function jsonld_remove_base($base, $iri) {
  669. // skip IRI processing
  670. if($base === null) {
  671. return $iri;
  672. }
  673. if(is_string($base)) {
  674. $base = jsonld_parse_url($base);
  675. }
  676. // establish base root
  677. $root = '';
  678. if($base['href'] !== '') {
  679. $root .= "{$base['protocol']}//{$base['authority']}";
  680. } else if(strpos($iri, '//') === false) {
  681. // support network-path reference with empty base
  682. $root .= '//';
  683. }
  684. // IRI not relative to base
  685. if($root === '' || strpos($iri, $root) !== 0) {
  686. return $iri;
  687. }
  688. // remove root from IRI
  689. $rel = jsonld_parse_url(substr($iri, strlen($root)));
  690. // remove path segments that match (do not remove last segment unless there
  691. // is a hash or query)
  692. $base_segments = explode('/', $base['normalizedPath']);
  693. $iri_segments = explode('/', $rel['normalizedPath']);
  694. $last = ($rel['query'] || $rel['fragment']) ? 0 : 1;
  695. while(count($base_segments) > 0 && count($iri_segments) > $last) {
  696. if($base_segments[0] !== $iri_segments[0]) {
  697. break;
  698. }
  699. array_shift($base_segments);
  700. array_shift($iri_segments);
  701. }
  702. // use '../' for each non-matching base segment
  703. $rval = '';
  704. if(count($base_segments) > 0) {
  705. // don't count the last segment (if it ends with '/' last path doesn't
  706. // count and if it doesn't end with '/' it isn't a path)
  707. array_pop($base_segments);
  708. foreach($base_segments as $segment) {
  709. $rval .= '../';
  710. }
  711. }
  712. // prepend remaining segments
  713. $rval .= implode('/', $iri_segments);
  714. // add query and hash
  715. if($rel['query'] !== null) {
  716. $rval .= "?{$rel['query']}";
  717. }
  718. if($rel['fragment'] !== null) {
  719. $rval .= "#{$rel['fragment']}";
  720. }
  721. if($rval === '') {
  722. $rval = './';
  723. }
  724. return $rval;
  725. }
  726. /**
  727. * A JSON-LD processor.
  728. */
  729. class JsonLdProcessor {
  730. /** XSD constants */
  731. const XSD_BOOLEAN = 'http://www.w3.org/2001/XMLSchema#boolean';
  732. const XSD_DOUBLE = 'http://www.w3.org/2001/XMLSchema#double';
  733. const XSD_INTEGER = 'http://www.w3.org/2001/XMLSchema#integer';
  734. const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
  735. /** RDF constants */
  736. const RDF_LIST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#List';
  737. const RDF_FIRST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first';
  738. const RDF_REST = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest';
  739. const RDF_NIL = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil';
  740. const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
  741. const RDF_LANGSTRING =
  742. 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString';
  743. /** Restraints */
  744. const MAX_CONTEXT_URLS = 10;
  745. /** Processor-specific RDF dataset parsers. */
  746. protected $rdfParsers = null;
  747. /**
  748. * Constructs a JSON-LD processor.
  749. */
  750. public function __construct() {}
  751. /**
  752. * Performs JSON-LD compaction.
  753. *
  754. * @param mixed $input the JSON-LD object to compact.
  755. * @param mixed $ctx the context to compact with.
  756. * @param assoc $options the compaction options.
  757. * [base] the base IRI to use.
  758. * [compactArrays] true to compact arrays to single values when
  759. * appropriate, false not to (default: true).
  760. * [graph] true to always output a top-level graph (default: false).
  761. * [skipExpansion] true to assume the input is expanded and skip
  762. * expansion, false not to, defaults to false.
  763. * [activeCtx] true to also return the active context used.
  764. * [documentLoader(url)] the document loader.
  765. *
  766. * @return mixed the compacted JSON-LD output.
  767. */
  768. public function compact($input, $ctx, $options) {
  769. global $jsonld_default_load_document;
  770. if($ctx === null) {
  771. throw new JsonLdException(
  772. 'The compaction context must not be null.',
  773. 'jsonld.CompactError', 'invalid local context');
  774. }
  775. // nothing to compact
  776. if($input === null) {
  777. return null;
  778. }
  779. self::setdefaults($options, array(
  780. 'base' => is_string($input) ? $input : '',
  781. 'compactArrays' => true,
  782. 'graph' => false,
  783. 'skipExpansion' => false,
  784. 'activeCtx' => false,
  785. 'documentLoader' => $jsonld_default_load_document,
  786. 'link' => false));
  787. if($options['link']) {
  788. // force skip expansion when linking, "link" is not part of the
  789. // public API, it should only be called from framing
  790. $options['skipExpansion'] = true;
  791. }
  792. if($options['skipExpansion'] === true) {
  793. $expanded = $input;
  794. } else {
  795. // expand input
  796. try {
  797. $expanded = $this->expand($input, $options);
  798. } catch(JsonLdException $e) {
  799. throw new JsonLdException(
  800. 'Could not expand input before compaction.',
  801. 'jsonld.CompactError', null, null, $e);
  802. }
  803. }
  804. // process context
  805. $active_ctx = $this->_getInitialContext($options);
  806. try {
  807. $active_ctx = $this->processContext($active_ctx, $ctx, $options);
  808. } catch(JsonLdException $e) {
  809. throw new JsonLdException(
  810. 'Could not process context before compaction.',
  811. 'jsonld.CompactError', null, null, $e);
  812. }
  813. // do compaction
  814. $compacted = $this->_compact($active_ctx, null, $expanded, $options);
  815. if($options['compactArrays'] &&
  816. !$options['graph'] && is_array($compacted)) {
  817. if(count($compacted) === 1) {
  818. // simplify to a single item
  819. $compacted = $compacted[0];
  820. } else if(count($compacted) === 0) {
  821. // simplify to an empty object
  822. $compacted = new stdClass();
  823. }
  824. } else if($options['graph']) {
  825. // always use array if graph option is on
  826. $compacted = self::arrayify($compacted);
  827. }
  828. // follow @context key
  829. if(is_object($ctx) && property_exists($ctx, '@context')) {
  830. $ctx = $ctx->{'@context'};
  831. }
  832. // build output context
  833. $ctx = self::copy($ctx);
  834. $ctx = self::arrayify($ctx);
  835. // remove empty contexts
  836. $tmp = $ctx;
  837. $ctx = array();
  838. foreach($tmp as $v) {
  839. if(!is_object($v) || count(array_keys((array)$v)) > 0) {
  840. $ctx[] = $v;
  841. }
  842. }
  843. // remove array if only one context
  844. $ctx_length = count($ctx);
  845. $has_context = ($ctx_length > 0);
  846. if($ctx_length === 1) {
  847. $ctx = $ctx[0];
  848. }
  849. // add context and/or @graph
  850. if(is_array($compacted)) {
  851. // use '@graph' keyword
  852. $kwgraph = $this->_compactIri($active_ctx, '@graph');
  853. $graph = $compacted;
  854. $compacted = new stdClass();
  855. if($has_context) {
  856. $compacted->{'@context'} = $ctx;
  857. }
  858. $compacted->{$kwgraph} = $graph;
  859. } else if(is_object($compacted) && $has_context) {
  860. // reorder keys so @context is first
  861. $graph = $compacted;
  862. $compacted = new stdClass();
  863. $compacted->{'@context'} = $ctx;
  864. foreach($graph as $k => $v) {
  865. $compacted->{$k} = $v;
  866. }
  867. }
  868. if($options['activeCtx']) {
  869. return array('compacted' => $compacted, 'activeCtx' => $active_ctx);
  870. }
  871. return $compacted;
  872. }
  873. /**
  874. * Performs JSON-LD expansion.
  875. *
  876. * @param mixed $input the JSON-LD object to expand.
  877. * @param assoc $options the options to use:
  878. * [base] the base IRI to use.
  879. * [expandContext] a context to expand with.
  880. * [keepFreeFloatingNodes] true to keep free-floating nodes,
  881. * false not to, defaults to false.
  882. * [documentLoader(url)] the document loader.
  883. *
  884. * @return array the expanded JSON-LD output.
  885. */
  886. public function expand($input, $options) {
  887. global $jsonld_default_load_document;
  888. self::setdefaults($options, array(
  889. 'keepFreeFloatingNodes' => false,
  890. 'documentLoader' => $jsonld_default_load_document));
  891. // if input is a string, attempt to dereference remote document
  892. if(is_string($input)) {
  893. $remote_doc = call_user_func($options['documentLoader'], $input);
  894. } else {
  895. $remote_doc = (object)array(
  896. 'contextUrl' => null,
  897. 'documentUrl' => null,
  898. 'document' => $input);
  899. }
  900. try {
  901. if($remote_doc->document === null) {
  902. throw new JsonLdException(
  903. 'No remote document found at the given URL.',
  904. 'jsonld.NullRemoteDocument');
  905. }
  906. if(is_string($remote_doc->document)) {
  907. $remote_doc->document = self::_parse_json($remote_doc->document);
  908. }
  909. } catch(Exception $e) {
  910. throw new JsonLdException(
  911. 'Could not retrieve a JSON-LD document from the URL.',
  912. 'jsonld.LoadDocumentError', 'loading document failed',
  913. array('remoteDoc' => $remote_doc), $e);
  914. }
  915. // set default base
  916. self::setdefault($options, 'base', $remote_doc->documentUrl ?: '');
  917. // build meta-object and retrieve all @context urls
  918. $input = (object)array(
  919. 'document' => self::copy($remote_doc->document),
  920. 'remoteContext' => (object)array(
  921. '@context' => $remote_doc->contextUrl));
  922. if(isset($options['expandContext'])) {
  923. $expand_context = self::copy($options['expandContext']);
  924. if(is_object($expand_context) &&
  925. property_exists($expand_context, '@context')) {
  926. $input->expandContext = $expand_context;
  927. } else {
  928. $input->expandContext = (object)array('@context' => $expand_context);
  929. }
  930. }
  931. // retrieve all @context URLs in the input
  932. try {
  933. $this->_retrieveContextUrls(
  934. $input, new stdClass(), $options['documentLoader'], $options['base']);
  935. } catch(Exception $e) {
  936. throw new JsonLdException(
  937. 'Could not perform JSON-LD expansion.',
  938. 'jsonld.ExpandError', null, null, $e);
  939. }
  940. $active_ctx = $this->_getInitialContext($options);
  941. $document = $input->document;
  942. $remote_context = $input->remoteContext->{'@context'};
  943. // process optional expandContext
  944. if(property_exists($input, 'expandContext')) {
  945. $active_ctx = self::_processContext(
  946. $active_ctx, $input->expandContext, $options);
  947. }
  948. // process remote context from HTTP Link Header
  949. if($remote_context) {
  950. $active_ctx = self::_processContext(
  951. $active_ctx, $remote_context, $options);
  952. }
  953. // do expansion
  954. $expanded = $this->_expand($active_ctx, null, $document, $options, false);
  955. // optimize away @graph with no other properties
  956. if(is_object($expanded) && property_exists($expanded, '@graph') &&
  957. count(array_keys((array)$expanded)) === 1) {
  958. $expanded = $expanded->{'@graph'};
  959. } else if($expanded === null) {
  960. $expanded = array();
  961. }
  962. // normalize to an array
  963. return self::arrayify($expanded);
  964. }
  965. /**
  966. * Performs JSON-LD flattening.
  967. *
  968. * @param mixed $input the JSON-LD to flatten.
  969. * @param ctx the context to use to compact the flattened output, or null.
  970. * @param assoc $options the options to use:
  971. * [base] the base IRI to use.
  972. * [expandContext] a context to expand with.
  973. * [documentLoader(url)] the document loader.
  974. *
  975. * @return array the flattened output.
  976. */
  977. public function flatten($input, $ctx, $options) {
  978. global $jsonld_default_load_document;
  979. self::setdefaults($options, array(
  980. 'base' => is_string($input) ? $input : '',
  981. 'documentLoader' => $jsonld_default_load_document));
  982. try {
  983. // expand input
  984. $expanded = $this->expand($input, $options);
  985. } catch(Exception $e) {
  986. throw new JsonLdException(
  987. 'Could not expand input before flattening.',
  988. 'jsonld.FlattenError', null, null, $e);
  989. }
  990. // do flattening
  991. $flattened = $this->_flatten($expanded);
  992. if($ctx === null) {
  993. return $flattened;
  994. }
  995. // compact result (force @graph option to true, skip expansion)
  996. $options['graph'] = true;
  997. $options['skipExpansion'] = true;
  998. try {
  999. $compacted = $this->compact($flattened, $ctx, $options);
  1000. } catch(Exception $e) {
  1001. throw new JsonLdException(
  1002. 'Could not compact flattened output.',
  1003. 'jsonld.FlattenError', null, null, $e);
  1004. }
  1005. return $compacted;
  1006. }
  1007. /**
  1008. * Performs JSON-LD framing.
  1009. *
  1010. * @param mixed $input the JSON-LD object to frame.
  1011. * @param stdClass $frame the JSON-LD frame to use.
  1012. * @param $options the framing options.
  1013. * [base] the base IRI to use.
  1014. * [expandContext] a context to expand with.
  1015. * [embed] default @embed flag: '@last', '@always', '@never', '@link'
  1016. * (default: '@last').
  1017. * [explicit] default @explicit flag (default: false).
  1018. * [requireAll] default @requireAll flag (default: true).
  1019. * [omitDefault] default @omitDefault flag (default: false).
  1020. * [documentLoader(url)] the document loader.
  1021. *
  1022. * @return stdClass the framed JSON-LD output.
  1023. */
  1024. public function frame($input, $frame, $options) {
  1025. global $jsonld_default_load_document;
  1026. self::setdefaults($options, array(
  1027. 'base' => is_string($input) ? $input : '',
  1028. 'compactArrays' => true,
  1029. 'embed' => '@last',
  1030. 'explicit' => false,
  1031. 'requireAll' => true,
  1032. 'omitDefault' => false,
  1033. 'documentLoader' => $jsonld_default_load_document));
  1034. // if frame is a string, attempt to dereference remote document
  1035. if(is_string($frame)) {
  1036. $remote_frame = call_user_func($options['documentLoader'], $frame);
  1037. } else {
  1038. $remote_frame = (object)array(
  1039. 'contextUrl' => null,
  1040. 'documentUrl' => null,
  1041. 'document' => $frame);
  1042. }
  1043. try {
  1044. if($remote_frame->document === null) {
  1045. throw new JsonLdException(
  1046. 'No remote document found at the given URL.',
  1047. 'jsonld.NullRemoteDocument');
  1048. }
  1049. if(is_string($remote_frame->document)) {
  1050. $remote_frame->document = self::_parse_json($remote_frame->document);
  1051. }
  1052. } catch(Exception $e) {
  1053. throw new JsonLdException(
  1054. 'Could not retrieve a JSON-LD document from the URL.',
  1055. 'jsonld.LoadDocumentError', 'loading document failed',
  1056. array('remoteDoc' => $remote_frame), $e);
  1057. }
  1058. // preserve frame context
  1059. $frame = $remote_frame->document;
  1060. if($frame !== null) {
  1061. $ctx = (property_exists($frame, '@context') ?
  1062. $frame->{'@context'} : new stdClass());
  1063. if($remote_frame->contextUrl !== null) {
  1064. if($ctx !== null) {
  1065. $ctx = $remote_frame->contextUrl;
  1066. } else {
  1067. $ctx = self::arrayify($ctx);
  1068. $ctx[] = $remote_frame->contextUrl;
  1069. }
  1070. $frame->{'@context'} = $ctx;
  1071. }
  1072. }
  1073. try {
  1074. // expand input
  1075. $expanded = $this->expand($input, $options);
  1076. } catch(Exception $e) {
  1077. throw new JsonLdException(
  1078. 'Could not expand input before framing.',
  1079. 'jsonld.FrameError', null, null, $e);
  1080. }
  1081. try {
  1082. // expand frame
  1083. $opts = $options;
  1084. $opts['keepFreeFloatingNodes'] = true;
  1085. $expanded_frame = $this->expand($frame, $opts);
  1086. } catch(Exception $e) {
  1087. throw new JsonLdException(
  1088. 'Could not expand frame before framing.',
  1089. 'jsonld.FrameError', null, null, $e);
  1090. }
  1091. // do framing
  1092. $framed = $this->_frame($expanded, $expanded_frame, $options);
  1093. try {
  1094. // compact result (force @graph option to true, skip expansion, check
  1095. // for linked embeds)
  1096. $options['graph'] = true;
  1097. $options['skipExpansion'] = true;
  1098. $options['link'] = new ArrayObject();
  1099. $options['activeCtx'] = true;
  1100. $result = $this->compact($framed, $ctx, $options);
  1101. } catch(Exception $e) {
  1102. throw new JsonLdException(
  1103. 'Could not compact framed output.',
  1104. 'jsonld.FrameError', null, null, $e);
  1105. }
  1106. $compacted = $result['compacted'];
  1107. $active_ctx = $result['activeCtx'];
  1108. // get graph alias
  1109. $graph = $this->_compactIri($active_ctx, '@graph');
  1110. // remove @preserve from results
  1111. $options['link'] = new ArrayObject();
  1112. $compacted->{$graph} = $this->_removePreserve(
  1113. $active_ctx, $compacted->{$graph}, $options);
  1114. return $compacted;
  1115. }
  1116. /**
  1117. * Performs JSON-LD normalization.
  1118. *
  1119. * @param mixed $input the JSON-LD object to normalize.
  1120. * @param assoc $options the options to use:
  1121. * [base] the base IRI to use.
  1122. * [expandContext] a context to expand with.
  1123. * [inputFormat] the format if input is not JSON-LD:
  1124. * 'application/nquads' for N-Quads.
  1125. * [format] the format if output is a string:
  1126. * 'application/nquads' for N-Quads.
  1127. * [documentLoader(url)] the document loader.
  1128. *
  1129. * @return mixed the normalized output.
  1130. */
  1131. public function normalize($input, $options) {
  1132. global $jsonld_default_load_document;
  1133. self::setdefaults($options, array(
  1134. 'base' => is_string($input) ? $input : '',
  1135. 'documentLoader' => $jsonld_default_load_document));
  1136. if(isset($options['inputFormat'])) {
  1137. if($options['inputFormat'] != 'application/nquads') {
  1138. throw new JsonLdException(
  1139. 'Unknown normalization input format.', 'jsonld.NormalizeError');
  1140. }
  1141. $dataset = $this->parseNQuads($input);
  1142. } else {
  1143. try {
  1144. // convert to RDF dataset then do normalization
  1145. $opts = $options;
  1146. if(isset($opts['format'])) {
  1147. unset($opts['format']);
  1148. }
  1149. $opts['produceGeneralizedRdf'] = false;
  1150. $dataset = $this->toRDF($input, $opts);
  1151. } catch(Exception $e) {
  1152. throw new JsonLdException(
  1153. 'Could not convert input to RDF dataset before normalization.',
  1154. 'jsonld.NormalizeError', null, null, $e);
  1155. }
  1156. }
  1157. // do normalization
  1158. return $this->_normalize($dataset, $options);
  1159. }
  1160. /**
  1161. * Converts an RDF dataset to JSON-LD.
  1162. *
  1163. * @param mixed $dataset a serialized string of RDF in a format specified
  1164. * by the format option or an RDF dataset to convert.
  1165. * @param assoc $options the options to use:
  1166. * [format] the format if input is a string:
  1167. * 'application/nquads' for N-Quads (default).
  1168. * [useRdfType] true to use rdf:type, false to use @type
  1169. * (default: false).
  1170. * [useNativeTypes] true to convert XSD types into native types
  1171. * (boolean, integer, double), false not to (default: false).
  1172. *
  1173. * @return array the JSON-LD output.
  1174. */
  1175. public function fromRDF($dataset, $options) {
  1176. global $jsonld_rdf_parsers;
  1177. self::setdefaults($options, array(
  1178. 'useRdfType' => false,
  1179. 'useNativeTypes' => false));
  1180. if(!isset($options['format']) && is_string($dataset)) {
  1181. // set default format to nquads
  1182. $options['format'] = 'application/nquads';
  1183. }
  1184. // handle special format
  1185. if(isset($options['format']) && $options['format']) {
  1186. // supported formats (processor-specific and global)
  1187. if(($this->rdfParsers !== null &&
  1188. !property_exists($this->rdfParsers, $options['format'])) ||
  1189. $this->rdfParsers === null &&
  1190. !property_exists($jsonld_rdf_parsers, $options['format'])) {
  1191. throw new JsonLdException(
  1192. 'Unknown input format.',
  1193. 'jsonld.UnknownFormat', null, array('format' => $options['format']));
  1194. }
  1195. if($this->rdfParsers !== null) {
  1196. $callable = $this->rdfParsers->{$options['format']};
  1197. } else {
  1198. $callable = $jsonld_rdf_parsers->{$options['format']};
  1199. }
  1200. $dataset = call_user_func($callable, $dataset);
  1201. }
  1202. // convert from RDF
  1203. return $this->_fromRDF($dataset, $options);
  1204. }
  1205. /**
  1206. * Outputs the RDF dataset found in the given JSON-LD object.
  1207. *
  1208. * @param mixed $input the JSON-LD object.
  1209. * @param assoc $options the options to use:
  1210. * [base] the base IRI to use.
  1211. * [expandContext] a context to expand with.
  1212. * [format] the format to use to output a string:
  1213. * 'application/nquads' for N-Quads.
  1214. * [produceGeneralizedRdf] true to output generalized RDF, false
  1215. * to produce only standard RDF (default: false).
  1216. * [documentLoader(url)] the document loader.
  1217. *
  1218. * @return mixed the resulting RDF dataset (or a serialization of it).
  1219. */
  1220. public function toRDF($input, $options) {
  1221. global $jsonld_default_load_document;
  1222. self::setdefaults($options, array(
  1223. 'base' => is_string($input) ? $input : '',
  1224. 'produceGeneralizedRdf' => false,
  1225. 'documentLoader' => $jsonld_default_load_document));
  1226. try {
  1227. // expand input
  1228. $expanded = $this->expand($input, $options);
  1229. } catch(JsonLdException $e) {
  1230. throw new JsonLdException(
  1231. 'Could not expand input before serialization to RDF.',
  1232. 'jsonld.RdfError', null, null, $e);
  1233. }
  1234. // create node map for default graph (and any named graphs)
  1235. $namer = new UniqueNamer('_:b');
  1236. $node_map = (object)array('@default' => new stdClass());
  1237. $this->_createNodeMap($expanded, $node_map, '@default', $namer);
  1238. // output RDF dataset
  1239. $dataset = new stdClass();
  1240. $graph_names = array_keys((array)$node_map);
  1241. sort($graph_names);
  1242. foreach($graph_names as $graph_name) {
  1243. $graph = $node_map->{$graph_name};
  1244. // skip relative IRIs
  1245. if($graph_name === '@default' || self::_isAbsoluteIri($graph_name)) {
  1246. $dataset->{$graph_name} = $this->_graphToRDF($graph, $namer, $options);
  1247. }
  1248. }
  1249. $rval = $dataset;
  1250. // convert to output format
  1251. if(isset($options['format']) && $options['format']) {
  1252. // supported formats
  1253. if($options['format'] === 'application/nquads') {
  1254. $rval = self::toNQuads($dataset);
  1255. } else {
  1256. throw new JsonLdException(
  1257. 'Unknown output format.', 'jsonld.UnknownFormat',
  1258. null, array('format' => $options['format']));
  1259. }
  1260. }
  1261. return $rval;
  1262. }
  1263. /**
  1264. * Processes a local context, resolving any URLs as necessary, and returns a
  1265. * new active context in its callback.
  1266. *
  1267. * @param stdClass $active_ctx the current active context.
  1268. * @param mixed $local_ctx the local context to process.
  1269. * @param assoc $options the options to use:
  1270. * [documentLoader(url)] the document loader.
  1271. *
  1272. * @return stdClass the new active context.
  1273. */
  1274. public function processContext($active_ctx, $local_ctx, $options) {
  1275. global $jsonld_default_load_document;
  1276. self::setdefaults($options, array(
  1277. 'base' => '',
  1278. 'documentLoader' => $jsonld_default_load_document));
  1279. // return initial context early for null context
  1280. if($local_ctx === null) {
  1281. return $this->_getInitialContext($options);
  1282. }
  1283. // retrieve URLs in local_ctx
  1284. $local_ctx = self::copy($local_ctx);
  1285. if(is_string($local_ctx) or (
  1286. is_object($local_ctx) && !property_exists($local_ctx, '@context'))) {
  1287. $local_ctx = (object)array('@context' => $local_ctx);
  1288. }
  1289. try {
  1290. $this->_retrieveContextUrls(
  1291. $local_ctx, new stdClass(),
  1292. $options['documentLoader'], $options['base']);
  1293. } catch(Exception $e) {
  1294. throw new JsonLdException(
  1295. 'Could not process JSON-LD context.',
  1296. 'jsonld.ContextError', null, null, $e);
  1297. }
  1298. // process context
  1299. return $this->_processContext($active_ctx, $local_ctx, $options);
  1300. }
  1301. /**
  1302. * Returns true if the given subject has the given property.
  1303. *
  1304. * @param stdClass $subject the subject to check.
  1305. * @param string $property the property to look for.
  1306. *
  1307. * @return bool true if the subject has the given property, false if not.
  1308. */
  1309. public static function hasProperty($subject, $property) {
  1310. $rval = false;
  1311. if(property_exists($subject, $property)) {
  1312. $value = $subject->{$property};
  1313. $rval = (!is_array($value) || count($value) > 0);
  1314. }
  1315. return $rval;
  1316. }
  1317. /**
  1318. * Determines if the given value is a property of the given subject.
  1319. *
  1320. * @param stdClass $subject the subject to check.
  1321. * @param string $property the property to check.
  1322. * @param mixed $value the value to check.
  1323. *
  1324. * @return bool true if the value exists, false if not.
  1325. */
  1326. public static function hasValue($subject, $property, $value) {
  1327. $rval = false;
  1328. if(self::hasProperty($subject, $property)) {
  1329. $val = $subject->{$property};
  1330. $is_list = self::_isList($val);
  1331. if(is_array($val) || $is_list) {
  1332. if($is_list) {
  1333. $val = $val->{'@list'};
  1334. }
  1335. foreach($val as $v) {
  1336. if(self::compareValues($value, $v)) {
  1337. $rval = true;
  1338. break;
  1339. }
  1340. }
  1341. } else if(!is_array($value)) {
  1342. // avoid matching the set of values with an array value parameter
  1343. $rval = self::compareValues($value, $val);
  1344. }
  1345. }
  1346. return $rval;
  1347. }
  1348. /**
  1349. * Adds a value to a subject. If the value is an array, all values in the
  1350. * array will be added.
  1351. *
  1352. * Note: If the value is a subject that already exists as a property of the
  1353. * given subject, this method makes no attempt to deeply merge properties.
  1354. * Instead, the value will not be added.
  1355. *
  1356. * @param stdClass $subject the subject to add the value to.
  1357. * @param string $property the property that relates the value to the subject.
  1358. * @param mixed $value the value to add.
  1359. * @param assoc [$options] the options to use:
  1360. * [propertyIsArray] true if the property is always an array, false
  1361. * if not (default: false).
  1362. * [allowDuplicate] true to allow duplicates, false not to (uses a
  1363. * simple shallow comparison of subject ID or value)
  1364. * (default: true).
  1365. */
  1366. public static function addValue(
  1367. $subject, $property, $value, $options=array()) {
  1368. self::setdefaults($options, array(
  1369. 'allowDuplicate' => true,
  1370. 'propertyIsArray' => false));
  1371. if(is_array($value)) {
  1372. if(count($value) === 0 && $options['propertyIsArray'] &&
  1373. !property_exists($subject, $property)) {
  1374. $subject->{$property} = array();
  1375. }
  1376. foreach($value as $v) {
  1377. self::addValue($subject, $property, $v, $options);
  1378. }
  1379. } else if(property_exists($subject, $property)) {
  1380. // check if subject already has value if duplicates not allowed
  1381. $has_value = (!$options['allowDuplicate'] &&
  1382. self::hasValue($subject, $property, $value));
  1383. // make property an array if value not present or always an array
  1384. if(!is_array($subject->{$property}) &&
  1385. (!$has_value || $options['propertyIsArray'])) {
  1386. $subject->{$property} = array($subject->{$property});
  1387. }
  1388. // add new value
  1389. if(!$has_value) {
  1390. $subject->{$property}[] = $value;
  1391. }
  1392. } else {
  1393. // add new value as set or single value
  1394. $subject->{$property} = ($options['propertyIsArray'] ?
  1395. array($value) : $value);
  1396. }
  1397. }
  1398. /**
  1399. * Gets all of the values for a subject's property as an array.
  1400. *
  1401. * @param stdClass $subject the subject.
  1402. * @param string $property the property.
  1403. *
  1404. * @return array all of the values for a subject's property as an array.
  1405. */
  1406. public static function getValues($subject, $property) {
  1407. $rval = (property_exists($subject, $property) ?
  1408. $subject->{$property} : array());
  1409. return self::arrayify($rval);
  1410. }
  1411. /**
  1412. * Removes a property from a subject.
  1413. *
  1414. * @param stdClass $subject the subject.
  1415. * @param string $property the property.
  1416. */
  1417. public static function removeProperty($subject, $property) {
  1418. unset($subject->{$property});
  1419. }
  1420. /**
  1421. * Removes a value from a subject.
  1422. *
  1423. * @param stdClass $subject the subject.
  1424. * @param string $property the property that relates the value to the subject.
  1425. * @param mixed $value the value to remove.
  1426. * @param assoc [$options] the options to use:
  1427. * [propertyIsArray] true if the property is always an array,
  1428. * false if not (default: false).
  1429. */
  1430. public static function removeValue(
  1431. $subject, $property, $value, $options=array()) {
  1432. self::setdefaults($options, array(
  1433. 'propertyIsArray' => false));
  1434. // filter out value
  1435. $filter = function($e) use ($value) {
  1436. return !sel

Large files files are truncated, but you can click here to view the full file