/** * @inheritdoc */ protected function _before() { $this->account = new NostoAccount('platform-00000000'); // The first 16 chars of the SSO token are used as the encryption key. $token = new NostoApiToken('sso', '01098d0fc84ded7c4226820d5d1207c69243cbb3637dc4bc2a216dafcf09d783'); $this->account->addApiToken($token); }
/** * Tests the "ssoLogin" method for the NostoAccount class. */ public function testAccountSingleSignOn() { $account = new NostoAccount('platform-test'); $meta = new NostoAccountMetaDataIframe(); $this->specify('account sso without api token', function () use($account, $meta) { $this->assertFalse($account->ssoLogin($meta)); }); }
/** * Test the account deletion with the required SSO token. */ public function testDeletingAccountWithToken() { $account = new NostoAccount('platform-test'); $token = new NostoApiToken('sso', '123'); $account->addApiToken($token); $this->specify('account is deleted', function () use($account) { $account->delete(); }); }
/** * Builds the API request and returns it. * * @param NostoAccount $account the Nosto account object. * @param string|null $customerId the Nosto customer ID of the user who placed the order. * @return NostoApiRequest the request object. */ protected function initApiRequest(NostoAccount $account, $customerId) { $request = new NostoApiRequest(); $request->setContentType('application/json'); if (!empty($customerId)) { $request->setPath(NostoApiRequest::PATH_ORDER_TAGGING); $request->setReplaceParams(array('{m}' => $account->getName(), '{cid}' => $customerId)); } else { $request->setPath(NostoApiRequest::PATH_UNMATCHED_ORDER_TAGGING); $request->setReplaceParams(array('{m}' => $account->getName())); } return $request; }
/** * Tests that existing accounts can be synced from Nosto. * Accounts are synced using OAuth2 Authorization Code method. * We are only testing that we can start and act on the steps in the OAuth request cycle. */ public function testSyncingExistingAccount() { $meta = new NostoOAuthClientMetaData(); $client = new NostoOAuthClient($meta); $this->specify('oauth authorize url can be created', function () use($client) { $this->assertEquals('http://localhost:3000?client_id=client-id&redirect_uri=http%3A%2F%2Fmy.shop.com%2Fnosto%2Foauth&response_type=code&scope=sso products&lang=en', $client->getAuthorizationUrl()); }); $account = NostoAccount::syncFromNosto($meta, 'test123'); $this->specify('account was created', function () use($account, $meta) { $this->assertInstanceOf('NostoAccount', $account); $this->assertEquals('platform-00000000', $account->getName()); }); $this->specify('account has api token sso', function () use($account, $meta) { $token = $account->getApiToken('sso'); $this->assertInstanceOf('NostoApiToken', $token); $this->assertEquals('sso', $token->getName()); $this->assertNotEmpty($token->getValue()); }); $this->specify('account has api token products', function () use($account, $meta) { $token = $account->getApiToken('products'); $this->assertInstanceOf('NostoApiToken', $token); $this->assertEquals('products', $token->getName()); $this->assertNotEmpty($token->getValue()); }); $this->specify('account is connected to nosto', function () use($account, $meta) { $this->assertTrue($account->isConnectedToNosto()); }); }
/** * Tests that new accounts can be created successfully. */ public function testCreatingNewAccount() { /** @var NostoAccount $account */ /** @var NostoAccountMetaData $meta */ $meta = new NostoAccountMetaData(); $account = NostoAccount::create($meta); $this->specify('account was created', function () use($account, $meta) { $this->assertInstanceOf('NostoAccount', $account); $this->assertEquals($meta->getPlatform() . '-' . $meta->getName(), $account->getName()); }); $this->specify('account has api token sso', function () use($account, $meta) { $token = $account->getApiToken('sso'); $this->assertInstanceOf('NostoApiToken', $token); $this->assertEquals('sso', $token->getName()); $this->assertNotEmpty($token->getValue()); }); $this->specify('account has api token products', function () use($account, $meta) { $token = $account->getApiToken('products'); $this->assertInstanceOf('NostoApiToken', $token); $this->assertEquals('products', $token->getName()); $this->assertNotEmpty($token->getValue()); }); $this->specify('account is connected to nosto', function () use($account, $meta) { $this->assertTrue($account->isConnectedToNosto()); }); }
/** * Returns the url for the account administration iframe. * If the passed account is null, then the url will point to the start page where a new account can be created. * * @param NostoAccountMetaDataIframeInterface $meta the iframe meta data. * @param NostoAccount|null $account the account to return the url for. * @param array $params additional parameters to add to the iframe url. * @return string the iframe url. * @throws NostoException if the url cannot be created. */ public function getUrl(NostoAccountMetaDataIframeInterface $meta, NostoAccount $account = null, array $params = array()) { $queryParams = http_build_query(array_merge(array('lang' => strtolower($meta->getLanguageIsoCode()), 'ps_version' => $meta->getVersionPlatform(), 'nt_version' => $meta->getVersionModule(), 'product_pu' => $meta->getPreviewUrlProduct(), 'category_pu' => $meta->getPreviewUrlCategory(), 'search_pu' => $meta->getPreviewUrlSearch(), 'cart_pu' => $meta->getPreviewUrlCart(), 'front_pu' => $meta->getPreviewUrlFront(), 'shop_lang' => strtolower($meta->getLanguageIsoCodeShop()), 'shop_name' => $meta->getShopName(), 'unique_id' => $meta->getUniqueId(), 'fname' => $meta->getFirstName(), 'lname' => $meta->getLastName(), 'email' => $meta->getEmail()), $params)); if ($account !== null && $account->isConnectedToNosto()) { try { $url = $account->ssoLogin($meta) . '?' . $queryParams; } catch (NostoException $e) { // If the SSO fails, we show a "remove account" page to the user in order to // allow to remove Nosto and start over. // The only case when this should happen is when the api token for some // reason is invalid, which is the case when switching between environments. $url = NostoHttpRequest::buildUri($this->getBaseUrl() . self::IFRAME_URI_UNINSTALL . '?' . $queryParams, array('{platform}' => $meta->getPlatform())); } } else { $url = NostoHttpRequest::buildUri($this->getBaseUrl() . self::IFRAME_URI_INSTALL . '?' . $queryParams, array('{platform}' => $meta->getPlatform())); } return $url; }
/** * @param Store $store * @return \NostoAccount */ public function build(Store $store) { $metaData = new \NostoAccount(); try { $metaData->setTitle(implode(' - ', [$store->getWebsite()->getName(), $store->getGroup()->getName(), $store->getName()])); $metaData->setName(substr(sha1(rand()), 0, 8)); $metaData->setFrontPageUrl(\NostoHttpRequest::replaceQueryParamInUrl('___store', $store->getCode(), $store->getBaseUrl(UrlInterface::URL_TYPE_WEB))); $metaData->setCurrency(new \NostoCurrencyCode($store->getBaseCurrencyCode())); $lang = substr($store->getConfig('general/locale/code'), 0, 2); $metaData->setLanguage(new \NostoLanguageCode($lang)); $lang = substr($this->_localeResolver->getLocale(), 0, 2); $metaData->setOwnerLanguage(new \NostoLanguageCode($lang)); $owner = $this->_accountOwnerMetaBuilder->build(); $metaData->setOwner($owner); $billing = $this->_accountBillingMetaBuilder->build($store); $metaData->setBilling($billing); } catch (\NostoException $e) { $this->_logger->error($e, ['exception' => $e]); } return $metaData; }
/** * Encrypts and returns the data. * * @param NostoAccount $account the account to export the data for. * @param NostoExportCollectionInterface $collection the data collection to export. * @return string the encrypted data. */ public static function export(NostoAccount $account, NostoExportCollectionInterface $collection) { $data = ''; // Use the first 16 chars of the SSO token as secret for encryption. $token = $account->getApiToken(NostoApiToken::API_SSO); if (!empty($token)) { $tokenValue = $token->getValue(); $secret = substr($tokenValue, 0, 16); if (!empty($secret)) { $iv = phpseclib_Crypt_Random::string(16); $cipher = new NostoCipher(); $cipher->setSecret($secret); $cipher->setIV($iv); $cipherText = $cipher->encrypt($collection->getJson()); // Prepend the IV to the cipher string so that nosto can parse and use it. // There is no security concern with sending the IV as plain text. $data = $iv . $cipherText; } } return $data; }
/** * Tests that we can build an authenticated url for the config iframe. */ public function testIframeUrlAuthentication() { /** @var NostoAccount $account */ $account = new NostoAccount('platform-00000000'); $meta = new NostoAccountMetaDataIframe(); $baseUrl = isset($_ENV['NOSTO_WEB_HOOK_BASE_URL']) ? $_ENV['NOSTO_WEB_HOOK_BASE_URL'] : NostoHttpRequest::$baseUrl; $url = $account->getIframeUrl($meta); $this->specify('install iframe url was created', function () use($url, $baseUrl) { $validUrl = $baseUrl . '/hub/platform/install?lang=en&ps_version=1.0.0&nt_version=1.0.0&product_pu=http%3A%2F%2Fmy.shop.com%2Fproducts%2Fproduct123%3Fnostodebug%3Dtrue&category_pu=http%3A%2F%2Fmy.shop.com%2Fproducts%2Fcategory123%3Fnostodebug%3Dtrue&search_pu=http%3A%2F%2Fmy.shop.com%2Fsearch%3Fquery%3Dred%3Fnostodebug%3Dtrue&cart_pu=http%3A%2F%2Fmy.shop.com%2Fcart%3Fnostodebug%3Dtrue&front_pu=http%3A%2F%2Fmy.shop.com%3Fnostodebug%3Dtrue&shop_lang=en&shop_name=Shop+Name&unique_id=123&fname=James&lname=Kirk&email=james.kirk%40example.com'; $this->assertEquals($validUrl, $url); }); $tokenSso = new NostoApiToken(NostoApiToken::API_SSO, '01098d0fc84ded7c4226820d5d1207c69243cbb3637dc4bc2a216dafcf09d783'); $tokenProducts = new NostoApiToken(NostoApiToken::API_PRODUCTS, 'X1098d0fc84ded7c4226820d5d1207c69243cbb3637dc4bc2a216dafcf09d783'); $account->addApiToken($tokenSso); $account->addApiToken($tokenProducts); $url = $account->getIframeUrl($meta); $this->specify('auth iframe url was created', function () use($url, $baseUrl) { $validUrl = 'https://nosto.com/auth/sso/sso%2Bplatform-00000000@nostosolutions.com/xAd1RXcmTMuLINVYaIZJJg?lang=en&ps_version=1.0.0&nt_version=1.0.0&product_pu=http%3A%2F%2Fmy.shop.com%2Fproducts%2Fproduct123%3Fnostodebug%3Dtrue&category_pu=http%3A%2F%2Fmy.shop.com%2Fproducts%2Fcategory123%3Fnostodebug%3Dtrue&search_pu=http%3A%2F%2Fmy.shop.com%2Fsearch%3Fquery%3Dred%3Fnostodebug%3Dtrue&cart_pu=http%3A%2F%2Fmy.shop.com%2Fcart%3Fnostodebug%3Dtrue&front_pu=http%3A%2F%2Fmy.shop.com%3Fnostodebug%3Dtrue&shop_lang=en&shop_name=Shop+Name&unique_id=123&fname=James&lname=Kirk&email=james.kirk%40example.com'; $this->assertEquals($validUrl, $url); }); }
/** * Tests that batch product re-crawl API requests can be made. */ public function testSendingBatchProductReCrawl() { $account = new NostoAccount('platform-00000000'); $product = new NostoProduct(); $collection = new NostoExportProductCollection(); $collection[] = $product; $token = new NostoApiToken('products', '01098d0fc84ded7c4226820d5d1207c69243cbb3637dc4bc2a216dafcf09d783'); $account->addApiToken($token); $result = NostoProductReCrawl::sendBatch($collection, $account); $this->specify('successful batch product re-crawl', function () use($result) { $this->assertTrue($result); }); }
/** * Handles the redirect from Nosto oauth2 authorization server when an * existing account is connected to a store. * This is handled in the front end as the oauth2 server validates the * "return_url" sent in the first step of the authorization cycle, and * requires it to be from the same domain that the account is configured * for and only redirects to that domain. */ public function indexAction() { // If the "Add Store Code to Urls" setting is set to "No" under // System -> Configuration -> Web -> Url Options, then Magento won't // set the store context based on the "___store" GET parameter if the // store does NOT belong to the default website. When this setting is // "Yes", then the store code will be a part of the url path and then // the correct context is set by Magento, regardless of the website the // store belongs to. // If the "___store" parameter is present in the url in the current // store context is not that store, then switch the store context. if (($storeCode = $this->getRequest()->getParam('___store')) !== null) { $store = Mage::app()->getStore($storeCode); if ($store && $store->getId() !== Mage::app()->getStore()->getId()) { Mage::app()->setCurrentStore($store->getCode()); } } $request = $this->getRequest(); $store = Mage::app()->getStore(); if (($code = $request->getParam('code')) !== null) { try { /** @var Nosto_Tagging_Helper_Oauth $oauthHelper */ $oauthHelper = Mage::helper('nosto_tagging/oauth'); $account = NostoAccount::syncFromNosto($oauthHelper->getMetaData($store), $code); /** @var Nosto_Tagging_Helper_Account $accountHelper */ $accountHelper = Mage::helper('nosto_tagging/account'); if ($accountHelper->save($account, $store)) { $params = array('message_type' => NostoMessage::TYPE_SUCCESS, 'message_code' => NostoMessage::CODE_ACCOUNT_CONNECT, 'store' => (int) $store->getId(), '_store' => Mage_Core_Model_App::ADMIN_STORE_ID); } else { throw new NostoException('Failed to connect account'); } } catch (NostoException $e) { Mage::log("\n" . $e->__toString(), Zend_Log::ERR, Nosto_Tagging_Model_Base::LOG_FILE_NAME); $params = array('message_type' => NostoMessage::TYPE_ERROR, 'message_code' => NostoMessage::CODE_ACCOUNT_CONNECT, 'store' => (int) $store->getId(), '_store' => Mage_Core_Model_App::ADMIN_STORE_ID); } $this->_redirect('adminhtml/nosto/redirectProxy', $params); } elseif (($error = $request->getParam('error')) !== null) { $logMsg = $error; if (($reason = $request->getParam('error_reason')) !== null) { $logMsg .= ' - ' . $reason; } if (($desc = $request->getParam('error_description')) !== null) { $logMsg .= ' - ' . $desc; } Mage::log("\n" . $logMsg, Zend_Log::ERR, Nosto_Tagging_Model_Base::LOG_FILE_NAME); $this->_redirect('adminhtml/nosto/redirectProxy', array('message_type' => NostoMessage::TYPE_ERROR, 'message_code' => NostoMessage::CODE_ACCOUNT_CONNECT, 'message_text' => $desc, 'store' => (int) $store->getId(), '_store' => Mage_Core_Model_App::ADMIN_STORE_ID)); } else { $this->norouteAction(); } }
/** * @inheritdoc */ public function initContent() { $id_lang = (int) Tools::getValue('language_id', $this->module->getContext()->language->id); if (($code = Tools::getValue('code')) !== false) { // The user accepted the authorization request. // The authorization server responded with a code that can be used to exchange for the access token. try { $meta = new NostoTaggingMetaOauth(); $meta->setModuleName($this->module->name); $meta->setModulePath($this->module->getPath()); $meta->loadData($this->module->getContext(), $id_lang); $account = NostoAccount::syncFromNosto($meta, $code); if (!Nosto::helper('nosto_tagging/account')->save($account, $id_lang)) { throw new NostoException('Failed to save account.'); } $msg = $this->module->l('Account %s successfully connected to Nosto.', 'oauth2'); $this->redirectToModuleAdmin(array('language_id' => $id_lang, 'oauth_success' => sprintf($msg, $account->getName()))); } catch (NostoException $e) { Nosto::helper('nosto_tagging/logger')->error(__CLASS__ . '::' . __FUNCTION__ . ' - ' . $e->getMessage(), $e->getCode()); $msg = $this->module->l('Account could not be connected to Nosto. Please contact Nosto support.', 'oauth2'); $this->redirectToModuleAdmin(array('language_id' => $id_lang, 'oauth_error' => $msg)); } } elseif (($error = Tools::getValue('error')) !== false) { $message_parts = array($error); if (($error_reason = Tools::getValue('error_reason')) !== false) { $message_parts[] = $error_reason; } if (($error_description = Tools::getValue('error_description')) !== false) { $message_parts[] = urldecode($error_description); } Nosto::helper('nosto_tagging/logger')->error(__CLASS__ . '::' . __FUNCTION__ . ' - ' . implode(' - ', $message_parts), 200); // Prefer to show the error description sent from Nosto to the user when something is wrong. // These messages are localized to users current back office language. if (!empty($error_description)) { $msg = urldecode($error_description); } elseif (!empty($error_reason) && $error_reason === 'user_denied') { $msg = $this->module->l('Account could not be connected to Nosto. You rejected the connection request.', 'oauth2'); } else { $msg = $this->module->l('Account could not be connected to Nosto. Please contact Nosto support.', 'oauth2'); } $this->redirectToModuleAdmin(array('language_id' => $id_lang, 'oauth_error' => $msg)); } $this->notFound(); }
/** * Finds and returns an account for given criteria. * * @param null|int $lang_id the ID of the language. * @param null|int $id_shop_group the ID of the shop context. * @param null|int $id_shop the ID of the shop. * @return NostoAccount|null the account with loaded API tokens, or null if not found. */ public function find($lang_id = null, $id_shop_group = null, $id_shop = null) { /** @var NostoTaggingHelperConfig $helper_config */ $helper_config = Nosto::helper('nosto_tagging/config'); $account_name = $helper_config->getAccountName($lang_id, $id_shop_group, $id_shop); if (!empty($account_name)) { $account = new NostoAccount($account_name); $tokens = array(); foreach (NostoApiToken::getApiTokenNames() as $token_name) { $token_value = $helper_config->getToken($token_name, $lang_id, $id_shop_group, $id_shop); if (!empty($token_value)) { $tokens[$token_name] = $token_value; } } if (!empty($tokens)) { foreach ($tokens as $name => $value) { $account->addApiToken(new NostoApiToken($name, $value)); } } return $account; } return null; }
/** * Sends a currency exchange rate update request to Nosto via the API. * * Checks if multi currency is enabled for the store before attempting to * send the exchange rates. * * @param NostoAccount $account the account for which tp update the rates. * @param Mage_Core_Model_Store $store the store which rates are to be updated. * * @return bool */ public function updateCurrencyExchangeRates(NostoAccount $account, Mage_Core_Model_Store $store) { /** @var Nosto_Tagging_Helper_Data $helper */ $helper = Mage::helper('nosto_tagging'); if (!$helper->isMultiCurrencyMethodExchangeRate($store)) { Mage::log(sprintf('Currency update called without exchange method enabled for account %s', $account->getName()), Zend_Log::DEBUG, Nosto_Tagging_Model_Base::LOG_FILE_NAME); return false; } $currencyCodes = $store->getAvailableCurrencyCodes(true); $baseCurrencyCode = $store->getBaseCurrencyCode(); /** @var Nosto_Tagging_Helper_Currency $helper */ $helper = Mage::helper('nosto_tagging/currency'); try { /** @var Nosto_Tagging_Model_Collection_Rates $collection */ $collection = $helper->getExchangeRateCollection($baseCurrencyCode, $currencyCodes); $service = new NostoOperationExchangeRate($account, $collection); return $service->update(); } catch (NostoException $e) { Mage::log("\n" . $e, Zend_Log::ERR, Nosto_Tagging_Model_Base::LOG_FILE_NAME); } return false; }
/** * Checks if this account is the same as the given account. * They are considered equal if their name property match. The tokens are not relevant in the comparison, * as they are not required by the account upon creation. * * @param NostoAccount $account the account to check. * @return bool true if equals. */ public function equals(NostoAccount $account) { return $account->getName() === $this->getName(); }
/** * Signs the user in to Nosto via SSO. * * Requires that the account has a valid sso token associated with it. * * @param NostoAccount $account the account to sign into. * @param NostoAccountMetaSingleSignOnInterface $meta the SSO meta-data. * @return string a secure login url. * * @throws NostoException on failure. */ public function sso(NostoAccount $account, NostoAccountMetaSingleSignOnInterface $meta) { $token = $account->getApiToken(NostoApiToken::API_SSO); if (is_null($token)) { throw new NostoException(sprintf('No `%s` API token found for account "%s".', NostoApiToken::API_SSO, $account->getName())); } $request = new NostoHttpRequest(); $request->setUrl(NostoHttpRequest::$baseUrl . NostoHttpRequest::PATH_SSO_AUTH); $request->setReplaceParams(array('{platform}' => $meta->getPlatform(), '{email}' => $meta->getEmail())); $request->setContentType('application/x-www-form-urlencoded'); $request->setAuthBasic('', $token->getValue()); $response = $request->post(http_build_query(array('fname' => $meta->getFirstName(), 'lname' => $meta->getLastName()))); if ($response->getCode() !== 200) { throw Nosto::createHttpException('Failed to sign into Nosto using Single Sign On.', $request, $response); } $result = $response->getJsonResult(); if (empty($result->login_url)) { throw new NostoException('No "login_url" returned when logging in employee to Nosto'); } return $result->login_url; }
/** * Tests that product delete API requests can be made. */ public function testSendingProductDelete() { $account = new NostoAccount('platform-00000000'); $product = new NostoProduct(); $token = new NostoApiToken('products', '01098d0fc84ded7c4226820d5d1207c69243cbb3637dc4bc2a216dafcf09d783'); $account->addApiToken($token); $op = new NostoOperationProduct($account); $op->addProduct($product); $result = $op->delete(); $this->specify('successful product delete', function () use($result) { $this->assertTrue($result); }); }
/** * Returns the account with associated api tokens for the store view scope. * * @param Mage_Core_Model_Store|null $store the account store view. * * @return NostoAccount|null the account or null if not found. */ public function find(Mage_Core_Model_Store $store = null) { if ($store === null) { $store = Mage::app()->getStore(); } $accountName = $store->getConfig(self::XML_PATH_ACCOUNT); if (!empty($accountName)) { $account = new NostoAccount($accountName); $tokens = json_decode($store->getConfig(self::XML_PATH_TOKENS), true); if (is_array($tokens) && !empty($tokens)) { foreach ($tokens as $name => $value) { $token = new NostoApiToken($name, $value); $account->addApiToken($token); } } return $account; } return null; }
/** * Creates a new Nosto account for the current scope using the Nosto API. */ public function createAccountAction() { $this->getResponse()->setHeader('Content-type', 'application/json', true); /** @var Nosto_Tagging_Helper_Account $accountHelper */ $accountHelper = Mage::helper('nosto_tagging/account'); $store = $this->getSelectedStore(); if ($this->getRequest()->isPost() && $store !== null) { try { $email = $this->getRequest()->getPost('email'); $details = $this->getRequest()->getPost('details'); $meta = $accountHelper->getMetaData($store); if (Zend_Validate::is($email, 'EmailAddress')) { $meta->getOwner()->setEmail($email); } if (!empty($details)) { $meta->setDetails(json_decode($details)); } $account = NostoAccount::create($meta); if ($accountHelper->save($account, $store)) { $accountHelper->updateCurrencyExchangeRates($account, $store); $responseBody = array('success' => true, 'redirect_url' => $accountHelper->getIframeUrl($store, $account, array('message_type' => NostoMessage::TYPE_SUCCESS, 'message_code' => NostoMessage::CODE_ACCOUNT_CREATE))); } } catch (NostoException $e) { Mage::log("\n" . $e->__toString(), Zend_Log::ERR, Nosto_Tagging_Model_Base::LOG_FILE_NAME); } } if (!isset($responseBody)) { $responseBody = array('success' => false, 'redirect_url' => $accountHelper->getIframeUrl($store, null, array('message_type' => NostoMessage::TYPE_ERROR, 'message_code' => NostoMessage::CODE_ACCOUNT_CREATE))); } $this->getResponse()->setBody(json_encode($responseBody)); }
/** * Saves the account and the associated api tokens for the store. * * @param \NostoAccount $account the account to save. * @param Store $store the store. * * @return bool true on success, false otherwise. */ public function saveAccount(\NostoAccount $account, Store $store) { if ((int) $store->getId() < 1) { return false; } $tokens = array(); foreach ($account->getTokens() as $token) { $tokens[$token->getName()] = $token->getValue(); } $this->_config->save(self::XML_PATH_ACCOUNT, $account->getName(), ScopeInterface::SCOPE_STORES, $store->getId()); $this->_config->save(self::XML_PATH_TOKENS, json_encode($tokens), ScopeInterface::SCOPE_STORES, $store->getId()); $store->resetConfig(); return true; }