From 7be6ef1bb4c2b7e9c53dee2e495a7179df768b0a Mon Sep 17 00:00:00 2001 From: btry Date: Mon, 12 Feb 2018 18:53:18 +0100 Subject: [PATCH 01/32] fix(locales): invalid domain fix #872 --- inc/form.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/form.class.php b/inc/form.class.php index 49ec80b4c..6912ce127 100644 --- a/inc/form.class.php +++ b/inc/form.class.php @@ -152,7 +152,7 @@ public function getSearchOptionsNew() { 'id' => '9', 'table' => $this->getTable(), 'field' => 'access_rights', - 'name' => __('Access'), + 'name' => __('Access', 'formcreator'), 'datatype' => 'specific', 'searchtype' => [ '0' => 'equals', From 78e7cd6a55d931c6b02ed34e4bc8bdc3bb8b25af Mon Sep 17 00:00:00 2001 From: btry Date: Mon, 12 Feb 2018 15:29:15 +0100 Subject: [PATCH 02/32] refactor: use docopt as a package --- RoboFile.php | 1 - composer.json | 3 +- composer.lock | 53 +- tools/cliinstall.php | 2 +- tools/docopt.php | 1110 ------------------------------------------ 5 files changed, 53 insertions(+), 1116 deletions(-) delete mode 100644 tools/docopt.php diff --git a/RoboFile.php b/RoboFile.php index 79000f0cf..9d85615dc 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -1,5 +1,4 @@ = 5.4.0", - "ext-xml": "*" + "ext-xml": "*", + "docopt/docopt": "^1.0" }, "require-dev": { "phpunit/phpunit": "^4.8 || ^5.7 || ^6.0", diff --git a/composer.lock b/composer.lock index eac379960..f9404aa8f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,55 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "130b51a4df0023be055391815fc0332d", - "packages": [], + "content-hash": "3dfe16d90045ea07263a85ddaea87777", + "packages": [ + { + "name": "docopt/docopt", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/docopt/docopt.php.git", + "reference": "d2ee65c2fe4be78f945a48edd02be45843b39423" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/docopt/docopt.php/zipball/d2ee65c2fe4be78f945a48edd02be45843b39423", + "reference": "d2ee65c2fe4be78f945a48edd02be45843b39423", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "4.1.*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/docopt.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Blake Williams", + "email": "code@shabbyrobe.org", + "homepage": "http://docopt.org/", + "role": "Developer" + } + ], + "description": "Port of Python's docopt for PHP 5.3", + "homepage": "http://github.com/docopt/docopt.php", + "keywords": [ + "cli", + "docs" + ], + "time": "2015-10-30T03:21:23+00:00" + } + ], "packages-dev": [ { "name": "consolidation/annotated-command", @@ -230,7 +277,7 @@ } ], "description": "Modern task runner", - "time": "2017-03-11T19:48:06+00:00" + "time": "2017-03-11 19:48:06" }, { "name": "container-interop/container-interop", diff --git a/tools/cliinstall.php b/tools/cliinstall.php index 14b05c2cc..e742de0ff 100644 --- a/tools/cliinstall.php +++ b/tools/cliinstall.php @@ -1,5 +1,5 @@ - */ - -namespace Docopt; - -/** - * Return true if all cased characters in the string are uppercase and there is - * at least one cased character, false otherwise. - * Python method with no known equivalent in PHP. - */ -function is_upper($string) { - return preg_match('/[A-Z]/', $string) && !preg_match('/[a-z]/', $string); -} - -/** - * Return True if any element of the iterable is true. If the iterable is empty, return False. - * Python method with no known equivalent in PHP. - */ -function any($iterable) { - foreach ($iterable as $element) { - if ($element) { - return true; - } - } - return false; -} - -/** - * The PHP version of this function doesn't work properly if the values aren't scalar. - */ -function array_count_values($array) { - $counts = []; - foreach ($array as $v) { - if ($v && is_scalar($v)) { - $key = $v; - } else if (is_object($v)) { - $key = spl_object_hash($v); - } else { - $key = serialize($v); - } - - if (!isset($counts[$key])) { - $counts[$key] = array($v, 1); - } else { - $counts[$key][1]++; - } - } - return $counts; -} - -/** - * The PHP version of this doesn't support array iterators - */ -function array_filter($input, $callback, $reKey=false) { - if ($input instanceof \ArrayIterator) { - $input = $input->getArrayCopy(); - } - - $filtered = \array_filter($input, $callback); - if ($reKey) { - $filtered = array_values($filtered); - } - return $filtered; -} - -/** - * The PHP version of this doesn't support array iterators - */ -function array_merge() { - $values = func_get_args(); - $resolved = []; - foreach ($values as $v) { - if ($v instanceof \ArrayIterator) { - $resolved[] = $v->getArrayCopy(); - } else { - $resolved[] = $v; - } - } - return call_user_func_array('array_merge', $resolved); -} - -function ends_with($str, $test) { - $len = strlen($test); - return substr_compare($str, $test, -$len, $len) === 0; -} - -function get_class_name($obj) { - $cls = get_class($obj); - return substr($cls, strpos($cls, '\\')+1); -} - -function dump($val) { - if (is_array($val) || $val instanceof \Traversable) { - echo '['; - $cur = []; - foreach ($val as $i) { - $cur[] = $i->dump(); - } - echo implode(', ', $cur); - echo ']'; - } else { - echo $val->dump(); - } -} - -function dump_scalar($scalar) { - if ($scalar === null) { - return 'None'; - } else if ($scalar === false) { - return 'False'; - } else if ($scalar === true) { - return 'True'; - } else if (is_int($scalar) || is_float($scalar)) { - return $scalar; - } else { - return "'$scalar'"; - } -} - -/** - * Error in construction of usage-message by developer - */ -class LanguageError extends \Exception -{ -} - -/** - * Exit in case user invoked program with incorrect arguments. - * DocoptExit equivalent. - */ -class ExitException extends \RuntimeException -{ - public static $usage; - - public $status; - - public function __construct($message=null, $status=1) { - parent::__construct(trim($message.PHP_EOL.static::$usage)); - $this->status = $status; - } -} - -class Pattern -{ - public function __toString() { - return serialize($this); - } - - public function hash() { - return crc32((string)$this); - } - - public function fix() { - $this->fixIdentities(); - $this->fixRepeatingArguments(); - return $this; - } - - /** - * Make pattern-tree tips point to same object if they are equal. - */ - public function fixIdentities($uniq=null) { - if (!isset($this->children) || !$this->children) { - return $this; - } - - if (!$uniq) { - $uniq = array_unique($this->flat()); - } - - foreach ($this->children as $i=>$c) { - if (!$c instanceof ParentPattern) { - if (!in_array($c, $uniq)) { - // Not sure if this is a true substitute for 'assert c in uniq' - throw new \UnexpectedValueException(); - } - $this->children[$i] = $uniq[array_search($c, $uniq)]; - } else { - $c->fixIdentities($uniq); - } - } - } - - /** - * Fix elements that should accumulate/increment values. - */ - public function fixRepeatingArguments() { - $either = []; - foreach ($this->either()->children as $c) { - $either[] = $c->children; - } - - foreach ($either as $case) { - $case = array_map( - function($value) { return $value[0]; }, - array_filter(array_count_values($case), function($value) { return $value[1] > 1; }) - ); - - foreach ($case as $e) { - if ($e instanceof Argument || ($e instanceof Option && $e->argcount)) { - if (!$e->value) { - $e->value = []; - } else if (!is_array($e->value) && !$e->value instanceof \Traversable) { - $e->value = preg_split('/\s+/', $e->value); - } - } - if ($e instanceof Command || ($e instanceof Option && $e->argcount == 0)) { - $e->value = 0; - } - } - } - - return $this; - } - - /** - * Transform pattern into an equivalent, with only top-level Either. - */ - public function either() { - // Currently the pattern will not be equivalent, but more "narrow", - // although good enough to reason about list arguments. - $ret = []; - $groups = array(array($this)); - while ($groups) { - $children = array_pop($groups); - $types = []; - foreach ($children as $c) { - if (is_object($c)) { - $cls = get_class($c); - $types[] = substr($cls, strrpos($cls, '\\')+1); - } - } - - if (in_array('Either', $types)) { - $either = null; - foreach ($children as $c) { - if ($c instanceof Either) { - $either = $c; - break; - } - } - - unset($children[array_search($either, $children)]); - foreach ($either->children as $c) { - $groups[] = array_merge(array($c), $children); - } - } else if (in_array('Required', $types)) { - $required = null; - foreach ($children as $c) { - if ($c instanceof Required) { - $required = $c; - break; - } - } - unset($children[array_search($required, $children)]); - $groups[] = array_merge($required->children, $children); - } else if (in_array('Optional', $types)) { - $optional = null; - foreach ($children as $c) { - if ($c instanceof Optional) { - $optional = $c; - break; - } - } - unset($children[array_search($optional, $children)]); - $groups[] = array_merge($optional->children, $children); - } else if (in_array('AnyOptions', $types)) { - $optional = null; - foreach ($children as $c) { - if ($c instanceof AnyOptions) { - $optional = $c; - break; - } - } - unset($children[array_search($optional, $children)]); - $groups[] = array_merge($optional->children, $children); - } else if (in_array('OneOrMore', $types)) { - $oneormore = null; - foreach ($children as $c) { - if ($c instanceof OneOrMore) { - $oneormore = $c; - break; - } - } - unset($children[array_search($oneormore, $children)]); - $groups[] = array_merge($oneormore->children, $oneormore->children, $children); - } else { - $ret[] = $children; - } - } - - $rs = []; - foreach ($ret as $e) { - $rs[] = new Required($e); - } - return new Either($rs); - } - - public function name() { - } - - public function __get($name) { - if ($name == 'name') { - return $this->name(); - } else { - throw new \BadMethodCallException("Unknown property $name"); - } - } -} - -class ChildPattern extends Pattern -{ - public function flat($types=[]) { - $types = is_array($types) ? $types : array($types); - - if (!$types || in_array(get_class_name($this), $types)) { - return array($this); - } else { - return []; - } - } - - public function match($left, $collected=null) { - if (!$collected) { - $collected = []; - } - - list ($pos, $match) = $this->singleMatch($left); - if (!$match) { - return array(false, $left, $collected); - } - - $left_ = $left; - unset($left_[$pos]); - $left_ = array_values($left_); - - $name = $this->name; - $sameName = array_filter($collected, function ($a) use ($name) { return $name == $a->name; }, true); - - if (is_int($this->value) || is_array($this->value) || $this->value instanceof \Traversable) { - if (is_int($this->value)) { - $increment = 1; - } else { - $increment = is_string($match->value) ? array($match->value) : $match->value; - } - - if (!$sameName) { - $match->value = $increment; - return array(true, $left_, array_merge($collected, array($match))); - } - - if (is_array($increment) || $increment instanceof \Traversable) { - $sameName[0]->value = array_merge($sameName[0]->value, $increment); - } else { - $sameName[0]->value += $increment; - } - - return array(true, $left_, $collected); - } - - return array(true, $left_, array_merge($collected, array($match))); - } -} - -class ParentPattern extends Pattern -{ - public $children = []; - - public function __construct($children=null) { - if (!$children) { - $children = []; - } else if ($children instanceof Pattern) { - $children = array($children); - } - - foreach ($children as $c) { - $this->children[] = $c; - } - } - - public function flat($types=[]) { - $types = is_array($types) ? $types : array($types); - if (in_array(get_class_name($this), $types)) { - return array($this); - } - - $flat = []; - foreach ($this->children as $c) { - $flat = array_merge($flat, $c->flat($types)); - } - return $flat; - } - - public function dump() { - $out = get_class_name($this).'('; - $cd = []; - foreach ($this->children as $c) { - $cd[] = $c->dump(); - } - $out .= implode(', ', $cd).')'; - return $out; - } -} - -class Argument extends ChildPattern -{ - public $name; - public $value; - - public function __construct($name, $value=null) { - $this->name = $name; - $this->value = $value; - } - - public function singleMatch($left) { - foreach ($left as $n=>$p) { - if ($p instanceof Argument) { - return array($n, new Argument($this->name, $p->value)); - } - } - - return array(null, null); - } - - public static function parse($source) { - $name = null; - $value = null; - - if (preg_match_all('@(<\S*?>)@', $source, $matches)) { - $name = $matches[0][0]; - } - if (preg_match_all('@\[default: (.*)\]@i', $source, $matches)) { - $value = $matches[0][1]; - } - - return new static($name, $value); - } - - public function dump() { - return "Argument('".dump_scalar($this->name)."', ".dump_scalar($this->value)."')"; - } -} - -class Command extends Argument -{ - public $name; - public $value; - - public function __construct($name, $value=false) { - $this->name = $name; - $this->value = $value; - } - - function singleMatch($left) { - foreach ($left as $n=>$p) { - if ($p instanceof Argument) { - if ($p->value == $this->name) { - return array($n, new Command($this->name, true)); - } else { - break; - } - } - } - return array(null, null); - } -} - -class Option extends ChildPattern -{ - public $short; - public $long; - - public function __construct($short=null, $long=null, $argcount=0, $value=false) { - if ($argcount != 0 && $argcount != 1) { - throw new \InvalidArgumentException(); - } - - $this->short = $short; - $this->long = $long; - $this->argcount = $argcount; - $this->value = $value; - - // Python checks "value is False". maybe we should check "$value === false" - if (!$value && $argcount) { - $this->value = null; - } - } - - public static function parse($optionDescription) { - $short = null; - $long = null; - $argcount = 0; - $value = false; - - $exp = explode(' ', trim($optionDescription), 2); - $options = $exp[0]; - $description = isset($exp[1]) ? $exp[1] : ''; - - $options = str_replace(',', ' ', str_replace('=', ' ', $options)); - foreach (preg_split('/\s+/', $options) as $s) { - if (strpos($s, '--')===0) { - $long = $s; - } else if ($s && $s[0] == '-') { - $short = $s; - } else { - $argcount = 1; - } - } - - if ($argcount) { - $value = null; - if (preg_match('@\[default: (.*)\]@i', $description, $match)) { - $value = $match[1]; - } - } - - return new static($short, $long, $argcount, $value); - } - - public function singleMatch($left) { - foreach ($left as $n=>$p) { - if ($this->name == $p->name) { - return array($n, $p); - } - } - return array(null, null); - } - - public function name() { - return $this->long ?: $this->short; - } - - public function dump() { - return "Option('{$this->short}', ".dump_scalar($this->long).", ".dump_scalar($this->argcount).", ".dump_scalar($this->value).")"; - } -} - -class Required extends ParentPattern -{ - public function match($left, $collected=null) { - if (!$collected) { - $collected = []; - } - - $l = $left; - $c = $collected; - - foreach ($this->children as $p) { - list ($matched, $l, $c) = $p->match($l, $c); - if (!$matched) { - return array(false, $left, $collected); - } - } - - return array(true, $l, $c); - } -} - -class Optional extends ParentPattern -{ - public function match($left, $collected=null) { - if (!$collected) { - $collected = []; - } - - foreach ($this->children as $p) { - list($m, $left, $collected) = $p->match($left, $collected); - } - - return array(true, $left, $collected); - } -} - -/** - * Marker/placeholder for [options] shortcut. - */ -class AnyOptions extends Optional -{ -} - -class OneOrMore extends ParentPattern -{ - public function match($left, $collected=null) { - if (count($this->children) != 1) { - throw new \UnexpectedValueException(); - } - - if (!$collected) { - $collected = []; - } - - $l = $left; - $c = $collected; - - $lnew = []; - $matched = true; - $times = 0; - - while ($matched) { - // could it be that something didn't match but changed l or c? - list ($matched, $l, $c) = $this->children[0]->match($l, $c); - if ($matched) { - $times += 1; - } - if ($lnew == $l) { - break; - } - $lnew = $l; - } - - if ($times >= 1) { - return array(true, $l, $c); - } else { - return array(false, $left, $collected); - } - } -} - -class Either extends ParentPattern -{ - public function match($left, $collected=null) { - if (!$collected) { - $collected = []; - } - - $outcomes = []; - foreach ($this->children as $p) { - list ($matched, $dump1, $dump2) = $outcome = $p->match($left, $collected); - if ($matched) { - $outcomes[] = $outcome; - } - } - if ($outcomes) { - // return min(outcomes, key=lambda outcome: len(outcome[1])) - $min = null; - $ret = null; - foreach ($outcomes as $o) { - $cnt = count($o[1]); - if ($min === null || $cnt < $min) { - $min = $cnt; - $ret = $o; - } - } - return $ret; - } else { - return array(false, $left, $collected); - } - } -} - -class TokenStream extends \ArrayIterator -{ - public $error; - - public function __construct($source, $error) { - if (!is_array($source)) { - $source = preg_split('/\s+/', trim($source)); - } - - parent::__construct($source); - - $this->error = $error; - } - - function move() { - $item = $this->current(); - $this->next(); - return $item; - } - - function raiseException($message) { - $class = __NAMESPACE__.'\\'.$this->error; - throw new $class($message); - } -} - -/** - * long ::= '--' chars [ ( ' ' | '=' ) chars ] ; - */ -function parse_long($tokens, \ArrayIterator $options) { - $token = $tokens->move(); - $exploded = explode('=', $token, 2); - if (count($exploded) == 2) { - $long = $exploded[0]; - $eq = '='; - $value = $exploded[1]; - } else { - $long = $token; - $eq = null; - $value = null; - } - - if (strpos($long, '--') !== 0) { - throw new \UnexpectedValueExeption(); - } - - if (!$value) { - $value = null; - } - - $similar = array_filter($options, function($o) use ($long) { return $o->long && $o->long == $long; }, true); - if ('ExitException' == $tokens->error && !$similar) { - $similar = array_filter($options, function($o) use ($long) { return $o->long && strpos($o->long, $long)===0; }, true); - } - - if (count($similar) > 1) { - // might be simply specified ambiguously 2+ times? - $tokens->raiseException("$long is not a unique prefix: ".implode(', ', array_map(function($o) { return $o->long; }, $similar))); - } else if (count($similar) < 1) { - $argcount = $eq == '=' ? 1 : 0; - $o = new Option(null, $long, $argcount); - $options[] = $o; - if ($tokens->error == 'ExitException') { - $o = new Option(null, $long, $argcount, $argcount ? $value : true); - } - } else { - $o = new Option($similar[0]->short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); - if ($o->argcount == 0) { - if ($value !== null) { - $tokens->raiseException("{$o->long} must not have an argument"); - } - } else { - if ($value === null) { - if ($tokens->current() === null) { - $tokens->raiseException("{$o->long} requires argument"); - } - $value = $tokens->move(); - } - } - if ($tokens->error == 'ExitException') { - $o->value = $value !== null ? $value : true; - } - } - - return array($o); -} - -/** - * shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ; - */ -function parse_shorts($tokens, \ArrayIterator $options) { - $token = $tokens->move(); - - if (strpos($token, '-') !== 0 || strpos($token, '--') === 0) { - throw new \UnexpectedValueExeption(); - } - - $left = ltrim($token, '-'); - $parsed = []; - while ($left != '') { - $short = '-'.$left[0]; - $left = substr($left, 1); - $similar = []; - foreach ($options as $o) { - if ($o->short == $short) { - $similar[] = $o; - } - } - - $similarCnt = count($similar); - if ($similarCnt > 1) { - $tokens->raiseException("$short is specified ambiguously $similarCnt times"); - } else if ($similarCnt < 1) { - $o = new Option($short, null, 0); - $options[] = $o; - if ($tokens->error == 'ExitException') { - $o = new Option($short, null, 0, true); - } - } else { - $o = new Option($short, $similar[0]->long, $similar[0]->argcount, $similar[0]->value); - $value = null; - if ($o->argcount != 0) { - if ($left == '') { - if ($tokens->current() === null) { - $tokens->raiseException("$short requires argument"); - } - $value = $tokens->move(); - } else { - $value = $left; - $left = ''; - } - } - if ($tokens->error == 'ExitException') { - $o->value = $value !== null ? $value : true; - } - } - $parsed[] = $o; - } - - return $parsed; -} - -function parse_pattern($source, \ArrayIterator $options) { - $tokens = new TokenStream(preg_replace('@([\[\]\(\)\|]|\.\.\.)@', ' $1 ', $source), 'LanguageError'); - - $result = parse_expr($tokens, $options); - if ($tokens->current() != null) { - $tokens->raiseException('unexpected ending: '.implode(' ', $tokens)); - } - return new Required($result); -} - -/** - * expr ::= seq ( '|' seq )* ; - */ -function parse_expr($tokens, \ArrayIterator $options) { - $seq = parse_seq($tokens, $options); - if ($tokens->current() != '|') { - return $seq; - } - - $result = null; - if (count($seq) > 1) { - $result = array(new Required($seq)); - } else { - $result = $seq; - } - - while ($tokens->current() == '|') { - $tokens->move(); - $seq = parse_seq($tokens, $options); - if (count($seq) > 1) { - $result[] = new Required($seq); - } else { - $result = array_merge($result, $seq); - } - } - - if (count($result) > 1) { - return new Either($result); - } else { - return $result; - } -} - -/** - * seq ::= ( atom [ '...' ] )* ; - */ -function parse_seq($tokens, \ArrayIterator $options) { - $result = []; - $not = array(null, '', ']', ')', '|'); - while (!in_array($tokens->current(), $not, true)) { - $atom = parse_atom($tokens, $options); - if ($tokens->current() == '...') { - $atom = array(new OneOrMore($atom)); - $tokens->move(); - } - if ($atom instanceof \ArrayIterator) { - $atom = $atom->getArrayCopy(); - } - if ($atom) { - $result = array_merge($result, $atom); - } - } - return $result; -} - -/** - * atom ::= '(' expr ')' | '[' expr ']' | 'options' - * | long | shorts | argument | command ; - */ -function parse_atom($tokens, \ArrayIterator $options) { - $token = $tokens->current(); - $result = []; - if ($token == '(' || $token == '[') { - $tokens->move(); - - static $index; - if (!$index) { - $index = array('('=>array(')', __NAMESPACE__.'\Required'), '['=>array(']', __NAMESPACE__.'\Optional')); - } - list ($matching, $pattern) = $index[$token]; - - $result = new $pattern(parse_expr($tokens, $options)); - if ($tokens->move() != $matching) { - $tokens->raiseException("Unmatched '$token'"); - } - - return array($result); - } else if ($token == 'options') { - $tokens->move(); - return array(new AnyOptions); - } else if (strpos($token, '--') === 0 && $token != '--') { - return parse_long($tokens, $options); - } else if (strpos($token, '-') === 0 && $token != '-' && $token != '--') { - return parse_shorts($tokens, $options); - } else if (strpos($token, '<') === 0 && ends_with($token, '>') || is_upper($token)) { - return array(new Argument($tokens->move())); - } else { - return array(new Command($tokens->move())); - } -} - -/** - * Parse command-line argument vector. - * - * If options_first: - * argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; - * else: - * argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; - */ -function parse_argv($tokens, \ArrayIterator $options, $optionsFirst=false) { - $parsed = []; - - while ($tokens->current() !== null) { - if ($tokens->current() == '--') { - foreach ($tokens as $v) { - $parsed[] = new Argument(null, $v); - } - return $parsed; - } else if (strpos($tokens->current(), '--')===0) { - $parsed = array_merge($parsed, parse_long($tokens, $options)); - } else if (strpos($tokens->current(), '-')===0 && $tokens->current() != '-') { - $parsed = array_merge($parsed, parse_shorts($tokens, $options)); - } else if ($optionsFirst) { - return array_merge($parsed, array_map(function($v) { return new Argument(null, $v); }, $tokens)); - } else { - $parsed[] = new Argument(null, $tokens->move()); - } - } - return $parsed; -} - -function parse_defaults($doc) { - $splitTmp = array_slice(preg_split('@\n[ ]*(<\S+?>|-\S+?)@', $doc, null, PREG_SPLIT_DELIM_CAPTURE), 1); - $split = []; - for ($cnt = count($splitTmp), $i=0; $i < $cnt; $i+=2) { - $split[] = $splitTmp[$i] . (isset($splitTmp[$i+1]) ? $splitTmp[$i+1] : ''); - } - $options = new \ArrayIterator(); - foreach ($split as $s) { - if (strpos($s, '-') === 0) { - $options[] = Option::parse($s); - } - } - return $options; -} - -function printable_usage($doc) { - $usageSplit = preg_split("@([Uu][Ss][Aa][Gg][Ee]:)@", $doc, null, PREG_SPLIT_DELIM_CAPTURE); - - if (count($usageSplit) < 3) { - throw new LanguageError('"usage:" (case-insensitive) not found.'); - } else if (count($usageSplit) > 3) { - throw new LanguageError('More than one "usage:" (case-insensitive).'); - } - - $split = preg_split("@\n\s*\n@", implode('', array_slice($usageSplit, 1))); - - return trim($split[0]); -} - -function formal_usage($printableUsage) { - $pu = array_slice(preg_split('/\s+/', $printableUsage), 1); - - $ret = []; - foreach (array_slice($pu, 1) as $s) { - if ($s == $pu[0]) { - $ret[] = ') | ('; - } else { - $ret[] = $s; - } - } - - return '( '.implode(' ', $ret).' )'; -} - -function extras($help, $version, $options, $doc) { - $ofound = false; - $vfound = false; - foreach ($options as $o) { - if ($o->value && ($o->name == '-h' || $o->name == '--help')) { - $ofound = true; - } - if ($o->value && $o->name == '--version') { - $vfound = true; - } - } - if ($help && $ofound) { - ExitException::$usage = null; - throw new ExitException($doc, 0); - } - if ($version && $vfound) { - ExitException::$usage = null; - throw new ExitException($version, 0); - } -} - -/** - * API compatibility with python docopt - */ -function docopt($doc, $params=[]) { - $argv = []; - if (isset($params['argv'])) { - $argv = $params['argv']; - unset($params['argv']); - } - $h = new Handler($params); - return $h->handle($doc, $argv); -} - -/** - * Use a class in PHP because we can't autoload functions yet. - */ -class Handler -{ - public $exit = true; - public $help = true; - public $optionsFirst = false; - public $version; - - public function __construct($options=[]) { - foreach ($options as $k=>$v) { - $this->$k = $v; - } - } - - function handle($doc, $argv=null) { - try { - if (!$argv && isset($_SERVER['argv'])) { - $argv = array_slice($_SERVER['argv'], 1); - } - - ExitException::$usage = printable_usage($doc); - $options = parse_defaults($doc); - - $formalUse = formal_usage(ExitException::$usage); - $pattern = parse_pattern($formalUse, $options); - $argv = parse_argv(new TokenStream($argv, 'ExitException'), $options, $this->optionsFirst); - foreach ($pattern->flat('AnyOptions') as $ao) { - $docOptions = parse_defaults($doc); - $ao->children = array_diff((array)$docOptions, $pattern->flat('Option')); - } - - extras($this->help, $this->version, $argv, $doc); - - list($matched, $left, $collected) = $pattern->fix()->match($argv); - if ($matched && !$left) { - $return = []; - foreach (array_merge($pattern->flat(), $collected) as $a) { - $name = $a->name; - if ($name) { - $return[$name] = $a->value; - } - } - return new Response($return); - } - throw new ExitException(); - } catch (ExitException $ex) { - $this->handleExit($ex); - return new Response(null, $ex->status, $ex->getMessage()); - } - } - - function handleExit(ExitException $ex) { - if ($this->exit) { - echo $ex->getMessage().PHP_EOL; - exit($ex->status); - } - } -} - -class Response implements \ArrayAccess, \IteratorAggregate -{ - public $status; - public $output; - public $args; - - public function __construct($args, $status=0, $output='') { - $this->args = $args ?: []; - $this->status = $status; - $this->output = $output; - } - - public function __get($name) { - if ($name == 'success') { - return $this->status === 0; - } else { - throw new \BadMethodCallException("Unknown property $name"); - } - } - - public function offsetExists($offset) { - return isset($this->args[$offset]); - } - - public function offsetGet($offset) { - return $this->args[$offset]; - } - - public function offsetSet($offset, $value) { - $this->args[$offset] = $value; - } - - public function offsetUnset($offset) { - unset($this->args[$offset]); - } - - public function getIterator () { - return new \ArrayIterator($this->args); - } -} From e978ca3d686bb87be339431f446d1abe5cf1b23e Mon Sep 17 00:00:00 2001 From: btry Date: Mon, 12 Feb 2018 15:30:24 +0100 Subject: [PATCH 03/32] chore: add tool to update source headers --- RoboFile.php | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/RoboFile.php b/RoboFile.php index 9d85615dc..104322a47 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -319,4 +319,110 @@ protected function sourceUpdatePackageJson($version) { protected function sourceUpdateComposerJson($version) { $this->updateJsonFile('composer.json', $version); } + + /** + * Update headers in source files + */ + public function codeHeadersUpdate() { + $toUpdate = $this->getTrackedFiles('HEAD'); + foreach ($toUpdate as $file) { + $this->replaceSourceHeader($file); + } + } + + /** + * Read the header template from a file + * @throws Exception + * @return string + */ + protected function getHeaderTemplate() { + if (empty($this->headerTemplate)) { + $this->headerTemplate = file_get_contents(__DIR__ . '/tools/HEADER'); + if (empty($this->headerTemplate)) { + throw new Exception('Header template file not found'); + } + } + + $copyrightRegex = "#Copyright (\(c\)|©) (\d{4}-)?(\d{4}) #iUm"; + $year = date("Y"); + $replacement = 'Copyright © ${2}' . $year . ' '; + $this->headerTemplate = preg_replace($copyrightRegex, $replacement, $this->headerTemplate); + + return $this->headerTemplate; + } + + /** + * Format header template for a file type based on extension + * + * @param string $extension + * @param string $template + * @return string + */ + protected function getFormatedHeaderTemplate($extension, $template) { + switch ($extension) { + case 'php': + $lines = explode("\n", $template); + foreach ($lines as &$line) { + $line = rtrim(" * $line"); + } + return implode("\n", $lines); + break; + + default: + return $template; + } + } + + /** + * Update source code header in a source file + * @param string $filename + */ + protected function replaceSourceHeader($filename) { + $filename = __DIR__ . "/$filename"; + + // define regex for the file type + $ext = pathinfo($filename, PATHINFO_EXTENSION); + switch ($ext) { + case 'php': + $prefix = "\<\?php\\n/\*(\*)?\\n"; + $replacementPrefix = "getHeaderTemplate()); + $formatedHeader = $replacementPrefix . $this->getFormatedHeaderTemplate($ext, $header) . $replacementSuffix; + + // get the content of the file to update + $source = file_get_contents($filename); + + // update authors in formated template + $headerMatch = []; + $originalAuthors = []; + $authors = []; + $authorsRegex = "#^.*(\@author .*)$#Um"; + preg_match('#^' . $prefix . '(.*)' . $suffix . '#Us', $source, $headerMatch); + if (isset($headerMatch[0])) { + $originalHeader = $headerMatch[0]; + preg_match_all($authorsRegex, $originalHeader, $originalAuthors); + if (isset($originalAuthors[1])) { + $originalAuthors = $this->getFormatedHeaderTemplate($ext, implode("\n", $originalAuthors[1])); + $formatedHeader = preg_replace($authorsRegex, $originalAuthors, $formatedHeader, 1); + } + } + + // replace the header if it exists + $source = preg_replace('#^' . $prefix . '(.*)' . $suffix . '#Us', $formatedHeader, $source, 1); + if (empty($source)) { + throw new Exception("An error occurred while processing $filename"); + } + + file_put_contents($filename, $source); + } } From e7318dbb5240d74df02600366874b060c78138e2 Mon Sep 17 00:00:00 2001 From: btry Date: Mon, 12 Feb 2018 17:43:40 +0100 Subject: [PATCH 04/32] docs: add file header --- RoboFile.php | 27 ++++++++- RoboFilePlugin.php | 32 +++++++++- ajax/dropdown_values.php | 33 +++++++++++ ajax/homepage_forms.php | 33 +++++++++++ ajax/homepage_link.php | 33 +++++++++++ ajax/homepage_wizard.php | 33 +++++++++++ ajax/ldap_filter.php | 33 +++++++++++ ajax/question.php | 33 +++++++++++ ajax/question_condition.php | 33 +++++++++++ ajax/section.php | 33 +++++++++++ ajax/showfields.php | 33 +++++++++++ ajax/target.php | 33 +++++++++++ css/print_form.css | 33 +++++++++++ css/print_form_answer.css | 33 +++++++++++ css/styles.css | 33 +++++++++++ front/category.form.php | 33 +++++++++++ front/category.php | 33 +++++++++++ front/entityconfig.form.php | 33 +++++++++++ front/export.php | 33 +++++++++++ front/form.form.php | 33 +++++++++++ front/form.php | 33 +++++++++++ front/form_answer.form.php | 33 +++++++++++ front/form_answer.php | 33 +++++++++++ front/form_profile.form.php | 33 +++++++++++ front/formdisplay.php | 33 +++++++++++ front/formlist.php | 33 +++++++++++ front/issue.form.php | 33 +++++++++++ front/issue.php | 33 +++++++++++ front/item_targetticket.form.php | 33 +++++++++++ front/knowbaseitem.form.php | 33 +++++++++++ front/question.form.php | 33 +++++++++++ front/reservation.form.php | 33 +++++++++++ front/reservation.php | 33 +++++++++++ front/reservationitem.php | 33 +++++++++++ front/section.form.php | 33 +++++++++++ front/target.form.php | 33 +++++++++++ front/targetchange.form.php | 33 +++++++++++ front/targetticket.form.php | 33 +++++++++++ front/wizard.php | 33 +++++++++++ front/wizardfeeds.php | 33 +++++++++++ hook.php | 32 +++++++++- inc/answer.class.php | 33 +++++++++++ inc/category.class.php | 33 +++++++++++ inc/common.class.php | 33 +++++++++++ inc/composite.class.php | 33 +++++++++++ inc/entityconfig.class.php | 33 +++++++++++ inc/field.class.php | 33 +++++++++++ inc/field.interface.php | 33 +++++++++++ inc/fieldinterface.class.php | 33 +++++++++++ inc/fields.class.php | 33 +++++++++++ inc/fields/actorfield.class.php | 33 +++++++++++ inc/fields/checkboxesfield.class.php | 33 +++++++++++ inc/fields/datefield.class.php | 33 +++++++++++ inc/fields/datetimefield.class.php | 33 +++++++++++ inc/fields/descriptionfield.class.php | 33 +++++++++++ inc/fields/dropdownfield.class.php | 3 + inc/fields/emailfield.class.php | 33 +++++++++++ inc/fields/filefield.class.php | 33 +++++++++++ inc/fields/floatfield.class.php | 33 +++++++++++ inc/fields/glpiselectfield.class.php | 33 +++++++++++ inc/fields/hiddenfield.class.php | 33 +++++++++++ inc/fields/integerfield.class.php | 33 +++++++++++ inc/fields/ipfield.class.php | 33 +++++++++++ inc/fields/ldapselectfield.class.php | 33 +++++++++++ inc/fields/multiselectfield.class.php | 33 +++++++++++ inc/fields/radiosfield.class.php | 33 +++++++++++ inc/fields/selectfield.class.php | 33 +++++++++++ inc/fields/tagfield.class.php | 33 +++++++++++ inc/fields/textareafield.class.php | 33 +++++++++++ inc/fields/textfield.class.php | 33 +++++++++++ inc/fields/urgencyfield.class.php | 33 +++++++++++ inc/form.class.php | 33 +++++++++++ inc/form_answer.class.php | 33 +++++++++++ inc/form_profile.class.php | 33 +++++++++++ inc/form_validator.class.php | 33 +++++++++++ inc/formlist.class.php | 34 ++++++++++- inc/issue.class.php | 33 +++++++++++ inc/item_targetticket.class.php | 33 +++++++++++ inc/notificationtargetform_answer.class.php | 33 +++++++++++ inc/question.class.php | 33 +++++++++++ inc/question_condition.class.php | 33 +++++++++++ inc/section.class.php | 34 +++++++++++ inc/target.class.php | 33 +++++++++++ inc/target_actor.class.php | 33 +++++++++++ inc/targetbase.class.php | 33 +++++++++++ inc/targetchange.class.php | 33 +++++++++++ inc/targetchange_actor.class.php | 33 +++++++++++ inc/targetticket.class.php | 33 +++++++++++ inc/targetticket_actor.class.php | 33 +++++++++++ inc/wizard.class.php | 33 +++++++++++ install/update_0.0_2.5.php | 36 +++++++++++- install/update_2.5_2.6.php | 37 +++++++++++- install/update_2.6_2.6.1.php | 33 +++++++++++ install/update_dev.php | 37 ++++++++++++ tests/0000_Install/PluginInstallTest.php | 61 +++++++++++--------- tests/0000_Install/SaveInstallTest.php | 61 +++++++++++--------- tests/9000_Uninstall/PluginUninstallTest.php | 61 +++++++++++--------- tools/HEADER | 46 ++++++++------- 98 files changed, 3193 insertions(+), 113 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 104322a47..a0dcce8e5 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -361,6 +361,7 @@ protected function getHeaderTemplate() { protected function getFormatedHeaderTemplate($extension, $template) { switch ($extension) { case 'php': + case 'css': $lines = explode("\n", $template); foreach ($lines as &$line) { $line = rtrim(" * $line"); @@ -390,6 +391,12 @@ protected function replaceSourceHeader($filename) { $replacementSuffix = "\n */"; break; + case 'css': + $prefix = "/\*(\*)?\\n"; + $replacementPrefix = "/**\n"; + $suffix = "\\n( )?\*/"; + $replacementSuffix = "\n */"; + break; default: // Unhandled file format return; @@ -411,9 +418,27 @@ protected function replaceSourceHeader($filename) { if (isset($headerMatch[0])) { $originalHeader = $headerMatch[0]; preg_match_all($authorsRegex, $originalHeader, $originalAuthors); + if (!is_array($originalAuthors)) { + $originalAuthors = [$originalAuthors]; + } if (isset($originalAuthors[1])) { + $originalAuthors[1] = array_unique($originalAuthors[1]); $originalAuthors = $this->getFormatedHeaderTemplate($ext, implode("\n", $originalAuthors[1])); - $formatedHeader = preg_replace($authorsRegex, $originalAuthors, $formatedHeader, 1); + $countOfAuthors = preg_match_all($authorsRegex, $formatedHeader); + if ($countOfAuthors !== false) { + // Empty all author lines except the last one + $formatedHeader = preg_replace($authorsRegex, '', $formatedHeader, $countOfAuthors - 1); + // remove the lines previously reduced to zero + $lines = explode("\n", $formatedHeader); + $formatedHeader = []; + foreach ($lines as $line) { + if ($line !== '') { + $formatedHeader[] = $line; + }; + } + $formatedHeader = implode("\n", $formatedHeader); + $formatedHeader = preg_replace($authorsRegex, $originalAuthors, $formatedHeader, 1); + } } } diff --git a/RoboFilePlugin.php b/RoboFilePlugin.php index 602a56d7c..02c8d029c 100644 --- a/RoboFilePlugin.php +++ b/RoboFilePlugin.php @@ -1,8 +1,36 @@ "; diff --git a/ajax/homepage_wizard.php b/ajax/homepage_wizard.php index af17dc668..4fb09dd78 100644 --- a/ajax/homepage_wizard.php +++ b/ajax/homepage_wizard.php @@ -1,4 +1,37 @@ = 2.5.0 to 2.6.0 + * @param Migration $migration */ function plugin_formcreator_update_2_6(Migration $migration) { global $DB; diff --git a/install/update_2.6_2.6.1.php b/install/update_2.6_2.6.1.php index a3601495c..b0e4af384 100644 --- a/install/update_2.6_2.6.1.php +++ b/install/update_2.6_2.6.1.php @@ -1,4 +1,37 @@ . - -------------------------------------------------------------------------- - @package Formcreator - @author the Formcreator plugin team - @copyright Copyright (c) 2015 Formcreator plugin team - @license GPLv2+ http://www.gnu.org/licenses/gpl.txt - @link https://github.com/PluginsGLPI/Formcreator - @link http://www.glpi-project.org/ - @since 0.1.33 - ---------------------------------------------------------------------- -*/ +/** + * LICENSE + * + * Copyright © 2011-2018 Teclib' + * + * This file is part of Formcreator Plugin for GLPI. + * + * Formcreator is a plugin that allow creation of custom, easy to access forms + * for users when they want to create one or more GLPI tickets. + * + * Formcreator Plugin for GLPI is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * Formcreator Plugin for GLPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * If not, see http://www.gnu.org/licenses/. + * ------------------------------------------------------------------------------ + * @author the Formcreator plugin team + * @author Thierry Bugier + * @author Jérémy Moreau + * @copyright Copyright © 2018 Teclib + * @license GPLv2 https://www.gnu.org/licenses/gpl2.txt + * @link https://github.com/pluginsGLPI/formcreator/ + * @link http://plugins.glpi-project.org/#/plugin/formcreator + * ------------------------------------------------------------------------------ + */ class SaveInstallTest extends CommonDBTestCase { diff --git a/tests/9000_Uninstall/PluginUninstallTest.php b/tests/9000_Uninstall/PluginUninstallTest.php index 5439162bd..a02aec3c2 100644 --- a/tests/9000_Uninstall/PluginUninstallTest.php +++ b/tests/9000_Uninstall/PluginUninstallTest.php @@ -1,32 +1,37 @@ . - -------------------------------------------------------------------------- +Formcreator Plugin for GLPI is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +If not, see http://www.gnu.org/licenses/. +------------------------------------------------------------------------------ +@author Jérémy Moreau +@author Thierry Bugier +@copyright Copyright © 2018 Teclib +@license GPLv2 https://www.gnu.org/licenses/gpl2.txt +@link https://github.com/pluginsGLPI/formcreator/ +@link http://plugins.glpi-project.org/#/plugin/formcreator +------------------------------------------------------------------------------ From 39e350af87fd72c74c38f476fbc96d7092709068 Mon Sep 17 00:00:00 2001 From: btry Date: Mon, 12 Feb 2018 17:46:36 +0100 Subject: [PATCH 05/32] refactor: move cli installer script to follow GLPI --- hook.php | 5 +++++ {tools => scripts}/cliinstall.php | 0 2 files changed, 5 insertions(+) rename {tools => scripts}/cliinstall.php (100%) diff --git a/hook.php b/hook.php index 2401965b3..ba7c68fd7 100644 --- a/hook.php +++ b/hook.php @@ -32,6 +32,11 @@ * @link http://plugins.glpi-project.org/#/plugin/formcreator * ------------------------------------------------------------------------------ */ + +/** + * Install all necessary elements for the plugin + * @return boolean True if success + */ function plugin_formcreator_install() { spl_autoload_register('plugin_formcreator_autoload'); diff --git a/tools/cliinstall.php b/scripts/cliinstall.php similarity index 100% rename from tools/cliinstall.php rename to scripts/cliinstall.php From b901bda697b150aab2a9006b390812d9bcb080f6 Mon Sep 17 00:00:00 2001 From: btry Date: Tue, 13 Feb 2018 10:42:01 +0100 Subject: [PATCH 06/32] test: fix unit tests --- tests/inc/CommonDBTestCase.php | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/inc/CommonDBTestCase.php b/tests/inc/CommonDBTestCase.php index 5237bfe92..cf6f9c6c5 100644 --- a/tests/inc/CommonDBTestCase.php +++ b/tests/inc/CommonDBTestCase.php @@ -127,14 +127,25 @@ protected static function mysql_dump($dbuser = '', $dbhost = '', $dbpassword = ' } protected static function setupGLPIFramework() { - global $CFG_GLPI, $DB, $LOADED_PLUGINS; + global $CFG_GLPI, $DB, $LOADED_PLUGINS, $PLUGIN_HOOKS, $AJAX_INCLUDE, $PLUGINS_INCLUDED; + if (session_status() == PHP_SESSION_ACTIVE) { + session_write_close(); + } $LOADED_PLUGINS = null; + $PLUGINS_INCLUDED = null; + $AJAX_INCLUDE = null; $_SESSION = []; $_SESSION['glpi_use_mode'] = Session::NORMAL_MODE; // Prevents notice in execution of GLPI_ROOT . /inc/includes.php + if (is_readable(GLPI_ROOT . "/config/config.php")) { + $configFile = "/config/config.php"; + } else { + $configFile = "/inc/config.php"; + } + include (GLPI_ROOT . $configFile); require (GLPI_ROOT . "/inc/includes.php"); - - $DB = new DB(); + $_SESSION['glpi_use_mode'] = Session::DEBUG_MODE; + \Toolbox::setDebugMode(); include_once (GLPI_ROOT . "/inc/timer.class.php"); @@ -144,13 +155,14 @@ protected static function setupGLPIFramework() { ini_set("memory_limit", "-1"); ini_set("max_execution_time", "0"); - //ini_set('session.use_cookies', 0); //disable session cookies + if (session_status() == PHP_SESSION_ACTIVE) { + session_write_close(); + } + session_start(); $_SESSION['MESSAGE_AFTER_REDIRECT'] = []; } protected static function login($name, $password, $noauto = false) { - global $DB; - Session::start(); $_SESSION['glpi_use_mode'] = Session::NORMAL_MODE; $auth = new Auth(); @@ -159,5 +171,4 @@ protected static function login($name, $password, $noauto = false) { return $result; } - } From c4d1a4cec548843117c540836b6ce783f41475dc Mon Sep 17 00:00:00 2001 From: btry Date: Wed, 14 Feb 2018 10:36:04 +0100 Subject: [PATCH 07/32] fix(field): some fields output a duplicate HTML div --- inc/fields/dropdownfield.class.php | 3 +-- inc/fields/multiselectfield.class.php | 2 -- inc/fields/selectfield.class.php | 6 ++---- inc/fields/tagfield.class.php | 3 +-- inc/fields/urgencyfield.class.php | 3 +-- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/inc/fields/dropdownfield.class.php b/inc/fields/dropdownfield.class.php index 506d985dc..7695cb1fc 100644 --- a/inc/fields/dropdownfield.class.php +++ b/inc/fields/dropdownfield.class.php @@ -6,7 +6,6 @@ class PluginFormcreatorDropdownField extends PluginFormcreatorField { public function displayField($canEdit = true) { if ($canEdit) { - echo '
'; if (!empty($this->fields['values'])) { $rand = mt_rand(); $required = $this->fields['required'] ? ' required' : ''; @@ -46,7 +45,7 @@ public function displayField($canEdit = true) { $itemtype::dropdown($dparams); } - echo '
' . PHP_EOL; + echo PHP_EOL; echo ''; } else { - echo '
'; echo nl2br($this->getAnswer()); - echo '
' . PHP_EOL; + echo PHP_EOL; } } diff --git a/inc/fields/tagfield.class.php b/inc/fields/tagfield.class.php index f4fb6c37f..8d9c41869 100644 --- a/inc/fields/tagfield.class.php +++ b/inc/fields/tagfield.class.php @@ -41,7 +41,6 @@ public function displayField($canEdit = true) { $rand = mt_rand(); $required = $this->fields['required'] ? ' required' : ''; - echo '
'; $values = []; $obj = new PluginTagTag(); @@ -61,7 +60,7 @@ public function displayField($canEdit = true) { 'rand' => $rand, 'multiple' => true, ]); - echo '
' . PHP_EOL; + echo PHP_EOL; echo '