update Piwik to version 2.16 (fixes #91)

This commit is contained in:
oliver 2016-04-10 18:55:57 +02:00
commit d885a4baa9
5833 changed files with 418860 additions and 226988 deletions

View file

@ -0,0 +1,47 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker;
use Piwik\Tracker\Visitor;
class IdSite extends VisitDimension
{
protected $columnName = 'idsite';
// we do not install or define column definition here as we need to create this column when installing as there is
// an index on it. Currently we do not define the index here... although we could overwrite the install() method
// and add column 'idsite' and add index. Problem is there is also an index
// INDEX(idsite, config_id, visit_last_action_time) and we maybe not be sure whether config_id already exists at
// installing point (we do not know whether visit_last_action_time or idsite column would be added first).
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return $request->getIdSite();
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return $request->getIdSite();
}
}

View file

@ -0,0 +1,45 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable\Row;
use Piwik\Piwik;
use Piwik\Plugin\ProcessedMetric;
/**
* The average number of actions per visit. Calculated as:
*
* nb_actions / nb_visits
*
* nb_actions & nb_visits are calculated during archiving.
*/
class ActionsPerVisit extends ProcessedMetric
{
public function getName()
{
return 'nb_actions_per_visit';
}
public function compute(Row $row)
{
$actions = $this->getMetric($row, 'nb_actions');
$visits = $this->getMetric($row, 'nb_visits');
return Piwik::getQuotientSafe($actions, $visits, $precision = 1);
}
public function getTranslatedName()
{
return Piwik::translate('General_ColumnActionsPerVisit');
}
public function getDependentMetrics()
{
return array('nb_actions', 'nb_visits');
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable\Row;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\ProcessedMetric;
/**
* The average number of seconds spent on the site per visit. Calculated as:
*
* sum_visit_length / nb_visits
*
* sum_visit_length & nb_visits are calculated during archiving.
*
* @api
*/
class AverageTimeOnSite extends ProcessedMetric
{
public function getName()
{
return 'avg_time_on_site';
}
public function compute(Row $row)
{
$sumVisitLength = $this->getMetric($row, 'sum_visit_length');
$nbVisits = $this->getMetric($row, 'nb_visits');
return Piwik::getQuotientSafe($sumVisitLength, $nbVisits, $precision = 0);
}
public function format($value, Formatter $formatter)
{
return $formatter->getPrettyTimeFromSeconds($value);
}
public function getTranslatedName()
{
return Piwik::translate('General_ColumnAvgTimeOnSite');
}
public function getDependentMetrics()
{
return array('sum_visit_length', 'nb_visits');
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable\Row;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\ProcessedMetric;
/**
* The percentage of visits that leave the site without visiting another page. Calculated
* as:
*
* bounce_count / nb_visits
*
* bounce_count & nb_visits are calculated by an Archiver.
*/
class BounceRate extends ProcessedMetric
{
public function getName()
{
return 'bounce_rate';
}
public function getTranslatedName()
{
return Piwik::translate('General_ColumnBounceRate');
}
public function getDependentMetrics()
{
return array('bounce_count', 'nb_visits');
}
public function format($value, Formatter $formatter)
{
return $formatter->getPrettyPercentFromQuotient($value);
}
public function compute(Row $row)
{
$bounceCount = $this->getMetric($row, 'bounce_count');
$visits = $this->getMetric($row, 'nb_visits');
return Piwik::getQuotientSafe($bounceCount, $visits, $precision = 2);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable\Row;
use Piwik\Plugin\ProcessedMetric;
class CallableProcessedMetric extends ProcessedMetric
{
private $name;
private $callback;
private $dependentMetrics;
public function __construct($name, $callback, $dependentMetrics = array())
{
$this->name = $name;
$this->callback = $callback;
$this->dependentMetrics = $dependentMetrics;
}
public function getName()
{
return $this->name;
}
public function compute(Row $row)
{
if ($this->callback) {
return call_user_func($this->callback, $row);
}
}
public function getTranslatedName()
{
return '';
}
public function getDependentMetrics()
{
return $this->dependentMetrics;
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable\Row;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\ProcessedMetric;
/**
* The percent of visits that result in a conversion. Calculated as:
*
* nb_visits_converted / nb_visits
*
* nb_visits_converted & nb_visits are calculated by the archiving process.
*/
class ConversionRate extends ProcessedMetric
{
public function getName()
{
return 'conversion_rate';
}
public function getTranslatedName()
{
return Piwik::translate('General_ColumnConversionRate');
}
public function getDependentMetrics()
{
return array('nb_visits_converted', 'nb_visits');
}
public function format($value, Formatter $formatter)
{
return $formatter->getPrettyPercentFromQuotient($value);
}
public function compute(Row $row)
{
$nbVisitsConverted = $this->getMetric($row, 'nb_visits_converted');
$nbVisits = $this->getMetric($row, 'nb_visits');
return Piwik::getQuotientSafe($nbVisitsConverted, $nbVisits, $precision = 4);
}
}

View file

@ -0,0 +1,123 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ProcessedMetric;
/**
* Calculates evolution values for any other metric. An evolution is the percent change from a
* point in the past to the present. They are computed as:
*
* (current value - value in past) / value in past
*
* @api
*/
class EvolutionMetric extends ProcessedMetric
{
/**
* @var Metric|string
*/
private $wrapped;
/**
* @var string
*/
private $evolutionMetricName;
/**
* @var int
*/
private $quotientPrecision;
/**
* @var DataTable
*/
private $pastData;
/**
* Constructor.
*
* @param string|Metric $wrapped The metric used to calculate the evolution.
* @param DataTable $pastData The data in the past to use when calculating evolutions.
* @param string|false $evolutionMetricName The name of the evolution processed metric. Defaults to
* $wrapped's name with `'_evolution'` appended.
* @param int $quotientPrecision The percent's quotient precision.
*/
public function __construct($wrapped, DataTable $pastData, $evolutionMetricName = false, $quotientPrecision = 0)
{
$this->wrapped = $wrapped;
$this->pastData = $pastData;
if (empty($evolutionMetricName)) {
$wrappedName = $this->getWrappedName();
$evolutionMetricName = $wrappedName . '_evolution';
}
$this->evolutionMetricName = $evolutionMetricName;
$this->quotientPrecision = $quotientPrecision;
}
public function getName()
{
return $this->evolutionMetricName;
}
public function getTranslatedName()
{
return $this->wrapped instanceof Metric ? $this->wrapped->getTranslatedName() : $this->getName();
}
public function compute(Row $row)
{
$columnName = $this->getWrappedName();
$pastRow = $this->getPastRowFromCurrent($row);
$currentValue = $this->getMetric($row, $columnName);
$pastValue = $pastRow ? $this->getMetric($pastRow, $columnName) : 0;
$dividend = $currentValue - $pastValue;
$divisor = $pastValue;
if ($dividend == 0) {
return 0;
} else if ($divisor == 0) {
return 1;
} else {
return Piwik::getQuotientSafe($dividend, $divisor, $this->quotientPrecision + 2);
}
}
public function format($value, Formatter $formatter)
{
return $formatter->getPrettyPercentFromQuotient($value);
}
public function getDependentMetrics()
{
return array($this->getWrappedName());
}
protected function getWrappedName()
{
return $this->wrapped instanceof Metric ? $this->wrapped->getName() : $this->wrapped;
}
/**
* public for Insights use.
*/
public function getPastRowFromCurrent(Row $row)
{
return $this->pastData->getRowFromLabel($row->getColumn('label'));
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
/**
* Percent of visits in the whole table. Calculated as:
*
* nb_visits / sum(all nb_visits in table)
*
* nb_visits is calculated by core archiving process.
*/
class VisitsPercent extends ProcessedMetric
{
private $cachedTotalVisits = null;
private $forceTotalVisits = null;
/**
* Constructor.
*
* @param int|null $totalVisits The forced value of total visits to use.
*/
public function __construct($totalVisits = null)
{
$this->forceTotalVisits = $totalVisits;
}
public function getName()
{
return 'nb_visits_percentage';
}
public function getTranslatedName()
{
return Piwik::translate('General_ColumnPercentageVisits');
}
public function compute(Row $row)
{
$visits = $this->getMetric($row, 'nb_visits');
return Piwik::getQuotientSafe($visits, $this->cachedTotalVisits, $precision = 2);
}
public function format($value, Formatter $formatter)
{
return $formatter->getPrettyPercentFromQuotient($value);
}
public function getDependentMetrics()
{
return array('nb_visits');
}
public function beforeCompute($report, DataTable $table)
{
if ($this->forceTotalVisits === null) {
$this->cachedTotalVisits = array_sum($this->getMetricValues($table, 'nb_visits'));
} else {
$this->cachedTotalVisits = $this->forceTotalVisits;
}
return true; // always compute
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Date;
use Piwik\Db;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker;
class ServerTime extends ActionDimension
{
protected $columnName = 'server_time';
protected $columnType = 'DATETIME NOT NULL';
public function install()
{
$changes = parent::install();
$changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
return $changes;
}
public function onNewAction(Request $request, Visitor $visitor, Action $action)
{
$timestamp = $request->getCurrentTimestamp();
return Date::getDatetimeFromTimestamp($timestamp);
}
}

View file

@ -0,0 +1,140 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Cache;
use Piwik\DataTable;
use Piwik\DataTable\Map;
use Piwik\Metrics;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Segment;
use Piwik\Plugins\VisitsSummary\API as VisitsSummaryApi;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker\Action;
/**
* UserId dimension.
*/
class UserId extends VisitDimension
{
/**
* @var string
*/
protected $columnName = 'user_id';
/**
* @var string
*/
protected $columnType = 'VARCHAR(200) NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setType('dimension');
$segment->setSegment('userId');
$segment->setCategory(Piwik::translate('General_Visit'));
$segment->setName('General_UserId');
$segment->setAcceptedValues('any non empty unique string identifying the user (such as an email address or a username).');
$segment->setSqlSegment('log_visit.user_id');
$segment->setRequiresAtLeastViewAccess(true);
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed|false
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return $request->getForcedUserId();
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
*
* @return mixed|false
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
return $request->getForcedUserId();
}
public function isUsedInAtLeastOneSite($idSites, $period, $date)
{
if ($period === 'day' || $period === 'week') {
$period = 'month';
}
if ($period === 'range') {
$period = 'day';
}
foreach ($idSites as $idSite) {
if ($this->isUsedInSiteCached($idSite, $period, $date)) {
return true;
}
}
return false;
}
private function isUsedInSiteCached($idSite, $period, $date)
{
$cache = Cache::getTransientCache();
$key = sprintf('%d.%s.%s', $idSite, $period, $date);
if (!$cache->contains($key)) {
$result = $this->isUsedInSite($idSite, $period, $date);
$cache->save($key, $result);
}
return $cache->fetch($key);
}
private function isUsedInSite($idSite, $period, $date)
{
$result = VisitsSummaryApi::getInstance()->get($idSite, $period, $date, false, 'nb_users');
return $this->hasDataTableUsers($result);
}
public function hasDataTableUsers(DataTable\DataTableInterface $result)
{
if ($result instanceof Map) {
foreach ($result->getDataTables() as $table) {
if ($this->hasDataTableUsers($table)) {
return true;
}
}
}
if (!$result->getRowsCount()) {
return false;
}
$firstRow = $result->getFirstRow();
if ($firstRow instanceof DataTable\Row && $firstRow->hasColumn(Metrics::INDEX_NB_USERS)) {
$metric = Metrics::INDEX_NB_USERS;
} else {
$metric = 'nb_users';
}
$numUsers = $result->getColumn($metric);
$numUsers = array_sum($numUsers);
return !empty($numUsers);
}
}

View file

@ -0,0 +1,33 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Date;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker;
use Piwik\Tracker\Visitor;
class VisitFirstActionTime extends VisitDimension
{
protected $columnName = 'visit_first_action_time';
protected $columnType = 'DATETIME NOT NULL';
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return Date::getDatetimeFromTimestamp($request->getCurrentTimestamp());
}
}

View file

@ -0,0 +1,131 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\GoalManager;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitGoalBuyer extends VisitDimension
{
// log_visit.visit_goal_buyer
const TYPE_BUYER_NONE = 0;
const TYPE_BUYER_ORDERED = 1;
const TYPE_BUYER_OPEN_CART = GoalManager::TYPE_BUYER_OPEN_CART;
const TYPE_BUYER_ORDERED_AND_OPEN_CART = GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART;
protected static $visitEcommerceStatus = array(
self::TYPE_BUYER_NONE => 'none',
self::TYPE_BUYER_ORDERED => 'ordered',
self::TYPE_BUYER_OPEN_CART => 'abandonedCart',
self::TYPE_BUYER_ORDERED_AND_OPEN_CART => 'orderedThenAbandonedCart',
);
protected $columnName = 'visit_goal_buyer';
protected $columnType = 'TINYINT(1) NOT NULL';
protected function configureSegments()
{
$example = Piwik::translate('General_EcommerceVisitStatusEg', '"&segment=visitEcommerceStatus==ordered,visitEcommerceStatus==orderedThenAbandonedCart"');
$acceptedValues = implode(", ", self::$visitEcommerceStatus) . '. ' . $example;
$segment = new Segment();
$segment->setSegment('visitEcommerceStatus');
$segment->setName('General_EcommerceVisitStatusDesc');
$segment->setAcceptedValues($acceptedValues);
$segment->setSqlFilterValue(__NAMESPACE__ . '\VisitGoalBuyer::getVisitEcommerceStatus');
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return $this->getBuyerType($request);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return int
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
$goalBuyer = $visitor->getVisitorColumn($this->columnName);
// Ecommerce buyer status
$visitEcommerceStatus = $this->getBuyerType($request, $goalBuyer);
if ($visitEcommerceStatus != self::TYPE_BUYER_NONE
// only update if the value has changed (prevents overwriting the value in case a request has
// updated it in the meantime)
&& $visitEcommerceStatus != $goalBuyer) {
return $visitEcommerceStatus;
}
return false;
}
public static function getVisitEcommerceStatus($status)
{
$id = array_search($status, self::$visitEcommerceStatus);
if ($id === false) {
throw new \Exception("Invalid 'visitEcommerceStatus' segment value $status");
}
return $id;
}
/**
* @ignore
*/
public static function getVisitEcommerceStatusFromId($id)
{
if (!isset(self::$visitEcommerceStatus[$id])) {
throw new \Exception("Unexpected ECommerce status value ");
}
return self::$visitEcommerceStatus[$id];
}
private function getBuyerType(Request $request, $existingType = self::TYPE_BUYER_NONE)
{
$isRequestEcommerce = $request->getMetadata('Ecommerce', 'isRequestEcommerce');
if (!$isRequestEcommerce) {
return $existingType;
}
$isGoalAnOrder = $request->getMetadata('Ecommerce', 'isGoalAnOrder');
if ($isGoalAnOrder) {
return self::TYPE_BUYER_ORDERED;
}
// request is Add to Cart
if ($existingType == self::TYPE_BUYER_ORDERED
|| $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART
) {
return self::TYPE_BUYER_ORDERED_AND_OPEN_CART;
}
return self::TYPE_BUYER_OPEN_CART;
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitGoalConverted extends VisitDimension
{
protected $columnName = 'visit_goal_converted';
protected $columnType = 'TINYINT(1) NOT NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setSegment('visitConverted');
$segment->setName('General_VisitConvertedGoal');
$segment->setAcceptedValues('0, 1');
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return 0;
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onConvertedVisit(Request $request, Visitor $visitor, $action)
{
return 1;
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Segment;
/**
* Dimension for the log_visit.idvisit column. This column is added in the CREATE TABLE
* statement, so this dimension exists only to configure a segment.
*/
class VisitId extends VisitDimension
{
protected function configureSegments()
{
parent::configureSegments();
$segment = new Segment();
$segment->setType('dimension');
$segment->setCategory(Piwik::translate('General_Visit'));
$segment->setName(Piwik::translate('General_Visit') . " ID");
$segment->setSegment('visitId');
$segment->setAcceptedValues('Any integer.');
$segment->setSqlSegment('log_visit.idvisit');
$segment->setRequiresAtLeastViewAccess(true);
$this->addSegment($segment);
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Segment;
/**
* Dimension for the log_visit.location_ip column. This column is added in the CREATE TABLE
* statement, so this dimension exists only to configure a segment.
*/
class VisitIp extends VisitDimension
{
protected function configureSegments()
{
parent::configureSegments();
$segment = new Segment();
$segment->setType('metric');
$segment->setCategory(Piwik::translate('General_Visit'));
$segment->setName('General_VisitorIP');
$segment->setSegment('visitIp');
$segment->setAcceptedValues('13.54.122.1. </code>Select IP ranges with notation: <code>visitIp>13.54.122.0;visitIp<13.54.122.255');
$segment->setSqlSegment('log_visit.location_ip');
$segment->setSqlFilterValue(array('Piwik\Network\IPUtils', 'stringToBinaryIP'));
$segment->setRequiresAtLeastViewAccess(true);
$this->addSegment($segment);
}
}

View file

@ -0,0 +1,60 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Date;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker;
use Piwik\Tracker\Visitor;
/**
* This dimension holds the best guess for a visit's end time. It is set the last action
* time for each visit. `ping=1` requests can be sent to update the dimension value so
* it can be a more accurate guess of the time the visitor spent on the site.
*
* Note: though it is named 'visit last action time' it actually refers to the visit's last action's
* end time.
*/
class VisitLastActionTime extends VisitDimension
{
protected $columnName = 'visit_last_action_time';
// we do not install or define column definition here as we need to create this column when installing as there is
// an index on it. Currently we do not define the index here... although we could overwrite the install() method
// and add column 'visit_last_action_time' and add index. Problem is there is also an index
// INDEX(idsite, config_id, visit_last_action_time) and we maybe not be sure whether idsite already exists at
// installing point (we do not know whether idsite column will be added first).
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return Date::getDatetimeFromTimestamp($request->getCurrentTimestamp());
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
if ($request->getParam('ping') == 1) {
return false;
}
return $this->onNewVisit($request, $visitor, $action);
}
}

View file

@ -0,0 +1,108 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Config;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitTotalTime extends VisitDimension
{
protected $columnName = 'visit_total_time';
protected $columnType = 'SMALLINT(5) UNSIGNED NOT NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setSegment('visitDuration');
$segment->setName('General_ColumnVisitDuration');
$segment->setType(Segment::TYPE_METRIC);
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$totalTime = Config::getInstance()->Tracker['default_time_one_page_visit'];
$totalTime = $this->cleanupVisitTotalTime($totalTime);
return $totalTime;
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onExistingVisit(Request $request, Visitor $visitor, $action)
{
$firstActionTime = $visitor->getVisitorColumn('visit_first_action_time');
$totalTime = 1 + $request->getCurrentTimestamp() - $firstActionTime;
$totalTime = $this->cleanupVisitTotalTime($totalTime);
return $totalTime;
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return int
*/
public function onConvertedVisit(Request $request, Visitor $visitor, $action)
{
if (!$visitor->isVisitorKnown()) {
return false;
}
$totalTime = $visitor->getVisitorColumn('visit_total_time');
// If a pageview and goal conversion in the same second, with previously a goal conversion recorded
// the request would not "update" the row since all values are the same as previous
// therefore the request below throws exception, instead we make sure the UPDATE will affect the row
$totalTime = $totalTime + $request->getParam('idgoal');
// +2 to offset idgoal=-1 and idgoal=0
$totalTime = $totalTime + 2;
return $this->cleanupVisitTotalTime($totalTime);
}
public function getRequiredVisitFields()
{
return array('visit_first_action_time');
}
private function cleanupVisitTotalTime($t)
{
$t = (int)$t;
if ($t < 0) {
$t = 0;
}
$smallintMysqlLimit = 65534;
if ($t > $smallintMysqlLimit) {
$t = $smallintMysqlLimit;
}
return $t;
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitorDaysSinceFirst extends VisitDimension
{
protected $columnName = 'visitor_days_since_first';
protected $columnType = 'SMALLINT(5) UNSIGNED NOT NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setType(Segment::TYPE_METRIC);
$segment->setSegment('daysSinceFirstVisit');
$segment->setName('General_DaysSinceFirstVisit');
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return $request->getDaysSinceFirstVisit();
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return $visitor->getVisitorColumn($this->columnName);
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitorDaysSinceOrder extends VisitDimension
{
protected $columnName = 'visitor_days_since_order';
protected $columnType = 'SMALLINT(5) UNSIGNED NOT NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setSegment('daysSinceLastEcommerceOrder');
$segment->setName('General_DaysSinceLastEcommerceOrder');
$segment->setType(Segment::TYPE_METRIC);
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$daysSinceLastOrder = $request->getDaysSinceLastOrder();
if ($daysSinceLastOrder === false) {
$daysSinceLastOrder = 0;
}
return $daysSinceLastOrder;
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return $visitor->getVisitorColumn($this->columnName);
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Segment;
/**
* Dimension for the log_visit.idvisitor column. This column is added in the CREATE TABLE
* statement, so this dimension exists only to configure a segment.
*/
class VisitorId extends VisitDimension
{
protected function configureSegments()
{
parent::configureSegments();
$segment = new Segment();
$segment->setType('dimension');
$segment->setCategory(Piwik::translate('General_Visit'));
$segment->setName('General_VisitorID');
$segment->setSegment('visitorId');
$segment->setAcceptedValues('34c31e04394bdc63 - any 16 Hexadecimal chars ID, which can be fetched using the Tracking API function getVisitorId()');
$segment->setSqlSegment('log_visit.idvisitor');
$segment->setSqlFilterValue(array('Piwik\Common', 'convertVisitorIdToBin'));
$segment->setRequiresAtLeastViewAccess(true);
$this->addSegment($segment);
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitorReturning extends VisitDimension
{
const IS_RETURNING_CUSTOMER = 2;
const IS_RETURNING = 1;
const IS_NEW = 0;
protected $columnName = 'visitor_returning';
protected $columnType = 'TINYINT(1) NOT NULL';
protected $conversionField = true;
protected function configureSegments()
{
$acceptedValues = 'new, returning, returningCustomer. ';
$acceptedValues .= Piwik::translate('General_VisitTypeExample', '"&segment=visitorType==returning,visitorType==returningCustomer"');
$segment = new Segment();
$segment->setSegment('visitorType');
$segment->setName('General_VisitType');
$segment->setAcceptedValues($acceptedValues);
$segment->setSqlFilterValue(function ($type) {
return $type == "new" ? 0 : ($type == "returning" ? 1 : 2);
});
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
$visitCount = $request->getVisitCount();
$daysSinceLastVisit = $request->getDaysSinceLastVisit();
$daysSinceLastOrder = $request->getDaysSinceLastOrder();
$isReturningCustomer = ($daysSinceLastOrder !== false);
if ($isReturningCustomer) {
return self::IS_RETURNING_CUSTOMER;
}
if ($visitCount > 1 || $visitor->isVisitorKnown() || $daysSinceLastVisit > 0) {
return self::IS_RETURNING;
}
return self::IS_NEW;
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return $visitor->getVisitorColumn($this->columnName);
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\Columns;
use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugins\CoreHome\Segment;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
class VisitsCount extends VisitDimension
{
protected $columnName = 'visitor_count_visits';
protected $columnType = 'SMALLINT(5) UNSIGNED NOT NULL';
protected function configureSegments()
{
$segment = new Segment();
$segment->setType(Segment::TYPE_METRIC);
$segment->setSegment('visitCount');
$segment->setName('General_NumberOfVisits');
$this->addSegment($segment);
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onNewVisit(Request $request, Visitor $visitor, $action)
{
return $request->getVisitCount();
}
/**
* @param Request $request
* @param Visitor $visitor
* @param Action|null $action
* @return mixed
*/
public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
{
return $visitor->getVisitorColumn($this->columnName);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -13,29 +13,78 @@ use Piwik\API\Request;
use Piwik\Common;
use Piwik\Date;
use Piwik\FrontController;
use Piwik\Menu\MenuMain;
use Piwik\Menu\MenuReporting;
use Piwik\Notification\Manager as NotificationManager;
use Piwik\Piwik;
use Piwik\Plugin\Report;
use Piwik\Plugins\CoreHome\DataTableRowAction\MultiRowEvolution;
use Piwik\Plugins\CoreHome\DataTableRowAction\RowEvolution;
use Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient;
use Piwik\Plugins\Dashboard\DashboardManagerControl;
use Piwik\Plugins\UsersManager\API;
use Piwik\Site;
use Piwik\Translation\Translator;
use Piwik\UpdateCheck;
use Piwik\Url;
use Piwik\View;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
use Piwik\Plugin\Widgets as PluginWidgets;
/**
*
*/
class Controller extends \Piwik\Plugin\Controller
{
function getDefaultAction()
/**
* @var Translator
*/
private $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
parent::__construct();
}
public function getDefaultAction()
{
return 'redirectToCoreHomeIndex';
}
public function renderReportMenu(Report $report)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkSitePermission();
$report->checkIsEnabled();
$menuTitle = $report->getMenuTitle();
if (empty($menuTitle)) {
throw new Exception('This report is not supposed to be displayed in the menu, please define a $menuTitle in your report.');
}
$menuTitle = $this->translator->translate($menuTitle);
$content = $this->renderReportWidget($report);
return View::singleReport($menuTitle, $content);
}
public function renderReportWidget(Report $report)
{
Piwik::checkUserHasSomeViewAccess();
$this->checkSitePermission();
$report->checkIsEnabled();
return $report->render();
}
public function renderWidget(PluginWidgets $widget, $method)
{
Piwik::checkUserHasSomeViewAccess();
return $widget->$method();
}
function redirectToCoreHomeIndex()
{
$defaultReport = API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), API::PREFERENCE_DEFAULT_REPORT);
@ -48,9 +97,11 @@ class Controller extends \Piwik\Plugin\Controller
) {
$module = 'MultiSites';
}
if ($defaultReport == Piwik::getLoginPluginName()) {
$module = Piwik::getLoginPluginName();
}
$idSite = Common::getRequestVar('idSite', false, 'int');
parent::redirectToIndex($module, $action, $idSite);
}
@ -58,10 +109,15 @@ class Controller extends \Piwik\Plugin\Controller
public function showInContext()
{
$controllerName = Common::getRequestVar('moduleToLoad');
$actionName = Common::getRequestVar('actionToLoad', 'index');
$actionName = Common::getRequestVar('actionToLoad', 'index');
if($controllerName == 'API') {
throw new Exception("Showing API requests in context is not supported for security reasons. Please change query parameter 'moduleToLoad'.");
}
if ($actionName == 'showInContext') {
throw new Exception("Preventing infinite recursion...");
}
$view = $this->getDefaultIndexView();
$view->content = FrontController::getInstance()->fetchDispatch($controllerName, $actionName);
return $view->render();
@ -77,7 +133,7 @@ class Controller extends \Piwik\Plugin\Controller
{
$view = new View('@CoreHome/getDefaultIndexView');
$this->setGeneralVariablesView($view);
$view->menu = MenuMain::getInstance()->getMenu();
$view->menu = MenuReporting::getInstance()->getMenu();
$view->dashboardSettingsControl = new DashboardManagerControl();
$view->content = '';
return $view;
@ -91,12 +147,16 @@ class Controller extends \Piwik\Plugin\Controller
) {
return;
}
$websiteId = Common::getRequestVar('idSite', false, 'int');
if ($websiteId) {
$website = new Site($websiteId);
$datetimeCreationDate = $website->getCreationDate()->getDatetime();
$datetimeCreationDate = $website->getCreationDate()->getDatetime();
$creationDateLocalTimezone = Date::factory($datetimeCreationDate, $website->getTimezone())->toString('Y-m-d');
$todayLocalTimezone = Date::factory('now', $website->getTimezone())->toString('Y-m-d');
$todayLocalTimezone = Date::factory('now', $website->getTimezone())->toString('Y-m-d');
if ($creationDateLocalTimezone == $todayLocalTimezone) {
Piwik::redirectToModule('CoreHome', 'index',
array('date' => 'today',
@ -181,32 +241,6 @@ class Controller extends \Piwik\Plugin\Controller
return $view->render();
}
/**
* Renders and echo's the in-app donate form w/ slider.
*/
public function getDonateForm()
{
$view = new View('@CoreHome/getDonateForm');
if (Common::getRequestVar('widget', false)
&& Piwik::hasUserSuperUserAccess()
) {
$view->footerMessage = Piwik::translate('CoreHome_OnlyForSuperUserAccess');
}
return $view->render();
}
/**
* Renders and echo's HTML that displays the Piwik promo video.
*/
public function getPromoVideo()
{
$view = new View('@CoreHome/getPromoVideo');
$view->shareText = Piwik::translate('CoreHome_SharePiwikShort');
$view->shareTextLong = Piwik::translate('CoreHome_SharePiwikLong');
$view->promoVideoUrl = 'http://www.youtube.com/watch?v=OslfF_EH81g';
return $view->render();
}
/**
* Redirects the user to a paypal so they can donate to Piwik.
*/
@ -224,19 +258,19 @@ class Controller extends \Piwik\Plugin\Controller
$url = "https://www.paypal.com/cgi-bin/webscr?" . Url::getQueryStringFromParameters($parameters);
header("Location: $url");
Url::redirectToUrl($url);
exit;
}
public function getSiteSelector()
public function saveViewDataTableParameters()
{
return "<div piwik-siteselector class=\"sites_autocomplete\" switch-site-on-select=\"false\"></div>";
}
Piwik::checkUserIsNotAnonymous();
$this->checkTokenInUrl();
public function getPeriodSelector()
{
$view = new View("@CoreHome/_periodSelect");
$this->setGeneralVariablesView($view);
return $view->render();
$reportId = Common::getRequestVar('report_id', null, 'string');
$parameters = (array) Common::getRequestVar('parameters', null, 'json');
$login = Piwik::getCurrentUserLogin();
ViewDataTableManager::saveViewDataTableParameters($login, $reportId, $parameters);
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -8,43 +8,59 @@
*/
namespace Piwik\Plugins\CoreHome;
use Piwik\WidgetsList;
/**
*
*/
class CoreHome extends \Piwik\Plugin
{
/**
* @see Piwik\Plugin::getListHooksRegistered
* @see Piwik\Plugin::registerEvents
*/
public function getListHooksRegistered()
public function registerEvents()
{
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'WidgetsList.addWidgets' => 'addWidgets',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
'AssetManager.filterMergedJavaScripts' => 'filterMergedJavaScripts',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
'Live.getAllVisitorDetails' => 'extendVisitorDetails',
);
}
/**
* Adds the donate form widget.
*/
public function addWidgets()
public function filterMergedJavaScripts(&$mergedContent)
{
WidgetsList::add('Example Widgets', 'CoreHome_SupportPiwik', 'CoreHome', 'getDonateForm');
WidgetsList::add('Example Widgets', 'Installation_Welcome', 'CoreHome', 'getPromoVideo');
$mergedContent = preg_replace('/(sourceMappingURL=(.*?).map)/', '', $mergedContent);
}
public function extendVisitorDetails(&$visitor, $details)
{
$instance = new Visitor($details);
$visitor['userId'] = $instance->getUserId();
$visitor['visitorType'] = $instance->getVisitorReturning();
$visitor['visitorTypeIcon'] = $instance->getVisitorReturningIcon();
$visitor['visitConverted'] = $instance->isVisitorGoalConverted();
$visitor['visitConvertedIcon'] = $instance->getVisitorGoalConvertedIcon();
$visitor['visitCount'] = $instance->getVisitCount();
$visitor['firstActionTimestamp'] = $instance->getTimestampFirstAction();
$visitor['visitEcommerceStatus'] = $instance->getVisitEcommerceStatus();
$visitor['visitEcommerceStatusIcon'] = $instance->getVisitEcommerceStatusIcon();
$visitor['daysSinceFirstVisit'] = $instance->getDaysSinceFirstVisit();
$visitor['daysSinceLastEcommerceOrder'] = $instance->getDaysSinceLastEcommerceOrder();
$visitor['visitDuration'] = $instance->getVisitLength();
$visitor['visitDurationPretty'] = $instance->getVisitLengthPretty();
}
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.css";
$stylesheets[] = "libs/jquery/themes/base/jquery-ui.min.css";
$stylesheets[] = "libs/jquery/stylesheets/jquery.jscrollpane.css";
$stylesheets[] = "libs/jquery/stylesheets/scroll.less";
$stylesheets[] = "plugins/Zeitgeist/stylesheets/base.less";
$stylesheets[] = "libs/bower_components/ngDialog/css/ngDialog.min.css";
$stylesheets[] = "libs/bower_components/ngDialog/css/ngDialog-theme-default.min.css";
$stylesheets[] = "plugins/Morpheus/stylesheets/base.less";
$stylesheets[] = "plugins/Morpheus/stylesheets/main.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/coreHome.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/menu.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/dataTable.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/cloud.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css";
@ -54,26 +70,37 @@ class CoreHome extends \Piwik\Plugin
$stylesheets[] = "plugins/CoreHome/stylesheets/color_manager.css";
$stylesheets[] = "plugins/CoreHome/stylesheets/sparklineColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/notification.less";
$stylesheets[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/zen-mode.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/layout.less";
$stylesheets[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/dialogtoggler/ngdialog.less";
$stylesheets[] = "plugins/CoreHome/angularjs/notification/notification.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/quick-access/quick-access.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/selector/selector.directive.less";
}
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "libs/jquery/jquery.js";
$jsFiles[] = "libs/jquery/jquery-ui.js";
$jsFiles[] = "libs/bower_components/jquery/dist/jquery.min.js";
$jsFiles[] = "libs/bower_components/jquery-ui/ui/minified/jquery-ui.min.js";
$jsFiles[] = "libs/jquery/jquery.browser.js";
$jsFiles[] = "libs/jquery/jquery.truncate.js";
$jsFiles[] = "libs/jquery/jquery.scrollTo.js";
$jsFiles[] = "libs/jquery/jquery.history.js";
$jsFiles[] = "libs/jquery/jquery.jscrollpane.js";
$jsFiles[] = "libs/jquery/jquery.mousewheel.js";
$jsFiles[] = "libs/bower_components/jquery.scrollTo/jquery.scrollTo.min.js";
$jsFiles[] = "libs/bower_components/jScrollPane/script/jquery.jscrollpane.min.js";
$jsFiles[] = "libs/bower_components/jquery-mousewheel/jquery.mousewheel.min.js";
$jsFiles[] = "libs/jquery/mwheelIntent.js";
$jsFiles[] = "libs/javascript/sprintf.js";
$jsFiles[] = "libs/angularjs/angular.min.js";
$jsFiles[] = "libs/angularjs/angular-sanitize.min.js";
$jsFiles[] = "libs/angularjs/angular-animate.min.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js";
$jsFiles[] = "libs/bower_components/sprintf/dist/sprintf.min.js";
$jsFiles[] = "libs/bower_components/mousetrap/mousetrap.min.js";
$jsFiles[] = "libs/bower_components/angular/angular.min.js";
$jsFiles[] = "libs/bower_components/angular-sanitize/angular-sanitize.js";
$jsFiles[] = "libs/bower_components/angular-animate/angular-animate.js";
$jsFiles[] = "libs/bower_components/angular-cookies/angular-cookies.js";
$jsFiles[] = "libs/bower_components/ngDialog/js/ngDialog.min.js";
$jsFiles[] = "plugins/Morpheus/javascripts/piwikHelper.js";
$jsFiles[] = "plugins/Morpheus/javascripts/ajaxHelper.js";
$jsFiles[] = "plugins/Morpheus/javascripts/jquery.icheck.min.js";
$jsFiles[] = "plugins/Morpheus/javascripts/morpheus.js";
$jsFiles[] = "plugins/Morpheus/javascripts/layout.js";
$jsFiles[] = "plugins/CoreHome/javascripts/require.js";
$jsFiles[] = "plugins/CoreHome/javascripts/uiControl.js";
$jsFiles[] = "plugins/CoreHome/javascripts/dataTable.js";
@ -88,38 +115,64 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreHome/javascripts/top_controls.js";
$jsFiles[] = "plugins/CoreHome/javascripts/donate.js";
$jsFiles[] = "libs/jqplot/jqplot-custom.min.js";
$jsFiles[] = "plugins/CoreHome/javascripts/promo.js";
$jsFiles[] = "plugins/CoreHome/javascripts/color_manager.js";
$jsFiles[] = "plugins/CoreHome/javascripts/notification.js";
$jsFiles[] = "plugins/CoreHome/javascripts/notification_parser.js";
$jsFiles[] = "plugins/CoreHome/javascripts/numberFormatter.js";
$jsFiles[] = "plugins/CoreHome/angularjs/piwikAppConfig.js";
$jsFiles[] = "plugins/CoreHome/angularjs/piwikApp.config.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/service.module.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/piwik.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/services/piwik-api.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/filter.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/filter.module.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/translate.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/startfrom.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/evolution.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/length.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/trim.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/pretty-url.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/htmldecode.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/filters/ucfirst.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/directive.module.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/autocomplete-matched.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/ignore-click.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/onenter.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/focusif.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/dialog.js";
$jsFiles[] = "plugins/CoreHome/angularjs/common/directives/translate.js";
$jsFiles[] = "plugins/CoreHome/angularjs/piwikApp.js";
$jsFiles[] = "plugins/CoreHome/angularjs/anchorLinkFix.js";
$jsFiles[] = "plugins/CoreHome/angularjs/http404check.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-model.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/history/history.service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline-directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-model.service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector.controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler.controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/dialogtoggler/dialogtoggler-urllistener.service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/notification/notification.controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/notification/notification.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/ajax-form/ajax-form.controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/quick-access/quick-access.controller.js";
$jsFiles[] = "plugins/CoreHome/angularjs/quick-access/quick-access.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/selector/selector.directive.js";
}
public function getClientSideTranslationKeys(&$translationKeys)
@ -128,8 +181,10 @@ class CoreHome extends \Piwik\Plugin
$translationKeys[] = 'General_Loading';
$translationKeys[] = 'General_Show';
$translationKeys[] = 'General_Hide';
$translationKeys[] = 'General_YearShort';
$translationKeys[] = 'General_Website';
$translationKeys[] = 'Intl_Year_Short';
$translationKeys[] = 'General_MultiSitesSummary';
$translationKeys[] = 'General_SearchNoResults';
$translationKeys[] = 'CoreHome_YouAreUsingTheLatestVersion';
$translationKeys[] = 'CoreHome_IncludeRowsWithLowPopulation';
$translationKeys[] = 'CoreHome_ExcludeRowsWithLowPopulation';
@ -140,6 +195,8 @@ class CoreHome extends \Piwik\Plugin
$translationKeys[] = 'CoreHome_FlattenDataTable';
$translationKeys[] = 'CoreHome_UnFlattenDataTable';
$translationKeys[] = 'CoreHome_ExternalHelp';
$translationKeys[] = 'CoreHome_ClickToEditX';
$translationKeys[] = 'CoreHome_Menu';
$translationKeys[] = 'SitesManager_NotFound';
$translationKeys[] = 'Annotations_ViewAndAddAnnotations';
$translationKeys[] = 'General_RowEvolutionRowActionTooltipTitle';
@ -149,53 +206,78 @@ class CoreHome extends \Piwik\Plugin
$translationKeys[] = 'Annotations_HideAnnotationsFor';
$translationKeys[] = 'General_LoadingPopover';
$translationKeys[] = 'General_LoadingPopoverFor';
$translationKeys[] = 'General_ShortMonth_1';
$translationKeys[] = 'General_ShortMonth_2';
$translationKeys[] = 'General_ShortMonth_3';
$translationKeys[] = 'General_ShortMonth_4';
$translationKeys[] = 'General_ShortMonth_5';
$translationKeys[] = 'General_ShortMonth_6';
$translationKeys[] = 'General_ShortMonth_7';
$translationKeys[] = 'General_ShortMonth_8';
$translationKeys[] = 'General_ShortMonth_9';
$translationKeys[] = 'General_ShortMonth_10';
$translationKeys[] = 'General_ShortMonth_11';
$translationKeys[] = 'General_ShortMonth_12';
$translationKeys[] = 'General_LongMonth_1';
$translationKeys[] = 'General_LongMonth_2';
$translationKeys[] = 'General_LongMonth_3';
$translationKeys[] = 'General_LongMonth_4';
$translationKeys[] = 'General_LongMonth_5';
$translationKeys[] = 'General_LongMonth_6';
$translationKeys[] = 'General_LongMonth_7';
$translationKeys[] = 'General_LongMonth_8';
$translationKeys[] = 'General_LongMonth_9';
$translationKeys[] = 'General_LongMonth_10';
$translationKeys[] = 'General_LongMonth_11';
$translationKeys[] = 'General_LongMonth_12';
$translationKeys[] = 'General_ShortDay_1';
$translationKeys[] = 'General_ShortDay_2';
$translationKeys[] = 'General_ShortDay_3';
$translationKeys[] = 'General_ShortDay_4';
$translationKeys[] = 'General_ShortDay_5';
$translationKeys[] = 'General_ShortDay_6';
$translationKeys[] = 'General_ShortDay_7';
$translationKeys[] = 'General_LongDay_1';
$translationKeys[] = 'General_LongDay_2';
$translationKeys[] = 'General_LongDay_3';
$translationKeys[] = 'General_LongDay_4';
$translationKeys[] = 'General_LongDay_5';
$translationKeys[] = 'General_LongDay_6';
$translationKeys[] = 'General_LongDay_7';
$translationKeys[] = 'General_DayMo';
$translationKeys[] = 'General_DayTu';
$translationKeys[] = 'General_DayWe';
$translationKeys[] = 'General_DayTh';
$translationKeys[] = 'General_DayFr';
$translationKeys[] = 'General_DaySa';
$translationKeys[] = 'General_DaySu';
$translationKeys[] = 'Intl_Month_Short_StandAlone_1';
$translationKeys[] = 'Intl_Month_Short_StandAlone_2';
$translationKeys[] = 'Intl_Month_Short_StandAlone_3';
$translationKeys[] = 'Intl_Month_Short_StandAlone_4';
$translationKeys[] = 'Intl_Month_Short_StandAlone_5';
$translationKeys[] = 'Intl_Month_Short_StandAlone_6';
$translationKeys[] = 'Intl_Month_Short_StandAlone_7';
$translationKeys[] = 'Intl_Month_Short_StandAlone_8';
$translationKeys[] = 'Intl_Month_Short_StandAlone_9';
$translationKeys[] = 'Intl_Month_Short_StandAlone_10';
$translationKeys[] = 'Intl_Month_Short_StandAlone_11';
$translationKeys[] = 'Intl_Month_Short_StandAlone_12';
$translationKeys[] = 'Intl_Month_Long_StandAlone_1';
$translationKeys[] = 'Intl_Month_Long_StandAlone_2';
$translationKeys[] = 'Intl_Month_Long_StandAlone_3';
$translationKeys[] = 'Intl_Month_Long_StandAlone_4';
$translationKeys[] = 'Intl_Month_Long_StandAlone_5';
$translationKeys[] = 'Intl_Month_Long_StandAlone_6';
$translationKeys[] = 'Intl_Month_Long_StandAlone_7';
$translationKeys[] = 'Intl_Month_Long_StandAlone_8';
$translationKeys[] = 'Intl_Month_Long_StandAlone_9';
$translationKeys[] = 'Intl_Month_Long_StandAlone_10';
$translationKeys[] = 'Intl_Month_Long_StandAlone_11';
$translationKeys[] = 'Intl_Month_Long_StandAlone_12';
$translationKeys[] = 'Intl_Day_Short_StandAlone_1';
$translationKeys[] = 'Intl_Day_Short_StandAlone_2';
$translationKeys[] = 'Intl_Day_Short_StandAlone_3';
$translationKeys[] = 'Intl_Day_Short_StandAlone_4';
$translationKeys[] = 'Intl_Day_Short_StandAlone_5';
$translationKeys[] = 'Intl_Day_Short_StandAlone_6';
$translationKeys[] = 'Intl_Day_Short_StandAlone_7';
$translationKeys[] = 'Intl_Day_Long_StandAlone_1';
$translationKeys[] = 'Intl_Day_Long_StandAlone_2';
$translationKeys[] = 'Intl_Day_Long_StandAlone_3';
$translationKeys[] = 'Intl_Day_Long_StandAlone_4';
$translationKeys[] = 'Intl_Day_Long_StandAlone_5';
$translationKeys[] = 'Intl_Day_Long_StandAlone_6';
$translationKeys[] = 'Intl_Day_Long_StandAlone_7';
$translationKeys[] = 'Intl_Day_Min_StandAlone_1';
$translationKeys[] = 'Intl_Day_Min_StandAlone_2';
$translationKeys[] = 'Intl_Day_Min_StandAlone_3';
$translationKeys[] = 'Intl_Day_Min_StandAlone_4';
$translationKeys[] = 'Intl_Day_Min_StandAlone_5';
$translationKeys[] = 'Intl_Day_Min_StandAlone_6';
$translationKeys[] = 'Intl_Day_Min_StandAlone_7';
$translationKeys[] = 'General_And';
$translationKeys[] = 'General_All';
$translationKeys[] = 'General_Search';
$translationKeys[] = 'General_Clear';
$translationKeys[] = 'General_MoreDetails';
$translationKeys[] = 'General_Help';
$translationKeys[] = 'General_MoreDetails';
$translationKeys[] = 'General_Help';
$translationKeys[] = 'General_Id';
$translationKeys[] = 'General_Name';
$translationKeys[] = 'General_JsTrackingTag';
$translationKeys[] = 'General_Yes';
$translationKeys[] = 'General_No';
$translationKeys[] = 'General_Edit';
$translationKeys[] = 'General_Delete';
$translationKeys[] = 'General_Default';
$translationKeys[] = 'General_LoadingData';
$translationKeys[] = 'General_ErrorRequest';
$translationKeys[] = 'General_YourChangesHaveBeenSaved';
$translationKeys[] = 'General_LearnMore';
$translationKeys[] = 'CoreHome_UndoPivotBySubtable';
$translationKeys[] = 'CoreHome_PivotBySubtable';
$translationKeys[] = 'CoreHome_QuickAccessTitle';
$translationKeys[] = 'CoreHome_Segments';
$translationKeys[] = 'CoreHome_MenuEntries';
$translationKeys[] = 'SitesManager_Sites';
$translationKeys[] = 'CoreHome_ChangeCurrentWebsite';
$translationKeys[] = 'General_CreatedByUser';
}
}

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later

View file

@ -1,6 +1,6 @@
<?php
/**
* Piwik - Open source web analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -9,12 +9,14 @@
namespace Piwik\Plugins\CoreHome\DataTableRowAction;
use Exception;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\NumberFormatter;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Piwik;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
use Piwik\Url;
@ -80,18 +82,21 @@ class RowEvolution
* @param null|string $graphType
* @throws Exception
*/
public function __construct($idSite, $date, $graphType = null)
public function __construct($idSite, $date, $graphType = 'graphEvolution')
{
$this->apiMethod = Common::getRequestVar('apiMethod', '', 'string');
if (empty($this->apiMethod)) throw new Exception("Parameter apiMethod not set.");
$this->label = ResponseBuilder::getLabelFromRequest($_GET);
$this->label = $this->label[0];
$this->label = DataTablePostProcessor::getLabelFromRequest($_GET);
if (!is_array($this->label)) {
throw new Exception("Expected label to be an array, got instead: " . $this->label);
}
$this->label = Common::unsanitizeInputValue($this->label[0]);
if ($this->label === '') throw new Exception("Parameter label not set.");
$this->period = Common::getRequestVar('period', '', 'string');
if (empty($this->period)) throw new Exception("Parameter period not set.");
PeriodFactory::checkPeriodIsEnabled($this->period);
$this->idSite = $idSite;
$this->graphType = $graphType;
@ -140,6 +145,7 @@ class RowEvolution
{
list($apiModule, $apiAction) = explode('.', $this->apiMethod);
// getQueryStringFromParameters expects sanitised query parameter values
$parameters = array(
'method' => 'API.getRowEvolution',
'label' => $this->label,
@ -194,7 +200,9 @@ class RowEvolution
$view->config->columns_to_display = array_keys($metrics ? : $this->graphMetrics);
}
$view->requestConfig->request_parameters_to_modify['label'] = '';
$view->config->show_goals = false;
$view->config->show_search = false;
$view->config->show_all_views_icons = false;
$view->config->show_active_view_icon = false;
$view->config->show_related_reports = false;
@ -224,7 +232,10 @@ class RowEvolution
$change = isset($metricData['change']) ? $metricData['change'] : false;
list($first, $last) = $this->getFirstAndLastDataPointsForMetric($metric);
$details = Piwik::translate('RowEvolution_MetricBetweenText', array($first, $last));
$details = Piwik::translate('RowEvolution_MetricBetweenText', array(
NumberFormatter::getInstance()->format($first),
NumberFormatter::getInstance()->format($last)
));
if ($change !== false) {
$lowerIsBetter = Metrics::isLowerValueBetter($metric);
@ -251,7 +262,11 @@ class RowEvolution
$min = isset($metricData['min']) ? $metricData['min'] : 0;
$min .= $unit;
$max .= $unit;
$minmax = Piwik::translate('RowEvolution_MetricMinMax', array($metricData['name'], $min, $max));
$minmax = Piwik::translate('RowEvolution_MetricMinMax', array(
$metricData['name'],
NumberFormatter::getInstance()->formatNumber($min),
NumberFormatter::getInstance()->formatNumber($max)
));
$newMetric = array(
'label' => $metricData['name'],
@ -263,6 +278,15 @@ class RowEvolution
if (!empty($metricData['logo'])) {
$newMetric['logo'] = $metricData['logo'];
}
// TODO: this check should be determined by metric metadata, not hardcoded here
if ($metric == 'nb_users'
&& $first == 0
&& $last == 0
) {
$newMetric['hide'] = true;
}
$metrics[] = $newMetric;
$i++;
}
@ -334,7 +358,7 @@ class RowEvolution
$labelPretty = $dataTableMap->getColumn('label_html');
$labelPretty = array_filter($labelPretty, 'strlen');
$labelPretty = current($labelPretty);
if(!empty($labelPretty)) {
if (!empty($labelPretty)) {
return $labelPretty;
}
return $rowLabel;

View file

@ -0,0 +1,69 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome;
use Piwik\Db;
use Piwik\Menu\MenuTop;
use Piwik\Menu\MenuUser;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
class Menu extends \Piwik\Plugin\Menu
{
public function configureTopMenu(MenuTop $menu)
{
$login = Piwik::getCurrentUserLogin();
$user = APIUsersManager::getInstance()->getUser($login);
if (!empty($user['alias'])) {
$login = $user['alias'];
}
if (Plugin\Manager::getInstance()->isPluginActivated('Feedback')) {
$menu->registerMenuIcon('General_Help', 'icon-help');
$menu->addItem('General_Help', null, array('module' => 'Feedback', 'action' => 'index'), $order = 990, Piwik::translate('General_Help'));
}
$menu->registerMenuIcon($login, 'icon-user');
if (Piwik::isUserIsAnonymous()) {
if (Plugin\Manager::getInstance()->isPluginActivated('ScheduledReports')) {
$menu->addItem($login, null, array('module' => 'ScheduledReports', 'action' => 'index'), 970, Piwik::translate('UsersManager_PersonalSettings'));
} else {
$menu->addItem($login, null, array('module' => 'API', 'action' => 'listAllAPI'), 970, Piwik::translate('API_ReportingApiReference'));
}
} else {
$tooltip = sprintf('%s: %s', Piwik::translate('UsersManager_PersonalSettings'), $login);
$menu->addItem($login, null, array('module' => 'UsersManager', 'action' => 'userSettings'), 970, $tooltip);
}
$module = $this->getLoginModule();
if (Piwik::isUserIsAnonymous()) {
$menu->registerMenuIcon('Login_LogIn', 'icon-sign-in');
$menu->addItem('Login_LogIn', null, array('module' => $module, 'action' => false), 1000, Piwik::translate('Login_LogIn'));
} else {
$menu->registerMenuIcon('General_Logout', 'icon-sign-out');
$menu->addItem('General_Logout', null, array('module' => $module, 'action' => 'logout', 'idSite' => null), 1000, Piwik::translate('General_Logout'));
}
}
public function configureUserMenu(MenuUser $menu)
{
$menu->addPersonalItem(null, array(), 1, false);
$menu->addManageItem(null, array(), 2, false);
$menu->addPlatformItem(null, array(), 3, false);
}
private function getLoginModule()
{
return Piwik::getLoginPluginName();
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome;
/**
* CoreHome segment base class
*/
class Segment extends \Piwik\Plugin\Segment
{
protected function init()
{
$this->setCategory('General_Visit');
}
}

View file

@ -0,0 +1,223 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Plugins\CoreHome\Tracker;
use Piwik\Common;
use Piwik\Date;
use Piwik\Config;
use Piwik\EventDispatcher;
use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Tracker\Cache;
use Piwik\Tracker\Request;
use Piwik\Tracker\RequestProcessor;
use Piwik\Tracker\Settings;
use Piwik\Tracker\Visit\VisitProperties;
use Piwik\Tracker\VisitExcluded;
use Piwik\Tracker\VisitorRecognizer;
use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig;
/**
* Encapsulates core tracking logic related to visits.
*
* ## Request Metadata
*
* This RequestProcessor exposes the following metadata for the **CoreHome** plugin:
*
* * **visitorId**: A hash that identifies the current visitor being tracked. This value is
* calculated using the Piwik\Tracker\Settings;:getConfigId() method.
*
* Set in `processRequestParams()`.
*
* * **isVisitorKnown**: True if the current visitor has visited the site before. False if
* otherwise.
*
* Set in `processRequestParams()`.
*
* * **isNewVisit**: True if the current action is the start of a new visit, false if it
* is part of an ongoing visit.
*
* Set in `processRequestParams()`. Other RequestProcessors can override
* this value to force a new visit or stop a new visit.
*
* * **visitorNotFoundInDb**: True if the current visit could not be updated.
*
* Set by the Visit object.
*/
class VisitRequestProcessor extends RequestProcessor
{
// TODO: much of the logic in this class should be moved to new service class
/**
* @var EventDispatcher
*/
private $eventDispatcher;
/**
* @var VisitorRecognizer
*/
private $visitorRecognizer;
/**
* @var Settings
*/
private $userSettings;
/**
* @var int
*/
private $visitStandardLength;
public function __construct(EventDispatcher $eventDispatcher, VisitorRecognizer $visitorRecognizer, Settings $userSettings,
$visitStandardLength)
{
$this->eventDispatcher = $eventDispatcher;
$this->visitorRecognizer = $visitorRecognizer;
$this->userSettings = $userSettings;
$this->visitStandardLength = $visitStandardLength;
}
public function processRequestParams(VisitProperties $visitProperties, Request $request)
{
// the IP is needed by isExcluded() and GoalManager->recordGoals()
$visitProperties->setProperty('location_ip', $request->getIp());
// TODO: move VisitExcluded logic to here (or move to service class stored in DI)
$excluded = new VisitExcluded($request, $visitProperties->getProperty('location_ip'));
if ($excluded->isExcluded()) {
return true;
}
$privacyConfig = new PrivacyManagerConfig();
$ip = $request->getIpString();
if ($privacyConfig->useAnonymizedIpForVisitEnrichment) {
$ip = $visitProperties->getProperty('location_ip');
}
// visitor recognition
$visitorId = $this->userSettings->getConfigId($request, $ip);
$request->setMetadata('CoreHome', 'visitorId', $visitorId);
$isKnown = $this->visitorRecognizer->findKnownVisitor($visitorId, $visitProperties, $request);
$request->setMetadata('CoreHome', 'isVisitorKnown', $isKnown);
$isNewVisit = $this->isVisitNew($visitProperties, $request);
$request->setMetadata('CoreHome', 'isNewVisit', $isNewVisit);
return false;
}
public function afterRequestProcessed(VisitProperties $visitProperties, Request $request)
{
$ip = $visitProperties->getProperty('location_ip');
/**
* Triggered after visits are tested for exclusion so plugins can modify the IP address
* persisted with a visit.
*
* This event is primarily used by the **PrivacyManager** plugin to anonymize IP addresses.
*
* @param string &$ip The visitor's IP address.
*/
$this->eventDispatcher->postEvent('Tracker.setVisitorIp', array(&$ip));
$visitProperties->setProperty('location_ip', $ip);
}
/**
* Determines if the tracker if the current action should be treated as the start of a new visit or
* an action in an existing visit.
*
* Note: public only for tests.
*
* @param VisitProperties $visitProperties The current visit/visitor information.
* @param Request $request
* @return bool
*/
public function isVisitNew(VisitProperties $visitProperties, Request $request)
{
$isKnown = $request->getMetadata('CoreHome', 'isVisitorKnown');
if (!$isKnown) {
return true;
}
$isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit($visitProperties, $request);
if (!$isLastActionInTheSameVisit) {
Common::printDebug("Visitor detected, but last action was more than 30 minutes ago...");
return true;
}
$wasLastActionYesterday = $this->wasLastActionNotToday($visitProperties, $request);
$forceNewVisitAtMidnight = (bool) Config::getInstance()->Tracker['create_new_visit_after_midnight'];
if ($wasLastActionYesterday && $forceNewVisitAtMidnight) {
Common::printDebug("Visitor detected, but last action was yesterday...");
return true;
}
return false;
}
/**
* Returns true if the last action was done during the last 30 minutes
* @return bool
*/
protected function isLastActionInTheSameVisit(VisitProperties $visitProperties, Request $request)
{
$lastActionTime = $visitProperties->getProperty('visit_last_action_time');
return isset($lastActionTime)
&& false !== $lastActionTime
&& ($lastActionTime > ($request->getCurrentTimestamp() - $this->visitStandardLength));
}
/**
* Returns true if the last action was not today.
* @param VisitProperties $visitor
* @return bool
*/
private function wasLastActionNotToday(VisitProperties $visitProperties, Request $request)
{
$lastActionTime = $visitProperties->getProperty('visit_last_action_time');
if (empty($lastActionTime)) {
return false;
}
$idSite = $request->getIdSite();
$timezone = $this->getTimezoneForSite($idSite);
if (empty($timezone)) {
throw new UnexpectedWebsiteFoundException('An unexpected website was found, check idSite in the request');
}
$date = Date::factory((int)$lastActionTime, $timezone);
$now = $request->getCurrentTimestamp();
$now = Date::factory((int)$now, $timezone);
return $date->toString() !== $now->toString();
}
private function getTimezoneForSite($idSite) // TODO: duplicate function in Visit
{
try {
$site = Cache::getCacheWebsiteAttributes($idSite);
} catch (UnexpectedWebsiteFoundException $e) {
return null;
}
if (!empty($site['timezone'])) {
return $site['timezone'];
}
return null;
}
}

View file

@ -0,0 +1,114 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome;
use Piwik\Metrics\Formatter;
use Piwik\Plugins\CoreHome\Columns\VisitGoalBuyer;
class Visitor
{
private $details = array();
private $metricsFormatter = null;
public function __construct($details)
{
$this->details = $details;
$this->metricsFormatter = new Formatter();
}
function getTimestampFirstAction()
{
return strtotime($this->details['visit_first_action_time']);
}
function getVisitEcommerceStatusIcon()
{
$status = $this->getVisitEcommerceStatus();
if (in_array($status, array('ordered', 'orderedThenAbandonedCart'))) {
return "plugins/Morpheus/images/ecommerceOrder.gif";
} elseif ($status == 'abandonedCart') {
return "plugins/Morpheus/images/ecommerceAbandonedCart.gif";
}
// Note: it is important that there is no icon when there was no ecommerce conversion
return null;
}
function getVisitEcommerceStatus()
{
return VisitGoalBuyer::getVisitEcommerceStatusFromId($this->details['visit_goal_buyer']);
}
function isVisitorGoalConverted()
{
return $this->details['visit_goal_converted'];
}
function getVisitorGoalConvertedIcon()
{
return $this->isVisitorGoalConverted()
? "plugins/Morpheus/images/goal.png"
: null;
}
function getDaysSinceFirstVisit()
{
return $this->details['visitor_days_since_first'];
}
function getDaysSinceLastEcommerceOrder()
{
return $this->details['visitor_days_since_order'];
}
function getVisitorReturning()
{
$type = $this->details['visitor_returning'];
return $type == 2
? 'returningCustomer'
: ($type == 1
? 'returning'
: 'new');
}
function getVisitorReturningIcon()
{
$type = $this->getVisitorReturning();
if ($type == 'returning'
|| $type == 'returningCustomer'
) {
return "plugins/Live/images/returningVisitor.gif";
}
return null;
}
function getVisitCount()
{
return $this->details['visitor_count_visits'];
}
function getVisitLength()
{
return $this->details['visit_total_time'];
}
function getVisitLengthPretty()
{
return $this->metricsFormatter->getPrettyTimeFromSeconds($this->details['visit_total_time'], true);
}
function getUserId()
{
if (isset($this->details['user_id'])
&& strlen($this->details['user_id']) > 0) {
return $this->details['user_id'];
}
return null;
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome;
use Piwik\Common;
use Piwik\Piwik;
use Piwik\Translation\Translator;
use Piwik\View;
class Widgets extends \Piwik\Plugin\Widgets
{
protected $category = 'About Piwik';
/**
* @var Translator
*/
private $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
protected function init()
{
$this->addWidget('CoreHome_SupportPiwik', 'getDonateForm');
$this->addWidget('Installation_Welcome', 'getPromoVideo');
}
/**
* Renders and echo's the in-app donate form w/ slider.
*/
public function getDonateForm()
{
$view = new View('@CoreHome/getDonateForm');
if (Common::getRequestVar('widget', false)
&& Piwik::hasUserSuperUserAccess()) {
$view->footerMessage = $this->translator->translate('CoreHome_OnlyForSuperUserAccess');
}
return $view->render();
}
/**
* Renders and echo's HTML that displays the Piwik promo video.
*/
public function getPromoVideo()
{
$view = new View('@CoreHome/getPromoVideo');
$view->shareText = $this->translator->translate('CoreHome_SharePiwikShort');
$view->shareTextLong = $this->translator->translate('CoreHome_SharePiwikLong');
$view->promoVideoUrl = 'https://www.youtube.com/watch?v=OslfF_EH81g';
return $view->render();
}
}

View file

@ -0,0 +1,85 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('AjaxFormController', AjaxFormController);
AjaxFormController.$inject = ['piwikApi', '$filter'];
function AjaxFormController(piwikApi, $filter) {
var vm = this;
/**
* Set to non-null when a form submit request returns successfully. When successful, it will
* be the entire JSON parsed response of the request.
*
* @type {null|string}
*/
vm.successfulPostResponse = null;
/**
* Set to non-null when a form submit request results in an error. When an error occurs,
* it will be set to the string error message.
*
* @type {null|string}
*/
vm.errorPostResponse = null;
/**
* true if currently submitting a POST request, false if otherwise.
*
* @type {bool}
*/
vm.isSubmitting = false;
vm.submitForm = submitForm;
/**
* Sends a POST to the configured API method.
*/
function submitForm() {
var postParams;
vm.successfulPostResponse = null;
vm.errorPostResponse = null;
if (vm.sendJsonPayload) {
postParams = {data: JSON.stringify(vm.data)};
} else {
postParams = vm.data;
}
vm.isSubmitting = true;
piwikApi.post(
{ // GET params
module: 'API',
method: vm.submitApiMethod
},
postParams,
{ // request options
createErrorNotification: !vm.noErrorNotification
}
).then(function (response) {
vm.successResponse = response;
if (!vm.noSuccessNotification) {
var UI = require('piwik/UI');
var notification = new UI.Notification();
notification.show($filter('translate')('General_YourChangesHaveBeenSaved'), {
context: 'success',
type: 'toast',
id: 'ajaxHelper'
});
notification.scrollToNotification();
}
})['catch'](function (errorMessage) {
vm.errorPostResponse = errorMessage;
})['finally'](function () {
vm.isSubmitting = false;
});
}
}
})();

View file

@ -0,0 +1,142 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* AngularJS directive that manages an AJAX form.
*
* This directive will detect inputs & selects defined within an element and when a
* submit button is clicked, will post data from the inputs & selects to a Piwik API method.
*
* When the POST request is finished the result will, by default, be displayed as a
* notification.
*
* This directive accepts the following attributes:
*
* - **save-api-method**: **required** The Piwik API method that handles the POST request.
* - **send-json-payload**: Whether to send the data as a form encoded URL or to send it as JSON.
* If sending as JSON, the payload will still be a form encoded value,
* but will contain a JSON object like `{data: {...form data...}}`.
*
* This is for forms with lots of fields where having the same number
* of parameters in an API method would not be desired.
* - **no-error-notification**: If true, does not display an error notification if the AJAX post
* fails.
* - **no-success-notification**: If true, does not display an error notification if the AJAX
* results in success.
*
* **Custom Success/Error Handling**
*
* On success/failure, the response will be stored in controller scope. Child elements of a
* piwik-ajax-form element can access this data, and thus, can customize what happens when
* a form submit succeeds/fails.
*
* See the ajax-form.controller.js file for more info.
*
* Usage:
*
* <div piwik-ajax-form
* save-api-method="'MyPlugin.myFormSaveMethod'"
* send-json-payload="true"
* ng-model="myFormData">
*
* <h2>My Form</h2>
* <input name="myOption" value="myDefaultValue" type="text" />
* <input name="myOtherOption" type="checkbox" checked="checked" />
* <input type="submit" value="Submit" ng-disabled="ajaxForm.isSubmitting" />
*
* <div piwik-notification context='error' ng-show="errorPostResponse">ERROR!</div>
* </div>
*/
(function () {
angular.module('piwikApp').directive('piwikAjaxForm', piwikAjaxForm);
piwikAjaxForm.$inject = ['$parse'];
function piwikAjaxForm($parse) {
return {
restrict: 'A',
scope: {
submitApiMethod: '=',
sendJsonPayload: '=',
noErrorNotification: '=',
noSuccessNotification: '=',
useCustomDataBinding: '='
},
require: '?ngModel',
controller: 'AjaxFormController',
controllerAs: 'ajaxForm',
transclude: true,
compile: function (element, attrs) {
attrs.noErrorNotification = !! attrs.noErrorNotification;
return function (scope, element, attrs, ngModel, transclude) {
if (!scope.submitApiMethod) {
throw new Error("submitApiMethod is required");
}
scope.ajaxForm.submitApiMethod = scope.submitApiMethod;
scope.ajaxForm.sendJsonPayload = scope.sendJsonPayload;
scope.ajaxForm.noErrorNotification = scope.noErrorNotification;
scope.ajaxForm.noSuccessNotification = scope.noSuccessNotification;
scope.ajaxForm.data = {};
// if a model is supplied, initiate form data w/ model value
if (ngModel) {
var ngModelGetter = $parse(attrs.ngModel); // probably redundant, but I cannot find another way to
// get the ng model value here
scope.ajaxForm.data = ngModelGetter(scope.$parent);
}
// on change of any input, change appropriate value in model, but only if requested
if (!scope.useCustomDataBinding) {
element.on('change', 'input,select', function () {
setFormValueFromInput(this);
});
}
// on submit call controller submit method
element.on('click', 'input[type=submit]', function () {
scope.ajaxForm.submitForm();
});
// make sure child elements can access this directive's scope
transclude(scope, function(clone, scope) {
if (!scope.useCustomDataBinding) {
var $inputs = clone.find('input,select').not('[type=submit]');
// initialize form data to input values (include <select>s
$inputs.each(function () {
setFormValueFromInput(this, true);
});
}
element.append(clone);
});
function setFormValueFromInput(inputElement, skipScopeApply) {
var $ = angular.element,
name = $(inputElement).attr('name'),
val;
if ($(inputElement).attr('type') == 'checkbox') {
val = $(inputElement).is(':checked');
} else {
val = $(inputElement).val();
}
scope.ajaxForm.data[name] = val;
if (!skipScopeApply) {
scope.$apply();
}
}
};
}
};
}
})();

View file

@ -1,12 +1,12 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* See http://dev.piwik.org/trac/ticket/4795 "linking to #hash tag does not work after merging AngularJS"
* See https://github.com/piwik/piwik/issues/4795 "linking to #hash tag does not work after merging AngularJS"
*/
(function () {
@ -32,7 +32,12 @@
return;
}
var $node = $('#' + hash);
try {
var $node = $('#' + hash);
} catch (err) {
// on jquery syntax error, ignore so nothing is logged to the console
return;
}
if ($node && $node.length) {
scrollToAnchorNode($node);
@ -80,7 +85,9 @@
{
angular.module('piwikApp').run(['$rootScope', function ($rootScope) {
$rootScope.$on('$locationChangeStart', function (event, newUrl, oldUrl, $location) {
$rootScope.$on('$locationChangeStart', onLocationChangeStart);
function onLocationChangeStart (event, newUrl, oldUrl, $location) {
if (!newUrl) {
return;
@ -98,7 +105,7 @@
var hash = newUrl.substr(hashPos + 2);
scrollToAnchorIfPossible(hash, event);
});
}
}]);
}

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,28 +15,39 @@
* <div piwik-autocomplete-matched="searchTerm">{{ name }}</div>
* <input type="text" ng-model="searchTerm">
*/
angular.module('piwikApp.directive').directive('piwikAutocompleteMatched', function() {
return function(scope, element, attrs) {
var searchTerm;
(function () {
angular.module('piwikApp.directive').directive('piwikAutocompleteMatched', piwikAutocompleteMatched);
scope.$watch(attrs.piwikAutocompleteMatched, function(value) {
searchTerm = value;
updateText();
});
piwikAutocompleteMatched.$inject = ['piwik', '$sanitize'];
function updateText () {
if (!searchTerm || !element) {
return;
function piwikAutocompleteMatched(piwik, $sanitize) {
return {
priority: 10, // makes sure to render after other directives, otherwise the content might be overwritten again see https://github.com/piwik/piwik/pull/8467
link: function (scope, element, attrs) {
var searchTerm;
scope.$watch(attrs.piwikAutocompleteMatched, function (value) {
searchTerm = value;
updateText();
});
function updateText() {
if (!searchTerm || !element) {
return;
}
var content = piwik.helper.htmlEntities(element.text());
var startTerm = content.toLowerCase().indexOf(searchTerm.toLowerCase());
if (-1 !== startTerm) {
var word = content.substr(startTerm, searchTerm.length);
var escapedword = $sanitize(piwik.helper.htmlEntities(word));
content = content.replace(word, '<span class="autocompleteMatched">' + escapedword + '</span>');
element.html(content);
}
}
}
var content = element.html();
var startTerm = content.toLowerCase().indexOf(searchTerm.toLowerCase());
if (-1 !== startTerm) {
var word = content.substr(startTerm, searchTerm.length);
content = content.replace(word, '<span class="autocompleteMatched">' + word + '</span>');
element.html(content);
}
}
};
});
};
}
})();

View file

@ -1,43 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('piwikAutocompleteMatchedDirective', function() {
var $compile;
var $rootScope;
beforeEach(module('piwikApp.directive'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
function assertRenderedContentIs(query, expectedResult) {
var template = '<div piwik-autocomplete-matched="\'' + query + '\'">My Content</div>';
var element = $compile(template)($rootScope);
$rootScope.$digest();
expect(element.html()).to.eql(expectedResult);
}
describe('#piwikAutocompleteMatched()', function() {
it('should not change anything if query does not match the text', function() {
assertRenderedContentIs('Whatever', 'My Content');
});
it('should wrap the matching part and find case insensitive', function() {
assertRenderedContentIs('y cont', 'M<span class="autocompleteMatched">y Cont</span>ent');
});
it('should be able to wrap the whole content', function() {
assertRenderedContentIs('my content', '<span class="autocompleteMatched">My Content</span>');
});
it('should find matching content case sensitive', function() {
assertRenderedContentIs('My Co', '<span class="autocompleteMatched">My Co</span>ntent');
});
});
});

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -15,27 +15,36 @@
* </div>
* Will execute the "executeMyFunction" function in the current scope once the yes button is pressed.
*/
angular.module('piwikApp.directive').directive('piwikDialog', function(piwik) {
(function () {
angular.module('piwikApp.directive').directive('piwikDialog', piwikDialog);
return {
restrict: 'A',
link: function(scope, element, attrs) {
piwikDialog.$inject = ['piwik', '$parse'];
element.css('display', 'none');
function piwikDialog(piwik, $parse) {
element.on( "dialogclose", function() {
scope.$eval(attrs.piwikDialog+'=false');
});
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.piwikDialog, function(newValue, oldValue) {
if (newValue) {
piwik.helper.modalConfirm(element, {yes: function() {
if (attrs.yes) {
scope.$eval(attrs.yes);
}
}});
}
});
}
};
});
element.css('display', 'none');
element.on( "dialogclose", function() {
setTimeout(function () {
scope.$apply($parse(attrs.piwikDialog).assign(scope, false));
}, 0);
});
scope.$watch(attrs.piwikDialog, function(newValue, oldValue) {
if (newValue) {
piwik.helper.modalConfirm(element, {yes: function() {
if (attrs.yes) {
scope.$eval(attrs.yes);
setTimeout(function () { scope.$apply(); }, 0);
}
}});
}
});
}
};
}
})();

View file

@ -0,0 +1,9 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.directive', []);
})();

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,29 +12,35 @@
* Example:
* <div piwik-focus-anywhere-but-here="closeDialog()">my dialog</div>
*/
angular.module('piwikApp.directive').directive('piwikFocusAnywhereButHere', function($document){
return {
restrict: 'A',
link: function(scope, element, attr, ctrl) {
(function () {
angular.module('piwikApp.directive').directive('piwikFocusAnywhereButHere', piwikFocusAnywhereButHere);
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
scope.$apply(attr.piwikFocusAnywhereButHere);
piwikFocusAnywhereButHere.$inject = ['$document'];
function piwikFocusAnywhereButHere($document){
return {
restrict: 'A',
link: function(scope, element, attr, ctrl) {
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
scope.$apply(attr.piwikFocusAnywhereButHere);
}
}
}
function onEscapeHandler (event) {
if (event.which === 27) {
scope.$apply(attr.piwikFocusAnywhereButHere);
function onEscapeHandler (event) {
if (event.which === 27) {
scope.$apply(attr.piwikFocusAnywhereButHere);
}
}
}
$document.on('keyup', onEscapeHandler);
$document.on('mouseup', onClickOutsideElement);
scope.$on('$destroy', function() {
$document.off('mouseup', onClickOutsideElement);
$document.off('keyup', onEscapeHandler);
});
}
};
});
$document.on('keyup', onEscapeHandler);
$document.on('mouseup', onClickOutsideElement);
scope.$on('$destroy', function() {
$document.off('mouseup', onClickOutsideElement);
$document.off('keyup', onEscapeHandler);
});
}
};
}
})();

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -11,17 +11,23 @@
* Example:
* <input type="text" piwik-focus-if="view.editName">
*/
angular.module('piwikApp.directive').directive('piwikFocusIf', function($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.piwikFocusIf, function(newValue, oldValue) {
if (newValue) {
$timeout(function () {
element[0].focus();
}, 5);
}
});
}
};
});
(function () {
angular.module('piwikApp.directive').directive('piwikFocusIf', piwikFocusIf);
piwikFocusIf.$inject = ['$timeout'];
function piwikFocusIf($timeout) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch(attrs.piwikFocusIf, function(newValue, oldValue) {
if (newValue) {
$timeout(function () {
element[0].focus();
}, 5);
}
});
}
};
}
})();

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,10 +12,14 @@
* Example
* <a piwik-ignore-click ng-click="doSomething()" href="/">my link</a>
*/
angular.module('piwikApp.directive').directive('piwikIgnoreClick', function() {
return function(scope, element, attrs) {
$(element).click(function(event) {
event.preventDefault();
});
};
});
(function () {
angular.module('piwikApp.directive').directive('piwikIgnoreClick', piwikIgnoreClick);
function piwikIgnoreClick() {
return function(scope, element, attrs) {
$(element).click(function(event) {
event.preventDefault();
});
};
}
})();

View file

@ -1,5 +1,5 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
@ -12,16 +12,20 @@
* <div piwik-onenter="save()">
* <div piwik-onenter="showList=false">
*/
angular.module('piwikApp.directive').directive('piwikOnenter', function() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.piwikOnenter, {'event': event});
});
(function () {
angular.module('piwikApp.directive').directive('piwikOnenter', piwikOnenter);
event.preventDefault();
}
});
};
});
function piwikOnenter() {
return function(scope, element, attrs) {
element.bind("keydown keypress", function(event) {
if(event.which === 13) {
scope.$apply(function(){
scope.$eval(attrs.piwikOnenter, {'event': event});
});
event.preventDefault();
}
});
};
}
})();

View file

@ -0,0 +1,36 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Directive for easy & safe complex internationalization. This directive allows
* users to embed the sprintf arguments used in internationalization inside an HTML
* element. Since the HTML will eventually be sanitized by AngularJS, HTML can be used
* within the sprintf args. Using the filter, this is not possible w/o manually sanitizing
* and creating trusted HTML, which is not as safe.
*
* Note: nesting this directive is not supported.
*
* Usage:
* <span piwik-translate="Plugin_TranslationToken">
* first arg::<strong>second arg</strong>::{{ unsafeDataThatWillBeSanitized }}
* </span>
*/
(function () {
angular.module('piwikApp.directive').directive('piwikTranslate', piwikTranslate);
function piwikTranslate() {
return {
priority: 1,
restrict: 'A',
compile: function(element, attrs) {
var parts = element.html().split('::'),
translated = _pk_translate(attrs.piwikTranslate, parts);
element.html(translated);
}
};
}
})();

View file

@ -1,44 +1,49 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('evolution', evolutionFilter);
angular.module('piwikApp.filter').filter('evolution', function() {
function evolutionFilter() {
function calculateEvolution(currentValue, pastValue)
{
pastValue = parseInt(pastValue, 10);
currentValue = parseInt(currentValue, 10) - pastValue;
function calculateEvolution(currentValue, pastValue)
{
pastValue = parseInt(pastValue, 10);
currentValue = parseInt(currentValue, 10) - pastValue;
if (currentValue === 0 || isNaN(currentValue)) {
evolution = 0;
} else if (pastValue === 0 || isNaN(pastValue)) {
evolution = 100;
} else {
evolution = (currentValue / pastValue) * 100;
var evolution;
if (currentValue === 0 || isNaN(currentValue)) {
evolution = 0;
} else if (pastValue === 0 || isNaN(pastValue)) {
evolution = 100;
} else {
evolution = (currentValue / pastValue) * 100;
}
return evolution;
}
return evolution;
}
function formatEvolution(evolution)
{
evolution = Math.round(evolution);
function formatEvolution(evolution)
{
evolution = Math.round(evolution);
if (evolution > 0) {
evolution = '+' + evolution;
}
if (evolution > 0) {
evolution = '+' + evolution;
evolution += '%';
return evolution;
}
evolution += '%';
return function(currentValue, pastValue) {
var evolution = calculateEvolution(currentValue, pastValue);
return evolution;
return formatEvolution(evolution);
};
}
return function(currentValue, pastValue) {
var evolution = calculateEvolution(currentValue, pastValue);
return formatEvolution(evolution);
};
});
})();

View file

@ -1,7 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.filter', []);

View file

@ -1,8 +1,9 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.service', []);
(function () {
angular.module('piwikApp.filter', []);
})();

View file

@ -0,0 +1,26 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('htmldecode', htmldecode);
htmldecode.$inject = ['piwik'];
/**
* Be aware that this filter can cause XSS so only use it when you're sure it is safe.
* Eg it should be safe when it is afterwards escaped by angular sanitize again.
*/
function htmldecode(piwik) {
return function(text) {
if (text && text.length) {
return piwik.helper.htmlDecode(text);
}
return text;
};
}
})();

View file

@ -0,0 +1,21 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('length', length);
function length() {
return function(stringOrArray) {
if (stringOrArray && stringOrArray.length) {
return stringOrArray.length;
}
return 0;
};
}
})();

View file

@ -0,0 +1,16 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('prettyUrl', prettyUrl);
function prettyUrl() {
return function(input) {
return input.trim().replace('http://', '');
};
}
})();

View file

@ -1,13 +1,16 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('startFrom', startFrom);
angular.module('piwikApp.filter').filter('startFrom', function() {
return function(input, start) {
start = +start; //parse to int
return input.slice(start);
};
});
function startFrom() {
return function(input, start) {
start = +start; //parse to int
return input.slice(start);
};
}
})();

View file

@ -0,0 +1,41 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
describe('startFromFilter', function() {
var startFrom;
beforeEach(module('piwikApp.filter'));
beforeEach(inject(function($injector) {
var $filter = $injector.get('$filter');
startFrom = $filter('startFrom');
}));
describe('#startFrom()', function() {
it('should return all entries if index is zero', function() {
var result = startFrom([1,2,3], 0);
expect(result).to.eql([1,2,3]);
});
it('should return only partial entries if filter is higher than zero', function() {
var result = startFrom([1,2,3], 2);
expect(result).to.eql([3]);
});
it('should return no entries if start is higher than input length', function() {
var result = startFrom([1,2,3], 11);
expect(result).to.eql([]);
});
});
});
})();

View file

@ -1,40 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('startFromFilter', function() {
var startFrom;
beforeEach(module('piwikApp.filter'));
beforeEach(inject(function($injector) {
var $filter = $injector.get('$filter');
startFrom = $filter('startFrom');
}));
describe('#startFrom()', function() {
it('should return all entries if index is zero', function() {
var result = startFrom([1,2,3], 0);
expect(result).to.eql([1,2,3]);
});
it('should return only partial entries if filter is higher than zero', function() {
var result = startFrom([1,2,3], 2);
expect(result).to.eql([3]);
});
it('should return no entries if start is higher than input length', function() {
var result = startFrom([1,2,3], 11);
expect(result).to.eql([]);
});
});
});

View file

@ -1,19 +1,22 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('translate', translate);
angular.module('piwikApp.filter').filter('translate', function() {
function translate() {
return function(key, value1, value2, value3) {
var values = [];
if (arguments && arguments.length > 1) {
for (var index = 1; index < arguments.length; index++) {
values.push(arguments[index]);
return function(key, value1, value2, value3) {
var values = [];
if (arguments && arguments.length > 1) {
for (var index = 1; index < arguments.length; index++) {
values.push(arguments[index]);
}
}
}
return _pk_translate(key, values);
};
});
return _pk_translate(key, values);
};
}
})();

View file

@ -0,0 +1,20 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('trim', trim);
function trim() {
return function(string) {
if (string) {
return $.trim('' + string);
}
return string;
};
}
})();

View file

@ -0,0 +1,21 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.filter').filter('ucfirst', ucfirst);
function ucfirst() {
return function(value) {
if (!value) {
return value;
}
var firstLetter = (value + '').charAt(0).toUpperCase();
return firstLetter + value.substr(1);
};
}
})();

View file

@ -1,186 +1,305 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.service').factory('piwikApi', function ($http, $q, $rootScope, piwik, $window) {
// see https://github.com/piwik/piwik/issues/5094 used to detect an ad blocker
var hasBlockedContent = false;
var url = 'index.php';
var format = 'json';
var getParams = {};
var postParams = {};
var requestHandle = null;
(function () {
angular.module('piwikApp.service').factory('piwikApi', piwikApiService);
var piwikApi = {};
piwikApiService.$inject = ['$http', '$q', '$rootScope', 'piwik', '$window'];
/**
* Adds params to the request.
* If params are given more then once, the latest given value is used for the request
*
* @param {object} params
* @return {void}
*/
function addParams (params) {
if (typeof params == 'string') {
params = piwik.broadcast.getValuesFromUrl(params);
function piwikApiService ($http, $q, $rootScope, piwik, $window) {
var url = 'index.php';
var format = 'json';
var getParams = {};
var postParams = {};
var allRequests = [];
/**
* Adds params to the request.
* If params are given more then once, the latest given value is used for the request
*
* @param {object} params
* @return {void}
*/
function addParams (params) {
if (typeof params == 'string') {
params = piwik.broadcast.getValuesFromUrl(params);
}
for (var key in params) {
getParams[key] = params[key];
}
}
for (var key in params) {
getParams[key] = params[key];
function reset () {
getParams = {};
postParams = {};
}
}
function reset () {
getParams = {};
postParams = {};
}
function isErrorResponse(response) {
return response && response.result == 'error';
}
/**
* Send the request
* @return $promise
*/
function send () {
function createResponseErrorNotification(response, options) {
if (response.message
&& options.createErrorNotification
) {
var UI = require('piwik/UI');
var notification = new UI.Notification();
notification.show(response.message, {
context: 'error',
type: 'toast',
id: 'ajaxHelper',
placeat: options.placeat
});
notification.scrollToNotification();
}
}
var deferred = $q.defer();
var requestHandle = deferred;
/**
* Send the request
* @return $promise
*/
function send (options) {
if (!options) {
options = {};
}
var onError = function (message) {
deferred.reject(message);
requestHandle = null;
};
if (options.createErrorNotification === undefined) {
options.createErrorNotification = true;
}
var onSuccess = function (response) {
if (response && response.result == 'error') {
function onSuccess(response)
{
response = response.data;
if (response.message) {
onError(response.message);
if (!angular.isDefined(response) || response === null) {
return $q.reject(null);
var UI = require('piwik/UI');
var notification = new UI.Notification();
notification.show(response.message, {
context: 'error',
type: 'toast',
id: 'ajaxHelper'
});
notification.scrollToNotification();
} else if (isErrorResponse(response)) {
createResponseErrorNotification(response, options);
return $q.reject(response.message || null);
} else {
onError(null);
return response;
}
}
function onError(response)
{
var message = 'Something went wrong';
if (response && (response.status === 0 || response.status === -1)) {
message = 'Request was possibly aborted';
}
} else {
deferred.resolve(response);
return $q.reject(message);
}
requestHandle = null;
};
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
// ie 8,9,10 caches ajax requests, prevent this
'cache-control': 'no-cache'
};
var deferred = $q.defer(),
requestPromise = deferred.promise;
var ajaxCall = {
method: 'POST',
url: url,
responseType: format,
params: _mixinDefaultGetParams(getParams),
data: $.param(getPostParams(postParams)),
timeout: deferred.promise,
headers: headers
};
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
// ie 8,9,10 caches ajax requests, prevent this
'cache-control': 'no-cache'
};
$http(ajaxCall).success(onSuccess).error(onError);
var ajaxCall = {
method: 'POST',
url: url,
responseType: format,
params: _mixinDefaultGetParams(getParams),
data: $.param(getPostParams(postParams)),
timeout: requestPromise,
headers: headers
};
return deferred.promise;
}
var promise = $http(ajaxCall).then(onSuccess, onError);
// we can't modify requestPromise directly and add an abort method since for some reason it gets
// removed after then/finally/catch is called.
var addAbortMethod = function (to, deferred) {
return {
then: function () {
return addAbortMethod(to.then.apply(to, arguments), deferred);
},
'finally': function () {
return addAbortMethod(to['finally'].apply(to, arguments), deferred);
},
'catch': function () {
return addAbortMethod(to['catch'].apply(to, arguments), deferred);
},
abort: function () {
deferred.resolve();
return this;
}
};
};
var request = addAbortMethod(promise, deferred);
allRequests.push(request);
return request;
}
/**
* Get the parameters to send as POST
*
* @param {object} params parameter object
* @return {object}
* @private
*/
function getPostParams (params) {
params.token_auth = piwik.token_auth;
return params;
}
/**
* Mixin the default parameters to send as GET
*
* @param {object} getParamsToMixin parameter object
* @return {object}
* @private
*/
function _mixinDefaultGetParams (getParamsToMixin) {
var segment = piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1]);
// we have to decode the value manually because broadcast will not decode anything itself. if we don't,
// angular will encode it again before sending the value in an HTTP request.
segment = decodeURIComponent(segment);
var defaultParams = {
idSite: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'),
period: piwik.period || piwik.broadcast.getValueFromUrl('period'),
segment: segment
};
// never append token_auth to url
if (getParamsToMixin.token_auth) {
getParamsToMixin.token_auth = null;
delete getParamsToMixin.token_auth;
}
for (var key in defaultParams) {
if (!getParamsToMixin[key] && !postParams[key] && defaultParams[key]) {
getParamsToMixin[key] = defaultParams[key];
}
}
// handle default date & period if not already set
if (!getParamsToMixin.date && !postParams.date) {
getParamsToMixin.date = piwik.currentDateString || piwik.broadcast.getValueFromUrl('date');
if (getParamsToMixin.period == 'range' && piwik.currentDateString) {
getParamsToMixin.date = piwik.startDateString + ',' + getParamsToMixin.date;
}
}
return getParamsToMixin;
}
function abortAll() {
reset();
allRequests.forEach(function (request) {
request.abort();
});
allRequests = [];
}
function abort () {
abortAll();
}
/**
* Perform a reading API request.
* @param getParams
*/
function fetch (getParams, options) {
getParams.module = getParams.module || 'API';
getParams.format = 'JSON2';
addParams(getParams, 'GET');
var promise = send(options);
reset();
return promise;
}
function post(getParams, _postParams_, options) {
if (_postParams_) {
postParams = _postParams_;
}
return fetch(getParams, options);
}
/**
* Convenience method that will perform a bulk request using Piwik's API.getBulkRequest method.
* Bulk requests allow you to execute multiple Piwik requests with one HTTP request.
*
* @param {object[]} requests
* @param {object} options
* @return {HttpPromise} a promise that is resolved when the request finishes. The argument passed
* to the .then(...) callback will be an array with one element per request
* made.
*/
function bulkFetch(requests, options) {
var bulkApiRequestParams = {
urls: requests.map(function (requestObj) { return '?' + $.param(requestObj); })
};
var deferred = $q.defer(),
requestPromise = post({method: "API.getBulkRequest"}, bulkApiRequestParams, options).then(function (response) {
if (!(response instanceof Array)) {
response = [response];
}
// check for errors
for (var i = 0; i != response.length; ++i) {
var specificResponse = response[i];
if (isErrorResponse(specificResponse)) {
deferred.reject(specificResponse.message || null);
createResponseErrorNotification(specificResponse, options || {});
return;
}
}
deferred.resolve(response);
})['catch'](function () {
deferred.reject.apply(deferred, arguments);
});
return deferred.promise;
}
/**
* Get the parameters to send as POST
*
* @param {object} params parameter object
* @return {object}
* @private
*/
function getPostParams () {
return {
token_auth: piwik.token_auth
bulkFetch: bulkFetch,
post: post,
fetch: fetch,
/**
* @deprecated
*/
abort: abort,
abortAll: abortAll
};
}
/**
* Mixin the default parameters to send as GET
*
* @param {object} getParamsToMixin parameter object
* @return {object}
* @private
*/
function _mixinDefaultGetParams (getParamsToMixin) {
var defaultParams = {
idSite: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'),
period: piwik.period || piwik.broadcast.getValueFromUrl('period'),
segment: piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1])
};
// never append token_auth to url
if (getParamsToMixin.token_auth) {
getParamsToMixin.token_auth = null;
delete getParamsToMixin.token_auth;
}
for (var key in defaultParams) {
if (!getParamsToMixin[key] && !postParams[key] && defaultParams[key]) {
getParamsToMixin[key] = defaultParams[key];
}
}
// handle default date & period if not already set
if (!getParamsToMixin.date && !postParams.date) {
getParamsToMixin.date = piwik.currentDateString || piwik.broadcast.getValueFromUrl('date');
if (getParamsToMixin.period == 'range' && piwik.currentDateString) {
getParamsToMixin.date = piwik.startDateString + ',' + getParamsToMixin.date;
}
}
return getParamsToMixin;
}
piwikApi.abort = function () {
reset();
if (requestHandle) {
requestHandle.resolve();
requestHandle = null;
}
};
/**
* Perform a reading API request.
* @param getParams
*/
piwikApi.fetch = function (getParams) {
getParams.module = 'API';
getParams.format = 'JSON';
addParams(getParams, 'GET');
var promise = send();
reset();
return promise;
};
piwikApi.post = function (getParams, _postParams_) {
if (_postParams_) {
postParams = _postParams_;
}
return this.fetch(getParams);
};
return piwikApi;
});
})();

