Completed
Push — develop ( ce465f...dc3813 )
by David
02:29 queued 11s
created

Analysis_Response_Ops::get_json()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
4
namespace Wordlift\Analysis\Response;
5
6
use stdClass;
7
8
class Analysis_Response_Ops {
9
10
	/**
11
	 * The analysis response json.
12
	 *
13
	 * @since 3.21.5
14
	 * @access private
15
	 * @var mixed $json Holds the analysis response json.
16
	 */
17
	private $json;
18
19
	/**
20
	 * Holds the {@link Wordlift_Entity_Uri_Service}.
21
	 *
22
	 * @since 3.21.5
23
	 * @access private
24
	 * @var \Wordlift_Entity_Uri_Service $entity_uri_service The {@link Wordlift_Entity_Uri_Service} instance.
25
	 */
26
	private $entity_uri_service;
27
28
	private $entity_service;
29
30
	/**
31
	 * @var \Wordlift_Entity_Type_Service
32
	 */
33
	private $entity_type_service;
34
	/**
35
	 * @var \Wordlift_Post_Image_Storage
36
	 */
37
	private $post_image_storage;
38
39
	/**
40
	 * Analysis_Response_Ops constructor.
41
	 *
42
	 * @param \Wordlift_Entity_Uri_Service $entity_uri_service The {@link Wordlift_Entity_Uri_Service}.
43
	 * @param \Wordlift_Entity_Service $entity_service The {@link Wordlift_Entity_Service}.
44
	 * @param \Wordlift_Entity_Type_Service $entity_type_service The {@link Wordlift_Entity_Type_Service}.
45
	 * @param \Wordlift_Post_Image_Storage $post_image_storage A {@link Wordlift_Post_Image_Storage} instance.
46
	 * @param mixed $json The analysis response json.
47
	 *
48
	 * @since 3.21.5
49
	 */
50
	public function __construct( $entity_uri_service, $entity_service, $entity_type_service, $post_image_storage, $json ) {
51
52
		$this->json                = $json;
53
		$this->entity_uri_service  = $entity_uri_service;
54
		$this->entity_service      = $entity_service;
55
		$this->entity_type_service = $entity_type_service;
56
		$this->post_image_storage  = $post_image_storage;
57
58
	}
59
60
	/**
61
	 * Switches remote entities, i.e. entities with id outside the local dataset, to local entities.
62
	 *
63
	 * The function takes all the entities that have an id which is not local. For each remote entity, a list of URIs
64
	 * is built comprising the entity id and the sameAs. Then a query is issued in the local database to find potential
65
	 * matches from the local vocabulary.
66
	 *
67
	 * If found, the entity id is swapped with the local id and the remote id is added to the sameAs.
68
	 *
69
	 * @return Analysis_Response_Ops The current Analysis_Response_Ops instance.
70
	 */
71
	public function make_entities_local() {
72
73
		if ( ! isset( $this->json->entities ) ) {
74
			return $this;
75
		}
76
77
		// Get the URIs.
78
		$uris = array_keys( get_object_vars( $this->json->entities ) );
79
80
		// Filter only the external URIs.
81
		$entity_uri_service = $this->entity_uri_service;
82
		$external_uris      = array_filter( $uris, function ( $item ) use ( $entity_uri_service ) {
83
			return ! $entity_uri_service->is_internal( $item );
84
		} );
85
86
		// Preload the URIs.
87
		$entity_uri_service->preload_uris( $external_uris );
88
89
		$mappings = array();
90
		foreach ( $external_uris as $external_uri ) {
91
			$entity = $entity_uri_service->get_entity( $external_uri );
92
			if ( null !== $entity ) {
93
94
				// Get the internal URI.
95
				$internal_uri              = $this->entity_service->get_uri( $entity->ID );
96
				$mappings[ $external_uri ] = $internal_uri;
97
			}
98
		}
99
100
		foreach ( $mappings as $external_uri => $internal_uri ) {
101
102
			// Move the data from the external URI to the internal URI.
103
			if ( ! isset( $this->json->entities->{$internal_uri} ) ) {
104
				$this->json->entities->{$internal_uri} = $this->json->entities->{$external_uri};
105
			}
106
107
			// Ensure sameAs is an array.
108
			if ( ! isset( $this->json->entities->{$internal_uri}->sameAs )
109
			     || ! is_array( $this->json->entities->{$internal_uri}->sameAs ) ) {
110
				$this->json->entities->{$internal_uri}->sameAs = array();
111
			}
112
113
			// Add the external URI as sameAs.
114
			$this->json->entities->{$internal_uri}->sameAs[] = $external_uri;
115
116
			// Finally remove the external URI.
117
			unset( $this->json->entities->{$external_uri} );
118
		}
119
120
		if ( isset( $this->json->annotations ) ) {
121
			foreach ( $this->json->annotations as $key => $annotation ) {
122
				if ( isset( $annotation->entityMatches ) ) {
123
					foreach ( $annotation->entityMatches as $match ) {
124
						if ( isset( $match->entityId ) && isset( $mappings[ $match->entityId ] ) ) {
125
							$match->entityId = $mappings[ $match->entityId ];
126
						}
127
					}
128
				}
129
			}
130
		}
131
132
		return $this;
133
	}
134
135
	/**
136
	 * Add occurrences by parsing the provided html content.
137
	 *
138
	 * @param string $content The html content with annotations.
139
	 *
140
	 * @return Analysis_Response_Ops The {@link Analysis_Response_Ops} instance.
141
	 *
142
	 * @since 3.23.7 refactor the regex pattern to take into account that there might be css classes between textannotation
143
	 *  and disambiguated.
144
	 *
145
	 * @link https://github.com/insideout10/wordlift-plugin/issues/1001
146
	 */
147
	public function add_occurrences( $content ) {
148
149
		// Try to get all the disambiguated annotations and bail out if an error occurs.
150
		if ( false === preg_match_all(
151
				'|<span\s+id="([^"]+)"\s+class="textannotation\s+(?:\S+\s+)?disambiguated(?=[\s"])[^"]*"\s+itemid="([^"]*)">(.*?)</span>|',
152
				$content,
153
				$matches,
154
				PREG_OFFSET_CAPTURE
155
			) ) {
156
			return $this;
157
		}
158
159
		if ( empty( $matches ) ) {
160
			return $this;
161
		}
162
163
		$parse_data = array_reduce( range( 0, count( $matches[1] ) - 1 ), function ( $carry, $i ) use ( $matches ) {
164
			if ( empty( $matches[0] ) ) {
165
				return $carry;
166
			}
167
168
			$start         = $matches[0][ $i ][1];
169
			$end           = $start + strlen( $matches[0][ $i ][0] );
170
			$annotation_id = $matches[1][ $i ][0];
171
			$item_id       = $matches[2][ $i ][0];
172
			$text          = $matches[3][ $i ][0];
173
174
			$annotation               = new StdClass;
175
			$annotation->annotationId = $annotation_id;
176
			$annotation->start        = $start;
177
			$annotation->end          = $end;
178
			$annotation->text         = $text;
179
180
			$entity_match                = new StdClass;
181
			$entity_match->confidence    = 100;
182
			$entity_match->entityId      = $item_id;
183
			$annotation->entityMatches[] = $entity_match;
184
185
			$carry['annotations'][ $annotation_id ] = $annotation;
186
			$carry['occurrences'][ $item_id ][]     = $annotation_id;
187
188
			return $carry;
189
		}, array( 'annotations' => array(), 'occurrences' => array(), ) );
190
191
		$annotations = $parse_data['annotations'];
192
		$occurrences = $parse_data['occurrences'];
193
194
		foreach ( array_keys( $occurrences ) as $item_id ) {
195
196
			// If the entity isn't there, add it.
197
			if ( ! isset( $this->json->entities->{$item_id} ) ) {
198
				$entity = $this->get_local_entity( $item_id );
199
200
				// Entity not found in the local vocabulary, continue to the next one.
201
				if ( false === $entity ) {
202
					continue;
203
				}
204
205
				$this->json->entities->{$item_id} = $entity;
206
			}
207
		}
208
209
		// Here we're adding back some data structures required by the client-side code.
210
		//
211
		// We're adding:
212
		//  1. the .entities[entity_id].occurrences array with the annotations' ids.
213
		//  2. the .entities[entity_id].annotations[annotation_id] = { id: annotation_id } map.
214
		//
215
		// Before 3.23.0 this was done by the client-side code located in src/coffee/editpost-widget/app.services.AnalysisService.coffee
216
		// function `preselect`, which was called by src/coffee/editpost-widget/app.services.EditorService.coffee in
217
		// `embedAnalysis`.
218
		foreach ( $this->json->entities as $id => $entity ) {
219
			$this->json->entities->{$id}->occurrences = isset( $occurrences[ $id ] ) ? $occurrences[ $id ] : array();;
220
221
			foreach ( $this->json->entities->{$id}->occurrences as $annotation_id ) {
222
				$this->json->entities->{$id}->annotations[ $annotation_id ] = array(
223
					'id' => $annotation_id,
224
				);
225
			}
226
		}
227
228
		// Add the missing annotations. This allows the analysis response to work also if we didn't receive results
229
		// from the analysis API.
230
		foreach ( $annotations as $annotation_id => $annotation ) {
231
232
			if ( ! isset( $this->json->annotations->{$annotation_id} ) ) {
233
				$this->json->annotations->{$annotation_id} = $annotation;
234
			}
235
236
		}
237
238
		return $this;
239
	}
240
241
	private function get_local_entity( $uri ) {
242
243
		$entity = $this->entity_uri_service->get_entity( $uri );
244
245
		if ( null === $entity ) {
246
			return false;
247
		}
248
249
		$type   = $this->entity_type_service->get( $entity->ID );
250
		$images = $this->post_image_storage->get( $entity->ID );
251
252
		return (object) array(
253
			'id'          => $uri,
254
			'label'       => $entity->post_title,
255
			'description' => $entity->post_content,
256
			'sameAs'      => wl_schema_get_value( $entity->ID, 'sameAs' ),
257
			'mainType'    => str_replace( 'wl-', '', $type['css_class'] ),
258
			'types'       => wl_get_entity_rdf_types( $entity->ID ),
259
			'images'      => $images,
260
		);
261
	}
262
263
	/**
264
	 * Return the JSON response.
265
	 *
266
	 * @return mixed The JSON response.
267
	 * @since 3.24.2
268
	 */
269
	public function get_json() {
270
271
		return $this->json;
272
	}
273
274
	/**
275
	 * Get the string representation of the JSON.
276
	 *
277
	 * @return false|string The string representation or false in case of error.
278
	 */
279
	public function to_string() {
280
281
		// Add the `JSON_UNESCAPED_UNICODE` only for PHP 5.4+.
282
		$options = ( version_compare( PHP_VERSION, '5.4', '>=' )
283
			? 256 : 0 );
284
285
		return wp_json_encode( $this->json, $options );
286
	}
287
288
	/**
289
	 * Create an Analysis_Response_Ops instance given the provided JSON structure.
290
	 *
291
	 * @param mixed $json The JSON structure.
292
	 *
293
	 * @return Analysis_Response_Ops A new Analysis_Response_Ops instance.
294
	 */
295
	public static function create( $json ) {
296
297
		return new static(
298
			\Wordlift_Entity_Uri_Service::get_instance(),
299
			\Wordlift_Entity_Service::get_instance(),
300
			\Wordlift_Entity_Type_Service::get_instance(),
301
			\Wordlift_Storage_Factory::get_instance()->post_images(),
302
			$json );
303
	}
304
305
	/**
306
	 * Create an Analysis_Response_Ops instance given the provided http response.
307
	 *
308
	 * @param array $response {
309
	 *
310
	 * @type string $body The response body.
311
	 * }
312
	 *
313
	 * @return Analysis_Response_Ops A new Analysis_Response_Ops instance.
314
	 * @throws \Exception if the provided response doesn't contain a `body` element.
315
	 */
316
	public static function create_with_response( $response ) {
317
318
		if ( ! isset( $response['body'] ) ) {
319
			throw new \Exception( "`body` is required in response." );
320
		}
321
322
		return static::create( json_decode( $response['body'] ) );
323
	}
324
325
}
326