RDFParser.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * This file is part of the PHPExiftool package.
  4. *
  5. * (c) Alchemy <support@alchemy.fr>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace PHPExiftool;
  11. use PHPExiftool\Driver\TagInterface;
  12. use PHPExiftool\Driver\TagFactory;
  13. use PHPExiftool\Driver\Metadata\Metadata;
  14. use PHPExiftool\Driver\Metadata\MetadataBag;
  15. use PHPExiftool\Driver\Value\Binary;
  16. use PHPExiftool\Driver\Value\Mono;
  17. use PHPExiftool\Driver\Value\Multi;
  18. use PHPExiftool\Driver\Value\ValueInterface;
  19. use PHPExiftool\Exception\LogicException;
  20. use PHPExiftool\Exception\ParseError;
  21. use PHPExiftool\Exception\RuntimeException;
  22. use PHPExiftool\Exception\TagUnknown;
  23. use Doctrine\Common\Collections\ArrayCollection;
  24. /**
  25. * Exiftool RDF output Parser
  26. *
  27. * @author Romain Neutron <imprec@gmail.com>
  28. */
  29. class RDFParser
  30. {
  31. /**
  32. * RDF Namespace
  33. */
  34. const RDF_NAMESPACE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
  35. protected $XML;
  36. protected $DOM;
  37. protected $DOMXpath;
  38. protected $registeredPrefixes;
  39. public function __destruct()
  40. {
  41. $this->close();
  42. }
  43. /**
  44. * Opens an XML file for parsing
  45. *
  46. * @param string $XML
  47. * @return RDFParser
  48. */
  49. public function open($XML)
  50. {
  51. $this->close();
  52. $this->XML = $XML;
  53. return $this;
  54. }
  55. /**
  56. * Close the current opened XML file and reset internals
  57. *
  58. * @return RDFParser
  59. */
  60. public function close()
  61. {
  62. $this->XML = null;
  63. $this->DOMXpath = null;
  64. $this->DOM = null;
  65. $this->registeredPrefixes = array();
  66. return $this;
  67. }
  68. /**
  69. * Parse a XML string and returns an ArrayCollection of FileEntity
  70. *
  71. * @return ArrayCollection
  72. */
  73. public function ParseEntities()
  74. {
  75. /**
  76. * A default Exiftool XML can contains many RDF Descriptions
  77. */
  78. $Entities = new ArrayCollection();
  79. foreach ($this->getDomXpath()->query('/rdf:RDF/rdf:Description') as $node) {
  80. /**
  81. * Let's create a DOMDocument containing a single RDF result
  82. */
  83. $Dom = new \DOMDocument();
  84. $DomRootElement = $Dom->createElementNS(self::RDF_NAMESPACE, 'rdf:RDF');
  85. $DomRootElement->appendChild($Dom->importNode($node, true));
  86. $Dom->appendChild($DomRootElement);
  87. $LocalXpath = new \DOMXPath($Dom);
  88. $LocalXpath->registerNamespace('rdf', self::RDF_NAMESPACE);
  89. $RDFDescriptionRoot = $LocalXpath->query('/rdf:RDF/rdf:Description');
  90. /**
  91. * Let's associate a Description to the corresponding file
  92. */
  93. $file = $RDFDescriptionRoot->item(0)->getAttribute('rdf:about');
  94. $Entities->set($file, new FileEntity($file, $Dom, new static()));
  95. }
  96. return $Entities;
  97. }
  98. /**
  99. * Parse an Entity associated DOM, returns the metadatas
  100. *
  101. * @return MetadataBag
  102. */
  103. public function ParseMetadatas()
  104. {
  105. $nodes = $this->getDomXpath()->query('/rdf:RDF/rdf:Description/*');
  106. $metadatas = new MetadataBag();
  107. foreach ($nodes as $node) {
  108. $tagname = $this->normalize($node->nodeName);
  109. try {
  110. $tag = TagFactory::getFromRDFTagname($tagname);
  111. } catch (TagUnknown $e) {
  112. continue;
  113. }
  114. $metaValue = $this->readNodeValue($node, $tag);
  115. $metadata = new Metadata($tag, $metaValue);
  116. $metadatas->set($tagname, $metadata);
  117. }
  118. return $metadatas;
  119. }
  120. /**
  121. * Returns the first result for a user defined query against the RDF
  122. *
  123. * @param string $query
  124. * @return ValueInterface The value
  125. */
  126. public function Query($query)
  127. {
  128. $QueryParts = explode(':', $query);
  129. $DomXpath = $this->getDomXpath();
  130. if ( ! in_array($QueryParts[0], $this->registeredPrefixes)) {
  131. return null;
  132. }
  133. $nodes = $DomXpath->query('/rdf:RDF/rdf:Description/' . $query);
  134. if ($nodes instanceof \DOMNodeList && $nodes->length > 0) {
  135. return $this->readNodeValue($nodes->item(0));
  136. }
  137. return null;
  138. }
  139. /**
  140. * Normalize a tagname based on namespaces redirections
  141. *
  142. * @param string $tagname The tagname to normalize
  143. * @return string The normalized tagname
  144. */
  145. protected function normalize($tagname)
  146. {
  147. static $namespacesRedirection = array(
  148. 'CIFF' => array('Canon', 'CanonRaw'),
  149. );
  150. foreach ($namespacesRedirection as $from => $to) {
  151. if (strpos($tagname, $from . ':') !== 0) {
  152. continue;
  153. }
  154. foreach ((array) $to as $substit) {
  155. $supposedTagname = str_replace($from . ':', $substit . ':', $tagname);
  156. if (TagFactory::hasFromRDFTagname($supposedTagname)) {
  157. return $supposedTagname;
  158. }
  159. }
  160. }
  161. return $tagname;
  162. }
  163. /**
  164. * Extract all XML namespaces declared in a XML
  165. *
  166. * @param \DOMDocument $dom
  167. * @return array The namespaces declared in XML
  168. */
  169. protected static function getNamespacesFromXml(\DOMDocument $dom)
  170. {
  171. $namespaces = array();
  172. $XML = $dom->saveXML();
  173. $pattern = "(xmlns:([a-zA-Z-_0-9]+)=['|\"]{1}(https?:[/{2,4}|\\{2,4}][\w:#%/;$()~_?/\-=\\\.&]*)['|\"]{1})";
  174. preg_match_all($pattern, $XML, $matches, PREG_PATTERN_ORDER, 0);
  175. foreach ($matches[2] as $key => $value) {
  176. $namespaces[$matches[1][$key]] = $value;
  177. }
  178. return $namespaces;
  179. }
  180. /**
  181. * Read the node value, decode it if needed
  182. *
  183. * @param \DOMNode $node The node to read
  184. * @param TagInterface $tag The tag associated
  185. * @return ValueInterface The value extracted
  186. */
  187. protected function readNodeValue(\DOMNode $node, TagInterface $tag = null)
  188. {
  189. $nodeName = $this->normalize($node->nodeName);
  190. if (is_null($tag) && TagFactory::hasFromRDFTagname($nodeName)) {
  191. $tag = TagFactory::getFromRDFTagname($nodeName);
  192. }
  193. if ($node->getElementsByTagNameNS(self::RDF_NAMESPACE, 'Bag')->length > 0) {
  194. $ret = array();
  195. foreach ($node->getElementsByTagNameNS(self::RDF_NAMESPACE, 'li') as $nodeElement) {
  196. $ret[] = $nodeElement->nodeValue;
  197. }
  198. if (is_null($tag) || $tag->isMulti()) {
  199. return new Multi($ret);
  200. } else {
  201. return new Mono(implode(' ', $ret));
  202. }
  203. } elseif ($node->getAttribute('rdf:datatype') === 'http://www.w3.org/2001/XMLSchema#base64Binary') {
  204. if (is_null($tag) || $tag->isBinary()) {
  205. return Binary::loadFromBase64(trim($node->nodeValue));
  206. } else {
  207. return new Mono(base64_decode(trim($node->nodeValue)));
  208. }
  209. } else {
  210. if ( ! is_null($tag) && $tag->isMulti()) {
  211. return new Multi($node->nodeValue);
  212. } else {
  213. return new Mono($node->nodeValue);
  214. }
  215. }
  216. }
  217. /**
  218. * Compute the DOMDocument from the XML
  219. *
  220. * @return \DOMDocument
  221. * @throws LogicException
  222. * @throws ParseError
  223. */
  224. protected function getDom()
  225. {
  226. if (! $this->XML) {
  227. throw new LogicException('You must open an XML first');
  228. }
  229. if (! $this->DOM) {
  230. $this->DOM = new \DOMDocument;
  231. /**
  232. * We shut up the warning to exclude an exception in case Warnings are
  233. * transformed in exception
  234. */
  235. if ( ! @$this->DOM->loadXML($this->XML)) {
  236. throw new ParseError('Unable to load XML');
  237. }
  238. }
  239. return $this->DOM;
  240. }
  241. /**
  242. * Compute the DOMXpath from the DOMDocument
  243. *
  244. * @return \DOMXpath The DOMXpath object related to the XML
  245. * @throws RuntimeException
  246. */
  247. protected function getDomXpath()
  248. {
  249. if (! $this->DOMXpath) {
  250. try {
  251. $this->DOMXpath = new \DOMXPath($this->getDom());
  252. } catch (ParseError $e) {
  253. throw new RuntimeException('Unable to parse the XML');
  254. }
  255. $this->DOMXpath->registerNamespace('rdf', self::RDF_NAMESPACE);
  256. foreach (static::getNamespacesFromXml($this->getDom()) as $prefix => $uri) {
  257. $this->registeredPrefixes = array_merge($this->registeredPrefixes, (array) $prefix);
  258. $this->DOMXpath->registerNamespace($prefix, $uri);
  259. }
  260. }
  261. return $this->DOMXpath;
  262. }
  263. }