View file

@ -0,0 +1,273 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
describe('piwikApiClient', function () {
var piwikApi,
$httpBackend;
if (!window.piwik) window.piwik = {};
if (!window.piwik.UI) window.piwik.UI = {};
if (!window.piwik.UI.Notification) {
window.piwik.UI.Notification = function () {
this.show = function () {};
this.scrollToNotification = function () {};
return this;
};
}
beforeEach(module('piwikApp.service'));
beforeEach(inject(function($injector) {
piwikApi = $injector.get('piwikApi');
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('POST', /.*getBulkRequest.*/, /.*errorAction.*/).respond(function (method, url, data, headers) {
url = url.replace(/date=[^&]+/, "date=");
var errorResponse = {result: 'error', message: "error message"},
successResponse= "Response #2: " + url + " - " + data;
return [200, [errorResponse, successResponse]];
});
$httpBackend.when('POST', /.*getBulkRequest.*/).respond(function (method, url, data, headers) {
url = url.replace(/date=[^&]+/, "date=");
var responses = [
"Response #1: " + url + " - " + data,
"Response #2: " + url + " - " + data
];
return [200, JSON.stringify(responses)];
});
$httpBackend.when('POST', /.*/).respond(function (method, url, data, headers) {
url = url.replace(/date=[^&]+/, "date=");
return [200, "Request url: " + url];
});
}));
it("should successfully send a request to Piwik when fetch is called", function (done) {
piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day");
done();
}).catch(function (ex) {
done(ex);
});
$httpBackend.flush();
});
it("should chain multiple then callbacks correctly when a fetch succeeds", function (done) {
var firstThenDone = false;
piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
firstThenDone = true;
return "newval";
}).then(function (response) {
expect(firstThenDone).to.equal(true);
expect(response).to.equal("newval");
done();
}).catch(function (ex) {
done(ex);
});
$httpBackend.flush();
});
it("should fail when multiple aborts are issued", function (done) {
var request = piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
done(new Error("Aborted request succeeded but should fail!"));
}).catch(function (ex) {
done();
});
request.abort();
request.abort();
$httpBackend.flush();
request.abort();
});
it("should send multiple requests concurrently when fetch is called more than once", function (done) {
var request1Done, request2Done;
function finishIfBothDone() {
if (request1Done && request2Done) {
done();
}
}
piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day");
request1Done = true;
finishIfBothDone();
}).catch(function (ex) {
done(ex);
});
piwikApi.fetch({
method: "SomeOtherPlugin.action"
}).then(function (response) {
expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day");
request2Done = true;
finishIfBothDone();
}).catch(function (ex) {
done(ex);
});
$httpBackend.flush();
});
it("should abort individual requests when abort() is called on a promise", function (done) {
var request1Done, request2Done;
function finishIfBothDone() {
if (request1Done && request2Done) {
done();
}
}
var request = piwikApi.fetch({
method: "SomePlugin.waitAction"
}).then(function (response) {
done(new Error("Aborted request finished!"));
}).catch(function (ex) {
request1Done = true;
finishIfBothDone();
});
piwikApi.fetch({
method: "SomeOtherPlugin.action"
}).then(function (response) {
expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day");
request2Done = true;
finishIfBothDone();
}).catch(function (ex) {
done(ex);
});
request.abort();
$httpBackend.flush();
});
it("should abort all requests when abortAll() is called on the piwikApi", function (done) {
var request1Done, request2Done;
function finishIfBothDone() {
if (request1Done && request2Done) {
done();
}
}
piwikApi.fetch({
method: "SomePlugin.waitAction"
}).then(function (response) {
done(new Error("Aborted request finished (request 1)!"));
}).catch(function (ex) {
request1Done = true;
finishIfBothDone();
});
piwikApi.fetch({
method: "SomePlugin.waitAction"
}).then(function (response) {
done(new Error("Aborted request finished (request 2)!"));
}).catch(function (ex) {
request2Done = true;
finishIfBothDone();
});
piwikApi.abortAll();
$httpBackend.flush();
});
it("should perform a bulk request correctly when bulkFetch is called on the piwikApi", function (done) {
piwikApi.bulkFetch([
{
method: "SomePlugin.action",
param: "value"
},
{
method: "SomeOtherPlugin.action"
}
]).then(function (response) {
var restOfExpected = "index.php?date=&format=JSON2&idSite=1&method=API.getBulkRequest&" +
"module=API&period=day - urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3D" +
"value&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action&token_auth=100bf5eeeed1468f3f9d93750044d3dd";
expect(response.length).to.equal(2);
expect(response[0]).to.equal("Response #1: " + restOfExpected);
expect(response[1]).to.equal("Response #2: " + restOfExpected);
done();
}).catch(function (ex) {
done(ex);
});
$httpBackend.flush();
});
it("should correctly handle errors in a bulk request response", function (done) {
piwikApi.bulkFetch([
{
method: "SomePlugin.errorAction"
},
{
method: "SomeOtherPlugin.whatever"
}
]).then(function (response) {
done(new Error("promise resolved after bulkFetch request returned an error (response = " + JSON.stringify(response) + ")"));
}).catch(function (error) {
expect(error).to.equal("error message");
done();
});
$httpBackend.flush();
});
it("shuld correctly handle errors in a bulk request response, regardless of error location", function (done) {
piwikApi.bulkFetch([
{
method: "SomeOtherPlugin.whatever"
},
{
method: "SomePlugin.errorAction"
}
]).then(function (response) {
done(new Error("promise resolved after bulkFetch request returned an error (response = " + JSON.stringify(response) + ")"));
}).catch(function (error) {
expect(error).to.equal("error message");
done();
});
$httpBackend.flush();
});
});
})();

