@@ -9,7 +9,7 @@ var plugin = {},
     async = require('async'),
     log = common.log('compare:api');

-(function(plugin) {
+(function() {

     plugins.register('/o/compare/events', function(ob) {
         var params = ob.params;

var plugin = {},
    plugins = require('../../pluginManager.js'),
    countlyModel = require('../../../api/lib/countly.model.js'),
    countlySession = countlyModel.load("users"),
    countlyCommon = require('../../../api/lib/countly.common.js'),
    common = require('../../../api/utils/common.js'),
    fetch = require('../../../api/parts/data/fetch.js'),
    crypto = require('crypto'),
    async = require('async'),
    log = common.log('compare:api');

(function(plugin) {

    plugins.register('/o/compare/events', function(ob) {
        var params = ob.params;

        if (params.qstring.events) {
            try {
                params.qstring.events = JSON.parse(params.qstring.events);
            }
            catch (SyntaxError) {
                log.w('Parse /o/compare/events JSON failed');
            }
        }

        if (!params.qstring.events || params.qstring.events.length == 0) {
            return common.returnMessage(params, 400, 'Missing parameter: events');
        }

        if (params.qstring.events.length > 10) {
            return common.returnMessage(params, 400, 'Maximum length for parameter events is 10');
        }

        ob.validateUserForDataReadAPI(params, function() {
            var eventKeysArr = params.qstring.events;
            var collectionNames = [];

            for (var i = 0; i < eventKeysArr.length; i++) {
                collectionNames.push(
                    "events" + crypto.createHash('sha1').update(eventKeysArr[i] + params.app_id).digest('hex')
                );
            }

            async.map(collectionNames, getEventData, function(err, allEventData) {
                var outputObj = {};

                for (var i = 0; i < allEventData.length; i++) {
                    outputObj[eventKeysArr[i]] = allEventData[i];
                }

                common.returnOutput(params, outputObj);
            });
        });

        function getEventData(collectionName, callback) {
            fetch.getTimeObjForEvents(collectionName, params, function(output) {
                callback(null, output || {});
            });
        }

        return true;
    });

    plugins.register('/o/compare/apps', function(ob) {
        var params = ob.params;

        if (params.qstring.apps) {
            try {
                params.qstring.apps = JSON.parse(params.qstring.apps);
            }
            catch (SyntaxError) {
                log.w('Parse /o/compare/apps JSON failed');
            }
        }

        if (!params.qstring.apps || params.qstring.apps.length == 0) {
            return common.returnMessage(params, 400, 'Missing parameter: apps');
        }

        if (params.qstring.apps.length > 10) {
            return common.returnMessage(params, 400, 'Maximum length for parameter apps is 10');
        }

        var appsToFetch = params.qstring.apps;

        for (var i = 0; i < appsToFetch.length; i++) {
            if (appsToFetch[0].length != 24) {
                return common.returnMessage(params, 400, 'Invalid app id length in apps parameter, each app id should be 24 characters long');
            }
        }

        params.qstring.app_id = appsToFetch[0];

        ob.validateUserForDataReadAPI(params, function() {
            if (!params.member.global_admin) {
                for (var i = 0; i < appsToFetch.length; i++) {
                    if (params.member && params.member.user_of) {
                        if (params.member.user_of.indexOf(appsToFetch[i]) == -1) {
                            return common.returnMessage(params, 401, 'User does not have view rights for one or more apps provided in apps parameter');
                        }
                    }
                    else {
                        return common.returnMessage(params, 401, 'User does not have view rights for one or more apps provided in apps parameter');
                    }
                }
            }

            for (var i = 0; i < appsToFetch.length; i++) {
                appsToFetch[i] = common.db.ObjectID(appsToFetch[i]);
            }

            common.db.collection("apps").find({_id: {$in: appsToFetch}}, {_id: 1, name: 1}).toArray(function(err, apps) {

                function extractData(db, props) {
                    var chartData = [
                            { data: [], label: "", color: '#333933' }
                        ],
                        dataProps = [];
                    dataProps.push(props);
                    return countlyCommon.extractChartData(db, countlySession.clearObject, chartData, dataProps).chartDP[0].data;
                }

                function setAppId(inAppId) {
                    params.app_id = inAppId + "";
                }

                countlyCommon.setTimezone(params.appTimezone);

                async.map(apps, function(app, callback) {
                    console.log(JSON.stringify(app));
                    setAppId(app._id);

                    fetch.getTimeObj('users', params, function(usersDoc) {

                        // We need to set app_id once again here because after the callback
                        // it is reset to it's original value
                        setAppId(app._id);

                        fetch.getTotalUsersObj("users", params, function(dbTotalUsersObj) {
                            countlySession.setDb(usersDoc || {});
                            countlySession.setTotalUsersObj(fetch.formatTotalUsersObj(dbTotalUsersObj));

                            var sessionData = countlySession.getSessionData();
                            var charts = {
                                "total-users": extractData(usersDoc || {}, {
                                    name: "t",
                                    func: function(dataObj) {
                                        return dataObj.u;
                                    }
                                }),
                                "new-users": extractData(usersDoc || {}, { name: "n" }),
                                "total-sessions": extractData(usersDoc || {}, { name: "t" }),
                                "time-spent": extractData(usersDoc || {}, {
                                    name: "average",
                                    func: function(dataObj) {
                                        return ((dataObj.t == 0) ? 0 : ((dataObj.d / dataObj.t) / 60).toFixed(1));
                                    }
                                }),
                                "total-time-spent": extractData(usersDoc || {}, {
                                    name: "t",
                                    func: function(dataObj) {
                                        return ((dataObj.d / 60).toFixed(1));
                                    }
                                }),
                                "avg-events-served": extractData(usersDoc || {}, {
                                    name: "average",
                                    func: function(dataObj) {
                                        return ((dataObj.u == 0) ? 0 : ((dataObj.e / dataObj.u).toFixed(1)));
                                    }
                                })
                            };

                            var data = {id: app._id, name: app.name, sessions: sessionData.total_sessions, users: sessionData.total_users, newusers: sessionData.new_users, duration: sessionData.total_time, avgduration: sessionData.avg_time, charts: charts};

                            callback(null, data);
                        });
                    });
                },
                function(err, res) {
                    common.returnOutput(params, res);
                });
            });
        });

        return true;
    });

}(plugin));

module.exports = plugin;


@@ -55,7 +55,7 @@ var LinkTypesObjectSchema = {
     properties: [
         {name: 'objectCol',  type: 'TestObject'},
         {name: 'objectCol1', type: Realm.Types.OBJECT, objectType: 'TestObject'},
-        {name: 'arrayCol',   type: Realm.Types.ARRAY, objectType: 'TestObject'},
+        {name: 'arrayCol',   type: Realm.Types.LIST, objectType: 'TestObject'},
     ]
 };

////////////////////////////////////////////////////////////////////////////
//
// Copyright 2015 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

'use strict';

var TestObjectSchema = {
  name: 'TestObject',
  properties: [
    {name: 'doubleCol', type: Realm.Types.DOUBLE},
  ]
};

function PersonObject() {}
PersonObject.prototype.schema = {
  name: 'PersonObject',
  properties: [
    {name: 'name', type: Realm.Types.STRING},
    {name: 'age',  type: Realm.Types.DOUBLE},
  ]
};
PersonObject.prototype.description = function() {
    return this.name + ' ' + this.age;
};

var BasicTypesObjectSchema = {
    name: 'BasicTypesObject',
    properties: [
        {name: 'boolCol',   type: Realm.Types.BOOL},
        {name: 'intCol',    type: Realm.Types.INT},
        {name: 'floatCol',  type: Realm.Types.FLOAT},
        {name: 'doubleCol', type: Realm.Types.DOUBLE},
        {name: 'stringCol', type: Realm.Types.STRING},
        {name: 'dateCol',   type: Realm.Types.DATE},
        {name: 'dataCol',   type: Realm.Types.DATA},
    ]
};

var LinkTypesObjectSchema = {
    name: 'LinkTypesObject',
    properties: [
        {name: 'objectCol',  type: 'TestObject'},
        {name: 'objectCol1', type: Realm.Types.OBJECT, objectType: 'TestObject'},
        {name: 'arrayCol',   type: Realm.Types.ARRAY, objectType: 'TestObject'},
    ]
};

var IntPrimaryObjectSchema = {
  name: 'IntPrimaryObject',
  primaryKey: 'primaryCol',
  properties: [
    {name: 'primaryCol', type: Realm.Types.INT},
    {name: 'valueCol',   type: Realm.Types.STRING},
  ]
};

var AllTypesObjectSchema = {
  name: 'AllTypesObject',
  primaryKey: 'primaryCol',
  properties: [
    {name: 'primaryCol',type: Realm.Types.STRING},
    {name: 'boolCol',   type: Realm.Types.BOOL},
    {name: 'intCol',    type: Realm.Types.INT},
    {name: 'floatCol',  type: Realm.Types.FLOAT},
    {name: 'doubleCol', type: Realm.Types.DOUBLE},
    {name: 'stringCol', type: Realm.Types.STRING},
    {name: 'dateCol',   type: Realm.Types.DATE},
    {name: 'dataCol',   type: Realm.Types.DATA},
    {name: 'objectCol', type: 'TestObject'},
    {name: 'arrayCol',  type: Realm.Types.ARRAY, objectType: 'TestObject'},
  ]
};

var DefaultValuesObjectSchema = {
  name: 'DefaultValuesObject',
  properties: [
    {name: 'boolCol',   type: Realm.Types.BOOL,   default: true},
    {name: 'intCol',    type: Realm.Types.INT,    default: -1},
    {name: 'floatCol',  type: Realm.Types.FLOAT,  default: -1.1},
    {name: 'doubleCol', type: Realm.Types.DOUBLE, default: -1.11},
    {name: 'stringCol', type: Realm.Types.STRING, default: 'defaultString'},
    {name: 'dateCol',   type: Realm.Types.DATE,   default: new Date(1.111)},
    {name: 'dataCol',   type: Realm.Types.DATA,   default: 'defaultData'},
    {name: 'objectCol', type: 'TestObject',     default: [1]},
    {name: 'nullObjectCol', type: 'TestObject', default: null},
    {name: 'arrayCol',  type: Realm.Types.ARRAY, objectType: 'TestObject', default: [[2]]},
  ]
};


@@ -1,5 +1,5 @@
 /*global helpers */
-function normalizeRelatedNodes(node, xpath) {
+function normalizeRelatedNodes(node, options) {
 	'use strict';
 	['any','all','none'].forEach((type) => {
 		if (!Array.isArray(node[type])) {

/*global helpers */
function normalizeRelatedNodes(node, xpath) {
	'use strict';
	['any','all','none'].forEach((type) => {
		if (!Array.isArray(node[type])) {
			return;
		}
		node[type].filter((checkRes) => Array.isArray(checkRes.relatedNodes))
		.forEach((checkRes) => {
			checkRes.relatedNodes = checkRes.relatedNodes.map((relatedNode) => {
				var res = {
					html: relatedNode.source,
					target: relatedNode.selector
				};
				if (xpath) {
					res.xpath = relatedNode.xpath;
				}
				return res;
			});
		});

	});
}

var resultKeys = axe.constants.resultGroups;
helpers.processAggregate = function (results, options) {
	var resultObject = axe.utils.aggregateResult(results);

	resultObject.timestamp = new Date().toISOString();
	resultObject.url = window.location.href;

	resultKeys.forEach(function (key) {
		resultObject[key] = (resultObject[key] || []).map(function (ruleResult) {
			ruleResult = Object.assign({}, ruleResult);

			if (Array.isArray(ruleResult.nodes) && ruleResult.nodes.length > 0) {
				ruleResult.nodes = ruleResult.nodes.map(function (subResult) {
					if (typeof subResult.node === 'object') {
						subResult.html = subResult.node.source;
						subResult.target = subResult.node.selector;

						if (options.xpath) {
							subResult.xpath = subResult.node.xpath;
						}
					}
					delete subResult.result;
					delete subResult.node;

					normalizeRelatedNodes(subResult, options.xpath);

					return subResult;
				});
			}

			resultKeys.forEach((key) => delete ruleResult[key]);
			delete ruleResult.pageLevel;
			delete ruleResult.result;

			return ruleResult;
		});
	});

	return resultObject;
};


@@ -65,7 +65,7 @@ const Header = ( { children } ) => {
 				</section>
 			</header>
 			<LegacyErrorNotification />
-			{ featureFlags.storeErrorNotifications.enabled && <ErrorNotifications /> }
+			{ storeErrorNotifications && <ErrorNotifications /> }
 		</Fragment>
 	);
 };

/**
 * Header component.
 *
 * Site Kit by Google, Copyright 2019 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { Fragment } from '@wordpress/element';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import Logo from './Logo';
import UserMenu from './UserMenu';
import LegacyErrorNotification from './legacy-notifications/error-notification';
import ErrorNotifications from './notifications/ErrorNotifications';
import { STORE_NAME as CORE_USER } from '../googlesitekit/datastore/user/constants';
const { useSelect } = Data;

const Header = ( { children } ) => {
	const isAuthenticated = useSelect( ( select ) => select( CORE_USER ).isAuthenticated() );

	return (
		<Fragment>

							<Logo />

							{ isAuthenticated && children }
							{ isAuthenticated && <UserMenu /> }

			<LegacyErrorNotification />
			{ featureFlags.storeErrorNotifications.enabled && <ErrorNotifications /> }
		</Fragment>
	);
};

export default Header;


@@ -45,14 +45,14 @@ public class VirtualAuthenticatorTest extends JUnit4TestBase {
     + "      id: \"localhost\","
     + "      name: \"Selenium WebDriver Test\","
     + "    },"
-    + "    challenge: Uint8Array.from(\"challenge\"),"
+    + "    challenge: Int8Array.from(\"challenge\"),"
     + "    pubKeyCredParams: ["
     + "      {type: \"public-key\", alg: -7},"
     + "    ],"
     + "    user: {"
     + "      name: \"name\","
     + "      displayName: \"displayName\","
-    + "      id: Uint8Array.from([1]),"
+    + "      id: Int8Array.from([1]),"
     + "    },"
     + "  }, options);"

// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.selenium.virtualauthenticator;

import static org.assertj.core.api.Assumptions.assumeThat;
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.environment.webserver.Page;
import org.openqa.selenium.testing.JUnit4TestBase;
import org.openqa.selenium.testing.NotYetImplemented;
import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator;
import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions;

import java.util.Map;

public class VirtualAuthenticatorTest extends JUnit4TestBase {

  private final String script =
      "async function registerCredential(options = {}) {"
    + "  options = Object.assign({"
    + "    authenticatorSelection: {"
    + "      requireResidentKey: false,"
    + "    },"
    + "    rp: {"
    + "      id: \"localhost\","
    + "      name: \"Selenium WebDriver Test\","
    + "    },"
    + "    challenge: Uint8Array.from(\"challenge\"),"
    + "    pubKeyCredParams: ["
    + "      {type: \"public-key\", alg: -7},"
    + "    ],"
    + "    user: {"
    + "      name: \"name\","
    + "      displayName: \"displayName\","
    + "      id: Uint8Array.from([1]),"
    + "    },"
    + "  }, options);"

    + "  try {"
    + "    const credential = await navigator.credentials.create({publicKey: options});"
    + "    return {"
    + "      status: \"OK\","
    + "      credential: {"
    + "        id: credential.id,"
    + "        rawId: Array.from(new Uint8Array(credential.rawId)),"
    + "        transports: credential.response.getTransports(),"
    + "      }"
    + "    };"
    + "  } catch (error) {"
    + "    return {status: error.toString()};"
    + "  }"
    + "}"

    + "async function getCredential(credential, options = {}) {"
    + "  options = Object.assign({"
    + "    challenge: Uint8Array.from(\"Winter is Coming\"),"
    + "    rpId: \"localhost\","
    + "    allowCredentials: [credential],"
    + "    userVerification: \"preferred\","
    + "  }, options);"

    + "  try {"
    + "    const attestation = await navigator.credentials.get({publicKey: options});"
    + "    return {"
    + "      status: \"OK\","
    + "      attestation,"
    + "    };"
    + "  } catch (error) {"
    + "    return {status: error.toString()};"
    + "  }"
    + "}";

  @Before
  public void setup() {
    assumeThat(driver).isInstanceOf(HasVirtualAuthenticator.class);
    driver.get(appServer.create(new Page()
        .withTitle("Virtual Authenticator Test")
        .withScripts(script)));
  }

  @Test
  public void testCreateAuthenticator() {
    // Register a credential on the Virtual Authenticator.
    VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions();
    ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
    Map<String, Object> response = (Map<String, Object>)
      ((JavascriptExecutor) driver).executeAsyncScript(
        "registerCredential().then(arguments[arguments.length - 1]);");
    assertThat(response.get("status")).isEqualTo("OK");

    // Attempt to use the credential to get an assertion.
    Object credentialId = ((Map<String, Object>) response.get("credential")).get("rawId");
    response = (Map<String, Object>)
      ((JavascriptExecutor) driver).executeAsyncScript(
        "getCredential({"
      + "  \"type\": \"public-key\","
      + "  \"id\": Uint8Array.from(arguments[0]),"
      + "}).then(arguments[arguments.length - 1]);", credentialId);

    assertThat(response.get("status")).isEqualTo("OK");
  }

  @Test
  public void testRemoveAuthenticator() {
    VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions();
    VirtualAuthenticator authenticator =
      ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
    ((HasVirtualAuthenticator) driver).removeVirtualAuthenticator(authenticator);
    // no exceptions.
  }
}


@@ -73,8 +73,13 @@
      * @param arr
      * @returns {String}
      */
-    stringify: function (arr) {
-      var r, rLen, c, cLen, str = '', val;
+    stringify(arr) {
+      let r;
+      let rLen;
+      let c;
+      let cLen;
+      let str = '';
+      let val;

       for (r = 0, rLen = arr.length; r < rLen; r += 1) {
         cLen = arr[r].length;

/**
 * SheetClip - Spreadsheet Clipboard Parser
 * version 0.2
 *
 * This tiny library transforms JavaScript arrays to strings that are pasteable by LibreOffice, OpenOffice,
 * Google Docs and Microsoft Excel.
 *
 * Copyright 2012, Marcin Warpechowski
 * Licensed under the MIT license.
 * http://github.com/warpech/sheetclip/
 */
/*jslint white: true*/
(function (global) {
  "use strict";

  function countQuotes(str) {
    return str.split('"').length - 1;
  }

  var SheetClip = {
    /**
     * Decode spreadsheet string into array
     *
     * @param {String} str
     * @returns {Array}
     */
    parse: function (str) {
      var r, rLen, rows, arr = [], a = 0, c, cLen, multiline, last;

      rows = str.replace(/\r\n|\r/g, '\n').split('\n');

      if (rows.length > 1 && rows[rows.length - 1] === '') {
        rows.pop();
      }
      for (r = 0, rLen = rows.length; r < rLen; r += 1) {
        rows[r] = rows[r].split('\t');

        for (c = 0, cLen = rows[r].length; c < cLen; c += 1) {
          if (!arr[a]) {
            arr[a] = [];
          }
          if (multiline && c === 0) {
            last = arr[a].length - 1;
            arr[a][last] = arr[a][last] + '\n' + rows[r][0];

            if (multiline && (countQuotes(rows[r][0]) & 1)) { //& 1 is a bitwise way of performing mod 2
              multiline = false;
              arr[a][last] = arr[a][last].substring(0, arr[a][last].length - 1).replace(/""/g, '"');
            }
          }
          else {
            if (c === cLen - 1 && rows[r][c].indexOf('"') === 0 && (countQuotes(rows[r][c]) & 1)) {
              arr[a].push(rows[r][c].substring(1).replace(/""/g, '"'));
              multiline = true;
            }
            else {
              arr[a].push(rows[r][c].replace(/""/g, '"'));
              multiline = false;
            }
          }
        }
        if (!multiline) {
          a += 1;
        }
      }

      return arr;
    },

    /**
     * Encode array into valid spreadsheet string
     *
     * @param arr
     * @returns {String}
     */
    stringify: function (arr) {
      var r, rLen, c, cLen, str = '', val;

      for (r = 0, rLen = arr.length; r < rLen; r += 1) {
        cLen = arr[r].length;

        for (c = 0; c < cLen; c += 1) {
          if (c > 0) {
            str += '\t';
          }
          val = arr[r][c];

          if (typeof val === 'string') {
            if (val.indexOf('\n') > -1) {
              str += '"' + val.replace(/"/g, '""') + '"';
            }
            else {
              str += val;
            }
          }
          else if (val === null || val === void 0) { // void 0 resolves to undefined
            str += '';
          }
          else {
            str += val;
          }
        }

        if (r !== rLen - 1) {
          str += '\n';
        }
      }

      return str;
    }
  };

  if (typeof exports !== 'undefined') {
    exports.parse = SheetClip.parse;
    exports.stringify = SheetClip.stringify;
  } else {
    global.SheetClip = SheetClip;
  }
}(window));


@@ -84,6 +84,19 @@ export const reducer = Data.addInitializeReducer(
 		report.reducer,
 		tags.reducer,
 		urlchannels.reducer,
+		settings.reducer,
+		adblocker.reducer,
+		// TODO: Revisit better way to handle and retrieve errors.
+		( state, { type, payload } ) => {
+			if ( 'RECEIVE_ERROR' === type ) {
+				const { error } = payload;
+				return {
+					...state,
+					error,
+				};
+			}
+			return { ...state };
+		},
 	)
 );

/**
 * modules/adsense data store
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import Modules from 'googlesitekit-modules';
import accounts from './accounts';
import alerts from './alerts';
import clients from './clients';
import report from './report';
import tags from './tags';
import urlchannels from './urlchannels';

const baseModuleStore = Modules.createModuleStore( 'adsense', {
	settingSlugs: [
		'accountID',
		'clientID',
		'useSnippet',
		'accountStatus',
		'siteStatus',
	],
} );

export const STORE_NAME = baseModuleStore.STORE_NAME;

export const INITIAL_STATE = Data.collectState(
	baseModuleStore.INITIAL_STATE,
	accounts.INITIAL_STATE,
	alerts.INITIAL_STATE,
	clients.INITIAL_STATE,
	report.INITIAL_STATE,
	tags.INITIAL_STATE,
	urlchannels.INITIAL_STATE,
);

export const actions = Data.addInitializeAction( Data.collectActions(
	baseModuleStore.actions,
	accounts.actions,
	alerts.actions,
	clients.actions,
	report.actions,
	tags.actions,
	urlchannels.actions,
) );

export const controls = Data.collectControls(
	baseModuleStore.controls,
	accounts.controls,
	alerts.controls,
	clients.controls,
	report.controls,
	tags.controls,
	urlchannels.controls,
);

export const reducer = Data.addInitializeReducer(
	INITIAL_STATE,
	Data.collectReducers(
		baseModuleStore.reducer,
		accounts.reducer,
		alerts.reducer,
		clients.reducer,
		report.reducer,
		tags.reducer,
		urlchannels.reducer,
	)
);

export const resolvers = Data.collectResolvers(
	baseModuleStore.resolvers,
	accounts.resolvers,
	alerts.resolvers,
	clients.resolvers,
	report.resolvers,
	tags.resolvers,
	urlchannels.resolvers,
);

export const selectors = Data.collectSelectors(
	baseModuleStore.selectors,
	accounts.selectors,
	alerts.selectors,
	clients.selectors,
	report.selectors,
	tags.selectors,
	urlchannels.selectors,
	{
		// TODO: Revisit better way to handle and retrieve errors.
		getError( state ) {
			const { error } = state;

			return error;
		},
	},
);

const store = {
	actions,
	controls,
	reducer,
	resolvers,
	selectors,
};

// Register this store on the global registry.
Data.registerStore( STORE_NAME, store );

export default store;


@@ -66,8 +66,8 @@ function missingRequiredChildren(node, childRoles, all, role) {
 	// combobox exceptions
 	if (role === 'combobox') {
 		// remove 'textbox' from missing roles if combobox is a native text-type input or owns a 'searchbox'
-		var textboxIndex = missing.indexOf('textbox');
-		var textTypeInputs = ['text', 'search', 'email', 'url', 'tel'];
+		const textboxIndex = missing.indexOf('textbox');
+		const textTypeInputs = ['text', 'search', 'email', 'url', 'tel'];
 		if (
 			(textboxIndex >= 0 &&
 				(node.nodeName.toUpperCase() === 'INPUT' &&

const requiredOwned = axe.commons.aria.requiredOwned;
const implicitNodes = axe.commons.aria.implicitNodes;
const matchesSelector = axe.utils.matchesSelector;
const idrefs = axe.commons.dom.idrefs;
const hasContentVirtual = axe.commons.dom.hasContentVirtual;
const reviewEmpty =
	options && Array.isArray(options.reviewEmpty) ? options.reviewEmpty : [];

function owns(node, virtualTree, role, ariaOwned) {
	if (node === null) {
		return false;
	}
	var implicit = implicitNodes(role),
		selector = ['[role="' + role + '"]'];

	if (implicit) {
		selector = selector.concat(
			implicit.map(implicitSelector => implicitSelector + ':not([role])')
		);
	}

	selector = selector.join(',');
	return ariaOwned
		? matchesSelector(node, selector) ||
				!!axe.utils.querySelectorAll(virtualTree, selector)[0]
		: !!axe.utils.querySelectorAll(virtualTree, selector)[0];
}

function ariaOwns(nodes, role) {
	var index, length;

	for (index = 0, length = nodes.length; index < length; index++) {
		if (nodes[index] === null) {
			continue;
		}
		const virtualTree = axe.utils.getNodeFromTree(nodes[index]);
		if (owns(nodes[index], virtualTree, role, true)) {
			return true;
		}
	}
	return false;
}

function missingRequiredChildren(node, childRoles, all, role) {
	var index,
		length = childRoles.length,
		missing = [],
		ownedElements = idrefs(node, 'aria-owns');

	for (index = 0; index < length; index++) {
		var childRole = childRoles[index];
		if (
			owns(node, virtualNode, childRole) ||
			ariaOwns(ownedElements, childRole)
		) {
			if (!all) {
				return null;
			}
		} else {
			if (all) {
				missing.push(childRole);
			}
		}
	}

	// combobox exceptions
	if (role === 'combobox') {
		// remove 'textbox' from missing roles if combobox is a native text-type input or owns a 'searchbox'
		var textboxIndex = missing.indexOf('textbox');
		var textTypeInputs = ['text', 'search', 'email', 'url', 'tel'];
		if (
			(textboxIndex >= 0 &&
				(node.nodeName.toUpperCase() === 'INPUT' &&
					textTypeInputs.includes(node.type))) ||
			(owns(node, virtualNode, 'searchbox') ||
				ariaOwns(ownedElements, 'searchbox'))
		) {
			missing.splice(textboxIndex, 1);
		}

		// remove 'listbox' from missing roles if combobox is collapsed
		var listboxIndex = missing.indexOf('listbox');
		var expanded = node.getAttribute('aria-expanded');
		if (listboxIndex >= 0 && (!expanded || expanded === 'false')) {
			missing.splice(listboxIndex, 1);
		}
	}

	if (missing.length) {
		return missing;
	}
	if (!all && childRoles.length) {
		return childRoles;
	}
	return null;
}

function hasDecendantWithRole(node) {
	return (
		node.children &&
		node.children.some(child => {
			const role = axe.commons.aria.getRole(child);
			return (
				!['presentation', 'none', null].includes(role) ||
				hasDecendantWithRole(child)
			);
		})
	);
}

var role = node.getAttribute('role');
var required = requiredOwned(role);

if (!required) {
	return true;
}

var all = false;
var childRoles = required.one;
if (!childRoles) {
	var all = true;
	childRoles = required.all;
}

var missing = missingRequiredChildren(node, childRoles, all, role);

if (!missing) {
	return true;
}

this.data(missing);

// Only review empty nodes when a node is both empty and does not have an aria-owns relationship
if (
	reviewEmpty.includes(role) &&
	!hasContentVirtual(virtualNode, false, true) &&
	!hasDecendantWithRole(virtualNode) &&
	idrefs(node, 'aria-owns').length === 0
) {
	return undefined;
} else {
	return false;
}


@@ -1,7 +1,10 @@
 'use strict';

 const { expect } = require('chai');
+const { MongoClient } = require('../../../src');
 const { AggregateOperation } = require('../../../src/operations/aggregate');
+const { isHello } = require('../../../src/utils');
+const { HELLO, cleanup, createServer } = require('../../tools/mongodb-mock');

 describe('AggregateOperation', function () {
   const db = 'test';

'use strict';

const { expect } = require('chai');
const { AggregateOperation } = require('../../../src/operations/aggregate');

describe('AggregateOperation', function () {
  const db = 'test';

  describe('#constructor', function () {
    context('when out is in the options', function () {
      const operation = new AggregateOperation(db, [], { out: 'test', dbName: db });

      it('sets trySecondaryWrite to true', function () {
        expect(operation.trySecondaryWrite).to.be.true;
      });
    });

    context('when $out is the last stage', function () {
      const operation = new AggregateOperation(db, [{ $out: 'test' }], { dbName: db });

      it('sets trySecondaryWrite to true', function () {
        expect(operation.trySecondaryWrite).to.be.true;
      });
    });

    context('when $out is not the last stage', function () {
      const operation = new AggregateOperation(db, [{ $out: 'test' }, { $project: { name: 1 } }], {
        dbName: db
      });

      it('sets trySecondaryWrite to false', function () {
        expect(operation.trySecondaryWrite).to.be.false;
      });
    });

    context('when $merge is the last stage', function () {
      const operation = new AggregateOperation(db, [{ $merge: { into: 'test' } }], { dbName: db });

      it('sets trySecondaryWrite to true', function () {
        expect(operation.trySecondaryWrite).to.be.true;
      });
    });

    context('when $merge is not the last stage', function () {
      const operation = new AggregateOperation(
        db,
        [{ $merge: { into: 'test' } }, { $project: { name: 1 } }],
        { dbName: db }
      );

      it('sets trySecondaryWrite to false', function () {
        expect(operation.trySecondaryWrite).to.be.false;
      });
    });

    context('when no writable stages in empty pipeline', function () {
      const operation = new AggregateOperation(db, [], { dbName: db });

      it('sets trySecondaryWrite to false', function () {
        expect(operation.trySecondaryWrite).to.be.false;
      });
    });

    context('when no writable stages', function () {
      const operation = new AggregateOperation(db, [{ $project: { name: 1 } }], { dbName: db });

      it('sets trySecondaryWrite to false', function () {
        expect(operation.trySecondaryWrite).to.be.false;
      });
    });
  });
});


@@ -69,14 +69,10 @@ describe( 'core/site reset', () => {
 			it( 'does not require any params', () => {
 				expect( async () => {
 					const response = true;
-					fetch
-						.doMockOnceIf(
-							/^\/google-site-kit\/v1\/core\/site\/data\/reset/
-						)
-						.mockResponseOnce(
-							JSON.stringify( response ),
-							{ status: 200 }
-						);
+					fetchMock.once(
+						/^\/google-site-kit\/v1\/core\/site\/data\/reset/,
+						{ body: JSON.stringify( response ), status: 200 }
+					);

 					await registry.dispatch( STORE_NAME ).reset();
 				} ).not.toThrow();

/**
 * core/site data store: reset connection tests.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import apiFetch from '@wordpress/api-fetch';

/**
 * Internal dependencies
 */
import {
	createTestRegistry,
	muteConsole,
	subscribeUntil,
	unsubscribeFromAll,
} from 'tests/js/utils';
import { STORE_NAME } from './constants';

describe( 'core/site reset', () => {
	let apiFetchSpy;
	let registry;

	beforeEach( () => {
		registry = createTestRegistry();

		apiFetchSpy = jest.spyOn( { apiFetch }, 'apiFetch' );
	} );

	afterEach( () => {
		unsubscribeFromAll( registry );
		apiFetchSpy.mockRestore();
	} );

	describe( 'actions', () => {
		describe( 'fetchReset', () => {
			it( 'sets isDoingReset ', async () => {
				const response = true;
				fetch
					.doMockOnceIf(
						/^\/google-site-kit\/v1\/core\/site\/data\/reset/
					)
					.mockResponseOnce(
						JSON.stringify( response ),
						{ status: 200 }
					);

				registry.dispatch( STORE_NAME ).fetchReset();
				expect( registry.select( STORE_NAME ).isDoingReset() ).toEqual( true );
			} );
		} );

		describe( 'reset', () => {
			it( 'does not require any params', () => {
				expect( async () => {
					const response = true;
					fetch
						.doMockOnceIf(
							/^\/google-site-kit\/v1\/core\/site\/data\/reset/
						)
						.mockResponseOnce(
							JSON.stringify( response ),
							{ status: 200 }
						);

					await registry.dispatch( STORE_NAME ).reset();
				} ).not.toThrow();
			} );

			it( 'resets connection ', async () => {
				const response = true;
				fetch
					.doMockOnceIf(
						/^\/google-site-kit\/v1\/core\/site\/data\/reset/
					)
					.mockResponseOnce(
						JSON.stringify( response ),
						{ status: 200 }
					);

				registry
					.dispatch( STORE_NAME )
					.receiveConnection( { connected: true, resettable: true } );

				await registry.dispatch( STORE_NAME ).reset();
				expect( fetch ).toHaveBeenCalledTimes( 1 );

				fetch
					.doMockOnceIf(
						/^\/google-site-kit\/v1\/core\/site\/data\/connection/
					)
					.mockResponseOnce(
						JSON.stringify( { connected: false, resettable: false } ),
						{ status: 200 }
					);

				// After a successful reset, `connection` should be `undefined` again.
				const connection = await registry.select( STORE_NAME ).getConnection();
				expect( connection ).toEqual( undefined );
			} );

			it( 'does not reset local connection if reset request fails', async () => {
				// Make sure there is existing data in the store so we can ensure
				// it isn't reset.
				registry.dispatch( STORE_NAME ).receiveConnection(
					{ connected: true, resettable: true }
				);

				const response = {
					code: 'internal_server_error',
					message: 'Internal server error',
					data: { status: 500 },
				};
				fetch
					.doMockOnceIf(
						/^\/google-site-kit\/v1\/core\/site\/data\/reset/
					)
					.mockResponseOnce(
						JSON.stringify( response ),
						{ status: 500 }
					);

				muteConsole( 'error' );
				registry.dispatch( STORE_NAME ).reset();
				await subscribeUntil( registry, () => registry.select( STORE_NAME ).isDoingReset() === false );

				expect( fetch ).toHaveBeenCalledTimes( 1 );

				// After a failed reset, `connection` should still exist.
				const connection = registry.select( STORE_NAME ).getConnection();
				expect( connection ).toEqual( { connected: true, resettable: true } );
			} );
		} );
	} );
} );


@@ -20,7 +20,6 @@
  * WordPress dependencies
  */
 import { Fragment } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';

 /**
  * External dependencies

/**
 * ModuleApp component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classNames from 'classnames';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import Header from '../Header';
import Alert from '../Alert';
import ModuleHeader from './ModuleHeader';
import WidgetContextRenderer from '../../googlesitekit/widgets/components/WidgetContextRenderer';
import HelpMenu from '../help/HelpMenu';
import DateRangeSelector from '../DateRangeSelector';
import HelpMenuLink from '../help/HelpMenuLink';
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';

const { useSelect } = Data;

function ModuleApp( { moduleSlug } ) {
	const screenWidgetContext = useSelect( ( select ) =>
		select( CORE_MODULES ).getScreenWidgetContext( moduleSlug )
	);
	const moduleConnected = useSelect( ( select ) =>
		select( CORE_MODULES ).isModuleConnected( moduleSlug )
	);
	const getModuleHeader = () => <ModuleHeader moduleSlug={ moduleSlug } />;

	return (
		<Fragment>
			<Header>
				<HelpMenu>
					{ moduleSlug === 'adsense' && (
						<HelpMenuLink
							gaEventLabel="adsense_help"
							href="https://support.google.com/adsense/"
						>
							{ __( 'Get help with AdSense', 'google-site-kit' ) }
						</HelpMenuLink>
					) }
				</HelpMenu>
				{ moduleConnected && <DateRangeSelector /> }
			</Header>
			<Alert module={ moduleSlug } />
			<WidgetContextRenderer
				slug={ screenWidgetContext }
				className={ classNames( [
					'googlesitekit-module-page',
					`googlesitekit-module-page--${ moduleSlug }`,
				] ) }
				Header={ getModuleHeader }
			/>
		</Fragment>
	);
}

ModuleApp.propTypes = {
	moduleSlug: PropTypes.string,
};

export default ModuleApp;


@@ -1,5 +1,15 @@
-define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', 'serverNotifications', 'connectionManager', 'emby-button', 'listViewStyle'], function (events, globalize, dom, datefns, dfnshelper, userSettings, serverNotifications, connectionManager) {
-    'use strict';
+import events from 'events';
+import globalize from 'globalize';
+import dom from 'dom';
+import * as datefns from 'date-fns';
+import dfnshelper from 'dfnshelper';
+import userSettings from 'userSettings';
+import serverNotifications from 'serverNotifications';
+import connectionManager from 'connectionManager';
+import 'emby-button';
+import 'listViewStyle';
+
+/* eslint-disable indent */

     function getEntryHtml(entry, apiClient) {
         var html = '';

define(['events', 'globalize', 'dom', 'date-fns', 'dfnshelper', 'userSettings', 'serverNotifications', 'connectionManager', 'emby-button', 'listViewStyle'], function (events, globalize, dom, datefns, dfnshelper, userSettings, serverNotifications, connectionManager) {
    'use strict';

    function getEntryHtml(entry, apiClient) {
        var html = '';
        html += '
';
        var color = '#00a4dc';
        var icon = 'notifications';

        if ('Error' == entry.Severity || 'Fatal' == entry.Severity || 'Warn' == entry.Severity) {
            color = '#cc0000';
            icon = 'notification_important';
        }

        if (entry.UserId && entry.UserPrimaryImageTag) {
            html += '";
        } else {
            html += '';
        }

        html += '
';
        html += '
';
        html += entry.Name;
        html += '
';
        html += '
';
        html += datefns.formatRelative(Date.parse(entry.Date), Date.parse(new Date()), { locale: dfnshelper.getLocale() });
        html += '
';
        html += '
';
        html += entry.ShortOverview || '';
        html += '
';
        html += '
';

        if (entry.Overview) {
            html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">

                    </button>`;
        }

        html += '
';

        return html;
    }

    function renderList(elem, apiClient, result, startIndex, limit) {
        elem.innerHTML = result.Items.map(function (i) {
            return getEntryHtml(i, apiClient);
        }).join('');
    }

    function reloadData(instance, elem, apiClient, startIndex, limit) {
        if (null == startIndex) {
            startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0');
        }

        limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7');
        var minDate = new Date();
        var hasUserId = 'false' !== elem.getAttribute('data-useractivity');

        if (hasUserId) {
            minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
        } else {
            minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
        }

        ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
            startIndex: startIndex,
            limit: limit,
            minDate: minDate.toISOString(),
            hasUserId: hasUserId
        })).then(function (result) {
            elem.setAttribute('data-activitystartindex', startIndex);
            elem.setAttribute('data-activitylimit', limit);
            if (!startIndex) {
                var activityContainer = dom.parentWithClass(elem, 'activityContainer');

                if (activityContainer) {
                    if (result.Items.length) {
                        activityContainer.classList.remove('hide');
                    } else {
                        activityContainer.classList.add('hide');
                    }
                }
            }

            instance.items = result.Items;
            renderList(elem, apiClient, result, startIndex, limit);
        });
    }

    function onActivityLogUpdate(e, apiClient, data) {
        var options = this.options;

        if (options && options.serverId === apiClient.serverId()) {
            reloadData(this, options.element, apiClient);
        }
    }

    function onListClick(e) {
        var btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');

        if (btnEntryInfo) {
            var id = btnEntryInfo.getAttribute('data-id');
            var items = this.items;

            if (items) {
                var item = items.filter(function (i) {
                    return i.Id.toString() === id;
                })[0];

                if (item) {
                    showItemOverview(item);
                }
            }
        }
    }

    function showItemOverview(item) {
        require(['alert'], function (alert) {
            alert({
                text: item.Overview
            });
        });
    }

    function ActivityLog(options) {
        this.options = options;
        var element = options.element;
        element.classList.add('activityLogListWidget');
        element.addEventListener('click', onListClick.bind(this));
        var apiClient = connectionManager.getApiClient(options.serverId);
        reloadData(this, element, apiClient);
        var onUpdate = onActivityLogUpdate.bind(this);
        this.updateFn = onUpdate;
        events.on(serverNotifications, 'ActivityLogEntry', onUpdate);
        apiClient.sendMessage('ActivityLogEntryStart', '0,1500');
    }

    ActivityLog.prototype.destroy = function () {
        var options = this.options;

        if (options) {
            options.element.classList.remove('activityLogListWidget');
            connectionManager.getApiClient(options.serverId).sendMessage('ActivityLogEntryStop', '0,1500');
        }

        var onUpdate = this.updateFn;

        if (onUpdate) {
            events.off(serverNotifications, 'ActivityLogEntry', onUpdate);
        }

        this.items = null;
        this.options = null;
    };

    return ActivityLog;
});


@@ -23,6 +23,10 @@ import 'css!./style';
             source = entry;
         }

+        if (!entry.target.classList.contains('blurhashed')) {
+            itemBlurhashing(entry);
+        }
+
         if (entry.intersectionRatio > 0) {
             if (source) fillImageElement(entry.target, source);
         } else if (!source) {

import * as lazyLoader from 'lazyLoader';
import * as userSettings from 'userSettings';
import 'css!./style';
/* eslint-disable indent */

    export function lazyImage(elem, source = elem.getAttribute('data-src')) {
        if (!source) {
            return;
        }

        fillImageElement(elem, source);
    }

    export function fillImage(entry) {
        if (!entry) {
            throw new Error('entry cannot be null');
        }

        var source = undefined;
        if (entry.target) {
            source = entry.target.getAttribute('data-src');
        } else {
            source = entry;
        }

        if (entry.intersectionRatio > 0) {
            if (source) fillImageElement(entry.target, source);
        } else if (!source) {
            emptyImageElement(entry.target);
        }
    }

    function fillImageElement(elem, url) {
        if (url === undefined) {
            throw new Error('url cannot be undefined');
        }

        let preloaderImg = new Image();
        preloaderImg.src = url;

        preloaderImg.addEventListener('load', () => {
            if (elem.tagName !== 'IMG') {
                elem.style.backgroundImage = "url('" + url + "')";
            } else {
                elem.setAttribute('src', url);
            }

            if (userSettings.enableFastFadein()) {
                elem.classList.add('lazy-image-fadein-fast');
            } else {
                elem.classList.add('lazy-image-fadein');
            }

            elem.removeAttribute('data-src');
        });
    }

    function emptyImageElement(elem) {
        var url;

        if (elem.tagName !== 'IMG') {
            url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, '');
            elem.style.backgroundImage = 'none';
        } else {
            url = elem.getAttribute('src');
            elem.setAttribute('src', '');
        }

        elem.setAttribute('data-src', url);

        elem.classList.remove('lazy-image-fadein-fast');
        elem.classList.remove('lazy-image-fadein');
    }

    export function lazyChildren(elem) {
        lazyLoader.lazyChildren(elem, fillImage);
    }

    export function getPrimaryImageAspectRatio(items) {

        var values = [];

        for (var i = 0, length = items.length; i < length; i++) {

            var ratio = items[i].PrimaryImageAspectRatio || 0;

            if (!ratio) {
                continue;
            }

            values[values.length] = ratio;
        }

        if (!values.length) {
            return null;
        }

        // Use the median
        values.sort(function (a, b) {
            return a - b;
        });

        var half = Math.floor(values.length / 2);

        var result;

        if (values.length % 2) {
            result = values[half];
        } else {
            result = (values[half - 1] + values[half]) / 2.0;
        }

        // If really close to 2:3 (poster image), just return 2:3
        var aspect2x3 = 2 / 3;
        if (Math.abs(aspect2x3 - result) <= 0.15) {
            return aspect2x3;
        }

        // If really close to 16:9 (episode image), just return 16:9
        var aspect16x9 = 16 / 9;
        if (Math.abs(aspect16x9 - result) <= 0.2) {
            return aspect16x9;
        }

        // If really close to 1 (square image), just return 1
        if (Math.abs(1 - result) <= 0.15) {
            return 1;
        }

        // If really close to 4:3 (poster image), just return 2:3
        var aspect4x3 = 4 / 3;
        if (Math.abs(aspect4x3 - result) <= 0.15) {
            return aspect4x3;
        }

        return result;
    }

    export function fillImages(elems) {

        for (var i = 0, length = elems.length; i < length; i++) {
            var elem = elems[0];
            fillImage(elem);
        }
    }

/* eslint-enable indent */
export default {
    fillImages: fillImages,
    fillImage: fillImage,
    lazyImage: lazyImage,
    lazyChildren: lazyChildren,
    getPrimaryImageAspectRatio: getPrimaryImageAspectRatio
};


@@ -8,7 +8,6 @@ const EditorPanel = require('./EditorPanel')
 const PanelTopBar = require('./PickerPanelTopBar')
 const FileCard = require('./FileCard')
 const Slide = require('./Slide')
-const isDragDropSupported = require('@uppy/utils/lib/isDragDropSupported')

 // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
 // https://github.com/ghosh/micromodal

const { h } = require('preact')
const classNames = require('classnames')
const FileList = require('./FileList')
const AddFiles = require('./AddFiles')
const AddFilesPanel = require('./AddFilesPanel')
const PickerPanelContent = require('./PickerPanelContent')
const EditorPanel = require('./EditorPanel')
const PanelTopBar = require('./PickerPanelTopBar')
const FileCard = require('./FileCard')
const Slide = require('./Slide')
const isDragDropSupported = require('@uppy/utils/lib/isDragDropSupported')

// http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
// https://github.com/ghosh/micromodal

const WIDTH_XL = 900
const WIDTH_LG = 700
const WIDTH_MD = 576
const HEIGHT_MD = 400

module.exports = function Dashboard (props) {
  const noFiles = props.totalFileCount === 0
  const isSizeMD = props.containerWidth > WIDTH_MD

  const wrapperClassName = classNames({
    'uppy-Root': props.isTargetDOMEl,
  })

  const dashboardClassName = classNames({
    'uppy-Dashboard': true,
    'uppy-Dashboard--isDisabled': props.disabled,
    'uppy-Dashboard--animateOpenClose': props.animateOpenClose,
    'uppy-Dashboard--isClosing': props.isClosing,
    'uppy-Dashboard--isDraggingOver': props.isDraggingOver,
    'uppy-Dashboard--modal': !props.inline,
    'uppy-size--md': props.containerWidth > WIDTH_MD,
    'uppy-size--lg': props.containerWidth > WIDTH_LG,
    'uppy-size--xl': props.containerWidth > WIDTH_XL,
    'uppy-size--height-md': props.containerHeight > HEIGHT_MD,
    'uppy-Dashboard--isAddFilesPanelVisible': props.showAddFilesPanel,
    'uppy-Dashboard--isInnerWrapVisible': props.areInsidesReadyToBeVisible,
  })

  // Important: keep these in sync with the percent width values in `src/components/FileItem/index.scss`.
  let itemsPerRow = 1 // mobile
  if (props.containerWidth > WIDTH_XL) {
    itemsPerRow = 5
  } else if (props.containerWidth > WIDTH_LG) {
    itemsPerRow = 4
  } else if (props.containerWidth > WIDTH_MD) {
    itemsPerRow = 3
  }

  const showFileList = props.showSelectedFiles && !noFiles

  const numberOfFilesForRecovery = props.recoveredState ? Object.keys(props.recoveredState.files).length : null
  const numberOfGhosts = props.files ? Object.keys(props.files).filter((fileID) => props.files[fileID].isGhost).length : null

  const renderRestoredText = () => {
    if (numberOfGhosts > 0) {
      return props.i18n('recoveredXFiles', {
        smart_count: numberOfGhosts,
      })
    }

    return props.i18n('recoveredAllFiles')
  }

  const dashboard = (
    <div
      className={dashboardClassName}
      data-uppy-theme={props.theme}
      data-uppy-num-acquirers={props.acquirers.length}
      data-uppy-drag-drop-supported={!props.disableLocalFiles && isDragDropSupported()}
      aria-hidden={props.inline ? 'false' : props.isHidden}
      aria-disabled={props.disabled}
      aria-label={!props.inline ? props.i18n('dashboardWindowTitle') : props.i18n('dashboardTitle')}
      onPaste={props.handlePaste}
      onDragOver={props.handleDragOver}
      onDragLeave={props.handleDragLeave}
      onDrop={props.handleDrop}
    >

        {!props.inline ? (
          <button
            className="uppy-u-reset uppy-Dashboard-close"
            type="button"
            aria-label={props.i18n('closeModal')}
            title={props.i18n('closeModal')}
            onClick={props.closeModal}
          >
            ×
          </button>
        ) : null}

            {props.i18n('dropHint')}

          {showFileList && <PanelTopBar {...props} />}

          {numberOfFilesForRecovery && (

              <svg className="uppy-Dashboard-serviceMsg-icon" aria-hidden="true" focusable="false" width="21" height="16" viewBox="0 0 24 19">
                <g transform="translate(0 -1)" fill="none" fillRule="evenodd">
                  <path d="M12.857 1.43l10.234 17.056A1 1 0 0122.234 20H1.766a1 1 0 01-.857-1.514L11.143 1.429a1 1 0 011.714 0z" fill="#FFD300" />
                  <path fill="#000" d="M11 6h2l-.3 8h-1.4z" />
                  <circle fill="#000" cx="12" cy="17" r="1" />
                </g>
              </svg>

                {props.i18n('sessionRestored')}

                {renderRestoredText()}

          )}

          {showFileList ? (
            <FileList
              {...props}
              itemsPerRow={itemsPerRow}
            />
          ) : (
            <AddFiles {...props} isSizeMD={isSizeMD} />
          )}

          <Slide>
            {props.showAddFilesPanel ? <AddFilesPanel key="AddFiles" {...props} isSizeMD={isSizeMD} /> : null}
          </Slide>

          <Slide>
            {props.fileCardFor ? <FileCard key="FileCard" {...props} /> : null}
          </Slide>

          <Slide>
            {props.activePickerPanel ? <PickerPanelContent key="Picker" {...props} /> : null}
          </Slide>

          <Slide>
            {props.showFileEditor ? <EditorPanel key="Editor" {...props} /> : null}
          </Slide>

            {props.progressindicators.map((target) => {
              return props.getPlugin(target.id).render(props.state)
            })}

  )

  return (
    // Wrap it for RTL language support

      {dashboard}

  )
}


@@ -9,7 +9,9 @@ Page({
     ],
     fileList3: [{ url: 'https://img.yzcdn.cn/vant/sand.jpg' }],
     fileList4: [],
-    fileList5: []
+    fileList5: [],
+    fileList6: [],
+    cloudPath: []
   },

   beforeRead(event) {

import Page from '../../common/page';

Page({
  data: {
    fileList: [],
    fileList2: [
      { url: 'https://img.yzcdn.cn/vant/leaf.jpg' },
      { url: 'https://img.yzcdn.cn/vant/tree.jpg' }
    ],
    fileList3: [{ url: 'https://img.yzcdn.cn/vant/sand.jpg' }],
    fileList4: [],
    fileList5: []
  },

  beforeRead(event) {
    const { file, callback = () => {} } = event.detail;
    if (file.path.indexOf('jpeg') < 0) {
      wx.showToast({ title: '请选择jpg图片上传', icon: 'none' });
      callback(false);
      return;
    }
    callback(true);
  },

  afterRead(event) {
    const { file, name } = event.detail;
    const fileList = this.data[`fileList${name}`];

    this.setData({ [`fileList${name}`]: fileList.concat(file) });
  },

  oversize() {
    wx.showToast({ title: '文件超出大小限制', icon: 'none' });
  },

  delete(event) {
    const { index, name } = event.detail;
    const fileList = this.data[`fileList${name}`];
    fileList.splice(index, 1);
    this.setData({ [`fileList${name}`]: fileList });
  },

  clickPreview() {}
});


@@ -15,15 +15,18 @@ function PanelTopBar (props) {
       <div class="uppy-DashboardContent-title" role="heading" aria-level="h1">
         <DashboardContentTitle {...props} />
       </div>
-      <button class="uppy-DashboardContent-addMore"
-        type="button"
-        aria-label={props.i18n('addMoreFiles')}
-        title={props.i18n('addMoreFiles')}
-        onclick={() => props.toggleAddFilesPanel(true)}>
-        <svg class="UppyIcon" width="15" height="15" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg">
-          <path d="M7,6 L13,6 L13,7 L7,7 L7,13 L6,13 L6,7 L0,7 L0,6 L6,6 L6,0 L7,0 L7,6 Z" />
-        </svg>
-      </button>
+      { notOverFileLimit &&
+        <button class="uppy-DashboardContent-addMore"
+          type="button"
+          aria-label={props.i18n('addMoreFiles')}
+          title={props.i18n('addMoreFiles')}
+          onclick={() => props.toggleAddFilesPanel(true)}>
+          <svg class="UppyIcon" width="15" height="15" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg">
+            <path d="M7,6 L13,6 L13,7 L7,7 L7,13 L6,13 L6,7 L0,7 L0,6 L6,6 L6,0 L7,0 L7,6 Z" />
+          </svg>
+        </button>
+      }
+
     </div>
   )
 }

const { h } = require('preact')

function DashboardContentTitle (props) {
  if (props.newFiles.length) {
    return props.i18n('xFilesSelected', { smart_count: props.newFiles.length })
  }
}

function PanelTopBar (props) {
  return (

      <button class="uppy-DashboardContent-back"
        type="button"
        onclick={props.cancelAll}>{props.i18n('cancel')}</button>

        <DashboardContentTitle {...props} />

      <button class="uppy-DashboardContent-addMore"
        type="button"
        aria-label={props.i18n('addMoreFiles')}
        title={props.i18n('addMoreFiles')}
        onclick={() => props.toggleAddFilesPanel(true)}>
        <svg class="UppyIcon" width="15" height="15" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg">
          <path d="M7,6 L13,6 L13,7 L7,7 L7,13 L6,13 L6,7 L0,7 L0,6 L6,6 L6,0 L7,0 L7,6 Z" />
        </svg>
      </button>

  )
}

module.exports = PanelTopBar


@@ -21,4 +21,4 @@ exports.createAdminUser = function () {
             password
         };
     });
-}
+}

'use strict';
const require_method = require;
function node_require(module) {
    return require_method(module);
}

const Realm = node_require('realm');

const adminName = "realm-admin"
const password = '';

exports.createAdminUser = function () {
    const credentials = Realm.Sync.Credentials.usernamePassword(adminName, password);
    return Realm.Sync.User.login('http://127.0.0.1:9080', credentials).then((user) => {
        if (!user.isAdmin) {
            throw new Error(`${adminName} user is not an admin user on this server`);
        }

        return {
            username: adminName,
            password
        };
    });
}


@@ -52,9 +52,6 @@ function DashboardUniqueVisitorsWidget() {
 	} = useSelect( ( select ) => {
 		const store = select( STORE_NAME );

-		const accountID = store.getAccountID();
-		const profileID = store.getProfileID();
-		const internalWebPropertyID = store.getInternalWebPropertyID();
 		const commonArgs = {
 			dateRange: select( CORE_USER ).getDateRange(),
 		};

/**
 * DashboardAllTrafficWidget component.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * WordPress dependencies
 */
import { __, _x } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { STORE_NAME } from '../../datastore/constants';
import { STORE_NAME as CORE_SITE } from '../../../../googlesitekit/datastore/site/constants';
import { STORE_NAME as CORE_USER } from '../../../../googlesitekit/datastore/user/constants';
import whenActive from '../../../../util/when-active';
import PreviewBlock from '../../../../components/PreviewBlock';
import DataBlock from '../../../../components/data-block';
import Sparkline from '../../../../components/Sparkline';
import AnalyticsInactiveCTA from '../../../../components/AnalyticsInactiveCTA';
import { changeToPercent, readableLargeNumber } from '../../../../util';
import ReportError from '../../../../components/ReportError';
import ReportZero from '../../../../components/ReportZero';
import parseDimensionStringToDate from '../../util/parseDimensionStringToDate';
import applyEntityToReportPath from '../../util/applyEntityToReportPath';
import { isZeroReport } from '../../util';

const { useSelect } = Data;

function DashboardUniqueVisitorsWidget() {
	const {
		loading,
		error,
		sparkData,
		serviceURL,
		visitorsData,
	} = useSelect( ( select ) => {
		const store = select( STORE_NAME );

		const accountID = store.getAccountID();
		const profileID = store.getProfileID();
		const internalWebPropertyID = store.getInternalWebPropertyID();
		const commonArgs = {
			dateRange: select( CORE_USER ).getDateRange(),
		};

		const url = select( CORE_SITE ).getCurrentEntityURL();
		if ( url ) {
			commonArgs.url = url;
		}
		const sparklineArgs = {
			dimensions: 'ga:date',
			metrics: [
				{
					expression: 'ga:users',
					alias: 'Users',
				},
			],
			...commonArgs,
		};

		// This request needs to be separate from the sparkline request because it would result in a different total if it included the ga:date dimension.
		const args = {
			multiDateRange: 1,
			metrics: [
				{
					expression: 'ga:users',
					alias: 'Total Users',
				},
			],
			...commonArgs,
		};

		return {
			loading: ! store.hasFinishedResolution( 'getReport', [ sparklineArgs ] ) || ! store.hasFinishedResolution( 'getReport', [ args ] ),
			error: store.getErrorForSelector( 'getReport', [ sparklineArgs ] ) || store.getErrorForSelector( 'getReport', [ args ] ),
			// Due to the nature of these queries, we need to run them separately.
			sparkData: store.getReport( sparklineArgs ),
			serviceURL: store.getServiceURL(
				{
					path: applyEntityToReportPath( url, `/report/visitors-overview/a${ accountID }w${ internalWebPropertyID }p${ profileID }/` ),
				}
			),
			visitorsData: store.getReport( args ),
		};
	} );

	if ( loading ) {
		return <PreviewBlock width="100%" height="202px" />;
	}

	if ( error ) {
		return <ReportError moduleSlug="analytics" error={ error } />;
	}

	if ( isZeroReport( sparkData ) || isZeroReport( visitorsData ) ) {
		return <ReportZero moduleSlug="analytics" />;
	}

	const sparkLineData = [
		[
			{ type: 'date', label: 'Day' },
			{ type: 'number', label: 'Bounce Rate' },
		],
	];
	const dataRows = sparkData[ 0 ].data.rows;

	// Loop the rows to build the chart data.
	for ( let i = 0; i < dataRows.length; i++ ) {
		const { values } = dataRows[ i ].metrics[ 0 ];
		const dateString = dataRows[ i ].dimensions[ 0 ];
		const date = parseDimensionStringToDate( dateString );
		sparkLineData.push( [
			date,
			values[ 0 ],
		] );
	}

	const { totals } = visitorsData[ 0 ].data;
	const totalUsers = totals[ 0 ].values;
	const previousTotalUsers = totals[ 1 ].values;
	const totalUsersChange = changeToPercent( previousTotalUsers, totalUsers );

	return (
		<DataBlock
			className="overview-total-users"
			title={ __( 'Unique Visitors', 'google-site-kit' ) }
			datapoint={ readableLargeNumber( totalUsers ) }
			change={ totalUsersChange }
			changeDataUnit="%"
			source={ {
				name: _x( 'Analytics', 'Service name', 'google-site-kit' ),
				link: serviceURL,
				external: true,
			} }
			sparkline={
				sparkLineData &&
					<Sparkline
						data={ sparkLineData }
						change={ totalUsersChange }
					/>
			}
		/>
	);
}

export default whenActive( {
	moduleName: 'analytics',
	fallbackComponent: AnalyticsInactiveCTA,
} )( DashboardUniqueVisitorsWidget );


@@ -23,9 +23,4 @@ describe('truncateString', () => {
     expect(truncateString('hello world', 100)).toEqual('hello world')
     expect(truncateString('hello world', 11)).toEqual('hello world')
   })
-
-  it('should not truncate the string if it is too short to be meaningfully truncated', () => {
-    expect(truncateString('abc', 2)).toEqual('ab')
-    expect(truncateString('abc', 1)).toEqual('a')
-  })
 })

const truncateString = require('./truncateString')

describe('truncateString', () => {
  it('should truncate the string to the length', () => {
    expect(truncateString('abcdefghijkl', 14)).toEqual('abcdefghijkl')
    expect(truncateString('abcdefghijkl', 13)).toEqual('abcdefghijkl')
    expect(truncateString('abcdefghijkl', 12)).toEqual('abcdefghijkl')
    expect(truncateString('abcdefghijkl', 11)).toEqual('abcd...ijkl')
    expect(truncateString('abcdefghijkl', 10)).toEqual('abcd...jkl')
    expect(truncateString('abcdefghijkl', 9)).toEqual('abc...jkl')
    expect(truncateString('abcdefghijkl', 8)).toEqual('abc...kl')
    expect(truncateString('abcdefghijkl', 7)).toEqual('ab...kl')
    expect(truncateString('abcdefghijkl', 6)).toEqual('ab...l')
    expect(truncateString('abcdefghijkl', 5)).toEqual('a...l')
    expect(truncateString('abcdefghijkl', 4)).toEqual('a...')
    expect(truncateString('abcdefghijkl', 3)).toEqual('abc')
    expect(truncateString('abcdefghijkl', 2)).toEqual('ab')
    expect(truncateString('abcdefghijkl', 1)).toEqual('a')
    expect(truncateString('abcdefghijkl', 0)).toEqual('')
  })

  it('should not truncate the string if it is already short enough', () => {
    expect(truncateString('hello world', 100)).toEqual('hello world')
    expect(truncateString('hello world', 11)).toEqual('hello world')
  })

  it('should not truncate the string if it is too short to be meaningfully truncated', () => {
    expect(truncateString('abc', 2)).toEqual('ab')
    expect(truncateString('abc', 1)).toEqual('a')
  })
})


@@ -14,7 +14,7 @@ import localHooks from './../mixins/localHooks';
 class Transformation {
   constructor(range, options) {
     /**
-     * Instance of the SelectionRange, holder for coordinates applied to the table.
+     * Instance of the SelectionRange, holder for visual coordinates applied to the table.
      *
      * @type {SelectionRange}
      */

import { CellCoords } from './../3rdparty/walkontable/src';
import { mixin } from './../helpers/object';
import localHooks from './../mixins/localHooks';

/**
 * The Transformation class implements algorithms for transforming coordinates based on current settings
 * passed to the Handsontable.
 *
 * Transformation is always applied relative to the current selection.
 *
 * @class Transformation
 * @util
 */
class Transformation {
  constructor(range, options) {
    /**
     * Instance of the SelectionRange, holder for coordinates applied to the table.
     *
     * @type {SelectionRange}
     */
    this.range = range;
    /**
     * Additional options which define the state of the settings which can infer transformation.
     *
     * @type {Object}
     */
    this.options = options;
  }

  /**
   * Selects cell relative to current cell (if possible).
   *
   * @param {Number} rowDelta Rows number to move, value can be passed as negative number.
   * @param {Number} colDelta Columns number to move, value can be passed as negative number.
   * @param {Boolean} force If `true` the new rows/columns will be created if necessary. Otherwise, row/column will
   *                        be created according to `minSpareRows/minSpareCols` settings of Handsontable.
   * @returns {CellCoords}
   */
  transformStart(rowDelta, colDelta, force) {
    const delta = new CellCoords(rowDelta, colDelta);

    this.runLocalHooks('beforeTransformStart', delta);

    let totalRows = this.options.countRows();
    let totalCols = this.options.countCols();
    const fixedRowsBottom = this.options.fixedRowsBottom();
    const minSpareRows = this.options.minSpareRows();
    const minSpareCols = this.options.minSpareCols();
    const autoWrapRow = this.options.autoWrapRow();
    const autoWrapCol = this.options.autoWrapCol();
    const highlightCoords = this.range.current().highlight;

    if (highlightCoords.row + rowDelta > totalRows - 1) {
      if (force && minSpareRows > 0 && !(fixedRowsBottom && highlightCoords.row >= totalRows - fixedRowsBottom - 1)) {
        this.runLocalHooks('insertRowRequire', totalRows);
        totalRows = this.options.countRows();

      } else if (autoWrapCol) {
        delta.row = 1 - totalRows;
        delta.col = highlightCoords.col + delta.col === totalCols - 1 ? 1 - totalCols : 1;
      }
    } else if (autoWrapCol && highlightCoords.row + delta.row < 0 && highlightCoords.col + delta.col >= 0) {
      delta.row = totalRows - 1;
      delta.col = highlightCoords.col + delta.col === 0 ? totalCols - 1 : -1;
    }

    if (highlightCoords.col + delta.col > totalCols - 1) {
      if (force && minSpareCols > 0) {
        this.runLocalHooks('insertColRequire', totalCols);
        totalCols = this.options.countCols();

      } else if (autoWrapRow) {
        delta.row = highlightCoords.row + delta.row === totalRows - 1 ? 1 - totalRows : 1;
        delta.col = 1 - totalCols;
      }
    } else if (autoWrapRow && highlightCoords.col + delta.col < 0 && highlightCoords.row + delta.row >= 0) {
      delta.row = highlightCoords.row + delta.row === 0 ? totalRows - 1 : -1;
      delta.col = totalCols - 1;
    }

    const coords = new CellCoords(highlightCoords.row + delta.row, highlightCoords.col + delta.col);
    let rowTransformDir = 0;
    let colTransformDir = 0;

    if (coords.row < 0) {
      rowTransformDir = -1;
      coords.row = 0;

    } else if (coords.row > 0 && coords.row >= totalRows) {
      rowTransformDir = 1;
      coords.row = totalRows - 1;
    }

    if (coords.col < 0) {
      colTransformDir = -1;
      coords.col = 0;

    } else if (coords.col > 0 && coords.col >= totalCols) {
      colTransformDir = 1;
      coords.col = totalCols - 1;
    }
    this.runLocalHooks('afterTransformStart', coords, rowTransformDir, colTransformDir);

    return coords;
  }

  /**
   * Sets selection end cell relative to current selection end cell (if possible).
   *
   * @param {Number} rowDelta Rows number to move, value can be passed as negative number.
   * @param {Number} colDelta Columns number to move, value can be passed as negative number.
   * @returns {CellCoords}
   */
  transformEnd(rowDelta, colDelta) {
    const delta = new CellCoords(rowDelta, colDelta);

    this.runLocalHooks('beforeTransformEnd', delta);

    const totalRows = this.options.countRows();
    const totalCols = this.options.countCols();
    const cellRange = this.range.current();
    const coords = new CellCoords(cellRange.to.row + delta.row, cellRange.to.col + delta.col);
    let rowTransformDir = 0;
    let colTransformDir = 0;

    if (coords.row < 0) {
      rowTransformDir = -1;
      coords.row = 0;

    } else if (coords.row > 0 && coords.row >= totalRows) {
      rowTransformDir = 1;
      coords.row = totalRows - 1;
    }

    if (coords.col < 0) {
      colTransformDir = -1;
      coords.col = 0;

    } else if (coords.col > 0 && coords.col >= totalCols) {
      colTransformDir = 1;
      coords.col = totalCols - 1;
    }
    this.runLocalHooks('afterTransformEnd', coords, rowTransformDir, colTransformDir);

    return coords;
  }
}

mixin(Transformation, localHooks);

export default Transformation;


@@ -60,6 +60,42 @@ if (process.env.NODE_ENV !== 'production') {
 			'onRenderControls',
 			createDocUrl('onRenderControls')
 		);
+
+		if (props.onRenderActions) {
+			renderFunctionReturnContentsLackDisplayName(
+				COMPONENT,
+				'onRenderActions',
+				props.onRenderActions(),
+				PAGE_HEADER_CONTROL,
+				true
+			);
+		} else if (props.contentRight) {
+			renderFunctionReturnContentsLackDisplayName(
+				COMPONENT,
+				'contentRight',
+				props.contentRight,
+				PAGE_HEADER_CONTROL,
+				true
+			);
+		}
+
+		if (props.onRenderControls) {
+			renderFunctionReturnContentsLackDisplayName(
+				COMPONENT,
+				'onRenderControls',
+				props.onRenderControls(),
+				PAGE_HEADER_CONTROL,
+				true
+			);
+		} else if (props.navRight) {
+			renderFunctionReturnContentsLackDisplayName(
+				COMPONENT,
+				'navRight',
+				props.navRight,
+				PAGE_HEADER_CONTROL,
+				true
+			);
+		}
 	};
 }

/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */
/* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */
/* eslint-disable import/no-mutable-exports */

import deprecatedPropertyValue from '../../utilities/warning/deprecated-property-value';
import deprecatedProperty from '../../utilities/warning/deprecated-property';
import getComponentDocFn from '../../utilities/get-component-doc';

let checkProps = function checkPropsFunction() {};

if (process.env.NODE_ENV !== 'production') {
	checkProps = function checkPropsFunction(COMPONENT, props, jsonDoc) {
		const createDocUrl = getComponentDocFn(jsonDoc);
		if (props.variant === 'objectHome') {
			deprecatedPropertyValue(
				COMPONENT,
				{
					propAsString: 'variant',
					propValue: props.variant,
					deprecatedPropValue: 'objectHome',
					replacementPropAsValue: 'object-home',
				},
				`Using value of variants in camelCase is deprecated. Use kebab-case ('object-home') instead. ${createDocUrl(
					'variant'
				)}`
			);
		}
		if (props.variant === 'recordHome') {
			deprecatedPropertyValue(
				COMPONENT,
				{
					propAsString: 'variant',
					propValue: props.variant,
					deprecatedPropValue: 'recordHome',
					replacementPropAsValue: 'record-home',
				},
				`Using value of variants in camelCase is deprecated. Use kebab-case ('record-home') instead. ${createDocUrl(
					'variant'
				)}`
			);
		}
		if (props.variant === 'relatedList') {
			deprecatedPropertyValue(
				COMPONENT,
				{
					propAsString: 'variant',
					propValue: props.variant,
					deprecatedPropValue: 'relatedList',
					replacementPropAsValue: 'related-list',
				},
				`Using value of variants in camelCase is deprecated. Use kebab-case ('related-list') instead. ${createDocUrl(
					'variant'
				)}`
			);
		}
		deprecatedProperty(
			COMPONENT,
			props.navRight,
			'navRight',
			'onRenderControls',
			createDocUrl('onRenderControls')
		);
	};
}

export default checkProps;


@@ -129,6 +129,10 @@ beforeAll( async() => {
 	capturePageEventsForTearDown();
 	enablePageDialogAccept();
 	observeConsoleLogging();
+	if ( process.env.DEBUG_REST ) {
+		page.on( 'request', observeRestRequest );
+		page.on( 'response', observeRestResponse );
+	}
 	await setBrowserViewport( 'large' );
 } );

/**
 * External dependencies
 */
import { get } from 'lodash';

/**
 * WordPress dependencies
 */
import {
	clearLocalStorage,
	enablePageDialogAccept,
	setBrowserViewport,
} from '@wordpress/e2e-test-utils';

/**
 * Environment variables
 */
const { PUPPETEER_TIMEOUT } = process.env;

/**
 * Set of console logging types observed to protect against unexpected yet
 * handled (i.e. not catastrophic) errors or warnings. Each key corresponds
 * to the Puppeteer ConsoleMessage type, its value the corresponding function
 * on the console global object.
 *
 * @type {Object<string,string>}
 */
const OBSERVED_CONSOLE_MESSAGE_TYPES = {
	warning: 'warn',
	error: 'error',
};

/**
 * Array of page event tuples of [ eventName, handler ].
 *
 * @type {Array}
 */
const pageEvents = [];

// The Jest timeout is increased because these tests are a bit slow
jest.setTimeout( PUPPETEER_TIMEOUT || 100000 );

/**
 * Adds an event listener to the page to handle additions of page event
 * handlers, to assure that they are removed at test teardown.
 */
function capturePageEventsForTearDown() {
	page.on( 'newListener', ( eventName, listener ) => {
		pageEvents.push( [ eventName, listener ] );
	} );
}

/**
 * Removes all bound page event handlers.
 */
function removePageEvents() {
	pageEvents.forEach( ( [ eventName, handler ] ) => {
		page.removeListener( eventName, handler );
	} );
}

/**
 * Adds a page event handler to emit uncaught exception to process if one of
 * the observed console logging types is encountered.
 */
function observeConsoleLogging() {
	page.on( 'console', ( message ) => {
		const type = message.type();
		if ( ! OBSERVED_CONSOLE_MESSAGE_TYPES.hasOwnProperty( type ) ) {
			return;
		}

		let text = message.text();

		// An exception is made for _blanket_ deprecation warnings: Those
		// which log regardless of whether a deprecated feature is in use.
		if ( text.includes( 'This is a global warning' ) ) {
			return;
		}

		// Viewing posts on the front end can result in this error, which
		// has nothing to do with Gutenberg.
		if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
			return;
		}

		// A bug present in WordPress 5.2 will produce console warnings when
		// loading the Dashicons font. These can be safely ignored, as they do
		// not otherwise regress on application behavior. This logic should be
		// removed once the associated ticket has been closed.
		//
		// See: https://core.trac.wordpress.org/ticket/47183
		if (
			text.startsWith( 'Failed to decode downloaded font:' ) ||
			text.startsWith( 'OTS parsing error:' )
		) {
			return;
		}

		const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ];

		// As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of
		// type JSHandle for error logging, instead of the expected string.
		//
		// See: https://github.com/GoogleChrome/puppeteer/issues/3397
		//
		// The recommendation there to asynchronously resolve the error value
		// upon a console event may be prone to a race condition with the test
		// completion, leaving a possibility of an error not being surfaced
		// correctly. Instead, the logic here synchronously inspects the
		// internal object shape of the JSHandle to find the error text. If it
		// cannot be found, the default text value is used instead.
		text = get( message.args(), [ 0, '_remoteObject', 'description' ], text );

		// Disable reason: We intentionally bubble up the console message
		// which, unless the test explicitly anticipates the logging via
		// @wordpress/jest-console matchers, will cause the intended test
		// failure.

		// eslint-disable-next-line no-console
		console[ logFunction ]( text );
	} );
}

// Before every test suite run, delete all content created by the test. This ensures
// other posts/comments/etc. aren't dirtying tests and tests don't depend on
// each other's side-effects.
beforeAll( async() => {
	capturePageEventsForTearDown();
	enablePageDialogAccept();
	observeConsoleLogging();
	await setBrowserViewport( 'large' );
} );

afterEach( async() => {
	await clearLocalStorage();
	await setBrowserViewport( 'large' );
} );

afterAll( () => {
	removePageEvents();
} );


@@ -95,4 +95,5 @@ export default function ModulePopularPagesWidget( { Widget, WidgetReportError }
 ModulePopularPagesWidget.propTypes = {
 	Widget: PropTypes.elementType.isRequired,
 	WidgetReportError: PropTypes.elementType.isRequired,
+	WidgetReportZero: PropTypes.elementType.isRequired,
 };

/**
 * ModulePopularPagesWidget component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { CORE_USER } from '../../../../../googlesitekit/datastore/user/constants';
import { DATE_RANGE_OFFSET, MODULES_ANALYTICS } from '../../../datastore/constants';
import PreviewTable from '../../../../../components/PreviewTable';
import Header from './Header';
import Table from './Table';
import Footer from './Footer';
const { useSelect } = Data;

export default function ModulePopularPagesWidget( { Widget, WidgetReportError } ) {
	const { startDate, endDate } = useSelect( ( select ) => select( CORE_USER ).getDateRangeDates( {
		offsetDays: DATE_RANGE_OFFSET,
	} ) );

	const args = {
		startDate,
		endDate,
		dimensions: [
			'ga:pageTitle',
			'ga:pagePath',
		],
		metrics: [
			{
				expression: 'ga:pageviews',
				alias: 'Pageviews',
			},
			{
				expression: 'ga:uniquePageviews',
				alias: 'Unique Pageviews',
			},
			{
				expression: 'ga:bounceRate',
				alias: 'Bounce rate',
			},
		],
		orderby: [
			{
				fieldName: 'ga:pageviews',
				sortOrder: 'DESCENDING',
			},
		],
		limit: 10,
	};

	const report = useSelect( ( select ) => select( MODULES_ANALYTICS ).getReport( args ) );
	const loaded = useSelect( ( select ) => select( MODULES_ANALYTICS ).hasFinishedResolution( 'getReport', [ args ] ) );
	const error = useSelect( ( select ) => select( MODULES_ANALYTICS ).getErrorForSelector( 'getReport', [ args ] ) );

	if ( error ) {
		return <WidgetReportError error={ error } />;
	}

	return (
		<Widget
			Header={ Header }
			Footer={ Footer }
			noPadding
		>
			{ ! loaded && (
				<PreviewTable padding />
			) }
			{ loaded && (
				<Table report={ report } />
			) }
		</Widget>
	);
}

ModulePopularPagesWidget.propTypes = {
	Widget: PropTypes.elementType.isRequired,
	WidgetReportError: PropTypes.elementType.isRequired,
};


@@ -82,7 +82,7 @@ class Router implements \Laminas\Log\LoggerAwareInterface
      * @return string|bool
      */
     public function getUrl(RecordDriver $driver, $size = 'small',
-        $resolveDynamic = true
+        $resolveDynamic = true, $testLoadImage = false
     ) {
         // Try to build thumbnail:
         $thumb = $driver->tryMethod('getThumbnail', [$size]);

<?php
/**
 * Cover image router
 *
 * PHP version 7
 *
 * Copyright (C) Villanova University 2016.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @category VuFind
 * @package  Cover_Generator
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
 * @link     https://vufind.org/wiki/configuration:external_content Wiki
 */
namespace VuFind\Cover;

use VuFind\Cover\Loader as CoverLoader;
use VuFind\RecordDriver\AbstractBase as RecordDriver;

/**
 * Cover image router
 *
 * @category VuFind
 * @package  Cover_Generator
 * @author   Demian Katz <demian.katz@villanova.edu>
 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
 * @link     https://vufind.org/wiki/configuration:external_content Wiki
 */
class Router implements \Laminas\Log\LoggerAwareInterface
{
    use \VuFind\Log\LoggerAwareTrait;

    /**
     * Base URL for dynamic cover images.
     *
     * @var string
     */
    protected $dynamicUrl;

    /**
     * Cover loader
     *
     * @var CoverLoader
     */
    protected $coverLoader;

    /**
     * Constructor
     *
     * @param string      $url         Base URL for dynamic cover images.
     * @param CoverLoader $coverLoader Cover loader
     */
    public function __construct($url, CoverLoader $coverLoader)
    {
        $this->dynamicUrl = $url;
        $this->coverLoader = $coverLoader;
    }

    /**
     * Generate a thumbnail URL (return false if unsupported; return null to indicate
     * that a subsequent AJAX check is needed).
     *
     * @param RecordDriver $driver         Record driver
     * @param string       $size           Size of thumbnail (small, medium or large;
     * small is default).
     * @param bool         $resolveDynamic Should we resolve dynamic cover data into
     * a URL (true) or simply return false (false)?
     *
     * @return string|bool
     */
    public function getUrl(RecordDriver $driver, $size = 'small',
        $resolveDynamic = true
    ) {
        // Try to build thumbnail:
        $thumb = $driver->tryMethod('getThumbnail', [$size]);

        // No thumbnail?  Return false:
        if (empty($thumb)) {
            return false;
        }

        // Array? It's parameters to send to the cover generator:
        if (is_array($thumb)) {
            if (!$resolveDynamic) {
                return null;
            }
            $dynamicUrl =  $this->dynamicUrl . '?' . http_build_query($thumb);
        } else {
            return $thumb;
        }

        $settings = is_array($thumb) ? array_merge($thumb, ['size' => $size])
            : ['size' => $size];
        $handlers = $this->coverLoader->getHandlers();
        $ids = $this->coverLoader->getIdentifiersForSettings($settings);

        foreach ($handlers as $handler) {
            try {
                // Is the current provider appropriate for the available data?
                if ($handler['handler']->supports($ids)
                    && $handler['handler']->useDirectUrls()
                ) {
                    $nextDirectUrl = $handler['handler']
                        ->getUrl($handler['key'], $size, $ids);
                    if ($nextDirectUrl !== false) {
                        $directUrl = $nextDirectUrl;
                        break;
                    }
                }
            } catch (\Exception $e) {
                $this->debug(
                    get_class($e) . ' during processing of '
                    . get_class($handler['handler']) . ': ' . $e->getMessage()
                );
            }
        }
        return $directUrl ?? $dynamicUrl ?? false;
    }
}


@@ -29,7 +29,7 @@ return [
     |
     */

-    'lifetime' => 120,
+    'lifetime' => env('SESSION_LIFETIME', 120),

     'expire_on_close' => false,

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Default Session Driver
    |--------------------------------------------------------------------------
    |
    | This option controls the default session "driver" that will be used on
    | requests. By default, we will use the lightweight native driver but
    | you may specify any of the other wonderful drivers provided here.
    |
    | Supported: "file", "cookie", "database", "apc",
    |            "memcached", "redis", "array"
    |
    */

    'driver' => 'file',

    /*
    |--------------------------------------------------------------------------
    | Session Lifetime
    |--------------------------------------------------------------------------
    |
    | Here you may specify the number of minutes that you wish the session
    | to be allowed to remain idle for it is expired. If you want them
    | to immediately expire when the browser closes, set it to zero.
    |
    */

    'lifetime' => 120,

    'expire_on_close' => false,

    /*
    |--------------------------------------------------------------------------
    | Session Encryption
    |--------------------------------------------------------------------------
    |
    | This option allows you to easily specify that all of your session data
    | should be encrypted before it is stored. All encryption will be run
    | automatically by Laravel and you can use the Session like normal.
    |
    */

    'encrypt' => false,

    /*
    |--------------------------------------------------------------------------
    | Session File Location
    |--------------------------------------------------------------------------
    |
    | When using the native session driver, we need a location where session
    | files may be stored. A default has been set for you but a different
    | location may be specified. This is only needed for file sessions.
    |
    */

    'files' => storage_path('framework/sessions'),

    /*
    |--------------------------------------------------------------------------
    | Session Database Connection
    |--------------------------------------------------------------------------
    |
    | When using the "database" session driver, you may specify the database
    | connection that should be used to manage your sessions. This should
    | correspond to a connection in your "database" configuration file.
    |
    */

    'connection' => null,

    /*
    |--------------------------------------------------------------------------
    | Session Database Table
    |--------------------------------------------------------------------------
    |
    | When using the "database" session driver, you may specify the table we
    | should use to manage the sessions. Of course, a sensible default is
    | provided for you; however, you are free to change this as needed.
    |
    */

    'table' => 'sessions',

    /*
    |--------------------------------------------------------------------------
    | Session Sweeping Lottery
    |--------------------------------------------------------------------------
    |
    | Some session drivers must manually sweep their storage location to get
    | rid of old sessions from storage. Here are the chances that it will
    | happen on a given request. By default, the odds are 2 out of 100.
    |
    */

    'lottery' => [2, 100],

    /*
    |--------------------------------------------------------------------------
    | Session Cookie Name
    |--------------------------------------------------------------------------
    |
    | Here you may change the name of the cookie used to identify a session
    | instance by ID. The name specified here will get used every time a
    | new session cookie is created by the framework for every driver.
    |
    */

    'cookie' => 'october_session',

    /*
    |--------------------------------------------------------------------------
    | Session Cookie Path
    |--------------------------------------------------------------------------
    |
    | The session cookie path determines the path for which the cookie will
    | be regarded as available. Typically, this will be the root path of
    | your application but you are free to change this when necessary.
    |
    */

    'path' => '/',

    /*
    |--------------------------------------------------------------------------
    | Session Cookie Domain
    |--------------------------------------------------------------------------
    |
    | Here you may change the domain of the cookie used to identify a session
    | in your application. This will determine which domains the cookie is
    | available to in your application. A sensible default has been set.
    |
    */

    'domain' => null,

    /*
    |--------------------------------------------------------------------------
    | HTTPS Only Cookies
    |--------------------------------------------------------------------------
    |
    | By setting this option to true, session cookies will only be sent back
    | to the server if the browser has a HTTPS connection. This will keep
    | the cookie from being sent to you if it can not be done securely.
    |
    */

    'secure' => false,

];


@@ -4,8 +4,52 @@ declare(strict_types=1);

 namespace Shopsys\FrameworkBundle\Model\Product\Search;

+use Shopsys\FrameworkBundle\Component\Domain\Domain;
+use Shopsys\FrameworkBundle\Component\Elasticsearch\IndexDefinitionLoader;
+use Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser;
+use Shopsys\FrameworkBundle\Model\Product\Elasticsearch\ProductIndex;
+use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData;
+
 class FilterQueryFactory
 {
+    /**
+     * @var \Shopsys\FrameworkBundle\Model\Product\Search\ProductFilterDataToQueryTransformer
+     */
+    protected $productFilterDataToQueryTransformer;
+
+    /**
+     * @var \Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser
+     */
+    protected $currentCustomerUser;
+
+    /**
+     * @var \Shopsys\FrameworkBundle\Component\Elasticsearch\IndexDefinitionLoader
+     */
+    protected $indexDefinitionLoader;
+
+    /**
+     * @var \Shopsys\FrameworkBundle\Component\Domain\Domain
+     */
+    protected $domain;
+
+    /**
+     * @param \Shopsys\FrameworkBundle\Model\Product\Search\ProductFilterDataToQueryTransformer $productFilterDataToQueryTransformer
+     * @param \Shopsys\FrameworkBundle\Model\Customer\User\CurrentCustomerUser $currentCustomerUser
+     * @param \Shopsys\FrameworkBundle\Component\Elasticsearch\IndexDefinitionLoader $indexDefinitionLoader
+     * @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain
+     */
+    public function __construct(
+        ProductFilterDataToQueryTransformer $productFilterDataToQueryTransformer,
+        CurrentCustomerUser $currentCustomerUser,
+        IndexDefinitionLoader $indexDefinitionLoader,
+        Domain $domain
+    ) {
+        $this->productFilterDataToQueryTransformer = $productFilterDataToQueryTransformer;
+        $this->currentCustomerUser = $currentCustomerUser;
+        $this->indexDefinitionLoader = $indexDefinitionLoader;
+        $this->domain = $domain;
+    }
+
     /**
      * @param string $indexName
      * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery

<?php

declare(strict_types=1);

namespace Shopsys\FrameworkBundle\Model\Product\Search;

class FilterQueryFactory
{
    /**
     * @param string $indexName
     * @return \Shopsys\FrameworkBundle\Model\Product\Search\FilterQuery
     */
    public function create(string $indexName): FilterQuery
    {
        return new FilterQuery($indexName);
    }
}