PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 1.3.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v1.3.1
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / Segment.php
matomo / app / core Last commit date
API 6 years ago Access 6 years ago Application 6 years ago Archive 6 years ago ArchiveProcessor 6 years ago Archiver 6 years ago AssetManager 6 years ago Auth 6 years ago Category 6 years ago CliMulti 6 years ago Columns 6 years ago Composer 6 years ago Concurrency 6 years ago Config 6 years ago Container 6 years ago CronArchive 6 years ago DataAccess 5 years ago DataFiles 6 years ago DataTable 6 years ago Db 6 years ago DeviceDetector 5 years ago Email 6 years ago Exception 6 years ago Http 6 years ago Intl 6 years ago Mail 6 years ago Measurable 6 years ago Menu 6 years ago Metrics 6 years ago Notification 6 years ago Period 6 years ago Plugin 6 years ago ProfessionalServices 6 years ago Report 6 years ago ReportRenderer 6 years ago Scheduler 6 years ago Segment 6 years ago Session 6 years ago Settings 6 years ago Tracker 5 years ago Translation 6 years ago UpdateCheck 6 years ago Updater 6 years ago Updates 6 years ago Validators 6 years ago View 6 years ago ViewDataTable 6 years ago Visualization 6 years ago Widget 6 years ago .htaccess 6 years ago Access.php 6 years ago Archive.php 6 years ago ArchiveProcessor.php 6 years ago AssetManager.php 6 years ago Auth.php 6 years ago BaseFactory.php 6 years ago Cache.php 6 years ago CacheId.php 6 years ago CliMulti.php 6 years ago Common.php 6 years ago Config.php 6 years ago Console.php 6 years ago Context.php 6 years ago Cookie.php 5 years ago CronArchive.php 5 years ago DataArray.php 6 years ago DataTable.php 6 years ago Date.php 6 years ago Db.php 6 years ago DbHelper.php 6 years ago Development.php 6 years ago DeviceDetectorFactory.php 6 years ago ErrorHandler.php 6 years ago EventDispatcher.php 6 years ago ExceptionHandler.php 6 years ago FileIntegrity.php 6 years ago Filechecks.php 6 years ago Filesystem.php 6 years ago FrontController.php 6 years ago Http.php 6 years ago IP.php 6 years ago Log.php 6 years ago LogDeleter.php 6 years ago Mail.php 6 years ago Metrics.php 6 years ago MetricsFormatter.php 6 years ago Nonce.php 5 years ago Notification.php 6 years ago NumberFormatter.php 6 years ago Option.php 5 years ago Period.php 6 years ago Piwik.php 6 years ago Plugin.php 6 years ago Profiler.php 6 years ago ProxyHeaders.php 6 years ago ProxyHttp.php 6 years ago QuickForm2.php 6 years ago RankingQuery.php 6 years ago Registry.php 6 years ago ReportRenderer.php 6 years ago ScheduledTask.php 6 years ago Segment.php 6 years ago Sequence.php 6 years ago Session.php 6 years ago SettingsPiwik.php 6 years ago SettingsServer.php 6 years ago Singleton.php 6 years ago Site.php 6 years ago TCPDF.php 6 years ago TaskScheduler.php 6 years ago Theme.php 6 years ago Timer.php 6 years ago Tracker.php 6 years ago Translate.php 6 years ago Twig.php 6 years ago Unzip.php 6 years ago UpdateCheck.php 6 years ago Updater.php 6 years ago Updates.php 6 years ago Url.php 6 years ago UrlHelper.php 6 years ago Version.php 5 years ago View.php 6 years ago bootstrap.php 6 years ago dispatch.php 6 years ago testMinimumPhpVersion.php 6 years ago
Segment.php
494 lines
1 <?php
2 /**
3 * Piwik - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik;
10
11 use Exception;
12 use Piwik\API\Request;
13 use Piwik\ArchiveProcessor\Rules;
14 use Piwik\Container\StaticContainer;
15 use Piwik\DataAccess\LogQueryBuilder;
16 use Piwik\Plugins\SegmentEditor\SegmentEditor;
17 use Piwik\Segment\SegmentExpression;
18
19 /**
20 * Limits the set of visits Piwik uses when aggregating analytics data.
21 *
22 * A segment is a condition used to filter visits. They can, for example,
23 * select visits that have a specific browser or come from a specific
24 * country, or both.
25 *
26 * Plugins that aggregate data stored in Piwik can support segments by
27 * using this class when generating aggregation SQL queries.
28 *
29 * ### Examples
30 *
31 * **Basic usage**
32 *
33 * $idSites = array(1,2,3);
34 * $segmentStr = "browserCode==ff;countryCode==CA";
35 * $segment = new Segment($segmentStr, $idSites);
36 *
37 * $query = $segment->getSelectQuery(
38 * $select = "table.col1, table2.col2",
39 * $from = array("table", "table2"),
40 * $where = "table.col3 = ?",
41 * $bind = array(5),
42 * $orderBy = "table.col1 DESC",
43 * $groupBy = "table2.col2"
44 * );
45 *
46 * Db::fetchAll($query['sql'], $query['bind']);
47 *
48 * **Creating a _null_ segment**
49 *
50 * $idSites = array(1,2,3);
51 * $segment = new Segment('', $idSites);
52 * // $segment->getSelectQuery will return a query that selects all visits
53 *
54 * @api
55 */
56 class Segment
57 {
58 /**
59 * @var SegmentExpression
60 */
61 protected $segmentExpression = null;
62
63 /**
64 * @var string
65 */
66 protected $string = null;
67
68 /**
69 * @var string
70 */
71 protected $originalString = null;
72
73 /**
74 * @var array
75 */
76 protected $idSites = null;
77
78 /**
79 * @var LogQueryBuilder
80 */
81 private $segmentQueryBuilder;
82
83 /**
84 * @var bool
85 */
86 private $isSegmentEncoded;
87
88 /**
89 * Truncate the Segments to 8k
90 */
91 const SEGMENT_TRUNCATE_LIMIT = 8192;
92
93 /**
94 * Constructor.
95 *
96 * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`.
97 * @param array $idSites The list of sites the segment will be used with. Some segments are
98 * dependent on the site, such as goal segments.
99 * @throws
100 */
101 public function __construct($segmentCondition, $idSites)
102 {
103 $this->segmentQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
104
105 $segmentCondition = trim($segmentCondition);
106 if (!SettingsPiwik::isSegmentationEnabled()
107 && !empty($segmentCondition)
108 ) {
109 throw new Exception("The Super User has disabled the Segmentation feature.");
110 }
111
112 $this->originalString = $segmentCondition;
113
114 // The segment expression can be urlencoded. Unfortunately, both the encoded and decoded versions
115 // can usually be parsed successfully. To pick the right one, we try both and pick the one w/ more
116 // successfully parsed subexpressions.
117 $subexpressionsDecoded = 0;
118 try {
119 $this->initializeSegment(urldecode($segmentCondition), $idSites);
120 $subexpressionsDecoded = $this->segmentExpression->getSubExpressionCount();
121 } catch (Exception $e) {
122 // ignore
123 }
124
125 $subexpressionsRaw = 0;
126 try {
127 $this->initializeSegment($segmentCondition, $idSites);
128 $subexpressionsRaw = $this->segmentExpression->getSubExpressionCount();
129 } catch (Exception $e) {
130 // ignore
131 }
132
133 if ($subexpressionsRaw > $subexpressionsDecoded) {
134 $this->initializeSegment($segmentCondition, $idSites);
135 $this->isSegmentEncoded = false;
136 } else {
137 $this->initializeSegment(urldecode($segmentCondition), $idSites);
138 $this->isSegmentEncoded = true;
139 }
140 }
141
142 /**
143 * Returns the segment expression.
144 * @return SegmentExpression
145 * @api since Piwik 3.2.0
146 */
147 public function getSegmentExpression()
148 {
149 return $this->segmentExpression;
150 }
151
152 private function getAvailableSegments()
153 {
154 // segment metadata
155 if (empty($this->availableSegments)) {
156 $this->availableSegments = Request::processRequest('API.getSegmentsMetadata', array(
157 'idSites' => $this->idSites,
158 '_hideImplementationData' => 0,
159 'filter_limit' => -1,
160 'filter_offset' => 0,
161 '_showAllSegments' => 1,
162 ), []);
163 }
164
165 return $this->availableSegments;
166 }
167
168 private function getSegmentByName($name)
169 {
170 $segments = $this->getAvailableSegments();
171
172 foreach ($segments as $segment) {
173 if ($segment['segment'] == $name && !empty($name)) {
174
175 // check permission
176 if (isset($segment['permission']) && $segment['permission'] != 1) {
177 throw new NoAccessException("You do not have enough permission to access the segment " . $name);
178 }
179
180 return $segment;
181 }
182 }
183
184 throw new Exception("Segment '$name' is not a supported segment.");
185 }
186
187 /**
188 * @param $string
189 * @param $idSites
190 * @throws Exception
191 */
192 protected function initializeSegment($string, $idSites)
193 {
194 // As a preventive measure, we restrict the filter size to a safe limit
195 $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
196
197 $this->string = $string;
198 $this->idSites = $idSites;
199 $segment = new SegmentExpression($string);
200 $this->segmentExpression = $segment;
201
202 // parse segments
203 $expressions = $segment->parseSubExpressions();
204 $expressions = $this->getExpressionsWithUnionsResolved($expressions);
205
206 // convert segments name to sql segment
207 // check that user is allowed to view this segment
208 // and apply a filter to the value to match if necessary (to map DB fields format)
209 $cleanedExpressions = array();
210 foreach ($expressions as $expression) {
211 $operand = $expression[SegmentExpression::INDEX_OPERAND];
212 $cleanedExpression = $this->getCleanedExpression($operand);
213 $expression[SegmentExpression::INDEX_OPERAND] = $cleanedExpression;
214 $cleanedExpressions[] = $expression;
215 }
216
217 $segment->setSubExpressionsAfterCleanup($cleanedExpressions);
218 }
219
220 private function getExpressionsWithUnionsResolved($expressions)
221 {
222 $expressionsWithUnions = array();
223 foreach ($expressions as $expression) {
224 $operand = $expression[SegmentExpression::INDEX_OPERAND];
225 $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
226
227 $availableSegment = $this->getSegmentByName($name);
228
229 if (!empty($availableSegment['unionOfSegments'])) {
230 $count = 0;
231 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
232 $count++;
233 $operator = SegmentExpression::BOOL_OPERATOR_OR; // we connect all segments within that union via OR
234 if ($count === count($availableSegment['unionOfSegments'])) {
235 $operator = $expression[SegmentExpression::INDEX_BOOL_OPERATOR];
236 }
237
238 $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion;
239 $expressionsWithUnions[] = array(
240 SegmentExpression::INDEX_BOOL_OPERATOR => $operator,
241 SegmentExpression::INDEX_OPERAND => $operand
242 );
243 }
244 } else {
245 $expressionsWithUnions[] = array(
246 SegmentExpression::INDEX_BOOL_OPERATOR => $expression[SegmentExpression::INDEX_BOOL_OPERATOR],
247 SegmentExpression::INDEX_OPERAND => $operand
248 );
249 }
250 }
251
252 return $expressionsWithUnions;
253 }
254
255 /**
256 * Returns `true` if the segment is empty, `false` if otherwise.
257 */
258 public function isEmpty()
259 {
260 return $this->segmentExpression->isEmpty();
261 }
262
263 /**
264 * Detects whether the Piwik instance is configured to be able to archive this segment. It checks whether the segment
265 * will be either archived via browser or cli archiving. It does not check if the segment has been archived. If you
266 * want to know whether the segment has been archived, the actual report data needs to be requested.
267 *
268 * This method does not take any date/period into consideration. Meaning a Piwik instance might be able to archive
269 * this segment in general, but not for a certain period if eg the archiving of range dates is disabled.
270 *
271 * @return bool
272 */
273 public function willBeArchived()
274 {
275 if ($this->isEmpty()) {
276 return true;
277 }
278
279 $idSites = $this->idSites;
280 if (!is_array($idSites)) {
281 $idSites = array($this->idSites);
282 }
283
284 return Rules::isRequestAuthorizedToArchive()
285 || Rules::isBrowserArchivingAvailableForSegments()
286 || Rules::isSegmentPreProcessed($idSites, $this);
287 }
288
289 protected $availableSegments = array();
290
291 protected function getCleanedExpression($expression)
292 {
293 $name = $expression[SegmentExpression::INDEX_OPERAND_NAME];
294 $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR];
295 $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE];
296
297 $segment = $this->getSegmentByName($name);
298 $sqlName = $segment['sqlSegment'];
299
300 if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY
301 && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
302
303 if (isset($segment['sqlFilterValue'])) {
304 $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
305 }
306
307 // apply presentation filter
308 if (isset($segment['sqlFilter'])) {
309 $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
310
311 if(is_null($value)) { // null is returned in TableLogAction::getIdActionFromSegment()
312 return array(null, $matchType, null);
313 }
314
315 // sqlFilter-callbacks might return arrays for more complex cases
316 // e.g. see TableLogAction::getIdActionFromSegment()
317 if (is_array($value) && isset($value['SQL'])) {
318 // Special case: returned value is a sub sql expression!
319 $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS;
320 }
321 }
322 }
323
324 return array($sqlName, $matchType, $value);
325 }
326
327 /**
328 * Returns the segment condition.
329 *
330 * @return string
331 */
332 public function getString()
333 {
334 return $this->string;
335 }
336
337 /**
338 * Returns a hash of the segment condition, or the empty string if the segment
339 * condition is empty.
340 *
341 * @return string
342 */
343 public function getHash()
344 {
345 if (empty($this->string)) {
346 return '';
347 }
348 return self::getSegmentHash($this->string);
349 }
350
351 public static function getSegmentHash($definition)
352 {
353 // urldecode to normalize the string, as browsers may send slightly different payloads for the same archive
354 return md5(urldecode($definition));
355 }
356
357 /**
358 * Extend an SQL query that aggregates data over one of the 'log_' tables with segment expressions.
359 *
360 * @param string $select The select clause. Should NOT include the **SELECT** just the columns, eg,
361 * `'t1.col1 as col1, t2.col2 as col2'`.
362 * @param array|string $from Array of table names (without prefix), eg, `array('log_visit', 'log_conversion')`.
363 * @param false|string $where (optional) Where clause, eg, `'t1.col1 = ? AND t2.col2 = ?'`.
364 * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
365 * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
366 * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
367 * @param int $limit Limit number of result to $limit
368 * @param int $offset Specified the offset of the first row to return
369 * @param bool $forceGroupBy Force the group by and not using a subquery. Note: This may make the query slower see https://github.com/matomo-org/matomo/issues/9200#issuecomment-183641293
370 * A $groupBy value needs to be set for this to work.
371 * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
372 * Use only when you're not aggregating or it will sample the data.
373 * @return string The entire select query.
374 */
375 public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0, $forceGroupBy = false)
376 {
377 $segmentExpression = $this->segmentExpression;
378
379 $limitAndOffset = null;
380 if($limit > 0) {
381 $limitAndOffset = (int) $offset . ', ' . (int) $limit;
382 }
383
384 try {
385 if ($forceGroupBy && $groupBy) {
386 $this->segmentQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
387 }
388 $result = $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind,
389 $groupBy, $orderBy, $limitAndOffset);
390 } catch (Exception $e) {
391 if ($forceGroupBy && $groupBy) {
392 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
393 }
394 throw $e;
395 }
396
397 if ($forceGroupBy && $groupBy) {
398 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
399 }
400 return $result;
401 }
402
403 /**
404 * Returns the segment string.
405 *
406 * @return string
407 */
408 public function __toString()
409 {
410 return (string) $this->getString();
411 }
412
413 /**
414 * Combines this segment with another segment condition, if the segment condition is not already
415 * in the segment.
416 *
417 * The combination is naive in that it does not take order of operations into account.
418 *
419 * @param string $segment
420 * @param string $operator The operator to use. Should be either SegmentExpression::AND_DELIMITER
421 * or SegmentExpression::OR_DELIMITER.
422 * @param string $segmentCondition The segment condition to add.
423 * @return string
424 * @throws Exception
425 */
426 public static function combine($segment, $operator, $segmentCondition)
427 {
428 if (empty($segment)) {
429 return $segmentCondition;
430 }
431
432 if (empty($segmentCondition)
433 || self::containsCondition($segment, $operator, $segmentCondition)
434 ) {
435 return $segment;
436 }
437
438 return $segment . $operator . $segmentCondition;
439 }
440
441 private static function containsCondition($segment, $operator, $segmentCondition)
442 {
443 // check when segment/condition are of same encoding
444 return strpos($segment, $operator . $segmentCondition) !== false
445 || strpos($segment, $segmentCondition . $operator) !== false
446
447 // check when both operator & condition are urlencoded in $segment
448 || strpos($segment, urlencode($operator . $segmentCondition)) !== false
449 || strpos($segment, urlencode($segmentCondition . $operator)) !== false
450
451 // check when operator is not urlencoded, but condition is in $segment
452 || strpos($segment, $operator . urlencode($segmentCondition)) !== false
453 || strpos($segment, urlencode($segmentCondition) . $operator) !== false
454
455 // check when segment condition is urlencoded & $segment isn't
456 || strpos($segment, $operator . urldecode($segmentCondition)) !== false
457 || strpos($segment, urldecode($segmentCondition) . $operator) !== false
458
459 || $segment === $segmentCondition
460 || $segment === urlencode($segmentCondition)
461 || $segment === urldecode($segmentCondition);
462 }
463
464 public function getStoredSegmentName($idSite)
465 {
466 $segment = $this->getString();
467 if (empty($segment)) {
468 return Piwik::translate('SegmentEditor_DefaultAllVisits');
469 }
470
471 $availableSegments = SegmentEditor::getAllSegmentsForSite($idSite);
472
473 $foundStoredSegment = null;
474 foreach ($availableSegments as $storedSegment) {
475 if ($storedSegment['definition'] == $segment
476 || $storedSegment['definition'] == urldecode($segment)
477 || $storedSegment['definition'] == urlencode($segment)
478
479 || $storedSegment['definition'] == $this->originalString
480 || $storedSegment['definition'] == urldecode($this->originalString)
481 || $storedSegment['definition'] == urlencode($this->originalString)
482 ) {
483 $foundStoredSegment = $storedSegment;
484 }
485 }
486
487 if (isset($foundStoredSegment)) {
488 return $foundStoredSegment['name'];
489 }
490
491 return $this->isSegmentEncoded ? urldecode($segment) : $segment;
492 }
493 }
494