View file

@ -1,13 +1,16 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp.service').service('piwik', piwikService);
angular.module('piwikApp.service').service('piwik', function () {
function piwikService() {
piwik.helper = piwikHelper;
piwik.broadcast = broadcast;
return piwik;
});
piwik.helper = piwikHelper;
piwik.broadcast = broadcast;
return piwik;
}
})();

View file

@ -0,0 +1,38 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
describe('piwikService', function() {
var piwikService;
beforeEach(module('piwikApp.service'));
beforeEach(inject(function($injector) {
piwikService = $injector.get('piwik');
}));
describe('#piwikService', function() {
it('should be the same as piwik global var', function() {
piwik.should.equal(piwikService);
});
it('should mixin broadcast', function() {
expect(piwikService.broadcast).to.be.an('object');
});
it('should mixin piwikHelper', function() {
expect(piwikService.helper).to.be.an('object');
});
});
describe('#piwik_url', function() {
it('should contain the piwik url', function() {
expect(piwikService.piwik_url).to.eql('http://localhost/');
});
});
});
})();

View file

@ -1,37 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
describe('piwikService', function() {
var piwikService;
beforeEach(module('piwikApp.service'));
beforeEach(inject(function($injector) {
piwikService = $injector.get('piwik');
}));
describe('#piwikService', function() {
it('should be the same as piwik global var', function() {
piwik.should.equal(piwikService);
});
it('should mixin broadcast', function() {
expect(piwikService.broadcast).to.be.an('object');
});
it('should mixin piwikHelper', function() {
expect(piwikService.helper).to.be.an('object');
});
});
describe('#piwik_url', function() {
it('should contain the piwik url', function() {
expect(piwikService.piwik_url).to.eql('http://localhost/');
});
});
});

