Jump to content

iwato

Members
  • Posts

    1,506
  • Joined

  • Last visited

  • Days Won

    4

Everything posted by iwato

  1. It is difficult for me to believe that I am still spending time to learn Javascript and PHP as subject material in and of themselves, but I am fascinated by language no matter its form, and these are especially useful languages. Indeed, I have recently discovered that jQuery can be incorporated into Javascript even at the prototype level. Please see below a functional example of this discovery. Indeed, a function is a function no matter where it is applied. Still, it is not quite that for which I was hoping (see question below). The HTML <body> <div id='test_array'>Can you see this?</div> </body> The Javascript Array.prototype.testArray = function() { //console.log(this.length); for (n = 0; n < this.length; n++) { console.log("ToArrayType : " + this[n]); $('#test_array').append($('<p></p>').html(this[n])); } }; arrayObject = ["a", "b", "c", "d", "e"]; arrayObject.testArray(); QUESTION: How would you replace the selector '#test_array' in such a way that the function testArray() would display its contents in the very place that it is invoked. The this pseudo variable does not work in this instance. Even the creation of a new, empty <div> tag fails. Roddy
  2. I could find nothing in the file that resembles an HTTP request. I agree that this is one of the key files for making requests. There are others as well, however. Let's go one step at a time. <?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\API; use Exception; use Piwik\Access; use Piwik\Cache; use Piwik\Common; use Piwik\DataTable; use Piwik\Exception\PluginDeactivatedException; use Piwik\IP; use Piwik\Log; use Piwik\Piwik; use Piwik\Plugin\Manager as PluginManager; use Piwik\Plugins\CoreHome\LoginWhitelist; use Piwik\SettingsServer; use Piwik\Url; use Piwik\UrlHelper; /** * Dispatches API requests to the appropriate API method. * * The Request class is used throughout Piwik to call API methods. The difference * between using Request and calling API methods directly is that Request * will do more after calling the API including: applying generic filters, applying queued filters, * and handling the **flat** and **label** query parameters. * * Additionally, the Request class will **forward current query parameters** to the request * which is more convenient than calling {@link Piwik\Common::getRequestVar()} many times over. * * In most cases, using a Request object to query the API is the correct approach. * * ### Post-processing * * The return value of API methods undergo some extra processing before being returned by Request. * * ### Output Formats * * The value returned by Request will be serialized to a certain format before being returned. * * ### Examples * * **Basic Usage** * * $request = new Request('method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week' * . '&format=xml&filter_limit=5&filter_offset=0') * $result = $request->process(); * echo $result; * * **Getting a unrendered DataTable** * * // use the convenience method 'processRequest' * $dataTable = Request::processRequest('UserLanguage.getLanguage', array( * 'idSite' => 1, * 'date' => 'yesterday', * 'period' => 'week', * 'filter_limit' => 5, * 'filter_offset' => 0 * * 'format' => 'original', // this is the important bit * )); * echo "This DataTable has " . $dataTable->getRowsCount() . " rows."; * * @see http://piwik.org/docs/analytics-api * @api */ class Request { private $request = null; /** * Converts the supplied request string into an array of query paramater name/value * mappings. The current query parameters (everything in `$_GET` and `$_POST`) are * forwarded to request array before it is returned. * * @param string|array|null $request The base request string or array, eg, * `'module=UserLanguage&action=getLanguage'`. * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded * from this. Defaults to `$_GET + $_POST`. * @return array */ public static function getRequestArrayFromString($request, $defaultRequest = null) { if ($defaultRequest === null) { $defaultRequest = self::getDefaultRequest(); $requestRaw = self::getRequestParametersGET(); if (!empty($requestRaw['segment'])) { $defaultRequest['segment'] = $requestRaw['segment']; } if (!isset($defaultRequest['format_metrics'])) { $defaultRequest['format_metrics'] = 'bc'; } } $requestArray = $defaultRequest; if (!is_null($request)) { if (is_array($request)) { $requestParsed = $request; } else { $request = trim($request); $request = str_replace(array("\n", "\t"), '', $request); $requestParsed = UrlHelper::getArrayFromQueryString($request); } $requestArray = $requestParsed + $defaultRequest; } foreach ($requestArray as &$element) { if (!is_array($element)) { $element = trim($element); } } return $requestArray; } /** * Constructor. * * @param string|array $request Query string that defines the API call (must at least contain a **method** parameter), * eg, `'method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week&format=xml'` * If a request is not provided, then we use the values in the `$_GET` and `$_POST` * superglobals. * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded * from this. Defaults to `$_GET + $_POST`. */ public function __construct($request = null, $defaultRequest = null) { $this->request = self::getRequestArrayFromString($request, $defaultRequest); $this->sanitizeRequest(); $this->renameModuleAndActionInRequest(); } /** * For backward compatibility: Piwik API still works if module=Referers, * we rewrite to correct renamed plugin: Referrers * * @param $module * @param $action * @return array( $module, $action ) * @ignore */ public static function getRenamedModuleAndAction($module, $action) { /** * This event is posted in the Request dispatcher and can be used * to overwrite the Module and Action to dispatch. * This is useful when some Controller methods or API methods have been renamed or moved to another plugin. * * @param $module string * @param $action string */ Piwik::postEvent('Request.getRenamedModuleAndAction', array(&$module, &$action)); return array($module, $action); } /** * Make sure that the request contains no logical errors */ private function sanitizeRequest() { // The label filter does not work with expanded=1 because the data table IDs have a different meaning // depending on whether the table has been loaded yet. expanded=1 causes all tables to be loaded, which // is why the label filter can't descend when a recursive label has been requested. // To fix this, we remove the expanded parameter if a label parameter is set. if (isset($this->request['label']) && !empty($this->request['label']) && isset($this->request['expanded']) && $this->request['expanded'] ) { unset($this->request['expanded']); } } /** * Dispatches the API request to the appropriate API method and returns the result * after post-processing. * * Post-processing includes: * * - flattening if **flat** is 0 * - running generic filters unless **disable_generic_filters** is set to 1 * - URL decoding label column values * - running queued filters unless **disable_queued_filters** is set to 1 * - removing columns based on the values of the **hideColumns** and **showColumns** query parameters * - filtering rows if the **label** query parameter is set * - converting the result to the appropriate format (ie, XML, JSON, etc.) * * If `'original'` is supplied for the output format, the result is returned as a PHP * object. * * @throws PluginDeactivatedException if the module plugin is not activated. * @throws Exception if the requested API method cannot be called, if required parameters for the * API method are missing or if the API method throws an exception and the **format** * query parameter is **original**. * @return DataTable|Map|string The data resulting from the API call. */ public function process() { // read the format requested for the output data $outputFormat = strtolower(Common::getRequestVar('format', 'xml', 'string', $this->request)); $disablePostProcessing = $this->shouldDisablePostProcessing(); // create the response $response = new ResponseBuilder($outputFormat, $this->request); if ($disablePostProcessing) { $response->disableDataTablePostProcessor(); } $corsHandler = new CORSHandler(); $corsHandler->handle(); $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $this->request); $shouldReloadAuth = false; try { // IP check is needed here as we cannot listen to API.Request.authenticate as it would then not return proper API format response. // We can also not do it by listening to API.Request.dispatch as by then the user is already authenticated and we want to make sure // to not expose any information in case the IP is not whitelisted. $whitelist = new LoginWhitelist(); if ($whitelist->shouldCheckWhitelist() && $whitelist->shouldWhitelistApplyToAPI()) { $ip = IP::getIpFromHeader(); $whitelist->checkIsWhitelisted($ip); } // read parameters $moduleMethod = Common::getRequestVar('method', null, 'string', $this->request); list($module, $method) = $this->extractModuleAndMethod($moduleMethod); list($module, $method) = self::getRenamedModuleAndAction($module, $method); PluginManager::getInstance()->checkIsPluginActivated($module); $apiClassName = self::getClassNameAPI($module); if ($shouldReloadAuth = self::shouldReloadAuthUsingTokenAuth($this->request)) { $access = Access::getInstance(); $tokenAuthToRestore = $access->getTokenAuth(); $hadSuperUserAccess = $access->hasSuperUserAccess(); self::forceReloadAuthUsingTokenAuth($tokenAuth); } // call the method $returnedValue = Proxy::getInstance()->call($apiClassName, $method, $this->request); $toReturn = $response->getResponse($returnedValue, $module, $method); } catch (Exception $e) { Log::debug($e); $toReturn = $response->getResponseException($e); } if ($shouldReloadAuth) { $this->restoreAuthUsingTokenAuth($tokenAuthToRestore, $hadSuperUserAccess); } return $toReturn; } private function restoreAuthUsingTokenAuth($tokenToRestore, $hadSuperUserAccess) { // if we would not make sure to unset super user access, the tokenAuth would be not authenticated and any // token would just keep super user access (eg if the token that was reloaded before had super user access) Access::getInstance()->setSuperUserAccess(false); // we need to restore by reloading the tokenAuth as some permissions could have been removed in the API // request etc. Otherwise we could just store a clone of Access::getInstance() and restore here self::forceReloadAuthUsingTokenAuth($tokenToRestore); if ($hadSuperUserAccess && !Access::getInstance()->hasSuperUserAccess()) { // we are in context of `doAsSuperUser()` and need to restore this behaviour Access::getInstance()->setSuperUserAccess(true); } } /** * Returns the name of a plugin's API class by plugin name. * * @param string $plugin The plugin name, eg, `'Referrers'`. * @return string The fully qualified API class name, eg, `'\Piwik\Plugins\Referrers\API'`. */ public static function getClassNameAPI($plugin) { return sprintf('\Piwik\Plugins\%s\API', $plugin); } /** * @ignore * @internal * @param bool $isRootRequestApiRequest */ public static function setIsRootRequestApiRequest($isRootRequestApiRequest) { Cache::getTransientCache()->save('API.setIsRootRequestApiRequest', $isRootRequestApiRequest); } /** * Detect if the root request (the actual request) is an API request or not. To detect whether an API is currently * request within any request, have a look at {@link isApiRequest()}. * * @return bool * @throws Exception */ public static function isRootRequestApiRequest() { $isApi = Cache::getTransientCache()->fetch('API.setIsRootRequestApiRequest'); return !empty($isApi); } /** * Detect if request is an API request. Meaning the module is 'API' and an API method having a valid format was * specified. Note that this method will return true even if the actual request is for example a regular UI * reporting page request but within this request we are currently processing an API request (eg a * controller calls Request::processRequest('API.getMatomoVersion')). To find out if the root request is an API * request or not, call {@link isRootRequestApiRequest()} * * @param array $request eg array('module' => 'API', 'method' => 'Test.getMethod') * @return bool * @throws Exception */ public static function isApiRequest($request) { $module = Common::getRequestVar('module', '', 'string', $request); $method = Common::getRequestVar('method', '', 'string', $request); return $module === 'API' && !empty($method) && (count(explode('.', $method)) === 2); } /** * If the token_auth is found in the $request parameter, * the current session will be authenticated using this token_auth. * It will overwrite the previous Auth object. * * @param array $request If null, uses the default request ($_GET) * @return void * @ignore */ public static function reloadAuthUsingTokenAuth($request = null) { // if a token_auth is specified in the API request, we load the right permissions $token_auth = Common::getRequestVar('token_auth', '', 'string', $request); if (self::shouldReloadAuthUsingTokenAuth($request)) { self::forceReloadAuthUsingTokenAuth($token_auth); } } /** * The current session will be authenticated using this token_auth. * It will overwrite the previous Auth object. * * @param string $tokenAuth * @return void */ private static function forceReloadAuthUsingTokenAuth($tokenAuth) { /** * Triggered when authenticating an API request, but only if the **token_auth** * query parameter is found in the request. * * Plugins that provide authentication capabilities should subscribe to this event * and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`) * is setup to use `$token_auth` when its `authenticate()` method is executed. * * @param string $token_auth The value of the **token_auth** query parameter. */ Piwik::postEvent('API.Request.authenticate', array($tokenAuth)); Access::getInstance()->reloadAccess(); SettingsServer::raiseMemoryLimitIfNecessary(); } private static function shouldReloadAuthUsingTokenAuth($request) { if (is_null($request)) { $request = self::getDefaultRequest(); } if (!isset($request['token_auth'])) { // no token is given so we just keep the current loaded user return false; } // a token is specified, we need to reload auth in case it is different than the current one, even if it is empty $tokenAuth = Common::getRequestVar('token_auth', '', 'string', $request); // not using !== is on purpose as getTokenAuth() might return null whereas $tokenAuth is '' . In this case // we do not need to reload. return $tokenAuth != Access::getInstance()->getTokenAuth(); } /** * Returns array($class, $method) from the given string $class.$method * * @param string $parameter * @throws Exception * @return array */ private function extractModuleAndMethod($parameter) { $a = explode('.', $parameter); if (count($a) != 2) { throw new Exception("The method name is invalid. Expected 'module.methodName'"); } return $a; } /** * Helper method that processes an API request in one line using the variables in `$_GET` * and `$_POST`. * * @param string $method The API method to call, ie, `'Actions.getPageTitles'`. * @param array $paramOverride The parameter name-value pairs to use instead of what's * in `$_GET` & `$_POST`. * @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded * from this. Defaults to `$_GET + $_POST`. * * To avoid using any parameters from $_GET or $_POST, set this to an empty `array()`. * @return mixed The result of the API request. See {@link process()}. */ public static function processRequest($method, $paramOverride = array(), $defaultRequest = null) { $params = array(); $params['format'] = 'original'; $params['serialize'] = '0'; $params['module'] = 'API'; $params['method'] = $method; $params = $paramOverride + $params; // process request $request = new Request($params, $defaultRequest); return $request->process(); } /** * Returns the original request parameters in the current query string as an array mapping * query parameter names with values. The result of this function will not be affected * by any modifications to `$_GET` and will not include parameters in `$_POST`. * * @return array */ public static function getRequestParametersGET() { if (empty($_SERVER['QUERY_STRING'])) { return array(); } $GET = UrlHelper::getArrayFromQueryString($_SERVER['QUERY_STRING']); return $GET; } /** * Returns the URL for the current requested report w/o any filter parameters. * * @param string $module The API module. * @param string $action The API action. * @param array $queryParams Query parameter overrides. * @return string */ public static function getBaseReportUrl($module, $action, $queryParams = array()) { $params = array_merge($queryParams, array('module' => $module, 'action' => $action)); return Request::getCurrentUrlWithoutGenericFilters($params); } /** * Returns the current URL without generic filter query parameters. * * @param array $params Query parameter values to override in the new URL. * @return string */ public static function getCurrentUrlWithoutGenericFilters($params) { // unset all filter query params so the related report will show up in its default state, // unless the filter param was in $queryParams $genericFiltersInfo = DataTableGenericFilter::getGenericFiltersInformation(); foreach ($genericFiltersInfo as $filter) { foreach ($filter[1] as $queryParamName => $queryParamInfo) { if (!isset($params[$queryParamName])) { $params[$queryParamName] = null; } } } return Url::getCurrentQueryStringWithParametersModified($params); } /** * Returns whether the DataTable result will have to be expanded for the * current request before rendering. * * @return bool * @ignore */ public static function shouldLoadExpanded() { // if filter_column_recursive & filter_pattern_recursive are supplied, and flat isn't supplied // we have to load all the child subtables. return Common::getRequestVar('filter_column_recursive', false) !== false && Common::getRequestVar('filter_pattern_recursive', false) !== false && !self::shouldLoadFlatten(); } /** * @return bool */ public static function shouldLoadFlatten() { return Common::getRequestVar('flat', false) == 1; } /** * Returns the segment query parameter from the original request, without modifications. * * @return array|bool */ public static function getRawSegmentFromRequest() { // we need the URL encoded segment parameter, we fetch it from _SERVER['QUERY_STRING'] instead of default URL decoded _GET $segmentRaw = false; $segment = Common::getRequestVar('segment', '', 'string'); if (!empty($segment)) { $request = Request::getRequestParametersGET(); if (!empty($request['segment'])) { $segmentRaw = $request['segment']; } } return $segmentRaw; } private function renameModuleAndActionInRequest() { if (empty($this->request['apiModule'])) { return; } if (empty($this->request['apiAction'])) { $this->request['apiAction'] = null; } list($this->request['apiModule'], $this->request['apiAction']) = $this->getRenamedModuleAndAction($this->request['apiModule'], $this->request['apiAction']); } /** * @return array */ private static function getDefaultRequest() { return $_GET + $_POST; } private function shouldDisablePostProcessing() { $shouldDisable = false; /** * After an API method returns a value, the value is post processed (eg, rows are sorted * based on the `filter_sort_column` query parameter, rows are truncated based on the * `filter_limit`/`filter_offset` parameters, amongst other things). * * If you're creating a plugin that needs to disable post processing entirely for * certain requests, use this event. * * @param bool &$shouldDisable Set this to true to disable datatable post processing for a request. * @param array $request The request parameters. */ Piwik::postEvent('Request.shouldDisablePostProcessing', [&$shouldDisable, $this->request]); return $shouldDisable; } }
  3. if (isset($_POST['methodName'])) { $result = []; foreach ($_POST['methodName'] as $method) { $method = filter_var($method, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH); if ($method == '_get') { $url = 'https://.../matomo/index.php?module=API&action=index&method=VisitsSummary.get&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=...'; $curl_request = curl_init(); curl_setopt($curl_request, CURLOPT_URL, $url); curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, true); $result[] = curl_exec($curl_request); if(curl_errno($curl_request)) { echo 'Curl error: ' . curl_error($curl_request); } curl_close($curl_request); } if ($method == '_getSumVisitsLengthPretty') { $url = 'https://.../matomo/index.php?module=API&action=index&method=VisitsSummary.getSumVisitsLengthPretty&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=...'; $curl_request = curl_init(); curl_setopt($curl_request, CURLOPT_URL, $url); curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, true); $result[] = curl_exec($curl_request); if(curl_errno($curl_request)) { echo 'Curl error: ' . curl_error($curl_request); } curl_close($curl_request); } } echo json_encode($result); } Roddy
  4. OK. I managed the AJAX and response, but for some reason I am unable to read properly what was returned. Returned Format: Array [ "{"nb_uniq_visitors":49,"nb_users":0…", "{"value":"17 hours 23 min"}" ] Attempted, but Failed Read Format: $.ajax({ url: '.../VisitsSummary.php', method: 'GET', data: {methodName : '_get', methodName : '_getSumVisitsLengthPretty'}, dataType: 'JSON', statusCode: { 404: function() { alert( "Page not found" ); }}, success: function(visitsSummary) { console.log(visitsSummary); var tv = visitsSummary[0].nb_visits; var uv = visitsSummary[0].nb_uniq_visitors; var visits_per_unique_visitor = Math.round(tv/uv * 10)/10; $('#so_total_visits').html(visitsSummary[0].nb_visits); $('#so_unique_visitors').html(visitsSummary[0].nb_uniq_visitors); $('#visits_per_visitor').html(visits_per_unique_visitor); $('#so_average_time_spent').html(visitsSummary[0].avg_time_on_site); } }); There were no messages. Simply nothing appeared. Any suggestions? Roddy
  5. To the best of my knowledge there never was an API per se. Simply a poorly constructed body of code that did not work. When I reported its dysfunctionality they took it down, but replaced it with nothing. Most unfortunately, I was not provided with an alternative body of code. There was no explanation, only acknowledgement and gratitude for my having reported the dysfunctionality. Roddy
  6. The links of the widget are not pointing to the proper places. My goal is not to be successful with one widget, however. My goal is to be successful with all widgets. There appear to be several possibilities: 1) cURL is stripping away important information. 2) The iframe src attribute is read differently depending on the source of the information (HTTP Request vs simple file address). Whatever is going on I must discover a way to process the discrepancy automatically and uniformally across widgets. Roddy
  7. Yes, the following appears to work: method: 'POST', data: {methodName['_get','_getSumVisitsLengthPretty']}, I know this because it is confirmed by the FireFox Network tab. The parameters sent are: methodName[ ] = "_get" methodName[ ] = "_getSumVisitsLengthPretty" What is not working is the PHP file, and I am baffled -- even after having cut the file in half and run only one variable. There is an internal server error, and I cannot find a mistake in the code. The file permissions have been verified and are correct. <?php ini_set('log_errors', 1); ini_set('error_log', dirname(__FILE__) . DIRECTORY_SEPARATOR . 'error.log'); ini_set('html_errors', 0); ini_set('display_errors', 0); error_reporting(E_ALL); if (isset($_POST['methodName'])) { $method_name = filter_var($_POST['methodName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH); if ($method_name[] == '_get') { $url = 'https://.../matomo/index.php?module=API&action=index&method=VisitsSummary.get&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=...'; $curl_request = curl_init(); curl_setopt($curl_request, CURLOPT_URL, $url); curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, false); curl_exec($curl_request); if(curl_errno($curl_request)) { echo 'Curl error: ' . curl_error($curl_request); } curl_close($curl_request); } } ?> In addition to the above problem I do not understand how to combine the two resulting JSON objects into a single JSON object that can be read by the AJAX success function. Even if I were to set the parameter value of the curl_setopt( ) from false to true and assign the return value of each curl_exec() function to a variable, how would I combine the resulting two values? Is it a simple concatenation of two strings? Must I rebuild the two JSON strings/objects from the two values? Is there a PHP function that handles this sort of thing automatically? Roddy
  8. I am using $.ajax() in this case, not $.get(), and AJAX does not like the following: {"methodName[]": '_get', "methodName[]" : '_getSumVisitsLengthPretty'} It returns nothing. In contrast, if I enter the following, AJAX produces good data for the object returned by the .get() method for the VisitsSummary class. Nothing, of course, is returned for the .getSumVisitsLengthPretty() method. {methodName: '_get'}
  9. So, it is the element in which the src attribute is located that determines how the content is loaded? Roddy
  10. Are you suggesting that the class designed to allow direct PHP access to the Matomo server without HTTP requests, actually uses HTTP requests to communicate with the server, anyway? If this is true, then I would like to thank you very much for this discovery. It moves contrary to my whole reason for wanting to avoid having to use HTTP requests -- better performance. Matomo is well-known on the internet for being resource-intensive, and my host server is already complaining. I am under tremendous pressure to move up to a VPS cloud,, but I am financially reluctant. I am just not there yet. Roddy
  11. When referring to the name of an element in an associative array you must learn to enclose the name of the element in either single or double quotation marks. Incorrect: $r[nature] Correct: $r['nature'] Roddy
  12. The term $r[symbol] looks like an unworkable mixture of PHP and Javascript. If $r is the name of a PHP array, then symbol must be enclosed in either single or double quotation marks. Also, what is present to cause the appearance of $r[symbol] to be read as the value of an element of a PHP array? Roddy
  13. QUESTION: Does Javascript called from a cached file behave the same as Javascript called in a freshly loaded file? Roddy
  14. BACKGROUND: I have now tried a number of ways to make two cURL calls within the same Javascript (jQuery) routine. For each new way, I have discovered what I believe to be a reasonable explanation for my failure. For my most recent attempt, however, a good explanation is not forthcoming. To be sure, single AJAX calls with a single cURL call for each yield the desired results. Simply I cannot seem to combine these results simultaneously on the same page. The scenario given below, for example, returns the desired outcome for the second cURL call, but not the first. JAVASCRIPT: $.ajax({ url: '.../VisitsSummary.php', method: 'GET', data: {methodName : '_get', methodName : '_getSumVisitsLengthPretty'}, dataType: 'JSON', statusCode: { 404: function() { alert( "Page not found" ); }}, success: function(visitsSummary_data) { console.log(visitsSummary_data); var tv = visitsSummary_data.nb_visits; var uv = visitsSummary_data.nb_uniq_visitors; var visits_per_unique_visitor = Math.round(tv/uv * 10)/10; $('#so_total_visits').html(visitsSummary_data.nb_visits); $('#so_unique_visitors').html(visitsSummary_data.nb_uniq_visitors); $('#visits_per_visitor').html(visits_per_unique_visitor); $('#so_average_time_spent').html(visitsSummary_data.avg_time_on_site); } }); PHP: if (isset($_GET['methodName'])) { $method_name = $_GET['methodName']; if ($_GET['methodName'] == '_get') { $url = 'https://.../index.php?module=API&action=index&method=VisitsSummary.get&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=...'; $curl_request = curl_init(); curl_setopt($curl_request, CURLOPT_URL, $url); curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, false); curl_exec($curl_request); if(curl_errno($curl_request)) { echo 'Curl error: ' . curl_error($curl_request); } curl_close($curl_request); } if ($_GET['methodName'] == '_getSumVisitsLengthPretty') { $url = 'https://.../index.php?module=API&action=index&method=VisitsSummary.getSumVisitsLengthPretty&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=...'; $curl_request = curl_init(); curl_setopt($curl_request, CURLOPT_URL, $url); curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, false); curl_exec($curl_request); if(curl_errno($curl_request)) { echo 'Curl error: ' . curl_error($curl_request); } curl_close($curl_request); } } QUESTION: Can you find anything wrong with the above strategy? If so, what would you recommend to replace it? Roddy
  15. Dsonesuk, I apologize for taking so long to return to this topic, but it was much more that I was expecting and required time to digest it. If I have understood correctly, its purpose is to show that everything in Javascript is indeed an object -- even functions. It is for this reason that one can even add a function to a function via the prototype property. Please confirm or disconfirm where appropriate. Roddy
  16. jQuery vs Javascript I am well aware that W3Schools prefers to teach Javascript in its most basic form, and I have learned much about the language as a result, but jQuery does much more than simplify the writing of Javascript statements. It also corrects for cross-browser discrepancies and clearly separates form and function in its plug-ins. Further, it looks for the simplest way to achieve a task and codifies it. in brief, on a statement per statement basis, Javascript will always perform better, for the reasons that you have given. Overall, however, jQuery serves to correct for coding inefficiency. An important thing that I have learned since I began coding, is that there are often many ways to achieve the same task. My gosh! As a beginner, I know for a fact that switch statements are slower than multiple if-else statements. Still I use them, for they make it so much easier to understand what I am doing as I do it, and what I have done when I return to it many months later. Roddy p.s. 1 x 10 = 10 (a full order larger) 1 x 10 x 3 = 30 (an order and a multiple larger) 1 x 3 = 3 (not an order larger, rather a multiple larger)
  17. By changing the file's extension from .html to .php and then using the $.get() method to call it in its modified format the problem was resolved. Unfortunately, I am still confused as to why this modification was necessary, when I have other files with the HTML extension that have no trouble processing PHP. Roddy
  18. OK. This makes sense, and a little research has revealed the file that handles the parsed query string. Obviously I could by-pass the need to write a query string by simply supplying the appropriate values for the public functions that generate the desired reports. It would appear that my next challenge would be to make sure that everything that is needed is present when I call the appropriate method. Below is the file in which the appropriate functions are found. Does a quick review of the file suggest that my idea is plausible? INSIDE MATOMO <./matomo/core/FrontController.php> namespace Piwik; use Exception; use Piwik\API\Request; use Piwik\Container\StaticContainer; use Piwik\Exception\AuthenticationFailedException; <?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; use Exception; use Piwik\API\Request; use Piwik\Container\StaticContainer; use Piwik\Exception\AuthenticationFailedException; use Piwik\Exception\DatabaseSchemaIsNewerThanCodebaseException; use Piwik\Exception\PluginDeactivatedException; use Piwik\Exception\StylesheetLessCompileException; use Piwik\Http\ControllerResolver; use Piwik\Http\Router; use Piwik\Plugins\CoreAdminHome\CustomLogo; /** * This singleton dispatches requests to the appropriate plugin Controller. * * Piwik uses this class for all requests that go through **index.php**. Plugins can * use it to call controller actions of other plugins. * * ### Examples * * **Forwarding controller requests** * * public function myConfiguredRealtimeMap() * { * $_GET['changeVisitAlpha'] = false; * $_GET['removeOldVisits'] = false; * $_GET['showFooterMessage'] = false; * return FrontController::getInstance()->dispatch('UserCountryMap', 'realtimeMap'); * } * * **Using other plugin controller actions** * * public function myPopupWithRealtimeMap() * { * $_GET['changeVisitAlpha'] = false; * $_GET['removeOldVisits'] = false; * $_GET['showFooterMessage'] = false; * $realtimeMap = FrontController::getInstance()->dispatch('UserCountryMap', 'realtimeMap'); * * $view = new View('@MyPlugin/myPopupWithRealtimeMap.twig'); * $view->realtimeMap = $realtimeMap; * return $realtimeMap->render(); * } * * For a detailed explanation, see the documentation [here](https://developer.piwik.org/guides/how-piwik-works). * * @method static \Piwik\FrontController getInstance() */ class FrontController extends Singleton { const DEFAULT_MODULE = 'CoreHome'; /** * Set to false and the Front Controller will not dispatch the request * * @var bool */ public static $enableDispatch = true; /** * @var bool */ private $initialized = false; /** * @param $lastError * @return string * @throws AuthenticationFailedException * @throws Exception */ private static function generateSafeModeOutputFromError($lastError) { Common::sendResponseCode(500); $controller = FrontController::getInstance(); try { $controller->init(); $message = $controller->dispatch('CorePluginsAdmin', 'safemode', array($lastError)); } catch(Exception $e) { // may fail in safe mode (eg. global.ini.php not found) $message = sprintf("Matomo encoutered an error: %s (which lead to: %s)", $lastError['message'], $e->getMessage()); } return $message; } /** * @param Exception $e * @return string */ private static function generateSafeModeOutputFromException($e) { $error = array( 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ); return self::generateSafeModeOutputFromError($error); } /** * Executes the requested plugin controller method. * * @throws Exception|\Piwik\Exception\PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist, * there is not enough permission, etc. * * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`. * @param string $action The controller method name, eg, `'realtimeMap'`. * @param array $parameters Array of parameters to pass to the controller method. * @return void|mixed The returned value of the call. This is the output of the controller method. * @api */ public function dispatch($module = null, $action = null, $parameters = null) { if (self::$enableDispatch === false) { return; } $filter = new Router(); $redirection = $filter->filterUrl(Url::getCurrentUrl()); if ($redirection !== null) { Url::redirectToUrl($redirection); return; } try { $result = $this->doDispatch($module, $action, $parameters); return $result; } catch (NoAccessException $exception) { Log::debug($exception); /** * Triggered when a user with insufficient access permissions tries to view some resource. * * This event can be used to customize the error that occurs when a user is denied access * (for example, displaying an error message, redirecting to a page other than login, etc.). * * @param \Piwik\NoAccessException $exception The exception that was caught. */ Piwik::postEvent('User.isNotAuthorized', array($exception), $pending = true); } catch (\Twig_Error_Runtime $e) { echo $this->generateSafeModeOutputFromException($e); exit; } catch(StylesheetLessCompileException $e) { echo $this->generateSafeModeOutputFromException($e); exit; } catch(\Error $e) { echo $this->generateSafeModeOutputFromException($e); exit; } } /** * Executes the requested plugin controller method and returns the data, capturing anything the * method `echo`s. * * _Note: If the plugin controller returns something, the return value is returned instead * of whatever is in the output buffer._ * * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`. * @param string $actionName The controller action name, eg, `'realtimeMap'`. * @param array $parameters Array of parameters to pass to the controller action method. * @return string The `echo`'d data or the return value of the controller action. * @deprecated */ public function fetchDispatch($module = null, $actionName = null, $parameters = null) { ob_start(); $output = $this->dispatch($module, $actionName, $parameters); // if nothing returned we try to load something that was printed on the screen if (empty($output)) { $output = ob_get_contents(); } else { // if something was returned, flush output buffer as it is meant to be written to the screen ob_flush(); } ob_end_clean(); return $output; } /** * Called at the end of the page generation */ public function __destruct() { try { if (class_exists('Piwik\\Profiler') && !SettingsServer::isTrackerApiRequest() ) { // in tracker mode Piwik\Tracker\Db\Pdo\Mysql does currently not implement profiling Profiler::displayDbProfileReport(); Profiler::printQueryCount(); } } catch (Exception $e) { Log::debug($e); } } // Should we show exceptions messages directly rather than display an html error page? public static function shouldRethrowException() { // If we are in no dispatch mode, eg. a script reusing Piwik libs, // then we should return the exception directly, rather than trigger the event "bad config file" // which load the HTML page of the installer with the error. return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH) || Common::isPhpCliMode() || SettingsServer::isArchivePhpTriggered(); } public static function setUpSafeMode() { register_shutdown_function(array('\\Piwik\\FrontController', 'triggerSafeModeWhenError')); } public static function triggerSafeModeWhenError() { $lastError = error_get_last(); if (!empty($lastError) && $lastError['type'] == E_ERROR) { $message = self::generateSafeModeOutputFromError($lastError); echo $message; } } /** * Must be called before dispatch() * - checks that directories are writable, * - loads the configuration file, * - loads the plugin, * - inits the DB connection, * - etc. * * @throws Exception * @return void */ public function init() { if ($this->initialized) { return; } $this->initialized = true; $tmpPath = StaticContainer::get('path.tmp'); $directoriesToCheck = array( $tmpPath, $tmpPath . '/assets/', $tmpPath . '/cache/', $tmpPath . '/logs/', $tmpPath . '/tcpdf/', $tmpPath . '/templates_c/', ); Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck); $this->handleMaintenanceMode(); $this->handleProfiler(); $this->handleSSLRedirection(); Plugin\Manager::getInstance()->loadPluginTranslations(); Plugin\Manager::getInstance()->loadActivatedPlugins(); // try to connect to the database try { Db::createDatabaseObject(); Db::fetchAll("SELECT DATABASE()"); } catch (Exception $exception) { if (self::shouldRethrowException()) { throw $exception; } Log::debug($exception); /** * Triggered when Piwik cannot connect to the database. * * This event can be used to start the installation process or to display a custom error * message. * * @param Exception $exception The exception thrown from creating and testing the database * connection. */ Piwik::postEvent('Db.cannotConnectToDb', array($exception), $pending = true); throw $exception; } // try to get an option (to check if data can be queried) try { Option::get('TestingIfDatabaseConnectionWorked'); } catch (Exception $exception) { if (self::shouldRethrowException()) { throw $exception; } Log::debug($exception); /** * Triggered when Piwik cannot access database data. * * This event can be used to start the installation process or to display a custom error * message. * * @param Exception $exception The exception thrown from trying to get an option value. */ Piwik::postEvent('Config.badConfigurationFile', array($exception), $pending = true); throw $exception; } // Init the Access object, so that eg. core/Updates/* can enforce Super User and use some APIs Access::getInstance(); /** * Triggered just after the platform is initialized and plugins are loaded. * * This event can be used to do early initialization. * * _Note: At this point the user is not authenticated yet._ */ Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen'); $this->throwIfPiwikVersionIsOlderThanDBSchema(); $module = Piwik::getModule(); $action = Piwik::getAction(); if (empty($module) || empty($action) || $module !== 'Installation' || !in_array($action, array('getInstallationCss', 'getInstallationJs'))) { \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins(); } // ensure the current Piwik URL is known for later use if (method_exists('Piwik\SettingsPiwik', 'getPiwikUrl')) { SettingsPiwik::getPiwikUrl(); } /** * Triggered before the user is authenticated, when the global authentication object * should be created. * * Plugins that provide their own authentication implementation should use this event * to set the global authentication object (which must derive from {@link Piwik\Auth}). * * **Example** * * Piwik::addAction('Request.initAuthenticationObject', function() { * StaticContainer::getContainer()->set('Piwik\Auth', new MyAuthImplementation()); * }); */ Piwik::postEvent('Request.initAuthenticationObject'); try { $authAdapter = StaticContainer::get('Piwik\Auth'); } catch (Exception $e) { $message = "Authentication object cannot be found in the container. Maybe the Login plugin is not activated? <br />You can activate the plugin by adding:<br /> <code>Plugins[] = Login</code><br /> under the <code>[Plugins]</code> section in your config/config.ini.php"; $ex = new AuthenticationFailedException($message); $ex->setIsHtmlMessage(); throw $ex; } Access::getInstance()->reloadAccess($authAdapter); // Force the auth to use the token_auth if specified, so that embed dashboard // and all other non widgetized controller methods works fine if (Common::getRequestVar('token_auth', false, 'string') !== false) { Request::reloadAuthUsingTokenAuth(); } SettingsServer::raiseMemoryLimitIfNecessary(); \Piwik\Plugin\Manager::getInstance()->postLoadPlugins(); /** * Triggered after the platform is initialized and after the user has been authenticated, but * before the platform has handled the request. * * Piwik uses this event to check for updates to Piwik. */ Piwik::postEvent('Platform.initialized'); } protected function prepareDispatch($module, $action, $parameters) { if (is_null($module)) { $module = Common::getRequestVar('module', self::DEFAULT_MODULE, 'string'); } if (is_null($action)) { $action = Common::getRequestVar('action', false); } if (SettingsPiwik::isPiwikInstalled() && ($module !== 'API' || ($action && $action !== 'index')) ) { Session::start(); $this->closeSessionEarlyForFasterUI(); } if (is_null($parameters)) { $parameters = array(); } if (!ctype_alnum($module)) { throw new Exception("Invalid module name '$module'"); } list($module, $action) = Request::getRenamedModuleAndAction($module, $action); if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) { throw new PluginDeactivatedException($module); } return array($module, $action, $parameters); } protected function handleMaintenanceMode() { if ((Config::getInstance()->General['maintenance_mode'] != 1) || Common::isPhpCliMode()) { return; } Common::sendResponseCode(503); $logoUrl = 'plugins/Morpheus/images/logo.svg'; $faviconUrl = 'plugins/CoreHome/images/favicon.png'; try { $logo = new CustomLogo(); if ($logo->hasSVGLogo()) { $logoUrl = $logo->getSVGLogoUrl(); } else { $logoUrl = $logo->getHeaderLogoUrl(); } $faviconUrl = $logo->getPathUserFavicon(); } catch (Exception $ex) { } $recordStatistics = Config::getInstance()->Tracker['record_statistics']; $trackMessage = ''; if ($recordStatistics) { $trackMessage = 'Your analytics data will continue to be tracked as normal.'; } else { $trackMessage = 'While the maintenance mode is active, data tracking is disabled.'; } $page = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/maintenance.tpl'); $page = str_replace('%logoUrl%', $logoUrl, $page); $page = str_replace('%faviconUrl%', $faviconUrl, $page); $page = str_replace('%piwikTitle%', Piwik::getRandomTitle(), $page); $page = str_replace('%trackMessage%', $trackMessage, $page); echo $page; exit; } protected function handleSSLRedirection() { // Specifically disable for the opt out iframe if (Piwik::getModule() == 'CoreAdminHome' && Piwik::getAction() == 'optOut') { return; } // Disable Https for VisitorGenerator if (Piwik::getModule() == 'VisitorGenerator') { return; } if (Common::isPhpCliMode()) { return; } // proceed only when force_ssl = 1 if (!SettingsPiwik::isHttpsForced()) { return; } Url::redirectToHttps(); } private function closeSessionEarlyForFasterUI() { $isDashboardReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=CoreHome&action=index') !== false; $isAllWebsitesReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=MultiSites&action=index') !== false; if ($isDashboardReferrer && !empty($_POST['token_auth']) && Common::getRequestVar('widget', 0, 'int') === 1 ) { Session::close(); } if (($isDashboardReferrer || $isAllWebsitesReferrer) && Common::getRequestVar('viewDataTable', '', 'string') === 'sparkline' ) { Session::close(); } } private function handleProfiler() { if (!empty($_GET['xhprof'])) { $mainRun = $_GET['xhprof'] == 1; // core:archive command sets xhprof=2 Profiler::setupProfilerXHProf($mainRun); } } /** * @param $module * @param $action * @param $parameters * @return mixed */ private function doDispatch($module, $action, $parameters) { list($module, $action, $parameters) = $this->prepareDispatch($module, $action, $parameters); /** * Triggered directly before controller actions are dispatched. * * This event can be used to modify the parameters passed to one or more controller actions * and can be used to change the controller action being dispatched to. * * @param string &$module The name of the plugin being dispatched to. * @param string &$action The name of the controller method being dispatched to. * @param array &$parameters The arguments passed to the controller action. */ Piwik::postEvent('Request.dispatch', array(&$module, &$action, &$parameters)); /** @var ControllerResolver $controllerResolver */ $controllerResolver = StaticContainer::get('Piwik\Http\ControllerResolver'); $controller = $controllerResolver->getController($module, $action, $parameters); /** * Triggered directly before controller actions are dispatched. * * This event exists for convenience and is triggered directly after the {@hook Request.dispatch} * event is triggered. * * It can be used to do the same things as the {@hook Request.dispatch} event, but for one controller * action only. Using this event will result in a little less code than {@hook Request.dispatch}. * * @param array &$parameters The arguments passed to the controller action. */ Piwik::postEvent(sprintf('Controller.%s.%s', $module, $action), array(&$parameters)); $result = call_user_func_array($controller, $parameters); /** * Triggered after a controller action is successfully called. * * This event exists for convenience and is triggered immediately before the {@hook Request.dispatch.end} * event is triggered. * * It can be used to do the same things as the {@hook Request.dispatch.end} event, but for one * controller action only. Using this event will result in a little less code than * {@hook Request.dispatch.end}. * * @param mixed &$result The result of the controller action. * @param array $parameters The arguments passed to the controller action. */ Piwik::postEvent(sprintf('Controller.%s.%s.end', $module, $action), array(&$result, $parameters)); /** * Triggered after a controller action is successfully called. * * This event can be used to modify controller action output (if any) before the output is returned. * * @param mixed &$result The controller action result. * @param array $parameters The arguments passed to the controller action. */ Piwik::postEvent('Request.dispatch.end', array(&$result, $module, $action, $parameters)); return $result; } /** * This method ensures that Piwik Platform cannot be running when using a NEWER database. */ private function throwIfPiwikVersionIsOlderThanDBSchema() { // When developing this situation happens often when switching branches if (Development::isEnabled()) { return; } $updater = new Updater(); $dbSchemaVersion = $updater->getCurrentComponentVersion('core'); $current = Version::VERSION; if (-1 === version_compare($current, $dbSchemaVersion)) { $messages = array( Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)), Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'), // we cannot fill in the Super User emails as we are failing before Authentication was ready Piwik::translate('General_ExceptionContactSupportGeneric', array('', '')) ); throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages)); } } } Roddy
  19. Though I did not see any 404 errors I certainly saw links that appeared to be failing in their destination. So, what is it about a src attribute that insures that everything is loaded into the widget before it is displayed in the iframe? I will be far less eager to take advantage of Matomo's widgets, if I have to reconfigure each and every broken link for each and every widget that I decide to incorporate into my webpage. Roddy
  20. Recently my hostserver modified their system and notified me of an .htaccess file that they placed on my website so that my HTML files could handle PHP. I opened and examined it, but saw nothing out of the ordinary. In fact, it appears very similar to what I use to use on my own test server before my hard disk failure. The file's permissions are 644. Might the problem be related to the absence of header tags? Once again, the file is little more than a text file with an .html extension. The document begins and ends with the same <div> element. Roddy
  21. No, you have the wrong idea about Matomo. I have nicknamed it Google Analytics without Google, for unlike Google everything is yours: the data that you collect as well as the application itself reside on your own server. Now, Matomo does offer a cloud, but you are by no means obligated to use it. Further, Matomo is a very portable self-contained package. Even, it has its own vendor folder for dependencies. Once you have downloaded and installed the package, you are free to do with it what you please within the normal restrictions of open-source software. Yes, there is an API. In fact, in the manual the word API is used everywhere to the point where it no longer makes any sense. In any case, I have been told that all reporting requests must pass through a file called index.php. With this understanding I composed a .php file with the following contents and ran the same query string that I successfully ran using an HTTP request from another file on my website.. The variable dump produces boolean false. ini_set('log_errors', 1); ini_set('error_log', dirname(__FILE__) . DIRECTORY_SEPARATOR . 'error.log'); ini_set('html_errors', 0); ini_set('display_errors', 0); error_reporting(E_ALL); define('PIWIK_INCLUDE_PATH', realpath('../../../../the_path_to_the_matomo_folder/matomo')); $result = file_get_contents(PIWIK_INCLUDE_PATH . DIRECTORY_SEPARATOR . 'index.php?module=API&action=index&method=VisitsSummary.get&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=my_authorization_token'); var_dump($result); When I examine the error.log file I am told [02-Jun-2018 01:32:25 UTC] PHP Warning: file_get_contents(/home/.../public_html/path_to_matomo_folder/php/matomo/index.php?module=API&action=index&method=VisitsSummary.get&idSite=1&period=year&date=today&format=json&token_auth=&token_auth=my_authorization_token): failed to open stream: No such file or directory in /home/.../public_html/path_to the _folder_containing_the_requesting_file/direct_request.php on line 9 Line 9 is file_get_contents() method containing the path to the index.php file. Roddy
  22. Although more complex than what I am writing here, one may think of Matomo, at least for our purposes, as divided into two parts: tracking and reporting. Although one may track with PHP, I am using Javascript for this purpose. On the reporting side Matomo offered either of two means: indirect HTTP requests with a GET query string, or direct access via a large variety of get methods categorize by the PHP classes that defined them. As Matomo is known on the internet for being resource intensive, and as I am working on shared SSL server, I prefer the latter means of accessing the report methods. In this spirit I brought it to the attention of Matomo that their recommended code did not perform as suggested, and they have since removed it from their site. What they did not do is replace it. Thus, I am left to achieve on my own what appears to be the most efficient manner, or resign myself to what now appears to be prohibited. I have since been told, by the way, that indirect access via HTTP requests is common procedure. Considering, however, that the Matomo application and my web application both lie on the same server in the same document tree -- albeit under different add-on domain names --, it seems unreasonable to have to make HTTP requests to achieve my reporting goals. In the Matomo forum set up for Matomo users Matomo appears to have adopted a stance of silence on this matter. It is for this reason that I have taken it up with W3Schools. Roddy
  23. It is configured, but the file in question does not respond as hoped. <?php echo date("r"); ?> Roddy
  24. Dear Kayut! You wrote: I provided you with a solution to your problem. Your gratitude: It does have much to do with the solution to your problem, however. This is the forum for jQuery, as are most forums dealing with Javascript. Your goal is to iterate across a series of div elements that share a common class. I provided you with the easiest solution of which I am aware. Hopefully you have read and understood well Ingolme's explanation, for it is unlikely that you will ever receive another response from me. To everyone else, when I think of order, I think in terms of multiples of 10. For example, two orders of magnitude would be some number of a 100 times faster. For the forum membership please see measurethat.net. jquery_vs_js.tiff
×
×
  • Create New...