PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.3.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.3.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 5 years ago Access 5 years ago Application 5 years ago Archive 5 years ago ArchiveProcessor 5 years ago Archiver 5 years ago AssetManager 5 years ago Auth 5 years ago Category 5 years ago CliMulti 5 years ago Columns 5 years ago Composer 5 years ago Concurrency 5 years ago Config 5 years ago Container 5 years ago CronArchive 5 years ago DataAccess 5 years ago DataFiles 5 years ago DataTable 5 years ago Db 5 years ago DeviceDetector 5 years ago Email 5 years ago Exception 5 years ago Http 5 years ago Intl 5 years ago Mail 5 years ago Measurable 5 years ago Menu 5 years ago Metrics 5 years ago Notification 5 years ago Period 5 years ago Plugin 5 years ago ProfessionalServices 5 years ago Report 5 years ago ReportRenderer 5 years ago Scheduler 5 years ago Segment 5 years ago Session 5 years ago Settings 5 years ago Tracker 5 years ago Translation 5 years ago UpdateCheck 5 years ago Updater 5 years ago Updates 5 years ago Validators 5 years ago View 5 years ago ViewDataTable 5 years ago Visualization 5 years ago Widget 5 years ago .htaccess 6 years ago Access.php 5 years ago Archive.php 5 years ago ArchiveProcessor.php 5 years ago AssetManager.php 5 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 5 years ago Common.php 5 years ago Config.php 5 years ago Console.php 5 years ago Context.php 5 years ago Cookie.php 5 years ago CronArchive.php 5 years ago DataArray.php 5 years ago DataTable.php 5 years ago Date.php 5 years ago Db.php 5 years ago DbHelper.php 5 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 5 years ago ExceptionHandler.php 5 years ago FileIntegrity.php 5 years ago Filechecks.php 5 years ago Filesystem.php 5 years ago FrontController.php 5 years ago Http.php 5 years ago IP.php 5 years ago Log.php 5 years ago LogDeleter.php 5 years ago Mail.php 5 years ago Metrics.php 5 years ago NoAccessException.php 5 years ago Nonce.php 5 years ago Notification.php 5 years ago NumberFormatter.php 5 years ago Option.php 5 years ago Period.php 5 years ago Piwik.php 5 years ago Plugin.php 5 years ago Profiler.php 5 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 5 years ago QuickForm2.php 5 years ago RankingQuery.php 5 years ago ReportRenderer.php 5 years ago Segment.php 5 years ago Sequence.php 5 years ago Session.php 5 years ago SettingsPiwik.php 5 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 5 years ago SupportedBrowser.php 5 years ago TCPDF.php 5 years ago Theme.php 5 years ago Timer.php 5 years ago Tracker.php 5 years ago Twig.php 5 years ago Unzip.php 5 years ago UpdateCheck.php 5 years ago Updater.php 5 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 5 years ago UrlHelper.php 5 years ago Version.php 5 years ago View.php 5 years ago bootstrap.php 5 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 5 years ago
Segment.php
652 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\Container\StaticContainer;
15 use Piwik\DataAccess\LogQueryBuilder;
16 use Piwik\Plugins\SegmentEditor\SegmentEditor;
17 use Piwik\Segment\SegmentExpression;
18 use Piwik\Plugins\SegmentEditor\Model as SegmentEditorModel;
19 use Piwik\Cache;
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 private function getAvailableSegments()
184 {
185 // segment metadata
186 if (empty($this->availableSegments)) {
187 $this->availableSegments = Request::processRequest('API.getSegmentsMetadata', array(
188 'idSites' => $this->idSites,
189 '_hideImplementationData' => 0,
190 'filter_limit' => -1,
191 'filter_offset' => 0,
192 '_showAllSegments' => 1,
193 ), []);
194 }
195
196 return $this->availableSegments;
197 }
198
199 private function getSegmentByName($name)
200 {
201 $segments = $this->getAvailableSegments();
202
203 foreach ($segments as $segment) {
204 if ($segment['segment'] == $name && !empty($name)) {
205
206 // check permission
207 if (isset($segment['permission']) && $segment['permission'] != 1) {
208 throw new NoAccessException("You do not have enough permission to access the segment " . $name);
209 }
210
211 return $segment;
212 }
213 }
214
215 throw new Exception("Segment '$name' is not a supported segment.");
216 }
217
218 /**
219 * @param $string
220 * @param $idSites
221 * @throws Exception
222 */
223 protected function initializeSegment($string, $idSites)
224 {
225 // As a preventive measure, we restrict the filter size to a safe limit
226 $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT);
227
228 $this->string = $string;
229
230 if (empty($idSites)) {
231 $idSites = [];
232 } else if (!is_array($idSites)) {
233 $idSites = [$idSites];
234 }
235 $this->idSites = $idSites;
236 $segment = new SegmentExpression($string);
237 $this->segmentExpression = $segment;
238
239 // parse segments
240 $expressions = $segment->parseSubExpressions();
241 $expressions = $this->getExpressionsWithUnionsResolved($expressions);
242
243 // convert segments name to sql segment
244 // check that user is allowed to view this segment
245 // and apply a filter to the value to match if necessary (to map DB fields format)
246 $cleanedExpressions = array();
247 foreach ($expressions as $expression) {
248 $operand = $expression[SegmentExpression::INDEX_OPERAND];
249 $expression[SegmentExpression::INDEX_OPERAND] = $this->getCleanedExpression($operand);
250 $cleanedExpressions[] = $expression;
251 }
252
253 $segment->setSubExpressionsAfterCleanup($cleanedExpressions);
254 }
255
256 private function getExpressionsWithUnionsResolved($expressions)
257 {
258 $expressionsWithUnions = array();
259 foreach ($expressions as $expression) {
260 $operand = $expression[SegmentExpression::INDEX_OPERAND];
261 $name = $operand[SegmentExpression::INDEX_OPERAND_NAME];
262
263 $availableSegment = $this->getSegmentByName($name);
264
265 // We leave segments using !@ and != operands untouched for segments not on log_visit table as they will be build using a subquery
266 if (!$this->doesSegmentNeedSubquery($operand[SegmentExpression::INDEX_OPERAND_OPERATOR], $name) && !empty($availableSegment['unionOfSegments'])) {
267 $count = 0;
268 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
269 $count++;
270 $operator = SegmentExpression::BOOL_OPERATOR_OR; // we connect all segments within that union via OR
271 if ($count === count($availableSegment['unionOfSegments'])) {
272 $operator = $expression[SegmentExpression::INDEX_BOOL_OPERATOR];
273 }
274
275 $operand[SegmentExpression::INDEX_OPERAND_NAME] = $segmentNameOfUnion;
276 $expressionsWithUnions[] = array(
277 SegmentExpression::INDEX_BOOL_OPERATOR => $operator,
278 SegmentExpression::INDEX_OPERAND => $operand
279 );
280 }
281 } else {
282 $expressionsWithUnions[] = array(
283 SegmentExpression::INDEX_BOOL_OPERATOR => $expression[SegmentExpression::INDEX_BOOL_OPERATOR],
284 SegmentExpression::INDEX_OPERAND => $operand
285 );
286 }
287 }
288
289 return $expressionsWithUnions;
290 }
291
292 private function isVisitSegment($name)
293 {
294 $availableSegment = $this->getSegmentByName($name);
295
296 if (!empty($availableSegment['unionOfSegments'])) {
297 foreach ($availableSegment['unionOfSegments'] as $segmentNameOfUnion) {
298 $unionSegment = $this->getSegmentByName($segmentNameOfUnion);
299 if (strpos($unionSegment['sqlSegment'], 'log_visit.') === 0) {
300 return true;
301 }
302 }
303 } else if (strpos($availableSegment['sqlSegment'], 'log_visit.') === 0) {
304 return true;
305 }
306
307 return false;
308 }
309
310 private function doesSegmentNeedSubquery($operator, $segmentName)
311 {
312 $requiresSubQuery = in_array($operator, [
313 SegmentExpression::MATCH_DOES_NOT_CONTAIN,
314 SegmentExpression::MATCH_NOT_EQUAL
315 ]) && !$this->isVisitSegment($segmentName);
316
317 if ($requiresSubQuery && empty($this->startDate) && empty($this->endDate)) {
318 if (Development::isEnabled()) {
319 $e = new Exception();
320 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());
321 }
322 return false;
323 }
324
325 return $requiresSubQuery;
326 }
327
328 private function getInvertedOperatorForSubQuery($operator)
329 {
330 if ($operator === SegmentExpression::MATCH_DOES_NOT_CONTAIN) {
331 return SegmentExpression::MATCH_CONTAINS;
332 } else if ($operator === SegmentExpression::MATCH_NOT_EQUAL) {
333 return SegmentExpression::MATCH_EQUAL;
334 }
335
336 throw new Exception("Operator not support for subqueries");
337 }
338
339 /**
340 * Returns `true` if the segment is empty, `false` if otherwise.
341 */
342 public function isEmpty()
343 {
344 return $this->segmentExpression->isEmpty();
345 }
346
347 /**
348 * Detects whether the Piwik instance is configured to be able to archive this segment. It checks whether the segment
349 * will be either archived via browser or cli archiving. It does not check if the segment has been archived. If you
350 * want to know whether the segment has been archived, the actual report data needs to be requested.
351 *
352 * This method does not take any date/period into consideration. Meaning a Piwik instance might be able to archive
353 * this segment in general, but not for a certain period if eg the archiving of range dates is disabled.
354 *
355 * @return bool
356 */
357 public function willBeArchived()
358 {
359 if ($this->isEmpty()) {
360 return true;
361 }
362
363 $idSites = $this->idSites;
364
365 return Rules::isRequestAuthorizedToArchive()
366 || Rules::isBrowserArchivingAvailableForSegments()
367 || Rules::isSegmentPreProcessed($idSites, $this);
368 }
369
370 protected $availableSegments = array();
371
372 protected function getCleanedExpression($expression)
373 {
374 $name = $expression[SegmentExpression::INDEX_OPERAND_NAME];
375 $matchType = $expression[SegmentExpression::INDEX_OPERAND_OPERATOR];
376 $value = $expression[SegmentExpression::INDEX_OPERAND_VALUE];
377
378 $segment = $this->getSegmentByName($name);
379 $sqlName = $segment['sqlSegment'];
380
381 // Build subqueries for segments that are not on log_visit table but use !@ or != as operator
382 // This is required to ensure segments like actionUrl!@value really do not include any visit having an action containing `value`
383 if ($this->doesSegmentNeedSubquery($matchType, $name)) {
384 $operator = $this->getInvertedOperatorForSubQuery($matchType);
385 $stringSegment = $name . $operator . $value;
386 $segmentObj = new Segment($stringSegment, $this->idSites, $this->startDate, $this->endDate);
387
388 $select = 'log_visit.idvisit';
389 $from = 'log_visit';
390 $datetimeField = 'visit_last_action_time';
391 $where = [];
392 $bind = [];
393 if (!empty($this->idSites)) {
394 $where[] = "$from.idsite IN (" . Common::getSqlStringFieldsArray($this->idSites) . ")";
395 $bind = $this->idSites;
396 }
397 if ($this->startDate instanceof Date) {
398 $where[] = "$from.$datetimeField >= ?";
399 $bind[] = $this->startDate->toString(Date::DATE_TIME_FORMAT);
400 }
401 if ($this->endDate instanceof Date) {
402 $where[] = "$from.$datetimeField <= ?";
403 $bind[] = $this->endDate->toString(Date::DATE_TIME_FORMAT);
404 }
405
406 $logQueryBuilder = StaticContainer::get('Piwik\DataAccess\LogQueryBuilder');
407 $forceGroupByBackup = $logQueryBuilder->getForcedInnerGroupBySubselect();
408 $logQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
409 $query = $segmentObj->getSelectQuery($select, $from, implode(' AND ', $where), $bind);
410 $logQueryBuilder->forceInnerGroupBySubselect($forceGroupByBackup);
411
412 return ['log_visit.idvisit', SegmentExpression::MATCH_ACTIONS_NOT_CONTAINS, $query];
413 }
414
415 if ($matchType != SegmentExpression::MATCH_IS_NOT_NULL_NOR_EMPTY
416 && $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
417
418 if (isset($segment['sqlFilterValue'])) {
419 $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
420 }
421
422 // apply presentation filter
423 if (isset($segment['sqlFilter'])) {
424 $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType, $name);
425
426 if(is_null($value)) { // null is returned in TableLogAction::getIdActionFromSegment()
427 return array(null, $matchType, null);
428 }
429
430 // sqlFilter-callbacks might return arrays for more complex cases
431 // e.g. see TableLogAction::getIdActionFromSegment()
432 if (is_array($value) && isset($value['SQL'])) {
433 // Special case: returned value is a sub sql expression!
434 $matchType = SegmentExpression::MATCH_ACTIONS_CONTAINS;
435 }
436 }
437 }
438
439 return array($sqlName, $matchType, $value);
440 }
441
442 /**
443 * Returns the segment condition.
444 *
445 * @return string
446 */
447 public function getString()
448 {
449 return $this->string;
450 }
451
452 /**
453 * Returns a hash of the segment condition, or the empty string if the segment
454 * condition is empty.
455 *
456 * @return string
457 */
458 public function getHash()
459 {
460 if (empty($this->string)) {
461 return '';
462 }
463 return self::getSegmentHash($this->string);
464 }
465
466 public static function getSegmentHash($definition)
467 {
468 $cache = Cache::getEagerCache();
469 $cacheKey = self::CACHE_KEY . md5($definition);
470
471 if ($cache->contains($cacheKey)) {
472 return $cache->fetch($cacheKey);
473 }
474
475 $defaultHash = md5(urldecode($definition));
476
477 // if the cache for segments already built, but this segment was not found,
478 // we return the default segment, this can be a segment from url or
479 // something like "visitorType==new"
480 if ($cache->contains(self::SEGMENT_HAS_BUILT_CACHE_KEY)) {
481 return $defaultHash;
482 }
483
484 // the segment hash is not built yet, let's do it
485 $model = new SegmentEditorModel();
486 $segments = $model->getAllSegmentsAndIgnoreVisibility();
487
488 foreach ($segments as $segment) {
489 $cacheKeyTemp = self::CACHE_KEY . md5($segment['definition']);
490 $cache->save($cacheKeyTemp, $segment['hash']);
491
492 $cacheKeyTemp = self::CACHE_KEY . md5(urldecode($segment['definition']));
493 $cache->save($cacheKeyTemp, $segment['hash']);
494
495 $cacheKeyTemp = self::CACHE_KEY . md5(urlencode($segment['definition']));
496 $cache->save($cacheKeyTemp, $segment['hash']);
497 }
498
499 $cache->save(self::SEGMENT_HAS_BUILT_CACHE_KEY, true);
500
501 // if we found the segment, return it's hash, but maybe this
502 // segment is not stored in the db, return the default
503 if ($cache->contains($cacheKey)) {
504 return $cache->fetch($cacheKey);
505 }
506
507 return $defaultHash;
508 }
509
510 /**
511 * Extend an SQL query that aggregates data over one of the 'log_' tables with segment expressions.
512 *
513 * @param string $select The select clause. Should NOT include the **SELECT** just the columns, eg,
514 * `'t1.col1 as col1, t2.col2 as col2'`.
515 * @param array|string $from Array of table names (without prefix), eg, `array('log_visit', 'log_conversion')`.
516 * @param false|string $where (optional) Where clause, eg, `'t1.col1 = ? AND t2.col2 = ?'`.
517 * @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
518 * @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
519 * @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
520 * @param int $limit Limit number of result to $limit
521 * @param int $offset Specified the offset of the first row to return
522 * @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
523 * A $groupBy value needs to be set for this to work.
524 * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
525 * Use only when you're not aggregating or it will sample the data.
526 * @return string The entire select query.
527 */
528 public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0, $offset = 0, $forceGroupBy = false)
529 {
530 $segmentExpression = $this->segmentExpression;
531
532 $limitAndOffset = null;
533 if($limit > 0) {
534 $limitAndOffset = (int) $offset . ', ' . (int) $limit;
535 }
536
537 try {
538 if ($forceGroupBy && $groupBy) {
539 $this->segmentQueryBuilder->forceInnerGroupBySubselect(LogQueryBuilder::FORCE_INNER_GROUP_BY_NO_SUBSELECT);
540 }
541 $result = $this->segmentQueryBuilder->getSelectQueryString($segmentExpression, $select, $from, $where, $bind,
542 $groupBy, $orderBy, $limitAndOffset);
543 } catch (Exception $e) {
544 if ($forceGroupBy && $groupBy) {
545 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
546 }
547 throw $e;
548 }
549
550 if ($forceGroupBy && $groupBy) {
551 $this->segmentQueryBuilder->forceInnerGroupBySubselect('');
552 }
553 return $result;
554 }
555
556 /**
557 * Returns the segment string.
558 *
559 * @return string
560 */
561 public function __toString()
562 {
563 return (string) $this->getString();
564 }
565
566 /**
567 * Combines this segment with another segment condition, if the segment condition is not already
568 * in the segment.
569 *
570 * The combination is naive in that it does not take order of operations into account.
571 *
572 * @param string $segment
573 * @param string $operator The operator to use. Should be either SegmentExpression::AND_DELIMITER
574 * or SegmentExpression::OR_DELIMITER.
575 * @param string $segmentCondition The segment condition to add.
576 * @return string
577 * @throws Exception
578 */
579 public static function combine($segment, $operator, $segmentCondition)
580 {
581 if (empty($segment)) {
582 return $segmentCondition;
583 }
584
585 if (empty($segmentCondition)
586 || self::containsCondition($segment, $operator, $segmentCondition)
587 ) {
588 return $segment;
589 }
590
591 return $segment . $operator . $segmentCondition;
592 }
593
594 private static function containsCondition($segment, $operator, $segmentCondition)
595 {
596 // check when segment/condition are of same encoding
597 return strpos($segment, $operator . $segmentCondition) !== false
598 || strpos($segment, $segmentCondition . $operator) !== false
599
600 // check when both operator & condition are urlencoded in $segment
601 || strpos($segment, urlencode($operator . $segmentCondition)) !== false
602 || strpos($segment, urlencode($segmentCondition . $operator)) !== false
603
604 // check when operator is not urlencoded, but condition is in $segment
605 || strpos($segment, $operator . urlencode($segmentCondition)) !== false
606 || strpos($segment, urlencode($segmentCondition) . $operator) !== false
607
608 // check when segment condition is urlencoded & $segment isn't
609 || strpos($segment, $operator . urldecode($segmentCondition)) !== false
610 || strpos($segment, urldecode($segmentCondition) . $operator) !== false
611
612 || $segment === $segmentCondition
613 || $segment === urlencode($segmentCondition)
614 || $segment === urldecode($segmentCondition);
615 }
616
617 public function getStoredSegmentName($idSite)
618 {
619 $segment = $this->getString();
620 if (empty($segment)) {
621 return Piwik::translate('SegmentEditor_DefaultAllVisits');
622 }
623
624 $availableSegments = SegmentEditor::getAllSegmentsForSite($idSite);
625
626 $foundStoredSegment = null;
627 foreach ($availableSegments as $storedSegment) {
628 if ($storedSegment['definition'] == $segment
629 || $storedSegment['definition'] == urldecode($segment)
630 || $storedSegment['definition'] == urlencode($segment)
631
632 || $storedSegment['definition'] == $this->originalString
633 || $storedSegment['definition'] == urldecode($this->originalString)
634 || $storedSegment['definition'] == urlencode($this->originalString)
635 ) {
636 $foundStoredSegment = $storedSegment;
637 }
638 }
639
640 if (isset($foundStoredSegment)) {
641 return $foundStoredSegment['name'];
642 }
643
644 return $this->isSegmentEncoded ? urldecode($segment) : $segment;
645 }
646
647 public function getOriginalString()
648 {
649 return $this->originalString;
650 }
651 }
652