PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.15.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.15.0
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 2 years ago Access 3 years ago Application 4 years ago Archive 3 years ago ArchiveProcessor 2 years ago Archiver 5 years ago AssetManager 3 years ago Auth 3 years ago Category 5 years ago Changes 3 years ago CliMulti 4 years ago Columns 3 years ago Concurrency 3 years ago Config 4 years ago Container 4 years ago CronArchive 3 years ago DataAccess 2 years ago DataFiles 5 years ago DataTable 2 years ago Db 3 years ago DeviceDetector 3 years ago Email 5 years ago Exception 3 years ago Http 4 years ago Intl 4 years ago Mail 3 years ago Measurable 5 years ago Menu 3 years ago Metrics 4 years ago Notification 4 years ago Period 4 years ago Plugin 2 years ago ProfessionalServices 4 years ago Report 5 years ago ReportRenderer 2 years ago Scheduler 4 years ago Segment 3 years ago Session 4 years ago Settings 3 years ago Tracker 3 years ago Translation 4 years ago UpdateCheck 5 years ago Updater 3 years ago Updates 3 years ago Validators 4 years ago View 4 years ago ViewDataTable 3 years ago Visualization 4 years ago Widget 5 years ago Access.php 4 years ago Archive.php 3 years ago ArchiveProcessor.php 2 years ago AssetManager.php 4 years ago Auth.php 5 years ago AuthResult.php 5 years ago BaseFactory.php 5 years ago Cache.php 5 years ago CacheId.php 5 years ago CliMulti.php 4 years ago Common.php 3 years ago Config.php 3 years ago Console.php 3 years ago Context.php 5 years ago Cookie.php 3 years ago CronArchive.php 3 years ago DataArray.php 4 years ago DataTable.php 2 years ago Date.php 3 years ago Db.php 4 years ago DbHelper.php 3 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 3 years ago ExceptionHandler.php 3 years ago FileIntegrity.php 3 years ago Filechecks.php 4 years ago Filesystem.php 3 years ago FrontController.php 3 years ago Http.php 3 years ago IP.php 4 years ago Log.php 4 years ago LogDeleter.php 4 years ago Mail.php 3 years ago Metrics.php 3 years ago NoAccessException.php 5 years ago Nonce.php 3 years ago Notification.php 5 years ago NumberFormatter.php 4 years ago Option.php 4 years ago Period.php 5 years ago Piwik.php 3 years ago Plugin.php 4 years ago Profiler.php 4 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 3 years ago QuickForm2.php 5 years ago RankingQuery.php 3 years ago ReportRenderer.php 4 years ago Segment.php 3 years ago Sequence.php 5 years ago Session.php 3 years ago SettingsPiwik.php 3 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 3 years ago SiteContentDetector.php 2 years ago SupportedBrowser.php 3 years ago TCPDF.php 5 years ago Theme.php 5 years ago Timer.php 5 years ago Tracker.php 4 years ago Twig.php 4 years ago Unzip.php 5 years ago UpdateCheck.php 5 years ago Updater.php 4 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 2 years ago UrlHelper.php 3 years ago Version.php 2 years ago View.php 3 years ago bootstrap.php 3 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 3 years ago
Segment.php
665 lines
1 <?php
2 /**
3 * Matomo - 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\Cache as PiwikCache;
15 use Piwik\Container\StaticContainer;
16 use Piwik\DataAccess\LogQueryBuilder;
17 use Piwik\Plugins\SegmentEditor\SegmentEditor;
18 use Piwik\Segment\SegmentExpression;
19 use Piwik\Plugins\SegmentEditor\Model as SegmentEditorModel;
20
21 /**
22 * Limits the set of visits Piwik uses when aggregating analytics data.
23 *
24 * A segment is a condition used to filter visits. They can, for example,
25 * select visits that have a specific browser or come from a specific
26 * country, or both.
27 *
28 * Plugins that aggregate data stored in Piwik can support segments by
29 * using this class when generating aggregation SQL queries.
30 *
31 * ### Examples
32 *
33 * **Basic usage**
34 *
35 * $idSites = array(1,2,3);
36 * $segmentStr = "browserCode==ff;countryCode==CA";
37 * $segment = new Segment($segmentStr, $idSites);
38 *
39 * $query = $segment->getSelectQuery(
40 * $select = "table.col1, table2.col2",
41 * $from = array("table", "table2"),
42 * $where = "table.col3 = ?",
43 * $bind = array(5),
44 * $orderBy = "table.col1 DESC",
45 * $groupBy = "table2.col2"
46 * );
47 *
48 * Db::fetchAll($query['sql'], $query['bind']);
49 *
50 * **Creating a _null_ segment**
51 *
52 * $idSites = array(1,2,3);
53 * $segment = new Segment('', $idSites);
54 * // $segment->getSelectQuery will return a query that selects all visits
55 *
56 * @api
57 */
58 class Segment
59 {
60 /**
61 * @var SegmentExpression
62 */
63 protected $segmentExpression = null;
64
65 /**
66 * @var string
67 */
68 protected $string = null;
69
70 /**
71 * @var string
72 */
73 protected $originalString = null;
74
75 /**
76 * @var array
77 */
78 protected $idSites = null;
79
80 /**
81 * @var Date
82 */
83 protected $startDate = null;
84
85 /**
86 * @var Date
87 */
88 protected $endDate = null;
89
90 /**
91 * @var LogQueryBuilder
92 */
93 private $segmentQueryBuilder;
94
95 /**
96 * @var bool
97 */
98 private $isSegmentEncoded;
99
100 /**
101 * Truncate the Segments to 8k
102 */
103 const SEGMENT_TRUNCATE_LIMIT = 8192;
104
105 const CACHE_KEY = 'segmenthashes';
106 const SEGMENT_HAS_BUILT_CACHE_KEY ='segmenthashbuilt';
107
108 /**
109 * Constructor.
110 *
111 * When using segments that contain a != or !@ condition on a non visit dimension (e.g. action, conversion, ...) it
112 * is needed to use a subquery to get correct results. To avoid subqueries that fetch too many data it's required to
113 * set a startDate and/or an endDate in this case. That date will be used to limit the subquery (along with possibly
114 * given idSites). If no startDate and endDate is given for such a segment it will generate a query that directly
115 * joins the according tables, but trigger a php warning as results might be incorrect.
116 *
117 * @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`.
118 * @param array $idSites The list of sites the segment will be used with. Some segments are
119 * dependent on the site, such as goal segments.
120 * @param Date|null $startDate start date used to limit subqueries
121 * @param Date|null $endDate end date used to limit subqueries
122 * @throws
123 */
124 public function __construct($segmentCondition, $idSites, Date $startDate = null, Date $endDate = null)
125 {
126 $this->segmentQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
127
128 $segmentCondition = trim($segmentCondition ?: '');
129 if (!SettingsPiwik::isSegmentationEnabled()
130 && !empty($segmentCondition)
131 ) {
132 throw new Exception("The Super User has disabled the Segmentation feature.");
133 }
134
135 $this->originalString = $segmentCondition;
136
137 if ($startDate instanceof Date) {
138 $this->startDate = $startDate;
139 }
140
141 if ($endDate instanceof Date) {
142 $this->endDate = $endDate;
143 }
144
145 // The segment expression can be urlencoded. Unfortunately, both the encoded and decoded versions
146 // can usually be parsed successfully. To pick the right one, we try both and pick the one w/ more
147 // successfully parsed subexpressions.
148 $subexpressionsDecoded = 0;
149 try {
150 $this->initializeSegment(urldecode($segmentCondition), $idSites);
151 $subexpressionsDecoded = $this->segmentExpression->getSubExpressionCount();
152 } catch (Exception $e) {
153 // ignore
154 }
155
156 $subexpressionsRaw = 0;
157 try {
158 $this->initializeSegment($segmentCondition, $idSites);
159 $subexpressionsRaw = $this->segmentExpression->getSubExpressionCount();
160 } catch (Exception $e) {
161 // ignore
162 }
163
164 if ($subexpressionsRaw > $subexpressionsDecoded) {
165 $this->initializeSegment($segmentCondition, $idSites);
166 $this->isSegmentEncoded = false;
167 } else {
168 $this->initializeSegment(urldecode($segmentCondition), $idSites);
169 $this->isSegmentEncoded = true;
170 }
171 }
172
173 /**
174 * Returns the segment expression.
175 * @return SegmentExpression
176 * @api since Piwik 3.2.0
177 */
178 public function getSegmentExpression()
179 {
180 return $this->segmentExpression;
181 }
182
183 /**
184 * @throws Exception
185 */
186 private function getAvailableSegments()
187 {
188 // start cache
189 $cache = PiwikCache::getTransientCache();
190
191 //covert cache id
192 $cacheId = 'API.getSegmentsMetadata.'.SettingsPiwik::getPiwikInstanceId() . '.' . implode(",", $this->idSites);
193
194 //fetch cache lockId
195 $this->availableSegments = $cache->fetch($cacheId);
196 // segment metadata
197 if (empty($this->availableSegments)) {
198
199 $this->availableSegments = Request::processRequest('API.getSegmentsMetadata', array(
200 'idSites' => $this->idSites,
201 '_hideImplementationData' => 0,
202 'filter_limit' => -1,
203 'filter_offset' => 0,
204 '_showAllSegments' => 1,
205 ), []);
206 $cache->save($cacheId, $this->availableSegments);
207 }
208
209 return $this->availableSegments;
210 }
211
212 private function getSegmentByName($name)
213 {
214 $segments = $this->getAvailableSegments();
215
216 foreach ($segments as $segment) {
217 if ($segment['segment'] == $name && !empty($name)) {
218
219 // check permission
220 if (isset($segment['permission']) && $segment['permission'] != 1) {
221 throw new NoAccessException("You do not have enough permission to access the segment " . $name);
222 }
223
224 return $segment;
225 }
226 }
227
228 throw new Exception("Segment '$name' is not a supported segment.");
229 }
230
231 /**
232 * @param $string
233 * @param $idSites
234 * @throws Exception
235 */
236 protected function initializeSegment($string, $idSites)
237 {
238 // As a preventive measure, we restrict the filter size to a safe limit
239 $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
240
241 $this->string = $string;
242
243 if (empty($idSites)) {
244 $idSites = [];
245 } else if (!is_array($idSites)) {
246 $idSites = [$idSites];
247 }
248 $this->idSites = $idSites;
249 $segment = new SegmentExpression($string);
250 $this->segmentExpression = $segment;
251
252 // parse segments
253 $expressions = $segment->parseSubExpressions();
254 $expressions = $this->getExpressionsWithUnionsResolved($expressions);
255
256 // convert segments name to sql segment
257 // check that user is allowed to view this segment
258 // and apply a filter to the value to match if necessary (to map DB fields format)
259 $cleanedExpressions = array();
260 foreach ($expressions as $expression) {
261 $operand = $expression[SegmentExpression::INDEX_OPERAND];
262 $expression[SegmentExpression::INDEX_OPERAND] = $this->getCleanedExpression($operand);
263 $cleanedExpressions[] = $expression;
264 }
265
266 $segment->setSubExpressionsAfterCleanup($cleanedExpressions);
267 }
268
269 private function getExpressionsWithUnionsResolved($expressions)
270 {
271 $expressionsWithUnions = array();
272 foreach ($expressions as $expression) {
273 $operand = $expression[SegmentExpression::INDEX_OPERAND];
274 $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
275
276 $availableSegment = $this->getSegmentByName($name);
277
278 // We leave segments using !@ and != operands untouched for segments not on log_visit table as they will be build using a subquery
279 if (!$this->doesSegmentNeedSubquery($operand[SegmentExpression::INDEX_OPERAND_OPERATOR], $name) && !empty($availableSegment['unionOfSegments'])) {
280 $count = 0;
281 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
282 $count++;
283 $operator = SegmentExpression::BOOL_OPERATOR_OR; // we connect all segments within that union via OR
284 if ($count === count($availableSegment['unionOfSegments'])) {
285 $operator = $expression[SegmentExpression::INDEX_BOOL_OPERATOR];
286 }
287
288 $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion;
289 $expressionsWithUnions[] = array(
290 SegmentExpression::INDEX_BOOL_OPERATOR => $operator,
291 SegmentExpression::INDEX_OPERAND => $operand
292 );
293 }
294 } else {
295 $expressionsWithUnions[] = array(
296 SegmentExpression::INDEX_BOOL_OPERATOR => $expression[SegmentExpression::INDEX_BOOL_OPERATOR],
297 SegmentExpression::INDEX_OPERAND => $operand
298 );
299 }
300 }
301
302 return $expressionsWithUnions;
303 }
304
305 private function isVisitSegment($name)
306 {
307 $availableSegment = $this->getSegmentByName($name);
308
309 if (!empty($availableSegment['unionOfSegments'])) {
310 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
311 $unionSegment = $this->getSegmentByName($segmentNameOfUnion);
312 if (strpos($unionSegment['sqlSegment'], 'log_visit.') === 0) {
313 return true;
314 }
315 }
316 } else if (strpos($availableSegment['sqlSegment'], 'log_visit.') === 0) {
317 return true;
318 }
319
320 return false;
321 }
322
323 private function doesSegmentNeedSubquery($operator, $segmentName)
324 {
325 $requiresSubQuery = in_array($operator, [
326 SegmentExpression::MATCH_DOES_NOT_CONTAIN,
327 SegmentExpression::MATCH_NOT_EQUAL
328 ]) && !$this->isVisitSegment($segmentName);
329
330 if ($requiresSubQuery && empty($this->startDate) && empty($this->endDate)) {
331 if (Development::isEnabled()) {
332 $e = new Exception();
333 Log::warning("Avoiding segment subquery due to missing start date and/or an end date. Please ensure a start date and/or end date is set when initializing a segment if it's used to build a query. Stacktrace:\n" . $e->getTraceAsString());
334 }
335 return false;
336 }
337
338 return $requiresSubQuery;
339 }
340
341 private function getInvertedOperatorForSubQuery($operator)
342 {
343 if ($operator === SegmentExpression::MATCH_DOES_NOT_CONTAIN) {
344 return SegmentExpression::MATCH_CONTAINS;
345 } else if ($operator === SegmentExpression::MATCH_NOT_EQUAL) {
346 return SegmentExpression::MATCH_EQUAL;
347 }
348
349 throw new Exception("Operator not support for subqueries");
350 }
351
352 /**
353 * Returns `true` if the segment is empty, `false` if otherwise.
354 */
355 public function isEmpty()
356 {
357 return $this->segmentExpression->isEmpty();
358 }
359
360 /**
361 * Detects whether the Piwik instance is configured to be able to archive this segment. It checks whether the segment
362 * will be either archived via browser or cli archiving. It does not check if the segment has been archived. If you
363 * want to know whether the segment has been archived, the actual report data needs to be requested.
364 *
365 * This method does not take any date/period into consideration. Meaning a Piwik instance might be able to archive
366 * this segment in general, but not for a certain period if eg the archiving of range dates is disabled.
367 *
368 * @return bool
369 */
370 public function willBeArchived()
371 {
372 if ($this->isEmpty()) {
373 return true;
374 }
375
376 $idSites = $this->idSites;
377
378 return Rules::isRequestAuthorizedToArchive()
379 || Rules::isBrowserArchivingAvailableForSegments()
380 || Rules::isSegmentPreProcessed($idSites, $this);
381 }
382
383 protected $availableSegments = array();
384
385 protected function getCleanedExpression($expression)
386 {
387 $name = $expression[SegmentExpression::INDEX_OPERAND_NAME];
388 $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR];
389 $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE];
390
391 $segment = $this->getSegmentByName($name);
392 $sqlName = $segment['sqlSegment'];
393
394 // Build subqueries for segments that are not on log_visit table but use !@ or != as operator
395 // This is required to ensure segments like actionUrl!@value really do not include any visit having an action containing `value`
396 if ($this->doesSegmentNeedSubquery($matchType, $name)) {
397 $operator = $this->getInvertedOperatorForSubQuery($matchType);
398 $stringSegment = $name . $operator . $value;
399 $segmentObj = new Segment($stringSegment, $this->idSites, $this->startDate, $this->endDate);
400
401 $select = 'log_visit.idvisit';
402 $from = 'log_visit';
403 $datetimeField = 'visit_last_action_time';
404 $where = [];
405 $bind = [];
406 if (!empty($this->idSites)) {
407 $where[] = "$from.idsite IN (" . Common::getSqlStringFieldsArray($this->idSites) . ")";
408 $bind = $this->idSites;
409 }
410 if ($this->startDate instanceof Date) {
411 $where[] = "$from.$datetimeField >= ?";
412 $bind[] = $this->startDate->toString(Date::DATE_TIME_FORMAT);
413 }
414 if ($this->endDate instanceof Date) {
415 $where[] = "$from.$datetimeField <= ?";
416 $bind[] = $this->endDate->toString(Date::DATE_TIME_FORMAT);
417 }
418
419 $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
420 $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
421 $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
422 $query = $segmentObj->getSelectQuery($select, $from, implode(' AND ', $where), $bind);
423 $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
424
425 return ['log_visit.idvisit', SegmentExpression::MATCH_ACTIONS_NOT_CONTAINS, $query];
426 }
427
428 if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY
429 && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
430
431 if (isset($segment['sqlFilterValue'])) {
432 $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
433 }
434
435 // apply presentation filter
436 if (isset($segment['sqlFilter'])) {
437 $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
438
439 if(is_null($value)) { // null is returned in TableLogAction::getIdActionFromSegment()
440 return array(null, $matchType, null);
441 }
442
443 // sqlFilter-callbacks might return arrays for more complex cases
444 // e.g. see TableLogAction::getIdActionFromSegment()
445 if (is_array($value) && isset($value['SQL'])) {
446 // Special case: returned value is a sub sql expression!
447 $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS;
448 }
449 }
450 }
451
452 return array($sqlName, $matchType, $value);
453 }
454
455 /**
456 * Returns the segment condition.
457 *
458 * @return string
459 */
460 public function getString()
461 {
462 return $this->string;
463 }
464
465 /**
466 * Returns a hash of the segment condition, or the empty string if the segment
467 * condition is empty.
468 *
469 * @return string
470 */
471 public function getHash()
472 {
473 if (empty($this->string)) {
474 return '';
475 }
476 return self::getSegmentHash($this->string);
477 }
478
479 public static function getSegmentHash($definition)
480 {
481 $cache = Cache::getEagerCache();
482 $cacheKey = self::CACHE_KEY . md5($definition);
483
484 if ($cache->contains($cacheKey)) {
485 return $cache->fetch($cacheKey);
486 }
487
488 $defaultHash = md5(urldecode($definition));
489
490 // if the cache for segments already built, but this segment was not found,
491 // we return the default segment, this can be a segment from url or
492 // something like "visitorType==new"
493 if ($cache->contains(self::SEGMENT_HAS_BUILT_CACHE_KEY)) {
494 return $defaultHash;
495 }
496
497 // the segment hash is not built yet, let's do it
498 $model = new SegmentEditorModel();
499 $segments = $model->getAllSegmentsAndIgnoreVisibility();
500
501 foreach ($segments as $segment) {
502 $cacheKeyTemp = self::CACHE_KEY . md5($segment['definition']);
503 $cache->save($cacheKeyTemp, $segment['hash']);
504
505 $cacheKeyTemp = self::CACHE_KEY . md5(urldecode($segment['definition']));
506 $cache->save($cacheKeyTemp, $segment['hash']);
507
508 $cacheKeyTemp = self::CACHE_KEY . md5(urlencode($segment['definition']));
509 $cache->save($cacheKeyTemp, $segment['hash']);
510 }
511
512 $cache->save(self::SEGMENT_HAS_BUILT_CACHE_KEY, true);
513
514 // if we found the segment, return it's hash, but maybe this
515 // segment is not stored in the db, return the default
516 if ($cache->contains($cacheKey)) {
517 return $cache->fetch($cacheKey);
518 }
519
520 return $defaultHash;
521 }
522
523 /**
524 * Extend an SQL query that aggregates data over one of the 'log_' tables with segment expressions.
525 *
526 * @param string $select The select clause. Should NOT include the **SELECT** just the columns, eg,
527 * `'t1.col1 as col1, t2.col2 as col2'`.
528 * @param array|string $from Array of table names (without prefix), eg, `array('log_visit', 'log_conversion')`.
529 * @param false|string $where (optional) Where clause, eg, `'t1.col1 = ? AND t2.col2 = ?'`.
530 * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
531 * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
532 * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
533 * @param int $limit Limit number of result to $limit
534 * @param int $offset Specified the offset of the first row to return
535 * @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
536 * A $groupBy value needs to be set for this to work.
537 * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
538 * Use only when you're not aggregating or it will sample the data.
539 * @return array The entire select query.
540 */
541 public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0, $forceGroupBy = false)
542 {
543 $segmentExpression = $this->segmentExpression;
544
545 $limitAndOffset = null;
546 if($limit > 0) {
547 $limitAndOffset = (int) $offset . ', ' . (int) $limit;
548 }
549
550 try {
551 if ($forceGroupBy && $groupBy) {
552 $this->segmentQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
553 }
554 $result = $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind,
555 $groupBy, $orderBy, $limitAndOffset);
556 } catch (Exception $e) {
557 if ($forceGroupBy && $groupBy) {
558 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
559 }
560 throw $e;
561 }
562
563 if ($forceGroupBy && $groupBy) {
564 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
565 }
566 return $result;
567 }
568
569 /**
570 * Returns the segment string.
571 *
572 * @return string
573 */
574 public function __toString()
575 {
576 return (string) $this->getString();
577 }
578
579 /**
580 * Combines this segment with another segment condition, if the segment condition is not already
581 * in the segment.
582 *
583 * The combination is naive in that it does not take order of operations into account.
584 *
585 * @param string $segment
586 * @param string $operator The operator to use. Should be either SegmentExpression::AND_DELIMITER
587 * or SegmentExpression::OR_DELIMITER.
588 * @param string $segmentCondition The segment condition to add.
589 * @return string
590 * @throws Exception
591 */
592 public static function combine($segment, $operator, $segmentCondition)
593 {
594 if (empty($segment)) {
595 return $segmentCondition;
596 }
597
598 if (empty($segmentCondition)
599 || self::containsCondition($segment, $operator, $segmentCondition)
600 ) {
601 return $segment;
602 }
603
604 return $segment . $operator . $segmentCondition;
605 }
606
607 private static function containsCondition($segment, $operator, $segmentCondition)
608 {
609 // check when segment/condition are of same encoding
610 return strpos($segment, $operator . $segmentCondition) !== false
611 || strpos($segment, $segmentCondition . $operator) !== false
612
613 // check when both operator & condition are urlencoded in $segment
614 || strpos($segment, urlencode($operator . $segmentCondition)) !== false
615 || strpos($segment, urlencode($segmentCondition . $operator)) !== false
616
617 // check when operator is not urlencoded, but condition is in $segment
618 || strpos($segment, $operator . urlencode($segmentCondition)) !== false
619 || strpos($segment, urlencode($segmentCondition) . $operator) !== false
620
621 // check when segment condition is urlencoded & $segment isn't
622 || strpos($segment, $operator . urldecode($segmentCondition)) !== false
623 || strpos($segment, urldecode($segmentCondition) . $operator) !== false
624
625 || $segment === $segmentCondition
626 || $segment === urlencode($segmentCondition)
627 || $segment === urldecode($segmentCondition);
628 }
629
630 public function getStoredSegmentName($idSite)
631 {
632 $segment = $this->getString();
633 if (empty($segment)) {
634 return Piwik::translate('SegmentEditor_DefaultAllVisits');
635 }
636
637 $availableSegments = SegmentEditor::getAllSegmentsForSite($idSite);
638
639 $foundStoredSegment = null;
640 foreach ($availableSegments as $storedSegment) {
641 if ($storedSegment['definition'] == $segment
642 || $storedSegment['definition'] == urldecode($segment)
643 || $storedSegment['definition'] == urlencode($segment)
644
645 || $storedSegment['definition'] == $this->originalString
646 || $storedSegment['definition'] == urldecode($this->originalString)
647 || $storedSegment['definition'] == urlencode($this->originalString)
648 ) {
649 $foundStoredSegment = $storedSegment;
650 }
651 }
652
653 if (isset($foundStoredSegment)) {
654 return $foundStoredSegment['name'];
655 }
656
657 return $this->isSegmentEncoded ? urldecode($segment) : $segment;
658 }
659
660 public function getOriginalString()
661 {
662 return $this->originalString;
663 }
664 }
665