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