UrlCondition::getValidationPattern()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 26
ccs 12
cts 12
cp 1
rs 9.8333
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2
1
<?php
2
/**
3
 * @package   WPEmerge
4
 * @author    Atanas Angelov <[email protected]>
5
 * @copyright 2017-2019 Atanas Angelov
6
 * @license   https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0
7
 * @link      https://wpemerge.com/
8
 */
9
10
namespace WPEmerge\Routing\Conditions;
11
12
use WPEmerge\Exceptions\ConfigurationException;
13
use WPEmerge\Helpers\Url as UrlUtility;
14
use WPEmerge\Requests\RequestInterface;
15
use WPEmerge\Support\Arr;
16
17
/**
18
 * Check against the current url
19
 */
20
class UrlCondition implements ConditionInterface, UrlableInterface, CanFilterQueryInterface {
21
	const WILDCARD = '*';
22
23
	/**
24
	 * URL to check against.
25
	 *
26
	 * @var string
27
	 */
28
	protected $url = '';
29
30
	/**
31
	 * URL where.
32
	 *
33
	 * @var array<string, string>
34
	 */
35
	protected $url_where = [];
36
37
	/**
38
	 * Pattern to detect parameters in urls.
39
	 *
40
	 * @suppress HtmlUnknownTag
41
	 * @var string
42
	 */
43
	protected $url_pattern = '~
44
		(?:/)                     # match leading slash
45
		(?:\{)                    # opening curly brace
46
			(?P<name>[a-z]\w*)    # string starting with a-z and followed by word characters for the parameter name
47
			(?P<optional>\?)?     # optionally allow the user to mark the parameter as option using a literal ?
48
		(?:\})                    # closing curly brace
49
		(?=/)                     # lookahead for a trailing slash
50
	~ix';
51
52
	/**
53
	 * Pattern to detect valid parameters in url segments.
54
	 *
55
	 * @var string
56
	 */
57
	protected $parameter_pattern = '[^/]+';
58
59
	/**
60
	 * Constructor.
61
	 *
62
	 * @codeCoverageIgnore
63
	 * @param string $url
64
	 * @param array<string, string> $where
65
	 */
66
	public function __construct( $url, $where = [] ) {
67
		$this->setUrl( $url );
68
		$this->setUrlWhere( $where );
69
	}
70
71
	/**
72
	 * Make a new instance.
73
	 *
74
	 * @codeCoverageIgnore
75
	 * @param string $url
76
	 * @param array<string, string> $where
77
	 * @return self
78
	 */
79
	protected function make( $url, $where = [] ) {
80
		return new self( $url, $where );
81
	}
82
83
	/**
84
	 * {@inheritDoc}
85
	 */
86 1
	protected function whereIsSatisfied( RequestInterface $request ) {
87 1
		$where = $this->getUrlWhere();
88 1
		$arguments = $this->getArguments( $request );
89
90 1
		foreach ( $where as $parameter => $pattern ) {
91 1
			$value = Arr::get( $arguments, $parameter, '' );
92
93 1
			if ( ! preg_match( $pattern, $value ) ) {
94 1
				return false;
95
			}
96
		}
97
98 1
		return true;
99
	}
100
101
	/**
102
	 * {@inheritDoc}
103
	 */
104 3
	public function isSatisfied( RequestInterface $request ) {
105 3
		if ( $this->getUrl() === static::WILDCARD ) {
106 1
			return true;
107
		}
108
109 2
		$validation_pattern = $this->getValidationPattern( $this->getUrl() );
110 2
		$url = UrlUtility::addTrailingSlash( UrlUtility::getPath( $request ) );
111 2
		$match = (bool) preg_match( $validation_pattern, $url );
112
113 2
		if ( ! $match || empty( $this->getUrlWhere() ) ) {
114 2
			return $match;
115
		}
116
117 1
		return $this->whereIsSatisfied( $request );
118
	}
119
120
	/**
121
	 * {@inheritDoc}
122
	 */
123 1
	public function getArguments( RequestInterface $request ) {
124 1
		$validation_pattern = $this->getValidationPattern( $this->getUrl() );
125 1
		$url = UrlUtility::addTrailingSlash( UrlUtility::getPath( $request ) );
126 1
		$matches = [];
127 1
		$success = preg_match( $validation_pattern, $url, $matches );
128
129 1
		if ( ! $success ) {
130 1
			return []; // this should not normally happen
131
		}
132
133 1
		$arguments = [];
134 1
		$parameter_names = $this->getParameterNames( $this->getUrl() );
135 1
		foreach ( $parameter_names as $parameter_name ) {
136 1
			$arguments[ $parameter_name ] = isset( $matches[ $parameter_name ] ) ? $matches[ $parameter_name ] : '';
137
		}
138
139 1
		return $arguments;
140
	}
141
142
	/**
143
	 * Get the url for this condition.
144
	 *
145
	 * @return string
146
	 */
147 2
	public function getUrl() {
148 2
		return $this->url;
149
	}
150
151
	/**
152
	 * Set the url for this condition.
153
	 *
154
	 * @param  string $url
155
	 * @return void
156
	 */
157 2
	public function setUrl( $url ) {
158 2
		if ( $url !== static::WILDCARD ) {
159 1
			$url = UrlUtility::addLeadingSlash( UrlUtility::addTrailingSlash( $url ) );
160
		}
161
162 2
		$this->url = $url;
163 2
	}
164
165
	/**
166
	 * {@inheritDoc}
167
	 */
168 1
	public function getUrlWhere() {
169 1
		return $this->url_where;
170
	}
171
172
	/**
173
	 * {@inheritDoc}
174
	 */
175 1
	public function setUrlWhere( $where ) {
176 1
		$this->url_where = $where;
177 1
	}
178
179
	/**
180
	 * Append a url to this one returning a new instance.
181
	 *
182
	 * @param  string                $url
183
	 * @param  array<string, string> $where
184
	 * @return static
185
	 */
186 3
	public function concatenate( $url, $where = [] ) {
187 3
		if ( $this->getUrl() === static::WILDCARD || $url === static::WILDCARD ) {
188 1
			return $this->make( static::WILDCARD );
189
		}
190
191 2
		$leading = UrlUtility::addLeadingSlash( UrlUtility::removeTrailingSlash( $this->getUrl() ), true );
192 2
		$trailing = UrlUtility::addLeadingSlash( UrlUtility::addTrailingSlash( $url ) );
193
194 2
		return $this->make( $leading . $trailing, array_merge(
195 2
			$this->getUrlWhere(),
196
			$where
197
		) );
198
	}
199
200
	/**
201
	 * Get parameter names as defined in the url.
202
	 *
203
	 * @param  string   $url
204
	 * @return string[]
205
	 */
206 1
	protected function getParameterNames( $url ) {
207 1
		$matches = [];
208 1
		preg_match_all( $this->url_pattern, $url, $matches );
209 1
		return $matches['name'];
210
	}
211
212
	/**
213
	 * Validation pattern replace callback.
214
	 *
215
	 * @param  array  $matches
216
	 * @param  array  $parameters
217
	 * @return string
218
	 */
219 1
	protected function replacePatternParameterWithPlaceholder( $matches, &$parameters ) {
220 1
		$name = $matches['name'];
221 1
		$optional = ! empty( $matches['optional'] );
222
223 1
		$replacement = '/(?P<' . $name . '>' . $this->parameter_pattern . ')';
224
225 1
		if ( $optional ) {
226 1
			$replacement = '(?:' . $replacement . ')?';
227
		}
228
229 1
		$hash = sha1( implode( '_', [
230 1
			count( $parameters ),
231 1
			$replacement,
232 1
			uniqid( 'wpemerge_', true ),
233
		] ) );
234 1
		$placeholder = '___placeholder_' . $hash . '___';
235 1
		$parameters[ $placeholder ] = $replacement;
236
237 1
		return $placeholder;
238
	}
239
240
	/**
241
	 * Get pattern to test whether normal urls match the parameter-based one.
242
	 *
243
	 * @param  string  $url
244
	 * @param  boolean $wrap
245
	 * @return string
246
	 */
247 1
	public function getValidationPattern( $url, $wrap = true ) {
248 1
		$parameters = [];
249
250
		// Replace all parameters with placeholders
251
		$validation_pattern = preg_replace_callback( $this->url_pattern, function ( $matches ) use ( &$parameters ) {
252 1
			return $this->replacePatternParameterWithPlaceholder( $matches, $parameters );
253 1
		}, $url );
254
255
		// Quote the remaining string so that it does not get evaluated as a pattern.
256 1
		$validation_pattern = preg_quote( $validation_pattern, '~' );
257
258
		// Replace the placeholders with the real parameter patterns.
259 1
		$validation_pattern = str_replace(
260 1
			array_keys( $parameters ),
261 1
			array_values( $parameters ),
262
			$validation_pattern
263
		);
264
265
		// Match the entire url; make trailing slash optional.
266 1
		$validation_pattern = '^' . $validation_pattern . '?$';
267
268 1
		if ( $wrap ) {
269 1
			$validation_pattern = '~' . $validation_pattern . '~';
270
		}
271
272 1
		return $validation_pattern;
273
	}
274
275
	/**
276
	 * {@inheritDoc}
277
	 */
278 2
	public function toUrl( $arguments = [] ) {
279
		$url = preg_replace_callback( $this->url_pattern, function ( $matches ) use ( $arguments ) {
280 2
			$name = $matches['name'];
281 2
			$optional = ! empty( $matches['optional'] );
282 2
			$value = '/' . urlencode( Arr::get( $arguments, $name, '' ) );
283
284 2
			if ( $value === '/' ) {
285 2
				if ( ! $optional ) {
286 1
					throw new ConfigurationException( "Required URL parameter \"$name\" is not specified." );
287
				}
288
289 1
				$value = '';
290
			}
291
292 1
			return $value;
293 2
		}, $this->getUrl() );
294
295 1
		return home_url( UrlUtility::addLeadingSlash( UrlUtility::removeTrailingSlash( $url ) ) );
296
	}
297
}
298