1 <?php
2 /**
3 * @package FrameworkOnFramework
4 * @subpackage utils
5 * @copyright Copyright (C) 2010-2016 Nicholas K. Dionysopoulos / Akeeba Ltd. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE.txt
7 */
8
9 // Protect from unauthorized access
10 defined('FOF_INCLUDED') or die;
11
12 /**
13 * A helper class to read and parse "collection" update XML files over the web
14 */
15 class FOFUtilsUpdateCollection
16 {
17 /**
18 * Reads a "collection" XML update source and returns the complete tree of categories
19 * and extensions applicable for platform version $jVersion
20 *
21 * @param string $url The collection XML update source URL to read from
22 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
23 *
24 * @return array A list of update sources applicable to $jVersion
25 */
26 public function getAllUpdates($url, $jVersion = null)
27 {
28 // Get the target platform
29 if (is_null($jVersion))
30 {
31 $jVersion = JVERSION;
32 }
33
34 // Initialise return value
35 $updates = array(
36 'metadata' => array(
37 'name' => '',
38 'description' => '',
39 ),
40 'categories' => array(),
41 'extensions' => array(),
42 );
43
44 // Download and parse the XML file
45 $donwloader = new FOFDownload();
46 $xmlSource = $donwloader->getFromURL($url);
47
48 try
49 {
50 $xml = new SimpleXMLElement($xmlSource, LIBXML_NONET);
51 }
52 catch(Exception $e)
53 {
54 return $updates;
55 }
56
57 // Sanity check
58 if (($xml->getName() != 'extensionset'))
59 {
60 unset($xml);
61
62 return $updates;
63 }
64
65 // Initialise return value with the stream metadata (name, description)
66 $rootAttributes = $xml->attributes();
67 foreach ($rootAttributes as $k => $v)
68 {
69 $updates['metadata'][$k] = (string)$v;
70 }
71
72 // Initialise the raw list of updates
73 $rawUpdates = array(
74 'categories' => array(),
75 'extensions' => array(),
76 );
77
78 // Segregate the raw list to a hierarchy of extension and category entries
79 /** @var SimpleXMLElement $extension */
80 foreach ($xml->children() as $extension)
81 {
82 switch ($extension->getName())
83 {
84 case 'category':
85 // These are the parameters we expect in a category
86 $params = array(
87 'name' => '',
88 'description' => '',
89 'category' => '',
90 'ref' => '',
91 'targetplatformversion' => $jVersion,
92 );
93
94 // These are the attributes of the element
95 $attributes = $extension->attributes();
96
97 // Merge them all
98 foreach ($attributes as $k => $v)
99 {
100 $params[$k] = (string)$v;
101 }
102
103 // We can't have a category with an empty category name
104 if (empty($params['category']))
105 {
106 continue;
107 }
108
109 // We can't have a category with an empty ref
110 if (empty($params['ref']))
111 {
112 continue;
113 }
114
115 if (empty($params['description']))
116 {
117 $params['description'] = $params['category'];
118 }
119
120 if (!array_key_exists($params['category'], $rawUpdates['categories']))
121 {
122 $rawUpdates['categories'][$params['category']] = array();
123 }
124
125 $rawUpdates['categories'][$params['category']][] = $params;
126
127 break;
128
129 case 'extension':
130 // These are the parameters we expect in a category
131 $params = array(
132 'element' => '',
133 'type' => '',
134 'version' => '',
135 'name' => '',
136 'detailsurl' => '',
137 'targetplatformversion' => $jVersion,
138 );
139
140 // These are the attributes of the element
141 $attributes = $extension->attributes();
142
143 // Merge them all
144 foreach ($attributes as $k => $v)
145 {
146 $params[$k] = (string)$v;
147 }
148
149 // We can't have an extension with an empty element
150 if (empty($params['element']))
151 {
152 continue;
153 }
154
155 // We can't have an extension with an empty type
156 if (empty($params['type']))
157 {
158 continue;
159 }
160
161 // We can't have an extension with an empty version
162 if (empty($params['version']))
163 {
164 continue;
165 }
166
167 if (empty($params['name']))
168 {
169 $params['name'] = $params['element'] . ' ' . $params['version'];
170 }
171
172 if (!array_key_exists($params['type'], $rawUpdates['extensions']))
173 {
174 $rawUpdates['extensions'][$params['type']] = array();
175 }
176
177 if (!array_key_exists($params['element'], $rawUpdates['extensions'][$params['type']]))
178 {
179 $rawUpdates['extensions'][$params['type']][$params['element']] = array();
180 }
181
182 $rawUpdates['extensions'][$params['type']][$params['element']][] = $params;
183 break;
184
185 default:
186 break;
187 }
188 }
189
190 unset($xml);
191
192 if (!empty($rawUpdates['categories']))
193 {
194 foreach ($rawUpdates['categories'] as $category => $entries)
195 {
196 $update = $this->filterListByPlatform($entries, $jVersion);
197 $updates['categories'][$category] = $update;
198 }
199 }
200
201 if (!empty($rawUpdates['extensions']))
202 {
203 foreach ($rawUpdates['extensions'] as $type => $extensions)
204 {
205 $updates['extensions'][$type] = array();
206
207 if (!empty($extensions))
208 {
209 foreach ($extensions as $element => $entries)
210 {
211 $update = $this->filterListByPlatform($entries, $jVersion);
212 $updates['extensions'][$type][$element] = $update;
213 }
214 }
215 }
216 }
217
218 return $updates;
219 }
220
221 /**
222 * Filters a list of updates, returning only those available for the
223 * specified platform version $jVersion
224 *
225 * @param array $updates An array containing update definitions (categories or extensions)
226 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
227 *
228 * @return array|null The update definition that is compatible, or null if none is compatible
229 */
230 private function filterListByPlatform($updates, $jVersion = null)
231 {
232 // Get the target platform
233 if (is_null($jVersion))
234 {
235 $jVersion = JVERSION;
236 }
237
238 $versionParts = explode('.', $jVersion, 4);
239 $platformVersionMajor = $versionParts[0];
240 $platformVersionMinor = (count($versionParts) > 1) ? $platformVersionMajor . '.' . $versionParts[1] : $platformVersionMajor;
241 $platformVersionNormal = (count($versionParts) > 2) ? $platformVersionMinor . '.' . $versionParts[2] : $platformVersionMinor;
242 $platformVersionFull = (count($versionParts) > 3) ? $platformVersionNormal . '.' . $versionParts[3] : $platformVersionNormal;
243
244 $pickedExtension = null;
245 $pickedSpecificity = -1;
246
247 foreach ($updates as $update)
248 {
249 // Test the target platform
250 $targetPlatform = (string)$update['targetplatformversion'];
251
252 if ($targetPlatform === $platformVersionFull)
253 {
254 $pickedExtension = $update;
255 $pickedSpecificity = 4;
256 }
257 elseif (($targetPlatform === $platformVersionNormal) && ($pickedSpecificity <= 3))
258 {
259 $pickedExtension = $update;
260 $pickedSpecificity = 3;
261 }
262 elseif (($targetPlatform === $platformVersionMinor) && ($pickedSpecificity <= 2))
263 {
264 $pickedExtension = $update;
265 $pickedSpecificity = 2;
266 }
267 elseif (($targetPlatform === $platformVersionMajor) && ($pickedSpecificity <= 1))
268 {
269 $pickedExtension = $update;
270 $pickedSpecificity = 1;
271 }
272 }
273
274 return $pickedExtension;
275 }
276
277 /**
278 * Returns only the category definitions of a collection
279 *
280 * @param string $url The URL of the collection update source
281 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
282 *
283 * @return array An array of category update definitions
284 */
285 public function getCategories($url, $jVersion = null)
286 {
287 $allUpdates = $this->getAllUpdates($url, $jVersion);
288
289 return $allUpdates['categories'];
290 }
291
292 /**
293 * Returns the update source for a specific category
294 *
295 * @param string $url The URL of the collection update source
296 * @param string $category The category name you want to get the update source URL of
297 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
298 *
299 * @return string|null The update stream URL, or null if it's not found
300 */
301 public function getCategoryUpdateSource($url, $category, $jVersion = null)
302 {
303 $allUpdates = $this->getAllUpdates($url, $jVersion);
304
305 if (array_key_exists($category, $allUpdates['categories']))
306 {
307 return $allUpdates['categories'][$category]['ref'];
308 }
309 else
310 {
311 return null;
312 }
313 }
314
315 /**
316 * Get a list of updates for extensions only, optionally of a specific type
317 *
318 * @param string $url The URL of the collection update source
319 * @param string $type The extension type you want to get the update source URL of, empty to get all
320 * extension types
321 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
322 *
323 * @return array|null An array of extension update definitions or null if none is found
324 */
325 public function getExtensions($url, $type = null, $jVersion = null)
326 {
327 $allUpdates = $this->getAllUpdates($url, $jVersion);
328
329 if (empty($type))
330 {
331 return $allUpdates['extensions'];
332 }
333 elseif (array_key_exists($type, $allUpdates['extensions']))
334 {
335 return $allUpdates['extensions'][$type];
336 }
337 else
338 {
339 return null;
340 }
341 }
342
343 /**
344 * Get the update source URL for a specific extension, based on the type and element, e.g.
345 * type=file and element=joomla is Joomla! itself.
346 *
347 * @param string $url The URL of the collection update source
348 * @param string $type The extension type you want to get the update source URL of
349 * @param string $element The extension element you want to get the update source URL of
350 * @param string $jVersion Joomla! version to fetch updates for, or null to use JVERSION
351 *
352 * @return string|null The update source URL or null if the extension is not found
353 */
354 public function getExtensionUpdateSource($url, $type, $element, $jVersion = null)
355 {
356 $allUpdates = $this->getExtensions($url, $type, $jVersion);
357
358 if (empty($allUpdates))
359 {
360 return null;
361 }
362 elseif (array_key_exists($element, $allUpdates))
363 {
364 return $allUpdates[$element]['detailsurl'];
365 }
366 else
367 {
368 return null;
369 }
370 }
371 }