Completed
Push — address-as-title ( 9b6eeb...601934 )
by Peter
11:07
created

MapsDisplayMapRenderer::handleMarkerData()   C

Complexity

Conditions 7
Paths 40

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 22
nc 40
nop 2
dl 0
loc 40
rs 6.7272
c 0
b 0
f 0
1
<?php
2
use Maps\Element;
3
use Maps\Elements\Line;
4
use Maps\Elements\Location;
5
6
/**
7
 * Class handling the #display_map rendering.
8
 *
9
 * @licence GNU GPL v2+
10
 * @author Jeroen De Dauw < [email protected] >
11
 * @author Kim Eik
12
 */
13
class MapsDisplayMapRenderer {
14
15
	/**
16
	 * @since 2.0
17
	 *
18
	 * @var iMappingService
19
	 */
20
	protected $service;
21
22
	/**
23
	 * Constructor.
24
	 *
25
	 * @param iMappingService $service
26
	 */
27
	public function __construct( iMappingService $service ) {
28
		$this->service = $service;
29
	}
30
31
	/**
32
	 * Returns the HTML to display the map.
33
	 *
34
	 * @since 2.0
35
	 *
36
	 * @param array $params
37
	 * @param Parser $parser
38
	 * @param string $mapName
39
	 *
40
	 * @return string
41
	 */
42
	protected function getMapHTML( array $params, Parser $parser, $mapName ) {
43
		return Html::rawElement(
44
			'div',
45
			[
46
				'id' => $mapName,
47
				'style' => "width: {$params['width']}; height: {$params['height']}; background-color: #cccccc; overflow: hidden;",
48
				'class' => 'maps-map maps-' . $this->service->getName()
49
			],
50
			wfMessage( 'maps-loading-map' )->inContentLanguage()->escaped() .
51
				Html::element(
52
					'div',
53
					[ 'style' => 'display:none', 'class' => 'mapdata' ],
54
					FormatJson::encode( $this->getJSONObject( $params, $parser ) )
55
				)
56
		);
57
	}
58
59
	/**
60
	 * Returns a PHP object to encode to JSON with the map data.
61
	 *
62
	 * @since 2.0
63
	 *
64
	 * @param array $params
65
	 * @param Parser $parser
66
	 *
67
	 * @return mixed
68
	 */
69
	protected function getJSONObject( array $params, Parser $parser ) {
0 ignored issues
show
Unused Code introduced by
The parameter $parser is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
70
		return $params;
71
	}
72
73
	/**
74
	 * Handles the request from the parser hook by doing the work that's common for all
75
	 * mapping services, calling the specific methods and finally returning the resulting output.
76
	 *
77
	 * @param array $params
78
	 * @param Parser $parser
79
	 *
80
	 * @return string
81
	 */
82
	public final function renderMap( array $params, Parser $parser ) {
83
		$this->handleMarkerData( $params, $parser );
84
85
		$mapName = $this->service->getMapId();
86
87
		$output = $this->getMapHTML( $params, $parser, $mapName );
88
89
		$configVars = Skin::makeVariablesScript( $this->service->getConfigVariables() );
90
91
		$this->service->addDependencies( $parser );
92
		$parser->getOutput()->addHeadItem( $configVars );
93
94
		return $output;
95
	}
96
97
	/**
98
	 * Converts the data in the coordinates parameter to JSON-ready objects.
99
	 * These get stored in the locations parameter, and the coordinates on gets deleted.
100
	 *
101
	 * FIXME: complexity
102
	 *
103
	 * @since 1.0
104
	 *
105
	 * @param array &$params
106
	 * @param Parser $parser
107
	 */
108
	protected function handleMarkerData( array &$params, Parser $parser ) {
109
		if ( is_object( $params['centre'] ) ) {
110
			$params['centre'] = $params['centre']->getJSONObject();
111
		}
112
113
		$parserClone = clone $parser;
114
115
		if ( is_object( $params['wmsoverlay'] ) ) {
116
			$params['wmsoverlay'] = $params['wmsoverlay']->getJSONObject();
117
		}
118
119
		$iconUrl = MapsMapper::getFileUrl( $params['icon'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsMapper::getFileUrl() has been deprecated.

This method has been deprecated.

Loading history...
120
		$visitedIconUrl = MapsMapper::getFileUrl( $params['visitedicon'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsMapper::getFileUrl() has been deprecated.

This method has been deprecated.

Loading history...
121
		$params['locations'] = [];
122
123
		/**
124
		 * @var Location $location
125
		 */
126
		foreach ( $params['coordinates'] as $location ) {
127
			$jsonObj = $location->getJSONObject( $params['title'], $params['label'], $iconUrl, '', '',$visitedIconUrl);
128
129
			$jsonObj['title'] = $parserClone->parse( $jsonObj['title'], $parserClone->getTitle(), new ParserOptions() )->getText();
130
			$jsonObj['text'] = $parserClone->parse( $jsonObj['text'], $parserClone->getTitle(), new ParserOptions() )->getText();
131
			$jsonObj['inlineLabel'] = strip_tags($parserClone->parse( $jsonObj['inlineLabel'], $parserClone->getTitle(), new ParserOptions() )->getText(),'<a><img>');
132
133
			$hasTitleAndtext = $jsonObj['title'] !== '' && $jsonObj['text'] !== '';
134
			$jsonObj['text'] = ( $hasTitleAndtext ? '<b>' . $jsonObj['title'] . '</b><hr />' : $jsonObj['title'] ) . $jsonObj['text'];
135
			$jsonObj['title'] = strip_tags( $jsonObj['title'] );
136
137
			$params['locations'][] = $jsonObj;
138
		}
139
140
		unset( $params['coordinates'] );
141
142
		$this->handleShapeData( $params, $parserClone );
143
144
		if ( $params['mappingservice'] === 'openlayers' ) {
145
			$params['layers'] = self::evilOpenLayersHack( $params['layers'] );
0 ignored issues
show
Deprecated Code introduced by
The method MapsDisplayMapRenderer::evilOpenLayersHack() has been deprecated.

This method has been deprecated.

Loading history...
146
		}
147
	}
148
149
	protected function handleShapeData( array &$params, Parser $parserClone ) {
150
		$textContainers = [
151
			&$params['lines'] ,
152
			&$params['polygons'] ,
153
			&$params['circles'] ,
154
			&$params['rectangles'],
155
			&$params['imageoverlays'], // FIXME: this is Google Maps specific!!
156
		];
157
158
		foreach ( $textContainers as &$textContainer ) {
159
			if ( is_array( $textContainer ) ) {
160
				foreach ( $textContainer as &$obj ) {
161
					if ( $obj instanceof Element ) {
162
						$obj = $obj->getArrayValue();
163
					}
164
165
					$obj['title'] = $parserClone->parse( $obj['title'] , $parserClone->getTitle() , new ParserOptions() )->getText();
166
					$obj['text'] = $parserClone->parse( $obj['text'] , $parserClone->getTitle() , new ParserOptions() )->getText();
167
168
					$hasTitleAndtext = $obj['title'] !== '' && $obj['text'] !== '';
169
					$obj['text'] = ( $hasTitleAndtext ? '<b>' . $obj['title'] . '</b><hr />' : $obj['title'] ) . $obj['text'];
170
					$obj['title'] = strip_tags( $obj['title'] );
171
				}
172
			}
173
		}
174
	}
175
176
	/**
177
	 * FIXME
178
	 *
179
	 * Temporary hack until the mapping service handling gets a proper refactor
180
	 * This kind of JS construction is also rather evil and should not be done at this point
181
	 *
182
	 * @since 3.0
183
	 * @deprecated
184
	 *
185
	 * @param string[] $layers
186
	 *
187
	 * @return string[]
188
	 */
189
	public static function evilOpenLayersHack( $layers ) {
190
		global $egMapsOLLayerGroups, $egMapsOLAvailableLayers;
191
192
		$layerDefs = [];
193
		$layerNames = [];
194
195
		foreach ( $layers as $layerOrGroup ) {
0 ignored issues
show
Bug introduced by
The expression $layers of type object<MapsLayerGroup>|array<integer,string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
196
			$lcLayerOrGroup = strtolower( $layerOrGroup );
197
198
			// Layer groups. Loop over all items and add them if not present yet:
199
			if ( array_key_exists( $lcLayerOrGroup, $egMapsOLLayerGroups ) ) {
200
				foreach ( $egMapsOLLayerGroups[$lcLayerOrGroup] as $layerName ) {
201
					if ( !in_array( $layerName, $layerNames ) ) {
202
						if ( is_array( $egMapsOLAvailableLayers[$layerName] ) ) {
203
							$layerDefs[] = 'new ' . $egMapsOLAvailableLayers[$layerName][0];
204
						}
205
						else {
206
							$layerDefs[] = 'new ' . $egMapsOLAvailableLayers[$layerName];
207
						}
208
						$layerNames[] = $layerName;
209
					}
210
				}
211
			}
212
			// Single layers. Add them if not present yet:
213
			elseif ( array_key_exists( $lcLayerOrGroup, $egMapsOLAvailableLayers ) ) {
214
				if ( !in_array( $lcLayerOrGroup, $layerNames ) ) {
215
					if ( is_array( $egMapsOLAvailableLayers[$lcLayerOrGroup] ) ) {
216
						$layerDefs[] = 'new ' . $egMapsOLAvailableLayers[$lcLayerOrGroup][0];
217
					}
218
					else {
219
						$layerDefs[] = 'new ' . $egMapsOLAvailableLayers[$lcLayerOrGroup];
220
					}
221
222
					$layerNames[] = $lcLayerOrGroup;
223
				}
224
			}
225
			// Image layers. Check validity and add if not present yet:
226
			else {
227
				$layerParts = explode( ';', $layerOrGroup, 2 );
228
				$layerGroup = $layerParts[0];
229
				$layerName = count( $layerParts ) > 1 ? $layerParts[1] : null;
230
231
				$title = Title::newFromText( $layerGroup, Maps_NS_LAYER );
232
233
				if ( $title !== null && $title->getNamespace() == Maps_NS_LAYER ) {
234
					// TODO: FIXME: This shouldn't be here and using $wgParser, instead it should
235
					//  be somewhere around MapsBaseMap::renderMap. But since we do a lot more than
236
					//  'parameter manipulation' in here, we already diminish the information needed
237
					//  for this which will never arrive there.
238
					global $wgParser;
239
					// add dependency to the layer page so if the layer definition gets updated,
240
					// the page where it is used will be updated as well:
241
					$rev = Revision::newFromTitle( $title );
242
					$revId = null;
243
					if( $rev !== null ) {
244
						$revId = $rev->getId();
245
					}
246
					$wgParser->getOutput()->addTemplate( $title, $title->getArticleID(), $revId );
247
248
					// if the whole layer group is not yet loaded into the map and the group exists:
249
					if( !in_array( $layerGroup, $layerNames )
250
						&& $title->exists()
251
					) {
252
						if( $layerName !== null ) {
253
							// load specific layer with name:
254
							$layer = MapsLayers::loadLayer( $title, $layerName );
255
							$layers = new MapsLayerGroup( $layer );
0 ignored issues
show
Bug introduced by
It seems like $layer defined by \MapsLayers::loadLayer($title, $layerName) on line 254 can be null; however, MapsLayerGroup::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
256
							$usedLayer = $layerOrGroup;
257
						}
258
						else {
259
							// load all layers from group:
260
							$layers = MapsLayers::loadLayerGroup( $title );
261
							$usedLayer = $layerGroup;
262
						}
263
264
						foreach( $layers->getLayers() as $layer ) {
265
							if( ( // make sure named layer is only taken once (in case it was requested on its own before)
266
									$layer->getName() === null
267
									|| !in_array( $layerGroup . ';' . $layer->getName(), $layerNames )
268
								)
269
								&& $layer->isOk()
270
							) {
271
								$layerDefs[] = $layer->getJavaScriptDefinition();
272
							}
273
						}
274
275
						$layerNames[] = $usedLayer; // have to add this after loop of course!
276
					}
277
				}
278
				else {
279
					wfWarn( "Invalid layer ($layerOrGroup) encountered after validation." );
280
				}
281
			}
282
		}
283
284
		MapsMappingServices::getServiceInstance( 'openlayers' )->addLayerDependencies( self::getLayerDependencies( $layerNames ) );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface iMappingService as the method addLayerDependencies() does only exist in the following implementations of said interface: MapsOpenLayers.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
285
286
//		print_r( $layerDefs );
287
//		die();
288
		return $layerDefs;
289
	}
290
291
	/**
292
	 * FIXME
293
	 * @see evilOpenLayersHack
294
	 */
295
	private static function getLayerDependencies( array $layerNames ) {
296
		global $egMapsOLLayerDependencies, $egMapsOLAvailableLayers;
297
298
		$layerDependencies = [];
299
300
		foreach ( $layerNames as $layerName ) {
301
			if ( array_key_exists( $layerName, $egMapsOLAvailableLayers ) // The layer must be defined in php
302
				&& is_array( $egMapsOLAvailableLayers[$layerName] ) // The layer must be an array...
303
				&& count( $egMapsOLAvailableLayers[$layerName] ) > 1 // ...with a second element...
304
				&& array_key_exists( $egMapsOLAvailableLayers[$layerName][1], $egMapsOLLayerDependencies ) ) { //...that is a dependency.
305
				$layerDependencies[] = $egMapsOLLayerDependencies[$egMapsOLAvailableLayers[$layerName][1]];
306
			}
307
		}
308
309
		return array_unique( $layerDependencies );
310
	}
311
	
312
}
313