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 */
9namespace Piwik\Plugin;
10use Exception;
11use Piwik\Development;
12
13/**
14 * Creates a new segment that can be used for instance within the {@link \Piwik\Columns\Dimension::configureSegment()}
15 * method. Make sure to set at least the following values: {@link setName()}, {@link setSegment()},
16 * {@link setSqlSegment()}, {@link setType()} and {@link setCategory()}. If you are using a segment in the context of a
17 * dimension the type and the SQL segment is usually set for you automatically.
18 *
19 * Example:
20 * ```
21 $segment = new \Piwik\Plugin\Segment();
22 $segment->setType(\Piwik\Plugin\Segment::TYPE_DIMENSION);
23 $segment->setName('General_EntryKeyword');
24 $segment->setCategory('General_Visit');
25 $segment->setSegment('entryKeyword');
26 $segment->setSqlSegment('log_visit.entry_keyword');
27 $segment->setAcceptedValues('Any keywords people search for on your website such as "help" or "imprint"');
28 ```
29 * @api
30 * @since 2.5.0
31 */
32class Segment
33{
34    /**
35     * Segment type 'dimension'. Can be used along with {@link setType()}.
36     * @api
37     */
38    const TYPE_DIMENSION = 'dimension';
39
40    /**
41     * Segment type 'metric'. Can be used along with {@link setType()}.
42     * @api
43     */
44    const TYPE_METRIC = 'metric';
45
46    private $type;
47    private $category;
48    private $name;
49    private $segment;
50    private $sqlSegment;
51    private $sqlFilter;
52    private $sqlFilterValue;
53    private $acceptValues;
54    private $permission;
55    private $suggestedValuesCallback;
56    private $unionOfSegments;
57    private $isInternalSegment = false;
58    private $suggestedValuesApi = '';
59
60    /**
61     * If true, this segment will only be visible to a registered user (see API.getSegmentsMetadata).
62     *
63     * @var bool
64     */
65    private $requiresRegisteredUser = false;
66
67    /**
68     * @ignore
69     */
70    final public function __construct()
71    {
72        $this->init();
73    }
74
75    /**
76     * Here you can initialize this segment and set any default values. It is called directly after the object is
77     * created.
78     * @api
79     */
80    protected function init()
81    {
82    }
83
84    /**
85     * Here you should explain which values are accepted/useful for your segment, for example:
86     * "1, 2, 3, etc." or "comcast.net, proxad.net, etc.". If the value needs any special encoding you should mention
87     * this as well. For example "Any URL including protocol. The URL must be URL encoded."
88     *
89     * @param string $acceptedValues
90     * @api
91     */
92    public function setAcceptedValues($acceptedValues)
93    {
94        $this->acceptValues = $acceptedValues;
95    }
96
97    /**
98     * Set (overwrite) the category this segment belongs to. It should be a translation key such as 'General_Actions'
99     * or 'General_Visit'.
100     * @param string $category
101     * @api
102     */
103    public function setCategory($category)
104    {
105        $this->category = $category;
106    }
107
108    /**
109     * Set (overwrite) the segment display name. This name will be visible in the API and the UI. It should be a
110     * translation key such as 'Actions_ColumnEntryPageTitle' or 'Resolution_ColumnResolution'.
111     * @param string $name
112     * @api
113     */
114    public function setName($name)
115    {
116        $this->name = $name;
117    }
118
119    /**
120     * Set (overwrite) the name of the segment. The name should be lower case first and has to be unique. The segment
121     * name defined here needs to be set in the URL to actually apply this segment. Eg if the segment is 'searches'
122     * you need to set "&segment=searches>0" in the UI.
123     * @param string $segment
124     * @api
125     */
126    public function setSegment($segment)
127    {
128        $this->segment = $segment;
129        $this->check();
130    }
131
132    /**
133     * Sometimes you want users to set values that differ from the way they are actually stored. For instance if you
134     * want to allow to filter by any URL than you might have to resolve this URL to an action id. Or a country name
135     * maybe has to be mapped to a 2 letter country code. You can do this by specifying either a callable such as
136     * `array('Classname', 'methodName')` or by passing a closure. There will be four values passed to the given closure
137     * or callable: `string $valueToMatch`, `string $segment` (see {@link setSegment()}), `string $matchType`
138     * (eg SegmentExpression::MATCH_EQUAL or any other match constant of this class) and `$segmentName`.
139     *
140     * If the closure returns NULL, then Piwik assumes the segment sub-string will not match any visitor.
141     *
142     * @param string|\Closure $sqlFilter
143     * @api
144     */
145    public function setSqlFilter($sqlFilter)
146    {
147        $this->sqlFilter = $sqlFilter;
148    }
149
150    /**
151     * Similar to {@link setSqlFilter()} you can map a given segment value to another value. For instance you could map
152     * "new" to 0, 'returning' to 1 and any other value to '2'. You can either define a callable or a closure. There
153     * will be only one value passed to the closure or callable which contains the value a user has set for this
154     * segment. This callback is called shortly before {@link setSqlFilter()}.
155     * @param string|array $sqlFilterValue
156     * @api
157     */
158    public function setSqlFilterValue($sqlFilterValue)
159    {
160        $this->sqlFilterValue = $sqlFilterValue;
161    }
162
163    /**
164     * Defines to which column in the MySQL database the segment belongs: 'mytablename.mycolumnname'. Eg
165     * 'log_visit.idsite'. When a segment is applied the given or filtered value will be compared with this column.
166     *
167     * @param string $sqlSegment
168     * @api
169     */
170    public function setSqlSegment($sqlSegment)
171    {
172        $this->sqlSegment = $sqlSegment;
173        $this->check();
174    }
175
176    /**
177     * Set a list of segments that should be used instead of fetching the values from a single column.
178     * All set segments will be applied via an OR operator.
179     *
180     * @param array $segments
181     * @api
182     */
183    public function setUnionOfSegments($segments)
184    {
185        $this->unionOfSegments = $segments;
186        $this->check();
187    }
188
189    /**
190     * @return array
191     * @ignore
192     */
193    public function getUnionOfSegments()
194    {
195        return $this->unionOfSegments;
196    }
197
198    /**
199     * @return string
200     * @ignore
201     */
202    public function getSqlSegment()
203    {
204        return $this->sqlSegment;
205    }
206
207    /**
208     * @return string
209     * @ignore
210     */
211    public function getSqlFilterValue()
212    {
213        return $this->sqlFilterValue;
214    }
215
216    /**
217     * @return string
218     * @ignore
219     */
220    public function getAcceptValues()
221    {
222        return $this->acceptValues;
223    }
224
225    /**
226     * @return string
227     * @ignore
228     */
229    public function getSqlFilter()
230    {
231        return $this->sqlFilter;
232    }
233
234    /**
235     * Set (overwrite) the type of this segment which is usually either a 'dimension' or a 'metric'.
236     * @param string $type See constansts TYPE_*
237     * @api
238     */
239    public function setType($type)
240    {
241        $this->type = $type;
242    }
243
244    /**
245     * @return string
246     * @ignore
247     */
248    public function getType()
249    {
250        return $this->type;
251    }
252
253    /**
254     * @return string
255     * @ignore
256     */
257    public function getName()
258    {
259        return $this->name;
260    }
261
262    /**
263     * @return string
264     * @ignore
265     */
266    public function getCategoryId()
267    {
268        return $this->category;
269    }
270
271    /**
272     * Returns the name of this segment as it should appear in segment expressions.
273     *
274     * @return string
275     */
276    public function getSegment()
277    {
278        return $this->segment;
279    }
280
281    /**
282     * @return string
283     * @ignore
284     */
285    public function getSuggestedValuesCallback()
286    {
287        return $this->suggestedValuesCallback;
288    }
289
290    /**
291     * Set callback which will be executed when user will call for suggested values for segment.
292     *
293     * @param callable $suggestedValuesCallback
294     */
295    public function setSuggestedValuesCallback($suggestedValuesCallback)
296    {
297        $this->suggestedValuesCallback = $suggestedValuesCallback;
298    }
299
300    /**
301     * @return string
302     * @ignore
303     */
304    public function getSuggestedValuesApi()
305    {
306        return $this->suggestedValuesApi;
307    }
308
309    /**
310     * Set callback which will be executed when user will call for suggested values for segment.
311     *
312     * @param string $suggestedValuesApi
313     */
314    public function setSuggestedValuesApi($suggestedValuesApi)
315    {
316        if (!empty($suggestedValuesApi) && is_string($suggestedValuesApi)) {
317            if (Development::isEnabled() && strpos($suggestedValuesApi, '.get') === false) {
318                throw new Exception('Invalid suggested values API defined, expecting ".get" to be present.');
319            }
320        } else {
321            $suggestedValuesApi = '';
322        }
323        $this->suggestedValuesApi = $suggestedValuesApi;
324    }
325
326    /**
327     * You can restrict the access to this segment by passing a boolean `false`. For instance if you want to make
328     * a certain segment only available to users having super user access you could do the following:
329     * `$segment->setPermission(Piwik::hasUserSuperUserAccess());`
330     * @param bool $permission
331     * @api
332     */
333    public function setPermission($permission)
334    {
335        $this->permission = $permission;
336    }
337
338    /**
339     * @return array
340     * @ignore
341     */
342    public function toArray()
343    {
344        $segment = array(
345            'type'       => $this->type,
346            'category'   => $this->category,
347            'name'       => $this->name,
348            'segment'    => $this->segment,
349            'sqlSegment' => $this->sqlSegment,
350        );
351
352        if (!empty($this->unionOfSegments)) {
353            $segment['unionOfSegments'] = $this->unionOfSegments;
354        }
355
356        if (!empty($this->sqlFilter)) {
357            $segment['sqlFilter'] = $this->sqlFilter;
358        }
359
360        if (!empty($this->sqlFilterValue)) {
361            $segment['sqlFilterValue'] = $this->sqlFilterValue;
362        }
363
364        if (!empty($this->acceptValues)) {
365            $segment['acceptedValues'] = $this->acceptValues;
366        }
367
368        if (isset($this->permission)) {
369            $segment['permission'] = $this->permission;
370        }
371
372        if (is_callable($this->suggestedValuesCallback)) {
373            $segment['suggestedValuesCallback'] = $this->suggestedValuesCallback;
374        }
375
376        if (is_string($this->suggestedValuesApi) && !empty($this->suggestedValuesApi)) {
377            $segment['suggestedValuesApi'] = $this->suggestedValuesApi;
378        }
379
380        return $segment;
381    }
382
383    /**
384     * Returns true if this segment should only be visible to registered users (see API.getSegmentsMetadata),
385     * false if it should always be visible to any user (even the anonymous user).
386     *
387     * @return boolean
388     * @ignore
389     */
390    public function isRequiresRegisteredUser()
391    {
392        return $this->requiresRegisteredUser;
393    }
394
395    /**
396     * Sets whether the segment should only be visible to registered users. If set to false it will be even visible to
397     * the anonymous user
398     *
399     * @param boolean $requiresRegisteredUser
400     * @ignore
401     */
402    public function setRequiresRegisteredUser($requiresRegisteredUser)
403    {
404        $this->requiresRegisteredUser = $requiresRegisteredUser;
405    }
406
407    /**
408     * Sets whether the segment is for internal use only and should not be visible in the UI or in API metadata output.
409     * These types of segments are, for example, used in unions for other segments, but have no value to users.
410     *
411     * @param bool $value
412     */
413    public function setIsInternal($value)
414    {
415        $this->isInternalSegment = $value;
416    }
417
418    /**
419     * Gets whether the segment is for internal use only and should not be visible in the UI or in API metadata output.
420     * These types of segments are, for example, used in unions for other segments, but have no value to users.
421     *
422     * @return bool
423     */
424    public function isInternal()
425    {
426        return $this->isInternalSegment;
427    }
428
429    private function check()
430    {
431        if ($this->sqlSegment && $this->unionOfSegments) {
432            throw new Exception(sprintf('Union of segments and SQL segment is set for segment "%s", use only one of them', $this->name));
433        }
434
435        if ($this->segment && $this->unionOfSegments && in_array($this->segment, $this->unionOfSegments, true)) {
436            throw new Exception(sprintf('The segment %s contains a union segment to itself', $this->name));
437        }
438    }
439}
440