View file

@ -1,8 +1,9 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp.directive', []);
(function () {
angular.module('piwikApp.service', []);
})();

View file

@ -0,0 +1,90 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* AngularJS service that handles the popover query parameter for Piwik's angular code.
*
* If the popover parameter's first part is the name of an existing AngularJS directive,
* a dialog is created using ngDialog with the contents being an element with that directive.
* The other parts of the parameter are treated as attributes for the element, eg,
* `"mydirective:myparam=val:myotherparam=val2"`.
*
* It should not be necessary to use this service directly, instead the piwik-dialogtoggler
* directive should be used.
*
* TODO: popover as a query parameter refers less to dialogs and more to any popup window
* (ie, not necessarily modal). should replace it w/ 'dialog' or maybe 'modal'.
*/
(function () {
angular.module('piwikApp').factory('piwikDialogtogglerUrllistener', piwikDialogtogglerUrllistener);
piwikDialogtogglerUrllistener.$inject = ['$rootScope', '$location', '$injector', '$rootElement', 'ngDialog'];
function piwikDialogtogglerUrllistener($rootScope, $location, $injector, $rootElement, ngDialog) {
var service = {},
dialogQueryParamName = 'popover';
function getHtmlFromDialogQueryParam(paramValue) {
var info = paramValue.split(':'),
directiveName = info.shift(),
dialogContent = '';
dialogContent += '<div ' + directiveName;
angular.forEach(info, function (argumentAssignment) {
var pair = argumentAssignment.split('='),
key = pair[0],
value = pair[1];
dialogContent += ' ' + key + '="' + decodeURIComponent(value) + '"';
});
dialogContent += '/>';
return dialogContent;
}
function directiveExists(directiveAttributeString) {
// NOTE: directiveNormalize is not exposed by angularjs and the devs don't seem to want to expose it:
// https://github.com/angular/angular.js/issues/7955
// so logic is duplicated here.
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i,
directiveName = angular.element.camelCase(directiveAttributeString.replace(PREFIX_REGEXP, ''));
return $injector.has(directiveName + 'Directive');
}
service.checkUrlForDialog = function () {
var dialogParamValue = $location.search()[dialogQueryParamName];
if (dialogParamValue && directiveExists(dialogParamValue)) {
var dialog = ngDialog.open({
template: getHtmlFromDialogQueryParam(dialogParamValue),
plain: true,
className: ''
});
dialog.closePromise.then(function () {
$location.search(dialogQueryParamName, null);
});
}
};
service.propagatePersistedDialog = function (directive, attributes) {
var paramValue = directive;
angular.forEach(attributes, function (value, name) {
paramValue += ':' + name + '=' + encodeURIComponent(value);
});
$location.search(dialogQueryParamName, paramValue);
};
$rootScope.$on('$locationChangeSuccess', function () {
service.checkUrlForDialog();
});
service.checkUrlForDialog(); // check on initial page load
return service;
}
})();

