class-wc-api-xml-handler.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. <?php
  2. /**
  3. * WooCommerce API
  4. *
  5. * Handles parsing XML request bodies and generating XML responses
  6. *
  7. * @author WooThemes
  8. * @category API
  9. * @package WooCommerce/API
  10. * @since 2.1
  11. * @version 2.1
  12. */
  13. if ( ! defined( 'ABSPATH' ) ) {
  14. exit; // Exit if accessed directly
  15. }
  16. class WC_API_XML_Handler implements WC_API_Handler {
  17. /** @var XMLWriter instance */
  18. private $xml;
  19. /**
  20. * Add some response filters
  21. *
  22. * @since 2.1
  23. */
  24. public function __construct() {
  25. // tweak sales report response data
  26. add_filter( 'woocommerce_api_report_response', array( $this, 'format_sales_report_data' ), 100 );
  27. // tweak product response data
  28. add_filter( 'woocommerce_api_product_response', array( $this, 'format_product_data' ), 100 );
  29. }
  30. /**
  31. * Get the content type for the response
  32. *
  33. * @since 2.1
  34. * @return string
  35. */
  36. public function get_content_type() {
  37. return 'application/xml; charset=' . get_option( 'blog_charset' );
  38. }
  39. /**
  40. * Parse the raw request body entity
  41. *
  42. * @since 2.1
  43. * @param string $data the raw request body
  44. * @return array
  45. */
  46. public function parse_body( $data ) {
  47. // TODO: implement simpleXML parsing
  48. }
  49. /**
  50. * Generate an XML response given an array of data
  51. *
  52. * @since 2.1
  53. * @param array $data the response data
  54. * @return string
  55. */
  56. public function generate_response( $data ) {
  57. $this->xml = new XMLWriter();
  58. $this->xml->openMemory();
  59. $this->xml->setIndent( true );
  60. $this->xml->startDocument( '1.0', 'UTF-8' );
  61. $root_element = key( $data );
  62. $data = $data[ $root_element ];
  63. switch ( $root_element ) {
  64. case 'orders':
  65. $data = array( 'order' => $data );
  66. break;
  67. case 'order_notes':
  68. $data = array( 'order_note' => $data );
  69. break;
  70. case 'customers':
  71. $data = array( 'customer' => $data );
  72. break;
  73. case 'coupons':
  74. $data = array( 'coupon' => $data );
  75. break;
  76. case 'products':
  77. $data = array( 'product' => $data );
  78. break;
  79. case 'product_reviews':
  80. $data = array( 'product_review' => $data );
  81. break;
  82. default:
  83. $data = apply_filters( 'woocommerce_api_xml_data', $data, $root_element );
  84. break;
  85. }
  86. // generate xml starting with the root element and recursively generating child elements
  87. $this->array_to_xml( $root_element, $data );
  88. $this->xml->endDocument();
  89. return $this->xml->outputMemory();
  90. }
  91. /**
  92. * Convert array into XML by recursively generating child elements
  93. *
  94. * @since 2.1
  95. * @param string|array $element_key - name for element, e.g. <OrderID>
  96. * @param string|array $element_value - value for element, e.g. 1234
  97. * @return string - generated XML
  98. */
  99. private function array_to_xml( $element_key, $element_value = array() ) {
  100. if ( is_array( $element_value ) ) {
  101. // handle attributes
  102. if ( '@attributes' === $element_key ) {
  103. foreach ( $element_value as $attribute_key => $attribute_value ) {
  104. $this->xml->startAttribute( $attribute_key );
  105. $this->xml->text( $attribute_value );
  106. $this->xml->endAttribute();
  107. }
  108. return;
  109. }
  110. // handle multi-elements (e.g. multiple <Order> elements)
  111. if ( is_numeric( key( $element_value ) ) ) {
  112. // recursively generate child elements
  113. foreach ( $element_value as $child_element_key => $child_element_value ) {
  114. $this->xml->startElement( $element_key );
  115. foreach ( $child_element_value as $sibling_element_key => $sibling_element_value ) {
  116. $this->array_to_xml( $sibling_element_key, $sibling_element_value );
  117. }
  118. $this->xml->endElement();
  119. }
  120. } else {
  121. // start root element
  122. $this->xml->startElement( $element_key );
  123. // recursively generate child elements
  124. foreach ( $element_value as $child_element_key => $child_element_value ) {
  125. $this->array_to_xml( $child_element_key, $child_element_value );
  126. }
  127. // end root element
  128. $this->xml->endElement();
  129. }
  130. } else {
  131. // handle single elements
  132. if ( '@value' == $element_key ) {
  133. $this->xml->text( $element_value );
  134. } else {
  135. // wrap element in CDATA tags if it contains illegal characters
  136. if ( false !== strpos( $element_value, '<' ) || false !== strpos( $element_value, '>' ) ) {
  137. $this->xml->startElement( $element_key );
  138. $this->xml->writeCdata( $element_value );
  139. $this->xml->endElement();
  140. } else {
  141. $this->xml->writeElement( $element_key, $element_value );
  142. }
  143. }
  144. return;
  145. }
  146. }
  147. /**
  148. * Adjust the sales report array format to change totals keyed with the sales date to become an
  149. * attribute for the totals element instead
  150. *
  151. * @since 2.1
  152. * @param array $data
  153. * @return array
  154. */
  155. public function format_sales_report_data( $data ) {
  156. if ( ! empty( $data['totals'] ) ) {
  157. foreach ( $data['totals'] as $date => $totals ) {
  158. unset( $data['totals'][ $date ] );
  159. $data['totals'][] = array_merge( array( '@attributes' => array( 'date' => $date ) ), $totals );
  160. }
  161. }
  162. return $data;
  163. }
  164. /**
  165. * Adjust the product data to handle options for attributes without a named child element and other
  166. * fields that have no named child elements (e.g. categories = array( 'cat1', 'cat2' ) )
  167. *
  168. * Note that the parent product data for variations is also adjusted in the same manner as needed
  169. *
  170. * @since 2.1
  171. * @param array $data
  172. * @return array
  173. */
  174. public function format_product_data( $data ) {
  175. // handle attribute values
  176. if ( ! empty( $data['attributes'] ) ) {
  177. foreach ( $data['attributes'] as $attribute_key => $attribute ) {
  178. if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
  179. foreach ( $attribute['options'] as $option_key => $option ) {
  180. unset( $data['attributes'][ $attribute_key ]['options'][ $option_key ] );
  181. $data['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
  182. }
  183. }
  184. }
  185. }
  186. // simple arrays are fine for JSON, but XML requires a child element name, so this adjusts the data
  187. // array to define a child element name for each field
  188. $fields_to_fix = array(
  189. 'related_ids' => 'related_id',
  190. 'upsell_ids' => 'upsell_id',
  191. 'cross_sell_ids' => 'cross_sell_id',
  192. 'categories' => 'category',
  193. 'tags' => 'tag',
  194. );
  195. foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
  196. if ( ! empty( $data[ $parent_field_name ] ) ) {
  197. foreach ( $data[ $parent_field_name ] as $field_key => $field ) {
  198. unset( $data[ $parent_field_name ][ $field_key ] );
  199. $data[ $parent_field_name ][ $child_field_name ][] = array( $field );
  200. }
  201. }
  202. }
  203. // handle adjusting the parent product for variations
  204. if ( ! empty( $data['parent'] ) ) {
  205. // attributes
  206. if ( ! empty( $data['parent']['attributes'] ) ) {
  207. foreach ( $data['parent']['attributes'] as $attribute_key => $attribute ) {
  208. if ( ! empty( $attribute['options'] ) && is_array( $attribute['options'] ) ) {
  209. foreach ( $attribute['options'] as $option_key => $option ) {
  210. unset( $data['parent']['attributes'][ $attribute_key ]['options'][ $option_key ] );
  211. $data['parent']['attributes'][ $attribute_key ]['options']['option'][] = array( $option );
  212. }
  213. }
  214. }
  215. }
  216. // fields
  217. foreach ( $fields_to_fix as $parent_field_name => $child_field_name ) {
  218. if ( ! empty( $data['parent'][ $parent_field_name ] ) ) {
  219. foreach ( $data['parent'][ $parent_field_name ] as $field_key => $field ) {
  220. unset( $data['parent'][ $parent_field_name ][ $field_key ] );
  221. $data['parent'][ $parent_field_name ][ $child_field_name ][] = array( $field );
  222. }
  223. }
  224. }
  225. }
  226. return $data;
  227. }
  228. }