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 }