View file

@ -0,0 +1,68 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Controller for the piwikDialogToggler directive. Adds a couple methods to the
* scope allowing elements to open and close dialogs.
*/
(function () {
angular.module('piwikApp').controller('DialogTogglerController', DialogTogglerController);
DialogTogglerController.$inject = ['$scope', 'piwik', 'ngDialog', 'piwikDialogtogglerUrllistener'];
function DialogTogglerController($scope, piwik, ngDialog, piwikDialogtogglerUrllistener) {
/**
* Open a new dialog window using ngDialog.
*
* @param {object|string} contentsInfo If an object, it is assumed to be ngDialog open(...) config and is
* passed to ngDialog.open unaltered.
* If a string that beings with '#', we assume it is an ID of an element
* with the dialog contents. (Note: ngDialog doesn't appear to support arbitrary
* selectors).
* If a string that ends with .html, we assume it is a link to a an angular
* template.
* Otherwise we assume it is a raw angular
* @return {object} Returns the result of ngDialog.open. Can be used to close the dialog or listen for
* when the dialog is closed.
*/
$scope.open = function (contentsInfo) {
var ngDialogInfo;
if (typeof(contentsInfo) == 'object') { // is info to pass directly to ngDialog
ngDialogInfo = contentsInfo;
} else if (contentsInfo.substr(0, 1) == '#') { // is ID of an element
ngDialogInfo = {template: contentsInfo.substr(1)};
} else if (contentsInfo.substr(-4) == '.html') { // is a link to an .html file
ngDialogInfo = {template: contentsInfo};
} else { // is a raw HTML string
ngDialogInfo = {template: contentsInfo, plain: true};
}
return ngDialog.open(ngDialogInfo);
};
/**
* Opens a persisted dialog. Persisted dialogs are dialogs that will be launched on reload
* of the current URL. They are accomplished by modifying the URL and adding a 'popover'
* query parameter.
*
* @param {string} directive The denormalized name of an angularjs directive. An element with
* this directive will be the contents of the dialog.
* @param {object} attributes Key value mapping of the HTML attributes to add to the dialog's
* contents element.
*/
$scope.persist = function (directive, attributes) {
piwikDialogtogglerUrllistener.propagatePersistedDialog(directive, attributes);
};
/**
* Closes the currently open dialog window.
*/
$scope.close = function () {
ngDialog.close();
};
}
})();

