[ Index ]

PHP Cross Reference of phpBB-3.1.12-deutsch

title

Body

[close]

/phpbb/event/ -> php_exporter.php (source)

   1  <?php
   2  /**
   3  *
   4  * This file is part of the phpBB Forum Software package.
   5  *
   6  * @copyright (c) phpBB Limited <https://www.phpbb.com>
   7  * @license GNU General Public License, version 2 (GPL-2.0)
   8  *
   9  * For full copyright and license information, please see
  10  * the docs/CREDITS.txt file.
  11  *
  12  */
  13  
  14  namespace phpbb\event;
  15  
  16  /**
  17  * Class php_exporter
  18  * Crawls through a list of files and grabs all php-events
  19  */
  20  class php_exporter
  21  {
  22      /** @var string Path where we look for files*/
  23      protected $path;
  24  
  25      /** @var string phpBB Root Path */
  26      protected $root_path;
  27  
  28      /** @var string The minimum version for the events to return */
  29      protected $min_version;
  30  
  31      /** @var string The maximum version for the events to return */
  32      protected $max_version;
  33  
  34      /** @var string */
  35      protected $current_file;
  36  
  37      /** @var string */
  38      protected $current_event;
  39  
  40      /** @var int */
  41      protected $current_event_line;
  42  
  43      /** @var array */
  44      protected $events;
  45  
  46      /** @var array */
  47      protected $file_lines;
  48  
  49      /**
  50      * @param string $phpbb_root_path
  51      * @param mixed $extension    String 'vendor/ext' to filter, null for phpBB core
  52      * @param string $min_version
  53      * @param string $max_version
  54      */
  55  	public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null)
  56      {
  57          $this->root_path = $phpbb_root_path;
  58          $this->path = $phpbb_root_path;
  59          $this->events = $this->file_lines = array();
  60          $this->current_file = $this->current_event = '';
  61          $this->current_event_line = 0;
  62          $this->min_version = $min_version;
  63          $this->max_version = $max_version;
  64  
  65          $this->path = $this->root_path;
  66          if ($extension)
  67          {
  68              $this->path .= 'ext/' . $extension . '/';
  69          }
  70      }
  71  
  72      /**
  73      * Get the list of all events
  74      *
  75      * @return array        Array with events: name => details
  76      */
  77  	public function get_events()
  78      {
  79          return $this->events;
  80      }
  81  
  82      /**
  83      * Set current event data
  84      *
  85      * @param string    $name    Name of the current event (used for error messages)
  86      * @param int    $line    Line where the current event is placed in
  87      * @return null
  88      */
  89  	public function set_current_event($name, $line)
  90      {
  91          $this->current_event = $name;
  92          $this->current_event_line = $line;
  93      }
  94  
  95      /**
  96      * Set the content of this file
  97      *
  98      * @param array $content        Array with the lines of the file
  99      * @return null
 100      */
 101  	public function set_content($content)
 102      {
 103          $this->file_lines = $content;
 104      }
 105  
 106      /**
 107      * Crawl the phpBB/ directory for php events
 108      * @return int    The number of events found
 109      */
 110  	public function crawl_phpbb_directory_php()
 111      {
 112          $files = $this->get_recursive_file_list();
 113          $this->events = array();
 114          foreach ($files as $file)
 115          {
 116              $this->crawl_php_file($file);
 117          }
 118          ksort($this->events);
 119  
 120          return sizeof($this->events);
 121      }
 122  
 123      /**
 124      * Returns a list of files in $dir
 125      *
 126      * @return    array    List of files (including the path)
 127      */
 128  	public function get_recursive_file_list()
 129      {
 130          try
 131          {
 132              $iterator = new \RecursiveIteratorIterator(
 133                  new \phpbb\event\recursive_event_filter_iterator(
 134                      new \RecursiveDirectoryIterator(
 135                          $this->path,
 136                          \FilesystemIterator::SKIP_DOTS
 137                      ),
 138                      $this->path
 139                  ),
 140                  \RecursiveIteratorIterator::LEAVES_ONLY
 141              );
 142          }
 143          catch (\Exception $e)
 144          {
 145              return array();
 146          }
 147  
 148          $files = array();
 149          foreach ($iterator as $file_info)
 150          {
 151              /** @var \RecursiveDirectoryIterator $file_info */
 152              $relative_path = $iterator->getInnerIterator()->getSubPathname();
 153              $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path);
 154          }
 155  
 156          return $files;
 157      }
 158  
 159      /**
 160      * Format the php events as a wiki table
 161      *
 162      * @param string $action
 163      * @return string
 164      */
 165  	public function export_events_for_wiki($action = '')
 166      {
 167          if ($action === 'diff')
 168          {
 169              $wiki_page = '=== PHP Events (Hook Locations) ===' . "\n";
 170          }
 171          else
 172          {
 173              $wiki_page = '= PHP Events (Hook Locations) =' . "\n";
 174          }
 175          $wiki_page .= '{| class="sortable zebra" cellspacing="0" cellpadding="5"' . "\n";
 176          $wiki_page .= '! Identifier !! Placement !! Arguments !! Added in Release !! Explanation' . "\n";
 177          foreach ($this->events as $event)
 178          {
 179              $wiki_page .= '|- id="' . $event['event'] . '"' . "\n";
 180              $wiki_page .= '| [[#' . $event['event'] . '|' . $event['event'] . ']] || ' . $event['file'] . ' || ' . implode(', ', $event['arguments']) . ' || ' . $event['since'] . ' || ' . $event['description'] . "\n";
 181          }
 182          $wiki_page .= '|}' . "\n";
 183  
 184          return $wiki_page;
 185      }
 186  
 187      /**
 188      * @param string $file
 189      * @return int Number of events found in this file
 190      * @throws \LogicException
 191      */
 192  	public function crawl_php_file($file)
 193      {
 194          $this->current_file = $file;
 195          $this->file_lines = array();
 196          $content = file_get_contents($this->path . $this->current_file);
 197          $num_events_found = 0;
 198  
 199          if (strpos($content, "dispatcher->trigger_event('") || strpos($content, "dispatcher->dispatch('"))
 200          {
 201              $this->set_content(explode("\n", $content));
 202              for ($i = 0, $num_lines = sizeof($this->file_lines); $i < $num_lines; $i++)
 203              {
 204                  $event_line = false;
 205                  $found_trigger_event = strpos($this->file_lines[$i], "dispatcher->trigger_event('");
 206                  $arguments = array();
 207                  if ($found_trigger_event !== false)
 208                  {
 209                      $event_line = $i;
 210                      $this->set_current_event($this->get_event_name($event_line, false), $event_line);
 211  
 212                      // Find variables of the event
 213                      $arguments = $this->get_vars_from_array();
 214                      $doc_vars = $this->get_vars_from_docblock();
 215                      $this->validate_vars_docblock_array($arguments, $doc_vars);
 216                  }
 217                  else
 218                  {
 219                      $found_dispatch = strpos($this->file_lines[$i], "dispatcher->dispatch('");
 220                      if ($found_dispatch !== false)
 221                      {
 222                          $event_line = $i;
 223                          $this->set_current_event($this->get_event_name($event_line, true), $event_line);
 224                      }
 225                  }
 226  
 227                  if ($event_line)
 228                  {
 229                      // Validate @event
 230                      $event_line_num = $this->find_event();
 231                      $this->validate_event($this->current_event, $this->file_lines[$event_line_num]);
 232  
 233                      // Validate @since
 234                      $since_line_num = $this->find_since();
 235                      $since = $this->validate_since($this->file_lines[$since_line_num]);
 236  
 237                      $changed_line_nums = $this->find_changed('changed');
 238                      if (empty($changed_line_nums))
 239                      {
 240                          $changed_line_nums = $this->find_changed('change');
 241                      }
 242                      $changed_versions = array();
 243                      if (!empty($changed_line_nums))
 244                      {
 245                          foreach ($changed_line_nums as $changed_line_num)
 246                          {
 247                              $changed_versions[] = $this->validate_changed($this->file_lines[$changed_line_num]);
 248                          }
 249                      }
 250  
 251                      if (!$this->version_is_filtered($since))
 252                      {
 253                          $valid_version = false;
 254                          foreach ($changed_versions as $changed)
 255                          {
 256                              $valid_version = $valid_version || $this->version_is_filtered($changed);
 257                          }
 258  
 259                          if (!$valid_version)
 260                          {
 261                              continue;
 262                          }
 263                      }
 264  
 265                      // Find event description line
 266                      $description_line_num = $this->find_description();
 267                      $description = substr(trim($this->file_lines[$description_line_num]), strlen('* '));
 268  
 269                      if (isset($this->events[$this->current_event]))
 270                      {
 271                          throw new \LogicException("The event '{$this->current_event}' from file "
 272                              . "'{$this->current_file}:{$event_line_num}' already exists in file "
 273                              . "'{$this->events[$this->current_event]['file']}'", 10);
 274                      }
 275  
 276                      sort($arguments);
 277                      $this->events[$this->current_event] = array(
 278                          'event'            => $this->current_event,
 279                          'file'            => $this->current_file,
 280                          'arguments'        => $arguments,
 281                          'since'            => $since,
 282                          'description'    => $description,
 283                      );
 284                      $num_events_found++;
 285                  }
 286              }
 287          }
 288  
 289          return $num_events_found;
 290      }
 291  
 292      /**
 293       * The version to check
 294       *
 295       * @param string $version
 296       * @return bool
 297       */
 298  	protected function version_is_filtered($version)
 299      {
 300          return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<='))
 301              && (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>='));
 302      }
 303  
 304      /**
 305      * Find the name of the event inside the dispatch() line
 306      *
 307      * @param int $event_line
 308      * @param bool $is_dispatch Do we look for dispatch() or trigger_event() ?
 309      * @return string    Name of the event
 310      * @throws \LogicException
 311      */
 312  	public function get_event_name($event_line, $is_dispatch)
 313      {
 314          $event_text_line = $this->file_lines[$event_line];
 315          $event_text_line = ltrim($event_text_line, "\t ");
 316  
 317          if ($is_dispatch)
 318          {
 319              $regex = '#\$([a-z](?:[a-z0-9_]|->)*)';
 320              $regex .= '->dispatch\(';
 321              $regex .= '\'' . $this->preg_match_event_name() . '\'';
 322              $regex .= '\);#';
 323          }
 324          else
 325          {
 326              $regex = '#extract\(\$([a-z](?:[a-z0-9_]|->)*)';
 327              $regex .= '->trigger_event\(';
 328              $regex .= '\'' . $this->preg_match_event_name() . '\'';
 329              $regex .= ', compact\(\$vars\)\)\);#';
 330          }
 331  
 332          $match = array();
 333          preg_match($regex, $event_text_line, $match);
 334          if (!isset($match[2]))
 335          {
 336              throw new \LogicException("Can not find event name in line '{$event_text_line}' "
 337                  . "in file '{$this->current_file}:{$event_line}'", 1);
 338          }
 339  
 340          return $match[2];
 341      }
 342  
 343      /**
 344      * Returns a regex match for the event name
 345      *
 346      * @return string
 347      */
 348  	protected function preg_match_event_name()
 349      {
 350          return '([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)';
 351      }
 352  
 353      /**
 354      * Find the $vars array
 355      *
 356      * @return array        List of variables
 357      * @throws \LogicException
 358      */
 359  	public function get_vars_from_array()
 360      {
 361          $line = ltrim($this->file_lines[$this->current_event_line - 1], "\t");
 362          if ($line === ');')
 363          {
 364              $vars_array = $this->get_vars_from_multi_line_array();
 365          }
 366          else
 367          {
 368              $vars_array = $this->get_vars_from_single_line_array($line);
 369          }
 370  
 371          foreach ($vars_array as $var)
 372          {
 373              if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var))
 374              {
 375                  throw new \LogicException("Found invalid var '{$var}' in array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
 376              }
 377          }
 378  
 379          sort($vars_array);
 380          return $vars_array;
 381      }
 382  
 383      /**
 384      * Find the variables in single line array
 385      *
 386      * @param    string    $line
 387      * @param    bool    $throw_multiline    Throw an exception when there are too
 388      *                                        many arguments in one line.
 389      * @return array        List of variables
 390      * @throws \LogicException
 391      */
 392  	public function get_vars_from_single_line_array($line, $throw_multiline = true)
 393      {
 394          $match = array();
 395          preg_match('#^\$vars = array\(\'([a-zA-Z0-9_\' ,]+)\'\);$#', $line, $match);
 396  
 397          if (isset($match[1]))
 398          {
 399              $vars_array = explode("', '", $match[1]);
 400              if ($throw_multiline && sizeof($vars_array) > 6)
 401              {
 402                  throw new \LogicException('Should use multiple lines for $vars definition '
 403                      . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 404              }
 405              return $vars_array;
 406          }
 407          else
 408          {
 409              throw new \LogicException("Can not find '\$vars = array();'-line for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
 410          }
 411      }
 412  
 413      /**
 414      * Find the variables in single line array
 415      *
 416      * @return array        List of variables
 417      * @throws \LogicException
 418      */
 419  	public function get_vars_from_multi_line_array()
 420      {
 421          $current_vars_line = 2;
 422          $var_lines = array();
 423          while (ltrim($this->file_lines[$this->current_event_line - $current_vars_line], "\t") !== '$vars = array(')
 424          {
 425              $var_lines[] = substr(trim($this->file_lines[$this->current_event_line - $current_vars_line]), 0, -1);
 426  
 427              $current_vars_line++;
 428              if ($current_vars_line > $this->current_event_line)
 429              {
 430                  // Reached the start of the file
 431                  throw new \LogicException("Can not find end of \$vars array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 432              }
 433          }
 434  
 435          return $this->get_vars_from_single_line_array('$vars = array(' . implode(", ", $var_lines) . ');', false);
 436      }
 437  
 438      /**
 439      * Find the $vars array
 440      *
 441      * @return array        List of variables
 442      * @throws \LogicException
 443      */
 444  	public function get_vars_from_docblock()
 445      {
 446          $doc_vars = array();
 447          $current_doc_line = 1;
 448          $found_comment_end = false;
 449          while (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t") !== '/**')
 450          {
 451              if (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t ") === '*/')
 452              {
 453                  $found_comment_end = true;
 454              }
 455  
 456              if ($found_comment_end)
 457              {
 458                  $var_line = trim($this->file_lines[$this->current_event_line - $current_doc_line]);
 459                  $var_line = preg_replace('!\s+!', ' ', $var_line);
 460                  if (strpos($var_line, '* @var ') === 0)
 461                  {
 462                      $doc_line = explode(' ', $var_line, 5);
 463                      if (sizeof($doc_line) !== 5)
 464                      {
 465                          throw new \LogicException("Found invalid line '{$this->file_lines[$this->current_event_line - $current_doc_line]}' "
 466                          . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
 467                      }
 468                      $doc_vars[] = $doc_line[3];
 469                  }
 470              }
 471  
 472              $current_doc_line++;
 473              if ($current_doc_line > $this->current_event_line)
 474              {
 475                  // Reached the start of the file
 476                  throw new \LogicException("Can not find end of docblock for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 477              }
 478          }
 479  
 480          if (empty($doc_vars))
 481          {
 482              // Reached the start of the file
 483              throw new \LogicException("Can not find @var lines for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
 484          }
 485  
 486          foreach ($doc_vars as $var)
 487          {
 488              if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var))
 489              {
 490                  throw new \LogicException("Found invalid @var '{$var}' in docblock for event "
 491                      . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 4);
 492              }
 493          }
 494  
 495          sort($doc_vars);
 496          return $doc_vars;
 497      }
 498  
 499      /**
 500      * Find the "@since" Information line
 501      *
 502      * @return int Absolute line number
 503      * @throws \LogicException
 504      */
 505  	public function find_since()
 506      {
 507          return $this->find_tag('since', array('event', 'var'));
 508      }
 509  
 510      /**
 511      * Find the "@changed" Information lines
 512      *
 513      * @param string $tag_name Should be 'change', not 'changed'
 514      * @return array Absolute line numbers
 515      * @throws \LogicException
 516      */
 517  	public function find_changed($tag_name)
 518      {
 519          $lines = array();
 520          $last_line = 0;
 521          try
 522          {
 523              while ($line = $this->find_tag($tag_name, array('since'), $last_line))
 524              {
 525                  $lines[] = $line;
 526                  $last_line = $line;
 527              }
 528          }
 529          catch (\LogicException $e)
 530          {
 531              // Not changed? No problem!
 532          }
 533  
 534          return $lines;
 535      }
 536  
 537      /**
 538      * Find the "@event" Information line
 539      *
 540      * @return int Absolute line number
 541      */
 542  	public function find_event()
 543      {
 544          return $this->find_tag('event', array());
 545      }
 546  
 547      /**
 548      * Find a "@*" Information line
 549      *
 550      * @param string $find_tag        Name of the tag we are trying to find
 551      * @param array $disallowed_tags        List of tags that must not appear between
 552      *                                    the tag and the actual event
 553      * @param int $skip_to_line        Skip lines until this one
 554      * @return int Absolute line number
 555      * @throws \LogicException
 556      */
 557  	public function find_tag($find_tag, $disallowed_tags, $skip_to_line = 0)
 558      {
 559          $find_tag_line = $skip_to_line ? $this->current_event_line - $skip_to_line + 1 : 0;
 560          $found_comment_end = ($skip_to_line) ? true : false;
 561          while (strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $find_tag . ' ') !== 0)
 562          {
 563              if ($found_comment_end && ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '/**')
 564              {
 565                  // Reached the start of this doc block
 566                  throw new \LogicException("Can not find '@{$find_tag}' information for event "
 567                      . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
 568              }
 569  
 570              foreach ($disallowed_tags as $disallowed_tag)
 571              {
 572                  if ($found_comment_end && strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $disallowed_tag) === 0)
 573                  {
 574                      // Found @var after the @since
 575                      throw new \LogicException("Found '@{$disallowed_tag}' information after '@{$find_tag}' for event "
 576                          . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
 577                  }
 578              }
 579  
 580              if (ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t ") === '*/')
 581              {
 582                  $found_comment_end = true;
 583              }
 584  
 585              $find_tag_line++;
 586              if ($find_tag_line >= $this->current_event_line)
 587              {
 588                  // Reached the start of the file
 589                  throw new \LogicException("Can not find '@{$find_tag}' information for event "
 590                      . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 591              }
 592          }
 593  
 594          return $this->current_event_line - $find_tag_line;
 595      }
 596  
 597      /**
 598      * Find a "@*" Information line
 599      *
 600      * @return int Absolute line number
 601      * @throws \LogicException
 602      */
 603  	public function find_description()
 604      {
 605          $find_desc_line = 0;
 606          while (ltrim($this->file_lines[$this->current_event_line - $find_desc_line], "\t") !== '/**')
 607          {
 608              $find_desc_line++;
 609              if ($find_desc_line > $this->current_event_line)
 610              {
 611                  // Reached the start of the file
 612                  throw new \LogicException("Can not find a description for event "
 613                      . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
 614              }
 615          }
 616  
 617          $find_desc_line = $this->current_event_line - $find_desc_line + 1;
 618  
 619          $desc = trim($this->file_lines[$find_desc_line]);
 620          if (strpos($desc, '* @') === 0 || $desc[0] !== '*' || substr($desc, 1) == '')
 621          {
 622              // First line of the doc block is a @-line, empty or only contains "*"
 623              throw new \LogicException("Can not find a description for event "
 624                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 625          }
 626  
 627          return $find_desc_line;
 628      }
 629  
 630      /**
 631      * Validate "@since" Information
 632      *
 633      * @param string $line
 634      * @return string
 635      * @throws \LogicException
 636      */
 637  	public function validate_since($line)
 638      {
 639          $match = array();
 640          preg_match('#^\* @since (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)$#', ltrim($line, "\t "), $match);
 641          if (!isset($match[1]))
 642          {
 643              throw new \LogicException("Invalid '@since' information for event "
 644                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
 645          }
 646  
 647          return $match[1];
 648      }
 649  
 650      /**
 651      * Validate "@changed" Information
 652      *
 653      * @param string $line
 654      * @return string
 655      * @throws \LogicException
 656      */
 657  	public function validate_changed($line)
 658      {
 659          $match = array();
 660          $line = str_replace("\t", ' ', ltrim($line, "\t "));
 661          preg_match('#^\* @changed (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)( (?:.*))?$#', $line, $match);
 662          if (!isset($match[2]))
 663          {
 664              throw new \LogicException("Invalid '@changed' information for event "
 665                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
 666          }
 667  
 668          return $match[2];
 669      }
 670  
 671      /**
 672      * Validate "@event" Information
 673      *
 674      * @param string $event_name
 675      * @param string $line
 676      * @return string
 677      * @throws \LogicException
 678      */
 679  	public function validate_event($event_name, $line)
 680      {
 681          $event = substr(ltrim($line, "\t "), strlen('* @event '));
 682  
 683          if ($event !== trim($event))
 684          {
 685              throw new \LogicException("Invalid '@event' information for event "
 686                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
 687          }
 688  
 689          if ($event !== $event_name)
 690          {
 691              throw new \LogicException("Event name does not match '@event' tag for event "
 692                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
 693          }
 694  
 695          return $event;
 696      }
 697  
 698      /**
 699      * Validates that two arrays contain the same strings
 700      *
 701      * @param array $vars_array        Variables found in the array line
 702      * @param array $vars_docblock    Variables found in the doc block
 703      * @return null
 704      * @throws \LogicException
 705      */
 706  	public function validate_vars_docblock_array($vars_array, $vars_docblock)
 707      {
 708          $vars_array = array_unique($vars_array);
 709          $vars_docblock = array_unique($vars_docblock);
 710          $sizeof_vars_array = sizeof($vars_array);
 711  
 712          if ($sizeof_vars_array !== sizeof($vars_docblock) || $sizeof_vars_array !== sizeof(array_intersect($vars_array, $vars_docblock)))
 713          {
 714              throw new \LogicException("\$vars array does not match the list of '@var' tags for event "
 715                  . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
 716          }
 717      }
 718  }


Generated: Thu Jan 11 00:25:41 2018 Cross-referenced by PHPXref 0.7.1