2008-08-31 05:56:49 +00:00
|
|
|
<?php
|
2012-09-02 11:07:02 +00:00
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
2017-10-11 22:07:48 +00:00
|
|
|
use Wikimedia\Equivset\Equivset;
|
|
|
|
|
2008-08-31 05:56:49 +00:00
|
|
|
class AbuseFilterParser {
|
2018-11-08 14:34:32 +00:00
|
|
|
/**
|
2019-08-21 09:01:50 +00:00
|
|
|
* @var array[] Contains the AFPTokens for the code being parsed
|
2018-11-08 14:34:32 +00:00
|
|
|
*/
|
|
|
|
public $mTokens;
|
|
|
|
/**
|
|
|
|
* @var int The position of the current token
|
|
|
|
*/
|
|
|
|
public $mPos;
|
|
|
|
/**
|
|
|
|
* @var bool Are we inside a short circuit evaluation?
|
|
|
|
*/
|
|
|
|
public $mShortCircuit;
|
|
|
|
/**
|
|
|
|
* @var bool Are we allowed to use short-circuit evaluation?
|
|
|
|
*/
|
|
|
|
public $mAllowShort;
|
|
|
|
/**
|
|
|
|
* @var AFPToken The current token
|
|
|
|
*/
|
2018-10-03 12:02:00 +00:00
|
|
|
public $mCur;
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
|
|
|
* @var AbuseFilterVariableHolder
|
|
|
|
*/
|
2018-12-27 17:06:56 +00:00
|
|
|
public $mVariables;
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-01-24 10:33:01 +00:00
|
|
|
/**
|
|
|
|
* @var int The current amount of conditions being consumed
|
|
|
|
*/
|
|
|
|
protected $mCondCount;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var bool Whether the condition limit is enabled.
|
|
|
|
*/
|
|
|
|
protected $condLimitEnabled = true;
|
|
|
|
|
2019-08-10 16:34:42 +00:00
|
|
|
/**
|
|
|
|
* @var string|null The ID of the filter being parsed, if available. Can also be "global-$ID"
|
|
|
|
*/
|
|
|
|
protected $mFilter;
|
2019-08-11 13:11:20 +00:00
|
|
|
/**
|
|
|
|
* @var bool Whether we can allow retrieving _builtin_ variables not included in $this->mVariables
|
|
|
|
*/
|
|
|
|
protected $allowMissingVariables = false;
|
2019-08-10 16:34:42 +00:00
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
/**
|
|
|
|
* @var BagOStuff Used to cache the AST (in CachingParser) and the tokens
|
|
|
|
*/
|
|
|
|
protected $cache;
|
|
|
|
/**
|
|
|
|
* @var LoggerInterface Used for debugging
|
|
|
|
*/
|
|
|
|
protected $logger;
|
|
|
|
/**
|
|
|
|
* @var Language Content language, used for language-dependent functions
|
|
|
|
*/
|
|
|
|
protected $contLang;
|
2019-09-16 16:53:36 +00:00
|
|
|
/**
|
|
|
|
* @var IBufferingStatsdDataFactory
|
|
|
|
*/
|
|
|
|
protected $statsd;
|
2019-08-21 10:04:10 +00:00
|
|
|
|
2019-11-16 15:32:36 +00:00
|
|
|
public const FUNCTIONS = [
|
2009-03-07 01:26:42 +00:00
|
|
|
'lcase' => 'funcLc',
|
2013-04-17 16:36:10 +00:00
|
|
|
'ucase' => 'funcUc',
|
2009-03-07 01:26:42 +00:00
|
|
|
'length' => 'funcLen',
|
|
|
|
'string' => 'castString',
|
|
|
|
'int' => 'castInt',
|
|
|
|
'float' => 'castFloat',
|
|
|
|
'bool' => 'castBool',
|
|
|
|
'norm' => 'funcNorm',
|
|
|
|
'ccnorm' => 'funcCCNorm',
|
2017-09-19 23:54:03 +00:00
|
|
|
'ccnorm_contains_any' => 'funcCCNormContainsAny',
|
2017-11-13 19:13:07 +00:00
|
|
|
'ccnorm_contains_all' => 'funcCCNormContainsAll',
|
2009-03-07 01:26:42 +00:00
|
|
|
'specialratio' => 'funcSpecialRatio',
|
|
|
|
'rmspecials' => 'funcRMSpecials',
|
|
|
|
'rmdoubles' => 'funcRMDoubles',
|
|
|
|
'rmwhitespace' => 'funcRMWhitespace',
|
|
|
|
'count' => 'funcCount',
|
|
|
|
'rcount' => 'funcRCount',
|
2017-11-07 18:44:10 +00:00
|
|
|
'get_matches' => 'funcGetMatches',
|
2009-03-09 12:39:52 +00:00
|
|
|
'ip_in_range' => 'funcIPInRange',
|
2009-03-26 02:03:32 +00:00
|
|
|
'contains_any' => 'funcContainsAny',
|
2017-11-13 19:13:07 +00:00
|
|
|
'contains_all' => 'funcContainsAll',
|
2018-02-09 11:24:01 +00:00
|
|
|
'equals_to_any' => 'funcEqualsToAny',
|
2009-04-01 05:05:23 +00:00
|
|
|
'substr' => 'funcSubstr',
|
|
|
|
'strlen' => 'funcLen',
|
|
|
|
'strpos' => 'funcStrPos',
|
|
|
|
'str_replace' => 'funcStrReplace',
|
2011-10-18 17:57:33 +00:00
|
|
|
'rescape' => 'funcStrRegexEscape',
|
2009-04-01 06:53:18 +00:00
|
|
|
'set' => 'funcSetVar',
|
|
|
|
'set_var' => 'funcSetVar',
|
2018-01-31 00:32:14 +00:00
|
|
|
'sanitize' => 'funcSanitize',
|
2017-06-15 14:23:34 +00:00
|
|
|
];
|
2009-10-07 13:57:06 +00:00
|
|
|
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
/**
|
2019-08-21 09:01:50 +00:00
|
|
|
* The minimum and maximum amount of arguments required by each function.
|
|
|
|
* @var int[][]
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
*/
|
2019-10-03 12:13:26 +00:00
|
|
|
const FUNC_ARG_COUNT = [
|
2019-08-21 09:01:50 +00:00
|
|
|
'lcase' => [ 1, 1 ],
|
|
|
|
'ucase' => [ 1, 1 ],
|
|
|
|
'length' => [ 1, 1 ],
|
|
|
|
'string' => [ 1, 1 ],
|
|
|
|
'int' => [ 1, 1 ],
|
|
|
|
'float' => [ 1, 1 ],
|
|
|
|
'bool' => [ 1, 1 ],
|
|
|
|
'norm' => [ 1, 1 ],
|
|
|
|
'ccnorm' => [ 1, 1 ],
|
|
|
|
'ccnorm_contains_any' => [ 2, INF ],
|
|
|
|
'ccnorm_contains_all' => [ 2, INF ],
|
|
|
|
'specialratio' => [ 1, 1 ],
|
|
|
|
'rmspecials' => [ 1, 1 ],
|
|
|
|
'rmdoubles' => [ 1, 1 ],
|
|
|
|
'rmwhitespace' => [ 1, 1 ],
|
|
|
|
'count' => [ 1, 2 ],
|
|
|
|
'rcount' => [ 1, 2 ],
|
|
|
|
'get_matches' => [ 2, 2 ],
|
|
|
|
'ip_in_range' => [ 2, 2 ],
|
|
|
|
'contains_any' => [ 2, INF ],
|
|
|
|
'contains_all' => [ 2, INF ],
|
|
|
|
'equals_to_any' => [ 2, INF ],
|
|
|
|
'substr' => [ 2, 3 ],
|
|
|
|
'strlen' => [ 1, 1 ],
|
|
|
|
'strpos' => [ 2, 3 ],
|
|
|
|
'str_replace' => [ 3, 3 ],
|
|
|
|
'rescape' => [ 1, 1 ],
|
|
|
|
'set' => [ 2, 2 ],
|
|
|
|
'set_var' => [ 2, 2 ],
|
|
|
|
'sanitize' => [ 1, 1 ],
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
];
|
|
|
|
|
2009-04-01 06:53:18 +00:00
|
|
|
// Functions that affect parser state, and shouldn't be cached.
|
2019-11-16 15:32:36 +00:00
|
|
|
public const ACTIVE_FUNCTIONS = [
|
2009-04-01 06:53:18 +00:00
|
|
|
'funcSetVar',
|
2017-06-15 14:23:34 +00:00
|
|
|
];
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-11-16 15:32:36 +00:00
|
|
|
public const KEYWORDS = [
|
2016-08-24 04:52:58 +00:00
|
|
|
'in' => 'keywordIn',
|
|
|
|
'like' => 'keywordLike',
|
|
|
|
'matches' => 'keywordLike',
|
|
|
|
'contains' => 'keywordContains',
|
|
|
|
'rlike' => 'keywordRegex',
|
|
|
|
'irlike' => 'keywordRegexInsensitive',
|
2017-09-19 23:54:03 +00:00
|
|
|
'regex' => 'keywordRegex',
|
2017-06-15 14:23:34 +00:00
|
|
|
];
|
2016-08-24 04:52:58 +00:00
|
|
|
|
2019-08-02 20:37:17 +00:00
|
|
|
/**
|
|
|
|
* @var array Cached results of functions
|
|
|
|
*/
|
|
|
|
protected $funcCache = [];
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-10-11 22:07:48 +00:00
|
|
|
/**
|
|
|
|
* @var Equivset
|
|
|
|
*/
|
|
|
|
protected static $equivset;
|
|
|
|
|
2013-01-07 00:02:41 +00:00
|
|
|
/**
|
|
|
|
* Create a new instance
|
|
|
|
*
|
2019-08-21 10:04:10 +00:00
|
|
|
* @param Language $contLang Content language, used for language-dependent function
|
|
|
|
* @param BagOStuff $cache Used to cache the AST (in CachingParser) and the tokens
|
|
|
|
* @param LoggerInterface $logger Used for debugging
|
2018-05-25 23:31:49 +00:00
|
|
|
* @param AbuseFilterVariableHolder|null $vars
|
2013-01-07 00:02:41 +00:00
|
|
|
*/
|
2019-08-21 10:04:10 +00:00
|
|
|
public function __construct(
|
|
|
|
Language $contLang,
|
|
|
|
BagOStuff $cache,
|
|
|
|
LoggerInterface $logger,
|
|
|
|
AbuseFilterVariableHolder $vars = null
|
|
|
|
) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->resetState();
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->contLang = $contLang;
|
|
|
|
$this->cache = $cache;
|
|
|
|
$this->logger = $logger;
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd = new NullStatsdDataFactory;
|
Add a new class for methods related to running filters
Currently we strongly abuse (pardon the pun) the AbuseFilter class: its
purpose should be to hold static functions intended as generic utility
functions (e.g. to format messages, determine whether a filter is global
etc.), but we actually use it for all methods related to running filters.
This patch creates a new class, AbuseFilterRunner, containing all such
methods, which have been made non-static. This leads to several
improvements (also for related methods and the parser), and opens the
way to further improve the code.
Aside from making the code prettier, less global and easier to test,
this patch could also produce a performance improvement, although I
don't have tools to measure that.
Also note that many public methods have been removed, and almost any of
them has been made protected; a couple of them (the ones used from outside)
are left for back-compat, and will be removed in the future.
Change-Id: I2eab2e50356eeb5224446ee2d0df9c787ae95b80
2018-12-07 17:46:02 +00:00
|
|
|
if ( $vars ) {
|
2018-12-27 17:06:56 +00:00
|
|
|
$this->mVariables = $vars;
|
2013-01-07 00:02:41 +00:00
|
|
|
}
|
2019-08-11 13:11:20 +00:00
|
|
|
$this->mVariables->setLogger( $logger );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-10 16:34:42 +00:00
|
|
|
/**
|
|
|
|
* @param string $filter
|
|
|
|
*/
|
|
|
|
public function setFilter( $filter ) {
|
|
|
|
$this->mFilter = $filter;
|
|
|
|
}
|
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
/**
|
|
|
|
* @param BagOStuff $cache
|
|
|
|
*/
|
|
|
|
public function setCache( BagOStuff $cache ) {
|
|
|
|
$this->cache = $cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param LoggerInterface $logger
|
|
|
|
*/
|
|
|
|
public function setLogger( LoggerInterface $logger ) {
|
|
|
|
$this->logger = $logger;
|
|
|
|
}
|
|
|
|
|
2019-09-16 16:53:36 +00:00
|
|
|
/**
|
|
|
|
* @param IBufferingStatsdDataFactory $statsd
|
|
|
|
*/
|
|
|
|
public function setStatsd( IBufferingStatsdDataFactory $statsd ) {
|
|
|
|
$this->statsd = $statsd;
|
|
|
|
}
|
|
|
|
|
2019-01-24 10:33:01 +00:00
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
public function getCondCount() {
|
|
|
|
return $this->mCondCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reset the conditions counter
|
|
|
|
*/
|
|
|
|
public function resetCondCount() {
|
|
|
|
$this->mCondCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For use in batch scripts and the like
|
|
|
|
*
|
|
|
|
* @param bool $enable True to enable the limit, false to disable it
|
Add a new class for methods related to running filters
Currently we strongly abuse (pardon the pun) the AbuseFilter class: its
purpose should be to hold static functions intended as generic utility
functions (e.g. to format messages, determine whether a filter is global
etc.), but we actually use it for all methods related to running filters.
This patch creates a new class, AbuseFilterRunner, containing all such
methods, which have been made non-static. This leads to several
improvements (also for related methods and the parser), and opens the
way to further improve the code.
Aside from making the code prettier, less global and easier to test,
this patch could also produce a performance improvement, although I
don't have tools to measure that.
Also note that many public methods have been removed, and almost any of
them has been made protected; a couple of them (the ones used from outside)
are left for back-compat, and will be removed in the future.
Change-Id: I2eab2e50356eeb5224446ee2d0df9c787ae95b80
2018-12-07 17:46:02 +00:00
|
|
|
* @codeCoverageIgnore
|
2019-01-24 10:33:01 +00:00
|
|
|
*/
|
|
|
|
public function toggleConditionLimit( $enable ) {
|
|
|
|
$this->condLimitEnabled = $enable;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $val The amount to increase the conditions count of.
|
|
|
|
* @throws MWException
|
|
|
|
*/
|
|
|
|
protected function raiseCondCount( $val = 1 ) {
|
|
|
|
global $wgAbuseFilterConditionLimit;
|
|
|
|
|
|
|
|
$this->mCondCount += $val;
|
|
|
|
|
|
|
|
if ( $this->condLimitEnabled && $this->mCondCount > $wgAbuseFilterConditionLimit ) {
|
|
|
|
throw new MWException( 'Condition limit reached.' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
/**
|
|
|
|
* Resets the state of the parser.
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
public function resetState() {
|
2017-06-15 14:23:34 +00:00
|
|
|
$this->mTokens = [];
|
2018-12-27 17:06:56 +00:00
|
|
|
$this->mVariables = new AbuseFilterVariableHolder;
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->mPos = 0;
|
2009-03-25 11:48:33 +00:00
|
|
|
$this->mShortCircuit = false;
|
|
|
|
$this->mAllowShort = true;
|
2019-01-24 10:33:01 +00:00
|
|
|
$this->mCondCount = 0;
|
2019-08-10 16:34:42 +00:00
|
|
|
$this->mFilter = null;
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-02 20:37:17 +00:00
|
|
|
/**
|
|
|
|
* Clears the array of cached function results
|
|
|
|
*/
|
|
|
|
public function clearFuncCache() {
|
|
|
|
$this->funcCache = [];
|
|
|
|
}
|
|
|
|
|
2018-12-08 18:20:04 +00:00
|
|
|
/**
|
|
|
|
* @param AbuseFilterVariableHolder $vars
|
|
|
|
*/
|
|
|
|
public function setVariables( AbuseFilterVariableHolder $vars ) {
|
|
|
|
$this->mVariables = $vars;
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $filter
|
2019-09-10 17:14:49 +00:00
|
|
|
* @return true When successful
|
|
|
|
* @throws AFPUserVisibleException
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
public function checkSyntax( $filter ) {
|
2019-08-11 13:11:20 +00:00
|
|
|
$this->allowMissingVariables = true;
|
2018-04-29 17:52:45 +00:00
|
|
|
$origAS = $this->mAllowShort;
|
2008-08-31 05:56:49 +00:00
|
|
|
try {
|
2009-03-25 11:48:33 +00:00
|
|
|
$this->mAllowShort = false;
|
2018-10-03 14:38:41 +00:00
|
|
|
$this->intEval( $filter );
|
2019-08-19 16:28:57 +00:00
|
|
|
} finally {
|
|
|
|
$this->mAllowShort = $origAS;
|
2019-08-11 13:11:20 +00:00
|
|
|
$this->allowMissingVariables = false;
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2008-08-31 05:56:49 +00:00
|
|
|
return true;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-04-29 17:52:45 +00:00
|
|
|
* Move to the next token
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2015-02-04 18:25:21 +00:00
|
|
|
protected function move() {
|
2015-08-25 19:57:23 +00:00
|
|
|
list( $this->mCur, $this->mPos ) = $this->mTokens[$this->mPos];
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-04-05 11:47:42 +00:00
|
|
|
|
2018-09-22 08:48:16 +00:00
|
|
|
/**
|
|
|
|
* Get the next token. This is similar to move() but doesn't change class members,
|
|
|
|
* allowing to look ahead without rolling back the state.
|
|
|
|
*
|
|
|
|
* @return AFPToken
|
|
|
|
*/
|
|
|
|
protected function getNextToken() {
|
|
|
|
return $this->mTokens[$this->mPos][0];
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
|
|
|
* getState() function allows parser state to be rollbacked to several tokens back
|
|
|
|
* @return AFPParserState
|
|
|
|
*/
|
2009-04-05 11:47:42 +00:00
|
|
|
protected function getState() {
|
|
|
|
return new AFPParserState( $this->mCur, $this->mPos );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
|
|
|
* setState() function allows parser state to be rollbacked to several tokens back
|
|
|
|
* @param AFPParserState $state
|
|
|
|
*/
|
2009-04-05 11:47:42 +00:00
|
|
|
protected function setState( AFPParserState $state ) {
|
|
|
|
$this->mCur = $state->token;
|
|
|
|
$this->mPos = $state->pos;
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2009-03-25 11:48:33 +00:00
|
|
|
protected function skipOverBraces() {
|
|
|
|
$braces = 1;
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type !== AFPToken::TNONE && $braces > 0 ) {
|
2009-03-25 11:48:33 +00:00
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TBRACE ) {
|
|
|
|
if ( $this->mCur->value === '(' ) {
|
2009-03-25 11:48:33 +00:00
|
|
|
$braces++;
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( $this->mCur->value === ')' ) {
|
2009-03-25 11:48:33 +00:00
|
|
|
$braces--;
|
|
|
|
}
|
2019-08-02 11:49:34 +00:00
|
|
|
} elseif ( $this->mCur->type === AFPToken::TID ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
// T214674, define non-existing variables. @see docs of
|
|
|
|
// AbuseFilterCachingParser::discardWithHoisting for a detailed explanation of this branch
|
2019-08-02 11:49:34 +00:00
|
|
|
$next = $this->getNextToken();
|
2019-08-06 12:14:55 +00:00
|
|
|
if (
|
|
|
|
in_array( $this->mCur->value, [ 'set', 'set_var' ] ) &&
|
|
|
|
$next->type === AFPToken::TBRACE && $next->value === '('
|
|
|
|
) {
|
|
|
|
// This is for setter functions.
|
|
|
|
$this->move();
|
|
|
|
$braces++;
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type === AFPToken::TSTRING ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
if ( !$this->mVariables->varIsSet( $next->value ) ) {
|
|
|
|
$this->setUserVariable( $next->value, new AFPData( AFPData::DUNDEFINED ) );
|
|
|
|
}
|
2019-08-06 12:14:55 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Simple assignment with :=
|
|
|
|
$varname = $this->mCur->value;
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type === AFPToken::TOP && $next->value === ':=' ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
if ( !$this->mVariables->varIsSet( $varname ) ) {
|
|
|
|
$this->setUserVariable( $varname, new AFPData( AFPData::DUNDEFINED ) );
|
|
|
|
}
|
2019-08-06 12:14:55 +00:00
|
|
|
} elseif ( $next->type === AFPToken::TSQUAREBRACKET && $next->value === '[' ) {
|
|
|
|
if ( !$this->mVariables->varIsSet( $varname ) ) {
|
|
|
|
throw new AFPUserVisibleException( 'unrecognisedvar',
|
|
|
|
$next->pos,
|
|
|
|
[ $varname ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->setUserVariable( $varname, new AFPData( AFPData::DUNDEFINED ) );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
|
|
|
}
|
2009-03-25 11:48:33 +00:00
|
|
|
}
|
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === ')' ) ) {
|
2017-06-15 14:23:34 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos, [ ')' ] );
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2009-03-25 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $code
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return bool
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
public function parse( $code ) {
|
|
|
|
return $this->intEval( $code )->toBool();
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $filter
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return string
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
public function evaluateExpression( $filter ) {
|
|
|
|
return $this->intEval( $filter )->toString();
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $code
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2017-11-07 18:44:10 +00:00
|
|
|
public function intEval( $code ) {
|
2019-09-16 16:53:36 +00:00
|
|
|
$startTime = microtime( true );
|
2018-07-17 15:17:44 +00:00
|
|
|
// Reset all class members to their default value
|
2019-09-02 08:25:56 +00:00
|
|
|
$tokenizer = new AbuseFilterTokenizer( $this->cache, $this->logger );
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->mTokens = $tokenizer->getTokens( $code );
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->mPos = 0;
|
2009-03-25 11:48:33 +00:00
|
|
|
$this->mShortCircuit = false;
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-03 15:52:14 +00:00
|
|
|
$result = new AFPData( AFPData::DEMPTY );
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelEntry( $result );
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2019-08-12 09:18:15 +00:00
|
|
|
if ( $result->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
$result = new AFPData( AFPData::DBOOL, false );
|
|
|
|
}
|
|
|
|
|
2019-09-16 16:53:36 +00:00
|
|
|
$this->statsd->timing( 'abusefilter_oldparser_full', microtime( true ) - $startTime );
|
2008-08-31 05:56:49 +00:00
|
|
|
return $result;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2008-08-31 05:56:49 +00:00
|
|
|
/* Levels */
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2011-08-24 22:11:52 +00:00
|
|
|
/**
|
|
|
|
* Handles unexpected characters after the expression
|
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
2011-08-24 22:11:52 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelEntry( &$result ) {
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->doLevelSemicolon( $result );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TNONE ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedatend',
|
2017-06-15 14:23:34 +00:00
|
|
|
$this->mCur->pos, [ $this->mCur->type ]
|
2015-09-28 18:03:35 +00:00
|
|
|
);
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2010-02-13 14:10:36 +00:00
|
|
|
|
2011-08-24 22:11:52 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles multiple expressions delimited by a semicolon
|
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param AFPData &$result
|
2011-08-24 22:11:52 +00:00
|
|
|
*/
|
2009-04-05 11:47:42 +00:00
|
|
|
protected function doLevelSemicolon( &$result ) {
|
2009-04-01 06:53:18 +00:00
|
|
|
do {
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TSTATEMENTSEPARATOR ) {
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->doLevelSet( $result );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
} while ( $this->mCur->type === AFPToken::TSTATEMENTSEPARATOR );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
|
2011-08-24 22:11:52 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles assignments (:=)
|
2011-08-24 22:11:52 +00:00
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
2011-08-24 22:11:52 +00:00
|
|
|
*/
|
2009-04-05 11:47:42 +00:00
|
|
|
protected function doLevelSet( &$result ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID ) {
|
2009-04-05 11:47:42 +00:00
|
|
|
$varname = $this->mCur->value;
|
|
|
|
$prev = $this->getState();
|
|
|
|
$this->move();
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->move();
|
2019-08-20 17:36:59 +00:00
|
|
|
$checkEmpty = $result->getType() === AFPData::DEMPTY;
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->doLevelSet( $result );
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $checkEmpty && $result->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'var assignment' );
|
2019-08-20 17:36:59 +00:00
|
|
|
}
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->setUserVariable( $varname, $result );
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2009-04-05 11:47:42 +00:00
|
|
|
return;
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2019-08-03 13:21:53 +00:00
|
|
|
// We allow builtin variables to both check for override (e.g. added_lines[] :='x')
|
|
|
|
// and for T198531
|
2019-08-11 13:11:20 +00:00
|
|
|
$array = $this->getVarValue( $varname );
|
2019-08-03 15:52:14 +00:00
|
|
|
if ( $array->getType() !== AFPData::DARRAY && $array->getType() !== AFPData::DUNDEFINED ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2019-08-02 11:49:34 +00:00
|
|
|
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
$idx = 'new';
|
|
|
|
} else {
|
2009-05-22 06:42:10 +00:00
|
|
|
$this->setState( $prev );
|
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$idx = new AFPData( AFPData::DEMPTY );
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->doLevelSemicolon( $idx );
|
|
|
|
$idx = $idx->toInt();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2010-02-13 14:10:36 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2019-08-02 11:49:34 +00:00
|
|
|
if ( $array->getType() === AFPData::DARRAY ) {
|
|
|
|
if ( count( $array->toArray() ) <= $idx ) {
|
|
|
|
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
|
2019-08-24 09:48:20 +00:00
|
|
|
[ $idx, count( $array->getData() ) ] );
|
2019-11-04 17:45:58 +00:00
|
|
|
} elseif ( $idx < 0 ) {
|
|
|
|
throw new AFPUserVisibleException( 'negativeindex', $this->mCur->pos, [ $idx ] );
|
2019-08-02 11:49:34 +00:00
|
|
|
}
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':=' ) {
|
2019-11-02 13:00:51 +00:00
|
|
|
if ( $this->isReservedIdentifier( $varname ) ) {
|
2019-08-03 13:21:53 +00:00
|
|
|
// Ideally we should've aborted before trying to parse the index
|
|
|
|
throw new AFPUserVisibleException( 'overridebuiltin', $this->mCur->pos, [ $varname ] );
|
|
|
|
}
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->move();
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TNONE ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'array assignment' );
|
2019-08-20 17:36:59 +00:00
|
|
|
}
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->doLevelSet( $result );
|
2019-08-02 11:49:34 +00:00
|
|
|
if ( $array->getType() === AFPData::DARRAY ) {
|
2019-08-03 15:52:14 +00:00
|
|
|
// If it's a DUNDEFINED, leave it as is
|
2019-08-02 11:49:34 +00:00
|
|
|
$array = $array->toArray();
|
|
|
|
if ( $idx === 'new' ) {
|
|
|
|
$array[] = $result;
|
|
|
|
} else {
|
|
|
|
$array[$idx] = $result;
|
|
|
|
}
|
|
|
|
$this->setUserVariable( $varname, new AFPData( AFPData::DARRAY, $array ) );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2009-04-05 17:11:17 +00:00
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
$this->setState( $prev );
|
|
|
|
}
|
2009-04-05 11:47:42 +00:00
|
|
|
} else {
|
|
|
|
$this->setState( $prev );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$this->doLevelConditions( $result );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles conditionals: if-then-else and ternary operator
|
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2008-08-31 15:12:55 +00:00
|
|
|
protected function doLevelConditions( &$result ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'if' ) {
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->move();
|
|
|
|
$this->doLevelBoolOps( $result );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'then' ) ) {
|
2009-03-25 11:36:38 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
'then',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->move();
|
2009-03-18 23:28:35 +00:00
|
|
|
|
2019-08-03 15:52:14 +00:00
|
|
|
$r1 = new AFPData( AFPData::DEMPTY );
|
2019-08-20 17:36:59 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-09-04 09:57:51 +00:00
|
|
|
$isTrue = $result->getType() === AFPData::DUNDEFINED ? false : $result->toBool();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( !$isTrue ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$scOrig = wfSetVar( $this->mShortCircuit, $this->mAllowShort, true );
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelConditions( $r1 );
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( !$isTrue ) {
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->mShortCircuit = $scOrig;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-20 16:19:31 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'else' ) {
|
|
|
|
$this->move();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-20 16:19:31 +00:00
|
|
|
if ( $isTrue ) {
|
|
|
|
$scOrig = wfSetVar( $this->mShortCircuit, $this->mAllowShort, true );
|
|
|
|
}
|
|
|
|
$this->doLevelConditions( $r2 );
|
|
|
|
if ( $isTrue ) {
|
|
|
|
$this->mShortCircuit = $scOrig;
|
|
|
|
}
|
2019-08-20 17:36:59 +00:00
|
|
|
} else {
|
|
|
|
// DNULL is assumed as default in case of a missing else
|
|
|
|
$r2 = new AFPData( AFPData::DNULL );
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TKEYWORD && $this->mCur->value === 'end' ) ) {
|
2009-03-25 11:36:38 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
'end',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->move();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-09-04 09:57:51 +00:00
|
|
|
$isTrue = $result->getType() === AFPData::DUNDEFINED ? false : $result->toBool();
|
|
|
|
if ( $isTrue ) {
|
2008-08-31 15:12:55 +00:00
|
|
|
$result = $r1;
|
|
|
|
} else {
|
|
|
|
$result = $r2;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$this->doLevelBoolOps( $result );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '?' ) {
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r1 = new AFPData( AFPData::DEMPTY );
|
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-09-04 09:57:51 +00:00
|
|
|
$isTrue = $result->getType() === AFPData::DUNDEFINED ? false : $result->toBool();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( !$isTrue ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$scOrig = wfSetVar( $this->mShortCircuit, $this->mAllowShort, true );
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelConditions( $r1 );
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( !$isTrue ) {
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->mShortCircuit = $scOrig;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TOP && $this->mCur->value === ':' ) ) {
|
2009-03-25 11:36:38 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
':',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->move();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( $isTrue ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$scOrig = wfSetVar( $this->mShortCircuit, $this->mAllowShort, true );
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelConditions( $r2 );
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'ternary else' );
|
2019-08-20 17:36:59 +00:00
|
|
|
}
|
2013-03-23 05:39:27 +00:00
|
|
|
if ( $isTrue ) {
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->mShortCircuit = $scOrig;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2010-02-13 14:10:36 +00:00
|
|
|
if ( $isTrue ) {
|
2008-08-31 15:12:55 +00:00
|
|
|
$result = $r1;
|
|
|
|
} else {
|
|
|
|
$result = $r2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles boolean operators (&, |, ^)
|
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelBoolOps( &$result ) {
|
|
|
|
$this->doLevelCompares( $result );
|
2017-06-15 14:23:34 +00:00
|
|
|
$ops = [ '&', '|', '^' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2019-08-12 09:18:15 +00:00
|
|
|
$curVal = $result->getType() === AFPData::DUNDEFINED ? false : $result->toBool();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-10-03 12:02:00 +00:00
|
|
|
// We can go on quickly as either one statement with | is true or one with & is false
|
2019-08-12 09:18:15 +00:00
|
|
|
if ( ( $op === '&' && !$curVal ) || ( $op === '|' && $curVal ) ) {
|
2019-08-19 16:28:57 +00:00
|
|
|
$scOrig = wfSetVar( $this->mShortCircuit, $this->mAllowShort, true );
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->doLevelCompares( $r2 );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'bool operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2019-08-19 16:28:57 +00:00
|
|
|
$this->mShortCircuit = $scOrig;
|
2019-08-12 09:18:15 +00:00
|
|
|
$result = new AFPData( AFPData::DBOOL, $curVal );
|
2009-03-19 00:18:03 +00:00
|
|
|
continue;
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelCompares( $r2 );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'bool operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2019-08-12 12:40:51 +00:00
|
|
|
$result = $result->boolOp( $r2, $op );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles comparison operators
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelCompares( &$result ) {
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelSumRels( $result );
|
2019-03-21 15:48:35 +00:00
|
|
|
$equalityOps = [ '==', '===', '!=', '!==', '=' ];
|
|
|
|
$orderOps = [ '<', '>', '<=', '>=' ];
|
|
|
|
// Only allow either a single operation, or a combination of a single equalityOps and a single
|
|
|
|
// orderOps. This resembles what PHP does, and allows `a < b == c` while rejecting `a < b < c`
|
|
|
|
$allowedOps = array_merge( $equalityOps, $orderOps );
|
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $allowedOps ) ) {
|
|
|
|
$allowedOps = in_array( $this->mCur->value, $equalityOps ) ?
|
|
|
|
array_diff( $allowedOps, $equalityOps ) :
|
|
|
|
array_diff( $allowedOps, $orderOps );
|
2008-08-31 05:56:49 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelSumRels( $r2 );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'compare operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
2019-03-21 15:48:35 +00:00
|
|
|
continue;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2019-01-24 10:33:01 +00:00
|
|
|
$this->raiseCondCount();
|
2019-08-12 12:40:51 +00:00
|
|
|
$result = $result->compareOp( $r2, $op );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles sum-related operations (+ and -)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 15:12:55 +00:00
|
|
|
protected function doLevelSumRels( &$result ) {
|
|
|
|
$this->doLevelMulRels( $result );
|
2017-06-15 14:23:34 +00:00
|
|
|
$ops = [ '+', '-' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2008-08-31 15:12:55 +00:00
|
|
|
$this->doLevelMulRels( $r2 );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'sum operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
2019-01-24 22:28:49 +00:00
|
|
|
continue;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $op === '+' ) {
|
2019-08-12 11:53:38 +00:00
|
|
|
$result = $result->sum( $r2 );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $op === '-' ) {
|
2019-08-12 11:53:38 +00:00
|
|
|
$result = $result->sub( $r2 );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles multiplication-related operations (*, / and %)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 15:12:55 +00:00
|
|
|
protected function doLevelMulRels( &$result ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelPow( $result );
|
2017-06-15 14:23:34 +00:00
|
|
|
$ops = [ '*', '/', '%' ];
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && in_array( $this->mCur->value, $ops ) ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$op = $this->mCur->value;
|
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelPow( $r2 );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'multiplication operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
2019-01-24 22:28:49 +00:00
|
|
|
continue;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2019-08-12 12:40:51 +00:00
|
|
|
$result = $result->mulRel( $r2, $op, $this->mCur->pos );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2008-08-31 15:12:55 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles powers (**)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelPow( &$result ) {
|
|
|
|
$this->doLevelBoolInvert( $result );
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '**' ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$expanent = new AFPData( AFPData::DEMPTY );
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelBoolInvert( $expanent );
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $expanent->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'power operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
2019-01-24 22:28:49 +00:00
|
|
|
continue;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2019-08-12 11:53:38 +00:00
|
|
|
$result = $result->pow( $expanent );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles boolean inversion (!)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelBoolInvert( &$result ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && $this->mCur->value === '!' ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->move();
|
2019-08-20 17:36:59 +00:00
|
|
|
$checkEmpty = $result->getType() === AFPData::DEMPTY;
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelSpecialWords( $result );
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $checkEmpty && $result->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'bool inversion' );
|
2019-08-20 17:36:59 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
|
|
|
return;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2019-08-12 11:53:38 +00:00
|
|
|
$result = $result->boolInvert();
|
2008-08-31 05:56:49 +00:00
|
|
|
} else {
|
|
|
|
$this->doLevelSpecialWords( $result );
|
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles keywords (in, like, rlike, contains, ...)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelSpecialWords( &$result ) {
|
|
|
|
$this->doLevelUnarys( $result );
|
2009-10-07 13:57:06 +00:00
|
|
|
$keyword = strtolower( $this->mCur->value );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TKEYWORD
|
2019-11-16 15:32:36 +00:00
|
|
|
&& isset( self::KEYWORDS[$keyword] )
|
2016-08-24 04:52:58 +00:00
|
|
|
) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->move();
|
2019-08-03 15:52:14 +00:00
|
|
|
$r2 = new AFPData( AFPData::DEMPTY );
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->doLevelUnarys( $r2 );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-03 15:35:00 +00:00
|
|
|
if ( $r2->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'keyword operand' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
|
|
|
|
2009-10-07 13:57:06 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
|
|
|
return;
|
2009-03-18 23:28:35 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-11-08 14:02:17 +00:00
|
|
|
$result = $this->callKeyword( $keyword, $result, $r2 );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles unary plus and minus, like in -5 or -(2 * +2)
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelUnarys( &$result ) {
|
|
|
|
$op = $this->mCur->value;
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TOP && ( $op === "+" || $op === "-" ) ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->move();
|
2019-08-20 17:36:59 +00:00
|
|
|
$checkEmpty = $result->getType() === AFPData::DEMPTY;
|
2018-04-16 15:37:10 +00:00
|
|
|
$this->doLevelArrayElements( $result );
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $checkEmpty && $result->getType() === AFPData::DEMPTY ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'unary operand' );
|
2019-08-20 17:36:59 +00:00
|
|
|
}
|
2016-04-09 13:53:16 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
|
|
|
return;
|
2016-04-09 13:53:16 +00:00
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $op === '-' ) {
|
2019-08-12 11:53:38 +00:00
|
|
|
$result = $result->unaryMinus();
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
2018-04-16 15:37:10 +00:00
|
|
|
$this->doLevelArrayElements( $result );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-04-05 17:11:17 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles array elements, parsing expressions like array[number]
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2018-04-16 15:37:10 +00:00
|
|
|
protected function doLevelArrayElements( &$result ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->doLevelBraces( $result );
|
2018-08-26 08:34:42 +00:00
|
|
|
while ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === '[' ) {
|
2019-08-03 15:52:14 +00:00
|
|
|
$idx = new AFPData( AFPData::DEMPTY );
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->doLevelSemicolon( $idx );
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( !( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound', $this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[ ']', $this->mCur->type, $this->mCur->value ] );
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
|
|
|
$idx = $idx->toInt();
|
2019-01-24 10:10:22 +00:00
|
|
|
if ( $result->getType() === AFPData::DARRAY ) {
|
|
|
|
if ( count( $result->getData() ) <= $idx ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
throw new AFPUserVisibleException( 'outofbounds', $this->mCur->pos,
|
2019-01-24 10:10:22 +00:00
|
|
|
[ $idx, count( $result->getData() ) ] );
|
2019-11-04 17:45:58 +00:00
|
|
|
} elseif ( $idx < 0 ) {
|
|
|
|
throw new AFPUserVisibleException( 'negativeindex', $this->mCur->pos, [ $idx ] );
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
2019-10-09 10:37:38 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeArraySuspiciousNullable Guaranteed to be array
|
2019-01-24 10:10:22 +00:00
|
|
|
$result = $result->getData()[$idx];
|
2019-08-03 15:52:14 +00:00
|
|
|
} elseif ( $result->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
2009-04-05 17:11:17 +00:00
|
|
|
} else {
|
2018-04-16 15:37:10 +00:00
|
|
|
throw new AFPUserVisibleException( 'notarray', $this->mCur->pos, [] );
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles brackets, only ( and )
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelBraces( &$result ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === '(' ) {
|
2019-08-20 17:36:59 +00:00
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type === AFPToken::TBRACE && $next->value === ')' ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'parenthesized expression' );
|
2019-08-20 17:36:59 +00:00
|
|
|
// We don't need DUNDEFINED here
|
|
|
|
$this->move();
|
|
|
|
$this->move();
|
2009-03-25 11:48:33 +00:00
|
|
|
} else {
|
2019-08-20 17:36:59 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
|
|
|
$this->skipOverBraces();
|
|
|
|
} else {
|
|
|
|
$this->doLevelSemicolon( $result );
|
|
|
|
}
|
|
|
|
if ( !( $this->mCur->type === AFPToken::TBRACE && $this->mCur->value === ')' ) ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'expectednotfound',
|
|
|
|
$this->mCur->pos,
|
|
|
|
[ ')', $this->mCur->type, $this->mCur->value ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->move();
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
} else {
|
|
|
|
$this->doLevelFunction( $result );
|
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles functions
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelFunction( &$result ) {
|
2019-11-16 15:32:36 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TID && isset( self::FUNCTIONS[$this->mCur->value] ) ) {
|
2019-08-06 18:59:45 +00:00
|
|
|
$fname = $this->mCur->value;
|
2008-08-31 05:56:49 +00:00
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== '(' ) {
|
2009-03-25 11:36:38 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
'(',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2009-03-25 11:48:33 +00:00
|
|
|
|
2009-10-07 13:57:06 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2019-08-04 22:01:41 +00:00
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
2009-03-25 11:48:33 +00:00
|
|
|
$this->skipOverBraces();
|
|
|
|
$this->move();
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
// The result doesn't matter.
|
|
|
|
return;
|
2009-03-25 11:48:33 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-06-15 14:23:34 +00:00
|
|
|
$args = [];
|
2019-08-03 15:35:00 +00:00
|
|
|
$next = $this->getNextToken();
|
|
|
|
if ( $next->type !== AFPToken::TBRACE || $next->value !== ')' ) {
|
2019-08-06 18:59:45 +00:00
|
|
|
if ( ( $fname === 'set' || $fname === 'set_var' ) ) {
|
|
|
|
$state = $this->getState();
|
|
|
|
$this->move();
|
|
|
|
$next = $this->getNextToken();
|
|
|
|
if (
|
|
|
|
$this->mCur->type !== AFPToken::TSTRING ||
|
|
|
|
(
|
|
|
|
$next->type !== AFPToken::TCOMMA &&
|
|
|
|
// Let this fail later, when checking parameters count
|
|
|
|
!( $next->type === AFPToken::TBRACE && $next->value === ')' )
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw new AFPUserVisibleException( 'variablevariable', $this->mCur->pos, [] );
|
|
|
|
} else {
|
|
|
|
$this->setState( $state );
|
|
|
|
}
|
|
|
|
}
|
2019-08-03 15:35:00 +00:00
|
|
|
do {
|
2019-08-03 15:52:14 +00:00
|
|
|
$r = new AFPData( AFPData::DEMPTY );
|
2018-06-27 11:22:52 +00:00
|
|
|
$this->doLevelSemicolon( $r );
|
2019-09-06 16:28:53 +00:00
|
|
|
if ( $r->getType() === AFPData::DEMPTY && !$this->functionIsVariadic( $fname ) ) {
|
2019-09-16 11:13:44 +00:00
|
|
|
$this->logEmptyOperand( 'non-variadic function argument' );
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2018-06-27 11:22:52 +00:00
|
|
|
$args[] = $r;
|
2019-08-03 15:35:00 +00:00
|
|
|
} while ( $this->mCur->type === AFPToken::TCOMMA );
|
|
|
|
} else {
|
|
|
|
$this->move();
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TBRACE || $this->mCur->value !== ')' ) {
|
2009-03-25 11:36:38 +00:00
|
|
|
throw new AFPUserVisibleException( 'expectednotfound',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
')',
|
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-03-18 23:28:35 +00:00
|
|
|
$this->move();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$result = $this->callFunc( $fname, $args );
|
2008-08-31 05:56:49 +00:00
|
|
|
} else {
|
|
|
|
$this->doLevelAtom( $result );
|
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2018-06-30 18:35:49 +00:00
|
|
|
* Handles the return value
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData &$result
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function doLevelAtom( &$result ) {
|
|
|
|
$tok = $this->mCur->value;
|
2015-09-28 18:03:35 +00:00
|
|
|
switch ( $this->mCur->type ) {
|
2008-08-31 05:56:49 +00:00
|
|
|
case AFPToken::TID:
|
2010-08-19 21:12:09 +00:00
|
|
|
if ( $this->mShortCircuit ) {
|
2019-08-03 15:52:14 +00:00
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
|
|
|
} else {
|
|
|
|
$var = strtolower( $tok );
|
|
|
|
$result = $this->getVarValue( $var );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
break;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TSTRING:
|
|
|
|
$result = new AFPData( AFPData::DSTRING, $tok );
|
2008-08-31 05:56:49 +00:00
|
|
|
break;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TFLOAT:
|
|
|
|
$result = new AFPData( AFPData::DFLOAT, $tok );
|
2008-08-31 05:56:49 +00:00
|
|
|
break;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TINT:
|
|
|
|
$result = new AFPData( AFPData::DINT, $tok );
|
2008-08-31 05:56:49 +00:00
|
|
|
break;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TKEYWORD:
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $tok === "true" ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
$result = new AFPData( AFPData::DBOOL, true );
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( $tok === "false" ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
$result = new AFPData( AFPData::DBOOL, false );
|
2018-08-26 08:34:42 +00:00
|
|
|
} elseif ( $tok === "null" ) {
|
2019-05-23 10:55:20 +00:00
|
|
|
$result = new AFPData( AFPData::DNULL );
|
2010-08-19 21:12:09 +00:00
|
|
|
} else {
|
2009-10-07 13:57:06 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unrecognisedkeyword',
|
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[ $tok ]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
break;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TNONE:
|
2018-04-04 21:14:25 +00:00
|
|
|
// Handled at entry level
|
|
|
|
return;
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TBRACE:
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->value === ')' ) {
|
2018-04-04 21:14:25 +00:00
|
|
|
// Handled at the entry level
|
|
|
|
return;
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2015-09-28 18:03:35 +00:00
|
|
|
case AFPToken::TSQUAREBRACKET:
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->value === '[' ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
$array = [];
|
2016-04-09 13:35:35 +00:00
|
|
|
while ( true ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->move();
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
break;
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2019-08-03 15:52:14 +00:00
|
|
|
$item = new AFPData( AFPData::DEMPTY );
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->doLevelSet( $item );
|
2018-04-16 15:37:10 +00:00
|
|
|
$array[] = $item;
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type === AFPToken::TSQUAREBRACKET && $this->mCur->value === ']' ) {
|
2009-04-05 17:11:17 +00:00
|
|
|
break;
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $this->mCur->type !== AFPToken::TCOMMA ) {
|
2010-08-19 21:12:09 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'expectednotfound',
|
2009-04-05 17:11:17 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[ ', or ]', $this->mCur->type, $this->mCur->value ]
|
2010-08-19 21:12:09 +00:00
|
|
|
);
|
|
|
|
}
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
2018-04-16 15:37:10 +00:00
|
|
|
$result = new AFPData( AFPData::DARRAY, $array );
|
2009-04-05 17:11:17 +00:00
|
|
|
break;
|
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
default:
|
2009-10-07 13:57:06 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'unexpectedtoken',
|
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->type,
|
|
|
|
$this->mCur->value
|
2017-06-15 14:23:34 +00:00
|
|
|
]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
$this->move();
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2009-04-05 11:47:42 +00:00
|
|
|
/* End of levels */
|
|
|
|
|
2019-08-03 13:21:53 +00:00
|
|
|
/**
|
2019-08-11 13:11:20 +00:00
|
|
|
* Check whether a variable exists, being either built-in or user-defined. Doesn't include
|
|
|
|
* disabled variables.
|
2019-08-03 13:21:53 +00:00
|
|
|
*
|
|
|
|
* @param string $varname
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function varExists( $varname ) {
|
|
|
|
$builderValues = AbuseFilter::getBuilderValues();
|
|
|
|
|
|
|
|
return array_key_exists( $varname, $builderValues['vars'] ) ||
|
|
|
|
$this->mVariables->varIsSet( $varname );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $var
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2009-02-26 12:15:14 +00:00
|
|
|
protected function getVarValue( $var ) {
|
2010-02-13 14:10:36 +00:00
|
|
|
$var = strtolower( $var );
|
2018-02-18 13:44:17 +00:00
|
|
|
$deprecatedVars = AbuseFilter::getDeprecatedVariables();
|
2019-08-03 13:21:53 +00:00
|
|
|
|
2018-02-18 13:44:17 +00:00
|
|
|
if ( array_key_exists( $var, $deprecatedVars ) ) {
|
2019-08-21 10:04:10 +00:00
|
|
|
$this->logger->debug( "AbuseFilter: deprecated variable $var used." );
|
2019-08-11 13:11:20 +00:00
|
|
|
$var = $deprecatedVars[ $var ];
|
|
|
|
}
|
2019-11-16 15:32:36 +00:00
|
|
|
if ( array_key_exists( $var, AbuseFilter::DISABLED_VARS ) ) {
|
2019-08-11 13:11:20 +00:00
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'disabledvar',
|
|
|
|
$this->mCur->pos,
|
|
|
|
[ $var ]
|
|
|
|
);
|
2018-02-18 13:44:17 +00:00
|
|
|
}
|
2019-08-03 13:21:53 +00:00
|
|
|
if ( !$this->varExists( $var ) ) {
|
2009-10-07 13:57:06 +00:00
|
|
|
throw new AFPUserVisibleException(
|
2019-08-11 13:11:20 +00:00
|
|
|
'unrecognisedvar',
|
2009-10-07 13:57:06 +00:00
|
|
|
$this->mCur->pos,
|
2017-06-15 14:23:34 +00:00
|
|
|
[ $var ]
|
2009-10-07 13:57:06 +00:00
|
|
|
);
|
2009-02-26 12:15:14 +00:00
|
|
|
}
|
2019-08-11 13:11:20 +00:00
|
|
|
|
|
|
|
// It's a built-in, non-disabled variable (either set or unset), or a set custom variable
|
|
|
|
$flags = $this->allowMissingVariables
|
|
|
|
? AbuseFilterVariableHolder::GET_LAX
|
|
|
|
: AbuseFilterVariableHolder::GET_STRICT;
|
2019-09-12 11:47:51 +00:00
|
|
|
return $this->mVariables->getVar( $var, $flags, $this->mFilter );
|
2009-02-26 12:15:14 +00:00
|
|
|
}
|
2009-04-05 11:47:42 +00:00
|
|
|
|
2019-08-03 13:21:53 +00:00
|
|
|
/**
|
|
|
|
* Check whether the given name refers to a built-in variable, including
|
|
|
|
* deprecated and disabled variables.
|
|
|
|
*
|
|
|
|
* @param string $varname
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function isBuiltinVar( $varname ) {
|
|
|
|
$builderValues = AbuseFilter::getBuilderValues();
|
|
|
|
$deprecatedVars = AbuseFilter::getDeprecatedVariables();
|
|
|
|
|
|
|
|
return array_key_exists( $varname, $builderValues['vars'] ) ||
|
2019-11-16 15:32:36 +00:00
|
|
|
array_key_exists( $varname, AbuseFilter::DISABLED_VARS ) ||
|
2019-08-03 13:21:53 +00:00
|
|
|
array_key_exists( $varname, $deprecatedVars );
|
|
|
|
}
|
|
|
|
|
2019-11-02 13:00:51 +00:00
|
|
|
/**
|
|
|
|
* Check whether the given name is a reserved identifier, e.g. the name of a built-in variable,
|
|
|
|
* function, or keyword.
|
|
|
|
*
|
|
|
|
* @param string $name
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function isReservedIdentifier( $name ) {
|
|
|
|
return $this->isBuiltinVar( $name ) ||
|
|
|
|
array_key_exists( $name, self::FUNCTIONS ) ||
|
|
|
|
// We need to check for true, false, if/then/else etc. because, even if they have a different
|
|
|
|
// AFPToken type, they may be used inside set/set_var()
|
|
|
|
in_array( $name, AbuseFilterTokenizer::KEYWORDS, true );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $name
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param mixed $value
|
2012-03-11 20:40:04 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2009-04-05 11:47:42 +00:00
|
|
|
protected function setUserVariable( $name, $value ) {
|
2019-11-02 13:00:51 +00:00
|
|
|
if ( $this->isReservedIdentifier( $name ) ) {
|
2017-06-15 14:23:34 +00:00
|
|
|
throw new AFPUserVisibleException( 'overridebuiltin', $this->mCur->pos, [ $name ] );
|
2010-08-19 21:12:09 +00:00
|
|
|
}
|
2018-12-27 17:06:56 +00:00
|
|
|
$this->mVariables->setVar( $name, $value );
|
2009-04-05 11:47:42 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
/**
|
2019-08-21 09:01:50 +00:00
|
|
|
* Check that a built-in function has been provided the right amount of arguments
|
2018-10-03 15:19:40 +00:00
|
|
|
*
|
|
|
|
* @param array $args The arguments supplied to the function
|
|
|
|
* @param string $func The function name
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2019-08-21 09:01:50 +00:00
|
|
|
protected function checkArgCount( $args, $func ) {
|
2019-10-03 12:13:26 +00:00
|
|
|
if ( !array_key_exists( $func, self::FUNC_ARG_COUNT ) ) {
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
throw new InvalidArgumentException( "$func is not a valid function." );
|
|
|
|
}
|
2019-10-03 12:13:26 +00:00
|
|
|
list( $min, $max ) = self::FUNC_ARG_COUNT[ $func ];
|
2019-08-21 09:01:50 +00:00
|
|
|
if ( count( $args ) < $min ) {
|
2018-10-03 15:19:40 +00:00
|
|
|
throw new AFPUserVisibleException(
|
2019-08-21 09:01:50 +00:00
|
|
|
$min === 1 ? 'noparams' : 'notenoughargs',
|
2018-10-03 15:19:40 +00:00
|
|
|
$this->mCur->pos,
|
2019-08-21 09:01:50 +00:00
|
|
|
[ $func, $min, count( $args ) ]
|
2018-10-03 15:19:40 +00:00
|
|
|
);
|
2019-08-21 09:01:50 +00:00
|
|
|
} elseif ( count( $args ) > $max ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'toomanyargs',
|
|
|
|
$this->mCur->pos,
|
|
|
|
[ $func, $max, count( $args ) ]
|
|
|
|
);
|
2018-10-03 15:19:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
/**
|
|
|
|
* Helper to call a built-in function.
|
|
|
|
*
|
|
|
|
* @param string $fname The name of the function as found in the filter code
|
|
|
|
* @param AFPData[] $args Arguments for the function
|
|
|
|
* @return AFPData The return value of the function
|
|
|
|
* @throws InvalidArgumentException if given an invalid func
|
|
|
|
*/
|
|
|
|
protected function callFunc( $fname, array $args ) : AFPData {
|
2019-11-16 15:32:36 +00:00
|
|
|
if ( !array_key_exists( $fname, self::FUNCTIONS ) ) {
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
throw new InvalidArgumentException( "$fname is not a valid function." );
|
|
|
|
}
|
|
|
|
|
2019-11-16 15:32:36 +00:00
|
|
|
$funcHandler = self::FUNCTIONS[$fname];
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$funcHash = md5( $funcHandler . serialize( $args ) );
|
|
|
|
|
|
|
|
if ( isset( $this->funcCache[$funcHash] ) &&
|
2019-11-16 15:32:36 +00:00
|
|
|
!in_array( $funcHandler, self::ACTIVE_FUNCTIONS )
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
) {
|
|
|
|
$result = $this->funcCache[$funcHash];
|
|
|
|
} else {
|
2019-08-21 09:01:50 +00:00
|
|
|
$this->checkArgCount( $args, $fname );
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$this->raiseCondCount();
|
2019-10-01 16:27:56 +00:00
|
|
|
|
|
|
|
// Any undefined argument should be special-cased by the function, but that would be too
|
|
|
|
// much overhead. We also cannot skip calling the handler in case it's making further
|
|
|
|
// validation (T234339). So temporarily replace the DUNDEFINED with a DNULL.
|
|
|
|
// @todo This is subpar.
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$hasUndefinedArg = false;
|
2019-10-01 16:27:56 +00:00
|
|
|
foreach ( $args as $i => $arg ) {
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
if ( $arg->type === AFPData::DUNDEFINED ) {
|
2019-10-01 16:27:56 +00:00
|
|
|
$args[$i] = new AFPData( AFPData::DNULL );
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$hasUndefinedArg = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( $hasUndefinedArg ) {
|
2019-10-01 16:27:56 +00:00
|
|
|
$this->$funcHandler( $args );
|
Better handling of function params in CachingParser
This patch includes various fixes to how func arguments are handled in
CachingParser:
- Add a comment about a future improvement of checkSyntax, which we
could limit to try building the AST.
- Having enough args for each function is now also checked when
building the AST. This allows implementing the previous point without
stopping to report notenoughargs at syntaxcheck-time (otherwise it'd be
a runtime error). And it also ensure that we check for the params count
inside skipped branches, e.g. inside if/else: these were already only
discovered at runtime in CachingParser. The old parser is not affected
by this change, because when checking syntax it will always execute
all branches, and at runtime it will skip braces altogether.
- Fix arg count for CachingParser, which previously added a bogus param
in case of a function called without parameters. This was fixed for
the other parser in I484fe2994292970276150d2e417801453339e540, and I
just ported the updated fix. Also note that the CachingParser was
already failing for e.g. `count()`, but instead of complaining about
missing arguments, it failed hard when trying to pass NULL to
evalNode.
- Fixed some tests not to use setExpectedException, which caused the
previous point to remain unnoticed: calling that method prevents the
loop from continuing, and thus only the AbuseFilterParser part was
being executed. The new implementation checks the exception ID and is
thus more future-proof if the i18n message changes.
- Fixed some function names in error reporting for the old parser.
- The arg count is now checked outside of the function handlers, thus
it's no more necessary to call checkEnoughArguments at the beginning
of each handler. This also produces clearer error messages in case of
aliases (e.g. set/set_var).
- Check the args count even if some of the args are DUNDEFINED. This is
much easier now that the check is outside of the handler. This will
make syntax check fail for e.g. `contains_any(added_lines)`.
Bug: T156095
Change-Id: I446a307e5395ea8cc8ec5ca5d5390b074bea2f24
2019-08-20 09:43:37 +00:00
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
|
|
|
} else {
|
|
|
|
$result = $this->$funcHandler( $args );
|
|
|
|
}
|
|
|
|
$this->funcCache[$funcHash] = $result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( count( $this->funcCache ) > 1000 ) {
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
$this->clearFuncCache();
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2019-11-08 14:02:17 +00:00
|
|
|
/**
|
|
|
|
* Helper to invoke a built-in keyword. Note that this assumes that $kname is
|
|
|
|
* a valid keyword name.
|
|
|
|
*
|
|
|
|
* @param string $kname
|
|
|
|
* @param AFPData $lhs
|
|
|
|
* @param AFPData $rhs
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function callKeyword( $kname, AFPData $lhs, AFPData $rhs ) : AFPData {
|
2019-11-16 15:32:36 +00:00
|
|
|
$func = self::KEYWORDS[$kname];
|
2019-10-01 16:27:56 +00:00
|
|
|
$this->raiseCondCount();
|
2019-11-08 14:02:17 +00:00
|
|
|
|
2019-10-01 16:27:56 +00:00
|
|
|
$hasUndefinedOperand = false;
|
|
|
|
if ( $lhs->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
$lhs = new AFPData( AFPData::DNULL );
|
|
|
|
$hasUndefinedOperand = true;
|
|
|
|
}
|
|
|
|
if ( $rhs->getType() === AFPData::DUNDEFINED ) {
|
|
|
|
$rhs = new AFPData( AFPData::DNULL );
|
|
|
|
$hasUndefinedOperand = true;
|
|
|
|
}
|
|
|
|
if ( $hasUndefinedOperand ) {
|
|
|
|
// We need to run the handler with bogus args, see the comment in self::callFunc (T234339)
|
|
|
|
// @todo Likewise, this is subpar.
|
|
|
|
// @phan-suppress-next-line PhanParamTooMany Not every function needs the position
|
|
|
|
$this->$func( $lhs, $rhs, $this->mCur->pos );
|
|
|
|
$result = new AFPData( AFPData::DUNDEFINED );
|
2019-11-08 14:02:17 +00:00
|
|
|
} else {
|
|
|
|
// @phan-suppress-next-line PhanParamTooMany Not every function needs the position
|
|
|
|
$result = $this->$func( $lhs, $rhs, $this->mCur->pos );
|
|
|
|
}
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
|
2009-10-07 13:57:06 +00:00
|
|
|
// Built-in functions
|
2012-03-11 20:40:04 +00:00
|
|
|
|
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcLc( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $this->contLang->lc( $s ) );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2013-04-17 16:36:10 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2013-04-17 16:36:10 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcUc( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
|
|
|
|
2019-08-21 10:04:10 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $this->contLang->uc( $s ) );
|
2015-09-28 18:03:35 +00:00
|
|
|
}
|
2013-04-17 16:36:10 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcLen( $args ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $args[0]->type === AFPData::DARRAY ) {
|
2018-04-16 15:37:10 +00:00
|
|
|
// Don't use toString on arrays, but count
|
2018-10-03 15:19:40 +00:00
|
|
|
$val = count( $args[0]->data );
|
|
|
|
} else {
|
|
|
|
$val = mb_strlen( $args[0]->toString(), 'utf-8' );
|
2012-12-20 01:19:55 +00:00
|
|
|
}
|
2015-09-28 18:03:35 +00:00
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
return new AFPData( AFPData::DINT, $val );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcSpecialRatio( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
|
|
|
if ( !strlen( $s ) ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DFLOAT, 0 );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2008-08-31 05:56:49 +00:00
|
|
|
$nospecials = $this->rmspecials( $s );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
|
|
|
$val = 1. - ( ( mb_strlen( $nospecials ) / mb_strlen( $s ) ) );
|
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DFLOAT, $val );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcCount( $args ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( $args[0]->type === AFPData::DARRAY && count( $args ) === 1 ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DINT, count( $args[0]->data ) );
|
2009-04-05 17:11:17 +00:00
|
|
|
}
|
|
|
|
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( count( $args ) === 1 ) {
|
2010-08-19 21:12:09 +00:00
|
|
|
$count = count( explode( ',', $args[0]->toString() ) );
|
2008-08-31 05:56:49 +00:00
|
|
|
} else {
|
|
|
|
$needle = $args[0]->toString();
|
|
|
|
$haystack = $args[1]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-11-13 19:13:07 +00:00
|
|
|
// T62203: Keep empty parameters from causing PHP warnings
|
2016-04-17 06:32:27 +00:00
|
|
|
if ( $needle === '' ) {
|
|
|
|
$count = 0;
|
|
|
|
} else {
|
|
|
|
$count = substr_count( $haystack, $needle );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DINT, $count );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2009-03-07 01:26:42 +00:00
|
|
|
protected function funcRCount( $args ) {
|
2018-08-26 08:34:42 +00:00
|
|
|
if ( count( $args ) === 1 ) {
|
2010-08-19 21:12:09 +00:00
|
|
|
$count = count( explode( ',', $args[0]->toString() ) );
|
2009-03-07 01:26:42 +00:00
|
|
|
} else {
|
2011-02-10 17:25:25 +00:00
|
|
|
$needle = $args[0]->toString();
|
2009-03-07 01:26:42 +00:00
|
|
|
$haystack = $args[1]->toString();
|
|
|
|
|
2018-04-04 21:14:25 +00:00
|
|
|
// Munge the regex
|
2009-03-25 12:43:53 +00:00
|
|
|
$needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
|
2009-03-22 10:31:26 +00:00
|
|
|
$needle = "/$needle/u";
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-07-17 15:17:44 +00:00
|
|
|
// Suppress and restore needed per T177744
|
2018-06-30 14:20:33 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2016-07-28 22:35:40 +00:00
|
|
|
$count = preg_match_all( $needle, $haystack );
|
2018-06-30 14:20:33 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-10-21 23:20:07 +00:00
|
|
|
if ( $count === false ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'regexfailure',
|
|
|
|
$this->mCur->pos,
|
2018-10-08 12:48:08 +00:00
|
|
|
[ $needle ]
|
2015-10-21 23:20:07 +00:00
|
|
|
);
|
2009-06-18 20:13:52 +00:00
|
|
|
}
|
2009-03-07 01:26:42 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DINT, $count );
|
2009-03-07 01:26:42 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-11-07 18:44:10 +00:00
|
|
|
/**
|
|
|
|
* Returns an array of matches of needle in the haystack, the first one for the whole regex,
|
|
|
|
* the other ones for every capturing group.
|
|
|
|
*
|
|
|
|
* @param array $args
|
2018-04-16 15:37:10 +00:00
|
|
|
* @return AFPData An array of matches.
|
2017-11-07 18:44:10 +00:00
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
|
|
|
protected function funcGetMatches( $args ) {
|
|
|
|
$needle = $args[0]->toString();
|
|
|
|
$haystack = $args[1]->toString();
|
|
|
|
|
|
|
|
// Count the amount of capturing groups in the submitted pattern.
|
|
|
|
// This way we can return a fixed-dimension array, much easier to manage.
|
2018-11-01 09:21:36 +00:00
|
|
|
// ToDo: Find a better way to do this.
|
2017-11-07 18:44:10 +00:00
|
|
|
// First, strip away escaped parentheses
|
|
|
|
$sanitized = preg_replace( '/(\\\\\\\\)*\\\\\(/', '', $needle );
|
2018-11-01 09:21:36 +00:00
|
|
|
// Then strip starting parentheses of non-capturing groups, including
|
|
|
|
// atomics, lookaheads and so on, even if not every of them is supported.
|
|
|
|
$sanitized = str_replace( '(?', '', $sanitized );
|
|
|
|
// And also strip "(*", used with backtracking verbs like (*FAIL)
|
|
|
|
$sanitized = str_replace( '(*', '', $sanitized );
|
2017-11-07 18:44:10 +00:00
|
|
|
// Finally create an array of falses with dimension = # of capturing groups
|
|
|
|
$groupscount = substr_count( $sanitized, '(' ) + 1;
|
|
|
|
$falsy = array_fill( 0, $groupscount, false );
|
|
|
|
|
|
|
|
// Munge the regex by escaping slashes
|
|
|
|
$needle = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $needle );
|
|
|
|
$needle = "/$needle/u";
|
|
|
|
|
|
|
|
// Suppress and restore are here for the same reason as T177744
|
2018-02-12 10:29:11 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
2017-11-07 18:44:10 +00:00
|
|
|
$check = preg_match( $needle, $haystack, $matches );
|
2018-02-12 10:29:11 +00:00
|
|
|
Wikimedia\restoreWarnings();
|
2017-11-07 18:44:10 +00:00
|
|
|
|
|
|
|
if ( $check === false ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'regexfailure',
|
|
|
|
$this->mCur->pos,
|
2018-10-08 12:48:08 +00:00
|
|
|
[ $needle ]
|
2017-11-07 18:44:10 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returned array has non-empty positions identical to the ones returned
|
|
|
|
// by the third parameter of a standard preg_match call ($matches in this case).
|
2018-10-03 12:02:00 +00:00
|
|
|
// We want an union with falsy to return a fixed-dimension array.
|
2017-11-07 18:44:10 +00:00
|
|
|
return AFPData::newFromPHPVar( $matches + $falsy );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
* @throws AFPUserVisibleException
|
|
|
|
*/
|
2009-03-09 12:39:52 +00:00
|
|
|
protected function funcIPInRange( $args ) {
|
|
|
|
$ip = $args[0]->toString();
|
|
|
|
$range = $args[1]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2016-03-07 17:09:13 +00:00
|
|
|
if ( !IP::isValidRange( $range ) ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'invalidiprange',
|
|
|
|
$this->mCur->pos,
|
|
|
|
[ $range ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2009-03-09 12:39:52 +00:00
|
|
|
$result = IP::isInRange( $ip, $range );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DBOOL, $result );
|
2009-03-09 12:39:52 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcCCNorm( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
|
|
|
$s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
|
2008-08-31 05:56:49 +00:00
|
|
|
$s = $this->ccnorm( $s );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $s );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-01-31 00:32:14 +00:00
|
|
|
/**
|
|
|
|
* @param array $args
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcSanitize( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
|
|
|
|
|
|
|
$s = html_entity_decode( $s, ENT_QUOTES, 'UTF-8' );
|
|
|
|
$s = Sanitizer::decodeCharReferences( $s );
|
|
|
|
|
|
|
|
return new AFPData( AFPData::DSTRING, $s );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2009-03-26 02:03:32 +00:00
|
|
|
protected function funcContainsAny( $args ) {
|
|
|
|
$s = array_shift( $args );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-04-10 11:28:34 +00:00
|
|
|
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true ) );
|
2017-09-19 23:54:03 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-09-19 23:54:03 +00:00
|
|
|
/**
|
2017-11-13 19:13:07 +00:00
|
|
|
* @param array $args
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcContainsAll( $args ) {
|
|
|
|
$s = array_shift( $args );
|
|
|
|
|
2018-04-10 11:28:34 +00:00
|
|
|
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, false ) );
|
2017-11-13 19:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Normalize and search a string for multiple substrings in OR mode
|
2017-09-19 23:54:03 +00:00
|
|
|
*
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2017-09-19 23:54:03 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcCCNormContainsAny( $args ) {
|
|
|
|
$s = array_shift( $args );
|
|
|
|
|
2018-04-10 11:28:34 +00:00
|
|
|
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, true, true ) );
|
2017-11-13 19:13:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Normalize and search a string for multiple substrings in AND mode
|
|
|
|
*
|
|
|
|
* @param array $args
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcCCNormContainsAll( $args ) {
|
|
|
|
$s = array_shift( $args );
|
|
|
|
|
2018-04-10 11:28:34 +00:00
|
|
|
return new AFPData( AFPData::DBOOL, self::contains( $s, $args, false, true ) );
|
2017-09-19 23:54:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search for substrings in a string
|
|
|
|
*
|
2017-11-13 19:13:07 +00:00
|
|
|
* Use is_any to determine wether to use logic OR (true) or AND (false).
|
|
|
|
*
|
2017-09-19 23:54:03 +00:00
|
|
|
* Use normalize = true to make use of ccnorm and
|
|
|
|
* normalize both sides of the search.
|
|
|
|
*
|
2018-04-06 08:45:15 +00:00
|
|
|
* @param AFPData $string
|
|
|
|
* @param AFPData[] $values
|
2018-04-10 11:28:34 +00:00
|
|
|
* @param bool $is_any
|
2017-09-19 23:54:03 +00:00
|
|
|
* @param bool $normalize
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
2018-04-10 11:28:34 +00:00
|
|
|
protected static function contains( $string, $values, $is_any = true, $normalize = false ) {
|
2017-09-19 23:54:03 +00:00
|
|
|
$string = $string->toString();
|
2018-02-09 11:24:01 +00:00
|
|
|
|
|
|
|
if ( $string === '' ) {
|
2017-09-19 23:54:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( $normalize ) {
|
|
|
|
$string = self::ccnorm( $string );
|
|
|
|
}
|
|
|
|
|
2017-11-13 19:13:07 +00:00
|
|
|
foreach ( $values as $needle ) {
|
|
|
|
$needle = $needle->toString();
|
2017-09-19 23:54:03 +00:00
|
|
|
if ( $normalize ) {
|
2017-11-13 19:13:07 +00:00
|
|
|
$needle = self::ccnorm( $needle );
|
|
|
|
}
|
|
|
|
if ( $needle === '' ) {
|
|
|
|
// T62203: Keep empty parameters from causing PHP warnings
|
|
|
|
continue;
|
2017-09-19 23:54:03 +00:00
|
|
|
}
|
|
|
|
|
2017-11-13 19:13:07 +00:00
|
|
|
$is_found = strpos( $string, $needle ) !== false;
|
|
|
|
if ( $is_found === $is_any ) {
|
2018-07-17 15:17:44 +00:00
|
|
|
// If I'm here and it's ANY (OR) => something is found.
|
|
|
|
// If I'm here and it's ALL (AND) => nothing is found.
|
|
|
|
// In both cases, we've had enough.
|
2017-11-13 19:13:07 +00:00
|
|
|
return $is_found;
|
2009-03-26 02:03:32 +00:00
|
|
|
}
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-07-17 15:17:44 +00:00
|
|
|
// If I'm here and it's ANY (OR) => nothing was found: return false ($is_any is true)
|
|
|
|
// If I'm here and it's ALL (AND) => everything was found: return true ($is_any is false)
|
2019-02-06 09:28:26 +00:00
|
|
|
return !$is_any;
|
2009-03-26 02:03:32 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-02-09 11:24:01 +00:00
|
|
|
/**
|
|
|
|
* @param array $args
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function funcEqualsToAny( $args ) {
|
|
|
|
$s = array_shift( $args );
|
|
|
|
|
|
|
|
return new AFPData( AFPData::DBOOL, self::equalsToAny( $s, $args ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the given string is equals to any of the following strings
|
|
|
|
*
|
2018-04-29 17:52:45 +00:00
|
|
|
* @param AFPData $string
|
|
|
|
* @param AFPData[] $values
|
2018-02-09 11:24:01 +00:00
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected static function equalsToAny( $string, $values ) {
|
|
|
|
$string = $string->toString();
|
|
|
|
|
|
|
|
foreach ( $values as $needle ) {
|
|
|
|
$needle = $needle->toString();
|
|
|
|
|
|
|
|
if ( $string === $needle ) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $s
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return mixed
|
|
|
|
*/
|
2017-09-19 23:54:03 +00:00
|
|
|
protected static function ccnorm( $s ) {
|
2018-07-17 15:17:44 +00:00
|
|
|
// Instantiate a single version of the equivset so the data is only loaded once.
|
2017-10-11 22:07:48 +00:00
|
|
|
if ( !self::$equivset ) {
|
|
|
|
self::$equivset = new Equivset();
|
2009-04-23 03:37:51 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-10-11 22:07:48 +00:00
|
|
|
return self::$equivset->normalize( $s );
|
2010-02-13 14:10:36 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $s
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return array|string
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function rmspecials( $s ) {
|
2012-03-11 20:40:04 +00:00
|
|
|
return preg_replace( '/[^\p{L}\p{N}]/u', '', $s );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $s
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return array|string
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function rmdoubles( $s ) {
|
2009-10-07 13:57:06 +00:00
|
|
|
return preg_replace( '/(.)\1+/us', '\1', $s );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param string $s
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return array|string
|
|
|
|
*/
|
2009-02-18 19:42:01 +00:00
|
|
|
protected function rmwhitespace( $s ) {
|
|
|
|
return preg_replace( '/\s+/u', '', $s );
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcRMSpecials( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2010-02-13 14:10:36 +00:00
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $this->rmspecials( $s ) );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2010-02-13 14:10:36 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2009-02-18 19:42:01 +00:00
|
|
|
protected function funcRMWhitespace( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $this->rmwhitespace( $s ) );
|
2009-02-18 19:42:01 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcRMDoubles( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $this->rmdoubles( $s ) );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function funcNorm( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
|
|
|
$s = $this->ccnorm( $s );
|
2008-08-31 05:56:49 +00:00
|
|
|
$s = $this->rmdoubles( $s );
|
2008-09-22 14:03:45 +00:00
|
|
|
$s = $this->rmspecials( $s );
|
2009-02-18 19:42:01 +00:00
|
|
|
$s = $this->rmwhitespace( $s );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $s );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2009-04-01 05:05:23 +00:00
|
|
|
protected function funcSubstr( $args ) {
|
|
|
|
$s = $args[0]->toString();
|
|
|
|
$offset = $args[1]->toInt();
|
2018-10-03 15:19:40 +00:00
|
|
|
$length = isset( $args[2] ) ? $args[2]->toInt() : null;
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
$result = mb_substr( $s, $offset, $length );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, $result );
|
2009-04-01 05:05:23 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2009-04-01 05:05:23 +00:00
|
|
|
protected function funcStrPos( $args ) {
|
|
|
|
$haystack = $args[0]->toString();
|
|
|
|
$needle = $args[1]->toString();
|
2018-10-03 15:19:40 +00:00
|
|
|
$offset = isset( $args[2] ) ? $args[2]->toInt() : 0;
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2017-11-13 19:13:07 +00:00
|
|
|
// T62203: Keep empty parameters from causing PHP warnings
|
2014-01-18 17:05:03 +00:00
|
|
|
if ( $needle === '' ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DINT, -1 );
|
2014-01-18 17:05:03 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 15:19:40 +00:00
|
|
|
$result = mb_strpos( $haystack, $needle, $offset );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2016-01-06 20:17:41 +00:00
|
|
|
if ( $result === false ) {
|
2015-09-28 18:03:35 +00:00
|
|
|
$result = -1;
|
2016-01-06 20:17:41 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DINT, $result );
|
2009-04-01 05:05:23 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2009-04-01 05:05:23 +00:00
|
|
|
protected function funcStrReplace( $args ) {
|
|
|
|
$subject = $args[0]->toString();
|
|
|
|
$search = $args[1]->toString();
|
|
|
|
$replace = $args[2]->toString();
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
return new AFPData( AFPData::DSTRING, str_replace( $search, $replace, $subject ) );
|
2009-04-01 05:05:23 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2011-10-18 17:57:33 +00:00
|
|
|
protected function funcStrRegexEscape( $args ) {
|
|
|
|
$string = $args[0]->toString();
|
|
|
|
|
2015-09-28 18:03:35 +00:00
|
|
|
// preg_quote does not need the second parameter, since rlike takes
|
|
|
|
// care of the delimiter symbol itself
|
|
|
|
return new AFPData( AFPData::DSTRING, preg_quote( $string ) );
|
2011-10-18 17:57:33 +00:00
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return mixed
|
|
|
|
*/
|
2009-04-01 06:53:18 +00:00
|
|
|
protected function funcSetVar( $args ) {
|
|
|
|
$varName = $args[0]->toString();
|
|
|
|
$value = $args[1];
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2009-04-05 11:47:42 +00:00
|
|
|
$this->setUserVariable( $varName, $value );
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2009-04-01 06:53:18 +00:00
|
|
|
return $value;
|
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2019-08-12 12:23:46 +00:00
|
|
|
/**
|
|
|
|
* Checks if $a contains $b
|
|
|
|
*
|
|
|
|
* @param AFPData $a
|
|
|
|
* @param AFPData $b
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function containmentKeyword( AFPData $a, AFPData $b ) {
|
|
|
|
$a = $a->toString();
|
|
|
|
$b = $b->toString();
|
|
|
|
|
|
|
|
if ( $a === '' || $b === '' ) {
|
|
|
|
return new AFPData( AFPData::DBOOL, false );
|
|
|
|
}
|
|
|
|
|
|
|
|
return new AFPData( AFPData::DBOOL, strpos( $a, $b ) !== false );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AFPData $a
|
|
|
|
* @param AFPData $b
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function keywordIn( AFPData $a, AFPData $b ) {
|
|
|
|
return $this->containmentKeyword( $b, $a );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AFPData $a
|
|
|
|
* @param AFPData $b
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function keywordContains( AFPData $a, AFPData $b ) {
|
|
|
|
return $this->containmentKeyword( $a, $b );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AFPData $str
|
|
|
|
* @param AFPData $pattern
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function keywordLike( AFPData $str, AFPData $pattern ) {
|
|
|
|
$str = $str->toString();
|
2019-11-16 15:32:36 +00:00
|
|
|
$pattern = '#^' . strtr( preg_quote( $pattern->toString(), '#' ), AFPData::WILDCARD_MAP ) . '$#u';
|
2019-08-12 12:23:46 +00:00
|
|
|
Wikimedia\suppressWarnings();
|
|
|
|
$result = preg_match( $pattern, $str );
|
|
|
|
Wikimedia\restoreWarnings();
|
|
|
|
|
|
|
|
return new AFPData( AFPData::DBOOL, (bool)$result );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AFPData $str
|
|
|
|
* @param AFPData $regex
|
|
|
|
* @param int $pos
|
|
|
|
* @param bool $insensitive
|
|
|
|
* @return AFPData
|
|
|
|
* @throws Exception
|
|
|
|
*/
|
|
|
|
protected function keywordRegex( AFPData $str, AFPData $regex, $pos, $insensitive = false ) {
|
|
|
|
$str = $str->toString();
|
|
|
|
$pattern = $regex->toString();
|
|
|
|
|
|
|
|
$pattern = preg_replace( '!(\\\\\\\\)*(\\\\)?/!', '$1\/', $pattern );
|
|
|
|
$pattern = "/$pattern/u";
|
|
|
|
|
|
|
|
if ( $insensitive ) {
|
|
|
|
$pattern .= 'i';
|
|
|
|
}
|
|
|
|
|
|
|
|
Wikimedia\suppressWarnings();
|
|
|
|
$result = preg_match( $pattern, $str );
|
|
|
|
Wikimedia\restoreWarnings();
|
|
|
|
if ( $result === false ) {
|
|
|
|
throw new AFPUserVisibleException(
|
|
|
|
'regexfailure',
|
|
|
|
// Coverage bug
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
$pos,
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
[ $pattern ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new AFPData( AFPData::DBOOL, (bool)$result );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param AFPData $str
|
|
|
|
* @param AFPData $regex
|
|
|
|
* @param int $pos
|
|
|
|
* @return AFPData
|
|
|
|
*/
|
|
|
|
protected function keywordRegexInsensitive( AFPData $str, AFPData $regex, $pos ) {
|
|
|
|
return $this->keywordRegex( $str, $regex, $pos, true );
|
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function castString( $args ) {
|
2018-10-03 15:19:40 +00:00
|
|
|
return AFPData::castTypes( $args[0], AFPData::DSTRING );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function castInt( $args ) {
|
2018-10-03 15:19:40 +00:00
|
|
|
return AFPData::castTypes( $args[0], AFPData::DINT );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function castFloat( $args ) {
|
2018-10-03 15:19:40 +00:00
|
|
|
return AFPData::castTypes( $args[0], AFPData::DFLOAT );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2009-10-07 13:57:06 +00:00
|
|
|
|
2012-03-11 20:40:04 +00:00
|
|
|
/**
|
2017-10-06 18:52:31 +00:00
|
|
|
* @param array $args
|
2012-03-11 20:40:04 +00:00
|
|
|
* @return AFPData
|
|
|
|
*/
|
2008-08-31 05:56:49 +00:00
|
|
|
protected function castBool( $args ) {
|
2018-10-03 15:19:40 +00:00
|
|
|
return AFPData::castTypes( $args[0], AFPData::DBOOL );
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|
2019-08-03 15:35:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Log empty operands for T156096
|
|
|
|
*
|
|
|
|
* @param string $type Type of the empty operand
|
2019-09-16 11:13:44 +00:00
|
|
|
*/
|
|
|
|
protected function logEmptyOperand( $type ) {
|
|
|
|
if ( $this->mFilter !== null ) {
|
|
|
|
$this->logger->warning(
|
|
|
|
'DEPRECATED! Found empty operand of type `{op_type}` when parsing filter: {filter}. ' .
|
|
|
|
'This is deprecated since 1.34 and support will be discontinued soon. Please fix ' .
|
|
|
|
'the affected filter!',
|
|
|
|
[
|
|
|
|
'op_type' => $type,
|
|
|
|
'filter' => $this->mFilter
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
2019-08-03 15:35:00 +00:00
|
|
|
}
|
2019-09-06 16:28:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $fname
|
|
|
|
* @return bool
|
|
|
|
* @see AFPTreeParser::functionIsVariadic
|
|
|
|
*/
|
|
|
|
protected function functionIsVariadic( $fname ) {
|
2019-10-03 12:13:26 +00:00
|
|
|
if ( !array_key_exists( $fname, self::FUNC_ARG_COUNT ) ) {
|
2019-09-06 16:28:53 +00:00
|
|
|
throw new InvalidArgumentException( "Function $fname is not valid" );
|
|
|
|
}
|
2019-10-03 12:13:26 +00:00
|
|
|
return self::FUNC_ARG_COUNT[$fname][1] === INF;
|
2019-09-06 16:28:53 +00:00
|
|
|
}
|
2008-08-31 05:56:49 +00:00
|
|
|
}
|