View file

@ -0,0 +1,30 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Directive for an element (such as a link) that creates and/or closes dialogs.
*
* Usage:
* <a piwik-dialogtoggler href="#" ng-click="open(...)" />
*
* or:
*
* <div piwik-dialogtoggler>
* <a href="#" ng-click="open(...)">Open</a>
* <a href="#" ng-click="close()">Close</a>
* </div>
*/
(function () {
angular.module('piwikApp').directive('piwikDialogtoggler', piwikDialogtoggler);
function piwikDialogtoggler() {
return {
restrict: 'A',
controller: 'DialogTogglerController'
};
}
})();

View file

@ -0,0 +1,70 @@
.ngdialog {
position:absolute;
}
.ngdialog-overlay {
opacity: 0.6;
background: none #000;
position: fixed;
z-index: 1000;
}
.ngdialog-content {
z-index: 1001;
width: 950px;
border-radius: 4px;
margin: 0 auto;
max-width: 100%;
background-color: @theme-color-background-base;
padding: 1em 18px;
position: relative;
top: 100px;
h2:first-of-type {
line-height:24px;
padding:0 0 1em;
}
}
// remove some ngdialog animations (the remaining one is required for closing the dialog)
.ngdialog-overlay, .ngdialog.ngdialog-closing .ngdialog-overlay,.ngdialog-content {
-webkit-animation: none;
animation: none;
}
.ngdialog-close {
// close button should be styled the same as other buttons
.submit;
position: absolute;
right: 9px;
top: 18px;
width: 21px;
margin: 0 0 0 0;
height: 20px;
&:before {
font-family:inherit;
content:'';
display:inline-block;
// center in div
position:absolute;
top: 50%;
left: 50%;
margin-top: -8px;
margin-left: -8px;
// from jquery-ui css
background-image: url(libs/jquery/themes/base/images/ui-icons_888888_256x240.png);
background-position: -96px -128px;
width: 16px;
height: 16px;
opacity:0.5;
}
&:hover:before {
background-image: url(libs/jquery/themes/base/images/ui-icons_454545_256x240.png);
}
}

View file

@ -1,66 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
*
* <h2 piwik-enriched-headline>All Websites Dashboard</h2>
* -> uses "All Websites Dashboard" as featurename
*
* <h2 piwik-enriched-headline feature-name="All Websites Dashboard">All Websites Dashboard (Total: 309 Visits)</h2>
* -> custom featurename
*
* <h2 piwik-enriched-headline help-url="http://piwik.org/guide">All Websites Dashboard</h2>
* -> shows help icon and links to external url
*
* <h2 piwik-enriched-headline>All Websites Dashboard
* <div class="inlineHelp>My <strong>inline help</strong></div>
* </h2>
* -> shows help icon to display inline help on click. Note: You can combine inlinehelp and help-url
*/
angular.module('piwikApp').directive('piwikEnrichedHeadline', function($document, piwik, $filter){
var defaults = {
helpUrl: ''
};
return {
transclude: true,
restrict: 'A',
scope: {
helpUrl: '@',
featureName: '@'
},
templateUrl: 'plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.html?cb=' + piwik.cacheBuster,
compile: function (element, attrs) {
for (var index in defaults) {
if (!attrs[index]) { attrs[index] = defaults[index]; }
}
return function (scope, element, attrs) {
var helpNode = $('[ng-transclude] .inlineHelp', element);
if ((!helpNode || !helpNode.length) && element.next()) {
// hack for reports :(
helpNode = element.next().find('.reportDocumentation');
}
if (helpNode && helpNode.length) {
if ($.trim(helpNode.text())) {
scope.inlineHelp = $.trim(helpNode.html());
}
helpNode.remove();
}
if (!attrs.featureName) {
attrs.featureName = $.trim(element.text());
}
};
}
};
});

View file

@ -1,18 +1,22 @@
<div class="enrichedHeadline"
ng-mouseenter="view.showIcons=true" ng-mouseleave="view.showIcons=false">
<span ng-transclude></span>
<div ng-show="!editUrl" class="title" ng-transclude tabindex="6"></div>
<a ng-show="editUrl" class="title" href="{{ editUrl }}"
title="{{ 'CoreHome_ClickToEditX'|translate:featureName }}"
ng-transclude ></a>
<span ng-show="view.showIcons">
<a ng-if="helpUrl && !inlineHelp"
rel="noreferrer"
target="_blank"
href="{{ helpUrl }}"
title="{{ 'CoreHome_ExternalHelp'|translate }}"
class="helpIcon"></a>
class="helpIcon"><span class="icon-help"></span></a>
<a ng-if="inlineHelp"
title="{{ 'General_Help'|translate }}"
ng-click="view.showInlineHelp=!view.showInlineHelp"
class="helpIcon"></a>
class="helpIcon"><span class="icon-help"></span></a>
<div class="ratingIcons"
piwik-rate-feature
@ -22,8 +26,9 @@
<div class="inlineHelp" ng-show="view.showIcons && view.showInlineHelp">
<div ng-bind-html="inlineHelp"></div>
<a ng-if="helpUrl"
rel="noreferrer"
target="_blank"
href="{{ helpUrl }}"
class="readMore">{{ 'General_MoreDetails'|translate }}</a>
</div>
</div>
</div>

View file

@ -0,0 +1,77 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
*
* <h2 piwik-enriched-headline>All Websites Dashboard</h2>
* -> uses "All Websites Dashboard" as featurename
*
* <h2 piwik-enriched-headline feature-name="All Websites Dashboard">All Websites Dashboard (Total: 309 Visits)</h2>
* -> custom featurename
*
* <h2 piwik-enriched-headline help-url="http://piwik.org/guide">All Websites Dashboard</h2>
* -> shows help icon and links to external url
*
* <h2 piwik-enriched-headline edit-url="index.php?module=Foo&action=bar&id=4">All Websites Dashboard</h2>
* -> makes the headline clickable linking to the specified url
*
* <h2 piwik-enriched-headline>All Websites Dashboard
* <div class="inlineHelp">My <strong>inline help</strong></div>
* </h2>
* -> shows help icon to display inline help on click. Note: You can combine inlinehelp and help-url
*/
(function () {
angular.module('piwikApp').directive('piwikEnrichedHeadline', piwikEnrichedHeadline);
piwikEnrichedHeadline.$inject = ['$document', 'piwik', '$filter'];
function piwikEnrichedHeadline($document, piwik, $filter){
var defaults = {
helpUrl: '',
editUrl: ''
};
return {
transclude: true,
restrict: 'A',
scope: {
helpUrl: '@',
editUrl: '@',
featureName: '@'
},
templateUrl: 'plugins/CoreHome/angularjs/enrichedheadline/enrichedheadline.directive.html?cb=' + piwik.cacheBuster,
compile: function (element, attrs) {
for (var index in defaults) {
if (!attrs[index]) { attrs[index] = defaults[index]; }
}
return function (scope, element, attrs) {
var helpNode = $('[ng-transclude] .inlineHelp', element);
if ((!helpNode || !helpNode.length) && element.next()) {
// hack for reports :(
helpNode = element.next().find('.reportDocumentation');
}
if (helpNode && helpNode.length) {
if ($.trim(helpNode.text())) {
scope.inlineHelp = $.trim(helpNode.html());
}
helpNode.remove();
}
if (!attrs.featureName) {
attrs.featureName = $.trim(element.find('.title').first().text());
}
};
}
};
}
})();

View file

@ -0,0 +1,60 @@
.inlineHelp {
display: none;
}
[piwik-enriched-headline] {
visibility: hidden;
height: 47px;
}
[piwik-enriched-headline].ng-isolate-scope {
visibility: visible;
height: auto;
}
.enrichedHeadline {
min-height: 22px;
a.title {
cursor: pointer;
}
.title {
color: @color-black-piwik;
display:inline-block;
}
.inlineHelp {
display: block;
background: #F7F7F7;
font-size: 12px;
font-weight: normal;
border: 1px solid #E4E5E4;
margin: 10px 0 10px 0;
padding: 10px;
border-radius: 4px;
max-width: 500px;
.readMore {
margin-top: 10px;
display: inline-block;
font-weight: bold;
}
}
.ratingIcons {
display: inline-block;
vertical-align: bottom;
}
.helpIcon {
cursor: pointer;
display: inline-block;
margin: 0 0 -1px 4px;
opacity: 0.3;
font-size: 15px;
&:hover {
opacity: 0.9;
}
}
}

View file

@ -1,44 +0,0 @@
.inlineHelp {
display: none;
}
.enrichedHeadline {
min-height: 22px;
.inlineHelp {
display:block;
background: #F7F7F7;
font-size: 12px;
font-weight: normal;
border: 1px solid #E4E5E4;
margin: 10px 0 10px 0;
padding: 10px;
border-radius: 4px;
max-width: 500px;
.readMore {
margin-top: 10px;
display: inline-block;
font-weight: bold;
}
}
.ratingIcons {
display:inline-block;
vertical-align: bottom;
}
.helpIcon:hover {
opacity: 0.9;
}
.helpIcon {
cursor: pointer;
display:inline-block;
margin: 0px 0px -1px 4px;
width: 16px;
opacity: 0.3;
height: 16px;
background: url(plugins/CoreHome/angularjs/enrichedheadline/help.png) no-repeat;
}
}

View file

@ -0,0 +1,114 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* History service. Provides the ability to change the window hash, and makes sure broadcast.pageload
* is called on every change.
*
* This service replaces the previously used jQuery history extension.
*
* Should only be used by the broadcast object.
*/
(function (window, $, broadcast) {
angular.module('piwikApp').service('historyService', historyService);
historyService.$inject = ['$location', '$rootScope'];
function historyService($location, $rootScope) {
var service = {};
service.load = load;
service.init = init;
return service;
function init() {
if ($location.path() != '/') {
changePathToSearch();
}
$rootScope.$on('$locationChangeSuccess', function () {
loadCurrentPage();
});
loadCurrentPage();
}
// currently, the AJAX content URL is stored in $location.search(), but before it was stored in $location.path().
// this function makes sure URLs like http://piwik.net/?...#/module=Whatever&action=whatever still work.
function changePathToSearch() {
var path = $location.path();
if (!path
|| path == '/'
) {
return;
}
var searchParams = broadcast.getValuesFromUrl('?' + path.substring(1));
// NOTE: we don't need to decode the parameters since $location.path() will decode the string itself
$location.search(searchParams);
$location.path('');
}
function loadCurrentPage() {
var searchObject = $location.search(),
searchString = [];
for (var name in searchObject) {
if (!searchObject.hasOwnProperty(name) || name == '_') {
continue;
}
// if more than one query parameter of the same name is supplied, angular will return all of them as
// an array. we only want to use the last one, though.
if (searchObject[name] instanceof Array) {
searchObject[name] = searchObject[name][searchObject[name].length - 1];
}
var value = searchObject[name];
if (name != 'columns') { // the columns query parameter is not urldecoded in PHP code. TODO: this should be fixed in 3.0
value = encodeURIComponent(value);
}
searchString.push(name + '=' + value);
}
searchString = searchString.join('&');
// the location hash will have a / prefix, which broadcast.pageload doesn't want
broadcast.pageload(searchString);
}
function load(hash) {
// make sure the hash is just the query parameter values, w/o a starting #, / or ? char. broadcast.pageload & $location.path should get neither
hash = normalizeHash(hash);
var currentHash = normalizeHash(location.hash);
if (currentHash === hash) {
loadCurrentPage(); // it would not trigger a location change success event as URL is the same, call it manually
} else if (hash) {
$location.search(hash);
} else {
// NOTE: this works around a bug in angularjs. when unsetting the hash (ie, removing in the URL),
// angular will enter an infinite loop of digests. this is because $locationWatch will trigger
// $locationChangeStart if $browser.url() != $location.absUrl(), and $browser.url() will contain
// the '#' character and $location.absUrl() will not. so the watch continues to trigger the event.
$location.search('_=');
}
setTimeout(function () { $rootScope.$apply(); }, 1);
}
function normalizeHash(hash) {
var chars = ['#', '/', '?'];
for (var i = 0; i != chars.length; ++i) {
var charToRemove = chars[i];
if (hash.charAt(0) == charToRemove) {
hash = hash.substring(1);
}
}
return hash;
}
}
})(window, jQuery, broadcast);

View file

@ -0,0 +1,52 @@
(function () {
angular.module('piwikApp').factory('http404CheckInterceptor', http404CheckInterceptor);
http404CheckInterceptor.$inject = ['$q'];
function http404CheckInterceptor($q) {
function isClientError(rejection)
{
if (rejection.status === 500) {
return true;
}
return rejection.status >= 400 && rejection.status < 408;
}
return {
'responseError': function(rejection) {
if (rejection &&
isClientError(rejection) &&
rejection.config &&
rejection.config.url &&
-1 !== rejection.config.url.indexOf('.html') &&
-1 !== rejection.config.url.indexOf('plugins')) {
var posEndUrl = rejection.config.url.indexOf('.html') + 5;
var url = rejection.config.url.substr(0, posEndUrl);
var message = 'Please check your server configuration. You may want to whitelist "*.html" files from the "plugins" directory.';
message += ' The HTTP status code is ' + rejection.status + ' for URL "' + url + '"';
var UI = require('piwik/UI');
var notification = new UI.Notification();
notification.show(message, {
title: 'Failed to load HTML file:',
context: 'error',
id: 'Network_HtmlFileLoadingError'
});
}
return $q.reject(rejection);
}
};
}
angular.module('piwikApp').config(['$httpProvider',function($httpProvider) {
$httpProvider.interceptors.push('http404CheckInterceptor');
}]);
})();

View file

@ -0,0 +1,31 @@
<div piwik-focus-anywhere-but-here="view.showItems=false" class="menuDropdown">
<span class="title"
ng-click="view.showItems=!view.showItems"
title="{{ tooltip }}"
ng-bind-html="menuTitle"/>
<div class="items borderedControl" ng-show="view.showItems">
<div class="search" ng-if="showSearch && view.showItems">
<input type="text"
piwik-focus-if="view.showItems"
ng-model="view.searchTerm"
placeholder="{{ 'General_Search'|translate }}"
ng-change="searchItems(view.searchTerm)">
<img title="{{ 'General_Search'|translate }}"
ng-show="!view.searchTerm"
class="search_ico"
src="plugins/Morpheus/images/search_ico.png"/>
<img title="{{ 'General_Clear'|translate }}"
ng-show="view.searchTerm"
ng-click="view.searchTerm='';searchItems('')"
class="reset"
src="plugins/CoreHome/images/reset_search.png"/>
</div>
<div ng-transclude>
</div>
</div>
</div>

View file

@ -0,0 +1,73 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-menudropdown menu-title="MyMenuItem" tooltip="My Tooltip" show-search="false">
* <a class="item" href="/url">An Item</a>
* <a class="item disabled">Disabled</a>
* <a class="item active">Active item</a>
* <hr class="item separator"/>
* <a class="item disabled category">Category</a>
* <a class="item" href="/url"></a>
* </div>
*/
(function () {
angular.module('piwikApp').directive('piwikMenudropdown', piwikMenudropdown);
function piwikMenudropdown(){
return {
transclude: true,
replace: true,
restrict: 'A',
scope: {
menuTitle: '@',
tooltip: '@',
showSearch: '=',
menuTitleChangeOnClick: '='
},
templateUrl: 'plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html?cb=' + piwik.cacheBuster,
link: function(scope, element, attrs) {
element.find('.item').on('click', function () {
var $self = angular.element(this);
if ($self.hasClass('disabled') || $self.hasClass('separator')) {
return;
}
if (scope.menuTitleChangeOnClick !== false) {
scope.menuTitle = $self.text().replace(/[\u0000-\u2666]/g, function(c) {
return '&#'+c.charCodeAt(0)+';';
});
}
scope.$eval('view.showItems = false');
scope.$apply();
element.find('.item').removeClass('active');
$self.addClass('active');
});
scope.searchItems = function (searchTerm)
{
searchTerm = searchTerm.toLowerCase();
element.find('.item').each(function (index, node) {
var $node = angular.element(node);
if (-1 === $node.text().toLowerCase().indexOf(searchTerm)) {
$node.hide();
} else {
$node.show();
}
});
};
}
};
}
})();

View file

@ -0,0 +1,105 @@
.menuDropdown {
display: inline-block;
padding-right: 14px;
color: @theme-color-link;
.title {
position: relative;
cursor: pointer;
&:after {
content: '';
position: absolute;
top: 5px;
right: -15px;
color: @theme-color-link;
display: inline;
font-size: 1px;
height: 0px;
width: 0px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid @theme-color-link;
}
}
.items {
z-index: 200;
position: absolute;
border: 1px solid @color-silver-l80;
background: @theme-color-background-base;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
padding: 0 !important;
.search {
margin: 15px 6px 10px 6px;
display: block;
.search_ico {
position: absolute;
right: 25px;
top: 27px;
margin: 0px;
left: initial;
}
.reset {
position: absolute;
top: 25px;
cursor: pointer;
margin: 0px;
right: 25px;
left: initial;
}
input {
margin: 0px;
width: 100%;
&::-ms-clear {
display: none;
}
}
}
.item {
display: block;
color: @theme-color-text !important;
text-decoration: none !important;
padding: 6px 25px 6px 6px !important;
font-size: 11px;
float: none;
text-align: left;
&:hover {
background: @theme-color-background-tinyContrast;
}
&.active {
background-color: @theme-color-background-tinyContrast;
}
&.category {
color: @theme-color-text-light !important
}
&.separator {
padding: 0px !important;
border-bottom: 0px;
margin: 0px;
}
&.separator,
&.disabled {
opacity: 0.5;
cursor: default;
&:hover {
background: @theme-color-background-base;
}
}
}
}
}

View file

@ -0,0 +1,34 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('NotificationController', NotificationController);
NotificationController.$inject = ['piwikApi'];
function NotificationController(piwikApi) {
/**
* Marks a persistent notification as read so it will not reappear on the next page
* load.
*/
this.markNotificationAsRead = function () {
var notificationId = this.notificationId;
if (!notificationId) {
return;
}
piwikApi.post(
{ // GET params
module: 'CoreHome',
action: 'markNotificationAsRead'
},
{ // POST params
notificationId: notificationId
}
);
};
}
})();

