1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544:
<?php
namespace Apptus\ESales\Connector;
use Apptus\ESales\Connector\Time\TimeInterval;
use Apptus\Util\XML\XmlListDecoder;
/**
* Abstract super class for {@see CloudConnector} and {@see OnPremConnector}.
*
* Imports can be carried out using the import* methods such as {@see importConfiguration()}. <br>
* Exports can in a similar manner be done with the export* methods such as {@see exportConfiguration()}.
*
* The XML data formats are documented in the section <i>Importing data</i> on <a href="http://zone.apptus.com">Apptus Zone</a> (http://zone.apptus.com).<br>
* The format for configuration and panels isn't documented publicly. They are generated by eSales Manager and these exports and
* imports should be used for backup and migration only. Manually editing these files isn't supported and should not be done.
*
* The procedure for imports can be found in the <i>eSales tutorial</i> on <a href="http://zone.apptus.com">Apptus Zone</a> (http://zone.apptus.com).
*
* Use the {@see session()} method to get {@see Session} instances. These are used to notify eSales and query panels.
* View the {@see Session} documentation for details.
*
* @see CloudConnector
* @see OnPremConnector
*/
abstract class Connector {
/** @internal */
protected $secureCluster = null;
/** @internal */
public $cluster = null;
/**
* @internal
* @param Cluster Cluster used for secure requests (imports, defrag, etc.).
* @param Cluster Cluster used for normal requests (queries, notifications, etc.).
*/
protected function __construct($secureCluster, $cluster) {
if (!$secureCluster instanceof Cluster) {
$type = gettype($secureCluster) === 'object' ? get_class($secureCluster) : gettype($secureCluster);
throw new \InvalidArgumentException('Invalid cluster type. Expected Cluster, got ' . $type);
}
if (!$cluster instanceof Cluster) {
$type = gettype($cluster) === 'object' ? get_class($cluster) : gettype($cluster);
throw new \InvalidArgumentException('Invalid cluster type. Expected Cluster, got ' . $type);
}
$this->secureCluster = $secureCluster;
$this->cluster = $cluster;
}
/**
* Gets a session for the specified session key.
*
* Calling this method does not affect the session in the eSales server.
* A notification or panel query on the object created by this method,
* is necessary for the properties to stick to the session.
*
* When calling this method with null in place of customerKey and/or market,
* the server will keep the current values of these properties for this session.
*
* @param string
* The session key to identify the session.
* @param string
* The customer key for the session.
* @param string
* The market for the session.
* @throws \InvalidArgumentException
* @return Session
* A new Session.
*/
public final function session($sessionKey, $customerKey = null, $market = null) {
if ($sessionKey === null || $sessionKey === '') {
throw new \InvalidArgumentException('Invalid session key: ' . $sessionKey);
}
if (!is_string($sessionKey)) {
$type = gettype($sessionKey) === 'object' ? get_class($sessionKey) : gettype($sessionKey);
throw new \InvalidArgumentException('Invalid session key type. Expected string, got ' . $type);
}
return new Session($sessionKey, $this->secureCluster, $this->cluster, $customerKey, $market);
}
/**
* Return a status report from the eSales cluster as a string with an XML document.
*
* @throws RequestFailedException if the request to the cluster fails.
* @return string
* The status report in XML.
*/
public function status() {
return $this->secureCluster->query('/esales/cluster/status', array(), null, LoadBalancer::PATIENCE_LONG);
}
/**
* Start a GDPR job on the cluster to remove all data related to the given customer key.
*
* Data will be removed both from memory and disk.
* "Linked" customer keys will also be removed (i.e. customer keys that have been notified in the same session).
*
* @param string $customerKey The customer key to remove.
* @return EventDataJobResult An object containing the job ID of the created job.
* @throws IOException
*/
public function createRemoveCustomerDataJob($customerKey) {
try {
$response = $this->cluster->postAndGet('/event-data-job/v1/create-remove-customer-job?customer_key=' . $customerKey, array(), null, null);
return EventDataJobResult::parse($response);
} catch (MalformedJsonException $e) {
throw new IOException('Failed to parse response. Message:' . $e->getMessage());
}
}
/**
* Start a GDPR job on the cluster to export all data related to the given customer key.
*
* "Linked" customer keys will also be exported (i.e. customer keys that have been notified in the same session).
* Status of the job can be checked with {@see checkCustomerDataJobStatus()} and when the status is
* {@see Status::DONE}, the resulting zip file can be downloaded using
* {@see downloadExportCustomerDataJobResult()}.
*
* @param string $customerKey The customer key to export.
* @return EventDataJobResult An object containing the job ID of the created job.
* @throws IOException
*/
public function createExportCustomerDataJob($customerKey) {
try {
$response = $this->secureCluster->postAndGet('/event-data-job/v1/create-export-customer-job?customer_key=' . $customerKey, array(), null, null);
return EventDataJobResult::parse($response);
} catch (MalformedJsonException $e) {
throw new IOException("Failed to parse response. Message: " . $e->getMessage());
}
}
/**
* Check the status of a GDPR customer data job.
*
* @param string $jid The job ID.
* @return EventDataJobResult An object containing the status of the job.
* @throws IOException
* @throws ClusterUnavailableException
*/
public function checkCustomerDataJobStatus($jid) {
try {
$response = $this->secureCluster->query('/event-data-job/v1/job-status?jid=' . $jid, new ArgMap());
return EventDataJobResult::parse($response);
} catch (MalformedJsonException $e) {
throw new IOException("Failed to parse response. Message: " . $e->getMessage());
}
}
/**
* Download the result of a GDPR export customer data job.
*
* @param string $jid The job ID.
* @return An InputStream with the resulting zip file.
* @throws ClusterUnavailableException
*/
public function downloadExportCustomerDataJobResult($jid) {
return $this->secureCluster->query('/event-data-job/v1/export-customer-job-result?jid=' . $jid, new ArgMap());
}
/**
* Download the result of a GDPR export customer data job.
*
* @param string $jid The job ID.
* @param destination Where to save the resulting zip file.
* @return int The number of bytes downloaded.
* @throws IOException
* @throws ClusterUnavailableException
*/
public function downloadExportCustomerDataJobResultToDestination($jid, $destination) {
return $this->_save($this->downloadExportCustomerDataJobResult($jid), $destination);
}
/**
* @internal
*/
protected final function _importHelper($importData, $name, $type) {
$args = new ArgMap();
if ($name !== null) {
$args->put('import_name', $name);
}
$args->put('type', $type);
$compressionMode = $this->secureCluster->compressionMode();
if ($importData->getExtension() === 'gz' || $importData->getExtension() === 'gzip') {
$compressionMode = CompressionMode::PRE_COMPRESSED_GZIP;
}
$response = $this->secureCluster->postAndGet('/esales/cluster/import', $args, null, $importData->getContent(), $compressionMode);
self::readLongQueryResponse($response);
}
/**
* @internal
*/
public static function readLongQueryResponse($response) {
$failed = false;
$busy = false;
$lines = preg_split('/(\r?\n)|(\n?\r)/', $response);
for ($i = 0; $i < count($lines); $i++) {
$line = trim($lines[$i]);
if ($failed) {
if ($busy) {
throw new BusyClusterException($line);
}
throw new RequestFailedException($line);
}
if ($line === 'success') {
return;
} elseif ($line === 'fail') {
if ($lines[$i + 1] === 'busy') {
$busy = true;
$i++;
}
$failed = true;
} elseif ($line === 'working') {
continue;
} else {
throw new RequestFailedException('Unexpected response');
}
}
}
/**
* Imports products from the specified file or string to the eSales cluster.
*
* @param resource|string
* An opened file or a string with the XML.
* @param string
* A unique identifier for the request. If null, a generated id will be used.
* @throws IOException if the file could not be read.
* @throws BusyClusterException if the cluster is busy with another task
* @throws RequestFailedException if the request to the cluster fails.
*/
public final function importProducts($importFile, $name = null) {
$this->_importHelper($this->_getInputResource($importFile), $name, 'products');
}
/**
* Imports panels from the specified file or string to the eSales cluster.
*
* @param resource|string
* An opened file or a string with the XML.
* @param string
* A unique identifier for the request. If null, a generated id will be used.
* @throws IOException if the file could not be read.
* @throws BusyClusterException if the cluster is busy with another task
* @throws RequestFailedException if the request to the cluster fails.
*/
public final function importPanels($importFile, $name = null) {
$this->_importHelper($this->_getInputResource($importFile), $name, 'panels');
}
/**
* Imports configuration from the specified file or string to the eSales cluster.
*
* @param resource|string
* An opened file or a string with the XML.
* @param string
* A unique identifier for the request. If null, a generated id will be used.
* @throws IOException if the file could not be read.
* @throws BusyClusterException if the cluster is busy with another task
* @throws RequestFailedException if the request to the cluster fails.
*/
public final function importConfiguration($importFile, $name = null) {
$this->_importHelper($this->_getInputResource($importFile), $name, 'configuration');
}
/**
* Imports synonyms from the specified file to the eSales cluster.
*
* @param resource|string
* An opened file or a string with the XML.
* @param string
* A unique identifier for the request. If null, a generated id will be used.
* @throws IOException if the file could not be read.
* @throws BusyClusterException if the cluster is busy with another task
* @throws RequestFailedException if the request to the cluster fails.
*/
public final function importSynonyms($importFile, $name = null) {
$this->_importHelper($this->_getInputResource($importFile), $name, 'synonyms');
}
/**
* Imports ads from the specified file to the eSales cluster.
*
* @param resource|string
* An opened file or a string with the XML.
* @param string
* A unique identifier for the request. If null, a generated id will be used.
* @throws IOException if the file could not be read.
* @throws BusyClusterException if the cluster is busy with another task
* @throws RequestFailedException if the request to the cluster fails.
*/
public final function importAds($importFile, $name = null) {
$this->_importHelper($this->_getInputResource($importFile), $name, 'ads');
}
/**
* @internal
* @param string|resource
* @throws IOException
* @throws \InvalidArgumentException
* @return string
*/
protected final function _getInputResource($file) {
if (is_string($file)) {
return new ImportData($file, null);
} elseif (is_resource($file)) {
$r = '';
while (!feof($file)) {
$buffer = fread($file, 4096);
if ($buffer === false) {
throw new IOException('Failed to read from resource');
}
$r .= $buffer;
}
$resourceUri = stream_get_meta_data($file)['uri'];
return new ImportData($r, pathinfo($resourceUri, PATHINFO_EXTENSION));
} else {
$type = gettype($file) === 'object' ? get_class($file) : gettype($file);
throw new \InvalidArgumentException('String or resource handle expected, got: ' . $type);
}
}
/**
* @internal
*/
protected final function _exportHelper($type, $dest) {
$xml = $this->secureCluster->getExport($type);
if ($dest === null) {
return $xml;
}
return $this->_save($xml, $dest);
}
/**
* Exports everything from the eSales cluster as an update file.
*
* @param resource|string|null
* An opened destination file or a string with a filename.
* @throws RequestFailedException if the request to the cluster fails.
* @throws IOException if the file could not be written to.
* @return int|string The number of bytes written or the XML string itself if no file was given.
*/
public final function exportProducts($dest = null) {
return $this->_exportHelper('products', $dest);
}
/**
* Exports panels from the eSales cluster to a definition file.
*
* @param resource|string|null
* An opened destination file or a string with a filename.
* @throws RequestFailedException if the request to the cluster fails.
* @throws IOException if the file could not be written to.
* @return int|string The number of bytes written or the XML string itself if no file was given.
*/
public final function exportPanels($dest = null) {
return $this->_exportHelper('panels', $dest);
}
/**
* Exports configuration from the eSales cluster to an update file.
*
* @param resource|string|null
* An opened destination file or a string with a filename.
* @throws RequestFailedException if the request to the cluster fails.
* @throws IOException if the file could not be written to.
* @return int|string The number of bytes written or the XML string itself if no file was given.
*/
public final function exportConfiguration($dest = null) {
return $this->_exportHelper('configuration', $dest);
}
/**
* Exports synonyms from the eSales cluster as an update file.
*
* @param resource|string|null
* An opened destination file or a string with a filename.
* @throws RequestFailedException if the request to the cluster fails.
* @throws IOException if the file could not be written to.
* @return int|string The number of bytes written or the XML string itself if no file was given.
*/
public final function exportSynonyms($dest = null) {
return $this->_exportHelper('synonyms', $dest);
}
/**
* Exports ads from the eSales cluster as an update file.
*
* @param resource|string|null
* An opened destination file or a string with a filename.
* @throws RequestFailedException if the request to the cluster fails.
* @throws IOException if the file could not be written to.
* @return int|string The number of bytes written or the XML string itself if no file was given.
*/
public final function exportAds($dest = null) {
return $this->_exportHelper('ads', $dest);
}
/**
* Saves the content from a string to a file.
*
* @internal
* @param string
* Input string.
* @param resource|string
* Destination file.
* @throws IOException
* @return int The size of the file in bytes.
*/
private function _save($input, $dest) {
$output = $this->_getOutputResource($dest);
$ok = fwrite($output, $input);
if ($ok === false) {
throw new IOException('Could not write to file.');
}
fflush($output);
//fclose($output); // Tests use temporary files that gets deleted when closed, so don't close here.
return $ok;
}
private function _getOutputResource($file) {
if (is_string($file)) {
if (is_file($file) && is_writable($file)) {
return fopen($file, 'w');
} else {
throw new IOException('Could not open file for writing: ' . $file);
}
} elseif (is_resource($file)) {
return $file;
} else {
throw new \InvalidArgumentException('Filename or opened file expected, got: ' . $file);
}
}
/**
* Returns a list of available markets from the eSales cluster. Never returns null.
*
* @throws RequestFailedException
* @return array An indexed array of markets.
*/
public function availableMarkets() {
$args = new ArgMap();
$xml = $this->secureCluster->query('/esales/markets', $args);
return XmlListDecoder::decode('markets', 'market', $xml);
}
/**
* Creates a reporter for a market during a specified time interval.
*
* The reporter can be used to fetch reports from the eSales cluster.
*
* @param string
* The market for the reports.
* @param \Apptus\ESales\Connector\Time\TimeInterval
* The time interval to fetch reports for.
* @return Reporter
* A Reporter object that can be used to fetch reports.
*/
public function reporter($market, TimeInterval $interval) {
return new Reporter($this->secureCluster, $market, $interval);
}
/**
* Creates a reporter for the unknown market (sessions without a market property), during a specified time interval.
*
* The reporter can be used to fetch reports from the eSales cluster.
*
* @param \Apptus\ESales\Connector\Time\TimeInterval
* The time interval for the reports.
* @return Reporter
* A Reporter object that can be used to fetch reports.
*/
public function reporterForUnknownMarket(TimeInterval $interval) {
return new Reporter($this->secureCluster, 'Unknown', $interval);
}
/**
* Return the last 100 notifications received by the eSales cluster.
*
* @param string
* Type of notification to fetch. If null all types of notifications will be included.
* @throws RequestFailedException if the request to the cluster fails.
* @return string
* A string containing the last 100 notifications
*/
public function latestNotifications($type = null) {
$args = null;
if ($type !== null) {
$args = new ArgMap();
$args->put('type', $type);
}
return $this->secureCluster->query('/esales/cluster/notifications', $args);
}
/**
* Gets a log from a server. Server index should be an index in the range [0, number_of_servers - 1].
* The servers are indexed in the same order as they appear in the cluster string. The server
* indexes can also be retrieved from the status command.
* Name is the name of a log. Available log names can be retrieved from {@see serverLogNames()}.
*
* @param int $index An index in the range [0, number of servers - 1]
* @param string $name The name of the log to be retrieved.
* @return string The log as a string
* @throws RequestFailedException if the request to the cluster fails.
*/
public function serverLog($index, $name) {
$args = new ArgMap();
$args->put('index', $index);
$args->put('name', $name);
return $this->secureCluster->query('/esales/cluster/log', $args, null, LoadBalancer::PATIENCE_VERY_LONG);
}
/**
* Gets a list of available log names.
*
* @return array A list of available log names.
* @throws RequestFailedException if the request to the cluster fails.
*/
public function serverLogNames() {
$xml = $this->secureCluster->query('/esales/log_names');
return XmlListDecoder::decode("log_names", "log_name", $xml);
}
/**
* Gets connector version.
*
* @return string Connector version.
*/
public static function getVersion() {
// This file will be filtered on build and the tags below replaced with the correct numbers.
return HttpRequestHelper::getConnectorVersion();
}
}