diff --git a/.travis.yml b/.travis.yml index e447347..8f770fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,9 @@ language: php php: - 7.0 - 7.1 + - 7.2 + - 7.3 + - nightly sudo: false diff --git a/CHANGELOG.MD b/CHANGELOG.MD index e69de29..5517773 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -0,0 +1,10 @@ +## [1.1.0] + +- Added column and recording of snapshot request URIs +- Added functionality to view diff of raw snapshot data from backend app +- Added functionality to delete snapshot entries from backend app +- Added functional tests + +## [1.0.0] + +- Initial release \ No newline at end of file diff --git a/Components/FineDiff.php b/Components/FineDiff.php new file mode 100644 index 0000000..711f660 --- /dev/null +++ b/Components/FineDiff.php @@ -0,0 +1,579 @@ +granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity; + $this->edits = []; + $this->from_text = $from_text; + $this->doDiff($from_text, $to_text); + } + + public function getOps() + { + return $this->edits; + } + + public function getOpcodes() + { + $opcodes = []; + foreach ($this->edits as $edit) { + $opcodes[] = $edit->getOpcode(); + } + + return implode('', $opcodes); + } + + public function renderDiffToHTML() + { + $in_offset = 0; + ob_start(); + foreach ($this->edits as $edit) { + $n = $edit->getFromLen(); + if ($edit instanceof FineDiffCopyOp) { + FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n); + } else { + if ($edit instanceof FineDiffDeleteOp) { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + } else { + if ($edit instanceof FineDiffInsertOp) { + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } else /* if ( $edit instanceof FineDiffReplaceOp ) */ { + FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n); + FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen()); + } + } + } + $in_offset += $n; + } + + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Return an opcodes string describing the diff between a "From" and a + * "To" string + */ + public static function getDiffOpcodes($from, $to, $granularities = null) + { + $diff = new FineDiff($from, $to, $granularities); + + return $diff->getOpcodes(); + } + + /**------------------------------------------------------------------------ + * Return an iterable collection of diff ops from an opcodes string + */ + public static function getDiffOpsFromOpcodes($opcodes) + { + $diffops = new FineDiffOps(); + FineDiff::renderFromOpcodes(null, $opcodes, [$diffops, 'appendOpcode']); + + return $diffops->edits; + } + + /**------------------------------------------------------------------------ + * Re-create the "To" string from the "From" string and an "Opcodes" string + */ + public static function renderToTextFromOpcodes($from, $opcodes) + { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, ['FineDiff', 'renderToTextFromOpcode']); + + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Render the diff to an HTML string + */ + public static function renderDiffToHTMLFromOpcodes($from, $opcodes) + { + ob_start(); + FineDiff::renderFromOpcodes($from, $opcodes, ['FineDiff', 'renderDiffToHTMLFromOpcode']); + + return ob_get_clean(); + } + + /**------------------------------------------------------------------------ + * Generic opcodes parser, user must supply callback for handling + * single opcode + */ + public static function renderFromOpcodes($from, $opcodes, $callback) + { + if (!is_callable($callback)) { + return; + } + $opcodes_len = strlen($opcodes); + $from_offset = $opcodes_offset = 0; + while ($opcodes_offset < $opcodes_len) { + $opcode = substr($opcodes, $opcodes_offset, 1); + $opcodes_offset++; + $n = intval(substr($opcodes, $opcodes_offset)); + if ($n) { + $opcodes_offset += strlen(strval($n)); + } else { + $n = 1; + } + if ($opcode === 'c') { // copy n characters from source + call_user_func($callback, 'c', $from, $from_offset, $n, ''); + $from_offset += $n; + } else { + if ($opcode === 'd') { // delete n characters from source + call_user_func($callback, 'd', $from, $from_offset, $n, ''); + $from_offset += $n; + } else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes + call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n); + $opcodes_offset += 1 + $n; + } + } + } + } + + /**------------------------------------------------------------------------ + * + * Private section + * + */ + + /** + * Entry point to compute the diff. + */ + private function doDiff($from_text, $to_text) + { + $this->last_edit = false; + $this->stackpointer = 0; + $this->from_text = $from_text; + $this->from_offset = 0; + // can't diff without at least one granularity specifier + if (empty($this->granularityStack)) { + return; + } + $this->_processGranularity($from_text, $to_text); + } + + /** + * This is the recursive function which is responsible for + * handling/increasing granularity. + * + * Incrementally increasing the granularity is key to compute the + * overall diff in a very efficient way. + */ + private function _processGranularity($from_segment, $to_segment) + { + $delimiters = $this->granularityStack[$this->stackpointer++]; + $has_next_stage = $this->stackpointer < count($this->granularityStack); + foreach (FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit) { + // increase granularity + if ($fragment_edit instanceof FineDiffReplaceOp && $has_next_stage) { + $this->_processGranularity( + substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()), + $fragment_edit->getText() + ); + } // fuse copy ops whenever possible + else { + if ($fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp) { + $this->edits[count($this->edits) - 1]->increase($fragment_edit->getFromLen()); + $this->from_offset += $fragment_edit->getFromLen(); + } else { + /* $fragment_edit instanceof FineDiffCopyOp */ + /* $fragment_edit instanceof FineDiffDeleteOp */ + /* $fragment_edit instanceof FineDiffInsertOp */ + $this->edits[] = $this->last_edit = $fragment_edit; + $this->from_offset += $fragment_edit->getFromLen(); + } + } + } + $this->stackpointer--; + } + + /** + * This is the core algorithm which actually perform the diff itself, + * fragmenting the strings as per specified delimiters. + * + * This function is naturally recursive, however for performance purpose + * a local job queue is used instead of outright recursivity. + */ + private static function doFragmentDiff($from_text, $to_text, $delimiters) + { + // Empty delimiter means character-level diffing. + // In such case, use code path optimized for character-level + // diffing. + if (empty($delimiters)) { + return FineDiff::doCharDiff($from_text, $to_text); + } + + $result = []; + + // fragment-level diffing + $from_text_len = strlen($from_text); + $to_text_len = strlen($to_text); + $from_fragments = FineDiff::extractFragments($from_text, $delimiters); + $to_fragments = FineDiff::extractFragments($to_text, $delimiters); + + $jobs = [[0, $from_text_len, 0, $to_text_len]]; + + $cached_array_keys = []; + + while ($job = array_pop($jobs)) { + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + + // catch easy cases first + $from_segment_length = $from_segment_end - $from_segment_start; + $to_segment_length = $to_segment_end - $to_segment_start; + if (!$from_segment_length || !$to_segment_length) { + if ($from_segment_length) { + $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length); + } else { + if ($to_segment_length) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, + $to_segment_length)); + } + } + continue; + } + + // find longest copy operation for the current segments + $best_copy_length = 0; + + $from_base_fragment_index = $from_segment_start; + + $cached_array_keys_for_current_segment = []; + + while ($from_base_fragment_index < $from_segment_end) { + $from_base_fragment = $from_fragments[$from_base_fragment_index]; + $from_base_fragment_length = strlen($from_base_fragment); + // performance boost: cache array keys + if (!isset($cached_array_keys_for_current_segment[$from_base_fragment])) { + if (!isset($cached_array_keys[$from_base_fragment])) { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, + $from_base_fragment, true); + } else { + $to_all_fragment_indices = $cached_array_keys[$from_base_fragment]; + } + // get only indices which falls within current segment + if ($to_segment_start > 0 || $to_segment_end < $to_text_len) { + $to_fragment_indices = []; + foreach ($to_all_fragment_indices as $to_fragment_index) { + if ($to_fragment_index < $to_segment_start) { + continue; + } + if ($to_fragment_index >= $to_segment_end) { + break; + } + $to_fragment_indices[] = $to_fragment_index; + } + $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices; + } else { + $to_fragment_indices = $to_all_fragment_indices; + } + } else { + $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment]; + } + // iterate through collected indices + foreach ($to_fragment_indices as $to_base_fragment_index) { + $fragment_index_offset = $from_base_fragment_length; + // iterate until no more match + for (;;) { + $fragment_from_index = $from_base_fragment_index + $fragment_index_offset; + if ($fragment_from_index >= $from_segment_end) { + break; + } + $fragment_to_index = $to_base_fragment_index + $fragment_index_offset; + if ($fragment_to_index >= $to_segment_end) { + break; + } + if ($from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index]) { + break; + } + $fragment_length = strlen($from_fragments[$fragment_from_index]); + $fragment_index_offset += $fragment_length; + } + if ($fragment_index_offset > $best_copy_length) { + $best_copy_length = $fragment_index_offset; + $best_from_start = $from_base_fragment_index; + $best_to_start = $to_base_fragment_index; + } + } + $from_base_fragment_index += strlen($from_base_fragment); + // If match is larger than half segment size, no point trying to find better + // TODO: Really? + if ($best_copy_length >= $from_segment_length / 2) { + break; + } + // no point to keep looking if what is left is less than + // current best match + if ($from_base_fragment_index + $best_copy_length >= $from_segment_end) { + break; + } + } + + if ($best_copy_length) { + $jobs[] = [$from_segment_start, $best_from_start, $to_segment_start, $best_to_start]; + $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length); + $jobs[] = [ + $best_from_start + $best_copy_length, + $from_segment_end, + $best_to_start + $best_copy_length, + $to_segment_end, + ]; + } else { + $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_length, + substr($to_text, $to_segment_start, $to_segment_length)); + } + } + + ksort($result, SORT_NUMERIC); + + return array_values($result); + } + + /** + * Perform a character-level diff. + * + * The algorithm is quite similar to doFragmentDiff(), except that + * the code path is optimized for character-level diff -- strpos() is + * used to find out the longest common subequence of characters. + * + * We try to find a match using the longest possible subsequence, which + * is at most the length of the shortest of the two strings, then incrementally + * reduce the size until a match is found. + * + * I still need to study more the performance of this function. It + * appears that for long strings, the generic doFragmentDiff() is more + * performant. For word-sized strings, doCharDiff() is somewhat more + * performant. + */ + private static function doCharDiff($from_text, $to_text) + { + $result = []; + $jobs = [[0, strlen($from_text), 0, strlen($to_text)]]; + while ($job = array_pop($jobs)) { + // get the segments which must be diff'ed + list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job; + $from_segment_len = $from_segment_end - $from_segment_start; + $to_segment_len = $to_segment_end - $to_segment_start; + + // catch easy cases first + if (!$from_segment_len || !$to_segment_len) { + if ($from_segment_len) { + $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len); + } else { + if ($to_segment_len) { + $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, + $to_segment_len)); + } + } + continue; + } + if ($from_segment_len >= $to_segment_len) { + $copy_len = $to_segment_len; + while ($copy_len) { + $to_copy_start = $to_segment_start; + $to_copy_start_max = $to_segment_end - $copy_len; + while ($to_copy_start <= $to_copy_start_max) { + $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), + substr($to_text, $to_copy_start, $copy_len)); + if ($from_copy_start !== false) { + $from_copy_start += $from_segment_start; + break 2; + } + $to_copy_start++; + } + $copy_len--; + } + } else { + $copy_len = $from_segment_len; + while ($copy_len) { + $from_copy_start = $from_segment_start; + $from_copy_start_max = $from_segment_end - $copy_len; + while ($from_copy_start <= $from_copy_start_max) { + $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), + substr($from_text, $from_copy_start, $copy_len)); + if ($to_copy_start !== false) { + $to_copy_start += $to_segment_start; + break 2; + } + $from_copy_start++; + } + $copy_len--; + } + } + // match found + if ($copy_len) { + $jobs[] = [$from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start]; + $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len); + $jobs[] = [ + $from_copy_start + $copy_len, + $from_segment_end, + $to_copy_start + $copy_len, + $to_segment_end, + ]; + } // no match, so delete all, insert all + else { + $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, + substr($to_text, $to_segment_start, $to_segment_len)); + } + } + ksort($result, SORT_NUMERIC); + + return array_values($result); + } + + /** + * Efficiently fragment the text into an array according to + * specified delimiters. + * No delimiters means fragment into single character. + * The array indices are the offset of the fragments into + * the input string. + * A sentinel empty fragment is always added at the end. + * Careful: No check is performed as to the validity of the + * delimiters. + */ + private static function extractFragments($text, $delimiters) + { + // special case: split into characters + if (empty($delimiters)) { + $chars = str_split($text, 1); + $chars[strlen($text)] = ''; + + return $chars; + } + $fragments = []; + $start = $end = 0; + for (;;) { + $end += strcspn($text, $delimiters, $end); + $end += strspn($text, $delimiters, $end); + if ($end === $start) { + break; + } + $fragments[$start] = substr($text, $start, $end - $start); + $start = $end; + } + $fragments[$start] = ''; + + return $fragments; + } + + /** + * Stock opcode renderers + */ + private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) + { + if ($opcode === 'c' || $opcode === 'i') { + echo substr($from, $from_offset, $from_len); + } + } + + private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) + { + if ($opcode === 'c') { + echo htmlentities(substr($from, $from_offset, $from_len)); + } else { + if ($opcode === 'd') { + $deletion = substr($from, $from_offset, $from_len); + if (strcspn($deletion, " \n\r") === 0) { + $deletion = str_replace(["\n", "\r"], ['\n', '\r'], $deletion); + } + echo '', htmlentities($deletion), ''; + } else /* if ( $opcode === 'i' ) */ { + echo '', htmlentities(substr($from, $from_offset, $from_len)), ''; + } + } + } +} diff --git a/Components/FineDiffCopyOp.php b/Components/FineDiffCopyOp.php new file mode 100644 index 0000000..1e61b9f --- /dev/null +++ b/Components/FineDiffCopyOp.php @@ -0,0 +1,76 @@ +len = $len; + } + + public function getFromLen() + { + return $this->len; + } + + public function getToLen() + { + return $this->len; + } + + public function getOpcode() + { + if ($this->len === 1) { + return 'c'; + } + + return "c{$this->len}"; + } + + public function increase($size) + { + return $this->len += $size; + } +} diff --git a/Components/FineDiffDeleteOp.php b/Components/FineDiffDeleteOp.php new file mode 100644 index 0000000..1345287 --- /dev/null +++ b/Components/FineDiffDeleteOp.php @@ -0,0 +1,71 @@ +fromLen = $len; + } + + public function getFromLen() + { + return $this->fromLen; + } + + public function getToLen() + { + return 0; + } + + public function getOpcode() + { + if ($this->fromLen === 1) { + return 'd'; + } + + return "d{$this->fromLen}"; + } +} diff --git a/Components/FineDiffInsertOp.php b/Components/FineDiffInsertOp.php new file mode 100644 index 0000000..7ab41d4 --- /dev/null +++ b/Components/FineDiffInsertOp.php @@ -0,0 +1,77 @@ +text = $text; + } + + public function getFromLen() + { + return 0; + } + + public function getToLen() + { + return strlen($this->text); + } + + public function getText() + { + return $this->text; + } + + public function getOpcode() + { + $to_len = strlen($this->text); + if ($to_len === 1) { + return "i:{$this->text}"; + } + + return "i{$to_len}:{$this->text}"; + } +} diff --git a/Components/FineDiffOp.php b/Components/FineDiffOp.php new file mode 100644 index 0000000..83ea780 --- /dev/null +++ b/Components/FineDiffOp.php @@ -0,0 +1,53 @@ +fromLen = $fromLen; + $this->text = $text; + } + + public function getFromLen() + { + return $this->fromLen; + } + + public function getToLen() + { + return strlen($this->text); + } + + public function getText() + { + return $this->text; + } + + public function getOpcode() + { + if ($this->fromLen === 1) { + $del_opcode = 'd'; + } else { + $del_opcode = "d{$this->fromLen}"; + } + $to_len = strlen($this->text); + if ($to_len === 1) { + return "{$del_opcode}i:{$this->text}"; + } + + return "{$del_opcode}i{$to_len}:{$this->text}"; + } +} diff --git a/Controllers/Backend/ViewSnapshots.php b/Controllers/Backend/ViewSnapshots.php index 684a746..2cfef1a 100644 --- a/Controllers/Backend/ViewSnapshots.php +++ b/Controllers/Backend/ViewSnapshots.php @@ -1,5 +1,7 @@ from('view_snapshots') @@ -55,4 +58,89 @@ public function listAction() ['success' => true, 'data' => $data, 'total' => $total] ); } + + /** + * @throws Enlight_Controller_Exception + * @throws Exception + */ + public function diffAction() + { + $sessionFrom = $this->Request()->getParam('sessionFrom'); + $stepFrom = $this->Request()->getParam('stepFrom'); + $sessionTo = $this->Request()->getParam('sessionTo', $sessionFrom); + $stepTo = $this->Request()->getParam('stepTo'); + + if (empty($sessionFrom)) { + throw new Enlight_Controller_Exception('sessionFrom is missing'); + } + + if (empty($stepFrom)) { + throw new Enlight_Controller_Exception('stepFrom is missing'); + } + + if (empty($stepTo)) { + throw new Enlight_Controller_Exception('stepTo is missing'); + } + + /** @var Diff $differ */ + $differ = $this->get('frosh_view_snapshots.services.diff'); + $dataFrom = $this->getSnapshotStep($sessionFrom, $stepFrom); + $dataTo = $this->getSnapshotStep($sessionTo, $stepTo); + + $this->View()->assign([ + 'success' => true, + 'data' => [ + 'sessionID' => $differ->diffPlain($dataFrom['sessionID'], $dataTo['sessionID'])->renderDiffToHTML(), + 'template' => $differ->diffPlain($dataFrom['template'], $dataTo['template'])->renderDiffToHTML(), + 'requestURI' => $differ->diffPlain($dataFrom['requestURI'], $dataTo['requestURI'])->renderDiffToHTML(), + 'step' => $differ->diffPlain($dataFrom['step'], $dataTo['step'])->renderDiffToHTML(), + 'variables' => $differ->diffSerialized($dataFrom['variables'], $dataTo['variables'])->renderDiffToHTML(), + 'params' => $differ->diffJson($dataFrom['params'], $dataTo['params'])->renderDiffToHTML(), + ], + ]); + } + + /** + * @param string $sessionFrom + * @param int $stepFrom + * + * @throws Exception + * + * @return array + */ + protected function getSnapshotStep($sessionFrom, $stepFrom) + { + $qb = $this->container->get('dbal_connection')->createQueryBuilder(); + + $qb->select(['*']) + ->from('view_snapshots') + ->where( + $qb->expr()->eq('sessionID', ':sessionId'), + $qb->expr()->eq('step', ':step') + ) + ->setParameter('sessionId', $sessionFrom) + ->setParameter('step', $stepFrom) + ; + + return $qb->execute()->fetch(PDO::FETCH_ASSOC); + } + + /** + * @throws \Exception + */ + public function deleteAction() + { + $id = (int) $this->Request()->get('id'); + + $this->container->get('dbal_connection')->delete( + 'view_snapshots', + [ + 'id' => $id, + ] + ); + + $this->View()->assign( + ['success' => true] + ); + } } diff --git a/FroshViewSnapshots.php b/FroshViewSnapshots.php index 87833b5..0406e49 100644 --- a/FroshViewSnapshots.php +++ b/FroshViewSnapshots.php @@ -26,6 +26,8 @@ public function build(ContainerBuilder $container) /** * @param InstallContext $context + * + * @throws \Exception */ public function install(InstallContext $context) { @@ -44,19 +46,34 @@ public function activate(ActivateContext $context) /** * @param UpdateContext $context + * + * @throws \Exception */ public function update(UpdateContext $context) { + $currentVersion = $context->getCurrentVersion(); + $sql = ''; + + if (version_compare($currentVersion, '1.1.0', '<')) { + $sql .= file_get_contents($this->getPath() . '/Resources/sql/update.1.1.0.sql'); + + $this->container->get('dbal_connection')->query($sql); + } + $context->scheduleClearCache(InstallContext::CACHE_LIST_ALL); } /** * @param UninstallContext $context + * + * @throws \Exception */ public function uninstall(UninstallContext $context) { $sql = file_get_contents($this->getPath() . '/Resources/sql/uninstall.sql'); $this->container->get('dbal_connection')->query($sql); + + $context->scheduleClearCache(InstallContext::CACHE_LIST_ALL); } } diff --git a/README.MD b/README.MD index 9804a37..ce0e056 100644 --- a/README.MD +++ b/README.MD @@ -21,6 +21,13 @@ articles within the basket or the order confirmation. * Backend component to view recorded sessions * View recorded snapshots * Step forward/backward between snapshots within a recorded session +* Compare recordings and their steps + +### New with v. 1.1.0 + +* View complete diffs of raw data by selecting two snapshots in the backend app + +_Note: The library used to generate diffs is [FineDiff](https://github.com/gorhill/PHP-FineDiff) and it was integrated into this project as it is not available as composer package (yet)_ ## Usage @@ -72,4 +79,8 @@ JavaScript console to issue commands: ## Requirements * Shopware 5.3.4 or higher -* PHP 7.0 +* PHP 5.6 or higher + +## Dependencies + +* Depends on [finediff](https://github.com/gorhill/PHP-FineDiff) by Raymond Hill diff --git a/Resources/services.xml b/Resources/services.xml index ed597bf..8303f7d 100644 --- a/Resources/services.xml +++ b/Resources/services.xml @@ -5,6 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + %frosh_view_snapshots.plugin_dir% diff --git a/Resources/sql/install.sql b/Resources/sql/install.sql index a6bb226..eebabb7 100644 --- a/Resources/sql/install.sql +++ b/Resources/sql/install.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS `view_snapshots` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `sessionID` VARCHAR(255) NOT NULL, `template` VARCHAR(255) NOT NULL, + `requestURI` varchar(255) NOT NULL, `step` INT(11) NOT NULL, `variables` LONGTEXT NULL, `params` LONGTEXT NULL, diff --git a/Resources/sql/update.1.1.0.sql b/Resources/sql/update.1.1.0.sql new file mode 100644 index 0000000..715bb8d --- /dev/null +++ b/Resources/sql/update.1.1.0.sql @@ -0,0 +1 @@ +ALTER TABLE `view_snapshots` ADD `requestURI` varchar(255) COLLATE 'utf8_unicode_ci' NOT NULL AFTER `template`; \ No newline at end of file diff --git a/Resources/views/backend/view_snapshots/controller/main.js b/Resources/views/backend/view_snapshots/controller/main.js index b634f3b..2b53cb6 100644 --- a/Resources/views/backend/view_snapshots/controller/main.js +++ b/Resources/views/backend/view_snapshots/controller/main.js @@ -18,6 +18,121 @@ Ext.define('Shopware.apps.ViewSnapshots.controller.Main', { me.callParent(arguments); + me.control({ + 'view-snapshot-window': { + 'select': me.onGridSelect, + 'delete': me.onSnapshotDelete + } + }); + + }, + + onGridSelect: function(selectionModel, rec) { + var selection = selectionModel.getSelection(); + + if (selection.length > 2) { + selectionModel.deselectAll(); + selectionModel.select(rec); + + return; + } + + if (selection.length === 2) { + var me = this, + firstRecord = selection[0], + secondRecord = selection[1], + loadMask = new Ext.LoadMask(me.mainWindow); + + loadMask.show(); + + Ext.Ajax.request({ + url: '{url action="diff"}', + method: 'GET', + params: { + sessionFrom: firstRecord.get('sessionID'), + stepFrom: firstRecord.get('step'), + sessionTo: secondRecord.get('sessionID'), + stepTo: secondRecord.get('step') + }, + success: function(response){ + var responseObj = Ext.JSON.decode(response.responseText); + + Ext.create('Ext.window.Window', { + title: 'Diff', + height: 600, + width: 800, + modal: true, + layout: { + type: 'vbox', + align: 'stretch', + pack : 'start' + }, + items: [{ + xtype: 'panel', + title: 'Session ID', + html: '{literal}{/literal}' + + responseObj.data.sessionID + }, { + xtype: 'panel', + title: 'Step', + html: responseObj.data.step + }, { + xtype: 'panel', + title: 'Template', + html: responseObj.data.template + }, { + xtype: 'panel', + title: 'URI', + html: responseObj.data.requestURI + }, { + xtype: 'panel', + title: 'Params', + flex: 1, + autoScroll: true, + collapsible: true, + html: '
' +
+                                responseObj.data.params +
+                                '
' + }, { + xtype: 'panel', + title: 'Variables', + flex: 2, + autoScroll: true, + html: '
' +
+                                responseObj.data.variables +
+                                '
' + }] + }).show(); + + loadMask.hide(); + }, + failure: function(response){ + console.log(response); + + loadMask.hide(); + } + }); + } + }, + + onSnapshotDelete: function (view, rowIndex) { + var me = this, + store = me.getStore('Snapshot'); + + me.record = store.getAt(rowIndex); + + if (me.record instanceof Ext.data.Model && me.record.get('id') > 0) { + Ext.MessageBox.confirm('Delete?', 'Are you sure you want to delete the snapshot?', function (response) { + if (response !== 'yes') { + return; + } + me.record.destroy({ + callback: function() { + store.load(); + } + }); + }); + } } }); diff --git a/Resources/views/backend/view_snapshots/model/snapshot.js b/Resources/views/backend/view_snapshots/model/snapshot.js index b66bbab..dd1a3a7 100644 --- a/Resources/views/backend/view_snapshots/model/snapshot.js +++ b/Resources/views/backend/view_snapshots/model/snapshot.js @@ -1,8 +1,11 @@ Ext.define('Shopware.apps.ViewSnapshots.model.Snapshot', { extend : 'Ext.data.Model', - fields : [ 'sessionID', 'template', 'step', 'url' ], + fields : [ 'sessionID', 'template', 'step', 'url', 'requestURI' ], proxy: { type : 'ajax', + api : { + destroy : '{url action="delete"}' + }, reader : { type : 'json', root : 'data', diff --git a/Resources/views/backend/view_snapshots/view/grid.js b/Resources/views/backend/view_snapshots/view/grid.js index 82f2de8..f8984d7 100644 --- a/Resources/views/backend/view_snapshots/view/grid.js +++ b/Resources/views/backend/view_snapshots/view/grid.js @@ -27,6 +27,11 @@ Ext.define('Shopware.apps.ViewSnapshots.view.Grid', { flex: 1, dataIndex: 'template' }, + { + header: 'URI', + flex: 1, + dataIndex: 'requestURI' + }, { header: 'Step', dataIndex: 'step', @@ -34,12 +39,14 @@ Ext.define('Shopware.apps.ViewSnapshots.view.Grid', { }, { xtype: 'actioncolumn', - width: 30, + width: 60, items: me.getActionColumnItems() } ]; }, getActionColumnItems: function () { + var me = this; + return [ { iconCls: 'x-action-col-icon sprite-globe--arrow', @@ -50,6 +57,13 @@ Ext.define('Shopware.apps.ViewSnapshots.view.Grid', { window.open(record.get('url'), '_blank'); } + }, + { + iconCls: 'x-action-col-icon sprite-minus-circle-frame', + tooltip: 'Delete', + handler: function (view, rowIndex, colIndex, item) { + me.fireEvent('delete', view, rowIndex, colIndex, item); + } } ]; }, diff --git a/Resources/views/backend/view_snapshots/view/window.js b/Resources/views/backend/view_snapshots/view/window.js index be00851..7ee6c16 100644 --- a/Resources/views/backend/view_snapshots/view/window.js +++ b/Resources/views/backend/view_snapshots/view/window.js @@ -14,7 +14,10 @@ Ext.define('Shopware.apps.ViewSnapshots.view.Window', { { xtype: 'view-snapshot-window', store: me.store, - flex: 1 + flex: 1, + selModel: new Ext.selection.CheckboxModel({ + checkOnly: true + }) } ]; diff --git a/Services/Diff.php b/Services/Diff.php new file mode 100644 index 0000000..c302e20 --- /dev/null +++ b/Services/Diff.php @@ -0,0 +1,90 @@ +diffPlain($this->prettyPrintSerialized($from), $this->prettyPrintSerialized($to)); + } + + /** + * @param string $from + * @param string $to + * + * @return FineDiff + */ + public function diffJson($from, $to) + { + $fromData = json_decode($from, true); + $toData = json_decode($to, true); + + if (is_array($fromData) && is_array($toData)) { + return $this->diffArray($fromData, $toData); + } + + return $this->diffPlain($from, $to); + } + + /** + * @param array $from + * @param array $to + * + * @return FineDiff + */ + public function diffArray(array $from, array $to) + { + return $this->diffPlain( + json_encode($this->sortArrayRecursive($from), JSON_PRETTY_PRINT), + json_encode($this->sortArrayRecursive($to), JSON_PRETTY_PRINT) + ); + } + + /** + * @param array $data + * + * @return array + */ + protected function sortArrayRecursive(array $data) + { + foreach ($data as &$value) { + if (is_array($value)) { + $value = $this->sortArrayRecursive($value); + } + } + + ksort($data); + + return $data; + } + + /** + * @param string $serialized + * + * @return string + */ + protected function prettyPrintSerialized($serialized) + { + return print_r(unserialize($serialized), true); + } +} diff --git a/Subscriber/Dispatch.php b/Subscriber/Dispatch.php index 278b8f2..f16661b 100644 --- a/Subscriber/Dispatch.php +++ b/Subscriber/Dispatch.php @@ -111,6 +111,7 @@ public function onPostDispatchSecureFrontend(\Enlight_Controller_ActionEventArgs 'variables' => $variables, 'params' => $params, 'step' => $step, + 'requestURI' => $request->getPathInfo(), ] ); } diff --git a/composer.json b/composer.json index 54119a1..ad2bf8f 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "installer-name": "FroshViewSnapshots" }, "require": { + "php": "^5.6 || ^7.0", "composer/installers": "~1.0" }, "scripts": { diff --git a/plugin.xml b/plugin.xml index 5d6c566..0bbbf67 100644 --- a/plugin.xml +++ b/plugin.xml @@ -3,13 +3,36 @@ - 1.0.0 + 1.1.0 Friends of Shopware MIT https://friendsofshopware.github.io/ Friends of Shopware + + + +
  • Spalte hinzugefügt zur Aufnahme von Snapshot Request URIs
  • +
  • Funktionalität hinzugefügt zur Ansicht eines Diff von Rohdaten zweier Snapshots
  • +
  • Funktionalität hinzugefügt zum Löschen von Snapshot-Einträgen über die Backend App
  • +
  • Functional Tests hinzugefügt
  • + + ]]> +
    + + +
  • Added column and recording of snapshot request URIs
  • +
  • Added functionality to view diff of raw snapshot data from backend app
  • +
  • Added functionality to delete snapshot entries from backend app
  • +
  • Added functional tests
  • + + ]]> +
    +
    + Initial Release Initial Release diff --git a/tests/Functional/ViewportSnapshotsTest.php b/tests/Functional/ViewportSnapshotsTest.php new file mode 100644 index 0000000..cb02bd2 --- /dev/null +++ b/tests/Functional/ViewportSnapshotsTest.php @@ -0,0 +1,35 @@ +Template()->disableSecurity(); + } + + public function testSnapshotRecording() + { + Shopware()->Session()->offsetSet('isSessionRecorded', true); + + $this->dispatch('/'); + + $sql = 'SELECT * FROM view_snapshots WHERE sessionID = ? AND requestURI = ?'; + $snapshot = Shopware()->Db()->fetchRow($sql, [ + Shopware()->Session()->get('sessionId'), + '/', + ]); + + $variables = unserialize($snapshot['variables']); + + $this->assertTrue(is_array($variables) && !empty($variables)); + } + + public function testSnapshotReplay() + { + $this->dispatch('/snapshots/load/session/' . Shopware()->Session()->get('sessionId')); + + $this->assertTrue(strpos($this->Response()->getBody(), 'is--ctl-index') !== false); + } +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 4fcdc64..b1c4e2f 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -11,7 +11,7 @@ verbose="true"> - + ./Functional