View file

@ -0,0 +1,8 @@
<div class="notification system">
<button type="button" class="close" data-dismiss="alert" ng-if="!noclear" ng-click="notification.markNotificationAsRead()">&times;</button>
<strong ng-if="title">{{ title }}</strong>
<!-- ng-transclude causes directive child elements to be added here -->
<div ng-transclude></div>
</div>

View file

@ -0,0 +1,95 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Directive to show a notification.
*
* Note: using this directive is preferred over the Notification class (which uses jquery
* exclusively).
*
* Supports the following attributes:
*
* * **context**: Either 'success', 'error', 'info', 'warning'
* * **type**: Either 'toast', 'persistent', 'transient'
* * **noclear**: If truthy, no clear button is displayed. For persistent notifications, has no effect.
*
* Usage:
*
* <div piwik-notification context="success" type="persistent" noclear="true">
* <strong>Info: </strong>My notification message.
* </div>
*/
(function () {
angular.module('piwikApp').directive('piwikNotification', piwikNotification);
piwikNotification.$inject = ['piwik', '$timeout'];
function piwikNotification(piwik, $timeout) {
return {
restrict: 'A',
scope: {
notificationId: '@?',
title: '@?notificationTitle', // TODO: shouldn't need this since the title can be specified within
// HTML of the node that uses the directive.
context: '@?',
type: '@?',
noclear: '@?'
},
transclude: true,
templateUrl: 'plugins/CoreHome/angularjs/notification/notification.directive.html?cb=' + piwik.cacheBuster,
controller: 'NotificationController',
controllerAs: 'notification',
link: function (scope, element) {
if (scope.notificationId) {
closeExistingNotificationHavingSameIdIfNeeded(scope.notificationId, element);
}
if (scope.context) {
element.children('.notification').addClass('notification-' + scope.context);
}
if (scope.type == 'persistent') {
// otherwise it is never possible to dismiss the notification
scope.noclear = false;
}
if ('toast' == scope.type) {
addToastEvent();
}
if (!scope.noclear) {
addCloseEvent();
}
function addToastEvent() {
$timeout(function () {
element.fadeOut('slow', function() {
element.remove();
});
}, 12 * 1000);
}
function addCloseEvent() {
element.on('click', '.close', function (event) {
if (event && event.delegateTarget) {
angular.element(event.delegateTarget).remove();
}
});
}
function closeExistingNotificationHavingSameIdIfNeeded(id, notificationElement) {
// TODO: instead of doing a global query for notification, there should be a notification-container
// directive that manages notifications.
var existingNode = angular.element('[notification-id=' + id + ']').not(notificationElement);
if (existingNode && existingNode.length) {
existingNode.remove();
}
}
}
};
}
})();

View file

@ -0,0 +1,34 @@
.system.notification {
.alert;
// We have to use !important because the default button style is crazy
.close {
position: relative;
top: -5px;
right: -10px;
padding: 0 !important;
background: transparent !important;
border: none !important;
float: right;
font-size: 20px !important;
font-weight: bold;
line-height: 20px !important;
color: inherit !important;
opacity: 0.3;
filter: alpha(opacity=30);
}
&.notification-success {
.alert-success;
}
&.notification-warning {
.alert-warning;
}
&.notification-danger,
&.notification-error {
.alert-danger;
}
&.notification-info {
.alert-info;
}
}

View file

@ -1,6 +1,10 @@
angular.module('piwikApp.config', []);
(function () {
angular.module('piwikApp.config', []);
if ('undefined' === (typeof piwik) || !piwik) {
return;
}
var piwikAppConfig = angular.module('piwikApp.config');
// we probably want this later as a separate config file, till then it serves as a "bridge"
for (var index in piwik.config) {

View file

@ -1,16 +1,19 @@
/*!
* Piwik - Web Analytics
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp', [
'ngSanitize',
'ngAnimate',
'piwikApp.config',
'piwikApp.service',
'piwikApp.directive',
'piwikApp.filter'
]);
angular.module('app', []);
(function () {
angular.module('piwikApp', [
'ngSanitize',
'ngAnimate',
'ngCookies',
'ngDialog',
'piwikApp.config',
'piwikApp.service',
'piwikApp.directive',
'piwikApp.filter'
]);
angular.module('app', []);
})();

View file

@ -0,0 +1,86 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('QuickAccessController', QuickAccessController);
QuickAccessController.$inject = ['$scope', '$filter', 'siteSelectorModel'];
function QuickAccessController($scope, $filter, siteSelectorModel){
this.menuItems = [];
this.numMenuItems = 0;
this.sitesModel = siteSelectorModel;
this.onKeypress = function (event) {
var areSearchResultsDisplayed = $scope.search && $scope.search.term && $scope.view && $scope.view.searchActive;
var isTabKey = 9 == event.which
var isEscKey = 27 == event.which
if (38 == event.which) {
$scope.highlightPreviousItem();
event.preventDefault();
} else if (40 == event.which) {
$scope.highlightNextItem();
event.preventDefault();
} else if (13 == event.which) {
$scope.clickQuickAccessMenuItem();
} else if (isTabKey && areSearchResultsDisplayed) {
$scope.deactivateSearch();
} else if (isEscKey && areSearchResultsDisplayed) {
$scope.deactivateSearch();
}
};
this.searchMenu = function (searchTerm) {
searchTerm = searchTerm.toLowerCase();
var index = -1;
var menuItemsIndex = {};
var menuItems = [];
var moveToCategory = function (i, submenuItem) {
submenuItem = angular.copy(submenuItem); // force rerender of element to prevent weird side effects
submenuItem.menuIndex = ++index; // needed for proper highlighting with arrow keys
var category = submenuItem.category;
if (!(category in menuItemsIndex)) {
menuItems.push({title: category, items: []});
menuItemsIndex[category] = menuItems.length - 1;
}
var indexOfCategory = menuItemsIndex[category];
menuItems[indexOfCategory].items.push(submenuItem);
};
$scope.resetSearchIndex();
if ($scope.hasSitesSelector) {
this.sitesModel.searchSite(searchTerm);
}
var topMenuItems = $filter('filter')($scope.getTopMenuItems(), searchTerm);
var leftMenuItems = $filter('filter')($scope.getLeftMenuItems(), searchTerm);
var segmentItems = $filter('filter')($scope.getSegmentItems(), searchTerm);
$.each(topMenuItems, moveToCategory);
$.each(leftMenuItems, moveToCategory);
$.each(segmentItems, moveToCategory);
this.numMenuItems = topMenuItems.length + leftMenuItems.length + segmentItems.length;
this.menuItems = menuItems;
};
this.selectSite = function (idsite) {
this.sitesModel.loadSite(idsite);
};
this.selectMenuItem = function (index) {
$scope.selectMenuItem(index);
};
}
})();

View file

@ -0,0 +1,38 @@
<div class="quick-access"
ng-class="{active: view.searchActive, expanded: view.searchActive}"
piwik-focus-anywhere-but-here="view.searchActive = false;">
<span class="icon-search" ng-hide="search.term || view.searchActive"
ng-mouseenter="view.searchActive=true"></span>
<input class="s"
title="{{ quickAccessTitle }}"
ng-keydown="quickAccess.onKeypress($event)"
ng-change="view.searchActive=true;quickAccess.searchMenu(search.term)"
ng-focus="view.searchActive=true"
ng-model="search.term" piwik-focus-if="view.searchActive"
type="text" tabindex="2"/>
<ul ng-hide="!search.term || !view.searchActive || (quickAccess.numMenuItems > 0) || (quickAccess.sitesModel.sites | length)">
<li class="no-result">{{ 'General_SearchNoResults' | translate }}</li>
</ul>
<ul ng-show="search.term && view.searchActive" ng-repeat="subcategory in quickAccess.menuItems">
<li class="quick-access-category"
ng-click="search.term = subcategory.title;quickAccess.searchMenu(search.term)">{{ subcategory.title }}</li>
<li class="result"
ng-class="{selected: submenuEntry.menuIndex == search.index}"
ng-mouseenter="search.index=submenuEntry.menuIndex"
ng-click="quickAccess.selectMenuItem(submenuEntry.index)"
ng-repeat="submenuEntry in subcategory.items"><a>{{ submenuEntry.name | trim }}</a></li>
</ul>
<ul ng-show="search.term && view.searchActive">
<li class="quick-access-category websiteCategory"
ng-show="hasSitesSelector && ((quickAccess.sitesModel.sites | length) || quickAccess.sitesModel.isLoading)"
>{{ 'SitesManager_Sites' | translate }}</li>
<li class="no-result"
ng-show="hasSitesSelector && quickAccess.sitesModel.isLoading">{{ 'MultiSites_LoadingWebsites' | translate }}</li>
<li class="result"
ng-show="hasSitesSelector && !quickAccess.sitesModel.isLoading"
ng-mouseenter="search.index=(quickAccess.numMenuItems + $index)"
ng-class="{selected: (quickAccess.numMenuItems + $index) == search.index}"
ng-click="quickAccess.selectSite(site.idsite)"
ng-repeat="site in quickAccess.sitesModel.sites"><a ng-bind-html="site.name"></a></li>
</ul>
</div>

View file

@ -0,0 +1,282 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-dialog="showDialog">...</div>
* Will show dialog once showDialog evaluates to true.
*
* Will execute the "executeMyFunction" function in the current scope once the yes button is pressed.
*/
(function () {
angular.module('piwikApp').directive('piwikQuickAccess', QuickAccessDirective);
QuickAccessDirective.$inject = ['$rootElement', '$timeout', 'piwik', '$filter'];
function QuickAccessDirective ($rootElement, $timeout, piwik, $filter) {
return {
restrict: 'A',
replace: true,
scope: {},
templateUrl: 'plugins/CoreHome/angularjs/quick-access/quick-access.directive.html?cb=' + piwik.cacheBuster,
controller: 'QuickAccessController',
controllerAs: 'quickAccess',
link: function (scope, element, attrs) {
var menuIndex = -1; // the menu index is used to identify which element to click
var topMenuItems = []; // cache for top menu items
var leftMenuItems = []; // cache for left menu items
var segmentItems = []; // cache for segment items
var hasSegmentSelector = angular.element('.segmentEditorPanel').length;
scope.hasSitesSelector = angular.element('.top_controls [piwik-siteselector]').length;
var translate = $filter('translate');
var searchAreasTitle = '';
var searchAreas = [translate('CoreHome_MenuEntries')]
if (hasSegmentSelector) {
searchAreas.push(translate('CoreHome_Segments'))
}
if (scope.hasSitesSelector) {
searchAreas.push(translate('SitesManager_Sites'))
}
while (searchAreas.length) {
searchAreasTitle += searchAreas.shift();
if (searchAreas.length >= 2) {
searchAreasTitle += ', ';
} else if (searchAreas.length === 1) {
searchAreasTitle += ' ' + translate('General_And') + ' ';
}
}
scope.quickAccessTitle = translate('CoreHome_QuickAccessTitle', searchAreasTitle);
function trim(str) {
return str.replace(/^\s+|\s+$/g,'');
}
scope.getTopMenuItems = function()
{
if (topMenuItems && topMenuItems.length) {
return topMenuItems;
}
var category = _pk_translate('CoreHome_Menu');
$rootElement.find('#topRightBar .navbar-right li > a').each(function (index, element) {
var $element = $(element);
if ($element.is('#topmenu-usersmanager')) {
// ignore languages manager
return;
}
var text = trim($element.text());
if (!text) {
text = trim($element.attr('title')); // possibly a icon, use title instead
}
if (text) {
topMenuItems.push({name: text, index: ++menuIndex, category: category});
$element.attr('quick_access', menuIndex);
}
});
return topMenuItems;
};
scope.getLeftMenuItems = function ()
{
if (leftMenuItems && leftMenuItems.length) {
return leftMenuItems;
}
$rootElement.find('#secondNavBar .menuTab').each(function (index, element) {
var $element = angular.element(element);
var category = trim($element.find('> .item').text());
if (category && -1 !== category.lastIndexOf("\n")) {
// remove "\n\nMenu"
category = trim(category.substr(0, category.lastIndexOf("\n")));
}
$element.find('li .item').each(function (i, element) {
var $element = angular.element(element);
var text = trim($element.text());
if (text) {
leftMenuItems.push({name: text, category: category, index: ++menuIndex});
$element.attr('quick_access', menuIndex);
}
})
});
return leftMenuItems;
};
scope.getSegmentItems = function()
{
if (!hasSegmentSelector) {
return [];
}
if (segmentItems && segmentItems.length) {
return segmentItems;
}
var category = _pk_translate('CoreHome_Segments');
$rootElement.find('.segmentList [data-idsegment]').each(function (index, element) {
var $element = angular.element(element);
var text = trim($element.find('.segname').text());
if (text) {
segmentItems.push({name: text, category: category, index: ++menuIndex});
$element.attr('quick_access', menuIndex);
}
});
return segmentItems;
};
scope.activateSearch = function()
{
scope.$eval('view.searchActive = true');
$timeout(function () {
scope.$apply();
}, 0);
};
scope.deactivateSearch = function()
{
scope.$eval('search.term = ""');
scope.$eval('view.searchActive = false');
element.find('input').blur();
$timeout(function () {
scope.$apply();
}, 0);
};
function isElementInViewport(element) {
var rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= $(window).height() &&
rect.right <= $(window).width()
);
}
function getCurrentlySelectedElement(index)
{
var results = element.find('li.result');
if (results && results.length && results[scope.search.index]) {
return $(results[scope.search.index]);
}
}
function makeSureSelectedItemIsInViewport() {
var element = getCurrentlySelectedElement();
if (element && element[0] && !isElementInViewport(element[0])) {
scrollFirstElementIntoView(element);
}
}
function scrollFirstElementIntoView(element)
{
if (element && element[0] && element[0].scrollIntoView) {
// make sure search is visible
element[0].scrollIntoView();
}
}
scope.highlightPreviousItem = function()
{
if (0 >= (scope.search.index - 1)) {
scope.search.index = 0;
} else {
scope.search.index--;
}
makeSureSelectedItemIsInViewport();
};
scope.resetSearchIndex = function () {
scope.search.index = 0;
makeSureSelectedItemIsInViewport();
};
scope.highlightNextItem = function()
{
var numTotal = element.find('li.result').length;
if (numTotal <= (scope.search.index + 1)) {
scope.search.index = numTotal - 1;
} else {
scope.search.index++;
}
makeSureSelectedItemIsInViewport();
};
scope.clickQuickAccessMenuItem = function()
{
var selectedMenuElement = getCurrentlySelectedElement();
if (selectedMenuElement) {
$timeout(function () {
selectedMenuElement.click();
}, 20);
}
};
scope.selectMenuItem = function(index)
{
var target = $rootElement.find('[quick_access=' + index + ']');
if (target && target.length && target[0]) {
scope.deactivateSearch();
var actualTarget = target[0];
var href = $(actualTarget).attr('href');
if (href && href.length > 10 && actualTarget && actualTarget.click) {
try {
actualTarget.click();
} catch (e) {
$(actualTarget).click();
}
} else {
$(actualTarget).click();
}
}
};
Mousetrap.bind('f', function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false; // IE
}
scrollFirstElementIntoView(element);
scope.activateSearch();
});
}
};
}
})();

View file

@ -0,0 +1,55 @@
.quick-access {
position: relative;
li {
font-size: 11px;
}
li a {
padding: 10px 19px;
display: inline-block;
text-decoration: none;
word-break: break-all;
}
.icon-search {
position: absolute;
font-size: 14px;
top: 10px;
left: 10px;
}
input {
width:100%;
height: 100%;
border: 0 !important;
box-shadow: 0 0 !important;
border-radius: 0 !important;
font-size: 11px;
&:focus {
outline: none;
}
}
.selected {
background-color: @theme-color-background-tinyContrast !important;
}
.quick-access-category {
text-align: left !important;
font-size: 11px;
padding: 5px 5px 5px 10px;
cursor: pointer;
}
.result {
cursor: pointer;
}
.quick-access-category:hover {
background: none !important;
}
.no-result {
padding: 10px 19px;
cursor: default;
}
.websiteCategory {
cursor: default;
}
}

View file

@ -0,0 +1,87 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
*
*/
(function () {
angular.module('piwikApp').directive('piwikExpandOnClick', piwikExpandOnClick);
piwikExpandOnClick.$inject = ['$document'];
function piwikExpandOnClick($document){
return {
restrict: 'A',
link: function(scope, element, attr) {
element.find('.title').on('click', function () {
element.toggleClass('expanded');
});
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
element.removeClass('expanded');
}
}
function onEscapeHandler (event) {
if (event.which === 27) {
element.removeClass('expanded');
}
}
$document.on('keyup', onEscapeHandler);
$document.on('mouseup', onClickOutsideElement);
scope.$on('$destroy', function() {
$document.off('mouseup', onClickOutsideElement);
$document.off('keyup', onEscapeHandler);
});
}
};
}
angular.module('piwikApp').directive('piwikExpandOnHover', piwikExpandOnHover);
piwikExpandOnHover.$inject = ['$document'];
function piwikExpandOnHover($document){
return {
restrict: 'A',
link: function(scope, element, attr) {
element.on('mouseenter', '.title', function () {
element.addClass('expanded');
});
element.on('mouseleave', function () {
element.removeClass('expanded');
});
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
element.removeClass('expanded');
}
}
function onEscapeHandler (event) {
if (event.which === 27) {
element.removeClass('expanded');
}
}
$document.on('keyup', onEscapeHandler);
$document.on('mouseup', onClickOutsideElement);
scope.$on('$destroy', function() {
$document.off('mouseup', onClickOutsideElement);
$document.off('keyup', onEscapeHandler);
});
}
};
}
})();

View file

@ -0,0 +1,59 @@
.piwikSelector {
display: inline-block;
line-height: 0;
span.title,
a.title {
.font-default(11px, 12px);
display: inline-block;
width: 100%;
padding: 11px 19px 10px;
white-space: nowrap;
cursor: pointer;
text-transform: uppercase;
text-decoration: none;
color: @theme-color-text;
&.activityIndicator {
background: url(plugins/Morpheus/images/loading-blue.gif) no-repeat right 9px;
.icon {
visibility: hidden;
}
}
.icon {
padding-left: 6px;
display: inline-block;
vertical-align: top;
&.iconHidden {
visibility: hidden;
}
}
.icon:not(.icon-fixed) {
float: right;
&:after {
clear:right;
content: ' ';
}
}
&:hover, &:focus {
text-decoration: none;
}
}
.dropdown {
.font-default(11px, 15px);
display: none;
padding: 5px 19px 11px 19px;
}
&.expanded {
.dropdown {
display: block;
}
}
}

View file

@ -1,49 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp').controller('SiteSelectorController', function($scope, siteSelectorModel, piwik, AUTOCOMPLETE_MIN_SITES){
$scope.model = siteSelectorModel;
$scope.autocompleteMinSites = AUTOCOMPLETE_MIN_SITES;
$scope.selectedSite = {id: '', name: ''};
$scope.activeSiteId = piwik.idSite;
$scope.switchSite = function (site) {
$scope.selectedSite.id = site.idsite;
if (site.name === $scope.allSitesText) {
$scope.selectedSite.name = $scope.allSitesText;
} else {
$scope.selectedSite.name = site.name.replace(/[\u0000-\u2666]/g, function(c) {
return '&#'+c.charCodeAt(0)+';';
});
}
if (!$scope.switchSiteOnSelect || $scope.activeSiteId == site.idsite) {
return;
}
if (site.idsite == 'all') {
piwik.broadcast.propagateNewPage('module=MultiSites&action=index');
} else {
piwik.broadcast.propagateNewPage('segment=&idSite=' + site.idsite, false);
}
};
$scope.getUrlAllSites = function () {
var newParameters = 'module=MultiSites&action=index';
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters);
};
$scope.getUrlForSiteId = function (idSite) {
var idSiteParam = 'idSite=' + idSite;
var newParameters = 'segment=&' + idSiteParam;
var hash = piwik.broadcast.isHashExists() ? piwik.broadcast.getHashFromUrl() : "";
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters) +
'#' + piwik.helper.getQueryStringWithParametersModified(hash.substring(1), newParameters);
};
});

View file

@ -1,80 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-siteselector>
*
* More advanced example
* <div piwik-siteselector
* show-selected-site="true" show-all-sites-item="true" switch-site-on-select="true"
* all-sites-location="top|bottom" all-sites-text="test" show-selected-site="true"
* show-all-sites-item="true">
*
* Within a form
* <div piwik-siteselector input-name="siteId">
*
* Events:
* Triggers a `change` event on any change
* <div piwik-siteselector id="mySelector">
* $('#mySelector').on('change', function (event) { event.id/event.name })
*/
angular.module('piwikApp').directive('piwikSiteselector', function($document, piwik, $filter){
var defaults = {
name: '',
siteid: piwik.idSite,
sitename: piwik.siteName,
allSitesLocation: 'bottom',
allSitesText: $filter('translate')('General_MultiSitesSummary'),
showSelectedSite: 'false',
showAllSitesItem: 'true',
switchSiteOnSelect: 'true'
};
return {
restrict: 'A',
scope: {
showSelectedSite: '=',
showAllSitesItem: '=',
switchSiteOnSelect: '=',
inputName: '@name',
allSitesText: '@',
allSitesLocation: '@'
},
templateUrl: 'plugins/CoreHome/angularjs/siteselector/siteselector.html?cb=' + piwik.cacheBuster,
controller: 'SiteSelectorController',
compile: function (element, attrs) {
for (var index in defaults) {
if (!attrs[index]) { attrs[index] = defaults[index]; }
}
return function (scope, element, attrs) {
// selectedSite.id|.name + model is hard-coded but actually the directive should not know about this
scope.selectedSite.id = attrs.siteid;
scope.selectedSite.name = attrs.sitename;
if (!attrs.siteid || !attrs.sitename) {
scope.model.loadInitialSites();
}
scope.$watch('selectedSite.id', function (newValue, oldValue, scope) {
if (newValue != oldValue) {
element.attr('siteid', newValue);
element.trigger('change', scope.selectedSite);
}
});
/** use observe to monitor attribute changes
attrs.$observe('maxsitenamewidth', function(val) {
// for instance trigger a function or whatever
}) */
};
}
};
});

View file

@ -1,75 +0,0 @@
/*!
* Piwik - Web Analytics
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
angular.module('piwikApp').factory('siteSelectorModel', function (piwikApi, $filter) {
var model = {};
model.sites = [];
model.hasMultipleWebsites = false;
model.isLoading = false;
model.firstSiteName = '';
var initialSites = null;
model.updateWebsitesList = function (sites) {
if (!sites || !sites.length) {
model.sites = [];
return [];
}
angular.forEach(sites, function (site) {
if (site.group) site.name = '[' + site.group + '] ' + site.name;
});
model.sites = $filter('orderBy')(sites, '+name');
if (!model.firstSiteName) {
model.firstSiteName = model.sites[0].name;
}
model.hasMultipleWebsites = model.hasMultipleWebsites || sites.length > 1;
return model.sites;
};
model.searchSite = function (term) {
if (!term) {
model.loadInitialSites();
return;
}
if (model.isLoading) {
piwikApi.abort();
}
model.isLoading = true;
return piwikApi.fetch({
method: 'SitesManager.getPatternMatchSites',
pattern: term
}).then(function (response) {
return model.updateWebsitesList(response);
})['finally'](function () { // .finally() is not IE8 compatible see https://github.com/angular/angular.js/commit/f078762d48d0d5d9796dcdf2cb0241198677582c
model.isLoading = false;
});
};
model.loadInitialSites = function () {
if (initialSites) {
model.sites = initialSites;
return;
}
this.searchSite('%').then(function (websites) {
initialSites = websites;
});
};
return model;
});

View file

@ -0,0 +1,131 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').factory('siteSelectorModel', siteSelectorModel);
siteSelectorModel.$inject = ['piwikApi', '$filter', 'piwik'];
function siteSelectorModel(piwikApi, $filter, piwik) {
var initialSites = null;
var limitPromise = null;
var model = {
sites : [],
hasMultipleWebsites : false,
isLoading : false,
firstSiteName : '',
onlySitesWithAdminAccess: false,
updateWebsitesList: updateWebsitesList,
searchSite: searchSite,
loadSite: loadSite,
loadInitialSites: loadInitialSites
};
return model;
function updateWebsitesList(sites) {
if (!sites || !sites.length) {
model.sites = [];
return [];
}
angular.forEach(sites, function (site) {
if (site.group) site.name = '[' + site.group + '] ' + site.name;
if (!site.name) {
return;
}
// Escape site names, see https://github.com/piwik/piwik/issues/7531
site.name = site.name.replace(/[\u0000-\u2666]/g, function(c) {
return '&#'+c.charCodeAt(0)+';';
});
});
model.sites = sortSites(sites);
if (!model.firstSiteName) {
model.firstSiteName = model.sites[0].name;
}
model.hasMultipleWebsites = model.hasMultipleWebsites || sites.length > 1;
return model.sites;
}
function searchSite(term) {
if (!term) {
loadInitialSites();
return;
}
if (model.isLoading) {
if (model.currentRequest) {
model.currentRequest.abort();
} else if (limitPromise) {
limitPromise.abort();
limitPromise = null;
}
}
model.isLoading = true;
if (!limitPromise) {
limitPromise = piwikApi.fetch({method: 'SitesManager.getNumWebsitesToDisplayPerPage'});
}
return limitPromise.then(function (response) {
var limit = response.value;
var methodToCall = 'SitesManager.getPatternMatchSites';
if (model.onlySitesWithAdminAccess) {
methodToCall = 'SitesManager.getSitesWithAdminAccess';
}
model.currentRequest = piwikApi.fetch({
method: methodToCall,
limit: limit,
pattern: term
});
return model.currentRequest;
}).then(function (response) {
if (angular.isDefined(response)) {
return updateWebsitesList(response);
}
})['finally'](function () { // .finally() is not IE8 compatible see https://github.com/angular/angular.js/commit/f078762d48d0d5d9796dcdf2cb0241198677582c
model.isLoading = false;
model.currentRequest = null;
});
}
function loadSite(idsite) {
if (idsite == 'all') {
piwik.broadcast.propagateNewPage('module=MultiSites&action=index');
} else {
piwik.broadcast.propagateNewPage('segment=&idSite=' + idsite, false);
}
}
function sortSites(websites)
{
return $filter('orderBy')(websites, '+name');
}
function loadInitialSites() {
if (initialSites) {
model.sites = initialSites;
return;
}
searchSite('%').then(function () {
initialSites = model.sites
});
}
}
})();

View file

@ -0,0 +1,42 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
angular.module('piwikApp').controller('SiteSelectorController', SiteSelectorController);
SiteSelectorController.$inject = ['$scope', 'siteSelectorModel', 'piwik', 'AUTOCOMPLETE_MIN_SITES'];
function SiteSelectorController($scope, siteSelectorModel, piwik, AUTOCOMPLETE_MIN_SITES){
$scope.model = siteSelectorModel;
$scope.autocompleteMinSites = AUTOCOMPLETE_MIN_SITES;
$scope.selectedSite = {id: '', name: ''};
$scope.activeSiteId = piwik.idSite;
$scope.switchSite = function (site) {
$scope.selectedSite = {id: site.idsite, name: site.name};
if (!$scope.switchSiteOnSelect || $scope.activeSiteId == site.idsite) {
return;
}
$scope.model.loadSite(site.idsite);
};
$scope.getUrlAllSites = function () {
var newParameters = 'module=MultiSites&action=index';
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters);
};
$scope.getUrlForSiteId = function (idSite) {
var idSiteParam = 'idSite=' + idSite;
var newParameters = 'segment=&' + idSiteParam;
var hash = piwik.broadcast.isHashExists() ? piwik.broadcast.getHashFromUrl() : "";
return piwik.helper.getCurrentQueryStringWithParametersModified(newParameters) +
'#' + piwik.helper.getQueryStringWithParametersModified(hash.substring(1), newParameters);
};
}
})();

View file

@ -1,25 +1,32 @@
<div piwik-focus-anywhere-but-here="view.showSitesList=false" class="custom_select"
ng-class="{'sites_autocomplete--dropdown': (model.hasMultipleWebsites || showAllSitesItem || !model.sites.length)}">
<div piwik-focus-anywhere-but-here="view.showSitesList=false"
class="siteSelector piwikSelector borderedControl"
ng-class="{'expanded': view.showSitesList}">
<script type="text/ng-template" id="siteselector_allsiteslink.html">
<div ng-click="switchSite({idsite: 'all', name: allSitesText});view.showSitesList=false;"
class="custom_select_all">
<a href="{{ getUrlAllSites() }}"
piwik-ignore-click
ng-bind-html="allSitesText"></a>
ng-bind-html="allSitesText" tabindex="4"></a>
</div>
</script>
<input ng-if="inputName" type="hidden" name="{{ inputName }}" ng-value="selectedSite.id"/>
<a ng-click="view.showSitesList=!view.showSitesList; view.showSitesList && model.loadInitialSites()"
<a ng-click="view.showSitesList=!view.showSitesList; view.showSitesList && !model.isLoading && model.loadInitialSites();"
piwik-onenter="view.showSitesList=!view.showSitesList; view.showSitesList && !model.isLoading && model.loadInitialSites();"
href="javascript:void(0)"
class="custom_select_main_link"
ng-class="{'loading': model.isLoading}">
<span ng-bind-html="selectedSite.name || model.firstSiteName">?</span>
title="{{ 'CoreHome_ChangeCurrentWebsite'|translate:((selectedSite.name || model.firstSiteName)|htmldecode) }}"
ng-class="{'loading': model.isLoading}"
class="title" tabindex="4">
<span class="icon icon-arrow-bottom"
ng-class="{'iconHidden': model.isLoading, 'collapsed': !view.showSitesList}"></span>
<span>{{ 'General_Website'| translate }}:
<span ng-bind-html="selectedSite.name || model.firstSiteName">?</span>
</span>
</a>
<div ng-show="view.showSitesList" class="custom_select_block">
<div ng-show="view.showSitesList" class="dropdown">
<div ng-if="allSitesLocation=='top' && showAllSitesItem"
ng-include="'siteselector_allsiteslink.html'"></div>
@ -29,7 +36,9 @@
ng-repeat="site in model.sites"
ng-hide="!showSelectedSite && activeSiteId==site.idsite">
<a piwik-ignore-click href="{{ getUrlForSiteId(site.idsite) }}"
piwik-autocomplete-matched="view.searchTerm">{{ site.name }}</a>
piwik-autocomplete-matched="view.searchTerm"
title="{{ site.name|htmldecode }}"
ng-bind-html="site.name" tabindex="4"></a>
</li>
</ul>
<ul ng-show="!model.sites.length && view.searchTerm" class="ui-autocomplete ui-front ui-menu ui-widget ui-widget-content ui-corner-all siteSelect">
@ -44,13 +53,12 @@
<div class="custom_select_search" ng-show="autocompleteMinSites <= model.sites.length || view.searchTerm">
<input type="text"
piwik-focus-if="view.showSitesList"
ng-click="view.searchTerm=''"
ng-model="view.searchTerm"
ng-change="model.searchSite(view.searchTerm)"
placeholder="{{ 'General_Search' | translate }}"
class="websiteSearch inp"/>
<input type="submit"
ng-click="model.searchSite(view.searchTerm)"
value="{{ 'General_Search' | translate }}" class="but"/>
<img title="Clear"
ng-show="view.searchTerm"
ng-click="view.searchTerm=''; model.loadInitialSites()"

View file

@ -0,0 +1,99 @@
/*!
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
/**
* Usage:
* <div piwik-siteselector>
*
* More advanced example
* <div piwik-siteselector
* show-selected-site="true" show-all-sites-item="true" switch-site-on-select="true"
* all-sites-location="top|bottom" all-sites-text="test" show-selected-site="true"
* show-all-sites-item="true" only-sites-with-admin-access="true">
*
* Within a form
* <div piwik-siteselector input-name="siteId">
*
* Events:
* Triggers a `change` event on any change
* <div piwik-siteselector id="mySelector">
* $('#mySelector').on('change', function (event) { event.id/event.name })
*/
(function () {
angular.module('piwikApp').directive('piwikSiteselector', piwikSiteselector);
piwikSiteselector.$inject = ['$document', 'piwik', '$filter', '$timeout'];
function piwikSiteselector($document, piwik, $filter, $timeout){
var defaults = {
name: '',
siteid: piwik.idSite,
sitename: piwik.siteName,
allSitesLocation: 'bottom',
allSitesText: $filter('translate')('General_MultiSitesSummary'),
showSelectedSite: 'false',
showAllSitesItem: 'true',
switchSiteOnSelect: 'true',
onlySitesWithAdminAccess: 'false'
};
return {
restrict: 'A',
scope: {
showSelectedSite: '=',
showAllSitesItem: '=',
switchSiteOnSelect: '=',
onlySitesWithAdminAccess: '=',
inputName: '@name',
allSitesText: '@',
allSitesLocation: '@'
},
require: "?ngModel",
templateUrl: 'plugins/CoreHome/angularjs/siteselector/siteselector.directive.html?cb=' + piwik.cacheBuster,
controller: 'SiteSelectorController',
compile: function (element, attrs) {
for (var index in defaults) {
if (attrs[index] === undefined) {
attrs[index] = defaults[index];
}
}
return function (scope, element, attrs, ngModel) {
scope.selectedSite = {id: attrs.siteid, name: attrs.sitename};
scope.model.onlySitesWithAdminAccess = scope.onlySitesWithAdminAccess;
scope.model.loadInitialSites();
if (ngModel) {
ngModel.$setViewValue(scope.selectedSite);
}
scope.$watch('selectedSite.id', function (newValue, oldValue, scope) {
if (newValue != oldValue) {
element.attr('siteid', newValue);
element.trigger('change', scope.selectedSite);
}
});
scope.$watch('selectedSite', function (newValue) {
if (ngModel) {
ngModel.$setViewValue(newValue);
}
});
scope.$watch('view.showSitesList', function (newValue) {
element.toggleClass('expanded', !! newValue);
});
$timeout(function () {
initTopControls();
});
};
}
};
}
})();

View file

@ -0,0 +1,156 @@
.autocompleteMatched {
color: #5256BE;
font-weight: bold;
}
.siteSelector {
a.title {
.icon.collapsed.iconHidden {
visibility: visible;
}
}
.dropdown {
max-width: 210px;
}
}
#content {
.sites_autocomplete {
position: static !important;
height: 36px;
z-index: 99;
vertical-align: middle;
> .siteSelector {
position: absolute;
z-index: 9999;
}
a.title {
text-decoration: none;
}
}
}
.siteSelector.expanded {
.loading {
background: url(plugins/Morpheus/images/loading-blue.gif) no-repeat 94% 11px;
}
}
.siteSelector a.title,
.siteSelector .custom_select_ul_list li a,
.siteSelector .custom_select_all a,
.siteSelector .custom_select_main_link > span {
display: inline-block;
max-width: 210px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0;
color: @theme-color-text;
text-transform: uppercase;
width: 100%;
}
.siteSelector a.title {
> span {
max-width: 161px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
}
span {
vertical-align: top;
}
}
.siteSelector .custom_select_ul_list,
.siteSelector ul.ui-autocomplete {
position: relative;
list-style: none;
line-height: 18px;
padding: 0 0 15px 0;
box-shadow: none !important;
}
.siteSelector .custom_select_ul_list {
padding: 0 0 5px 0;
}
.siteSelector .dropdown {
padding-top: 0;
}
.siteSelector .custom_select_ul_list li a,
.siteSelector .custom_select_all a {
line-height: 18px;
height: auto;
display: block;
text-decoration: none;
padding-left: 5px;
margin-left: -5px;
}
.siteSelector .custom_select_ul_list li a:hover,
.siteSelector .custom_select_all a:hover {
background: #ebeae6;
}
.siteSelector .custom_select_all a {
text-decoration: none;
margin: 0 0 5px -5px;
}
.siteSelector .custom_select_search {
margin: 0;
height: 33px;
display: block;
white-space: nowrap;
position: relative;
padding-top: 4px;
.inp {
vertical-align: top;
width: 100%;
padding: 4px 6px !important;
border: 1px solid #d0d0d0 !important;
background: transparent !important;
font-size: 11px !important;
color: #454545 !important;
}
.reset {
position: absolute;
top: 10px;
right: 4px;
cursor: pointer;
}
}
.siteSelector {
width: auto;
}
.sites_selector_container>.siteSelector {
padding-left: 12px;
}
.custom_selector_container .ui-menu-item,
.custom_selector_container .ui-menu-item a {
float:none;position:static
}
.custom_select_block_show {
height: auto;
overflow: visible;
max-width:inherit;
}
.sites_selector_container {
padding-top: 5px;
}
.siteSelect a {
white-space: normal;
text-align: left;
}

View file

@ -1,177 +0,0 @@
/*sites_autocomplete*/
.sites_autocomplete {
position: absolute;
font-size: 12px;
display: inline-block;
height: 30px; /* Hack to not push the dashboard widget below */
}
.sites_selector_in_dashboard {
margin-top:10px;
}
.top_bar_sites_selector {
float: right
}
.top_bar_sites_selector > label {
display: inline-block;
padding: 7px 0 6px 0;
float: left;
font-size: 12px;
}
.top_bar_sites_selector > .sites_autocomplete {
position: static;
padding-left: 12px;
}
.autocompleteMatched {
color: #5256BE;
font-weight: bold;
}
.sites_autocomplete .custom_select {
float: left;
position: relative;
z-index: 19;
background: #fff url(plugins/Zeitgeist/images/sites_selection.png) repeat-x 0 0;
border: 1px solid #d4d4d4;
color: #255792;
border-radius: 4px;
cursor: pointer;
min-width: 165px;
padding: 5px 6px 4px;
}
.sites_autocomplete .custom_select_main_link {
display: block;
text-decoration: none;
background: none;
cursor: default;
height:1.4em;
}
.sites_autocomplete .custom_select_ul_list li a,
.sites_autocomplete .custom_select_all a,
.sites_autocomplete .custom_select_main_link > span {
display: inline-block;
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 20px 0 4px;
}
.sites_autocomplete--dropdown .custom_select_main_link:not(.loading):before {
content: " \25BC";
position: absolute;
right: 0;
font-size: 0.8em;
margin-top: 0.2em;
color: #444;
}
.sites_autocomplete--dropdown .custom_select_main_link {
cursor: pointer;
position: relative;
}
.sites_autocomplete .custom_select_main_link.loading {
background: url(plugins/Zeitgeist/images/loading-blue.gif) no-repeat right 3px;
}
.sites_autocomplete .custom_select_ul_list,
.sites_autocomplete ul.ui-autocomplete {
position: relative;
list-style: none;
line-height: 18px;
padding: 0 0 15px 0;
}
.sites_autocomplete .custom_select_ul_list li a,
.sites_autocomplete .custom_select_all a {
line-height: 18px;
height: auto;
display: block;
text-decoration: none;
}
.sites_autocomplete .custom_select_ul_list li a:hover,
.sites_autocomplete .custom_select_all a:hover {
background: #ebeae6;
}
.sites_autocomplete .custom_select_all a {
text-decoration: none;
margin: 0 0 5px 0;
}
.sites_autocomplete .custom_select_search {
margin: 0 0 0 4px;
height: 26px;
display: block;
white-space: nowrap;
background: url(plugins/Zeitgeist/images/search_bg.png) no-repeat 0 0;
}
.sites_autocomplete .custom_select_search .inp {
vertical-align: top;
width: 114px;
padding: 2px 6px;
border: 0;
background: transparent;
font-size: 10px;
color: #454545;
}
.sites_autocomplete {
width: 165px;
}
.sites_autocomplete .custom_select_search .but {
vertical-align: top;
font-size: 10px;
border: 0;
background: transparent;
width: 21px;
height: 17px;
overflow: hidden;
opacity: 0;
cursor: pointer;
}
.sites_selector_container>.sites_autocomplete {
padding-left: 12px;
}
.custom_selector_container .ui-menu-item,
.custom_selector_container .ui-menu-item a {
float:none;position:static
}
.custom_select_search .reset {
position: relative; top: 4px; left: -44px; cursor: pointer;
}
.custom_select_block {
overflow: hidden;
max-width: inherit;
visibility: visible;
}
.custom_select_block_show {
height: auto;
overflow: visible;
max-width:inherit;
}
.sites_selector_container {
padding-top: 5px;
}
.siteSelect a {
white-space: normal;
text-align: left;
}

View file

@ -0,0 +1,8 @@
<?php
return array(
'Piwik\Plugins\CoreHome\Tracker\VisitRequestProcessor' => DI\object()
->constructorParameter('visitStandardLength', DI\get('ini.Tracker.visit_standard_length'))
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Some files were not shown because too many files have changed in this diff Show more